feat!: the token on every route, Secure Config Only goes (step 4b)

Under Secured every HTTP route but the public ones needs the token
(docs/adr/0006-two-security-modes-eleven-http-routes.md):
- isPublicRoute, isWebAsset and originMatchesHost in src/HttpAccess.h,
  with native tests.
- One middleware checks the token. It runs once the body is in, so
  /restore, /updateFirmware and /updateApplication check it on their
  first chunk and drop a refused upload.
- Static serving answers the web UI's files only: shades.cfg and
  shades.tmp are no longer served.
- The wildcard CORS headers go; WEB_CORS turns them back on for UI work
  from file://.
- The socket refuses a handshake whose Origin doesn't name its Host.
- "Secure Config Only" goes: a device set to it is Secured, and
  /loginContext drops permissions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-27 23:03:59 +01:00
parent 13ec38823b
commit ecb0260f61
16 changed files with 392 additions and 54 deletions

View file

@ -79,14 +79,15 @@ class Schema:
class ProtoClient:
def __init__(self, host, port=80, path="/ws", params=None, timeout=10):
def __init__(self, host, port=80, path="/ws", params=None, timeout=10, headers=None):
self.host = host
query = ("?" + urllib.parse.urlencode(params)) if params else ""
self.sock = socket.create_connection((host, port), timeout=timeout)
key = base64.b64encode(os.urandom(16)).decode()
extra = "".join(f"{k}: {v}\r\n" for k, v in (headers or {}).items())
self.sock.sendall((
f"GET {path}{query} HTTP/1.1\r\nHost: {host}:{port}\r\nUpgrade: websocket\r\n"
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode())
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n{extra}\r\n").encode())
head = b""
while b"\r\n\r\n" not in head:
chunk = self.sock.recv(1024)