test(09-01): verify x-forwarded-for over a real socket, not just presence
CI / check (push) Failing after 6m14s
CI / check (push) Failing after 6m14s
The prior test drove the proxying app via Hono's in-process app.request() harness, which has no real Node socket — so it could only assert the header was non-empty-or-absent, not that the real client IP round-trips. Spin the proxying app up with @hono/node-server (real loopback socket) and assert the upstream actually receives 127.0.0.1/::1, exercising the same remoteAddress lookup arenaProxy uses in production. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,24 @@ function buildProxyingApp() {
|
||||
return app
|
||||
}
|
||||
|
||||
// A real Node HTTP server for the proxying app itself, needed to exercise
|
||||
// the actual socket.remoteAddress lookup arenaProxy uses for x-forwarded-for
|
||||
// — Hono's in-process app.request() harness has no real socket to read.
|
||||
async function withRealProxyingServer<T>(fn: (baseUrl: string) => Promise<T>): Promise<T> {
|
||||
const app = buildProxyingApp()
|
||||
let server: ServerType
|
||||
const baseUrl = await new Promise<string>((resolve) => {
|
||||
server = serve({ fetch: app.fetch, port: 0 }, (info) => {
|
||||
resolve(`http://127.0.0.1:${(info as AddressInfo).port}`)
|
||||
})
|
||||
})
|
||||
try {
|
||||
return await fn(baseUrl)
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()))
|
||||
}
|
||||
}
|
||||
|
||||
describe('arenaProxy', () => {
|
||||
const originalEnv = process.env.ARENA_UPSTREAM_URL
|
||||
|
||||
@@ -229,22 +247,23 @@ describe('arenaProxy', () => {
|
||||
|
||||
it('forwards the client address in x-forwarded-for', async () => {
|
||||
process.env.ARENA_UPSTREAM_URL = upstreamUrl
|
||||
const app = buildProxyingApp()
|
||||
|
||||
const res = await app.request('/api/echo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
// Drive the proxying app over a REAL socket (loopback) so
|
||||
// c.env.incoming.socket.remoteAddress is actually populated, exercising
|
||||
// the real code path instead of Hono's in-process app.request() harness.
|
||||
const body = await withRealProxyingServer(async (baseUrl) => {
|
||||
const res = await fetch(`${baseUrl}/api/echo`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
return await res.json() as { xff: string | null }
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
const body = await res.json() as { xff: string | null }
|
||||
// app.request() drives the Hono app directly (no real Node socket), so the
|
||||
// remoteAddress lookup this proxy relies on may legitimately be
|
||||
// undetermined here — the behavioral contract is that the header is
|
||||
// either forwarded (non-empty) or cleanly absent, never invented/garbage.
|
||||
if (body.xff !== null) {
|
||||
expect(body.xff.length).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
expect(body.xff).toBeTruthy()
|
||||
// Loopback connection — either IPv4 or IPv6-mapped loopback form.
|
||||
expect(body.xff).toMatch(/127\.0\.0\.1|::1|::ffff:127\.0\.0\.1/)
|
||||
})
|
||||
|
||||
it('answers 502 when the arena is unreachable', async () => {
|
||||
|
||||
Reference in New Issue
Block a user