Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added EnvHttpProxyAgent to support HTTP_PROXY #2994

Merged
merged 10 commits into from
Apr 19, 2024
Prev Previous commit
Next Next commit
refactor(env-http-proxy-agent): parse NO_PROXY in constructor
  • Loading branch information
10xLaCroixDrinker committed Apr 2, 2024
commit 3b7cd3bdf595f37377a2eea4e336699f1cb2ade3
42 changes: 25 additions & 17 deletions lib/dispatcher/env-http-proxy-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const DEFAULT_PORTS = {
}

class EnvHttpProxyAgent extends DispatcherBase {
#neverProxy = false
#alwaysProxy = false
#noProxyEntries = []

constructor (opts) {
super()

Expand All @@ -29,6 +33,19 @@ class EnvHttpProxyAgent extends DispatcherBase {
} else {
this[kHttpsProxyAgent] = this[kHttpProxyAgent]
}

const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy || ''
10xLaCroixDrinker marked this conversation as resolved.
Show resolved Hide resolved
this.#neverProxy = NO_PROXY === '*'
this.#noProxyEntries = NO_PROXY.split(/[,\s]/)
.filter(Boolean)
.map((entry) => {
const parsed = entry.match(/^(.+):(\d+)$/)
return {
hostname: (parsed ? parsed[1] : entry).toLowerCase(),
port: parsed ? Number.parseInt(parsed[2], 10) : 0
}
})
this.#alwaysProxy = this.#noProxyEntries.length === 0
}

[kDispatch] (opts, handler) {
Expand Down Expand Up @@ -74,33 +91,24 @@ class EnvHttpProxyAgent extends DispatcherBase {
}

#shouldProxy (hostname, port) {
const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy
if (!NO_PROXY) {
return true // Always proxy if NO_PROXY is not set.
if (this.#alwaysProxy) {
return true // Always proxy if NO_PROXY is not set or empty.
}
if (NO_PROXY === '*') {
if (this.#neverProxy) {
return false // Never proxy if wildcard is set.
}

return NO_PROXY.split(/[,\s]/).filter((entry) => !!entry.length).every(function (entry) {
const parsed = entry.match(/^(.+):(\d+)$/)
let parsedHostname = (parsed ? parsed[1] : entry).toLowerCase()
const parsedPort = parsed ? Number.parseInt(parsed[2], 10) : 0
if (parsedPort && parsedPort !== port) {
return this.#noProxyEntries.every(function (entry) {
10xLaCroixDrinker marked this conversation as resolved.
Show resolved Hide resolved
if (entry.port && entry.port !== port) {
return true // Skip if ports don't match.
}

if (!/^[.*]/.test(parsedHostname)) {
if (!/^[.*]/.test(entry.hostname)) {
// No wildcards, so proxy if there is not an exact match.
return hostname !== parsedHostname
}

if (parsedHostname.startsWith('*')) {
// Remove leading wildcard.
parsedHostname = parsedHostname.slice(1)
return hostname !== entry.hostname
}
// Don't proxy if the hostname ends with the no_proxy host.
return !hostname.endsWith(parsedHostname)
return !hostname.endsWith(entry.hostname.replace(/^\*/, ''))
})
}
}
Expand Down
48 changes: 36 additions & 12 deletions test/env-http-proxy-agent.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const { tspl } = require('@matteo.collina/tspl')
const { test, describe, after, beforeEach, afterEach } = require('node:test')
const { test, describe, after, beforeEach } = require('node:test')
const sinon = require('sinon')
const { EnvHttpProxyAgent, ProxyAgent, Agent, fetch, MockAgent } = require('..')
const { kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent, kClosed, kDestroyed } = require('../lib/core/symbols')
Expand Down Expand Up @@ -143,50 +143,51 @@ test('uses the appropriate proxy for the protocol', async (t) => {
})

describe('NO_PROXY', () => {
let dispatcher
let doesNotProxy
let usesProxyAgent

beforeEach(() => {
({ dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks())
})

afterEach(() => dispatcher.close())

test('set to *', async (t) => {
t = tspl(t, { plan: 2 })
process.env.NO_PROXY = '*'
const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks()
t.ok(await doesNotProxy('https://example.com'))
t.ok(await doesNotProxy('http://example.com'))
return dispatcher.close()
})

test('set but empty', async (t) => {
t = tspl(t, { plan: 1 })
process.env.NO_PROXY = ''
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
return dispatcher.close()
})

test('no entries (comma)', async (t) => {
t = tspl(t, { plan: 1 })
process.env.NO_PROXY = ','
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
return dispatcher.close()
})

test('no entries (whitespace)', async (t) => {
t = tspl(t, { plan: 1 })
process.env.NO_PROXY = ' '
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
return dispatcher.close()
})

test('no entries (multiple whitespace / commas)', async (t) => {
t = tspl(t, { plan: 1 })
process.env.NO_PROXY = ',\t,,,\n, ,\r'
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com'))
return dispatcher.close()
})

test('single host', async (t) => {
t = tspl(t, { plan: 9 })
process.env.NO_PROXY = 'example'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await doesNotProxy('http://example'))
t.ok(await doesNotProxy('http://example:80'))
t.ok(await doesNotProxy('http://example:0'))
Expand All @@ -196,11 +197,13 @@ describe('NO_PROXY', () => {
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example'))
return dispatcher.close()
})

test('subdomain', async (t) => {
t = tspl(t, { plan: 8 })
process.env.NO_PROXY = 'sub.example'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:0'))
Expand All @@ -209,11 +212,13 @@ describe('NO_PROXY', () => {
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://no.sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub-example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.sub'))
return dispatcher.close()
})

test('host + port', async (t) => {
t = tspl(t, { plan: 12 })
process.env.NO_PROXY = 'example:80, localhost:3000'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await doesNotProxy('http://example'))
t.ok(await doesNotProxy('http://example:80'))
t.ok(await doesNotProxy('http://example:0'))
Expand All @@ -226,11 +231,13 @@ describe('NO_PROXY', () => {
t.ok(await doesNotProxy('https://localhost:3000/'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost:3001/'))
t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://localhost:3001/'))
return dispatcher.close()
})

test('host suffix', async (t) => {
t = tspl(t, { plan: 9 })
process.env.NO_PROXY = '.example'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337'))
Expand All @@ -240,11 +247,13 @@ describe('NO_PROXY', () => {
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no'))
t.ok(await doesNotProxy('http://a.b.example'))
return dispatcher.close()
})

test('host suffix with *.', async (t) => {
t = tspl(t, { plan: 9 })
process.env.NO_PROXY = '*.example'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337'))
Expand All @@ -254,11 +263,13 @@ describe('NO_PROXY', () => {
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no'))
t.ok(await doesNotProxy('http://a.b.example'))
return dispatcher.close()
})

test('substring suffix', async (t) => {
t = tspl(t, { plan: 10 })
process.env.NO_PROXY = '*example'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await doesNotProxy('http://example'))
t.ok(await doesNotProxy('http://example:80'))
t.ok(await doesNotProxy('http://example:1337'))
Expand All @@ -269,22 +280,26 @@ describe('NO_PROXY', () => {
t.ok(await doesNotProxy('http://a.b.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example'))
return dispatcher.close()
})

test('arbitrary wildcards are NOT supported', async (t) => {
t = tspl(t, { plan: 6 })
process.env.NO_PROXY = '.*example'
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://x.prefexample'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example'))
return dispatcher.close()
})

test('IP addresses', async (t) => {
t = tspl(t, { plan: 12 })
process.env.NO_PROXY = '[::1],[::2]:80,10.0.0.1,10.0.0.2:80'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await doesNotProxy('http://[::1]/'))
t.ok(await doesNotProxy('http://[::1]:80/'))
t.ok(await doesNotProxy('http://[::1]:1337/'))
Expand All @@ -297,41 +312,50 @@ describe('NO_PROXY', () => {
t.ok(await doesNotProxy('http://10.0.0.2/'))
t.ok(await doesNotProxy('http://10.0.0.2:80/'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://10.0.0.2:1337/'))
return dispatcher.close()
})

test('CIDR is NOT supported', async (t) => {
t = tspl(t, { plan: 2 })
env.NO_PROXY = '127.0.0.1/32'
process.env.NO_PROXY = '127.0.0.1/32'
const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://127.0.0.1/32'))
return dispatcher.close()
})

test('127.0.0.1 does NOT match localhost', async (t) => {
t = tspl(t, { plan: 2 })
process.env.NO_PROXY = '127.0.0.1'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await doesNotProxy('http://127.0.0.1'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost'))
return dispatcher.close()
})

test('protocols that have a default port', async (t) => {
t = tspl(t, { plan: 6 })
process.env.NO_PROXY = 'xxx:21,xxx:70,xxx:80,xxx:443'
const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks()
t.ok(await doesNotProxy('http://xxx'))
t.ok(await doesNotProxy('http://xxx:80'))
t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://xxx:1337'))
t.ok(await doesNotProxy('https://xxx'))
t.ok(await doesNotProxy('https://xxx:443'))
t.ok(await usesProxyAgent(kHttpsProxyAgent, 'https://xxx:1337'))
return dispatcher.close()
})

test('should not be case-sensitive', async (t) => {
t = tspl(t, { plan: 6 })
process.env.no_proxy = 'XXX YYY ZzZ'
const { dispatcher, doesNotProxy } = createEnvHttpProxyAgentWithMocks()
t.ok(await doesNotProxy('http://xxx'))
t.ok(await doesNotProxy('http://XXX'))
t.ok(await doesNotProxy('http://yyy'))
t.ok(await doesNotProxy('http://YYY'))
t.ok(await doesNotProxy('http://ZzZ'))
t.ok(await doesNotProxy('http://zZz'))
return dispatcher.close()
})
})