@@ -95,6 +95,19 @@ function ipToInt(ip) {
9595// When the proxy protocol is HTTPS, the modified request needs to be sent after
9696// TLS handshake with the proxy server. Same goes to the HTTPS request tunnel establishment.
9797
98+ /**
99+ * @callback ProxyBypassMatchFn
100+ * @param {string } host - Host to match against the bypass list.
101+ * @param {string } [hostWithPort] - Host with port to match against the bypass list.
102+ * @returns {boolean } - True if the host should be bypassed, false otherwise.
103+ */
104+
105+ /**
106+ * @typedef {object } ProxyConnectionOptions
107+ * @property {string } host - Hostname of the proxy server.
108+ * @property {number } port - Port of the proxy server.
109+ */
110+
98111/**
99112 * Represents the proxy configuration for an agent. The built-in http and https agent
100113 * implementation have one of this when they are configured to use a proxy.
@@ -105,9 +118,28 @@ function ipToInt(ip) {
105118 * @property {string } protocol - Protocol of the proxy server, e.g. 'http:' or 'https:'.
106119 * @property {string|undefined } auth - proxy-authorization header value, if username or password is provided.
107120 * @property {Array<string> } bypassList - List of hosts to bypass the proxy.
108- * @property {object } proxyConnectionOptions - Options for connecting to the proxy server.
121+ * @property {ProxyConnectionOptions } proxyConnectionOptions - Options for connecting to the proxy server.
109122 */
110123class ProxyConfig {
124+ /** @type {Array<string> } */
125+ #bypassList = [ ] ;
126+ /** @type {Array<ProxyBypassMatchFn> } */
127+ #bypassMatchFns = [ ] ;
128+
129+ /** @type {ProxyConnectionOptions } */
130+ get proxyConnectionOptions ( ) {
131+ return {
132+ host : this . hostname ,
133+ port : this . port ,
134+ } ;
135+ }
136+
137+ /**
138+ * @param {string } proxyUrl - The URL of the proxy server, e.g. 'http://localhost:8080'.
139+ * @param {boolean } [keepAlive] - Whether to keep the connection alive.
140+ * This is not used in the current implementation but can be used in the future.
141+ * @param {string } [noProxyList] - Comma-separated list of hosts to bypass the proxy.
142+ */
111143 constructor ( proxyUrl , keepAlive , noProxyList ) {
112144 const { host, hostname, port, protocol, username, password } = new URL ( proxyUrl ) ;
113145 this . href = proxyUrl ; // Full URL of the proxy server.
@@ -121,59 +153,94 @@ class ProxyConfig {
121153 const auth = `${ decodeURIComponent ( username ) } :${ decodeURIComponent ( password ) } ` ;
122154 this . auth = `Basic ${ Buffer . from ( auth ) . toString ( 'base64' ) } ` ;
123155 }
156+
124157 if ( noProxyList ) {
125- this . bypassList = noProxyList . split ( ',' ) . map ( ( entry ) => entry . trim ( ) . toLowerCase ( ) ) ;
126- } else {
127- this . bypassList = [ ] ; // No bypass list provided.
158+ this . # bypassList = noProxyList
159+ . split ( ',' )
160+ . map ( ( entry ) => entry . trim ( ) . toLowerCase ( ) ) ;
128161 }
129- this . proxyConnectionOptions = {
130- host : this . hostname ,
131- port : this . port ,
132- } ;
133- }
134162
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.
163+ if ( this . #bypassList. length === 0 ) {
164+ this . shouldUseProxy = ( ) => true ; // No bypass list, always use the proxy.
165+ } else if ( this . #bypassList. includes ( '*' ) ) {
166+ this . shouldUseProxy = ( ) => false ; // '*' in the bypass list means to bypass all hosts.
167+ } else {
168+ this . #buildBypassMatchFns( ) ;
169+ // Use the bypass match functions to determine if the proxy should be used.
170+ this . shouldUseProxy = this . #match. bind ( this ) ;
141171 }
172+ }
142173
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 ;
174+ #buildBypassMatchFns( bypassList = this . #bypassList) {
175+ this . #bypassMatchFns = [ ] ;
176+
177+ for ( const entry of this . #bypassList) {
178+ if (
179+ // Handle wildcard entries like *.example.com
180+ entry . startsWith ( '*.' ) ||
181+ // Follow curl's behavior: strip leading dot before matching suffixes.
182+ entry . startsWith ( '.' )
183+ ) {
184+ const suffix = entry . split ( '' ) ;
185+ suffix . shift ( ) ; // Remove the leading dot or asterisk.
186+ const suffixLength = suffix . length ;
187+ if ( suffixLength === 0 ) {
188+ // If the suffix is empty, it means to match all hosts.
189+ this . #bypassMatchFns. push ( ( ) => true ) ;
190+ continue ;
191+ }
192+ this . #bypassMatchFns. push ( ( host ) => {
193+ const hostLength = host . length ;
194+ const offset = hostLength - suffixLength ;
195+ if ( offset < 0 ) return false ; // Host is shorter than the suffix.
196+ for ( let i = 0 ; i < suffixLength ; i ++ ) {
197+ if ( host [ offset + i ] !== suffix [ i ] ) {
198+ return false ;
199+ }
200+ }
201+ return true ;
202+ } ) ;
203+ continue ;
156204 }
157205
158- // Handle wildcards like *.example.com
159- if ( entry . startsWith ( '*.' ) && host . endsWith ( entry . substring ( 1 ) ) ) return false ;
160-
161206 // Handle IP ranges (simple format like 192.168.1.0-192.168.1.255)
162207 // 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- }
208+ const { 0 : startIP , 1 : endIP } = entry . split ( '-' ) . map ( ( ip ) => ip . trim ( ) ) ;
209+ if ( entry . includes ( '-' ) && startIP && endIP && isIPv4 ( startIP ) && isIPv4 ( endIP ) ) {
210+ const startInt = ipToInt ( startIP ) ;
211+ const endInt = ipToInt ( endIP ) ;
212+ this . #bypassMatchFns. push ( ( host ) => {
213+ if ( isIPv4 ( host ) ) {
214+ const hostInt = ipToInt ( host ) ;
215+ return hostInt >= startInt && hostInt <= endInt ;
216+ }
217+ return false ;
218+ } ) ;
219+ continue ;
173220 }
174221
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.
222+ // Handle simple host or IP entries
223+ this . #bypassMatchFns. push ( ( host , hostWithPort ) => {
224+ return ( host === entry || hostWithPort === entry ) ;
225+ } ) ;
226+ }
227+ }
228+
229+ get bypassList ( ) {
230+ // Return a copy of the bypass list to prevent external modification.
231+ return [ ...this . #bypassList] ;
232+ }
233+
234+ // See: https://about.gitlab.com/blog/we-need-to-talk-no-proxy
235+ // TODO(joyeecheung): share code with undici.
236+ #match( hostname , port ) {
237+ const host = hostname . toLowerCase ( ) ;
238+ const hostWithPort = port ? `${ host } :${ port } ` : host ;
239+
240+ for ( const bypassMatchFn of this . #bypassMatchFns) {
241+ if ( bypassMatchFn ( host , hostWithPort ) ) {
242+ return false ; // If any bypass function matches, do not use the proxy.
243+ }
177244 }
178245
179246 return true ; // If no matches found, use the proxy.
0 commit comments