mirror of
https://git.coolaj86.com/coolaj86/telebit-relay.js.git
synced 2025-12-24 18:48:39 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07965d8eac | |||
| 98d31fc8d7 | |||
| 986102c79e | |||
| 4a2199dc71 | |||
| 6b25c7e1f5 | |||
| 7f3c8ee96d | |||
| 2b62ac2109 | |||
| c300636477 |
@ -1,3 +1,4 @@
|
|||||||
|
| Sponsored by [ppl](https://ppl.family) | **tunnel-server.js** | [tunnel-client.js](https://git.coolaj86.com/coolaj86/tunnel-client.js) |
|
||||||
# stunneld.js
|
# stunneld.js
|
||||||
|
|
||||||
A server that works in combination with [stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js)
|
A server that works in combination with [stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js)
|
||||||
@ -20,6 +21,12 @@ 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?)
|
*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`:
|
||||||
|
|||||||
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
|
||||||
@ -96,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);
|
||||||
@ -145,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
|
||||||
|
|||||||
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stunneld",
|
"name": "stunneld",
|
||||||
"version": "0.9.2",
|
"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": {
|
||||||
|
|||||||
222
wstunneld.js
222
wstunneld.js
@ -1,6 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var sni = require('sni');
|
|
||||||
var url = require('url');
|
var url = require('url');
|
||||||
var PromiseA = require('bluebird');
|
var PromiseA = require('bluebird');
|
||||||
var jwt = require('jsonwebtoken');
|
var jwt = require('jsonwebtoken');
|
||||||
@ -12,68 +11,20 @@ function timeoutPromise(duration) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var Devices = {};
|
var Devices = require('./lib/device-tracker');
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 socketId = packer.socketToId(ws.upgradeReq.socket);
|
console.log(ws);
|
||||||
|
var socketId = packer.socketToId(upgradeReq.socket);
|
||||||
var remotes = {};
|
var remotes = {};
|
||||||
|
|
||||||
function logName() {
|
function logName() {
|
||||||
@ -178,6 +129,7 @@ module.exports.create = function (copts) {
|
|||||||
// domains and the list of all this websocket's remotes.
|
// domains and the list of all this websocket's remotes.
|
||||||
token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(',');
|
token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(',');
|
||||||
token.ws = ws;
|
token.ws = ws;
|
||||||
|
token.upgradeReq = upgradeReq;
|
||||||
token.clients = {};
|
token.clients = {};
|
||||||
|
|
||||||
token.pausedConns = [];
|
token.pausedConns = [];
|
||||||
@ -202,7 +154,7 @@ module.exports.create = function (copts) {
|
|||||||
|
|
||||||
token.domains.forEach(function (domainname) {
|
token.domains.forEach(function (domainname) {
|
||||||
console.log('domainname', domainname);
|
console.log('domainname', domainname);
|
||||||
Devices.add(deviceLists, domainname, token);
|
Devices.add(copts.deviceLists, domainname, token);
|
||||||
});
|
});
|
||||||
remotes[jwtoken] = token;
|
remotes[jwtoken] = token;
|
||||||
console.log("added token '" + token.deviceId + "' to websocket", socketId);
|
console.log("added token '" + token.deviceId + "' to websocket", socketId);
|
||||||
@ -218,9 +170,10 @@ module.exports.create = function (copts) {
|
|||||||
// Prevent any more browser connections being sent to this remote, and any existing
|
// Prevent any more browser connections being sent to this remote, and any existing
|
||||||
// connections from trying to send more data across the connection.
|
// connections from trying to send more data across the connection.
|
||||||
remote.domains.forEach(function (domainname) {
|
remote.domains.forEach(function (domainname) {
|
||||||
Devices.remove(deviceLists, domainname, remote);
|
Devices.remove(copts.deviceLists, domainname, remote);
|
||||||
});
|
});
|
||||||
remote.ws = null;
|
remote.ws = null;
|
||||||
|
remote.upgradeReq = null;
|
||||||
|
|
||||||
// Close all of the existing browser connections associated with this websocket connection.
|
// Close all of the existing browser connections associated with this websocket connection.
|
||||||
Object.keys(remote.clients).forEach(function (cid) {
|
Object.keys(remote.clients).forEach(function (cid) {
|
||||||
@ -232,7 +185,7 @@ module.exports.create = function (copts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var firstToken;
|
var firstToken;
|
||||||
var authn = (ws.upgradeReq.headers.authorization||'').split(/\s+/);
|
var authn = (upgradeReq.headers.authorization||'').split(/\s+/);
|
||||||
if (authn[0] && 'basic' === authn[0].toLowerCase()) {
|
if (authn[0] && 'basic' === authn[0].toLowerCase()) {
|
||||||
try {
|
try {
|
||||||
authn = new Buffer(authn[1], 'base64').toString('ascii').split(':');
|
authn = new Buffer(authn[1], 'base64').toString('ascii').split(':');
|
||||||
@ -240,7 +193,7 @@ module.exports.create = function (copts) {
|
|||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
}
|
}
|
||||||
if (!firstToken) {
|
if (!firstToken) {
|
||||||
firstToken = url.parse(ws.upgradeReq.url, true).query.access_token;
|
firstToken = url.parse(upgradeReq.url, true).query.access_token;
|
||||||
}
|
}
|
||||||
if (firstToken) {
|
if (firstToken) {
|
||||||
var err = addToken(firstToken);
|
var err = addToken(firstToken);
|
||||||
@ -434,154 +387,9 @@ module.exports.create = function (copts) {
|
|||||||
sendTunnelMsg(null, [1, 'hello', [unpacker._version], Object.keys(commandHandlers)], 'control');
|
sendTunnelMsg(null, [1, 'hello', [unpacker._version], Object.keys(commandHandlers)], 'control');
|
||||||
}
|
}
|
||||||
|
|
||||||
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.ws.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];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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(deviceLists, servername)) {
|
|
||||||
pipeWs(servername, service, conn, Devices.next(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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tcp: onTcpConnection
|
tcp: onTcpConnection
|
||||||
, ws: onWsConnection
|
, ws: onWsConnection
|
||||||
, isClientDomain: Devices.exist.bind(null, deviceLists)
|
, isClientDomain: Devices.exist.bind(null, copts.deviceLists)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user