Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
1 change: 1 addition & 0 deletions packages/adapter-node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface AdapterOptions {
host?: string;
};
};
trustProxy?: boolean;
}

declare function plugin(options?: AdapterOptions): Adapter;
Expand Down
6 changes: 4 additions & 2 deletions packages/adapter-node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export default function ({
protocol: protocol_header_env = 'PROTOCOL_HEADER',
host: host_header_env = 'HOST_HEADER'
} = {}
} = {}
} = {},
trustProxy = false
} = {}) {
return {
name: '@sveltejs/adapter-node',
Expand Down Expand Up @@ -52,7 +53,8 @@ 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),
TRUST_PROXY: JSON.stringify(trustProxy)
}
});

Expand Down
72 changes: 72 additions & 0 deletions packages/adapter-node/src/get-client-address.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const v4 = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
const v6 = /^([a-f0-9]+:|:){1,8}(:|[a-f0-9]*)?/;

/**
* Determines if a string resembles an IP address. It does _not_ check that
* the IP address is valid, since that's a much more complex task and
* it's not really necessary here
* @param {string | void} str
*/
function is_ip_like(str) {
return str ? v4.test(str) || v6.test(str) : false;
}

/**
* Extract the client IP address from an x-forwarded-for header
* @param {string | void} str
*/
function extract_from_x_forwarded_for(str) {
if (!str) return null;

return str
.split(', ')
.map((address) => {
// strip port, if this is an IPv4 address
const parts = address.split(':');
if (parts.length === 2) return parts[0];

return address;
})
.find(is_ip_like);
}

const candidates = [
'cf-connecting-ip',
'fastly-client-ip',
'true-client-ip',
'x-real-ip',
'x-cluster-client-ip',
'x-forwarded',
'forwarded-for',
'forwarded'
];

/** @param {import('http').IncomingMessage} req */
export function get_client_address(req) {
const headers = /** @type {Record<string, string | void>} */ (req.headers);

// this follows the order of checks from https://github.com/pbojinov/request-ip/blob/master/src/index.js

if (is_ip_like(headers['x-client-ip'])) {
return headers['x-client-ip'];
}

{
const address = extract_from_x_forwarded_for(headers['x-forwarded-for']);
if (address) return address;
}

for (const candidate of candidates) {
const value = headers[candidate];
if (is_ip_like(value)) return value;
}

return (
req.connection?.remoteAddress ||
// @ts-expect-error
req.connection?.socket?.remoteAddress ||
req.socket?.remoteAddress ||
// @ts-expect-error
req.info?.remoteAddress
);
}
1 change: 1 addition & 0 deletions packages/adapter-node/src/handler.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ declare global {
const ORIGIN: string;
const HOST_HEADER: string;
const PROTOCOL_HEADER: string;
const TRUST_PROXY: boolean;
}

export const handler: Handle;
16 changes: 14 additions & 2 deletions packages/adapter-node/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { fileURLToPath } from 'url';
import { getRequest, setResponse } from '@sveltejs/kit/node';
import { Server } from 'SERVER';
import { manifest } from 'MANIFEST';
import { get_client_address } from './get-client-address';

/* global ORIGIN, PROTOCOL_HEADER, HOST_HEADER */
/* global ORIGIN, PROTOCOL_HEADER, HOST_HEADER, TRUST_PROXY */

const server = new Server(manifest);
const origin = ORIGIN;
Expand Down Expand Up @@ -45,7 +46,18 @@ const ssr = async (req, res) => {
return res.end(err.reason || 'Invalid request body');
}

setResponse(res, await server.respond(request));
setResponse(
res,
await server.respond(request, {
getClientAddress: () => {
if (TRUST_PROXY) {
return get_client_address(req);
}

throw new Error('You must enable the adapter-node trustProxy option to read clientAddress');
}
})
);
};

/** @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 @@ -230,6 +230,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');
}
116 changes: 63 additions & 53 deletions packages/kit/src/core/dev/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,62 +259,72 @@ export async function create_plugin(config, cwd) {

const template = load_template(cwd, config);

const rendered = await respond(request, {
amp: config.kit.amp,
csp: config.kit.csp,
dev: true,
floc: config.kit.floc,
get_stack: (error) => {
return fix_stack_trace(error);
},
handle_error: (error, event) => {
hooks.handleError({
error: new Proxy(error, {
get: (target, property) => {
if (property === 'stack') {
return fix_stack_trace(error);
const rendered = await respond(
request,
{
amp: config.kit.amp,
csp: config.kit.csp,
dev: true,
floc: config.kit.floc,
get_stack: (error) => {
return fix_stack_trace(error);
},
handle_error: (error, event) => {
hooks.handleError({
error: new Proxy(error, {
get: (target, property) => {
if (property === 'stack') {
return fix_stack_trace(error);
}

return Reflect.get(target, property, target);
}

return Reflect.get(target, property, target);
}),
event,

// TODO remove for 1.0
// @ts-expect-error
get request() {
throw new Error(
'request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details'
);
}
}),
event,

// TODO remove for 1.0
// @ts-expect-error
get request() {
throw new Error(
'request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details'
);
}
});
},
hooks,
hydrate: config.kit.browser.hydrate,
manifest,
method_override: config.kit.methodOverride,
paths: {
base: config.kit.paths.base,
assets
},
prefix: '',
prerender: config.kit.prerender.enabled,
read: (file) => fs.readFileSync(path.join(config.kit.files.assets, file)),
root,
router: config.kit.browser.router,
template: ({ head, body, assets, nonce }) => {
return (
template
.replace(/%svelte\.assets%/g, assets)
.replace(/%svelte\.nonce%/g, nonce)
// head and body must be replaced last, in case someone tries to sneak in %svelte.assets% etc
.replace('%svelte.head%', () => head)
.replace('%svelte.body%', () => body)
);
});
},
hooks,
hydrate: config.kit.browser.hydrate,
manifest,
method_override: config.kit.methodOverride,
paths: {
base: config.kit.paths.base,
assets
},
prefix: '',
prerender: config.kit.prerender.enabled,
read: (file) => fs.readFileSync(path.join(config.kit.files.assets, file)),
root,
router: config.kit.browser.router,
template: ({ head, body, assets, nonce }) => {
return (
template
.replace(/%svelte\.assets%/g, assets)
.replace(/%svelte\.nonce%/g, nonce)
// head and body must be replaced last, in case someone tries to sneak in %svelte.assets% etc
.replace('%svelte.head%', () => head)
.replace('%svelte.body%', () => body)
);
},
template_contains_nonce: template.includes('%svelte.nonce%'),
trailing_slash: config.kit.trailingSlash
},
template_contains_nonce: template.includes('%svelte.nonce%'),
trailing_slash: config.kit.trailingSlash
});
{
getClientAddress: () => {
const { remoteAddress } = req.socket;
if (remoteAddress) return remoteAddress;
throw new Error('Could not determine clientAddress');
}
}
);

if (rendered) {
setResponse(res, rendered);
Expand Down
Loading