-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Automagic socket sharing for net/tls connections via proxy server #702
base: master
Are you sure you want to change the base?
Changes from all commits
d0d2e19
a959ccf
461fce7
a84d76d
df6b08d
1fd0f23
88549e2
ea020f1
3517f12
5eaabb1
37b4ef6
68d177b
cb8ebfc
c79791e
73c4203
df06435
81e2541
1de84f0
45ad7d5
e1de8fa
23c6177
196ec35
0f6fc6c
127c9db
d3466c8
e0c804b
dd56bcf
1698c33
769ff68
c3dc3d5
f5b4340
7cec5f7
f97fd03
bcb8a19
7c3421c
2ecc1fa
24ec2b8
aa1fe42
2fdfd78
fdc6ae3
f01660a
b615030
2c8f8f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
var util = require('util'), | ||
events = require('events'), | ||
net = require('net'), | ||
tls = require('tls'), | ||
streamplex = require('_streamplex'); | ||
|
||
// NOTE: this list may not be exhaustive, see also https://tools.ietf.org/html/rfc5735#section-4 | ||
var _PROXY_LOCAL = "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 127.0.0.0/8 localhost"; | ||
|
||
var _PROXY_DBG = ('_PROXY_DBG' in process.env) || false, | ||
PROXY_HOST = process.env.PROXY_HOST || "proxy.tessel.io", | ||
PROXY_PORT = +process.env.PROXY_PORT || 443, | ||
PROXY_TRUSTED = +process.env.PROXY_TRUSTED || 0, | ||
PROXY_TOKEN = process.env.PROXY_TOKEN || process.env.TM_API_KEY, | ||
PROXY_LOCAL = process.env.PROXY_LOCAL || _PROXY_LOCAL, | ||
PROXY_IDLE = +process.env.PROXY_IDLE || 90e3, | ||
PROXY_CERT = process.env.PROXY_CERT || null; | ||
|
||
/** | ||
* Tunnel helpers | ||
*/ | ||
|
||
function createTunnel(cb) { | ||
if (_PROXY_DBG) console.log("TUNNEL -> START", new Date()); | ||
tls.connect({host:PROXY_HOST, port:PROXY_PORT, proxy:false, ca:(PROXY_CERT && [PROXY_CERT])}, function () { | ||
var proxySocket = this, | ||
tunnel = streamplex(streamplex.B_SIDE); | ||
tunnel.pipe(proxySocket).pipe(tunnel); | ||
proxySocket.on('error', shutdownTunnel); | ||
proxySocket.on('close', shutdownTunnel); | ||
proxySocket.on('error', cb); | ||
|
||
var idleTimeout; | ||
tunnel.on('inactive', function () { | ||
if (_PROXY_DBG) console.log("TUNNEL -> inactive", new Date()); | ||
idleTimeout = setTimeout(shutdownTunnel, PROXY_IDLE); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should idleTimeout be cleared if it is already set? (Failsafe) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The timeout is cleared by the 'active' event which would have to happen before another 'inactive' one. |
||
}); | ||
tunnel.on('active', function () { | ||
if (_PROXY_DBG) console.log("TUNNEL -> active", new Date()); | ||
clearTimeout(idleTimeout); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be failsafey, maybe this?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if clearTimeout follows spec, it should be just fine to call it with null (or any other invalid!) values. |
||
}); | ||
|
||
tunnel.sendMessage({token:PROXY_TOKEN}); | ||
tunnel.once('message', function (d) { | ||
if (_PROXY_DBG) console.log("TUNNEL: auth response?", d); | ||
proxySocket.removeListener('error', cb); | ||
if (!d.authed) cb(new Error("Authorization failed.")); | ||
else cb(null, tunnel); | ||
}); | ||
function shutdownTunnel(e) { | ||
if (_PROXY_DBG) console.log("TUNNEL -> STOP", new Date()); | ||
tunnel.destroy(e); | ||
if (this !== proxySocket) proxySocket.end(); | ||
proxySocket.removeListener('close', shutdownTunnel); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this also be removed as the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess including clearTimeout There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing the 'close' listener is specifically to prevent |
||
} | ||
}).on('error', cb); | ||
} | ||
|
||
var tunnelKeeper = new events.EventEmitter(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am the Tunnel Master I am the Gate Keeper There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in commit ecdoaf6bf5a2b1 |
||
|
||
tunnelKeeper.getTunnel = function (cb) { // CAUTION: syncronous callback! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this have to be synchronous? (Rather than, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is internal code, I didn't see a reason to artificially delay |
||
if (this._tunnel) return cb(null, this._tunnel); | ||
|
||
var self = this; | ||
if (!this._pending) createTunnel(function (e, tunnel) { | ||
delete self._pending; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if (e) return self.emit('tunnel', e); | ||
|
||
self._tunnel = tunnel; | ||
tunnel.on('close', function () { | ||
self._tunnel = null; | ||
}); | ||
var streamProto = Object.create(ProxiedSocket.prototype); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just curious, why object.create instead of new? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If it weren't for needing a link between socket instances and their associated tunnel (next two lines) the logic that uses this would just |
||
streamProto._tunnel = tunnel; | ||
tunnel._streamProto = streamProto; | ||
self.emit('tunnel', null, tunnel); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd call you out on doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is an internal event, whose parameters are always |
||
}); | ||
this._pending = true; | ||
this.once('tunnel', cb); | ||
}; | ||
|
||
var local_matchers = PROXY_LOCAL.split(' ').map(function (str) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See PROXY_LOCAL splitting comment earlier There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incidentally, can we get a test case in for the local_matchers generator? Since it's a lot of bit fiddling, it'd be good to ensure it never regresses subtley There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah…agree this is a pretty reasonable thing to have test coverage for. I suppose I'll need to refactor this a bit and expose via an [underscored] property on the [underscored] module for it. |
||
var parts = str.split('/'); | ||
if (parts.length > 1) { | ||
// IPv4 + mask | ||
var bits = +parts[1], | ||
mask = 0xFFFFFFFF << (32-bits) >>> 0, | ||
base = net._ipStrToInt(parts[0]) & mask; // NOTE: left signed to match test below | ||
return function (addr, host) { | ||
return ((addr & mask) === base); | ||
}; | ||
} else if (str[0] === '.') { | ||
// base including subdomains | ||
str = str.slice(1); | ||
return function (addr, host) { | ||
var idx = host.lastIndexOf(str); | ||
return (~idx && idx + str.length === host.length); | ||
}; | ||
} else return function (addr, host) { | ||
// exact domain/address | ||
return (host === str); | ||
} | ||
}); | ||
|
||
function protoForConnection(host, port, opts, cb) { // CAUTION: syncronous callback! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same: can just be made to always be async? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be if the optimization above is removed. |
||
var addr = (net.isIPv4(host)) ? net._ipStrToInt(host) : null, | ||
force_local = !PROXY_TOKEN || (opts._secure && !PROXY_TRUSTED) || (opts.proxy === false), | ||
local = force_local || local_matchers.some(function (matcher) { return matcher(addr, host); }); | ||
if (_PROXY_DBG) { | ||
if (force_local) console.log( | ||
"Forced to use local socket to \"%s\". [token: %s, secure/trusted: %s/%s, opts override: %s]", | ||
host, Boolean(PROXY_TOKEN), Boolean(opts._secure), Boolean(PROXY_TRUSTED), (opts.proxy === false) | ||
); | ||
else console.log("Proxied socket to \"%s\"? %s", host, !local); | ||
} | ||
if (local) cb(null, net._CC3KSocket.prototype); | ||
else tunnelKeeper.getTunnel(function (e, tunnel) { | ||
if (e) return cb(e); | ||
cb(null, tunnel._streamProto); | ||
}); | ||
} | ||
|
||
/** | ||
* ProxiedSocket | ||
*/ | ||
|
||
function ProxiedSocket(opts) { | ||
if (!(this instanceof ProxiedSocket)) return new ProxiedSocket(opts); | ||
net.Socket.call(this, opts); | ||
this._tunnel = this._opts.tunnel; | ||
this._setup(this._opts); | ||
} | ||
util.inherits(ProxiedSocket, net.Socket); | ||
|
||
ProxiedSocket.prototype._setup = function () { | ||
var type = (this._secure) ? 'tls' : 'net'; | ||
this._transport = this._tunnel.createStream(type); | ||
|
||
var self = this; | ||
// TODO: it'd be great if we is-a substream instead of has-a… | ||
this._transport.on('data', function (d) { | ||
var more = self.push(d); | ||
if (!more) self._transport.pause(); | ||
}); | ||
this._transport.on('end', function () { | ||
self.push(null); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is self._transport need any cleanup past There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not quite sure this is what you're asking, but ProxiedSocket may never emit a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 0674dbe |
||
}); | ||
|
||
function reEmit(evt) { | ||
self._transport.on(evt, function test() { | ||
var args = Array.prototype.concat.apply([evt], arguments); | ||
self.emit.apply(self, args); | ||
}); | ||
} | ||
['connect', 'secureConnect', 'error', 'timeout', 'close'].forEach(reEmit); | ||
}; | ||
|
||
ProxiedSocket.prototype._read = function () { | ||
this._transport.resume(); | ||
}; | ||
ProxiedSocket.prototype._write = function (buf, enc, cb) { | ||
this._transport.write(buf, enc, cb); | ||
}; | ||
|
||
ProxiedSocket.prototype._connect = function (port, host) { | ||
this.remotePort = port; | ||
this.remoteAddress = host; | ||
this._transport.remoteEmit('_pls_connect', port, host); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OP PLS |
||
}; | ||
|
||
ProxiedSocket.prototype.setTimeout = function (msecs, cb) { | ||
this._transport.remoteEmit('_pls_timeout', msecs); | ||
if (cb) { | ||
if (msecs) this.once('timeout', cb); | ||
else this.removeListener('timeout', cb); | ||
} | ||
}; | ||
|
||
ProxiedSocket.prototype.destroy = function () { | ||
this._transport.destroy(); | ||
this.end(); | ||
}; | ||
|
||
exports._protoForConnection = protoForConnection; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Want to move splitting logic to here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not particularly, the env variable as defaulted here gets processed all in once place later I don't see a compelling reason to split the
split
part of that logic away?