Files
ESP32_Serial_Swiss_Army_Knife/tests/web_ui_session/run.py
T

90 lines
4.5 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 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()