progress towards telebit

This commit is contained in:
AJ ONeal 2018-05-27 04:26:34 -06:00
parent ca2e825fe7
commit e8c580d115
7 changed files with 486 additions and 119 deletions

View File

@ -91,16 +91,10 @@ email: 'jon@example.com' # must be valid (for certificate recovery and
agree_tos: true # agree to the Telebit, Greenlock, and Let's Encrypt TOSes
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'))"
remote_options:
https_redirect: false # don't redirect http to https remotely
secret: '' # Secret with which to sign Tokens for authorization
token: '' # A signed Token for authorization
servernames: # servernames that will be forwarded here
- example.com
local_ports: # ports to forward
3000: 'http'
8443: 'https'
5050: true
```
<!--
@ -152,8 +146,8 @@ You can **integrate telebit.js into your existing codebase** or use the **standa
Telebit CLI
-----------
Installs as `stunnel.js` with the alias `jstunnel`
(for those that regularly use `stunnel` but still like commandline completion).
Installs Telebit Remote as `telebit`
(for those that regularly use `telebit` but still like commandline completion).
### Install
@ -162,44 +156,44 @@ npm install -g telebit
```
```bash
npm install -g 'git+https://git@git.coolaj86.com/coolaj86/tunnel-client.js.git#v1'
npm install -g 'https://git.coolaj86.com/coolaj86/telebit.js.git#v1'
```
Or if you want to bow down to the kings of the centralized dictator-net:
How to use `stunnel.js` with your own instance of `stunneld.js`:
How to use Telebit Remote with your own instance of Telebit Relay:
```bash
stunnel.js \
telebit \
--locals <<external domain name>> \
--stunneld wss://<<tunnel domain>>:<<tunnel port>> \
--relay wss://<<tunnel domain>>:<<tunnel port>> \
--secret <<128-bit hex key>>
```
```bash
stunnel.js --locals john.example.com --stunneld wss://tunnel.example.com:443 --secret abc123
telebit --locals john.example.com --relay wss://tunnel.example.com:443 --secret abc123
```
```bash
stunnel.js \
telebit \
--locals <<protocol>>:<<external domain name>>:<<local port>> \
--stunneld wss://<<tunnel domain>>:<<tunnel port>> \
--relay wss://<<tunnel domain>>:<<tunnel port>> \
--secret <<128-bit hex key>>
```
```bash
stunnel.js \
telebit \
--locals http:john.example.com:3000,https:john.example.com \
--stunneld wss://tunnel.example.com:443 \
--relay wss://tunnel.example.com:443 \
--secret abc123
```
```
--secret the same secret used by stunneld (used for authentication)
--secret the same secret used by the Telebit Relay (for authentication)
--locals comma separated list of <proto>:<servername>:<port> to which
incoming http and https should be forwarded
--stunneld the domain or ip address at which you are running stunneld.js
-k, --insecure ignore invalid ssl certificates from stunneld
--relay the domain or ip address at which you are running Telebit Relay
-k, --insecure ignore invalid ssl certificates from relay
```
Node.js Library
@ -208,10 +202,10 @@ Node.js Library
### Example
```javascript
var stunnel = require('stunnel');
var Telebit = require('telebit');
stunnel.connect({
stunneld: 'wss://tunnel.example.com'
Telebit.connect({
relay: 'wss://tunnel.example.com'
, token: '...'
, locals: [
// defaults to sending http to local port 80 and https to local port 443
@ -251,7 +245,7 @@ local handler and the tunnel handler.
You could do a little magic like this:
```js
stunnel.connect({
Telebit.connect({
// ...
, net: {
createConnection: function (info, cb) {
@ -286,6 +280,15 @@ stunnel.connect({
});
```
TODO
====
Install for user
* https://wiki.archlinux.org/index.php/Systemd/User
* https://developer.apple.com/library/content/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html
* `sudo launchctl load -w ~/Library/LaunchAgents/cloud.telebit.remote`
* https://serverfault.com/questions/194832/how-to-start-stop-restart-launchd-services-from-the-command-line
Browser Library
=======

View File

@ -4,9 +4,43 @@
var pkg = require('../package.json');
var program = require('commander');
var url = require('url');
var stunnel = require('../wsclient.js');
var remote = require('../wsclient.js');
var argv = process.argv.slice(2);
//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 --config <path>');
console.info('');
console.info('Example:');
console.info('');
console.info('\ttelebit --config /etc/telebit/telebit.yml');
console.info('');
console.info('Config:');
console.info('');
console.info('\tSee https://git.coolaj86.com/coolaj86/telebit.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();
}
var domainsMap = {};
var services = {};
@ -114,6 +148,78 @@ function collectProxies(val, memo) {
return memo;
}
function connectTunnel() {
var state = {};
var services = { https: {}, http: {}, tcp: {} };
state.net = {
createConnection: function (info, cb) {
// data is the hello packet / first chunk
// info = { data, servername, port, host, remoteFamily, remoteAddress, remotePort }
var net = require('net');
// socket = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] };
var socket = net.createConnection({ port: info.port, host: info.host }, cb);
return socket;
}
};
// Note: the remote needs to know:
// what servernames to forward
// what ports to forward
// what udp ports to forward
// redirect http to https automatically
// redirect www to nowww automatically
Object.keys(state.config.localPorts).forEach(function (port) {
var proto = state.config.localPorts[port];
if (!proto) { return; }
if ('http' === proto) {
state.config.servernames.forEach(function (servername) {
services.http[servername] = port;
});
return;
}
if ('https' === proto) {
state.config.servernames.forEach(function (servername) {
services.https[servername] = port;
});
return;
}
if (true === proto) { proto = 'tcp'; }
if ('tcp' !== proto) { throw new Error("unsupported protocol '" + proto + "'"); }
//services[proxy.protocol]['*'] = proxy.port;
//services[proxy.protocol][proxy.hostname] = proxy.port;
services[proto]['*'] = port;
});
Object.keys(program.services).forEach(function (protocol) {
var subServices = program.services[protocol];
Object.keys(subServices).forEach(function (hostname) {
console.info('[local proxy]', protocol + '://' + hostname + ' => ' + subServices[hostname]);
});
});
console.info('');
var tun = remote.connect({
relay: state.config.relay
, locals: state.config.servernames
, services: state.services
, net: state.net
, insecure: state.config.relay_ignore_invalid_certificates
, token: state.config.token
});
function sigHandler() {
console.log('SIGINT');
// We want to handle cleanup properly unless something is broken in our cleanup process
// that prevents us from exitting, in which case we want the user to be able to send
// the signal again and exit the way it normally would.
process.removeListener('SIGINT', sigHandler);
tun.end();
}
process.on('SIGINT', sigHandler);
}
var program = require('commander');
program
.version(pkg.version)
//.command('jsurl <url>')
@ -134,63 +240,22 @@ program
.parse(process.argv)
;
function connectTunnel() {
program.net = {
createConnection: function (info, cb) {
// data is the hello packet / first chunk
// info = { data, servername, port, host, remoteFamily, remoteAddress, remotePort }
var net = require('net');
// socket = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] };
var socket = net.createConnection({ port: info.port, host: info.host }, cb);
return socket;
}
};
Object.keys(program.services).forEach(function (protocol) {
var subServices = program.services[protocol];
Object.keys(subServices).forEach(function (hostname) {
console.info('[local proxy]', protocol + '://' + hostname + ' => ' + subServices[hostname]);
});
});
console.info('');
var tun = stunnel.connect({
stunneld: program.stunneld
, locals: program.locals
, services: program.services
, net: program.net
, insecure: program.insecure
, token: program.token
});
function sigHandler() {
console.log('SIGINT');
// We want to handle cleanup properly unless something is broken in our cleanup process
// that prevents us from exitting, in which case we want the user to be able to send
// the signal again and exit the way it normally would.
process.removeListener('SIGINT', sigHandler);
tun.end();
}
process.on('SIGINT', sigHandler);
}
function rawTunnel() {
program.stunneld = program.stunneld || 'wss://tunnel.daplie.com';
program.relay = program.relay || 'wss://telebit.cloud';
if (!(program.secret || program.token)) {
console.error("You must use --secret or --token with --stunneld");
console.error("You must use --secret or --token with --relay");
process.exit(1);
return;
}
var location = url.parse(program.stunneld);
var location = url.parse(program.relay);
if (!location.protocol || /\./.test(location.protocol)) {
program.stunneld = 'wss://' + program.stunneld;
location = url.parse(program.stunneld);
program.relay = 'wss://' + program.relay;
location = url.parse(program.relay);
}
var aud = location.hostname + (location.port ? ':' + location.port : '');
program.stunneld = location.protocol + '//' + aud;
program.relay = location.protocol + '//' + aud;
if (!program.token) {
var jwt = require('jsonwebtoken');
@ -205,41 +270,6 @@ function rawTunnel() {
connectTunnel();
}
function daplieTunnel() {
//var OAUTH3 = require('oauth3.js');
var Oauth3Cli = require('oauth3.js/bin/oauth3.js');
require('oauth3.js/oauth3.tunnel.js');
return Oauth3Cli.login({
email: program.email
, providerUri: program.oauth3Url || 'oauth3.org'
}).then(function (oauth3) {
var data = { device: null, domains: [] };
var domains = Object.keys(domainsMap).filter(Boolean);
if (program.device) {
// TODO use device API to select device by id
data.device = { hostname: program.device };
if (true === program.device) {
data.device.hostname = require('os').hostname();
console.log("Using device hostname '" + data.device.hostname + "'");
}
}
if (domains.length) {
data.domains = domains;
}
return oauth3.api('tunnel.token', { data: data }).then(function (results) {
var token = new Buffer(results.jwt.split('.')[1], 'base64').toString('utf8');
console.info('');
console.info('tunnel token issued:');
console.info(token);
console.info('');
program.token = results.jwt;
program.stunneld = results.tunnelUrl || ('wss://' + token.aud + '/');
connectTunnel();
});
});
}
program.locals = (program.locals || []).concat(program.domains || []);
program.locals.forEach(function (proxy) {
// Create a map from which we can derive a list of all domains we want forwarded to us.
@ -282,11 +312,6 @@ services.http['*'] = services.http['*'] || services.https['*'];
program.services = services;
if (!(program.secret || program.token) && !program.stunneld) {
daplieTunnel();
}
else {
rawTunnel();
}
}());

View File

@ -0,0 +1,66 @@
# Pre-req
# sudo adduser telebit --home /opt/telebit
# sudo mkdir -p /opt/telebit/
# sudo chown -R telebit:telebit /opt/telebit/
[Unit]
Description=Telebit Relay
Documentation=https://git.coolaj86.com/coolaj86/telebit.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-abnormal
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
# custom directory cannot be set and will be the place where gitea exists, not the working directory
ExecStart=/opt/telebit/bin/node /opt/telebit/bin/telebit.js --config /etc/telebit/telebit.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 /etc/telebit
# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories
; ReadWritePaths=/opt/telebit /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

16
examples/telebit.yml Normal file
View File

@ -0,0 +1,16 @@
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
relay: wss://telebit.cloud # Which Telebit Relay to use
secret: '' # Token or Secret to use for authorization
token: '' # Token or Secret to use for authorization
remote_options:
https_redirect: true # redirect http to https remotely (default)
servernames: # hostnames that direct to the Telebit Relay admin console
- example.com
- example.net
local_ports: # ports to forward
3000: 'http'
8443: 'https'
5050: true

8
examples/telebit.yml.tpl Normal file
View File

@ -0,0 +1,8 @@
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
remote_options:
https_redirect: true # redirect http to https remotely (default)
local_ports: # ports to forward
3001: 'http'
9443: 'https'

249
installer/get.sh Normal file
View File

@ -0,0 +1,249 @@
#!/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_relay=${2:-}
my_servernames=${3:-}
my_secret=${4:-}
my_user="telebit"
my_app="telebit"
my_bin="telebit.js"
my_name="Telebit Remote"
my_repo="telebit.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_relay}" ]; then
echo "What relay will you be using?"
echo ""
read -p "relay (ex: wss://telebit.cloud): " my_relay
echo ""
# UX - just want a smooth transition
sleep 0.5
fi
if [ -z "${my_servernames}" ]; then
echo "What servername(s) will you be relaying here?"
echo ""
read -p "domain (ex: example.com,example.net): " my_servernames
echo ""
# UX - just want a smooth transition
sleep 0.5
fi
if [ -z "${my_secret}" ]; then
echo "What's your authorization for the relay server?"
echo ""
read -p "auth: " my_secret
echo ""
# UX - just want a smooth transition
sleep 0.5
fi
echo ""
if [ -z "${TELEBIT_PATH:-}" ]; then
echo 'TELEBIT_PATH="'${TELEBIT_PATH:-}'"'
TELEBIT_PATH=/opt/$my_app
fi
echo "Installing $my_name to '$TELEBIT_PATH'"
echo "Installing node.js dependencies into $TELEBIT_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_PATH/lib/node_modules"
export NPM_CONFIG_PREFIX="$TELEBIT_PATH"
export PATH="$TELEBIT_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_PATH/bin/node"
my_secret=$($my_node -e "console.info(crypto.randomBytes(16).toString('hex'))")
my_npm="$my_node $TELEBIT_PATH/bin/npm"
my_tmp="$TELEBIT_PATH/tmp"
mkdir -p $my_tmp
echo "sudo mkdir -p '$TELEBIT_PATH'"
sudo mkdir -p "$TELEBIT_PATH"
echo "sudo mkdir -p '/etc/$my_user/'"
sudo mkdir -p "/etc/$my_user/"
set +e
#https://git.coolaj86.com/coolaj86/telebit.js.git
#https://git.coolaj86.com/coolaj86/telebit.js/archive/:tree:.tar.gz
#https://git.coolaj86.com/coolaj86/telebit.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_PATH/ > /dev/null 2>&1
cp -ar $TELEBIT_PATH/$my_repo/* $TELEBIT_PATH/ > /dev/null
rm -rf $TELEBIT_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_PATH/
else
echo "Neither tar nor unzip found. Abort."
exit 13
fi
set -e
pushd $TELEBIT_PATH >/dev/null
$my_npm install >/dev/null 2>/dev/null
popd >/dev/null
cat << EOF > $TELEBIT_PATH/bin/$my_app
#!/bin/bash
$my_node $TELEBIT_PATH/bin/$my_bin
EOF
chmod a+x $TELEBIT_PATH/bin/$my_app
echo "sudo ln -sf $TELEBIT_PATH/bin/$my_app /usr/local/bin/$my_app"
sudo ln -sf $TELEBIT_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_PATH/bin/node"
sudo setcap cap_net_bind_service=+ep $TELEBIT_PATH/bin/node
fi
set -e
if [ -z "$(cat /etc/passwd | grep $my_user)" ]; then
echo "sudo adduser --home $TELEBIT_PATH --gecos '' --disabled-password $my_user"
sudo adduser --home $TELEBIT_PATH --gecos '' --disabled-password $my_user >/dev/null 2>&1
fi
if [ ! -f "/etc/$my_user/$my_app.yml" ]; then
echo "### Creating config file from template. sudo may be required"
#echo "sudo rsync -a examples/$my_app.yml /etc/$my_user/$my_app.yml"
sudo bash -c "echo 'email: $my_email' >> /etc/$my_user/$my_app.yml"
sudo bash -c "echo 'secret: $my_secret' >> /etc/$my_user/$my_app.yml"
sudo bash -c "echo 'servernames: [ $my_servernames ]' >> /etc/$my_user/$my_app.yml"
sudo bash -c "cat examples/$my_app.yml.tpl >> /etc/$my_user/$my_app.yml"
fi
echo "sudo chown -R $my_user '$TELEBIT_PATH' '/etc/$my_user'"
sudo chown -R $my_user "$TELEBIT_PATH" "/etc/$my_user"
echo "### Adding $my_app is a system service"
echo "sudo rsync -a $TELEBIT_PATH/dist/etc/systemd/system/$my_app.service /etc/systemd/system/$my_app.service"
sudo rsync -a $TELEBIT_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 /etc/$my_user/$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 /etc/$my_user/$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 /etc/$my_user/$my_app.yml"
echo ""
sleep 1

View File

@ -415,7 +415,7 @@ function run(copts) {
}
, onOpen: function () {
console.info("[open] connected to '" + copts.stunneld + "'");
console.info("[open] connected to '" + copts.relay + "'");
wsHandlers.refreshTimeout();
timeoutId = setTimeout(wsHandlers.checkTimeout, activityTimeout);
@ -500,8 +500,8 @@ function run(copts) {
timeoutId = null;
var machine = require('tunnel-packer').create(packerHandlers);
console.info("[connect] '" + copts.stunneld + "'");
var tunnelUrl = copts.stunneld.replace(/\/$/, '') + '/?access_token=' + tokens[0];
console.info("[connect] '" + copts.relay + "'");
var tunnelUrl = copts.relay.replace(/\/$/, '') + '/?access_token=' + tokens[0];
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !copts.insecure });
wstunneler.on('open', wsHandlers.onOpen);
wstunneler.on('close', wsHandlers.onClose);