Files
web-terminal/control-panel/src/security/request.ts
Yaojia Wang 675de771c7 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.
2026-07-19 19:47:51 +02:00

27 lines
1.3 KiB
TypeScript

/**
* 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)
}