Compare commits
63 Commits
25 changed files with 15191 additions and 736 deletions
File diff suppressed because it is too large
@ -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,234 @@ |
|||
<!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/telebit.js"></script> |
|||
<script src="/js/telebit-token.js"></script> |
|||
<script src="/js/app.js"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,513 @@ |
|||
;(function () { |
|||
'use strict'; |
|||
|
|||
var Vue = window.Vue; |
|||
var Telebit = window.TELEBIT; |
|||
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({ |
|||
url: "/api/config" |
|||
, method: "GET" |
|||
}).then(function (resp) { |
|||
var json = resp.body; |
|||
appData.config = json; |
|||
return json; |
|||
}); |
|||
}; |
|||
api.status = function apiStatus() { |
|||
return Telebit.reqLocalAsync({ url: "/api/status", method: "GET" }).then(function (resp) { |
|||
var json = resp.body; |
|||
return json; |
|||
}); |
|||
}; |
|||
api.http = function apiHttp(o) { |
|||
var opts = { |
|||
url: "/api/http" |
|||
, method: "POST" |
|||
, headers: { 'Content-Type': 'application/json' } |
|||
, json: { name: o.name, handler: o.handler, indexes: o.indexes } |
|||
}; |
|||
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 = { |
|||
url: "/api/ssh" |
|||
, method: "POST" |
|||
, headers: { 'Content-Type': 'application/json' } |
|||
, json: { port: port } |
|||
}; |
|||
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 = { |
|||
url: "/api/enable" |
|||
, method: "POST" |
|||
//, headers: { 'Content-Type': 'application/json' }
|
|||
}; |
|||
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 = { |
|||
url: "/api/disable" |
|||
, method: "POST" |
|||
//, headers: { 'Content-Type': 'application/json' }
|
|||
}; |
|||
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"); |
|||
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 |
|||
}); |
|||
|
|||
|
|||
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); |
|||
}); |
|||
|
|||
window.api = api; |
|||
|
|||
setTimeout(function () { |
|||
document.body.hidden = false; |
|||
}, 50); |
|||
|
|||
}()); |
@ -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,338 @@ |
|||
;(function (exports) { |
|||
'use strict'; |
|||
|
|||
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('@coolaj86/urequest')); |
|||
common.reqLocalAsync = require('util').promisify(require('@coolaj86/urequest')); |
|||
} |
|||
|
|||
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
@ -0,0 +1,102 @@ |
|||
'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(/\/$/, '') + '/'); |
|||
if (!urlstr.match(todo[0])) { |
|||
//console.log("[eggspress] pattern doesn't match", todo[0], req.url);
|
|||
next(); |
|||
return; |
|||
} else if ('string' === typeof todo[0] && 0 !== urlstr.match(todo[0]).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); |
|||
} |
|||
|
|||
try { |
|||
console.log("[eggspress] matched pattern", todo[0], req.url); |
|||
var p = todo[1](req, res, next); |
|||
if (p && p.catch) { |
|||
p.catch(fail); |
|||
} |
|||
} catch(e) { |
|||
fail(e); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
res.send = eggSend; |
|||
|
|||
next(); |
|||
}; |
|||
|
|||
app.use = function (pattern, fn) { |
|||
return app._use('', pattern, fn); |
|||
}; |
|||
[ 'HEAD', 'GET', 'POST', 'DELETE' ].forEach(function (method) { |
|||
app[method.toLowerCase()] = function (pattern, fn) { |
|||
return app._use(method, pattern, fn); |
|||
}; |
|||
}); |
|||
|
|||
app.post = function (pattern, fn) { |
|||
return app._use('POST', pattern, fn); |
|||
}; |
|||
app._use = function (method, pattern, fn) { |
|||
// 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, fn, method.toLowerCase()]); |
|||
return app; |
|||
}; |
|||
|
|||
return app; |
|||
}; |
@ -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,37 @@ |
|||
'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 + keyext)); |
|||
})); |
|||
}); |
|||
} |
|||
, insecure: true |
|||
}; |
|||
}; |
@ -0,0 +1,22 @@ |
|||
'use strict'; |
|||
|
|||
var keystore = require('./keystore.js').create({ |
|||
configDir: require('path').join(require('os').homedir(), '.local/telebit/') |
|||
}); |
|||
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.get(name).then(function (val2) { |
|||
console.log("get2", val2); |
|||
}); |
|||
}).catch(function (err) { |
|||
console.log('badness', err); |
|||
}); |
|||
}); |
|||
} |
|||
return jwk; |
|||
}); |
@ -0,0 +1,49 @@ |
|||
'use strict'; |
|||
|
|||
module.exports.create = function (opts) { |
|||
var service = opts.name || "Telebit"; |
|||
var keytar; |
|||
try { |
|||
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,175 @@ |
|||
'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.createErrorHandler = function (replay, opts, cb) { |
|||
return function (err) { |
|||
// 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) { |
|||
if (opts._taketwo) { |
|||
cb(err); |
|||
return; |
|||
} |
|||
require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) { |
|||
if (err) { cb(err); return; } |
|||
opts._taketwo = true; |
|||
setTimeout(function () { |
|||
replay(opts, cb); |
|||
}, 2500); |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
cb(err); |
|||
}; |
|||
}; |
|||
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); |
|||
}); |
|||
|
|||
req.on('error', RC.createErrorHandler(RC.request, opts, fn)); |
|||
|
|||
// 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 |
|||
, 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
@ -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