MAJOR: Updates for Authenticated Web UI and CLI #30
|
@ -1,6 +1,8 @@
|
|||
# Telebit™ Remote
|
||||
|
||||
Because friends don't let friends localhost™
|
||||
The T-Rex Long-Arm of the Internet
|
||||
|
||||
<small>because friends don't let friends localhost</small>
|
||||
|
||||
| Sponsored by [ppl](https://ppl.family)
|
||||
| **Telebit Remote**
|
||||
|
@ -524,7 +526,7 @@ rm -rf ~/.config/telebit ~/.local/share/telebit
|
|||
Browser Library
|
||||
=======
|
||||
|
||||
This is implemented with websockets, so you should be able to
|
||||
This is implemented with websockets, so browser compatibility is a hopeful future outcome. Would love help.
|
||||
|
||||
LICENSE
|
||||
=======
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -21,6 +21,7 @@ if ('rsync' === process.argv[2]) {
|
|||
require('sclient/bin/sclient.js');
|
||||
return;
|
||||
}
|
||||
// handle ssh client rather than ssh https tunnel
|
||||
if ('ssh' === process.argv[2] && /[\w-]+\.[a-z]{2,}/i.test(process.argv[3])) {
|
||||
process.argv.splice(1,1,'sclient');
|
||||
process.argv.splice(2,1,'ssh');
|
||||
|
|
985
bin/telebitd.js
985
bin/telebitd.js
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Telebit Documentation</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="v-app">
|
||||
<h1>Telebit (Remote) Documentation</h1>
|
||||
|
||||
<section>
|
||||
<h2>GET /api/config</h2>
|
||||
<pre><code>{{ config }}</code></pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>GET /api/status</h2>
|
||||
<pre><code>{{ status }}</code></pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>POST /api/init</h2>
|
||||
<form v-on:submit.stop.prevent="initialize">
|
||||
|
||||
<label for="-email">Email:</label>
|
||||
<input id="-email" v-model="init.email" type="text" placeholder="john@example.com">
|
||||
<br>
|
||||
|
||||
<label for="-teletos"><input id="-teletos" v-model="init.teletos" type="checkbox">
|
||||
Accept Telebit Terms of Service</label>
|
||||
<br>
|
||||
|
||||
<label for="-letos"><input id="-letos" v-model="init.letos" type="checkbox">
|
||||
Accept Let's Encrypt Terms of Service</label>
|
||||
<br>
|
||||
|
||||
</form>
|
||||
<pre><code>{{ init }}</code></pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>POST /api/http</h2>
|
||||
<pre><code>{{ http }}</code></pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>POST /api/tcp</h2>
|
||||
<pre><code>{{ tcp }}</code></pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>POST /api/ssh</h2>
|
||||
<pre><code>{{ ssh }}</code></pre>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/vue.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,235 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Telebit Setup</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>document.body.hidden = true;</script>
|
||||
|
||||
<div class="v-app">
|
||||
<h1>Telebit (Remote) Setup v{{ config.version }}</h1>
|
||||
|
||||
<section v-if="views.flash.error">
|
||||
{{ views.flash.error }}
|
||||
</section>
|
||||
|
||||
<section v-if="views.section.loading">
|
||||
Loading...
|
||||
</section>
|
||||
|
||||
<section v-if="views.section.setup">
|
||||
<h2>Create Account</h2>
|
||||
<form v-on:submit.stop.prevent="initialize">
|
||||
|
||||
<label for="-email">Email:</label>
|
||||
<input id="-email" v-model="init.email" type="text" placeholder="john@example.com" required>
|
||||
<br>
|
||||
|
||||
<label for="-teletos"><input id="-teletos" v-model="init.teletos" type="checkbox" required>
|
||||
Accept Telebit Terms of Service</label>
|
||||
<br>
|
||||
|
||||
<label for="-letos"><input id="-letos" v-model="init.letos" type="checkbox" required>
|
||||
Accept Let's Encrypt Terms of Service</label>
|
||||
<br>
|
||||
|
||||
<label for="-notifications">Notification Preferences</label>
|
||||
<select id="-notifications" v-model="init.notifications">
|
||||
<option value="newsletter">Occassional Newsletter</option>
|
||||
<option value="important" default><strong>Important Messages Only</strong></option>
|
||||
<option value="required">Required Only</option>
|
||||
</select>
|
||||
|
||||
<small>
|
||||
<p v-if="'newsletter' == init.notifications">
|
||||
You'll receive a friendly note now and then in addition to the important updates.
|
||||
</p>
|
||||
<p v-if="'important' == init.notifications">
|
||||
You'll only receive updates that we believe will be of the most value to you, and the required updates.
|
||||
</p>
|
||||
<p v-if="'required' == init.notifications">
|
||||
You'll only receive security updates, transactional and account-related messages, and legal notices.
|
||||
</p>
|
||||
</small>
|
||||
|
||||
<details><summary><small>Advanced</small></summary>
|
||||
|
||||
<label for="-relay">Relay:</label>
|
||||
<input id="-relay" v-model="init.relay" type="text" placeholder="telebit.cloud">
|
||||
<br>
|
||||
<button type="button" v-on:click="defaultRelay">Use Default</button>
|
||||
<button type="button" v-on:click="betaRelay">Use Beta</button>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<label for="-acme-server">ACME (Let's Encrypt) Server:</label>
|
||||
<input id="-acme-server" v-model="init.acmeServer" type="text" placeholder="https://acme-v02.api.letsencrypt.org/directory">
|
||||
<br>
|
||||
<button type="button" v-on:click="productionAcme">Use Production</button>
|
||||
<button type="button" v-on:click="stagingAcme">Use Staging</button>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
</details>
|
||||
|
||||
<button type="submit">Accept & Continue</button>
|
||||
|
||||
</form>
|
||||
<pre><code>{{ init }}</code></pre>
|
||||
</section>
|
||||
|
||||
<section v-if="views.section.advanced">
|
||||
<h2>Advanced Setup for {{ init.relay }}</h2>
|
||||
<form v-on:submit.stop.prevent="advance">
|
||||
|
||||
<strong><label for="-secret">Relay Shared Secret:</label></strong>
|
||||
<input id="-secret" v-model="init.secret" type="text" placeholder="ex: xxxxxxxxxxxx">
|
||||
<br>
|
||||
|
||||
<strong><label for="-domains">Domains:</label></strong>
|
||||
<br>
|
||||
<small>(comma separated list of domains to use for http, tls, https, etc)</small>
|
||||
<br>
|
||||
<input id="-domains" v-model="init.domains" type="text" placeholder="ex: whatever.com, example.com">
|
||||
<br>
|
||||
|
||||
<strong><label for="-ports">TCP Ports:</label></strong>
|
||||
<br>
|
||||
<small>(comman separated list of ports, excluding 80 and 443, typically port over 1024)</small>
|
||||
<br>
|
||||
<input id="-ports" v-model="init.ports" type="text" placeholder="ex: 5050, 3000, 8080">
|
||||
<br>
|
||||
|
||||
<label for="-telemetry"><input id="-telemetry" v-model="init.telemetry" type="checkbox">
|
||||
Contribute to Telebit by sharing telemetry</label>
|
||||
<br>
|
||||
|
||||
<button type="submit">Finish</button>
|
||||
|
||||
</form>
|
||||
<pre><code>{{ init }}</code></pre>
|
||||
</section>
|
||||
|
||||
<section v-if="views.section.otp">
|
||||
<pre><code><h2>{{ init.otp }}</h2></code></pre>
|
||||
</section>
|
||||
|
||||
<section v-if="views.section.status">
|
||||
http://localhost:{{ status.port }}
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<section v-if="views.section.status_chooser">
|
||||
<button v-on:click.prevent.stop="changeState('status/share')">Share Files & Folders</button>
|
||||
<button v-on:click.prevent.stop="changeState('status/host')">Host a Website or Webapp</button>
|
||||
<button v-on:click.prevent.stop="changeState('status/access')">Remote Access via SSH</button>
|
||||
</section>
|
||||
|
||||
<section v-if="views.section.status_access">
|
||||
SSH:
|
||||
<span v-if="status.ssh">{{ status.ssh }}
|
||||
<button v-on:click="ssh(-1)">Disable SSH</button></span>
|
||||
<span v-if="!status.ssh"><input type="text" v-model="state.ssh" placeholder="22">
|
||||
<button v-on:click="ssh(state.ssh)">Enable SSH</button></span>
|
||||
<br>
|
||||
<br>
|
||||
<div v-if="state.ssh_active">SSH is currently running</div>
|
||||
<div v-if="!state.ssh_active">SSH is not currently running</div>
|
||||
<br>
|
||||
<div v-if="state.ssh_insecure">Password Authentication is NOT disabled.
|
||||
Please consider updating your <code>sshd_config</code> and restarting ssh.
|
||||
<pre><code>{{ status }}</code></pre>
|
||||
</div>
|
||||
<div v-if="!state.ssh_insecure">Key-Only Authentication is enabled :)</div>
|
||||
<br>
|
||||
<div class="alert alert-info">
|
||||
<strong>Important:</strong> Accessing this device with other SSH clients:
|
||||
<br>
|
||||
In order to use your other ssh clients with telebit you will need to put them into
|
||||
<strong>ssh+https mode</strong>.
|
||||
|
||||
We recommend downloading <code><a href="https://telebit.cloud/sclient/" target="_blank">sclient</a></code>
|
||||
to do so, because it makes it as simple as adding <code>-o ProxyCommand="sclient %h"</code> to your
|
||||
ssh command to enable ssh+https:
|
||||
<pre><code>ssh -o ProxyCommand="sclient %h" {{ newHttp.name }}</code></pre>
|
||||
<br>
|
||||
However, most clients can also use <code>openssl s_client</code>, which does the same thing, but is
|
||||
more difficult to remember:
|
||||
<pre><code>proxy_cmd='openssl s_client -connect %h:443 -servername %h -quiet'
|
||||
ssh -o ProxyCommand="$proxy_cmd" hot-skunk-45.telebit.io</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="views.section.status_share">
|
||||
Path Hosting:
|
||||
<ul>
|
||||
<li v-for="domain in status.pathHosting">
|
||||
<form v-on:submit.prevent.stop="changePathHost(domain, domain.path)">
|
||||
{{ domain.name }}
|
||||
<input type="text" v-model="domain.path" v-bind:placeholder="domain.handler">
|
||||
<button type="submit"
|
||||
v-if="domain.handler == domain.path">Save</button>
|
||||
<button type="button" v-on:click="deletePathHost(domain)">X</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
<form v-on:submit.prevent.stop="createShare(newHttp.sub, newHttp.name, newHttp.handler)">
|
||||
<input v-model="newHttp.sub" type="text" placeholder="subdomain (ex: pub)">
|
||||
<select v-model="newHttp.name">
|
||||
<option v-for="w in status.wildDomains" v-bind:value="w.name">{{ w.name }}</option>
|
||||
</select>
|
||||
<input v-model="newHttp.handler" type="text" placeholder="path (ex: ~/Public)" required>
|
||||
<button>Add</button>
|
||||
</form>
|
||||
<br>
|
||||
</section>
|
||||
|
||||
<section v-if="views.section.status_host">
|
||||
Port Forwarding:
|
||||
<ul>
|
||||
<li v-for="domain in status.portForwards">
|
||||
<form v-on:submit.prevent.stop="changePortForward(domain, domain._port)">
|
||||
{{ domain.name }}
|
||||
<input type="text" v-model="domain._port" v-bind:placeholder="domain.handler">
|
||||
<button type="submit"
|
||||
v-if="domain.handler == domain._port">Save</button>
|
||||
<button type="button" v-on:click="deletePortForward(domain)">X</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
<form v-on:submit="createHost(newHttp.sub, newHttp.name, newHttp.handler)">
|
||||
<input v-model="newHttp.sub" type="text" placeholder="subdomain (ex: api)">
|
||||
<select v-model="newHttp.name">
|
||||
<option v-for="w in status.wildDomains" v-bind:value="w.name">{{ w.name }}</option>
|
||||
</select>
|
||||
<input v-model="newHttp.handler" type="number" placeholder="port (ex: 3000)" required>
|
||||
<button>Add</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<br>
|
||||
Uptime: {{ statusUptime }}
|
||||
<br>
|
||||
Runtime: {{ statusRuntime }}
|
||||
<br>
|
||||
Reconnects: {{ status.reconnects }}
|
||||
|
||||
<details><summary><small>Advanced</small></summary>
|
||||
<button v-if="!status.enabled" v-on:click="enable">Enable Traffic</button>
|
||||
<button v-if="status.enabled" v-on:click="disable">Disable Traffic</button>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<pre><code>{{ status }}</code></pre>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/vue.js"></script>
|
||||
<script src="/js/bluecrypt-acme.js"></script>
|
||||
<script src="/js/telebit.js"></script>
|
||||
<script src="/js/telebit-token.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,608 @@
|
|||
;(function () {
|
||||
'use strict';
|
||||
|
||||
var Vue = window.Vue;
|
||||
var Telebit = window.TELEBIT;
|
||||
var Keypairs = window.Keypairs;
|
||||
var ACME = window.ACME;
|
||||
var api = {};
|
||||
|
||||
/*
|
||||
function safeFetch(url, opts) {
|
||||
var controller = new AbortController();
|
||||
var tok = setTimeout(function () {
|
||||
controller.abort();
|
||||
}, 4000);
|
||||
if (!opts) {
|
||||
opts = {};
|
||||
}
|
||||
opts.signal = controller.signal;
|
||||
return window.fetch(url, opts).finally(function () {
|
||||
clearTimeout(tok);
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
api.config = function apiConfig() {
|
||||
return Telebit.reqLocalAsync({
|
||||
method: "GET"
|
||||
, url: "/api/config"
|
||||
, key: api._key
|
||||
}).then(function (resp) {
|
||||
var json = resp.body;
|
||||
appData.config = json;
|
||||
return json;
|
||||
});
|
||||
};
|
||||
api.status = function apiStatus() {
|
||||
return Telebit.reqLocalAsync({
|
||||
method: "GET"
|
||||
, url: "/api/status"
|
||||
, key: api._key
|
||||
}).then(function (resp) {
|
||||
var json = resp.body;
|
||||
return json;
|
||||
});
|
||||
};
|
||||
api.http = function apiHttp(o) {
|
||||
var opts = {
|
||||
method: "POST"
|
||||
, url: "/api/http"
|
||||
, headers: { 'Content-Type': 'application/json' }
|
||||
, json: { name: o.name, handler: o.handler, indexes: o.indexes }
|
||||
, key: api._key
|
||||
};
|
||||
return Telebit.reqLocalAsync(opts).then(function (resp) {
|
||||
var json = resp.body;
|
||||
appData.initResult = json;
|
||||
return json;
|
||||
}).catch(function (err) {
|
||||
window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2)));
|
||||
});
|
||||
};
|
||||
api.ssh = function apiSsh(port) {
|
||||
var opts = {
|
||||
method: "POST"
|
||||
, url: "/api/ssh"
|
||||
, headers: { 'Content-Type': 'application/json' }
|
||||
, json: { port: port }
|
||||
, key: api._key
|
||||
};
|
||||
return Telebit.reqLocalAsync(opts).then(function (resp) {
|
||||
var json = resp.body;
|
||||
appData.initResult = json;
|
||||
return json;
|
||||
}).catch(function (err) {
|
||||
window.alert("Error: [init] " + (err.message || JSON.stringify(err, null, 2)));
|
||||
});
|
||||
};
|
||||
api.enable = function apiEnable() {
|
||||
var opts = {
|
||||
method: "POST"
|
||||
, url: "/api/enable"
|
||||
//, headers: { 'Content-Type': 'application/json' }
|
||||
, key: api._key
|
||||
};
|
||||
return Telebit.reqLocalAsync(opts).then(function (resp) {
|
||||
var json = resp.body;
|
||||
console.log('enable', json);
|
||||
return json;
|
||||
}).catch(function (err) {
|
||||
window.alert("Error: [enable] " + (err.message || JSON.stringify(err, null, 2)));
|
||||
});
|
||||
};
|
||||
api.disable = function apiDisable() {
|
||||
var opts = {
|
||||
method: "POST"
|
||||
, url: "/api/disable"
|
||||
//, headers: { 'Content-Type': 'application/json' }
|
||||
, key: api._key
|
||||
};
|
||||
return Telebit.reqLocalAsync(opts).then(function (resp) {
|
||||
var json = resp.body;
|
||||
console.log('disable', json);
|
||||
return json;
|
||||
}).catch(function (err) {
|
||||
window.alert("Error: [disable] " + (err.message || JSON.stringify(err, null, 2)));
|
||||
});
|
||||
};
|
||||
|
||||
function showOtp(otp, pollUrl) {
|
||||
localStorage.setItem('poll_url', pollUrl);
|
||||
telebitState.pollUrl = pollUrl;
|
||||
appData.init.otp = otp;
|
||||
changeState('otp');
|
||||
}
|
||||
function doConfigure() {
|
||||
if (telebitState.dir.pair_request) {
|
||||
telebitState._can_pair = true;
|
||||
}
|
||||
|
||||
//
|
||||
// Read config from form
|
||||
//
|
||||
|
||||
// Create Empty Config, If Necessary
|
||||
if (!telebitState.config) { telebitState.config = {}; }
|
||||
if (!telebitState.config.greenlock) { telebitState.config.greenlock = {}; }
|
||||
|
||||
// Populate Config
|
||||
if (appData.init.teletos && appData.init.letos) { telebitState.config.agreeTos = true; }
|
||||
if (appData.init.relay) { telebitState.config.relay = appData.init.relay; }
|
||||
if (appData.init.email) { telebitState.config.email = appData.init.email; }
|
||||
if ('undefined' !== typeof appData.init.letos) { telebitState.config.greenlock.agree = appData.init.letos; }
|
||||
if ('newsletter' === appData.init.notifications) {
|
||||
telebitState.config.newsletter = true; telebitState.config.communityMember = true;
|
||||
}
|
||||
if ('important' === appData.init.notifications) { telebitState.config.communityMember = true; }
|
||||
if (appData.init.acmeVersion) { telebitState.config.greenlock.version = appData.init.acmeVersion; }
|
||||
if (appData.init.acmeServer) { telebitState.config.greenlock.server = appData.init.acmeServer; }
|
||||
|
||||
// Temporary State
|
||||
telebitState._otp = Telebit.otp();
|
||||
appData.init.otp = telebitState._otp;
|
||||
|
||||
return Telebit.authorize(telebitState, showOtp).then(function () {
|
||||
return changeState('status');
|
||||
});
|
||||
}
|
||||
|
||||
// TODO test for internet connectivity (and telebit connectivity)
|
||||
var DEFAULT_RELAY = 'telebit.cloud';
|
||||
var BETA_RELAY = 'telebit.ppl.family';
|
||||
var TELEBIT_RELAYS = [
|
||||
DEFAULT_RELAY
|
||||
, BETA_RELAY
|
||||
];
|
||||
var PRODUCTION_ACME = 'https://acme-v02.api.letsencrypt.org/directory';
|
||||
var STAGING_ACME = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
||||
var appData = {
|
||||
config: {}
|
||||
, status: {}
|
||||
, init: {
|
||||
teletos: true
|
||||
, letos: true
|
||||
, notifications: "important"
|
||||
, relay: DEFAULT_RELAY
|
||||
, telemetry: true
|
||||
, acmeServer: PRODUCTION_ACME
|
||||
}
|
||||
, state: {}
|
||||
, views: {
|
||||
flash: {
|
||||
error: ""
|
||||
}
|
||||
, section: {
|
||||
loading: true
|
||||
, setup: false
|
||||
, advanced: false
|
||||
, otp: false
|
||||
, status: false
|
||||
}
|
||||
}
|
||||
, newHttp: {}
|
||||
};
|
||||
var telebitState = {};
|
||||
var appMethods = {
|
||||
initialize: function () {
|
||||
console.log("call initialize");
|
||||
return requestAccountHelper().then(function (/*key*/) {
|
||||
if (!appData.init.relay) {
|
||||
appData.init.relay = DEFAULT_RELAY;
|
||||
}
|
||||
appData.init.relay = appData.init.relay.toLowerCase();
|
||||
telebitState = { relay: appData.init.relay };
|
||||
|
||||
return Telebit.api.directory(telebitState).then(function (dir) {
|
||||
if (!dir.api_host) {
|
||||
window.alert("Error: '" + telebitState.relay + "' does not appear to be a valid telebit service");
|
||||
return;
|
||||
}
|
||||
|
||||
telebitState.dir = dir;
|
||||
|
||||
// If it's one of the well-known relays
|
||||
if (-1 !== TELEBIT_RELAYS.indexOf(appData.init.relay)) {
|
||||
return doConfigure();
|
||||
} else {
|
||||
changeState('advanced');
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.error(err);
|
||||
window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2)));
|
||||
});
|
||||
});
|
||||
}
|
||||
, advance: function () {
|
||||
return doConfigure();
|
||||
}
|
||||
, productionAcme: function () {
|
||||
console.log("prod acme:");
|
||||
appData.init.acmeServer = PRODUCTION_ACME;
|
||||
console.log(appData.init.acmeServer);
|
||||
}
|
||||
, stagingAcme: function () {
|
||||
console.log("staging acme:");
|
||||
appData.init.acmeServer = STAGING_ACME;
|
||||
console.log(appData.init.acmeServer);
|
||||
}
|
||||
, defaultRelay: function () {
|
||||
appData.init.relay = DEFAULT_RELAY;
|
||||
}
|
||||
, betaRelay: function () {
|
||||
appData.init.relay = BETA_RELAY;
|
||||
}
|
||||
, enable: function () {
|
||||
api.enable();
|
||||
}
|
||||
, disable: function () {
|
||||
api.disable();
|
||||
}
|
||||
, ssh: function (port) {
|
||||
// -1 to disable
|
||||
// 0 is auto (22)
|
||||
// 1-65536
|
||||
api.ssh(port || 22);
|
||||
}
|
||||
, createShare: function (sub, domain, handler) {
|
||||
if (sub) {
|
||||
domain = sub + '.' + domain;
|
||||
}
|
||||
api.http({ name: domain, handler: handler, indexes: true });
|
||||
appData.newHttp = {};
|
||||
}
|
||||
, createHost: function (sub, domain, handler) {
|
||||
if (sub) {
|
||||
domain = sub + '.' + domain;
|
||||
}
|
||||
api.http({ name: domain, handler: handler, 'x-forwarded-for': name });
|
||||
appData.newHttp = {};
|
||||
}
|
||||
, changePortForward: function (domain, port) {
|
||||
api.http({ name: domain.name, handler: port });
|
||||
}
|
||||
, deletePortForward: function (domain) {
|
||||
api.http({ name: domain.name, handler: 'none' });
|
||||
}
|
||||
, changePathHost: function (domain, path) {
|
||||
api.http({ name: domain.name, handler: path });
|
||||
}
|
||||
, deletePathHost: function (domain) {
|
||||
api.http({ name: domain.name, handler: 'none' });
|
||||
}
|
||||
, changeState: changeState
|
||||
};
|
||||
var appStates = {
|
||||
setup: function () {
|
||||
appData.views.section = { setup: true };
|
||||
}
|
||||
, advanced: function () {
|
||||
appData.views.section = { advanced: true };
|
||||
}
|
||||
, otp: function () {
|
||||
appData.views.section = { otp: true };
|
||||
}
|
||||
, status: function () {
|
||||
function exitState() {
|
||||
clearInterval(tok);
|
||||
}
|
||||
|
||||
var tok = setInterval(updateStatus, 2000);
|
||||
|
||||
return updateStatus().then(function () {
|
||||
appData.views.section = { status: true, status_chooser: true };
|
||||
return exitState;
|
||||
});
|
||||
}
|
||||
};
|
||||
appStates.status.share = function () {
|
||||
function exitState() {
|
||||
clearInterval(tok);
|
||||
}
|
||||
|
||||
var tok = setInterval(updateStatus, 2000);
|
||||
|
||||
appData.views.section = { status: true, status_share: true };
|
||||
return updateStatus().then(function () {
|
||||
return exitState;
|
||||
});
|
||||
};
|
||||
appStates.status.host = function () {
|
||||
function exitState() {
|
||||
clearInterval(tok);
|
||||
}
|
||||
|
||||
var tok = setInterval(updateStatus, 2000);
|
||||
|
||||
appData.views.section = { status: true, status_host: true };
|
||||
return updateStatus().then(function () {
|
||||
return exitState;
|
||||
});
|
||||
};
|
||||
appStates.status.access = function () {
|
||||
function exitState() {
|
||||
clearInterval(tok);
|
||||
}
|
||||
|
||||
var tok = setInterval(updateStatus, 2000);
|
||||
|
||||
appData.views.section = { status: true, status_access: true };
|
||||
return updateStatus().then(function () {
|
||||
return exitState;
|
||||
});
|
||||
};
|
||||
|
||||
function updateStatus() {
|
||||
return api.status().then(function (status) {
|
||||
if (status.error) {
|
||||
appData.views.flash.error = status.error.message || JSON.stringify(status.error, null, 2);
|
||||
}
|
||||
var wilddomains = [];
|
||||
var rootdomains = [];
|
||||
var subdomains = [];
|
||||
var directories = [];
|
||||
var portforwards = [];
|
||||
var free = [];
|
||||
appData.status = status;
|
||||
if ('maybe' === status.ssh_requests_password) {
|
||||
appData.status.ssh_active = false;
|
||||
} else {
|
||||
appData.status.ssh_active = true;
|
||||
if ('yes' === status.ssh_requests_password) {
|
||||
appData.status.ssh_insecure = true;
|
||||
}
|
||||
}
|
||||
if ('yes' === status.ssh_password_authentication) {
|
||||
appData.status.ssh_insecure = true;
|
||||
}
|
||||
if ('yes' === status.ssh_permit_root_login) {
|
||||
appData.status.ssh_insecure = true;
|
||||
}
|
||||
|
||||
// only update what's changed
|
||||
if (appData.state.ssh !== appData.status.ssh) {
|
||||
appData.state.ssh = appData.status.ssh;
|
||||
}
|
||||
if (appData.state.ssh_insecure !== appData.status.ssh_insecure) {
|
||||
appData.state.ssh_insecure = appData.status.ssh_insecure;
|
||||
}
|
||||
if (appData.state.ssh_active !== appData.status.ssh_active) {
|
||||
appData.state.ssh_active = appData.status.ssh_active;
|
||||
}
|
||||
Object.keys(appData.status.servernames).forEach(function (k) {
|
||||
var s = appData.status.servernames[k];
|
||||
s.name = k;
|
||||
if (s.wildcard) { wilddomains.push(s); }
|
||||
if (!s.sub && !s.wildcard) { rootdomains.push(s); }
|
||||
if (s.sub) { subdomains.push(s); }
|
||||
if (s.handler) {
|
||||
if (s.handler.toString() === parseInt(s.handler, 10).toString()) {
|
||||
s._port = s.handler;
|
||||
portforwards.push(s);
|
||||
} else {
|
||||
s.path = s.handler;
|
||||
directories.push(s);
|
||||
}
|
||||
} else {
|
||||
free.push(s);
|
||||
}
|
||||
});
|
||||
appData.status.portForwards = portforwards;
|
||||
appData.status.pathHosting = directories;
|
||||
appData.status.wildDomains = wilddomains;
|
||||
appData.newHttp.name = (appData.status.wildDomains[0] || {}).name;
|
||||
appData.state.ssh = (appData.status.ssh > 0) && appData.status.ssh || undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function changeState(newstate) {
|
||||
var newhash = '#/' + newstate + '/';
|
||||
if (location.hash === newhash) {
|
||||
if (!telebitState.firstState) {
|
||||
telebitState.firstState = true;
|
||||
setState();
|
||||
}
|
||||
}
|
||||
location.hash = newhash;
|
||||
}
|
||||
/*globals Promise*/
|
||||
window.addEventListener('hashchange', setState, false);
|
||||
function setState(/*ev*/) {
|
||||
//ev.oldURL
|
||||
//ev.newURL
|
||||
if (appData.exit) {
|
||||
console.log('previous state exiting');
|
||||
appData.exit.then(function (exit) {
|
||||
if ('function' === typeof exit) {
|
||||
exit();
|
||||
}
|
||||
});
|
||||
}
|
||||
var parts = location.hash.substr(1).replace(/^\//, '').replace(/\/$/, '').split('/').filter(Boolean);
|
||||
var fn = appStates;
|
||||
parts.forEach(function (s) {
|
||||
console.log("state:", s);
|
||||
fn = fn[s];
|
||||
});
|
||||
appData.exit = Promise.resolve(fn());
|
||||
//appMethods.states[newstate]();
|
||||
}
|
||||
|
||||
function msToHumanReadable(ms) {
|
||||
var uptime = ms;
|
||||
var uptimed = uptime / 1000;
|
||||
var minute = 60;
|
||||
var hour = 60 * minute;
|
||||
var day = 24 * hour;
|
||||
var days = 0;
|
||||
var times = [];
|
||||
while (uptimed > day) {
|
||||
uptimed -= day;
|
||||
days += 1;
|
||||
}
|
||||
times.push(days + " days ");
|
||||
var hours = 0;
|
||||
while (uptimed > hour) {
|
||||
uptimed -= hour;
|
||||
hours += 1;
|
||||
}
|
||||
times.push(hours.toString().padStart(2, "0") + " h ");
|
||||
var minutes = 0;
|
||||
while (uptimed > minute) {
|
||||
uptimed -= minute;
|
||||
minutes += 1;
|
||||
}
|
||||
times.push(minutes.toString().padStart(2, "0") + " m ");
|
||||
var seconds = Math.round(uptimed);
|
||||
times.push(seconds.toString().padStart(2, "0") + " s ");
|
||||
return times.join('');
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: ".v-app"
|
||||
, data: appData
|
||||
, computed: {
|
||||
statusProctime: function () {
|
||||
return msToHumanReadable(this.status.proctime);
|
||||
}
|
||||
, statusRuntime: function () {
|
||||
return msToHumanReadable(this.status.runtime);
|
||||
}
|
||||
, statusUptime: function () {
|
||||
return msToHumanReadable(this.status.uptime);
|
||||
}
|
||||
}
|
||||
, methods: appMethods
|
||||
});
|
||||
|
||||
function requestAccountHelper() {
|
||||
function reset() {
|
||||
changeState('setup');
|
||||
setState();
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
appData.init.email = localStorage.getItem('email');
|
||||
if (!appData.init.email) {
|
||||
// don't resolve
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
return requestAccount(appData.init.email).then(function (key) {
|
||||
if (!key) { throw new Error("[SANITY] Error: completed without key"); }
|
||||
resolve(key);
|
||||
}).catch(function (err) {
|
||||
appData.init.email = "";
|
||||
localStorage.removeItem('email');
|
||||
console.error(err);
|
||||
window.alert("something went wrong");
|
||||
// don't resolve
|
||||
reset();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function run() {
|
||||
return requestAccountHelper().then(function (key) {
|
||||
api._key = key;
|
||||
// TODO create session instance of Telebit
|
||||
Telebit._key = key;
|
||||
// 😁 1. Get ACME directory
|
||||
// 😁 2. Fetch ACME account
|
||||
// 3. Test if account has access
|
||||
// 4. Show command line auth instructions to auth
|
||||
// 😁 5. Sign requests / use JWT
|
||||
// 😁 6. Enforce token required for config, status, etc
|
||||
// 7. Move admin interface to standard ports (admin.foo-bar-123.telebit.xyz)
|
||||
api.config().then(function (config) {
|
||||
telebitState.config = config;
|
||||
if (config.greenlock) {
|
||||
appData.init.acmeServer = config.greenlock.server;
|
||||
}
|
||||
if (config.relay) {
|
||||
appData.init.relay = config.relay;
|
||||
}
|
||||
if (config.email) {
|
||||
appData.init.email = config.email;
|
||||
}
|
||||
if (config.agreeTos) {
|
||||
appData.init.letos = config.agreeTos;
|
||||
appData.init.teletos = config.agreeTos;
|
||||
}
|
||||
if (config._otp) {
|
||||
appData.init.otp = config._otp;
|
||||
}
|
||||
|
||||
telebitState.pollUrl = config._pollUrl || localStorage.getItem('poll_url');
|
||||
|
||||
if ((!config.token && !config._otp) || !config.relay || !config.email || !config.agreeTos) {
|
||||
changeState('setup');
|
||||
setState();
|
||||
return;
|
||||
}
|
||||
if (!config.token && config._otp) {
|
||||
changeState('otp');
|
||||
setState();
|
||||
// this will skip ahead as necessary
|
||||
return Telebit.authorize(telebitState, showOtp).then(function () {
|
||||
return changeState('status');
|
||||
});
|
||||
}
|
||||
|
||||
// TODO handle default state
|
||||
changeState('status');
|
||||
}).catch(function (err) {
|
||||
appData.views.flash.error = err.message || JSON.stringify(err, null, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// TODO protect key with passphrase (or QR code?)
|
||||
function getKey() {
|
||||
var jwk;
|
||||
try {
|
||||
jwk = JSON.parse(localStorage.getItem('key'));
|
||||
} catch(e) {
|
||||
// ignore
|
||||
}
|
||||
if (jwk && jwk.kid && jwk.d) {
|
||||
return Promise.resolve(jwk);
|
||||
}
|
||||
return Keypairs.generate().then(function (pair) {
|
||||
jwk = pair.private;
|
||||
localStorage.setItem('key', JSON.stringify(jwk));
|
||||
return jwk;
|
||||
});
|
||||
}
|
||||
|
||||
function requestAccount(email) {
|
||||
return getKey().then(function (jwk) {
|
||||
// creates new or returns existing
|
||||
var acme = ACME.create({});
|
||||
var url = window.location.protocol + '//' + window.location.host + '/acme/directory';
|
||||
return acme.init(url).then(function () {
|
||||
return acme.accounts.create({
|
||||
agreeToTerms: function (tos) { return tos; }
|
||||
, accountKeypair: { privateKeyJwk: jwk }
|
||||
, email: email
|
||||
}).then(function (account) {
|
||||
console.log('account:');
|
||||
console.log(account);
|
||||
if (account.id) {
|
||||
localStorage.setItem('email', email);
|
||||
}
|
||||
return jwk;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.api = api;
|
||||
run();
|
||||
setTimeout(function () {
|
||||
document.body.hidden = false;
|
||||
}, 50);
|
||||
|
||||
// Debug
|
||||
window.changeState = changeState;
|
||||
}());
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,116 @@
|
|||
;(function (exports) {
|
||||
'use strict';
|
||||
|
||||
/* global Promise */
|
||||
var PromiseA;
|
||||
if ('undefined' !== typeof Promise) {
|
||||
PromiseA = Promise;
|
||||
} else {
|
||||
throw new Error("no Promise implementation defined");
|
||||
}
|
||||
|
||||
var common = exports.TELEBIT || require('./lib/common.js');
|
||||
|
||||
common.authorize = common.getToken = function getToken(state, showOtp) {
|
||||
state.relay = state.config.relay;
|
||||
|
||||
// { _otp, config: {} }
|
||||
return common.api.token(state, {
|
||||
error: function (err) { console.error("[Error] common.api.token handlers.error: \n", err); return PromiseA.reject(err); }
|
||||
, directory: function (dir) {
|
||||
/*console.log('[directory] Telebit Relay Discovered:', dir);*/
|
||||
state._apiDirectory = dir;
|
||||
return PromiseA.resolve();
|
||||
}
|
||||
, tunnelUrl: function (tunnelUrl) {
|
||||
//console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl);
|
||||
state.wss = tunnelUrl;
|
||||
return PromiseA.resolve();
|
||||
}
|
||||
, requested: function (authReq, pollUrl) {
|
||||
console.log("[requested] Pairing Requested");
|
||||
state._otp = state._otp = authReq.otp;
|
||||
|
||||
if (!state.config.token && state._can_pair) {
|
||||
console.info("0000".replace(/0000/g, state._otp));
|
||||
showOtp(authReq.otp, pollUrl);
|
||||
}
|
||||
|
||||
return PromiseA.resolve();
|
||||
}
|
||||
, connect: function (pretoken) {
|
||||
console.log("[connect] Enabling Pairing Locally...");
|
||||
state.config.pretoken = pretoken;
|
||||
state._connecting = true;
|
||||
|
||||
// This will only be saved to the session
|
||||
state.config._otp = state._otp;
|
||||
return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config, json: true }).then(function () {
|
||||
console.info("waiting...");
|
||||
return PromiseA.resolve();
|
||||
}).catch(function (err) {
|
||||
state._error = err;
|
||||
console.error("Error while initializing config [connect]:");
|
||||
console.error(err);
|
||||
return PromiseA.reject(err);
|
||||
});
|
||||
}
|
||||
, offer: function (token) {
|
||||
//console.log("[offer] Pairing Enabled by Relay");
|
||||
state.token = token;
|
||||
state.config.token = token;
|
||||
if (state._error) {
|
||||
return;
|
||||
}
|
||||
state._connecting = true;
|
||||
try {
|
||||
//require('jsonwebtoken').decode(token);
|
||||
token = token.split('.');
|
||||
token[0] = token[0].replace(/_/g, '/').replace(/-/g, '+');
|
||||
while (token[0].length % 4) { token[0] += '='; }
|
||||
btoa(token[0]);
|
||||
token[1] = token[1].replace(/_/g, '/').replace(/-/g, '+');
|
||||
while (token[1].length % 4) { token[1] += '='; }
|
||||
btoa(token[1]);
|
||||
//console.log(require('jsonwebtoken').decode(token));
|
||||
} catch(e) {
|
||||
console.warn("[warning] could not decode token");
|
||||
}
|
||||
return common.reqLocalAsync({ url: '/api/config', method: 'POST', body: state.config, json: true }).then(function () {
|
||||
//console.log("Pairing Enabled Locally");
|
||||
return PromiseA.resolve();
|
||||
}).catch(function (err) {
|
||||
state._error = err;
|
||||
console.error("Error while initializing config [offer]:");
|
||||
console.error(err);
|
||||
return PromiseA.reject(err);
|
||||
});
|
||||
}
|
||||
, granted: function (/*_*/) { /*console.log("[grant] Pairing complete!");*/ return PromiseA.resolve(); }
|
||||
, end: function () {
|
||||
return common.reqLocalAsync({ url: '/api/enable', method: 'POST', body: [], json: true }).then(function () {
|
||||
console.info("Success");
|
||||
|
||||
// workaround for https://github.com/nodejs/node/issues/21319
|
||||
if (state._useTty) {
|
||||
setTimeout(function () {
|
||||
console.info("Some fun things to try first:\n");
|
||||
console.info(" ~/telebit http ~/public");
|
||||
console.info(" ~/telebit tcp 5050");
|
||||
console.info(" ~/telebit ssh auto");
|
||||
console.info();
|
||||
console.info("Press any key to continue...");
|
||||
console.info();
|
||||
process.exit(0);
|
||||
}, 0.5 * 1000);
|
||||
return;
|
||||
}
|
||||
// end workaround
|
||||
|
||||
//parseCli(state);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
}('undefined' === typeof module ? window : module.exports));
|
|
@ -0,0 +1,365 @@
|
|||
;(function (exports) {
|
||||
'use strict';
|
||||
|
||||
var Keypairs = window.Keypairs;
|
||||
var common = exports.TELEBIT = {};
|
||||
common.debug = true;
|
||||
|
||||
/* global Promise */
|
||||
var PromiseA;
|
||||
if ('undefined' !== typeof Promise) {
|
||||
PromiseA = Promise;
|
||||
} else {
|
||||
throw new Error("no Promise implementation defined");
|
||||
}
|
||||
|
||||
/*globals AbortController*/
|
||||
if ('undefined' !== typeof fetch) {
|
||||
common._requestAsync = function (opts) {
|
||||
// funnel requests through the local server
|
||||
// (avoid CORS, for now)
|
||||
var relayOpts = {
|
||||
url: '/api/relay'
|
||||
, method: 'POST'
|
||||
, headers: {
|
||||
'Content-Type': 'application/json'
|
||||
, 'Accepts': 'application/json'
|
||||
}
|
||||
, body: JSON.stringify(opts)
|
||||
};
|
||||
var controller = new AbortController();
|
||||
var tok = setTimeout(function () {
|
||||
controller.abort();
|
||||
}, 4000);
|
||||
if (!relayOpts) {
|
||||
relayOpts = {};
|
||||
}
|
||||
relayOpts.signal = controller.signal;
|
||||
return window.fetch(relayOpts.url, relayOpts).then(function (resp) {
|
||||
clearTimeout(tok);
|
||||
return resp.json().then(function (json) {
|
||||
if (json.error) {
|
||||
return PromiseA.reject(new Error(json.error && json.error.message || JSON.stringify(json.error)));
|
||||
}
|
||||
return json;
|
||||
});
|
||||
});
|
||||
};
|
||||
common._reqLocalAsync = function (opts) {
|
||||
if (!opts) { opts = {}; }
|
||||
if (opts.json && true !== opts.json) {
|
||||
opts.body = opts.json;
|
||||
opts.json = true;
|
||||
}
|
||||
if (opts.json) {
|
||||
if (!opts.headers) { opts.headers = {}; }
|
||||
if (opts.body) {
|
||||
opts.headers['Content-Type'] = 'application/json';
|
||||
if ('string' !== typeof opts.body) {
|
||||
opts.body = JSON.stringify(opts.body);
|
||||
}
|
||||
} else {
|
||||
opts.headers.Accepts = 'application/json';
|
||||
}
|
||||
}
|
||||
var controller = new AbortController();
|
||||
var tok = setTimeout(function () {
|
||||
controller.abort();
|
||||
}, 4000);
|
||||
opts.signal = controller.signal;
|
||||
return window.fetch(opts.url, opts).then(function (resp) {
|
||||
clearTimeout(tok);
|
||||
return resp.json().then(function (json) {
|
||||
var headers = {};
|
||||
resp.headers.forEach(function (k, v) {
|
||||
headers[k] = v;
|
||||
});
|
||||
return { statusCode: resp.status, headers: headers, body: json };
|
||||
});
|
||||
});
|
||||
};
|
||||
} else {
|
||||
common._requestAsync = require('util').promisify(require('@root/request'));
|
||||
common._reqLocalAsync = require('util').promisify(require('@root/request'));
|
||||
}
|
||||
common._sign = function (opts) {
|
||||
var p;
|
||||
if ('POST' === opts.method || opts.json) {
|
||||
p = Keypairs.signJws({ jwk: opts.key || common._key, payload: opts.json || {} }).then(function (jws) {
|
||||
opts.json = jws;
|
||||
});
|
||||
} else {
|
||||
p = Keypairs.signJwt({ jwk: opts.key || common._key, claims: { iss: false, exp: '60s' } }).then(function (jwt) {
|
||||
if (!opts.headers) { opts.headers = {}; }
|
||||
opts.headers.Authorization = 'Bearer ' + jwt;
|
||||
});
|
||||
}
|
||||
return p.then(function () {
|
||||
return opts;
|
||||
});
|
||||
};
|
||||
common.requestAsync = function (opts) {
|
||||
return common._sign(opts).then(function (opts) {
|
||||
return common._requestAsync(opts);
|
||||
});
|
||||
};
|
||||
common.reqLocalAsync = function (opts) {
|
||||
return common._sign(opts).then(function (opts) {
|
||||
return common._reqLocalAsync(opts);
|
||||
});
|
||||
};
|
||||
|
||||
common.parseUrl = function (hostname) {
|
||||
// add scheme, if missing
|
||||
if (!/:\/\//.test(hostname)) {
|
||||
hostname = 'https://' + hostname;
|
||||
}
|
||||
var location = new URL(hostname);
|
||||
hostname = location.hostname + (location.port ? ':' + location.port : '');
|
||||
hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
|
||||
return hostname;
|
||||
};
|
||||
common.parseHostname = function (hostname) {
|
||||
var location = {};
|
||||
try {
|
||||
location = new URL(hostname);
|
||||
} catch(e) {
|
||||
// ignore
|
||||
}
|
||||
if (!location.protocol || /\./.test(location.protocol)) {
|
||||
hostname = 'https://' + hostname;
|
||||
location = new URL(hostname);
|
||||
}
|
||||
//hostname = location.hostname + (location.port ? ':' + location.port : '');
|
||||
//hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
|
||||
return location.hostname;
|
||||
};
|
||||
|
||||
common.apiDirectory = '_apis/telebit.cloud/index.json';
|
||||
|
||||
common.otp = function getOtp() {
|
||||
return Math.round(Math.random() * 9999).toString().padStart(4, '0');
|
||||
};
|
||||
common.signToken = function (state) {
|
||||
var JWT = require('./jwt.js');
|
||||
var tokenData = {
|
||||
domains: Object.keys(state.config.servernames || {}).filter(function (name) {
|
||||
return /\./.test(name);
|
||||
})
|
||||
, ports: Object.keys(state.config.ports || {}).filter(function (port) {
|
||||
port = parseInt(port, 10);
|
||||
return port > 0 && port <= 65535;
|
||||
})
|
||||
, aud: state._relayUrl
|
||||
, iss: Math.round(Date.now() / 1000)
|
||||
};
|
||||
|
||||
return JWT.sign(tokenData, state.config.secret);
|
||||
};
|
||||
common.promiseTimeout = function (ms) {
|
||||
var tok;
|
||||
var p = new PromiseA(function (resolve) {
|
||||
tok = setTimeout(function () {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
p.cancel = function () {
|
||||
clearTimeout(tok);
|
||||
};
|
||||
return p;
|
||||
};
|
||||
common.api = {};
|
||||
common.api.directory = function (state) {
|
||||
console.log('[DEBUG] state:');
|
||||
console.log(state);
|
||||
state._relayUrl = common.parseUrl(state.relay);
|
||||
if (!state._relays) { state._relays = {}; }
|
||||
if (state._relays[state._relayUrl]) {
|
||||
return PromiseA.resolve(state._relays[state._relayUrl]);
|
||||
}
|
||||
return common.requestAsync({ url: state._relayUrl + common.apiDirectory, json: true }).then(function (resp) {
|
||||
var dir = resp.body;
|
||||
state._relays[state._relayUrl] = dir;
|
||||
return dir;
|
||||
});
|
||||
};
|
||||
common.api._parseWss = function (state, dir) {
|
||||
if (!dir || !dir.api_host) {
|
||||
dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } };
|
||||
}
|
||||
state._relayHostname = common.parseHostname(state.relay);
|
||||
return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname;
|
||||
};
|
||||
common.api.wss = function (state) {
|
||||
return common.api.directory(state).then(function (dir) {
|
||||
return common.api._parseWss(state, dir);
|
||||
});
|
||||
};
|
||||
common.api.token = function (state, handlers) {
|
||||
|
||||
var firstReady = true;
|
||||
function pollStatus(req) {
|
||||
if (common.debug) { console.log('[debug] pollStatus called'); }
|
||||
if (common.debug) { console.log(req); }
|
||||
return common.requestAsync(req).then(function checkLocation(resp) {
|
||||
var body = resp.body;
|
||||
if (common.debug) { console.log('[debug] checkLocation'); }
|
||||
if (common.debug) { console.log(body); }
|
||||
// pending, try again
|
||||
if ('pending' === body.status && resp.headers.location) {
|
||||
if (common.debug) { console.log('[debug] pending'); }
|
||||
return common.promiseTimeout(2 * 1000).then(function () {
|
||||
return pollStatus({ url: resp.headers.location, json: true });
|
||||
});
|
||||
} else if ('ready' === body.status) {
|
||||
if (common.debug) { console.log('[debug] ready'); }
|
||||
if (firstReady) {
|
||||
if (common.debug) { console.log('[debug] first ready'); }
|
||||
firstReady = false;
|
||||
// falls through on purpose
|
||||
PromiseA.resolve(handlers.offer(body.access_token)).then(function () {
|
||||
/*ignore*/
|
||||
});
|
||||
}
|
||||
return common.promiseTimeout(2 * 1000).then(function () {
|
||||
return pollStatus(req);
|
||||
});
|
||||
} else if ('complete' === body.status) {
|
||||
if (common.debug) { console.log('[debug] complete'); }
|
||||
return PromiseA.resolve(handlers.granted(null)).then(function () {
|
||||
return PromiseA.resolve(handlers.end(null)).then(function () {});
|
||||
});
|
||||
} else {
|
||||
if (common.debug) { console.log('[debug] bad status'); }
|
||||
var err = new Error("Bad State:" + body.status);
|
||||
err._request = req;
|
||||
return PromiseA.reject(err);
|
||||
}
|
||||
}).catch(function (err) {
|
||||
if (common.debug) { console.log('[debug] pollStatus error'); }
|
||||
err._request = req;
|
||||
err._hint = '[telebitd.js] pair request';
|
||||
return PromiseA.resolve(handlers.error(err)).then(function () {});
|
||||
});
|
||||
}
|
||||
|
||||
// directory, requested, connect, tunnelUrl, offer, granted, end
|
||||
function requestAuth(dir) {
|
||||
if (common.debug) { console.log('[debug] after dir'); }
|
||||
state.wss = common.api._parseWss(state, dir);
|
||||
|
||||
return PromiseA.resolve(handlers.tunnelUrl(state.wss)).then(function () {
|
||||
if (common.debug) { console.log('[debug] after tunnelUrl'); }
|
||||
if (state.config.secret /* && !state.config.token */) {
|
||||
// TODO make token here in the browser
|
||||
//state.config._token = common.signToken(state);
|
||||
}
|
||||
state.token = state.token || state.config.token || state.config._token;
|
||||
if (state.token) {
|
||||
if (common.debug) { console.log('[debug] token via token or secret'); }
|
||||
// { token, pretoken }
|
||||
return PromiseA.resolve(handlers.connect(state.token)).then(function () {
|
||||
return PromiseA.resolve(handlers.end(null));
|
||||
});
|
||||
}
|
||||
|
||||
if (!dir.pair_request) {
|
||||
if (common.debug) { console.log('[debug] no dir, connect'); }
|
||||
return PromiseA.resolve(handlers.error(new Error("No token found or generated, and no pair_request api found.")));
|
||||
}
|
||||
|
||||
// TODO sign token with own private key, including public key and thumbprint
|
||||
// (much like ACME JOSE account)
|
||||
// TODO handle agree
|
||||
var otp = state._otp; // common.otp();
|
||||
var authReq = {
|
||||
subject: state.config.email
|
||||
, subject_scheme: 'mailto'
|
||||
// TODO create domains list earlier
|
||||
, scope: (state.config._servernames || Object.keys(state.config.servernames || {}))
|
||||
.concat(state.config._ports || Object.keys(state.config.ports || {})).join(',')
|
||||
, otp: otp
|
||||
// TODO make call to daemon for this info beforehand
|
||||
/*
|
||||
, hostname: os.hostname()
|
||||
// Used for User-Agent
|
||||
, os_type: os.type()
|
||||
, os_platform: os.platform()
|
||||
, os_release: os.release()
|
||||
, os_arch: os.arch()
|
||||
*/
|
||||
};
|
||||
var pairRequestUrl = new URL(dir.pair_request.pathname, 'https://' + dir.api_host.replace(/:hostname/g, state._relayHostname));
|
||||
console.log('pairRequestUrl:', pairRequestUrl);
|
||||
//console.log('pairRequestUrl:', JSON.stringify(pairRequestUrl.toJSON()));
|
||||
var req = {
|
||||
// WHATWG URL defines .toJSON() but, of course, it's not implemented
|
||||
// because... why would we implement JavaScript objects in the DOM
|
||||
// when we can have perfectly incompatible non-JS objects?
|
||||
url: {
|
||||
host: pairRequestUrl.host
|
||||
, hostname: pairRequestUrl.hostname
|
||||
, href: pairRequestUrl.href
|
||||
, pathname: pairRequestUrl.pathname
|
||||
// because why wouldn't node require 'path' on a json object and accept 'pathname' on a URL object...
|
||||
// https://twitter.com/coolaj86/status/1053947919890403328
|
||||
, path: pairRequestUrl.pathname
|
||||
, port: pairRequestUrl.port || null
|
||||
, protocol: pairRequestUrl.protocol
|
||||
, search: pairRequestUrl.search || null
|
||||
}
|
||||
, method: dir.pair_request.method
|
||||
, json: authReq
|
||||
};
|
||||
|
||||
return common.requestAsync(req).then(function doFirst(resp) {
|
||||
var body = resp.body;
|
||||
if (common.debug) { console.log('[debug] first req'); }
|
||||
if (!body.access_token && !body.jwt) {
|
||||
return PromiseA.reject(new Error("something wrong with pre-authorization request"));
|
||||
}
|
||||
return PromiseA.resolve(handlers.requested(authReq, resp.headers.location)).then(function () {
|
||||
return PromiseA.resolve(handlers.connect(body.access_token || body.jwt)).then(function () {
|
||||
var err;
|
||||
if (!resp.headers.location) {
|
||||
err = new Error("bad authentication request response");
|
||||
err._resp = resp.toJSON && resp.toJSON();
|
||||
return PromiseA.resolve(handlers.error(err)).then(function () {});
|
||||
}
|
||||
return common.promiseTimeout(2 * 1000).then(function () {
|
||||
return pollStatus({ url: resp.headers.location, json: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (common.debug) { console.log('[debug] gotoFirst error'); }
|
||||
err._request = req;
|
||||
err._hint = '[telebitd.js] pair request';
|
||||
return PromiseA.resolve(handlers.error(err)).then(function () {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (state.pollUrl) {
|
||||
return pollStatus({ url: state.pollUrl, json: true });
|
||||
}
|
||||
|
||||
// backwards compat (TODO verify we can remove this)
|
||||
var failoverDir = '{ "api_host": ":hostname", "tunnel": { "method": "wss", "pathname": "" } }';
|
||||
return common.api.directory(state).then(function (dir) {
|
||||
console.log('[debug] [directory]', dir);
|
||||
if (!dir.api_host) { dir = JSON.parse(failoverDir); }
|
||||
return dir;
|
||||
}).catch(function (err) {
|
||||
console.warn('[warn] [directory] fetch fail, using failover');
|
||||
console.warn(err);
|
||||
return JSON.parse(failoverDir);
|
||||
}).then(function (dir) {
|
||||
return PromiseA.resolve(handlers.directory(dir)).then(function () {
|
||||
console.log('[debug] [directory]', dir);
|
||||
return requestAuth(dir);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
}('undefined' !== typeof module ? module.exports : window));
|
File diff suppressed because it is too large
Load Diff
|
@ -9,7 +9,7 @@ var fs = require('fs');
|
|||
var mkdirp = require('mkdirp');
|
||||
var os = require('os');
|
||||
var homedir = os.homedir();
|
||||
var urequest = require('@coolaj86/urequest');
|
||||
var urequest = require('@root/request');
|
||||
|
||||
common._NOTIFICATIONS = {
|
||||
'newsletter': [ 'newsletter', 'communityMember' ]
|
||||
|
@ -118,7 +118,7 @@ common.otp = function getOtp() {
|
|||
return Math.round(Math.random() * 9999).toString().padStart(4, '0');
|
||||
};
|
||||
common.signToken = function (state) {
|
||||
var jwt = require('jsonwebtoken');
|
||||
var JWT = require('./jwt.js');
|
||||
var tokenData = {
|
||||
domains: Object.keys(state.config.servernames || {}).filter(function (name) {
|
||||
return /\./.test(name);
|
||||
|
@ -131,7 +131,7 @@ common.signToken = function (state) {
|
|||
, iss: Math.round(Date.now() / 1000)
|
||||
};
|
||||
|
||||
return jwt.sign(tokenData, state.config.secret);
|
||||
return JWT.sign(tokenData, state.config.secret);
|
||||
};
|
||||
common.api = {};
|
||||
common.api.directory = function (state, next) {
|
||||
|
|
|
@ -28,6 +28,7 @@ function TelebitRemote(state) {
|
|||
EventEmitter.call(this);
|
||||
var me = this;
|
||||
var priv = {};
|
||||
var path = require('path');
|
||||
|
||||
//var defaultHttpTimeout = (2 * 60);
|
||||
//var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000;
|
||||
|
@ -39,8 +40,9 @@ function TelebitRemote(state) {
|
|||
priv.tokens = [];
|
||||
var auth;
|
||||
if(!state.sortingHat) {
|
||||
state.sortingHat = "./sorting-hat.js";
|
||||
state.sortingHat = path.join(__dirname, '../sorting-hat.js');
|
||||
}
|
||||
state._connectionHandler = require(state.sortingHat);
|
||||
if (state.token) {
|
||||
if ('undefined' === state.token) {
|
||||
throw new Error("passed string 'undefined' as token");
|
||||
|
@ -349,7 +351,7 @@ function TelebitRemote(state) {
|
|||
|
||||
// TODO use readable streams instead
|
||||
wstunneler._socket.pause();
|
||||
require(state.sortingHat).assign(state, tun, function (err, conn) {
|
||||
state._connectionHandler.assign(state, tun, function (err, conn) {
|
||||
if (err) {
|
||||
err.message = err.message.replace(/:tun_id/, tun._id);
|
||||
packerHandlers._onConnectError(cid, tun, err);
|
||||
|
@ -472,12 +474,12 @@ function TelebitRemote(state) {
|
|||
priv.timeoutId = null;
|
||||
var machine = Packer.create(packerHandlers);
|
||||
|
||||
console.info("[telebit:lib/remote.js] [connect] '" + (state.wss || state.relay) + "'");
|
||||
console.info("[telebit:lib/daemon.js] [connect] '" + (state.wss || state.relay) + "'");
|
||||
var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth;
|
||||
wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure });
|
||||
// XXXXXX
|
||||
wstunneler.on('open', function () {
|
||||
console.info("[telebit:lib/remote.js] [open] connected to '" + (state.wss || state.relay) + "'");
|
||||
console.info("[telebit:lib/daemon.js] [open] connected to '" + (state.wss || state.relay) + "'");
|
||||
me.emit('connect');
|
||||
priv.refreshTimeout();
|
||||
priv.timeoutId = setTimeout(priv.checkTimeout, activityTimeout);
|
|
@ -0,0 +1,120 @@
|
|||
'use strict';
|
||||
|
||||
function eggSend(obj) {
|
||||
/*jslint validthis: true*/
|
||||
var me = this;
|
||||
if (!me.getHeader('content-type')) {
|
||||
me.setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
me.end(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
module.exports = function eggspress() {
|
||||
//var patternsMap = {};
|
||||
var allPatterns = [];
|
||||
var app = function (req, res) {
|
||||
var patterns = allPatterns.slice(0).reverse();
|
||||
function next(err) {
|
||||
if (err) {
|
||||
req.end(err.message);
|
||||
return;
|
||||
}
|
||||
var todo = patterns.pop();
|
||||
if (!todo) {
|
||||
console.log('[eggspress] Did not match any patterns', req.url);
|
||||
require('finalhandler')(req, res)();
|
||||
return;
|
||||
}
|
||||
|
||||
// '', GET, POST, DELETE
|
||||
if (todo[2] && req.method.toLowerCase() !== todo[2]) {
|
||||
//console.log("[eggspress] HTTP method doesn't match", req.url);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
var urlstr = (req.url.replace(/\/$/, '') + '/');
|
||||
var match = urlstr.match(todo[0]);
|
||||
if (!match) {
|
||||
//console.log("[eggspress] pattern doesn't match", todo[0], req.url);
|
||||
next();
|
||||
return;
|
||||
} else if ('string' === typeof todo[0] && 0 !== match.index) {
|
||||
//console.log("[eggspress] string pattern is not the start", todo[0], req.url);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
function fail(e) {
|
||||
console.error("[eggspress] error", todo[2], todo[0], req.url);
|
||||
console.error(e);
|
||||
// TODO make a nice error message
|
||||
res.end(e.message);
|
||||
}
|
||||
|
||||
console.log("[eggspress] matched pattern", todo[0], req.url);
|
||||
if ('function' === typeof todo[1]) {
|
||||
// TODO this is prep-work
|
||||
todo[1] = [todo[1]];
|
||||
}
|
||||
|
||||
var fns = todo[1].slice(0);
|
||||
req.params = match.slice(1);
|
||||
|
||||
function nextTodo(err) {
|
||||
if (err) { fail(err); return; }
|
||||
var fn = fns.shift();
|
||||
if (!fn) { next(err); return; }
|
||||
try {
|
||||
var p = fn(req, res, nextTodo);
|
||||
if (p && p.catch) {
|
||||
p.catch(fail);
|
||||
}
|
||||
} catch(e) {
|
||||
fail(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
nextTodo();
|
||||
}
|
||||
|
||||
res.send = eggSend;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
app.use = function (pattern) {
|
||||
var fns = Array.prototype.slice.call(arguments, 1);
|
||||
return app._use('', pattern, fns);
|
||||
};
|
||||
[ 'HEAD', 'GET', 'POST', 'DELETE' ].forEach(function (method) {
|
||||
app[method.toLowerCase()] = function (pattern) {
|
||||
var fns = Array.prototype.slice.call(arguments, 1);
|
||||
return app._use(method, pattern, fns);
|
||||
};
|
||||
});
|
||||
|
||||
app.post = function (pattern) {
|
||||
var fns = Array.prototype.slice.call(arguments, 1);
|
||||
return app._use('POST', pattern, fns);
|
||||
};
|
||||
app._use = function (method, pattern, fns) {
|
||||
// always end in a slash, for now
|
||||
if ('string' === typeof pattern) {
|
||||
pattern = pattern.replace(/\/$/, '') + '/';
|
||||
}
|
||||
/*
|
||||
if (!patternsMap[pattern]) {
|
||||
patternsMap[pattern] = [];
|
||||
}
|
||||
patternsMap[pattern].push(fn);
|
||||
patterns = Object.keys(patternsMap).sort(function (a, b) {
|
||||
return b.length - a.length;
|
||||
});
|
||||
*/
|
||||
allPatterns.push([pattern, fns, method.toLowerCase()]);
|
||||
return app;
|
||||
};
|
||||
|
||||
return app;
|
||||
};
|
|
@ -476,5 +476,13 @@ By using Telebit you agree to:
|
|||
Enter your email to agree and login/create your account:
|
||||
"
|
||||
|
||||
fail_relay_check = "===================
|
||||
WARNING
|
||||
===================
|
||||
|
||||
[{{status_code}}] '{{url}}'
|
||||
This server does not describe a current telebit version (but it may still work).
|
||||
"
|
||||
|
||||
[daemon]
|
||||
version = "telebit daemon v{version}"
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
var crypto = require('crypto');
|
||||
var FAT = require('jsonwebtoken');
|
||||
var JWT = require('./jwt.js');
|
||||
|
||||
var key = "justanothersecretsecret";
|
||||
var keyid = crypto.createHash('sha256').update(key).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
|
||||
var tok1 = FAT.sign({ foo: "hello" }, key, { keyid: keyid });
|
||||
var tok2 = JWT.sign({ foo: "hello" }, key);
|
||||
|
||||
if (tok1 !== tok2) {
|
||||
console.error(JWT.decode(tok1));
|
||||
console.error(JWT.decode(tok2));
|
||||
throw new Error("our jwt doesn't match auth0/jsonwebtoken");
|
||||
}
|
||||
|
||||
console.info('Pass');
|
|
@ -0,0 +1,43 @@
|
|||
'use strict';
|
||||
|
||||
var crypto = require('crypto');
|
||||
var JWT = module.exports;
|
||||
|
||||
JWT.decode = function (jwt) {
|
||||
var parts;
|
||||
|
||||
try {
|
||||
parts = jwt.split('.');
|
||||
return {
|
||||
header: JSON.parse(Buffer.from(parts[0], 'base64'))
|
||||
, payload: JSON.parse(Buffer.from(parts[1], 'base64'))
|
||||
, signature: parts[2] //Buffer.from(parts[2], 'base64')
|
||||
};
|
||||
} catch(e) {
|
||||
throw new Error("JWT Parse Error: could not split, base64 decode, and JSON.parse token " + jwt);
|
||||
}
|
||||
};
|
||||
|
||||
JWT.verify = function (jwt) {
|
||||
var decoded = JWT.decode(jwt);
|
||||
throw new Error("not implemented yet");
|
||||
};
|
||||
|
||||
function base64ToUrlSafe(str) {
|
||||
return str
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '')
|
||||
;
|
||||
}
|
||||
|
||||
JWT.sign = function (claims, key) {
|
||||
if (!claims.iat && false !== claims.iat) {
|
||||
claims.iat = Math.round(Date.now()/1000);
|
||||
}
|
||||
var thumb = base64ToUrlSafe(crypto.createHash('sha256').update(key).digest('base64'));
|
||||
var protect = base64ToUrlSafe(Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid: thumb })).toString('base64'));
|
||||
var payload = base64ToUrlSafe(Buffer.from(JSON.stringify(claims)).toString('base64'));
|
||||
var signature = base64ToUrlSafe(crypto.createHmac('sha256', key).update(protect + '.' + payload).digest('base64'));
|
||||
return protect + '.' + payload + '.' + signature;
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
'use strict';
|
||||
/*global Promise*/
|
||||
|
||||
var fs = require('fs').promises;
|
||||
var path = require('path');
|
||||
|
||||
module.exports.create = function (opts) {
|
||||
var keyext = '.key';
|
||||
return {
|
||||
getPassword: function (service, name) {
|
||||
var f = path.join(opts.configDir, name + keyext);
|
||||
return fs.readFile(f, 'utf8').catch(function (err) {
|
||||
if ('ENOEXIST' === err.code) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
, setPassword: function (service, name, key) {
|
||||
var f = path.join(opts.configDir, name + keyext);
|
||||
return fs.writeFile(f, key, 'utf8');
|
||||
}
|
||||
, deletePassword: function (service, name) {
|
||||
var f = path.join(opts.configDir, name + keyext);
|
||||
return fs.unlink(f);
|
||||
}
|
||||
, findCredentials: function (/*service*/) {
|
||||
return fs.readdir(opts.configDir).then(function (nodes) {
|
||||
return Promise.all(nodes.filter(function (node) {
|
||||
return keyext === node.slice(-4);
|
||||
}).map(function (node) {
|
||||
return fs.readFile(path.join(opts.configDir, node), 'utf8').then(function (data) {
|
||||
return { password: data };
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
, insecure: true
|
||||
};
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
var keystore = require('./keystore.js').create({
|
||||
configDir: require('path').join(require('os').homedir(), '.config/telebit/')
|
||||
, fallback: true
|
||||
});
|
||||
|
||||
var name = "testy-mctestface-1";
|
||||
return keystore.get(name).then(function (jwk) {
|
||||
console.log("get1", typeof jwk, jwk);
|
||||
if (!jwk || !jwk.kty) {
|
||||
return require('keypairs').generate().then(function (jwk) {
|
||||
var json = JSON.stringify(jwk.private);
|
||||
return keystore.set(name, json).then(function () {
|
||||
return keystore.all().then(function (vals) {
|
||||
console.log("All", vals);
|
||||
return keystore.get(name).then(function (val2) {
|
||||
console.log("get2", val2);
|
||||
});
|
||||
});
|
||||
}).catch(function (err) {
|
||||
console.log('badness', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
return jwk;
|
||||
});
|
||||
}());
|
|
@ -0,0 +1,52 @@
|
|||
'use strict';
|
||||
|
||||
module.exports.create = function (opts) {
|
||||
var service = opts.name || "Telebit";
|
||||
var keytar;
|
||||
try {
|
||||
if (opts.fallback) {
|
||||
throw new Error("forced fallback");
|
||||
}
|
||||
keytar = require('keytar');
|
||||
// TODO test that long "passwords" (JWTs and JWKs) can be stored in all OSes
|
||||
} catch(e) {
|
||||
console.warn("Could not load native key management. Keys will be stored in plain text.");
|
||||
keytar = require('./keystore-fallback.js').create(opts);
|
||||
keytar.insecure = true;
|
||||
}
|
||||
|
||||
return {
|
||||
get: function (name) {
|
||||
return keytar.getPassword(service, name).then(maybeParse);
|
||||
}
|
||||
, set: function (name, value) {
|
||||
return keytar.setPassword(service, name, maybeStringify(value));
|
||||
}
|
||||
, delete: function (name) {
|
||||
return keytar.deletePassword(service, name);
|
||||
}
|
||||
, all: function () {
|
||||
return keytar.findCredentials(service).then(function (list) {
|
||||
return list.map(function (el) {
|
||||
el.password = maybeParse(el.password);
|
||||
return el;
|
||||
});
|
||||
});
|
||||
}
|
||||
, insecure: keytar.insecure
|
||||
};
|
||||
};
|
||||
|
||||
function maybeParse(str) {
|
||||
if (str && '{' === str[0]) {
|
||||
return JSON.parse(str);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function maybeStringify(obj) {
|
||||
if ('string' !== typeof obj && 'object' === typeof obj) {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
return obj;
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
'use strict';
|
||||
|
||||
var os = require('os');
|
||||
var path = require('path');
|
||||
var http = require('http');
|
||||
var keypairs = require('keypairs');
|
||||
|
||||
var common = require('../cli-common.js');
|
||||
|
||||
/*
|
||||
function packConfig(config) {
|
||||
return Object.keys(config).map(function (key) {
|
||||
var val = config[key];
|
||||
if ('undefined' === val) {
|
||||
throw new Error("'undefined' used as a string value");
|
||||
}
|
||||
if ('undefined' === typeof val) {
|
||||
//console.warn('[DEBUG]', key, 'is present but undefined');
|
||||
return;
|
||||
}
|
||||
if (val && 'object' === typeof val && !Array.isArray(val)) {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
return key + ':' + val; // converts arrays to strings with ,
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
module.exports.create = function (state) {
|
||||
common._init(
|
||||
// make a default working dir and log dir
|
||||
state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit')
|
||||
, (state._clientConfig.root && path.join(state._clientConfig.root, 'etc'))
|
||||
|| path.resolve(common.DEFAULT_CONFIG_PATH, '..')
|
||||
);
|
||||
state._ipc = common.pipename(state._clientConfig, true);
|
||||
|
||||
function makeResponder(service, resp, fn) {
|
||||
var body = '';
|
||||
|
||||
function finish() {
|
||||
var err;
|
||||
|
||||
if (200 !== resp.statusCode) {
|
||||
err = new Error(body || ('get ' + service + ' failed'));
|
||||
err.statusCode = resp.statusCode;
|
||||
err.code = "E_REQUEST";
|
||||
}
|
||||
|
||||
if (body) {
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch(e) {
|
||||
console.error('Error:', err);
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
fn(err, body);
|
||||
}
|
||||
|
||||
if (!resp.headers['content-length'] && !resp.headers['content-type']) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO use readable
|
||||
resp.on('data', function (chunk) {
|
||||
body += chunk.toString();
|
||||
});
|
||||
resp.on('end', finish);
|
||||
}
|
||||
|
||||
var RC = {};
|
||||
RC.resolve = function (pathstr) {
|
||||
// TODO use real hostname and return reqOpts rather than string?
|
||||
return 'http://localhost:' + (RC.port({}).port||'1').toString() + '/' + pathstr.replace(/^\//, '');
|
||||
};
|
||||
RC.port = function (reqOpts) {
|
||||
var fs = require('fs');
|
||||
var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port');
|
||||
if (fs.existsSync(portFile)) {
|
||||
reqOpts.host = 'localhost';
|
||||
reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10);
|
||||
if (!state.ipc) {
|
||||
state.ipc = {};
|
||||
}
|
||||
state.ipc.type = 'port';
|
||||
state.ipc.path = path.dirname(state._ipc.path);
|
||||
state.ipc.port = reqOpts.port;
|
||||
} else {
|
||||
reqOpts.socketPath = state._ipc.path;
|
||||
}
|
||||
return reqOpts;
|
||||
};
|
||||
RC.createRelauncher = function (replay, opts, cb) {
|
||||
return function (err) {
|
||||
/*global Promise*/
|
||||
var p = new Promise(function (resolve, reject) {
|
||||
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
|
||||
// ECONNREFUSED - leftover socket just needs to be restarted
|
||||
if ('ENOENT' !== err.code && 'ECONNREFUSED' !== err.code) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// retried and failed again: quit
|
||||
if (opts._taketwo) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
|
||||
if (err) { reject(err); return; }
|
||||
opts._taketwo = true;
|
||||
setTimeout(function () {
|
||||
if (replay.length <= 1) {
|
||||
replay(opts).then(resolve).catch(reject);
|
||||
return;
|
||||
} else {
|
||||
replay(opts, function (err, res) {
|
||||
if (err) { reject(err); }
|
||||
else { resolve(res); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, 2500);
|
||||
});
|
||||
return;
|
||||
});
|
||||
if (cb) {
|
||||
p.then(function () { cb(null); }).catch(function (err) { cb(err); });
|
||||
}
|
||||
return p;
|
||||
};
|
||||
};
|
||||
RC.request = function request(opts, fn) {
|
||||
if (!opts) { opts = {}; }
|
||||
var service = opts.service || 'config';
|
||||
/*
|
||||
var args = opts.data;
|
||||
if (args && 'control' === service) {
|
||||
args = packConfig(args);
|
||||
}
|
||||
var json = JSON.stringify(opts.data);
|
||||
*/
|
||||
var url = '/rpc/' + service;
|
||||
/*
|
||||
if (json) {
|
||||
url += ('?_body=' + encodeURIComponent(json));
|
||||
}
|
||||
*/
|
||||
var method = opts.method || (opts.data && 'POST') || 'GET';
|
||||
var reqOpts = {
|
||||
method: method
|
||||
, path: url
|
||||
};
|
||||
reqOpts = RC.port(reqOpts);
|
||||
var req = http.request(reqOpts, function (resp) {
|
||||
makeResponder(service, resp, fn);
|
||||
});
|
||||
|
||||
var errHandler = RC.createRelauncher(RC.request, opts, fn);
|
||||
req.on('error', errHandler);
|
||||
|
||||
// Simple GET
|
||||
if ('POST' !== method || !opts.data) {
|
||||
return keypairs.signJwt({
|
||||
jwk: state.key
|
||||
, claims: { iss: false, exp: Math.round(Date.now()/1000) + (15 * 60) }
|
||||
//TODO , exp: '15m'
|
||||
}).then(function (jwt) {
|
||||
req.setHeader("Authorization", 'Bearer ' + jwt);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
return keypairs.signJws({
|
||||
jwk: state.key
|
||||
, protected: {
|
||||
// alg will be filled out automatically
|
||||
jwk: state.pub
|
||||
, kid: false
|
||||
, nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
|
||||
// TODO make localhost exceptional
|
||||
, url: RC.resolve(reqOpts.path)
|
||||
}
|
||||
, payload: JSON.stringify(opts.data)
|
||||
}).then(function (jws) {
|
||||
req.setHeader("Content-Type", 'application/jose+json');
|
||||
req.write(JSON.stringify(jws));
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
return RC;
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
'use strict';
|
||||
|
||||
/*global Promise*/
|
||||
var PromiseA = Promise;
|
||||
var crypto = require('crypto');
|
||||
var util = require('util');
|
||||
var readFile = util.promisify(require('fs').readFile);
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
function sshAllowsPassword(user) {
|
||||
// SSH on Windows is a thing now (beta 2015, standard 2018)
|
||||
// https://stackoverflow.com/questions/313111/is-there-a-dev-null-on-windows
|
||||
var nullfile = '/dev/null';
|
||||
if (/^win/i.test(process.platform)) {
|
||||
nullfile = 'NUL';
|
||||
}
|
||||
var args = [
|
||||
'ssh', '-v', '-n'
|
||||
, '-o', 'Batchmode=yes'
|
||||
, '-o', 'StrictHostKeyChecking=no'
|
||||
, '-o', 'UserKnownHostsFile=' + nullfile
|
||||
, user + '@localhost'
|
||||
, '| true'
|
||||
];
|
||||
return new PromiseA(function (resolve) {
|
||||
// not using promisify because all 3 arguments convey information
|
||||
exec(args.join(' '), function (err, stdout, stderr) {
|
||||
stdout = (stdout||'').toString('utf8');
|
||||
stderr = (stderr||'').toString('utf8');
|
||||
if (/\bpassword\b/.test(stdout) || /\bpassword\b/.test(stderr)) {
|
||||
resolve('yes');
|
||||
return;
|
||||
}
|
||||
if (/\bAuthentications\b/.test(stdout) || /\bAuthentications\b/.test(stderr)) {
|
||||
resolve('no');
|
||||
return;
|
||||
}
|
||||
resolve('maybe');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.checkSecurity = function () {
|
||||
var conf = {};
|
||||
var noRootPasswordRe = /(?:^|[\r\n]+)\s*PermitRootLogin\s+(prohibit-password|without-password|no)\s*/i;
|
||||
var noPasswordRe = /(?:^|[\r\n]+)\s*PasswordAuthentication\s+(no)\s*/i;
|
||||
var sshdConf = '/etc/ssh/sshd_config';
|
||||
if (/^win/i.test(process.platform)) {
|
||||
// TODO use %PROGRAMDATA%\ssh\sshd_config
|
||||
sshdConf = 'C:\\ProgramData\\ssh\\sshd_config';
|
||||
}
|
||||
return readFile(sshdConf, null).then(function (sshd) {
|
||||
sshd = sshd.toString('utf8');
|
||||
var match;
|
||||
match = sshd.match(noRootPasswordRe);
|
||||
conf.permit_root_login = match ? match[1] : 'yes';
|
||||
match = sshd.match(noPasswordRe);
|
||||
conf.password_authentication = match ? match[1] : 'yes';
|
||||
}).catch(function () {
|
||||
// ignore error as that might not be the correct sshd_config location
|
||||
}).then(function () {
|
||||
var doesntExist = crypto.randomBytes(16).toString('hex');
|
||||
return sshAllowsPassword(doesntExist).then(function (maybe) {
|
||||
conf.requests_password = maybe;
|
||||
});
|
||||
}).then(function () {
|
||||
return conf;
|
||||
});
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
module.exports.checkSecurity().then(function (conf) {
|
||||
console.log(conf);
|
||||
return conf;
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "telebit",
|
||||
"version": "0.20.8",
|
||||
"version": "0.21.0-wip.1",
|
||||
"description": "Break out of localhost. Connect to any device from anywhere over any tcp port or securely in a browser. A secure tunnel. A poor man's reverse VPN.",
|
||||
"main": "lib/remote.js",
|
||||
"main": "lib/daemon/index.js",
|
||||
"files": [
|
||||
"bin",
|
||||
"lib",
|
||||
|
@ -53,11 +53,12 @@
|
|||
},
|
||||
"homepage": "https://git.coolaj86.com/coolaj86/telebit.js#readme",
|
||||
"dependencies": {
|
||||
"@coolaj86/urequest": "^1.3.5",
|
||||
"@root/request": "^1.3.10",
|
||||
"finalhandler": "^1.1.1",
|
||||
"greenlock": "^2.3.1",
|
||||
"js-yaml": "^3.11.0",
|
||||
"jsonwebtoken": "^7.1.9",
|
||||
"greenlock": "^2.6.7",
|
||||
"js-yaml": "^3.13.1",
|
||||
"keyfetch": "^1.1.8",
|
||||
"keypairs": "^1.2.14",
|
||||
"mkdirp": "^0.5.1",
|
||||
"proxy-packer": "^2.0.2",
|
||||
"ps-list": "^5.0.0",
|
||||
|
@ -72,6 +73,9 @@
|
|||
"toml": "^0.4.1",
|
||||
"ws": "^6.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"keytar": "^4.6.0"
|
||||
},
|
||||
"trulyOptionalDependencies": {
|
||||
"bluebird": "^3.5.1"
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ var pin = Math.round(Math.random() * 999999).toString().padStart(6, '0'); // '32
|
|||
|
||||
console.log('Pair Code:', pin);
|
||||
|
||||
var urequest = require('@coolaj86/urequest');
|
||||
var urequest = require('@root/request');
|
||||
var req = {
|
||||
url: 'https://api.telebit.ppl.family/api/telebit.cloud/pair_request'
|
||||
, method: 'POST'
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
var stateUrl = 'https://api.telebit.ppl.family/api/telebit.cloud/pair_state/bca27428719e9c67805359f1';
|
||||
|
||||
var urequest = require('@coolaj86/urequest');
|
||||
var urequest = require('@root/request');
|
||||
var req = {
|
||||
url: stateUrl
|
||||
, method: 'GET'
|
||||
|
|
|
@ -11,7 +11,7 @@ Launcher._killAll = function (fn) {
|
|||
var psList = require('ps-list');
|
||||
psList().then(function (procs) {
|
||||
procs.forEach(function (proc) {
|
||||
if ('node' === proc.name && /\btelebitd\b/i.test(proc.cmd)) {
|
||||
if ('node' === proc.name && /\btelebit(d| daemon)\b/i.test(proc.cmd)) {
|
||||
console.log(proc);
|
||||
process.kill(proc.pid);
|
||||
return true;
|
||||
|
@ -45,37 +45,7 @@ Launcher._detect = function (things, fn) {
|
|||
}
|
||||
}
|
||||
|
||||
// could have used "command-exists" but I'm trying to stay low-dependency
|
||||
// os.platform(), os.type()
|
||||
if (!/^win/i.test(os.platform())) {
|
||||
if (/^darwin/i.test(os.platform())) {
|
||||
exec('command -v launchctl', things._execOpts, function (err, stdout, stderr) {
|
||||
err = Launcher._getError(err, stderr);
|
||||
fn(err, 'launchctl');
|
||||
});
|
||||
} else {
|
||||
exec('command -v systemctl', things._execOpts, function (err, stdout, stderr) {
|
||||
err = Launcher._getError(err, stderr);
|
||||
fn(err, 'systemctl');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// https://stackoverflow.com/questions/17908789/how-to-add-an-item-to-registry-to-run-at-startup-without-uac
|
||||
// wininit? regedit? SCM?
|
||||
// REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /V "My App" /t REG_SZ /F /D "C:\MyAppPath\MyApp.exe"
|
||||
// https://www.microsoft.com/developerblog/2015/11/09/reading-and-writing-to-the-windows-registry-in-process-from-node-js/
|
||||
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-add
|
||||
// https://social.msdn.microsoft.com/Forums/en-US/5b318f44-281e-4098-8dee-3ba8435fa391/add-registry-key-for-autostart-of-app-in-ice?forum=quebectools
|
||||
// utils.elevate
|
||||
// https://github.com/CatalystCode/windows-registry-node
|
||||
exec('where reg.exe', things._execOpts, function (err, stdout, stderr) {
|
||||
//console.log((stdout||'').trim());
|
||||
if (stderr) {
|
||||
console.error(stderr);
|
||||
}
|
||||
fn(err, 'reg.exe');
|
||||
});
|
||||
}
|
||||
require('./which.js').launcher(things._execOpts, fn);
|
||||
};
|
||||
Launcher.install = function (things, fn) {
|
||||
if (!fn) { fn = function (err) { if (err) { console.error(err); } }; }
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
'use strict';
|
||||
|
||||
var os = require('os');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
var which = module.exports;
|
||||
|
||||
which._getError = function getError(err, stderr) {
|
||||
if (err) { return err; }
|
||||
if (stderr) {
|
||||
err = new Error(stderr);
|
||||
err.code = 'EWHICH';
|
||||
return err;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.which = function (cmd, execOpts, fn) {
|
||||
return module.exports._which({
|
||||
mac: cmd
|
||||
, linux: cmd
|
||||
, win: cmd
|
||||
}, execOpts, fn);
|
||||
};
|
||||
module.exports.launcher = function (execOpts, fn) {
|
||||
return module.exports._which({
|
||||
mac: 'launchctl'
|
||||
, linux: 'systemctl'
|
||||
, win: 'reg.exe'
|
||||
}, execOpts, fn);
|
||||
};
|
||||
module.exports._which = function (progs, execOpts, fn) {
|
||||
// could have used "command-exists" but I'm trying to stay low-dependency
|
||||
// os.platform(), os.type()
|
||||
if (!/^win/i.test(os.platform())) {
|
||||
if (/^darwin/i.test(os.platform())) {
|
||||
exec('command -v ' + progs.mac, execOpts, function (err, stdout, stderr) {
|
||||
err = which._getError(err, stderr);
|
||||
fn(err, progs.mac);
|
||||
});
|
||||
} else {
|
||||
exec('command -v ' + progs.linux, execOpts, function (err, stdout, stderr) {
|
||||
err = which._getError(err, stderr);
|
||||
fn(err, progs.linux);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// https://stackoverflow.com/questions/17908789/how-to-add-an-item-to-registry-to-run-at-startup-without-uac
|
||||
// wininit? regedit? SCM?
|
||||
// REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /V "My App" /t REG_SZ /F /D "C:\MyAppPath\MyApp.exe"
|
||||
// https://www.microsoft.com/developerblog/2015/11/09/reading-and-writing-to-the-windows-registry-in-process-from-node-js/
|
||||
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-add
|
||||
// https://social.msdn.microsoft.com/Forums/en-US/5b318f44-281e-4098-8dee-3ba8435fa391/add-registry-key-for-autostart-of-app-in-ice?forum=quebectools
|
||||
// utils.elevate
|
||||
// https://github.com/CatalystCode/windows-registry-node
|
||||
exec('where ' + progs.win, execOpts, function (err, stdout, stderr) {
|
||||
//console.log((stdout||'').trim());
|
||||
if (stderr) {
|
||||
console.error(stderr);
|
||||
}
|
||||
fn(err, progs.win);
|
||||
});
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue