Constrain terminal layout and externalize TLS memory

Improve CSP and favicon handling while preventing terminal resize loops.
Configure mbedTLS allocations in external PSRAM and document its
security implications.
This commit is contained in:
2026-08-24 20:52:10 +02:00
parent 5f7ea5b79d
commit c018bfe361
3 changed files with 48 additions and 19 deletions
+38 -16
View File
@@ -25,13 +25,14 @@ static const char s_index_html[] =
"--line:#29364a;--text:#e8eef8;--muted:#91a0b5;--accent:#55c2ff;"
"--good:#52d68b;--warn:#ffc857;--bad:#ff6b7a;--radius:14px}\n"
"*{box-sizing:border-box}\n"
"html,body{height:100%;margin:0}\n"
"html,body{height:100%;margin:0;overflow:hidden}\n"
"body{background:radial-gradient(circle at top left,#142033 0,var(--bg) 42rem);"
"color:var(--text);font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"
"\"Segoe UI\",sans-serif}\n"
"button{font:inherit}\n"
".page{min-height:100%;max-width:1440px;margin:auto;padding:clamp(14px,2.5vw,32px);"
"display:grid;grid-template-rows:auto auto minmax(360px,1fr);gap:16px}\n"
".page{height:100%;height:100dvh;min-height:0;max-width:1440px;margin:auto;"
"padding:clamp(14px,2.5vw,32px);display:grid;"
"grid-template-rows:auto auto minmax(0,1fr);gap:16px}\n"
".topbar{display:flex;align-items:center;justify-content:space-between;gap:16px}\n"
".brand{display:flex;align-items:center;gap:12px;min-width:0}\n"
".logo{width:42px;height:42px;border:1px solid #347ba5;border-radius:12px;"
@@ -69,12 +70,13 @@ static const char s_index_html[] =
".input-state{margin:0;color:var(--warn);font-size:13px}\n"
".input-state[data-enabled=true]{color:var(--good)}\n"
".connection-detail{margin:0;color:var(--muted);font-size:12px;min-height:1.45em}\n"
".terminal-panel{min-height:0;padding:10px;display:flex;flex-direction:column;overflow:hidden}\n"
".terminal-panel{min-width:0;min-height:0;padding:10px;display:flex;flex-direction:column;overflow:hidden}\n"
".terminal-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;"
"padding:1px 5px 9px;color:var(--muted);font-size:12px}\n"
".terminal-title{color:var(--text);font-weight:750;letter-spacing:.02em}\n"
"#terminal{flex:1;min-height:0;border-radius:9px;overflow:hidden;background:#080c12;padding:8px}\n"
"#terminal .xterm{height:100%}\n"
"#terminal{flex:1;min-width:0;min-height:0;border-radius:9px;overflow:hidden;"
"background:#080c12;padding:8px}\n"
"#terminal .xterm{width:100%;height:100%}\n"
"#terminal .xterm-viewport{border-radius:7px}\n"
"@media(max-width:850px){.dashboard{grid-template-columns:1fr}.controls{align-items:flex-start}"
".status-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}\n"
@@ -82,8 +84,9 @@ static const char s_index_html[] =
".logo{width:36px;height:36px}.status-grid{padding:10px;gap:8px}"
".status-item{padding:9px}.controls{padding:12px}.terminal-panel{padding:7px}"
".button-row{display:grid;grid-template-columns:1fr 1fr;width:100%}"
".button:last-child{grid-column:1/-1}.page{grid-template-rows:auto auto minmax(420px,1fr)}}\n"
".button:last-child{grid-column:1/-1}.page{grid-template-rows:auto auto minmax(0,1fr)}}\n"
"</style>\n"
"<link rel=\"icon\" href=\"data:,\">\n"
"<link rel=\"stylesheet\" href=\"/assets/xterm.css\">\n"
"<script defer src=\"/assets/xterm.js\"></script>\n"
"<script defer src=\"/assets/addon-fit.js\"></script>\n"
@@ -173,6 +176,8 @@ static const char s_app_js[] =
"let writerId = 0;\n"
"let unloading = false;\n"
"let fitFrame = 0;\n"
"let lastFitWidth = 0;\n"
"let lastFitHeight = 0;\n"
"let statusInFlight = false;\n"
"let statusTimer = null;\n"
"const setBadge = (target, text, tone) => {\n"
@@ -329,14 +334,27 @@ static const char s_app_js[] =
" reconnectDelay = 1000;\n"
" connect();\n"
"});\n"
"const scheduleFit = () => {\n"
" if (fitFrame !== 0) return;\n"
" fitFrame = window.requestAnimationFrame(() => {\n"
" fitFrame = 0;\n"
" try { fitAddon.fit(); } catch (_) {}\n"
" });\n"
"const fitTerminal = () => {\n"
" fitFrame = 0;\n"
" const bounds = terminalHost.getBoundingClientRect();\n"
" const width = Math.floor(bounds.width);\n"
" const height = Math.floor(bounds.height);\n"
" if (width < 1 || height < 1 || (width === lastFitWidth && height === lastFitHeight)) return;\n"
" lastFitWidth = width;\n"
" lastFitHeight = height;\n"
" try {\n"
" const dimensions = fitAddon.proposeDimensions();\n"
" if (dimensions && dimensions.cols > 0 && dimensions.rows > 0 &&\n"
" (dimensions.cols !== terminal.cols || dimensions.rows !== terminal.rows)) {\n"
" terminal.resize(dimensions.cols, dimensions.rows);\n"
" }\n"
" } catch (_) {}\n"
"};\n"
"if ('ResizeObserver' in window) new ResizeObserver(scheduleFit).observe(terminalHost);\n"
"const scheduleFit = () => {\n"
" if (fitFrame === 0) fitFrame = window.requestAnimationFrame(fitTerminal);\n"
"};\n"
"const resizeObserver = 'ResizeObserver' in window ? new ResizeObserver(scheduleFit) : null;\n"
"if (resizeObserver !== null) resizeObserver.observe(terminalHost);\n"
"window.addEventListener('resize', scheduleFit);\n"
"const textValue = (value, fallback) => typeof value === 'string' && value.length > 0 ? value : fallback;\n"
"const updateStatus = (status) => {\n"
@@ -397,6 +415,9 @@ static const char s_app_js[] =
" ++connectionGeneration;\n"
" clearReconnectTimer();\n"
" if (statusTimer !== null) window.clearInterval(statusTimer);\n"
" if (fitFrame !== 0) window.cancelAnimationFrame(fitFrame);\n"
" if (resizeObserver !== null) resizeObserver.disconnect();\n"
" window.removeEventListener('resize', scheduleFit);\n"
" if (ticketAbort !== null) ticketAbort.abort();\n"
" if (socket !== null) socket.close();\n"
" socket = null;\n"
@@ -484,8 +505,9 @@ static esp_err_t set_response_headers(httpd_req_t *request,
if (result == ESP_OK && response->content_security_policy) {
result = httpd_resp_set_hdr(
request, "Content-Security-Policy",
"default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; "
"connect-src 'self'; base-uri 'none'; form-action 'none'; "
"default-src 'none'; script-src 'self'; script-src-elem 'self'; "
"style-src 'self' 'unsafe-inline'; img-src data:; connect-src 'self'; "
"base-uri 'none'; form-action 'none'; "
"frame-ancestors 'none'");
}
return result;