#!/usr/bin/env python3 """Compile the production renderer; test its C contract and rendered JavaScript.""" import base64 import ctypes as C import hashlib from html.parser import HTMLParser import json import os from pathlib import Path import re import shlex import subprocess import tempfile HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] STUB = r''' #include "web_login_ui.h" #include static int calls, sends, fail_at, send_error, count; static const char *keys[8], *values[8], *html, *type; void reset(int fail, int error) { calls = sends = count = 0; fail_at = fail; send_error = error; html = type = NULL; } int call_count(void) { return calls; } int send_count(void) { return sends; } int header_count(void) { return count; } const char *header_key(int i) { return keys[i]; } const char *header_value(int i) { return values[i]; } const char *body(void) { return html; } const char *content_type(void) { return type; } esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *v) { (void)r; if (++calls == fail_at) return 73; type = v; return ESP_OK; } esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *k, const char *v) { (void)r; if (++calls == fail_at) return 73; if (count == 8) return 74; keys[count] = k; values[count++] = v; return ESP_OK; } esp_err_t httpd_resp_send(httpd_req_t *r, const char *v, ssize_t n) { (void)r; ++sends; if (n < 0 || (size_t)n != strlen(v)) return 75; html = v; return send_error; } ''' class Assets(HTMLParser): def handle_starttag(self, tag, attrs): attrs = dict(attrs) assert 'src' not in attrs and 'srcset' not in attrs, (tag, attrs) assert tag not in ('link', 'iframe', 'object', 'embed'), tag assert not any(k.startswith('on') for k in attrs), attrs def run(): with tempfile.TemporaryDirectory(prefix='web-login-ui-') as directory: tmp = Path(directory) (tmp / 'esp_err.h').write_text('#pragma once\ntypedef int esp_err_t;\n' '#define ESP_OK 0\n#define ESP_ERR_INVALID_ARG 258\n') (tmp / 'esp_http_server.h').write_text('''#pragma once #include "esp_err.h" #include typedef struct { int unused; } httpd_req_t; esp_err_t httpd_resp_set_type(httpd_req_t *, const char *); esp_err_t httpd_resp_set_hdr(httpd_req_t *, const char *, const char *); esp_err_t httpd_resp_send(httpd_req_t *, const char *, ssize_t); ''') (tmp / 'stub.c').write_text(STUB) subprocess.run(shlex.split(os.environ.get('CC', 'cc')) + [ '-std=c11', '-Wall', '-Wextra', '-Werror', '-shared', '-fPIC', '-I', str(tmp), '-I', str(ROOT / 'src'), str(tmp / 'stub.c'), str(ROOT / 'src/web_login_ui.c'), '-o', str(tmp / 'renderer.so')], check=True) lib = C.CDLL(str(tmp / 'renderer.so')) lib.reset.argtypes = [C.c_int, C.c_int] lib.web_login_ui_send_response.argtypes = [C.c_void_p] for name in ('header_key', 'header_value', 'body', 'content_type'): getattr(lib, name).restype = C.c_char_p for name in ('header_key', 'header_value'): getattr(lib, name).argtypes = [C.c_int] request = C.c_int() send = lambda: lib.web_login_ui_send_response(C.byref(request)) lib.reset(0, 0) assert lib.web_login_ui_send_response(None) == 258 assert lib.call_count() == lib.send_count() == 0 assert send() == 0 and lib.send_count() == 1 count, calls = lib.header_count(), lib.call_count() assert 0 < count <= 8 headers = {lib.header_key(i).decode(): lib.header_value(i).decode() for i in range(count)} assert len(headers) == count assert lib.content_type() == b'text/html; charset=utf-8' html = lib.body().decode() for key, value in {'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer', 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY'}.items(): assert headers[key] == value, (key, headers) for failure in range(1, calls + 1): lib.reset(failure, 0) assert send() == 73 and lib.send_count() == 0 assert lib.call_count() == failure, 'header processing did not stop' lib.reset(0, 91) assert send() == 91 and lib.send_count() == 1 print('PASS C: NULL, headers, every setter failure, send error propagation', flush=True) Assets().feed(html) assert not re.search(r'url\s*\(|@import', html, re.I) scripts = re.findall(r'', html, re.S) assert len(scripts) == 1 digest = base64.b64encode(hashlib.sha256(scripts[0].encode()).digest()).decode() policy = dict(part.strip().split(' ', 1) for part in headers['Content-Security-Policy'].split(';') if part.strip()) expected = {"default-src": "'none'", "script-src": f"'sha256-{digest}'", "connect-src": "'self'", "base-uri": "'none'", "form-action": "'none'", "frame-ancestors": "'none'", "style-src": "'unsafe-inline'"} # Report behavioral failures even when the script and CSP hash drift. (tmp / 'rendered.json').write_text(json.dumps({'html': html, 'headers': headers, 'script': scripts[0]})) result = subprocess.run(['node', str(HERE / 'browser.cjs'), str(tmp / 'rendered.json')], timeout=30) assert policy == expected, f'CSP mismatch: expected {expected}, got {policy}' assert result.returncode == 0, 'Node browser tests failed' print(f'PASS HTML: standalone assets, {count}/8 header slots, exact CSP SHA256 {digest}') if __name__ == '__main__': run()