Deploy: fix JS served as JSON envelope #54

Merged
will.anderson merged 2 commits from dev into stage 2026-05-10 22:35:02 +00:00
2 changed files with 109 additions and 31 deletions
+91 -30
View File
@@ -1331,12 +1331,19 @@ static void http_emit_headers_from_map(JsonBuf* b, el_val_t headers_map,
/* Parse the envelope produced by http_response(). On success returns 1 and /* Parse the envelope produced by http_response(). On success returns 1 and
* populates *out_status, *out_headers_map (an ElMap el_val_t caller must * populates *out_status, *out_headers_map (an ElMap el_val_t caller must
* el_release), and *out_body (allocated). On failure returns 0. * el_release via out_parsed_root), and *out_body (malloc'd, caller frees).
* On failure returns 0.
* *
* Implementation: feeds the entire envelope through the recursive-descent * Implementation: manual field scanner does NOT run json_parse on the full
* JSON parser (which builds proper ElMap/ElList values), then pulls the * envelope. Running the recursive-descent JSON parser on a 4050 KB envelope
* three top-level fields by name. Avoids re-stringifying the headers map * (common when the body contains minified/obfuscated JavaScript) fails because
* since json_stringify() does not support nested objects. */ * the parser allocates intermediate ElMap nodes for the whole structure.
* Instead we scan directly:
* "status" strtol scan
* "headers" brace-depth scan to extract the object literal, then
* json_parse only that small substring (always < 1 KB)
* "body" jp_parse_string_raw to unescape the JSON string in one pass,
* without building any intermediate data structures */
static int http_parse_envelope(const char* s, int* out_status, static int http_parse_envelope(const char* s, int* out_status,
el_val_t* out_headers_map, char** out_body, el_val_t* out_headers_map, char** out_body,
el_val_t* out_parsed_root) { el_val_t* out_parsed_root) {
@@ -1344,37 +1351,91 @@ static int http_parse_envelope(const char* s, int* out_status,
if (strncmp(s, EL_HTTP_RESPONSE_TAG, if (strncmp(s, EL_HTTP_RESPONSE_TAG,
sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0; sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0;
el_val_t parsed = json_parse(EL_STR(s)); /* ── status ──────────────────────────────────────────────────────────── */
if (parsed == EL_NULL) return 0; int status = 200;
{
int status = 200; const char* sp = strstr(s, "\"status\":");
el_val_t hmap = 0; if (sp) {
char* body = NULL; const char* np = sp + 9;
while (*np == ' ' || *np == '\t') np++;
el_val_t sv = el_map_get(parsed, EL_STR("status")); long sc = strtol(np, NULL, 10);
if (sv != 0) { if (sc >= 100 && sc <= 599) status = (int)sc;
/* status comes back as an integer — el_val_t holds it directly. */ }
long sc = (long)sv;
if (sc >= 100 && sc <= 599) status = (int)sc;
} }
el_val_t hv = el_map_get(parsed, EL_STR("headers")); /* ── headers ─────────────────────────────────────────────────────────── */
if (hv != 0) { el_val_t hmap = 0;
ElMap* hm = (ElMap*)(uintptr_t)hv; el_val_t parsed_hdrs = EL_NULL;
if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = hv; {
const char* hp = strstr(s, "\"headers\":");
if (hp) {
hp += 10;
while (*hp == ' ' || *hp == '\t') hp++;
if (*hp == '{') {
/* Scan for matching '}', honouring nested objects and strings */
const char* hobj_start = hp;
const char* cp = hp + 1;
int depth = 1, in_str = 0;
while (*cp && depth > 0) {
if (in_str) {
if (*cp == '\\' && *(cp + 1)) { cp += 2; continue; }
if (*cp == '"') in_str = 0;
} else {
if (*cp == '"') in_str = 1;
else if (*cp == '{') depth++;
else if (*cp == '}') { if (--depth == 0) break; }
}
cp++;
}
if (depth == 0) {
/* cp points at the closing '}'; extract the object literal */
size_t hlen = (size_t)(cp - hobj_start + 1);
char* hobj = malloc(hlen + 1);
if (hobj) {
memcpy(hobj, hobj_start, hlen);
hobj[hlen] = '\0';
/* Headers are always simple k/v string pairs — json_parse
* is safe on this small substring (typically < 1 KB). */
parsed_hdrs = json_parse(EL_STR(hobj));
free(hobj);
if (parsed_hdrs != EL_NULL) {
ElMap* hm = (ElMap*)(uintptr_t)parsed_hdrs;
if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = parsed_hdrs;
}
}
}
}
}
} }
el_val_t bv = el_map_get(parsed, EL_STR("body")); /* ── body ────────────────────────────────────────────────────────────── */
if (bv != 0) { /* Search forward so we don't accidentally match "body": inside a header
const char* bs = EL_CSTR(bv); * value. http_response() always appends the body field last. */
if (bs) body = el_strdup(bs); char* body = NULL;
{
const char* bp = strstr(s, "\"body\":");
if (bp) {
bp += 7;
while (*bp == ' ' || *bp == '\t') bp++;
if (*bp == '"') {
/* jp_parse_string_raw unescapes a JSON string in one pass,
* producing a plain malloc'd C string. Caller frees it. */
JsonParser jp = { .p = bp, .end = bp + strlen(bp), .err = 0 };
char* parsed = jp_parse_string_raw(&jp);
if (!jp.err) {
body = parsed;
} else {
free(parsed);
}
}
}
if (!body) body = strdup("");
} }
if (!body) body = el_strdup("");
*out_status = status; *out_status = status;
*out_headers_map = hmap; *out_headers_map = hmap;
*out_body = body; *out_body = body;
*out_parsed_root = parsed; /* caller releases to free hmap + entries */ *out_parsed_root = parsed_hdrs; /* caller el_release()s to free hmap */
return 1; return 1;
} }
+18 -1
View File
@@ -907,6 +907,10 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
// Compiled client-side JS: /js/* // Compiled client-side JS: /js/*
// Served from dist/js/ (compiled by elc --target=js at build time). // Served from dist/js/ (compiled by elc --target=js at build time).
// LANDING_ROOT/js maps to the dist/js output directory in the image. // LANDING_ROOT/js maps to the dist/js output directory in the image.
// Returns an http_response envelope with explicit Content-Type so the
// browser executes the file as JavaScript http_detect_content_type()
// mis-identifies minified/obfuscated JS as JSON because many obfuscated
// bundles start with '[' (which is also a JSON array opener).
if str_starts_with(path, "/js/") { if str_starts_with(path, "/js/") {
let rel: String = str_slice(path, 4, str_len(path)) let rel: String = str_slice(path, 4, str_len(path))
let abs: String = src_dir + "/js/" + rel let abs: String = src_dir + "/js/" + rel
@@ -914,7 +918,7 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
if str_eq(content, "") { if str_eq(content, "") {
return "{\"__status__\":404,\"error\":\"not found\"}" return "{\"__status__\":404,\"error\":\"not found\"}"
} }
return content return http_response(200, js_headers_json(), content)
} }
// Brand assets: /brand/* // Brand assets: /brand/*
@@ -1936,6 +1940,19 @@ fn sec_headers_json() -> String {
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}" + "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
} }
// Headers for compiled JS assets. Explicitly sets Content-Type so the browser
// treats them as JavaScript regardless of what http_detect_content_type()
// infers from the content (minified/obfuscated JS can trip the JSON heuristic).
fn js_headers_json() -> String {
"{\"Content-Type\":\"application/javascript; charset=utf-8\","
+ "\"Cache-Control\":\"public, max-age=3600\","
+ "\"Strict-Transport-Security\":\"max-age=63072000; includeSubDomains; preload\","
+ "\"X-Content-Type-Options\":\"nosniff\","
+ "\"X-Frame-Options\":\"SAMEORIGIN\","
+ "\"Referrer-Policy\":\"strict-origin-when-cross-origin\","
+ "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\"}"
}
fn handle_request(method: String, path: String, body: String) -> String { fn handle_request(method: String, path: String, body: String) -> String {
let inner_resp: String = handle_request_inner(method, path, body) let inner_resp: String = handle_request_inner(method, path, body)
// Detect envelope already set by inner handler (starts with // Detect envelope already set by inner handler (starts with