/** * 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)); }