Merge branch 'commercial' of https://git.ppl.family/ppl/commercial.telebit-relay.js into commercial

This commit is contained in:
AJ ONeal 2018-07-08 02:57:42 +00:00
commit de9aab8195
15 changed files with 576 additions and 93 deletions

View File

@ -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

View File

@ -0,0 +1,5 @@
servernames: [ 'telebit.cloud' ]
email: 'coolaj86@gmail.com'
agree_tos: true
community_member: false
vhost: /srv/www/:hostname

View File

@ -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

View File

@ -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;
}
});

View File

@ -0,0 +1 @@
_apis

View File

@ -0,0 +1 @@
oauth3.org

View File

@ -0,0 +1 @@
../assets/oauth3.org/_apis/oauth3.org

View File

@ -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

View File

@ -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>

View File

@ -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);

View File

@ -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);
}
});
// anything in the 200 range
if (2 === Math.floor(resp.statusCode / 100)) {
console.log("[DEBUG] email was sent, or so they say");
console.log(resp.body);
} 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 = {};

View File

@ -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,13 +258,24 @@ 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');
// 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
@ -273,6 +289,7 @@ var Server = {
// these aren't needed internally once they're sent
grant.jwt = null;
}
}
/*
if (!Array.isArray(grant.domains) || !grant.domains.length) {
@ -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) {

View File

@ -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) {

View File

@ -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"