Compare commits
8 Commits
commercial
...
master
Author | SHA1 | Date |
---|---|---|
AJ ONeal | 5a395a299a | |
Maciej Krüger | eb36af8269 | |
AJ ONeal | 50c0449206 | |
AJ ONeal | d48707d265 | |
AJ ONeal | 60f85144a9 | |
AJ ONeal | 5dfe25ed95 | |
AJ ONeal | c9d6b46f0f | |
AJ ONeal | 0a67728239 |
|
@ -43,3 +43,12 @@ jspm_packages
|
|||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Snapcraft
|
||||
/parts/
|
||||
/prime/
|
||||
/stage/
|
||||
.snapcraft
|
||||
*.snap
|
||||
*.tar.bz2
|
||||
|
||||
|
|
|
@ -47,7 +47,13 @@ function applyConfig(config) {
|
|||
} else {
|
||||
state.Promise = require('bluebird');
|
||||
}
|
||||
state.tlsOptions = {}; // TODO just close the sockets that would use this early? or use the admin servername
|
||||
state.tlsOptions = {
|
||||
// Handles disconnected devices
|
||||
// TODO allow user to opt-in to wildcard hosting for a better error page?
|
||||
SNICallback: function (servername, cb) {
|
||||
return state.greenlock.tlsOptions.SNICallback(state.config.webminDomain || state.servernames[0], cb);
|
||||
}
|
||||
}; // TODO just close the sockets that would use this early? or use the admin servername
|
||||
state.config = config;
|
||||
state.servernames = config.servernames || [];
|
||||
state.secret = state.config.secret;
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
var timeago = require('./ago.js').AGO;
|
||||
|
||||
function test() {
|
||||
[ 1.5 * 1000 // a moment ago
|
||||
, 4.5 * 1000 // moments ago
|
||||
, 10 * 1000 // 10 seconds ago
|
||||
, 59 * 1000 // a minute ago
|
||||
, 60 * 1000 // a minute ago
|
||||
, 61 * 1000 // a minute ago
|
||||
, 119 * 1000 // a minute ago
|
||||
, 120 * 1000 // 2 minutes ago
|
||||
, 121 * 1000 // 2 minutes ago
|
||||
, (60 * 60 * 1000) - 1000 // 59 minutes ago
|
||||
, 1 * 60 * 60 * 1000 // an hour ago
|
||||
, 1.5 * 60 * 60 * 1000 // an hour ago
|
||||
, 2.5 * 60 * 60 * 1000 // 2 hours ago
|
||||
, 1.5 * 24 * 60 * 60 * 1000 // a day ago
|
||||
, 2.5 * 24 * 60 * 60 * 1000 // 2 days ago
|
||||
, 7 * 24 * 60 * 60 * 1000 // a week ago
|
||||
, 14 * 24 * 60 * 60 * 1000 // 2 weeks ago
|
||||
, 27 * 24 * 60 * 60 * 1000 // 3 weeks ago
|
||||
, 28 * 24 * 60 * 60 * 1000 // 4 weeks ago
|
||||
, 29 * 24 * 60 * 60 * 1000 // 4 weeks ago
|
||||
, 1.5 * 30 * 24 * 60 * 60 * 1000 // a month ago
|
||||
, 2.5 * 30 * 24 * 60 * 60 * 1000 // 2 months ago
|
||||
, (12 * 30 * 24 * 60 * 60 * 1000) + 1000 // 12 months ago
|
||||
, 13 * 30 * 24 * 60 * 60 * 1000 // over a year ago
|
||||
].forEach(function (d) {
|
||||
console.log(d, '=', timeago(d));
|
||||
});
|
||||
}
|
||||
|
||||
test();
|
|
@ -0,0 +1,50 @@
|
|||
;(function (exports) {
|
||||
'use strict';
|
||||
|
||||
exports.AGO = function timeago(ms) {
|
||||
var ago = Math.floor(ms / 1000);
|
||||
var part = 0;
|
||||
|
||||
if (ago < 2) { return "a moment ago"; }
|
||||
if (ago < 5) { return "moments ago"; }
|
||||
if (ago < 60) { return ago + " seconds ago"; }
|
||||
|
||||
if (ago < 120) { return "a minute ago"; }
|
||||
if (ago < 3600) {
|
||||
while (ago >= 60) { ago -= 60; part += 1; }
|
||||
return part + " minutes ago";
|
||||
}
|
||||
|
||||
if (ago < 7200) { return "an hour ago"; }
|
||||
if (ago < 86400) {
|
||||
while (ago >= 3600) { ago -= 3600; part += 1; }
|
||||
return part + " hours ago";
|
||||
}
|
||||
|
||||
if (ago < 172800) { return "a day ago"; }
|
||||
if (ago < 604800) {
|
||||
while (ago >= 172800) { ago -= 172800; part += 1; }
|
||||
return part + " days ago";
|
||||
}
|
||||
|
||||
if (ago < 1209600) { return "a week ago"; }
|
||||
if (ago < 2592000) {
|
||||
while (ago >= 604800) { ago -= 604800; part += 1; }
|
||||
return part + " weeks ago";
|
||||
}
|
||||
|
||||
if (ago < 5184000) { return "a month ago"; }
|
||||
if (ago < 31536001) {
|
||||
while (ago >= 2592000) { ago -= 2592000; part += 1; }
|
||||
return part + " months ago";
|
||||
}
|
||||
|
||||
if (ago < 315360000) { // 10 years
|
||||
return "more than year ago";
|
||||
}
|
||||
|
||||
// TODO never
|
||||
return "";
|
||||
};
|
||||
|
||||
}('undefined' !== typeof module ? module.exports : window));
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var Devices = module.exports;
|
||||
// TODO enumerate store's keys and device's keys for documentation
|
||||
Devices.addPort = function (store, serverport, newDevice) {
|
||||
// TODO make special
|
||||
return Devices.add(store, serverport, newDevice, true);
|
||||
|
@ -14,6 +15,7 @@ Devices.add = function (store, servername, newDevice, isPort) {
|
|||
if (!store._domains) { store._domains = {}; }
|
||||
if (!store._domains[servername]) { store._domains[servername] = []; }
|
||||
store._domains[servername].push(newDevice);
|
||||
Devices.touch(store, servername);
|
||||
|
||||
// add device
|
||||
// TODO only use a device id
|
||||
|
@ -126,7 +128,11 @@ Devices.active = function (store, id) {
|
|||
};
|
||||
*/
|
||||
Devices.exist = function (store, servername) {
|
||||
return !!(Devices.list(store, servername).length);
|
||||
if (Devices.list(store, servername).length) {
|
||||
Devices.touch(store, servername);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Devices.next = function (store, servername) {
|
||||
var devices = Devices.list(store, servername);
|
||||
|
@ -138,5 +144,20 @@ Devices.next = function (store, servername) {
|
|||
device = devices[devices._index || 0];
|
||||
devices._index = (devices._index || 0) + 1;
|
||||
|
||||
if (device) { Devices.touch(store, servername); }
|
||||
return device;
|
||||
};
|
||||
Devices.touchDevice = function (store, device) {
|
||||
// TODO use device.id (which will be pubkey thumbprint) and store._devices[id].domainsMap
|
||||
Object.keys(device.domainsMap).forEach(function (servername) {
|
||||
Devices.touch(store, servername);
|
||||
});
|
||||
};
|
||||
Devices.touch = function (store, servername) {
|
||||
if (!store._recency) { store._recency = {}; }
|
||||
store._recency[servername] = Date.now();
|
||||
};
|
||||
Devices.lastSeen = function (store, servername) {
|
||||
if (!store._recency) { store._recency = {}; }
|
||||
return store._recency[servername] || 0;
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ function noSniCallback(tag) {
|
|||
var err = new Error("[noSniCallback] no handler set for '" + tag + "':'" + servername + "'");
|
||||
console.error(err.message);
|
||||
cb(new Error(err));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports.create = function (state) {
|
||||
|
@ -72,38 +72,44 @@ module.exports.create = function (state) {
|
|||
state.tlsInvalidSniServer.on('tlsClientError', function () {
|
||||
console.error('tlsClientError InvalidSniServer');
|
||||
});
|
||||
state.httpsInvalid = function (servername, socket) {
|
||||
state.createHttpInvalid = function (opts) {
|
||||
return http.createServer(function (req, res) {
|
||||
if (!opts.servername) {
|
||||
res.statusCode = 422;
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO use req.headers.host instead of servername (since domain fronting is disabled anyway)
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.end(
|
||||
"<h1>Oops!</h1>"
|
||||
+ "<p>It looks like '" + encodeURIComponent(opts.servername) + "' isn't connected right now.</p>"
|
||||
+ "<p><small>Last seen: " + opts.ago + "</small></p>"
|
||||
+ "<p><small>Error: 502 Bad Gateway</small></p>"
|
||||
);
|
||||
});
|
||||
};
|
||||
state.httpsInvalid = function (opts, socket) {
|
||||
// none of these methods work:
|
||||
// httpsServer.emit('connection', socket); // this didn't work
|
||||
// tlsServer.emit('connection', socket); // this didn't work either
|
||||
//console.log('chunkLen', firstChunk.byteLength);
|
||||
|
||||
console.log('[httpsInvalid] servername', servername);
|
||||
console.log('[httpsInvalid] servername', opts.servername);
|
||||
//state.tlsInvalidSniServer.emit('connection', wrapSocket(socket));
|
||||
var tlsInvalidSniServer = tls.createServer(state.tlsOptions, function (tlsSocket) {
|
||||
console.log('[tlsInvalid] 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;
|
||||
}
|
||||
|
||||
res.end(
|
||||
"You came in hot looking for '" + servername + "' and, granted, the IP address for that domain"
|
||||
+ " 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.");
|
||||
});
|
||||
httpInvalidSniServer.emit('connection', tlsSocket);
|
||||
// We create an entire http server object because it's difficult to figure out
|
||||
// how to access the original tlsSocket to get the servername
|
||||
state.createHttpInvalid(opts).emit('connection', tlsSocket);
|
||||
});
|
||||
tlsInvalidSniServer.on('tlsClientError', function () {
|
||||
console.error('tlsClientError InvalidSniServer httpsInvalid');
|
||||
|
|
|
@ -172,6 +172,7 @@ var Server = {
|
|||
, _initSocketHandlers: function (state, srv) {
|
||||
function refreshTimeout() {
|
||||
srv.lastActivity = Date.now();
|
||||
Devices.touchDevice(state.deviceLists, srv);
|
||||
}
|
||||
|
||||
function checkTimeout() {
|
||||
|
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
var sni = require('sni');
|
||||
var pipeWs = require('./pipe-ws.js');
|
||||
var ago = require('./ago.js').AGO;
|
||||
var up = Date.now();
|
||||
|
||||
function fromUptime(ms) {
|
||||
if (ms) {
|
||||
return ago(Date.now() - ms);
|
||||
} else {
|
||||
return "Not seen since relay restarted, " + ago(Date.now() - up);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.createTcpConnectionHandler = function (state) {
|
||||
var Devices = state.Devices;
|
||||
|
@ -27,6 +37,16 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
var str;
|
||||
var m;
|
||||
|
||||
if (!firstChunk) {
|
||||
try {
|
||||
conn.end();
|
||||
} catch(e) {
|
||||
console.error("[lib/unwrap-tls.js] Error:", e);
|
||||
conn.destroy();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//conn.pause();
|
||||
conn.unshift(firstChunk);
|
||||
|
||||
|
@ -38,8 +58,15 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
|
||||
// defer after return (instead of being in many places)
|
||||
function deferData(fn) {
|
||||
if (fn) {
|
||||
if ('httpsInvalid' === fn) {
|
||||
state[fn]({
|
||||
servername: servername
|
||||
, ago: fromUptime(Devices.lastSeen(state.deviceLists, servername))
|
||||
}, conn);
|
||||
} else if (fn) {
|
||||
state[fn](servername, conn);
|
||||
} else {
|
||||
console.error("[SANITY ERROR] '" + fn + "' doesn't have a state handler");
|
||||
}
|
||||
/*
|
||||
process.nextTick(function () {
|
||||
|
@ -48,33 +75,81 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
*/
|
||||
}
|
||||
|
||||
function tryTls() {
|
||||
var vhost;
|
||||
|
||||
if (!state.servernames.length) {
|
||||
console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)");
|
||||
deferData('httpsSetupServer');
|
||||
return;
|
||||
var httpOutcomes = {
|
||||
missingServername: function () {
|
||||
console.log("[debug] [http] missing servername");
|
||||
// TODO use a more specific error page
|
||||
deferData('handleInsecureHttp');
|
||||
}
|
||||
|
||||
if (-1 !== state.servernames.indexOf(servername)) {
|
||||
if (state.debug) { console.log("[Admin]", servername); }
|
||||
deferData('httpsTunnel');
|
||||
return;
|
||||
, requiresSetup: function () {
|
||||
console.log("[debug] [http] requires setup");
|
||||
// TODO Insecure connections for setup will not work on secure domains (i.e. .app)
|
||||
state.httpSetupServer.emit('connection', conn);
|
||||
}
|
||||
|
||||
if (state.config.nowww && /^www\./i.test(servername)) {
|
||||
console.log("TODO: use www bare redirect");
|
||||
, isInternal: function () {
|
||||
console.log("[debug] [http] is known internally (admin)");
|
||||
if (/well-known/.test(str)) {
|
||||
deferData('handleHttp');
|
||||
} else {
|
||||
deferData('handleInsecureHttp');
|
||||
}
|
||||
}
|
||||
, isVhost: function () {
|
||||
console.log("[debug] [http] is vhost (normal server)");
|
||||
if (/well-known/.test(str)) {
|
||||
deferData('handleHttp');
|
||||
} else {
|
||||
deferData('handleInsecureHttp');
|
||||
}
|
||||
}
|
||||
, assumeExternal: function () {
|
||||
console.log("[debug] [http] assume external");
|
||||
var service = 'http';
|
||||
|
||||
if (!servername) {
|
||||
if (!Devices.exist(state.deviceLists, servername)) {
|
||||
// It would be better to just re-read the host header rather
|
||||
// than creating a whole server object, but this is a "rare"
|
||||
// case and I'm feeling lazy right now.
|
||||
console.log("[debug] [http] no device connected");
|
||||
state.createHttpInvalid({
|
||||
servername: servername
|
||||
, ago: fromUptime(Devices.lastSeen(state.deviceLists, servername))
|
||||
}).emit('connection', conn);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO make https redirect configurable on a per-domain basis
|
||||
// /^\/\.well-known\/acme-challenge\//.test(str)
|
||||
if (/well-known/.test(str)) {
|
||||
// HTTP
|
||||
console.log("[debug] [http] passthru");
|
||||
pipeWs(servername, service, Devices.next(state.deviceLists, servername), conn, serviceport);
|
||||
return;
|
||||
} else {
|
||||
console.log("[debug] [http] redirect to https");
|
||||
deferData('handleInsecureHttp');
|
||||
}
|
||||
}
|
||||
};
|
||||
var tlsOutcomes = {
|
||||
missingServername: function () {
|
||||
if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); }
|
||||
deferData('httpsInvalid');
|
||||
return;
|
||||
}
|
||||
|
||||
function run() {
|
||||
var nextDevice = Devices.next(state.deviceLists, servername);
|
||||
, requiresSetup: function () {
|
||||
console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)");
|
||||
deferData('httpsSetupServer');
|
||||
}
|
||||
, isInternal: function () {
|
||||
if (state.debug) { console.log("[Admin]", servername); }
|
||||
deferData('httpsTunnel');
|
||||
}
|
||||
, isVhost: function (vhost) {
|
||||
if (state.debug) { console.log("[tcp] [vhost]", state.config.vhost, "=>", vhost); }
|
||||
deferData('httpsVhost');
|
||||
}
|
||||
, assumeExternal: function () {
|
||||
var nextDevice = Devices.next(state.deviceLists, servername);
|
||||
if (!nextDevice) {
|
||||
if (state.debug) { console.log("No devices match the given servername"); }
|
||||
deferData('httpsInvalid');
|
||||
|
@ -82,27 +157,33 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
}
|
||||
|
||||
if (state.debug) { console.log("pipeWs(servername, service, deviceLists['" + servername + "'], socket)"); }
|
||||
deferData();
|
||||
pipeWs(servername, service, nextDevice, conn, serviceport);
|
||||
}
|
||||
};
|
||||
|
||||
function handleConnection(outcomes) {
|
||||
var vhost;
|
||||
|
||||
// No routing information available
|
||||
if (!servername) { outcomes.missingServername(); return; }
|
||||
// Server needs to be set up
|
||||
if (!state.servernames.length) { outcomes.requiresSetup(); return; }
|
||||
// This is one of the admin domains
|
||||
if (-1 !== state.servernames.indexOf(servername)) { outcomes.isInternal(); return; }
|
||||
|
||||
// TODO don't run an fs check if we already know this is working elsewhere
|
||||
//if (!state.validHosts) { state.validHosts = {}; }
|
||||
if (state.config.vhost) {
|
||||
vhost = state.config.vhost.replace(/:hostname/, (servername||'reallydoesntexist'));
|
||||
if (state.debug) { console.log("[tcp] [vhost]", state.config.vhost, "=>", vhost); }
|
||||
//state.httpsVhost(servername, conn);
|
||||
//return;
|
||||
vhost = state.config.vhost.replace(/:hostname/, servername);
|
||||
require('fs').readdir(vhost, function (err, nodes) {
|
||||
if (state.debug && err) { console.log("VHOST error", err); }
|
||||
if (err || !nodes) { run(); return; }
|
||||
//if (nodes) { deferData('httpsVhost'); return; }
|
||||
deferData('httpsVhost');
|
||||
if (err || !nodes) { outcomes.assumeExternal(); return; }
|
||||
outcomes.isVhost(vhost);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
run();
|
||||
outcomes.assumeExternal();
|
||||
}
|
||||
|
||||
// https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
|
||||
|
@ -111,40 +192,19 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
service = 'https';
|
||||
servername = (sni(firstChunk)||'').toLowerCase().trim();
|
||||
if (state.debug) { console.log("[tcp] tls hello from '" + servername + "'"); }
|
||||
tryTls();
|
||||
handleConnection(tlsOutcomes);
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstChunk[0] > 32 && firstChunk[0] < 127) {
|
||||
// (probably) HTTP
|
||||
str = firstChunk.toString();
|
||||
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
|
||||
servername = (m && m[1].toLowerCase() || '').split(':')[0];
|
||||
if (state.debug) { console.log("[tcp] http hostname '" + servername + "'"); }
|
||||
|
||||
if (/HTTP\//i.test(str)) {
|
||||
if (!state.servernames.length) {
|
||||
console.info("[tcp] No admin servername. Entering setup mode.");
|
||||
deferData();
|
||||
state.httpSetupServer.emit('connection', conn);
|
||||
return;
|
||||
}
|
||||
|
||||
service = 'http';
|
||||
// TODO make https redirect configurable
|
||||
// /^\/\.well-known\/acme-challenge\//.test(str)
|
||||
if (/well-known/.test(str)) {
|
||||
// HTTP
|
||||
if (Devices.exist(state.deviceLists, servername)) {
|
||||
deferData();
|
||||
pipeWs(servername, service, Devices.next(state.deviceLists, servername), conn, serviceport);
|
||||
return;
|
||||
}
|
||||
deferData('handleHttp');
|
||||
return;
|
||||
}
|
||||
|
||||
// redirect to https
|
||||
deferData('handleInsecureHttp');
|
||||
handleConnection(httpOutcomes);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
name: telebit-relay
|
||||
version: '0.20.0'
|
||||
summary: Because friends don't let friends localhost
|
||||
description: |
|
||||
A server that works in combination with Telebit Remote
|
||||
to allow you to serve http and https from any computer,
|
||||
anywhere through a secure tunnel.
|
||||
|
||||
grade: stable
|
||||
confinement: strict
|
||||
|
||||
apps:
|
||||
telebit-relay:
|
||||
command: telebit-relay --config $SNAP_COMMON/config.yml
|
||||
plugs: [network, network-bind]
|
||||
daemon: simple
|
||||
|
||||
parts:
|
||||
telebit-relay:
|
||||
plugin: nodejs
|
||||
node-engine: 10.13.0
|
||||
source: .
|
||||
override-build: |
|
||||
snapcraftctl build
|
Loading…
Reference in New Issue