2018-06-15 08:45:47 +00:00
|
|
|
'use strict';
|
2018-06-19 23:40:58 +00:00
|
|
|
|
2018-06-08 08:23:05 +00:00
|
|
|
var fs = require('fs');
|
2018-06-19 23:40:58 +00:00
|
|
|
var path = require('path');
|
|
|
|
var util = require('util');
|
|
|
|
var crypto = require('crypto');
|
2018-06-15 08:45:47 +00:00
|
|
|
var escapeHtml = require('escape-html');
|
2018-06-19 23:40:58 +00:00
|
|
|
var jwt = require('jsonwebtoken');
|
|
|
|
var requestAsync = util.promisify(require('request'));
|
|
|
|
|
2018-06-06 10:58:01 +00:00
|
|
|
var _auths = module.exports._auths = {};
|
2018-06-19 23:40:58 +00:00
|
|
|
|
|
|
|
function sendMail(state, auth) {
|
|
|
|
console.log('[DEBUG] ext auth', auth);
|
|
|
|
/*
|
|
|
|
curl -s --user 'api:YOUR_API_KEY' \
|
|
|
|
https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \
|
|
|
|
-F from='Excited User <mailgun@YOUR_DOMAIN_NAME>' \
|
|
|
|
-F to=YOU@YOUR_DOMAIN_NAME \
|
|
|
|
-F to=bar@example.com \
|
|
|
|
-F subject='Hello' \
|
|
|
|
-F text='Testing some Mailgun awesomeness!'
|
|
|
|
*/
|
|
|
|
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={{secret}}\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:<br>"
|
|
|
|
+ '<br>'
|
|
|
|
+ ' <a href="https://' + state.config.webminDomain + '/login/#/magic={{secret}}">Confirm Device</a><br>'
|
|
|
|
+ '<br>'
|
|
|
|
+ ' <small>or copy and paste this link:</small><br>'
|
|
|
|
+ ' <small>https://' + state.config.webminDomain + '/login/#/magic={{secret}}</small><br>'
|
|
|
|
+ '<br>'
|
|
|
|
+ "({{os_arch}} {{os_platform}} {{os_release}})<br>"
|
|
|
|
+ '<br>'
|
|
|
|
;
|
|
|
|
[ 'id', 'secret', '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);
|
|
|
|
});
|
2018-06-20 09:02:00 +00:00
|
|
|
|
2018-06-19 23:40:58 +00:00
|
|
|
return requestAsync({
|
|
|
|
url: state.config.mailer.url
|
|
|
|
, method: 'POST'
|
|
|
|
, auth: { user: 'api', pass: state.config.mailer.apiKey }
|
|
|
|
, formData: {
|
|
|
|
from: state.config.mailer.from
|
|
|
|
, to: auth.subject
|
|
|
|
, subject: subj
|
|
|
|
, text: text
|
|
|
|
, html: html
|
|
|
|
}
|
|
|
|
}).then(function (resp) {
|
|
|
|
fs.writeFile(path.join(__dirname, 'emails', auth.subject), JSON.stringify(auth), function (err) {
|
|
|
|
if (err) {
|
|
|
|
console.error('[ERROR] in writing auth details');
|
|
|
|
console.error(err);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
console.log("[DEBUG] email was sent, or so they say");
|
|
|
|
console.log(resp.body);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports.pairRequest = function (opts) {
|
2018-06-06 10:58:01 +00:00
|
|
|
console.log("It's auth'n time!");
|
2018-06-06 09:18:13 +00:00
|
|
|
var state = opts.state;
|
2018-06-19 23:40:58 +00:00
|
|
|
var auth = opts.auth;
|
|
|
|
var jwt = require('jsonwebtoken');
|
|
|
|
|
|
|
|
auth.id = crypto.randomBytes(12).toString('hex');
|
|
|
|
auth.secret = crypto.randomBytes(12).toString('hex');
|
|
|
|
//var id = crypto.randomBytes(16).toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
|
2018-06-20 09:02:00 +00:00
|
|
|
|
|
|
|
console.log("[DEBUG] !!state", !!state);
|
|
|
|
console.log("[DEBUG] !!auth", !!auth);
|
2018-06-19 23:40:58 +00:00
|
|
|
return sendMail(state, auth).then(function () {
|
|
|
|
var now = Date.now();
|
|
|
|
var authnToken = {
|
|
|
|
domains: []
|
|
|
|
, ports: []
|
|
|
|
, aud: state.config.webminDomain
|
|
|
|
, iss: Math.round(now / 1000)
|
|
|
|
, id: auth.id
|
|
|
|
, pin: auth.otp
|
|
|
|
, hostname: auth.hostname
|
|
|
|
};
|
|
|
|
_auths[auth.id] = _auths[auth.secret] = {
|
|
|
|
dt: now
|
|
|
|
, authn: jwt.sign(authnToken, state.secret)
|
|
|
|
, pin: auth.otp
|
|
|
|
, id: auth.id
|
|
|
|
, secret: auth.secret
|
|
|
|
};
|
|
|
|
authnToken.jwt = _auths[auth.id].authn;
|
|
|
|
// return empty token which will receive grants upon authorization
|
|
|
|
return authnToken;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
module.exports.pairPin = function (opts) {
|
|
|
|
var state = opts.state;
|
|
|
|
return state.Promise.resolve().then(function () {
|
|
|
|
var pin = opts.pin;
|
|
|
|
var secret = opts.secret;
|
|
|
|
var auth = _auths[secret];
|
|
|
|
|
|
|
|
if (!auth || auth.secret !== opts.secret) {
|
|
|
|
throw new Error("I can't even right now - bad magic link id");
|
|
|
|
}
|
|
|
|
|
|
|
|
// XXX security, we want to check the pin if it's supported serverside,
|
|
|
|
// regardless of what the client sends. This bad logic is just for testing.
|
|
|
|
if (pin && auth.pin && pin !== auth.pin) {
|
|
|
|
throw new Error("I can't even right now - bad device pair pin");
|
|
|
|
}
|
|
|
|
|
2018-06-20 09:02:00 +00:00
|
|
|
auth._paired = true;
|
|
|
|
//delete _auths[auth.id];
|
2018-06-19 23:40:58 +00:00
|
|
|
var hri = require('human-readable-ids').hri;
|
|
|
|
var hrname = hri.random() + '.' + state.config.sharedDomain;
|
|
|
|
var authzToken = {
|
|
|
|
domains: [ hrname ]
|
|
|
|
, ports: [ (1024 + 1) + Math.round(Math.random() * 6300) ]
|
|
|
|
, aud: state.config.webminDomain
|
|
|
|
, iss: Math.round(Date.now() / 1000)
|
|
|
|
, id: auth.id
|
|
|
|
, hostname: auth.hostname
|
|
|
|
};
|
|
|
|
authzToken.jwt = jwt.sign(authzToken, state.secret);
|
|
|
|
fs.writeFile(path.join(__dirname, 'emails', auth.subject + '.data'), JSON.stringify(authzToken), function (err) {
|
|
|
|
if (err) {
|
|
|
|
console.error('[ERROR] in writing token details');
|
|
|
|
console.error(err);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return authzToken;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
module.exports.pairState = function (opts) {
|
|
|
|
var state = opts.state;
|
|
|
|
var auth = opts.auth;
|
|
|
|
var resolve = opts.resolve;
|
|
|
|
var reject = opts.reject;
|
|
|
|
|
|
|
|
// TODO use global interval whenever the number of active links is high
|
|
|
|
var t = setTimeout(function () {
|
|
|
|
console.log("[Magic Link] Timeout for '" + auth.subject + "'");
|
|
|
|
delete _auths[auth.id];
|
|
|
|
var err = new Error("Login Failure: Magic Link was not clicked within 5 minutes");
|
|
|
|
err.code = 'E_LOGIN_TIMEOUT';
|
|
|
|
reject();
|
|
|
|
}, 2 * 60 * 60 * 1000);
|
|
|
|
|
|
|
|
function authorize(pin) {
|
|
|
|
console.log("mighty auth'n ranger!");
|
|
|
|
clearTimeout(t);
|
|
|
|
return module.exports.pairPin({ secret: auth.secret, pin: pin }).then(function (tokenData) {
|
|
|
|
// TODO call state object with socket info rather than resolve
|
|
|
|
resolve(tokenData);
|
|
|
|
return tokenData;
|
|
|
|
}, function (err) {
|
|
|
|
reject(err);
|
|
|
|
return state.Promise.reject(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
_auths[auth.id].resolve = authorize;
|
|
|
|
_auths[auth.id].reject = reject;
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports.authenticate = function (opts) {
|
|
|
|
var jwt = require('jsonwebtoken');
|
2018-06-06 09:18:13 +00:00
|
|
|
var jwtoken = opts.auth;
|
2018-06-19 23:40:58 +00:00
|
|
|
var auth = opts.auth;
|
|
|
|
var state = opts.state;
|
2018-06-06 09:18:13 +00:00
|
|
|
|
2018-06-06 10:58:01 +00:00
|
|
|
if ('object' === typeof auth && /^.+@.+\..+$/.test(auth.subject)) {
|
2018-06-19 23:40:58 +00:00
|
|
|
return module.exports.pairRequest(opts).then(function () {
|
2018-06-06 10:58:01 +00:00
|
|
|
return new state.Promise(function (resolve, reject) {
|
2018-06-19 23:40:58 +00:00
|
|
|
opts.resolve = resolve;
|
|
|
|
opts.reject = reject;
|
|
|
|
module.exports.pairState(opts);
|
2018-06-06 09:18:13 +00:00
|
|
|
});
|
2018-06-06 10:58:01 +00:00
|
|
|
});
|
2018-06-06 09:18:13 +00:00
|
|
|
}
|
|
|
|
|
2018-06-06 10:58:01 +00:00
|
|
|
console.log("just trying a normal token...");
|
2018-06-15 08:45:47 +00:00
|
|
|
var decoded;
|
2018-06-06 09:18:13 +00:00
|
|
|
try {
|
|
|
|
decoded = jwt.decode(jwtoken, { complete: true });
|
|
|
|
} catch(e) {
|
|
|
|
decoded = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return state.defaults.authenticate(opts.auth);
|
|
|
|
};
|
2018-06-19 23:40:58 +00:00
|
|
|
|
2018-06-07 05:26:15 +00:00
|
|
|
//var loaded = false;
|
2018-06-06 10:58:01 +00:00
|
|
|
var express = require('express');
|
|
|
|
var app = express();
|
2018-06-15 08:45:47 +00:00
|
|
|
var staticApp = express();
|
|
|
|
var nowww = require('nowww')();
|
|
|
|
var CORS = require('connect-cors');
|
2018-06-19 23:40:58 +00:00
|
|
|
var bodyParser = require('body-parser');
|
2018-06-20 09:02:00 +00:00
|
|
|
var urls = {
|
|
|
|
pairState: '/api/telebit.cloud/pair_state/:id'
|
|
|
|
};
|
2018-06-15 08:45:47 +00:00
|
|
|
staticApp.use('/', express.static(path.join(__dirname, 'admin')));
|
|
|
|
app.use('/api', CORS({}));
|
2018-06-20 09:02:00 +00:00
|
|
|
app.use('/api', function (req, res, next) {
|
|
|
|
next();
|
|
|
|
req.on('data', function (chunk) {
|
|
|
|
console.log('chunk', chunk.toString());
|
|
|
|
});
|
|
|
|
req.on('end', function () {
|
|
|
|
console.log('end');
|
|
|
|
});
|
|
|
|
});
|
2018-06-19 23:40:58 +00:00
|
|
|
app.use('/api', bodyParser.json());
|
|
|
|
// From Device
|
|
|
|
app.post('/api/telebit.cloud/pair_request', function (req, res) {
|
|
|
|
var auth = req.body;
|
2018-06-20 09:02:00 +00:00
|
|
|
console.log('[ext] pair_request (request)', req.headers);
|
|
|
|
console.log('[ext] pair_request (request)', req.body);
|
|
|
|
module.exports.pairRequest({ state: req._state, auth: auth }).then(function (tokenData) {
|
|
|
|
console.log('[ext] pair_request (response)', tokenData);
|
2018-06-19 23:40:58 +00:00
|
|
|
// res.send({ success: true, message: "pair request sent" });
|
2018-06-20 09:02:00 +00:00
|
|
|
var stateUrl = 'https://' + req._state.config.apiDomain + urls.pairState.replace(/:id/g, tokenData.id);
|
|
|
|
res.statusCode = 201;
|
|
|
|
res.setHeader('Location', stateUrl);
|
|
|
|
res.setHeader('Link', '<' + stateUrl + '>;rel="next"');
|
2018-06-19 23:40:58 +00:00
|
|
|
res.send(tokenData);
|
|
|
|
}, function (err) {
|
2018-06-20 09:02:00 +00:00
|
|
|
console.error(err);
|
|
|
|
res.send({ error: { code: err.code, message: err.toString() } });
|
2018-06-19 23:40:58 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
// From Browser
|
|
|
|
app.post('/api/telebit.cloud/pair_code', function (req, res) {
|
|
|
|
var auth = req.body;
|
|
|
|
return module.exports.pairPin({ secret: auth.magic, pin: auth.pin }).then(function (tokenData) {
|
|
|
|
res.send(tokenData);
|
|
|
|
}, function (err) {
|
|
|
|
res.send({ error: err });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
// From Device (polling)
|
2018-06-20 09:02:00 +00:00
|
|
|
app.get(urls.pairState, function (req, res) {
|
2018-06-19 23:40:58 +00:00
|
|
|
// check if pair is complete
|
|
|
|
// respond immediately if so
|
|
|
|
// wait for a little bit otherwise
|
|
|
|
// respond if/when it completes
|
|
|
|
// or respond after time if it does not complete
|
2018-06-20 09:02:00 +00:00
|
|
|
var auth = _auths[req.params.id];
|
|
|
|
if (!auth) {
|
|
|
|
res.send({ status: 'invalid' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (true === auth.paired) {
|
|
|
|
res.send({
|
|
|
|
status: 'ready', access_token: _auths[req.params.id].jwt
|
|
|
|
, grant: { domains: auth.domains || [], ports: auth.ports || [] }
|
|
|
|
});
|
|
|
|
} else if (false === _auths[req.params.id].paired) {
|
|
|
|
res.send({ status: 'failed', error: { message: "device pairing failed" } });
|
|
|
|
} else {
|
|
|
|
res.send({ status: 'pending' });
|
|
|
|
}
|
2018-06-19 23:40:58 +00:00
|
|
|
});
|
|
|
|
// From Browser
|
|
|
|
app.get('/api/telebit.cloud/magic/:magic/:pin?', function (req, res) {
|
2018-06-15 08:45:47 +00:00
|
|
|
console.log("DEBUG telebit.cloud magic");
|
2018-06-06 10:58:01 +00:00
|
|
|
var tokenData;
|
2018-06-15 08:45:47 +00:00
|
|
|
var magic = req.params.magic || req.query.magic;
|
2018-06-19 23:40:58 +00:00
|
|
|
var pin = req.params.pin || req.query.pin;
|
2018-06-15 09:00:16 +00:00
|
|
|
console.log("DEBUG telebit.cloud magic 1a", magic);
|
2018-06-19 23:40:58 +00:00
|
|
|
if (_auths[magic] && magic === _auths[magic].secret) {
|
2018-06-15 08:45:47 +00:00
|
|
|
console.log("DEBUG telebit.cloud magic 1b");
|
2018-06-19 23:40:58 +00:00
|
|
|
tokenData = _auths[magic].resolve(pin);
|
2018-06-15 08:45:47 +00:00
|
|
|
console.log("DEBUG telebit.cloud magic 1c");
|
|
|
|
res.send(tokenData);
|
2018-06-06 10:58:01 +00:00
|
|
|
} else {
|
2018-06-15 08:45:47 +00:00
|
|
|
console.log("DEBUG telebit.cloud magic 2");
|
2018-06-15 09:00:16 +00:00
|
|
|
res.send({ error: { code: "E_TOKEN", message: "Invalid or expired magic link. (" + magic + ")" } });
|
2018-06-15 08:45:47 +00:00
|
|
|
console.log("DEBUG telebit.cloud magic 2b");
|
2018-06-06 10:58:01 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
module.exports.webadmin = function (state, req, res) {
|
2018-06-07 05:26:15 +00:00
|
|
|
//if (!loaded) { loaded = true; app.use('/', state.defaults.webadmin); }
|
2018-06-06 10:58:01 +00:00
|
|
|
console.log('[DEBUG] extensions webadmin');
|
2018-06-15 08:45:47 +00:00
|
|
|
var host = (req.headers.host || '').toLowerCase().split(':')[0];
|
|
|
|
if (state.config.webminDomain === host) {
|
|
|
|
console.log("DEBUG going to static");
|
|
|
|
staticApp(req, res);
|
|
|
|
return;
|
|
|
|
}
|
2018-06-20 09:02:00 +00:00
|
|
|
if ((state.config.apiDomain || ('api.' + state.config.webminDomain )) === host) {
|
2018-06-15 08:45:47 +00:00
|
|
|
console.log("DEBUG going to api");
|
2018-06-19 23:40:58 +00:00
|
|
|
req._state = state;
|
2018-06-15 08:45:47 +00:00
|
|
|
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.");
|
2018-06-06 10:58:01 +00:00
|
|
|
};
|