The HTTP fetch provider resolved each hostname, rejected any non-public answer, and pinned the connection. That refused every page behind TUN-mode proxies with fake-IP DNS and did not confine a model that has shell network access. Requests now go through the installed proxy dispatcher and the system resolver with no address restriction. HttpFetchResolver and the HttpFetchProvider resolver parameter are removed; the upgrade guide covers the migration. Co-authored-by: turtle1999 <[email protected]>
112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
|
|
import type { AddressInfo } from 'node:net'
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
|
|
import { HttpFetchProvider } from '@deepseek-ai/dsh-web-fetch-http'
|
|
import type { HttpFetchLimits } from '@deepseek-ai/dsh-web-fetch-http'
|
|
|
|
const limits: HttpFetchLimits = {
|
|
maxResponseBytes: 5_000_000,
|
|
maxBodyChars: 100_000,
|
|
timeoutMs: 5_000,
|
|
maxRedirects: 5,
|
|
userAgent: 'test-agent/1.0',
|
|
}
|
|
|
|
/** Absolute-form targets the fake proxy saw; a populated entry proves the hop was tunnelled. */
|
|
let proxied: string[]
|
|
let proxy: Server
|
|
let origin: Server
|
|
let proxyUrl: string
|
|
let originUrl: string
|
|
|
|
/**
|
|
* The target for every assertion about a tunnelled hop. Loopback cannot serve: no policy routes
|
|
* this machine through a proxy. The host never resolves; the proxy answers the absolute-form request.
|
|
*/
|
|
const proxyTarget = 'http://origin.test/page'
|
|
let disposeProxy: (() => Promise<void>) | undefined
|
|
|
|
function listen(server: Server): Promise<AddressInfo> {
|
|
return new Promise((resolve) => {
|
|
server.listen(0, '127.0.0.1', () => { resolve(server.address() as AddressInfo) })
|
|
})
|
|
}
|
|
|
|
function respond(_request: IncomingMessage, response: ServerResponse, body: string): void {
|
|
response.writeHead(200, { 'content-type': 'text/plain' })
|
|
response.end(body)
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
proxied = []
|
|
proxy = createServer((request, response) => {
|
|
proxied.push(request.url ?? '')
|
|
respond(request, response, 'via-proxy')
|
|
})
|
|
origin = createServer((request, response) => { respond(request, response, 'direct') })
|
|
const [proxyAddress, originAddress] = await Promise.all([listen(proxy), listen(origin)])
|
|
proxyUrl = `http://127.0.0.1:${String(proxyAddress.port)}`
|
|
originUrl = `http://127.0.0.1:${String(originAddress.port)}/page`
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await disposeProxy?.()
|
|
disposeProxy = undefined
|
|
await Promise.all([
|
|
new Promise<void>((resolve) => { proxy.close(() => { resolve() }) }),
|
|
new Promise<void>((resolve) => { origin.close(() => { resolve() }) }),
|
|
])
|
|
})
|
|
|
|
/**
|
|
* Install the policy of a user who exported one proxy for both schemes; the fixture disposes it
|
|
* after every case.
|
|
*/
|
|
async function installProxy(): Promise<() => Promise<void>> {
|
|
const env = { get: (name: string) => (name === 'HTTP_PROXY' || name === 'HTTPS_PROXY' ? { value: proxyUrl } : undefined) }
|
|
return await installProxyFromEnvironment(env, () => undefined)
|
|
}
|
|
|
|
describe('fetching through a proxy', () => {
|
|
it('tunnels the request through the installed proxy', async () => {
|
|
disposeProxy = await installProxy()
|
|
|
|
const result = await new HttpFetchProvider(limits).fetch({ url: proxyTarget })
|
|
|
|
expect(result.body.content).toBe('via-proxy')
|
|
expect(proxied).toEqual([proxyTarget])
|
|
})
|
|
|
|
it('connects directly to a host the policy does not proxy', async () => {
|
|
// A resolved policy never routes loopback through a proxy.
|
|
disposeProxy = await installProxy()
|
|
|
|
const result = await new HttpFetchProvider(limits).fetch({ url: originUrl })
|
|
|
|
expect(result.body.content).toBe('direct')
|
|
expect(proxied).toEqual([])
|
|
})
|
|
|
|
it('still refuses a cross-origin redirect on the proxied path', async () => {
|
|
proxy.removeAllListeners('request')
|
|
proxy.on('request', (request, response) => {
|
|
proxied.push(request.url ?? '')
|
|
response.writeHead(302, { location: 'http://elsewhere.example/next' })
|
|
response.end()
|
|
})
|
|
disposeProxy = await installProxy()
|
|
|
|
await expect(new HttpFetchProvider(limits).fetch({ url: proxyTarget }))
|
|
.rejects.toThrow(expect.objectContaining({ code: 'WEB_REDIRECT_BLOCKED' }))
|
|
})
|
|
|
|
it('still refuses a URL the transport policy rejects before any hop', async () => {
|
|
disposeProxy = await installProxy()
|
|
|
|
await expect(new HttpFetchProvider(limits).fetch({ url: 'ftp://example.com/x' }))
|
|
.rejects.toThrow(expect.objectContaining({ code: 'WEB_INVALID_URL' }))
|
|
expect(proxied).toEqual([])
|
|
})
|
|
})
|