Skip to content

Commit 7fd23a2

Browse files
committed
get IP, host, and protocol from trusted proxy
1 parent 6339591 commit 7fd23a2

File tree

1 file changed

+144
-11
lines changed

1 file changed

+144
-11
lines changed

src/Request.ts

Lines changed: 144 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,22 @@ export class Request<A> {
1717
public readonly method: Request.Method;
1818

1919
/**
20-
* The request URL.
20+
* The original request address, as sent by the last peer. The {@link !URL} `protocol` is always set to `http:` and
21+
* the `host` (and related) are always taken from the `HOST` environment variable or defaulted to `localhost`.
22+
*
23+
* If basic authentication is available to this request via headers, the `username` and `password` fields are
24+
* available in the {@link URL} object.
25+
*/
26+
public readonly originalUrl: Readonly<URL>;
27+
28+
/**
29+
* The address requested by the client (first peer). If the request originated from a trusted proxy, this address
30+
* will be constructed based on protocol and host provided by the proxy. If the proxy does not specify protocol,
31+
* `http:` will be used as a default. If the proxy does not specify host, will use the `HOST` environment variable
32+
* or default to `localhost`. If the proxy is not trusted, will be the same as {@link Request#originalUrl}.
33+
*
34+
* If basic authentication is available to this request via headers, the `username` and `password` fields are
35+
* available in the {@link URL} object.
2136
*/
2237
public readonly url: Readonly<URL>;
2338

@@ -26,13 +41,28 @@ export class Request<A> {
2641
*/
2742
public readonly headers: Readonly<Headers>;
2843

44+
/**
45+
* The `Host` header provided by the client (first peer). If the request originated from a trusted proxy, this
46+
* will be obtained from the proxy. Otherwise, if the proxy does not specify the original `Host` header, or the
47+
* proxy is untrusted, this will be the same as the `Host` header in {@link Request#headers} (and `null` if not
48+
* set).
49+
*/
50+
public readonly host: string | null;
51+
2952
/**
3053
* Request body readable stream.
3154
*/
3255
public readonly bodyStream: stream.Readable;
3356

3457
/**
35-
* IP address of request sender.
58+
* The IP address of the request sender (last peer). This might be a proxy.
59+
*/
60+
public readonly originalIp: IPv4 | IPv6;
61+
62+
/**
63+
* IP address of client (first peer). If the request originated from a trusted proxy, this will be the client IP
64+
* indicated by the proxy. Otherwise, if the proxy specifies no client IP, or the proxy is untrusted, this will be
65+
* the proxy IP and equivalent to {@link Request#originalIp}.
3666
*/
3767
public readonly ip: IPv4 | IPv6;
3868

@@ -54,25 +84,34 @@ export class Request<A> {
5484
/**
5585
* Construct a new Request.
5686
* @param method See {@link Request#method}.
87+
* @param originalUrl See {@link Request#originalUrl}.
5788
* @param url See {@link Request#url}.
5889
* @param headers See {@link Request#headers}.
90+
* @param host See {@link Request#host}.
5991
* @param bodyStream See {@link Request#bodyStream}.
92+
* @param originalIp See {@link Request#originalIp}.
6093
* @param ip See {@link Request#ip}.
6194
* @param server See {@link Request#server}.
6295
* @throws {@link !URIError} If the request URL path name contains an invalid URI escape sequence.
6396
*/
6497
public constructor(
6598
method: Request<A>["method"],
99+
originalUrl: Request<A>["originalUrl"],
66100
url: Request<A>["url"],
67101
headers: Request<A>["headers"],
102+
host: Request<A>["host"],
68103
bodyStream: Request<A>["bodyStream"],
104+
originalIp: Request<A>["originalIp"],
69105
ip: Request<A>["ip"],
70106
server: Request<A>["server"]
71107
) {
72108
this.method = method;
109+
this.originalUrl = originalUrl;
73110
this.url = url;
74111
this.headers = headers;
112+
this.host = host;
75113
this.bodyStream = bodyStream;
114+
this.originalIp = originalIp;
76115
this.ip = ip;
77116
this.server = server;
78117

@@ -104,6 +143,19 @@ export class Request<A> {
104143
* @throws {@link Request.SocketClosedError} If the request socket was closed before the request could be handled.
105144
*/
106145
public static incomingMessage<A>(incomingMessage: http.IncomingMessage, server: Server<A>) {
146+
const remoteAddress = incomingMessage.socket.remoteAddress;
147+
if (remoteAddress === undefined)
148+
throw new Request.SocketClosedError();
149+
const ip = IPAddress.fromString(remoteAddress);
150+
const isTrustedProxy = server.trustedProxies.has(ip);
151+
152+
const headers = Request.headersFromNodeDict(incomingMessage.headers);
153+
154+
const proxy = isTrustedProxy ? this.getClientInfoFromTrustedProxy(headers) : {};
155+
156+
const clientIp = proxy.ip ?? ip;
157+
const clientHost = proxy.host ?? headers.get("host");
158+
107159
const auth =
108160
incomingMessage.headers.authorization
109161
?.toLowerCase()
@@ -114,18 +166,25 @@ export class Request<A> {
114166
).toString()
115167
: null;
116168

117-
const url = `http://${auth ? `${auth}@` : ""}${process.env.HOST ?? "localhost"}${incomingMessage.url ?? "/"}`;
118-
if (!URL.canParse(url))
169+
const originalUrl = `http://${auth ? `${auth}@` : ""}${process.env.HOST ?? "localhost"}${incomingMessage.url ?? "/"}`;
170+
if (!URL.canParse(originalUrl))
119171
throw new Request.BadUrlError(incomingMessage.url);
120-
121-
const headers = Request.headersFromNodeDict(incomingMessage.headers);
122-
123-
const remoteAddress = incomingMessage.socket.remoteAddress;
124-
if (remoteAddress === undefined)
125-
throw new Request.SocketClosedError();
172+
const clientUrl = new URL(originalUrl);
173+
if (proxy.protocol !== undefined)
174+
clientUrl.protocol = proxy.protocol + ":";
126175

127176
try {
128-
return new Request<A>(incomingMessage.method as Request.Method, new URL(url), headers, incomingMessage, IPAddress.fromString(remoteAddress), server);
177+
return new Request<A>(
178+
incomingMessage.method as Request.Method,
179+
new URL(originalUrl),
180+
clientUrl,
181+
headers,
182+
clientHost,
183+
incomingMessage,
184+
ip,
185+
clientIp,
186+
server
187+
);
129188
}
130189
catch (e) {
131190
if (e instanceof URIError)
@@ -148,6 +207,77 @@ export class Request<A> {
148207
);
149208
}
150209

210+
/**
211+
* Extract client IP, protocol, and host, from the information provided by a trusted proxy.
212+
* @param headers The HTTP headers sent by a trusted proxy.
213+
*/
214+
private static getClientInfoFromTrustedProxy(headers: Headers): {ip?: IPv4 | IPv6, host?: string, protocol?: "http" | "https"} {
215+
if (headers.has("forwarded")) {
216+
const forwarded = headers.get("forwarded")!.split(",")[0]!.trim();
217+
const forwardedPairs = forwarded.split(";");
218+
let ip: IPv4 | IPv6 | undefined = undefined;
219+
let host: string | undefined = undefined;
220+
let protocol: "http" | "https" | undefined = undefined;
221+
for (const pair of forwardedPairs) {
222+
let [key, value] = pair.split("=") as [key: string, value?: string];
223+
key = key.trim().toLowerCase();
224+
value = value?.trim();
225+
if (value === undefined || value === "")
226+
continue;
227+
if (value.startsWith("\"") && value.endsWith("\""))
228+
value = value.slice(1, -1);
229+
230+
switch (key) {
231+
case "for": {
232+
if (ip !== undefined)
233+
break;
234+
const [address] = value.split(":") as [ip: string, port: `${number}`];
235+
if (address.startsWith("[") && address.endsWith("]"))
236+
ip = IPv6.fromString(address.slice(1, -1));
237+
else
238+
ip = IPv4.fromString(address);
239+
break;
240+
}
241+
case "host": {
242+
if (host !== undefined)
243+
break;
244+
host = value;
245+
break;
246+
}
247+
case "proto": {
248+
if (protocol !== undefined)
249+
break;
250+
if (value !== "http" && value !== "https")
251+
break;
252+
protocol = value;
253+
break;
254+
}
255+
}
256+
}
257+
258+
return {ip, host, protocol};
259+
}
260+
261+
let ip: IPv4 | IPv6 | undefined = undefined;
262+
if (headers.has("x-forwarded-for")) {
263+
const address = headers.get("x-forwarded-for")!.split(",")[0]!;
264+
ip = IPAddress.fromString(address.trim());
265+
}
266+
else if (headers.has("x-real-ip")) {
267+
ip = IPAddress.fromString(headers.get("x-real-ip")!.trim());
268+
}
269+
270+
const host = headers.get("x-forwarded-host") ?? undefined;
271+
const proto = headers.get("x-forwarded-proto") ?? undefined;
272+
let protocol: "http" | "https" | undefined = undefined;
273+
if (proto !== undefined && proto !== "http" && proto !== "https")
274+
protocol = undefined;
275+
else
276+
protocol = proto;
277+
278+
return {ip, host, protocol};
279+
}
280+
151281
/**
152282
* Attempt to obtain authorisation for this request with one of the {@link Server}’s {@link Authenticator}s.
153283
* @returns `null` if the request lacks authorisation information.
@@ -168,9 +298,12 @@ export class Request<A> {
168298
return new AuthenticatedRequest<A>(
169299
authorisation,
170300
this.method,
301+
this.originalUrl,
171302
this.url,
172303
this.headers,
304+
this.host,
173305
this.bodyStream,
306+
this.originalIp,
174307
this.ip,
175308
this.server,
176309
);

0 commit comments

Comments
 (0)