mirror of
https://git.coolaj86.com/coolaj86/telebit-relay.js.git
synced 2025-04-21 11:00:37 +00:00
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
07965d8eac | |||
98d31fc8d7 | |||
986102c79e | |||
4a2199dc71 | |||
6b25c7e1f5 | |||
7f3c8ee96d | |||
2b62ac2109 | |||
c300636477 | |||
89718a8a07 | |||
|
64eec91d11 | ||
|
ae91fd5049 | ||
|
061999cc34 | ||
|
68e5116ba0 | ||
|
6fa7f50894 | ||
|
a8be74d77e | ||
|
96ab344b71 | ||
|
bbdb09902b | ||
|
d013de932f | ||
|
701aa99a30 | ||
|
aff82cebe9 | ||
|
54ca2782dd | ||
|
c9f2c52afd | ||
|
5c7d65546b | ||
|
d82530e1db | ||
|
8e71ae02cf | ||
|
7112bfdbb2 | ||
|
7205b86fd3 | ||
|
3d5f4a773d | ||
|
b4a300cc64 | ||
|
a1fbde7d8e | ||
|
40c797b729 | ||
|
65df12ecb3 | ||
|
02d195798f | ||
|
ed06c7e79e |
16
.jshintrc
Normal file
16
.jshintrc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{ "node": true
|
||||||
|
, "browser": true
|
||||||
|
, "jquery": true
|
||||||
|
, "strict": true
|
||||||
|
, "indent": 2
|
||||||
|
, "onevar": true
|
||||||
|
, "laxcomma": true
|
||||||
|
, "laxbreak": true
|
||||||
|
, "eqeqeq": true
|
||||||
|
, "immed": true
|
||||||
|
, "undef": true
|
||||||
|
, "unused": true
|
||||||
|
, "latedef": true
|
||||||
|
, "curly": true
|
||||||
|
, "trailing": true
|
||||||
|
}
|
37
README.md
37
README.md
@ -1,21 +1,7 @@
|
|||||||
<!-- BANNER_TPL_BEGIN -->
|
| Sponsored by [ppl](https://ppl.family) | **tunnel-server.js** | [tunnel-client.js](https://git.coolaj86.com/coolaj86/tunnel-client.js) |
|
||||||
|
|
||||||
About Daplie: We're taking back the Internet!
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Down with Google, Apple, and Facebook!
|
|
||||||
|
|
||||||
We're re-decentralizing the web and making it read-write again - one home cloud system at a time.
|
|
||||||
|
|
||||||
Tired of serving the Empire? Come join the Rebel Alliance:
|
|
||||||
|
|
||||||
<a href="mailto:jobs@daplie.com">jobs@daplie.com</a> | [Invest in Daplie on Wefunder](https://daplie.com/invest/) | [Pre-order Cloud](https://daplie.com/preorder/), The World's First Home Server for Everyone
|
|
||||||
|
|
||||||
<!-- BANNER_TPL_END -->
|
|
||||||
|
|
||||||
# stunneld.js
|
# stunneld.js
|
||||||
|
|
||||||
A server that works in combination with [stunnel.js](https://github.com/Daplie/node-tunnel-client)
|
A server that works in combination with [stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js)
|
||||||
to allow you to serve http and https from any computer, anywhere through a secure tunnel.
|
to allow you to serve http and https from any computer, anywhere through a secure tunnel.
|
||||||
|
|
||||||
CLI
|
CLI
|
||||||
@ -27,9 +13,20 @@ Installs as `stunnel.js` with the alias `jstunnel`
|
|||||||
### Install
|
### Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g stunnel
|
npm install -g stunneld
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then `dist/etc/systemd/system/stunneld.service` should be copied to `/etc/systemd/system/stunneld.service` and
|
||||||
|
the ARGUMENTS, such as SECRET, MUST BE CHANGED.
|
||||||
|
|
||||||
|
*TODO*: make `--config /path/to/config` the only argument (and have the secret auto-generated on first run?)
|
||||||
|
|
||||||
|
## Note: Use node.js v8.x
|
||||||
|
|
||||||
|
There is a bug in node v9.x that causes stunneld to crash.
|
||||||
|
|
||||||
|
https://github.com/nodejs/node/issues/20241
|
||||||
|
|
||||||
### Advanced Usage
|
### Advanced Usage
|
||||||
|
|
||||||
How to use `stunnel.js` with your own instance of `stunneld.js`:
|
How to use `stunnel.js` with your own instance of `stunneld.js`:
|
||||||
@ -62,11 +59,11 @@ but those generally cost $5 - $20 / month and so it's probably
|
|||||||
cheaper to purchase data transfer (which we supply, obviously),
|
cheaper to purchase data transfer (which we supply, obviously),
|
||||||
which is only $1/month for most people.
|
which is only $1/month for most people.
|
||||||
|
|
||||||
Just use the client ([stunnel.js](https://github.com/Daplie/node-tunnel-client))
|
Just use the client ([stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js))
|
||||||
with Daplie's tunneling service (the default) and save yourself the monthly fee
|
with this tunneling service (the default) and save yourself the monthly fee
|
||||||
by only paying for the data you need.
|
by only paying for the data you need.
|
||||||
|
|
||||||
* Daplie Tunnel (zero setup)
|
* Node WS Tunnel (zero setup)
|
||||||
* Heroku (zero cost)
|
* Heroku (zero cost)
|
||||||
* Chunk Host (best deal per TB/month)
|
* Chunk Host (best deal per TB/month)
|
||||||
|
|
||||||
|
4
bin/generate-secret.js
Executable file
4
bin/generate-secret.js
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
console.log(require('crypto').randomBytes(16).toString('hex'));
|
13
bin/install-stunneld-js.sh
Normal file
13
bin/install-stunneld-js.sh
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
rm -rf ./node-installer.sh
|
||||||
|
curl -fsSL bit.ly/node-installer -o ./node-installer.sh
|
||||||
|
bash ./node-installer.sh --dev-deps
|
||||||
|
|
||||||
|
git clone https://git.coolaj86.com/coolaj86/tunnel-server.js.git
|
||||||
|
pushd tunnel-server.js/
|
||||||
|
npm install
|
||||||
|
my_secret=$(node bin/generate-secret.js)
|
||||||
|
echo "Your secret is:\n\n\t"$my_secret
|
||||||
|
echo "node bin/server.js --servernames tunnel.example.com --secret $my_secret"
|
||||||
|
popd
|
@ -9,11 +9,11 @@ var stunneld = require('../wstunneld.js');
|
|||||||
var greenlock = require('greenlock');
|
var greenlock = require('greenlock');
|
||||||
|
|
||||||
function collectServernames(val, memo) {
|
function collectServernames(val, memo) {
|
||||||
val.split(/,/).forEach(function (servername) {
|
var lowerCase = val.split(/,/).map(function (servername) {
|
||||||
memo.push(servername.toLowerCase());
|
return servername.toLowerCase();
|
||||||
});
|
});
|
||||||
|
|
||||||
return memo;
|
return memo.concat(lowerCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectProxies(val, memo) {
|
function collectProxies(val, memo) {
|
||||||
@ -65,8 +65,7 @@ function collectProxies(val, memo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function collectPorts(val, memo) {
|
function collectPorts(val, memo) {
|
||||||
memo = memo.concat(val.split(/,/g).filter(Boolean));
|
return memo.concat(val.split(/,/g).map(Number).filter(Boolean));
|
||||||
return memo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
program
|
program
|
||||||
@ -97,7 +96,7 @@ program.ports.forEach(function (port) {
|
|||||||
|
|
||||||
program.servernames = Object.keys(servernamesMap);
|
program.servernames = Object.keys(servernamesMap);
|
||||||
if (!program.servernames.length) {
|
if (!program.servernames.length) {
|
||||||
throw new Error('must specify at least one server or servername');
|
throw new Error('You must give this server at least one servername for its admin interface. Example:\n\n\t--servernames tunnel.example.com,tunnel.example.net');
|
||||||
}
|
}
|
||||||
|
|
||||||
program.ports = Object.keys(portsMap);
|
program.ports = Object.keys(portsMap);
|
||||||
@ -113,7 +112,7 @@ if (!program.secret) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO letsencrypt
|
// TODO letsencrypt
|
||||||
program.tlsOptions = require('localhost.daplie.com-certificates').merge({});
|
program.tlsOptions = require('localhost.daplie.me-certificates').merge({});
|
||||||
|
|
||||||
function approveDomains(opts, certs, cb) {
|
function approveDomains(opts, certs, cb) {
|
||||||
// This is where you check your database and associated
|
// This is where you check your database and associated
|
||||||
@ -146,8 +145,8 @@ if (!program.email || !program.agreeTos) {
|
|||||||
else {
|
else {
|
||||||
program.greenlock = greenlock.create({
|
program.greenlock = greenlock.create({
|
||||||
|
|
||||||
//server: 'staging'
|
version: 'draft-11'
|
||||||
server: 'https://acme-v01.api.letsencrypt.org/directory'
|
, server: 'https://acme-v02.api.letsencrypt.org/directory'
|
||||||
|
|
||||||
, challenges: {
|
, challenges: {
|
||||||
// TODO dns-01
|
// TODO dns-01
|
||||||
|
23
dist/etc/systemd/system/stunneld.service
vendored
Normal file
23
dist/etc/systemd/system/stunneld.service
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daplie Tunnel Server
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target systemd-networkd-wait-online.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# Always restart, unless it's restarting fast enough for us to believe it's completely broken
|
||||||
|
Restart=always
|
||||||
|
StartLimitInterval=10
|
||||||
|
StartLimitBurst=3
|
||||||
|
|
||||||
|
User=www-data
|
||||||
|
Group=www-data
|
||||||
|
WorkingDirectory=/srv/stunneld
|
||||||
|
# TODO needs --config option and these options should go in a config file
|
||||||
|
ExecStart=/srv/stunneld/bin/stunneld.js --servernames tunnel.example.com --secret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' --email tunnel@example.com --agree-tos
|
||||||
|
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
50
handlers.js
50
handlers.js
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
var tls = require('tls');
|
var tls = require('tls');
|
||||||
var packerStream = require('tunnel-packer').Stream;
|
var wrapSocket = require('tunnel-packer').wrapSocket;
|
||||||
var redirectHttps = require('redirect-https')();
|
var redirectHttps = require('redirect-https')();
|
||||||
|
|
||||||
module.exports.create = function (program) {
|
module.exports.create = function (program) {
|
||||||
@ -44,7 +44,7 @@ module.exports.create = function (program) {
|
|||||||
// SNI is not recogonized / cannot be handled
|
// SNI is not recogonized / cannot be handled
|
||||||
//
|
//
|
||||||
program.httpInvalidSniServer = http.createServer(function (req, res) {
|
program.httpInvalidSniServer = http.createServer(function (req, res) {
|
||||||
res.end("You're doing strange things that make me feel uncomfortable. Please don't touch me there any more.");
|
res.end("This is an old error message that shouldn't be actually be acessible anymore. If you get this please tell AJ so that he finds where it was still referenced and removes it");
|
||||||
});
|
});
|
||||||
program.tlsInvalidSniServer = tls.createServer(program.tlsOptions, function (tlsSocket) {
|
program.tlsInvalidSniServer = tls.createServer(program.tlsOptions, function (tlsSocket) {
|
||||||
console.log('tls connection');
|
console.log('tls connection');
|
||||||
@ -57,19 +57,34 @@ module.exports.create = function (program) {
|
|||||||
// tlsServer.emit('connection', socket); // this didn't work either
|
// tlsServer.emit('connection', socket); // this didn't work either
|
||||||
//console.log('chunkLen', firstChunk.byteLength);
|
//console.log('chunkLen', firstChunk.byteLength);
|
||||||
|
|
||||||
var myDuplex = packerStream.create(socket);
|
|
||||||
|
|
||||||
console.log('httpsInvalid servername', servername);
|
console.log('httpsInvalid servername', servername);
|
||||||
program.tlsInvalidSniServer.emit('connection', myDuplex);
|
//program.tlsInvalidSniServer.emit('connection', wrapSocket(socket));
|
||||||
|
var tlsInvalidSniServer = tls.createServer(program.tlsOptions, function (tlsSocket) {
|
||||||
|
console.log('tls connection');
|
||||||
|
// things get a little messed up here
|
||||||
|
var httpInvalidSniServer = http.createServer(function (req, res) {
|
||||||
|
if (!servername) {
|
||||||
|
res.statusCode = 422;
|
||||||
|
res.end(
|
||||||
|
"3. An inexplicable temporal shift of the quantum realm... that makes me feel uncomfortable.\n\n"
|
||||||
|
+ "[ERROR] No SNI header was sent. I can only think of two possible explanations for this:\n"
|
||||||
|
+ "\t1. You really love Windows XP and you just won't let go of Internet Explorer 6\n"
|
||||||
|
+ "\t2. You're writing a bot and you forgot to set the servername parameter\n"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
socket.on('data', function (chunk) {
|
res.end(
|
||||||
console.log('[' + Date.now() + '] socket data', chunk.byteLength);
|
"You came in hot looking for '" + servername + "' and, granted, the IP address for that domain"
|
||||||
myDuplex.push(chunk);
|
+ " must be pointing here (or else how could you be here?), nevertheless either it's not registered"
|
||||||
|
+ " in the internal system at all (which Seth says isn't even a thing) or there is no device"
|
||||||
|
+ " connected on the south side of the network which has informed me that it's ready to have traffic"
|
||||||
|
+ " for that domain forwarded to it (sorry I didn't check that deeply to determine which).\n\n"
|
||||||
|
+ "Either way, you're doing strange things that make me feel uncomfortable... Please don't touch me there any more.");
|
||||||
});
|
});
|
||||||
socket.on('error', function (err) {
|
httpInvalidSniServer.emit('connection', tlsSocket);
|
||||||
console.error('[error] httpsInvalid TODO close');
|
|
||||||
console.error(err);
|
|
||||||
});
|
});
|
||||||
|
tlsInvalidSniServer.emit('connection', wrapSocket(socket));
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -97,18 +112,7 @@ module.exports.create = function (program) {
|
|||||||
// tlsServer.emit('connection', socket); // this didn't work either
|
// tlsServer.emit('connection', socket); // this didn't work either
|
||||||
//console.log('chunkLen', firstChunk.byteLength);
|
//console.log('chunkLen', firstChunk.byteLength);
|
||||||
|
|
||||||
var myDuplex = packerStream.create(socket);
|
|
||||||
|
|
||||||
console.log('httpsTunnel (Admin) servername', servername);
|
console.log('httpsTunnel (Admin) servername', servername);
|
||||||
program.tlsTunnelServer.emit('connection', myDuplex);
|
program.tlsTunnelServer.emit('connection', wrapSocket(socket));
|
||||||
|
|
||||||
socket.on('data', function (chunk) {
|
|
||||||
console.log('[' + Date.now() + '] socket data', chunk.byteLength);
|
|
||||||
myDuplex.push(chunk);
|
|
||||||
});
|
|
||||||
socket.on('error', function (err) {
|
|
||||||
console.error('[error] httpsTunnel (Admin) TODO close');
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
55
lib/device-tracker.js
Normal file
55
lib/device-tracker.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Devices = module.exports;
|
||||||
|
Devices.add = function (store, servername, newDevice) {
|
||||||
|
var devices = store[servername] || [];
|
||||||
|
devices.push(newDevice);
|
||||||
|
store[servername] = devices;
|
||||||
|
};
|
||||||
|
Devices.remove = function (store, servername, device) {
|
||||||
|
var devices = store[servername] || [];
|
||||||
|
var index = devices.indexOf(device);
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
console.warn('attempted to remove non-present device', device.deviceId, 'from', servername);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return devices.splice(index, 1)[0];
|
||||||
|
};
|
||||||
|
Devices.list = function (store, servername) {
|
||||||
|
if (store[servername] && store[servername].length) {
|
||||||
|
return store[servername];
|
||||||
|
}
|
||||||
|
// There wasn't an exact match so check any of the wildcard domains, sorted longest
|
||||||
|
// first so the one with the biggest natural match with be found first.
|
||||||
|
var deviceList = [];
|
||||||
|
Object.keys(store).filter(function (pattern) {
|
||||||
|
return pattern[0] === '*' && store[pattern].length;
|
||||||
|
}).sort(function (a, b) {
|
||||||
|
return b.length - a.length;
|
||||||
|
}).some(function (pattern) {
|
||||||
|
var subPiece = pattern.slice(1);
|
||||||
|
if (subPiece === servername.slice(-subPiece.length)) {
|
||||||
|
console.log('"'+servername+'" matches "'+pattern+'"');
|
||||||
|
deviceList = store[pattern];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return deviceList;
|
||||||
|
};
|
||||||
|
Devices.exist = function (store, servername) {
|
||||||
|
return !!(Devices.list(store, servername).length);
|
||||||
|
};
|
||||||
|
Devices.next = function (store, servername) {
|
||||||
|
var devices = Devices.list(store, servername);
|
||||||
|
var device;
|
||||||
|
|
||||||
|
if (devices._index >= devices.length) {
|
||||||
|
devices._index = 0;
|
||||||
|
}
|
||||||
|
device = devices[devices._index || 0];
|
||||||
|
devices._index = (devices._index || 0) + 1;
|
||||||
|
|
||||||
|
return device;
|
||||||
|
};
|
153
lib/unwrap-tls.js
Normal file
153
lib/unwrap-tls.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var packer = require('tunnel-packer');
|
||||||
|
var sni = require('sni');
|
||||||
|
|
||||||
|
function pipeWs(servername, service, conn, remote) {
|
||||||
|
console.log('[pipeWs] servername:', servername, 'service:', service);
|
||||||
|
|
||||||
|
var browserAddr = packer.socketToAddr(conn);
|
||||||
|
browserAddr.service = service;
|
||||||
|
var cid = packer.addrToId(browserAddr);
|
||||||
|
conn.tunnelCid = cid;
|
||||||
|
console.log('[pipeWs] browser is', cid, 'home-cloud is', packer.socketToId(remote.upgradeReq.socket));
|
||||||
|
|
||||||
|
function sendWs(data, serviceOverride) {
|
||||||
|
if (remote.ws && (!conn.tunnelClosing || serviceOverride)) {
|
||||||
|
try {
|
||||||
|
remote.ws.send(packer.pack(browserAddr, data, serviceOverride), { binary: true });
|
||||||
|
// If we can't send data over the websocket as fast as this connection can send it to us
|
||||||
|
// (or there are a lot of connections trying to send over the same websocket) then we
|
||||||
|
// need to pause the connection for a little. We pause all connections if any are paused
|
||||||
|
// to make things more fair so a connection doesn't get stuck waiting for everyone else
|
||||||
|
// to finish because it got caught on the boundary. Also if serviceOverride is set it
|
||||||
|
// means the connection is over, so no need to pause it.
|
||||||
|
if (!serviceOverride && (remote.pausedConns.length || remote.ws.bufferedAmount > 1024*1024)) {
|
||||||
|
// console.log('pausing', cid, 'to allow web socket to catch up');
|
||||||
|
conn.pause();
|
||||||
|
remote.pausedConns.push(conn);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[pipeWs] error sending websocket message', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remote.clients[cid] = conn;
|
||||||
|
conn.on('data', function (chunk) {
|
||||||
|
console.log('[pipeWs] data from browser to tunneler', chunk.byteLength);
|
||||||
|
sendWs(chunk);
|
||||||
|
});
|
||||||
|
conn.on('error', function (err) {
|
||||||
|
console.warn('[pipeWs] browser connection error', err);
|
||||||
|
});
|
||||||
|
conn.on('close', function (hadErr) {
|
||||||
|
console.log('[pipeWs] browser connection closing');
|
||||||
|
sendWs(null, hadErr ? 'error': 'end');
|
||||||
|
delete remote.clients[cid];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.createTcpConnectionHandler = function (copts) {
|
||||||
|
var Devices = copts.Devices;
|
||||||
|
|
||||||
|
return function onTcpConnection(conn) {
|
||||||
|
// this works when I put it here, but I don't know if it's tls yet here
|
||||||
|
// httpsServer.emit('connection', socket);
|
||||||
|
//tls3000.emit('connection', socket);
|
||||||
|
|
||||||
|
//var tlsSocket = new tls.TLSSocket(socket, { secureContext: tls.createSecureContext(tlsOpts) });
|
||||||
|
//tlsSocket.on('data', function (chunk) {
|
||||||
|
// console.log('dummy', chunk.byteLength);
|
||||||
|
//});
|
||||||
|
|
||||||
|
//return;
|
||||||
|
conn.once('data', function (firstChunk) {
|
||||||
|
// BUG XXX: this assumes that the packet won't be chunked smaller
|
||||||
|
// than the 'hello' or the point of the 'Host' header.
|
||||||
|
// This is fairly reasonable, but there are edge cases where
|
||||||
|
// it does not hold (such as manual debugging with telnet)
|
||||||
|
// and so it should be fixed at some point in the future
|
||||||
|
|
||||||
|
// defer after return (instead of being in many places)
|
||||||
|
process.nextTick(function () {
|
||||||
|
conn.unshift(firstChunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
var service = 'tcp';
|
||||||
|
var servername;
|
||||||
|
var str;
|
||||||
|
var m;
|
||||||
|
|
||||||
|
function tryTls() {
|
||||||
|
if (-1 !== copts.servernames.indexOf(servername)) {
|
||||||
|
console.log("Lock and load, admin interface time!");
|
||||||
|
copts.httpsTunnel(servername, conn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!servername) {
|
||||||
|
console.log("No SNI was given, so there's nothing we can do here");
|
||||||
|
copts.httpsInvalid(servername, conn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextDevice = Devices.next(copts.deviceLists, servername);
|
||||||
|
if (!nextDevice) {
|
||||||
|
console.log("No devices match the given servername");
|
||||||
|
copts.httpsInvalid(servername, conn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])");
|
||||||
|
pipeWs(servername, service, conn, nextDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
|
||||||
|
if (22 === firstChunk[0]) {
|
||||||
|
// TLS
|
||||||
|
service = 'https';
|
||||||
|
servername = (sni(firstChunk)||'').toLowerCase();
|
||||||
|
console.log("tls hello servername:", servername);
|
||||||
|
tryTls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstChunk[0] > 32 && firstChunk[0] < 127) {
|
||||||
|
str = firstChunk.toString();
|
||||||
|
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
|
||||||
|
servername = (m && m[1].toLowerCase() || '').split(':')[0];
|
||||||
|
console.log('servername', servername);
|
||||||
|
if (/HTTP\//i.test(str)) {
|
||||||
|
service = 'http';
|
||||||
|
// TODO disallow http entirely
|
||||||
|
// /^\/\.well-known\/acme-challenge\//.test(str)
|
||||||
|
if (/well-known/.test(str)) {
|
||||||
|
// HTTP
|
||||||
|
if (Devices.exist(copts.deviceLists, servername)) {
|
||||||
|
pipeWs(servername, service, conn, Devices.next(copts.deviceLists, servername));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copts.handleHttp(servername, conn);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// redirect to https
|
||||||
|
copts.handleInsecureHttp(servername, conn);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Got unexpected connection", str);
|
||||||
|
conn.write(JSON.stringify({ error: {
|
||||||
|
message: "not sure what you were trying to do there..."
|
||||||
|
, code: 'E_INVALID_PROTOCOL' }
|
||||||
|
}));
|
||||||
|
conn.end();
|
||||||
|
});
|
||||||
|
conn.on('error', function (err) {
|
||||||
|
console.error('[error] tcp socket raw TODO forward and close');
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
25
package.json
25
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stunneld",
|
"name": "stunneld",
|
||||||
"version": "0.8.3",
|
"version": "0.10.0",
|
||||||
"description": "A pure-JavaScript tunnel daemon for http and https similar to a localtunnel.me server, but uses TLS (SSL) with ServerName Indication (SNI) over https to work even in harsh network conditions such as in student dorms and behind HOAs, corporate firewalls, public libraries, airports, airplanes, etc. Can also tunnel tls and plain tcp.",
|
"description": "A pure-JavaScript tunnel daemon for http and https similar to a localtunnel.me server, but uses TLS (SSL) with ServerName Indication (SNI) over https to work even in harsh network conditions such as in student dorms and behind HOAs, corporate firewalls, public libraries, airports, airplanes, etc. Can also tunnel tls and plain tcp.",
|
||||||
"main": "wstunneld.js",
|
"main": "wstunneld.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Daplie/node-tunnel-server.git"
|
"url": "git+https://git.coolaj86.com/coolaj86/tunnel-server.js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"server",
|
"server",
|
||||||
@ -42,18 +42,19 @@
|
|||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
"license": "(MIT OR Apache-2.0)",
|
"license": "(MIT OR Apache-2.0)",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Daplie/node-tunnel-server/issues"
|
"url": "https://git.coolaj86.com/coolaj86/tunnel-server.js/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Daplie/node-tunnel-server#readme",
|
"homepage": "https://git.coolaj86.com/coolaj86/tunnel-server.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cluster-store": "^2.0.4",
|
"bluebird": "^3.5.1",
|
||||||
"commander": "^2.9.0",
|
"cluster-store": "^2.0.8",
|
||||||
"greenlock": "^2.1.12",
|
"commander": "^2.15.1",
|
||||||
"jsonwebtoken": "^7.1.9",
|
"greenlock": "^2.2.4",
|
||||||
"localhost.daplie.com-certificates": "^1.2.3",
|
"jsonwebtoken": "^8.2.1",
|
||||||
"redirect-https": "^1.1.0",
|
"localhost.daplie.me-certificates": "^1.3.5",
|
||||||
|
"redirect-https": "^1.1.5",
|
||||||
"sni": "^1.0.0",
|
"sni": "^1.0.0",
|
||||||
"tunnel-packer": "^1.0.0",
|
"tunnel-packer": "^1.4.0",
|
||||||
"ws": "^2.2.3"
|
"ws": "^5.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
601
wstunneld.js
601
wstunneld.js
@ -1,95 +1,116 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var sni = require('sni');
|
|
||||||
var url = require('url');
|
var url = require('url');
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
var jwt = require('jsonwebtoken');
|
var jwt = require('jsonwebtoken');
|
||||||
var packer = require('tunnel-packer');
|
var packer = require('tunnel-packer');
|
||||||
|
|
||||||
var Devices = {};
|
function timeoutPromise(duration) {
|
||||||
Devices.add = function (store, servername, newDevice) {
|
return new PromiseA(function (resolve) {
|
||||||
var devices = Devices.list(store, servername);
|
setTimeout(resolve, duration);
|
||||||
devices.push(newDevice);
|
});
|
||||||
store[servername] = devices;
|
}
|
||||||
};
|
|
||||||
Devices.remove = function (store, servername, device) {
|
|
||||||
var devices = Devices.list(store, servername);
|
|
||||||
var index = devices.indexOf(device);
|
|
||||||
|
|
||||||
if (index < 0) {
|
var Devices = require('./lib/device-tracker');
|
||||||
var id = device.deviceId || device.servername || device.id;
|
|
||||||
console.warn('attempted to remove non-present device', id, 'from', servername);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return devices.splice(index, 1)[0];
|
|
||||||
};
|
|
||||||
Devices.list = function (store, servername) {
|
|
||||||
return store[servername] || [];
|
|
||||||
};
|
|
||||||
Devices.exist = function (store, servername) {
|
|
||||||
return (store[servername] || []).length;
|
|
||||||
};
|
|
||||||
Devices.next = function (store, servername) {
|
|
||||||
var devices = Devices.list(store, servername);
|
|
||||||
var device;
|
|
||||||
|
|
||||||
if (devices._index >= devices.length) {
|
|
||||||
devices._index = 0;
|
|
||||||
}
|
|
||||||
device = devices[devices._index || 0];
|
|
||||||
devices._index = (devices._index || 0) + 1;
|
|
||||||
|
|
||||||
return device;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.store = { Devices: Devices };
|
module.exports.store = { Devices: Devices };
|
||||||
module.exports.create = function (copts) {
|
module.exports.create = function (copts) {
|
||||||
var deviceLists = {};
|
copts.deviceLists = {};
|
||||||
|
//var deviceLists = {};
|
||||||
var activityTimeout = copts.activityTimeout || 2*60*1000;
|
var activityTimeout = copts.activityTimeout || 2*60*1000;
|
||||||
var pongTimeout = copts.pongTimeout || 10*1000;
|
var pongTimeout = copts.pongTimeout || 10*1000;
|
||||||
|
copts.Devices = Devices;
|
||||||
|
var onTcpConnection = require('./lib/unwrap-tls').createTcpConnectionHandler(copts);
|
||||||
|
|
||||||
function onWsConnection(ws) {
|
function onWsConnection(ws, upgradeReq) {
|
||||||
var location = url.parse(ws.upgradeReq.url, true);
|
console.log(ws);
|
||||||
var authn = (ws.upgradeReq.headers.authorization||'').split(/\s+/);
|
var socketId = packer.socketToId(upgradeReq.socket);
|
||||||
var jwtoken;
|
var remotes = {};
|
||||||
var token;
|
|
||||||
|
|
||||||
try {
|
function logName() {
|
||||||
if (authn[0]) {
|
var result = Object.keys(remotes).map(function (jwtoken) {
|
||||||
if ('basic' === authn[0].toLowerCase()) {
|
return remotes[jwtoken].deviceId;
|
||||||
authn = new Buffer(authn[1], 'base64').toString('ascii').split(':');
|
}).join(';');
|
||||||
|
|
||||||
|
return result || socketId;
|
||||||
}
|
}
|
||||||
/*
|
function sendTunnelMsg(addr, data, service) {
|
||||||
if (-1 !== [ 'bearer', 'jwk' ].indexOf(authn[0].toLowerCase())) {
|
ws.send(packer.pack(addr, data, service), {binary: true});
|
||||||
jwtoken = authn[1];
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
jwtoken = authn[1] || location.query.access_token;
|
|
||||||
} catch(e) {
|
|
||||||
jwtoken = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
function getBrowserConn(cid) {
|
||||||
token = jwt.verify(jwtoken, copts.secret);
|
var browserConn;
|
||||||
} catch(e) {
|
Object.keys(remotes).some(function (jwtoken) {
|
||||||
token = null;
|
if (remotes[jwtoken].clients[cid]) {
|
||||||
|
browserConn = remotes[jwtoken].clients[cid];
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
return browserConn;
|
||||||
if (!token || !token.name) {
|
|
||||||
console.log('location, token');
|
|
||||||
console.log(location.query.access_token);
|
|
||||||
console.log(token);
|
|
||||||
}
|
}
|
||||||
*/
|
function closeBrowserConn(cid) {
|
||||||
|
var remote;
|
||||||
if (!token) {
|
Object.keys(remotes).some(function (jwtoken) {
|
||||||
ws.send(JSON.stringify({ error: { message: "invalid access token", code: "E_INVALID_TOKEN" } }));
|
if (remotes[jwtoken].clients[cid]) {
|
||||||
ws.close();
|
remote = remotes[jwtoken];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!remote) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log('[wstunneld.js] DEBUG', token);
|
PromiseA.resolve().then(function () {
|
||||||
|
var conn = remote.clients[cid];
|
||||||
|
conn.tunnelClosing = true;
|
||||||
|
conn.end();
|
||||||
|
|
||||||
|
// If no data is buffered for writing then we don't need to wait for it to drain.
|
||||||
|
if (!conn.bufferSize) {
|
||||||
|
return timeoutPromise(500);
|
||||||
|
}
|
||||||
|
// Otherwise we want the connection to be able to finish, but we also want to impose
|
||||||
|
// a time limit for it to drain, since it shouldn't have more than 1MB buffered.
|
||||||
|
return new PromiseA(function (resolve) {
|
||||||
|
var timeoutId = setTimeout(resolve, 60*1000);
|
||||||
|
conn.once('drain', function () {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).then(function () {
|
||||||
|
if (remote.clients[cid]) {
|
||||||
|
console.warn(cid, 'browser connection still present after calling `end`');
|
||||||
|
remote.clients[cid].destroy();
|
||||||
|
return timeoutPromise(500);
|
||||||
|
}
|
||||||
|
}).then(function () {
|
||||||
|
if (remote.clients[cid]) {
|
||||||
|
console.error(cid, 'browser connection still present after calling `destroy`');
|
||||||
|
delete remote.clients[cid];
|
||||||
|
}
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.warn('failed to close browser connection', cid, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToken(jwtoken) {
|
||||||
|
if (remotes[jwtoken]) {
|
||||||
|
// return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var token;
|
||||||
|
try {
|
||||||
|
token = jwt.verify(jwtoken, copts.secret);
|
||||||
|
} catch (e) {
|
||||||
|
token = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return { message: "invalid access token", code: "E_INVALID_TOKEN" };
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(token.domains)) {
|
if (!Array.isArray(token.domains)) {
|
||||||
if ('string' === typeof token.name) {
|
if ('string' === typeof token.name) {
|
||||||
@ -97,72 +118,211 @@ module.exports.create = function (copts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(token.domains)) {
|
if (!Array.isArray(token.domains) || !token.domains.length) {
|
||||||
ws.send(JSON.stringify({ error: { message: "invalid server name", code: "E_INVALID_NAME" } }));
|
return { message: "invalid server name", code: "E_INVALID_NAME" };
|
||||||
|
}
|
||||||
|
if (token.domains.some(function (name) { return typeof name !== 'string'; })) {
|
||||||
|
return { message: "invalid server name", code: "E_INVALID_NAME" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the custom properties we need to manage this remote, then add it to all the relevant
|
||||||
|
// domains and the list of all this websocket's remotes.
|
||||||
|
token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(',');
|
||||||
|
token.ws = ws;
|
||||||
|
token.upgradeReq = upgradeReq;
|
||||||
|
token.clients = {};
|
||||||
|
|
||||||
|
token.pausedConns = [];
|
||||||
|
ws._socket.on('drain', function () {
|
||||||
|
// the websocket library has it's own buffer apart from node's socket buffer, but that one
|
||||||
|
// is much more difficult to watch, so we watch for the lower level buffer to drain and
|
||||||
|
// then check to see if the upper level buffer is still too full to write to. Note that
|
||||||
|
// the websocket library buffer has something to do with compression, so I'm not requiring
|
||||||
|
// that to be 0 before we start up again.
|
||||||
|
if (ws.bufferedAmount > 128*1024) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
token.pausedConns.forEach(function (conn) {
|
||||||
|
if (!conn.manualPause) {
|
||||||
|
// console.log('resuming', conn.tunnelCid, 'now that the web socket has caught up');
|
||||||
|
conn.resume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
token.pausedConns.length = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
token.domains.forEach(function (domainname) {
|
||||||
|
console.log('domainname', domainname);
|
||||||
|
Devices.add(copts.deviceLists, domainname, token);
|
||||||
|
});
|
||||||
|
remotes[jwtoken] = token;
|
||||||
|
console.log("added token '" + token.deviceId + "' to websocket", socketId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeToken(jwtoken) {
|
||||||
|
var remote = remotes[jwtoken];
|
||||||
|
if (!remote) {
|
||||||
|
return { message: 'specified token not present', code: 'E_INVALID_TOKEN'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent any more browser connections being sent to this remote, and any existing
|
||||||
|
// connections from trying to send more data across the connection.
|
||||||
|
remote.domains.forEach(function (domainname) {
|
||||||
|
Devices.remove(copts.deviceLists, domainname, remote);
|
||||||
|
});
|
||||||
|
remote.ws = null;
|
||||||
|
remote.upgradeReq = null;
|
||||||
|
|
||||||
|
// Close all of the existing browser connections associated with this websocket connection.
|
||||||
|
Object.keys(remote.clients).forEach(function (cid) {
|
||||||
|
closeBrowserConn(cid);
|
||||||
|
});
|
||||||
|
delete remotes[jwtoken];
|
||||||
|
console.log("removed token '" + remote.deviceId + "' from websocket", socketId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstToken;
|
||||||
|
var authn = (upgradeReq.headers.authorization||'').split(/\s+/);
|
||||||
|
if (authn[0] && 'basic' === authn[0].toLowerCase()) {
|
||||||
|
try {
|
||||||
|
authn = new Buffer(authn[1], 'base64').toString('ascii').split(':');
|
||||||
|
firstToken = authn[1];
|
||||||
|
} catch (err) { }
|
||||||
|
}
|
||||||
|
if (!firstToken) {
|
||||||
|
firstToken = url.parse(upgradeReq.url, true).query.access_token;
|
||||||
|
}
|
||||||
|
if (firstToken) {
|
||||||
|
var err = addToken(firstToken);
|
||||||
|
if (err) {
|
||||||
|
sendTunnelMsg(null, [0, err], 'control');
|
||||||
ws.close();
|
ws.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var remote = {};
|
var commandHandlers = {
|
||||||
remote.ws = ws;
|
add_token: addToken
|
||||||
remote.servername = (token.device && token.device.hostname) || token.domains.join(',');
|
, delete_token: function (token) {
|
||||||
remote.deviceId = (token.device && token.device.id) || null;
|
if (token !== '*') {
|
||||||
remote.id = packer.socketToId(ws.upgradeReq.socket);
|
return removeToken(token);
|
||||||
console.log("remote.id", remote.id);
|
}
|
||||||
remote.domains = token.domains;
|
var err;
|
||||||
remote.clients = {};
|
Object.keys(remotes).some(function (jwtoken) {
|
||||||
// TODO allow tls to be decrypted by server if client is actually a browser
|
err = removeToken(jwtoken);
|
||||||
// and we haven't implemented tls in the browser yet
|
return err;
|
||||||
// remote.decrypt = token.decrypt;
|
});
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var handlers = {
|
var packerHandlers = {
|
||||||
onmessage: function (opts) {
|
oncontrol: function (opts) {
|
||||||
// opts.data
|
var cmd, err;
|
||||||
var cid = packer.addrToId(opts);
|
try {
|
||||||
var cstream = remote.clients[cid];
|
cmd = JSON.parse(opts.data.toString());
|
||||||
|
} catch (err) {}
|
||||||
console.log("remote '" + remote.servername + " : " + remote.id + "' has data for '" + cid + "'", opts.data.byteLength);
|
if (!Array.isArray(cmd) || typeof cmd[0] !== 'number') {
|
||||||
|
var msg = 'received bad command "' + opts.data.toString() + '"';
|
||||||
if (!cstream) {
|
console.warn(msg, 'from websocket', socketId);
|
||||||
remote.ws.send(packer.pack(opts, null, 'error'));
|
sendTunnelMsg(null, [0, {message: msg, code: 'E_BAD_COMMAND'}], 'control');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cstream.browser.write(opts.data);
|
if (cmd[0] < 0) {
|
||||||
|
// We only ever send one command and we send it once, so we just hard coded the ID as 1.
|
||||||
|
if (cmd[0] === -1) {
|
||||||
|
if (cmd[1]) {
|
||||||
|
console.log('received error response to hello from', socketId, cmd[1]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.warn('received response to unknown command', cmd, 'from', socketId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd[0] === 0) {
|
||||||
|
console.warn('received dis-associated error from', socketId, cmd[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandHandlers[cmd[1]]) {
|
||||||
|
err = commandHandlers[cmd[1]].apply(null, cmd.slice(2));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
err = { message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' };
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTunnelMsg(null, [-cmd[0], err], 'control');
|
||||||
|
}
|
||||||
|
|
||||||
|
, onmessage: function (opts) {
|
||||||
|
var cid = packer.addrToId(opts);
|
||||||
|
console.log("remote '" + logName() + "' has data for '" + cid + "'", opts.data.byteLength);
|
||||||
|
|
||||||
|
var browserConn = getBrowserConn(cid);
|
||||||
|
if (!browserConn) {
|
||||||
|
sendTunnelMsg(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
browserConn.write(opts.data);
|
||||||
|
// tunnelRead is how many bytes we've read from the tunnel, and written to the browser.
|
||||||
|
browserConn.tunnelRead = (browserConn.tunnelRead || 0) + opts.data.byteLength;
|
||||||
|
// If we have more than 1MB buffered data we need to tell the other side to slow down.
|
||||||
|
// Once we've finished sending what we have we can tell the other side to keep going.
|
||||||
|
// If we've already sent the 'pause' message though don't send it again, because we're
|
||||||
|
// probably just dealing with data queued before our message got to them.
|
||||||
|
if (!browserConn.remotePaused && browserConn.bufferSize > 1024*1024) {
|
||||||
|
sendTunnelMsg(opts, browserConn.tunnelRead, 'pause');
|
||||||
|
browserConn.remotePaused = true;
|
||||||
|
|
||||||
|
browserConn.once('drain', function () {
|
||||||
|
sendTunnelMsg(opts, browserConn.tunnelRead, 'resume');
|
||||||
|
browserConn.remotePaused = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
, onpause: function (opts) {
|
||||||
|
var cid = packer.addrToId(opts);
|
||||||
|
console.log('[TunnelPause]', cid);
|
||||||
|
var browserConn = getBrowserConn(cid);
|
||||||
|
if (browserConn) {
|
||||||
|
browserConn.manualPause = true;
|
||||||
|
browserConn.pause();
|
||||||
|
} else {
|
||||||
|
sendTunnelMsg(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, onresume: function (opts) {
|
||||||
|
var cid = packer.addrToId(opts);
|
||||||
|
console.log('[TunnelResume]', cid);
|
||||||
|
var browserConn = getBrowserConn(cid);
|
||||||
|
if (browserConn) {
|
||||||
|
browserConn.manualPause = false;
|
||||||
|
browserConn.resume();
|
||||||
|
} else {
|
||||||
|
sendTunnelMsg(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
, onend: function (opts) {
|
, onend: function (opts) {
|
||||||
var cid = packer.addrToId(opts);
|
var cid = packer.addrToId(opts);
|
||||||
console.log('[TunnelEnd]', cid);
|
console.log('[TunnelEnd]', cid);
|
||||||
handlers._onend(cid);
|
closeBrowserConn(cid);
|
||||||
}
|
}
|
||||||
, onerror: function (opts) {
|
, onerror: function (opts) {
|
||||||
var cid = packer.addrToId(opts);
|
var cid = packer.addrToId(opts);
|
||||||
console.log('[TunnelError]', cid);
|
console.log('[TunnelError]', cid, opts.message);
|
||||||
handlers._onend(cid);
|
closeBrowserConn(cid);
|
||||||
}
|
|
||||||
, _onend: function (cid) {
|
|
||||||
var c = remote.clients[cid];
|
|
||||||
delete remote.clients[cid];
|
|
||||||
try {
|
|
||||||
c.browser.end();
|
|
||||||
} catch(e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
c.wrapped.end();
|
|
||||||
} catch(e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
remote.unpacker = packer.create(handlers);
|
var unpacker = packer.create(packerHandlers);
|
||||||
|
|
||||||
// Now that we have created our remote object we need to store it in the deviceList for
|
|
||||||
// each domainname we are supposed to be handling.
|
|
||||||
token.domains.forEach(function (domainname) {
|
|
||||||
console.log('domainname', domainname);
|
|
||||||
Devices.add(deviceLists, domainname, remote);
|
|
||||||
});
|
|
||||||
|
|
||||||
var lastActivity = Date.now();
|
var lastActivity = Date.now();
|
||||||
var timeoutId;
|
var timeoutId;
|
||||||
@ -182,11 +342,11 @@ module.exports.create = function (copts) {
|
|||||||
// Otherwise we check to see if the pong has also timed out, and if not we send a ping
|
// Otherwise we check to see if the pong has also timed out, and if not we send a ping
|
||||||
// and call this function again when the pong will have timed out.
|
// and call this function again when the pong will have timed out.
|
||||||
else if (silent < activityTimeout + pongTimeout) {
|
else if (silent < activityTimeout + pongTimeout) {
|
||||||
console.log('pinging', remote.deviceId || remote.servername);
|
console.log('pinging', logName());
|
||||||
try {
|
try {
|
||||||
remote.ws.ping();
|
ws.ping();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('failed to ping home cloud', remote.deviceId || remote.servername);
|
console.warn('failed to ping home cloud', logName());
|
||||||
}
|
}
|
||||||
timeoutId = setTimeout(checkTimeout, pongTimeout);
|
timeoutId = setTimeout(checkTimeout, pongTimeout);
|
||||||
}
|
}
|
||||||
@ -194,8 +354,8 @@ module.exports.create = function (copts) {
|
|||||||
// Last case means the ping we sent before didn't get a response soon enough, so we
|
// Last case means the ping we sent before didn't get a response soon enough, so we
|
||||||
// need to close the websocket connection.
|
// need to close the websocket connection.
|
||||||
else {
|
else {
|
||||||
console.log('home cloud', remote.deviceId || remote.servername, 'connection timed out');
|
console.log('home cloud', logName(), 'connection timed out');
|
||||||
remote.ws.close(1013, 'connection timeout');
|
ws.close(1013, 'connection timeout');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timeoutId = setTimeout(checkTimeout, activityTimeout);
|
timeoutId = setTimeout(checkTimeout, activityTimeout);
|
||||||
@ -208,199 +368,28 @@ module.exports.create = function (copts) {
|
|||||||
refreshTimeout();
|
refreshTimeout();
|
||||||
console.log('message from home cloud to tunneler to browser', chunk.byteLength);
|
console.log('message from home cloud to tunneler to browser', chunk.byteLength);
|
||||||
//console.log(chunk.toString());
|
//console.log(chunk.toString());
|
||||||
remote.unpacker.fns.addChunk(chunk);
|
unpacker.fns.addChunk(chunk);
|
||||||
});
|
});
|
||||||
|
|
||||||
function hangup() {
|
function hangup() {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
console.log('home cloud', remote.deviceId || remote.servername, 'connection closing');
|
console.log('home cloud', logName(), 'connection closing');
|
||||||
// the remote will handle closing its local connections
|
Object.keys(remotes).forEach(function (jwtoken) {
|
||||||
Object.keys(remote.clients).forEach(function (cid) {
|
removeToken(jwtoken);
|
||||||
try {
|
|
||||||
remote.clients[cid].browser.end();
|
|
||||||
} catch(e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
});
|
|
||||||
token.domains.forEach(function (domainname) {
|
|
||||||
Devices.remove(deviceLists, domainname, remote);
|
|
||||||
});
|
});
|
||||||
|
ws.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.on('close', hangup);
|
ws.on('close', hangup);
|
||||||
ws.on('error', hangup);
|
ws.on('error', hangup);
|
||||||
|
|
||||||
|
// We only ever send one command and we send it once, so we just hard code the ID as 1
|
||||||
|
sendTunnelMsg(null, [1, 'hello', [unpacker._version], Object.keys(commandHandlers)], 'control');
|
||||||
}
|
}
|
||||||
|
|
||||||
function pipeWs(servername, service, browser, remote) {
|
return {
|
||||||
console.log('pipeWs');
|
tcp: onTcpConnection
|
||||||
|
, ws: onWsConnection
|
||||||
//var remote = deviceLists[servername];
|
, isClientDomain: Devices.exist.bind(null, copts.deviceLists)
|
||||||
var ws = remote.ws;
|
|
||||||
//var address = packer.socketToAddr(ws.upgradeReq.socket);
|
|
||||||
var baddress = packer.socketToAddr(browser);
|
|
||||||
var cid = packer.addrToId(baddress);
|
|
||||||
console.log('servername:', servername);
|
|
||||||
console.log('service:', service);
|
|
||||||
baddress.service = service;
|
|
||||||
var wrapForRemote = packer.Transform.create({
|
|
||||||
id: cid
|
|
||||||
//, remoteId: remote.id
|
|
||||||
, address: baddress
|
|
||||||
, servername: servername
|
|
||||||
, service: service
|
|
||||||
});
|
|
||||||
console.log('home-cloud is', packer.socketToId(remote.ws.upgradeReq.socket));
|
|
||||||
console.log('browser is', cid);
|
|
||||||
var bstream = remote.clients[cid] = {
|
|
||||||
wrapped: browser.pipe(wrapForRemote)
|
|
||||||
, browser: browser
|
|
||||||
, address: baddress
|
|
||||||
};
|
};
|
||||||
//var bstream = remote.clients[cid] = wrapForRemote.pipe(browser);
|
|
||||||
bstream.wrapped.on('data', function (pchunk) {
|
|
||||||
// var chunk = socket.read();
|
|
||||||
console.log('[bstream] data from browser to tunneler', pchunk.byteLength);
|
|
||||||
//console.log(JSON.stringify(pchunk.toString()));
|
|
||||||
try {
|
|
||||||
ws.send(pchunk, { binary: true });
|
|
||||||
} catch(e) {
|
|
||||||
try {
|
|
||||||
bstream.browser.end();
|
|
||||||
} catch(e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bstream.wrapped.on('error', function (err) {
|
|
||||||
console.error('[error] bstream.wrapped.error');
|
|
||||||
console.error(err);
|
|
||||||
try {
|
|
||||||
ws.send(packer.pack(baddress, null, 'error'), { binary: true });
|
|
||||||
} catch(e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
bstream.browser.end();
|
|
||||||
} catch(e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
delete remote.clients[cid];
|
|
||||||
});
|
|
||||||
bstream.wrapped.on('end', function () {
|
|
||||||
try {
|
|
||||||
ws.send(packer.pack(baddress, null, 'end'), { binary: true });
|
|
||||||
} catch(e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
bstream.browser.end();
|
|
||||||
} catch(e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
delete remote.clients[cid];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTcpConnection(browser) {
|
|
||||||
// this works when I put it here, but I don't know if it's tls yet here
|
|
||||||
// httpsServer.emit('connection', socket);
|
|
||||||
//tls3000.emit('connection', socket);
|
|
||||||
|
|
||||||
//var tlsSocket = new tls.TLSSocket(socket, { secureContext: tls.createSecureContext(tlsOpts) });
|
|
||||||
//tlsSocket.on('data', function (chunk) {
|
|
||||||
// console.log('dummy', chunk.byteLength);
|
|
||||||
//});
|
|
||||||
|
|
||||||
//return;
|
|
||||||
browser.once('data', function (firstChunk) {
|
|
||||||
// BUG XXX: this assumes that the packet won't be chunked smaller
|
|
||||||
// than the 'hello' or the point of the 'Host' header.
|
|
||||||
// This is fairly reasonable, but there are edge cases where
|
|
||||||
// it does not hold (such as manual debugging with telnet)
|
|
||||||
// and so it should be fixed at some point in the future
|
|
||||||
|
|
||||||
// defer after return (instead of being in many places)
|
|
||||||
process.nextTick(function () {
|
|
||||||
browser.unshift(firstChunk);
|
|
||||||
});
|
|
||||||
|
|
||||||
var service = 'tcp';
|
|
||||||
var servername;
|
|
||||||
var str;
|
|
||||||
var m;
|
|
||||||
|
|
||||||
function tryTls() {
|
|
||||||
if (-1 !== copts.servernames.indexOf(servername)) {
|
|
||||||
console.log("Lock and load, admin interface time!");
|
|
||||||
copts.httpsTunnel(servername, browser);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!servername) {
|
|
||||||
console.log("No SNI was given, so there's nothing we can do here");
|
|
||||||
copts.httpsInvalid(servername, browser);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextDevice = Devices.next(deviceLists, servername);
|
|
||||||
if (!nextDevice) {
|
|
||||||
console.log("No devices match the given servername");
|
|
||||||
copts.httpsInvalid(servername, browser);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])");
|
|
||||||
pipeWs(servername, service, browser, nextDevice);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
|
|
||||||
if (22 === firstChunk[0]) {
|
|
||||||
// TLS
|
|
||||||
service = 'https';
|
|
||||||
servername = (sni(firstChunk)||'').toLowerCase();
|
|
||||||
console.log("tls hello servername:", servername);
|
|
||||||
tryTls();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstChunk[0] > 32 && firstChunk[0] < 127) {
|
|
||||||
str = firstChunk.toString();
|
|
||||||
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
|
|
||||||
servername = (m && m[1].toLowerCase() || '').split(':')[0];
|
|
||||||
console.log('servername', servername);
|
|
||||||
if (/HTTP\//i.test(str)) {
|
|
||||||
service = 'http';
|
|
||||||
// TODO disallow http entirely
|
|
||||||
// /^\/\.well-known\/acme-challenge\//.test(str)
|
|
||||||
if (/well-known/.test(str)) {
|
|
||||||
// HTTP
|
|
||||||
if (Devices.exist(deviceLists, servername)) {
|
|
||||||
pipeWs(servername, service, browser, Devices.next(deviceLists, servername));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
copts.handleHttp(servername, browser);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// redirect to https
|
|
||||||
copts.handleInsecureHttp(servername, browser);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("Got unexpected connection", str);
|
|
||||||
browser.write(JSON.stringify({ error: {
|
|
||||||
message: "not sure what you were trying to do there..."
|
|
||||||
, code: 'E_INVALID_PROTOCOL' }
|
|
||||||
}));
|
|
||||||
browser.end();
|
|
||||||
});
|
|
||||||
browser.on('error', function (err) {
|
|
||||||
console.error('[error] tcp socket raw TODO forward and close');
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return { tcp: onTcpConnection, ws: onWsConnection };
|
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user