// Throwaway verification of panel-auth against a server structured exactly // like dsh-host-webserver: a createServer callback ('request') plus an // 'upgrade' listener. Does not touch the running panel. import { createServer } from 'node:http' import { strict as assert } from 'node:assert' import { createGuard, installGuard } from './index.js' import { hashPassword, verifyPassword } from './crypto.js' const cfg = { users: [{ username: 'admin', passwordHash: hashPassword('s3cret-pass') }], secret: 'unit-test-secret-0123456789abcdef', realm: 'Test Realm', cookieName: 'test_cookie', cookieTtlSeconds: 3600, } const guard = createGuard({ getConfig: () => cfg, logger: null }) let upgradeCount = 0 const server = createServer((req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end('panel') }) server.on('upgrade', (req, socket, head) => { upgradeCount++ socket.end('HTTP/1.1 101 Switching Protocols\r\nUpgrade: test\r\nConnection: Upgrade\r\n\r\n') }) const disposer = installGuard(server, guard, { logger: null }) await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) const base = `http://127.0.0.1:${server.address().port}` const raw = (path, headers = {}) => fetch(base + path, { headers, redirect: 'manual' }) const upgrade = async (headers = []) => { const net = await import('node:net') const sock = net.connect(server.address().port, '127.0.0.1') const response = await new Promise((resolve) => { let buf = '' sock.on('data', (d) => { buf += d.toString() if (buf.includes('\r\n\r\n')) resolve(buf) }) const h = ['GET /api/events.mux HTTP/1.1', 'Host: 127.0.0.1', 'Connection: Upgrade', 'Upgrade: websocket', ...headers] sock.write(h.join('\r\n') + '\r\n\r\n') }) sock.destroy() return response } // 1. anonymous → 401 + challenge let r = await raw('/') assert.equal(r.status, 401) assert.match(r.headers.get('www-authenticate'), /^Basic realm="Test Realm"/) // 2. wrong password → 401 r = await raw('/', { authorization: 'Basic ' + Buffer.from('admin:wrong').toString('base64') }) assert.equal(r.status, 401) // 3. correct password → 200 + Set-Cookie r = await raw('/', { authorization: 'Basic ' + Buffer.from('admin:s3cret-pass').toString('base64') }) assert.equal(r.status, 200) assert.equal(await r.text(), 'panel') const setCookie = r.headers.get('set-cookie') assert.match(setCookie, /^test_cookie=.+; Path=\/; HttpOnly; SameSite=Lax; Max-Age=3600$/) const cookieValue = setCookie.split(';')[0].slice('test_cookie='.length) // 4. cookie alone → 200, no new cookie r = await raw('/', { cookie: `test_cookie=${cookieValue}` }) assert.equal(r.status, 200) assert.equal(r.headers.get('set-cookie'), null) // 5. tampered cookie → 401 r = await raw('/', { cookie: 'test_cookie=' + cookieValue.slice(0, -2) + 'xx' }) assert.equal(r.status, 401) // 6. websocket upgrade anonymous → 401 on socket assert.match(await upgrade(), /^HTTP\/1\.1 401 /) // 7. websocket upgrade with cookie → passes through to original handler assert.match(await upgrade([`Cookie: test_cookie=${cookieValue}`]), /^HTTP\/1\.1 101 /) assert.equal(upgradeCount, 1) // 8. hot-disable via config → anonymous passes through (fail-open) cfg.users = [] r = await raw('/') assert.equal(r.status, 200) cfg.users = [{ username: 'admin', passwordHash: hashPassword('s3cret-pass') }] // 9. dispose restores original behavior (anonymous → 200 again) disposer() r = await raw('/') assert.equal(r.status, 200) assert.equal(await r.text(), 'panel') // 10. hash round-trip sanity assert.equal(verifyPassword('s3cret-pass', cfg.users[0].passwordHash), true) assert.equal(verifyPassword('other', cfg.users[0].passwordHash), false) server.close() console.log('ALL PANEL-AUTH TESTS PASSED')