Packages

Common Gateway Interface (CGI) in Gleam

Current section

Files

Jump to
cgi src cgi.gleam
Raw

src/cgi.gleam

// Thank you to Steven vanZyl for [plug_cgi][1], which was used as a
// reference. Why is there so little information on the CGI protcol online
// these days?
//
// [1]: https://github.com/rushsteve1/plug_cgi
import envoy
import gleam/io
import gleam/int
import gleam/dict
import gleam/list
import gleam/string
import gleam/result
import gleam/option
import gleam/http
import gleam/http/request.{type Request, Request}
import gleam/http/response.{type Response, Response}
/// Load a CGI HTTP request and dispatch a response.
///
/// CGI works over stdin and stdout, so be sure your code does not use them for
/// this will likely cause the program to fail.
///
pub fn handle_request(f: fn(Request(BitArray)) -> Response(String)) -> Nil {
use request <- with_request_body(load_request())
f(request)
|> send_response
}
/// Send a CGI HTTP response by printing it to stdout.
///
pub fn send_response(response: Response(String)) -> Nil {
let s = response.status
io.println("status: " <> int.to_string(s) <> " " <> status_phrase(s))
let l = string.byte_size(response.body)
io.println("content-length: " <> int.to_string(l))
list.each(response.headers, fn(header) {
io.println(header.0 <> ": " <> header.1)
})
io.println("")
io.println(response.body)
}
/// Load a CGI HTTP request from the environment.
///
/// The body of the request is not loaded. Use `read_body` or
/// `with_request_body` to load the body into the request.
///
pub fn load_request() -> Request(BitArray) {
let env = envoy.all()
let method =
env
|> dict.get("REQUEST_METHOD")
|> result.try(http.parse_method)
|> result.unwrap(http.Get)
let port =
env
|> dict.get("SERVER_PORT")
|> result.try(int.parse)
|> option.from_result
let scheme = case dict.get(env, "HTTPS") {
Ok(_) -> http.Https
_ -> http.Http
}
let query = option.from_result(dict.get(env, "QUERY_STRING"))
let host =
dict.get(env, "SERVER_NAME")
|> result.unwrap("localhost")
let path =
dict.get(env, "PATH_INFO")
|> result.unwrap("/")
let headers =
env
|> dict.to_list
|> list.filter_map(fn(pair) {
case pair.0 {
"CONTENT_TYPE" -> Ok(#("content-type", pair.1))
"CONTENT_LENGTH" -> Ok(#("content-length", pair.1))
"HTTP_" <> name ->
Ok(#(string.replace(string.lowercase(name), "_", "-"), pair.1))
_ -> Error(Nil)
}
})
let body = <<>>
Request(
method: method,
headers: headers,
body: body,
scheme: scheme,
host: host,
port: port,
path: path,
query: query,
)
}
/// Load the body of a request from stdin.
///
/// Due to how IO works in JavaScript may not always work on JavaScript,
/// especially if the body ir larger or on Windows. Consider using the
/// `with_request_body` function instead, which works on all platforms.
///
pub fn read_body(request: Request(BitArray)) -> Request(BitArray) {
let body = read_body_sync(request_content_length(request))
Request(..request, body: body)
}
fn request_content_length(request: Request(BitArray)) -> Int {
request
|> request.get_header("content-length")
|> result.try(int.parse)
|> result.unwrap(0)
}
@external(erlang, "cgi_ffi", "read_body_sync")
@external(javascript, "./cgi_ffi.mjs", "read_body_sync")
fn read_body_sync(length: Int) -> BitArray
/// Load the body of a request from stdin, running a callback with the
/// response with the body.
///
pub fn with_request_body(
request: Request(BitArray),
handle: fn(Request(BitArray)) -> anything,
) -> Nil {
read_body_async(request_content_length(request), fn(body) {
handle(Request(..request, body: body))
})
}
@external(javascript, "./cgi_ffi.mjs", "read_body_async")
fn read_body_async(length: Int, handle: fn(BitArray) -> anything) -> Nil {
let body = read_body_sync(length)
handle(body)
Nil
}
fn status_phrase(status: Int) -> String {
case status {
100 -> "Continue"
101 -> "Switching Protocols"
102 -> "Processing"
103 -> "Early Hints"
200 -> "OK"
201 -> "Created"
202 -> "Accepted"
203 -> "Non-Authoritative Information"
204 -> "No Content"
205 -> "Reset Content"
206 -> "Partial Content"
207 -> "Multi-Status"
208 -> "Already Reported"
226 -> "IM Used"
300 -> "Multiple Choices"
301 -> "Moved Permanently"
302 -> "Found"
303 -> "See Other"
304 -> "Not Modified"
305 -> "Use Proxy"
306 -> "Switch Proxy"
307 -> "Temporary Redirect"
308 -> "Permanent Redirect"
400 -> "Bad Request"
401 -> "Unauthorized"
402 -> "Payment Required"
403 -> "Forbidden"
404 -> "Not Found"
405 -> "Method Not Allowed"
406 -> "Not Acceptable"
407 -> "Proxy Authentication Required"
408 -> "Request Timeout"
409 -> "Conflict"
410 -> "Gone"
411 -> "Length Required"
412 -> "Precondition Failed"
413 -> "Request Entity Too Large"
414 -> "Request-URI Too Long"
415 -> "Unsupported Media Type"
416 -> "Requested Range Not Satisfiable"
417 -> "Expectation Failed"
418 -> "I'm a teapot"
421 -> "Misdirected Request"
422 -> "Unprocessable Entity"
423 -> "Locked"
424 -> "Failed Dependency"
425 -> "Too Early"
426 -> "Upgrade Required"
428 -> "Precondition Required"
429 -> "Too Many Requests"
431 -> "Request Header Fields Too Large"
451 -> "Unavailable For Legal Reasons"
500 -> "Internal Server Error"
501 -> "Not Implemented"
502 -> "Bad Gateway"
503 -> "Service Unavailable"
504 -> "Gateway Timeout"
505 -> "HTTP Version Not Supported"
506 -> "Variant Also Negotiates"
507 -> "Insufficient Storage"
508 -> "Loop Detected"
510 -> "Not Extended"
511 -> "Network Authentication Required"
_ -> ""
}
}