| /** |
| * Module dependencies. |
| */ |
| |
| var net = require('net'); |
| var tls = require('tls'); |
| var url = require('url'); |
| var assert = require('assert'); |
| var Agent = require('agent-base'); |
| var inherits = require('util').inherits; |
| var debug = require('debug')('https-proxy-agent'); |
| |
| /** |
| * Module exports. |
| */ |
| |
| module.exports = HttpsProxyAgent; |
| |
| /** |
| * The `HttpsProxyAgent` implements an HTTP Agent subclass that connects to the |
| * specified "HTTP(s) proxy server" in order to proxy HTTPS requests. |
| * |
| * @api public |
| */ |
| |
| function HttpsProxyAgent(opts) { |
| if (!(this instanceof HttpsProxyAgent)) return new HttpsProxyAgent(opts); |
| if ('string' == typeof opts) opts = url.parse(opts); |
| if (!opts) |
| throw new Error( |
| 'an HTTP(S) proxy server `host` and `port` must be specified!' |
| ); |
| debug('creating new HttpsProxyAgent instance: %o', opts); |
| Agent.call(this, opts); |
| |
| var proxy = Object.assign({}, opts); |
| |
| // if `true`, then connect to the proxy server over TLS. defaults to `false`. |
| this.secureProxy = proxy.protocol |
| ? /^https:?$/i.test(proxy.protocol) |
| : false; |
| |
| // prefer `hostname` over `host`, and set the `port` if needed |
| proxy.host = proxy.hostname || proxy.host; |
| proxy.port = +proxy.port || (this.secureProxy ? 443 : 80); |
| |
| // ALPN is supported by Node.js >= v5. |
| // attempt to negotiate http/1.1 for proxy servers that support http/2 |
| if (this.secureProxy && !('ALPNProtocols' in proxy)) { |
| proxy.ALPNProtocols = ['http 1.1']; |
| } |
| |
| if (proxy.host && proxy.path) { |
| // if both a `host` and `path` are specified then it's most likely the |
| // result of a `url.parse()` call... we need to remove the `path` portion so |
| // that `net.connect()` doesn't attempt to open that as a unix socket file. |
| delete proxy.path; |
| delete proxy.pathname; |
| } |
| |
| this.proxy = proxy; |
| this.defaultPort = 443; |
| } |
| inherits(HttpsProxyAgent, Agent); |
| |
| /** |
| * Called when the node-core HTTP client library is creating a new HTTP request. |
| * |
| * @api public |
| */ |
| |
| HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { |
| var proxy = this.proxy; |
| |
| // create a socket connection to the proxy server |
| var socket; |
| if (this.secureProxy) { |
| socket = tls.connect(proxy); |
| } else { |
| socket = net.connect(proxy); |
| } |
| |
| // we need to buffer any HTTP traffic that happens with the proxy before we get |
| // the CONNECT response, so that if the response is anything other than an "200" |
| // response code, then we can re-play the "data" events on the socket once the |
| // HTTP parser is hooked up... |
| var buffers = []; |
| var buffersLength = 0; |
| |
| function read() { |
| var b = socket.read(); |
| if (b) ondata(b); |
| else socket.once('readable', read); |
| } |
| |
| function cleanup() { |
| socket.removeListener('end', onend); |
| socket.removeListener('error', onerror); |
| socket.removeListener('close', onclose); |
| socket.removeListener('readable', read); |
| } |
| |
| function onclose(err) { |
| debug('onclose had error %o', err); |
| } |
| |
| function onend() { |
| debug('onend'); |
| } |
| |
| function onerror(err) { |
| cleanup(); |
| fn(err); |
| } |
| |
| function ondata(b) { |
| buffers.push(b); |
| buffersLength += b.length; |
| var buffered = Buffer.concat(buffers, buffersLength); |
| var str = buffered.toString('ascii'); |
| |
| if (!~str.indexOf('\r\n\r\n')) { |
| // keep buffering |
| debug('have not received end of HTTP headers yet...'); |
| read(); |
| return; |
| } |
| |
| var firstLine = str.substring(0, str.indexOf('\r\n')); |
| var statusCode = +firstLine.split(' ')[1]; |
| debug('got proxy server response: %o', firstLine); |
| |
| if (200 == statusCode) { |
| // 200 Connected status code! |
| var sock = socket; |
| |
| // nullify the buffered data since we won't be needing it |
| buffers = buffered = null; |
| |
| if (opts.secureEndpoint) { |
| // since the proxy is connecting to an SSL server, we have |
| // to upgrade this socket connection to an SSL connection |
| debug( |
| 'upgrading proxy-connected socket to TLS connection: %o', |
| opts.host |
| ); |
| opts.socket = socket; |
| opts.servername = opts.servername || opts.host; |
| opts.host = null; |
| opts.hostname = null; |
| opts.port = null; |
| sock = tls.connect(opts); |
| } |
| |
| cleanup(); |
| req.once('socket', resume); |
| fn(null, sock); |
| } else { |
| // some other status code that's not 200... need to re-play the HTTP header |
| // "data" events onto the socket once the HTTP machinery is attached so |
| // that the node core `http` can parse and handle the error status code |
| cleanup(); |
| |
| // the original socket is closed, and a new closed socket is |
| // returned instead, so that the proxy doesn't get the HTTP request |
| // written to it (which may contain `Authorization` headers or other |
| // sensitive data). |
| // |
| // See: https://hackerone.com/reports/541502 |
| socket.destroy(); |
| socket = new net.Socket(); |
| socket.readable = true; |
| |
| |
| // save a reference to the concat'd Buffer for the `onsocket` callback |
| buffers = buffered; |
| |
| // need to wait for the "socket" event to re-play the "data" events |
| req.once('socket', onsocket); |
| |
| fn(null, socket); |
| } |
| } |
| |
| function onsocket(socket) { |
| debug('replaying proxy buffer for failed request'); |
| assert(socket.listenerCount('data') > 0); |
| |
| // replay the "buffers" Buffer onto the `socket`, since at this point |
| // the HTTP module machinery has been hooked up for the user |
| socket.push(buffers); |
| |
| // nullify the cached Buffer instance |
| buffers = null; |
| } |
| |
| socket.on('error', onerror); |
| socket.on('close', onclose); |
| socket.on('end', onend); |
| |
| read(); |
| |
| var hostname = opts.host + ':' + opts.port; |
| var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n'; |
| |
| var headers = Object.assign({}, proxy.headers); |
| if (proxy.auth) { |
| headers['Proxy-Authorization'] = |
| 'Basic ' + Buffer.from(proxy.auth).toString('base64'); |
| } |
| |
| // the Host header should only include the port |
| // number when it is a non-standard port |
| var host = opts.host; |
| if (!isDefaultPort(opts.port, opts.secureEndpoint)) { |
| host += ':' + opts.port; |
| } |
| headers['Host'] = host; |
| |
| headers['Connection'] = 'close'; |
| Object.keys(headers).forEach(function(name) { |
| msg += name + ': ' + headers[name] + '\r\n'; |
| }); |
| |
| socket.write(msg + '\r\n'); |
| }; |
| |
| /** |
| * Resumes a socket. |
| * |
| * @param {(net.Socket|tls.Socket)} socket The socket to resume |
| * @api public |
| */ |
| |
| function resume(socket) { |
| socket.resume(); |
| } |
| |
| function isDefaultPort(port, secure) { |
| return Boolean((!secure && port === 80) || (secure && port === 443)); |
| } |