You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.js 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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. this.defaultPort = 443;
  52. }
  53. inherits(HttpsProxyAgent, Agent);
  54. /**
  55. * Called when the node-core HTTP client library is creating a new HTTP request.
  56. *
  57. * @api public
  58. */
  59. HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) {
  60. var proxy = this.proxy;
  61. // create a socket connection to the proxy server
  62. var socket;
  63. if (this.secureProxy) {
  64. socket = tls.connect(proxy);
  65. } else {
  66. socket = net.connect(proxy);
  67. }
  68. // we need to buffer any HTTP traffic that happens with the proxy before we get
  69. // the CONNECT response, so that if the response is anything other than an "200"
  70. // response code, then we can re-play the "data" events on the socket once the
  71. // HTTP parser is hooked up...
  72. var buffers = [];
  73. var buffersLength = 0;
  74. function read() {
  75. var b = socket.read();
  76. if (b) ondata(b);
  77. else socket.once('readable', read);
  78. }
  79. function cleanup() {
  80. socket.removeListener('end', onend);
  81. socket.removeListener('error', onerror);
  82. socket.removeListener('close', onclose);
  83. socket.removeListener('readable', read);
  84. }
  85. function onclose(err) {
  86. debug('onclose had error %o', err);
  87. }
  88. function onend() {
  89. debug('onend');
  90. }
  91. function onerror(err) {
  92. cleanup();
  93. fn(err);
  94. }
  95. function ondata(b) {
  96. buffers.push(b);
  97. buffersLength += b.length;
  98. var buffered = Buffer.concat(buffers, buffersLength);
  99. var str = buffered.toString('ascii');
  100. if (!~str.indexOf('\r\n\r\n')) {
  101. // keep buffering
  102. debug('have not received end of HTTP headers yet...');
  103. read();
  104. return;
  105. }
  106. var firstLine = str.substring(0, str.indexOf('\r\n'));
  107. var statusCode = +firstLine.split(' ')[1];
  108. debug('got proxy server response: %o', firstLine);
  109. if (200 == statusCode) {
  110. // 200 Connected status code!
  111. var sock = socket;
  112. // nullify the buffered data since we won't be needing it
  113. buffers = buffered = null;
  114. if (opts.secureEndpoint) {
  115. // since the proxy is connecting to an SSL server, we have
  116. // to upgrade this socket connection to an SSL connection
  117. debug(
  118. 'upgrading proxy-connected socket to TLS connection: %o',
  119. opts.host
  120. );
  121. opts.socket = socket;
  122. opts.servername = opts.servername || opts.host;
  123. opts.host = null;
  124. opts.hostname = null;
  125. opts.port = null;
  126. sock = tls.connect(opts);
  127. }
  128. cleanup();
  129. req.once('socket', resume);
  130. fn(null, sock);
  131. } else {
  132. // some other status code that's not 200... need to re-play the HTTP header
  133. // "data" events onto the socket once the HTTP machinery is attached so
  134. // that the node core `http` can parse and handle the error status code
  135. cleanup();
  136. // the original socket is closed, and a new closed socket is
  137. // returned instead, so that the proxy doesn't get the HTTP request
  138. // written to it (which may contain `Authorization` headers or other
  139. // sensitive data).
  140. //
  141. // See: https://hackerone.com/reports/541502
  142. socket.destroy();
  143. socket = new net.Socket();
  144. socket.readable = true;
  145. // save a reference to the concat'd Buffer for the `onsocket` callback
  146. buffers = buffered;
  147. // need to wait for the "socket" event to re-play the "data" events
  148. req.once('socket', onsocket);
  149. fn(null, socket);
  150. }
  151. }
  152. function onsocket(socket) {
  153. debug('replaying proxy buffer for failed request');
  154. assert(socket.listenerCount('data') > 0);
  155. // replay the "buffers" Buffer onto the `socket`, since at this point
  156. // the HTTP module machinery has been hooked up for the user
  157. socket.push(buffers);
  158. // nullify the cached Buffer instance
  159. buffers = null;
  160. }
  161. socket.on('error', onerror);
  162. socket.on('close', onclose);
  163. socket.on('end', onend);
  164. read();
  165. var hostname = opts.host + ':' + opts.port;
  166. var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n';
  167. var headers = Object.assign({}, proxy.headers);
  168. if (proxy.auth) {
  169. headers['Proxy-Authorization'] =
  170. 'Basic ' + Buffer.from(proxy.auth).toString('base64');
  171. }
  172. // the Host header should only include the port
  173. // number when it is a non-standard port
  174. var host = opts.host;
  175. if (!isDefaultPort(opts.port, opts.secureEndpoint)) {
  176. host += ':' + opts.port;
  177. }
  178. headers['Host'] = host;
  179. headers['Connection'] = 'close';
  180. Object.keys(headers).forEach(function(name) {
  181. msg += name + ': ' + headers[name] + '\r\n';
  182. });
  183. socket.write(msg + '\r\n');
  184. };
  185. /**
  186. * Resumes a socket.
  187. *
  188. * @param {(net.Socket|tls.Socket)} socket The socket to resume
  189. * @api public
  190. */
  191. function resume(socket) {
  192. socket.resume();
  193. }
  194. function isDefaultPort(port, secure) {
  195. return Boolean((!secure && port === 80) || (secure && port === 443));
  196. }