Merge branch 'commercial' of https://git.ppl.family/ppl/commercial.telebit-relay.js into commercial
This commit is contained in:
commit
de9aab8195
|
@ -1,21 +1,24 @@
|
|||
email: 'jon@example.com' # must be valid (for certificate recovery and security alerts)
|
||||
agree_tos: true # agree to the Telebit, Greenlock, and Let's Encrypt TOSes
|
||||
community_member: true # receive infrequent relevant updates
|
||||
telemetry: true # contribute to project telemetric data
|
||||
email: coolaj86@gmail.com
|
||||
agree_tos: true
|
||||
community_member: true
|
||||
telemetry: true
|
||||
webmin_domain: example.com
|
||||
shared_domain: xm.pl
|
||||
servernames: # hostnames that direct to the Telebit Relay admin console
|
||||
- telebit.example.com
|
||||
- telebit.example.net
|
||||
vhost: /srv/www/:hostname # load secure websites at this path (uses template string, i.e. /var/www/:hostname/public)
|
||||
api_domain: example.com
|
||||
shared_domain: example.com
|
||||
servernames:
|
||||
- www.example.com
|
||||
- example.com
|
||||
- api.example.com
|
||||
vhost: /srv/www/:hostname
|
||||
greenlock:
|
||||
version: 'draft-11'
|
||||
server: 'https://acme-v02.api.letsencrypt.org/directory'
|
||||
store:
|
||||
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'))"
|
||||
strategy: le-store-certbot
|
||||
config_dir: /opt/telebit-relay/etc/acme
|
||||
secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
mailer:
|
||||
url: 'https://api.mailgun.net/v3/EXAMPLE.COM/messages'
|
||||
api_key: 'key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||
from: 'Example Mailer <MALIER@EXAMPLE.COM>'
|
||||
debug: true
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
servernames: [ 'telebit.cloud' ]
|
||||
email: 'coolaj86@gmail.com'
|
||||
agree_tos: true
|
||||
community_member: false
|
||||
vhost: /srv/www/:hostname
|
|
@ -179,6 +179,9 @@ if [ ! -f "$TELEBIT_RELAY_PATH/etc/$my_app.yml" ]; then
|
|||
sudo bash -c "echo 'email: $my_email' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'secret: $my_secret' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'servernames: [ $my_servername ]' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'webmin_domain: $my_servername' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'api_domain: $my_servername' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "echo 'shared_domain: $my_servername' >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
sudo bash -c "cat $TELEBIT_RELAY_PATH/examples/$my_app.yml.tpl >> $TELEBIT_RELAY_PATH/etc/$my_app.yml"
|
||||
fi
|
||||
|
||||
|
|
|
@ -2,9 +2,24 @@
|
|||
|
||||
var Devices = module.exports;
|
||||
Devices.add = function (store, servername, newDevice) {
|
||||
var devices = store[servername] || [];
|
||||
if (!store[servername]) {
|
||||
store[servername] = [];
|
||||
}
|
||||
var devices = store[servername];
|
||||
devices.push(newDevice);
|
||||
store[servername] = devices;
|
||||
};
|
||||
Devices.alias = function (store, servername, alias) {
|
||||
if (!store[servername]) {
|
||||
store[servername] = [];
|
||||
}
|
||||
if (!store[servername]._primary) {
|
||||
store[servername]._primary = servername;
|
||||
}
|
||||
if (!store[servername].aliases) {
|
||||
store[servername].aliases = {};
|
||||
}
|
||||
store[alias] = store[servername];
|
||||
store[servername].aliases[alias] = true;
|
||||
};
|
||||
Devices.remove = function (store, servername, device) {
|
||||
var devices = store[servername] || [];
|
||||
|
@ -17,9 +32,11 @@ Devices.remove = function (store, servername, device) {
|
|||
return devices.splice(index, 1)[0];
|
||||
};
|
||||
Devices.list = function (store, servername) {
|
||||
// efficient lookup first
|
||||
if (store[servername] && store[servername].length) {
|
||||
return store[servername];
|
||||
return store[servername]._primary && store[store[servername]._primary] || store[servername];
|
||||
}
|
||||
|
||||
// There wasn't an exact match so check any of the wildcard domains, sorted longest
|
||||
// first so the one with the biggest natural match with be found first.
|
||||
var deviceList = [];
|
||||
|
@ -28,10 +45,19 @@ Devices.list = function (store, servername) {
|
|||
}).sort(function (a, b) {
|
||||
return b.length - a.length;
|
||||
}).some(function (pattern) {
|
||||
// '.example.com' = '*.example.com'.split(1)
|
||||
var subPiece = pattern.slice(1);
|
||||
// '.com' = 'sub.example.com'.slice(-4)
|
||||
// '.example.com' = 'sub.example.com'.slice(-12)
|
||||
if (subPiece === servername.slice(-subPiece.length)) {
|
||||
console.log('"'+servername+'" matches "'+pattern+'"');
|
||||
console.log('[Devices.list] "'+servername+'" matches "'+pattern+'"');
|
||||
deviceList = store[pattern];
|
||||
|
||||
// Devices.alias(store, '*.example.com', 'sub.example.com'
|
||||
// '*.example.com' retrieves a reference to 'example.com'
|
||||
// and this reference then also referenced by 'sub.example.com'
|
||||
// Hence this O(n) check is replaced with the O(1) check above
|
||||
Devices.alias(store, pattern, servername);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
_apis
|
|
@ -0,0 +1 @@
|
|||
oauth3.org
|
|
@ -0,0 +1 @@
|
|||
../assets/oauth3.org/_apis/oauth3.org
|
|
@ -0,0 +1,90 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Telebit Account</title>
|
||||
</head>
|
||||
<body>
|
||||
<form class="js-auth-form">
|
||||
<input class="js-auth-subject" type="email"/>
|
||||
<button class="js-auth-submit" type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
<script src="assets/oauth3.org/oauth3.core.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
var OAUTH3 = window.OAUTH3;
|
||||
var oauth3 = OAUTH3.create({
|
||||
host: window.location.host
|
||||
, pathname: window.location.pathname.replace(/\/[^\/]*$/, '/')
|
||||
});
|
||||
var $ = function () { return document.querySelector.apply(document, arguments); }
|
||||
|
||||
function onChangeProvider(providerUri) {
|
||||
// example https://oauth3.org
|
||||
return oauth3.setIdentityProvider(providerUri);
|
||||
}
|
||||
|
||||
// This opens up the login window for the specified provider
|
||||
//
|
||||
function onClickLogin(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO check subject for provider viability
|
||||
return oauth3.authenticate({
|
||||
subject: $('.js-auth-subject').value
|
||||
}).then(function (session) {
|
||||
|
||||
console.info('Authentication was Successful:');
|
||||
console.log(session);
|
||||
|
||||
// You can use the PPID (or preferably a hash of it) as the login for your app
|
||||
// (it securely functions as both username and password which is known only by your app)
|
||||
// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key
|
||||
//
|
||||
console.info('Secure PPID (aka subject):', session.token.sub);
|
||||
|
||||
return oauth3.request({
|
||||
url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid.json'
|
||||
.replace(/:sub/g, session.token.sub)
|
||||
.replace(/:kid/g, session.token.iss)
|
||||
, session: session
|
||||
}).then(function (resp) {
|
||||
console.info("Public Key:");
|
||||
console.log(resp.data);
|
||||
|
||||
return oauth3.request({
|
||||
url: 'https://api.oauth3.org/api/issuer@oauth3.org/acl/profile'
|
||||
, session: session
|
||||
}).then(function (resp) {
|
||||
|
||||
console.info("Inspect Token:");
|
||||
console.log(resp.data);
|
||||
|
||||
return oauth3.request({
|
||||
url: 'https://api.telebit.cloud/api/telebit.cloud/account'
|
||||
, session: session
|
||||
}).then(function (resp) {
|
||||
|
||||
console.info("Telebit Account:");
|
||||
console.log(resp.data);
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}, function (err) {
|
||||
console.error('Authentication Failed:');
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
$('body form.js-auth-form').addEventListener('submit', onClickLogin);
|
||||
onChangeProvider('oauth3.org');
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 8e2e09f5823ae919c615c9c3b21114e01096b1ee
|
|
@ -29,6 +29,9 @@
|
|||
<p>Friends enable friends to share anything, access anywhere, connect anytime.</p>
|
||||
</center>
|
||||
|
||||
<a href="account.html#/login">Login</a>
|
||||
<a href="account.html#/create_account">Create Account</a>
|
||||
|
||||
<div style="width: 800px; margin: auto;">
|
||||
<div>
|
||||
<h2>Share and Test over HTTPS</h2>
|
||||
|
|
|
@ -19,7 +19,7 @@ function checkStatus() {
|
|||
}
|
||||
if ('complete' === data.status) {
|
||||
setTimeout(function () {
|
||||
window.document.body.innerHTML += ('<img src="https://' + domainname + '/_apis/telebit.cloud/clear.gif">');
|
||||
//window.document.body.innerHTML += ('<img src="https://' + domainname + '/_apis/telebit.cloud/clear.gif">');
|
||||
// TODO once this is loaded (even error) Let's Encrypt is done,
|
||||
// then it's time to redirect to the domain. Yay!
|
||||
}, 1 * 1000);
|
||||
|
|
|
@ -6,7 +6,16 @@ var util = require('util');
|
|||
var crypto = require('crypto');
|
||||
var escapeHtml = require('escape-html');
|
||||
var jwt = require('jsonwebtoken');
|
||||
var requestAsync = util.promisify(require('request'));
|
||||
var requestAsync = util.promisify(require('@coolaj86/urequest'));
|
||||
var readFileAsync = util.promisify(fs.readFile);
|
||||
var mkdirpAsync = util.promisify(require('mkdirp'));
|
||||
|
||||
var PromiseA;
|
||||
try {
|
||||
PromiseA = require('bluebird');
|
||||
} catch(e) {
|
||||
PromiseA = global.Promise;
|
||||
}
|
||||
|
||||
var _auths = module.exports._auths = {};
|
||||
var Auths = {};
|
||||
|
@ -77,6 +86,72 @@ Auths._clean = function () {
|
|||
});
|
||||
};
|
||||
|
||||
var sfs = require('safe-replace');
|
||||
var Accounts = {};
|
||||
Accounts._getTokenId = function (auth) {
|
||||
return auth.data.sub + '@' + (auth.data.iss||'').replace(/\/|\\/g, '-');
|
||||
};
|
||||
Accounts._accPath = function (req, accId) {
|
||||
return path.join(req._state.config.accountsDir, 'self', accId);
|
||||
};
|
||||
Accounts._subPath = function (req, id) {
|
||||
return path.join(req._state.config.accountsDir, 'oauth3', id);
|
||||
};
|
||||
Accounts._setSub = function (req, id, subData) {
|
||||
var subpath = Accounts._subPath(req, id);
|
||||
return mkdirpAsync(subpath).then(function () {
|
||||
return sfs.writeFileAsync(path.join(subpath, 'index.json'), JSON.stringify(subData));
|
||||
});
|
||||
};
|
||||
Accounts._setAcc = function (req, accId, acc) {
|
||||
var accpath = Accounts._accPath(req, accId);
|
||||
return mkdirpAsync(accpath).then(function () {
|
||||
return sfs.writeFileAsync(path.join(accpath, 'index.json'), JSON.stringify(acc));
|
||||
});
|
||||
};
|
||||
Accounts.create = function (req) {
|
||||
var id = Accounts._getTokenId(req.auth);
|
||||
var acc = {
|
||||
sub: crypto.randomBytes(16).toString('hex')
|
||||
// TODO use something from the request to know which of the domains to use
|
||||
, iss: req._state.config.webminDomain
|
||||
, contacts: []
|
||||
};
|
||||
var accId = Accounts._getTokenId(acc);
|
||||
acc.id = accId;
|
||||
|
||||
// TODO notify any non-authorized accounts that they've been added?
|
||||
return Accounts.getBySub(req).then(function (subData) {
|
||||
subData.accounts.push({ type: 'self', id: accId });
|
||||
acc.contacts.push({ type: 'oauth3', id: subData.id, sub: subData.sub, iss: subData.iss });
|
||||
return Accounts._setSub(req, id, subData).then(function () {
|
||||
return Accounts._setAcc(req, accId, acc).then(function () {
|
||||
return acc;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
/*
|
||||
// TODO an owner of an asset can give permission to another entity
|
||||
// but that does not mean that that owner has access to that entity's things
|
||||
// Example:
|
||||
// A 3rd party login's email verification cannot be trusted for auth
|
||||
// Only 1st party verifications can be trusted for authorization
|
||||
Accounts.link = function (req) {
|
||||
};
|
||||
*/
|
||||
Accounts.getBySub = function (req) {
|
||||
var id = Accounts._getTokenId(req.auth);
|
||||
var subpath = Accounts._subPath(req, id);
|
||||
return readFileAsync(path.join(subpath, 'index.json'), 'utf8').then(function (text) {
|
||||
return JSON.parse(text);
|
||||
}, function (/*err*/) {
|
||||
return null;
|
||||
}).then(function (links) {
|
||||
return links || { id: id, sub: req.auth.sub, iss: req.auth.iss, accounts: [] };
|
||||
});
|
||||
};
|
||||
|
||||
function sendMail(state, auth) {
|
||||
console.log('[DEBUG] ext auth', auth);
|
||||
/*
|
||||
|
@ -132,8 +207,188 @@ function sendMail(state, auth) {
|
|||
console.error(err);
|
||||
}
|
||||
});
|
||||
console.log("[DEBUG] email was sent, or so they say");
|
||||
console.log(resp.body);
|
||||
// anything in the 200 range
|
||||
if (2 === Math.floor(resp.statusCode / 100)) {
|
||||
console.log("[DEBUG] email was sent, or so they say");
|
||||
} else {
|
||||
console.error("[Error] email failed to send, or so they say:");
|
||||
console.error(resp.headers);
|
||||
console.error(resp.statusCode, resp.body);
|
||||
return PromiseA.reject(new Error("Error sending email: " + resp.statusCode + " " + resp.body));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO replace with OAuth3 function
|
||||
function oauth3Auth(req, res, next) {
|
||||
var jwt = require('jsonwebtoken');
|
||||
var verifyJwt = util.promisify(jwt.verify);
|
||||
var token = (req.headers.authorization||'').replace(/^bearer /i, '');
|
||||
var auth;
|
||||
var authData;
|
||||
|
||||
if (!token) {
|
||||
res.send({
|
||||
error: {
|
||||
code: "E_NOAUTH"
|
||||
, message: "no authorization header"
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
authData = jwt.decode(token, { complete: true });
|
||||
} catch(e) {
|
||||
authData = null;
|
||||
}
|
||||
|
||||
if (!authData) {
|
||||
res.send({
|
||||
error: {
|
||||
code: "E_PARSEAUTH"
|
||||
, message: "could not parse authorization header as JWT"
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auth = authData.payload;
|
||||
if (!auth.sub && ('*' === auth.aud || '*' === auth.azp)) {
|
||||
res.send({
|
||||
error: {
|
||||
code: "E_NOIMPL"
|
||||
, message: "missing 'sub' and a wildcard 'azp' or 'aud' indicates that this is an exchange token,"
|
||||
+ " however, this app has not yet implemented opaque token exchange"
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ([ 'sub', 'iss' ].some(function (key) {
|
||||
if ('string' !== typeof auth[key]) {
|
||||
res.send({
|
||||
error: {
|
||||
code: "E_PARSEAUTH"
|
||||
, message: "could not read property '" + key + "' of authorization token"
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
})) { return; }
|
||||
if ([ 'kid' ].some(function (key) {
|
||||
if (/\/|\\/.test(authData.header[key])) {
|
||||
res.send({
|
||||
error: {
|
||||
code: "E_PARSESUBJECT"
|
||||
, message: "'" + key + "' `" + JSON.stringify(authData.header[key]) + "' contains invalid characters"
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
})) { return; }
|
||||
if ([ 'sub', 'kid' ].some(function (key) {
|
||||
if (/\/|\\/.test(auth[key])) {
|
||||
res.send({
|
||||
error: {
|
||||
code: "E_PARSESUBJECT"
|
||||
, message: "'" + key + "' `" + JSON.stringify(auth[key]) + "' contains invalid characters"
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
})) { return; }
|
||||
|
||||
// TODO needs to work with app:// and custom://
|
||||
function prefixHttps(str) {
|
||||
return (str||'').replace(/^(https?:\/\/)?/i, 'https://');
|
||||
}
|
||||
|
||||
var url = require('url');
|
||||
var discoveryUrl = url.resolve(prefixHttps(auth.iss), '_apis/oauth3.org/index.json');
|
||||
console.log('discoveryUrl: ', discoveryUrl, auth.iss);
|
||||
return requestAsync({
|
||||
url: discoveryUrl
|
||||
, json: true
|
||||
}).then(function (resp) {
|
||||
|
||||
// TODO
|
||||
// it may be necessary to exchange the token,
|
||||
|
||||
if (200 !== resp.statusCode || 'object' !== typeof resp.body || !resp.body.retrieve_jwk
|
||||
|| 'string' !== typeof resp.body.retrieve_jwk.url || 'string' !== typeof resp.body.api) {
|
||||
res.send({
|
||||
error: {
|
||||
code: "E_NOTFOUND"
|
||||
, message: resp.statusCode + ": issuer `" + JSON.stringify(auth.iss)
|
||||
+ "' does not declare 'api' & 'retrieve_key' and hence the token you provided cannot be verified."
|
||||
, _status: resp.statusCode
|
||||
, _url: discoveryUrl
|
||||
, _body: resp.body
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
var keyUrl = url.resolve(
|
||||
prefixHttps(resp.body.api).replace(/:hostname/g, auth.iss)
|
||||
, resp.body.retrieve_jwk.url
|
||||
.replace(/:hostname/g, auth.iss)
|
||||
.replace(/:sub/g, auth.sub)
|
||||
// TODO
|
||||
.replace(/:kid/g, authData.header.kid || auth.iss)
|
||||
);
|
||||
console.log('keyUrl: ', keyUrl);
|
||||
return requestAsync({
|
||||
url: keyUrl
|
||||
, json: true
|
||||
}).then(function (resp) {
|
||||
var jwk = resp.body;
|
||||
if (200 !== resp.statusCode || 'object' !== typeof resp.body) {
|
||||
//headers.authorization
|
||||
res.send({
|
||||
error: {
|
||||
code: "E_NOTFOUND"
|
||||
, message: resp.statusCode + ": did not retrieve public key from `" + JSON.stringify(auth.iss)
|
||||
+ "' for token validation and hence the token you provided cannot be verified."
|
||||
, _status: resp.statusCode
|
||||
, _url: keyUrl
|
||||
, _body: resp.body
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var pubpem;
|
||||
try {
|
||||
pubpem = require('jwk-to-pem')(jwk, { private: false });
|
||||
} catch(e) {
|
||||
pubpem = null;
|
||||
}
|
||||
return verifyJwt(token, pubpem, {
|
||||
algorithms: [ 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512' ]
|
||||
}).then(function (decoded) {
|
||||
if (!decoded) {
|
||||
res.send({
|
||||
error: {
|
||||
code: "E_UNVERIFIED"
|
||||
, message: "retrieved jwk does not verify provided token."
|
||||
, _jwk: jwk
|
||||
}
|
||||
});
|
||||
}
|
||||
req.auth = {};
|
||||
req.auth.jwt = token;
|
||||
req.auth.data = auth;
|
||||
next();
|
||||
});
|
||||
});
|
||||
}, function (err) {
|
||||
res.send({
|
||||
error: {
|
||||
code: err.code || "E_GENERIC"
|
||||
, message: err.toString()
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -156,20 +411,24 @@ module.exports.pairRequest = function (opts) {
|
|||
, aud: state.config.webminDomain
|
||||
, iat: Math.round(now / 1000)
|
||||
, id: authReq.id
|
||||
, sub: authReq.subject
|
||||
, pin: pin
|
||||
, hostname: authReq.hostname
|
||||
};
|
||||
auth = {
|
||||
id: authReq.id
|
||||
, secret: authReq.secret
|
||||
, subject: authReq.subject
|
||||
, pin: pin
|
||||
, dt: now
|
||||
, exp: now + (2 * 60 * 60 * 1000)
|
||||
, authnData: authnData
|
||||
, authn: jwt.sign(authnData, state.secret)
|
||||
, request: authReq
|
||||
};
|
||||
|
||||
// Setting extra authnData
|
||||
auth.authn = jwt.sign(authnData, state.secret);
|
||||
authnData.jwt = auth.authn;
|
||||
auth.authnData = authnData;
|
||||
Auths.set(auth, authReq.id, authReq.secret);
|
||||
return authnData;
|
||||
});
|
||||
|
@ -179,16 +438,23 @@ module.exports.pairPin = function (opts) {
|
|||
return state.Promise.resolve().then(function () {
|
||||
var pin = opts.pin;
|
||||
var secret = opts.secret;
|
||||
var auth = Auths.getBySecretAndPin(secret, pin);
|
||||
var auth = Auths.getBySecret(secret);
|
||||
|
||||
console.log('[pairPin] validating secret and pin');
|
||||
if (!auth) {
|
||||
throw new Error("I can't even right now - bad magic link or pairing code");
|
||||
throw new Error("Invalid magic link token '" + secret + "'");
|
||||
}
|
||||
auth = Auths.getBySecretAndPin(secret, pin);
|
||||
if (!auth) {
|
||||
throw new Error("Invalid pairing code '" + pin + "' for magic link token '" + secret + "'");
|
||||
}
|
||||
|
||||
if (auth._offered) {
|
||||
console.log('[pairPin] already has offer to return');
|
||||
return auth._offered;
|
||||
}
|
||||
|
||||
console.log('[pairPin] generating offer');
|
||||
var hri = require('human-readable-ids').hri;
|
||||
var hrname = hri.random() + '.' + state.config.sharedDomain;
|
||||
// TODO check used / unused names and ports
|
||||
|
@ -202,9 +468,14 @@ module.exports.pairPin = function (opts) {
|
|||
};
|
||||
var pathname = path.join(__dirname, 'emails', auth.subject + '.' + hrname + '.data');
|
||||
auth.authz = jwt.sign(authzData, state.secret);
|
||||
auth.authzData = authzData;
|
||||
authzData.jwt = auth.authz;
|
||||
auth._offered = authzData;
|
||||
if (auth.resolve) {
|
||||
console.log('[pairPin] resolving');
|
||||
auth.resolve(auth);
|
||||
} else {
|
||||
console.log('[pairPin] not resolvable');
|
||||
}
|
||||
fs.writeFile(pathname, JSON.stringify(authzData), function (err) {
|
||||
if (err) {
|
||||
|
@ -212,16 +483,32 @@ module.exports.pairPin = function (opts) {
|
|||
console.error(err);
|
||||
}
|
||||
});
|
||||
auth._offered = authzData;
|
||||
return authzData;
|
||||
});
|
||||
};
|
||||
|
||||
// From a WS connection
|
||||
module.exports.authHelper = function (meta) {
|
||||
console.log('[authHelper] 1');
|
||||
var state = meta.state;
|
||||
console.log('[authHelper] 2');
|
||||
return state.Promise.resolve().then(function () {
|
||||
console.log('[authHelper] 3');
|
||||
var auth = meta.session;
|
||||
console.log('[authHelper] 4', auth);
|
||||
if (!auth || 'string' !== typeof auth.authz || 'object' !== typeof auth.authzData) {
|
||||
console.log('[authHelper] 5');
|
||||
console.error("[SANITY FAIL] should not complete auth without authz data and access_token");
|
||||
console.error(auth);
|
||||
return;
|
||||
}
|
||||
console.log("[authHelper] passing authzData right along", auth.authzData);
|
||||
return auth.authzData;
|
||||
});
|
||||
};
|
||||
// opts = { state: state, auth: auth_request OR access_token }
|
||||
module.exports.authenticate = function (opts) {
|
||||
var jwt = require('jsonwebtoken');
|
||||
var jwtoken = opts.auth;
|
||||
var authReq = opts.auth;
|
||||
var state = opts.state;
|
||||
var auth;
|
||||
var decoded;
|
||||
|
@ -241,7 +528,6 @@ module.exports.authenticate = function (opts) {
|
|||
// this will cause the websocket to disconnect
|
||||
|
||||
auth.resolve = function (auth) {
|
||||
opts.auth = auth.authz;
|
||||
auth.resolve = null;
|
||||
auth.reject = null;
|
||||
// NOTE XXX: This is premature in the sense that we can't be 100% sure
|
||||
|
@ -249,7 +535,12 @@ module.exports.authenticate = function (opts) {
|
|||
// sort of check that the client actually received the token
|
||||
// (i.e. when the grant event gets an ack)
|
||||
auth._claimed = true;
|
||||
return state.defaults.authenticate(opts.auth).then(resolve);
|
||||
// this is probably not necessary anymore
|
||||
opts.auth = auth.authz;
|
||||
return module.exports.authHelper({
|
||||
state: state
|
||||
, session: auth
|
||||
}).then(resolve);
|
||||
};
|
||||
auth.reject = function (err) {
|
||||
auth.resolve = null;
|
||||
|
@ -261,41 +552,43 @@ module.exports.authenticate = function (opts) {
|
|||
return auth.promise;
|
||||
}
|
||||
|
||||
if ('object' === typeof authReq && /^.+@.+\..+$/.test(authReq.subject)) {
|
||||
console.log("[ext token] Looks Like Auth Object");
|
||||
// Promise Authz on Auth Creds
|
||||
// TODO: remove
|
||||
if ('object' === typeof opts.auth && /^.+@.+\..+$/.test(opts.auth.subject)) {
|
||||
console.log("[wss.ext.authenticate] [1] Request Pair for Credentials");
|
||||
return module.exports.pairRequest(opts).then(function (authnData) {
|
||||
console.log("[ext token] Promises Like Auth Object");
|
||||
console.log("[wss.ext.authenticate] [2] Promise Authz on Pair Complete");
|
||||
var auth = Auths.get(authnData.id);
|
||||
return getPromise(auth);
|
||||
//getPromise(auth);
|
||||
//return state.defaults.authenticate(authnData.jwt);
|
||||
});
|
||||
}
|
||||
|
||||
console.log("[ext token] Trying Token Parse");
|
||||
try {
|
||||
decoded = jwt.decode(jwtoken, { complete: true });
|
||||
decoded = jwt.decode(opts.auth, { complete: true });
|
||||
auth = Auths.get(decoded.payload.id);
|
||||
} catch(e) {
|
||||
console.log("[ext token] Token Did Not Parse");
|
||||
console.log("[wss.ext.authenticate] [Error] could not parse token");
|
||||
decoded = null;
|
||||
}
|
||||
|
||||
console.log("[ext token] decoded auth token:");
|
||||
console.log("[wss.ext.authenticate] incoming token decoded:");
|
||||
console.log(decoded);
|
||||
|
||||
if (!auth) {
|
||||
console.log("[ext token] did not find auth object");
|
||||
console.log("[wss.ext.authenticate] no session / auth handshake. Pass to default auth");
|
||||
return state.defaults.authenticate(opts.auth);
|
||||
}
|
||||
|
||||
// TODO technically this could leak the token through a timing attack
|
||||
// but it would require already knowing the semi-secret id and having
|
||||
// completed the pair code
|
||||
if (auth && (auth.authn === jwtoken || auth.authz === jwtoken)) {
|
||||
if (auth.authn === opts.auth || auth.authz === opts.auth) {
|
||||
if (!auth.authz) {
|
||||
console.log("[ext token] Promise Authz");
|
||||
console.log("[wss.ext.authenticate] Create authz promise and passthru");
|
||||
return getPromise(auth);
|
||||
}
|
||||
|
||||
console.log("[ext token] Use Available Authz");
|
||||
// If they used authn but now authz is available, use authz
|
||||
// (i.e. connects, but no domains or ports)
|
||||
opts.auth = auth.authz;
|
||||
|
@ -304,8 +597,8 @@ module.exports.authenticate = function (opts) {
|
|||
auth._claimed = true;
|
||||
}
|
||||
|
||||
console.log("[ext token] Continue With Auth Token");
|
||||
return state.defaults.authenticate(opts.auth);
|
||||
console.log("[wss.ext.authenticate] Already using authz, skipping promise");
|
||||
return module.exports.authHelper({ state: state, session: auth });
|
||||
};
|
||||
|
||||
//var loaded = false;
|
||||
|
@ -319,9 +612,36 @@ var urls = {
|
|||
pairState: '/api/telebit.cloud/pair_state/:id'
|
||||
};
|
||||
staticApp.use('/', express.static(path.join(__dirname, 'admin')));
|
||||
app.use('/api', CORS({}));
|
||||
app.use('/api', CORS({
|
||||
credentials: true
|
||||
, headers: [ 'Authorization', 'X-Requested-With', 'X-HTTP-Method-Override', 'Content-Type', 'Accept' ]
|
||||
}));
|
||||
app.use('/api', bodyParser.json());
|
||||
|
||||
app.use('/api/telebit.cloud/account', oauth3Auth);
|
||||
app.get('/api/telebit.cloud/account', function (req, res) {
|
||||
Accounts.getBySub(req).then(function (subData) {
|
||||
res.send(subData);
|
||||
}, function (err) {
|
||||
res.send({
|
||||
error: {
|
||||
code: err.code || "E_GENERIC"
|
||||
, message: err.toString()
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
app.post('/api/telebit.cloud/account', function (req, res) {
|
||||
return Accounts.create(req).then(function (acc) {
|
||||
res.send({
|
||||
success: true
|
||||
, id: acc.id
|
||||
, sub: acc.sub
|
||||
, iss: acc.iss
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// From Device (which knows id, but not secret)
|
||||
app.post('/api/telebit.cloud/pair_request', function (req, res) {
|
||||
var auth = req.body;
|
||||
|
@ -345,7 +665,7 @@ app.post('/api/telebit.cloud/pair_request', function (req, res) {
|
|||
app.get('/api/telebit.cloud/pair_request/:secret', function (req, res) {
|
||||
var secret = req.params.secret;
|
||||
var auth = Auths.getBySecret(secret);
|
||||
var crypto = require('crypto');
|
||||
//var crypto = require('crypto');
|
||||
var response = {};
|
||||
|
||||
|
||||
|
|
102
lib/relay.js
102
lib/relay.js
|
@ -1,10 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
var url = require('url');
|
||||
var PromiseA = require('bluebird');
|
||||
var sni = require('sni');
|
||||
var Packer = require('proxy-packer');
|
||||
var PortServers = {};
|
||||
var PromiseA;
|
||||
try {
|
||||
PromiseA = require('bluebird');
|
||||
} catch(e) {
|
||||
PromiseA = global.Promise;
|
||||
}
|
||||
|
||||
function timeoutPromise(duration) {
|
||||
return new PromiseA(function (resolve) {
|
||||
|
@ -240,10 +245,10 @@ var Server = {
|
|||
|
||||
return result || srv.socketId;
|
||||
}
|
||||
, onAuth: function onAuth(state, srv, newAuth, grant) {
|
||||
, onAuth: function onAuth(state, srv, rawAuth, grant) {
|
||||
console.log('\n[relay.js] onAuth');
|
||||
console.log(newAuth);
|
||||
console.log(grant);
|
||||
console.log(rawAuth);
|
||||
//console.log(grant);
|
||||
//var stringauth;
|
||||
var err;
|
||||
if (!grant || 'object' !== typeof grant) {
|
||||
|
@ -253,25 +258,37 @@ var Server = {
|
|||
return state.Promise.reject(err);
|
||||
}
|
||||
|
||||
if ('string' !== typeof newAuth) {
|
||||
newAuth = JSON.stringify(newAuth);
|
||||
if ('string' !== typeof rawAuth) {
|
||||
rawAuth = JSON.stringify(rawAuth);
|
||||
}
|
||||
|
||||
console.log('check for upgrade token');
|
||||
if (grant.jwt && newAuth !== grant.jwt) {
|
||||
console.log('new token to send back');
|
||||
// Access Token
|
||||
Server.sendTunnelMsg(
|
||||
srv
|
||||
, null
|
||||
, [ 3
|
||||
, 'access_token'
|
||||
, { jwt: grant.jwt }
|
||||
]
|
||||
, 'control'
|
||||
);
|
||||
// these aren't needed internally once they're sent
|
||||
grant.jwt = null;
|
||||
// TODO don't fire the onAuth event on non-authz updates
|
||||
if (!grant.jwt && !(grant.domains||[]).length && !(grant.ports||[]).length) {
|
||||
console.log("[onAuth] nothing to offer at all");
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('[onAuth] check for upgrade token');
|
||||
//console.log(grant);
|
||||
if (grant.jwt) {
|
||||
if (rawAuth !== grant.jwt) {
|
||||
console.log('[onAuth] new token to send back');
|
||||
}
|
||||
// TODO only send token when new
|
||||
if (true) {
|
||||
// Access Token
|
||||
Server.sendTunnelMsg(
|
||||
srv
|
||||
, null
|
||||
, [ 3
|
||||
, 'access_token'
|
||||
, { jwt: grant.jwt }
|
||||
]
|
||||
, 'control'
|
||||
);
|
||||
// these aren't needed internally once they're sent
|
||||
grant.jwt = null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -288,18 +305,20 @@ var Server = {
|
|||
return state.Promise.reject(err);
|
||||
}
|
||||
|
||||
console.log('strolling through pleasantries');
|
||||
console.log('[onAuth] strolling through pleasantries');
|
||||
// Add the custom properties we need to manage this remote, then add it to all the relevant
|
||||
// domains and the list of all this websocket's grants.
|
||||
grant.domains.forEach(function (domainname) {
|
||||
console.log('add', domainname, 'to device lists');
|
||||
srv.domainsMap[domainname] = true;
|
||||
Devices.add(state.deviceLists, domainname, srv);
|
||||
// TODO allow subs to go to individual devices
|
||||
Devices.alias(state.deviceLists, domainname, '*.' + domainname);
|
||||
});
|
||||
srv.domains = Object.keys(srv.domainsMap);
|
||||
srv.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || srv.domains.join(',');
|
||||
grant.currentDesc = (grant.device && (grant.device.id || grant.device.hostname)) || grant.domains.join(',');
|
||||
grant.srv = srv;
|
||||
//grant.srv = srv;
|
||||
//grant.ws = srv.ws;
|
||||
//grant.upgradeReq = srv.upgradeReq;
|
||||
grant.clients = {};
|
||||
|
@ -344,7 +363,7 @@ var Server = {
|
|||
}
|
||||
grant.ports.forEach(openPort);
|
||||
|
||||
srv.grants[newAuth] = grant;
|
||||
srv.grants[rawAuth] = grant;
|
||||
console.info("[ws] authorized", srv.socketId, "for", grant.currentDesc);
|
||||
|
||||
console.log('notify of grants', grant.domains, grant.ports);
|
||||
|
@ -416,31 +435,35 @@ var Server = {
|
|||
process.nextTick(function () { conn.resume(); });
|
||||
});
|
||||
}
|
||||
, addToken: function addToken(state, srv, newAuth) {
|
||||
console.log("addToken", newAuth);
|
||||
if (srv.grants[newAuth]) {
|
||||
, addToken: function addToken(state, srv, rawAuth) {
|
||||
console.log("[addToken]", rawAuth);
|
||||
if (srv.grants[rawAuth]) {
|
||||
console.log("addToken - duplicate");
|
||||
// return { message: "token sent multiple times", code: "E_TOKEN_REPEAT" };
|
||||
return state.Promise.resolve(null);
|
||||
}
|
||||
|
||||
return state.authenticate({ auth: newAuth }).then(function (authnToken) {
|
||||
|
||||
console.log('\n[relay.js] newAuth');
|
||||
console.log(newAuth);
|
||||
return state.authenticate({ auth: rawAuth }).then(function (validatedTokenData) {
|
||||
console.log('\n[relay.js] rawAuth');
|
||||
console.log(rawAuth);
|
||||
|
||||
console.log('\n[relay.js] authnToken');
|
||||
console.log(authnToken);
|
||||
console.log(validatedTokenData);
|
||||
|
||||
if (authnToken.id) {
|
||||
state.srvs[authnToken.id] = state.srvs[authnToken.id] || {};
|
||||
state.srvs[authnToken.id].updateAuth = function (validToken) {
|
||||
return Server.onAuth(state, srv, newAuth, validToken);
|
||||
// For tracking state between token exchanges
|
||||
// and tacking on extra attributes (i.e. for extensions)
|
||||
// TODO close on delete
|
||||
if (!state.srvs[validatedTokenData.id]) {
|
||||
state.srvs[validatedTokenData.id] = {};
|
||||
}
|
||||
if (!state.srvs[validatedTokenData.id].updateAuth) {
|
||||
// be sure to always pass latest srv since the connection may change
|
||||
// and reuse the same token
|
||||
state.srvs[validatedTokenData.id].updateAuth = function (srv, validatedTokenData) {
|
||||
return Server.onAuth(state, srv, rawAuth, validatedTokenData);
|
||||
};
|
||||
}
|
||||
|
||||
// will return rejection if necessary
|
||||
return state.srvs[authnToken.id].updateAuth(authnToken);
|
||||
state.srvs[validatedTokenData.id].updateAuth(srv, validatedTokenData);
|
||||
});
|
||||
}
|
||||
, removeToken: function removeToken(state, srv, jwtoken) {
|
||||
|
@ -587,6 +610,7 @@ module.exports.create = function (state) {
|
|||
});
|
||||
|
||||
if (initToken) {
|
||||
console.log('[wss.onConnection] token provided in http headers');
|
||||
return Server.addToken(state, srv, initToken).then(function () {
|
||||
Server.init(state, srv);
|
||||
}).catch(function (err) {
|
||||
|
|
|
@ -46,6 +46,12 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
function tryTls() {
|
||||
var vhost;
|
||||
|
||||
if (!servername) {
|
||||
if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); }
|
||||
deferData('httpsInvalid');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.servernames.length) {
|
||||
console.info("[Setup] https => admin => setup => (needs bogus tls certs to start?)");
|
||||
deferData('httpsSetupServer');
|
||||
|
@ -62,12 +68,6 @@ module.exports.createTcpConnectionHandler = function (state) {
|
|||
console.log("TODO: use www bare redirect");
|
||||
}
|
||||
|
||||
if (!servername) {
|
||||
if (state.debug) { console.log("No SNI was given, so there's nothing we can do here"); }
|
||||
deferData('httpsInvalid');
|
||||
return;
|
||||
}
|
||||
|
||||
function run() {
|
||||
var nextDevice = Devices.next(state.deviceLists, servername);
|
||||
if (!nextDevice) {
|
||||
|
|
11
package.json
11
package.json
|
@ -37,8 +37,7 @@
|
|||
},
|
||||
"homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js",
|
||||
"dependencies": {
|
||||
"@coolaj86/urequest": "^1.1.1",
|
||||
"bluebird": "^3.5.1",
|
||||
"@coolaj86/urequest": "^1.3.2",
|
||||
"body-parser": "^1.18.3",
|
||||
"cluster-store": "^2.0.8",
|
||||
"connect-cors": "^0.5.6",
|
||||
|
@ -48,16 +47,22 @@
|
|||
"greenlock": "^2.2.4",
|
||||
"human-readable-ids": "^1.0.4",
|
||||
"js-yaml": "^3.11.0",
|
||||
"jsonwebtoken": "^8.2.1",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"jwk-to-pem": "^2.0.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"nowww": "^1.2.1",
|
||||
"proxy-packer": "^1.4.3",
|
||||
"recase": "^1.0.4",
|
||||
"redirect-https": "^1.1.5",
|
||||
"request": "^2.87.0",
|
||||
"safe-replace": "^1.0.3",
|
||||
"serve-static": "^1.13.2",
|
||||
"sni": "^1.0.0",
|
||||
"ws": "^5.1.1"
|
||||
},
|
||||
"trulyOptionalDependencies": {
|
||||
"bluebird": "^3.5.1"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"engines": {
|
||||
"node": "10.2.1"
|
||||
|
|
Loading…
Reference in New Issue