///|
/// An HTTP request, built by the boundary from whatever the host handed over.
///
/// `path` is the part routes are matched on and never contains a query string,
/// `query` is the raw text after `?`. Header names are lowercased and kept in arrival
/// order, so repeated headers survive a round trip. `params` starts empty and is
/// filled by the router when a `:name` or trailing `*` pattern matches the path.
pub struct Request {
http_method : String
path : String
query : String
headers : Array[(String, String)]
body : String
params : Array[(String, String)]
}
///|
/// The request line as the client wrote it, so a redirect can send the whole URI back.
pub fn Request::uri(self : Request) -> String {
if self.query.length() > 0 {
"\{self.path}?\{self.query}"
} else {
self.path
}
}
///|
/// A request header by lowercase name; absent reads as empty.
pub fn Request::header(self : Request, name : String) -> String {
let mut found = ""
for (key, value) in self.headers {
if key == name {
found = value
}
}
found
}
///|
/// Returns the request with one more header, for middleware that enriches the
/// request before it reaches the route. Repeated names stay separate entries.
pub fn Request::set_header(
self : Request,
name : String,
value : String,
) -> Request {
let headers : Array[(String, String)] = []
for entry in self.headers {
headers.push(entry)
}
headers.push((name, value))
{ ..self, headers, }
}
///|
/// One value of a `application/x-www-form-urlencoded` query parameter, with its percent
/// escapes and `+` signs decoded.
pub fn Request::query_param(self : Request, name : String) -> String {
let mut found = ""
for pair in self.query.split("&") {
match pair.split_once("=") {
None => ()
Some((key, value)) =>
if percent_decode(key.to_owned()) == name {
found = percent_decode(value.to_owned())
}
}
}
found
}
///|
/// Every value of a query parameter, in arrival order, each with its percent
/// escapes and `+` signs decoded — so `?tag=a&tag=b` reads back as both.
pub fn Request::query_all(self : Request, name : String) -> Array[String] {
let out : Array[String] = []
for pair in self.query.split("&") {
match pair.split_once("=") {
None => ()
Some((key, value)) =>
if percent_decode(key.to_owned()) == name {
out.push(percent_decode(value.to_owned()))
}
}
}
out
}
///|
/// A `application/x-www-form-urlencoded` body field, percent escapes and `+` signs
/// decoded. Bodies of other content types read as empty, so a handler can call
/// this unconditionally. The last value of a repeated field wins.
pub fn Request::form_param(self : Request, name : String) -> String {
let vals = self.form_all(name)
if vals.length() > 0 {
vals[vals.length() - 1]
} else {
""
}
}
///|
/// Every value of a `application/x-www-form-urlencoded` body field, in arrival order.
pub fn Request::form_all(self : Request, name : String) -> Array[String] {
let out : Array[String] = []
if self.header("content-type") != "application/x-www-form-urlencoded" {
return out
}
for pair in self.body.split("&") {
match pair.split_once("=") {
None => ()
Some((key, value)) =>
if percent_decode(key.to_owned()) == name {
out.push(percent_decode(value.to_owned()))
}
}
}
out
}
///|
/// A path parameter captured by the router, for example `id` in the route
/// `/user/:id`. The value captured by a trailing `*` wildcard is stored under
/// the name `*`. Absent names read as empty, and the last value wins.
pub fn Request::param(self : Request, name : String) -> String {
let mut found = ""
for (key, value) in self.params {
if key == name {
found = value
}
}
found
}
///|
/// Fields are readable so handlers can inspect what came back, but building one
/// goes through `response`/`response_with`.
pub struct Response {
status : Int32
headers : Array[(String, String)]
body : String
}
///|
/// A response header by lowercase name; absent reads as empty.
pub fn Response::header(self : Response, name : String) -> String {
let mut found = ""
for (key, value) in self.headers {
if key == name {
found = value
}
}
found
}
///|
/// Builds a response with an exact header list, for handlers that need to set
/// `location`, `set-cookie` or their own `content-length`.
pub fn response_with(
status : Int32,
headers : Array[(String, String)],
body : String,
) -> Response {
{ status, headers, body, }
}
///|
pub fn response(status : Int32, body : String) -> Response {
response_with(status, [("content-type", "text/plain; charset=utf-8")], body)
}
///|
/// Builds a JSON response. RFC 8259 defines JSON as UTF-8-only, so the content type
/// carries no charset parameter.
pub fn response_json(status : Int32, body : String) -> Response {
response_with(status, [("content-type", "application/json")], body)
}
///|
/// Builds an HTML response.
pub fn html(status : Int32, body : String) -> Response {
response_with(status, [("content-type", "text/html; charset=utf-8")], body)
}
///|
/// Returns a copy of the response with one more header. Repeated names stay as
/// separate entries, which is how `set-cookie` must be sent.
pub fn Response::set_header(
self : Response,
name : String,
value : String,
) -> Response {
let headers : Array[(String, String)] = []
for entry in self.headers {
headers.push(entry)
}
headers.push((name, value))
{ status: self.status, headers, body: self.body, }
}
///|
/// Builds a redirect response with a `location` header and an empty body.
/// Defaults to 302; pass `status~` for 301, 307 or 308.
pub fn redirect(to : String, status~ : Int32 = 302) -> Response {
response_with(status, [("location", to)], "")
}
///|
/// Status reason phrase. Unknown codes fall back to the generic class text, so a
/// serialized response stays valid HTTP.
pub fn reason_phrase(status : Int32) -> String {
if status == 200 {
"OK"
} else if status == 201 {
"Created"
} else if status == 202 {
"Accepted"
} else if status == 204 {
"No Content"
} else if status == 301 {
"Moved Permanently"
} else if status == 302 {
"Found"
} else if status == 303 {
"See Other"
} else if status == 307 {
"Temporary Redirect"
} else if status == 308 {
"Permanent Redirect"
} else if status == 400 {
"Bad Request"
} else if status == 401 {
"Unauthorized"
} else if status == 403 {
"Forbidden"
} else if status == 404 {
"Not Found"
} else if status == 405 {
"Method Not Allowed"
} else if status == 409 {
"Conflict"
} else if status == 410 {
"Gone"
} else if status == 413 {
"Content Too Large"
} else if status == 415 {
"Unsupported Media Type"
} else if status == 422 {
"Unprocessable Content"
} else if status == 429 {
"Too Many Requests"
} else if status == 500 {
"Internal Server Error"
} else if status == 501 {
"Not Implemented"
} else if status == 502 {
"Bad Gateway"
} else if status == 503 {
"Service Unavailable"
} else if status == 504 {
"Gateway Timeout"
} else if status >= 500 {
"Server Error"
} else if status >= 400 {
"Client Error"
} else if status >= 300 {
"Redirection"
} else {
"OK"
}
}
///|
/// `String` is UTF-16 on this target and `to_bytes`/`to_unchecked_string` merely
/// reinterpret those bytes, so the wire format never touches them. Everything that
/// crosses the boundary goes through `@utf8` instead.
fn utf8_bytes(text : String) -> Bytes {
@utf8.encode(text.view())
}
///|
fn utf8_length(text : String) -> Int32 {
utf8_bytes(text).length()
}
///|
/// Rejects a request whose body or headers are not valid UTF-8, up front, so the
/// field decoders below can stay total.
fn valid_utf8(raw : Bytes) -> Bool {
try {
let _ = @utf8.decode(raw)
true
} catch {
_ => false
}
}
///|
/// Decodes UTF-8, and reads as empty when it is not UTF-8 at all.
fn decode_utf8(raw : Bytes) -> String {
@utf8.decode(raw) catch {
_ => ""
}
}
///|
fn hex_digit(code : Int32) -> Int32? {
if code >= 48 && code <= 57 {
Some(code - 48)
} else if code >= 97 && code <= 102 {
Some(code - 87)
} else if code >= 65 && code <= 70 {
Some(code - 55)
} else {
None
}
}
///|
/// A `%` followed by two hex digits, as the byte it stands for.
fn hex_escape(high : Int32, low : Int32) -> Int32? {
match (hex_digit(high), hex_digit(low)) {
(Some(a), Some(b)) => Some(a * 16 + b)
_ => None
}
}
///|
/// Decodes `%XX` escapes and `+` as a URL query is encoded. Bytes are gathered first and
/// decoded once at the end, because `%C3%A9` spans two escapes and cannot be turned back
/// one character at a time.
fn percent_decode(text : String) -> String {
let src = utf8_bytes(text).to_array()
let n = src.length()
let out : Array[Byte] = []
let mut i = 0
while i < n {
let code = src[i].to_int()
if code == 37 && i + 2 < n {
match hex_escape(src[i + 1].to_int(), src[i + 2].to_int()) {
Some(value) => {
out.push(value.to_byte())
i = i + 3
}
None => {
out.push(src[i])
i = i + 1
}
}
} else if code == 43 {
out.push((32 : Int32).to_byte())
i = i + 1
} else {
out.push(src[i])
i = i + 1
}
}
decode_utf8(Bytes::from_array(out))
}
///|
/// Decodes `src[start, stop)` as UTF-8. Callers must have validated the input; the
/// fallback only guards against a range that straddles a boundary the scan chose.
fn decode_range(src : Array[Byte], start : Int32, stop : Int32) -> String {
let n = stop - start
guard n > 0 else { return "" }
let tmp : Array[Byte] = []
for i = start; i < stop; i = i + 1 {
tmp.push(src[i])
}
let b = Bytes::from_array(tmp)
@utf8.decode(b) catch {
_ => ""
}
}
///|
fn byte_is(b : Byte, target : Int32) -> Bool {
b.to_int() == target
}
///|
/// Finds `target` at or after `from`, returns its index or -1.
fn find_byte(src : Array[Byte], from : Int32, target : Int32) -> Int32 {
let n = src.length()
let mut i = from
while i < n {
if byte_is(src[i], target) {
return i
}
i += 1
}
-1
}
///|
/// End of the current line, skipping a trailing CR so LF-only fixtures work too.
/// Returns the content stop and the index the next line starts at.
fn line_end(src : Array[Byte], from : Int32) -> (Int32, Int32) {
let e = find_byte(src, from, 10)
if e < 0 {
let n = src.length()
(n, n)
} else {
let stop = if e > from && byte_is(src[e - 1], 13) { e - 1 } else { e }
(stop, e + 1)
}
}
///|
/// Lowercases ASCII letters in a header name, leaves every other byte untouched.
fn lowercase_range(src : Array[Byte], start : Int32, stop : Int32) -> String {
let n = stop - start
guard n > 0 else { return "" }
let tmp : Array[Byte] = []
for i = start; i < stop; i = i + 1 {
let v = src[i].to_int()
tmp.push(if v >= 65 && v <= 90 { (v + 32).to_byte() } else { src[i] })
}
decode_range(tmp, 0, n)
}
///|
/// Parses raw HTTP request bytes. Returns `None` for anything without a usable
/// request line, or with invalid UTF-8, which the boundary turns into a 400.
///
/// The body is whatever follows the blank line, taken whole: the host hands over a
/// complete request, so framing does not need `content-length`.
pub fn parse_request(raw : Bytes) -> Request? {
guard valid_utf8(raw) else { return None }
let src = raw.to_array()
let n = src.length()
guard n > 0 else { return None }
// request line: METHOD SP PATH SP VERSION
let (line_stop, next) = line_end(src, 0)
let sp1 = find_byte(src, 0, 32)
guard line_stop > 0 && sp1 > 0 && sp1 < line_stop else { return None }
let sp2 = find_byte(src, sp1 + 1, 32)
guard sp2 > sp1 && sp2 <= line_stop else { return None }
let verb = decode_range(src, 0, sp1)
let target = decode_range(src, sp1 + 1, sp2)
let (raw_path, query) = match target.split_once("?") {
None => (target, "")
Some((before, after)) => (before.to_owned(), after.to_owned())
}
// The path is percent-encoded on the wire (RFC 3986), and the CGI boundary
// receives it decoded by the gateway, so decoding here keeps routes matching
// the same way whichever boundary produced the request.
let path = percent_decode(raw_path)
let headers : Array[(String, String)] = []
let mut pos = next
let mut body_start = n
let mut framed = false
let mut scanning = true
while scanning && pos < n {
let (stop, after) = line_end(src, pos)
if stop == pos {
body_start = after
framed = true
scanning = false
} else {
let colon = find_byte(src, pos, 58)
if colon > pos && colon < stop {
let mut value = colon + 1
while value < stop &&
(byte_is(src[value], 32) || byte_is(src[value], 9)) {
value += 1
}
headers.push(
(lowercase_range(src, pos, colon), decode_range(src, value, stop)),
)
}
pos = after
}
}
// Without the blank line the head was cut off mid-transfer, so the body would be
// a guess.
guard framed else { return None }
Some({
http_method: verb,
path,
query,
headers,
params: [],
body: decode_range(src, body_start, n),
})
}
///|
/// CGI variables the runner exposes as `x-cgi-*` request headers. Mirroring them the
/// way the reference CGI implementation does keeps a handler written for a Rust WCGI
/// app working here unchanged.
let cgi_mirror_vars : Array[(String, String)] = [
("AUTH_TYPE", "x-cgi-auth-type"),
("CONTENT_LENGTH", "x-cgi-content-length"),
("CONTENT_TYPE", "x-cgi-content-type"),
("GATEWAY_INTERFACE", "x-cgi-gateway-interface"),
("PATH_INFO", "x-cgi-path-info"),
("PATH_TRANSLATED", "x-cgi-path-translated"),
("QUERY_STRING", "x-cgi-query-string"),
("REMOTE_ADDR", "x-cgi-remote-addr"),
("REMOTE_HOST", "x-cgi-remote-host"),
("REMOTE_IDENT", "x-cgi-remote-ident"),
("REMOTE_USER", "x-cgi-remote-user"),
("REQUEST_METHOD", "x-cgi-request-method"),
("SCRIPT_NAME", "x-cgi-script-name"),
("SERVER_PORT", "x-cgi-server-port"),
("SERVER_PROTOCOL", "x-cgi-server-protocol"),
("SERVER_SOFTWARE", "x-cgi-server-software"),
]
///|
/// CGI variable names are uppercase and exact; a missing one reads as empty rather
/// than absent, since every variable below treats empty as "not provided".
fn cgi_var(env : Array[(String, String)], name : String) -> String {
for (key, value) in env {
if key == name {
return value
}
}
""
}
///|
/// `ACCEPT_ENCODING` is the dash-cased form of the wire name `accept-encoding`: CGI
/// had to fold the dashes into underscores to put the header in the environment.
fn cgi_header_name(rest : String) -> String {
rest.replace(old="_", new="-").to_lower()
}
///|
fn cgi_header_key(header : (String, String)) -> String {
let (name, _) = header
name
}
///|
/// The first of these that is a path. RFC 3875 requires `PATH_INFO` and `SCRIPT_NAME` to
/// begin with `/`, so a gateway that puts the application's *name* in `SCRIPT_NAME` — the
/// way the reference WCGI package's manifest does it — is skipped instead of routed on.
fn cgi_path(
path_info : String,
script_name : String,
request_uri : String,
) -> String {
let uri = match request_uri.split_once("?") {
None => request_uri
Some((before, _after)) => before.to_owned()
}
let mut found = ""
for candidate in [path_info, script_name, uri] {
if found.length() == 0 && candidate.has_prefix("/") {
found = candidate
}
}
found
}
///|
/// Builds a `Request` from the CGI environment and the body read off stdin.
///
/// A request with no method, or with no variable holding a path, yields `None`, which the
/// boundary turns into a 400. Which variable that is is not fixed by the spec: a gateway
/// that splits the URI hands the app-relative tail in `PATH_INFO` and keeps its own name in
/// `SCRIPT_NAME`, a runner that does not split leaves everything in `SCRIPT_NAME`, and a
/// gateway that only forwards the request line puts it in the non-standard `REQUEST_URI`.
/// Trying them in that order routes all three the same way. `QUERY_STRING` stays separate,
/// so a route registered for `/health` also answers `/health?debug=1`. Client headers
/// arrive as `HTTP_*`, and their values are trimmed because the environment keeps the
/// optional whitespace RFC 3875 allows around the `=`.
pub fn request_from_cgi(
env : Array[(String, String)],
body : String,
) -> Request? {
let http_method = cgi_var(env, "REQUEST_METHOD")
let path = cgi_path(
cgi_var(env, "PATH_INFO"),
cgi_var(env, "SCRIPT_NAME"),
cgi_var(env, "REQUEST_URI"),
)
guard http_method.length() > 0 && path.length() > 0 else { return None }
let query = cgi_var(env, "QUERY_STRING")
let headers : Array[(String, String)] = []
for (key, value) in env {
match key.strip_prefix("HTTP_") {
None => ()
Some(rest) =>
if rest.length() > 0 {
headers.push(
(cgi_header_name(rest.to_owned()), value.trim().to_owned()),
)
}
}
}
for (name, header) in cgi_mirror_vars {
let value = cgi_var(env, name)
if value.length() > 0 {
headers.push((header, value))
}
}
Some({ http_method, path, query, headers, body, params: [], })
}
///|
/// A header name or value carrying a line break would break the framing the
/// serializers build, so such a header is dropped whole: fail closed rather
/// than emit a response a client could re-frame.
fn header_is_safe(name : String, value : String) -> Bool {
let text = name + value
!(text.contains("\r") || text.contains("\n"))
}
///|
/// Serializes a response the way CGI expects it on stdout: a `Status:` line, headers
/// sorted by name, a blank line, then the body, all separated by bare LF.
///
/// `Content-Length` is dropped on purpose: RFC 3875 gives it a special meaning here
/// (a non-parsed body, forwarded raw), which would stop the runner from framing the
/// response at all. `Status` is dropped for the same reason -- the status line is
/// already written.
pub fn serialize_cgi(res : Response) -> Bytes {
let out : Array[Byte] = []
push_utf8(out, "Status: \{res.status} \{reason_phrase(res.status)}\n")
let lines : Array[(String, String)] = []
for (name, value) in res.headers {
if name != "content-length" &&
name != "status" &&
header_is_safe(name, value) {
lines.push((name, value))
}
}
lines.sort_by((a, b) => cgi_header_key(a).lexical_compare(cgi_header_key(b)))
for (name, value) in lines {
push_utf8(out, "\{name}: \{value}\n")
}
push_utf8(out, "\n")
push_utf8(out, res.body)
Bytes::from_array(out)
}
///|
fn push_utf8(out : Array[Byte], text : String) -> Unit {
for b in utf8_bytes(text) {
out.push(b)
}
}
///|
/// Serializes to HTTP/1.1 response bytes. `content-length` is always present, which
/// is what lets the host know where the body ends.
pub fn serialize_response(res : Response) -> Bytes {
let out : Array[Byte] = []
push_utf8(out, "HTTP/1.1 \{res.status} \{reason_phrase(res.status)}\r\n")
let mut has_length = false
for (name, value) in res.headers {
if !header_is_safe(name, value) {
continue
}
if name == "content-length" {
has_length = true
}
push_utf8(out, "\{name}: \{value}\r\n")
}
if !has_length {
push_utf8(out, "content-length: \{utf8_length(res.body)}\r\n")
}
push_utf8(out, "\r\n")
push_utf8(out, res.body)
Bytes::from_array(out)
}