Skip to content

Commit 1e9bcf2

Browse files
Dmitry Nizovtsevbnoordhuis
Dmitry Nizovtsev
authored andcommitted
net, http, https: add localAddress option
Binds to a local address before making the outgoing connection.
1 parent 9ea5a4c commit 1e9bcf2

8 files changed

+186
-20
lines changed

doc/api/http.markdown

+1
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ Options:
436436
Defaults to `'localhost'`.
437437
- `hostname`: To support `url.parse()` `hostname` is preferred over `host`
438438
- `port`: Port of remote server. Defaults to 80.
439+
- `localAddress`: Local interface to bind for network connections.
439440
- `socketPath`: Unix Domain Socket (use one of host:port or socketPath)
440441
- `method`: A string specifying the HTTP request method. Defaults to `'GET'`.
441442
- `path`: Request path. Defaults to `'/'`. Should include query string if any.

doc/api/net.markdown

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ For TCP sockets, `options` argument should be an object which specifies:
6464

6565
- `host`: Host the client should connect to. Defaults to `'localhost'`.
6666

67+
- `localAddress`: Local interface to bind to for network connections.
68+
6769
For UNIX domain sockets, `options` argument should be an object which specifies:
6870

6971
- `path`: Path the client should connect to (Required).

lib/http.js

+30-14
Original file line numberDiff line numberDiff line change
@@ -981,8 +981,12 @@ function Agent(options) {
981981
self.requests = {};
982982
self.sockets = {};
983983
self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets;
984-
self.on('free', function(socket, host, port) {
984+
self.on('free', function(socket, host, port, localAddress) {
985985
var name = host + ':' + port;
986+
if (localAddress) {
987+
name += ':' + localAddress;
988+
}
989+
986990
if (self.requests[name] && self.requests[name].length) {
987991
self.requests[name].shift().onSocket(socket);
988992
if (self.requests[name].length === 0) {
@@ -1005,14 +1009,17 @@ exports.Agent = Agent;
10051009
Agent.defaultMaxSockets = 5;
10061010

10071011
Agent.prototype.defaultPort = 80;
1008-
Agent.prototype.addRequest = function(req, host, port) {
1012+
Agent.prototype.addRequest = function(req, host, port, localAddress) {
10091013
var name = host + ':' + port;
1014+
if (localAddress) {
1015+
name += ':' + localAddress;
1016+
}
10101017
if (!this.sockets[name]) {
10111018
this.sockets[name] = [];
10121019
}
10131020
if (this.sockets[name].length < this.maxSockets) {
10141021
// If we are under maxSockets create a new one.
1015-
req.onSocket(this.createSocket(name, host, port));
1022+
req.onSocket(this.createSocket(name, host, port, localAddress));
10161023
} else {
10171024
// We are over limit so we'll add it to the queue.
10181025
if (!this.requests[name]) {
@@ -1021,37 +1028,41 @@ Agent.prototype.addRequest = function(req, host, port) {
10211028
this.requests[name].push(req);
10221029
}
10231030
};
1024-
Agent.prototype.createSocket = function(name, host, port) {
1031+
Agent.prototype.createSocket = function(name, host, port, localAddress) {
10251032
var self = this;
1026-
var s = self.createConnection(port, host, self.options);
1033+
var options = util._extend({}, self.options);
1034+
options.port = port;
1035+
options.host = host;
1036+
options.localAddress = localAddress;
1037+
var s = self.createConnection(options);
10271038
if (!self.sockets[name]) {
10281039
self.sockets[name] = [];
10291040
}
10301041
this.sockets[name].push(s);
10311042
var onFree = function() {
1032-
self.emit('free', s, host, port);
1043+
self.emit('free', s, host, port, localAddress);
10331044
}
10341045
s.on('free', onFree);
10351046
var onClose = function(err) {
10361047
// This is the only place where sockets get removed from the Agent.
10371048
// If you want to remove a socket from the pool, just close it.
10381049
// All socket errors end in a close event anyway.
1039-
self.removeSocket(s, name, host, port);
1050+
self.removeSocket(s, name, host, port, localAddress);
10401051
}
10411052
s.on('close', onClose);
10421053
var onRemove = function() {
10431054
// We need this function for cases like HTTP 'upgrade'
10441055
// (defined by WebSockets) where we need to remove a socket from the pool
10451056
// because it'll be locked up indefinitely
1046-
self.removeSocket(s, name, host, port);
1057+
self.removeSocket(s, name, host, port, localAddress);
10471058
s.removeListener('close', onClose);
10481059
s.removeListener('free', onFree);
10491060
s.removeListener('agentRemove', onRemove);
10501061
}
10511062
s.on('agentRemove', onRemove);
10521063
return s;
10531064
};
1054-
Agent.prototype.removeSocket = function(s, name, host, port) {
1065+
Agent.prototype.removeSocket = function(s, name, host, port, localAddress) {
10551066
if (this.sockets[name]) {
10561067
var index = this.sockets[name].indexOf(s);
10571068
if (index !== -1) {
@@ -1064,8 +1075,7 @@ Agent.prototype.removeSocket = function(s, name, host, port) {
10641075
}
10651076
if (this.requests[name] && this.requests[name].length) {
10661077
// If we have pending requests and a socket gets closed a new one
1067-
// needs to be created to take over in the pool for the one that closed.
1068-
this.createSocket(name, host, port).emit('free');
1078+
this.createSocket(name, host, port, localAddress).emit('free');
10691079
}
10701080
};
10711081

@@ -1144,15 +1154,21 @@ function ClientRequest(options, cb) {
11441154
// If there is an agent we should default to Connection:keep-alive.
11451155
self._last = false;
11461156
self.shouldKeepAlive = true;
1147-
self.agent.addRequest(self, host, port);
1157+
self.agent.addRequest(self, host, port, options.localAddress);
11481158
} else {
11491159
// No agent, default to Connection:close.
11501160
self._last = true;
11511161
self.shouldKeepAlive = false;
11521162
if (options.createConnection) {
1153-
var conn = options.createConnection(port, host, options);
1163+
options.port = port;
1164+
options.host = host;
1165+
var conn = options.createConnection(options);
11541166
} else {
1155-
var conn = net.createConnection(port, host);
1167+
var conn = net.createConnection({
1168+
port: port,
1169+
host: host,
1170+
localAddress: options.localAddress
1171+
});
11561172
}
11571173
self.onSocket(conn);
11581174
}

lib/https.js

+21-3
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,30 @@ exports.createServer = function(opts, requestListener) {
5151

5252
// HTTPS agents.
5353

54-
function createConnection(port, host, options) {
55-
options.port = port;
56-
options.host = host;
54+
function createConnection(/* [port, host, options] */) {
55+
var options = {};
56+
57+
if (typeof arguments[0] === 'object') {
58+
options = arguments[0];
59+
} else if (typeof arguments[1] === 'object') {
60+
options = arguments[1];
61+
options.port = arguments[0];
62+
} else if (typeof arguments[2] === 'object') {
63+
options = arguments[2];
64+
options.port = arguments[0];
65+
options.host = arguments[1];
66+
} else {
67+
if (typeof arguments[0] === 'number') {
68+
options.port = arguments[0];
69+
}
70+
if (typeof arguments[1] === 'string') {
71+
options.host = arguments[1];
72+
}
73+
}
5774
return tls.connect(options);
5875
}
5976

77+
6078
function Agent(options) {
6179
http.Agent.call(this, options);
6280
this.createConnection = createConnection;

lib/net.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ function afterWrite(status, handle, req, buffer) {
527527
}
528528

529529

530-
function connect(self, address, port, addressType) {
530+
function connect(self, address, port, addressType, localAddress) {
531531
if (port) {
532532
self.remotePort = port;
533533
}
@@ -540,10 +540,19 @@ function connect(self, address, port, addressType) {
540540

541541
var connectReq;
542542
if (addressType == 6) {
543+
if (localAddress) {
544+
self._handle.bind6(localAddress);
545+
}
543546
connectReq = self._handle.connect6(address, port);
544547
} else if (addressType == 4) {
548+
if (localAddress) {
549+
self._handle.bind(localAddress);
550+
}
545551
connectReq = self._handle.connect(address, port);
546552
} else {
553+
if (localAddress) {
554+
self._handle.bind(localAddress);
555+
}
547556
connectReq = self._handle.connect(address, afterConnect);
548557
}
549558

@@ -615,7 +624,7 @@ Socket.prototype.connect = function(options, cb) {
615624
// expects remoteAddress to have a meaningful value
616625
ip = ip || (addressType === 4 ? '127.0.0.1' : '0:0:0:0:0:0:0:1');
617626

618-
connect(self, ip, options.port, addressType);
627+
connect(self, ip, options.port, addressType, options.localAddress);
619628
}
620629
});
621630
}

lib/tls.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1121,7 +1121,11 @@ exports.connect = function(/* [port, host], options, cb */) {
11211121
}
11221122

11231123
if (!options.socket) {
1124-
socket.connect(options.port, options.host);
1124+
socket.connect({
1125+
port: options.port,
1126+
host: options.host,
1127+
localAddress: options.localAddress
1128+
});
11251129
}
11261130

11271131
pair.on('secure', function() {

test/simple/test-http-localaddress.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
var common = require('../common');
23+
var http = require('http'),
24+
assert = require('assert');
25+
26+
if (['linux', 'win32'].indexOf(process.platform) == -1) {
27+
console.log('Skipping platform-specific test.');
28+
process.exit();
29+
}
30+
31+
var server = http.createServer(function (req, res) {
32+
console.log("Connect from: " + req.connection.remoteAddress);
33+
assert.equal('127.0.0.2', req.connection.remoteAddress);
34+
35+
req.on('end', function() {
36+
res.writeHead(200, { 'Content-Type': 'text/plain' });
37+
res.end('You are from: ' + req.connection.remoteAddress);
38+
});
39+
});
40+
41+
server.listen(common.PORT, "127.0.0.1", function() {
42+
var options = { host: 'localhost',
43+
port: common.PORT,
44+
path: '/',
45+
method: 'GET',
46+
localAddress: '127.0.0.2' };
47+
48+
var req = http.request(options, function(res) {
49+
res.on('end', function() {
50+
server.close();
51+
process.exit();
52+
});
53+
});
54+
req.end();
55+
});
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
var common = require('../common');
23+
var https = require('https'),
24+
fs = require('fs'),
25+
assert = require('assert');
26+
27+
if (['linux', 'win32'].indexOf(process.platform) == -1) {
28+
console.log('Skipping platform-specific test.');
29+
process.exit();
30+
}
31+
32+
var options = {
33+
key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'),
34+
cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem')
35+
};
36+
37+
var server = https.createServer(options, function (req, res) {
38+
console.log("Connect from: " + req.connection.socket.remoteAddress);
39+
assert.equal('127.0.0.2', req.connection.socket.remoteAddress);
40+
41+
req.on('end', function() {
42+
res.writeHead(200, { 'Content-Type': 'text/plain' });
43+
res.end('You are from: ' + req.connection.remoteAddress);
44+
});
45+
});
46+
47+
server.listen(common.PORT, "127.0.0.1", function() {
48+
var options = { host: 'localhost',
49+
port: common.PORT,
50+
path: '/',
51+
method: 'GET',
52+
localAddress: '127.0.0.2' };
53+
54+
var req = https.request(options, function(res) {
55+
res.on('end', function() {
56+
server.close();
57+
process.exit();
58+
});
59+
});
60+
req.end();
61+
});

0 commit comments

Comments
 (0)