150 lines
6.6 KiB
JavaScript
150 lines
6.6 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { createServer, request as httpRequest } from 'node:http'
|
|
import { connect as netConnect } from 'node:net'
|
|
import { mkdtemp } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import test from 'node:test'
|
|
import { parseTrustedHosts } from '../lib/config.mjs'
|
|
import { closeGatewayServer, createGatewayServer, HEALTH_PATH } from '../lib/proxy.mjs'
|
|
|
|
async function listen(server) {
|
|
await new Promise((resolve, reject) => {
|
|
server.once('error', reject)
|
|
server.listen(0, '127.0.0.1', resolve)
|
|
})
|
|
return server.address().port
|
|
}
|
|
|
|
async function close(server) {
|
|
server.closeAllConnections?.()
|
|
await new Promise(resolve => server.close(resolve))
|
|
}
|
|
|
|
function request(port, { path = '/', method = 'GET', headers = {}, body = '' } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const outgoing = httpRequest({ hostname: '127.0.0.1', port, path, method, headers }, response => {
|
|
const chunks = []
|
|
response.on('data', chunk => chunks.push(chunk))
|
|
response.on('end', () => resolve({
|
|
status: response.statusCode,
|
|
headers: response.headers,
|
|
body: Buffer.concat(chunks).toString('utf8'),
|
|
}))
|
|
})
|
|
outgoing.once('error', reject)
|
|
outgoing.end(body)
|
|
})
|
|
}
|
|
|
|
const initialized = Object.freeze({ status: async () => ({ state: 'initialized' }), handle: async () => false })
|
|
|
|
test('Gateway fails closed without an authenticated browser session while health remains available', async () => {
|
|
let upstreamRequests = 0
|
|
const upstream = createServer((_incoming, response) => { upstreamRequests += 1; response.end('upstream') })
|
|
const upstreamPort = await listen(upstream)
|
|
const gateway = createGatewayServer({ trustedHosts: parseTrustedHosts({}), upstreamPort })
|
|
const port = await listen(gateway)
|
|
try {
|
|
assert.equal((await request(port, { path: HEALTH_PATH, headers: { host: '127.0.0.1' } })).status, 200)
|
|
const navigation = await request(port, { headers: { accept: 'text/html', host: '127.0.0.1' } })
|
|
assert.equal(navigation.status, 303)
|
|
assert.match(navigation.headers.location, /^\/_dsh_platform\/auth\//)
|
|
assert.equal((await request(port, { path: '/api/sessions', headers: { host: '127.0.0.1' } })).status, 401)
|
|
assert.equal(upstreamRequests, 0)
|
|
} finally {
|
|
await Promise.all([closeGatewayServer(gateway), close(upstream)])
|
|
}
|
|
})
|
|
|
|
test('Gateway reports an unavailable authentication backend as a service failure', async () => {
|
|
const unavailable = new Error('access socket unavailable')
|
|
unavailable.browserAuthenticationBackend = true
|
|
const browserAuthentication = {
|
|
status: async () => { throw unavailable },
|
|
handle: async () => false,
|
|
}
|
|
const gateway = createGatewayServer({
|
|
trustedHosts: parseTrustedHosts({}), browserAuthentication, upstreamPort: 1,
|
|
})
|
|
const port = await listen(gateway)
|
|
try {
|
|
assert.equal((await request(port, {
|
|
path: HEALTH_PATH, headers: { host: '127.0.0.1' },
|
|
})).status, 200)
|
|
const unavailable = await request(port, { headers: { host: '127.0.0.1', accept: 'text/html' } })
|
|
assert.equal(unavailable.status, 503)
|
|
assert.match(unavailable.body, /authentication service unavailable/)
|
|
} finally { await closeGatewayServer(gateway) }
|
|
})
|
|
|
|
test('DSH and Management sessions authorize only their own route surfaces', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-gateway-session-domains-'))
|
|
const socketPath = join(root, 'management.sock')
|
|
const management = createServer((incoming, response) => response.end(`management:${incoming.url}`))
|
|
await new Promise((resolve, reject) => {
|
|
management.once('error', reject)
|
|
management.listen(socketPath, resolve)
|
|
})
|
|
const upstream = createServer((_incoming, response) => response.end('dsh'))
|
|
const upstreamPort = await listen(upstream)
|
|
const browserAuthentication = {
|
|
...initialized,
|
|
validateDsh: async request => ({ authenticated: request.headers.cookie === 'dsh=yes' }),
|
|
authorizePlugin: async request => request.headers.cookie === 'dsh=yes'
|
|
? { authorized: true, capability: { token: 'plugin-capability' } }
|
|
: { authorized: false },
|
|
validateManagement: async request => ({ authenticated: request.headers.cookie === 'management=yes' }),
|
|
enterManagement: async (_request, response) => { response.writeHead(303, { location: '/_dsh_platform/auth/management' }); response.end() },
|
|
}
|
|
const gateway = createGatewayServer({
|
|
trustedHosts: parseTrustedHosts({}), browserAuthentication,
|
|
managementSocketPath: socketPath, upstreamPort,
|
|
})
|
|
const port = await listen(gateway)
|
|
try {
|
|
assert.equal((await request(port, { headers: { host: '127.0.0.1', cookie: 'dsh=yes' } })).body, 'dsh')
|
|
assert.equal((await request(port, {
|
|
path: '/_dsh_platform/plugin-api/v1/status', headers: { host: '127.0.0.1', cookie: 'dsh=yes' },
|
|
})).body, 'management:/_dsh_platform/api/v1/status')
|
|
assert.equal((await request(port, {
|
|
path: '/_dsh_platform/api/v1/status', headers: { host: '127.0.0.1', cookie: 'dsh=yes' },
|
|
})).status, 401)
|
|
assert.equal((await request(port, {
|
|
path: '/_dsh_platform/api/v1/status', headers: { host: '127.0.0.1', cookie: 'management=yes' },
|
|
})).body, 'management:/_dsh_platform/api/v1/status')
|
|
assert.equal((await request(port, {
|
|
path: '/', headers: { host: '127.0.0.1', cookie: 'management=yes' },
|
|
})).status, 401)
|
|
} finally {
|
|
await Promise.all([closeGatewayServer(gateway), close(management), close(upstream)])
|
|
}
|
|
})
|
|
|
|
test('terminal WebSocket requires Management rather than DSH authentication', async () => {
|
|
const browserAuthentication = {
|
|
...initialized,
|
|
validateDsh: async () => ({ authenticated: true }),
|
|
validateManagement: async () => ({ authenticated: false }),
|
|
enterManagement: async () => {},
|
|
}
|
|
const gateway = createGatewayServer({ trustedHosts: parseTrustedHosts({}), browserAuthentication, upstreamPort: 1 })
|
|
const port = await listen(gateway)
|
|
const client = netConnect(port, '127.0.0.1')
|
|
try {
|
|
await new Promise((resolve, reject) => { client.once('connect', resolve); client.once('error', reject) })
|
|
client.write([
|
|
'GET /_dsh_platform/api/v1/terminal/sessions/123e4567-e89b-42d3-a456-426614174000/stream HTTP/1.1',
|
|
'Host: 127.0.0.1', 'Connection: Upgrade', 'Upgrade: websocket', '', '',
|
|
].join('\r\n'))
|
|
const response = await new Promise((resolve, reject) => {
|
|
client.once('data', data => resolve(data.toString('utf8')))
|
|
client.once('error', reject)
|
|
})
|
|
assert.match(response, /^HTTP\/1\.1 401 Unauthorized/)
|
|
} finally {
|
|
client.destroy()
|
|
await closeGatewayServer(gateway)
|
|
}
|
|
})
|