Files
dsh/upstream/container/platform/test/proxy-provider.test.mjs
T

294 lines
12 KiB
JavaScript

import assert from 'node:assert/strict'
import { once } from 'node:events'
import { createServer, request as httpRequest } from 'node:http'
import { createServer as createNetServer, connect } from 'node:net'
import test from 'node:test'
import { defaultProxyConfiguration, validateProxyConfiguration } from '../../control-plane/services/outbound-proxy/lib/contracts.mjs'
import { createOutboundProxyControl } from '../../control-plane/services/outbound-proxy/lib/control.mjs'
import { createScopedProxyServer } from '../../control-plane/services/outbound-proxy/lib/data-plane.mjs'
import { ProviderHandleStore, providerHandleInternals } from '../../control-plane/services/outbound-proxy/lib/provider-handles.mjs'
import { selectProxyRoute } from '../../control-plane/services/outbound-proxy/lib/policy.mjs'
const sockets = new WeakMap()
async function listen(server) {
const active = new Set()
sockets.set(server, active)
server.on('connection', socket => {
active.add(socket)
socket.once('close', () => active.delete(socket))
})
server.listen(0, '127.0.0.1')
await once(server, 'listening')
return server.address().port
}
async function close(server) {
for (const socket of sockets.get(server) ?? []) socket.destroy()
if (!server.listening) return
server.closeAllConnections?.()
await new Promise(resolve => server.close(resolve))
}
function snapshot({ revision = 'revision-one', proxyPort = null, policy = { proxyEnabled: false } } = {}) {
const defaults = defaultProxyConfiguration()
const value = validateProxyConfiguration({
...defaults,
enabled: proxyPort !== null,
proxy: {
...defaults.proxy,
host: proxyPort === null ? '' : '127.0.0.1',
port: proxyPort,
username: proxyPort === null ? '' : 'external-user',
password: proxyPort === null ? undefined : 'external-password',
},
modelApi: { default: defaults.modelApi.default, providers: { deepseek: policy } },
})
return Object.freeze({ revision, recovery: 'none', ...value })
}
test('model Provider switch selects direct or independent proxy routing', async () => {
const defaults = defaultProxyConfiguration()
const make = (policy, scopes = {}) => Object.freeze({
revision: 'revision-one',
recovery: 'none',
...validateProxyConfiguration({
...defaults,
enabled: true,
proxy: { ...defaults.proxy, host: '127.0.0.1', port: 7890 },
scopes: { ...defaults.scopes, ...scopes },
modelApi: { default: defaults.modelApi.default, providers: { custom: policy } },
}),
})
const route = state => selectProxyRoute({
snapshot: state, scope: 'modelApi', providerId: 'custom', host: 'api.example.test', port: 443,
})
assert.equal((await route(make({ proxyEnabled: false }, { dshCore: true }))).reason, 'provider-direct')
assert.equal((await route(make({ proxyEnabled: true }, { dshCore: false, dshPlugins: false }))).reason, 'provider-proxy')
assert.equal((await route(make({ proxyEnabled: true }, { dshPlugins: true }))).reason, 'provider-proxy')
})
test('unconfigured model Providers inherit the complete default policy', async () => {
const defaults = defaultProxyConfiguration()
const snapshot = Object.freeze({
revision: 'revision-one', recovery: 'none',
...validateProxyConfiguration({
...defaults,
enabled: true,
proxy: { ...defaults.proxy, host: '127.0.0.1', port: 7890 },
scopes: { ...defaults.scopes, dshCore: true },
modelApi: { default: { proxyEnabled: true }, providers: {} },
}),
})
const result = await selectProxyRoute({
snapshot, scope: 'modelApi', providerId: 'new-provider', host: 'api.example.test', port: 443,
})
assert.equal(result.reason, 'provider-proxy')
})
function request({ port, path = '/', method = 'GET', headers = {}, body }) {
return new Promise((resolve, reject) => {
const outgoing = httpRequest({ host: '127.0.0.1', port, path, method, headers }, response => {
const chunks = []
response.on('data', chunk => chunks.push(chunk))
response.once('end', () => resolve({
status: response.statusCode,
headers: response.headers,
body: Buffer.concat(chunks),
}))
})
outgoing.once('error', reject)
if (body !== undefined) outgoing.end(body)
else outgoing.end()
})
}
function rawExchange(port, value, waitFor = '\r\n\r\n') {
return new Promise((resolve, reject) => {
const socket = connect({ host: '127.0.0.1', port })
let bytes = Buffer.alloc(0)
const timer = setTimeout(() => {
socket.destroy()
reject(new Error('provider CONNECT fixture timed out'))
}, 3_000)
socket.once('connect', () => socket.write(value))
socket.on('data', chunk => {
bytes = Buffer.concat([bytes, chunk])
if (!bytes.includes(waitFor)) return
clearTimeout(timer)
resolve({ socket, bytes })
})
socket.once('error', reject)
socket.once('close', () => clearTimeout(timer))
})
}
test('issues opaque provider handles and rejects expiry or policy revision changes', () => {
let now = 1_000
let generated = 0
const handles = new ProviderHandleStore({
ttlMs: 100,
now: () => now,
random: size => Buffer.alloc(size, ++generated),
})
const state = snapshot()
const issued = handles.issue({ providerId: 'deepseek', policyRevision: state.revision }, state)
assert.match(issued.handle, providerHandleInternals.HANDLE_PATTERN)
assert.equal(Buffer.from(issued.handle, 'base64url').byteLength, providerHandleInternals.HANDLE_BYTES)
assert.equal(handles.resolve(`DSH-Provider ${issued.handle}`, state), 'deepseek')
assert.throws(() => handles.resolve(`Basic ${issued.handle}`, state), error => error.code === 'PROVIDER_HANDLE_INVALID')
assert.throws(() => handles.resolve(`DSH-Provider ${issued.handle}`, snapshot({ revision: 'revision-two' })), error => error.code === 'PROVIDER_HANDLE_INVALID')
const expiring = handles.issue({ providerId: 'deepseek', policyRevision: state.revision }, state)
now += 101
assert.throws(() => handles.resolve(`DSH-Provider ${expiring.handle}`, state), error => error.code === 'PROVIDER_HANDLE_INVALID')
assert.throws(() => handles.issue({ providerId: '../secret', policyRevision: state.revision }, state), /provider ID/)
assert.throws(() => handles.issue({ providerId: 'deepseek', policyRevision: 'stale' }, state), error => error.code === 'REVISION_CONFLICT' && error.statusCode === 409)
})
test('control socket issues a provider handle only for the current revision', async () => {
const state = snapshot()
const handles = new ProviderHandleStore()
const control = createOutboundProxyControl({
getSnapshot: () => state,
routeHealth: { status: () => ({}) },
providerHandles: handles,
})
const port = await listen(control)
try {
const current = await request({
port,
path: '/v1/provider-handles',
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ providerId: 'deepseek', policyRevision: state.revision }),
})
assert.equal(current.status, 201)
const issued = JSON.parse(current.body)
assert.equal(handles.resolve(`DSH-Provider ${issued.handle}`, state), 'deepseek')
const stale = await request({
port,
path: '/v1/provider-handles',
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ providerId: 'deepseek', policyRevision: 'stale' }),
})
assert.equal(stale.status, 409)
assert.equal(JSON.parse(stale.body).error.code, 'REVISION_CONFLICT')
const oversized = await request({
port,
path: '/v1/provider-handles',
method: 'POST',
body: JSON.stringify({ providerId: 'deepseek', policyRevision: state.revision, padding: 'x'.repeat(4096) }),
})
assert.equal(oversized.status, 400)
} finally {
await close(control)
}
})
test('model Provider entry requires a valid handle and strips it from a direct request', async () => {
let authorization
const target = createServer((incoming, response) => {
authorization = incoming.headers['proxy-authorization']
response.end('provider-direct')
})
const targetPort = await listen(target)
const state = snapshot()
const handles = new ProviderHandleStore()
const proxy = createScopedProxyServer({ scope: 'modelApi', getSnapshot: () => state, providerHandles: handles })
const proxyPort = await listen(proxy)
try {
const missing = await request({ port: proxyPort, path: `http://127.0.0.1:${targetPort}/` })
assert.equal(missing.status, 407)
assert.equal(missing.headers['x-dsh-proxy-error'], 'PROVIDER_HANDLE_INVALID')
assert.equal(missing.headers['proxy-authenticate'], 'DSH-Provider')
const issued = handles.issue({ providerId: 'deepseek', policyRevision: state.revision }, state)
const accepted = await request({
port: proxyPort,
path: `http://127.0.0.1:${targetPort}/`,
headers: { 'proxy-authorization': `DSH-Provider ${issued.handle}` },
})
assert.equal(accepted.status, 200)
assert.equal(accepted.body.toString(), 'provider-direct')
assert.equal(authorization, undefined)
} finally {
await close(proxy)
await close(target)
}
})
test('model Provider proxy replaces the internal handle with external Basic authentication', async () => {
let observed
const upstream = createServer((incoming, response) => {
observed = { path: incoming.url, authorization: incoming.headers['proxy-authorization'] }
response.end('provider-proxied')
})
const upstreamPort = await listen(upstream)
const state = snapshot({ proxyPort: upstreamPort, policy: { proxyEnabled: true } })
const handles = new ProviderHandleStore()
const issued = handles.issue({ providerId: 'deepseek', policyRevision: state.revision }, state)
const proxy = createScopedProxyServer({ scope: 'modelApi', getSnapshot: () => state, providerHandles: handles })
const proxyPort = await listen(proxy)
try {
const result = await request({
port: proxyPort,
path: 'http://provider.example/v1/models',
headers: { 'proxy-authorization': `DSH-Provider ${issued.handle}` },
})
assert.equal(result.status, 200)
assert.equal(result.body.toString(), 'provider-proxied')
assert.equal(observed.path, 'http://provider.example/v1/models')
assert.equal(observed.authorization, `Basic ${Buffer.from('external-user:external-password').toString('base64')}`)
assert.doesNotMatch(observed.authorization, new RegExp(issued.handle))
} finally {
await close(proxy)
await close(upstream)
}
})
test('model Provider CONNECT requires a handle and never forwards it to the external proxy', async () => {
let observed = ''
const upstream = createNetServer(socket => {
let requestBytes = Buffer.alloc(0)
socket.on('data', chunk => {
requestBytes = Buffer.concat([requestBytes, chunk])
if (!requestBytes.includes('\r\n\r\n')) return
observed = requestBytes.toString('latin1')
socket.removeAllListeners('data')
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
socket.on('data', value => socket.write(value))
})
})
const upstreamPort = await listen(upstream)
const state = snapshot({ proxyPort: upstreamPort, policy: { proxyEnabled: true } })
const handles = new ProviderHandleStore()
const issued = handles.issue({ providerId: 'deepseek', policyRevision: state.revision }, state)
const proxy = createScopedProxyServer({ scope: 'modelApi', getSnapshot: () => state, providerHandles: handles })
const proxyPort = await listen(proxy)
let socket
try {
const rejected = await rawExchange(proxyPort, 'CONNECT provider.example:443 HTTP/1.1\r\nHost: provider.example:443\r\n\r\n')
rejected.socket.destroy()
assert.match(rejected.bytes.toString('latin1'), /^HTTP\/1\.1 407/)
const accepted = await rawExchange(proxyPort, [
'CONNECT provider.example:443 HTTP/1.1',
'Host: provider.example:443',
`Proxy-Authorization: DSH-Provider ${issued.handle}`,
'',
'',
].join('\r\n'))
socket = accepted.socket
assert.match(accepted.bytes.toString('latin1'), /^HTTP\/1\.1 200/)
assert.match(observed, new RegExp(`Proxy-Authorization: Basic ${Buffer.from('external-user:external-password').toString('base64')}`, 'i'))
assert.doesNotMatch(observed, new RegExp(issued.handle))
} finally {
socket?.destroy()
await close(proxy)
await close(upstream)
}
})