1/** 2 * Module dependencies. 3 */ 4 5var net = require('net'); 6var tls = require('tls'); 7var url = require('url'); 8var assert = require('assert'); 9var Agent = require('agent-base'); 10var inherits = require('util').inherits; 11var debug = require('debug')('https-proxy-agent'); 12 13/** 14 * Module exports. 15 */ 16 17module.exports = HttpsProxyAgent; 18 19/** 20 * The `HttpsProxyAgent` implements an HTTP Agent subclass that connects to the 21 * specified "HTTP(s) proxy server" in order to proxy HTTPS requests. 22 * 23 * @api public 24 */ 25 26function HttpsProxyAgent(opts) { 27 if (!(this instanceof HttpsProxyAgent)) return new HttpsProxyAgent(opts); 28 if ('string' == typeof opts) opts = url.parse(opts); 29 if (!opts) 30 throw new Error( 31 'an HTTP(S) proxy server `host` and `port` must be specified!' 32 ); 33 debug('creating new HttpsProxyAgent instance: %o', opts); 34 Agent.call(this, opts); 35 36 var proxy = Object.assign({}, opts); 37 38 // if `true`, then connect to the proxy server over TLS. defaults to `false`. 39 this.secureProxy = proxy.protocol 40 ? /^https:?$/i.test(proxy.protocol) 41 : false; 42 43 // prefer `hostname` over `host`, and set the `port` if needed 44 proxy.host = proxy.hostname || proxy.host; 45 proxy.port = +proxy.port || (this.secureProxy ? 443 : 80); 46 47 // ALPN is supported by Node.js >= v5. 48 // attempt to negotiate http/1.1 for proxy servers that support http/2 49 if (this.secureProxy && !('ALPNProtocols' in proxy)) { 50 proxy.ALPNProtocols = ['http 1.1']; 51 } 52 53 if (proxy.host && proxy.path) { 54 // if both a `host` and `path` are specified then it's most likely the 55 // result of a `url.parse()` call... we need to remove the `path` portion so 56 // that `net.connect()` doesn't attempt to open that as a unix socket file. 57 delete proxy.path; 58 delete proxy.pathname; 59 } 60 61 this.proxy = proxy; 62 this.defaultPort = 443; 63} 64inherits(HttpsProxyAgent, Agent); 65 66/** 67 * Called when the node-core HTTP client library is creating a new HTTP request. 68 * 69 * @api public 70 */ 71 72HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { 73 var proxy = this.proxy; 74 75 // create a socket connection to the proxy server 76 var socket; 77 if (this.secureProxy) { 78 socket = tls.connect(proxy); 79 } else { 80 socket = net.connect(proxy); 81 } 82 83 // we need to buffer any HTTP traffic that happens with the proxy before we get 84 // the CONNECT response, so that if the response is anything other than an "200" 85 // response code, then we can re-play the "data" events on the socket once the 86 // HTTP parser is hooked up... 87 var buffers = []; 88 var buffersLength = 0; 89 90 function read() { 91 var b = socket.read(); 92 if (b) ondata(b); 93 else socket.once('readable', read); 94 } 95 96 function cleanup() { 97 socket.removeListener('end', onend); 98 socket.removeListener('error', onerror); 99 socket.removeListener('close', onclose); 100 socket.removeListener('readable', read); 101 } 102 103 function onclose(err) { 104 debug('onclose had error %o', err); 105 } 106 107 function onend() { 108 debug('onend'); 109 } 110 111 function onerror(err) { 112 cleanup(); 113 fn(err); 114 } 115 116 function ondata(b) { 117 buffers.push(b); 118 buffersLength += b.length; 119 var buffered = Buffer.concat(buffers, buffersLength); 120 var str = buffered.toString('ascii'); 121 122 if (!~str.indexOf('\r\n\r\n')) { 123 // keep buffering 124 debug('have not received end of HTTP headers yet...'); 125 read(); 126 return; 127 } 128 129 var firstLine = str.substring(0, str.indexOf('\r\n')); 130 var statusCode = +firstLine.split(' ')[1]; 131 debug('got proxy server response: %o', firstLine); 132 133 if (200 == statusCode) { 134 // 200 Connected status code! 135 var sock = socket; 136 137 // nullify the buffered data since we won't be needing it 138 buffers = buffered = null; 139 140 if (opts.secureEndpoint) { 141 // since the proxy is connecting to an SSL server, we have 142 // to upgrade this socket connection to an SSL connection 143 debug( 144 'upgrading proxy-connected socket to TLS connection: %o', 145 opts.host 146 ); 147 opts.socket = socket; 148 opts.servername = opts.servername || opts.host; 149 opts.host = null; 150 opts.hostname = null; 151 opts.port = null; 152 sock = tls.connect(opts); 153 } 154 155 cleanup(); 156 req.once('socket', resume); 157 fn(null, sock); 158 } else { 159 // some other status code that's not 200... need to re-play the HTTP header 160 // "data" events onto the socket once the HTTP machinery is attached so 161 // that the node core `http` can parse and handle the error status code 162 cleanup(); 163 164 // the original socket is closed, and a new closed socket is 165 // returned instead, so that the proxy doesn't get the HTTP request 166 // written to it (which may contain `Authorization` headers or other 167 // sensitive data). 168 // 169 // See: https://hackerone.com/reports/541502 170 socket.destroy(); 171 socket = new net.Socket(); 172 socket.readable = true; 173 174 175 // save a reference to the concat'd Buffer for the `onsocket` callback 176 buffers = buffered; 177 178 // need to wait for the "socket" event to re-play the "data" events 179 req.once('socket', onsocket); 180 181 fn(null, socket); 182 } 183 } 184 185 function onsocket(socket) { 186 debug('replaying proxy buffer for failed request'); 187 assert(socket.listenerCount('data') > 0); 188 189 // replay the "buffers" Buffer onto the `socket`, since at this point 190 // the HTTP module machinery has been hooked up for the user 191 socket.push(buffers); 192 193 // nullify the cached Buffer instance 194 buffers = null; 195 } 196 197 socket.on('error', onerror); 198 socket.on('close', onclose); 199 socket.on('end', onend); 200 201 read(); 202 203 var hostname = opts.host + ':' + opts.port; 204 var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n'; 205 206 var headers = Object.assign({}, proxy.headers); 207 if (proxy.auth) { 208 headers['Proxy-Authorization'] = 209 'Basic ' + Buffer.from(proxy.auth).toString('base64'); 210 } 211 212 // the Host header should only include the port 213 // number when it is a non-standard port 214 var host = opts.host; 215 if (!isDefaultPort(opts.port, opts.secureEndpoint)) { 216 host += ':' + opts.port; 217 } 218 headers['Host'] = host; 219 220 headers['Connection'] = 'close'; 221 Object.keys(headers).forEach(function(name) { 222 msg += name + ': ' + headers[name] + '\r\n'; 223 }); 224 225 socket.write(msg + '\r\n'); 226}; 227 228/** 229 * Resumes a socket. 230 * 231 * @param {(net.Socket|tls.Socket)} socket The socket to resume 232 * @api public 233 */ 234 235function resume(socket) { 236 socket.resume(); 237} 238 239function isDefaultPort(port, secure) { 240 return Boolean((!secure && port === 80) || (secure && port === 443)); 241} 242