- Add admin account list, create, role, delete, and password workflows - Execute identity-checked mutations through the existing dispatcher - Bound queued credential lifetime and wipe transient secrets - Add explicit password generation with saved-value acknowledgement - Handle self-revocation and uncertain outcomes without automatic retries - Register optional account routes without disrupting terminal transports - Expand host regressions and document contracts and pending target checks Validated host suites and pio run; hardware validation remains pending.
96 lines
5.1 KiB
Python
96 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Compile the actual C renderer and exercise its emitted app/loader in Node."""
|
|
import base64
|
|
import ctypes as C
|
|
import hashlib
|
|
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]
|
|
|
|
|
|
def run():
|
|
# Reuse only the HTTPD test double text, not the standalone login renderer.
|
|
source = (ROOT / 'tests/web_login_ui/run.py').read_text()
|
|
stub = source.split("STUB = r'''", 1)[1].split("'''", 1)[0].replace('web_login_ui.h', 'web_ui.h')
|
|
for asset in ('xterm_js_gz', 'xterm_css_gz', 'addon_fit_js_gz', 'logo_png'):
|
|
stub += f'\nconst unsigned char web_asset_{asset}[] = "stub";\nconst size_t web_asset_{asset}_size = 4;\n'
|
|
with tempfile.TemporaryDirectory(prefix='web-ui-session-') 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 <sys/types.h>
|
|
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_ui.c'), '-o', str(tmp / 'renderer.so')], check=True)
|
|
lib = C.CDLL(str(tmp / 'renderer.so'))
|
|
lib.web_ui_send_response.argtypes = [C.c_void_p, C.c_int]
|
|
for name in ('header_key', 'header_value', 'body', 'content_type'):
|
|
getattr(lib, name).restype = C.c_char_p
|
|
request = C.c_int()
|
|
send = lambda resource: lib.web_ui_send_response(C.byref(request), resource)
|
|
lib.reset(0, 0)
|
|
assert lib.web_ui_send_response(None, 0) == 258
|
|
assert send(99) == 258 and lib.call_count() == 0
|
|
rendered = {}
|
|
for resource in range(6):
|
|
lib.reset(0, 0)
|
|
assert send(resource) == 0
|
|
count, calls = lib.header_count(), lib.call_count()
|
|
assert count <= 8
|
|
headers = {lib.header_key(i).decode(): lib.header_value(i).decode() for i in range(count)}
|
|
assert headers['Cache-Control'] == ('no-store' if resource in (0, 4) else 'private, max-age=604800')
|
|
assert headers['X-Content-Type-Options'] == 'nosniff'
|
|
assert headers['Referrer-Policy'] == 'no-referrer'
|
|
if resource == 0:
|
|
rendered.update(html=lib.body().decode(), headers=headers)
|
|
if resource == 4:
|
|
rendered['script'] = lib.body().decode()
|
|
for failure in range(1, calls + 1):
|
|
lib.reset(failure, 0)
|
|
assert send(resource) == 73 and lib.send_count() == 0
|
|
lib.reset(0, 91)
|
|
assert send(resource) == 91
|
|
scripts = re.findall(r'<script>(.*?)</script>', rendered['html'], re.S)
|
|
assert len(scripts) == 1
|
|
rendered['loader'] = scripts[0]
|
|
digest = base64.b64encode(hashlib.sha256(scripts[0].encode()).digest()).decode()
|
|
csp = rendered['headers']['Content-Security-Policy']
|
|
assert csp.count(f"'sha256-{digest}'") == 2, 'loader CSP hash mismatch'
|
|
assert "frame-ancestors 'none'" in csp and "connect-src 'self'" in csp
|
|
assert rendered['html'].index('<script>') < rendered['html'].index('/assets/xterm.js')
|
|
assert '<a href="/login">' in rendered['html']
|
|
# Keep the mode selector last/rightmost when admin actions are visible.
|
|
assert (rendered['html'].index('id="terminal-title"') <
|
|
rendered['html'].index('id="admin-toggle"') <
|
|
rendered['html'].index('id="terminal-selector"'))
|
|
for field in ('account-password', 'account-password-confirm'):
|
|
assert re.search(r'id="' + field + r'" type="password" maxlength="64" autocomplete="new-password"', rendered['html'])
|
|
assert 'id="account-generated" readonly autocomplete="off"' in rendered['html']
|
|
assert 'id="account-password-saved" type="checkbox"' in rendered['html']
|
|
assert 'not applied yet' in rendered['html'] and 'no retrieval' in rendered['html']
|
|
assert 'JavaScript cannot securely zero strings' in rendered['html']
|
|
for forbidden in ('localStorage', 'sessionStorage', 'document.cookie', 'console.log', 'innerHTML', 'Authorization', 'clipboard', 'pushState', 'replaceState'):
|
|
assert forbidden not in rendered['script'] + rendered['loader'], forbidden
|
|
(tmp / 'rendered.json').write_text(json.dumps(rendered))
|
|
subprocess.run(['node', str(HERE / 'browser.cjs'), str(tmp / 'rendered.json')], check=True, timeout=30)
|
|
print('PASS C/HTML: all resource headers/failures, no-store app/document, exact loader CSP, safe fallback')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
run()
|