Compare commits
44 Commits
2efc6c3986
...
d700b51494
Author | SHA1 | Date |
---|---|---|
AJ ONeal | d700b51494 | |
AJ ONeal | c70ba4f4fd | |
AJ ONeal | 82619cf88d | |
AJ ONeal | 31932002c9 | |
AJ ONeal | 496d3862f8 | |
AJ ONeal | 5ddd85e14e | |
AJ ONeal | b2a7ecd39b | |
AJ ONeal | e5563b5842 | |
AJ ONeal | 224c3ac9cd | |
AJ ONeal | 73f26d6e05 | |
AJ ONeal | fdcf205b49 | |
AJ ONeal | 5a34b39ff3 | |
AJ ONeal | 22339275bb | |
AJ ONeal | ebe4003d27 | |
AJ ONeal | 846590e648 | |
AJ ONeal | d0ae3a1c0f | |
AJ ONeal | 3fe62c6b02 | |
AJ ONeal | c37147a012 | |
AJ ONeal | b086e1c0a5 | |
AJ ONeal | 934afd8a8d | |
AJ ONeal | df3c1c3b04 | |
AJ ONeal | 643b5a62ea | |
AJ ONeal | 05cb157cfc | |
AJ ONeal | 8f7dec1df1 | |
AJ ONeal | 9c57bac510 | |
AJ ONeal | e1ee55da02 | |
AJ ONeal | f6011ade83 | |
AJ ONeal | 539fb4e62a | |
AJ ONeal | d3a6ef96d6 | |
AJ ONeal | 28944a6933 | |
AJ ONeal | d566a06cb3 | |
AJ ONeal | dcb62b1e2c | |
AJ ONeal | 45aa3a4686 | |
AJ ONeal | f74b95909f | |
AJ ONeal | 194d8bcf20 | |
AJ ONeal | ce70c75378 | |
AJ ONeal | 4b38c27a71 | |
AJ ONeal | 127296fd5e | |
AJ ONeal | 535ce72565 | |
AJ ONeal | 3e029ee932 | |
AJ ONeal | 7db2f0f703 | |
AJ ONeal | d3022c246e | |
AJ ONeal | 5d8985732e | |
AJ ONeal | 1ba0557c2c |
52
LICENSE
52
LICENSE
|
@ -1,21 +1,41 @@
|
||||||
|
Copyright 2016 AJ ONeal
|
||||||
|
|
||||||
|
This is open source software; you can redistribute it and/or modify it under the
|
||||||
|
terms of either:
|
||||||
|
|
||||||
|
a) the "MIT License"
|
||||||
|
b) the "Apache-2.0 License"
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2016 Daplie, Inc
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
The above copyright notice and this permission notice shall be included in all
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
copies or substantial portions of the Software.
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
copies or substantial portions of the Software.
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
Apache-2.0 License Summary
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
you may not use this file except in compliance with the License.
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
You may obtain a copy of the License at
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
161
README.md
161
README.md
|
@ -1,85 +1,140 @@
|
||||||
# Telebit™ Relay
|
# Telebit Relay
|
||||||
|
|
||||||
A server that works in combination with [stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js)
|
Friends don't let friends localhost™
|
||||||
|
|
||||||
|
A server that works in combination with [Telebit Remote](https://git.coolaj86.com/coolaj86/telebit.js)
|
||||||
to allow you to serve http and https from any computer, anywhere through a secure tunnel.
|
to allow you to serve http and https from any computer, anywhere through a secure tunnel.
|
||||||
|
|
||||||
| Sponsored by [ppl](https://ppl.family) | **Telebit Relay** | [Telebit](https://git.coolaj86.com/coolaj86/tunnel-client.js) |
|
| Sponsored by [ppl](https://ppl.family) | **Telebit Relay** | [Telebit Remote](https://git.coolaj86.com/coolaj86/telebit.js) |
|
||||||
|
|
||||||
CLI
|
Features
|
||||||
===
|
========
|
||||||
|
|
||||||
Installs as `stunnel.js` with the alias `jstunnel`
|
* [x] Expose your bits even in the harshest of network environments
|
||||||
(for those that regularly use `stunnel` but still like commandline completion).
|
* [x] NAT, Home Routers
|
||||||
|
* [x] College Dorms, HOAs
|
||||||
|
* [x] Corporate Firewalls, Public libraries, Airports
|
||||||
|
* [x] and even Airplanes, yep
|
||||||
|
* [x] Automated HTTPS (Free SSL)
|
||||||
|
|
||||||
### Install
|
Install
|
||||||
|
=======
|
||||||
|
|
||||||
|
Mac & Linux
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Open Terminal and run this install script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g stunneld
|
curl -fsSL https://get.telebit.cloud/relay | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Then `dist/etc/systemd/system/stunneld.service` should be copied to `/etc/systemd/system/stunneld.service` and
|
Of course, feel free to inspect the install script before you run it.
|
||||||
the ARGUMENTS, such as SECRET, MUST BE CHANGED.
|
|
||||||
|
|
||||||
*TODO*: make `--config /path/to/config` the only argument (and have the secret auto-generated on first run?)
|
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`
|
||||||
|
for convenience.
|
||||||
|
|
||||||
## Note: Use node.js v8.x
|
You can customize the installation:
|
||||||
|
|
||||||
There is a bug in node v9.x that causes stunneld to crash.
|
|
||||||
|
|
||||||
https://github.com/nodejs/node/issues/20241
|
|
||||||
|
|
||||||
### Advanced Usage
|
|
||||||
|
|
||||||
How to use `stunnel.js` with your own instance of `stunneld.js`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
stunneld.js --servernames tunnel.example.com --protocols wss --secret abc123
|
export NODEJS_VER=v10.2
|
||||||
|
export TELEBIT_RELAY_PATH=/opt/telebit-relay
|
||||||
|
curl -fsSL https://get.telebit.cloud/relay
|
||||||
|
```
|
||||||
|
|
||||||
|
That will change the bundled version of node.js is bundled with Telebit Relay
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows & Node.js
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
1. Install [node.js](https://nodejs.org)
|
||||||
|
2. Open _Node.js_
|
||||||
|
2. Run the command `npm install -g telebit-relay`
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
====
|
||||||
|
|
||||||
|
```bash
|
||||||
|
telebit-relay --config /opt/telebit-relay/etc/telebit-relay.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
Options
|
Options
|
||||||
|
|
||||||
|
`/opt/telebit-relay/etc/telebit-relay.yml:`
|
||||||
```
|
```
|
||||||
--secret the same secret used by stunnel client (used for authentication)
|
email: 'jon@example.com' # must be valid (for certificate recovery and security alerts)
|
||||||
--serve comma separated list of <proto>:<servername>:<port> to which
|
agree_tos: true # agree to the Telebit, Greenlock, and Let's Encrypt TOSes
|
||||||
incoming http and https should be forwarded
|
community_member: true # receive infrequent relevant but non-critical updates
|
||||||
|
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
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Privileged Ports without sudo
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux
|
|
||||||
sudo setcap 'cap_net_bind_service=+ep' $(which node)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alterntive Methods
|
|
||||||
|
|
||||||
**NOT YET IMPLEMENTED**
|
|
||||||
|
|
||||||
We created this for anyone to use on their own server or VPS,
|
|
||||||
but those generally cost $5 - $20 / month and so it's probably
|
|
||||||
cheaper to purchase data transfer (which we supply, obviously),
|
|
||||||
which is only $1/month for most people.
|
|
||||||
|
|
||||||
Just use the client ([stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js))
|
|
||||||
with this tunneling service (the default) and save yourself the monthly fee
|
|
||||||
by only paying for the data you need.
|
|
||||||
|
|
||||||
* Node WS Tunnel (zero setup)
|
|
||||||
* Heroku (zero cost)
|
|
||||||
* Chunk Host (best deal per TB/month)
|
|
||||||
|
|
||||||
Security
|
Security
|
||||||
========
|
========
|
||||||
|
|
||||||
The bottom line: As with everything in life, there is no such thing as anonymity
|
The bottom line: As with everything in life, there is no such thing as anonymity
|
||||||
or absolute security. Only use stunneld services that you trust. :D
|
or absolute security. Only use Telebit Relays that you trust or self-host. :D
|
||||||
|
|
||||||
Even though the traffic is encrypted end-to-end, you can't just trust any stunneld service
|
Even though the traffic is encrypted end-to-end, you can't just trust any Telebit Relay
|
||||||
willy-nilly.
|
willy-nilly.
|
||||||
|
|
||||||
A man-in-the-middle attack is possible using Let's Encrypt since an evil stunneld service
|
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 and tls-sni-01 challenges without a problem
|
would be able to complete the http-01 challenges without a problem
|
||||||
(since that's where your DNS is pointed when you use the service).
|
(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
|
Also, the traffic could still be copied and stored for decryption is some era when quantum
|
||||||
computers exist (probably never).
|
computers exist (probably never).
|
||||||
|
|
||||||
|
Why?
|
||||||
|
====
|
||||||
|
|
||||||
|
We created this for anyone to use on their own server or VPS,
|
||||||
|
but those generally cost $5 - $20 / month and so it's probably
|
||||||
|
cheaper to purchase data transfer, which is only $1/month for
|
||||||
|
most people.
|
||||||
|
|
||||||
|
In keeping with our no lock-in policy, we release a version of
|
||||||
|
the server for anyone to use independently.
|
||||||
|
|
||||||
|
TODO show how to do on
|
||||||
|
|
||||||
|
* Node WS Tunnel (zero setup)
|
||||||
|
* Heroku (zero cost)
|
||||||
|
* Chunk Host (best deal per TB/month)
|
||||||
|
|
||||||
|
Useful Tidbits
|
||||||
|
===
|
||||||
|
|
||||||
|
## As a systemd service
|
||||||
|
|
||||||
|
`./dist/etc/systemd/system/telebit-relay.service` should be copied to `/etc/systemd/system/telebit-relay.service`.
|
||||||
|
|
||||||
|
The user and group `telebit` should be created.
|
||||||
|
|
||||||
|
## Use privileged ports without sudo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
sudo setcap 'cap_net_bind_service=+ep' $(which node)
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<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>
|
|
@ -0,0 +1,6 @@
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
document.body.hidden = false;
|
||||||
|
|
||||||
|
}());
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Telebit Relay</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
[TODO: Setup Interface]
|
||||||
|
<br>
|
||||||
|
<ul>
|
||||||
|
<li>Admin Server Name</li>
|
||||||
|
<li>Administrator Email</li>
|
||||||
|
<li>SSL ToS Agree</li>
|
||||||
|
<li>Community Member</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,13 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
rm -rf ./node-installer.sh
|
|
||||||
curl -fsSL bit.ly/node-installer -o ./node-installer.sh
|
|
||||||
bash ./node-installer.sh --dev-deps
|
|
||||||
|
|
||||||
git clone https://git.coolaj86.com/coolaj86/tunnel-server.js.git
|
|
||||||
pushd tunnel-server.js/
|
|
||||||
npm install
|
|
||||||
my_secret=$(node bin/generate-secret.js)
|
|
||||||
echo "Your secret is:\n\n\t"$my_secret
|
|
||||||
echo "node bin/server.js --servernames tunnel.example.com --secret $my_secret"
|
|
||||||
popd
|
|
194
bin/stunneld.js
194
bin/stunneld.js
|
@ -1,194 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var pkg = require('../package.json');
|
|
||||||
|
|
||||||
var program = require('commander');
|
|
||||||
var stunneld = require('../wstunneld.js');
|
|
||||||
var greenlock = require('greenlock');
|
|
||||||
|
|
||||||
function collectServernames(val, memo) {
|
|
||||||
var lowerCase = val.split(/,/).map(function (servername) {
|
|
||||||
return servername.toLowerCase();
|
|
||||||
});
|
|
||||||
|
|
||||||
return memo.concat(lowerCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectProxies(val, memo) {
|
|
||||||
var vals = val.split(/,/g);
|
|
||||||
vals.map(function (location) {
|
|
||||||
// http:john.example.com:3000
|
|
||||||
// http://john.example.com:3000
|
|
||||||
var parts = location.split(':');
|
|
||||||
if (1 === parts.length) {
|
|
||||||
parts[1] = parts[0];
|
|
||||||
parts[0] = 'wss';
|
|
||||||
}
|
|
||||||
if (2 === parts.length) {
|
|
||||||
if (/\./.test(parts[0])) {
|
|
||||||
parts[2] = parts[1];
|
|
||||||
parts[1] = parts[0];
|
|
||||||
parts[0] = 'wss';
|
|
||||||
}
|
|
||||||
if (!/\./.test(parts[1])) {
|
|
||||||
throw new Error("bad --serve option Example: wss://tunnel.example.com:1337");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parts[0] = parts[0].toLowerCase();
|
|
||||||
parts[1] = parts[1].toLowerCase().replace(/(\/\/)?/, '') || '*';
|
|
||||||
parts[2] = parseInt(parts[2], 10) || 0;
|
|
||||||
if (!parts[2]) {
|
|
||||||
// TODO grab OS list of standard ports?
|
|
||||||
if (-1 !== [ 'ws', 'http' ].indexOf(parts[0])) {
|
|
||||||
//parts[2] = 80;
|
|
||||||
}
|
|
||||||
else if (-1 !== [ 'wss', 'https' ].indexOf(parts[0])) {
|
|
||||||
//parts[2] = 443;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new Error("port must be specified - ex: tls:*:1337");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
protocol: parts[0]
|
|
||||||
, hostname: parts[1]
|
|
||||||
, port: parts[2]
|
|
||||||
};
|
|
||||||
}).forEach(function (val) {
|
|
||||||
memo.push(val);
|
|
||||||
});
|
|
||||||
|
|
||||||
return memo;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectPorts(val, memo) {
|
|
||||||
return memo.concat(val.split(/,/g).map(Number).filter(Boolean));
|
|
||||||
}
|
|
||||||
|
|
||||||
program
|
|
||||||
.version(pkg.version)
|
|
||||||
.option('--agree-tos', "Accept the Daplie and Let's Encrypt Terms of Service")
|
|
||||||
.option('--email <EMAIL>', "Email to use for Daplie and Let's Encrypt accounts")
|
|
||||||
.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 stunneld (used for JWT authentication)')
|
|
||||||
.parse(process.argv)
|
|
||||||
;
|
|
||||||
|
|
||||||
var portsMap = {};
|
|
||||||
var servernamesMap = {};
|
|
||||||
program.serve.forEach(function (proxy) {
|
|
||||||
servernamesMap[proxy.hostname] = true;
|
|
||||||
if (proxy.port) {
|
|
||||||
portsMap[proxy.port] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
program.servernames.forEach(function (name) {
|
|
||||||
servernamesMap[name] = true;
|
|
||||||
});
|
|
||||||
program.ports.forEach(function (port) {
|
|
||||||
portsMap[port] = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
program.servernames = Object.keys(servernamesMap);
|
|
||||||
if (!program.servernames.length) {
|
|
||||||
throw new Error('You must give this server at least one servername for its admin interface. Example:\n\n\t--servernames tunnel.example.com,tunnel.example.net');
|
|
||||||
}
|
|
||||||
|
|
||||||
program.ports = Object.keys(portsMap);
|
|
||||||
if (!program.ports.length) {
|
|
||||||
program.ports = [ 80, 443 ];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!program.secret) {
|
|
||||||
// TODO randomly generate and store in file?
|
|
||||||
console.warn("[SECURITY] you must provide --secret '" + require('crypto').randomBytes(16).toString('hex') + "'");
|
|
||||||
process.exit(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO letsencrypt
|
|
||||||
program.tlsOptions = require('localhost.daplie.me-certificates').merge({});
|
|
||||||
|
|
||||||
function approveDomains(opts, certs, cb) {
|
|
||||||
// This is where you check your database and associated
|
|
||||||
// email addresses with domains and agreements and such
|
|
||||||
|
|
||||||
// The domains being approved for the first time are listed in opts.domains
|
|
||||||
// Certs being renewed are listed in certs.altnames
|
|
||||||
if (certs) {
|
|
||||||
opts.domains = certs.altnames;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (-1 !== program.servernames.indexOf(opts.domain)) {
|
|
||||||
opts.email = program.email;
|
|
||||||
opts.agreeTos = program.agreeTos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: you can also change other options such as `challengeType` and `challenge`
|
|
||||||
// opts.challengeType = 'http-01';
|
|
||||||
// opts.challenge = require('le-challenge-fs').create({});
|
|
||||||
|
|
||||||
cb(null, { options: opts, certs: certs });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!program.email || !program.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("");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
program.greenlock = greenlock.create({
|
|
||||||
|
|
||||||
version: 'draft-11'
|
|
||||||
, server: 'https://acme-v02.api.letsencrypt.org/directory'
|
|
||||||
|
|
||||||
, challenges: {
|
|
||||||
// TODO dns-01
|
|
||||||
'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' })
|
|
||||||
}
|
|
||||||
|
|
||||||
, store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' })
|
|
||||||
|
|
||||||
, email: program.email
|
|
||||||
|
|
||||||
, agreeTos: program.agreeTos
|
|
||||||
|
|
||||||
, approveDomains: approveDomains
|
|
||||||
|
|
||||||
//, approvedDomains: program.servernames
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//program.tlsOptions.SNICallback = program.greenlock.httpsOptions.SNICallback;
|
|
||||||
/*
|
|
||||||
program.middleware = program.greenlock.middleware(function (req, res) {
|
|
||||||
res.end('Hello, World!');
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
require('../handlers').create(program); // adds directly to program for now...
|
|
||||||
|
|
||||||
//require('cluster-store').create().then(function (store) {
|
|
||||||
//program.store = store;
|
|
||||||
|
|
||||||
var net = require('net');
|
|
||||||
var netConnHandlers = stunneld.create(program); // { tcp, ws }
|
|
||||||
var WebSocketServer = require('ws').Server;
|
|
||||||
var wss = new WebSocketServer({ server: (program.httpTunnelServer || program.httpServer) });
|
|
||||||
wss.on('connection', netConnHandlers.ws);
|
|
||||||
program.ports.forEach(function (port) {
|
|
||||||
var tcp3000 = net.createServer();
|
|
||||||
tcp3000.listen(port, function () {
|
|
||||||
console.log('listening on ' + port);
|
|
||||||
});
|
|
||||||
tcp3000.on('connection', netConnHandlers.tcp);
|
|
||||||
});
|
|
||||||
//});
|
|
||||||
|
|
||||||
}());
|
|
|
@ -0,0 +1,343 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var pkg = require('../package.json');
|
||||||
|
|
||||||
|
var argv = process.argv.slice(2);
|
||||||
|
var relay = require('../');
|
||||||
|
var Greenlock = require('greenlock');
|
||||||
|
|
||||||
|
var confIndex = argv.indexOf('--config');
|
||||||
|
var confpath;
|
||||||
|
if (-1 === confIndex) {
|
||||||
|
confIndex = argv.indexOf('-c');
|
||||||
|
}
|
||||||
|
confpath = argv[confIndex + 1];
|
||||||
|
|
||||||
|
function help() {
|
||||||
|
console.info('');
|
||||||
|
console.info('Usage:');
|
||||||
|
console.info('');
|
||||||
|
console.info('\ttelebit-relay --config <path>');
|
||||||
|
console.info('');
|
||||||
|
console.info('Example:');
|
||||||
|
console.info('');
|
||||||
|
console.info('\ttelebit-relay --config /opt/telebit-relay/etc/telebit-relay.yml');
|
||||||
|
console.info('');
|
||||||
|
console.info('Config:');
|
||||||
|
console.info('');
|
||||||
|
console.info('\tSee https://git.coolaj86.com/coolaj86/telebit-relay.js');
|
||||||
|
console.info('');
|
||||||
|
console.info('');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-1 === confIndex || -1 !== argv.indexOf('-h') || -1 !== argv.indexOf('--help')) {
|
||||||
|
help();
|
||||||
|
}
|
||||||
|
if (!confpath || /^--/.test(confpath)) {
|
||||||
|
help();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyConfig(config) {
|
||||||
|
var state = { defaults: {}, ports: [ 80, 443 ], tcp: {} };
|
||||||
|
if ('undefined' !== typeof Promise) {
|
||||||
|
state.Promise = Promise;
|
||||||
|
} else {
|
||||||
|
state.Promise = require('bluebird');
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
if (!state.secret) {
|
||||||
|
state.secret = require('crypto').randomBytes(16).toString('hex');
|
||||||
|
console.info("");
|
||||||
|
console.info("Secret for this session:");
|
||||||
|
console.info("");
|
||||||
|
console.info("\t" + state.secret);
|
||||||
|
console.info("");
|
||||||
|
console.info("");
|
||||||
|
}
|
||||||
|
if (!state.config.greenlock) {
|
||||||
|
state.config.greenlock = {};
|
||||||
|
}
|
||||||
|
if (!state.config.greenlock.configDir) {
|
||||||
|
state.config.greenlock.configDir = require('os').homedir() + require('path').sep + 'acme';
|
||||||
|
}
|
||||||
|
|
||||||
|
function approveDomains(opts, certs, cb) {
|
||||||
|
if (state.debug) { console.log('[debug] approveDomains', opts.domains); }
|
||||||
|
// This is where you check your database and associated
|
||||||
|
// email addresses with domains and agreements and such
|
||||||
|
|
||||||
|
// The domains being approved for the first time are listed in opts.domains
|
||||||
|
// Certs being renewed are listed in certs.altnames
|
||||||
|
if (certs) {
|
||||||
|
opts.domains = certs.altnames;
|
||||||
|
cb(null, { options: opts, certs: certs });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.validHosts) { state.validHosts = {}; }
|
||||||
|
if (!state.validHosts[opts.domains[0]] && state.config.vhost) {
|
||||||
|
if (state.debug) { console.log('[sni] vhost checking is turned on'); }
|
||||||
|
var vhost = state.config.vhost.replace(/:hostname/, opts.domains[0]);
|
||||||
|
require('fs').readdir(vhost, function (err, nodes) {
|
||||||
|
if (state.debug) { console.log('[sni] checking fs vhost', opts.domains[0], !err); }
|
||||||
|
if (err) { check(); return; }
|
||||||
|
if (nodes) { approve(); }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function approve() {
|
||||||
|
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' })
|
||||||
|
};
|
||||||
|
opts.communityMember = state.config.communityMember;
|
||||||
|
cb(null, { options: opts, certs: certs });
|
||||||
|
}
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
if (state.debug) { console.log('[sni] checking servername'); }
|
||||||
|
if (-1 !== state.servernames.indexOf(opts.domain) || -1 !== (state._servernames||[]).indexOf(opts.domain)) {
|
||||||
|
approve();
|
||||||
|
} else {
|
||||||
|
cb(new Error("failed the approval chain '" + opts.domains[0] + "'"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.greenlock = Greenlock.create({
|
||||||
|
|
||||||
|
version: state.config.greenlock.version || 'draft-11'
|
||||||
|
, server: state.config.greenlock.server || 'https://acme-v02.api.letsencrypt.org/directory'
|
||||||
|
|
||||||
|
, store: require('le-store-certbot').create({ debug: state.config.debug || state.config.greenlock.debug, 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
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO specify extensions in config file
|
||||||
|
state.extensions = require('../lib/extensions');
|
||||||
|
} catch(e) {
|
||||||
|
if (state.debug) { console.log('[DEBUG] no extensions loaded', e); }
|
||||||
|
state.extensions = {};
|
||||||
|
}
|
||||||
|
require('../lib/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 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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.tcp[port] = net.createServer();
|
||||||
|
state.tcp[port].listen(port, function () {
|
||||||
|
console.info('[cli] Listening for TCP connections on', port);
|
||||||
|
});
|
||||||
|
state.tcp[port].on('connection', netConnHandlers.tcp);
|
||||||
|
});
|
||||||
|
//});
|
||||||
|
}
|
||||||
|
|
||||||
|
require('fs').readFile(confpath, 'utf8', function (err, text) {
|
||||||
|
var config;
|
||||||
|
|
||||||
|
var recase = require('recase').create({});
|
||||||
|
var camelCopy = recase.camelCopy.bind(recase);
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
console.error("\nCouldn't load config:\n\n\t" + err.message + "\n");
|
||||||
|
process.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = JSON.parse(text);
|
||||||
|
} catch(e1) {
|
||||||
|
try {
|
||||||
|
config = require('js-yaml').safeLoad(text);
|
||||||
|
} catch(e2) {
|
||||||
|
console.error(e1.message);
|
||||||
|
console.error(e2.message);
|
||||||
|
process.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyConfig(camelCopy(config));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function adjustArgs() {
|
||||||
|
function collectServernames(val, memo) {
|
||||||
|
var lowerCase = val.split(/,/).map(function (servername) {
|
||||||
|
return servername.toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
return memo.concat(lowerCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectProxies(val, memo) {
|
||||||
|
var vals = val.split(/,/g);
|
||||||
|
vals.map(function (location) {
|
||||||
|
// http:john.example.com:3000
|
||||||
|
// http://john.example.com:3000
|
||||||
|
var parts = location.split(':');
|
||||||
|
if (1 === parts.length) {
|
||||||
|
parts[1] = parts[0];
|
||||||
|
parts[0] = 'wss';
|
||||||
|
}
|
||||||
|
if (2 === parts.length) {
|
||||||
|
if (/\./.test(parts[0])) {
|
||||||
|
parts[2] = parts[1];
|
||||||
|
parts[1] = parts[0];
|
||||||
|
parts[0] = 'wss';
|
||||||
|
}
|
||||||
|
if (!/\./.test(parts[1])) {
|
||||||
|
throw new Error("bad --serve option Example: wss://tunnel.example.com:1337");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts[0] = parts[0].toLowerCase();
|
||||||
|
parts[1] = parts[1].toLowerCase().replace(/(\/\/)?/, '') || '*';
|
||||||
|
parts[2] = parseInt(parts[2], 10) || 0;
|
||||||
|
if (!parts[2]) {
|
||||||
|
// TODO grab OS list of standard ports?
|
||||||
|
if (-1 !== [ 'ws', 'http' ].indexOf(parts[0])) {
|
||||||
|
//parts[2] = 80;
|
||||||
|
}
|
||||||
|
else if (-1 !== [ 'wss', 'https' ].indexOf(parts[0])) {
|
||||||
|
//parts[2] = 443;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("port must be specified - ex: tls:*:1337");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
protocol: parts[0]
|
||||||
|
, hostname: parts[1]
|
||||||
|
, port: parts[2]
|
||||||
|
};
|
||||||
|
}).forEach(function (val) {
|
||||||
|
memo.push(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
return memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectPorts(val, memo) {
|
||||||
|
return memo.concat(val.split(/,/g).map(Number).filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
program
|
||||||
|
.version(pkg.version)
|
||||||
|
.option('--agree-tos', "Accept the Daplie and Let's Encrypt Terms of Service")
|
||||||
|
.option('--email <EMAIL>', "Email to use for Daplie and Let's Encrypt accounts")
|
||||||
|
.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)')
|
||||||
|
.parse(process.argv)
|
||||||
|
;
|
||||||
|
|
||||||
|
var portsMap = {};
|
||||||
|
var servernamesMap = {};
|
||||||
|
program.serve.forEach(function (proxy) {
|
||||||
|
servernamesMap[proxy.hostname] = true;
|
||||||
|
if (proxy.port) {
|
||||||
|
portsMap[proxy.port] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
program.servernames.forEach(function (name) {
|
||||||
|
servernamesMap[name] = true;
|
||||||
|
});
|
||||||
|
program.ports.forEach(function (port) {
|
||||||
|
portsMap[port] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
program.servernames = Object.keys(servernamesMap);
|
||||||
|
if (!program.servernames.length) {
|
||||||
|
throw new Error('You must give this server at least one servername for its admin interface. Example:\n\n\t--servernames tunnel.example.com,tunnel.example.net');
|
||||||
|
}
|
||||||
|
|
||||||
|
program.ports = Object.keys(portsMap);
|
||||||
|
if (!program.ports.length) {
|
||||||
|
program.ports = [ 80, 443 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!program.secret) {
|
||||||
|
// TODO randomly generate and store in file?
|
||||||
|
console.warn("[SECURITY] you must provide --secret '" + require('crypto').randomBytes(16).toString('hex') + "'");
|
||||||
|
process.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//program.tlsOptions.SNICallback = program.greenlock.httpsOptions.SNICallback;
|
||||||
|
/*
|
||||||
|
program.middleware = program.greenlock.middleware(function (req, res) {
|
||||||
|
res.end('Hello, World!');
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
//adjustArgs();
|
||||||
|
|
||||||
|
}());
|
|
@ -1,23 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Daplie Tunnel Server
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target systemd-networkd-wait-online.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
# Always restart, unless it's restarting fast enough for us to believe it's completely broken
|
|
||||||
Restart=always
|
|
||||||
StartLimitInterval=10
|
|
||||||
StartLimitBurst=3
|
|
||||||
|
|
||||||
User=www-data
|
|
||||||
Group=www-data
|
|
||||||
WorkingDirectory=/srv/stunneld
|
|
||||||
# TODO needs --config option and these options should go in a config file
|
|
||||||
ExecStart=/srv/stunneld/bin/stunneld.js --servernames tunnel.example.com --secret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' --email tunnel@example.com --agree-tos
|
|
||||||
|
|
||||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
||||||
NoNewPrivileges=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Pre-req
|
||||||
|
# sudo adduser telebit --home /opt/telebit-relay
|
||||||
|
# sudo mkdir -p /opt/telebit-relay/
|
||||||
|
# sudo chown -R telebit:telebit /opt/telebit-relay/
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Telebit Relay
|
||||||
|
Documentation=https://git.coolaj86.com/coolaj86/telebit-relay.js/
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target systemd-networkd-wait-online.service
|
||||||
|
|
||||||
|
[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
|
||||||
|
StartLimitInterval=10
|
||||||
|
StartLimitBurst=3
|
||||||
|
|
||||||
|
# User and group the process will run as
|
||||||
|
# (git is the de facto standard on most systems)
|
||||||
|
User=telebit
|
||||||
|
Group=telebit
|
||||||
|
|
||||||
|
WorkingDirectory=/opt/telebit-relay
|
||||||
|
# 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
|
||||||
|
ExecReload=/bin/kill -USR1 $MAINPID
|
||||||
|
|
||||||
|
# Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings.
|
||||||
|
# Unmodified gitea is not expected to use more than this.
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
LimitNPROC=64
|
||||||
|
|
||||||
|
# Use private /tmp and /var/tmp, which are discarded after gitea stops.
|
||||||
|
PrivateTmp=true
|
||||||
|
# Use a minimal /dev
|
||||||
|
PrivateDevices=true
|
||||||
|
# Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
|
||||||
|
ProtectHome=true
|
||||||
|
# Make /usr, /boot, /etc and possibly some more folders read-only.
|
||||||
|
ProtectSystem=full
|
||||||
|
# ... except /opt/gitea because we want a place for the database
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories
|
||||||
|
; ReadWritePaths=/opt/telebit-relay /etc/telebit
|
||||||
|
|
||||||
|
# The following additional security directives only work with systemd v229 or later.
|
||||||
|
# They further retrict privileges that can be gained by gitea.
|
||||||
|
# Note that you may have to add capabilities required by any plugins in use.
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
# Caveat: Some features may need additional capabilities.
|
||||||
|
# For example an "upload" may need CAP_LEASE
|
||||||
|
; CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_LEASE
|
||||||
|
; AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_LEASE
|
||||||
|
; NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,10 @@
|
||||||
|
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
|
|
@ -0,0 +1,15 @@
|
||||||
|
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:
|
||||||
|
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'))"
|
118
handlers.js
118
handlers.js
|
@ -1,118 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var http = require('http');
|
|
||||||
var tls = require('tls');
|
|
||||||
var wrapSocket = require('tunnel-packer').wrapSocket;
|
|
||||||
var redirectHttps = require('redirect-https')();
|
|
||||||
|
|
||||||
module.exports.create = function (program) {
|
|
||||||
var tunnelAdminTlsOpts = {};
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
program.httpServer = http.createServer(
|
|
||||||
program.greenlock && program.greenlock.middleware(notFound)
|
|
||||||
|| notFound
|
|
||||||
);
|
|
||||||
program.handleHttp = function (servername, socket) {
|
|
||||||
console.log("handleHttp('" + servername + "', socket)");
|
|
||||||
socket.__my_servername = servername;
|
|
||||||
program.httpServer.emit('connection', socket);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Probably something that needs to be redirected to https
|
|
||||||
function redirectHttpsAndClose(req, res) {
|
|
||||||
res.setHeader('Connection', 'close');
|
|
||||||
redirectHttps(req, res);
|
|
||||||
}
|
|
||||||
program.httpInsecureServer = http.createServer(
|
|
||||||
program.greenlock && program.greenlock.middleware(redirectHttpsAndClose)
|
|
||||||
|| redirectHttpsAndClose
|
|
||||||
);
|
|
||||||
program.handleInsecureHttp = function (servername, socket) {
|
|
||||||
console.log("handleInsecureHttp('" + servername + "', socket)");
|
|
||||||
socket.__my_servername = servername;
|
|
||||||
program.httpInsecureServer.emit('connection', socket);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// SNI is not recogonized / cannot be handled
|
|
||||||
//
|
|
||||||
program.httpInvalidSniServer = http.createServer(function (req, res) {
|
|
||||||
res.end("This is an old error message that shouldn't be actually be acessible anymore. If you get this please tell AJ so that he finds where it was still referenced and removes it");
|
|
||||||
});
|
|
||||||
program.tlsInvalidSniServer = tls.createServer(program.tlsOptions, function (tlsSocket) {
|
|
||||||
console.log('tls connection');
|
|
||||||
// things get a little messed up here
|
|
||||||
program.httpInvalidSniServer.emit('connection', tlsSocket);
|
|
||||||
});
|
|
||||||
program.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);
|
|
||||||
//program.tlsInvalidSniServer.emit('connection', wrapSocket(socket));
|
|
||||||
var tlsInvalidSniServer = tls.createServer(program.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.end(
|
|
||||||
"3. An inexplicable temporal shift of the quantum realm... that makes me feel uncomfortable.\n\n"
|
|
||||||
+ "[ERROR] No SNI header was sent. I can only think of two possible explanations for this:\n"
|
|
||||||
+ "\t1. You really love Windows XP and you just won't let go of Internet Explorer 6\n"
|
|
||||||
+ "\t2. You're writing a bot and you forgot to set the servername parameter\n"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end(
|
|
||||||
"You came in hot looking for '" + servername + "' and, granted, the IP address for that domain"
|
|
||||||
+ " must be pointing here (or else how could you be here?), nevertheless either it's not registered"
|
|
||||||
+ " in the internal system at all (which Seth says isn't even a thing) or there is no device"
|
|
||||||
+ " connected on the south side of the network which has informed me that it's ready to have traffic"
|
|
||||||
+ " for that domain forwarded to it (sorry I didn't check that deeply to determine which).\n\n"
|
|
||||||
+ "Either way, you're doing strange things that make me feel uncomfortable... Please don't touch me there any more.");
|
|
||||||
});
|
|
||||||
httpInvalidSniServer.emit('connection', tlsSocket);
|
|
||||||
});
|
|
||||||
tlsInvalidSniServer.emit('connection', wrapSocket(socket));
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// To ADMIN / CONTROL PANEL of the Tunnel Server Itself
|
|
||||||
//
|
|
||||||
program.httpTunnelServer = http.createServer(function (req, res) {
|
|
||||||
console.log('req.socket.encrypted', req.socket.encrypted);
|
|
||||||
res.end('Hello, World!');
|
|
||||||
});
|
|
||||||
Object.keys(program.tlsOptions).forEach(function (key) {
|
|
||||||
tunnelAdminTlsOpts[key] = program.tlsOptions[key];
|
|
||||||
});
|
|
||||||
tunnelAdminTlsOpts.SNICallback = (program.greenlock && program.greenlock.httpsOptions && function (servername, cb) {
|
|
||||||
console.log("time to handle '" + servername + "'");
|
|
||||||
program.greenlock.httpsOptions.SNICallback(servername, cb);
|
|
||||||
}) || tunnelAdminTlsOpts.SNICallback;
|
|
||||||
program.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) {
|
|
||||||
console.log('tls connection');
|
|
||||||
// things get a little messed up here
|
|
||||||
(program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket);
|
|
||||||
});
|
|
||||||
program.httpsTunnel = 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('httpsTunnel (Admin) servername', servername);
|
|
||||||
program.tlsTunnelServer.emit('connection', wrapSocket(socket));
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#<pre><code>
|
||||||
|
|
||||||
|
# This is a 3 step process
|
||||||
|
# 1. First we need to figure out whether to use wget or curl for fetching remote files
|
||||||
|
# 2. Next we need to figure out whether to use unzip or tar for downloading releases
|
||||||
|
# 3. We need to actually install the stuff
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
|
||||||
|
###############################
|
||||||
|
# #
|
||||||
|
# http_get #
|
||||||
|
# boilerplate for curl / wget #
|
||||||
|
# #
|
||||||
|
###############################
|
||||||
|
|
||||||
|
# See https://git.coolaj86.com/coolaj86/snippets/blob/master/bash/http-get.sh
|
||||||
|
|
||||||
|
_my_http_get=""
|
||||||
|
_my_http_opts=""
|
||||||
|
_my_http_out=""
|
||||||
|
|
||||||
|
detect_http_get()
|
||||||
|
{
|
||||||
|
set +e
|
||||||
|
if type -p curl >/dev/null 2>&1; then
|
||||||
|
_my_http_get="curl"
|
||||||
|
_my_http_opts="-fsSL"
|
||||||
|
_my_http_out="-o"
|
||||||
|
elif type -p wget >/dev/null 2>&1; then
|
||||||
|
_my_http_get="wget"
|
||||||
|
_my_http_opts="--quiet"
|
||||||
|
_my_http_out="-O"
|
||||||
|
else
|
||||||
|
echo "Aborted, could not find curl or wget"
|
||||||
|
return 7
|
||||||
|
fi
|
||||||
|
set -e
|
||||||
|
}
|
||||||
|
|
||||||
|
http_get()
|
||||||
|
{
|
||||||
|
$_my_http_get $_my_http_opts $_my_http_out "$2" "$1"
|
||||||
|
touch "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
http_bash()
|
||||||
|
{
|
||||||
|
_http_url=$1
|
||||||
|
my_args=${2:-}
|
||||||
|
rm -rf my-tmp-runner.sh
|
||||||
|
$_my_http_get $_my_http_opts $_my_http_out my-tmp-runner.sh "$_http_url"; bash my-tmp-runner.sh $my_args; rm my-tmp-runner.sh
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_http_get
|
||||||
|
|
||||||
|
###############################
|
||||||
|
## END HTTP_GET ##
|
||||||
|
###############################
|
||||||
|
|
||||||
|
my_email=${1:-}
|
||||||
|
my_servername=${2:-}
|
||||||
|
my_secret=""
|
||||||
|
my_user="telebit"
|
||||||
|
my_app="telebit-relay"
|
||||||
|
my_bin="telebit-relay.js"
|
||||||
|
my_name="Telebit Relay"
|
||||||
|
my_repo="telebit-relay.js"
|
||||||
|
|
||||||
|
if [ -z "${my_email}" ]; then
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
echo "Telebit uses Greenlock for free automated ssl through Let's Encrypt."
|
||||||
|
echo ""
|
||||||
|
echo "To accept the Terms of Service for Telebit, Greenlock and Let's Encrypt,"
|
||||||
|
echo "please enter your email."
|
||||||
|
echo ""
|
||||||
|
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 -p "domain (ex: telebit-relay.example.com): " my_servername
|
||||||
|
echo ""
|
||||||
|
# UX - just want a smooth transition
|
||||||
|
sleep 0.5
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
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"
|
||||||
|
# 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"
|
||||||
|
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_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"
|
||||||
|
mkdir -p $my_tmp
|
||||||
|
|
||||||
|
echo "sudo mkdir -p '$TELEBIT_RELAY_PATH'"
|
||||||
|
sudo mkdir -p "$TELEBIT_RELAY_PATH"
|
||||||
|
echo "sudo mkdir -p '/opt/$my_app/etc'"
|
||||||
|
sudo mkdir -p "/opt/$my_app/etc/"
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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/
|
||||||
|
else
|
||||||
|
echo "Neither tar nor unzip found. Abort."
|
||||||
|
exit 13
|
||||||
|
fi
|
||||||
|
set -e
|
||||||
|
|
||||||
|
pushd $TELEBIT_RELAY_PATH >/dev/null
|
||||||
|
$my_npm install >/dev/null 2>/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
cat << EOF > $TELEBIT_RELAY_PATH/bin/$my_app
|
||||||
|
#!/bin/bash
|
||||||
|
$my_node $TELEBIT_RELAY_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
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "/opt/$my_app/etc/$my_app.yml" ]; then
|
||||||
|
echo "### Creating config file from template. sudo may be required"
|
||||||
|
#echo "sudo rsync -a examples/$my_app.yml /opt/$my_app/etc/$my_app.yml"
|
||||||
|
sudo bash -c "echo 'email: $my_email' >> /opt/$my_app/etc/$my_app.yml"
|
||||||
|
sudo bash -c "echo 'secret: $my_secret' >> /opt/$my_app/etc/$my_app.yml"
|
||||||
|
sudo bash -c "echo 'servernames: [ $my_servername ]' >> /opt/$my_app/etc/$my_app.yml"
|
||||||
|
sudo bash -c "cat examples/$my_app.yml.tpl >> /opt/$my_app/etc/$my_app.yml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "sudo chown -R $my_user '$TELEBIT_RELAY_PATH' '/opt/$my_app/etc'"
|
||||||
|
sudo chown -R $my_user "$TELEBIT_RELAY_PATH" "/opt/$my_app/etc"
|
||||||
|
|
||||||
|
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
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
echo "sudo systemctl enable $my_app"
|
||||||
|
sudo systemctl enable $my_app
|
||||||
|
echo "sudo systemctl start $my_app"
|
||||||
|
sudo systemctl restart $my_app
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
echo "=============================================="
|
||||||
|
echo " Privacy Settings in Config"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
echo "The example config file /opt/$my_app/etc/$my_app.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."
|
||||||
|
echo ""
|
||||||
|
echo "Please edit the config file to meet your needs before starting."
|
||||||
|
echo ""
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
echo "=============================================="
|
||||||
|
echo "Installed successfully. Last steps:"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
echo "Edit the config and restart, if desired:"
|
||||||
|
echo ""
|
||||||
|
echo " sudo vim /opt/$my_app/etc/$my_app.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 /opt/$my_app/etc/$my_app.yml"
|
||||||
|
echo ""
|
||||||
|
sleep 1
|
|
@ -0,0 +1,5 @@
|
||||||
|
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
|
|
@ -0,0 +1,227 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var http = require('http');
|
||||||
|
var tls = require('tls');
|
||||||
|
var wrapSocket = require('proxy-packer').wrapSocket;
|
||||||
|
var redirectHttps = require('redirect-https')();
|
||||||
|
|
||||||
|
function noSniCallback(tag) {
|
||||||
|
return function _noSniCallback(servername, cb) {
|
||||||
|
var err = new Error("[noSniCallback] no handler set for '" + tag + "':'" + servername + "'");
|
||||||
|
console.error(err.message);
|
||||||
|
cb(new Error(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.create = function (state) {
|
||||||
|
var tunnelAdminTlsOpts = {};
|
||||||
|
var setupSniCallback;
|
||||||
|
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"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setupSniCallback(servername, cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Probably a reverse proxy on an internal network (or ACME challenge)
|
||||||
|
function notFound(req, res) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end("File not found.\n");
|
||||||
|
}
|
||||||
|
state.httpServer = http.createServer(
|
||||||
|
state.greenlock && state.greenlock.middleware(notFound)
|
||||||
|
|| notFound
|
||||||
|
);
|
||||||
|
state.handleHttp = function (servername, socket) {
|
||||||
|
console.log("handleHttp('" + servername + "', socket)");
|
||||||
|
socket.__my_servername = servername;
|
||||||
|
state.httpServer.emit('connection', socket);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Probably something that needs to be redirected to https
|
||||||
|
function redirectHttpsAndClose(req, res) {
|
||||||
|
res.setHeader('Connection', 'close');
|
||||||
|
redirectHttps(req, res);
|
||||||
|
}
|
||||||
|
state.httpInsecureServer = http.createServer(
|
||||||
|
state.greenlock && state.greenlock.middleware(redirectHttpsAndClose)
|
||||||
|
|| redirectHttpsAndClose
|
||||||
|
);
|
||||||
|
state.handleInsecureHttp = function (servername, socket) {
|
||||||
|
console.log("handleInsecureHttp('" + servername + "', socket)");
|
||||||
|
socket.__my_servername = servername;
|
||||||
|
state.httpInsecureServer.emit('connection', socket);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// SNI is not recogonized / cannot be handled
|
||||||
|
//
|
||||||
|
state.httpInvalidSniServer = http.createServer(function (req, res) {
|
||||||
|
res.end("This is an old error message that shouldn't be actually be acessible anymore. If you get this please tell AJ so that he finds where it was still referenced and removes it");
|
||||||
|
});
|
||||||
|
state.tlsInvalidSniServer = tls.createServer(state.tlsOptions, function (tlsSocket) {
|
||||||
|
console.log('tls connection');
|
||||||
|
// things get a little messed up here
|
||||||
|
state.httpInvalidSniServer.emit('connection', tlsSocket);
|
||||||
|
});
|
||||||
|
state.tlsInvalidSniServer.on('tlsClientError', function () {
|
||||||
|
console.error('tlsClientError InvalidSniServer');
|
||||||
|
});
|
||||||
|
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('[tlsInvalid] tls connection');
|
||||||
|
// things get a little messed up here
|
||||||
|
var httpInvalidSniServer = http.createServer(function (req, res) {
|
||||||
|
if (!servername) {
|
||||||
|
res.statusCode = 422;
|
||||||
|
res.end(
|
||||||
|
"3. An inexplicable temporal shift of the quantum realm... that makes me feel uncomfortable.\n\n"
|
||||||
|
+ "[ERROR] No SNI header was sent. I can only think of two possible explanations for this:\n"
|
||||||
|
+ "\t1. You really love Windows XP and you just won't let go of Internet Explorer 6\n"
|
||||||
|
+ "\t2. You're writing a bot and you forgot to set the servername parameter\n"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end(
|
||||||
|
"You came in hot looking for '" + servername + "' and, granted, the IP address for that domain"
|
||||||
|
+ " must be pointing here (or else how could you be here?), nevertheless either it's not registered"
|
||||||
|
+ " in the internal system at all (which Seth says isn't even a thing) or there is no device"
|
||||||
|
+ " connected on the south side of the network which has informed me that it's ready to have traffic"
|
||||||
|
+ " for that domain forwarded to it (sorry I didn't check that deeply to determine which).\n\n"
|
||||||
|
+ "Either way, you're doing strange things that make me feel uncomfortable... Please don't touch me there any more.");
|
||||||
|
});
|
||||||
|
httpInvalidSniServer.emit('connection', tlsSocket);
|
||||||
|
});
|
||||||
|
tlsInvalidSniServer.on('tlsClientError', function () {
|
||||||
|
console.error('tlsClientError InvalidSniServer httpsInvalid');
|
||||||
|
});
|
||||||
|
tlsInvalidSniServer.emit('connection', wrapSocket(socket));
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// To ADMIN / CONTROL PANEL of the Tunnel Server Itself
|
||||||
|
//
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.keys(state.tlsOptions).forEach(function (key) {
|
||||||
|
tunnelAdminTlsOpts[key] = state.tlsOptions[key];
|
||||||
|
});
|
||||||
|
if (state.greenlock && state.greenlock.tlsOptions) {
|
||||||
|
tunnelAdminTlsOpts.SNICallback = state.greenlock.tlsOptions.SNICallback;
|
||||||
|
} else {
|
||||||
|
console.log('[Admin] custom or null tlsOptions for SNICallback');
|
||||||
|
tunnelAdminTlsOpts.SNICallback = tunnelAdminTlsOpts.SNICallback || noSniCallback('admin');
|
||||||
|
}
|
||||||
|
state.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) {
|
||||||
|
if (state.debug) { console.log('[Admin] new tls-terminated 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');
|
||||||
|
});
|
||||||
|
state.httpsTunnel = 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);
|
||||||
|
|
||||||
|
if (state.debug) { console.log('[Admin] new raw tls connection for', servername); }
|
||||||
|
state.tlsTunnelServer.emit('connection', wrapSocket(socket));
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// First time setup
|
||||||
|
//
|
||||||
|
var serveSetup = require('serve-static')(__dirname + '/../admin/setup', { redirect: true });
|
||||||
|
var finalhandler = require('finalhandler');
|
||||||
|
state.httpSetupServer = http.createServer(function (req, res) {
|
||||||
|
if (req.socket.encrypted) {
|
||||||
|
serveSetup(req, res, finalhandler(req, res));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(state.greenlock && state.greenlock.middleware(redirectHttpsAndClose)
|
||||||
|
|| redirectHttpsAndClose)(req, res, function () {
|
||||||
|
console.log('[Setup] fallthrough to setup ui');
|
||||||
|
serveSetup(req, res, finalhandler(req, res));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
state.tlsSetupServer = tls.createServer(setupTlsOpts, function (tlsSocket) {
|
||||||
|
console.log('[Setup] terminated tls connection');
|
||||||
|
// things get a little messed up here
|
||||||
|
state.httpSetupServer.emit('connection', tlsSocket);
|
||||||
|
});
|
||||||
|
state.tlsSetupServer.on('tlsClientError', function () {
|
||||||
|
console.error('[Setup] tlsClientError SetupServer');
|
||||||
|
});
|
||||||
|
state.httpsSetupServer = function (servername, socket) {
|
||||||
|
console.log('[Setup] raw tls connection for', servername);
|
||||||
|
state._servernames = [servername];
|
||||||
|
state.config.agreeTos = true; // TODO: BUG XXX BAD, make user accept
|
||||||
|
setupSniCallback = state.greenlock.tlsOptions.SNICallback || noSniCallback('setup');
|
||||||
|
state.tlsSetupServer.emit('connection', wrapSocket(socket));
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// vhost
|
||||||
|
//
|
||||||
|
state.httpVhost = http.createServer(function (req, res) {
|
||||||
|
if (state.debug) { console.log('[vhost] 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 });
|
||||||
|
|
||||||
|
if (req.socket.encrypted) { serveVhost(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);
|
||||||
|
});
|
||||||
|
state.tlsVhost = tls.createServer(
|
||||||
|
{ SNICallback: function (servername, cb) {
|
||||||
|
if (state.debug) { console.log('[vhost] SNICallback for', servername); }
|
||||||
|
tunnelAdminTlsOpts.SNICallback(servername, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, function (tlsSocket) {
|
||||||
|
if (state.debug) { console.log('tlsVhost (local)'); }
|
||||||
|
state.httpVhost.emit('connection', tlsSocket);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
state.tlsVhost.on('tlsClientError', function (e) {
|
||||||
|
console.error('tlsClientError Vhost', e);
|
||||||
|
});
|
||||||
|
state.httpsVhost = function (servername, socket) {
|
||||||
|
if (state.debug) { console.log('[vhost] httpsVhost (local) for', servername); }
|
||||||
|
state.tlsVhost.emit('connection', wrapSocket(socket));
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,54 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Packer = require('proxy-packer');
|
||||||
|
|
||||||
|
module.exports = function pipeWs(servername, service, conn, remote, 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(remote.upgradeReq.socket);
|
||||||
|
|
||||||
|
//if (state.debug) { console.log('[pipeWs] client', cid, '=> remote', rid, 'for', servername, 'via', service); }
|
||||||
|
|
||||||
|
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] remote', rid, ' => client', cid, 'error sending websocket message', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remote.clients[cid] = conn;
|
||||||
|
|
||||||
|
conn.on('data', function (chunk) {
|
||||||
|
//if (state.debug) { console.log('[pipeWs] client', cid, ' => remote', 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 remote.clients[cid];
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,584 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var url = require('url');
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
|
var jwt = require('jsonwebtoken');
|
||||||
|
var Packer = require('proxy-packer');
|
||||||
|
var portServers = {};
|
||||||
|
|
||||||
|
function timeoutPromise(duration) {
|
||||||
|
return new PromiseA(function (resolve) {
|
||||||
|
setTimeout(resolve, duration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var Devices = require('./device-tracker');
|
||||||
|
var pipeWs = require('./pipe-ws.js');
|
||||||
|
|
||||||
|
module.exports.store = { Devices: Devices };
|
||||||
|
module.exports.create = function (state) {
|
||||||
|
state.deviceLists = {};
|
||||||
|
//var deviceLists = {};
|
||||||
|
var activityTimeout = state.activityTimeout || 2*60*1000;
|
||||||
|
var pongTimeout = state.pongTimeout || 10*1000;
|
||||||
|
state.Devices = Devices;
|
||||||
|
var onTcpConnection = require('./unwrap-tls').createTcpConnectionHandler(state);
|
||||||
|
|
||||||
|
// 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 onDynTcpConn(conn) {
|
||||||
|
var serviceport = this.address().port;
|
||||||
|
console.log('[DynTcpConn] new connection on', serviceport);
|
||||||
|
var remote = Devices.next(state.deviceLists, serviceport)
|
||||||
|
|
||||||
|
if (!remote) {
|
||||||
|
conn.write("[Sanity Error] I've got a blank space baby, but nowhere to write your name.");
|
||||||
|
conn.end();
|
||||||
|
try {
|
||||||
|
this.close();
|
||||||
|
} catch(e) {
|
||||||
|
console.error("[DynTcpConn] failed to close server:", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.once('data', function (firstChunk) {
|
||||||
|
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, client, remote, serviceport)
|
||||||
|
// remote.clients is managed as part of the piping process
|
||||||
|
if (state.debug) { console.log("[DynTcp]", serviceport, "piping to remote"); }
|
||||||
|
pipeWs(null, 'tcp', conn, remote, serviceport)
|
||||||
|
|
||||||
|
process.nextTick(function () { conn.resume(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWsConnection(ws, upgradeReq) {
|
||||||
|
var socketId = Packer.socketToId(upgradeReq.socket);
|
||||||
|
if (state.debug) { console.log('[ws] connection', socketId); }
|
||||||
|
|
||||||
|
var remotes = {};
|
||||||
|
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) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (firstToken) {
|
||||||
|
return addToken(firstToken, true).then(next).catch(function (err) {
|
||||||
|
sendTunnelMsg(null, [0, err], 'control');
|
||||||
|
ws.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
||||||
|
function onAuth(token) {
|
||||||
|
if ('string' !== typeof jwtoken) {
|
||||||
|
jwtoken = JSON.stringify(jwtoken);
|
||||||
|
}
|
||||||
|
var err;
|
||||||
|
if (!token) {
|
||||||
|
err = new Error("invalid access token");
|
||||||
|
err.code = "E_INVALID_TOKEN";
|
||||||
|
return state.Promise.reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.jwt && jwtoken !== token.jwt) {
|
||||||
|
// Access Token
|
||||||
|
sendTunnelMsg(
|
||||||
|
null
|
||||||
|
, [ 3
|
||||||
|
, 'access_token'
|
||||||
|
, { jwt: token.jwt }
|
||||||
|
]
|
||||||
|
, 'control'
|
||||||
|
);
|
||||||
|
// these aren't needed internally once they're sent
|
||||||
|
token.jwt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(token.domains)) {
|
||||||
|
if ('string' === typeof token.name) {
|
||||||
|
token.domains = [ token.name ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(token.domains) || !token.domains.length) {
|
||||||
|
err = new Error("invalid domains array");
|
||||||
|
err.code = "E_INVALID_NAME";
|
||||||
|
return state.Promise.reject(err);
|
||||||
|
}
|
||||||
|
if (token.domains.some(function (name) { return typeof name !== 'string'; })) {
|
||||||
|
err = new Error("invalid domain name(s)");
|
||||||
|
err.code = "E_INVALID_NAME";
|
||||||
|
return state.Promise.reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
Devices.add(state.deviceLists, domainname, token);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onDynTcpReadyHelper(serviceport) {
|
||||||
|
//token.dynamicPorts.push(serviceport);
|
||||||
|
Devices.add(state.deviceLists, serviceport, token);
|
||||||
|
//var hri = require('human-readable-ids').hri;
|
||||||
|
//var hrname = hri.random() + '.telebit.cloud';
|
||||||
|
//token.dynamicNames.push(hrname);
|
||||||
|
// TODO restrict to authenticated device
|
||||||
|
// TODO pull servername from config
|
||||||
|
// TODO remove hrname on disconnect
|
||||||
|
//Devices.add(state.deviceLists, hrname, token);
|
||||||
|
sendTunnelMsg(
|
||||||
|
null
|
||||||
|
, [ 2
|
||||||
|
, 'grant'
|
||||||
|
, [ ['ssh+https', token.domains[0], 443 ]
|
||||||
|
, ['ssh', 'ssh.telebit.cloud', serviceport ]
|
||||||
|
, ['tcp', 'tcp.telebit.cloud', serviceport]
|
||||||
|
, ['https', token.domains[0] ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, 'control'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DEBUG] got to firstToken check');
|
||||||
|
|
||||||
|
if (!token.ports) {
|
||||||
|
token.ports = [];
|
||||||
|
}
|
||||||
|
if (!firstToken || firstToken === jwtoken) {
|
||||||
|
if (!token.ports.length) {
|
||||||
|
token.ports.push( 0 );
|
||||||
|
}
|
||||||
|
firstToken = token.jwt || jwtoken;
|
||||||
|
}
|
||||||
|
|
||||||
|
//token.dynamicPorts = [];
|
||||||
|
//token.dynamicNames = [];
|
||||||
|
|
||||||
|
var onePortForNow = parseInt(token.ports[0], 10) || 0;
|
||||||
|
if (portServers[onePortForNow]) {
|
||||||
|
//token.ports = [];
|
||||||
|
token.server = portServers[onePortForNow];
|
||||||
|
token.server.on('connection', onDynTcpConn);
|
||||||
|
onDynTcpReadyHelper(onePortForNow);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
token.server = require('net').createServer(onDynTcpConn).listen(onePortForNow, function () {
|
||||||
|
var serviceport = this.address().port;
|
||||||
|
portServers[serviceport] = this;
|
||||||
|
console.info('[DynTcpConn] Port', serviceport, 'now open for', token.deviceId);
|
||||||
|
onDynTcpReadyHelper(serviceport);
|
||||||
|
});
|
||||||
|
token.server.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remotes[jwtoken] = token;
|
||||||
|
console.info("[ws] authorized", socketId, "for", token.deviceId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remotes[jwtoken]) {
|
||||||
|
// return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
|
||||||
|
return state.Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.authenticate({ auth: jwtoken }).then(onAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(state.deviceLists, domainname, remote);
|
||||||
|
});
|
||||||
|
remote.ports.forEach(function (portnumber) {
|
||||||
|
Devices.remove(state.deviceLists, portnumber, remote);
|
||||||
|
});
|
||||||
|
remote.ws = null;
|
||||||
|
remote.upgradeReq = null;
|
||||||
|
if (remote.server) {
|
||||||
|
remote.serverPort = remote.server.address().port;
|
||||||
|
remote.server.close(function () {
|
||||||
|
console.log("[DynTcpConn] closing server for ", remote.serverPort);
|
||||||
|
remote.serverPort = null;
|
||||||
|
});
|
||||||
|
remote.server = 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("[ws] removed token '" + remote.deviceId + "' from", socketId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
var commandHandlers = {
|
||||||
|
add_token: addToken
|
||||||
|
, auth: addToken
|
||||||
|
, authn: addToken
|
||||||
|
, authz: addToken
|
||||||
|
, delete_token: function (token) {
|
||||||
|
return state.Promise.resolve(function () {
|
||||||
|
var err;
|
||||||
|
|
||||||
|
if (token !== '*') {
|
||||||
|
err = removeToken(token);
|
||||||
|
if (err) { return state.Promise.reject(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(remotes).some(function (jwtoken) {
|
||||||
|
err = removeToken(jwtoken);
|
||||||
|
return err;
|
||||||
|
});
|
||||||
|
if (err) { return state.Promise.reject(err); }
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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', 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.warn('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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSuccess() {
|
||||||
|
sendTunnelMsg(null, [-cmd[0], null], 'control');
|
||||||
|
}
|
||||||
|
function onError(err) {
|
||||||
|
sendTunnelMsg(null, [-cmd[0], err], 'control');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commandHandlers[cmd[1]]) {
|
||||||
|
onError({ message: 'unknown command "'+cmd[1]+'"', code: 'E_UNKNOWN_COMMAND' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandHandlers[cmd[1]].apply(null, cmd.slice(2)).then(onSuccess, onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
, onmessage: function (tun) {
|
||||||
|
var cid = Packer.addrToId(tun);
|
||||||
|
if (state.debug) { console.log("remote '" + logName() + "' has data for '" + cid + "'", tun.data.byteLength); }
|
||||||
|
|
||||||
|
var browserConn = getBrowserConn(cid);
|
||||||
|
if (!browserConn) {
|
||||||
|
sendTunnelMsg(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) {
|
||||||
|
sendTunnelMsg(tun, browserConn.tunnelRead, 'pause');
|
||||||
|
browserConn.remotePaused = true;
|
||||||
|
|
||||||
|
browserConn.once('drain', function () {
|
||||||
|
sendTunnelMsg(tun, browserConn.tunnelRead, 'resume');
|
||||||
|
browserConn.remotePaused = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
, onpause: function (tun) {
|
||||||
|
var cid = Packer.addrToId(tun);
|
||||||
|
console.log('[TunnelPause]', cid);
|
||||||
|
var browserConn = getBrowserConn(cid);
|
||||||
|
if (browserConn) {
|
||||||
|
browserConn.manualPause = true;
|
||||||
|
browserConn.pause();
|
||||||
|
} else {
|
||||||
|
sendTunnelMsg(tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
, onresume: function (tun) {
|
||||||
|
var cid = Packer.addrToId(tun);
|
||||||
|
console.log('[TunnelResume]', cid);
|
||||||
|
var browserConn = getBrowserConn(cid);
|
||||||
|
if (browserConn) {
|
||||||
|
browserConn.manualPause = false;
|
||||||
|
browserConn.resume();
|
||||||
|
} else {
|
||||||
|
sendTunnelMsg(tun, {message: 'no matching connection', code: 'E_NO_CONN'}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
, onend: function (tun) {
|
||||||
|
var cid = Packer.addrToId(tun);
|
||||||
|
console.log('[TunnelEnd]', cid);
|
||||||
|
closeBrowserConn(cid);
|
||||||
|
}
|
||||||
|
, onerror: function (tun) {
|
||||||
|
var cid = Packer.addrToId(tun);
|
||||||
|
console.warn('[TunnelError]', cid, tun.message);
|
||||||
|
closeBrowserConn(cid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var unpacker = Packer.create(packerHandlers);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (state.debug) { 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.warn('home cloud', logName(), 'connection timed out');
|
||||||
|
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());
|
||||||
|
unpacker.fns.addChunk(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hangup() {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
console.log('[ws] device hangup', logName(), 'connection closing');
|
||||||
|
Object.keys(remotes).forEach(function (jwtoken) {
|
||||||
|
removeToken(jwtoken);
|
||||||
|
});
|
||||||
|
ws.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastActivity = Date.now();
|
||||||
|
var timeoutId;
|
||||||
|
|
||||||
|
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', forwardMessage);
|
||||||
|
ws.on('close', hangup);
|
||||||
|
ws.on('error', hangup);
|
||||||
|
|
||||||
|
// Status Code '1' for Status 'hello'
|
||||||
|
sendTunnelMsg(null, [1, 'hello', [unpacker._version], Object.keys(commandHandlers)], 'control');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tcp: onTcpConnection
|
||||||
|
, ws: onWsConnection
|
||||||
|
, isClientDomain: Devices.exist.bind(null, state.deviceLists)
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,57 +1,13 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var packer = require('tunnel-packer');
|
|
||||||
var sni = require('sni');
|
var sni = require('sni');
|
||||||
|
var pipeWs = require('./pipe-ws.js');
|
||||||
|
|
||||||
function pipeWs(servername, service, conn, remote) {
|
module.exports.createTcpConnectionHandler = function (state) {
|
||||||
console.log('[pipeWs] servername:', servername, 'service:', service);
|
var Devices = state.Devices;
|
||||||
|
|
||||||
var browserAddr = packer.socketToAddr(conn);
|
return function onTcpConnection(conn, serviceport) {
|
||||||
browserAddr.service = service;
|
serviceport = serviceport || conn.localPort;
|
||||||
var cid = packer.addrToId(browserAddr);
|
|
||||||
conn.tunnelCid = cid;
|
|
||||||
console.log('[pipeWs] browser is', cid, 'home-cloud is', packer.socketToId(remote.upgradeReq.socket));
|
|
||||||
|
|
||||||
function sendWs(data, serviceOverride) {
|
|
||||||
if (remote.ws && (!conn.tunnelClosing || serviceOverride)) {
|
|
||||||
try {
|
|
||||||
remote.ws.send(packer.pack(browserAddr, data, serviceOverride), { binary: true });
|
|
||||||
// If we can't send data over the websocket as fast as this connection can send it to us
|
|
||||||
// (or there are a lot of connections trying to send over the same websocket) then we
|
|
||||||
// need to pause the connection for a little. We pause all connections if any are paused
|
|
||||||
// to make things more fair so a connection doesn't get stuck waiting for everyone else
|
|
||||||
// to finish because it got caught on the boundary. Also if serviceOverride is set it
|
|
||||||
// means the connection is over, so no need to pause it.
|
|
||||||
if (!serviceOverride && (remote.pausedConns.length || remote.ws.bufferedAmount > 1024*1024)) {
|
|
||||||
// console.log('pausing', cid, 'to allow web socket to catch up');
|
|
||||||
conn.pause();
|
|
||||||
remote.pausedConns.push(conn);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[pipeWs] error sending websocket message', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remote.clients[cid] = conn;
|
|
||||||
conn.on('data', function (chunk) {
|
|
||||||
console.log('[pipeWs] data from browser to tunneler', chunk.byteLength);
|
|
||||||
sendWs(chunk);
|
|
||||||
});
|
|
||||||
conn.on('error', function (err) {
|
|
||||||
console.warn('[pipeWs] browser connection error', err);
|
|
||||||
});
|
|
||||||
conn.on('close', function (hadErr) {
|
|
||||||
console.log('[pipeWs] browser connection closing');
|
|
||||||
sendWs(null, hadErr ? 'error': 'end');
|
|
||||||
delete remote.clients[cid];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.createTcpConnectionHandler = function (copts) {
|
|
||||||
var Devices = copts.Devices;
|
|
||||||
|
|
||||||
return function onTcpConnection(conn) {
|
|
||||||
// this works when I put it here, but I don't know if it's tls yet here
|
// this works when I put it here, but I don't know if it's tls yet here
|
||||||
// httpsServer.emit('connection', socket);
|
// httpsServer.emit('connection', socket);
|
||||||
//tls3000.emit('connection', socket);
|
//tls3000.emit('connection', socket);
|
||||||
|
@ -63,6 +19,9 @@ module.exports.createTcpConnectionHandler = function (copts) {
|
||||||
|
|
||||||
//return;
|
//return;
|
||||||
conn.once('data', function (firstChunk) {
|
conn.once('data', function (firstChunk) {
|
||||||
|
conn.pause();
|
||||||
|
conn.unshift(firstChunk);
|
||||||
|
|
||||||
// BUG XXX: this assumes that the packet won't be chunked smaller
|
// BUG XXX: this assumes that the packet won't be chunked smaller
|
||||||
// than the 'hello' or the point of the 'Host' header.
|
// than the 'hello' or the point of the 'Host' header.
|
||||||
// This is fairly reasonable, but there are edge cases where
|
// This is fairly reasonable, but there are edge cases where
|
||||||
|
@ -70,9 +29,14 @@ module.exports.createTcpConnectionHandler = function (copts) {
|
||||||
// and so it should be fixed at some point in the future
|
// and so it should be fixed at some point in the future
|
||||||
|
|
||||||
// defer after return (instead of being in many places)
|
// defer after return (instead of being in many places)
|
||||||
process.nextTick(function () {
|
function deferData(fn) {
|
||||||
conn.unshift(firstChunk);
|
if (fn) {
|
||||||
});
|
state[fn](servername, conn)
|
||||||
|
}
|
||||||
|
process.nextTick(function () {
|
||||||
|
conn.resume();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var service = 'tcp';
|
var service = 'tcp';
|
||||||
var servername;
|
var servername;
|
||||||
|
@ -80,35 +44,68 @@ module.exports.createTcpConnectionHandler = function (copts) {
|
||||||
var m;
|
var m;
|
||||||
|
|
||||||
function tryTls() {
|
function tryTls() {
|
||||||
if (-1 !== copts.servernames.indexOf(servername)) {
|
var vhost;
|
||||||
console.log("Lock and load, admin interface time!");
|
|
||||||
copts.httpsTunnel(servername, conn);
|
if (!state.servernames.length) {
|
||||||
|
console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)");
|
||||||
|
deferData('httpsSetupServer');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (-1 !== state.servernames.indexOf(servername)) {
|
||||||
|
if (state.debug) { console.log("[Admin]", servername); }
|
||||||
|
deferData('httpsTunnel');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.config.nowww && /^www\./i.test(servername)) {
|
||||||
|
console.log("TODO: use www bare redirect");
|
||||||
|
}
|
||||||
|
|
||||||
if (!servername) {
|
if (!servername) {
|
||||||
console.log("No SNI was given, so there's nothing we can do here");
|
if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); }
|
||||||
copts.httpsInvalid(servername, conn);
|
deferData('httpsInvalid');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextDevice = Devices.next(copts.deviceLists, servername);
|
function run() {
|
||||||
if (!nextDevice) {
|
var nextDevice = Devices.next(state.deviceLists, servername);
|
||||||
console.log("No devices match the given servername");
|
if (!nextDevice) {
|
||||||
copts.httpsInvalid(servername, conn);
|
if (state.debug) { console.log("No devices match the given servername"); }
|
||||||
|
deferData('httpsInvalid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.debug) { console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])"); }
|
||||||
|
deferData();
|
||||||
|
pipeWs(servername, service, conn, nextDevice, serviceport);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO don't run an fs check if we already know this is working elsewhere
|
||||||
|
//if (!state.validHosts) { state.validHosts = {}; }
|
||||||
|
if (state.config.vhost) {
|
||||||
|
vhost = state.config.vhost.replace(/:hostname/, (servername||'reallydoesntexist'));
|
||||||
|
if (state.debug) { console.log("[tcp] [vhost]", state.config.vhost, "=>", vhost); }
|
||||||
|
//state.httpsVhost(servername, conn);
|
||||||
|
//return;
|
||||||
|
require('fs').readdir(vhost, function (err, nodes) {
|
||||||
|
if (state.debug && err) { console.log("VHOST error", err); }
|
||||||
|
if (err) { run(); return; }
|
||||||
|
//if (nodes) { deferData('httpsVhost'); return; }
|
||||||
|
deferData('httpsVhost');
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("pipeWs(servername, service, socket, deviceLists['" + servername + "'])");
|
run();
|
||||||
pipeWs(servername, service, conn, nextDevice);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
|
// https://github.com/mscdex/httpolyglot/issues/3#issuecomment-173680155
|
||||||
if (22 === firstChunk[0]) {
|
if (22 === firstChunk[0]) {
|
||||||
// TLS
|
// TLS
|
||||||
service = 'https';
|
service = 'https';
|
||||||
servername = (sni(firstChunk)||'').toLowerCase();
|
servername = (sni(firstChunk)||'').toLowerCase().trim();
|
||||||
console.log("tls hello servername:", servername);
|
if (state.debug) { console.log("[tcp] tls hello from '" + servername + "'"); }
|
||||||
tryTls();
|
tryTls();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -117,23 +114,32 @@ module.exports.createTcpConnectionHandler = function (copts) {
|
||||||
str = firstChunk.toString();
|
str = firstChunk.toString();
|
||||||
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
|
m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
|
||||||
servername = (m && m[1].toLowerCase() || '').split(':')[0];
|
servername = (m && m[1].toLowerCase() || '').split(':')[0];
|
||||||
console.log('servername', servername);
|
if (state.debug) { console.log("[tcp] http hostname '" + servername + "'"); }
|
||||||
|
|
||||||
if (/HTTP\//i.test(str)) {
|
if (/HTTP\//i.test(str)) {
|
||||||
|
if (!state.servernames.length) {
|
||||||
|
console.info("[tcp] No admin servername. Entering setup mode.");
|
||||||
|
deferData();
|
||||||
|
state.httpSetupServer.emit('connection', conn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
service = 'http';
|
service = 'http';
|
||||||
// TODO disallow http entirely
|
// TODO make https redirect configurable
|
||||||
// /^\/\.well-known\/acme-challenge\//.test(str)
|
// /^\/\.well-known\/acme-challenge\//.test(str)
|
||||||
if (/well-known/.test(str)) {
|
if (/well-known/.test(str)) {
|
||||||
// HTTP
|
// HTTP
|
||||||
if (Devices.exist(copts.deviceLists, servername)) {
|
if (Devices.exist(state.deviceLists, servername)) {
|
||||||
pipeWs(servername, service, conn, Devices.next(copts.deviceLists, servername));
|
deferData();
|
||||||
|
pipeWs(servername, service, conn, Devices.next(state.deviceLists, servername), serviceport);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
copts.handleHttp(servername, conn);
|
deferData('handleHttp');
|
||||||
}
|
return;
|
||||||
else {
|
|
||||||
// redirect to https
|
|
||||||
copts.handleInsecureHttp(servername, conn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redirect to https
|
||||||
|
deferData('handleInsecureHttp');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
42
package.json
42
package.json
|
@ -1,60 +1,58 @@
|
||||||
{
|
{
|
||||||
"name": "stunneld",
|
"name": "telebit-relay",
|
||||||
"version": "0.10.0",
|
"version": "0.12.1",
|
||||||
"description": "A pure-JavaScript tunnel daemon for http and https similar to a localtunnel.me server, but uses TLS (SSL) with ServerName Indication (SNI) over https to work even in harsh network conditions such as in student dorms and behind HOAs, corporate firewalls, public libraries, airports, airplanes, etc. Can also tunnel tls and plain tcp.",
|
"description": "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": "wstunneld.js",
|
"main": "lib/relay.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jstunneld": "bin/stunneld.js",
|
"telebit-relay": "bin/telebit-relay.js"
|
||||||
"stunneld.js": "bin/stunneld.js",
|
|
||||||
"stunneld-js": "bin/stunneld.js"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://git.coolaj86.com/coolaj86/tunnel-server.js.git"
|
"url": "https://git.coolaj86.com/coolaj86/telebit-relay.js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"server",
|
|
||||||
"daemon",
|
|
||||||
"tcp",
|
|
||||||
"tls",
|
|
||||||
"http",
|
"http",
|
||||||
"https",
|
"https",
|
||||||
"sni",
|
"sni",
|
||||||
"servername",
|
"servername",
|
||||||
"indication",
|
|
||||||
"stunnel",
|
"stunnel",
|
||||||
"secure",
|
"secure",
|
||||||
"securetunnel",
|
"securetunnel",
|
||||||
"secure-tunnel",
|
|
||||||
"tunnel",
|
"tunnel",
|
||||||
"localtunnel",
|
"localtunnel",
|
||||||
"localtunnel.me",
|
"localtunnel.me",
|
||||||
"proxy",
|
"proxy",
|
||||||
"reverse",
|
"reverse",
|
||||||
"reverse-proxy",
|
|
||||||
"reverseproxy",
|
|
||||||
"vpn",
|
"vpn",
|
||||||
"sni"
|
"sni",
|
||||||
|
"ngrok"
|
||||||
],
|
],
|
||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
"license": "(MIT OR Apache-2.0)",
|
"license": "(MIT OR Apache-2.0)",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://git.coolaj86.com/coolaj86/tunnel-server.js/issues"
|
"url": "https://git.coolaj86.com/coolaj86/telebit-relay.js/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://git.coolaj86.com/coolaj86/tunnel-server.js",
|
"homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bluebird": "^3.5.1",
|
"bluebird": "^3.5.1",
|
||||||
"cluster-store": "^2.0.8",
|
"cluster-store": "^2.0.8",
|
||||||
"commander": "^2.15.1",
|
"finalhandler": "^1.1.1",
|
||||||
"greenlock": "^2.2.4",
|
"greenlock": "^2.2.4",
|
||||||
|
"human-readable-ids": "^1.0.4",
|
||||||
|
"js-yaml": "^3.11.0",
|
||||||
"jsonwebtoken": "^8.2.1",
|
"jsonwebtoken": "^8.2.1",
|
||||||
"localhost.daplie.me-certificates": "^1.3.5",
|
"proxy-packer": "^1.4.3",
|
||||||
|
"recase": "^1.0.4",
|
||||||
"redirect-https": "^1.1.5",
|
"redirect-https": "^1.1.5",
|
||||||
|
"serve-static": "^1.13.2",
|
||||||
"sni": "^1.0.0",
|
"sni": "^1.0.0",
|
||||||
"tunnel-packer": "^1.4.0",
|
|
||||||
"ws": "^5.1.1"
|
"ws": "^5.1.1"
|
||||||
|
},
|
||||||
|
"engineStrict" : true,
|
||||||
|
"engines": {
|
||||||
|
"node": "10.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
395
wstunneld.js
395
wstunneld.js
|
@ -1,395 +0,0 @@
|
||||||
'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…
Reference in New Issue