diff --git a/examples/telebit-relay.yml b/examples/telebit-relay.yml index 239d017..ae74aeb 100644 --- a/examples/telebit-relay.yml +++ b/examples/telebit-relay.yml @@ -13,3 +13,7 @@ greenlock: strategy: le-store-certbot # certificate storage plugin config_dir: /etc/acme # directory for ssl certificates secret: '' # generate with node -e "console.log(crypto.randomBytes(16).toString('hex'))" +mailer: + url: 'https://api.mailgun.net/v3/EXAMPLE.COM/messages' + api_key: 'key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + from: 'Example Mailer ' diff --git a/lib/extensions/admin/login/index.html b/lib/extensions/admin/login/index.html new file mode 100644 index 0000000..19704e3 --- /dev/null +++ b/lib/extensions/admin/login/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + res.send("

Invalid Magic Link

" + + "
'" + magic + "' isn't a valid link.\nLinks are only good for 5 minutes, so act fast.\n"
+    + "(" + new Date(1000*((_auths[magic]||{}).dt||0)).toISOString() + ")
\n" + ); diff --git a/lib/extensions/admin/login/js/app.js b/lib/extensions/admin/login/js/app.js new file mode 100644 index 0000000..76f1547 --- /dev/null +++ b/lib/extensions/admin/login/js/app.js @@ -0,0 +1,23 @@ +(function () { +'use strict'; + +var magic = (window.location.hash || '').substr(2).replace(/magic=/, ''); + +if (magic) { + window.fetch('https://api.' + location.hostname + '/api/telebit.cloud/magic/' + magic, { + method: 'GET' + , cors: true + }).then(function (resp) { + return resp.json().then(function (json) { + document.querySelector('body').hidden = false; + document.querySelector('js-magic').hidden = false; + document.querySelector('js-token-data').innerText = JSON.stringify(json, null, 2); + document.querySelector('js-new-href').href = json.domains[0]; + document.querySelector('js-new-href').innerText = json.domains[0]; + }); + }); +} else { + document.querySelector('body').hidden = false; +} + +}()); diff --git a/lib/extensions/emails/.gitkeep b/lib/extensions/emails/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/extensions.js b/lib/extensions/index.js similarity index 57% rename from lib/extensions.js rename to lib/extensions/index.js index 371b1a9..8ccdf14 100644 --- a/lib/extensions.js +++ b/lib/extensions/index.js @@ -1,3 +1,4 @@ +'use strict'; /* curl -s --user 'api:YOUR_API_KEY' \ https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \ @@ -8,6 +9,7 @@ curl -s --user 'api:YOUR_API_KEY' \ -F text='Testing some Mailgun awesomeness!' */ var fs = require('fs'); +var escapeHtml = require('escape-html'); var _auths = module.exports._auths = {}; module.exports.authenticate = function (opts) { console.log("It's auth'n time!"); @@ -16,41 +18,48 @@ module.exports.authenticate = function (opts) { var state = opts.state; var jwtoken = opts.auth; var auth; - var mailer = { - user: 'wizard@telebit.cloud' - , secret: 'fbbf21d73c9d2f480bd0e71f5f18494e' - }; var crypto = require('crypto'); console.log('[DEBUG] ext auth', jwtoken); auth = jwtoken; if ('object' === typeof auth && /^.+@.+\..+$/.test(auth.subject)) { - console.log('parsed'); - var id = crypto.randomBytes(12).toString('hex'); - //var id = crypto.randomBytes(16).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,''); console.log("[DEBUG] gonna send email"); + auth.id = crypto.randomBytes(12).toString('hex'); + //var id = crypto.randomBytes(16).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,''); + var subj = 'Confirm New Device Connection'; + var text = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:\n" + + '\n' + + ' https://' + state.config.webminDomain + '/login/#/magic={{id}}\n' + + '\n' + + "({{os_arch}} {{os_platform}} {{os_release}})\n" + + '\n' + ; + var html = "You tried connecting with '{{hostname}}' for the first time. Confirm to continue connecting:
" + + '
' + + '       Confirm Device
' + + '
' + + '       or copy and paste this link:
' + + '       https://' + state.config.webminDomain + '/login/#/magic={{id}}
' + + '
' + + "({{os_arch}} {{os_platform}} {{os_release}})
" + + '
' + ; + [ 'id', 'hostname', 'os_arch', 'os_platform', 'os_release' ].forEach(function (key) { + var val = escapeHtml(auth[key]); + subj = subj.replace(new RegExp('{{' + key + '}}', 'g'), val); + text = text.replace(new RegExp('{{' + key + '}}', 'g'), val); + html = html.replace(new RegExp('{{' + key + '}}', 'g'), val); + }); return requestAsync({ - url: 'https://api.mailgun.net/v3/telebit.cloud/messages' + url: state.config.mailer.url , method: 'POST' - , auth: { user: 'api', pass: 'key-70ef48178081df19783ecfbe6fed5e9a' } + , auth: { user: 'api', pass: state.config.mailer.apiKey } , formData: { - from: 'Telebit Wizard ' + from: state.config.mailer.from , to: auth.subject - , subject: 'Confirm New Device Connection' - , text: "You tried connecting with '" + auth.hostname + "' for the first time. Confirm to continue connecting:\n" - + '\n' - + ' https://www.telebit.cloud/login/?magic=' + id + '\n' - + '\n' - + "(" + auth.os_arch + " " + auth.os_platform + " " + auth.os_release + ")\n" - + '\n' - , html: "You tried connecting with '" + auth.hostname + "' for the first time. Confirm to continue connecting:
" - + '
' - + '       Confirm Device
' - + '
' - + '       or copy and paste this link:
https://www.telebit.cloud/login/?magic=' + id + '

' - + '
' - + "(" + auth.os_arch + " " + auth.os_platform + " " + auth.os_release + ")
" - + '
' + , subject: subj + , text: text + , html: html } }).then(function (resp) { console.log("[DEBUG] email was sent, or so they say"); @@ -76,12 +85,12 @@ module.exports.authenticate = function (opts) { clearTimeout(t); delete _auths[id]; var hri = require('human-readable-ids').hri; - var hrname = hri.random() + '.telebit.cloud'; + var hrname = hri.random() + '.' + state.config.sharedDomain; var jwt = require('jsonwebtoken'); var tokenData = { domains: [ hrname ] , ports: [ 1024 + Math.round(Math.random() * 6300) ] - , aud: 'telebit.cloud' + , aud: state.config.webminDomain , iss: Math.round(Date.now() / 1000) , id: id , hostname: auth.hostname @@ -108,6 +117,7 @@ module.exports.authenticate = function (opts) { } console.log("just trying a normal token..."); + var decoded; try { decoded = jwt.decode(jwtoken, { complete: true }); } catch(e) { @@ -120,35 +130,45 @@ module.exports.authenticate = function (opts) { var path = require('path'); var express = require('express'); var app = express(); -app.use('/', express.static(path.join(__dirname, 'extensions/admin'))); -app.use('/login', function (req, res) { +var staticApp = express(); +var nowww = require('nowww')(); +var CORS = require('connect-cors'); +staticApp.use('/', express.static(path.join(__dirname, 'admin'))); +app.use('/api', CORS({})); +app.get('/api/telebit.cloud/magic/:magic', function (req, res) { + console.log("DEBUG telebit.cloud magic"); var tokenData; - var magic = req.query.magic; + var magic = req.params.magic || req.query.magic; + console.log("DEBUG telebit.cloud magic 1a"); if (_auths[magic]) { + console.log("DEBUG telebit.cloud magic 1b"); tokenData = _auths[magic].resolve(); - res.send('' - + '' - + '' - + '

Give us about 30 seconds...

' - + 'We\'re initializing our connection, redirecting you to your device at ' - + tokenData.domains[0] - + ', which will then take a few seconds to initialize as it gets your https certificates for peer-to-peer, end-to-end encryption' - + '
' - + '
' - + '
' + JSON.stringify(tokenData, null, 2) + '
' - + '' - ); + console.log("DEBUG telebit.cloud magic 1c"); + res.send(tokenData); } else { - res.send("

Invalid Magic Link

" - + "
'" + magic + "' isn't a valid link.\nLinks are only good for 5 minutes, so act fast.\n"
-    + "(" + new Date(1000*((_auths[magic]||{}).dt||0)).toISOString() + ")
\n" - ); + console.log("DEBUG telebit.cloud magic 2"); + res.send({ error: { code: "E_TOKEN", message: "Invalid or expired magic link." } }); + console.log("DEBUG telebit.cloud magic 2b"); } }); module.exports.webadmin = function (state, req, res) { //if (!loaded) { loaded = true; app.use('/', state.defaults.webadmin); } console.log('[DEBUG] extensions webadmin'); - app(req, res); + var host = (req.headers.host || '').toLowerCase().split(':')[0]; + if (state.config.webminDomain === host) { + console.log("DEBUG going to static"); + staticApp(req, res); + return; + } + if ('api.' + state.config.webminDomain === host) { + console.log("DEBUG going to api"); + app(req, res); + return; + } + if ('www.' + state.config.webminDomain === host) { + console.log("DEBUG going to www"); + nowww(req, res); + return; + } + res.end("Didn't recognize '" + escapeHtml(host) + "'. Not sure what to do."); }; diff --git a/package.json b/package.json index 79e9fee..fc849b8 100644 --- a/package.json +++ b/package.json @@ -39,12 +39,15 @@ "dependencies": { "bluebird": "^3.5.1", "cluster-store": "^2.0.8", + "connect-cors": "^0.5.6", + "escape-html": "^1.0.3", "express": "^4.16.3", "finalhandler": "^1.1.1", "greenlock": "^2.2.4", "human-readable-ids": "^1.0.4", "js-yaml": "^3.11.0", "jsonwebtoken": "^8.2.1", + "nowww": "^1.2.1", "proxy-packer": "^1.4.3", "recase": "^1.0.4", "redirect-https": "^1.1.5",