From 1e9bcf26ce9e93caeda3280b8186168b49ae621d Mon Sep 17 00:00:00 2001 From: Dmitry Nizovtsev Date: Thu, 23 Feb 2012 17:37:49 +0200 Subject: [PATCH] net, http, https: add localAddress option Binds to a local address before making the outgoing connection. --- doc/api/http.markdown | 1 + doc/api/net.markdown | 2 + lib/http.js | 44 +++++++++++++------ lib/https.js | 24 ++++++++-- lib/net.js | 13 +++++- lib/tls.js | 6 ++- test/simple/test-http-localaddress.js | 55 +++++++++++++++++++++++ test/simple/test-https-localaddress.js | 61 ++++++++++++++++++++++++++ 8 files changed, 186 insertions(+), 20 deletions(-) create mode 100644 test/simple/test-http-localaddress.js create mode 100644 test/simple/test-https-localaddress.js diff --git a/doc/api/http.markdown b/doc/api/http.markdown index b525e3d0ac..68ea23ccc2 100644 --- a/doc/api/http.markdown +++ b/doc/api/http.markdown @@ -436,6 +436,7 @@ Options: Defaults to `'localhost'`. - `hostname`: To support `url.parse()` `hostname` is preferred over `host` - `port`: Port of remote server. Defaults to 80. +- `localAddress`: Local interface to bind for network connections. - `socketPath`: Unix Domain Socket (use one of host:port or socketPath) - `method`: A string specifying the HTTP request method. Defaults to `'GET'`. - `path`: Request path. Defaults to `'/'`. Should include query string if any. diff --git a/doc/api/net.markdown b/doc/api/net.markdown index a99eaa4fd7..26c0d9dbf0 100644 --- a/doc/api/net.markdown +++ b/doc/api/net.markdown @@ -64,6 +64,8 @@ For TCP sockets, `options` argument should be an object which specifies: - `host`: Host the client should connect to. Defaults to `'localhost'`. + - `localAddress`: Local interface to bind to for network connections. + For UNIX domain sockets, `options` argument should be an object which specifies: - `path`: Path the client should connect to (Required). diff --git a/lib/http.js b/lib/http.js index f1e9941a51..74b9a65958 100644 --- a/lib/http.js +++ b/lib/http.js @@ -981,8 +981,12 @@ function Agent(options) { self.requests = {}; self.sockets = {}; self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets; - self.on('free', function(socket, host, port) { + self.on('free', function(socket, host, port, localAddress) { var name = host + ':' + port; + if (localAddress) { + name += ':' + localAddress; + } + if (self.requests[name] && self.requests[name].length) { self.requests[name].shift().onSocket(socket); if (self.requests[name].length === 0) { @@ -1005,14 +1009,17 @@ exports.Agent = Agent; Agent.defaultMaxSockets = 5; Agent.prototype.defaultPort = 80; -Agent.prototype.addRequest = function(req, host, port) { +Agent.prototype.addRequest = function(req, host, port, localAddress) { var name = host + ':' + port; + if (localAddress) { + name += ':' + localAddress; + } if (!this.sockets[name]) { this.sockets[name] = []; } if (this.sockets[name].length < this.maxSockets) { // If we are under maxSockets create a new one. - req.onSocket(this.createSocket(name, host, port)); + req.onSocket(this.createSocket(name, host, port, localAddress)); } else { // We are over limit so we'll add it to the queue. if (!this.requests[name]) { @@ -1021,29 +1028,33 @@ Agent.prototype.addRequest = function(req, host, port) { this.requests[name].push(req); } }; -Agent.prototype.createSocket = function(name, host, port) { +Agent.prototype.createSocket = function(name, host, port, localAddress) { var self = this; - var s = self.createConnection(port, host, self.options); + var options = util._extend({}, self.options); + options.port = port; + options.host = host; + options.localAddress = localAddress; + var s = self.createConnection(options); if (!self.sockets[name]) { self.sockets[name] = []; } this.sockets[name].push(s); var onFree = function() { - self.emit('free', s, host, port); + self.emit('free', s, host, port, localAddress); } s.on('free', onFree); var onClose = function(err) { // 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. - self.removeSocket(s, name, host, port); + self.removeSocket(s, name, host, port, localAddress); } s.on('close', onClose); var onRemove = function() { // 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 - self.removeSocket(s, name, host, port); + self.removeSocket(s, name, host, port, localAddress); s.removeListener('close', onClose); s.removeListener('free', onFree); s.removeListener('agentRemove', onRemove); @@ -1051,7 +1062,7 @@ Agent.prototype.createSocket = function(name, host, port) { s.on('agentRemove', onRemove); return s; }; -Agent.prototype.removeSocket = function(s, name, host, port) { +Agent.prototype.removeSocket = function(s, name, host, port, localAddress) { if (this.sockets[name]) { var index = this.sockets[name].indexOf(s); if (index !== -1) { @@ -1064,8 +1075,7 @@ Agent.prototype.removeSocket = function(s, name, host, port) { } if (this.requests[name] && this.requests[name].length) { // If we have pending requests and a socket gets closed a new one - // needs to be created to take over in the pool for the one that closed. - this.createSocket(name, host, port).emit('free'); + this.createSocket(name, host, port, localAddress).emit('free'); } }; @@ -1144,15 +1154,21 @@ function ClientRequest(options, cb) { // If there is an agent we should default to Connection:keep-alive. self._last = false; self.shouldKeepAlive = true; - self.agent.addRequest(self, host, port); + self.agent.addRequest(self, host, port, options.localAddress); } else { // No agent, default to Connection:close. self._last = true; self.shouldKeepAlive = false; if (options.createConnection) { - var conn = options.createConnection(port, host, options); + options.port = port; + options.host = host; + var conn = options.createConnection(options); } else { - var conn = net.createConnection(port, host); + var conn = net.createConnection({ + port: port, + host: host, + localAddress: options.localAddress + }); } self.onSocket(conn); } diff --git a/lib/https.js b/lib/https.js index 640de1dd00..9778354007 100644 --- a/lib/https.js +++ b/lib/https.js @@ -51,12 +51,30 @@ exports.createServer = function(opts, requestListener) { // HTTPS agents. -function createConnection(port, host, options) { - options.port = port; - options.host = host; +function createConnection(/* [port, host, options] */) { + var options = {}; + + if (typeof arguments[0] === 'object') { + options = arguments[0]; + } else if (typeof arguments[1] === 'object') { + options = arguments[1]; + options.port = arguments[0]; + } else if (typeof arguments[2] === 'object') { + options = arguments[2]; + options.port = arguments[0]; + options.host = arguments[1]; + } else { + if (typeof arguments[0] === 'number') { + options.port = arguments[0]; + } + if (typeof arguments[1] === 'string') { + options.host = arguments[1]; + } + } return tls.connect(options); } + function Agent(options) { http.Agent.call(this, options); this.createConnection = createConnection; diff --git a/lib/net.js b/lib/net.js index e9c0218156..af74ae13ce 100644 --- a/lib/net.js +++ b/lib/net.js @@ -527,7 +527,7 @@ function afterWrite(status, handle, req, buffer) { } -function connect(self, address, port, addressType) { +function connect(self, address, port, addressType, localAddress) { if (port) { self.remotePort = port; } @@ -540,10 +540,19 @@ function connect(self, address, port, addressType) { var connectReq; if (addressType == 6) { + if (localAddress) { + self._handle.bind6(localAddress); + } connectReq = self._handle.connect6(address, port); } else if (addressType == 4) { + if (localAddress) { + self._handle.bind(localAddress); + } connectReq = self._handle.connect(address, port); } else { + if (localAddress) { + self._handle.bind(localAddress); + } connectReq = self._handle.connect(address, afterConnect); } @@ -615,7 +624,7 @@ Socket.prototype.connect = function(options, cb) { // expects remoteAddress to have a meaningful value ip = ip || (addressType === 4 ? '127.0.0.1' : '0:0:0:0:0:0:0:1'); - connect(self, ip, options.port, addressType); + connect(self, ip, options.port, addressType, options.localAddress); } }); } diff --git a/lib/tls.js b/lib/tls.js index ec59dd648d..e181393a08 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -1121,7 +1121,11 @@ exports.connect = function(/* [port, host], options, cb */) { } if (!options.socket) { - socket.connect(options.port, options.host); + socket.connect({ + port: options.port, + host: options.host, + localAddress: options.localAddress + }); } pair.on('secure', function() { diff --git a/test/simple/test-http-localaddress.js b/test/simple/test-http-localaddress.js new file mode 100644 index 0000000000..1843d821be --- /dev/null +++ b/test/simple/test-http-localaddress.js @@ -0,0 +1,55 @@ +// 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. + +var common = require('../common'); +var http = require('http'), + assert = require('assert'); + +if (['linux', 'win32'].indexOf(process.platform) == -1) { + console.log('Skipping platform-specific test.'); + process.exit(); +} + +var server = http.createServer(function (req, res) { + console.log("Connect from: " + req.connection.remoteAddress); + assert.equal('127.0.0.2', req.connection.remoteAddress); + + req.on('end', function() { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('You are from: ' + req.connection.remoteAddress); + }); +}); + +server.listen(common.PORT, "127.0.0.1", function() { + var options = { host: 'localhost', + port: common.PORT, + path: '/', + method: 'GET', + localAddress: '127.0.0.2' }; + + var req = http.request(options, function(res) { + res.on('end', function() { + server.close(); + process.exit(); + }); + }); + req.end(); +}); diff --git a/test/simple/test-https-localaddress.js b/test/simple/test-https-localaddress.js new file mode 100644 index 0000000000..b171225be7 --- /dev/null +++ b/test/simple/test-https-localaddress.js @@ -0,0 +1,61 @@ +// 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. + +var common = require('../common'); +var https = require('https'), + fs = require('fs'), + assert = require('assert'); + +if (['linux', 'win32'].indexOf(process.platform) == -1) { + console.log('Skipping platform-specific test.'); + process.exit(); +} + +var options = { + key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'), + cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem') +}; + +var server = https.createServer(options, function (req, res) { + console.log("Connect from: " + req.connection.socket.remoteAddress); + assert.equal('127.0.0.2', req.connection.socket.remoteAddress); + + req.on('end', function() { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('You are from: ' + req.connection.remoteAddress); + }); +}); + +server.listen(common.PORT, "127.0.0.1", function() { + var options = { host: 'localhost', + port: common.PORT, + path: '/', + method: 'GET', + localAddress: '127.0.0.2' }; + + var req = https.request(options, function(res) { + res.on('end', function() { + server.close(); + process.exit(); + }); + }); + req.end(); +});