@@ -58,13 +58,22 @@ function traceEnd(...args) {
5858}
5959
6060function ipToInt ( ip ) {
61- const octets = ip . split ( '.' ) ;
62- let result = 0 ;
63- for ( let i = 0 ; i < octets . length ; i ++ ) {
64- result = ( result << 8 ) + NumberParseInt ( octets [ i ] ) ;
61+ let result = 0
62+ let multiplier = 1
63+ let octetShift = 0
64+ let code = 0
65+
66+ for ( let i = ip . length - 1 ; i >= 0 ; -- i ) {
67+ code = ip . charCodeAt ( i )
68+ if ( code !== 46 ) {
69+ result += ( ( code - 48 ) * multiplier ) << octetShift
70+ multiplier *= 10
71+ } else {
72+ octetShift += 8
73+ multiplier = 1
74+ }
6575 }
66- // Force unsigned 32-bit result
67- return result >>> 0 ;
76+ return result >>> 0
6877}
6978
7079// There are two factors in play when proxying the request:
@@ -95,6 +104,19 @@ function ipToInt(ip) {
95104// When the proxy protocol is HTTPS, the modified request needs to be sent after
96105// TLS handshake with the proxy server. Same goes to the HTTPS request tunnel establishment.
97106
107+ /**
108+ * @callback ProxyBypassMatchFn
109+ * @param {string } host - Host to match against the bypass list.
110+ * @param {string } [hostWithPort] - Host with port to match against the bypass list.
111+ * @returns {boolean } - True if the host should be bypassed, false otherwise.
112+ */
113+
114+ /**
115+ * @typedef {object } ProxyConnectionOptions
116+ * @property {string } host - Hostname of the proxy server.
117+ * @property {number } port - Port of the proxy server.
118+ */
119+
98120/**
99121 * Represents the proxy configuration for an agent. The built-in http and https agent
100122 * implementation have one of this when they are configured to use a proxy.
@@ -105,9 +127,28 @@ function ipToInt(ip) {
105127 * @property {string } protocol - Protocol of the proxy server, e.g. 'http:' or 'https:'.
106128 * @property {string|undefined } auth - proxy-authorization header value, if username or password is provided.
107129 * @property {Array<string> } bypassList - List of hosts to bypass the proxy.
108- * @property {object } proxyConnectionOptions - Options for connecting to the proxy server.
130+ * @property {ProxyConnectionOptions } proxyConnectionOptions - Options for connecting to the proxy server.
109131 */
110132class ProxyConfig {
133+ /** @type {Array<string> } */
134+ #bypassList = [ ] ;
135+ /** @type {Array<ProxyBypassMatchFn> } */
136+ #bypassMatchFns = [ ] ;
137+
138+ /** @type {ProxyConnectionOptions } */
139+ get proxyConnectionOptions ( ) {
140+ return {
141+ host : this . hostname ,
142+ port : this . port ,
143+ } ;
144+ }
145+
146+ /**
147+ * @param {string } proxyUrl - The URL of the proxy server, e.g. 'http://localhost:8080'.
148+ * @param {boolean } [keepAlive] - Whether to keep the connection alive.
149+ * This is not used in the current implementation but can be used in the future.
150+ * @param {string } [noProxyList] - Comma-separated list of hosts to bypass the proxy.
151+ */
111152 constructor ( proxyUrl , keepAlive , noProxyList ) {
112153 const { host, hostname, port, protocol, username, password } = new URL ( proxyUrl ) ;
113154 this . href = proxyUrl ; // Full URL of the proxy server.
@@ -121,59 +162,94 @@ class ProxyConfig {
121162 const auth = `${ decodeURIComponent ( username ) } :${ decodeURIComponent ( password ) } ` ;
122163 this . auth = `Basic ${ Buffer . from ( auth ) . toString ( 'base64' ) } ` ;
123164 }
165+
124166 if ( noProxyList ) {
125- this . bypassList = noProxyList . split ( ',' ) . map ( ( entry ) => entry . trim ( ) . toLowerCase ( ) ) ;
126- } else {
127- this . bypassList = [ ] ; // No bypass list provided.
167+ this . # bypassList = noProxyList
168+ . split ( ',' )
169+ . map ( ( entry ) => entry . trim ( ) . toLowerCase ( ) ) ;
128170 }
129- this . proxyConnectionOptions = {
130- host : this . hostname ,
131- port : this . port ,
132- } ;
133- }
134171
135- // See: https://about.gitlab.com/blog/we-need-to-talk-no-proxy
136- // TODO(joyeecheung): share code with undici.
137- shouldUseProxy ( hostname , port ) {
138- const bypassList = this . bypassList ;
139- if ( this . bypassList . length === 0 ) {
140- return true ; // No bypass list, always use the proxy.
172+ if ( this . #bypassList. length === 0 ) {
173+ this . shouldUseProxy = ( ) => true ; // No bypass list, always use the proxy.
174+ } else if ( this . #bypassList. includes ( '*' ) ) {
175+ this . shouldUseProxy = ( ) => false ; // '*' in the bypass list means to bypass all hosts.
176+ } else {
177+ this . #buildBypassMatchFns( ) ;
178+ // Use the bypass match functions to determine if the proxy should be used.
179+ this . shouldUseProxy = this . #match. bind ( this ) ;
141180 }
181+ }
142182
143- const host = hostname . toLowerCase ( ) ;
144- const hostWithPort = port ? `${ host } :${ port } ` : host ;
145-
146- for ( let i = 0 ; i < bypassList . length ; i ++ ) {
147- const entry = bypassList [ i ] ;
148-
149- if ( entry === '*' ) return false ; // * bypasses all hosts.
150- if ( entry === host || entry === hostWithPort ) return false ; // Matching host and host:port
151-
152- // Follow curl's behavior: strip leading dot before matching suffixes.
153- if ( entry . startsWith ( '.' ) ) {
154- const suffix = entry . substring ( 1 ) ;
155- if ( host . endsWith ( suffix ) ) return false ;
183+ #buildBypassMatchFns( bypassList = this . #bypassList) {
184+ this . #bypassMatchFns = [ ] ;
185+
186+ for ( const entry of this . #bypassList) {
187+ if (
188+ // Handle wildcard entries like *.example.com
189+ entry . startsWith ( '*.' ) ||
190+ // Follow curl's behavior: strip leading dot before matching suffixes.
191+ entry . startsWith ( '.' )
192+ ) {
193+ const suffix = entry . split ( '' ) ;
194+ suffix . shift ( ) ; // Remove the leading dot or asterisk.
195+ const suffixLength = suffix . length ;
196+ if ( suffixLength === 0 ) {
197+ // If the suffix is empty, it means to match all hosts.
198+ this . #bypassMatchFns. push ( ( ) => true ) ;
199+ continue ;
200+ }
201+ this . #bypassMatchFns. push ( ( host ) => {
202+ const hostLength = host . length ;
203+ const offset = hostLength - suffixLength ;
204+ if ( offset < 0 ) return false ; // Host is shorter than the suffix.
205+ for ( let i = 0 ; i < suffixLength ; i ++ ) {
206+ if ( host [ offset + i ] !== suffix [ i ] ) {
207+ return false ;
208+ }
209+ }
210+ return true ;
211+ } ) ;
212+ continue ;
156213 }
157214
158- // Handle wildcards like *.example.com
159- if ( entry . startsWith ( '*.' ) && host . endsWith ( entry . substring ( 1 ) ) ) return false ;
160-
161215 // Handle IP ranges (simple format like 192.168.1.0-192.168.1.255)
162216 // TODO(joyeecheung): support IPv6.
163- if ( entry . includes ( '-' ) && isIPv4 ( host ) ) {
164- let { 0 : startIP , 1 : endIP } = entry . split ( '-' ) ;
165- startIP = startIP . trim ( ) ;
166- endIP = endIP . trim ( ) ;
167- if ( startIP && endIP && isIPv4 ( startIP ) && isIPv4 ( endIP ) ) {
168- const hostInt = ipToInt ( host ) ;
169- const startInt = ipToInt ( startIP ) ;
170- const endInt = ipToInt ( endIP ) ;
171- if ( hostInt >= startInt && hostInt <= endInt ) return false ;
172- }
217+ const { 0 : startIP , 1 : endIP } = entry . split ( '-' ) . map ( ( ip ) => ip . trim ( ) ) ;
218+ if ( entry . includes ( '-' ) && startIP && endIP && isIPv4 ( startIP ) && isIPv4 ( endIP ) ) {
219+ const startInt = ipToInt ( startIP ) ;
220+ const endInt = ipToInt ( endIP ) ;
221+ this . #bypassMatchFns. push ( ( host ) => {
222+ if ( isIPv4 ( host ) ) {
223+ const hostInt = ipToInt ( host ) ;
224+ return hostInt >= startInt && hostInt <= endInt ;
225+ }
226+ return false ;
227+ } ) ;
228+ continue ;
173229 }
174230
175- // It might be useful to support CIDR notation, but it's not so widely supported
176- // in other tools as a de-facto standard to follow, so we don't implement it for now.
231+ // Handle simple host or IP entries
232+ this . #bypassMatchFns. push ( ( host , hostWithPort ) => {
233+ return ( host === entry || hostWithPort === entry ) ;
234+ } ) ;
235+ }
236+ }
237+
238+ get bypassList ( ) {
239+ // Return a copy of the bypass list to prevent external modification.
240+ return [ ...this . #bypassList] ;
241+ }
242+
243+ // See: https://about.gitlab.com/blog/we-need-to-talk-no-proxy
244+ // TODO(joyeecheung): share code with undici.
245+ #match( hostname , port ) {
246+ const host = hostname . toLowerCase ( ) ;
247+ const hostWithPort = port ? `${ host } :${ port } ` : host ;
248+
249+ for ( const bypassMatchFn of this . #bypassMatchFns) {
250+ if ( bypassMatchFn ( host , hostWithPort ) ) {
251+ return false ; // If any bypass function matches, do not use the proxy.
252+ }
177253 }
178254
179255 return true ; // If no matches found, use the proxy.
0 commit comments