Replace Web Basic Auth With Cookie Sessions
Add bounded login challenges, CSRF/origin enforcement, logout, and session-bound WebSocket admission. Isolate private HTTPD access behind a version-guarded adapter and add focused host coverage. Also let empty admin SSH input reach the normal console handler.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
#!/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']
|
||||
for forbidden in ('localStorage', 'sessionStorage', 'document.cookie', 'console.log', 'innerHTML', 'Authorization'):
|
||||
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()
|
||||
Reference in New Issue
Block a user