agent.js
  1  "use strict";
  2  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  3      function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  4      return new (P || (P = Promise))(function (resolve, reject) {
  5          function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  6          function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  7          function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  8          step((generator = generator.apply(thisArg, _arguments || [])).next());
  9      });
 10  };
 11  var __importDefault = (this && this.__importDefault) || function (mod) {
 12      return (mod && mod.__esModule) ? mod : { "default": mod };
 13  };
 14  Object.defineProperty(exports, "__esModule", { value: true });
 15  const net_1 = __importDefault(require("net"));
 16  const tls_1 = __importDefault(require("tls"));
 17  const once_1 = __importDefault(require("@tootallnate/once"));
 18  const crypto_1 = __importDefault(require("crypto"));
 19  const get_uri_1 = __importDefault(require("get-uri"));
 20  const debug_1 = __importDefault(require("debug"));
 21  const raw_body_1 = __importDefault(require("raw-body"));
 22  const url_1 = require("url");
 23  const http_proxy_agent_1 = require("http-proxy-agent");
 24  const https_proxy_agent_1 = require("https-proxy-agent");
 25  const socks_proxy_agent_1 = require("socks-proxy-agent");
 26  const pac_resolver_1 = __importDefault(require("pac-resolver"));
 27  const agent_base_1 = require("agent-base");
 28  const debug = debug_1.default('pac-proxy-agent');
 29  /**
 30   * The `PacProxyAgent` class.
 31   *
 32   * A few different "protocol" modes are supported (supported protocols are
 33   * backed by the `get-uri` module):
 34   *
 35   *   - "pac+data", "data" - refers to an embedded "data:" URI
 36   *   - "pac+file", "file" - refers to a local file
 37   *   - "pac+ftp", "ftp" - refers to a file located on an FTP server
 38   *   - "pac+http", "http" - refers to an HTTP endpoint
 39   *   - "pac+https", "https" - refers to an HTTPS endpoint
 40   *
 41   * @api public
 42   */
 43  class PacProxyAgent extends agent_base_1.Agent {
 44      constructor(uri, opts = {}) {
 45          super(opts);
 46          this.clearResolverPromise = () => {
 47              this.resolverPromise = undefined;
 48          };
 49          debug('Creating PacProxyAgent with URI %o and options %o', uri, opts);
 50          // Strip the "pac+" prefix
 51          this.uri = uri.replace(/^pac\+/i, '');
 52          this.opts = Object.assign({}, opts);
 53          this.cache = undefined;
 54          this.resolver = undefined;
 55          this.resolverHash = '';
 56          this.resolverPromise = undefined;
 57          // For `PacResolver`
 58          if (!this.opts.filename) {
 59              this.opts.filename = uri;
 60          }
 61      }
 62      /**
 63       * Loads the PAC proxy file from the source if necessary, and returns
 64       * a generated `FindProxyForURL()` resolver function to use.
 65       *
 66       * @api private
 67       */
 68      getResolver() {
 69          if (!this.resolverPromise) {
 70              this.resolverPromise = this.loadResolver();
 71              this.resolverPromise.then(this.clearResolverPromise, this.clearResolverPromise);
 72          }
 73          return this.resolverPromise;
 74      }
 75      loadResolver() {
 76          return __awaiter(this, void 0, void 0, function* () {
 77              try {
 78                  // (Re)load the contents of the PAC file URI
 79                  const code = yield this.loadPacFile();
 80                  // Create a sha1 hash of the JS code
 81                  const hash = crypto_1.default
 82                      .createHash('sha1')
 83                      .update(code)
 84                      .digest('hex');
 85                  if (this.resolver && this.resolverHash === hash) {
 86                      debug('Same sha1 hash for code - contents have not changed, reusing previous proxy resolver');
 87                      return this.resolver;
 88                  }
 89                  // Cache the resolver
 90                  debug('Creating new proxy resolver instance');
 91                  this.resolver = pac_resolver_1.default(code, this.opts);
 92                  // Store that sha1 hash for future comparison purposes
 93                  this.resolverHash = hash;
 94                  return this.resolver;
 95              }
 96              catch (err) {
 97                  if (this.resolver && err.code === 'ENOTMODIFIED') {
 98                      debug('Got ENOTMODIFIED response, reusing previous proxy resolver');
 99                      return this.resolver;
100                  }
101                  throw err;
102              }
103          });
104      }
105      /**
106       * Loads the contents of the PAC proxy file.
107       *
108       * @api private
109       */
110      loadPacFile() {
111          return __awaiter(this, void 0, void 0, function* () {
112              debug('Loading PAC file: %o', this.uri);
113              const rs = yield get_uri_1.default(this.uri, { cache: this.cache });
114              debug('Got `Readable` instance for URI');
115              this.cache = rs;
116              const buf = yield raw_body_1.default(rs);
117              debug('Read %o byte PAC file from URI', buf.length);
118              return buf.toString('utf8');
119          });
120      }
121      /**
122       * Called when the node-core HTTP client library is creating a new HTTP request.
123       *
124       * @api protected
125       */
126      callback(req, opts) {
127          return __awaiter(this, void 0, void 0, function* () {
128              const { secureEndpoint } = opts;
129              // First, get a generated `FindProxyForURL()` function,
130              // either cached or retrieved from the source
131              const resolver = yield this.getResolver();
132              // Calculate the `url` parameter
133              const defaultPort = secureEndpoint ? 443 : 80;
134              let path = req.path;
135              let search = null;
136              const firstQuestion = path.indexOf('?');
137              if (firstQuestion !== -1) {
138                  search = path.substring(firstQuestion);
139                  path = path.substring(0, firstQuestion);
140              }
141              const urlOpts = Object.assign(Object.assign({}, opts), { protocol: secureEndpoint ? 'https:' : 'http:', pathname: path, search, 
142                  // need to use `hostname` instead of `host` otherwise `port` is ignored
143                  hostname: opts.host, host: null, href: null, 
144                  // set `port` to null when it is the protocol default port (80 / 443)
145                  port: defaultPort === opts.port ? null : opts.port });
146              const url = url_1.format(urlOpts);
147              debug('url: %o', url);
148              let result = yield resolver(url);
149              // Default to "DIRECT" if a falsey value was returned (or nothing)
150              if (!result) {
151                  result = 'DIRECT';
152              }
153              const proxies = String(result)
154                  .trim()
155                  .split(/\s*;\s*/g)
156                  .filter(Boolean);
157              if (this.opts.fallbackToDirect && !proxies.includes('DIRECT')) {
158                  proxies.push('DIRECT');
159              }
160              for (const proxy of proxies) {
161                  let agent = null;
162                  let socket = null;
163                  const [type, target] = proxy.split(/\s+/);
164                  debug('Attempting to use proxy: %o', proxy);
165                  if (type === 'DIRECT') {
166                      // Direct connection to the destination endpoint
167                      socket = secureEndpoint ? tls_1.default.connect(opts) : net_1.default.connect(opts);
168                  }
169                  else if (type === 'SOCKS' || type === 'SOCKS5') {
170                      // Use a SOCKSv5h proxy
171                      agent = new socks_proxy_agent_1.SocksProxyAgent(`socks://${target}`);
172                  }
173                  else if (type === 'SOCKS4') {
174                      // Use a SOCKSv4a proxy
175                      agent = new socks_proxy_agent_1.SocksProxyAgent(`socks4a://${target}`);
176                  }
177                  else if (type === 'PROXY' ||
178                      type === 'HTTP' ||
179                      type === 'HTTPS') {
180                      // Use an HTTP or HTTPS proxy
181                      // http://dev.chromium.org/developers/design-documents/secure-web-proxy
182                      const proxyURL = `${type === 'HTTPS' ? 'https' : 'http'}://${target}`;
183                      const proxyOpts = Object.assign(Object.assign({}, this.opts), url_1.parse(proxyURL));
184                      if (secureEndpoint) {
185                          agent = new https_proxy_agent_1.HttpsProxyAgent(proxyOpts);
186                      }
187                      else {
188                          agent = new http_proxy_agent_1.HttpProxyAgent(proxyOpts);
189                      }
190                  }
191                  try {
192                      if (socket) {
193                          // "DIRECT" connection, wait for connection confirmation
194                          yield once_1.default(socket, 'connect');
195                          req.emit('proxy', { proxy, socket });
196                          return socket;
197                      }
198                      if (agent) {
199                          const s = yield agent.callback(req, opts);
200                          req.emit('proxy', { proxy, socket: s });
201                          return s;
202                      }
203                      throw new Error(`Could not determine proxy type for: ${proxy}`);
204                  }
205                  catch (err) {
206                      debug('Got error for proxy %o: %o', proxy, err);
207                      req.emit('proxy', { proxy, error: err });
208                  }
209              }
210              throw new Error(`Failed to establish a socket connection to proxies: ${JSON.stringify(proxies)}`);
211          });
212      }
213  }
214  exports.default = PacProxyAgent;
215  //# sourceMappingURL=agent.js.map