@@ -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.
0 commit comments