1'use strict' 2 3const { kProxy, kClose, kDestroy, kInterceptors } = require('./core/symbols') 4const { URL } = require('url') 5const Agent = require('./agent') 6const Pool = require('./pool') 7const DispatcherBase = require('./dispatcher-base') 8const { InvalidArgumentError, RequestAbortedError } = require('./core/errors') 9const buildConnector = require('./core/connect') 10 11const kAgent = Symbol('proxy agent') 12const kClient = Symbol('proxy client') 13const kProxyHeaders = Symbol('proxy headers') 14const kRequestTls = Symbol('request tls settings') 15const kProxyTls = Symbol('proxy tls settings') 16const kConnectEndpoint = Symbol('connect endpoint function') 17 18function defaultProtocolPort (protocol) { 19 return protocol === 'https:' ? 443 : 80 20} 21 22function buildProxyOptions (opts) { 23 if (typeof opts === 'string') { 24 opts = { uri: opts } 25 } 26 27 if (!opts || !opts.uri) { 28 throw new InvalidArgumentError('Proxy opts.uri is mandatory') 29 } 30 31 return { 32 uri: opts.uri, 33 protocol: opts.protocol || 'https' 34 } 35} 36 37function defaultFactory (origin, opts) { 38 return new Pool(origin, opts) 39} 40 41class ProxyAgent extends DispatcherBase { 42 constructor (opts) { 43 super(opts) 44 this[kProxy] = buildProxyOptions(opts) 45 this[kAgent] = new Agent(opts) 46 this[kInterceptors] = opts.interceptors && opts.interceptors.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) 47 ? opts.interceptors.ProxyAgent 48 : [] 49 50 if (typeof opts === 'string') { 51 opts = { uri: opts } 52 } 53 54 if (!opts || !opts.uri) { 55 throw new InvalidArgumentError('Proxy opts.uri is mandatory') 56 } 57 58 const { clientFactory = defaultFactory } = opts 59 60 if (typeof clientFactory !== 'function') { 61 throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') 62 } 63 64 this[kRequestTls] = opts.requestTls 65 this[kProxyTls] = opts.proxyTls 66 this[kProxyHeaders] = opts.headers || {} 67 68 const resolvedUrl = new URL(opts.uri) 69 const { origin, port, host, username, password } = resolvedUrl 70 71 if (opts.auth && opts.token) { 72 throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') 73 } else if (opts.auth) { 74 /* @deprecated in favour of opts.token */ 75 this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}` 76 } else if (opts.token) { 77 this[kProxyHeaders]['proxy-authorization'] = opts.token 78 } else if (username && password) { 79 this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}` 80 } 81 82 const connect = buildConnector({ ...opts.proxyTls }) 83 this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) 84 this[kClient] = clientFactory(resolvedUrl, { connect }) 85 this[kAgent] = new Agent({ 86 ...opts, 87 connect: async (opts, callback) => { 88 let requestedHost = opts.host 89 if (!opts.port) { 90 requestedHost += `:${defaultProtocolPort(opts.protocol)}` 91 } 92 try { 93 const { socket, statusCode } = await this[kClient].connect({ 94 origin, 95 port, 96 path: requestedHost, 97 signal: opts.signal, 98 headers: { 99 ...this[kProxyHeaders], 100 host 101 } 102 }) 103 if (statusCode !== 200) { 104 socket.on('error', () => {}).destroy() 105 callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)) 106 } 107 if (opts.protocol !== 'https:') { 108 callback(null, socket) 109 return 110 } 111 let servername 112 if (this[kRequestTls]) { 113 servername = this[kRequestTls].servername 114 } else { 115 servername = opts.servername 116 } 117 this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) 118 } catch (err) { 119 callback(err) 120 } 121 } 122 }) 123 } 124 125 dispatch (opts, handler) { 126 const { host } = new URL(opts.origin) 127 const headers = buildHeaders(opts.headers) 128 throwIfProxyAuthIsSent(headers) 129 return this[kAgent].dispatch( 130 { 131 ...opts, 132 headers: { 133 ...headers, 134 host 135 } 136 }, 137 handler 138 ) 139 } 140 141 async [kClose] () { 142 await this[kAgent].close() 143 await this[kClient].close() 144 } 145 146 async [kDestroy] () { 147 await this[kAgent].destroy() 148 await this[kClient].destroy() 149 } 150} 151 152/** 153 * @param {string[] | Record<string, string>} headers 154 * @returns {Record<string, string>} 155 */ 156function buildHeaders (headers) { 157 // When using undici.fetch, the headers list is stored 158 // as an array. 159 if (Array.isArray(headers)) { 160 /** @type {Record<string, string>} */ 161 const headersPair = {} 162 163 for (let i = 0; i < headers.length; i += 2) { 164 headersPair[headers[i]] = headers[i + 1] 165 } 166 167 return headersPair 168 } 169 170 return headers 171} 172 173/** 174 * @param {Record<string, string>} headers 175 * 176 * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers 177 * Nevertheless, it was changed and to avoid a security vulnerability by end users 178 * this check was created. 179 * It should be removed in the next major version for performance reasons 180 */ 181function throwIfProxyAuthIsSent (headers) { 182 const existProxyAuth = headers && Object.keys(headers) 183 .find((key) => key.toLowerCase() === 'proxy-authorization') 184 if (existProxyAuth) { 185 throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor') 186 } 187} 188 189module.exports = ProxyAgent 190