@@ -27,6 +27,47 @@ interface OakRequestOptions {
2727 secure ?: boolean ;
2828}
2929
30+ /** A parsed entry from the RFC 7239 `Forwarded` header. */
31+ interface ForwardedEntry {
32+ for ?: string ;
33+ proto ?: string ;
34+ host ?: string ;
35+ by ?: string ;
36+ }
37+
38+ /**
39+ * Parse the value of a `Forwarded` header per RFC 7239.
40+ *
41+ * Each forwarded-element is comma-separated; within each element,
42+ * forwarded-pairs are semicolon-separated as `key=value`. Values may be
43+ * optionally quoted.
44+ */
45+ function parseForwarded ( value : string ) : ForwardedEntry [ ] {
46+ const bounded = value . length > 4096 ? value . slice ( 0 , 4096 ) : value ;
47+ const result : ForwardedEntry [ ] = [ ] ;
48+ for ( const element of bounded . split ( "," ) ) {
49+ const entry : ForwardedEntry = { } ;
50+ for ( const pair of element . split ( ";" ) ) {
51+ const eqIdx = pair . indexOf ( "=" ) ;
52+ if ( eqIdx < 0 ) continue ;
53+ const key = pair . slice ( 0 , eqIdx ) . trim ( ) . toLowerCase ( ) ;
54+ let val = pair . slice ( eqIdx + 1 ) . trim ( ) ;
55+ if ( val . length >= 2 && val [ 0 ] === '"' && val [ val . length - 1 ] === '"' ) {
56+ // RFC 7230 §3.2.6 quoted-string unescaping: remove the surrounding
57+ // quotes and replace any backslash-escaped character with the character
58+ // itself (e.g. `\"` → `"`, `\\` → `\`).
59+ val = val . slice ( 1 , - 1 ) . replace ( / \\ ( .) / g, "$1" ) ;
60+ }
61+ if ( key === "for" || key === "proto" || key === "host" || key === "by" ) {
62+ entry [ key as keyof ForwardedEntry ] = val ;
63+ }
64+ }
65+ result . push ( entry ) ;
66+ if ( result . length >= 100 ) break ;
67+ }
68+ return result ;
69+ }
70+
3071/** An interface which provides information about the current request. The
3172 * instance related to the current request is available on the
3273 * {@linkcode Context}'s `.request` property.
@@ -37,6 +78,7 @@ interface OakRequestOptions {
3778 */
3879export class Request {
3980 #body: Body ;
81+ #forwarded?: ForwardedEntry [ ] | null ;
4082 #proxy: boolean ;
4183 #secure: boolean ;
4284 #serverRequest: ServerRequest ;
@@ -47,6 +89,14 @@ export class Request {
4789 return this . #serverRequest. remoteAddr ?? "" ;
4890 }
4991
92+ #getForwarded( ) : ForwardedEntry [ ] | null {
93+ if ( this . #forwarded === undefined ) {
94+ const value = this . #serverRequest. headers . get ( "forwarded" ) ;
95+ this . #forwarded = value ? parseForwarded ( value ) : null ;
96+ }
97+ return this . #forwarded;
98+ }
99+
50100 /** An interface to access the body of the request. This provides an API that
51101 * aligned to the **Fetch Request** API, but in a dedicated API.
52102 */
@@ -72,27 +122,34 @@ export class Request {
72122 }
73123
74124 /** Request remote address. When the application's `.proxy` is true, the
75- * `X-Forwarded-For` will be used to determine the requesting remote address.
125+ * `Forwarded` header (RFC 7239) will be checked first, falling back to
126+ * `X-Forwarded-For`, to determine the requesting remote address.
76127 */
77128 get ip ( ) : string {
78129 return ( this . #proxy ? this . ips [ 0 ] : this . #getRemoteAddr( ) ) ?? "" ;
79130 }
80131
81132 /** When the application's `.proxy` is `true`, this will be set to an array of
82133 * IPs, ordered from upstream to downstream, based on the value of the header
83- * `X-Forwarded-For`. When `false` an empty array is returned. */
134+ * `Forwarded` (RFC 7239) if present, otherwise `X-Forwarded-For`. When
135+ * `false` an empty array is returned. */
84136 get ips ( ) : string [ ] {
85- return this . #proxy
86- ? ( ( ) => {
87- const raw = this . #serverRequest. headers . get ( "x-forwarded-for" ) ??
88- this . #getRemoteAddr( ) ;
89- const bounded = raw . length > 4096 ? raw . slice ( 0 , 4096 ) : raw ;
90- return bounded
91- . split ( "," , 100 )
92- . map ( ( part ) => part . trim ( ) )
93- . filter ( ( part ) => part . length > 0 ) ;
94- } ) ( )
95- : [ ] ;
137+ if ( ! this . #proxy) {
138+ return [ ] ;
139+ }
140+ const forwarded = this . #getForwarded( ) ;
141+ if ( forwarded ) {
142+ return forwarded
143+ . map ( ( e ) => e . for )
144+ . filter ( ( f ) : f is string => f !== undefined && f . length > 0 ) ;
145+ }
146+ const raw = this . #serverRequest. headers . get ( "x-forwarded-for" ) ??
147+ this . #getRemoteAddr( ) ;
148+ const bounded = raw . length > 4096 ? raw . slice ( 0 , 4096 ) : raw ;
149+ return bounded
150+ . split ( "," , 100 )
151+ . map ( ( part ) => part . trim ( ) )
152+ . filter ( ( part ) => part . length > 0 ) ;
96153 }
97154
98155 /** The HTTP Method used by the request. */
@@ -125,7 +182,8 @@ export class Request {
125182
126183 /** A parsed URL for the request which complies with the browser standards.
127184 * When the application's `.proxy` is `true`, this value will be based off of
128- * the `X-Forwarded-Proto` and `X-Forwarded-Host` header values if present in
185+ * the `Forwarded` header (RFC 7239) if present, otherwise the
186+ * `X-Forwarded-Proto` and `X-Forwarded-Host` header values if present in
129187 * the request. */
130188 get url ( ) : URL {
131189 if ( ! this . #url) {
@@ -145,17 +203,29 @@ export class Request {
145203 let proto : string ;
146204 let host : string ;
147205 if ( this . #proxy) {
148- const xForwardedProto = serverRequest . headers . get (
149- "x-forwarded-proto" ,
150- ) ;
151- let maybeProto = xForwardedProto
152- ? xForwardedProto . split ( "," , 1 ) [ 0 ] . trim ( ) . toLowerCase ( )
153- : undefined ;
206+ const forwarded = this . #getForwarded( ) ;
207+ const firstForwarded = forwarded ?. [ 0 ] ;
208+ let maybeProto : string | undefined ;
209+ if ( firstForwarded ?. proto ) {
210+ maybeProto = firstForwarded . proto . toLowerCase ( ) ;
211+ } else {
212+ const xForwardedProto = serverRequest . headers . get (
213+ "x-forwarded-proto" ,
214+ ) ;
215+ maybeProto = xForwardedProto
216+ ? xForwardedProto . split ( "," , 1 ) [ 0 ] . trim ( ) . toLowerCase ( )
217+ : undefined ;
218+ }
154219 if ( maybeProto !== "http" && maybeProto !== "https" ) {
155220 maybeProto = undefined ;
156221 }
157222 proto = maybeProto ?? "http" ;
158- host = serverRequest . headers . get ( "x-forwarded-host" ) ??
223+ // The `host` value from the `Forwarded` header is used as-is, just
224+ // like the legacy `X-Forwarded-Host`. Both require `proxy: true`,
225+ // meaning the operator has declared that the upstream proxy is
226+ // trusted to set these headers correctly.
227+ host = firstForwarded ?. host ??
228+ serverRequest . headers . get ( "x-forwarded-host" ) ??
159229 this . #url?. hostname ??
160230 serverRequest . headers . get ( "host" ) ??
161231 serverRequest . headers . get ( ":authority" ) ?? "" ;
0 commit comments