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.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. 'use strict';
  2. /**
  3. * Module exports.
  4. */
  5. module.exports = exports = PacProxyAgent;
  6. /**
  7. * Supported "protocols". Delegates out to the `get-uri` module.
  8. */
  9. var getUri = require('get-uri');
  10. Object.defineProperty(exports, 'protocols', {
  11. enumerable: true,
  12. configurable: true,
  13. get: function () { return Object.keys(getUri.protocols); }
  14. });
  15. /**
  16. * Module dependencies.
  17. */
  18. var net = require('net');
  19. var tls = require('tls');
  20. var crypto = require('crypto');
  21. var parse = require('url').parse;
  22. var format = require('url').format;
  23. var Agent = require('agent-base');
  24. var HttpProxyAgent = require('http-proxy-agent');
  25. var HttpsProxyAgent = require('https-proxy-agent');
  26. var SocksProxyAgent = require('socks-proxy-agent');
  27. var PacResolver = require('pac-resolver');
  28. var getRawBody = require('raw-body');
  29. var inherits = require('util').inherits;
  30. var debug = require('debug')('pac-proxy-agent');
  31. /**
  32. * The `PacProxyAgent` class.
  33. *
  34. * A few different "protocol" modes are supported (supported protocols are
  35. * backed by the `get-uri` module):
  36. *
  37. * - "pac+data", "data" - refers to an embedded "data:" URI
  38. * - "pac+file", "file" - refers to a local file
  39. * - "pac+ftp", "ftp" - refers to a file located on an FTP server
  40. * - "pac+http", "http" - refers to an HTTP endpoint
  41. * - "pac+https", "https" - refers to an HTTPS endpoint
  42. *
  43. * @api public
  44. */
  45. function PacProxyAgent (uri, opts) {
  46. if (!(this instanceof PacProxyAgent)) return new PacProxyAgent(uri, opts);
  47. // was an options object passed in first?
  48. if ('object' === typeof uri) {
  49. opts = uri;
  50. // result of a url.parse() call?
  51. if (opts.href) {
  52. if (opts.path && !opts.pathname) {
  53. opts.pathname = opts.path;
  54. }
  55. opts.slashes = true;
  56. uri = format(opts);
  57. } else {
  58. uri = opts.uri;
  59. }
  60. }
  61. if (!opts) opts = {};
  62. if (!uri) throw new Error('a PAC file URI must be specified!');
  63. debug('creating PacProxyAgent with URI %o and options %o', uri, opts);
  64. Agent.call(this, connect);
  65. // strip the "pac+" prefix
  66. this.uri = uri.replace(/^pac\+/i, '');
  67. this.sandbox = opts.sandbox;
  68. this.proxy = opts;
  69. this.cache = this._resolver = null;
  70. }
  71. inherits(PacProxyAgent, Agent);
  72. /**
  73. * Loads the PAC proxy file from the source if necessary, and returns
  74. * a generated `FindProxyForURL()` resolver function to use.
  75. *
  76. * @param {Function} fn callback function
  77. * @api private
  78. */
  79. PacProxyAgent.prototype.loadResolver = function (fn) {
  80. var self = this;
  81. // kick things off by attempting to (re)load the contents of the PAC file URI
  82. this.loadPacFile(onpacfile);
  83. // loadPacFile() callback function
  84. function onpacfile (err, code) {
  85. if (err) {
  86. if ('ENOTMODIFIED' == err.code) {
  87. debug('got ENOTMODIFIED response, reusing previous proxy resolver');
  88. fn(null, self._resolver);
  89. } else {
  90. fn(err);
  91. }
  92. return;
  93. }
  94. // create a sha1 hash of the JS code
  95. var hash = crypto.createHash('sha1').update(code).digest('hex');
  96. if (self._resolver && self._resolver.hash == hash) {
  97. debug('same sha1 hash for code - contents have not changed, reusing previous proxy resolver');
  98. fn(null, self._resolver);
  99. return;
  100. }
  101. // cache the resolver
  102. debug('creating new proxy resolver instance');
  103. self._resolver = new PacResolver(code, {
  104. filename: self.uri,
  105. sandbox: self.sandbox
  106. });
  107. // store that sha1 hash on the resolver instance
  108. // for future comparison purposes
  109. self._resolver.hash = hash;
  110. fn(null, self._resolver);
  111. }
  112. };
  113. /**
  114. * Loads the contents of the PAC proxy file.
  115. *
  116. * @param {Function} fn callback function
  117. * @api private
  118. */
  119. PacProxyAgent.prototype.loadPacFile = function (fn) {
  120. debug('loading PAC file: %o', this.uri);
  121. var self = this;
  122. // delegate out to the `get-uri` module
  123. var opts = {};
  124. if (this.cache) {
  125. opts.cache = this.cache;
  126. }
  127. getUri(this.uri, opts, onstream);
  128. function onstream (err, rs) {
  129. if (err) return fn(err);
  130. debug('got stream.Readable instance for URI');
  131. self.cache = rs;
  132. getRawBody(rs, 'utf8', onbuffer);
  133. }
  134. function onbuffer (err, buf) {
  135. if (err) return fn(err);
  136. debug('read %o byte PAC file from URI', buf.length);
  137. fn(null, buf);
  138. }
  139. };
  140. /**
  141. * Called when the node-core HTTP client library is creating a new HTTP request.
  142. *
  143. * @api public
  144. */
  145. function connect (req, opts, fn) {
  146. var url;
  147. var host;
  148. var self = this;
  149. var secure = Boolean(opts.secureEndpoint);
  150. // first we need get a generated FindProxyForURL() function,
  151. // either cached or retreived from the source
  152. this.loadResolver(onresolver);
  153. // `loadResolver()` callback function
  154. function onresolver (err, FindProxyForURL) {
  155. if (err) return fn(err);
  156. // calculate the `url` parameter
  157. var defaultPort = secure ? 443 : 80;
  158. var path = req.path;
  159. var firstQuestion = path.indexOf('?');
  160. var search;
  161. if (-1 != firstQuestion) {
  162. search = path.substring(firstQuestion);
  163. path = path.substring(0, firstQuestion);
  164. }
  165. url = format(Object.assign({}, opts, {
  166. protocol: secure ? 'https:' : 'http:',
  167. pathname: path,
  168. search: search,
  169. // need to use `hostname` instead of `host` otherwise `port` is ignored
  170. hostname: opts.host,
  171. host: null,
  172. // set `port` to null when it is the protocol default port (80 / 443)
  173. port: defaultPort == opts.port ? null : opts.port
  174. }));
  175. // calculate the `host` parameter
  176. host = parse(url).hostname;
  177. debug('url: %o, host: %o', url, host);
  178. FindProxyForURL(url, host, onproxy);
  179. }
  180. // `FindProxyForURL()` callback function
  181. function onproxy (err, proxy) {
  182. if (err) return fn(err);
  183. // default to "DIRECT" if a falsey value was returned (or nothing)
  184. if (!proxy) proxy = 'DIRECT';
  185. var proxies = String(proxy).trim().split(/\s*;\s*/g).filter(Boolean);
  186. // XXX: right now, only the first proxy specified will be used
  187. var first = proxies[0];
  188. debug('using proxy: %o', first);
  189. var agent;
  190. var parts = first.split(/\s+/);
  191. var type = parts[0];
  192. if ('DIRECT' == type) {
  193. // direct connection to the destination endpoint
  194. var socket;
  195. if (secure) {
  196. socket = tls.connect(opts);
  197. } else {
  198. socket = net.connect(opts);
  199. }
  200. return fn(null, socket);
  201. } else if ('SOCKS' == type) {
  202. // use a SOCKS proxy
  203. agent = new SocksProxyAgent('socks://' + parts[1]);
  204. } else if ('PROXY' == type || 'HTTPS' == type) {
  205. // use an HTTP or HTTPS proxy
  206. // http://dev.chromium.org/developers/design-documents/secure-web-proxy
  207. var proxyURL = ('HTTPS' === type ? 'https' : 'http') + '://' + parts[1];
  208. var proxy = Object.assign({}, self.proxy, parse(proxyURL));
  209. if (secure) {
  210. agent = new HttpsProxyAgent(proxy);
  211. } else {
  212. agent = new HttpProxyAgent(proxy);
  213. }
  214. } else {
  215. throw new Error('Unknown proxy type: ' + type);
  216. }
  217. if (agent) agent.callback(req, opts, fn);
  218. }
  219. }