Compare commits

..

9 Commits
next ... stable

Author SHA1 Message Date
AJ ONeal
445e08c4d2
chore: bump version to v0.20.251025 2025-10-26 00:05:57 -06:00
AJ ONeal
7bc2193d09
fix(systemd): don't use system-only values for user unit 2025-10-25 23:40:03 -06:00
f0049c7f06 better param parsing 2019-05-28 17:07:45 -06:00
b5d57817cf fix some typos, move some phrases to the i18n toml file 2019-05-18 13:52:51 -06:00
1e3f7f671d use Root links (new branding) 2019-05-18 13:37:49 -06:00
00f3b3ab45 more descriptive wait message 2019-05-18 13:24:19 -06:00
fountainheadllc
19a42a596c Update 'lib/html/index.html' 2019-05-17 16:09:23 +00:00
fountainheadllc
de2290dd3e Update 'lib/html/index.html' 2019-05-17 03:14:11 +00:00
fountainheadllc
7db8a7a4ae Update 'lib/html/index.html'
Rachel's edits to index.html to make information easier to understand.
2019-05-15 00:28:15 +00:00
32 changed files with 783 additions and 18460 deletions

View File

@ -1,10 +1,7 @@
# Telebit™ Remote
# Telebit™ Remote | a [Root](https://rootprojects.org) project
The T-Rex Long-Arm of the Internet
Because friends don't let friends localhost™
<small>because friends don't let friends localhost</small>
| Sponsored by [ppl](https://ppl.family)
| **Telebit Remote**
| [Telebit Relay](https://git.coolaj86.com/coolaj86/telebit-relay.js)
| [sclient](https://telebit.cloud/sclient)
@ -526,7 +523,7 @@ rm -rf ~/.config/telebit ~/.local/share/telebit
Browser Library
=======
This is implemented with websockets, so browser compatibility is a hopeful future outcome. Would love help.
This is implemented with websockets, so you should be able to
LICENSE
=======

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,6 @@ 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');

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +0,0 @@
<!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>

View File

@ -1,235 +0,0 @@
<!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 &amp; 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 &amp; 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>

View File

@ -1,608 +0,0 @@
;(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

View File

@ -1,116 +0,0 @@
;(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));

View File

@ -1,365 +0,0 @@
;(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

View File

@ -9,7 +9,7 @@ var fs = require('fs');
var mkdirp = require('mkdirp');
var os = require('os');
var homedir = os.homedir();
var urequest = require('@root/request');
var urequest = require('@coolaj86/urequest');
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('./jwt.js');
var jwt = require('jsonwebtoken');
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) {

View File

@ -1,120 +0,0 @@
'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;
};

View File

@ -6,7 +6,7 @@ Telebit Remote is the T-Rex long-arm of the Internet. UNSTOPPABLE!
Using reliable HTTPS tunneling to establishing peer-to-peer connections,
Telebit is empowering the next generation of tinkerers. Access your devices.
Share your stuff. Be UNSTOPPABLE! (Join us at https://ppl.family)
Share your stuff. Be UNSTOPPABLE! (Join us at https://rootprojects.org)
Usage:
@ -135,7 +135,7 @@ usage: telebit http <path/port/none> [subdomain]
Use cases:
- Lazy man's AirDrop (works or lazy women too!)
- Lazy man's AirDrop (works for lazy women too!)
- Testing dev sites on a phone
- Sharing indie music and movies with friends"
@ -145,7 +145,7 @@ usage: telebit ssh <auto|port|none>
All https traffic will be inspected to see if it looks like ssh Once enabled all traffic that looks
ssh auto Make ssh Just Works (on port 22)
ssh auto Make ssh Just Work (on port 22)
ssh <port> forward ssh traffic to non-standard port
ex: telebit ssh 22 ex: explicitly forward ssh-looking packets to localhost:22
@ -464,6 +464,19 @@ code = "
==============================================
"
waiting = "waiting for you to check your email..."
success = "Success"
next_steps = "Some fun things to try first:
~/telebit http ~/Public
~/telebit tcp 5050
~/telebit ssh auto
Press any key to continue...
"
[remote.setup]
email = "Welcome!
@ -476,13 +489,5 @@ 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}"

View File

@ -51,18 +51,24 @@
<div>
<h2>You've claimed <span class="js-servername">{{servername}}</span></h2>
<p>Here's some ways you can use it:</p>
<p>Here are some ways you can use Telebit via Terminal or other Command Line Interface:</p>
<div class="code-block">
<pre><code>telebit http ~/Public # serve a public folder
telebit http 3000 # forward all https traffic to localhost:3000
telebit http none # remove all https handlers</code></pre>
<br />
<pre><code>~/telebit ssh auto # allows you to connect to your computer with <br /> ssh-over-https from a different computer</span></code></pre>
<pre><code>~/telebit http ~/Public # serve a public folder
~/telebit http 3000 # forward all https traffic to localhost:3000
~/telebit http none # remove all https handlers</code></pre>
</div>
</div>
<p>You can <em>always</em> tunnel <strong>SSH over HTTPS</strong>,
<p>And remember you can <em>always</em> tunnel <strong>SSH over HTTPS</strong>,
even while you're using it for something else:</p>
<div class="code-block"><pre><code>telebit ssh auto</code></pre>
<p>&nbsp;</p>
<details>
<p><summary><strong>Here are some examples for those of you that want to access files and folders remotely. </strong></summary></p>
<p><strong>This function allows you to connect one computer to another computer you also have SSH on.</strong></p>
<div class="code-block"><pre><code>~/telebit ssh <span class="js-servername">{{servername}}</span></code></pre>
<br>
<pre><code>telebit ssh <span class="js-servername">{{servername}}</span></code></pre>
- or -
<pre><code>ssh -o ProxyCommand='<a href="https://telebit.cloud/sclient">sclient</a> %h' <span class="js-servername">{{servername}}</span></code></pre>
- or -
@ -70,8 +76,7 @@ telebit http none # remove all https handlers</code></pre>
ssh -o ProxyCommand="$proxy_cmd" <span class="js-servername">{{servername}}</span></code></pre>
</div>
<pre><code>ssh -o ProxyCommand='openssl s_client -connect %h:443 -servername %h -quiet' <span class="js-servername">{{servername}}</span></code></pre>
</details>
<!--div class="js-port" hidden>
<h2>You've claimed port <span class="js-serviceport">{{serviceport}}</span></h2>
<p>Here's some ways you can use it:</p>
@ -87,4 +92,4 @@ ssh <span class="js-servername">{{servername}}</span> -p <span class="js-service
<script src="js/app.js"></script>
</body>
</html>
</html>

View File

@ -3,7 +3,7 @@
document.body.hidden = false;
var hash = window.location.hash.substr(1);
var hash = window.location.hash.replace(/^[\/#?]+/, '');
var query = window.location.search;
function parseQuery(search) {

View File

@ -1,19 +0,0 @@
'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');

View File

@ -1,43 +0,0 @@
'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;
};

View File

@ -1,39 +0,0 @@
'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
};
};

View File

@ -1,29 +0,0 @@
(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;
});
}());

View File

@ -1,52 +0,0 @@
'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;
}

View File

@ -1,196 +0,0 @@
'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;
};

View File

@ -28,7 +28,6 @@ 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;
@ -40,9 +39,8 @@ function TelebitRemote(state) {
priv.tokens = [];
var auth;
if(!state.sortingHat) {
state.sortingHat = path.join(__dirname, '../sorting-hat.js');
state.sortingHat = "./sorting-hat.js";
}
state._connectionHandler = require(state.sortingHat);
if (state.token) {
if ('undefined' === state.token) {
throw new Error("passed string 'undefined' as token");
@ -351,7 +349,7 @@ function TelebitRemote(state) {
// TODO use readable streams instead
wstunneler._socket.pause();
state._connectionHandler.assign(state, tun, function (err, conn) {
require(state.sortingHat).assign(state, tun, function (err, conn) {
if (err) {
err.message = err.message.replace(/:tun_id/, tun._id);
packerHandlers._onConnectError(cid, tun, err);
@ -474,12 +472,12 @@ function TelebitRemote(state) {
priv.timeoutId = null;
var machine = Packer.create(packerHandlers);
console.info("[telebit:lib/daemon.js] [connect] '" + (state.wss || state.relay) + "'");
console.info("[telebit:lib/remote.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/daemon.js] [open] connected to '" + (state.wss || state.relay) + "'");
console.info("[telebit:lib/remote.js] [open] connected to '" + (state.wss || state.relay) + "'");
me.emit('connect');
priv.refreshTimeout();
priv.timeoutId = setTimeout(priv.checkTimeout, activityTimeout);

View File

@ -1,76 +0,0 @@
'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;
});
}

1147
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{
"name": "telebit",
"version": "0.21.0-wip.1",
"version": "0.20.251025",
"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/daemon/index.js",
"main": "lib/remote.js",
"files": [
"bin",
"lib",
@ -53,12 +53,11 @@
},
"homepage": "https://git.coolaj86.com/coolaj86/telebit.js#readme",
"dependencies": {
"@root/request": "^1.3.10",
"@coolaj86/urequest": "^1.3.5",
"finalhandler": "^1.1.1",
"greenlock": "^2.6.7",
"js-yaml": "^3.13.1",
"keyfetch": "^1.1.8",
"keypairs": "^1.2.14",
"greenlock": "^2.3.1",
"js-yaml": "^3.11.0",
"jsonwebtoken": "^7.1.9",
"mkdirp": "^0.5.1",
"proxy-packer": "^2.0.2",
"ps-list": "^5.0.0",
@ -73,9 +72,6 @@
"toml": "^0.4.1",
"ws": "^6.0.0"
},
"optionalDependencies": {
"keytar": "^4.6.0"
},
"trulyOptionalDependencies": {
"bluebird": "^3.5.1"
},

View File

@ -5,7 +5,7 @@ var pin = Math.round(Math.random() * 999999).toString().padStart(6, '0'); // '32
console.log('Pair Code:', pin);
var urequest = require('@root/request');
var urequest = require('@coolaj86/urequest');
var req = {
url: 'https://api.telebit.ppl.family/api/telebit.cloud/pair_request'
, method: 'POST'

View File

@ -2,7 +2,7 @@
var stateUrl = 'https://api.telebit.ppl.family/api/telebit.cloud/pair_state/bca27428719e9c67805359f1';
var urequest = require('@root/request');
var urequest = require('@coolaj86/urequest');
var req = {
url: stateUrl
, method: 'GET'

View File

@ -34,9 +34,9 @@ ExecReload=/bin/kill -USR1 $MAINPID
# Use private /tmp and /var/tmp, which are discarded after this stops.
PrivateTmp=true
# Use a minimal /dev
;PrivateDevices=true
PrivateDevices=true
# Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
ProtectHome=true
;ProtectHome=true
# Make /usr, /boot, /etc and possibly some more folders read-only.
ProtectSystem=full
# ... except for a few because we want a place for config, logs, etc
@ -61,4 +61,5 @@ NoNewPrivileges=true
; NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
# For userspace service
WantedBy=default.target

View File

@ -62,6 +62,6 @@ NoNewPrivileges=true
[Install]
# For system-level service
;WantedBy=multi-user.target
WantedBy=multi-user.target
# For userspace service
WantedBy=default.target
;WantedBy=default.target

View File

@ -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 && /\btelebit(d| daemon)\b/i.test(proc.cmd)) {
if ('node' === proc.name && /\btelebitd\b/i.test(proc.cmd)) {
console.log(proc);
process.kill(proc.pid);
return true;
@ -45,7 +45,37 @@ Launcher._detect = function (things, fn) {
}
}
require('./which.js').launcher(things._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 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');
});
}
};
Launcher.install = function (things, fn) {
if (!fn) { fn = function (err) { if (err) { console.error(err); } }; }

View File

@ -1,63 +0,0 @@
'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);
});
}
};