Files
web-terminal/control-panel/src/security/cookies.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

54 lines
1.9 KiB
TypeScript

/**
* Cookie parse + Set-Cookie serialization — dependency-light (no @fastify/cookie), mirroring the
* base app's src/http/auth.ts discipline. Values are returned verbatim (NOT URL-decoded); the
* session token uses a cookie-safe charset (base64url + '.') so there is nothing to decode.
*/
/** The panel session cookie name. HttpOnly (JS can't read it → XSS can't exfiltrate it). */
export const SESSION_COOKIE_NAME = 'panel_session'
/**
* Parse a raw `Cookie:` header into a name→value map. Malformed pairs (no `=`, empty name) are
* ignored; last write wins on duplicate names.
*/
export function parseCookieHeader(header: string | undefined): Record<string, string> {
const out: Record<string, string> = {}
if (header === undefined || header === '') return out
for (const part of header.split(';')) {
const eq = part.indexOf('=')
if (eq <= 0) continue
const name = part.slice(0, eq).trim()
if (name === '') continue
out[name] = part.slice(eq + 1).trim()
}
return out
}
interface SetCookieOptions {
readonly value: string
readonly maxAgeSec: number
readonly secure: boolean
}
/**
* Build a `Set-Cookie` value. Flags: HttpOnly (no JS read), SameSite=Strict (cross-site pages can't
* ride the cookie — CSRF/CSWSH defence), Path=/, Max-Age, and Secure ONLY when `secure` (a Secure
* cookie is never sent over http:// — forcing it would break a loopback/http operator session).
*/
export function buildSetCookie(opts: SetCookieOptions): string {
const parts = [
`${SESSION_COOKIE_NAME}=${opts.value}`,
'Path=/',
`Max-Age=${opts.maxAgeSec}`,
'HttpOnly',
'SameSite=Strict',
]
if (opts.secure) parts.push('Secure')
return parts.join('; ')
}
/** Build a `Set-Cookie` that immediately expires the session cookie (logout). */
export function buildClearCookie(opts: { secure: boolean }): string {
return buildSetCookie({ value: '', maxAgeSec: 0, secure: opts.secure })
}