feat(control-panel): web admin UI for the zero-touch tunnel
Loopback Fastify auth-broker + esbuild SPA. Operator password login (constant-time, signed HttpOnly session cookie, per-forwarded-IP rate-limit) → session-gated proxy that mints a fresh 60s manage capability token per call to the control-plane admin API: list hosts, mint pairing codes (with QR + pair command), revoke hosts. Security headers + CSP, CP_URL pinned loopback (anti-SSRF), hostId dot-segment guard. 55 tests pass; security-reviewed. Deployed behind nginx panel.terminal.yaojia.wang.
This commit is contained in:
26
control-panel/src/security/request.ts
Normal file
26
control-panel/src/security/request.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Request-scoped security helpers shared by the route plugins: detect HTTPS (drives the Secure
|
||||
* cookie flag) and read/verify the session cookie off an incoming Fastify request.
|
||||
*/
|
||||
import type { FastifyRequest } from 'fastify'
|
||||
import { parseCookieHeader, SESSION_COOKIE_NAME } from './cookies.js'
|
||||
import { verifySessionToken } from './session.js'
|
||||
|
||||
/**
|
||||
* True iff the request arrived over HTTPS — directly (`socket.encrypted`) or via a TLS-terminating
|
||||
* edge that set `x-forwarded-proto: https`. Even though the panel binds loopback, a reverse proxy
|
||||
* may front it; honour XFP so the Secure flag is correct on the tunnel path.
|
||||
*/
|
||||
export function isSecureRequest(req: FastifyRequest): boolean {
|
||||
const xfp = req.headers['x-forwarded-proto']
|
||||
const proto = Array.isArray(xfp) ? xfp[0] : xfp
|
||||
if (typeof proto === 'string' && proto.split(',')[0]?.trim().toLowerCase() === 'https') return true
|
||||
const socket = req.raw.socket as { encrypted?: boolean } | undefined
|
||||
return socket?.encrypted === true
|
||||
}
|
||||
|
||||
/** True iff the request carries a valid, unexpired session cookie signed with `sessionSecret`. */
|
||||
export function requestIsAuthed(req: FastifyRequest, sessionSecret: string, nowMs: number): boolean {
|
||||
const cookies = parseCookieHeader(req.headers.cookie)
|
||||
return verifySessionToken(sessionSecret, cookies[SESSION_COOKIE_NAME], nowMs)
|
||||
}
|
||||
Reference in New Issue
Block a user