├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── http.js ├── https.js ├── index.js ├── license ├── package.json ├── readme.md └── test ├── server.js ├── test-http.js ├── test-https.js └── test-keepalive.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 4, 10 | "newcap": true, 11 | "noarg": true, 12 | "quotmark": "single", 13 | "regexp": true, 14 | "undef": true, 15 | "unused": true, 16 | "strict": true, 17 | "trailing": true, 18 | "smarttabs": true 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '0.10' 5 | - '0.12' 6 | - 'iojs' 7 | -------------------------------------------------------------------------------- /http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var net = require('net'); 4 | var util = require('util'); 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | var debug; 8 | 9 | if (util.debuglog) { 10 | debug = util.debuglog('http'); 11 | } else { 12 | debug = function (x) { 13 | if (process.env.NODE_DEBUG && /http/.test(process.env.NODE_DEBUG)) { 14 | console.error('HTTP: %s', x); 15 | } 16 | }; 17 | } 18 | 19 | // New Agent code. 20 | 21 | // The largest departure from the previous implementation is that 22 | // an Agent instance holds connections for a variable number of host:ports. 23 | // Surprisingly, this is still API compatible as far as third parties are 24 | // concerned. The only code that really notices the difference is the 25 | // request object. 26 | 27 | // Another departure is that all code related to HTTP parsing is in 28 | // ClientRequest.onSocket(). The Agent is now *strictly* 29 | // concerned with managing a connection pool. 30 | 31 | function Agent(options) { 32 | if (!(this instanceof Agent)) 33 | return new Agent(options); 34 | 35 | EventEmitter.call(this); 36 | 37 | var self = this; 38 | 39 | self.defaultPort = 80; 40 | self.protocol = 'http:'; 41 | 42 | self.options = util._extend({}, options); 43 | 44 | // don't confuse net and make it think that we're connecting to a pipe 45 | self.options.path = null; 46 | self.requests = {}; 47 | self.sockets = {}; 48 | self.freeSockets = {}; 49 | self.keepAliveMsecs = self.options.keepAliveMsecs || 1000; 50 | self.keepAlive = self.options.keepAlive || false; 51 | self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets; 52 | self.maxFreeSockets = self.options.maxFreeSockets || 256; 53 | 54 | self.on('free', function(socket, options) { 55 | var name = self.getName(options); 56 | debug('agent.on(free)', name); 57 | 58 | if (!socket.destroyed && 59 | self.requests[name] && self.requests[name].length) { 60 | self.requests[name].shift().onSocket(socket); 61 | if (self.requests[name].length === 0) { 62 | // don't leak 63 | delete self.requests[name]; 64 | } 65 | } else { 66 | // If there are no pending requests, then put it in 67 | // the freeSockets pool, but only if we're allowed to do so. 68 | var req = socket._httpMessage; 69 | if (req && 70 | req.shouldKeepAlive && 71 | !socket.destroyed && 72 | self.options.keepAlive) { 73 | var freeSockets = self.freeSockets[name]; 74 | var freeLen = freeSockets ? freeSockets.length : 0; 75 | var count = freeLen; 76 | if (self.sockets[name]) 77 | count += self.sockets[name].length; 78 | 79 | if (count >= self.maxSockets || freeLen >= self.maxFreeSockets) { 80 | self.removeSocket(socket, options); 81 | socket.destroy(); 82 | } else { 83 | freeSockets = freeSockets || []; 84 | self.freeSockets[name] = freeSockets; 85 | socket.setKeepAlive(true, self.keepAliveMsecs); 86 | socket.unref(); 87 | socket._httpMessage = null; 88 | self.removeSocket(socket, options); 89 | freeSockets.push(socket); 90 | } 91 | } else { 92 | self.removeSocket(socket, options); 93 | socket.destroy(); 94 | } 95 | } 96 | }); 97 | } 98 | 99 | util.inherits(Agent, EventEmitter); 100 | exports.Agent = Agent; 101 | 102 | Agent.defaultMaxSockets = Infinity; 103 | 104 | Agent.prototype.createConnection = net.createConnection; 105 | 106 | // Get the key for a given set of request options 107 | Agent.prototype.getName = function(options) { 108 | var name = ''; 109 | 110 | if (options.host) 111 | name += options.host; 112 | else 113 | name += 'localhost'; 114 | 115 | name += ':'; 116 | if (options.port) 117 | name += options.port; 118 | name += ':'; 119 | if (options.localAddress) 120 | name += options.localAddress; 121 | name += ':'; 122 | return name; 123 | }; 124 | 125 | Agent.prototype.addRequest = function(req, options) { 126 | // Legacy API: addRequest(req, host, port, path) 127 | if (typeof options === 'string') { 128 | options = { 129 | host: options, 130 | port: arguments[2], 131 | path: arguments[3] 132 | }; 133 | } 134 | 135 | // If we are not keepAlive agent and maxSockets is Infinity 136 | // then disable shouldKeepAlive 137 | if (!this.keepAlive && !Number.isFinite(this.maxSockets)) { 138 | req._last = true; 139 | req.shouldKeepAlive = false; 140 | } 141 | 142 | var name = this.getName(options); 143 | if (!this.sockets[name]) { 144 | this.sockets[name] = []; 145 | } 146 | 147 | var freeLen = this.freeSockets[name] ? this.freeSockets[name].length : 0; 148 | var sockLen = freeLen + this.sockets[name].length; 149 | 150 | if (freeLen) { 151 | // we have a free socket, so use that. 152 | var socket = this.freeSockets[name].shift(); 153 | debug('have free socket'); 154 | 155 | // don't leak 156 | if (!this.freeSockets[name].length) 157 | delete this.freeSockets[name]; 158 | 159 | socket.ref(); 160 | req.onSocket(socket); 161 | this.sockets[name].push(socket); 162 | } else if (sockLen < this.maxSockets) { 163 | debug('call onSocket', sockLen, freeLen); 164 | // If we are under maxSockets create a new one. 165 | req.onSocket(this.createSocket(req, options)); 166 | } else { 167 | debug('wait for socket'); 168 | // We are over limit so we'll add it to the queue. 169 | if (!this.requests[name]) { 170 | this.requests[name] = []; 171 | } 172 | this.requests[name].push(req); 173 | } 174 | }; 175 | 176 | Agent.prototype.createSocket = function(req, options) { 177 | var self = this; 178 | options = util._extend({}, options); 179 | options = util._extend(options, self.options); 180 | 181 | if (!options.servername) { 182 | options.servername = options.host; 183 | if (req) { 184 | var hostHeader = req.getHeader('host'); 185 | if (hostHeader) { 186 | options.servername = hostHeader.replace(/:.*$/, ''); 187 | } 188 | } 189 | } 190 | 191 | var name = self.getName(options); 192 | 193 | debug('createConnection', name, options); 194 | options.encoding = null; 195 | var s = self.createConnection(options); 196 | if (!self.sockets[name]) { 197 | self.sockets[name] = []; 198 | } 199 | this.sockets[name].push(s); 200 | debug('sockets', name, this.sockets[name].length); 201 | 202 | function onFree() { 203 | self.emit('free', s, options); 204 | } 205 | s.on('free', onFree); 206 | 207 | function onClose(err) { 208 | debug('CLIENT socket onClose'); 209 | // This is the only place where sockets get removed from the Agent. 210 | // If you want to remove a socket from the pool, just close it. 211 | // All socket errors end in a close event anyway. 212 | self.removeSocket(s, options); 213 | } 214 | s.on('close', onClose); 215 | 216 | function onRemove() { 217 | // We need this function for cases like HTTP 'upgrade' 218 | // (defined by WebSockets) where we need to remove a socket from the 219 | // pool because it'll be locked up indefinitely 220 | debug('CLIENT socket onRemove'); 221 | self.removeSocket(s, options); 222 | s.removeListener('close', onClose); 223 | s.removeListener('free', onFree); 224 | s.removeListener('agentRemove', onRemove); 225 | } 226 | s.on('agentRemove', onRemove); 227 | return s; 228 | }; 229 | 230 | Agent.prototype.removeSocket = function(s, options) { 231 | var name = this.getName(options); 232 | debug('removeSocket', name, 'destroyed:', s.destroyed); 233 | var sets = [this.sockets]; 234 | 235 | // If the socket was destroyed, remove it from the free buffers too. 236 | if (s.destroyed) 237 | sets.push(this.freeSockets); 238 | 239 | for (var sk = 0; sk < sets.length; sk++) { 240 | var sockets = sets[sk]; 241 | 242 | if (sockets[name]) { 243 | var index = sockets[name].indexOf(s); 244 | if (index !== -1) { 245 | sockets[name].splice(index, 1); 246 | // Don't leak 247 | if (sockets[name].length === 0) 248 | delete sockets[name]; 249 | } 250 | } 251 | } 252 | 253 | if (this.requests[name] && this.requests[name].length) { 254 | debug('removeSocket, have a request, make a socket'); 255 | var req = this.requests[name][0]; 256 | // If we have pending requests and a socket gets closed make a new one 257 | this.createSocket(req, options).emit('free'); 258 | } 259 | }; 260 | 261 | Agent.prototype.destroy = function() { 262 | var sets = [this.freeSockets, this.sockets]; 263 | for (var s = 0; s < sets.length; s++) { 264 | var set = sets[s]; 265 | var keys = Object.keys(set); 266 | for (var v = 0; v < keys.length; v++) { 267 | var setName = set[keys[v]]; 268 | for (var n = 0; n < setName.length; n++) { 269 | setName[n].destroy(); 270 | } 271 | } 272 | } 273 | }; 274 | 275 | exports.globalAgent = new Agent(); 276 | -------------------------------------------------------------------------------- /https.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tls = require('tls'); 4 | var http = require('./http.js'); 5 | var util = require('util'); 6 | var inherits = util.inherits; 7 | 8 | var debug; 9 | 10 | if (util.debuglog) { 11 | debug = util.debuglog('https'); 12 | } else { 13 | debug = function (x) { 14 | if (process.env.NODE_DEBUG && /http/.test(process.env.NODE_DEBUG)) { 15 | console.error('HTTPS: %s', x); 16 | } 17 | }; 18 | } 19 | function createConnection(port, host, options) { 20 | if (port !== null && typeof port === 'object') { 21 | options = port; 22 | } else if (host !== null && typeof host === 'object') { 23 | options = host; 24 | } else if (options === null || typeof options !== 'object') { 25 | options = {}; 26 | } 27 | 28 | if (typeof port === 'number') { 29 | options.port = port; 30 | } 31 | 32 | if (typeof host === 'string') { 33 | options.host = host; 34 | } 35 | 36 | debug('createConnection', options); 37 | return tls.connect(options); 38 | } 39 | 40 | 41 | function Agent(options) { 42 | http.Agent.call(this, options); 43 | this.defaultPort = 443; 44 | this.protocol = 'https:'; 45 | } 46 | inherits(Agent, http.Agent); 47 | Agent.prototype.createConnection = createConnection; 48 | 49 | Agent.prototype.getName = function(options) { 50 | var name = http.Agent.prototype.getName.call(this, options); 51 | 52 | name += ':'; 53 | if (options.ca) 54 | name += options.ca; 55 | 56 | name += ':'; 57 | if (options.cert) 58 | name += options.cert; 59 | 60 | name += ':'; 61 | if (options.ciphers) 62 | name += options.ciphers; 63 | 64 | name += ':'; 65 | if (options.key) 66 | name += options.key; 67 | 68 | name += ':'; 69 | if (options.pfx) 70 | name += options.pfx; 71 | 72 | name += ':'; 73 | if (options.rejectUnauthorized !== undefined) 74 | name += options.rejectUnauthorized; 75 | 76 | return name; 77 | }; 78 | 79 | var globalAgent = new Agent(); 80 | 81 | exports.globalAgent = globalAgent; 82 | exports.Agent = Agent; 83 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.http = require('./http.js'); 4 | exports.https = require('./https.js'); 5 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Vsevolod Strukchinsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infinity-agent", 3 | "version": "2.0.3", 4 | "description": "Creates HTTP/HTTPS Agent with Infinity maxSockets", 5 | "scripts": { 6 | "test": "tape test/test-*.js | tap-dot" 7 | }, 8 | "repository": "floatdrop/infinity-agent", 9 | "keywords": [ 10 | "http", 11 | "https", 12 | "agent", 13 | "maxSockets" 14 | ], 15 | "author": "Vsevolod Strukchinsky ", 16 | "license": "MIT", 17 | "files": [ 18 | "index.js", 19 | "http.js", 20 | "https.js" 21 | ], 22 | "devDependencies": { 23 | "mocha": "^2.1.0", 24 | "pem": "^1.7.2", 25 | "tap-dot": "^1.0.0", 26 | "tape": "^4.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # infinity-agent [![Build Status](https://travis-ci.org/floatdrop/infinity-agent.svg?branch=master)](https://travis-ci.org/floatdrop/infinity-agent) 2 | 3 | Node-core HTTP Agent for userland. 4 | 5 | ## Usage 6 | 7 | ```js 8 | var infinityAgent = require('infinity-agent'); 9 | 10 | var http = require('http'); 11 | var https = require('https'); 12 | 13 | http.get('http://google.com', {agent: infinityAgent.http.globalAgent}); 14 | https.get('http://google.com', {agent: infinityAgent.https.globalAgent}); 15 | ``` 16 | 17 | This package is a mirror of the Agent class in Node-core. 18 | 19 | There is one minor change in [addRequest](https://github.com/floatdrop/infinity-agent/blob/master/http.js#L135-L140) method: basically we disable keepAlive if agent is not configured for it, and `maxSockets` is set to `Infinity`. 20 | 21 | ## License 22 | 23 | MIT © [Vsevolod Strukchinsky](floatdrop@gmail.com) 24 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var http = require('http'); 3 | var https = require('https'); 4 | 5 | exports.port = 6767; 6 | exports.portSSL = 16167; 7 | 8 | exports.createServer = function (port) { 9 | port = port || exports.port; 10 | 11 | var s = http.createServer(function (req, resp) { 12 | s.emit(req.url, req, resp); 13 | }); 14 | 15 | s.host = 'localhost'; 16 | s.port = port; 17 | s.url = 'http://' + s.host + ':' + port; 18 | s.protocol = 'http'; 19 | 20 | return s; 21 | }; 22 | 23 | exports.createSSLServer = function (port, opts) { 24 | port = port || exports.portSSL; 25 | 26 | var s = https.createServer(opts, function (req, resp) { 27 | s.emit(req.url, req, resp); 28 | }); 29 | 30 | s.host = 'localhost'; 31 | s.port = port; 32 | s.url = 'https://' + s.host + ':' + port; 33 | s.protocol = 'https'; 34 | 35 | return s; 36 | }; 37 | -------------------------------------------------------------------------------- /test/test-http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var agent = require('../'); 5 | var tape = require('tape'); 6 | var server = require('./server.js'); 7 | var s = server.createServer(); 8 | 9 | s.on('/', function (req, res) { 10 | res.end('ok'); 11 | }); 12 | 13 | tape('setup', function (t) { 14 | s.listen(s.port, function () { 15 | t.end(); 16 | }); 17 | }); 18 | 19 | tape('should make request with new http agent', function (t) { 20 | http.get({ 21 | host: s.host, 22 | port: s.port, 23 | agent: agent.http.globalAgent 24 | }, function (res) { 25 | res 26 | .once('data', function (data) { 27 | t.equal(data.toString(), 'ok'); 28 | t.end(); 29 | }) 30 | .on('error', t.error); 31 | }); 32 | }); 33 | 34 | tape('cleanup', function (t) { 35 | s.close(); 36 | t.end(); 37 | }); 38 | -------------------------------------------------------------------------------- /test/test-https.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var https = require('https'); 3 | var agent = require('../'); 4 | var tape = require('tape'); 5 | var pem = require('pem'); 6 | var server = require('./server.js'); 7 | 8 | var s; 9 | var key; 10 | var cert; 11 | var caRootKey; 12 | var caRootCert; 13 | 14 | tape('root pem', function (t) { 15 | pem.createCertificate({ 16 | days:1, 17 | selfSigned:true 18 | }, function (err, keys) { 19 | caRootKey = keys.serviceKey; 20 | caRootCert = keys.certificate; 21 | t.end(); 22 | }); 23 | }); 24 | 25 | tape('pem', function (t) { 26 | pem.createCertificate({ 27 | serviceCertificate: caRootCert, 28 | serviceKey: caRootKey, 29 | serial: Date.now(), 30 | days: 500, 31 | country: '', 32 | state: '', 33 | locality: '', 34 | organization: '', 35 | organizationUnit: '', 36 | commonName: 'sindresorhus.com' 37 | }, function (err, keys) { 38 | key = keys.clientKey; 39 | cert = keys.certificate; 40 | t.end(); 41 | }); 42 | }); 43 | 44 | tape('setup', function (t) { 45 | s = server.createSSLServer(server.portSSL + 1, { 46 | key: key, 47 | cert: cert 48 | }); 49 | 50 | s.on('/', function (req, res) { 51 | res.end('ok'); 52 | }); 53 | 54 | s.listen(s.port, function () { 55 | t.end(); 56 | }); 57 | }); 58 | 59 | tape('should make request with new https agent', function (t) { 60 | https.get({ 61 | host: s.host, 62 | port: s.port, 63 | agent: new agent.https.Agent({ 64 | strictSSL: true, 65 | ca: caRootCert 66 | }), 67 | headers: {host: 'sindresorhus.com'} 68 | }, function (res) { 69 | res 70 | .on('error', t.error) 71 | .once('data', function (data) { 72 | t.equal(data.toString(), 'ok'); 73 | t.end(); 74 | }); 75 | }); 76 | }); 77 | 78 | tape('cleanup', function (t) { 79 | s.close(); 80 | t.end(); 81 | }); 82 | -------------------------------------------------------------------------------- /test/test-keepalive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var agent = require('../'); 5 | var tape = require('tape'); 6 | var server = require('./server.js'); 7 | var s = server.createServer(); 8 | 9 | s.on('/', function (req, res) { 10 | res.end(req.headers.connection); 11 | }); 12 | 13 | tape('setup', function (t) { 14 | s.listen(s.port, function () { 15 | t.end(); 16 | }); 17 | }); 18 | 19 | tape('should recieve connection: close header', function (t) { 20 | http.get({ 21 | host: s.host, 22 | port: s.port, 23 | agent: agent.http.globalAgent 24 | }, function (res) { 25 | res 26 | .once('data', function (data) { 27 | t.equal(data.toString(), 'close'); 28 | t.end(); 29 | }) 30 | .on('error', t.error); 31 | }); 32 | }); 33 | 34 | tape('cleanup', function (t) { 35 | s.close(); 36 | t.end(); 37 | }); 38 | --------------------------------------------------------------------------------