'use strict';
							 | 
						|
								const util = require('util');
							 | 
						|
								const urlutil = require('url');
							 | 
						|
								const http = require('http');
							 | 
						|
								const https = require('https');
							 | 
						|
								const debug = require('debug')('urllib');
							 | 
						|
								const ms = require('humanize-ms');
							 | 
						|
								let REQUEST_ID = 0;
							 | 
						|
								const MAX_VALUE = Math.pow(2, 31) - 10;
							 | 
						|
								const PROTO_RE = /^https?:\/\//i;
							 | 
						|
								
							 | 
						|
								function getAgent(agent, defaultAgent) {
							 | 
						|
								  return agent === undefined ? defaultAgent : agent;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function parseContentType(str) {
							 | 
						|
								  if (!str) {
							 | 
						|
								    return '';
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  return str.split(';')[0].trim().toLowerCase();
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function makeCallback(resolve, reject) {
							 | 
						|
								  return function (err, data, res) {
							 | 
						|
								    if (err) {
							 | 
						|
								      return reject(err);
							 | 
						|
								    }
							 | 
						|
								    resolve({
							 | 
						|
								      data: data,
							 | 
						|
								      status: res.statusCode,
							 | 
						|
								      headers: res.headers,
							 | 
						|
								      res: res
							 | 
						|
								    });
							 | 
						|
								  };
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// exports.TIMEOUT = ms('5s');
							 | 
						|
								exports.TIMEOUTS = [ms('300s'), ms('300s')];
							 | 
						|
								
							 | 
						|
								const TEXT_DATA_TYPES = ['json', 'text'];
							 | 
						|
								
							 | 
						|
								exports.request = function request(url, args, callback) {
							 | 
						|
								  // request(url, callback)
							 | 
						|
								  if (arguments.length === 2 && typeof args === 'function') {
							 | 
						|
								    callback = args;
							 | 
						|
								    args = null;
							 | 
						|
								  }
							 | 
						|
								  if (typeof callback === 'function') {
							 | 
						|
								    return exports.requestWithCallback(url, args, callback);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  return new Promise(function (resolve, reject) {
							 | 
						|
								    exports.requestWithCallback(url, args, makeCallback(resolve, reject));
							 | 
						|
								  });
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								exports.requestWithCallback = function requestWithCallback(url, args, callback) {
							 | 
						|
								  if (!url || (typeof url !== 'string' && typeof url !== 'object')) {
							 | 
						|
								    const msg = util.format('expect request url to be a string or a http request options, but got' + ' %j', url);
							 | 
						|
								    throw new Error(msg);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  if (arguments.length === 2 && typeof args === 'function') {
							 | 
						|
								    callback = args;
							 | 
						|
								    args = null;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  args = args || {};
							 | 
						|
								  if (REQUEST_ID >= MAX_VALUE) {
							 | 
						|
								    REQUEST_ID = 0;
							 | 
						|
								  }
							 | 
						|
								  const reqId = ++REQUEST_ID;
							 | 
						|
								
							 | 
						|
								  args.requestUrls = args.requestUrls || [];
							 | 
						|
								
							 | 
						|
								  const reqMeta = {
							 | 
						|
								    requestId: reqId,
							 | 
						|
								    url: url,
							 | 
						|
								    args: args,
							 | 
						|
								    ctx: args.ctx
							 | 
						|
								  };
							 | 
						|
								  if (args.emitter) {
							 | 
						|
								    args.emitter.emit('request', reqMeta);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  args.timeout = args.timeout || exports.TIMEOUTS;
							 | 
						|
								  args.maxRedirects = args.maxRedirects || 10;
							 | 
						|
								  args.streaming = args.streaming || args.customResponse;
							 | 
						|
								  const requestStartTime = Date.now();
							 | 
						|
								  let parsedUrl;
							 | 
						|
								
							 | 
						|
								  if (typeof url === 'string') {
							 | 
						|
								    if (!PROTO_RE.test(url)) {
							 | 
						|
								      // Support `request('www.server.com')`
							 | 
						|
								      url = 'https://' + url;
							 | 
						|
								    }
							 | 
						|
								    parsedUrl = urlutil.parse(url);
							 | 
						|
								  } else {
							 | 
						|
								    parsedUrl = url;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  const method = (args.type || args.method || parsedUrl.method || 'GET').toUpperCase();
							 | 
						|
								  let port = parsedUrl.port || 80;
							 | 
						|
								  let httplib = http;
							 | 
						|
								  let agent = getAgent(args.agent, exports.agent);
							 | 
						|
								  const fixJSONCtlChars = args.fixJSONCtlChars;
							 | 
						|
								
							 | 
						|
								  if (parsedUrl.protocol === 'https:') {
							 | 
						|
								    httplib = https;
							 | 
						|
								    agent = getAgent(args.httpsAgent, exports.httpsAgent);
							 | 
						|
								
							 | 
						|
								    if (!parsedUrl.port) {
							 | 
						|
								      port = 443;
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // request through proxy tunnel
							 | 
						|
								  // var proxyTunnelAgent = detectProxyAgent(parsedUrl, args);
							 | 
						|
								  // if (proxyTunnelAgent) {
							 | 
						|
								  //   agent = proxyTunnelAgent;
							 | 
						|
								  // }
							 | 
						|
								
							 | 
						|
								  const options = {
							 | 
						|
								    host: parsedUrl.hostname || parsedUrl.host || 'localhost',
							 | 
						|
								    path: parsedUrl.path || '/',
							 | 
						|
								    method: method,
							 | 
						|
								    port: port,
							 | 
						|
								    agent: agent,
							 | 
						|
								    headers: args.headers || {},
							 | 
						|
								    // default is dns.lookup
							 | 
						|
								    // https://github.com/nodejs/node/blob/master/lib/net.js#L986
							 | 
						|
								    // custom dnslookup require node >= 4.0.0
							 | 
						|
								    // https://github.com/nodejs/node/blob/archived-io.js-v0.12/lib/net.js#L952
							 | 
						|
								    lookup: args.lookup
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  if (Array.isArray(args.timeout)) {
							 | 
						|
								    options.requestTimeout = args.timeout[args.timeout.length - 1];
							 | 
						|
								  } else if (typeof args.timeout !== 'undefined') {
							 | 
						|
								    options.requestTimeout = args.timeout;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // const sslNames = [
							 | 
						|
								  //   'pfx',
							 | 
						|
								  //   'key',
							 | 
						|
								  //   'passphrase',
							 | 
						|
								  //   'cert',
							 | 
						|
								  //   'ca',
							 | 
						|
								  //   'ciphers',
							 | 
						|
								  //   'rejectUnauthorized',
							 | 
						|
								  //   'secureProtocol',
							 | 
						|
								  //   'secureOptions',
							 | 
						|
								  // ];
							 | 
						|
								  // for (let i = 0; i < sslNames.length; i++) {
							 | 
						|
								  //   const name = sslNames[i];
							 | 
						|
								  //   if (args.hasOwnProperty(name)) {
							 | 
						|
								  //     options[name] = args[name];
							 | 
						|
								  //   }
							 | 
						|
								  // }
							 | 
						|
								
							 | 
						|
								  // don't check ssl
							 | 
						|
								  // if (options.rejectUnauthorized === false && !options.hasOwnProperty('secureOptions')) {
							 | 
						|
								  //   options.secureOptions = require('constants').SSL_OP_NO_TLSv1_2;
							 | 
						|
								  // }
							 | 
						|
								
							 | 
						|
								  const auth = args.auth || parsedUrl.auth;
							 | 
						|
								  if (auth) {
							 | 
						|
								    options.auth = auth;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // content undefined  data 有值
							 | 
						|
								  let body = args.content || args.data;
							 | 
						|
								  const dataAsQueryString = method === 'GET' || method === 'HEAD' || args.dataAsQueryString;
							 | 
						|
								  if (!args.content) {
							 | 
						|
								    if (body && !(typeof body === 'string' || Buffer.isBuffer(body))) {
							 | 
						|
								      if (dataAsQueryString) {
							 | 
						|
								        // read: GET, HEAD, use query string
							 | 
						|
								        body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body);
							 | 
						|
								      } else {
							 | 
						|
								        let contentType = options.headers['Content-Type'] || options.headers['content-type'];
							 | 
						|
								        // auto add application/x-www-form-urlencoded when using urlencode form request
							 | 
						|
								        if (!contentType) {
							 | 
						|
								          if (args.contentType === 'json') {
							 | 
						|
								            contentType = 'application/json';
							 | 
						|
								          } else {
							 | 
						|
								            contentType = 'application/x-www-form-urlencoded';
							 | 
						|
								          }
							 | 
						|
								          options.headers['Content-Type'] = contentType;
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        if (parseContentType(contentType) === 'application/json') {
							 | 
						|
								          body = JSON.stringify(body);
							 | 
						|
								        } else {
							 | 
						|
								          // 'application/x-www-form-urlencoded'
							 | 
						|
								          body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body);
							 | 
						|
								        }
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // if it's a GET or HEAD request, data should be sent as query string
							 | 
						|
								  if (dataAsQueryString && body) {
							 | 
						|
								    options.path += (parsedUrl.query ? '&' : '?') + body;
							 | 
						|
								    body = null;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  let requestSize = 0;
							 | 
						|
								  if (body) {
							 | 
						|
								    let length = body.length;
							 | 
						|
								    if (!Buffer.isBuffer(body)) {
							 | 
						|
								      length = Buffer.byteLength(body);
							 | 
						|
								    }
							 | 
						|
								    requestSize = options.headers['Content-Length'] = length;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  if (args.dataType === 'json') {
							 | 
						|
								    options.headers.Accept = 'application/json';
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  if (typeof args.beforeRequest === 'function') {
							 | 
						|
								    // you can use this hook to change every thing.
							 | 
						|
								    args.beforeRequest(options);
							 | 
						|
								  }
							 | 
						|
								  let connectTimer = null;
							 | 
						|
								  let responseTimer = null;
							 | 
						|
								  let __err = null;
							 | 
						|
								  let connected = false; // socket connected or not
							 | 
						|
								  let keepAliveSocket = false; // request with keepalive socket
							 | 
						|
								  let responseSize = 0;
							 | 
						|
								  let statusCode = -1;
							 | 
						|
								  let responseAborted = false;
							 | 
						|
								  let remoteAddress = '';
							 | 
						|
								  let remotePort = '';
							 | 
						|
								  let timing = null;
							 | 
						|
								  if (args.timing) {
							 | 
						|
								    timing = {
							 | 
						|
								      // socket assigned
							 | 
						|
								      queuing: 0,
							 | 
						|
								      // dns lookup time
							 | 
						|
								      dnslookup: 0,
							 | 
						|
								      // socket connected
							 | 
						|
								      connected: 0,
							 | 
						|
								      // request sent
							 | 
						|
								      requestSent: 0,
							 | 
						|
								      // Time to first byte (TTFB)
							 | 
						|
								      waiting: 0,
							 | 
						|
								      contentDownload: 0
							 | 
						|
								    };
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function cancelConnectTimer() {
							 | 
						|
								    if (connectTimer) {
							 | 
						|
								      clearTimeout(connectTimer);
							 | 
						|
								      connectTimer = null;
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								  function cancelResponseTimer() {
							 | 
						|
								    if (responseTimer) {
							 | 
						|
								      clearTimeout(responseTimer);
							 | 
						|
								      responseTimer = null;
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function done(err, data, res) {
							 | 
						|
								    cancelResponseTimer();
							 | 
						|
								    if (!callback) {
							 | 
						|
								      console.warn(
							 | 
						|
								        '[urllib:warn] [%s] [%s] [worker:%s] %s %s callback twice!!!',
							 | 
						|
								        Date(),
							 | 
						|
								        reqId,
							 | 
						|
								        process.pid,
							 | 
						|
								        options.method,
							 | 
						|
								        url
							 | 
						|
								      );
							 | 
						|
								      // https://github.com/node-modules/urllib/pull/30
							 | 
						|
								      if (err) {
							 | 
						|
								        console.warn(
							 | 
						|
								          '[urllib:warn] [%s] [%s] [worker:%s] %s: %s\nstack: %s',
							 | 
						|
								          Date(),
							 | 
						|
								          reqId,
							 | 
						|
								          process.pid,
							 | 
						|
								          err.name,
							 | 
						|
								          err.message,
							 | 
						|
								          err.stack
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								    const cb = callback;
							 | 
						|
								    callback = null;
							 | 
						|
								    let headers = {};
							 | 
						|
								    if (res) {
							 | 
						|
								      statusCode = res.statusCode;
							 | 
						|
								      headers = res.headers;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // handle digest auth
							 | 
						|
								    // if (statusCode === 401 && headers['www-authenticate']
							 | 
						|
								    //   && (!args.headers || !args.headers.Authorization) && args.digestAuth) {
							 | 
						|
								    //   const authenticate = headers['www-authenticate'];
							 | 
						|
								    //   if (authenticate.indexOf('Digest ') >= 0) {
							 | 
						|
								    //     debug('Request#%d %s: got digest auth header WWW-Authenticate: %s', reqId, url, authenticate);
							 | 
						|
								    //     args.headers = args.headers || {};
							 | 
						|
								    //     args.headers.Authorization = digestAuthHeader(options.method, options.path, authenticate, args.digestAuth);
							 | 
						|
								    //     debug('Request#%d %s: auth with digest header: %s', reqId, url, args.headers.Authorization);
							 | 
						|
								    //     if (res.headers['set-cookie']) {
							 | 
						|
								    //       args.headers.Cookie = res.headers['set-cookie'].join(';');
							 | 
						|
								    //     }
							 | 
						|
								    //     return exports.requestWithCallback(url, args, cb);
							 | 
						|
								    //   }
							 | 
						|
								    // }
							 | 
						|
								
							 | 
						|
								    const requestUseTime = Date.now() - requestStartTime;
							 | 
						|
								    if (timing) {
							 | 
						|
								      timing.contentDownload = requestUseTime;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    debug(
							 | 
						|
								      '[%sms] done, %s bytes HTTP %s %s %s %s, keepAliveSocket: %s, timing: %j',
							 | 
						|
								      requestUseTime,
							 | 
						|
								      responseSize,
							 | 
						|
								      statusCode,
							 | 
						|
								      options.method,
							 | 
						|
								      options.host,
							 | 
						|
								      options.path,
							 | 
						|
								      keepAliveSocket,
							 | 
						|
								      timing
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    const response = {
							 | 
						|
								      status: statusCode,
							 | 
						|
								      statusCode: statusCode,
							 | 
						|
								      headers: headers,
							 | 
						|
								      size: responseSize,
							 | 
						|
								      aborted: responseAborted,
							 | 
						|
								      rt: requestUseTime,
							 | 
						|
								      keepAliveSocket: keepAliveSocket,
							 | 
						|
								      data: data,
							 | 
						|
								      requestUrls: args.requestUrls,
							 | 
						|
								      timing: timing,
							 | 
						|
								      remoteAddress: remoteAddress,
							 | 
						|
								      remotePort: remotePort
							 | 
						|
								    };
							 | 
						|
								
							 | 
						|
								    if (err) {
							 | 
						|
								      let agentStatus = '';
							 | 
						|
								      if (agent && typeof agent.getCurrentStatus === 'function') {
							 | 
						|
								        // add current agent status to error message for logging and debug
							 | 
						|
								        agentStatus = ', agent status: ' + JSON.stringify(agent.getCurrentStatus());
							 | 
						|
								      }
							 | 
						|
								      err.message +=
							 | 
						|
								        ', ' +
							 | 
						|
								        options.method +
							 | 
						|
								        ' ' +
							 | 
						|
								        url +
							 | 
						|
								        ' ' +
							 | 
						|
								        statusCode +
							 | 
						|
								        ' (connected: ' +
							 | 
						|
								        connected +
							 | 
						|
								        ', keepalive socket: ' +
							 | 
						|
								        keepAliveSocket +
							 | 
						|
								        agentStatus +
							 | 
						|
								        ')' +
							 | 
						|
								        '\nheaders: ' +
							 | 
						|
								        JSON.stringify(headers);
							 | 
						|
								      err.data = data;
							 | 
						|
								      err.path = options.path;
							 | 
						|
								      err.status = statusCode;
							 | 
						|
								      err.headers = headers;
							 | 
						|
								      err.res = response;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    cb(err, data, args.streaming ? res : response);
							 | 
						|
								
							 | 
						|
								    if (args.emitter) {
							 | 
						|
								      // keep to use the same reqMeta object on request event before
							 | 
						|
								      reqMeta.url = url;
							 | 
						|
								      reqMeta.socket = req && req.connection;
							 | 
						|
								      reqMeta.options = options;
							 | 
						|
								      reqMeta.size = requestSize;
							 | 
						|
								
							 | 
						|
								      args.emitter.emit('response', {
							 | 
						|
								        requestId: reqId,
							 | 
						|
								        error: err,
							 | 
						|
								        ctx: args.ctx,
							 | 
						|
								        req: reqMeta,
							 | 
						|
								        res: response
							 | 
						|
								      });
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function handleRedirect(res) {
							 | 
						|
								    let err = null;
							 | 
						|
								    if (args.followRedirect && statuses.redirect[res.statusCode]) {
							 | 
						|
								      // handle redirect
							 | 
						|
								      args._followRedirectCount = (args._followRedirectCount || 0) + 1;
							 | 
						|
								      const location = res.headers.location;
							 | 
						|
								      if (!location) {
							 | 
						|
								        err = new Error('Got statusCode ' + res.statusCode + ' but cannot resolve next location from headers');
							 | 
						|
								        err.name = 'FollowRedirectError';
							 | 
						|
								      } else if (args._followRedirectCount > args.maxRedirects) {
							 | 
						|
								        err = new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + url);
							 | 
						|
								        err.name = 'MaxRedirectError';
							 | 
						|
								      } else {
							 | 
						|
								        const newUrl = args.formatRedirectUrl ? args.formatRedirectUrl(url, location) : urlutil.resolve(url, location);
							 | 
						|
								        debug('Request#%d %s: `redirected` from %s to %s', reqId, options.path, url, newUrl);
							 | 
						|
								        // make sure timer stop
							 | 
						|
								        cancelResponseTimer();
							 | 
						|
								        // should clean up headers.Host on `location: http://other-domain/url`
							 | 
						|
								        if (args.headers && args.headers.Host && PROTO_RE.test(location)) {
							 | 
						|
								          args.headers.Host = null;
							 | 
						|
								        }
							 | 
						|
								        // avoid done will be execute in the future change.
							 | 
						|
								        const cb = callback;
							 | 
						|
								        callback = null;
							 | 
						|
								        exports.requestWithCallback(newUrl, args, cb);
							 | 
						|
								        return {
							 | 
						|
								          redirect: true,
							 | 
						|
								          error: null
							 | 
						|
								        };
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								    return {
							 | 
						|
								      redirect: false,
							 | 
						|
								      error: err
							 | 
						|
								    };
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  if (args.gzip) {
							 | 
						|
								    if (!options.headers['Accept-Encoding'] && !options.headers['accept-encoding']) {
							 | 
						|
								      options.headers['Accept-Encoding'] = 'gzip';
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function decodeContent(res, body, cb) {
							 | 
						|
								    const encoding = res.headers['content-encoding'];
							 | 
						|
								    // if (body.length === 0) {
							 | 
						|
								    //   return cb(null, body, encoding);
							 | 
						|
								    // }
							 | 
						|
								
							 | 
						|
								    // if (!encoding || encoding.toLowerCase() !== 'gzip') {
							 | 
						|
								    return cb(null, body, encoding);
							 | 
						|
								    // }
							 | 
						|
								
							 | 
						|
								    // debug('gunzip %d length body', body.length);
							 | 
						|
								    // zlib.gunzip(body, cb);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  const writeStream = args.writeStream;
							 | 
						|
								
							 | 
						|
								  debug('Request#%d %s %s with headers %j, options.path: %s', reqId, method, url, options.headers, options.path);
							 | 
						|
								
							 | 
						|
								  args.requestUrls.push(url);
							 | 
						|
								
							 | 
						|
								  function onResponse(res) {
							 | 
						|
								    if (timing) {
							 | 
						|
								      timing.waiting = Date.now() - requestStartTime;
							 | 
						|
								    }
							 | 
						|
								    debug('Request#%d %s `req response` event emit: status %d, headers: %j', reqId, url, res.statusCode, res.headers);
							 | 
						|
								
							 | 
						|
								    if (args.streaming) {
							 | 
						|
								      const result = handleRedirect(res);
							 | 
						|
								      if (result.redirect) {
							 | 
						|
								        res.resume();
							 | 
						|
								        return;
							 | 
						|
								      }
							 | 
						|
								      if (result.error) {
							 | 
						|
								        res.resume();
							 | 
						|
								        return done(result.error, null, res);
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      return done(null, null, res);
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    res.on('close', function () {
							 | 
						|
								      debug('Request#%d %s: `res close` event emit, total size %d', reqId, url, responseSize);
							 | 
						|
								    });
							 | 
						|
								
							 | 
						|
								    res.on('error', function () {
							 | 
						|
								      debug('Request#%d %s: `res error` event emit, total size %d', reqId, url, responseSize);
							 | 
						|
								    });
							 | 
						|
								
							 | 
						|
								    res.on('aborted', function () {
							 | 
						|
								      responseAborted = true;
							 | 
						|
								      debug('Request#%d %s: `res aborted` event emit, total size %d', reqId, url, responseSize);
							 | 
						|
								    });
							 | 
						|
								
							 | 
						|
								    if (writeStream) {
							 | 
						|
								      // If there's a writable stream to recieve the response data, just pipe the
							 | 
						|
								      // response stream to that writable stream and call the callback when it has
							 | 
						|
								      // finished writing.
							 | 
						|
								      //
							 | 
						|
								      // NOTE that when the response stream `res` emits an 'end' event it just
							 | 
						|
								      // means that it has finished piping data to another stream. In the
							 | 
						|
								      // meanwhile that writable stream may still writing data to the disk until
							 | 
						|
								      // it emits a 'close' event.
							 | 
						|
								      //
							 | 
						|
								      // That means that we should not apply callback until the 'close' of the
							 | 
						|
								      // writable stream is emited.
							 | 
						|
								      //
							 | 
						|
								      // See also:
							 | 
						|
								      // - https://github.com/TBEDP/urllib/commit/959ac3365821e0e028c231a5e8efca6af410eabb
							 | 
						|
								      // - http://nodejs.org/api/stream.html#stream_event_end
							 | 
						|
								      // - http://nodejs.org/api/stream.html#stream_event_close_1
							 | 
						|
								      const result = handleRedirect(res);
							 | 
						|
								      if (result.redirect) {
							 | 
						|
								        res.resume();
							 | 
						|
								        return;
							 | 
						|
								      }
							 | 
						|
								      if (result.error) {
							 | 
						|
								        res.resume();
							 | 
						|
								        // end ths stream first
							 | 
						|
								        writeStream.end();
							 | 
						|
								        return done(result.error, null, res);
							 | 
						|
								      }
							 | 
						|
								      // you can set consumeWriteStream false that only wait response end
							 | 
						|
								      if (args.consumeWriteStream === false) {
							 | 
						|
								        res.on('end', done.bind(null, null, null, res));
							 | 
						|
								      } else {
							 | 
						|
								        // node 0.10, 0.12: only emit res aborted, writeStream close not fired
							 | 
						|
								        // if (isNode010 || isNode012) {
							 | 
						|
								        //   first([
							 | 
						|
								        //     [ writeStream, 'close' ],
							 | 
						|
								        //     [ res, 'aborted' ],
							 | 
						|
								        //   ], function(_, stream, event) {
							 | 
						|
								        //     debug('Request#%d %s: writeStream or res %s event emitted', reqId, url, event);
							 | 
						|
								        //     done(__err || null, null, res);
							 | 
						|
								        //   });
							 | 
						|
								        if (false) {
							 | 
						|
								        } else {
							 | 
						|
								          writeStream.on('close', function () {
							 | 
						|
								            debug('Request#%d %s: writeStream close event emitted', reqId, url);
							 | 
						|
								            done(__err || null, null, res);
							 | 
						|
								          });
							 | 
						|
								        }
							 | 
						|
								      }
							 | 
						|
								      return res.pipe(writeStream);
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Otherwise, just concat those buffers.
							 | 
						|
								    //
							 | 
						|
								    // NOTE that the `chunk` is not a String but a Buffer. It means that if
							 | 
						|
								    // you simply concat two chunk with `+` you're actually converting both
							 | 
						|
								    // Buffers into Strings before concating them. It'll cause problems when
							 | 
						|
								    // dealing with multi-byte characters.
							 | 
						|
								    //
							 | 
						|
								    // The solution is to store each chunk in an array and concat them with
							 | 
						|
								    // 'buffer-concat' when all chunks is recieved.
							 | 
						|
								    //
							 | 
						|
								    // See also:
							 | 
						|
								    // http://cnodejs.org/topic/4faf65852e8fb5bc65113403
							 | 
						|
								
							 | 
						|
								    const chunks = [];
							 | 
						|
								
							 | 
						|
								    res.on('data', function (chunk) {
							 | 
						|
								      debug('Request#%d %s: `res data` event emit, size %d', reqId, url, chunk.length);
							 | 
						|
								      responseSize += chunk.length;
							 | 
						|
								      chunks.push(chunk);
							 | 
						|
								    });
							 | 
						|
								
							 | 
						|
								    res.on('end', function () {
							 | 
						|
								      const body = Buffer.concat(chunks, responseSize);
							 | 
						|
								      debug('Request#%d %s: `res end` event emit, total size %d, _dumped: %s', reqId, url, responseSize, res._dumped);
							 | 
						|
								
							 | 
						|
								      if (__err) {
							 | 
						|
								        // req.abort() after `res data` event emit.
							 | 
						|
								        return done(__err, body, res);
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      const result = handleRedirect(res);
							 | 
						|
								      if (result.error) {
							 | 
						|
								        return done(result.error, body, res);
							 | 
						|
								      }
							 | 
						|
								      if (result.redirect) {
							 | 
						|
								        return;
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      decodeContent(res, body, function (err, data, encoding) {
							 | 
						|
								        if (err) {
							 | 
						|
								          return done(err, body, res);
							 | 
						|
								        }
							 | 
						|
								        // if body not decode, dont touch it
							 | 
						|
								        if (!encoding && TEXT_DATA_TYPES.indexOf(args.dataType) >= 0) {
							 | 
						|
								          // try to decode charset
							 | 
						|
								          try {
							 | 
						|
								            data = decodeBodyByCharset(data, res);
							 | 
						|
								          } catch (e) {
							 | 
						|
								            debug('decodeBodyByCharset error: %s', e);
							 | 
						|
								            // if error, dont touch it
							 | 
						|
								            return done(null, data, res);
							 | 
						|
								          }
							 | 
						|
								
							 | 
						|
								          if (args.dataType === 'json') {
							 | 
						|
								            if (responseSize === 0) {
							 | 
						|
								              data = null;
							 | 
						|
								            } else {
							 | 
						|
								              const r = parseJSON(data, fixJSONCtlChars);
							 | 
						|
								              if (r.error) {
							 | 
						|
								                err = r.error;
							 | 
						|
								              } else {
							 | 
						|
								                data = r.data;
							 | 
						|
								              }
							 | 
						|
								            }
							 | 
						|
								          }
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        if (responseAborted) {
							 | 
						|
								          // err = new Error('Remote socket was terminated before `response.end()` was called');
							 | 
						|
								          // err.name = 'RemoteSocketClosedError';
							 | 
						|
								          debug('Request#%d %s: Remote socket was terminated before `response.end()` was called', reqId, url);
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        done(err, data, res);
							 | 
						|
								      });
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  let connectTimeout, responseTimeout;
							 | 
						|
								  if (Array.isArray(args.timeout)) {
							 | 
						|
								    connectTimeout = ms(args.timeout[0]);
							 | 
						|
								    responseTimeout = ms(args.timeout[1]);
							 | 
						|
								  } else {
							 | 
						|
								    // set both timeout equal
							 | 
						|
								    connectTimeout = responseTimeout = ms(args.timeout);
							 | 
						|
								  }
							 | 
						|
								  debug('ConnectTimeout: %d, ResponseTimeout: %d', connectTimeout, responseTimeout);
							 | 
						|
								
							 | 
						|
								  function startConnectTimer() {
							 | 
						|
								    debug('Connect timer ticking, timeout: %d', connectTimeout);
							 | 
						|
								    connectTimer = setTimeout(function () {
							 | 
						|
								      connectTimer = null;
							 | 
						|
								      if (statusCode === -1) {
							 | 
						|
								        statusCode = -2;
							 | 
						|
								      }
							 | 
						|
								      let msg = 'Connect timeout for ' + connectTimeout + 'ms';
							 | 
						|
								      let errorName = 'ConnectionTimeoutError';
							 | 
						|
								      if (!req.socket) {
							 | 
						|
								        errorName = 'SocketAssignTimeoutError';
							 | 
						|
								        msg += ', working sockets is full';
							 | 
						|
								      }
							 | 
						|
								      __err = new Error(msg);
							 | 
						|
								      __err.name = errorName;
							 | 
						|
								      __err.requestId = reqId;
							 | 
						|
								      debug('ConnectTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected);
							 | 
						|
								      abortRequest();
							 | 
						|
								    }, connectTimeout);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function startResposneTimer() {
							 | 
						|
								    debug('Response timer ticking, timeout: %d', responseTimeout);
							 | 
						|
								    responseTimer = setTimeout(function () {
							 | 
						|
								      responseTimer = null;
							 | 
						|
								      const msg = 'Response timeout for ' + responseTimeout + 'ms';
							 | 
						|
								      const errorName = 'ResponseTimeoutError';
							 | 
						|
								      __err = new Error(msg);
							 | 
						|
								      __err.name = errorName;
							 | 
						|
								      __err.requestId = reqId;
							 | 
						|
								      debug('ResponseTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected);
							 | 
						|
								      abortRequest();
							 | 
						|
								    }, responseTimeout);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  let req;
							 | 
						|
								  // request headers checker will throw error
							 | 
						|
								  options.mode = args.mode ? args.mode : '';
							 | 
						|
								  try {
							 | 
						|
								    req = httplib.request(options, onResponse);
							 | 
						|
								  } catch (err) {
							 | 
						|
								    return done(err);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // environment detection: browser or nodejs
							 | 
						|
								  if (typeof window === 'undefined') {
							 | 
						|
								    // start connect timer just after `request` return, and just in nodejs environment
							 | 
						|
								    startConnectTimer();
							 | 
						|
								  } else {
							 | 
						|
								    req.on('requestTimeout', function () {
							 | 
						|
								      if (statusCode === -1) {
							 | 
						|
								        statusCode = -2;
							 | 
						|
								      }
							 | 
						|
								      const msg = 'Connect timeout for ' + connectTimeout + 'ms';
							 | 
						|
								      const errorName = 'ConnectionTimeoutError';
							 | 
						|
								      __err = new Error(msg);
							 | 
						|
								      __err.name = errorName;
							 | 
						|
								      __err.requestId = reqId;
							 | 
						|
								      abortRequest();
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function abortRequest() {
							 | 
						|
								    debug('Request#%d %s abort, connected: %s', reqId, url, connected);
							 | 
						|
								    // it wont case error event when req haven't been assigned a socket yet.
							 | 
						|
								    if (!req.socket) {
							 | 
						|
								      __err.noSocket = true;
							 | 
						|
								      done(__err);
							 | 
						|
								    }
							 | 
						|
								    req.abort();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  if (timing) {
							 | 
						|
								    // request sent
							 | 
						|
								    req.on('finish', function () {
							 | 
						|
								      timing.requestSent = Date.now() - requestStartTime;
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  req.once('socket', function (socket) {
							 | 
						|
								    if (timing) {
							 | 
						|
								      // socket queuing time
							 | 
						|
								      timing.queuing = Date.now() - requestStartTime;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // https://github.com/nodejs/node/blob/master/lib/net.js#L377
							 | 
						|
								    // https://github.com/nodejs/node/blob/v0.10.40-release/lib/net.js#L352
							 | 
						|
								    // should use socket.socket on 0.10.x
							 | 
						|
								    // if (isNode010 && socket.socket) {
							 | 
						|
								    //   socket = socket.socket;
							 | 
						|
								    // }
							 | 
						|
								
							 | 
						|
								    const readyState = socket.readyState;
							 | 
						|
								    if (readyState === 'opening') {
							 | 
						|
								      socket.once('lookup', function (err, ip, addressType) {
							 | 
						|
								        debug('Request#%d %s lookup: %s, %s, %s', reqId, url, err, ip, addressType);
							 | 
						|
								        if (timing) {
							 | 
						|
								          timing.dnslookup = Date.now() - requestStartTime;
							 | 
						|
								        }
							 | 
						|
								        if (ip) {
							 | 
						|
								          remoteAddress = ip;
							 | 
						|
								        }
							 | 
						|
								      });
							 | 
						|
								      socket.once('connect', function () {
							 | 
						|
								        if (timing) {
							 | 
						|
								          // socket connected
							 | 
						|
								          timing.connected = Date.now() - requestStartTime;
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        // cancel socket timer at first and start tick for TTFB
							 | 
						|
								        cancelConnectTimer();
							 | 
						|
								        startResposneTimer();
							 | 
						|
								
							 | 
						|
								        debug('Request#%d %s new socket connected', reqId, url);
							 | 
						|
								        connected = true;
							 | 
						|
								        if (!remoteAddress) {
							 | 
						|
								          remoteAddress = socket.remoteAddress;
							 | 
						|
								        }
							 | 
						|
								        remotePort = socket.remotePort;
							 | 
						|
								      });
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    debug('Request#%d %s reuse socket connected, readyState: %s', reqId, url, readyState);
							 | 
						|
								    connected = true;
							 | 
						|
								    keepAliveSocket = true;
							 | 
						|
								    if (!remoteAddress) {
							 | 
						|
								      remoteAddress = socket.remoteAddress;
							 | 
						|
								    }
							 | 
						|
								    remotePort = socket.remotePort;
							 | 
						|
								
							 | 
						|
								    // reuse socket, timer should be canceled.
							 | 
						|
								    cancelConnectTimer();
							 | 
						|
								    startResposneTimer();
							 | 
						|
								  });
							 | 
						|
								
							 | 
						|
								  req.on('error', function (err) {
							 | 
						|
								    //TypeError for browser fetch api, Error for browser xmlhttprequest api
							 | 
						|
								    if (err.name === 'Error' || err.name === 'TypeError') {
							 | 
						|
								      err.name = connected ? 'ResponseError' : 'RequestError';
							 | 
						|
								    }
							 | 
						|
								    err.message += ' (req "error")';
							 | 
						|
								    debug('Request#%d %s `req error` event emit, %s: %s', reqId, url, err.name, err.message);
							 | 
						|
								    done(__err || err);
							 | 
						|
								  });
							 | 
						|
								
							 | 
						|
								  if (writeStream) {
							 | 
						|
								    writeStream.once('error', function (err) {
							 | 
						|
								      err.message += ' (writeStream "error")';
							 | 
						|
								      __err = err;
							 | 
						|
								      debug('Request#%d %s `writeStream error` event emit, %s: %s', reqId, url, err.name, err.message);
							 | 
						|
								      abortRequest();
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  if (args.stream) {
							 | 
						|
								    args.stream.pipe(req);
							 | 
						|
								    args.stream.once('error', function (err) {
							 | 
						|
								      err.message += ' (stream "error")';
							 | 
						|
								      __err = err;
							 | 
						|
								      debug('Request#%d %s `readStream error` event emit, %s: %s', reqId, url, err.name, err.message);
							 | 
						|
								      abortRequest();
							 | 
						|
								    });
							 | 
						|
								  } else {
							 | 
						|
								    req.end(body);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  req.requestId = reqId;
							 | 
						|
								  return req;
							 | 
						|
								};
							 |