Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expose event.clientAddress #4289

Merged
merged 16 commits into from
Mar 14, 2022
Merged
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'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ascorbic I wanted to confirm with you if we're doing this correctly because it seems to be undocumented, but is what is suggested in this support post: https://answers.netlify.com/t/is-the-client-ip-header-going-to-be-supported-long-term/11203

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ascorbic I wanted to confirm with you if we're doing this correctly because it seems to be undocumented, but is what is suggested in this support post: https://answers.netlify.com/t/is-the-client-ip-header-going-to-be-supported-long-term/11203

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benmccann Yes, that's correct. We recently cleaned up the internal headers sent to functions so that all remaining ones are now officially supported. https://answers.netlify.com/t/upcoming-change-stripping-exposed-netlify-headers-from-function-and-proxy-requests/52665

}
});

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