adc5c743a6
Session 36: prevent UAF when Array is moved into a struct field (Nexus headers), align selfhost cc flags with bootstrap debug/release, and add workspace/symbol search to bux-lsp 0.6. - Parser: zero headers after embedding so auto-drop is a no-op - Request_WantsKeepAlive uses RequestHeader_Get again safely - Selfhost: default -O0 -g; --release -O2 -DNDEBUG; BUX_CFLAGS - LSP: workspace/symbol + smoke; version 0.6.0
171 lines
5.9 KiB
Plaintext
171 lines
5.9 KiB
Plaintext
module Http {
|
|
|
|
import Std::Array::{Array, Array_Len, Array_Get};
|
|
import Std::String::{String_Eq, String_EndsWith, String_Contains, String_Len, String_StartsWith};
|
|
|
|
pub enum HttpMethod {
|
|
GET,
|
|
POST,
|
|
PUT,
|
|
DELETE,
|
|
PATCH,
|
|
HEAD,
|
|
OPTIONS,
|
|
UNKNOWN,
|
|
}
|
|
|
|
pub struct HeaderEntry {
|
|
key: String;
|
|
value: String;
|
|
}
|
|
|
|
pub struct HttpRequest {
|
|
method: HttpMethod;
|
|
path: String;
|
|
version: String;
|
|
body: String;
|
|
headers: Array<HeaderEntry>;
|
|
}
|
|
|
|
pub struct HttpResponse {
|
|
statusCode: int;
|
|
contentType: String;
|
|
body: String;
|
|
extraHeaders: String;
|
|
}
|
|
|
|
pub func Http_StatusText(code: int) -> String {
|
|
if code == 200 { return "OK"; }
|
|
if code == 201 { return "Created"; }
|
|
if code == 204 { return "No Content"; }
|
|
if code == 301 { return "Moved Permanently"; }
|
|
if code == 302 { return "Found"; }
|
|
if code == 304 { return "Not Modified"; }
|
|
if code == 400 { return "Bad Request"; }
|
|
if code == 401 { return "Unauthorized"; }
|
|
if code == 403 { return "Forbidden"; }
|
|
if code == 404 { return "Not Found"; }
|
|
if code == 405 { return "Method Not Allowed"; }
|
|
if code == 413 { return "Payload Too Large"; }
|
|
if code == 414 { return "URI Too Long"; }
|
|
if code == 500 { return "Internal Server Error"; }
|
|
if code == 501 { return "Not Implemented"; }
|
|
if code == 503 { return "Service Unavailable"; }
|
|
return "Unknown";
|
|
}
|
|
|
|
pub func Http_MimeType(path: String) -> String {
|
|
if String_EndsWith(path, ".html") || String_EndsWith(path, ".htm") { return "text/html; charset=utf-8"; }
|
|
if String_EndsWith(path, ".css") { return "text/css; charset=utf-8"; }
|
|
if String_EndsWith(path, ".js") { return "application/javascript; charset=utf-8"; }
|
|
if String_EndsWith(path, ".json") { return "application/json; charset=utf-8"; }
|
|
if String_EndsWith(path, ".xml") { return "application/xml; charset=utf-8"; }
|
|
if String_EndsWith(path, ".txt") { return "text/plain; charset=utf-8"; }
|
|
if String_EndsWith(path, ".png") { return "image/png"; }
|
|
if String_EndsWith(path, ".jpg") || String_EndsWith(path, ".jpeg") { return "image/jpeg"; }
|
|
if String_EndsWith(path, ".gif") { return "image/gif"; }
|
|
if String_EndsWith(path, ".svg") { return "image/svg+xml"; }
|
|
if String_EndsWith(path, ".ico") { return "image/x-icon"; }
|
|
if String_EndsWith(path, ".webp") { return "image/webp"; }
|
|
if String_EndsWith(path, ".woff2") { return "font/woff2"; }
|
|
if String_EndsWith(path, ".woff") { return "font/woff"; }
|
|
if String_EndsWith(path, ".wasm") { return "application/wasm"; }
|
|
return "application/octet-stream";
|
|
}
|
|
|
|
pub func Http_MethodName(m: HttpMethod) -> String {
|
|
match m {
|
|
HttpMethod::GET => "GET",
|
|
HttpMethod::POST => "POST",
|
|
HttpMethod::PUT => "PUT",
|
|
HttpMethod::DELETE => "DELETE",
|
|
HttpMethod::PATCH => "PATCH",
|
|
HttpMethod::HEAD => "HEAD",
|
|
HttpMethod::OPTIONS => "OPTIONS",
|
|
HttpMethod::UNKNOWN => "UNKNOWN",
|
|
}
|
|
}
|
|
|
|
pub func Http_NewResponse(code: int, contentType: String, body: String) -> HttpResponse {
|
|
var resp: HttpResponse;
|
|
resp.statusCode = code;
|
|
resp.contentType = contentType;
|
|
resp.body = body;
|
|
resp.extraHeaders = "";
|
|
return resp;
|
|
}
|
|
|
|
func CharLower(c: int) -> int {
|
|
if c >= 65 && c <= 90 {
|
|
return c + 32;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
/// Case-insensitive string equality (HTTP header names/values).
|
|
pub func String_EqIgnoreCase(a: String, b: String) -> bool {
|
|
let la: uint = String_Len(a);
|
|
let lb: uint = String_Len(b);
|
|
if la != lb {
|
|
return false;
|
|
}
|
|
var i: uint = 0;
|
|
while i < la {
|
|
if CharLower(a[i] as int) != CharLower(b[i] as int) {
|
|
return false;
|
|
}
|
|
i = i + 1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
pub func RequestHeader_Get(req: *HttpRequest, key: String) -> String {
|
|
// Index-based walk — for-in over Array<struct{String}> can corrupt string fields.
|
|
let n: uint = Array_Len<HeaderEntry>(&req.headers);
|
|
var i: uint = 0;
|
|
while i < n {
|
|
let entry: HeaderEntry = Array_Get<HeaderEntry>(&req.headers, i);
|
|
if String_EqIgnoreCase(entry.key, key) {
|
|
return entry.value;
|
|
}
|
|
i = i + 1;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/// HTTP/1.1 defaults to keep-alive; Connection: close forces close;
|
|
/// HTTP/1.0 needs explicit keep-alive. Uses parsed headers (safe after
|
|
/// Parser field-move ownership handoff).
|
|
pub func Request_WantsKeepAlive(req: *HttpRequest) -> bool {
|
|
let conn: String = RequestHeader_Get(req, "Connection");
|
|
if String_EqIgnoreCase(conn, "close") {
|
|
return false;
|
|
}
|
|
if String_EqIgnoreCase(conn, "keep-alive") {
|
|
return true;
|
|
}
|
|
if String_StartsWith(req.version, "HTTP/1.0") {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Fallback when only raw bytes are available.
|
|
pub func RawRequest_WantsKeepAlive(raw: String) -> bool {
|
|
if String_Contains(raw, "Connection: close") || String_Contains(raw, "connection: close") ||
|
|
String_Contains(raw, "CONNECTION: CLOSE") {
|
|
return false;
|
|
}
|
|
if String_Contains(raw, "HTTP/1.0") {
|
|
if String_Contains(raw, "Connection: keep-alive") ||
|
|
String_Contains(raw, "Connection: Keep-Alive") ||
|
|
String_Contains(raw, "connection: keep-alive") {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
}
|