Compare commits
13 Commits
Author | SHA1 | Date |
---|---|---|
AJ ONeal | e6e8113cca | |
AJ ONeal | 0fa68eef1e | |
AJ ONeal | ae43b0859b | |
AJ ONeal | 36ab30c9f2 | |
AJ ONeal | d60b458f47 | |
AJ ONeal | 3773abdfdb | |
AJ ONeal | db4e5c4f60 | |
AJ ONeal | cda10951d8 | |
AJ ONeal | 6b6ffd4647 | |
AJ ONeal | 5b90d5ef38 | |
AJ ONeal | a3b8cd6799 | |
AJ ONeal | 6e60fc1750 | |
AJ ONeal | 6c2f039533 |
|
@ -43,12 +43,3 @@ jspm_packages
|
|||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Snapcraft
|
||||
/parts/
|
||||
/prime/
|
||||
/stage/
|
||||
.snapcraft
|
||||
*.snap
|
||||
*.tar.bz2
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
, "immed": true
|
||||
, "undef": true
|
||||
, "unused": true
|
||||
, "latedef": "nofunc"
|
||||
, "latedef": true
|
||||
, "curly": true
|
||||
, "trailing": true
|
||||
}
|
||||
|
|
20
README.md
20
README.md
|
@ -173,23 +173,3 @@ The user and group `telebit` should be created.
|
|||
# Linux
|
||||
sudo setcap 'cap_net_bind_service=+ep' $(which node)
|
||||
```
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
The authentication method is abstract so that it can easily be implemented for various users and use cases.
|
||||
|
||||
```
|
||||
// bin/telebit-relay.js
|
||||
state.authenticate() // calls either state.extensions.authenticate or state.defaults.authenticate
|
||||
// which, in turn, calls Server.onAuth()
|
||||
|
||||
state.extensions = require('../lib/extensions');
|
||||
state.extensions.authenticate({
|
||||
state: state // lib/relay.js in-memory state
|
||||
, auth: 'xyz.abc.123' // arbitrary token, typically a JWT (default handler)
|
||||
})
|
||||
|
||||
// lib/relay.js
|
||||
Server.onAuth(state, srv, rawAuth, validatedTokenData);
|
||||
```
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var os = require('os');
|
||||
|
||||
var pkg = require('../package.json');
|
||||
|
||||
var argv = process.argv.slice(2);
|
||||
|
@ -47,13 +51,7 @@ function applyConfig(config) {
|
|||
} else {
|
||||
state.Promise = require('bluebird');
|
||||
}
|
||||
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.tlsOptions = {}; // 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;
|
||||
|
@ -73,54 +71,59 @@ function applyConfig(config) {
|
|||
state.config.greenlock.configDir = require('os').homedir() + require('path').sep + 'acme';
|
||||
}
|
||||
|
||||
// The domains being approved for the first time are listed in opts.domains
|
||||
// Certs being renewed are listed in certs.altnames
|
||||
function approveDomains(opts, certs, cb) {
|
||||
if (state.debug) { console.log('[debug] approveDomains', opts.domains); }
|
||||
// This is where you check your database and associated
|
||||
// email addresses with domains and agreements and such
|
||||
|
||||
// The domains being approved for the first time are listed in opts.domains
|
||||
// Certs being renewed are listed in certs.altnames
|
||||
if (certs) {
|
||||
opts.domains = certs.altnames;
|
||||
cb(null, { options: opts, certs: certs });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.validHosts) { state.validHosts = {}; }
|
||||
if (!state.validHosts[opts.domains[0]] && state.config.vhost) {
|
||||
if (state.debug) { console.log('[sni] vhost checking is turned on'); }
|
||||
var vhost = state.config.vhost.replace(/:hostname/, opts.domains[0]);
|
||||
require('fs').readdir(vhost, function (err, nodes) {
|
||||
if (state.debug) { console.log('[sni] checking fs vhost', opts.domains[0], !err); }
|
||||
if (err) { check(); return; }
|
||||
if (nodes) { approve(); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
function approve() {
|
||||
function allow() {
|
||||
state.validHosts[opts.domains[0]] = true;
|
||||
opts.email = state.config.email;
|
||||
opts.agreeTos = state.config.agreeTos;
|
||||
opts.communityMember = state.config.communityMember || state.config.greenlock.communityMember;
|
||||
opts.challenges = {
|
||||
// TODO dns-01
|
||||
'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' })
|
||||
'http-01': require('le-challenge-fs').create({ webrootPath: path.join(os.tmpdir(), 'acme-challenges') })
|
||||
};
|
||||
opts.communityMember = state.config.communityMember;
|
||||
cb(null, { options: opts, certs: certs });
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (state.debug) { console.log('[sni] checking servername'); }
|
||||
if (-1 !== state.servernames.indexOf(opts.domain) || -1 !== (state._servernames||[]).indexOf(opts.domain)) {
|
||||
approve();
|
||||
} else {
|
||||
cb(new Error("failed the approval chain '" + opts.domains[0] + "'"));
|
||||
}
|
||||
function deny() {
|
||||
cb(new Error("[bin/telebit-relay.js] failed the approval chain '" + opts.domains[0] + "'"));
|
||||
return;
|
||||
}
|
||||
|
||||
check();
|
||||
// 1) If the host was already allowed => allow
|
||||
if (!state.validHosts) { state.validHosts = {}; }
|
||||
if (state.validHosts[opts.domains[0]]) {
|
||||
allow();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) If the host is in the config => allow
|
||||
if (state.debug) { console.log('[sni] checking servername'); }
|
||||
if (-1 !== state.servernames.indexOf(opts.domain)
|
||||
|| -1 !== (state._servernames||[]).indexOf(opts.domain)) {
|
||||
allow();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) If dynamic vhosting is allowed
|
||||
// & a vhost folder exist for this domain => allow
|
||||
if (state.config.vhost) {
|
||||
if (state.debug) { console.log('[sni] vhost checking is turned on'); }
|
||||
var vhost = state.config.vhost.replace(/:hostname/, opts.domains[0]);
|
||||
require('fs').readdir(vhost, function (err, nodes) {
|
||||
if (state.debug) { console.log('[sni] checking fs vhost', opts.domains[0], !err); }
|
||||
if (err) { deny(); return; }
|
||||
if (nodes) { allow(); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) fallback => fail
|
||||
deny();
|
||||
}
|
||||
|
||||
state.greenlock = Greenlock.create({
|
||||
|
@ -202,7 +205,7 @@ function applyConfig(config) {
|
|||
//});
|
||||
}
|
||||
|
||||
require('fs').readFile(confpath, 'utf8', function (err, text) {
|
||||
fs.readFile(confpath, 'utf8', function (err, text) {
|
||||
var config;
|
||||
|
||||
var recase = require('recase').create({});
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
email: 'jon@example.com' # must be valid (for certificate recovery and security alerts)
|
||||
agree_tos: true # agree to the Telebit, Greenlock, and Let's Encrypt TOSes
|
||||
community_member: true # receive infrequent relevant updates
|
||||
telemetry: true # contribute to project telemetric data
|
||||
email: coolaj86@gmail.com
|
||||
agree_tos: true
|
||||
community_member: true
|
||||
telemetry: true
|
||||
webmin_domain: example.com
|
||||
shared_domain: xm.pl
|
||||
servernames: # hostnames that direct to the Telebit Relay admin console
|
||||
- telebit.example.com
|
||||
- telebit.example.net
|
||||
vhost: /srv/www/:hostname # load secure websites at this path (uses template string, i.e. /var/www/:hostname/public)
|
||||
api_domain: example.com
|
||||
shared_domain: example.com
|
||||
servernames:
|
||||
- www.example.com
|
||||
- example.com
|
||||
- api.example.com
|
||||
vhost: /srv/www/:hostname
|
||||
greenlock:
|
||||
version: 'draft-11'
|
||||
server: 'https://acme-v02.api.letsencrypt.org/directory'
|
||||
store:
|
||||
strategy: le-store-certbot # certificate storage plugin
|
||||
config_dir: /etc/acme # directory for ssl certificates
|
||||
secret: '' # generate with node -e "console.log(crypto.randomBytes(16).toString('hex'))"
|
||||
strategy: le-store-certbot
|
||||
config_dir: /opt/telebit-relay/etc/acme
|
||||
secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
debug: true
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
servernames: [ 'telebit.cloud' ]
|
||||
email: 'coolaj86@gmail.com'
|
||||
agree_tos: true
|
||||
community_member: false
|
||||
vhost: /srv/www/:hostname
|
|
@ -179,6 +179,9 @@ if [ ! -f "$TELEBIT_RELAY_PATH/etc/$my_app.yml" ]; then
|
|||
sudo bash -c "echo 'email: $my_email' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'secret: $my_secret' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'servernames: [ $my_servername ]' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'webmin_domain: $my_servername' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'api_domain: $my_servername' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'shared_domain: $my_servername' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "cat $TELEBIT_RELAY_PATH/examples/$my_app.yml.tpl >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
fi
|
||||
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
'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();
|
50
lib/ago.js
50
lib/ago.js
|
@ -1,50 +0,0 @@
|
|||
;(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,114 +1,57 @@
|
|||
'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);
|
||||
};
|
||||
Devices.add = function (store, servername, newDevice, isPort) {
|
||||
if (isPort) {
|
||||
if (!store._ports) { store._ports = {}; }
|
||||
}
|
||||
|
||||
// add domain (also handles ports at the moment)
|
||||
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
|
||||
var devId = newDevice.id || servername;
|
||||
if (!newDevice.__servername) {
|
||||
newDevice.__servername = servername;
|
||||
}
|
||||
if (!store._devices) { store._devices = {}; }
|
||||
if (!store._devices[devId]) {
|
||||
store._devices[devId] = newDevice;
|
||||
if (!store._devices[devId].domainsMap) { store._devices[devId].domainsMap = {}; }
|
||||
if (!store._devices[devId].domainsMap[servername]) { store._devices[devId].domainsMap[servername] = true; }
|
||||
Devices.add = function (store, servername, newDevice) {
|
||||
if (!store[servername]) {
|
||||
store[servername] = [];
|
||||
}
|
||||
var devices = store[servername];
|
||||
devices.push(newDevice);
|
||||
};
|
||||
Devices.alias = function (store, servername, alias) {
|
||||
if (!store._domains[servername]) {
|
||||
store._domains[servername] = [];
|
||||
if (!store[servername]) {
|
||||
store[servername] = [];
|
||||
}
|
||||
if (!store._domains[servername]._primary) {
|
||||
store._domains[servername]._primary = servername;
|
||||
if (!store[servername]._primary) {
|
||||
store[servername]._primary = servername;
|
||||
}
|
||||
if (!store._domains[servername].aliases) {
|
||||
store._domains[servername].aliases = {};
|
||||
if (!store[servername].aliases) {
|
||||
store[servername].aliases = {};
|
||||
}
|
||||
store._domains[alias] = store._domains[servername];
|
||||
store._domains[servername].aliases[alias] = true;
|
||||
store[alias] = store[servername];
|
||||
store[servername].aliases[alias] = true;
|
||||
};
|
||||
Devices.remove = function (store, servername, device) {
|
||||
// Check if this domain has an active device
|
||||
var devices = store._domains[servername] || [];
|
||||
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;
|
||||
}
|
||||
|
||||
// unlink this domain from this device
|
||||
var domainsMap = store._devices[devices[index].id || servername].domainsMap;
|
||||
delete domainsMap[servername];
|
||||
/*
|
||||
// remove device if no domains remain
|
||||
// nevermind, a device can hang around in limbo for a bit
|
||||
if (!Object.keys(domains).length) {
|
||||
delete store._devices[devices[index].id || servername];
|
||||
}
|
||||
*/
|
||||
|
||||
// unlink this device from this domain
|
||||
return devices.splice(index, 1)[0];
|
||||
};
|
||||
Devices.close = function (store, device) {
|
||||
var dev = store._devices[device.id || device.__servername];
|
||||
// because we're actually using names rather than don't have reliable deviceIds yet
|
||||
if (!dev) {
|
||||
Object.keys(store._devices).some(function (key) {
|
||||
if (store._devices[key].socketId === device.socketId) {
|
||||
// TODO double check that all domains are removed
|
||||
delete store._devices[key];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Devices.bySocket = function (store, socketId) {
|
||||
var dev;
|
||||
Object.keys(store._devices).some(function (k) {
|
||||
if (store._devices[k].socketId === socketId) {
|
||||
dev = store._devices[k];
|
||||
return dev;
|
||||
}
|
||||
});
|
||||
return dev;
|
||||
};
|
||||
Devices.list = function (store, servername) {
|
||||
console.log('[dontkeepme] servername', servername);
|
||||
// efficient lookup first
|
||||
if (store._domains[servername] && store._domains[servername].length) {
|
||||
// aliases have ._primary which is the name of the original
|
||||
return store._domains[servername]._primary && store._domains[store._domains[servername]._primary] || store._domains[servername];
|
||||
if (store[servername] && store[servername].length) {
|
||||
return store[servername]._primary && store[store[servername]._primary] || 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._domains).filter(function (pattern) {
|
||||
return pattern[0] === '*' && store._domains[pattern].length;
|
||||
Object.keys(store).filter(function (pattern) {
|
||||
return pattern[0] === '*' && store[pattern].length;
|
||||
}).sort(function (a, b) {
|
||||
return b.length - a.length;
|
||||
}).some(function (pattern) {
|
||||
// '.example.com' = '*.example.com'.split(1)
|
||||
var subPiece = pattern.slice(1);
|
||||
// '.com' = 'sub.example.com'.slice(-4)
|
||||
// '.example.com' = 'sub.example.com'.slice(-12)
|
||||
if (subPiece === servername.slice(-subPiece.length)) {
|
||||
console.log('[Devices.list] "'+servername+'" matches "'+pattern+'"');
|
||||
deviceList = store._domains[pattern];
|
||||
deviceList = store[pattern];
|
||||
|
||||
// Devices.alias(store, '*.example.com', 'sub.example.com'
|
||||
// '*.example.com' retrieves a reference to 'example.com'
|
||||
|
@ -121,18 +64,8 @@ Devices.list = function (store, servername) {
|
|||
|
||||
return deviceList;
|
||||
};
|
||||
/*
|
||||
Devices.active = function (store, id) {
|
||||
var dev = store._devices[id];
|
||||
return !!dev;
|
||||
};
|
||||
*/
|
||||
Devices.exist = function (store, servername) {
|
||||
if (Devices.list(store, servername).length) {
|
||||
Devices.touch(store, servername);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return !!(Devices.list(store, servername).length);
|
||||
};
|
||||
Devices.next = function (store, servername) {
|
||||
var devices = Devices.list(store, servername);
|
||||
|
@ -144,20 +77,5 @@ 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,44 +72,38 @@ module.exports.create = function (state) {
|
|||
state.tlsInvalidSniServer.on('tlsClientError', function () {
|
||||
console.error('tlsClientError InvalidSniServer');
|
||||
});
|
||||
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) {
|
||||
state.httpsInvalid = function (servername, 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', opts.servername);
|
||||
console.log('[httpsInvalid] servername', servername);
|
||||
//state.tlsInvalidSniServer.emit('connection', wrapSocket(socket));
|
||||
var tlsInvalidSniServer = tls.createServer(state.tlsOptions, function (tlsSocket) {
|
||||
console.log('[tlsInvalid] tls connection');
|
||||
// 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);
|
||||
// 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);
|
||||
});
|
||||
tlsInvalidSniServer.on('tlsClientError', function () {
|
||||
console.error('tlsClientError InvalidSniServer httpsInvalid');
|
||||
|
@ -142,21 +136,10 @@ module.exports.create = function (state) {
|
|||
console.log('[Admin] custom or null tlsOptions for SNICallback');
|
||||
tunnelAdminTlsOpts.SNICallback = tunnelAdminTlsOpts.SNICallback || noSniCallback('admin');
|
||||
}
|
||||
var MPROXY = Buffer.from("MPROXY");
|
||||
state.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) {
|
||||
if (state.debug) { console.log('[Admin] new tls-terminated connection'); }
|
||||
tlsSocket.once('readable', function () {
|
||||
var firstChunk = tlsSocket.read();
|
||||
tlsSocket.unshift(firstChunk);
|
||||
|
||||
if (0 === MPROXY.compare(firstChunk.slice(0, 4))) {
|
||||
tlsSocket.end("MPROXY isn't supported yet");
|
||||
return;
|
||||
}
|
||||
|
||||
// things get a little messed up here
|
||||
(state.httpTunnelServer || state.httpServer).emit('connection', tlsSocket);
|
||||
});
|
||||
// things get a little messed up here
|
||||
(state.httpTunnelServer || state.httpServer).emit('connection', tlsSocket);
|
||||
});
|
||||
state.tlsTunnelServer.on('tlsClientError', function () {
|
||||
console.error('tlsClientError TunnelServer client error');
|
||||
|
|
|
@ -16,13 +16,7 @@ module.exports = function pipeWs(servername, service, srv, conn, serviceport) {
|
|||
function sendWs(data, serviceOverride) {
|
||||
if (srv.ws && (!conn.tunnelClosing || serviceOverride)) {
|
||||
try {
|
||||
if (data && !Buffer.isBuffer(data)) {
|
||||
data = Buffer.from(JSON.stringify(data));
|
||||
}
|
||||
srv.ws.send(Packer.packHeader(browserAddr, data, serviceOverride), { binary: true });
|
||||
if (data) {
|
||||
srv.ws.send(data, { binary: true });
|
||||
}
|
||||
srv.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
|
||||
|
@ -45,10 +39,6 @@ module.exports = function pipeWs(servername, service, srv, conn, serviceport) {
|
|||
conn.serviceport = serviceport;
|
||||
conn.service = service;
|
||||
|
||||
// send peek at data too?
|
||||
srv.ws.send(Packer.packHeader(browserAddr, null, 'connection'), { binary: true });
|
||||
|
||||
// TODO convert to read stream?
|
||||
conn.on('data', function (chunk) {
|
||||
//if (state.debug) { console.log('[pipeWs] client', cid, ' => srv', rid, chunk.byteLength, 'bytes'); }
|
||||
sendWs(chunk);
|
||||
|
|
|
@ -6,7 +6,7 @@ var Server = require('./server.js');
|
|||
|
||||
module.exports.store = { Devices: Devices };
|
||||
module.exports.create = function (state) {
|
||||
state.deviceLists = { _domains: {}, _devices: {} };
|
||||
state.deviceLists = {};
|
||||
state.deviceCallbacks = {};
|
||||
state.srvs = {};
|
||||
|
||||
|
@ -33,16 +33,12 @@ module.exports.create = function (state) {
|
|||
var initToken;
|
||||
srv.ws = _ws;
|
||||
srv.upgradeReq = _upgradeReq;
|
||||
// TODO use device's ECDSA thumbprint as device id
|
||||
srv.id = null;
|
||||
srv.socketId = Packer.socketToId(srv.upgradeReq.socket);
|
||||
srv.grants = {};
|
||||
srv.clients = {};
|
||||
srv.domainsMap = {};
|
||||
srv.portsMap = {};
|
||||
srv.pausedConns = [];
|
||||
srv.domains = [];
|
||||
srv.ports = [];
|
||||
|
||||
if (state.debug) { console.log('[ws] connection', srv.socketId); }
|
||||
|
||||
|
@ -68,6 +64,7 @@ module.exports.create = function (state) {
|
|||
});
|
||||
|
||||
if (initToken) {
|
||||
console.log('[wss.onConnection] token provided in http headers');
|
||||
return Server.addToken(state, srv, initToken).then(function () {
|
||||
Server.init(state, srv);
|
||||
}).catch(function (err) {
|
||||
|
|
|
@ -96,13 +96,6 @@ var Server = {
|
|||
return srv._commandHandlers[cmd[1]].apply(null, cmd.slice(2)).then(onSuccess, onError);
|
||||
}
|
||||
|
||||
, onconnection: function (/*tun*/) {
|
||||
// I don't think this event can happen since this relay
|
||||
// is acting the part of the client, but just in case...
|
||||
// (in fact it should probably be explicitly disallowed)
|
||||
console.error("[SANITY FAIL] reverse connection start");
|
||||
}
|
||||
|
||||
, onmessage: function (tun) {
|
||||
var cid = Packer.addrToId(tun);
|
||||
if (state.debug) { console.log("remote '" + Server.logName(state, srv) + "' has data for '" + cid + "'", tun.data.byteLength); }
|
||||
|
@ -172,7 +165,6 @@ var Server = {
|
|||
, _initSocketHandlers: function (state, srv) {
|
||||
function refreshTimeout() {
|
||||
srv.lastActivity = Date.now();
|
||||
Devices.touchDevice(state.deviceLists, srv);
|
||||
}
|
||||
|
||||
function checkTimeout() {
|
||||
|
@ -215,13 +207,10 @@ var Server = {
|
|||
function hangup() {
|
||||
clearTimeout(srv.timeoutId);
|
||||
console.log('[ws] device hangup', Server.logName(state, srv), 'connection closing');
|
||||
// remove the allowed domains from the list (but leave the socket)
|
||||
Object.keys(srv.grants).forEach(function (jwtoken) {
|
||||
Server.removeToken(state, srv, jwtoken);
|
||||
});
|
||||
srv.ws.terminate();
|
||||
// remove the socket from the list, period
|
||||
Devices.close(state.deviceLists, srv);
|
||||
}
|
||||
|
||||
srv.lastActivity = Date.now();
|
||||
|
@ -245,11 +234,7 @@ var Server = {
|
|||
Server.sendTunnelMsg(srv, null, [1, 'hello', [srv.unpacker._version], Object.keys(srv._commandHandlers)], 'control');
|
||||
}
|
||||
, sendTunnelMsg: function sendTunnelMsg(srv, addr, data, service) {
|
||||
if (data && !Buffer.isBuffer()) {
|
||||
data = Buffer.from(JSON.stringify(data));
|
||||
}
|
||||
srv.ws.send(Packer.packHeader(addr, data, service), {binary: true});
|
||||
srv.ws.send(data, {binary: true});
|
||||
srv.ws.send(Packer.pack(addr, data, service), {binary: true});
|
||||
}
|
||||
, logName: function logName(state, srv) {
|
||||
var result = Object.keys(srv.grants).map(function (jwtoken) {
|
||||
|
@ -271,7 +256,6 @@ var Server = {
|
|||
return state.Promise.reject(err);
|
||||
}
|
||||
|
||||
// deprecated (for json object on connect)
|
||||
if ('string' !== typeof rawAuth) {
|
||||
rawAuth = JSON.stringify(rawAuth);
|
||||
}
|
||||
|
@ -286,12 +270,11 @@ var Server = {
|
|||
//console.log(grant);
|
||||
if (grant.jwt) {
|
||||
if (rawAuth !== grant.jwt) {
|
||||
console.log('[onAuth] token is new');
|
||||
console.log('[onAuth] new token to send back');
|
||||
}
|
||||
// TODO only send token when new
|
||||
if (true) {
|
||||
// Access Token
|
||||
console.log('[onAuth] sending back token');
|
||||
Server.sendTunnelMsg(
|
||||
srv
|
||||
, null
|
||||
|
@ -355,7 +338,7 @@ var Server = {
|
|||
srv.portsMap[serviceport] = PortServers[serviceport];
|
||||
srv.portsMap[serviceport].on('connection', tcpListener);
|
||||
srv.portsMap[serviceport].tcpListener = tcpListener;
|
||||
Devices.addPort(state.deviceLists, serviceport, srv);
|
||||
Devices.add(state.deviceLists, serviceport, srv);
|
||||
} else {
|
||||
try {
|
||||
console.log('use new', serviceport, 'for this connection');
|
||||
|
@ -363,7 +346,7 @@ var Server = {
|
|||
srv.portsMap[serviceport].tcpListener = tcpListener;
|
||||
srv.portsMap[serviceport].listen(serviceport, function () {
|
||||
console.info('[DynTcpConn] Port', serviceport, 'now open for', grant.currentDesc);
|
||||
Devices.addPort(state.deviceLists, serviceport, srv);
|
||||
Devices.add(state.deviceLists, serviceport, srv);
|
||||
});
|
||||
srv.portsMap[serviceport].on('error', function (e) {
|
||||
// TODO try again with random port
|
||||
|
@ -378,18 +361,19 @@ var Server = {
|
|||
}
|
||||
grant.ports.forEach(openPort);
|
||||
|
||||
console.info("[ws] authorized", srv.socketId, "for", grant.currentDesc);
|
||||
console.log('notify of grants', grant.domains, grant.ports);
|
||||
srv.grants[rawAuth] = grant;
|
||||
console.info("[ws] authorized", srv.socketId, "for", grant.currentDesc);
|
||||
|
||||
console.log('notify of grants', grant.domains, grant.ports);
|
||||
Server.sendTunnelMsg(
|
||||
srv
|
||||
, null
|
||||
, [ 2
|
||||
, 'grant'
|
||||
, [ ['ssh+https', grant.domains[0], 443 ]
|
||||
// TODO the shared domain should be token specific
|
||||
, ['ssh', 'ssh.' + state.config.sharedDomain, [grant.ports[0]] ]
|
||||
, ['tcp', 'tcp.' + state.config.sharedDomain, [grant.ports[0]] ]
|
||||
// TODO this should be account-specific
|
||||
, ['ssh', 'ssh.' + state.config.sharedDomain, grant.ports ]
|
||||
, ['tcp', 'tcp.' + state.config.sharedDomain, grant.ports ]
|
||||
, ['https', grant.domains[0] ]
|
||||
]
|
||||
]
|
||||
|
@ -413,16 +397,7 @@ var Server = {
|
|||
return;
|
||||
}
|
||||
|
||||
// When using raw TCP we're already paired to the client by port
|
||||
// and we can begin connecting right away, but we'll wait just a sec
|
||||
// to reject known bad connections
|
||||
var sendConnection = setTimeout(function () {
|
||||
conn.removeListener('data', peekFirstPacket);
|
||||
console.log("[debug tcp conn] Connecting possible telnet client to device...");
|
||||
pipeWs(null, 'tcp', nextDevice, conn, serviceport);
|
||||
}, 350);
|
||||
function peekFirstPacket(firstChunk) {
|
||||
clearTimeout(sendConnection);
|
||||
conn.once('data', function (firstChunk) {
|
||||
if (state.debug) { console.log("[DynTcp]", serviceport, "examining firstChunk from", Packer.socketToId(conn)); }
|
||||
conn.pause();
|
||||
//conn.unshift(firstChunk);
|
||||
|
@ -457,8 +432,7 @@ var Server = {
|
|||
pipeWs(null, 'tcp', nextDevice, conn, serviceport);
|
||||
|
||||
process.nextTick(function () { conn.resume(); });
|
||||
}
|
||||
conn.once('data', peekFirstPacket);
|
||||
});
|
||||
}
|
||||
, addToken: function addToken(state, srv, rawAuth) {
|
||||
console.log("[addToken]", rawAuth);
|
||||
|
@ -468,8 +442,6 @@ var Server = {
|
|||
return state.Promise.resolve(null);
|
||||
}
|
||||
|
||||
// [Extension] [Auth] This is where authentication is either handed off to
|
||||
// an extension or the default authencitation handler.
|
||||
return state.authenticate({ auth: rawAuth }).then(function (validatedTokenData) {
|
||||
console.log('\n[relay.js] rawAuth');
|
||||
console.log(rawAuth);
|
||||
|
|
|
@ -2,16 +2,6 @@
|
|||
|
||||
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;
|
||||
|
@ -28,26 +18,13 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
//});
|
||||
|
||||
//return;
|
||||
//conn.once('data', function (firstChunk) {
|
||||
//});
|
||||
conn.once('readable', function () {
|
||||
var firstChunk = conn.read();
|
||||
conn.once('data', function (firstChunk) {
|
||||
var service = 'tcp';
|
||||
var servername;
|
||||
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.pause();
|
||||
conn.unshift(firstChunk);
|
||||
|
||||
// BUG XXX: this assumes that the packet won't be chunked smaller
|
||||
|
@ -58,98 +35,41 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
|
||||
// defer after return (instead of being in many places)
|
||||
function deferData(fn) {
|
||||
if ('httpsInvalid' === fn) {
|
||||
state[fn]({
|
||||
servername: servername
|
||||
, ago: fromUptime(Devices.lastSeen(state.deviceLists, servername))
|
||||
}, conn);
|
||||
} else if (fn) {
|
||||
if (fn) {
|
||||
state[fn](servername, conn);
|
||||
} else {
|
||||
console.error("[SANITY ERROR] '" + fn + "' doesn't have a state handler");
|
||||
}
|
||||
/*
|
||||
process.nextTick(function () {
|
||||
conn.resume();
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
var httpOutcomes = {
|
||||
missingServername: function () {
|
||||
console.log("[debug] [http] missing servername");
|
||||
// TODO use a more specific error page
|
||||
deferData('handleInsecureHttp');
|
||||
}
|
||||
, 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);
|
||||
}
|
||||
, 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';
|
||||
function tryTls() {
|
||||
var vhost;
|
||||
|
||||
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 (!servername) {
|
||||
if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); }
|
||||
deferData('httpsInvalid');
|
||||
return;
|
||||
}
|
||||
, requiresSetup: function () {
|
||||
|
||||
if (!state.servernames.length) {
|
||||
console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)");
|
||||
deferData('httpsSetupServer');
|
||||
return;
|
||||
}
|
||||
, isInternal: function () {
|
||||
|
||||
if (-1 !== state.servernames.indexOf(servername)) {
|
||||
if (state.debug) { console.log("[Admin]", servername); }
|
||||
deferData('httpsTunnel');
|
||||
return;
|
||||
}
|
||||
, isVhost: function (vhost) {
|
||||
if (state.debug) { console.log("[tcp] [vhost]", state.config.vhost, "=>", vhost); }
|
||||
deferData('httpsVhost');
|
||||
|
||||
if (state.config.nowww && /^www\./i.test(servername)) {
|
||||
console.log("TODO: use www bare redirect");
|
||||
}
|
||||
, assumeExternal: function () {
|
||||
var nextDevice = Devices.next(state.deviceLists, servername);
|
||||
|
||||
function run() {
|
||||
var nextDevice = Devices.next(state.deviceLists, servername);
|
||||
if (!nextDevice) {
|
||||
if (state.debug) { console.log("No devices match the given servername"); }
|
||||
deferData('httpsInvalid');
|
||||
|
@ -157,33 +77,27 @@ 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);
|
||||
vhost = state.config.vhost.replace(/:hostname/, (servername||'reallydoesntexist'));
|
||||
if (state.debug) { console.log("[tcp] [vhost]", state.config.vhost, "=>", vhost); }
|
||||
//state.httpsVhost(servername, conn);
|
||||
//return;
|
||||
require('fs').readdir(vhost, function (err, nodes) {
|
||||
if (state.debug && err) { console.log("VHOST error", err); }
|
||||
if (err || !nodes) { outcomes.assumeExternal(); return; }
|
||||
outcomes.isVhost(vhost);
|
||||
if (err || !nodes) { run(); return; }
|
||||
//if (nodes) { deferData('httpsVhost'); return; }
|
||||
deferData('httpsVhost');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
outcomes.assumeExternal();
|
||||
run();
|
||||
}
|
||||
|
||||
// https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
|
||||
|
@ -192,19 +106,40 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
service = 'https';
|
||||
servername = (sni(firstChunk)||'').toLowerCase().trim();
|
||||
if (state.debug) { console.log("[tcp] tls hello from '" + servername + "'"); }
|
||||
handleConnection(tlsOutcomes);
|
||||
tryTls();
|
||||
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)) {
|
||||
handleConnection(httpOutcomes);
|
||||
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');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
10
package.json
10
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "telebit-relay",
|
||||
"version": "0.20.0",
|
||||
"version": "0.13.1",
|
||||
"description": "Friends don't let friends localhost. Expose your bits with a secure connection even from behind NAT, Firewalls, in a box, with a fox, on a train or in a plane... or a Raspberry Pi in your closet. An attempt to create a better localtunnel.me server, a more open ngrok. Uses Automated HTTPS (Free SSL) via ServerName Indication (SNI). Can also tunnel tls and plain tcp.",
|
||||
"main": "lib/relay.js",
|
||||
"bin": {
|
||||
|
@ -37,20 +37,22 @@
|
|||
},
|
||||
"homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js",
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.1",
|
||||
"cluster-store": "^2.0.8",
|
||||
"finalhandler": "^1.1.1",
|
||||
"greenlock": "^2.2.4",
|
||||
"human-readable-ids": "^1.0.4",
|
||||
"js-yaml": "^3.11.0",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"proxy-packer": "^2.0.0",
|
||||
"jsonwebtoken": "^8.2.1",
|
||||
"proxy-packer": "^1.4.3",
|
||||
"recase": "^1.0.4",
|
||||
"redirect-https": "^1.1.5",
|
||||
"serve-static": "^1.13.2",
|
||||
"sni": "^1.0.0",
|
||||
"ws": "^5.1.1"
|
||||
},
|
||||
"trulyOptionalDependencies": {
|
||||
"bluebird": "^3.5.1"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"engines": {
|
||||
"node": "10.2.1"
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
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