panel-auth: DSH web panel password gate plugin (Basic auth + signed cookie, fail-open)
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// 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')
|
||||
Reference in New Issue
Block a user