also looking at this
fix(test): stop a leaked spawn mock hanging the gateway CLI tests (#2267), pin forwarded headers (#2265)
#2268
4 files
+57
-4
| @@ -353,9 +353,7 @@ async function freePort(): Promise<number> { | ||
| 353 | 353 | return port |
| 354 | 354 | } |
| 355 | 355 | |
| 356 | const CLI_STARTS = process.platform !== 'linux' | |
| 357 | ||
| 358 | describe.skipIf(!CLI_STARTS)('rpx gateway (CLI, end to end)', () => { | |
| 356 | describe('rpx gateway (CLI, end to end)', () => { | |
| 359 | 357 | it('starts and prints its help', async () => { |
| 360 | 358 | const cli = path.join(import.meta.dir, '..', 'bin', 'cli.ts') |
| 361 | 359 | const child = spawn(process.execPath, [cli, '--help'], { stdio: ['ignore', 'pipe', 'pipe'] }) |
| @@ -45,7 +45,15 @@ describe('ProcessManager', () => { | ||
| 45 | 45 | }) |
| 46 | 46 | |
| 47 | 47 | afterEach(() => { |
| 48 | // No cleanup needed - Bun's spyOn handles cleanup automatically | |
| 48 | // Bun's spyOn does NOT restore on its own, and the `childProcess.spawn` spy | |
| 49 | // above is installed on the shared module object — so without this it stays | |
| 50 | // installed for every test file that runs after this one in the same `bun | |
| 51 | // test` process. Anything spawning a subprocess then gets the stub back | |
| 52 | // (pid 12345, no-op `on`/`stdout.on`), so its child never starts, never | |
| 53 | // prints and never exits: #2267, where `rpx --help` appeared to hang on | |
| 54 | // Linux CI purely because bun's file order happened to put this file first | |
| 55 | // there. Restore, and keep it restored. | |
| 56 | mock.restore() | |
| 49 | 57 | }) |
| 50 | 58 | |
| 51 | 59 | describe('isRunning', () => { |
| @@ -246,6 +246,50 @@ describe('client address forwarding', () => { | ||
| 246 | 246 | } |
| 247 | 247 | }) |
| 248 | 248 | |
| 249 | it('gives the upstream the whole forwarded set on the ordinary HTTP path', async () => { | |
| 250 | // #2265: the report was that an upstream saw only `x-real-ip` plus a rewritten | |
| 251 | // `Host`, with no `x-forwarded-host`/`-for`/`-proto`, leaving an app behind rpx | |
| 252 | // unable to learn the hostname or scheme the client actually used — a CSRF | |
| 253 | // origin guard comparing `Origin` against the host then rejected every real | |
| 254 | // browser request. The pooled and fallback transports each write the set | |
| 255 | // separately, so both are pinned here rather than only the one in use. | |
| 256 | const upstream = Bun.serve({ | |
| 257 | port: 0, | |
| 258 | hostname: '127.0.0.1', | |
| 259 | fetch: r => new Response(JSON.stringify({ | |
| 260 | host: r.headers.get('host'), | |
| 261 | for: r.headers.get('x-forwarded-for'), | |
| 262 | realIp: r.headers.get('x-real-ip'), | |
| 263 | forwardedHost: r.headers.get('x-forwarded-host'), | |
| 264 | proto: r.headers.get('x-forwarded-proto'), | |
| 265 | }), { headers: { 'content-type': 'application/json' } }), | |
| 266 | }) | |
| 267 | try { | |
| 268 | const handler = createProxyFetchHandler(() => ({ sourceHost: `127.0.0.1:${upstream.port}` })) | |
| 269 | const expected = { | |
| 270 | host: `127.0.0.1:${upstream.port}`, | |
| 271 | for: '198.51.100.42', | |
| 272 | realIp: '198.51.100.42', | |
| 273 | forwardedHost: 'bughq.org', | |
| 274 | proto: 'https', | |
| 275 | } | |
| 276 | ||
| 277 | // The pooled transport, which carries ordinary requests. | |
| 278 | const pooled = await handler(req('https://bughq.org/api/thing'), serverWithPeer('198.51.100.42')) | |
| 279 | expect(await pooled?.json()).toEqual(expected) | |
| 280 | ||
| 281 | // ...and the fetch fallback, which the pool declines an SSE request to. | |
| 282 | const fallback = await handler( | |
| 283 | req('https://bughq.org/api/thing', { accept: 'text/event-stream' }), | |
| 284 | serverWithPeer('198.51.100.42'), | |
| 285 | ) | |
| 286 | expect(await fallback?.json()).toEqual(expected) | |
| 287 | } | |
| 288 | finally { | |
| 289 | upstream.stop(true) | |
| 290 | } | |
| 291 | }) | |
| 292 | ||
| 249 | 293 | it('carries the peer onto the websocket upgrade too', async () => { |
| 250 | 294 | const handler = createProxyFetchHandler(() => ({ sourceHost: 'localhost:3002' })) |
| 251 | 295 | let data: any |
| @@ -183,6 +183,9 @@ describe('proxyViaPool', () => { | ||
| 183 | 183 | expect(got['x-forwarded-for']).toBe('203.0.113.7') |
| 184 | 184 | expect(got['x-real-ip']).toBe('203.0.113.7') |
| 185 | 185 | expect(got['x-forwarded-host']).toBe('site.test') |
| 186 | // rpx terminates TLS, so the upstream cannot otherwise tell the client arrived | |
| 187 | // over it — anything building absolute URLs or forcing HTTPS reads this. | |
| 188 | expect(got['x-forwarded-proto']).toBe('https') | |
| 186 | 189 | expect(got['x-custom']).toBe('keep-me') // unrelated client headers pass through |
| 187 | 190 | }) |
| 188 | 191 | |