Skip to content

Commit d51ab18

Browse files
authored
Get "real" client IP, host, and protocol from trusted proxy (#53)
2 parents e60efad + 27571ad commit d51ab18

File tree

2 files changed

+146
-10
lines changed

2 files changed

+146
-10
lines changed

src/Request.ts

Lines changed: 134 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,25 @@ export class Request<A> {
1919
public readonly method: Request.Method;
2020

2121
/**
22-
* The request URL.
22+
* The original request address, as sent by the last peer. The {@link !URL} `protocol` is always set to `http:` and
23+
* the `host` (and related) are always taken from the `HOST` environment variable or defaulted to `localhost`.
24+
*
25+
* If basic authentication is available to this request via headers, the `username` and `password` fields are
26+
* available in the {@link URL} object.
27+
*/
28+
public readonly originalUrl: Readonly<URL>;
29+
30+
/**
31+
* The address requested by the client (first peer). If the request originated from a trusted proxy, this address
32+
* will be constructed based on protocol and host provided by the proxy. If the proxy does not specify protocol,
33+
* `http:` will be used as a default. If the proxy does not specify host (or the proxy is not trusted), will use the
34+
* `Host` request header. If that is not specified either, will use the `HOST` environment variable or default to
35+
* `localhost`.
36+
*
37+
* In short, this is, as closely as possible, the exact URL address the client requested.
38+
*
39+
* If basic authentication is available to this request via headers, the `username` and `password` fields are
40+
* available in the {@link URL} object.
2341
*/
2442
public readonly url: Readonly<URL>;
2543

@@ -34,7 +52,14 @@ export class Request<A> {
3452
public readonly bodyStream: stream.Readable;
3553

3654
/**
37-
* IP address of request sender.
55+
* The IP address of the request sender (last peer). This might be a proxy.
56+
*/
57+
public readonly originalIp: IPv4 | IPv6;
58+
59+
/**
60+
* IP address of client (first peer). If the request originated from a trusted proxy, this will be the client IP
61+
* indicated by the proxy. Otherwise, if the proxy specifies no client IP, or the proxy is untrusted, this will be
62+
* the proxy IP and equivalent to {@link Request#originalIp}.
3863
*/
3964
public readonly ip: IPv4 | IPv6;
4065

@@ -56,25 +81,31 @@ export class Request<A> {
5681
/**
5782
* Construct a new Request.
5883
* @param method See {@link Request#method}.
84+
* @param originalUrl See {@link Request#originalUrl}.
5985
* @param url See {@link Request#url}.
6086
* @param headers See {@link Request#headers}.
6187
* @param bodyStream See {@link Request#bodyStream}.
88+
* @param originalIp See {@link Request#originalIp}.
6289
* @param ip See {@link Request#ip}.
6390
* @param server See {@link Request#server}.
6491
* @throws {@link !URIError} If the request URL path name contains an invalid URI escape sequence.
6592
*/
6693
public constructor(
6794
method: Request<A>["method"],
95+
originalUrl: Request<A>["originalUrl"],
6896
url: Request<A>["url"],
6997
headers: Request<A>["headers"],
7098
bodyStream: Request<A>["bodyStream"],
99+
originalIp: Request<A>["originalIp"],
71100
ip: Request<A>["ip"],
72101
server: Request<A>["server"]
73102
) {
74103
this.method = method;
104+
this.originalUrl = originalUrl;
75105
this.url = url;
76106
this.headers = headers;
77107
this.bodyStream = bodyStream;
108+
this.originalIp = originalIp;
78109
this.ip = ip;
79110
this.server = server;
80111

@@ -106,6 +137,18 @@ export class Request<A> {
106137
* @throws {@link Request.SocketClosedError} If the request socket was closed before the request could be handled.
107138
*/
108139
public static incomingMessage<A>(incomingMessage: http.IncomingMessage, server: Server<A>) {
140+
const remoteAddress = incomingMessage.socket.remoteAddress;
141+
if (remoteAddress === undefined)
142+
throw new Request.SocketClosedError();
143+
const ip = IPAddress.fromString(remoteAddress);
144+
const isTrustedProxy = server.trustedProxies.has(ip);
145+
146+
const headers = Request.headersFromNodeDict(incomingMessage.headers);
147+
148+
const proxy = isTrustedProxy ? this.getClientInfoFromTrustedProxy(headers) : {};
149+
150+
const clientIp = proxy.ip ?? ip;
151+
109152
const auth =
110153
incomingMessage.headers.authorization
111154
?.toLowerCase()
@@ -116,18 +159,28 @@ export class Request<A> {
116159
).toString()
117160
: null;
118161

119-
const url = `http://${auth ? `${auth}@` : ""}${process.env.HOST ?? "localhost"}${incomingMessage.url ?? "/"}`;
120-
if (!URL.canParse(url))
162+
const originalUrl = `http://${auth ? `${auth}@` : ""}${process.env.HOST ?? "localhost"}${incomingMessage.url ?? "/"}`;
163+
if (!URL.canParse(originalUrl))
121164
throw new Request.BadUrlError(incomingMessage.url);
165+
const clientUrl = new URL(originalUrl);
166+
if (proxy.protocol !== undefined)
167+
clientUrl.protocol = proxy.protocol + ":";
122168

123-
const headers = Request.headersFromNodeDict(incomingMessage.headers);
124-
125-
const remoteAddress = incomingMessage.socket.remoteAddress;
126-
if (remoteAddress === undefined)
127-
throw new Request.SocketClosedError();
169+
const clientHost = proxy.host ?? headers.get("host");
170+
if (clientHost !== null)
171+
clientUrl.host = clientHost;
128172

129173
try {
130-
return new Request<A>(incomingMessage.method as Request.Method, new URL(url), headers, incomingMessage, IPAddress.fromString(remoteAddress), server);
174+
return new Request<A>(
175+
incomingMessage.method as Request.Method,
176+
new URL(originalUrl),
177+
clientUrl,
178+
headers,
179+
incomingMessage,
180+
ip,
181+
clientIp,
182+
server
183+
);
131184
}
132185
catch (e) {
133186
if (e instanceof URIError)
@@ -150,6 +203,77 @@ export class Request<A> {
150203
);
151204
}
152205

206+
/**
207+
* Extract client IP, protocol, and host, from the information provided by a trusted proxy.
208+
* @param headers The HTTP headers sent by a trusted proxy.
209+
*/
210+
private static getClientInfoFromTrustedProxy(headers: Headers): {ip?: IPv4 | IPv6, host?: string, protocol?: "http" | "https"} {
211+
if (headers.has("forwarded")) {
212+
const forwarded = headers.get("forwarded")!.split(",")[0]!.trim();
213+
const forwardedPairs = forwarded.split(";");
214+
let ip: IPv4 | IPv6 | undefined = undefined;
215+
let host: string | undefined = undefined;
216+
let protocol: "http" | "https" | undefined = undefined;
217+
for (const pair of forwardedPairs) {
218+
let [key, value] = pair.split("=") as [key: string, value?: string];
219+
key = key.trim().toLowerCase();
220+
value = value?.trim();
221+
if (value === undefined || value === "")
222+
continue;
223+
if (value.startsWith("\"") && value.endsWith("\""))
224+
value = value.slice(1, -1);
225+
226+
switch (key) {
227+
case "for": {
228+
if (ip !== undefined)
229+
break;
230+
const [address] = value.split(":") as [ip: string, port: `${number}`];
231+
if (address.startsWith("[") && address.endsWith("]"))
232+
ip = IPv6.fromString(address.slice(1, -1));
233+
else
234+
ip = IPv4.fromString(address);
235+
break;
236+
}
237+
case "host": {
238+
if (host !== undefined)
239+
break;
240+
host = value;
241+
break;
242+
}
243+
case "proto": {
244+
if (protocol !== undefined)
245+
break;
246+
if (value !== "http" && value !== "https")
247+
break;
248+
protocol = value;
249+
break;
250+
}
251+
}
252+
}
253+
254+
return {ip, host, protocol};
255+
}
256+
257+
let ip: IPv4 | IPv6 | undefined = undefined;
258+
if (headers.has("x-forwarded-for")) {
259+
const address = headers.get("x-forwarded-for")!.split(",")[0]!;
260+
ip = IPAddress.fromString(address.trim());
261+
}
262+
else if (headers.has("x-real-ip")) {
263+
ip = IPAddress.fromString(headers.get("x-real-ip")!.trim());
264+
}
265+
266+
const host = headers.get("x-forwarded-host") ?? undefined;
267+
const proto = headers.get("x-forwarded-proto") ?? undefined;
268+
let protocol: "http" | "https" | undefined = undefined;
269+
if (proto !== undefined && proto !== "http" && proto !== "https")
270+
protocol = undefined;
271+
else
272+
protocol = proto;
273+
274+
return {ip, host, protocol};
275+
}
276+
153277
/**
154278
* Attempt to obtain authorisation for this request with one of the {@link Server}’s {@link Authenticator}s.
155279
* @returns `null` if the request lacks authorisation information.

src/Server.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {Network} from "@cldn/ip";
12
import EventEmitter from "node:events";
23
import http from "node:http";
34
import packageJson from "./package.json" with {type: "json"};
@@ -35,6 +36,10 @@ class Server<A> extends EventEmitter<Server.Events> {
3536
private readonly port?: number;
3637
private readonly copyOrigin: boolean;
3738
private readonly handleConditionalRequests: boolean;
39+
/**
40+
* The network of remote addresses of proxies to trust/
41+
*/
42+
public readonly trustedProxies: Network;
3843

3944
/**
4045
* Create a new HTTP server.
@@ -54,6 +59,7 @@ class Server<A> extends EventEmitter<Server.Events> {
5459
this.copyOrigin = options?.copyOrigin ?? false;
5560
this.handleConditionalRequests = options?.handleConditionalRequests ?? true;
5661
this._authenticators = options?.authenticators ?? [];
62+
this.trustedProxies = options?.trustedProxies ?? new Network();
5763

5864
if (this.port !== undefined) this.listen(this.port).then();
5965

@@ -237,6 +243,12 @@ namespace Server {
237243
* Authenticators for handling request authentication.
238244
*/
239245
readonly authenticators?: Authenticator<A>[];
246+
247+
/**
248+
* The network of trusted proxies. If specified, headers such as `Forwarded`, `X-Forwarded-For`, and `X-Real-IP`
249+
* will be trusted and automatically processed.
250+
*/
251+
readonly trustedProxies?: Network;
240252
}
241253

242254
/**

0 commit comments

Comments
 (0)