index.js 6.2 KB

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