@@ -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