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:
48
control-panel/test/session.test.ts
Normal file
48
control-panel/test/session.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { createSessionToken, verifySessionToken, SESSION_TTL_SEC } from '../src/security/session.js'
|
||||
|
||||
const SECRET = 'session-secret-value-1234567890'
|
||||
|
||||
describe('session token', () => {
|
||||
it('round-trips: a freshly minted token verifies', () => {
|
||||
const now = 1_000_000_000_000
|
||||
const token = createSessionToken(SECRET, now)
|
||||
expect(verifySessionToken(SECRET, token, now)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a token signed with a different secret', () => {
|
||||
const now = Date.now()
|
||||
const token = createSessionToken(SECRET, now)
|
||||
expect(verifySessionToken('another-secret-value-000000000', token, now)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a tampered MAC', () => {
|
||||
const now = Date.now()
|
||||
const token = createSessionToken(SECRET, now)
|
||||
const [payload] = token.split('.')
|
||||
expect(verifySessionToken(SECRET, `${payload}.deadbeef`, now)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a tampered (extended) expiry', () => {
|
||||
const now = Date.now()
|
||||
const token = createSessionToken(SECRET, now)
|
||||
const mac = token.split('.')[1]
|
||||
const farFuture = Math.floor(now / 1000) + 999999
|
||||
expect(verifySessionToken(SECRET, `${farFuture}.${mac}`, now)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an expired token', () => {
|
||||
const now = 1_000_000_000_000
|
||||
const token = createSessionToken(SECRET, now)
|
||||
const afterExpiry = now + (SESSION_TTL_SEC + 1) * 1000
|
||||
expect(verifySessionToken(SECRET, token, afterExpiry)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects malformed tokens', () => {
|
||||
const now = Date.now()
|
||||
expect(verifySessionToken(SECRET, undefined, now)).toBe(false)
|
||||
expect(verifySessionToken(SECRET, '', now)).toBe(false)
|
||||
expect(verifySessionToken(SECRET, 'no-dot', now)).toBe(false)
|
||||
expect(verifySessionToken(SECRET, 'notanumber.abcd', now)).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user