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.
		
		
		
		
		
			
		
			
				
					1318 lines
				
				42 KiB
			
		
		
			
		
	
	
					1318 lines
				
				42 KiB
			| 
											3 years ago
										 | 'use strict'; | ||
|  | 
 | ||
|  | var debug = require('debug')('urllib'); | ||
|  | var path = require('path'); | ||
|  | var dns = require('dns'); | ||
|  | var http = require('http'); | ||
|  | var https = require('https'); | ||
|  | var urlutil = require('url'); | ||
|  | var URL = urlutil.URL; | ||
|  | var util = require('util'); | ||
|  | var qs = require('qs'); | ||
|  | var ip = require('ip'); | ||
|  | var querystring = require('querystring'); | ||
|  | var zlib = require('zlib'); | ||
|  | var ua = require('default-user-agent'); | ||
|  | var digestAuthHeader = require('digest-header'); | ||
|  | var ms = require('humanize-ms'); | ||
|  | var statuses = require('statuses'); | ||
|  | var contentTypeParser = require('content-type'); | ||
|  | var first = require('ee-first'); | ||
|  | var pump = require('pump'); | ||
|  | var utility = require('utility'); | ||
|  | var FormStream = require('formstream'); | ||
|  | var detectProxyAgent = require('./detect_proxy_agent'); | ||
|  | 
 | ||
|  | var _Promise; | ||
|  | var _iconv; | ||
|  | 
 | ||
|  | var pkg = require('../package.json'); | ||
|  | 
 | ||
|  | var USER_AGENT = exports.USER_AGENT = ua('node-urllib', pkg.version); | ||
|  | var NODE_MAJOR_VERSION = parseInt(process.versions.node.split('.')[0]); | ||
|  | 
 | ||
|  | // change Agent.maxSockets to 1000
 | ||
|  | exports.agent = new http.Agent(); | ||
|  | exports.agent.maxSockets = 1000; | ||
|  | 
 | ||
|  | exports.httpsAgent = new https.Agent(); | ||
|  | exports.httpsAgent.maxSockets = 1000; | ||
|  | 
 | ||
|  | var LONG_STACK_DELIMITER = '\n    --------------------\n'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * The default request timeout(in milliseconds). | ||
|  |  * @type {Number} | ||
|  |  * @const | ||
|  |  */ | ||
|  | 
 | ||
|  | exports.TIMEOUT = ms('5s'); | ||
|  | exports.TIMEOUTS = [ms('5s'), ms('5s')]; | ||
|  | 
 | ||
|  | var REQUEST_ID = 0; | ||
|  | var MAX_VALUE = Math.pow(2, 31) - 10; | ||
|  | var isNode010 = /^v0\.10\.\d+$/.test(process.version); | ||
|  | var isNode012 = /^v0\.12\.\d+$/.test(process.version); | ||
|  | 
 | ||
|  | /** | ||
|  |  * support data types | ||
|  |  * will auto decode response body | ||
|  |  * @type {Array} | ||
|  |  */ | ||
|  | var TEXT_DATA_TYPES = [ | ||
|  |   'json', | ||
|  |   'text' | ||
|  | ]; | ||
|  | 
 | ||
|  | var PROTO_RE = /^https?:\/\//i; | ||
|  | 
 | ||
|  | // Keep-Alive: timeout=5, max=100
 | ||
|  | var KEEP_ALIVE_RE = /^timeout=(\d+)/i; | ||
|  | 
 | ||
|  | var SOCKET_REQUEST_COUNT = '_URLLIB_SOCKET_REQUEST_COUNT'; | ||
|  | var SOCKET_RESPONSE_COUNT = '_URLLIB_SOCKET_RESPONSE_COUNT'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Handle all http request, both http and https support well. | ||
|  |  * | ||
|  |  * @example | ||
|  |  * | ||
|  |  * ```js
 | ||
|  |  * // GET https://nodejs.org
 | ||
|  |  * urllib.request('https://nodejs.org', function(err, data, res) {}); | ||
|  |  * // POST https://nodejs.org
 | ||
|  |  * var args = { type: 'post', data: { foo: 'bar' } }; | ||
|  |  * urllib.request('https://nodejs.org', args, function(err, data, res) {}); | ||
|  |  * ```
 | ||
|  |  * | ||
|  |  * @param {String|Object} url: the request full URL. | ||
|  |  * @param {Object} [args]: optional | ||
|  |  *   - {Object} [data]: request data, will auto be query stringify. | ||
|  |  *   - {Boolean} [dataAsQueryString]: force convert `data` to query string. | ||
|  |  *   - {String|Buffer} [content]: optional, if set content, `data` will ignore. | ||
|  |  *   - {ReadStream} [stream]: read stream to sent. | ||
|  |  *   - {WriteStream} [writeStream]: writable stream to save response data. | ||
|  |  *       If you use this, callback's data should be null. | ||
|  |  *       We will just `pipe(ws, {end: true})`. | ||
|  |  *   - {consumeWriteStream} [true]: consume the writeStream, invoke the callback after writeStream close. | ||
|  |  *   - {Array<ReadStream|Buffer|String>|Object|ReadStream|Buffer|String} [files]: optional, | ||
|  |  *       The files will send with `multipart/form-data` format, base on `formstream`. | ||
|  |  *       If `method` not set, will use `POST` method by default. | ||
|  |  *   - {String} [method]: optional, could be GET | POST | DELETE | PUT, default is GET | ||
|  |  *   - {String} [contentType]: optional, request data type, could be `json`, default is undefined | ||
|  |  *   - {String} [dataType]: optional, response data type, could be `text` or `json`, default is buffer | ||
|  |  *   - {Boolean|Function} [fixJSONCtlChars]: optional, fix the control characters (U+0000 through U+001F) | ||
|  |  *       before JSON parse response. Default is `false`. | ||
|  |  *       `fixJSONCtlChars` can be a function, will pass data to the first argument. e.g.: `data = fixJSONCtlChars(data)` | ||
|  |  *   - {Object} [headers]: optional, request headers | ||
|  |  *   - {Boolean} [keepHeaderCase]: optional, by default will convert header keys to lowercase | ||
|  |  *   - {Number|Array} [timeout]: request timeout(in milliseconds), default is `exports.TIMEOUTS containing connect timeout and response timeout` | ||
|  |  *   - {Agent} [agent]: optional, http agent. Set `false` if you does not use agent. | ||
|  |  *   - {Agent} [httpsAgent]: optional, https agent. Set `false` if you does not use agent. | ||
|  |  *   - {String} [auth]: Basic authentication i.e. 'user:password' to compute an Authorization header. | ||
|  |  *   - {String} [digestAuth]: Digest authentication i.e. 'user:password' to compute an Authorization header. | ||
|  |  *   - {String|Buffer|Array} [ca]: An array of strings or Buffers of trusted certificates. | ||
|  |  *       If this is omitted several well known "root" CAs will be used, like VeriSign. | ||
|  |  *       These are used to authorize connections. | ||
|  |  *       Notes: This is necessary only if the server uses the self-signed certificate | ||
|  |  *   - {Boolean} [rejectUnauthorized]: If true, the server certificate is verified against the list of supplied CAs. | ||
|  |  *       An 'error' event is emitted if verification fails. Default: true. | ||
|  |  *   - {String|Buffer} [pfx]: A string or Buffer containing the private key, | ||
|  |  *       certificate and CA certs of the server in PFX or PKCS12 format. | ||
|  |  *   - {String|Buffer} [key]: A string or Buffer containing the private key of the client in PEM format. | ||
|  |  *       Notes: This is necessary only if using the client certificate authentication | ||
|  |  *   - {String|Buffer} [cert]: A string or Buffer containing the certificate key of the client in PEM format. | ||
|  |  *       Notes: This is necessary only if using the client certificate authentication | ||
|  |  *   - {String} [passphrase]: A string of passphrase for the private key or pfx. | ||
|  |  *   - {String} [ciphers]: A string describing the ciphers to use or exclude. | ||
|  |  *   - {String} [secureProtocol]: The SSL method to use, e.g. SSLv3_method to force SSL version 3. | ||
|  |  *       The possible values depend on your installation of OpenSSL and are defined in the constant SSL_METHODS. | ||
|  |  *   - {Boolean} [followRedirect]: Follow HTTP 3xx responses as redirects. defaults to false. | ||
|  |  *   - {Number} [maxRedirects]: The maximum number of redirects to follow, defaults to 10. | ||
|  |  *   - {Function(from, to)} [formatRedirectUrl]: Format the redirect url by your self. Default is `url.resolve(from, to)` | ||
|  |  *   - {Function(options)} [beforeRequest]: Before request hook, you can change every thing here. | ||
|  |  *   - {Boolean} [streaming]: let you get the res object when request connected, default is `false`. alias `customResponse` | ||
|  |  *   - {Boolean} [gzip]: Accept gzip response content and auto decode it, default is `false`. | ||
|  |  *   - {Boolean} [timing]: Enable timing or not, default is `false`. | ||
|  |  *   - {Function} [lookup]: Custom DNS lookup function, default is `dns.lookup`. | ||
|  |  *       Require node >= 4.0.0 and only work on `http` protocol. | ||
|  |  *   - {Boolean} [enableProxy]: optional, enable proxy request. Default is `false`. | ||
|  |  *   - {String|Object} [proxy]: optional proxy agent uri or options. Default is `null`. | ||
|  |  *   - {String} [socketPath]: optional, unix domain socket file path. | ||
|  |  *   - {Function} checkAddress: optional, check request address to protect from SSRF and similar attacks. | ||
|  |  * @param {Function} [callback]: callback(error, data, res). If missing callback, will return a promise object. | ||
|  |  * @return {HttpRequest} req object. | ||
|  |  * @api public | ||
|  |  */ | ||
|  | 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); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Promise
 | ||
|  |   if (!_Promise) { | ||
|  |     _Promise = require('any-promise'); | ||
|  |   } | ||
|  |   return new _Promise(function (resolve, reject) { | ||
|  |     exports.requestWithCallback(url, args, makeCallback(resolve, reject)); | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | // alias to curl
 | ||
|  | exports.curl = exports.request; | ||
|  | 
 | ||
|  | 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 | ||
|  |     }); | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | // yield urllib.requestThunk(url, args)
 | ||
|  | exports.requestThunk = function requestThunk(url, args) { | ||
|  |   return function (callback) { | ||
|  |     exports.requestWithCallback(url, args, function (err, data, res) { | ||
|  |       if (err) { | ||
|  |         return callback(err); | ||
|  |       } | ||
|  |       callback(null, { | ||
|  |         data: data, | ||
|  |         status: res.statusCode, | ||
|  |         headers: res.headers, | ||
|  |         res: res | ||
|  |       }); | ||
|  |     }); | ||
|  |   }; | ||
|  | }; | ||
|  | 
 | ||
|  | function requestWithCallback(url, args, callback) { | ||
|  |   var req; | ||
|  |   // requestWithCallback(url, callback)
 | ||
|  |   if (!url || (typeof url !== 'string' && typeof url !== 'object')) { | ||
|  |     var 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; | ||
|  |   } | ||
|  |   var reqId = ++REQUEST_ID; | ||
|  | 
 | ||
|  |   args.requestUrls = args.requestUrls || []; | ||
|  | 
 | ||
|  |   args.timeout = args.timeout || exports.TIMEOUTS; | ||
|  |   args.maxRedirects = args.maxRedirects || 10; | ||
|  |   args.streaming = args.streaming || args.customResponse; | ||
|  |   var requestStartTime = Date.now(); | ||
|  |   var parsedUrl; | ||
|  | 
 | ||
|  |   if (typeof url === 'string') { | ||
|  |     if (!PROTO_RE.test(url)) { | ||
|  |       // Support `request('www.server.com')`
 | ||
|  |       url = 'http://' + url; | ||
|  |     } | ||
|  |     if (URL) { | ||
|  |       parsedUrl = urlutil.parse(new URL(url).href); | ||
|  |     } else { | ||
|  |       parsedUrl = urlutil.parse(url); | ||
|  |     } | ||
|  |   } else { | ||
|  |     parsedUrl = url; | ||
|  |   } | ||
|  | 
 | ||
|  |   var reqMeta = { | ||
|  |     requestId: reqId, | ||
|  |     url: parsedUrl.href, | ||
|  |     args: args, | ||
|  |     ctx: args.ctx, | ||
|  |   }; | ||
|  |   if (args.emitter) { | ||
|  |     args.emitter.emit('request', reqMeta); | ||
|  |   } | ||
|  | 
 | ||
|  |   var method = (args.type || args.method || parsedUrl.method || 'GET').toUpperCase(); | ||
|  |   var port = parsedUrl.port || 80; | ||
|  |   var httplib = http; | ||
|  |   var agent = getAgent(args.agent, exports.agent); | ||
|  |   var 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; | ||
|  |   } | ||
|  | 
 | ||
|  |   var lookup = args.lookup; | ||
|  |   // check address to protect from SSRF and similar attacks
 | ||
|  |   if (args.checkAddress) { | ||
|  |     var _lookup = lookup || dns.lookup; | ||
|  |     lookup = function(host, dnsopts, callback) { | ||
|  |       _lookup(host, dnsopts, function emitLookup(err, ip, family) { | ||
|  |         // add check address logic in custom dns lookup
 | ||
|  |         if (!err && !args.checkAddress(ip, family)) { | ||
|  |           err = new Error('illegal address'); | ||
|  |           err.name = 'IllegalAddressError'; | ||
|  |           err.hostname = host; | ||
|  |           err.ip = ip; | ||
|  |           err.family = family; | ||
|  |         } | ||
|  |         callback(err, ip, family); | ||
|  |       }); | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   var requestSize = 0; | ||
|  |   var options = { | ||
|  |     host: parsedUrl.hostname || parsedUrl.host || 'localhost', | ||
|  |     path: parsedUrl.path || '/', | ||
|  |     method: method, | ||
|  |     port: port, | ||
|  |     agent: agent, | ||
|  |     headers: {}, | ||
|  |     // default is dns.lookup
 | ||
|  |     // https://github.com/nodejs/node/blob/master/lib/net.js#L986
 | ||
|  |     // custom dnslookup require node >= 4.0.0 (for http), node >=8 (for https)
 | ||
|  |     // https://github.com/nodejs/node/blob/archived-io.js-v0.12/lib/net.js#L952
 | ||
|  |     lookup: lookup, | ||
|  |   }; | ||
|  | 
 | ||
|  |   var originHeaderKeys = {}; | ||
|  |   if (args.headers) { | ||
|  |     // only allow enumerable and ownProperty value of args.headers
 | ||
|  |     var names = utility.getOwnEnumerables(args.headers, true); | ||
|  |     for (var i = 0; i < names.length; i++) { | ||
|  |       var name = names[i]; | ||
|  |       var key = name.toLowerCase(); | ||
|  |       if (key !== name) { | ||
|  |         originHeaderKeys[key] = name; | ||
|  |       } | ||
|  |       options.headers[key] = args.headers[name]; | ||
|  |     } | ||
|  |   } | ||
|  |   if (args.socketPath) { | ||
|  |     options.socketPath = args.socketPath; | ||
|  |   } | ||
|  | 
 | ||
|  |   var sslNames = [ | ||
|  |     'pfx', | ||
|  |     'key', | ||
|  |     'passphrase', | ||
|  |     'cert', | ||
|  |     'ca', | ||
|  |     'ciphers', | ||
|  |     'rejectUnauthorized', | ||
|  |     'secureProtocol', | ||
|  |     'secureOptions', | ||
|  |   ]; | ||
|  |   for (var i = 0; i < sslNames.length; i++) { | ||
|  |     var name = sslNames[i]; | ||
|  |     if (args.hasOwnProperty(name)) { | ||
|  |       options[name] = args[name]; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // fix rejectUnauthorized when major version < 12
 | ||
|  |   if (NODE_MAJOR_VERSION < 12) { | ||
|  |     if (options.rejectUnauthorized === false && !options.hasOwnProperty('secureOptions')) { | ||
|  |       options.secureOptions = require('constants').SSL_OP_NO_TLSv1_2; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   var auth = args.auth || parsedUrl.auth; | ||
|  |   if (auth) { | ||
|  |     options.auth = auth; | ||
|  |   } | ||
|  | 
 | ||
|  |   var body = null; | ||
|  |   var dataAsQueryString = false; | ||
|  | 
 | ||
|  |   if (args.files) { | ||
|  |     if (!options.method || options.method === 'GET' || options.method === 'HEAD') { | ||
|  |       options.method = 'POST'; | ||
|  |     } | ||
|  |     var files = args.files; | ||
|  |     var uploadFiles = []; | ||
|  |     if (Array.isArray(files)) { | ||
|  |       for (var i = 0; i < files.length; i++) { | ||
|  |         var field = 'file' + (i === 0 ? '' : i); | ||
|  |         uploadFiles.push([ field, files[i] ]); | ||
|  |       } | ||
|  |     } else { | ||
|  |       if (Buffer.isBuffer(files) || typeof files.pipe === 'function' || typeof files === 'string') { | ||
|  |         uploadFiles.push([ 'file', files ]); | ||
|  |       } else if (typeof files === 'object') { | ||
|  |         for (var field in files) { | ||
|  |           uploadFiles.push([ field, files[field] ]); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |     var form = new FormStream(); | ||
|  |     // set normal fields first
 | ||
|  |     if (args.data) { | ||
|  |       for (var fieldName in args.data) { | ||
|  |         form.field(fieldName, args.data[fieldName]); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     for (var i = 0; i < uploadFiles.length; i++) { | ||
|  |       var item = uploadFiles[i]; | ||
|  |       if (Buffer.isBuffer(item[1])) { | ||
|  |         form.buffer(item[0], item[1], 'bufferfile' + i); | ||
|  |       } else if (typeof item[1].pipe === 'function') { | ||
|  |         var filename = item[1].path || ('streamfile' + i); | ||
|  |         filename = path.basename(filename); | ||
|  |         form.stream(item[0], item[1], filename); | ||
|  |       } else { | ||
|  |         form.file(item[0], item[1]); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     var formHeaders = form.headers(); | ||
|  |     var formHeaderNames = utility.getOwnEnumerables(formHeaders, true); | ||
|  |     for (var i = 0; i < formHeaderNames.length; i++) { | ||
|  |       var name = formHeaderNames[i]; | ||
|  |       options.headers[name.toLowerCase()] = formHeaders[name]; | ||
|  |     } | ||
|  |     debug('set multipart headers: %j, method: %s', formHeaders, options.method); | ||
|  |     args.stream = form; | ||
|  |   } else { | ||
|  |     body = args.content || args.data; | ||
|  |     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 { | ||
|  |           var contentType = 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).type === 'application/json') { | ||
|  |             body = JSON.stringify(body); | ||
|  |           } else { | ||
|  |             // 'application/x-www-form-urlencoded'
 | ||
|  |             body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body); | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (body) { | ||
|  |     // if it's a GET or HEAD request, data should be sent as query string
 | ||
|  |     if (dataAsQueryString) { | ||
|  |       options.path += (parsedUrl.query ? '&' : '?') + body; | ||
|  |       body = null; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (body) { | ||
|  |       var length = body.length; | ||
|  |       if (!Buffer.isBuffer(body)) { | ||
|  |         length = Buffer.byteLength(body); | ||
|  |       } | ||
|  |       requestSize = length; | ||
|  | 
 | ||
|  |       options.headers['content-length'] = length.toString(); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (args.dataType === 'json') { | ||
|  |     if (!options.headers.accept) { | ||
|  |       options.headers.accept = 'application/json'; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (typeof args.beforeRequest === 'function') { | ||
|  |     // you can use this hook to change every thing.
 | ||
|  |     args.beforeRequest(options); | ||
|  |   } | ||
|  | 
 | ||
|  |   var connectTimer = null; | ||
|  |   var responseTimer = null; | ||
|  |   var __err = null; | ||
|  |   var connected = false; // socket connected or not
 | ||
|  |   var keepAliveSocket = false; // request with keepalive socket
 | ||
|  |   var socketHandledRequests = 0; // socket already handled request count
 | ||
|  |   var socketHandledResponses = 0; // socket already handled response count
 | ||
|  |   var responseSize = 0; | ||
|  |   var statusCode = -1; | ||
|  |   var statusMessage = null; | ||
|  |   var responseAborted = false; | ||
|  |   var remoteAddress = ''; | ||
|  |   var remotePort = ''; | ||
|  |   var 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; | ||
|  |       debug('Request#%d connect timer canceled', reqId); | ||
|  |     } | ||
|  |   } | ||
|  |   function cancelResponseTimer() { | ||
|  |     if (responseTimer) { | ||
|  |       clearTimeout(responseTimer); | ||
|  |       responseTimer = null; | ||
|  |       debug('Request#%d response timer canceled', reqId); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function done(err, data, res) { | ||
|  |     cancelConnectTimer(); | ||
|  |     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; | ||
|  |     } | ||
|  | 
 | ||
|  |     var cb = callback; | ||
|  |     callback = null; | ||
|  |     var headers = {}; | ||
|  |     if (res) { | ||
|  |       statusCode = res.statusCode; | ||
|  |       statusMessage = res.statusMessage; | ||
|  |       headers = res.headers; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (handleDigestAuth(res, cb)) { | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     var response = createCallbackResponse(data, res); | ||
|  | 
 | ||
|  |     debug('[%sms] done, %s bytes HTTP %s %s %s %s, keepAliveSocket: %s, timing: %j, socketHandledRequests: %s, socketHandledResponses: %s', | ||
|  |       response.requestUseTime, responseSize, statusCode, options.method, options.host, options.path, | ||
|  |       keepAliveSocket, timing, socketHandledRequests, socketHandledResponses); | ||
|  | 
 | ||
|  |     if (err) { | ||
|  |       var 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 | ||
|  |         + ', socketHandledRequests: ' + socketHandledRequests | ||
|  |         + ', socketHandledResponses: ' + socketHandledResponses + ')' | ||
|  |         + '\nheaders: ' + JSON.stringify(headers); | ||
|  |       err.data = data; | ||
|  |       err.path = options.path; | ||
|  |       err.status = statusCode; | ||
|  |       err.headers = headers; | ||
|  |       err.res = response; | ||
|  |       addLongStackTrace(err, req); | ||
|  |     } | ||
|  | 
 | ||
|  |     // only support agentkeepalive module for now
 | ||
|  |     // agentkeepalive@4: agent.options.freeSocketTimeout
 | ||
|  |     // agentkeepalive@3: agent.freeSocketKeepAliveTimeout
 | ||
|  |     var freeSocketTimeout = agent && (agent.options && agent.options.freeSocketTimeout || agent.freeSocketKeepAliveTimeout); | ||
|  |     if (agent && agent.keepAlive && freeSocketTimeout > 0 && | ||
|  |         statusCode >= 200 && headers.connection === 'keep-alive' && headers['keep-alive']) { | ||
|  |       // adjust freeSocketTimeout on the socket
 | ||
|  |       var m = KEEP_ALIVE_RE.exec(headers['keep-alive']); | ||
|  |       if (m) { | ||
|  |         var seconds = parseInt(m[1]); | ||
|  |         if (seconds > 0) { | ||
|  |           // network delay 500ms
 | ||
|  |           var serverSocketTimeout = seconds * 1000 - 500; | ||
|  |           if (serverSocketTimeout < freeSocketTimeout) { | ||
|  |             // https://github.com/node-modules/agentkeepalive/blob/master/lib/agent.js#L127
 | ||
|  |             // agentkeepalive@4
 | ||
|  |             var socket = res.socket || (req && req.socket); | ||
|  |             if (agent.options && agent.options.freeSocketTimeout) { | ||
|  |               socket.freeSocketTimeout = serverSocketTimeout; | ||
|  |             } else { | ||
|  |               socket.freeSocketKeepAliveTimeout = serverSocketTimeout; | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     cb(err, data, args.streaming ? res : response); | ||
|  | 
 | ||
|  |     emitResponseEvent(err, response); | ||
|  |   } | ||
|  | 
 | ||
|  |   function createAndEmitResponseEvent(data, res) { | ||
|  |     var response = createCallbackResponse(data, res); | ||
|  |     emitResponseEvent(null, response); | ||
|  |   } | ||
|  | 
 | ||
|  |   function createCallbackResponse(data, res) { | ||
|  |     var requestUseTime = Date.now() - requestStartTime; | ||
|  |     if (timing) { | ||
|  |       timing.contentDownload = requestUseTime; | ||
|  |     } | ||
|  | 
 | ||
|  |     var headers = res && res.headers || {}; | ||
|  |     var resStatusCode = res && res.statusCode || statusCode; | ||
|  |     var resStatusMessage = res && res.statusMessage || statusMessage; | ||
|  | 
 | ||
|  |     return { | ||
|  |       status: resStatusCode, | ||
|  |       statusCode: resStatusCode, | ||
|  |       statusMessage: resStatusMessage, | ||
|  |       headers: headers, | ||
|  |       size: responseSize, | ||
|  |       aborted: responseAborted, | ||
|  |       rt: requestUseTime, | ||
|  |       keepAliveSocket: keepAliveSocket, | ||
|  |       data: data, | ||
|  |       requestUrls: args.requestUrls, | ||
|  |       timing: timing, | ||
|  |       remoteAddress: remoteAddress, | ||
|  |       remotePort: remotePort, | ||
|  |       socketHandledRequests: socketHandledRequests, | ||
|  |       socketHandledResponses: socketHandledResponses, | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   function emitResponseEvent(err, response) { | ||
|  |     if (args.emitter) { | ||
|  |       // keep to use the same reqMeta object on request event before
 | ||
|  |       reqMeta.url = parsedUrl.href; | ||
|  |       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 handleDigestAuth(res, cb) { | ||
|  |     var headers = {}; | ||
|  |     if (res && res.headers) { | ||
|  |       headers = res.headers; | ||
|  |     } | ||
|  |     // handle digest auth
 | ||
|  |     if (statusCode === 401 && headers['www-authenticate'] | ||
|  |         && !options.headers.authorization && args.digestAuth) { | ||
|  |       var authenticate = headers['www-authenticate']; | ||
|  |       if (authenticate.indexOf('Digest ') >= 0) { | ||
|  |         debug('Request#%d %s: got digest auth header WWW-Authenticate: %s', reqId, url, authenticate); | ||
|  |         options.headers.authorization = digestAuthHeader(options.method, options.path, authenticate, args.digestAuth); | ||
|  |         debug('Request#%d %s: auth with digest header: %s', reqId, url, options.headers.authorization); | ||
|  |         if (res.headers['set-cookie']) { | ||
|  |           options.headers.cookie = res.headers['set-cookie'].join(';'); | ||
|  |         } | ||
|  |         args.headers = options.headers; | ||
|  |         exports.requestWithCallback(url, args, cb); | ||
|  |         return true; | ||
|  |       } | ||
|  |     } | ||
|  |     return false; | ||
|  |   } | ||
|  | 
 | ||
|  |   function handleRedirect(res) { | ||
|  |     var err = null; | ||
|  |     if (args.followRedirect && statuses.redirect[res.statusCode]) {  // handle redirect
 | ||
|  |       args._followRedirectCount = (args._followRedirectCount || 0) + 1; | ||
|  |       var 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 { | ||
|  |         var 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 (options.headers.host && PROTO_RE.test(location)) { | ||
|  |           options.headers.host = null; | ||
|  |           args.headers = options.headers; | ||
|  |         } | ||
|  |         // avoid done will be execute in the future change.
 | ||
|  |         var cb = callback; | ||
|  |         callback = null; | ||
|  |         exports.requestWithCallback(newUrl, args, cb); | ||
|  |         return { | ||
|  |           redirect: true, | ||
|  |           error: null | ||
|  |         }; | ||
|  |       } | ||
|  |     } | ||
|  |     return { | ||
|  |       redirect: false, | ||
|  |       error: err | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   // don't set user-agent
 | ||
|  |   if (args.headers && (args.headers['User-Agent'] === null || args.headers['user-agent'] === null)) { | ||
|  |     if (options.headers['user-agent']) { | ||
|  |       delete options.headers['user-agent']; | ||
|  |     } | ||
|  |   } else { | ||
|  |     // need to set user-agent
 | ||
|  |     var hasAgentHeader = options.headers['user-agent']; | ||
|  |     if (!hasAgentHeader) { | ||
|  |       options.headers['user-agent'] = USER_AGENT; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (args.gzip) { | ||
|  |     var isAcceptEncodingNull = (args.headers && (args.headers['Accept-Encoding'] === null || args.headers['accept-encoding'] === null)); | ||
|  |     if (!isAcceptEncodingNull) { | ||
|  |       var hasAcceptEncodingHeader = options.headers['accept-encoding']; | ||
|  |       if (!hasAcceptEncodingHeader) { | ||
|  |         options.headers['accept-encoding'] = 'gzip, deflate'; | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function decodeContent(res, body, cb) { | ||
|  |     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); | ||
|  |       var err = responseError || new Error('Remote socket was terminated before `response.end()` was called'); | ||
|  |       return cb(err); | ||
|  |     } | ||
|  |     var encoding = res.headers['content-encoding']; | ||
|  |     if (body.length === 0 || !encoding) { | ||
|  |       return cb(null, body, encoding); | ||
|  |     } | ||
|  | 
 | ||
|  |     encoding = encoding.toLowerCase(); | ||
|  |     switch (encoding) { | ||
|  |       case 'gzip': | ||
|  |       case 'deflate': | ||
|  |         debug('unzip %d length body', body.length); | ||
|  |         zlib.unzip(body, function(err, data) { | ||
|  |           if (err && err.name === 'Error') { | ||
|  |             err.name = 'UnzipError'; | ||
|  |           } | ||
|  |           cb(err, data); | ||
|  |         }); | ||
|  |         break; | ||
|  |       default: | ||
|  |         cb(null, body, encoding); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   var writeStream = args.writeStream; | ||
|  |   var isWriteStreamClose = false; | ||
|  | 
 | ||
|  |   debug('Request#%d %s %s with headers %j, options.path: %s', | ||
|  |     reqId, method, url, options.headers, options.path); | ||
|  | 
 | ||
|  |   args.requestUrls.push(parsedUrl.href); | ||
|  | 
 | ||
|  |   var hasResponse = false; | ||
|  |   var responseError; | ||
|  |   function onResponse(res) { | ||
|  |     hasResponse = true; | ||
|  |     socketHandledResponses = res.socket[SOCKET_RESPONSE_COUNT] = (res.socket[SOCKET_RESPONSE_COUNT] || 0) + 1; | ||
|  |     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) { | ||
|  |       var result = handleRedirect(res); | ||
|  |       if (result.redirect) { | ||
|  |         res.resume(); | ||
|  |         createAndEmitResponseEvent(null, res); | ||
|  |         return; | ||
|  |       } | ||
|  |       if (result.error) { | ||
|  |         res.resume(); | ||
|  |         return done(result.error, null, res); | ||
|  |       } | ||
|  | 
 | ||
|  |       return done(null, null, res); | ||
|  |     } | ||
|  | 
 | ||
|  |     res.on('error', function (err) { | ||
|  |       responseError = err; | ||
|  |       debug('Request#%d %s: `res error` event emit, total size %d, socket handled %s requests and %s responses', | ||
|  |         reqId, url, responseSize, socketHandledRequests, socketHandledResponses); | ||
|  |     }); | ||
|  | 
 | ||
|  |     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
 | ||
|  |       var result = handleRedirect(res); | ||
|  |       if (result.redirect) { | ||
|  |         res.resume(); | ||
|  |         createAndEmitResponseEvent(null, res); | ||
|  |         return; | ||
|  |       } | ||
|  |       if (result.error) { | ||
|  |         res.resume(); | ||
|  |         // end ths stream first
 | ||
|  |         writeStream.end(); | ||
|  |         done(result.error, null, res); | ||
|  |         return; | ||
|  |       } | ||
|  | 
 | ||
|  |       // you can set consumeWriteStream false that only wait response end
 | ||
|  |       if (args.consumeWriteStream === false) { | ||
|  |         res.on('end', done.bind(null, null, null, res)); | ||
|  |         pump(res, writeStream, function(err) { | ||
|  |           if (isWriteStreamClose) { | ||
|  |             return; | ||
|  |           } | ||
|  |           isWriteStreamClose = true; | ||
|  |           debug('Request#%d %s: writeStream close, error: %s', reqId, url, err); | ||
|  |         }); | ||
|  |         return; | ||
|  |       } | ||
|  | 
 | ||
|  |       // 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); | ||
|  |         }); | ||
|  |         res.pipe(writeStream); | ||
|  |         return; | ||
|  |       } | ||
|  | 
 | ||
|  |       debug('Request#%d %s: pump res to writeStream', reqId, url); | ||
|  |       pump(res, writeStream, function(err) { | ||
|  |         debug('Request#%d %s: writeStream close event emitted, error: %s, isWriteStreamClose: %s', | ||
|  |           reqId, url, err, isWriteStreamClose); | ||
|  |         if (isWriteStreamClose) { | ||
|  |           return; | ||
|  |         } | ||
|  |         isWriteStreamClose = true; | ||
|  |         done(__err || err, null, res); | ||
|  |       }); | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     // 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
 | ||
|  | 
 | ||
|  |     var 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); | ||
|  |     }); | ||
|  | 
 | ||
|  |     var isEmitted = false; | ||
|  |     function handleResponseCloseAndEnd(event) { | ||
|  |       debug('Request#%d %s: `res %s` event emit, total size %d, socket handled %s requests and %s responses', | ||
|  |         reqId, url, event, responseSize, socketHandledRequests, socketHandledResponses); | ||
|  |       if (isEmitted) { | ||
|  |         return; | ||
|  |       } | ||
|  |       isEmitted = true; | ||
|  | 
 | ||
|  |       var body = Buffer.concat(chunks, responseSize); | ||
|  |       debug('Request#%d %s: _dumped: %s', | ||
|  |         reqId, url, res._dumped); | ||
|  | 
 | ||
|  |       if (__err) { | ||
|  |         // req.abort() after `res data` event emit.
 | ||
|  |         return done(__err, body, res); | ||
|  |       } | ||
|  | 
 | ||
|  |       var result = handleRedirect(res); | ||
|  |       if (result.error) { | ||
|  |         return done(result.error, body, res); | ||
|  |       } | ||
|  |       if (result.redirect) { | ||
|  |         createAndEmitResponseEvent(null, res); | ||
|  |         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 { | ||
|  |               var r = parseJSON(data, fixJSONCtlChars); | ||
|  |               if (r.error) { | ||
|  |                 err = r.error; | ||
|  |               } else { | ||
|  |                 data = r.data; | ||
|  |               } | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  | 
 | ||
|  |         done(err, data, res); | ||
|  |       }); | ||
|  |     } | ||
|  | 
 | ||
|  |     // node >= 14 only emit close if req abort
 | ||
|  |     res.on('close', function () { | ||
|  |       handleResponseCloseAndEnd('close'); | ||
|  |     }); | ||
|  |     res.on('end', function () { | ||
|  |       handleResponseCloseAndEnd('end'); | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   var 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; | ||
|  |       } | ||
|  |       var msg = 'Connect timeout for ' + connectTimeout + 'ms'; | ||
|  |       var 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; | ||
|  |       var msg = 'Response timeout for ' + responseTimeout + 'ms'; | ||
|  |       var 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); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (args.checkAddress) { | ||
|  |     var hostname = parsedUrl.hostname; | ||
|  |     // if request hostname is ip, custom lookup wont excute
 | ||
|  |     var family = null; | ||
|  |     if (ip.isV4Format(hostname)) { | ||
|  |       family = 4; | ||
|  |     } else if (ip.isV6Format(hostname)) { | ||
|  |       family = 6; | ||
|  |     } | ||
|  |     if (family) { | ||
|  |       if (!args.checkAddress(hostname, family)) { | ||
|  |         var err = new Error('illegal address'); | ||
|  |         err.name = 'IllegalAddressError'; | ||
|  |         err.hostname = hostname; | ||
|  |         err.ip = hostname; | ||
|  |         err.family = family; | ||
|  |         return done(err); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // request headers checker will throw error
 | ||
|  |   try { | ||
|  |     var finalOptions = options; | ||
|  | 
 | ||
|  |     // restore origin header key
 | ||
|  |     if (args.keepHeaderCase) { | ||
|  |       var originKeys = Object.keys(originHeaderKeys); | ||
|  |       if (originKeys.length) { | ||
|  |         var finalHeaders = {}; | ||
|  |         var names = utility.getOwnEnumerables(options.headers, true); | ||
|  |         for (var i = 0; i < names.length; i++) { | ||
|  |           var name = names[i]; | ||
|  |           finalHeaders[originHeaderKeys[name] || name] = options.headers[name]; | ||
|  |         } | ||
|  | 
 | ||
|  |         finalOptions = Object.assign({}, options); | ||
|  |         finalOptions.headers = finalHeaders; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     req = httplib.request(finalOptions, onResponse); | ||
|  |     if (args.trace) { | ||
|  |       req._callSite = {}; | ||
|  |       Error.captureStackTrace(req._callSite, requestWithCallback); | ||
|  |     } | ||
|  |   } 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(); | ||
|  |   } | ||
|  | 
 | ||
|  |   var isRequestAborted = false; | ||
|  |   function abortRequest() { | ||
|  |     if (isRequestAborted) { | ||
|  |       return; | ||
|  |     } | ||
|  |     isRequestAborted = true; | ||
|  | 
 | ||
|  |     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; | ||
|  |     } | ||
|  | 
 | ||
|  |     var orginalSocketTimeout = getSocketTimeout(socket); | ||
|  |     if (orginalSocketTimeout && orginalSocketTimeout < responseTimeout) { | ||
|  |       // make sure socket live longer than the response timer
 | ||
|  |       var socketTimeout = responseTimeout + 500; | ||
|  |       debug('Request#%d socket.timeout(%s) < responseTimeout(%s), reset socket timeout to %s', | ||
|  |         reqId, orginalSocketTimeout, responseTimeout, socketTimeout); | ||
|  |       socket.setTimeout(socketTimeout); | ||
|  |     } | ||
|  | 
 | ||
|  |     socketHandledRequests = socket[SOCKET_REQUEST_COUNT] = (socket[SOCKET_REQUEST_COUNT] || 0) + 1; | ||
|  |     if (socket[SOCKET_RESPONSE_COUNT]) { | ||
|  |       socketHandledResponses = socket[SOCKET_RESPONSE_COUNT]; | ||
|  |     } | ||
|  | 
 | ||
|  |     var 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(); | ||
|  |   }); | ||
|  | 
 | ||
|  |   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(); | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   var isRequestDone = false; | ||
|  |   function handleRequestError(err) { | ||
|  |     if (!err) { | ||
|  |       return; | ||
|  |     } | ||
|  |     // only ignore request error if response has been received
 | ||
|  |     // if response has not received, socket error will emit on req
 | ||
|  |     if (isRequestDone && hasResponse) { | ||
|  |       return; | ||
|  |     } | ||
|  |     isRequestDone = true; | ||
|  | 
 | ||
|  |     if (err.name === 'Error') { | ||
|  |       err.name = connected ? 'ResponseError' : 'RequestError'; | ||
|  |     } | ||
|  |     debug('Request#%d %s `req error` event emit, %s: %s', reqId, url, err.name, err.message); | ||
|  |     done(__err || err); | ||
|  |   } | ||
|  |   if (args.stream) { | ||
|  |     debug('Request#%d pump args.stream to req', reqId); | ||
|  |     pump(args.stream, req, handleRequestError); | ||
|  |   } else { | ||
|  |     req.end(body, function () { | ||
|  |       isRequestDone = true; | ||
|  |     }); | ||
|  |   } | ||
|  |   // when stream already consumed, req's `finish` event is emitted and pump will ignore error after pipe finished
 | ||
|  |   // but if server response timeout later, we will abort the request and emit an error in req
 | ||
|  |   // so we must always manually listen to req's `error` event here to ensure this error is handled
 | ||
|  |   req.on('error', handleRequestError); | ||
|  |   req.requestId = reqId; | ||
|  |   return req; | ||
|  | } | ||
|  | 
 | ||
|  | exports.requestWithCallback = requestWithCallback; | ||
|  | 
 | ||
|  | var JSONCtlCharsMap = { | ||
|  |   '"': '\\"',       // \u0022
 | ||
|  |   '\\': '\\\\',     // \u005c
 | ||
|  |   '\b': '\\b',      // \u0008
 | ||
|  |   '\f': '\\f',      // \u000c
 | ||
|  |   '\n': '\\n',      // \u000a
 | ||
|  |   '\r': '\\r',      // \u000d
 | ||
|  |   '\t': '\\t'       // \u0009
 | ||
|  | }; | ||
|  | var JSONCtlCharsRE = /[\u0000-\u001F\u005C]/g; | ||
|  | 
 | ||
|  | function _replaceOneChar(c) { | ||
|  |   return JSONCtlCharsMap[c] || '\\u' + (c.charCodeAt(0) + 0x10000).toString(16).substr(1); | ||
|  | } | ||
|  | 
 | ||
|  | function replaceJSONCtlChars(str) { | ||
|  |   return str.replace(JSONCtlCharsRE, _replaceOneChar); | ||
|  | } | ||
|  | 
 | ||
|  | function parseJSON(data, fixJSONCtlChars) { | ||
|  |   var result = { | ||
|  |     error: null, | ||
|  |     data: null | ||
|  |   }; | ||
|  |   if (fixJSONCtlChars) { | ||
|  |     if (typeof fixJSONCtlChars === 'function') { | ||
|  |       data = fixJSONCtlChars(data); | ||
|  |     } else { | ||
|  |       // https://github.com/node-modules/urllib/pull/77
 | ||
|  |       // remote the control characters (U+0000 through U+001F)
 | ||
|  |       data = replaceJSONCtlChars(data); | ||
|  |     } | ||
|  |   } | ||
|  |   try { | ||
|  |     result.data = JSON.parse(data); | ||
|  |   } catch (err) { | ||
|  |     if (err.name === 'SyntaxError') { | ||
|  |       err.name = 'JSONResponseFormatError'; | ||
|  |     } | ||
|  |     if (data.length > 1024) { | ||
|  |       // show 0~512 ... -512~end data
 | ||
|  |       err.message += ' (data json format: ' + | ||
|  |         JSON.stringify(data.slice(0, 512)) + ' ...skip... ' + JSON.stringify(data.slice(data.length - 512)) + ')'; | ||
|  |     } else { | ||
|  |       err.message += ' (data json format: ' + JSON.stringify(data) + ')'; | ||
|  |     } | ||
|  |     result.error = err; | ||
|  |   } | ||
|  |   return result; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * decode response body by parse `content-type`'s charset | ||
|  |  * @param {Buffer} data | ||
|  |  * @param {Http(s)Response} res | ||
|  |  * @return {String} | ||
|  |  */ | ||
|  | function decodeBodyByCharset(data, res) { | ||
|  |   var type = res.headers['content-type']; | ||
|  |   if (!type) { | ||
|  |     return data.toString(); | ||
|  |   } | ||
|  | 
 | ||
|  |   var type = parseContentType(type); | ||
|  |   var charset = type.parameters.charset || 'utf-8'; | ||
|  | 
 | ||
|  |   if (!Buffer.isEncoding(charset)) { | ||
|  |     if (!_iconv) { | ||
|  |       _iconv = require('iconv-lite'); | ||
|  |     } | ||
|  |     return _iconv.decode(data, charset); | ||
|  |   } | ||
|  | 
 | ||
|  |   return data.toString(charset); | ||
|  | } | ||
|  | 
 | ||
|  | function getAgent(agent, defaultAgent) { | ||
|  |   return agent === undefined ? defaultAgent : agent; | ||
|  | } | ||
|  | 
 | ||
|  | function parseContentType(str) { | ||
|  |   try { | ||
|  |     return contentTypeParser.parse(str); | ||
|  |   } catch (err) { | ||
|  |     // ignore content-type error, tread as default
 | ||
|  |     return { parameters: {} }; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function addLongStackTrace(err, req) { | ||
|  |   if (!req) { | ||
|  |     return; | ||
|  |   } | ||
|  |   var callSiteStack = req._callSite && req._callSite.stack; | ||
|  |   if (!callSiteStack || typeof callSiteStack !== 'string') { | ||
|  |     return; | ||
|  |   } | ||
|  |   if (err._longStack) { | ||
|  |     return; | ||
|  |   } | ||
|  |   var index = callSiteStack.indexOf('\n'); | ||
|  |   if (index !== -1) { | ||
|  |     err._longStack = true; | ||
|  |     err.stack += LONG_STACK_DELIMITER + callSiteStack.substr(index + 1); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | // node 8 don't has timeout attribute on socket
 | ||
|  | // https://github.com/nodejs/node/pull/21204/files#diff-e6ef024c3775d787c38487a6309e491dR408
 | ||
|  | function getSocketTimeout(socket) { | ||
|  |   return socket.timeout || socket._idleTimeout; | ||
|  | } |