Compare commits
	
		
			No commits in common. "master" and "v0.11.0" have entirely different histories.
		
	
	
		
	
		
							
								
								
									
										15
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,10 +1,4 @@
 | 
			
		||||
node_modules.*
 | 
			
		||||
include
 | 
			
		||||
bin/node
 | 
			
		||||
bin/npm
 | 
			
		||||
bin/npx
 | 
			
		||||
share
 | 
			
		||||
etc
 | 
			
		||||
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
@ -43,12 +37,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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										81
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										81
									
								
								README.md
									
									
									
									
									
								
							@ -31,15 +31,15 @@ curl -fsSL https://get.telebit.cloud/relay | bash
 | 
			
		||||
 | 
			
		||||
Of course, feel free to inspect the install script before you run it.
 | 
			
		||||
 | 
			
		||||
This will install Telebit Relay to `/opt/telebit-relay` and
 | 
			
		||||
put a symlink to `/opt/telebit-relay/bin/telebit-relay` in `/usr/local/bin/telebit-relay`
 | 
			
		||||
This will install Telebit Relay to `/opt/telebitd` and
 | 
			
		||||
put a symlink to `/opt/telebitd/bin/telebitd` in `/usr/local/bin/telebitd`
 | 
			
		||||
for convenience.
 | 
			
		||||
 | 
			
		||||
You can customize the installation:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
export NODEJS_VER=v10.2
 | 
			
		||||
export TELEBIT_RELAY_PATH=/opt/telebit-relay
 | 
			
		||||
export TELEBITD_PATH=/opt/telebitd
 | 
			
		||||
curl -fsSL https://get.telebit.cloud/relay
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -49,7 +49,7 @@ and the path to which Telebit Relay installs.
 | 
			
		||||
You can get rid of the tos + email and server domain name prompts by providing them right away:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
curl -fsSL https://get.telebit.cloud/relay | bash -- jon@example.com telebit-relay.example.com
 | 
			
		||||
curl -fsSL https://get.telebit.cloud/relay | bash -- jon@example.com telebit.example.com
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Windows & Node.js
 | 
			
		||||
@ -57,57 +57,22 @@ Windows & Node.js
 | 
			
		||||
 | 
			
		||||
1. Install [node.js](https://nodejs.org)
 | 
			
		||||
2. Open _Node.js_
 | 
			
		||||
2. Run the command `npm install -g telebit-relay`
 | 
			
		||||
2. Run the command `npm install -g telebitd`
 | 
			
		||||
 | 
			
		||||
**Note**: Use node.js v8.x or v10.x
 | 
			
		||||
 | 
			
		||||
There is [a bug](https://github.com/nodejs/node/issues/20241) in node v9.x that causes telebit-relay to crash.
 | 
			
		||||
 | 
			
		||||
Manually Install
 | 
			
		||||
-----------
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
git clone https://git.coolaj86.com/coolaj86/telebit-relay.js.git telebit-relay
 | 
			
		||||
 | 
			
		||||
# we're very picky to due to bugs in various versions of v8, v9, and v10
 | 
			
		||||
export NODEJS_VER="v10.2.1"
 | 
			
		||||
 | 
			
		||||
# We can keep everything self-contained
 | 
			
		||||
export NPM_CONFIG_PREFIX=/opt/telebit-relay
 | 
			
		||||
export NODE_PATH=/opt/telebit-relay/lib/node_modules
 | 
			
		||||
 | 
			
		||||
curl -fsSL https://bit.ly/node-installer | bash -s -- --no-dev-deps
 | 
			
		||||
 | 
			
		||||
pushd /opt/telebit-relay
 | 
			
		||||
  bin/node bin/npm install
 | 
			
		||||
  rsync -a examples/telebit-relay.yml etc/telebit-relay.yml
 | 
			
		||||
  rsync -a dist/etc/systemd/system/telebit-relay.service /etc/systemd/system/telebit-relay.service
 | 
			
		||||
popd
 | 
			
		||||
 | 
			
		||||
# IMPORTANT: Season the config file to taste
 | 
			
		||||
# IMPORTANT: change your email address and domain
 | 
			
		||||
edit /opt/telebit-relay/etc/telebit-relay.yml
 | 
			
		||||
 | 
			
		||||
adduser --home /opt/telebit-relay --gecos '' --disabled-password telebit >/dev/null 2>&1
 | 
			
		||||
sudo chown -R telebit:telebit /opt/telebit-relay/
 | 
			
		||||
 | 
			
		||||
systemctl daemon-reload
 | 
			
		||||
systemctl restart telebit-relay
 | 
			
		||||
 | 
			
		||||
systemctl status telebit-relay
 | 
			
		||||
journalctl -xefu telebit-relay
 | 
			
		||||
```
 | 
			
		||||
There is [a bug](https://github.com/nodejs/node/issues/20241) in node v9.x that causes telebitd to crash.
 | 
			
		||||
 | 
			
		||||
Usage
 | 
			
		||||
====
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
telebit-relay --config /opt/telebit-relay/etc/telebit-relay.yml
 | 
			
		||||
telebitd --config /etc/telebit/telebitd.yml
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Options
 | 
			
		||||
 | 
			
		||||
`/opt/telebit-relay/etc/telebit-relay.yml:`
 | 
			
		||||
`/etc/telebit/telebitd.yml:`
 | 
			
		||||
```
 | 
			
		||||
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
 | 
			
		||||
@ -116,13 +81,13 @@ telemetry: true                # contribute to project telemetric data
 | 
			
		||||
secret: ''                 # JWT authorization secret. Generate like so:
 | 
			
		||||
                           # node -e "console.log(crypto.randomBytes(16).toString('hex'))"
 | 
			
		||||
servernames:               # hostnames that direct to the Telebit Relay admin console
 | 
			
		||||
  - telebit-relay.example.com
 | 
			
		||||
  - telebit-relay.example.net
 | 
			
		||||
  - telebit.example.com
 | 
			
		||||
  - telebit.example.net
 | 
			
		||||
vhost: /srv/www/:hostname  # securely serve local sites from this path (or false)
 | 
			
		||||
                           # (uses template string, i.e. /var/www/:hostname/public)
 | 
			
		||||
greenlock:
 | 
			
		||||
  store: le-store-certbot  # certificate storage plugin
 | 
			
		||||
  config_dir: /opt/telebit-relay/etc/acme    # directory for ssl certificates
 | 
			
		||||
  config_dir: /etc/acme    # directory for ssl certificates
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Security
 | 
			
		||||
@ -135,7 +100,7 @@ Even though the traffic is encrypted end-to-end, you can't just trust any Telebi
 | 
			
		||||
willy-nilly.
 | 
			
		||||
 | 
			
		||||
A man-in-the-middle attack is possible using Let's Encrypt since an evil Telebit Relay
 | 
			
		||||
would be able to complete the http-01 challenges without a problem
 | 
			
		||||
would be able to complete the http-01 and tls-sni-01 challenges without a problem
 | 
			
		||||
(since that's where your DNS is pointed when you use the service).
 | 
			
		||||
 | 
			
		||||
Also, the traffic could still be copied and stored for decryption is some era when quantum
 | 
			
		||||
@ -163,7 +128,7 @@ Useful Tidbits
 | 
			
		||||
 | 
			
		||||
## As a systemd service
 | 
			
		||||
 | 
			
		||||
`./dist/etc/systemd/system/telebit-relay.service` should be copied to `/etc/systemd/system/telebit-relay.service`.
 | 
			
		||||
`./dist/etc/systemd/system/telebitd.service` should be copied to `/etc/systemd/system/telebitd.service`.
 | 
			
		||||
 | 
			
		||||
The user and group `telebit` should be created.
 | 
			
		||||
 | 
			
		||||
@ -173,23 +138,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);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -1,7 +0,0 @@
 | 
			
		||||
{ "terms_of_service": ":hostname/tos/"
 | 
			
		||||
, "api_host": ":hostname"
 | 
			
		||||
, "tunnel": {
 | 
			
		||||
    "method": "wss"
 | 
			
		||||
  , "pathname": ""
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -2,14 +2,8 @@
 | 
			
		||||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
    <title>Telebit Relay</title>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <script>document.body.hidden = true;</script>
 | 
			
		||||
    <button class="js-login">Login</button>
 | 
			
		||||
    <button class="js-login">Sign Up</button>
 | 
			
		||||
    <br>
 | 
			
		||||
    [TODO: Admin Interface]
 | 
			
		||||
    <script src="js/app.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +0,0 @@
 | 
			
		||||
(function () {
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
document.body.hidden = false;
 | 
			
		||||
 | 
			
		||||
}());
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
var pkg = require('../package.json');
 | 
			
		||||
 | 
			
		||||
var argv = process.argv.slice(2);
 | 
			
		||||
var relay = require('../');
 | 
			
		||||
var telebitd = require('../telebitd.js');
 | 
			
		||||
var Greenlock = require('greenlock');
 | 
			
		||||
 | 
			
		||||
var confIndex = argv.indexOf('--config');
 | 
			
		||||
@ -19,15 +19,15 @@ function help() {
 | 
			
		||||
  console.info('');
 | 
			
		||||
  console.info('Usage:');
 | 
			
		||||
  console.info('');
 | 
			
		||||
  console.info('\ttelebit-relay --config <path>');
 | 
			
		||||
  console.info('\ttelebitd --config <path>');
 | 
			
		||||
  console.info('');
 | 
			
		||||
  console.info('Example:');
 | 
			
		||||
  console.info('');
 | 
			
		||||
  console.info('\ttelebit-relay --config /opt/telebit-relay/etc/telebit-relay.yml');
 | 
			
		||||
  console.info('\ttelebitd --config /etc/telebit/telebitd.yml');
 | 
			
		||||
  console.info('');
 | 
			
		||||
  console.info('Config:');
 | 
			
		||||
  console.info('');
 | 
			
		||||
  console.info('\tSee https://git.coolaj86.com/coolaj86/telebit-relay.js');
 | 
			
		||||
  console.info('\tSee https://git.coolaj86.com/coolaj86/telebitd.js');
 | 
			
		||||
  console.info('');
 | 
			
		||||
  console.info('');
 | 
			
		||||
  process.exit(0);
 | 
			
		||||
@ -41,19 +41,8 @@ if (!confpath || /^--/.test(confpath)) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function applyConfig(config) {
 | 
			
		||||
  var state = { defaults: {}, ports: [ 80, 443 ], tcp: {} };
 | 
			
		||||
  if ('undefined' !== typeof Promise) {
 | 
			
		||||
    state.Promise = Promise;
 | 
			
		||||
  } 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
 | 
			
		||||
  var state = { ports: [ 80, 443 ], tcp: {} };
 | 
			
		||||
  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;
 | 
			
		||||
@ -74,7 +63,7 @@ function applyConfig(config) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function approveDomains(opts, certs, cb) {
 | 
			
		||||
    if (state.debug) { console.log('[debug] approveDomains', opts.domains); }
 | 
			
		||||
    console.log('[debug] approveDomains', opts.domains);
 | 
			
		||||
    // This is where you check your database and associated
 | 
			
		||||
    // email addresses with domains and agreements and such
 | 
			
		||||
 | 
			
		||||
@ -86,12 +75,11 @@ function applyConfig(config) {
 | 
			
		||||
      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'); }
 | 
			
		||||
    if (state.config.vhost) {
 | 
			
		||||
      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); }
 | 
			
		||||
        console.log('[sni] checking fs vhost');
 | 
			
		||||
        if (err) { check(); return; } 
 | 
			
		||||
        if (nodes) { approve(); }
 | 
			
		||||
      });
 | 
			
		||||
@ -99,10 +87,8 @@ function applyConfig(config) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function approve() {
 | 
			
		||||
      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' })
 | 
			
		||||
@ -112,90 +98,61 @@ function applyConfig(config) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function check() {
 | 
			
		||||
      if (state.debug) { console.log('[sni] checking servername'); }
 | 
			
		||||
      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] + "'"));
 | 
			
		||||
      }
 | 
			
		||||
      console.log('Approve Domains cb');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    check();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
  if (!config.email || !config.agreeTos) {
 | 
			
		||||
    console.error("You didn't specify --email <EMAIL> and --agree-tos");
 | 
			
		||||
    console.error("(required for ACME / Let's Encrypt / Greenlock TLS/SSL certs)");
 | 
			
		||||
    console.error("");
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
  }
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
  state.greenlock = Greenlock.create({
 | 
			
		||||
 | 
			
		||||
    version: state.config.greenlock.version || 'draft-11'
 | 
			
		||||
  , server: state.config.greenlock.server || 'https://acme-v02.api.letsencrypt.org/directory'
 | 
			
		||||
    version: 'draft-11'
 | 
			
		||||
  , server: 'https://acme-v02.api.letsencrypt.org/directory'
 | 
			
		||||
  //, server: 'https://acme-staging-v02.api.letsencrypt.org/directory'
 | 
			
		||||
 | 
			
		||||
  , store: require('le-store-certbot').create({ debug: state.config.debug || state.config.greenlock.debug, webrootPath: '/tmp/acme-challenges' })
 | 
			
		||||
  , store: require('le-store-certbot').create({ debug: true, webrootPath: '/tmp/acme-challenges' })
 | 
			
		||||
 | 
			
		||||
  , approveDomains: approveDomains
 | 
			
		||||
  , telemetry: state.config.telemetry || state.config.greenlock.telemetry
 | 
			
		||||
  , configDir: state.config.greenlock.configDir
 | 
			
		||||
  , debug: state.config.debug || state.config.greenlock.debug
 | 
			
		||||
 | 
			
		||||
  , configDir: state.config.configDir
 | 
			
		||||
  , debug: true
 | 
			
		||||
 | 
			
		||||
  //, approvedDomains: program.servernames
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    // TODO specify extensions in config file
 | 
			
		||||
    state.extensions = require('../lib/extensions');
 | 
			
		||||
  } catch(e) {
 | 
			
		||||
    if ('ENOENT' !== e.code || state.debug) { console.log('[DEBUG] no extensions loaded', e); }
 | 
			
		||||
    state.extensions = {};
 | 
			
		||||
  }
 | 
			
		||||
  require('../lib/handlers').create(state); // adds directly to config for now...
 | 
			
		||||
  require('../handlers').create(state); // adds directly to config for now...
 | 
			
		||||
 | 
			
		||||
  //require('cluster-store').create().then(function (store) {
 | 
			
		||||
    //program.store = store;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    state.authenticate = function (opts) {
 | 
			
		||||
      if (state.extensions.authenticate) {
 | 
			
		||||
        try {
 | 
			
		||||
          return state.extensions.authenticate({
 | 
			
		||||
            state: state
 | 
			
		||||
          , auth: opts.auth
 | 
			
		||||
          });
 | 
			
		||||
        } catch(e) {
 | 
			
		||||
          console.error('Extension Error:');
 | 
			
		||||
          console.error(e);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return state.defaults.authenticate(opts.auth);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // default authenticator for single-user setup
 | 
			
		||||
    // (i.e. personal use on DO, Vultr, or RPi)
 | 
			
		||||
    state.defaults.authenticate = function onAuthenticate(jwtoken) {
 | 
			
		||||
      return state.Promise.resolve().then(function () {
 | 
			
		||||
        var jwt = require('jsonwebtoken');
 | 
			
		||||
        var auth;
 | 
			
		||||
        var token;
 | 
			
		||||
        var decoded;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
          token = jwt.verify(jwtoken, state.secret);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          token = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return token;
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var net = require('net');
 | 
			
		||||
    var netConnHandlers = relay.create(state); // { tcp, ws }
 | 
			
		||||
    var netConnHandlers = telebitd.create(state); // { tcp, ws }
 | 
			
		||||
    var WebSocketServer = require('ws').Server;
 | 
			
		||||
    var wss = new WebSocketServer({ server: (state.httpTunnelServer || state.httpServer) });
 | 
			
		||||
    wss.on('connection', netConnHandlers.ws);
 | 
			
		||||
    state.ports.forEach(function (port) {
 | 
			
		||||
      if (state.tcp[port]) {
 | 
			
		||||
        console.warn("[cli] skipping previously added port " + port);
 | 
			
		||||
        console.error("skipping previously added port " + port);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      state.tcp[port] = net.createServer();
 | 
			
		||||
      state.tcp[port].listen(port, function () {
 | 
			
		||||
        console.info('[cli] Listening for TCP connections on', port);
 | 
			
		||||
        console.log('listening plain TCP on ' + port);
 | 
			
		||||
      });
 | 
			
		||||
      state.tcp[port].on('connection', netConnHandlers.tcp);
 | 
			
		||||
    });
 | 
			
		||||
@ -300,7 +257,7 @@ function adjustArgs() {
 | 
			
		||||
    .option('--serve <URL>', 'comma separated list of <proto>:<//><servername>:<port> to which matching incoming http and https should forward (reverse proxy). Ex: https://john.example.com,tls:*:1337', collectProxies, [ ])
 | 
			
		||||
    .option('--ports <PORT>', 'comma separated list of ports on which to listen. Ex: 80,443,1337', collectPorts, [ ])
 | 
			
		||||
    .option('--servernames <STRING>', 'comma separated list of servernames to use for the admin interface. Ex: tunnel.example.com,tunnel.example.net', collectServernames, [ ])
 | 
			
		||||
    .option('--secret <STRING>', 'the same secret used by telebit-relay (used for JWT authentication)')
 | 
			
		||||
    .option('--secret <STRING>', 'the same secret used by telebitd (used for JWT authentication)')
 | 
			
		||||
    .parse(process.argv)
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
# Pre-req
 | 
			
		||||
# sudo adduser telebit --home /opt/telebit-relay
 | 
			
		||||
# sudo mkdir -p /opt/telebit-relay/
 | 
			
		||||
# sudo chown -R telebit:telebit /opt/telebit-relay/
 | 
			
		||||
# sudo adduser telebit --home /opt/telebitd
 | 
			
		||||
# sudo mkdir -p /opt/telebitd/
 | 
			
		||||
# sudo chown -R telebit:telebit /opt/telebitd/
 | 
			
		||||
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Telebit Relay
 | 
			
		||||
Documentation=https://git.coolaj86.com/coolaj86/telebit-relay.js/
 | 
			
		||||
Documentation=https://git.coolaj86.com/coolaj86/telebitd.js/
 | 
			
		||||
After=network-online.target
 | 
			
		||||
Wants=network-online.target systemd-networkd-wait-online.service
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ Wants=network-online.target systemd-networkd-wait-online.service
 | 
			
		||||
# Restart on crash (bad signal), but not on 'clean' failure (error exit code)
 | 
			
		||||
# Allow up to 3 restarts within 10 seconds
 | 
			
		||||
# (it's unlikely that a user or properly-running script will do this)
 | 
			
		||||
Restart=on-failure
 | 
			
		||||
Restart=on-abnormal
 | 
			
		||||
StartLimitInterval=10
 | 
			
		||||
StartLimitBurst=3
 | 
			
		||||
 | 
			
		||||
@ -22,9 +22,9 @@ StartLimitBurst=3
 | 
			
		||||
User=telebit
 | 
			
		||||
Group=telebit
 | 
			
		||||
 | 
			
		||||
WorkingDirectory=/opt/telebit-relay
 | 
			
		||||
WorkingDirectory=/opt/telebitd
 | 
			
		||||
# custom directory cannot be set and will be the place where gitea exists, not the working directory
 | 
			
		||||
ExecStart=/opt/telebit-relay/bin/node /opt/telebit-relay/bin/telebit-relay.js --config /opt/telebit-relay/etc/telebit-relay.yml
 | 
			
		||||
ExecStart=/opt/telebitd/bin/node /opt/telebitd/bin/telebitd.js --config /etc/telebit/telebitd.yml
 | 
			
		||||
ExecReload=/bin/kill -USR1 $MAINPID
 | 
			
		||||
 | 
			
		||||
# Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings.
 | 
			
		||||
@ -44,10 +44,10 @@ ProtectSystem=full
 | 
			
		||||
# and /var/log/gitea because we want a place where logs can go.
 | 
			
		||||
# This merely retains r/w access rights, it does not add any new.
 | 
			
		||||
# Must still be writable on the host!
 | 
			
		||||
ReadWriteDirectories=/opt/telebit-relay
 | 
			
		||||
ReadWriteDirectories=/opt/telebitd /etc/telebit
 | 
			
		||||
 | 
			
		||||
# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories
 | 
			
		||||
; ReadWritePaths=/opt/telebit-relay /etc/telebit
 | 
			
		||||
; ReadWritePaths=/opt/telebitd /etc/telebit
 | 
			
		||||
 | 
			
		||||
# The following additional security directives only work with systemd v229 or later.
 | 
			
		||||
# They further retrict privileges that can be gained by gitea.
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
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)
 | 
			
		||||
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'))"
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
agree_tos: true
 | 
			
		||||
community_member: true
 | 
			
		||||
telemetry: true
 | 
			
		||||
vhost: /srv/www/:hostname
 | 
			
		||||
greenlock:
 | 
			
		||||
  version: 'draft-11'
 | 
			
		||||
  server: 'https://acme-v02.api.letsencrypt.org/directory'
 | 
			
		||||
  store:
 | 
			
		||||
    strategy: le-store-certbot
 | 
			
		||||
  config_dir: /opt/telebit-relay/etc/acme
 | 
			
		||||
							
								
								
									
										12
									
								
								examples/telebitd.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								examples/telebitd.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
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
 | 
			
		||||
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)
 | 
			
		||||
greenlock:
 | 
			
		||||
  store: 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'))"
 | 
			
		||||
							
								
								
									
										7
									
								
								examples/telebitd.yml.tpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								examples/telebitd.yml.tpl
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
agree_tos: true
 | 
			
		||||
community_member: true
 | 
			
		||||
telemetry: true
 | 
			
		||||
vhost: /srv/www/:hostname
 | 
			
		||||
greenlock:
 | 
			
		||||
  store: le-store-certbot
 | 
			
		||||
  config_dir: /opt/telebitd/acme
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
var http = require('http');
 | 
			
		||||
var tls = require('tls');
 | 
			
		||||
var wrapSocket = require('proxy-packer').wrapSocket;
 | 
			
		||||
var wrapSocket = require('tunnel-packer').wrapSocket;
 | 
			
		||||
var redirectHttps = require('redirect-https')();
 | 
			
		||||
 | 
			
		||||
function noSniCallback(tag) {
 | 
			
		||||
@ -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) {
 | 
			
		||||
@ -19,8 +19,8 @@ module.exports.create = function (state) {
 | 
			
		||||
  var setupTlsOpts = {
 | 
			
		||||
    SNICallback: function (servername, cb) {
 | 
			
		||||
      if (!setupSniCallback) {
 | 
			
		||||
        console.error("[setup.SNICallback] No way to get https certificates...");
 | 
			
		||||
        cb(new Error("telebit-relay sni setup fail"));
 | 
			
		||||
        console.error("No way to get https certificates...");
 | 
			
		||||
        cb(new Error("telebitd sni setup fail"));
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      setupSniCallback(servername, cb);
 | 
			
		||||
@ -29,6 +29,7 @@ module.exports.create = function (state) {
 | 
			
		||||
 | 
			
		||||
  // Probably a reverse proxy on an internal network (or ACME challenge)
 | 
			
		||||
  function notFound(req, res) {
 | 
			
		||||
    console.log('req.socket.encrypted', req.socket.encrypted);
 | 
			
		||||
    res.statusCode = 404;
 | 
			
		||||
    res.end("File not found.\n");
 | 
			
		||||
  }
 | 
			
		||||
@ -52,7 +53,7 @@ module.exports.create = function (state) {
 | 
			
		||||
    || redirectHttpsAndClose
 | 
			
		||||
  );
 | 
			
		||||
  state.handleInsecureHttp = function (servername, socket) {
 | 
			
		||||
    console.log("[handlers] insecure http for '" + servername + "'");
 | 
			
		||||
    console.log("handleInsecureHttp('" + servername + "', socket)");
 | 
			
		||||
    socket.__my_servername = servername;
 | 
			
		||||
    state.httpInsecureServer.emit('connection', socket);
 | 
			
		||||
  };
 | 
			
		||||
@ -72,11 +73,20 @@ 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) {
 | 
			
		||||
  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', servername);
 | 
			
		||||
    //state.tlsInvalidSniServer.emit('connection', wrapSocket(socket));
 | 
			
		||||
    var tlsInvalidSniServer = tls.createServer(state.tlsOptions, function (tlsSocket) {
 | 
			
		||||
      console.log('tls connection');
 | 
			
		||||
      // things get a little messed up here
 | 
			
		||||
      var httpInvalidSniServer = http.createServer(function (req, res) {
 | 
			
		||||
        if (!servername) {
 | 
			
		||||
          res.statusCode = 422;
 | 
			
		||||
        res.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"
 | 
			
		||||
@ -86,30 +96,15 @@ module.exports.create = function (state) {
 | 
			
		||||
          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>"
 | 
			
		||||
      );
 | 
			
		||||
          "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.");
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
  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', opts.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);
 | 
			
		||||
      httpInvalidSniServer.emit('connection', tlsSocket);
 | 
			
		||||
    });
 | 
			
		||||
    tlsInvalidSniServer.on('tlsClientError', function () {
 | 
			
		||||
      console.error('tlsClientError InvalidSniServer httpsInvalid');
 | 
			
		||||
@ -120,44 +115,30 @@ module.exports.create = function (state) {
 | 
			
		||||
  //
 | 
			
		||||
  // To ADMIN / CONTROL PANEL of the Tunnel Server Itself
 | 
			
		||||
  //
 | 
			
		||||
  var serveAdmin = require('serve-static')(__dirname + '/../admin', { redirect: true });
 | 
			
		||||
  var serveAdmin = require('serve-static')(__dirname + '/admin', { redirect: true });
 | 
			
		||||
  var finalhandler = require('finalhandler');
 | 
			
		||||
  state.defaults.webadmin = function (req, res) {
 | 
			
		||||
    serveAdmin(req, res, finalhandler(req, res));
 | 
			
		||||
  };
 | 
			
		||||
  state.httpTunnelServer = http.createServer(function (req, res) {
 | 
			
		||||
    res.setHeader('connection', 'close');
 | 
			
		||||
    if (state.extensions.webadmin) {
 | 
			
		||||
      state.extensions.webadmin(state, req, res);
 | 
			
		||||
    } else {
 | 
			
		||||
      state.defaults.webadmin(req, res);
 | 
			
		||||
    }
 | 
			
		||||
    console.log('req.socket.encrypted', req.socket.encrypted);
 | 
			
		||||
    serveAdmin(req, res, finalhandler(req, res));
 | 
			
		||||
  });
 | 
			
		||||
  Object.keys(state.tlsOptions).forEach(function (key) {
 | 
			
		||||
    tunnelAdminTlsOpts[key] = state.tlsOptions[key];
 | 
			
		||||
  });
 | 
			
		||||
  if (state.greenlock && state.greenlock.tlsOptions) {
 | 
			
		||||
    tunnelAdminTlsOpts.SNICallback = state.greenlock.tlsOptions.SNICallback;
 | 
			
		||||
    console.log('greenlock tlsOptions for SNICallback');
 | 
			
		||||
    tunnelAdminTlsOpts.SNICallback = function (servername, cb) {
 | 
			
		||||
      console.log("time to handle '" + servername + "'");
 | 
			
		||||
      state.greenlock.tlsOptions.SNICallback(servername, cb);
 | 
			
		||||
    };
 | 
			
		||||
  } else {
 | 
			
		||||
    console.log('[Admin] custom or null tlsOptions for SNICallback');
 | 
			
		||||
    console.log('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;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    console.log('(Admin) tls connection');
 | 
			
		||||
    // 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');
 | 
			
		||||
  });
 | 
			
		||||
@ -167,36 +148,38 @@ module.exports.create = function (state) {
 | 
			
		||||
    // tlsServer.emit('connection', socket);    // this didn't work either
 | 
			
		||||
    //console.log('chunkLen', firstChunk.byteLength);
 | 
			
		||||
 | 
			
		||||
    if (state.debug) { console.log('[Admin] new raw tls connection for', servername); }
 | 
			
		||||
    console.log('httpsTunnel (Admin) servername', servername);
 | 
			
		||||
    state.tlsTunnelServer.emit('connection', wrapSocket(socket));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  //
 | 
			
		||||
  // First time setup
 | 
			
		||||
  //
 | 
			
		||||
  var serveSetup = require('serve-static')(__dirname + '/../admin/setup', { redirect: true });
 | 
			
		||||
  var serveSetup = require('serve-static')(__dirname + '/admin/setup', { redirect: true });
 | 
			
		||||
  var finalhandler = require('finalhandler');
 | 
			
		||||
  state.httpSetupServer = http.createServer(function (req, res) {
 | 
			
		||||
    console.log('req.socket.encrypted', req.socket.encrypted);
 | 
			
		||||
    if (req.socket.encrypted) {
 | 
			
		||||
      serveSetup(req, res, finalhandler(req, res));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    console.log('try greenlock middleware');
 | 
			
		||||
    (state.greenlock && state.greenlock.middleware(redirectHttpsAndClose)
 | 
			
		||||
      || redirectHttpsAndClose)(req, res, function () {
 | 
			
		||||
      console.log('[Setup] fallthrough to setup ui');
 | 
			
		||||
      console.log('fallthrough to setup ui');
 | 
			
		||||
      serveSetup(req, res, finalhandler(req, res));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  state.tlsSetupServer = tls.createServer(setupTlsOpts, function (tlsSocket) {
 | 
			
		||||
    console.log('[Setup] terminated tls connection');
 | 
			
		||||
    console.log('tls connection');
 | 
			
		||||
    // things get a little messed up here
 | 
			
		||||
    state.httpSetupServer.emit('connection', tlsSocket);
 | 
			
		||||
  });
 | 
			
		||||
  state.tlsSetupServer.on('tlsClientError', function () {
 | 
			
		||||
    console.error('[Setup] tlsClientError SetupServer');
 | 
			
		||||
    console.error('tlsClientError SetupServer');
 | 
			
		||||
  });
 | 
			
		||||
  state.httpsSetupServer = function (servername, socket) {
 | 
			
		||||
    console.log('[Setup] raw tls connection for', servername);
 | 
			
		||||
    console.log('httpsTunnel (Setup) servername', servername);
 | 
			
		||||
    state._servernames = [servername];
 | 
			
		||||
    state.config.agreeTos = true; // TODO: BUG XXX BAD, make user accept
 | 
			
		||||
    setupSniCallback = state.greenlock.tlsOptions.SNICallback || noSniCallback('setup');
 | 
			
		||||
@ -207,38 +190,39 @@ module.exports.create = function (state) {
 | 
			
		||||
  // vhost
 | 
			
		||||
  //
 | 
			
		||||
  state.httpVhost = http.createServer(function (req, res) {
 | 
			
		||||
    if (state.debug) { console.log('[vhost] encrypted?', req.socket.encrypted); }
 | 
			
		||||
    console.log('httpVhost (local)');
 | 
			
		||||
    console.log('req.socket.encrypted', req.socket.encrypted);
 | 
			
		||||
 | 
			
		||||
    var finalhandler = require('finalhandler');
 | 
			
		||||
    // TODO compare SNI to hostname?
 | 
			
		||||
    var host = (req.headers.host||'').toLowerCase().trim();
 | 
			
		||||
    var serveVhost = require('serve-static')(state.config.vhost.replace(/:hostname/g, host), { redirect: true });
 | 
			
		||||
    var serveSetup = require('serve-static')(state.config.vhost.replace(/:hostname/g, host), { redirect: true });
 | 
			
		||||
 | 
			
		||||
    if (req.socket.encrypted) { serveVhost(req, res, finalhandler(req, res)); return; }
 | 
			
		||||
    if (req.socket.encrypted) { serveSetup(req, res, finalhandler(req, res)); return; }
 | 
			
		||||
 | 
			
		||||
    if (!state.greenlock) {
 | 
			
		||||
      console.error("Cannot vhost without greenlock options");
 | 
			
		||||
      res.end("Cannot vhost without greenlock options");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state.greenlock.middleware(redirectHttpsAndClose);
 | 
			
		||||
    console.log('try greenlock middleware for vhost');
 | 
			
		||||
    (state.greenlock && state.greenlock.middleware(redirectHttpsAndClose)
 | 
			
		||||
      || redirectHttpsAndClose)(req, res, function () {
 | 
			
		||||
      console.log('fallthrough to vhost serving???');
 | 
			
		||||
      serveSetup(req, res, finalhandler(req, res));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  state.tlsVhost = tls.createServer(
 | 
			
		||||
    { SNICallback: function (servername, cb) {
 | 
			
		||||
        if (state.debug) { console.log('[vhost] SNICallback for', servername); }
 | 
			
		||||
        console.log('tlsVhost debug SNICallback', servername);
 | 
			
		||||
        tunnelAdminTlsOpts.SNICallback(servername, cb);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  , function (tlsSocket) {
 | 
			
		||||
      if (state.debug) { console.log('tlsVhost (local)'); }
 | 
			
		||||
      console.log('tlsVhost (local)');
 | 
			
		||||
      state.httpVhost.emit('connection', tlsSocket);
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
  state.tlsVhost.on('tlsClientError', function (e) {
 | 
			
		||||
    console.error('tlsClientError Vhost', e);
 | 
			
		||||
  state.tlsVhost.on('tlsClientError', function () {
 | 
			
		||||
    console.error('tlsClientError Vhost');
 | 
			
		||||
  });
 | 
			
		||||
  state.httpsVhost = function (servername, socket) {
 | 
			
		||||
    if (state.debug) { console.log('[vhost] httpsVhost (local) for', servername); }
 | 
			
		||||
    console.log('httpsVhost (local)', servername);
 | 
			
		||||
    state.tlsVhost.emit('connection', wrapSocket(socket));
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										118
									
								
								installer/get.sh
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								installer/get.sh
									
									
									
									
									
								
							@ -60,15 +60,18 @@ detect_http_get
 | 
			
		||||
##       END HTTP_GET        ##
 | 
			
		||||
###############################
 | 
			
		||||
 | 
			
		||||
echo ""
 | 
			
		||||
echo ""
 | 
			
		||||
echo ""
 | 
			
		||||
 | 
			
		||||
my_email=${1:-}
 | 
			
		||||
my_servername=${2:-}
 | 
			
		||||
my_secret=""
 | 
			
		||||
my_user="telebit"
 | 
			
		||||
my_app="telebit-relay"
 | 
			
		||||
my_bin="telebit-relay.js"
 | 
			
		||||
my_app="telebitd"
 | 
			
		||||
my_bin="telebitd.js"
 | 
			
		||||
my_name="Telebit Relay"
 | 
			
		||||
my_repo="telebit-relay.js"
 | 
			
		||||
exec 3<>/dev/tty
 | 
			
		||||
my_repo="telebitd.js"
 | 
			
		||||
 | 
			
		||||
if [ -z "${my_email}" ]; then
 | 
			
		||||
  echo ""
 | 
			
		||||
@ -78,116 +81,113 @@ if [ -z "${my_email}" ]; then
 | 
			
		||||
  echo "To accept the Terms of Service for Telebit, Greenlock and Let's Encrypt,"
 | 
			
		||||
  echo "please enter your email."
 | 
			
		||||
  echo ""
 | 
			
		||||
  read -u 3 -p "email: " my_email
 | 
			
		||||
  read -p "email: " my_email
 | 
			
		||||
  echo ""
 | 
			
		||||
  # UX - just want a smooth transition
 | 
			
		||||
  sleep 0.5
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -z "${my_servername}" ]; then
 | 
			
		||||
  echo "What is the domain of this server (for admin interface)?"
 | 
			
		||||
  echo ""
 | 
			
		||||
  read -u 3 -p "domain (ex: telebit-relay.example.com): " my_servername
 | 
			
		||||
  read -p "domain (ex: telebit.example.com): " my_servername
 | 
			
		||||
  echo ""
 | 
			
		||||
  # UX - just want a smooth transition
 | 
			
		||||
  sleep 0.5
 | 
			
		||||
fi
 | 
			
		||||
sleep 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if [ -z "${TELEBITD_PATH:-}" ]; then
 | 
			
		||||
  echo 'TELEBITD_PATH="'${TELEBITD_PATH:-}'"'
 | 
			
		||||
  TELEBITD_PATH=/opt/$my_app
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
echo ""
 | 
			
		||||
echo "Installing $my_name to '$TELEBITD_PATH'"
 | 
			
		||||
 | 
			
		||||
if [ -z "${TELEBIT_RELAY_PATH:-}" ]; then
 | 
			
		||||
  echo 'TELEBIT_RELAY_PATH="'${TELEBIT_RELAY_PATH:-}'"'
 | 
			
		||||
  TELEBIT_RELAY_PATH=/opt/$my_app
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
echo "Installing $my_name to '$TELEBIT_RELAY_PATH'"
 | 
			
		||||
 | 
			
		||||
echo "Installing node.js dependencies into $TELEBIT_RELAY_PATH"
 | 
			
		||||
echo "Installing node.js dependencies into $TELEBITD_PATH"
 | 
			
		||||
# v10.2+ has much needed networking fixes, but breaks ursa. v9.x has severe networking bugs. v8.x has working ursa, but requires tls workarounds"
 | 
			
		||||
NODEJS_VER="${NODEJS_VER:-v10}"
 | 
			
		||||
export NODEJS_VER
 | 
			
		||||
export NODE_PATH="$TELEBIT_RELAY_PATH/lib/node_modules"
 | 
			
		||||
export NPM_CONFIG_PREFIX="$TELEBIT_RELAY_PATH"
 | 
			
		||||
export PATH="$TELEBIT_RELAY_PATH/bin:$PATH"
 | 
			
		||||
export NODE_PATH="$TELEBITD_PATH/lib/node_modules"
 | 
			
		||||
export NPM_CONFIG_PREFIX="$TELEBITD_PATH"
 | 
			
		||||
export PATH="$TELEBITD_PATH/bin:$PATH"
 | 
			
		||||
sleep 1
 | 
			
		||||
http_bash https://git.coolaj86.com/coolaj86/node-installer.sh/raw/branch/master/install.sh --no-dev-deps >/dev/null 2>/dev/null
 | 
			
		||||
 | 
			
		||||
my_tree="master"
 | 
			
		||||
my_node="$TELEBIT_RELAY_PATH/bin/node"
 | 
			
		||||
my_node="$TELEBITD_PATH/bin/node"
 | 
			
		||||
my_secret=$($my_node -e "console.info(crypto.randomBytes(16).toString('hex'))")
 | 
			
		||||
my_npm="$my_node $TELEBIT_RELAY_PATH/bin/npm"
 | 
			
		||||
my_tmp="$TELEBIT_RELAY_PATH/tmp"
 | 
			
		||||
my_npm="$my_node $TELEBITD_PATH/bin/npm"
 | 
			
		||||
my_tmp="$TELEBITD_PATH/tmp"
 | 
			
		||||
mkdir -p $my_tmp
 | 
			
		||||
 | 
			
		||||
echo "sudo mkdir -p '$TELEBIT_RELAY_PATH'"
 | 
			
		||||
sudo mkdir -p "$TELEBIT_RELAY_PATH"
 | 
			
		||||
echo "sudo mkdir -p '$TELEBIT_RELAY_PATH/etc'"
 | 
			
		||||
sudo mkdir -p "$TELEBIT_RELAY_PATH/etc/"
 | 
			
		||||
echo "sudo mkdir -p '$TELEBITD_PATH'"
 | 
			
		||||
sudo mkdir -p "$TELEBITD_PATH"
 | 
			
		||||
echo "sudo mkdir -p '/etc/$my_user/'"
 | 
			
		||||
sudo mkdir -p "/etc/$my_user/"
 | 
			
		||||
 | 
			
		||||
set +e
 | 
			
		||||
#https://git.coolaj86.com/coolaj86/telebit-relay.js.git
 | 
			
		||||
#https://git.coolaj86.com/coolaj86/telebit-relay.js/archive/:tree:.tar.gz
 | 
			
		||||
#https://git.coolaj86.com/coolaj86/telebit-relay.js/archive/:tree:.zip
 | 
			
		||||
#https://git.coolaj86.com/coolaj86/telebitd.js.git
 | 
			
		||||
#https://git.coolaj86.com/coolaj86/telebitd.js/archive/:tree:.tar.gz
 | 
			
		||||
#https://git.coolaj86.com/coolaj86/telebitd.js/archive/:tree:.zip
 | 
			
		||||
my_unzip=$(type -p unzip)
 | 
			
		||||
my_tar=$(type -p tar)
 | 
			
		||||
if [ -n "$my_unzip" ]; then
 | 
			
		||||
  rm -f $my_tmp/$my_app-$my_tree.zip
 | 
			
		||||
  http_get https://git.coolaj86.com/coolaj86/$my_repo/archive/$my_tree.zip $my_tmp/$my_app-$my_tree.zip
 | 
			
		||||
  # -o means overwrite, and there is no option to strip
 | 
			
		||||
  $my_unzip -o $my_tmp/$my_app-$my_tree.zip -d $TELEBIT_RELAY_PATH/ > /dev/null 2>&1
 | 
			
		||||
  cp -ar  $TELEBIT_RELAY_PATH/$my_repo/* $TELEBIT_RELAY_PATH/ > /dev/null
 | 
			
		||||
  rm -rf $TELEBIT_RELAY_PATH/$my_bin
 | 
			
		||||
  $my_unzip -o $my_tmp/$my_app-$my_tree.zip -d $TELEBITD_PATH/ > /dev/null 2>&1
 | 
			
		||||
  cp -ar  $TELEBITD_PATH/$my_repo/* $TELEBITD_PATH/ > /dev/null
 | 
			
		||||
  rm -rf $TELEBITD_PATH/$my_bin
 | 
			
		||||
elif [ -n "$my_tar" ]; then
 | 
			
		||||
  rm -f $my_tmp/$my_app-$my_tree.tar.gz
 | 
			
		||||
  http_get https://git.coolaj86.com/coolaj86/$my_repo/archive/$my_tree.tar.gz $my_tmp/$my_app-$my_tree.tar.gz
 | 
			
		||||
  ls -lah $my_tmp/$my_app-$my_tree.tar.gz
 | 
			
		||||
  $my_tar -xzf $my_tmp/$my_app-$my_tree.tar.gz --strip 1 -C $TELEBIT_RELAY_PATH/
 | 
			
		||||
  $my_tar -xzf $my_tmp/$my_app-$my_tree.tar.gz --strip 1 -C $TELEBITD_PATH/
 | 
			
		||||
else
 | 
			
		||||
  echo "Neither tar nor unzip found. Abort."
 | 
			
		||||
  exit 13
 | 
			
		||||
fi
 | 
			
		||||
set -e
 | 
			
		||||
 | 
			
		||||
pushd $TELEBIT_RELAY_PATH >/dev/null
 | 
			
		||||
pushd $TELEBITD_PATH >/dev/null
 | 
			
		||||
  $my_npm install >/dev/null 2>/dev/null
 | 
			
		||||
popd >/dev/null
 | 
			
		||||
 | 
			
		||||
cat << EOF > $TELEBIT_RELAY_PATH/bin/$my_app
 | 
			
		||||
cat << EOF > $TELEBITD_PATH/bin/$my_app
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
$my_node $TELEBIT_RELAY_PATH/bin/$my_bin
 | 
			
		||||
$my_node $TELEBITD_PATH/bin/$my_bin
 | 
			
		||||
EOF
 | 
			
		||||
chmod a+x $TELEBIT_RELAY_PATH/bin/$my_app
 | 
			
		||||
echo "sudo ln -sf $TELEBIT_RELAY_PATH/bin/$my_app /usr/local/bin/$my_app"
 | 
			
		||||
sudo ln -sf $TELEBIT_RELAY_PATH/bin/$my_app /usr/local/bin/$my_app
 | 
			
		||||
chmod a+x $TELEBITD_PATH/bin/$my_app
 | 
			
		||||
echo "sudo ln -sf $TELEBITD_PATH/bin/$my_app /usr/local/bin/$my_app"
 | 
			
		||||
sudo ln -sf $TELEBITD_PATH/bin/$my_app /usr/local/bin/$my_app
 | 
			
		||||
 | 
			
		||||
set +e
 | 
			
		||||
if type -p setcap >/dev/null 2>&1; then
 | 
			
		||||
  #echo "Setting permissions to allow $my_app to run on port 80 and port 443 without sudo or root"
 | 
			
		||||
  echo "sudo setcap cap_net_bind_service=+ep $TELEBIT_RELAY_PATH/bin/node"
 | 
			
		||||
  sudo setcap cap_net_bind_service=+ep $TELEBIT_RELAY_PATH/bin/node
 | 
			
		||||
  echo "sudo setcap cap_net_bind_service=+ep $TELEBITD_PATH/bin/node"
 | 
			
		||||
  sudo setcap cap_net_bind_service=+ep $TELEBITD_PATH/bin/node
 | 
			
		||||
fi
 | 
			
		||||
set -e
 | 
			
		||||
 | 
			
		||||
if [ -z "$(cat /etc/passwd | grep $my_user)" ]; then
 | 
			
		||||
  echo "sudo adduser --home $TELEBIT_RELAY_PATH --gecos '' --disabled-password $my_user"
 | 
			
		||||
  sudo adduser --home $TELEBIT_RELAY_PATH --gecos '' --disabled-password $my_user >/dev/null 2>&1
 | 
			
		||||
  echo "sudo adduser --home $TELEBITD_PATH --gecos '' --disabled-password $my_user"
 | 
			
		||||
  sudo adduser --home $TELEBITD_PATH --gecos '' --disabled-password $my_user >/dev/null 2>&1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ ! -f "$TELEBIT_RELAY_PATH/etc/$my_app.yml" ]; then
 | 
			
		||||
if [ ! -f "/etc/$my_user/$my_app.yml" ]; then
 | 
			
		||||
  echo "### Creating config file from template. sudo may be required"
 | 
			
		||||
  #echo "sudo rsync -a examples/$my_app.yml $TELEBIT_RELAY_PATH/etc/$my_app.yml"
 | 
			
		||||
  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 "cat $TELEBIT_RELAY_PATH/examples/$my_app.yml.tpl >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
 | 
			
		||||
  #echo "sudo rsync -a examples/$my_app.yml /etc/$my_user/$my_app.yml"
 | 
			
		||||
  sudo bash -c "echo 'email: $my_email' >> /etc/$my_user/$my_app.yml"
 | 
			
		||||
  sudo bash -c "echo 'secret: $my_secret' >> /etc/$my_user/$my_app.yml"
 | 
			
		||||
  sudo bash -c "echo 'servernames: [ $my_servername ]' >> /etc/$my_user/$my_app.yml"
 | 
			
		||||
  sudo bash -c "cat examples/$my_app.yml.tpl >> /etc/$my_user/$my_app.yml"
 | 
			
		||||
  sudo bash -c "echo 'servernames: []' >> /etc/$my_user/$my_app.yml"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
echo "sudo chown -R $my_user '$TELEBIT_RELAY_PATH'"
 | 
			
		||||
sudo chown -R $my_user "$TELEBIT_RELAY_PATH"
 | 
			
		||||
echo "sudo chown -R $my_user '$TELEBITD_PATH' '/etc/$my_user'"
 | 
			
		||||
sudo chown -R $my_user "$TELEBITD_PATH" "/etc/$my_user"
 | 
			
		||||
 | 
			
		||||
echo "### Adding $my_app is a system service"
 | 
			
		||||
echo "sudo rsync -a $TELEBIT_RELAY_PATH/dist/etc/systemd/system/$my_app.service /etc/systemd/system/$my_app.service"
 | 
			
		||||
sudo rsync -a $TELEBIT_RELAY_PATH/dist/etc/systemd/system/$my_app.service /etc/systemd/system/$my_app.service
 | 
			
		||||
echo "sudo rsync -a $TELEBITD_PATH/dist/etc/systemd/system/$my_app.service /etc/systemd/system/$my_app.service"
 | 
			
		||||
sudo rsync -a $TELEBITD_PATH/dist/etc/systemd/system/$my_app.service /etc/systemd/system/$my_app.service
 | 
			
		||||
sudo systemctl daemon-reload
 | 
			
		||||
echo "sudo systemctl enable $my_app"
 | 
			
		||||
sudo systemctl enable $my_app
 | 
			
		||||
@ -202,7 +202,7 @@ echo "=============================================="
 | 
			
		||||
echo "  Privacy Settings in Config"
 | 
			
		||||
echo "=============================================="
 | 
			
		||||
echo ""
 | 
			
		||||
echo "The example config file $TELEBIT_RELAY_PATH/etc/$my_app.yml opts-in to"
 | 
			
		||||
echo "The example config file /etc/telebit/telebitd.yml opts-in to"
 | 
			
		||||
echo "contributing telemetrics and receiving infrequent relevant updates"
 | 
			
		||||
echo "(probably once per quarter or less) such as important notes on"
 | 
			
		||||
echo "a new release, an important API change, etc. No spam."
 | 
			
		||||
@ -219,13 +219,13 @@ echo "=============================================="
 | 
			
		||||
echo ""
 | 
			
		||||
echo "Edit the config and restart, if desired:"
 | 
			
		||||
echo ""
 | 
			
		||||
echo "    sudo vim $TELEBIT_RELAY_PATH/etc/$my_app.yml"
 | 
			
		||||
echo "    sudo vim /etc/telebit/telebitd.yml"
 | 
			
		||||
echo "    sudo systemctl restart $my_app"
 | 
			
		||||
echo ""
 | 
			
		||||
echo "Or disabled the service and start manually:"
 | 
			
		||||
echo ""
 | 
			
		||||
echo "    sudo systemctl stop $my_app"
 | 
			
		||||
echo "    sudo systemctl disable $my_app"
 | 
			
		||||
echo "    $my_app --config $TELEBIT_RELAY_PATH/etc/$my_app.yml"
 | 
			
		||||
echo "    $my_app --config /etc/$my_user/$my_app.yml"
 | 
			
		||||
echo ""
 | 
			
		||||
sleep 1
 | 
			
		||||
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
systemctl disable telebit-relay
 | 
			
		||||
systemctl stop telebit-relay
 | 
			
		||||
rm -rf /opt/telebit-relay/ /etc/system/systemd/telebit-relay.service /usr/local/bin/telebit-relay /etc/telebit/
 | 
			
		||||
userdel -r telebit
 | 
			
		||||
groupdel telebit
 | 
			
		||||
@ -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,138 +1,45 @@
 | 
			
		||||
'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.alias = function (store, servername, alias) {
 | 
			
		||||
  if (!store._domains[servername]) {
 | 
			
		||||
    store._domains[servername] = [];
 | 
			
		||||
  }
 | 
			
		||||
  if (!store._domains[servername]._primary) {
 | 
			
		||||
    store._domains[servername]._primary = servername;
 | 
			
		||||
  }
 | 
			
		||||
  if (!store._domains[servername].aliases) {
 | 
			
		||||
    store._domains[servername].aliases = {};
 | 
			
		||||
  }
 | 
			
		||||
  store._domains[alias] = store._domains[servername];
 | 
			
		||||
  store._domains[servername].aliases[alias] = true;
 | 
			
		||||
Devices.add = function (store, servername, newDevice) {
 | 
			
		||||
  var devices = store[servername] || [];
 | 
			
		||||
  devices.push(newDevice);
 | 
			
		||||
  store[servername] = devices;
 | 
			
		||||
};
 | 
			
		||||
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];
 | 
			
		||||
  }
 | 
			
		||||
  // 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) {
 | 
			
		||||
    var subPiece = pattern.slice(1);
 | 
			
		||||
    if (subPiece === servername.slice(-subPiece.length)) {
 | 
			
		||||
      console.log('[Devices.list] "'+servername+'" matches "'+pattern+'"');
 | 
			
		||||
      deviceList = store._domains[pattern];
 | 
			
		||||
 | 
			
		||||
      // Devices.alias(store, '*.example.com', 'sub.example.com'
 | 
			
		||||
      // '*.example.com' retrieves a reference to 'example.com'
 | 
			
		||||
      // and this reference then also referenced by 'sub.example.com'
 | 
			
		||||
      // Hence this O(n) check is replaced with the O(1) check above
 | 
			
		||||
      Devices.alias(store, pattern, servername);
 | 
			
		||||
      console.log('"'+servername+'" matches "'+pattern+'"');
 | 
			
		||||
      deviceList = store[pattern];
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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 +51,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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,67 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var Packer = require('proxy-packer');
 | 
			
		||||
 | 
			
		||||
module.exports = function pipeWs(servername, service, srv, conn, serviceport) {
 | 
			
		||||
  var browserAddr = Packer.socketToAddr(conn);
 | 
			
		||||
  var cid = Packer.addrToId(browserAddr);
 | 
			
		||||
  browserAddr.service = service;
 | 
			
		||||
  browserAddr.serviceport = serviceport;
 | 
			
		||||
  browserAddr.name = servername;
 | 
			
		||||
  conn.tunnelCid = cid;
 | 
			
		||||
  var rid = Packer.socketToId(srv.upgradeReq.socket);
 | 
			
		||||
 | 
			
		||||
  //if (state.debug) { console.log('[pipeWs] client', cid, '=> remote', rid, 'for', servername, 'via', service); }
 | 
			
		||||
 | 
			
		||||
  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 });
 | 
			
		||||
        }
 | 
			
		||||
        // 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 && (srv.pausedConns.length || srv.ws.bufferedAmount > 1024*1024)) {
 | 
			
		||||
          // console.log('pausing', cid, 'to allow web socket to catch up');
 | 
			
		||||
          conn.pause();
 | 
			
		||||
          srv.pausedConns.push(conn);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        console.warn('[pipeWs] srv', rid, ' => client', cid, 'error sending websocket message', err);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  srv.clients[cid] = conn;
 | 
			
		||||
  conn.servername = servername;
 | 
			
		||||
  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);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  conn.on('error', function (err) {
 | 
			
		||||
    console.warn('[pipeWs] client', cid, 'connection error:', err);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  conn.on('close', function (hadErr) {
 | 
			
		||||
    //if (state.debug) { console.log('[pipeWs] client', cid, 'closing'); }
 | 
			
		||||
    sendWs(null, hadErr ? 'error': 'end');
 | 
			
		||||
    delete srv.clients[cid];
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										87
									
								
								lib/relay.js
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								lib/relay.js
									
									
									
									
									
								
							@ -1,87 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var Packer = require('proxy-packer');
 | 
			
		||||
var Devices = require('./device-tracker');
 | 
			
		||||
var Server = require('./server.js');
 | 
			
		||||
 | 
			
		||||
module.exports.store = { Devices: Devices };
 | 
			
		||||
module.exports.create = function (state) {
 | 
			
		||||
  state.deviceLists = { _domains: {}, _devices: {} };
 | 
			
		||||
  state.deviceCallbacks = {};
 | 
			
		||||
  state.srvs = {};
 | 
			
		||||
 | 
			
		||||
  if (!parseInt(state.activityTimeout, 10)) {
 | 
			
		||||
    state.activityTimeout = 2 * 60 * 1000;
 | 
			
		||||
  }
 | 
			
		||||
  if (!parseInt(state.pongTimeout, 10)) {
 | 
			
		||||
    state.pongTimeout = 10 * 1000;
 | 
			
		||||
  }
 | 
			
		||||
  state.Devices = Devices;
 | 
			
		||||
 | 
			
		||||
  // TODO Use a Single TCP Handler
 | 
			
		||||
  // Issues:
 | 
			
		||||
  //   * dynamic ports are dedicated to a device or cluster
 | 
			
		||||
  //   * servernames could come in on ports that belong to a different device
 | 
			
		||||
  //   * servernames could come in that belong to no device
 | 
			
		||||
  //   * this could lead to an attack / security vulnerability with ACME certificates
 | 
			
		||||
  // Solutions
 | 
			
		||||
  //   * Restrict dynamic ports to a particular device
 | 
			
		||||
  //   * Restrict the use of servernames
 | 
			
		||||
 | 
			
		||||
  function onWsConnection(_ws, _upgradeReq) {
 | 
			
		||||
    var srv = {};
 | 
			
		||||
    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); }
 | 
			
		||||
 | 
			
		||||
    initToken = Server.parseAuth(state, srv);
 | 
			
		||||
 | 
			
		||||
    srv.ws._socket.on('drain', function () {
 | 
			
		||||
      // the websocket library has it's own buffer apart from node's socket buffer, but that one
 | 
			
		||||
      // is much more difficult to watch, so we watch for the lower level buffer to drain and
 | 
			
		||||
      // then check to see if the upper level buffer is still too full to write to. Note that
 | 
			
		||||
      // the websocket library buffer has something to do with compression, so I'm not requiring
 | 
			
		||||
      // that to be 0 before we start up again.
 | 
			
		||||
      if (srv.ws.bufferedAmount > 128*1024) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      srv.pausedConns.forEach(function (conn) {
 | 
			
		||||
        if (!conn.manualPause) {
 | 
			
		||||
          // console.log('resuming', conn.tunnelCid, 'now that the web socket has caught up');
 | 
			
		||||
          conn.resume();
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      srv.pausedConns.length = 0;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (initToken) {
 | 
			
		||||
      return Server.addToken(state, srv, initToken).then(function () {
 | 
			
		||||
        Server.init(state, srv);
 | 
			
		||||
      }).catch(function (err) {
 | 
			
		||||
        Server.sendTunnelMsg(srv, null, [0, err], 'control');
 | 
			
		||||
        srv.ws.close();
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      return Server.init(state, srv);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    tcp: require('./unwrap-tls').createTcpConnectionHandler(state)
 | 
			
		||||
  , ws: onWsConnection
 | 
			
		||||
  , isClientDomain: Devices.exist.bind(null, state.deviceLists)
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										580
									
								
								lib/server.js
									
									
									
									
									
								
							
							
						
						
									
										580
									
								
								lib/server.js
									
									
									
									
									
								
							@ -1,580 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var url = require('url');
 | 
			
		||||
var sni = require('sni');
 | 
			
		||||
var Packer = require('proxy-packer');
 | 
			
		||||
var PromiseA;
 | 
			
		||||
try {
 | 
			
		||||
  PromiseA = require('bluebird');
 | 
			
		||||
} catch(e) {
 | 
			
		||||
  PromiseA = global.Promise;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function timeoutPromise(duration) {
 | 
			
		||||
  return new PromiseA(function (resolve) {
 | 
			
		||||
    setTimeout(resolve, duration);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
var Devices = require('./device-tracker');
 | 
			
		||||
var pipeWs = require('./pipe-ws.js');
 | 
			
		||||
var PortServers = {};
 | 
			
		||||
var Server = {
 | 
			
		||||
  _initCommandHandlers: function (state, srv) {
 | 
			
		||||
    var commandHandlers = {
 | 
			
		||||
      add_token: function addToken(newAuth) {
 | 
			
		||||
        return Server.addToken(state, srv, newAuth);
 | 
			
		||||
      }
 | 
			
		||||
    , delete_token: function (token) {
 | 
			
		||||
        return state.Promise.resolve(function () {
 | 
			
		||||
          var err;
 | 
			
		||||
 | 
			
		||||
          if (token !== '*') {
 | 
			
		||||
            err = Server.removeToken(state, srv, token);
 | 
			
		||||
            if (err) { return state.Promise.reject(err); }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          Object.keys(srv.grants).some(function (jwtoken) {
 | 
			
		||||
            err = Server.removeToken(state, srv, jwtoken);
 | 
			
		||||
            return err;
 | 
			
		||||
          });
 | 
			
		||||
          if (err) { return state.Promise.reject(err); }
 | 
			
		||||
 | 
			
		||||
          return null;
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    commandHandlers.auth = commandHandlers.add_token;
 | 
			
		||||
    commandHandlers.authn = commandHandlers.add_token;
 | 
			
		||||
    commandHandlers.authz = commandHandlers.add_token;
 | 
			
		||||
    srv._commandHandlers = commandHandlers;
 | 
			
		||||
  }
 | 
			
		||||
, _initPackerHandlers: function (state, srv) {
 | 
			
		||||
    var packerHandlers = {
 | 
			
		||||
      oncontrol: function (tun) {
 | 
			
		||||
        var cmd;
 | 
			
		||||
        try {
 | 
			
		||||
          cmd = JSON.parse(tun.data.toString());
 | 
			
		||||
        } catch (e) {}
 | 
			
		||||
        if (!Array.isArray(cmd) || typeof cmd[0] !== 'number') {
 | 
			
		||||
          var msg = 'received bad command "' + tun.data.toString() + '"';
 | 
			
		||||
          console.warn(msg, 'from websocket', srv.socketId);
 | 
			
		||||
          Server.sendTunnelMsg(srv, null, [0, {message: msg, code: 'E_BAD_COMMAND'}], 'control');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cmd[0] < 0) {
 | 
			
		||||
          // We only ever send one command and we send it once, so we just hard coded the ID as 1.
 | 
			
		||||
          if (cmd[0] === -1) {
 | 
			
		||||
            if (cmd[1]) {
 | 
			
		||||
              console.warn('received error response to hello from', srv.socketId, cmd[1]);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            console.warn('received response to unknown command', cmd, 'from', srv.socketId);
 | 
			
		||||
          }
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cmd[0] === 0) {
 | 
			
		||||
          console.warn('received dis-associated error from', srv.socketId, cmd[1]);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function onSuccess() {
 | 
			
		||||
          Server.sendTunnelMsg(srv, null, [-cmd[0], null], 'control');
 | 
			
		||||
        }
 | 
			
		||||
        function onError(err) {
 | 
			
		||||
          Server.sendTunnelMsg(srv, null, [-cmd[0], err], 'control');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!srv._commandHandlers[cmd[1]]) {
 | 
			
		||||
          onError({ message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' });
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log('command:', cmd[1], cmd.slice(2));
 | 
			
		||||
        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); }
 | 
			
		||||
 | 
			
		||||
        var browserConn = Server.getBrowserConn(state, srv, cid);
 | 
			
		||||
        if (!browserConn) {
 | 
			
		||||
          Server.sendTunnelMsg(srv, tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        browserConn.write(tun.data);
 | 
			
		||||
        // tunnelRead is how many bytes we've read from the tunnel, and written to the browser.
 | 
			
		||||
        browserConn.tunnelRead = (browserConn.tunnelRead || 0) + tun.data.byteLength;
 | 
			
		||||
        // If we have more than 1MB buffered data we need to tell the other side to slow down.
 | 
			
		||||
        // Once we've finished sending what we have we can tell the other side to keep going.
 | 
			
		||||
        // If we've already sent the 'pause' message though don't send it again, because we're
 | 
			
		||||
        // probably just dealing with data queued before our message got to them.
 | 
			
		||||
        if (!browserConn.remotePaused && browserConn.bufferSize > 1024*1024) {
 | 
			
		||||
          Server.sendTunnelMsg(srv, tun, browserConn.tunnelRead, 'pause');
 | 
			
		||||
          browserConn.remotePaused = true;
 | 
			
		||||
 | 
			
		||||
          browserConn.once('drain', function () {
 | 
			
		||||
            Server.sendTunnelMsg(srv, tun, browserConn.tunnelRead, 'resume');
 | 
			
		||||
            browserConn.remotePaused = false;
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    , onpause: function (tun) {
 | 
			
		||||
        var cid = Packer.addrToId(tun);
 | 
			
		||||
        console.log('[TunnelPause]', cid);
 | 
			
		||||
        var browserConn = Server.getBrowserConn(state, srv, cid);
 | 
			
		||||
        if (browserConn) {
 | 
			
		||||
          browserConn.manualPause = true;
 | 
			
		||||
          browserConn.pause();
 | 
			
		||||
        } else {
 | 
			
		||||
          Server.sendTunnelMsg(srv, tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    , onresume: function (tun) {
 | 
			
		||||
        var cid = Packer.addrToId(tun);
 | 
			
		||||
        console.log('[TunnelResume]', cid);
 | 
			
		||||
        var browserConn = Server.getBrowserConn(state, srv, cid);
 | 
			
		||||
        if (browserConn) {
 | 
			
		||||
          browserConn.manualPause = false;
 | 
			
		||||
          browserConn.resume();
 | 
			
		||||
        } else {
 | 
			
		||||
          Server.sendTunnelMsg(srv, tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    , onend: function (tun) {
 | 
			
		||||
        var cid = Packer.addrToId(tun);
 | 
			
		||||
        console.log('[TunnelEnd]', cid);
 | 
			
		||||
        Server.closeBrowserConn(state, srv, cid);
 | 
			
		||||
      }
 | 
			
		||||
    , onerror: function (tun) {
 | 
			
		||||
        var cid = Packer.addrToId(tun);
 | 
			
		||||
        console.warn('[TunnelError]', cid, tun.message);
 | 
			
		||||
        Server.closeBrowserConn(state, srv, cid);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    srv._packerHandlers = packerHandlers;
 | 
			
		||||
    srv.unpacker = Packer.create(srv._packerHandlers);
 | 
			
		||||
  }
 | 
			
		||||
, _initSocketHandlers: function (state, srv) {
 | 
			
		||||
    function refreshTimeout() {
 | 
			
		||||
      srv.lastActivity = Date.now();
 | 
			
		||||
      Devices.touchDevice(state.deviceLists, srv);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function checkTimeout() {
 | 
			
		||||
      // Determine how long the connection has been "silent", ie no activity.
 | 
			
		||||
      var silent = Date.now() - srv.lastActivity;
 | 
			
		||||
 | 
			
		||||
      // If we have had activity within the last activityTimeout then all we need to do is
 | 
			
		||||
      // call this function again at the soonest time when the connection could be timed out.
 | 
			
		||||
      if (silent < state.activityTimeout) {
 | 
			
		||||
        srv.timeoutId = setTimeout(checkTimeout, state.activityTimeout - silent);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Otherwise we check to see if the pong has also timed out, and if not we send a ping
 | 
			
		||||
      // and call this function again when the pong will have timed out.
 | 
			
		||||
      else if (silent < state.activityTimeout + state.pongTimeout) {
 | 
			
		||||
        if (state.debug) { console.log('pinging', Server.logName(state, srv)); }
 | 
			
		||||
        try {
 | 
			
		||||
          srv.ws.ping();
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
          console.warn('failed to ping home cloud', Server.logName(state, srv));
 | 
			
		||||
        }
 | 
			
		||||
        srv.timeoutId = setTimeout(checkTimeout, state.pongTimeout);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Last case means the ping we sent before didn't get a response soon enough, so we
 | 
			
		||||
      // need to close the websocket connection.
 | 
			
		||||
      else {
 | 
			
		||||
        console.warn('home cloud', Server.logName(state, srv), 'connection timed out');
 | 
			
		||||
        srv.ws.close(1013, 'connection timeout');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function forwardMessage(chunk) {
 | 
			
		||||
      refreshTimeout();
 | 
			
		||||
      if (state.debug) { console.log('[ws] device => client : demultiplexing message ', chunk.byteLength, 'bytes'); }
 | 
			
		||||
      //console.log(chunk.toString());
 | 
			
		||||
      srv.unpacker.fns.addChunk(chunk);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
    srv.timeoutId = null;
 | 
			
		||||
    srv.timeoutId = setTimeout(checkTimeout, state.activityTimeout);
 | 
			
		||||
 | 
			
		||||
    // Note that our websocket library automatically handles pong responses on ping requests
 | 
			
		||||
    // before it even emits the event.
 | 
			
		||||
    srv.ws.on('ping', refreshTimeout);
 | 
			
		||||
    srv.ws.on('pong', refreshTimeout);
 | 
			
		||||
    srv.ws.on('message', forwardMessage);
 | 
			
		||||
    srv.ws.on('close', hangup);
 | 
			
		||||
    srv.ws.on('error', hangup);
 | 
			
		||||
  }
 | 
			
		||||
, init: function init(state, srv) {
 | 
			
		||||
    Server._initCommandHandlers(state, srv);
 | 
			
		||||
    Server._initPackerHandlers(state, srv);
 | 
			
		||||
    Server._initSocketHandlers(state, srv);
 | 
			
		||||
 | 
			
		||||
    // Status Code '1' for Status 'hello'
 | 
			
		||||
    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});
 | 
			
		||||
  }
 | 
			
		||||
, logName: function logName(state, srv) {
 | 
			
		||||
    var result = Object.keys(srv.grants).map(function (jwtoken) {
 | 
			
		||||
      return srv.grants[jwtoken].currentDesc;
 | 
			
		||||
    }).join(';');
 | 
			
		||||
 | 
			
		||||
    return result || srv.socketId;
 | 
			
		||||
  }
 | 
			
		||||
, onAuth: function onAuth(state, srv, rawAuth, grant) {
 | 
			
		||||
    console.log('\n[relay.js] onAuth');
 | 
			
		||||
    console.log(rawAuth);
 | 
			
		||||
    //console.log(grant);
 | 
			
		||||
    //var stringauth;
 | 
			
		||||
    var err;
 | 
			
		||||
    if (!grant || 'object' !== typeof grant) {
 | 
			
		||||
      console.log('[relay.js] invalid token', grant);
 | 
			
		||||
      err = new Error("invalid access token");
 | 
			
		||||
      err.code = "E_INVALID_TOKEN";
 | 
			
		||||
      return state.Promise.reject(err);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // deprecated (for json object on connect)
 | 
			
		||||
    if ('string' !== typeof rawAuth) {
 | 
			
		||||
      rawAuth = JSON.stringify(rawAuth);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO don't fire the onAuth event on non-authz updates
 | 
			
		||||
    if (!grant.jwt && !(grant.domains||[]).length && !(grant.ports||[]).length) {
 | 
			
		||||
      console.log("[onAuth] nothing to offer at all");
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log('[onAuth] check for upgrade token');
 | 
			
		||||
    //console.log(grant);
 | 
			
		||||
    if (grant.jwt) {
 | 
			
		||||
      if (rawAuth !== grant.jwt) {
 | 
			
		||||
        console.log('[onAuth] token is new');
 | 
			
		||||
      }
 | 
			
		||||
      // TODO only send token when new
 | 
			
		||||
      if (true) {
 | 
			
		||||
        // Access Token
 | 
			
		||||
        console.log('[onAuth] sending back token');
 | 
			
		||||
        Server.sendTunnelMsg(
 | 
			
		||||
          srv
 | 
			
		||||
        , null
 | 
			
		||||
        , [ 3
 | 
			
		||||
          , 'access_token'
 | 
			
		||||
          , { jwt: grant.jwt }
 | 
			
		||||
          ]
 | 
			
		||||
        , 'control'
 | 
			
		||||
        );
 | 
			
		||||
        // these aren't needed internally once they're sent
 | 
			
		||||
        grant.jwt = null;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    if (!Array.isArray(grant.domains) || !grant.domains.length) {
 | 
			
		||||
      err = new Error("invalid domains array");
 | 
			
		||||
      err.code = "E_INVALID_NAME";
 | 
			
		||||
      return state.Promise.reject(err);
 | 
			
		||||
    }
 | 
			
		||||
    */
 | 
			
		||||
    if (grant.domains.some(function (name) { return typeof name !== 'string'; })) {
 | 
			
		||||
      console.log('bad domain names');
 | 
			
		||||
      err = new Error("invalid domain name(s)");
 | 
			
		||||
      err.code = "E_INVALID_NAME";
 | 
			
		||||
      return state.Promise.reject(err);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log('[onAuth] strolling through pleasantries');
 | 
			
		||||
    // Add the custom properties we need to manage this remote, then add it to all the relevant
 | 
			
		||||
    // domains and the list of all this websocket's grants.
 | 
			
		||||
    grant.domains.forEach(function (domainname) {
 | 
			
		||||
      console.log('add', domainname, 'to device lists');
 | 
			
		||||
      srv.domainsMap[domainname] = true;
 | 
			
		||||
      Devices.add(state.deviceLists, domainname, srv);
 | 
			
		||||
      // TODO allow subs to go to individual devices
 | 
			
		||||
      Devices.alias(state.deviceLists, domainname, '*.' + domainname);
 | 
			
		||||
    });
 | 
			
		||||
    srv.domains = Object.keys(srv.domainsMap);
 | 
			
		||||
    srv.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || srv.domains.join(',');
 | 
			
		||||
    grant.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || grant.domains.join(',');
 | 
			
		||||
    //grant.srv = srv;
 | 
			
		||||
    //grant.ws = srv.ws;
 | 
			
		||||
    //grant.upgradeReq = srv.upgradeReq;
 | 
			
		||||
    grant.clients = {};
 | 
			
		||||
 | 
			
		||||
    if (!grant.ports) { grant.ports = []; }
 | 
			
		||||
 | 
			
		||||
    function openPort(serviceport) {
 | 
			
		||||
      function tcpListener(conn) {
 | 
			
		||||
        Server.onDynTcpConn(state, srv, srv.portsMap[serviceport], conn);
 | 
			
		||||
      }
 | 
			
		||||
      serviceport = parseInt(serviceport, 10) || 0;
 | 
			
		||||
      if (!serviceport) {
 | 
			
		||||
        // TODO error message about bad port
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (PortServers[serviceport]) {
 | 
			
		||||
        console.log('reuse', serviceport, 'for this connection');
 | 
			
		||||
        //grant.ports = [];
 | 
			
		||||
        srv.portsMap[serviceport] = PortServers[serviceport];
 | 
			
		||||
        srv.portsMap[serviceport].on('connection', tcpListener);
 | 
			
		||||
        srv.portsMap[serviceport].tcpListener = tcpListener;
 | 
			
		||||
        Devices.addPort(state.deviceLists, serviceport, srv);
 | 
			
		||||
      } else {
 | 
			
		||||
        try {
 | 
			
		||||
          console.log('use new', serviceport, 'for this connection');
 | 
			
		||||
          srv.portsMap[serviceport] = PortServers[serviceport] = require('net').createServer(tcpListener);
 | 
			
		||||
          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);
 | 
			
		||||
          });
 | 
			
		||||
          srv.portsMap[serviceport].on('error', function (e) {
 | 
			
		||||
            // TODO try again with random port
 | 
			
		||||
            console.error("Server Error assigning a dynamic port to a new connection:", e);
 | 
			
		||||
          });
 | 
			
		||||
        } catch(e) {
 | 
			
		||||
          // what a wonderful problem it will be the day that this bug needs to be fixed
 | 
			
		||||
          // (i.e. there are enough users to run out of ports)
 | 
			
		||||
          console.error("Error assigning a dynamic port to a new connection:", e);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    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;
 | 
			
		||||
    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]] ]
 | 
			
		||||
        , ['https', grant.domains[0] ]
 | 
			
		||||
        ]
 | 
			
		||||
      ]
 | 
			
		||||
    , 'control'
 | 
			
		||||
    );
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
, onDynTcpConn: function onDynTcpConn(state, srv, server, conn) {
 | 
			
		||||
    var serviceport = server.address().port;
 | 
			
		||||
    console.log('[DynTcpConn] new connection on', serviceport);
 | 
			
		||||
    var nextDevice = Devices.next(state.deviceLists, serviceport);
 | 
			
		||||
 | 
			
		||||
    if (!nextDevice) {
 | 
			
		||||
      conn.write("[Sanity Error] I've got a blank space baby, but nowhere to write your name.");
 | 
			
		||||
      conn.end();
 | 
			
		||||
      try {
 | 
			
		||||
        server.close();
 | 
			
		||||
      } catch(e) {
 | 
			
		||||
        console.error("[DynTcpConn] failed to close server:", e);
 | 
			
		||||
      }
 | 
			
		||||
      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);
 | 
			
		||||
      if (state.debug) { console.log("[DynTcp]", serviceport, "examining firstChunk from", Packer.socketToId(conn)); }
 | 
			
		||||
      conn.pause();
 | 
			
		||||
      //conn.unshift(firstChunk);
 | 
			
		||||
      conn._handle.onread(firstChunk.length, firstChunk);
 | 
			
		||||
 | 
			
		||||
      var servername;
 | 
			
		||||
      var hostname;
 | 
			
		||||
      var str;
 | 
			
		||||
      var m;
 | 
			
		||||
 | 
			
		||||
      if (22 === firstChunk[0]) {
 | 
			
		||||
        servername = (sni(firstChunk)||'').toLowerCase();
 | 
			
		||||
      } else if (firstChunk[0] > 32 && firstChunk[0] < 127) {
 | 
			
		||||
        str = firstChunk.toString();
 | 
			
		||||
        m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
 | 
			
		||||
        hostname = (m && m[1].toLowerCase() || '').split(':')[0];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (servername || hostname) {
 | 
			
		||||
        if (servername) {
 | 
			
		||||
          conn.write("TLS with sni is allowed only on standard ports. If you've registered '" + servername + "' use port 443.");
 | 
			
		||||
        } else {
 | 
			
		||||
          conn.write("HTTP with Host headers is not allowed on dynamic ports. If you've registered '" + hostname + "' use port 80.");
 | 
			
		||||
        }
 | 
			
		||||
        conn.end();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // pipeWs(servername, servicename, srv, client, serviceport)
 | 
			
		||||
      // remote.clients is managed as part of the piping process
 | 
			
		||||
      if (state.debug) { console.log("[DynTcp]", serviceport, "piping to srv (via loadbal)"); }
 | 
			
		||||
      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);
 | 
			
		||||
    if (srv.grants[rawAuth]) {
 | 
			
		||||
      console.log("addToken - duplicate");
 | 
			
		||||
      // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 | 
			
		||||
      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);
 | 
			
		||||
 | 
			
		||||
      console.log('\n[relay.js] authnToken');
 | 
			
		||||
      console.log(validatedTokenData);
 | 
			
		||||
 | 
			
		||||
      // For tracking state between token exchanges
 | 
			
		||||
      // and tacking on extra attributes (i.e. for extensions)
 | 
			
		||||
      // TODO close on delete
 | 
			
		||||
      if (!state.srvs[validatedTokenData.id]) {
 | 
			
		||||
        state.srvs[validatedTokenData.id] = {};
 | 
			
		||||
      }
 | 
			
		||||
      if (!state.srvs[validatedTokenData.id].updateAuth) {
 | 
			
		||||
        // be sure to always pass latest srv since the connection may change
 | 
			
		||||
        // and reuse the same token
 | 
			
		||||
        state.srvs[validatedTokenData.id].updateAuth = function (srv, validatedTokenData) {
 | 
			
		||||
          return Server.onAuth(state, srv, rawAuth, validatedTokenData);
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      state.srvs[validatedTokenData.id].updateAuth(srv, validatedTokenData);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
, removeToken: function removeToken(state, srv, jwtoken) {
 | 
			
		||||
    var grant = srv.grants[jwtoken];
 | 
			
		||||
    if (!grant) {
 | 
			
		||||
      return { message: 'specified token not present', code: 'E_INVALID_TOKEN'};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Prevent any more browser connections for this grant being sent to this srv,
 | 
			
		||||
    // and any existing connections from trying to send more data across the connection.
 | 
			
		||||
    grant.domains.forEach(function (domainname) {
 | 
			
		||||
      Devices.remove(state.deviceLists, domainname, srv);
 | 
			
		||||
    });
 | 
			
		||||
    grant.ports.forEach(function (portnumber) {
 | 
			
		||||
      Devices.remove(state.deviceLists, portnumber, srv);
 | 
			
		||||
      if (!srv.portsMap[portnumber]) { return; }
 | 
			
		||||
      try {
 | 
			
		||||
        srv.portsMap[portnumber].close(function () {
 | 
			
		||||
          console.log("[DynTcpConn] closing server for ", portnumber);
 | 
			
		||||
          delete srv.portsMap[portnumber];
 | 
			
		||||
          delete PortServers[portnumber];
 | 
			
		||||
        });
 | 
			
		||||
      } catch(e) { /*ignore*/ }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Close all of the existing browser connections associated with this websocket connection.
 | 
			
		||||
    Object.keys(grant.clients).forEach(function (cid) {
 | 
			
		||||
      Server.closeBrowserConn(state, srv, cid);
 | 
			
		||||
    });
 | 
			
		||||
    delete srv.grants[jwtoken];
 | 
			
		||||
    console.log("[ws] removed token '" + grant.currentDesc + "' from", srv.socketId);
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
, getBrowserConn: function getBrowserConn(state, srv, cid) {
 | 
			
		||||
    return srv.clients[cid];
 | 
			
		||||
  }
 | 
			
		||||
, closeBrowserConn: function closeBrowserConn(state, srv, cid) {
 | 
			
		||||
    if (!srv.clients[cid]) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    PromiseA.resolve().then(function () {
 | 
			
		||||
      var conn = srv.clients[cid];
 | 
			
		||||
      conn.tunnelClosing = true;
 | 
			
		||||
      conn.end();
 | 
			
		||||
 | 
			
		||||
      // If no data is buffered for writing then we don't need to wait for it to drain.
 | 
			
		||||
      if (!conn.bufferSize) {
 | 
			
		||||
        return timeoutPromise(500);
 | 
			
		||||
      }
 | 
			
		||||
      // Otherwise we want the connection to be able to finish, but we also want to impose
 | 
			
		||||
      // a time limit for it to drain, since it shouldn't have more than 1MB buffered.
 | 
			
		||||
      return new PromiseA(function (resolve) {
 | 
			
		||||
        var timeoutId = setTimeout(resolve, 60*1000);
 | 
			
		||||
        conn.once('drain', function () {
 | 
			
		||||
          clearTimeout(timeoutId);
 | 
			
		||||
          setTimeout(resolve, 500);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }).then(function () {
 | 
			
		||||
      if (srv.clients[cid]) {
 | 
			
		||||
        console.warn(cid, 'browser connection still present after calling `end`');
 | 
			
		||||
        srv.clients[cid].destroy();
 | 
			
		||||
        return timeoutPromise(500);
 | 
			
		||||
      }
 | 
			
		||||
    }).then(function () {
 | 
			
		||||
      if (srv.clients[cid]) {
 | 
			
		||||
        console.error(cid, 'browser connection still present after calling `destroy`');
 | 
			
		||||
        delete srv.clients[cid];
 | 
			
		||||
      }
 | 
			
		||||
    }).catch(function (err) {
 | 
			
		||||
      console.warn('failed to close browser connection', cid, err);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
, parseAuth: function parseAuth(state, srv) {
 | 
			
		||||
    var authn = (srv.upgradeReq.headers.authorization||'').split(/\s+/);
 | 
			
		||||
    if (authn[0] && 'basic' === authn[0].toLowerCase()) {
 | 
			
		||||
      try {
 | 
			
		||||
        authn = new Buffer(authn[1], 'base64').toString('ascii').split(':');
 | 
			
		||||
        return authn[1];
 | 
			
		||||
      } catch (err) { }
 | 
			
		||||
    }
 | 
			
		||||
    return url.parse(srv.upgradeReq.url, true).query.access_token;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = Server;
 | 
			
		||||
@ -1,23 +1,57 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var packer = require('tunnel-packer');
 | 
			
		||||
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);
 | 
			
		||||
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 (state) {
 | 
			
		||||
  var Devices = state.Devices;
 | 
			
		||||
module.exports.createTcpConnectionHandler = function (copts) {
 | 
			
		||||
  var Devices = copts.Devices;
 | 
			
		||||
 | 
			
		||||
  return function onTcpConnection(conn, serviceport) {
 | 
			
		||||
    serviceport = serviceport || conn.localPort;
 | 
			
		||||
  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);
 | 
			
		||||
@ -28,28 +62,7 @@ module.exports.createTcpConnectionHandler = function (state) {
 | 
			
		||||
    //});
 | 
			
		||||
 | 
			
		||||
    //return;
 | 
			
		||||
    //conn.once('data', function (firstChunk) {
 | 
			
		||||
    //});
 | 
			
		||||
    conn.once('readable', function () {
 | 
			
		||||
      var firstChunk = conn.read();
 | 
			
		||||
      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.unshift(firstChunk);
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
@ -57,154 +70,110 @@ module.exports.createTcpConnectionHandler = function (state) {
 | 
			
		||||
      // and so it should be fixed at some point in the future
 | 
			
		||||
 | 
			
		||||
      // 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) {
 | 
			
		||||
          state[fn](servername, conn);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.error("[SANITY ERROR] '" + fn + "' doesn't have a state handler");
 | 
			
		||||
        }
 | 
			
		||||
        /*
 | 
			
		||||
      process.nextTick(function () {
 | 
			
		||||
          conn.resume();
 | 
			
		||||
        conn.unshift(firstChunk);
 | 
			
		||||
      });
 | 
			
		||||
        */
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      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';
 | 
			
		||||
      var service = 'tcp';
 | 
			
		||||
      var servername;
 | 
			
		||||
      var str;
 | 
			
		||||
      var m;
 | 
			
		||||
 | 
			
		||||
          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');
 | 
			
		||||
        }
 | 
			
		||||
      , 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');
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (state.debug) { console.log("pipeWs(servername, service, deviceLists['" + servername + "'], socket)"); }
 | 
			
		||||
          pipeWs(servername, service, nextDevice, conn, serviceport);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      function handleConnection(outcomes) {
 | 
			
		||||
      function tryTls() {
 | 
			
		||||
        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; }
 | 
			
		||||
        console.log("");
 | 
			
		||||
 | 
			
		||||
        // 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);
 | 
			
		||||
        if (!copts.servernames.length) {
 | 
			
		||||
          console.log("https => admin => setup => (needs bogus tls certs to start?)");
 | 
			
		||||
          copts.httpsSetupServer(servername, conn);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (-1 !== copts.servernames.indexOf(servername)) {
 | 
			
		||||
          console.log("Lock and load, admin interface time!");
 | 
			
		||||
          copts.httpsTunnel(servername, conn);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (copts.config.nowww && /^www\./i.test(servername)) {
 | 
			
		||||
          console.log("TODO: use www bare redirect");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function run() {
 | 
			
		||||
          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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (copts.config.vhost) {
 | 
			
		||||
          console.log("VHOST path", copts.config.vhost);
 | 
			
		||||
          vhost = copts.config.vhost.replace(/:hostname/, (servername||''));
 | 
			
		||||
          console.log("VHOST name", vhost);
 | 
			
		||||
          conn.pause();
 | 
			
		||||
          //copts.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);
 | 
			
		||||
            console.log("VHOST error?", err);
 | 
			
		||||
            if (err) { run(); return; } 
 | 
			
		||||
            if (nodes) { copts.httpsVhost(servername, conn); }
 | 
			
		||||
          });
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        outcomes.assumeExternal();
 | 
			
		||||
        run();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
 | 
			
		||||
      if (22 === firstChunk[0]) {
 | 
			
		||||
        // TLS
 | 
			
		||||
        service = 'https';
 | 
			
		||||
        servername = (sni(firstChunk)||'').toLowerCase().trim();
 | 
			
		||||
        if (state.debug) { console.log("[tcp] tls hello from '" + servername + "'"); }
 | 
			
		||||
        handleConnection(tlsOutcomes);
 | 
			
		||||
        servername = (sni(firstChunk)||'').toLowerCase();
 | 
			
		||||
        console.log("tls hello servername:", servername);
 | 
			
		||||
        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 + "'"); }
 | 
			
		||||
        console.log('servername', servername);
 | 
			
		||||
 | 
			
		||||
        if (/HTTP\//i.test(str)) {
 | 
			
		||||
          handleConnection(httpOutcomes);
 | 
			
		||||
          if (!copts.servernames.length) {
 | 
			
		||||
            console.log('copts.httpSetupServer', copts.httpSetupServer);
 | 
			
		||||
            copts.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(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;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								package.json
									
									
									
									
									
								
							@ -1,17 +1,17 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "telebit-relay",
 | 
			
		||||
  "version": "0.20.0",
 | 
			
		||||
  "name": "telebitd",
 | 
			
		||||
  "version": "0.11.0",
 | 
			
		||||
  "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",
 | 
			
		||||
  "main": "telebitd.js",
 | 
			
		||||
  "bin": {
 | 
			
		||||
    "telebit-relay": "bin/telebit-relay.js"
 | 
			
		||||
    "telebitd": "bin/telebitd.js"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "test": "echo \"Error: no test specified\" && exit 1"
 | 
			
		||||
  },
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
    "url": "https://git.coolaj86.com/coolaj86/telebit-relay.js.git"
 | 
			
		||||
    "url": "https://git.coolaj86.com/coolaj86/telebitd.js.git"
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "http",
 | 
			
		||||
@ -33,26 +33,21 @@
 | 
			
		||||
  "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
 | 
			
		||||
  "license": "(MIT OR Apache-2.0)",
 | 
			
		||||
  "bugs": {
 | 
			
		||||
    "url": "https://git.coolaj86.com/coolaj86/telebit-relay.js/issues"
 | 
			
		||||
    "url": "https://git.coolaj86.com/coolaj86/telebitd.js/issues"
 | 
			
		||||
  },
 | 
			
		||||
  "homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js",
 | 
			
		||||
  "homepage": "https://git.coolaj86.com/coolaj86/telebitd.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",
 | 
			
		||||
    "recase": "^1.0.4",
 | 
			
		||||
    "redirect-https": "^1.1.5",
 | 
			
		||||
    "serve-static": "^1.13.2",
 | 
			
		||||
    "sni": "^1.0.0",
 | 
			
		||||
    "tunnel-packer": "^1.4.0",
 | 
			
		||||
    "ws": "^5.1.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
 | 
			
		||||
							
								
								
									
										395
									
								
								telebitd.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										395
									
								
								telebitd.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,395 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var url = require('url');
 | 
			
		||||
var PromiseA = require('bluebird');
 | 
			
		||||
var jwt = require('jsonwebtoken');
 | 
			
		||||
var packer = require('tunnel-packer');
 | 
			
		||||
 | 
			
		||||
function timeoutPromise(duration) {
 | 
			
		||||
  return new PromiseA(function (resolve) {
 | 
			
		||||
    setTimeout(resolve, duration);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var Devices = require('./lib/device-tracker');
 | 
			
		||||
 | 
			
		||||
module.exports.store = { Devices: Devices };
 | 
			
		||||
module.exports.create = function (copts) {
 | 
			
		||||
  copts.deviceLists = {};
 | 
			
		||||
  //var deviceLists = {};
 | 
			
		||||
  var activityTimeout = copts.activityTimeout || 2*60*1000;
 | 
			
		||||
  var pongTimeout = copts.pongTimeout || 10*1000;
 | 
			
		||||
  copts.Devices = Devices;
 | 
			
		||||
  var onTcpConnection = require('./lib/unwrap-tls').createTcpConnectionHandler(copts);
 | 
			
		||||
 | 
			
		||||
  function onWsConnection(ws, upgradeReq) {
 | 
			
		||||
    console.log(ws);
 | 
			
		||||
    var socketId = packer.socketToId(upgradeReq.socket);
 | 
			
		||||
    var remotes = {};
 | 
			
		||||
 | 
			
		||||
    function logName() {
 | 
			
		||||
      var result = Object.keys(remotes).map(function (jwtoken) {
 | 
			
		||||
        return remotes[jwtoken].deviceId;
 | 
			
		||||
      }).join(';');
 | 
			
		||||
 | 
			
		||||
      return result || socketId;
 | 
			
		||||
    }
 | 
			
		||||
    function sendTunnelMsg(addr, data, service) {
 | 
			
		||||
      ws.send(packer.pack(addr, data, service), {binary: true});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getBrowserConn(cid) {
 | 
			
		||||
      var browserConn;
 | 
			
		||||
      Object.keys(remotes).some(function (jwtoken) {
 | 
			
		||||
        if (remotes[jwtoken].clients[cid]) {
 | 
			
		||||
          browserConn = remotes[jwtoken].clients[cid];
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return browserConn;
 | 
			
		||||
    }
 | 
			
		||||
    function closeBrowserConn(cid) {
 | 
			
		||||
      var remote;
 | 
			
		||||
      Object.keys(remotes).some(function (jwtoken) {
 | 
			
		||||
        if (remotes[jwtoken].clients[cid]) {
 | 
			
		||||
          remote = remotes[jwtoken];
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      if (!remote) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      PromiseA.resolve().then(function () {
 | 
			
		||||
        var conn = remote.clients[cid];
 | 
			
		||||
        conn.tunnelClosing = true;
 | 
			
		||||
        conn.end();
 | 
			
		||||
 | 
			
		||||
        // If no data is buffered for writing then we don't need to wait for it to drain.
 | 
			
		||||
        if (!conn.bufferSize) {
 | 
			
		||||
          return timeoutPromise(500);
 | 
			
		||||
        }
 | 
			
		||||
        // Otherwise we want the connection to be able to finish, but we also want to impose
 | 
			
		||||
        // a time limit for it to drain, since it shouldn't have more than 1MB buffered.
 | 
			
		||||
        return new PromiseA(function (resolve) {
 | 
			
		||||
          var timeoutId = setTimeout(resolve, 60*1000);
 | 
			
		||||
          conn.once('drain', function () {
 | 
			
		||||
            clearTimeout(timeoutId);
 | 
			
		||||
            setTimeout(resolve, 500);
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      }).then(function () {
 | 
			
		||||
        if (remote.clients[cid]) {
 | 
			
		||||
          console.warn(cid, 'browser connection still present after calling `end`');
 | 
			
		||||
          remote.clients[cid].destroy();
 | 
			
		||||
          return timeoutPromise(500);
 | 
			
		||||
        }
 | 
			
		||||
      }).then(function () {
 | 
			
		||||
        if (remote.clients[cid]) {
 | 
			
		||||
          console.error(cid, 'browser connection still present after calling `destroy`');
 | 
			
		||||
          delete remote.clients[cid];
 | 
			
		||||
        }
 | 
			
		||||
      }).catch(function (err) {
 | 
			
		||||
        console.warn('failed to close browser connection', cid, err);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addToken(jwtoken) {
 | 
			
		||||
      if (remotes[jwtoken]) {
 | 
			
		||||
        // return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      var token;
 | 
			
		||||
      try {
 | 
			
		||||
        token = jwt.verify(jwtoken, copts.secret);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        token = null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!token) {
 | 
			
		||||
        return { message: "invalid access token", code: "E_INVALID_TOKEN" };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!Array.isArray(token.domains)) {
 | 
			
		||||
        if ('string' === typeof token.name) {
 | 
			
		||||
          token.domains = [ token.name ];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!Array.isArray(token.domains) || !token.domains.length) {
 | 
			
		||||
        return { message: "invalid server name", code: "E_INVALID_NAME" };
 | 
			
		||||
      }
 | 
			
		||||
      if (token.domains.some(function (name) { return typeof name !== 'string'; })) {
 | 
			
		||||
        return { message: "invalid server name", code: "E_INVALID_NAME" };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Add the custom properties we need to manage this remote, then add it to all the relevant
 | 
			
		||||
      // domains and the list of all this websocket's remotes.
 | 
			
		||||
      token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(',');
 | 
			
		||||
      token.ws = ws;
 | 
			
		||||
      token.upgradeReq = upgradeReq;
 | 
			
		||||
      token.clients = {};
 | 
			
		||||
 | 
			
		||||
      token.pausedConns = [];
 | 
			
		||||
      ws._socket.on('drain', function () {
 | 
			
		||||
        // the websocket library has it's own buffer apart from node's socket buffer, but that one
 | 
			
		||||
        // is much more difficult to watch, so we watch for the lower level buffer to drain and
 | 
			
		||||
        // then check to see if the upper level buffer is still too full to write to. Note that
 | 
			
		||||
        // the websocket library buffer has something to do with compression, so I'm not requiring
 | 
			
		||||
        // that to be 0 before we start up again.
 | 
			
		||||
        if (ws.bufferedAmount > 128*1024) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        token.pausedConns.forEach(function (conn) {
 | 
			
		||||
          if (!conn.manualPause) {
 | 
			
		||||
            // console.log('resuming', conn.tunnelCid, 'now that the web socket has caught up');
 | 
			
		||||
            conn.resume();
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        token.pausedConns.length = 0;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      token.domains.forEach(function (domainname) {
 | 
			
		||||
        console.log('domainname', domainname);
 | 
			
		||||
        Devices.add(copts.deviceLists, domainname, token);
 | 
			
		||||
      });
 | 
			
		||||
      remotes[jwtoken] = token;
 | 
			
		||||
      console.log("added token '" + token.deviceId + "' to websocket", socketId);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function removeToken(jwtoken) {
 | 
			
		||||
      var remote = remotes[jwtoken];
 | 
			
		||||
      if (!remote) {
 | 
			
		||||
        return { message: 'specified token not present', code: 'E_INVALID_TOKEN'};
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Prevent any more browser connections being sent to this remote, and any existing
 | 
			
		||||
      // connections from trying to send more data across the connection.
 | 
			
		||||
      remote.domains.forEach(function (domainname) {
 | 
			
		||||
        Devices.remove(copts.deviceLists, domainname, remote);
 | 
			
		||||
      });
 | 
			
		||||
      remote.ws = null;
 | 
			
		||||
      remote.upgradeReq = null;
 | 
			
		||||
 | 
			
		||||
      // Close all of the existing browser connections associated with this websocket connection.
 | 
			
		||||
      Object.keys(remote.clients).forEach(function (cid) {
 | 
			
		||||
        closeBrowserConn(cid);
 | 
			
		||||
      });
 | 
			
		||||
      delete remotes[jwtoken];
 | 
			
		||||
      console.log("removed token '" + remote.deviceId + "' from websocket", socketId);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var firstToken;
 | 
			
		||||
    var authn = (upgradeReq.headers.authorization||'').split(/\s+/);
 | 
			
		||||
    if (authn[0] && 'basic' === authn[0].toLowerCase()) {
 | 
			
		||||
      try {
 | 
			
		||||
        authn = new Buffer(authn[1], 'base64').toString('ascii').split(':');
 | 
			
		||||
        firstToken = authn[1];
 | 
			
		||||
      } catch (err) { }
 | 
			
		||||
    }
 | 
			
		||||
    if (!firstToken) {
 | 
			
		||||
      firstToken = url.parse(upgradeReq.url, true).query.access_token;
 | 
			
		||||
    }
 | 
			
		||||
    if (firstToken) {
 | 
			
		||||
      var err = addToken(firstToken);
 | 
			
		||||
      if (err) {
 | 
			
		||||
        sendTunnelMsg(null, [0, err], 'control');
 | 
			
		||||
        ws.close();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var commandHandlers = {
 | 
			
		||||
      add_token: addToken
 | 
			
		||||
    , delete_token: function (token) {
 | 
			
		||||
        if (token !== '*') {
 | 
			
		||||
          return removeToken(token);
 | 
			
		||||
        }
 | 
			
		||||
        var err;
 | 
			
		||||
        Object.keys(remotes).some(function (jwtoken) {
 | 
			
		||||
          err = removeToken(jwtoken);
 | 
			
		||||
          return err;
 | 
			
		||||
        });
 | 
			
		||||
        return err;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var packerHandlers = {
 | 
			
		||||
      oncontrol: function (opts) {
 | 
			
		||||
        var cmd, err;
 | 
			
		||||
        try {
 | 
			
		||||
          cmd = JSON.parse(opts.data.toString());
 | 
			
		||||
        } catch (err) {}
 | 
			
		||||
        if (!Array.isArray(cmd) || typeof cmd[0] !== 'number') {
 | 
			
		||||
          var msg = 'received bad command "' + opts.data.toString() + '"';
 | 
			
		||||
          console.warn(msg, 'from websocket', socketId);
 | 
			
		||||
          sendTunnelMsg(null, [0, {message: msg, code: 'E_BAD_COMMAND'}], 'control');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cmd[0] < 0) {
 | 
			
		||||
          // We only ever send one command and we send it once, so we just hard coded the ID as 1.
 | 
			
		||||
          if (cmd[0] === -1) {
 | 
			
		||||
            if (cmd[1]) {
 | 
			
		||||
              console.log('received error response to hello from', socketId, cmd[1]);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            console.warn('received response to unknown command', cmd, 'from', socketId);
 | 
			
		||||
          }
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cmd[0] === 0) {
 | 
			
		||||
          console.warn('received dis-associated error from', socketId, cmd[1]);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (commandHandlers[cmd[1]]) {
 | 
			
		||||
          err = commandHandlers[cmd[1]].apply(null, cmd.slice(2));
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          err = { message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sendTunnelMsg(null, [-cmd[0], err], 'control');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    , onmessage: function (opts) {
 | 
			
		||||
        var cid = packer.addrToId(opts);
 | 
			
		||||
        console.log("remote '" + logName() + "' has data for '" + cid + "'", opts.data.byteLength);
 | 
			
		||||
 | 
			
		||||
        var browserConn = getBrowserConn(cid);
 | 
			
		||||
        if (!browserConn) {
 | 
			
		||||
          sendTunnelMsg(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        browserConn.write(opts.data);
 | 
			
		||||
        // tunnelRead is how many bytes we've read from the tunnel, and written to the browser.
 | 
			
		||||
        browserConn.tunnelRead = (browserConn.tunnelRead || 0) + opts.data.byteLength;
 | 
			
		||||
        // If we have more than 1MB buffered data we need to tell the other side to slow down.
 | 
			
		||||
        // Once we've finished sending what we have we can tell the other side to keep going.
 | 
			
		||||
        // If we've already sent the 'pause' message though don't send it again, because we're
 | 
			
		||||
        // probably just dealing with data queued before our message got to them.
 | 
			
		||||
        if (!browserConn.remotePaused && browserConn.bufferSize > 1024*1024) {
 | 
			
		||||
          sendTunnelMsg(opts, browserConn.tunnelRead, 'pause');
 | 
			
		||||
          browserConn.remotePaused = true;
 | 
			
		||||
 | 
			
		||||
          browserConn.once('drain', function () {
 | 
			
		||||
            sendTunnelMsg(opts, browserConn.tunnelRead, 'resume');
 | 
			
		||||
            browserConn.remotePaused = false;
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    , onpause: function (opts) {
 | 
			
		||||
        var cid = packer.addrToId(opts);
 | 
			
		||||
        console.log('[TunnelPause]', cid);
 | 
			
		||||
        var browserConn = getBrowserConn(cid);
 | 
			
		||||
        if (browserConn) {
 | 
			
		||||
          browserConn.manualPause = true;
 | 
			
		||||
          browserConn.pause();
 | 
			
		||||
        } else {
 | 
			
		||||
          sendTunnelMsg(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    , onresume: function (opts) {
 | 
			
		||||
        var cid = packer.addrToId(opts);
 | 
			
		||||
        console.log('[TunnelResume]', cid);
 | 
			
		||||
        var browserConn = getBrowserConn(cid);
 | 
			
		||||
        if (browserConn) {
 | 
			
		||||
          browserConn.manualPause = false;
 | 
			
		||||
          browserConn.resume();
 | 
			
		||||
        } else {
 | 
			
		||||
          sendTunnelMsg(opts, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    , onend: function (opts) {
 | 
			
		||||
        var cid = packer.addrToId(opts);
 | 
			
		||||
        console.log('[TunnelEnd]', cid);
 | 
			
		||||
        closeBrowserConn(cid);
 | 
			
		||||
      }
 | 
			
		||||
    , onerror: function (opts) {
 | 
			
		||||
        var cid = packer.addrToId(opts);
 | 
			
		||||
        console.log('[TunnelError]', cid, opts.message);
 | 
			
		||||
        closeBrowserConn(cid);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    var unpacker = packer.create(packerHandlers);
 | 
			
		||||
 | 
			
		||||
    var lastActivity = Date.now();
 | 
			
		||||
    var timeoutId;
 | 
			
		||||
    function refreshTimeout() {
 | 
			
		||||
      lastActivity = Date.now();
 | 
			
		||||
    }
 | 
			
		||||
    function checkTimeout() {
 | 
			
		||||
      // Determine how long the connection has been "silent", ie no activity.
 | 
			
		||||
      var silent = Date.now() - lastActivity;
 | 
			
		||||
 | 
			
		||||
      // If we have had activity within the last activityTimeout then all we need to do is
 | 
			
		||||
      // call this function again at the soonest time when the connection could be timed out.
 | 
			
		||||
      if (silent < activityTimeout) {
 | 
			
		||||
        timeoutId = setTimeout(checkTimeout, activityTimeout-silent);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Otherwise we check to see if the pong has also timed out, and if not we send a ping
 | 
			
		||||
      // and call this function again when the pong will have timed out.
 | 
			
		||||
      else if (silent < activityTimeout + pongTimeout) {
 | 
			
		||||
        console.log('pinging', logName());
 | 
			
		||||
        try {
 | 
			
		||||
          ws.ping();
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
          console.warn('failed to ping home cloud', logName());
 | 
			
		||||
        }
 | 
			
		||||
        timeoutId = setTimeout(checkTimeout, pongTimeout);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Last case means the ping we sent before didn't get a response soon enough, so we
 | 
			
		||||
      // need to close the websocket connection.
 | 
			
		||||
      else {
 | 
			
		||||
        console.log('home cloud', logName(), 'connection timed out');
 | 
			
		||||
        ws.close(1013, 'connection timeout');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    timeoutId = setTimeout(checkTimeout, activityTimeout);
 | 
			
		||||
 | 
			
		||||
    // Note that our websocket library automatically handles pong responses on ping requests
 | 
			
		||||
    // before it even emits the event.
 | 
			
		||||
    ws.on('ping', refreshTimeout);
 | 
			
		||||
    ws.on('pong', refreshTimeout);
 | 
			
		||||
    ws.on('message', function forwardMessage(chunk) {
 | 
			
		||||
      refreshTimeout();
 | 
			
		||||
      console.log('message from home cloud to tunneler to browser', chunk.byteLength);
 | 
			
		||||
      //console.log(chunk.toString());
 | 
			
		||||
      unpacker.fns.addChunk(chunk);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function hangup() {
 | 
			
		||||
      clearTimeout(timeoutId);
 | 
			
		||||
      console.log('home cloud', logName(), 'connection closing');
 | 
			
		||||
      Object.keys(remotes).forEach(function (jwtoken) {
 | 
			
		||||
        removeToken(jwtoken);
 | 
			
		||||
      });
 | 
			
		||||
      ws.terminate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ws.on('close', hangup);
 | 
			
		||||
    ws.on('error', hangup);
 | 
			
		||||
 | 
			
		||||
    // We only ever send one command and we send it once, so we just hard code the ID as 1
 | 
			
		||||
    sendTunnelMsg(null, [1, 'hello', [unpacker._version], Object.keys(commandHandlers)], 'control');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    tcp: onTcpConnection
 | 
			
		||||
  , ws: onWsConnection
 | 
			
		||||
  , isClientDomain: Devices.exist.bind(null, copts.deviceLists)
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user