Skip to content

Commit

Permalink
expose event.clientAddress (#4289)
Browse files Browse the repository at this point in the history
* expose event.clientAddress

* Fix check error

* implement for dev and preview

* implement for Netlify

* implement cloudflare and vercel

* throw error if adapter does not specify getClientAddress

* update adapter-cloudflare-workers

* button up types

* add getClientAddress to adapter-node behind trustProxy option

* update docs

* changesets

* docs

* set address header explicitly

* lint

* error on misconfigured header

Co-authored-by: Ben McCann <[email protected]>
  • Loading branch information
Rich-Harris and benmccann authored Mar 14, 2022
1 parent c3c700f commit 268ee6b
Show file tree
Hide file tree
Showing 22 changed files with 234 additions and 77 deletions.
9 changes: 9 additions & 0 deletions .changeset/bright-taxis-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@sveltejs/adapter-cloudflare': patch
'@sveltejs/adapter-cloudflare-workers': patch
'@sveltejs/adapter-netlify': patch
'@sveltejs/adapter-node': patch
'@sveltejs/adapter-vercel': patch
---

Provide getClientAddress function
5 changes: 5 additions & 0 deletions .changeset/sour-hounds-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[breaking] require adapters to supply a getClientAddress function
5 changes: 5 additions & 0 deletions .changeset/wild-snails-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

expose client IP address as event.clientAddress
4 changes: 2 additions & 2 deletions documentation/docs/09-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ Within the `adapt` method, there are a number of things that an adapter should d
- Clear out the build directory
- Write SvelteKit output with `builder.writeClient`, `builder.writePrerendered`, `builder.writeServer`, and `builder.writeStatic`
- Output code that:
- Imports `App` from `${builder.getServerDirectory()}/app.js`
- Imports `Server` from `${builder.getServerDirectory()}/index.js`
- Instantiates the app with a manifest generated with `builder.generateManifest({ relativePath })`
- Listens for requests from the platform, converts them to a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) if necessary, calls the `render` function to generate a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) and responds with it
- Listens for requests from the platform, converts them to a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) if necessary, calls the `server.respond(request, { getClientAddress })` function to generate a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) and responds with it
- expose any platform-specific information to SvelteKit via the `platform` option passed to `server.respond`
- Globally shims `fetch` to work on the target platform, if necessary. SvelteKit provides a `@sveltejs/kit/install-fetch` helper for platforms that can use `node-fetch`
- Bundle the output to avoid needing to install dependencies on the target platform, if necessary
Expand Down
6 changes: 5 additions & 1 deletion packages/adapter-cloudflare-workers/files/entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ async function handle(event) {

// dynamically-generated pages
try {
return await server.respond(request);
return await server.respond(request, {
getClientAddress() {
return request.headers.get('cf-connecting-ip');
}
});
} catch (e) {
return new Response('Error rendering route:' + (e.message || e.toString()), { status: 500 });
}
Expand Down
7 changes: 6 additions & 1 deletion packages/adapter-cloudflare/files/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ export default {

// dynamically-generated pages
try {
return await server.respond(req, { platform: { env, context } });
return await server.respond(req, {
platform: { env, context },
getClientAddress() {
return req.headers.get('cf-connecting-ip');
}
});
} catch (e) {
return new Response('Error rendering route: ' + (e.message || e.toString()), { status: 500 });
}
Expand Down
7 changes: 6 additions & 1 deletion packages/adapter-netlify/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ export function init(manifest) {
const server = new Server(manifest);

return async (event, context) => {
const rendered = await server.respond(to_request(event), { platform: { context } });
const rendered = await server.respond(to_request(event), {
platform: { context },
getClientAddress() {
return event.headers['x-nf-client-connection-ip'];
}
});

const partial_response = {
statusCode: rendered.status,
Expand Down
30 changes: 29 additions & 1 deletion packages/adapter-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export default {
protocol: 'PROTOCOL_HEADER',
host: 'HOST_HEADER'
}
}
},
xForwardedForIndex: -1
})
}
};
Expand Down Expand Up @@ -63,6 +64,14 @@ PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build

> [`x-forwarded-proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) and [`x-forwarded-host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) are de facto standard headers that forward the original protocol and host if you're using a reverse proxy (think load balancers and CDNs). You should only set these variables if you trust the reverse proxy.
The [RequestEvent](https://kit.svelte.dev/docs/types#additional-types-requestevent) object passed to hooks and endpoints includes an `event.clientAddress` property representing the client's IP address. By default this is the connecting `remoteAddress`. If your server is behind one or more proxies (such as a load balancer), this value will contain the innermost proxy's IP address rather than the client's, so we need to specify an `ADDRESS_HEADER` to read the address from:

```
ADDRESS_HEADER=True-Client-IP node build
```

> Headers can easily be spoofed. As with `PROTOCOL_HEADER` and `HOST_HEADER`, you should [know what you're doing](https://adam-p.ca/blog/2022/03/x-forwarded-for/) before setting these.
All of these environment variables can be changed, if necessary, using the `env` option:

```js
Expand All @@ -71,6 +80,7 @@ env: {
port: 'MY_PORT_VARIABLE',
origin: 'MY_ORIGINURL',
headers: {
address: 'MY_ADDRESS_HEADER',
protocol: 'MY_PROTOCOL_HEADER',
host: 'MY_HOST_HEADER'
}
Expand All @@ -84,6 +94,24 @@ MY_ORIGINURL=https://my.site \
node build
```

### xForwardedForIndex

If the `ADDRESS_HEADER` is `X-Forwarded-For`, the header value will contain a comma-separated list of IP addresses. For example, if there are three proxies between your server and the client, proxy 3 will forward the addresses of the client and the first two proxies:

```
<client address>, <proxy 1 address>, <proxy 2 address>
```

To get the client address we could use `xForwardedFor: 0` or `xForwardedFor: -3`, which counts back from the number of addresses.

**X-Forwarded-For is [trivial to spoof](https://adam-p.ca/blog/2022/03/x-forwarded-for/), howevever**:

```
<spoofed address>, <client address>, <proxy 1 address>, <proxy 2 address>
```

For that reason you should always use a negative number (depending on the number of proxies) if you need to trust `event.clientAddress`. In the above example, `0` would yield the spoofed address while `-3` would continue to work.

## Custom server

The adapter creates two files in your build directory — `index.js` and `handler.js`. Running `index.js` — e.g. `node build`, if you use the default build directory — will start a server on the configured port.
Expand Down
2 changes: 2 additions & 0 deletions packages/adapter-node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ interface AdapterOptions {
port?: string;
origin?: string;
headers?: {
address?: string;
protocol?: string;
host?: string;
};
};
xForwardedForIndex?: number;
}

declare function plugin(options?: AdapterOptions): Adapter;
Expand Down
8 changes: 6 additions & 2 deletions packages/adapter-node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ export default function ({
port: port_env = 'PORT',
origin: origin_env = 'ORIGIN',
headers: {
address: address_header_env = 'ADDRESS_HEADER',
protocol: protocol_header_env = 'PROTOCOL_HEADER',
host: host_header_env = 'HOST_HEADER'
} = {}
} = {}
} = {},
xForwardedForIndex = -1
} = {}) {
return {
name: '@sveltejs/adapter-node',
Expand Down Expand Up @@ -52,7 +54,9 @@ export default function ({
PORT_ENV: JSON.stringify(port_env),
ORIGIN: origin_env ? `process.env[${JSON.stringify(origin_env)}]` : 'undefined',
PROTOCOL_HEADER: JSON.stringify(protocol_header_env),
HOST_HEADER: JSON.stringify(host_header_env)
HOST_HEADER: JSON.stringify(host_header_env),
ADDRESS_HEADER: JSON.stringify(address_header_env),
X_FORWARDED_FOR_INDEX: JSON.stringify(xForwardedForIndex)
}
});

Expand Down
2 changes: 2 additions & 0 deletions packages/adapter-node/src/handler.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import type { Handle } from '@sveltejs/kit';

declare global {
const ORIGIN: string;
const ADDRESS_HEADER: string;
const HOST_HEADER: string;
const PROTOCOL_HEADER: string;
const X_FORWARDED_FOR_INDEX: number;
}

export const handler: Handle;
37 changes: 35 additions & 2 deletions packages/adapter-node/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { getRequest, setResponse } from '@sveltejs/kit/node';
import { Server } from 'SERVER';
import { manifest } from 'MANIFEST';

/* global ORIGIN, PROTOCOL_HEADER, HOST_HEADER */
/* global ORIGIN, ADDRESS_HEADER, PROTOCOL_HEADER, HOST_HEADER, X_FORWARDED_FOR_INDEX */

const server = new Server(manifest);
const origin = ORIGIN;

const address_header = ADDRESS_HEADER && (process.env[ADDRESS_HEADER] || '').toLowerCase();
const protocol_header = PROTOCOL_HEADER && process.env[PROTOCOL_HEADER];
const host_header = (HOST_HEADER && process.env[HOST_HEADER]) || 'host';

Expand Down Expand Up @@ -45,7 +47,38 @@ const ssr = async (req, res) => {
return res.end(err.reason || 'Invalid request body');
}

setResponse(res, await server.respond(request));
if (address_header && !(address_header in req.headers)) {
throw new Error(
`Address header was specified with ${ADDRESS_HEADER}=${process.env[ADDRESS_HEADER]} but is absent from request`
);
}

setResponse(
res,
await server.respond(request, {
getClientAddress: () => {
if (address_header) {
const value = /** @type {string} */ (req.headers[address_header]) || '';

if (address_header === 'x-forwarded-for') {
const addresses = value.split(',');
return addresses[(addresses.length + X_FORWARDED_FOR_INDEX) % addresses.length].trim();
}

return value;
}

return (
req.connection?.remoteAddress ||
// @ts-expect-error
req.connection?.socket?.remoteAddress ||
req.socket?.remoteAddress ||
// @ts-expect-error
req.info?.remoteAddress
);
}
})
);
};

/** @param {import('polka').Middleware[]} handlers */
Expand Down
10 changes: 9 additions & 1 deletion packages/adapter-vercel/files/entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const server = new Server(manifest);
* @param {import('http').ServerResponse} res
*/
export default async (req, res) => {
/** @type {Request} */
let request;

try {
Expand All @@ -19,5 +20,12 @@ export default async (req, res) => {
return res.end(err.reason || 'Invalid request body');
}

setResponse(res, await server.respond(request));
setResponse(
res,
await server.respond(request, {
getClientAddress() {
return request.headers.get('x-forwarded-for');
}
})
);
};
2 changes: 2 additions & 0 deletions packages/kit/src/core/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ export async function build_server(

print_config_conflicts(conflicts, 'kit.vite.', 'build_server');

process.env.VITE_SVELTEKIT_ADAPTER_NAME = config.kit.adapter?.name;

const { chunks } = await create_build(merged_config);

/** @type {import('vite').Manifest} */
Expand Down
7 changes: 7 additions & 0 deletions packages/kit/src/core/build/prerender/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export async function prerender({ config, entries, files, log }) {
const dependencies = new Map();

const response = await server.respond(new Request(`http://sveltekit-prerender${encoded}`), {
getClientAddress,
prerender: {
default: config.kit.prerender.default,
dependencies
Expand Down Expand Up @@ -268,6 +269,7 @@ export async function prerender({ config, entries, files, log }) {
}

const rendered = await server.respond(new Request('http://sveltekit-prerender/[fallback]'), {
getClientAddress,
prerender: {
fallback: true,
default: false,
Expand All @@ -281,3 +283,8 @@ export async function prerender({ config, entries, files, log }) {

return prerendered;
}

/** @return {string} */
function getClientAddress() {
throw new Error('Cannot read clientAddress during prerendering');
}
Loading

0 comments on commit 268ee6b

Please sign in to comment.