Skip to content

Commit

Permalink
fix: smaller tweaks to proxy interceptor
Browse files Browse the repository at this point in the history
  • Loading branch information
metcoder95 committed Feb 28, 2024
1 parent 051caa1 commit b45ecad
Show file tree
Hide file tree
Showing 5 changed files with 794 additions and 206 deletions.
4 changes: 2 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ module.exports.Client = Client
module.exports.Pool = Pool
module.exports.BalancedPool = BalancedPool
module.exports.Agent = Agent
module.exports.ProxyAgent = Proxy.ProxyAgent
module.exports.ProxyAgent = ProxyAgent
module.exports.RetryAgent = RetryAgent
module.exports.RetryHandler = RetryHandler

module.exports.DecoratorHandler = DecoratorHandler
module.exports.RedirectHandler = RedirectHandler
module.exports.createRedirectInterceptor = createRedirectInterceptor
module.exports.interceptors = {
proxy: Proxy.interceptor,
proxy: require('./lib/interceptor/proxy'),
redirect: require('./lib/interceptor/redirect'),
retry: require('./lib/interceptor/retry')
}
Expand Down
221 changes: 19 additions & 202 deletions lib/interceptor/proxy.js
Original file line number Diff line number Diff line change
@@ -1,222 +1,39 @@
'use strict'

const { URL } = require('node:url')
const Agent = require('../agent')
const Pool = require('../pool')
const DispatcherBase = require('../dispatcher-base')
const { kProxy, kClose, kDestroy, kInterceptors } = require('../core/symbols')
const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
const buildConnector = require('../core/connect')
const { InvalidArgumentError } = require('../core/errors')
const ProxyAgent = require('../dispatcher/proxy-agent')
const DispatcherBase = require('../dispatcher/dispatcher-base')

const kAgent = Symbol('proxy agent')
const kClient = Symbol('proxy client')
const kProxyHeaders = Symbol('proxy headers')
const kRequestTls = Symbol('request tls settings')
const kProxyTls = Symbol('proxy tls settings')
const kConnectEndpoint = Symbol('connect endpoint function')

function defaultProtocolPort (protocol) {
return protocol === 'https:' ? 443 : 80
}

function defaultFactory (origin, opts) {
return new Pool(origin, opts)
}

class ProxyAgent extends DispatcherBase {
constructor (opts) {
class ProxyInterceptor extends DispatcherBase {
constructor (dispatcher, opts) {
super()

if (
!opts ||
(typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)
) {
throw new InvalidArgumentError('Proxy uri is mandatory')
}

const { clientFactory = defaultFactory } = opts
if (typeof clientFactory !== 'function') {
throw new InvalidArgumentError(
'Proxy opts.clientFactory must be a function.'
)
}

const url = this.#getUrl(opts)
const { href, origin, port, protocol, username, password } = url

this[kProxy] = { uri: href, protocol }
this[kAgent] = new Agent(opts)
this[kInterceptors] = opts.interceptors?.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent)
? opts.interceptors.ProxyAgent
: []
this[kRequestTls] = opts.requestTls
this[kProxyTls] = opts.proxyTls
this[kProxyHeaders] = opts.headers || {}

if (opts.auth && opts.token) {
throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
} else if (opts.auth) {
/* @deprecated in favour of opts.token */
this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`
} else if (opts.token) {
this[kProxyHeaders]['proxy-authorization'] = opts.token
} else if (username && password) {
this[kProxyHeaders]['proxy-authorization'] = `Basic
${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
}

const connect = buildConnector({ ...opts.proxyTls })
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
this[kClient] = clientFactory(url, { connect })
this[kAgent] = new Agent({
...opts,
connect: async (opts, callback) => {
let requestedHost = opts.host
if (!opts.port) {
requestedHost += `:${defaultProtocolPort(opts.protocol)}`
}
try {
const { socket, statusCode } = await this[kClient].connect({
origin,
port,
path: requestedHost,
signal: opts.signal,
headers: {
...this[kProxyHeaders],
host: requestedHost
}
})
if (statusCode !== 200) {
socket.on('error', () => {}).destroy()
callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)
)
}
if (opts.protocol !== 'https:') {
callback(null, socket)
return
}
let servername
if (this[kRequestTls]) {
servername = this[kRequestTls].servername
} else {
servername = opts.servername
}
this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
} catch (err) {
callback(err)
}
}
})
this.dispatcher = dispatcher
this.agent = new ProxyAgent(opts)
}

dispatch (opts, handler) {
const { host } = new URL(opts.origin)
const headers = buildHeaders(opts.headers)
throwIfProxyAuthIsSent(headers)
return this[kAgent].dispatch(
{
...opts,
headers: {
...headers,
host
}
},
handler
)
}

/**
* @param {import('../types/proxy-agent').ProxyAgent.Options | string | URL} opts
* @returns {URL}
*/
#getUrl (opts) {
if (typeof opts === 'string') {
return new URL(opts)
} else if (opts instanceof URL) {
return opts
} else {
return new URL(opts.uri)
}
return this.agent.dispatch(opts, handler)
}

async [kClose] () {
await this[kAgent].close()
await this[kClient].close()
}

async [kDestroy] () {
await this[kAgent].destroy()
await this[kClient].destroy()
close () {
return this.dispatcher.close().then(() => this.agent.close())
}
}

/**
* @param {string[] | Record<string, string>} headers
* @returns {Record<string, string>}
*/
function buildHeaders (headers) {
// When using undici.fetch, the headers list is stored
// as an array.
if (Array.isArray(headers)) {
/** @type {Record<string, string>} */
const headersPair = {}

for (let i = 0; i < headers.length; i += 2) {
headersPair[headers[i]] = headers[i + 1]
}

return headersPair
module.exports = opts => {
if (typeof opts === 'string') {
opts = { uri: opts }
}

return headers
}
if (!opts || (!opts.uri && !(opts instanceof URL))) {
throw new InvalidArgumentError('Proxy opts.uri or instance of URL is mandatory')
}

/**
* @param {Record<string, string>} headers
*
* Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers
* Nevertheless, it was changed and to avoid a security vulnerability by end users
* this check was created.
* It should be removed in the next major version for performance reasons
*/
function throwIfProxyAuthIsSent (headers) {
const existProxyAuth =
headers &&
Object.keys(headers).find(
key => key.toLowerCase() === 'proxy-authorization'
)
if (existProxyAuth) {
if (opts.auth && opts.token) {
throw new InvalidArgumentError(
'Proxy-Authorization should be sent in ProxyAgent constructor'
'opts.auth cannot be used in combination with opts.token'
)
}
}

module.exports = {
ProxyAgent,
interceptor: opts => {
if (typeof opts === 'string') {
opts = { uri: opts }
}

if (!opts || !opts.uri) {
throw new InvalidArgumentError('Proxy opts.uri is mandatory')
}

const { clientFactory = defaultFactory } = opts

if (typeof clientFactory !== 'function') {
throw new InvalidArgumentError(
'Proxy opts.clientFactory must be a function.'
)
}

if (opts.auth && opts.token) {
throw new InvalidArgumentError(
'opts.auth cannot be used in combination with opts.token'
)
}

return dispatcher => new ProxyAgent(dispatcher, opts)
}
return dispatcher => new ProxyInterceptor(dispatcher, opts)
}
2 changes: 1 addition & 1 deletion lib/interceptor/redirect.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const { InvalidArgumentError } = require('../core/errors')
const Dispatcher = require('../dispatcher-base')
const Dispatcher = require('../dispatcher/dispatcher-base')
const RedirectHandler = require('../handler/RedirectHandler')

class RedirectDispatcher extends Dispatcher {
Expand Down
2 changes: 1 addition & 1 deletion lib/interceptor/retry.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const Dispatcher = require('../dispatcher-base')
const Dispatcher = require('../dispatcher/dispatcher-base')
const RetryHandler = require('../handler/RetryHandler')

class RetryDispatcher extends Dispatcher {
Expand Down
Loading

0 comments on commit b45ecad

Please sign in to comment.