| // Copyright Joyent, Inc. and other Node contributors. |
| // |
| // Permission is hereby granted, free of charge, to any person obtaining a |
| // copy of this software and associated documentation files (the |
| // "Software"), to deal in the Software without restriction, including |
| // without limitation the rights to use, copy, modify, merge, publish, |
| // distribute, sublicense, and/or sell copies of the Software, and to permit |
| // persons to whom the Software is furnished to do so, subject to the |
| // following conditions: |
| // |
| // The above copyright notice and this permission notice shall be included |
| // in all copies or substantial portions of the Software. |
| // |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN |
| // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, |
| // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
| // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE |
| // USE OR OTHER DEALINGS IN THE SOFTWARE. |
| |
| 'use strict'; |
| |
| const { |
| NumberIsNaN, |
| ObjectKeys, |
| ObjectSetPrototypeOf, |
| ObjectValues, |
| Symbol, |
| } = primordials; |
| |
| const net = require('net'); |
| const EventEmitter = require('events'); |
| let debug = require('internal/util/debuglog').debuglog('http', (fn) => { |
| debug = fn; |
| }); |
| const { AsyncResource } = require('async_hooks'); |
| const { async_id_symbol } = require('internal/async_hooks').symbols; |
| const { |
| codes: { |
| ERR_INVALID_ARG_TYPE, |
| ERR_INVALID_OPT_VALUE, |
| ERR_OUT_OF_RANGE, |
| }, |
| } = require('internal/errors'); |
| const { once } = require('internal/util'); |
| const { validateNumber } = require('internal/validators'); |
| |
| const kOnKeylog = Symbol('onkeylog'); |
| const kRequestOptions = Symbol('requestOptions'); |
| const kRequestAsyncResource = Symbol('requestAsyncResource'); |
| // New Agent code. |
| |
| // The largest departure from the previous implementation is that |
| // an Agent instance holds connections for a variable number of host:ports. |
| // Surprisingly, this is still API compatible as far as third parties are |
| // concerned. The only code that really notices the difference is the |
| // request object. |
| |
| // Another departure is that all code related to HTTP parsing is in |
| // ClientRequest.onSocket(). The Agent is now *strictly* |
| // concerned with managing a connection pool. |
| |
| class ReusedHandle { |
| constructor(type, handle) { |
| this.type = type; |
| this.handle = handle; |
| } |
| } |
| |
| function freeSocketErrorListener(err) { |
| const socket = this; |
| debug('SOCKET ERROR on FREE socket:', err.message, err.stack); |
| socket.destroy(); |
| socket.emit('agentRemove'); |
| } |
| |
| function Agent(options) { |
| if (!(this instanceof Agent)) |
| return new Agent(options); |
| |
| EventEmitter.call(this); |
| |
| this.defaultPort = 80; |
| this.protocol = 'http:'; |
| |
| this.options = { ...options }; |
| |
| // Don't confuse net and make it think that we're connecting to a pipe |
| this.options.path = null; |
| this.requests = {}; |
| this.sockets = {}; |
| this.freeSockets = {}; |
| this.keepAliveMsecs = this.options.keepAliveMsecs || 1000; |
| this.keepAlive = this.options.keepAlive || false; |
| this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets; |
| this.maxFreeSockets = this.options.maxFreeSockets || 256; |
| this.scheduling = this.options.scheduling || 'fifo'; |
| this.maxTotalSockets = this.options.maxTotalSockets; |
| this.totalSocketCount = 0; |
| |
| if (this.scheduling !== 'fifo' && this.scheduling !== 'lifo') { |
| throw new ERR_INVALID_OPT_VALUE('scheduling', this.scheduling); |
| } |
| |
| if (this.maxTotalSockets !== undefined) { |
| validateNumber(this.maxTotalSockets, 'maxTotalSockets'); |
| if (this.maxTotalSockets <= 0 || NumberIsNaN(this.maxTotalSockets)) |
| throw new ERR_OUT_OF_RANGE('maxTotalSockets', '> 0', |
| this.maxTotalSockets); |
| } else { |
| this.maxTotalSockets = Infinity; |
| } |
| |
| this.on('free', (socket, options) => { |
| const name = this.getName(options); |
| debug('agent.on(free)', name); |
| |
| // TODO(ronag): socket.destroy(err) might have been called |
| // before coming here and have an 'error' scheduled. In the |
| // case of socket.destroy() below this 'error' has no handler |
| // and could cause unhandled exception. |
| |
| if (!socket.writable) { |
| socket.destroy(); |
| return; |
| } |
| |
| const requests = this.requests[name]; |
| if (requests && requests.length) { |
| const req = requests.shift(); |
| const reqAsyncRes = req[kRequestAsyncResource]; |
| if (reqAsyncRes) { |
| // Run request within the original async context. |
| reqAsyncRes.runInAsyncScope(() => { |
| asyncResetHandle(socket); |
| setRequestSocket(this, req, socket); |
| }); |
| req[kRequestAsyncResource] = null; |
| } else { |
| setRequestSocket(this, req, socket); |
| } |
| if (requests.length === 0) { |
| delete this.requests[name]; |
| } |
| return; |
| } |
| |
| // If there are no pending requests, then put it in |
| // the freeSockets pool, but only if we're allowed to do so. |
| const req = socket._httpMessage; |
| if (!req || !req.shouldKeepAlive || !this.keepAlive) { |
| socket.destroy(); |
| return; |
| } |
| |
| const freeSockets = this.freeSockets[name] || []; |
| const freeLen = freeSockets.length; |
| let count = freeLen; |
| if (this.sockets[name]) |
| count += this.sockets[name].length; |
| |
| if (this.totalSocketCount > this.maxTotalSockets || |
| count > this.maxSockets || |
| freeLen >= this.maxFreeSockets || |
| !this.keepSocketAlive(socket)) { |
| socket.destroy(); |
| return; |
| } |
| |
| this.freeSockets[name] = freeSockets; |
| socket[async_id_symbol] = -1; |
| socket._httpMessage = null; |
| this.removeSocket(socket, options); |
| |
| socket.once('error', freeSocketErrorListener); |
| freeSockets.push(socket); |
| }); |
| |
| // Don't emit keylog events unless there is a listener for them. |
| this.on('newListener', maybeEnableKeylog); |
| } |
| ObjectSetPrototypeOf(Agent.prototype, EventEmitter.prototype); |
| ObjectSetPrototypeOf(Agent, EventEmitter); |
| |
| function maybeEnableKeylog(eventName) { |
| if (eventName === 'keylog') { |
| this.removeListener('newListener', maybeEnableKeylog); |
| // Future sockets will listen on keylog at creation. |
| const agent = this; |
| this[kOnKeylog] = function onkeylog(keylog) { |
| agent.emit('keylog', keylog, this); |
| }; |
| // Existing sockets will start listening on keylog now. |
| for (const socket of ObjectValues(this.sockets)) { |
| socket.on('keylog', this[kOnKeylog]); |
| } |
| } |
| } |
| |
| Agent.defaultMaxSockets = Infinity; |
| |
| Agent.prototype.createConnection = net.createConnection; |
| |
| // Get the key for a given set of request options |
| Agent.prototype.getName = function getName(options) { |
| let name = options.host || 'localhost'; |
| |
| name += ':'; |
| if (options.port) |
| name += options.port; |
| |
| name += ':'; |
| if (options.localAddress) |
| name += options.localAddress; |
| |
| // Pacify parallel/test-http-agent-getname by only appending |
| // the ':' when options.family is set. |
| if (options.family === 4 || options.family === 6) |
| name += `:${options.family}`; |
| |
| if (options.socketPath) |
| name += `:${options.socketPath}`; |
| |
| return name; |
| }; |
| |
| Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, |
| localAddress/* legacy */) { |
| // Legacy API: addRequest(req, host, port, localAddress) |
| if (typeof options === 'string') { |
| options = { |
| host: options, |
| port, |
| localAddress |
| }; |
| } |
| |
| options = { ...options, ...this.options }; |
| if (options.socketPath) |
| options.path = options.socketPath; |
| |
| if (!options.servername && options.servername !== '') |
| options.servername = calculateServerName(options, req); |
| |
| const name = this.getName(options); |
| if (!this.sockets[name]) { |
| this.sockets[name] = []; |
| } |
| |
| const freeSockets = this.freeSockets[name]; |
| let socket; |
| if (freeSockets) { |
| while (freeSockets.length && freeSockets[0].destroyed) { |
| freeSockets.shift(); |
| } |
| socket = this.scheduling === 'fifo' ? |
| freeSockets.shift() : |
| freeSockets.pop(); |
| if (!freeSockets.length) |
| delete this.freeSockets[name]; |
| } |
| |
| const freeLen = freeSockets ? freeSockets.length : 0; |
| const sockLen = freeLen + this.sockets[name].length; |
| |
| if (socket) { |
| asyncResetHandle(socket); |
| this.reuseSocket(socket, req); |
| setRequestSocket(this, req, socket); |
| this.sockets[name].push(socket); |
| this.totalSocketCount++; |
| } else if (sockLen < this.maxSockets && |
| this.totalSocketCount < this.maxTotalSockets) { |
| debug('call onSocket', sockLen, freeLen); |
| // If we are under maxSockets create a new one. |
| this.createSocket(req, options, (err, socket) => { |
| if (err) |
| req.onSocket(socket, err); |
| else |
| setRequestSocket(this, req, socket); |
| }); |
| } else { |
| debug('wait for socket'); |
| // We are over limit so we'll add it to the queue. |
| if (!this.requests[name]) { |
| this.requests[name] = []; |
| } |
| |
| // Used to create sockets for pending requests from different origin |
| req[kRequestOptions] = options; |
| // Used to capture the original async context. |
| req[kRequestAsyncResource] = new AsyncResource('QueuedRequest'); |
| |
| this.requests[name].push(req); |
| } |
| }; |
| |
| Agent.prototype.createSocket = function createSocket(req, options, cb) { |
| options = { ...options, ...this.options }; |
| if (options.socketPath) |
| options.path = options.socketPath; |
| |
| if (!options.servername && options.servername !== '') |
| options.servername = calculateServerName(options, req); |
| |
| const name = this.getName(options); |
| options._agentKey = name; |
| |
| debug('createConnection', name, options); |
| options.encoding = null; |
| |
| const oncreate = once((err, s) => { |
| if (err) |
| return cb(err); |
| if (!this.sockets[name]) { |
| this.sockets[name] = []; |
| } |
| this.sockets[name].push(s); |
| this.totalSocketCount++; |
| debug('sockets', name, this.sockets[name].length, this.totalSocketCount); |
| installListeners(this, s, options); |
| cb(null, s); |
| }); |
| |
| const newSocket = this.createConnection(options, oncreate); |
| if (newSocket) |
| oncreate(null, newSocket); |
| }; |
| |
| function calculateServerName(options, req) { |
| let servername = options.host; |
| const hostHeader = req.getHeader('host'); |
| if (hostHeader) { |
| if (typeof hostHeader !== 'string') { |
| throw new ERR_INVALID_ARG_TYPE('options.headers.host', |
| 'String', hostHeader); |
| } |
| |
| // abc => abc |
| // abc:123 => abc |
| // [::1] => ::1 |
| // [::1]:123 => ::1 |
| if (hostHeader.startsWith('[')) { |
| const index = hostHeader.indexOf(']'); |
| if (index === -1) { |
| // Leading '[', but no ']'. Need to do something... |
| servername = hostHeader; |
| } else { |
| servername = hostHeader.substr(1, index - 1); |
| } |
| } else { |
| servername = hostHeader.split(':', 1)[0]; |
| } |
| } |
| // Don't implicitly set invalid (IP) servernames. |
| if (net.isIP(servername)) |
| servername = ''; |
| return servername; |
| } |
| |
| function installListeners(agent, s, options) { |
| function onFree() { |
| debug('CLIENT socket onFree'); |
| agent.emit('free', s, options); |
| } |
| s.on('free', onFree); |
| |
| function onClose(err) { |
| debug('CLIENT socket onClose'); |
| // This is the only place where sockets get removed from the Agent. |
| // If you want to remove a socket from the pool, just close it. |
| // All socket errors end in a close event anyway. |
| agent.removeSocket(s, options); |
| } |
| s.on('close', onClose); |
| |
| function onTimeout() { |
| debug('CLIENT socket onTimeout'); |
| |
| // Destroy if in free list. |
| // TODO(ronag): Always destroy, even if not in free list. |
| const sockets = agent.freeSockets; |
| for (const name of ObjectKeys(sockets)) { |
| if (sockets[name].includes(s)) { |
| return s.destroy(); |
| } |
| } |
| } |
| s.on('timeout', onTimeout); |
| |
| function onRemove() { |
| // We need this function for cases like HTTP 'upgrade' |
| // (defined by WebSockets) where we need to remove a socket from the |
| // pool because it'll be locked up indefinitely |
| debug('CLIENT socket onRemove'); |
| agent.removeSocket(s, options); |
| s.removeListener('close', onClose); |
| s.removeListener('free', onFree); |
| s.removeListener('timeout', onTimeout); |
| s.removeListener('agentRemove', onRemove); |
| } |
| s.on('agentRemove', onRemove); |
| |
| if (agent[kOnKeylog]) { |
| s.on('keylog', agent[kOnKeylog]); |
| } |
| } |
| |
| Agent.prototype.removeSocket = function removeSocket(s, options) { |
| const name = this.getName(options); |
| debug('removeSocket', name, 'writable:', s.writable); |
| const sets = [this.sockets]; |
| |
| // If the socket was destroyed, remove it from the free buffers too. |
| if (!s.writable) |
| sets.push(this.freeSockets); |
| |
| for (const sockets of sets) { |
| if (sockets[name]) { |
| const index = sockets[name].indexOf(s); |
| if (index !== -1) { |
| sockets[name].splice(index, 1); |
| // Don't leak |
| if (sockets[name].length === 0) |
| delete sockets[name]; |
| this.totalSocketCount--; |
| } |
| } |
| } |
| |
| let req; |
| if (this.requests[name] && this.requests[name].length) { |
| debug('removeSocket, have a request, make a socket'); |
| req = this.requests[name][0]; |
| } else { |
| // TODO(rickyes): this logic will not be FIFO across origins. |
| // There might be older requests in a different origin, but |
| // if the origin which releases the socket has pending requests |
| // that will be prioritized. |
| for (const prop in this.requests) { |
| // Check whether this specific origin is already at maxSockets |
| if (this.sockets[prop] && this.sockets[prop].length) break; |
| debug('removeSocket, have a request with different origin,' + |
| ' make a socket'); |
| req = this.requests[prop][0]; |
| options = req[kRequestOptions]; |
| break; |
| } |
| } |
| |
| if (req && options) { |
| req[kRequestOptions] = undefined; |
| // If we have pending requests and a socket gets closed make a new one |
| this.createSocket(req, options, (err, socket) => { |
| if (err) |
| req.onSocket(socket, err); |
| else |
| socket.emit('free'); |
| }); |
| } |
| |
| }; |
| |
| Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { |
| socket.setKeepAlive(true, this.keepAliveMsecs); |
| socket.unref(); |
| |
| const agentTimeout = this.options.timeout || 0; |
| if (socket.timeout !== agentTimeout) { |
| socket.setTimeout(agentTimeout); |
| } |
| |
| return true; |
| }; |
| |
| Agent.prototype.reuseSocket = function reuseSocket(socket, req) { |
| debug('have free socket'); |
| socket.removeListener('error', freeSocketErrorListener); |
| req.reusedSocket = true; |
| socket.ref(); |
| }; |
| |
| Agent.prototype.destroy = function destroy() { |
| for (const set of [this.freeSockets, this.sockets]) { |
| for (const key of ObjectKeys(set)) { |
| for (const setName of set[key]) { |
| setName.destroy(); |
| } |
| } |
| } |
| }; |
| |
| function setRequestSocket(agent, req, socket) { |
| req.onSocket(socket); |
| const agentTimeout = agent.options.timeout || 0; |
| if (req.timeout === undefined || req.timeout === agentTimeout) { |
| return; |
| } |
| socket.setTimeout(req.timeout); |
| } |
| |
| function asyncResetHandle(socket) { |
| // Guard against an uninitialized or user supplied Socket. |
| const handle = socket._handle; |
| if (handle && typeof handle.asyncReset === 'function') { |
| // Assign the handle a new asyncId and run any destroy()/init() hooks. |
| handle.asyncReset(new ReusedHandle(handle.getProviderType(), handle)); |
| socket[async_id_symbol] = handle.getAsyncId(); |
| } |
| } |
| |
| module.exports = { |
| Agent, |
| globalAgent: new Agent() |
| }; |