diff --git a/.changeset/forwarded-host.md b/.changeset/forwarded-host.md new file mode 100644 index 00000000..22368211 --- /dev/null +++ b/.changeset/forwarded-host.md @@ -0,0 +1,5 @@ +--- +"@monetizationos/proxy": minor +--- + +Origin requests carry the viewer's host and protocol as `X-Forwarded-Host` and `X-Forwarded-Proto`, so origins can build URLs on the public domain. `originRequestHeaders` can override either. diff --git a/src/stages/originRequest.ts b/src/stages/originRequest.ts index 6f34ae6f..3f0de0a5 100644 --- a/src/stages/originRequest.ts +++ b/src/stages/originRequest.ts @@ -3,9 +3,15 @@ import type { PipelineContext } from '../context' import getTargetUrl from './getTargetUrl' export default function performOriginRequest(ctx: PipelineContext, request: Request, originFetcher: Fetcher): Promise { - const targetUrl = getTargetUrl(new URL(request.url), ctx.config.originUrl) + const viewerUrl = new URL(request.url) + const targetUrl = getTargetUrl(viewerUrl, ctx.config.originUrl) const originRequest = new Request(targetUrl, request) + // The origin is fetched on its own host, so pass it the viewer's. originRequestHeaders can override these. + originRequest.headers.delete('Forwarded') + originRequest.headers.set('X-Forwarded-Host', viewerUrl.host) + originRequest.headers.set('X-Forwarded-Proto', viewerUrl.protocol.slice(0, -1)) + for (const [name, value] of Object.entries(ctx.config.originRequestHeaders)) { originRequest.headers.set(name, value) } diff --git a/test/stages/originRequest.spec.ts b/test/stages/originRequest.spec.ts index 6cd5db14..1abc8aaa 100644 --- a/test/stages/originRequest.spec.ts +++ b/test/stages/originRequest.spec.ts @@ -60,6 +60,31 @@ describe('performOriginRequest', () => { expect(headers.get('x-keep')).toBe('client-value') }) + it("sends the viewer's host and protocol as X-Forwarded-Host and X-Forwarded-Proto", async () => { + const fetcher = MockFetcher(() => new Response('ok')) + + await performOriginRequest( + ctx(), + new Request('http://news.example.com:8080/page', { + headers: { 'X-Forwarded-Host': 'other.example', 'X-Forwarded-Proto': 'https', Forwarded: 'host=other.example' }, + }), + fetcher, + ) + + const headers = fetcher.calls[0]!.request.headers + expect(headers.get('x-forwarded-host')).toBe('news.example.com:8080') + expect(headers.get('x-forwarded-proto')).toBe('http') + expect(headers.has('forwarded')).toBe(false) + }) + + it('lets origin request headers override the forwarded headers', async () => { + const fetcher = MockFetcher(() => new Response('ok')) + + await performOriginRequest(ctx({ 'X-Forwarded-Host': 'origin.example.com' }), new Request('https://news.example.com/page'), fetcher) + + expect(fetcher.calls[0]!.request.headers.get('x-forwarded-host')).toBe('origin.example.com') + }) + it('preserves method and body while adding origin request headers', async () => { const fetcher = MockFetcher(async (request) => { expect(request.method).toBe('POST')