DUNGEONS-34: simplify external HTTPS proxy setup

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-09-06 17:42:55 +05:00
parent 33dbf37ae3
commit 75e8fec4fe
6 changed files with 306 additions and 21 deletions

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ data/
*.log
frontend/dist
.webka-cache.json
webka.config.json

View File

@@ -24,15 +24,58 @@ The terminal prints the local URL, LAN URLs when available, and the setup-token
Press **Ctrl+C** to stop the app and any build subprocesses. Accounts remain in `data/`. The first login page creates the administrator account using the token from `data/setup-token`. The administrator cannot be deleted; resetting its password ends its sessions.
For HTTPS behind a reverse proxy, set `WEBKA_ORIGIN` to the exact public origin before launching. This override disables automatic HTTP aliases; optional `WEBKA_ALLOWED_ORIGINS` is a comma-separated list of additional trusted exact origins. Browser capture on another LAN device still requires trusted HTTPS; plain LAN HTTP permits viewing only.
For an existing HTTPS proxy, use the homepage setting below. Browser capture on another LAN device still requires a trusted HTTPS address.
For frontend hot reload, the separate development workflow remains available: run `cargo run --manifest-path backend/Cargo.toml` and `npm --prefix frontend run dev` in separate terminals, then open `http://localhost:3000`.
## Existing Nginx Proxy Manager on another machine
Set just the address you want to open in a browser (the address served by NPM):
```sh
npm run dev:all -- --homepage https://camera.example.com --port 3002
```
Replace `https://camera.example.com` with your actual HTTPS domain or IP, including a public port if needed. A bare hostname/IP is treated as HTTPS. A trailing slash is accepted; hosting under a subpath is not supported.
This mode automatically:
- Listens on `0.0.0.0` so the other machine can reach Webka (unless you explicitly set `--host`).
- Serves the frontend and API over **HTTP** on the chosen internal port; your existing NPM handles HTTPS.
- Uses the homepage for QR/share links, secure cookies, and allowed request origins.
- Disables mDNS, even if `WEBKA_MDNS=1` was previously set. No Caddy, `.local` hostname, or certificates on the Webka computer are needed.
In **Nginx Proxy Manager → Proxy Hosts**, configure:
| Setting | Value |
|---|---|
| Domain Names | Your homepage hostname, e.g. `camera.example.com` |
| Scheme | `http` |
| Forward Hostname / IP | The LAN IP of the **Webka computer**, e.g. `192.168.1.50` |
| Forward Port | `3002` (or your `--port`) |
| Websockets Support | Enabled |
| SSL | Select your existing trusted certificate; enable Force SSL |
Open the homepage HTTPS address on both the host and viewer. Its hostname must resolve to the **NPM machine**, and NPM must be able to reach the Webka machine's LAN IP/port. The homepage is the browser-facing address, not the internal upstream address. For an HTTPS IP address, the certificate must cover that IP and NPM must route it to Webka. See the [official NPM guide](https://nginxproxymanager.com/guide/) and [WebSocket proxy support](https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/backend/templates/proxy_host.conf).
To save the address, create `webka.config.json` in the repository root (or copy `webka.config.example.json`):
```json
{
"homepage": "https://camera.example.com",
"port": 3002
}
```
Then start with only `npm run dev:all`. `homepage` alone is sufficient if the internal port is the default `3000`. Optional `host` and `port` may also be saved; command-line options override the file. Restart to apply changes. This machine-specific file is ignored by Git. Homepage mode takes priority over legacy `WEBKA_ORIGIN`, `WEBKA_ALLOWED_ORIGINS`, and `WEBKA_MDNS` values.
Legacy manual proxy setup via `WEBKA_ORIGIN` and `WEBKA_ALLOWED_ORIGINS` remains supported when no homepage is configured. The launcher does not configure NPM or provision its certificates. The proxy carries page/API/signaling traffic; WebRTC media remains peer-to-peer on the LAN.
## Use on your LAN
A webpage **cannot launch a Rust server** on the device running its browser. Start the Rust service once on a computer on your LAN. A phone, tablet, or computer can then sign in as the administrator and act as the capturing host. This repository does not include a native iOS/Android service wrapper. Each service instance owns its account database.
Browser camera/screen APIs require a **secure context**: localhost on the service machine, or trusted HTTPS on a LAN device. LAN HTTP IP addresses cannot capture camera/screen. For LAN use, use the included `Caddyfile.example` with a trusted local certificate:
Browser camera/screen APIs require a **secure context**: localhost on the service machine, or trusted HTTPS on a LAN device. LAN HTTP IP addresses cannot capture camera/screen. If you already have an HTTPS proxy, use the homepage mode above. If you do not, the included `Caddyfile.example` is an optional alternative with a trusted local certificate:
1. Point `webka.local` at the service computer, through mDNS (`WEBKA_MDNS=1`), your router, or a hosts entry. `.local` resolution and multicast support vary by network/device; a local DNS name or IP with a matching trusted certificate also works.
2. Configure Caddy with the example below, and install/trust Caddy’s local CA on **every** client device. Keep this CA private.
@@ -93,6 +136,8 @@ cargo build --manifest-path backend/Cargo.toml
python3 scripts/smoke.py
# Optional POSIX integration test: reserves port 3002, tests startup/restart/stop.
python3 scripts/test-launcher.py
# Optional POSIX HTTPS proxy integration test (requires OpenSSL).
python3 scripts/test-proxy.py
```
`test:install` checks the frontend lockfile from a temporary folder without `node_modules`. When updating dependencies, run it with both npm 10 (bundled with Node 22) and npm 11; root `npm install` only installs the launcher package and does not repair the frontend lockfile.

View File

@@ -10,22 +10,42 @@ import { setTimeout as delay } from 'node:timers/promises';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
export function parseArgs(args) {
let host = '127.0.0.1', port = '3000';
export function readSettings(path = join(root, 'webka.config.json')) {
if (!existsSync(path)) return {};
let settings;
try { settings = JSON.parse(readFileSync(path, 'utf8')); }
catch { throw new Error('webka.config.json must contain valid JSON.'); }
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) throw new Error('webka.config.json must contain an object.');
for (const key of Object.keys(settings)) if (!['homepage', 'host', 'port'].includes(key)) throw new Error(`Unknown webka.config.json setting: ${key}`);
return settings;
}
export function normalizeHomepage(value) {
if (typeof value !== 'string' || !value.trim()) throw new Error('Homepage must be an HTTPS address, for example https://camera.example.com.');
let parsed;
try { parsed = new URL(value.includes('://') ? value.trim() : `https://${value.trim()}`); }
catch { throw new Error('Homepage must be a valid HTTPS hostname or IP address.'); }
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.pathname !== '/' || parsed.search || parsed.hash || ['0.0.0.0', '[::]'].includes(parsed.hostname)) throw new Error('Homepage must be an HTTPS address without a path, credentials, or query (use the address served by your proxy).');
return parsed.origin;
}
export function parseArgs(args, settings = {}) {
let host = settings.host, port = String(settings.port ?? '3000'), homepage = settings.homepage;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--help' || arg === '-h') return { help: true };
if (arg === '--host' || arg === '--port') {
if (arg === '--host' || arg === '--port' || arg === '--homepage') {
if (!args[i + 1] || args[i + 1].startsWith('--')) throw new Error(`${arg} needs a value.`);
if (arg === '--host') host = args[++i]; else port = args[++i];
if (arg === '--host') host = args[++i]; else if (arg === '--port') port = args[++i]; else homepage = args[++i];
} else if (arg.startsWith('--host=')) host = arg.slice(7);
else if (arg.startsWith('--port=')) port = arg.slice(7);
else throw new Error(`Unknown argument: ${arg}. Use --host and --port (two ordinary hyphens).`);
else if (arg.startsWith('--homepage=')) homepage = arg.slice(11);
else throw new Error(`Unknown argument: ${arg}. Use --host, --port, or --homepage (two ordinary hyphens).`);
}
if (homepage !== undefined) homepage = normalizeHomepage(homepage);
host ??= homepage ? '0.0.0.0' : '127.0.0.1';
if (host === 'localhost') host = '127.0.0.1';
if (!isIP(host)) throw new Error('--host must be an IPv4/IPv6 address or localhost.');
if (typeof host !== 'string' || !isIP(host)) throw new Error('--host must be an IPv4/IPv6 address or localhost.');
if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) throw new Error('--port must be between 1 and 65535.');
return { host, port: Number(port) };
return { host, port: Number(port), ...(homepage ? { homepage } : {}) };
}
const hostname = host => isIP(host) === 6 ? `[${host}]` : host;
const url = (host, port) => `http://${hostname(host)}:${port}`;
@@ -33,20 +53,30 @@ function privateIPv4(ip) {
const parts = ip.split('.').map(Number);
return isIP(ip) === 4 && (parts[0] === 10 || (parts[0] === 192 && parts[1] === 168) || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31));
}
export function addresses({ host, port }, interfaces = networkInterfaces(), env = process.env) {
export function addresses({ host, port, homepage }, interfaces = networkInterfaces(), env = process.env) {
const wildcard = host === '0.0.0.0' || host === '::';
const local = url(wildcard ? (host === '::' ? '::1' : '127.0.0.1') : host, port);
const loopback = host === '127.0.0.1' || host === '::1' || wildcard;
const lan = wildcard ? [...new Set(Object.values(interfaces).flat().filter(item => item && !item.internal && item.family === 'IPv4' && privateIPv4(item.address)).map(item => url(item.address, port)))] : [];
const origin = env.WEBKA_ORIGIN || lan[0] || (loopback ? `http://localhost:${port}` : local);
const origin = homepage || env.WEBKA_ORIGIN || lan[0] || (loopback ? `http://localhost:${port}` : local);
const automatic = [origin, local, ...(loopback ? [`http://localhost:${port}`] : []), ...lan];
// Explicit HTTPS/reverse-proxy setups retain their exact configured origin.
const allowed = env.WEBKA_ALLOWED_ORIGINS !== undefined ? env.WEBKA_ALLOWED_ORIGINS.split(',').filter(Boolean) : env.WEBKA_ORIGIN ? [origin] : automatic;
const allowed = homepage ? [origin] : env.WEBKA_ALLOWED_ORIGINS !== undefined ? env.WEBKA_ALLOWED_ORIGINS.split(',').filter(Boolean) : env.WEBKA_ORIGIN ? [origin] : automatic;
for (const value of [origin, ...allowed]) {
const parsed = new URL(value);
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin !== value || parsed.username || parsed.password) throw new Error(`Use an exact http(s) origin without a path: ${value}`);
}
return { local, lan, origin, allowed: [...new Set(allowed)] };
return { local, lan, origin, allowed: [...new Set(allowed)], proxy: !!homepage };
}
export function connectionEnv(config, urls, env = process.env) {
return {
WEBKA_BIND: `${hostname(config.host)}:${config.port}`,
WEBKA_ORIGIN: urls.origin,
WEBKA_ALLOWED_ORIGINS: urls.allowed.join(','),
WEBKA_PUBLIC_PORT: String(new URL(urls.origin).port || (urls.origin.startsWith('https:') ? 443 : 80)),
WEBKA_MDNS: urls.proxy ? '0' : (env.WEBKA_MDNS || '0'),
};
}
function fingerprint(paths) {
@@ -72,9 +102,9 @@ function assertPort(host, port) {
}
export async function main(args = process.argv.slice(2)) {
const config = parseArgs(args);
const config = parseArgs(args, args.includes('--help') || args.includes('-h') ? {} : readSettings());
if (config.help) {
console.log('Usage: npm run dev:all -- [--host 127.0.0.1] [--port 3000]\nBuilds and runs the frontend + backend together. Ctrl+C stops the app.');
console.log('Usage: npm run dev:all -- [--host 127.0.0.1] [--port 3000] [--homepage https://camera.example.com]\nA homepage enables external HTTPS proxy mode, defaults --host to 0.0.0.0, and disables mDNS.\nOptional webka.config.json saves homepage, host, and port. CLI options override saved settings.\nBuilds and runs the frontend + backend together. Ctrl+C stops the app.');
return;
}
const [major, minor] = process.versions.node.split('.').map(Number);
@@ -137,7 +167,7 @@ export async function main(args = process.argv.slice(2)) {
if (metadata.status !== 0) throw new Error('Could not locate the Rust executable.');
const executable = join(JSON.parse(metadata.stdout).target_directory, 'debug', process.platform === 'win32' ? 'webka.exe' : 'webka');
const data = resolve(root, process.env.WEBKA_DATA || 'data');
const { proc, completion } = run(executable, [], { ...process.env, WEBKA_BIND: `${hostname(config.host)}:${config.port}`, WEBKA_ORIGIN: urls.origin, WEBKA_ALLOWED_ORIGINS: urls.allowed.join(','), WEBKA_PUBLIC_PORT: String(new URL(urls.origin).port || (urls.origin.startsWith('https:') ? 443 : 80)), WEBKA_ASSETS: join(root, 'frontend/.output/public'), WEBKA_DATA: data });
const { proc, completion } = run(executable, [], { ...process.env, ...connectionEnv(config, urls), WEBKA_ASSETS: join(root, 'frontend/.output/public'), WEBKA_DATA: data });
// Observe exit failures immediately while checking readiness.
let failure;
completion.catch(e => { failure = e; });
@@ -153,11 +183,15 @@ export async function main(args = process.argv.slice(2)) {
}
if (!ready && !stopping) { stop(); await completion; throw new Error('The server did not become ready. Check the error above.'); }
if (stopping) { await completion; return; }
if (urls.proxy) {
console.log(`\nWebka is ready!\n Open in browser: ${urls.origin}\n Proxy upstream: HTTP to this Webka computer's LAN IP, port ${config.port}\n HTTPS: handled by your existing proxy\n mDNS: disabled`);
} else {
console.log(`\nWebka is ready!\n This computer: ${urls.local}`);
for (const address of urls.lan) console.log(` LAN: ${address}`);
if (process.env.WEBKA_ORIGIN) console.log(` Public URL: ${urls.origin}`);
if (urls.lan.length) console.log('Use the localhost/127.0.0.1 URL to capture on this computer. Capturing on a phone requires trusted HTTPS (see README).');
}
console.log(` Setup token: ${join(data, 'setup-token')}\n Stop: Ctrl+C\n`);
if (urls.lan.length) console.log('Use the localhost/127.0.0.1 URL to capture on this computer. Capturing on a phone requires trusted HTTPS (see README).\n');
await completion;
} finally {
if (child && child.exitCode === null) stop();

View File

@@ -1,6 +1,9 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseArgs, addresses } from './dev-all.mjs';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parseArgs, addresses, connectionEnv, readSettings } from './dev-all.mjs';
test('default and named host/port options', () => {
assert.deepEqual(parseArgs([]), { host: '127.0.0.1', port: 3000 });
@@ -26,3 +29,38 @@ test('explicit HTTPS origin does not silently authorize HTTP aliases', () => {
assert.throws(() => addresses(parseArgs([]), {}, { WEBKA_ORIGIN: 'https://webka.local/login' }));
assert.throws(() => addresses(parseArgs([]), {}, { WEBKA_ALLOWED_ORIGINS: '*' }));
});
test('homepage mode accepts proxy domain/IP, normalizes HTTPS and exposes the upstream', () => {
assert.deepEqual(parseArgs(['--homepage', 'camera.example.com', '--port', '3002']), { host: '0.0.0.0', port: 3002, homepage: 'https://camera.example.com' });
assert.deepEqual(parseArgs(['--homepage=https://192.168.1.10:8443/', '--host=127.0.0.1']), { host: '127.0.0.1', port: 3000, homepage: 'https://192.168.1.10:8443' });
for (const address of ['', 'http://camera.example.com', 'https://camera.example.com/watch', 'https://a:b@camera.example.com', 'https://camera.example.com?x=1', 'https://camera.example.com#x', 'https://0.0.0.0', 'https://[::]']) assert.throws(() => parseArgs(['--homepage', address]));
});
test('saved settings apply, with command-line overrides', () => {
const settings = { homepage: 'https://camera.example.com', port: 3002 };
assert.deepEqual(parseArgs([], settings), { host: '0.0.0.0', port: 3002, homepage: settings.homepage });
assert.deepEqual(parseArgs(['--homepage', 'https://other.example.com', '--port', '3003', '--host', '127.0.0.1'], settings), { host: '127.0.0.1', port: 3003, homepage: 'https://other.example.com' });
assert.throws(() => parseArgs([], { port: 'invalid' }));
});
test('configuration file loads a homepage and rejects malformed or misspelled settings', () => {
const directory = mkdtempSync(join(tmpdir(), 'webka-settings-'));
const path = join(directory, 'webka.config.json');
try {
assert.deepEqual(readSettings(path), {});
writeFileSync(path, JSON.stringify({ homepage: 'camera.example.com', port: 3002 }));
assert.deepEqual(parseArgs([], readSettings(path)), { host: '0.0.0.0', port: 3002, homepage: 'https://camera.example.com' });
for (const invalid of ['{', 'null', '[]', '{"homePage":"camera.example.com"}']) {
writeFileSync(path, invalid);
assert.throws(() => readSettings(path), /webka.config.json/);
}
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
test('homepage wins over old HTTPS/mDNS environment settings without allowing HTTP aliases', () => {
const env = { WEBKA_MDNS: '1', WEBKA_ORIGIN: 'https://old.local', WEBKA_ALLOWED_ORIGINS: 'http://unsafe.example.com' };
const config = parseArgs(['--homepage', 'https://camera.example.com:8443', '--port', '3002']);
const urls = addresses(config, networks, env);
assert.equal(urls.origin, config.homepage);
assert.deepEqual(urls.allowed, [config.homepage]);
assert.deepEqual(connectionEnv(config, urls, env), { WEBKA_BIND: '0.0.0.0:3002', WEBKA_ORIGIN: config.homepage, WEBKA_ALLOWED_ORIGINS: config.homepage, WEBKA_MDNS: '0', WEBKA_PUBLIC_PORT: '8443' });
});

164
scripts/test-proxy.py Normal file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""POSIX integration: HTTPS reverse proxy -> Webka HTTP upstream, no mDNS."""
import base64
import hashlib
import http.client
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import signal
import socket
import ssl
import subprocess
import tempfile
import threading
import time
import urllib.error
import urllib.request
root = Path(__file__).resolve().parents[1]
def free_port():
with socket.socket() as sock:
sock.bind(('127.0.0.1', 0))
return sock.getsockname()[1]
upstream_port = free_port()
proxy_port = free_port()
homepage = f'https://127.0.0.1:{proxy_port}'
class Proxy(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def log_message(self, *args):
pass
def forward(self):
body = self.rfile.read(int(self.headers.get('Content-Length', 0)))
headers = dict(self.headers)
headers['X-Forwarded-Proto'] = 'https'
headers['X-Forwarded-For'] = '192.168.1.99'
connection = http.client.HTTPConnection('127.0.0.1', upstream_port, timeout=5)
try:
connection.request(self.command, self.path, body=body, headers=headers)
response = connection.getresponse()
self.send_response(response.status)
for name, value in response.getheaders():
if name.lower() not in ['transfer-encoding', 'content-length', 'connection']:
self.send_header(name, value)
if response.status == 101:
self.send_header('Connection', 'Upgrade')
self.end_headers()
self.wfile.flush()
# Establish the WebSocket upgrade across TLS and forward its first server frame.
# Explicit HTTPConnection upgrade retains the underlying socket.
head = response.fp.read(2)
self.wfile.write(head)
size = head[1] & 127
if size == 126:
extended = response.fp.read(2)
self.wfile.write(extended)
size = int.from_bytes(extended, 'big')
self.wfile.write(response.fp.read(size))
self.wfile.flush()
self.close_connection = True
else:
data = response.read()
self.send_header('Content-Length', str(len(data)))
self.send_header('Connection', 'close')
self.end_headers()
self.wfile.write(data)
self.close_connection = True
finally:
connection.close()
do_GET = forward
do_POST = forward
with tempfile.TemporaryDirectory(prefix='webka-proxy-test-') as temporary:
directory = Path(temporary)
cert, key = directory / 'cert.pem', directory / 'key.pem'
subprocess.run(['openssl', 'req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-keyout', str(key), '-out', str(cert), '-days', '1', '-subj', '/CN=127.0.0.1', '-addext', 'subjectAltName=IP:127.0.0.1'], check=True, capture_output=True)
context = ssl.create_default_context(cafile=str(cert))
data = directory / 'data'
# Deliberately stale values must be overridden by homepage mode.
env = dict(os.environ, WEBKA_DATA=str(data), WEBKA_MDNS='1', WEBKA_ORIGIN='https://old.local', WEBKA_ALLOWED_ORIGINS='http://old.example.com')
proxy = ThreadingHTTPServer(('127.0.0.1', proxy_port), Proxy)
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain(cert, key)
proxy.socket = server_context.wrap_socket(proxy.socket, server_side=True)
thread = threading.Thread(target=proxy.serve_forever, daemon=True)
thread.start()
logfile = directory / 'launch.log'
try:
with logfile.open('w') as output:
proc = subprocess.Popen(['npm', 'run', 'dev:all', '--', '--homepage', homepage, '--port', str(upstream_port)], cwd=root, env=env, stdout=output, stderr=subprocess.STDOUT, start_new_session=True)
try:
for _ in range(1200):
if proc.poll() is not None:
raise AssertionError(logfile.read_text())
if 'Webka is ready!' in logfile.read_text():
break
time.sleep(0.5)
else:
raise AssertionError('Startup timeout')
assert 'mDNS: disabled' in logfile.read_text()
assert f'Open in browser: {homepage}' in logfile.read_text()
def request(path, body=None, origin=homepage, cookie=None):
headers = {'Origin': origin, 'Content-Type': 'application/json'}
if cookie:
headers['Cookie'] = cookie
req = urllib.request.Request(homepage + path, data=json.dumps(body).encode() if body is not None else None, headers=headers)
return urllib.request.urlopen(req, context=context, timeout=5)
with request('/login') as response:
assert response.status == 200 and 'text/html' in response.headers['Content-Type']
credentials = {'username': 'owner', 'password': 'proxy-test-password123', 'setup_token': (data / 'setup-token').read_text()}
with request('/api/bootstrap', credentials) as response:
assert response.status == 201
with request('/api/login', credentials) as response:
assert 'Secure' in response.headers['Set-Cookie']
assert 'HttpOnly' in response.headers['Set-Cookie']
cookie = response.headers['Set-Cookie'].split(';')[0]
with request('/api/status', cookie=cookie) as response:
assert json.load(response)['local_url'] == homepage + '/watch'
with request('/api/session', cookie=cookie) as response:
assert json.load(response)['role'] == 'admin'
for wrong_origin in ['https://old.local', f'http://127.0.0.1:{upstream_port}', 'http://old.example.com']:
try:
request('/api/logout', {}, origin=wrong_origin, cookie=cookie)
raise AssertionError('Unexpected origin allowed')
except urllib.error.HTTPError as error:
assert error.code == 403
# A real WSS handshake is forwarded to the HTTP WebSocket upstream.
ws_key = base64.b64encode(os.urandom(16)).decode()
conn = http.client.HTTPSConnection('127.0.0.1', proxy_port, context=context, timeout=5)
conn.request('GET', '/api/signal?mode=host', headers={'Origin': homepage, 'Cookie': cookie, 'Connection': 'Upgrade', 'Upgrade': 'websocket', 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Key': ws_key})
response = conn.getresponse()
assert response.status == 101
expected = base64.b64encode(hashlib.sha1((ws_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').encode()).digest()).decode()
assert response.headers['Sec-WebSocket-Accept'] == expected
head = response.fp.read(2)
size = head[1] & 127
if size == 126:
size = int.from_bytes(response.fp.read(2), 'big')
assert json.loads(response.fp.read(size))['type'] == 'welcome'
conn.close()
print('PASS: HTTPS proxy login, Secure cookie, public share URL, origin restrictions, WSS upgrade, mDNS disabled.', flush=True)
finally:
if proc.poll() is None:
os.killpg(proc.pid, signal.SIGINT)
try:
proc.wait(timeout=12)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
proc.wait()
raise AssertionError('Launcher did not stop')
finally:
proxy.shutdown()
proxy.server_close()
thread.join(timeout=5)
with socket.socket() as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', upstream_port))
print('PASS: proxy and launcher stopped; HTTP upstream port released.', flush=True)

View File

@@ -0,0 +1,3 @@
{
"homepage": "https://camera.example.com"
}