diff --git a/docs/latest/concepts/app.md b/docs/latest/concepts/app.md index 215fed0dcaa..2bd48df7b7c 100644 --- a/docs/latest/concepts/app.md +++ b/docs/latest/concepts/app.md @@ -37,6 +37,23 @@ With `basePath: "/my-app"`, a route registered at `/about` will respond to mounted alongside other apps. The base path is available in handlers via `ctx.config.basePath`. +### Reverse proxy support + +When running behind a reverse proxy (nginx, Caddy, etc.), set `trustProxy` to +make `ctx.url` reflect the client-facing URL instead of the internal one: + +```ts +const app = new App({ trustProxy: true }); +``` + +With this enabled, Fresh reads `X-Forwarded-Proto` and `X-Forwarded-Host` +headers and rewrites `ctx.url` accordingly. For example, if your proxy +terminates TLS and forwards `X-Forwarded-Proto: https`, `ctx.url.protocol` will +be `https:` instead of `http:`. + +> [warn]: Only enable `trustProxy` when your app is actually behind a trusted +> reverse proxy. Untrusted clients could otherwise spoof these headers. + All items are applied from top to bottom. This means that when you defined a middleware _after_ a `.get()` handler, it won't be included. diff --git a/packages/fresh/src/app.ts b/packages/fresh/src/app.ts index fe1f740fb87..90aac96b7ce 100644 --- a/packages/fresh/src/app.ts +++ b/packages/fresh/src/app.ts @@ -190,6 +190,7 @@ export class App { root: ".", basePath: config.basePath ?? "", mode: config.mode ?? "production", + trustProxy: config.trustProxy ?? false, }; } @@ -392,6 +393,8 @@ export class App { this.#onError, ); + const trustProxy = this.config.trustProxy; + return async ( req: Request, conn: Deno.ServeHandlerInfo = DEFAULT_CONN_INFO, @@ -400,6 +403,18 @@ export class App { // Prevent open redirect attacks url.pathname = url.pathname.replace(/\/+/g, "/"); + // Apply X-Forwarded-* headers when behind a reverse proxy + if (trustProxy) { + const proto = req.headers.get("x-forwarded-proto"); + if (proto) { + url.protocol = proto + ":"; + } + const host = req.headers.get("x-forwarded-host"); + if (host) { + url.host = host; + } + } + const method = req.method.toUpperCase() as Method; const matched = router.match(method, url); let { params, pattern, item: handler, methodMatch } = matched; diff --git a/packages/fresh/src/config.ts b/packages/fresh/src/config.ts index 7fae4f086e9..7fa763c43bd 100644 --- a/packages/fresh/src/config.ts +++ b/packages/fresh/src/config.ts @@ -11,6 +11,16 @@ export interface FreshConfig { * The mode Fresh can run in. */ mode?: "development" | "production"; + /** + * When enabled, Fresh will respect `X-Forwarded-Proto` and + * `X-Forwarded-Host` headers to construct `ctx.url`. Enable + * this when running behind a reverse proxy. + * + * Only enable `trustProxy` when your app is actually behind a trusted + * reverse proxy. Untrusted clients could otherwise spoof these headers. + * @default false + */ + trustProxy?: boolean; } /** @@ -27,6 +37,15 @@ export interface ResolvedFreshConfig { * The mode Fresh can run in. */ mode: "development" | "production"; + /** + * When enabled, Fresh will respect `X-Forwarded-Proto` and + * `X-Forwarded-Host` headers to construct `ctx.url`. Enable + * this when running behind a reverse proxy. + * + * Only enable `trustProxy` when your app is actually behind a trusted + * reverse proxy. Untrusted clients could otherwise spoof these headers. + */ + trustProxy: boolean; } export function parseDirPath( diff --git a/packages/fresh/src/middlewares/static_files_test.ts b/packages/fresh/src/middlewares/static_files_test.ts index d131bccc799..3ba5daacb51 100644 --- a/packages/fresh/src/middlewares/static_files_test.ts +++ b/packages/fresh/src/middlewares/static_files_test.ts @@ -100,6 +100,7 @@ Deno.test("static files - etag in production", async () => { root: "", basePath: "", mode: "production", + trustProxy: false, }, }, ); @@ -134,6 +135,7 @@ Deno.test("static files - no etag in development", async () => { root: "", basePath: "", mode: "development", + trustProxy: false, }, }, ); @@ -191,6 +193,7 @@ Deno.test("static files - disables caching in development", async () => { root: "", basePath: "", mode: "development", + trustProxy: false, }, }, ); @@ -215,6 +218,7 @@ Deno.test("static files - enables caching in production", async () => { root: "", basePath: "", mode: "production", + trustProxy: false, }, }, ); diff --git a/packages/fresh/src/test_utils.ts b/packages/fresh/src/test_utils.ts index c286b9a8819..ed6b6776238 100644 --- a/packages/fresh/src/test_utils.ts +++ b/packages/fresh/src/test_utils.ts @@ -67,6 +67,7 @@ const DEFAULT_CONFIG: ResolvedFreshConfig = { root: "", mode: "production", basePath: "", + trustProxy: false, }; export function serveMiddleware(