refactor and upgrade to proxy-packer

This commit is contained in:
AJ ONeal 2018-05-31 00:19:53 -06:00
parent 0c3f78147e
commit eada75a83e
4 changed files with 163 additions and 137 deletions

View File

@ -92,6 +92,20 @@ function connectTunnel() {
// what udp ports to forward // what udp ports to forward
// redirect http to https automatically // redirect http to https automatically
// redirect www to nowww automatically // redirect www to nowww automatically
if (state.config.http) {
Object.keys(state.config.http).forEach(function (hostname) {
if ('*' === hostname) {
state.config.servernames.forEach(function (servername) {
services.https[servername] = state.config.http[hostname];
services.http[servername] = 'redirect-https';
});
return;
}
services.https[hostname] = state.config.http[hostname];
services.http[hostname] = 'redirect-https';
});
}
/*
Object.keys(state.config.localPorts).forEach(function (port) { Object.keys(state.config.localPorts).forEach(function (port) {
var proto = state.config.localPorts[port]; var proto = state.config.localPorts[port];
if (!proto) { return; } if (!proto) { return; }
@ -113,6 +127,7 @@ function connectTunnel() {
//services[proxy.protocol][proxy.hostname] = proxy.port; //services[proxy.protocol][proxy.hostname] = proxy.port;
services[proto]['*'] = port; services[proto]['*'] = port;
}); });
*/
state.services = services; state.services = services;
Object.keys(services).forEach(function (protocol) { Object.keys(services).forEach(function (protocol) {
@ -127,6 +142,8 @@ function connectTunnel() {
// TODO Check undefined vs false for greenlock config // TODO Check undefined vs false for greenlock config
var tun = remote.connect({ var tun = remote.connect({
relay: state.config.relay relay: state.config.relay
, config: state.config
, sortingHat: state.config.sortingHat || './lib/sorting-hat.js'
, locals: state.config.servernames , locals: state.config.servernames
, services: state.services , services: state.services
, net: state.net , net: state.net

105
lib/sorting-hat.js Normal file
View File

@ -0,0 +1,105 @@
module.exports.assign = function (state, tun, cb) {
var net = state.net || require('net');
var service = tun.service.toLowerCase();
var portList = state.services[service];
var port;
if (!tun.name && !tun.serviceport) {
console.log('tun:\n',tun);
//console.warn(tun.data.toString());
cb(new Error("missing routing information for ':tun_id'"));
return;
}
port = portList[tun.name];
if (!port) {
// Check for any wildcard domains, sorted longest to shortest so the one with the
// biggest natural match will be found first.
Object.keys(portList).filter(function (pattern) {
return pattern[0] === '*' && pattern.length > 1;
}).sort(function (a, b) {
return b.length - a.length;
}).some(function (pattern) {
var subPiece = pattern.slice(1);
if (subPiece === tun.name.slice(-subPiece.length)) {
port = portList[pattern];
return true;
}
});
}
if (!port) {
port = portList['*'];
}
var createOpts = {
port: port
, host: '127.0.0.1'
, servername: tun.name
, name: tun.name
, serviceport: tun.serviceport
, data: tun.data
, remoteFamily: tun.family
, remoteAddress: tun.address
, remotePort: tun.port
};
var conn;
function handleNow(socket) {
var httpServer;
var tlsServer;
if ('https' === service) {
if (!state.greenlock) {
state.greenlock = require('greenlock').create(state.greenlockConfig);
}
httpServer = require('http').createServer(function (req, res) {
console.log('[hit http/s server]');
res.end('Hello, Encrypted Tunnel World!');
});
tlsServer = require('tls').createServer(state.greenlock.tlsOptions, function (tlsSocket) {
console.log('[hit tls server]');
httpServer.emit('connection', tlsSocket);
});
tlsServer.emit('connection', socket);
} else {
httpServer = require('http').createServer(state.greenlock.middleware(function (req, res) {
console.log('[hit pure http server]');
res.end('Hello, Encrypted Tunnel World!');
}));
// http://aj.telebit.cloud/.well-known/acme-challenge/blah
httpServer.emit('connection', socket);
}
}
if ('aj.telebit.cloud' === tun.name) {
console.log('NEW CONNECTION to AJ\'s telebit could');
// For performance it may be better to use socket-pair, needs testing
var socketPair = require('socket-pair');
conn = socketPair.create(function (err, other) {
if (err) { console.error('[Error] ' + err.message); }
handleNow(other);
if (createOpts.data) {
conn.write(createOpts.data);
}
});
/*
var streamPair = require('stream-pair');
var pair = streamPair.create();
conn = pair.other;
process.nextTick(function () {
if (createOpts.data) {
conn.write(createOpts.data);
}
});
*/
} else {
conn = net.createConnection(createOpts, function () {
// this will happen before 'data' or 'readable' is triggered
// We use the data from the createOpts object so that the createConnection function has
// the oppurtunity of removing/changing it if it wants/needs to handle it differently.
if (createOpts.data) {
conn.write(createOpts.data);
}
});
}
cb(null, conn);
};

View File

@ -53,9 +53,10 @@
"js-yaml": "^3.11.0", "js-yaml": "^3.11.0",
"jsonwebtoken": "^7.1.9", "jsonwebtoken": "^7.1.9",
"recase": "^1.0.4", "recase": "^1.0.4",
"redirect-https": "^1.1.5",
"sni": "^1.0.0", "sni": "^1.0.0",
"socket-pair": "^1.0.3", "socket-pair": "^1.0.3",
"tunnel-packer": "^1.2.0", "proxy-packer": "^1.4.3",
"ws": "^2.2.3" "ws": "^2.2.3"
} }
} }

175
remote.js
View File

@ -4,7 +4,7 @@
var WebSocket = require('ws'); var WebSocket = require('ws');
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var sni = require('sni'); var sni = require('sni');
var Packer = require('tunnel-packer'); var Packer = require('proxy-packer');
function timeoutPromise(duration) { function timeoutPromise(duration) {
return new PromiseA(function (resolve) { return new PromiseA(function (resolve) {
@ -12,16 +12,16 @@ function timeoutPromise(duration) {
}); });
} }
function run(copts) { function run(state) {
// jshint latedef:false // jshint latedef:false
var activityTimeout = copts.activityTimeout || (2*60 - 5)*1000; var activityTimeout = state.activityTimeout || (2*60 - 5)*1000;
var pongTimeout = copts.pongTimeout || 10*1000; var pongTimeout = state.pongTimeout || 10*1000;
// Allow the tunnel client to be created with no token. This will prevent the connection from // Allow the tunnel client to be created with no token. This will prevent the connection from
// being established initialy and allows the caller to use `.append` for the first token so // being established initialy and allows the caller to use `.append` for the first token so
// they can get a promise that will provide feedback about invalid tokens. // they can get a promise that will provide feedback about invalid tokens.
var tokens = []; var tokens = [];
if (copts.token) { if (state.token) {
tokens.push(copts.token); tokens.push(state.token);
} }
var wstunneler; var wstunneler;
@ -30,12 +30,13 @@ function run(copts) {
var localclients = {}; var localclients = {};
var pausedClients = []; var pausedClients = [];
var clientHandlers = { var clientHandlers = {
add: function (conn, cid, opts, servername) { add: function (conn, cid, tun) {
localclients[cid] = conn; localclients[cid] = conn;
console.info("[connect] new client '" + cid + "' for '" + servername + "' (" + clientHandlers.count() + " clients)"); console.info("[connect] new client '" + cid + "' for '" + tun.name + ":" + tun.serviceport + "' "
+ "(" + clientHandlers.count() + " clients)");
conn.tunnelCid = cid; conn.tunnelCid = cid;
conn.tunnelRead = opts.data.byteLength; conn.tunnelRead = tun.data.byteLength;
conn.tunnelWritten = 0; conn.tunnelWritten = 0;
conn.on('data', function onLocalData(chunk) { conn.on('data', function onLocalData(chunk) {
@ -50,7 +51,7 @@ function run(copts) {
// down the data we are getting to send over. We also want to pause all active connections // down the data we are getting to send over. We also want to pause all active connections
// if any connections are paused to make things more fair so one connection doesn't get // if any connections are paused to make things more fair so one connection doesn't get
// stuff waiting for all other connections to finish because it tried writing near the border. // stuff waiting for all other connections to finish because it tried writing near the border.
var bufSize = wsHandlers.sendMessage(Packer.pack(opts, chunk)); var bufSize = wsHandlers.sendMessage(Packer.pack(tun, chunk));
if (pausedClients.length || bufSize > 1024*1024) { if (pausedClients.length || bufSize > 1024*1024) {
// console.log('[onLocalData] paused connection', cid, 'to allow websocket to catch up'); // console.log('[onLocalData] paused connection', cid, 'to allow websocket to catch up');
conn.pause(); conn.pause();
@ -63,14 +64,14 @@ function run(copts) {
console.info("[onLocalEnd] connection '" + cid + "' ended, will probably close soon"); console.info("[onLocalEnd] connection '" + cid + "' ended, will probably close soon");
conn.tunnelClosing = true; conn.tunnelClosing = true;
if (!sentEnd) { if (!sentEnd) {
wsHandlers.sendMessage(Packer.pack(opts, null, 'end')); wsHandlers.sendMessage(Packer.pack(tun, null, 'end'));
sentEnd = true; sentEnd = true;
} }
}); });
conn.on('error', function onLocalError(err) { conn.on('error', function onLocalError(err) {
console.info("[onLocalError] connection '" + cid + "' errored:", err); console.info("[onLocalError] connection '" + cid + "' errored:", err);
if (!sentEnd) { if (!sentEnd) {
wsHandlers.sendMessage(Packer.pack(opts, {message: err.message, code: err.code}, 'error')); wsHandlers.sendMessage(Packer.pack(tun, {message: err.message, code: err.code}, 'error'));
sentEnd = true; sentEnd = true;
} }
}); });
@ -78,7 +79,7 @@ function run(copts) {
delete localclients[cid]; delete localclients[cid];
console.log('[onLocalClose] closed "' + cid + '" read:'+conn.tunnelRead+', wrote:'+conn.tunnelWritten+' (' + clientHandlers.count() + ' clients)'); console.log('[onLocalClose] closed "' + cid + '" read:'+conn.tunnelRead+', wrote:'+conn.tunnelWritten+' (' + clientHandlers.count() + ' clients)');
if (!sentEnd) { if (!sentEnd) {
wsHandlers.sendMessage(Packer.pack(opts, null, hadErr && 'error' || 'end')); wsHandlers.sendMessage(Packer.pack(tun, null, hadErr && 'error' || 'end'));
sentEnd = true; sentEnd = true;
} }
}); });
@ -254,132 +255,34 @@ function run(copts) {
wsHandlers.sendMessage(Packer.pack(null, [-cmd[0], err], 'control')); wsHandlers.sendMessage(Packer.pack(null, [-cmd[0], err], 'control'));
} }
, onmessage: function (opts) { , onmessage: function (tun) {
var net = copts.net || require('net'); var cid = tun._id = Packer.addrToId(tun);
var cid = Packer.addrToId(opts);
var service = opts.service.toLowerCase();
var portList = copts.services[service];
var servername;
var port;
var str; var str;
var m; var m;
if (clientHandlers.write(cid, opts)) { if ('http' === tun.service) {
return; str = tun.data.toString();
}
if (!portList) {
packerHandlers._onConnectError(cid, opts, new Error("unsupported service '" + service + "'"));
return;
}
if ('http' === service) {
str = opts.data.toString();
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
servername = (m && m[1].toLowerCase() || '').split(':')[0]; tun._name = tun._hostname = (m && m[1].toLowerCase() || '').split(':')[0];
} }
else if ('https' === service) { else if ('https' === tun.service || 'tls' === tun.service) {
servername = sni(opts.data); tun._name = tun._servername = sni(tun.data);
}
else {
servername = '*';
}
if (!servername) {
//console.warn(opts.data.toString());
packerHandlers._onConnectError(cid, opts, new Error("missing servername for '" + cid + "' " + opts.data.byteLength));
return;
}
port = portList[servername];
if (!port) {
// Check for any wildcard domains, sorted longest to shortest so the one with the
// biggest natural match will be found first.
Object.keys(portList).filter(function (pattern) {
return pattern[0] === '*' && pattern.length > 1;
}).sort(function (a, b) {
return b.length - a.length;
}).some(function (pattern) {
var subPiece = pattern.slice(1);
if (subPiece === servername.slice(-subPiece.length)) {
port = portList[pattern];
return true;
}
});
}
if (!port) {
port = portList['*'];
}
var createOpts = {
port: port
, host: '127.0.0.1'
, servername: servername
, data: opts.data
, remoteFamily: opts.family
, remoteAddress: opts.address
, remotePort: opts.port
};
var conn;
function handleNow(socket) {
var httpServer;
var tlsServer;
if ('https' === service) {
if (!copts.greenlock) {
copts.greenlock = require('greenlock').create(copts.greenlockConfig);
}
httpServer = require('http').createServer(function (req, res) {
console.log('[hit http/s server]');
res.end('Hello, Encrypted Tunnel World!');
});
tlsServer = require('tls').createServer(copts.greenlock.tlsOptions, function (tlsSocket) {
console.log('[hit tls server]');
httpServer.emit('connection', tlsSocket);
});
tlsServer.emit('connection', socket);
} else {
httpServer = require('http').createServer(copts.greenlock.middleware(function (req, res) {
console.log('[hit pure http server]');
res.end('Hello, Encrypted Tunnel World!');
}));
// http://aj.telebit.cloud/.well-known/acme-challenge/blah
httpServer.emit('connection', socket);
}
}
if ('aj.telebit.cloud' === servername) {
console.log('NEW CONNECTION to AJ\'s telebit could');
// For performance it may be better to use socket-pair, needs testing
var socketPair = require('socket-pair');
conn = socketPair.create(function (err, other) {
if (err) { console.error('[Error] ' + err.message); }
handleNow(other);
if (createOpts.data) {
conn.write(createOpts.data);
}
});
/*
var streamPair = require('stream-pair');
var pair = streamPair.create();
conn = pair.other;
process.nextTick(function () {
if (createOpts.data) {
conn.write(createOpts.data);
}
});
*/
} else { } else {
conn = net.createConnection(createOpts, function () { tun._name = '';
// this will happen before 'data' or 'readable' is triggered
// We use the data from the createOpts object so that the createConnection function has
// the oppurtunity of removing/changing it if it wants/needs to handle it differently.
if (createOpts.data) {
conn.write(createOpts.data);
}
});
} }
clientHandlers.add(conn, cid, opts, servername); if (clientHandlers.write(cid, tun)) { return; }
wstunneler.pause();
require(state.sortingHat).assign(state, tun, function (err, conn) {
if (err) {
err.message = err.message.replace(/:tun_id/, tun._id);
packerHandlers._onConnectError(cid, tun, err);
return;
}
clientHandlers.add(conn, cid, tun);
wstunneler.resume();
});
} }
, onpause: function (opts) { , onpause: function (opts) {
@ -465,7 +368,7 @@ function run(copts) {
} }
, onOpen: function () { , onOpen: function () {
console.info("[open] connected to '" + copts.relay + "'"); console.info("[open] connected to '" + state.relay + "'");
wsHandlers.refreshTimeout(); wsHandlers.refreshTimeout();
timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout); timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout);
@ -549,11 +452,11 @@ function run(copts) {
return; return;
} }
timeoutId = null; timeoutId = null;
var machine = require('tunnel-packer').create(packerHandlers); var machine = Packer.create(packerHandlers);
console.info("[connect] '" + copts.relay + "'"); console.info("[connect] '" + state.relay + "'");
var tunnelUrl = copts.relay.replace(/\/$/, '') + '/?access_token=' + tokens[0]; var tunnelUrl = state.relay.replace(/\/$/, '') + '/?access_token=' + tokens[0];
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !copts.insecure }); wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure });
wstunneler.on('open', wsHandlers.onOpen); wstunneler.on('open', wsHandlers.onOpen);
wstunneler.on('close', wsHandlers.onClose); wstunneler.on('close', wsHandlers.onClose);
wstunneler.on('error', wsHandlers.onError); wstunneler.on('error', wsHandlers.onError);