proxy-agent.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. 'use strict'
  2. const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
  3. const Agent = require('./agent')
  4. const Pool = require('./pool')
  5. const DispatcherBase = require('./dispatcher-base')
  6. const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
  7. const buildConnector = require('../core/connect')
  8. const Client = require('./client')
  9. const { channels } = require('../core/diagnostics')
  10. const Socks5ProxyAgent = require('./socks5-proxy-agent')
  11. const kAgent = Symbol('proxy agent')
  12. const kClient = Symbol('proxy client')
  13. const kProxyHeaders = Symbol('proxy headers')
  14. const kRequestTls = Symbol('request tls settings')
  15. const kProxyTls = Symbol('proxy tls settings')
  16. const kConnectEndpoint = Symbol('connect endpoint function')
  17. const kTunnelProxy = Symbol('tunnel proxy')
  18. function defaultProtocolPort (protocol) {
  19. return protocol === 'https:' ? 443 : 80
  20. }
  21. function defaultFactory (origin, opts) {
  22. return new Pool(origin, opts)
  23. }
  24. const noop = () => {}
  25. function defaultAgentFactory (origin, opts) {
  26. if (opts.connections === 1) {
  27. return new Client(origin, opts)
  28. }
  29. return new Pool(origin, opts)
  30. }
  31. class Http1ProxyWrapper extends DispatcherBase {
  32. #client
  33. constructor (proxyUrl, { headers = {}, connect, factory }) {
  34. if (!proxyUrl) {
  35. throw new InvalidArgumentError('Proxy URL is mandatory')
  36. }
  37. super()
  38. this[kProxyHeaders] = headers
  39. if (factory) {
  40. this.#client = factory(proxyUrl, { connect })
  41. } else {
  42. this.#client = new Client(proxyUrl, { connect })
  43. }
  44. }
  45. [kDispatch] (opts, handler) {
  46. const onHeaders = handler.onHeaders
  47. handler.onHeaders = function (statusCode, data, resume) {
  48. if (statusCode === 407) {
  49. if (typeof handler.onError === 'function') {
  50. handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)'))
  51. }
  52. return
  53. }
  54. if (onHeaders) onHeaders.call(this, statusCode, data, resume)
  55. }
  56. // Rewrite request as an HTTP1 Proxy request, without tunneling.
  57. const {
  58. origin,
  59. path = '/',
  60. headers = {}
  61. } = opts
  62. opts.path = origin + path
  63. if (!('host' in headers) && !('Host' in headers)) {
  64. const { host } = new URL(origin)
  65. headers.host = host
  66. }
  67. opts.headers = { ...this[kProxyHeaders], ...headers }
  68. return this.#client[kDispatch](opts, handler)
  69. }
  70. [kClose] () {
  71. return this.#client.close()
  72. }
  73. [kDestroy] (err) {
  74. return this.#client.destroy(err)
  75. }
  76. }
  77. class ProxyAgent extends DispatcherBase {
  78. constructor (opts) {
  79. if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
  80. throw new InvalidArgumentError('Proxy uri is mandatory')
  81. }
  82. const { clientFactory = defaultFactory } = opts
  83. if (typeof clientFactory !== 'function') {
  84. throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
  85. }
  86. const { proxyTunnel = true } = opts
  87. super()
  88. const url = this.#getUrl(opts)
  89. const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url
  90. this[kProxy] = { uri: href, protocol }
  91. this[kRequestTls] = opts.requestTls
  92. this[kProxyTls] = opts.proxyTls
  93. this[kProxyHeaders] = opts.headers || {}
  94. this[kTunnelProxy] = proxyTunnel
  95. if (opts.auth && opts.token) {
  96. throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
  97. } else if (opts.auth) {
  98. /* @deprecated in favour of opts.token */
  99. this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`
  100. } else if (opts.token) {
  101. this[kProxyHeaders]['proxy-authorization'] = opts.token
  102. } else if (username && password) {
  103. this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
  104. }
  105. const connect = buildConnector({ ...opts.proxyTls })
  106. this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
  107. const agentFactory = opts.factory || defaultAgentFactory
  108. const factory = (origin, options) => {
  109. const { protocol } = new URL(origin)
  110. // Handle SOCKS5 proxy
  111. if (this[kProxy].protocol === 'socks5:' || this[kProxy].protocol === 'socks:') {
  112. return new Socks5ProxyAgent(this[kProxy].uri, {
  113. headers: this[kProxyHeaders],
  114. connect,
  115. factory: agentFactory,
  116. username: opts.username || username,
  117. password: opts.password || password,
  118. proxyTls: opts.proxyTls,
  119. requestTls: opts.requestTls
  120. })
  121. }
  122. if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
  123. return new Http1ProxyWrapper(this[kProxy].uri, {
  124. headers: this[kProxyHeaders],
  125. connect,
  126. factory: agentFactory
  127. })
  128. }
  129. return agentFactory(origin, options)
  130. }
  131. // For SOCKS5 proxies, we don't need a client to the proxy itself
  132. // The SOCKS5 connection is handled within Socks5ProxyAgent
  133. if (protocol === 'socks5:' || protocol === 'socks:') {
  134. this[kClient] = null
  135. } else {
  136. this[kClient] = clientFactory(url, { connect })
  137. }
  138. this[kAgent] = new Agent({
  139. ...opts,
  140. factory,
  141. connect: async (opts, callback) => {
  142. // SOCKS5 proxies handle their own connections via Socks5ProxyAgent,
  143. // so this connect function should never be called for them.
  144. if (!this[kClient]) {
  145. callback(new InvalidArgumentError('Cannot establish tunnel connection without a proxy client'))
  146. return
  147. }
  148. let requestedPath = opts.host
  149. if (!opts.port) {
  150. requestedPath += `:${defaultProtocolPort(opts.protocol)}`
  151. }
  152. try {
  153. const connectParams = {
  154. origin,
  155. port,
  156. path: requestedPath,
  157. signal: opts.signal,
  158. headers: {
  159. ...this[kProxyHeaders],
  160. host: opts.host,
  161. ...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {})
  162. },
  163. servername: this[kProxyTls]?.servername || proxyHostname
  164. }
  165. const { socket, statusCode } = await this[kClient].connect(connectParams)
  166. if (statusCode !== 200) {
  167. socket.on('error', noop).destroy()
  168. callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`))
  169. return
  170. }
  171. if (channels.proxyConnected.hasSubscribers) {
  172. channels.proxyConnected.publish({
  173. socket,
  174. connectParams
  175. })
  176. }
  177. if (opts.protocol !== 'https:') {
  178. callback(null, socket)
  179. return
  180. }
  181. let servername
  182. if (this[kRequestTls]) {
  183. servername = this[kRequestTls].servername
  184. } else {
  185. servername = opts.servername
  186. }
  187. this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
  188. } catch (err) {
  189. if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
  190. // Throw a custom error to avoid loop in client.js#connect
  191. callback(new SecureProxyConnectionError(err))
  192. } else {
  193. callback(err)
  194. }
  195. }
  196. }
  197. })
  198. }
  199. dispatch (opts, handler) {
  200. const headers = buildHeaders(opts.headers)
  201. throwIfProxyAuthIsSent(headers)
  202. if (headers && !('host' in headers) && !('Host' in headers)) {
  203. const { host } = new URL(opts.origin)
  204. headers.host = host
  205. }
  206. return this[kAgent].dispatch(
  207. {
  208. ...opts,
  209. headers
  210. },
  211. handler
  212. )
  213. }
  214. /**
  215. * @param {import('../../types/proxy-agent').ProxyAgent.Options | string | URL} opts
  216. * @returns {URL}
  217. */
  218. #getUrl (opts) {
  219. if (typeof opts === 'string') {
  220. return new URL(opts)
  221. } else if (opts instanceof URL) {
  222. return opts
  223. } else {
  224. return new URL(opts.uri)
  225. }
  226. }
  227. [kClose] () {
  228. const promises = [this[kAgent].close()]
  229. if (this[kClient]) {
  230. promises.push(this[kClient].close())
  231. }
  232. return Promise.all(promises)
  233. }
  234. [kDestroy] () {
  235. const promises = [this[kAgent].destroy()]
  236. if (this[kClient]) {
  237. promises.push(this[kClient].destroy())
  238. }
  239. return Promise.all(promises)
  240. }
  241. }
  242. /**
  243. * @param {string[] | Record<string, string>} headers
  244. * @returns {Record<string, string>}
  245. */
  246. function buildHeaders (headers) {
  247. // When using undici.fetch, the headers list is stored
  248. // as an array.
  249. if (Array.isArray(headers)) {
  250. /** @type {Record<string, string>} */
  251. const headersPair = {}
  252. for (let i = 0; i < headers.length; i += 2) {
  253. headersPair[headers[i]] = headers[i + 1]
  254. }
  255. return headersPair
  256. }
  257. return headers
  258. }
  259. /**
  260. * @param {Record<string, string>} headers
  261. *
  262. * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers
  263. * Nevertheless, it was changed and to avoid a security vulnerability by end users
  264. * this check was created.
  265. * It should be removed in the next major version for performance reasons
  266. */
  267. function throwIfProxyAuthIsSent (headers) {
  268. const existProxyAuth = headers && Object.keys(headers)
  269. .find((key) => key.toLowerCase() === 'proxy-authorization')
  270. if (existProxyAuth) {
  271. throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor')
  272. }
  273. }
  274. module.exports = ProxyAgent