dns check complete

This commit is contained in:
AJ ONeal 2018-08-19 07:33:03 +00:00
parent 2819117f10
commit 2564b750e6
5 changed files with 263 additions and 40 deletions

View File

@ -16,16 +16,25 @@
<div v-if="hasAccount">
<h1>Account</h1>
<form v-on:submit="challengeDns()">
<form v-on:submit.prevent="challengeDns()">
Add a custom domain:
<input v-model="newDomain" placeholder="example.com" type="text" required/>
<button type="submit">Next</button>
</form>
<form v-on:submit="challengeEmail()">
<form v-on:submit.prevent="challengeEmail()">
Authorize another email:
<input v-model="newEmail" placeholder="jon@example.com" type="email" required/>
<button type="submit">Next</button>
</form>
<h3>Claims</h3>
<ol>
<li v-for="claim in claims">
<span>{{ claim.value }}</span>
<span v-if="'dns' === claim.type">TXT _claim-challenge.{{ claim.value }}: {{ claim.challenge }}</span>
<button v-on:click.prevent="checkDns(claim)">Check</button>
</li>
</ol>
<h3>Domains</h3>
<ol>
<li v-for="domain in domains">
{{ domain }}

View File

@ -1,3 +1,4 @@
/*global Vue*/
(function () {
'use strict';
var OAUTH3 = window.OAUTH3;
@ -5,33 +6,7 @@
host: window.location.host
, pathname: window.location.pathname.replace(/\/[^\/]*$/, '/')
});
var $ = function () { return document.querySelector.apply(document, arguments); }
var vueData = {
domains: []
, newDomain: null
, newEmail: null
, hasAccount: false
, token: null
};
var app = new Vue({
el: '.v-app'
, data: vueData
, methods: {
challengeDns: function () {
console.log("A new (DNS) challenger!", vueData);
}
, challengeEmail: function () {
console.log("A new (Email) challenger!", vueData);
}
}
});
function listStuff(data) {
//window.alert("TODO: show authorized devices, domains, and connectivity information");
vueData.hasAccount = true;
vueData.domains = data.domains;
}
var $ = function () { return document.querySelector.apply(document, arguments); };
var sessionStr = localStorage.getItem('session');
var session;
if (sessionStr) {
@ -42,6 +17,49 @@
}
}
var vueData = {
claims: []
, domains: []
, newDomain: null
, newDomainWildcard: false
, newEmail: null
, hasAccount: false
, token: null
};
var app = new Vue({
el: '.v-app'
, data: vueData
, methods: {
challengeDns: function () {
return oauth3.request({
url: 'https://api.' + location.hostname + '/api/telebit.cloud/account/authorizations/new'
, method: 'POST'
, session: session
, data: { type: 'dns', value: vueData.newDomain, wildcard: vueData.newDomainWildcard }
});
}
, checkDns: function (claim) {
return oauth3.request({
url: 'https://api.' + location.hostname + '/api/telebit.cloud/account/authorizations/new/:value/:challenge'
.replace(/:value/g, claim.value)
.replace(/:challenge/g, claim.challenge)
, method: 'POST'
, session: session
});
}
, challengeEmail: function () {
console.log("A new (Email) challenger!", vueData);
}
}
});
app = null;
function listStuff(data) {
//window.alert("TODO: show authorized devices, domains, and connectivity information");
vueData.hasAccount = true;
vueData.domains = data.domains;
vueData.claims = data.claims;
}
function loadAccount(session) {
return oauth3.request({
url: 'https://api.' + location.hostname + '/api/telebit.cloud/account'
@ -64,7 +82,7 @@
, method: 'POST'
, session: session
, body: {
email: email
email: vueData.newEmail
}
}).then(function (resp) {
listStuff(resp);
@ -124,7 +142,7 @@
console.log(resp.data);
localStorage.setItem('session', JSON.stringify(session));
loadAccount(session)
loadAccount(session);
});
});
@ -138,7 +156,7 @@
$('body form.js-auth-form').addEventListener('submit', onClickLogin);
onChangeProvider('oauth3.org');
if (session) {
vueData.token = session.access_token
vueData.token = session.access_token;
loadAccount(session);
}
}());

View File

@ -29,7 +29,12 @@ DB._load = function () {
DB._byId = {};
DB._grants = {};
DB._grantsMap = {};
DB._authz = {};
DB._perms.forEach(function (acc) {
if ('authz' === acc.type) {
DB._authz[acc.id] = acc;
return;
}
if (acc.id) {
// if account has an id
DB._byId[acc.id] = acc;
@ -129,6 +134,80 @@ DB.accounts.add = function (obj) {
}
});
};
DB.authorizations = {};
DB.authorizations.create = function (acc, claim) {
if (!acc.id || !claim.type || !claim.value) { throw new Error("requires account id"); }
var crypto = require('crypto');
var authz = DB._authz[acc.id];
if (!authz) {
authz = {
id: acc.id
, type: 'authz'
, claims: []
};
DB._authz[acc.id] = authz;
DB._perms.push(authz);
}
// TODO check for unique type:value pairing in claims
claim.challenge = crypto.randomBytes(16).toString('hex');
claim.createdAt = Date.now();
claim.verifiedAt = 0;
authz.claims.push(claim);
DB.save();
return JSON.parse(JSON.stringify(claim));
};
DB.authorizations.check = function (acc, claim) {
var authz = DB._authz[acc.id];
var vclaim = null;
if (!authz) {
return vclaim;
}
authz.claims.some(function (c) {
console.log('authz.check', c);
if (claim.challenge) {
if (c.challenge === claim.challenge) {
vclaim = JSON.parse(JSON.stringify(c));
return true;
}
} else if (claim.value === c.value) {
vclaim = JSON.parse(JSON.stringify(c));
return true;
}
});
return vclaim;
};
DB.authorizations.checkAll = function (acc) {
var authz = DB._authz[acc.id];
if (!authz) {
return [];
}
return authz.claims.map(function (claim) {
return JSON.parse(JSON.stringify(claim));
});
};
DB.authorizations.verify = function (acc, claim) {
var scmp = require('scmp');
var authz = DB._authz[acc.id];
var vclaim;
if (!authz) { return false; }
authz.claims.some(function (c) {
if (scmp(c.challenge, claim.challenge)) {
vclaim = c;
c.verifiedAt = Date.now();
return true;
}
});
if (vclaim) {
DB.save();
return true;
}
return false;
};
DB.domains = {};
DB.domains.available = function (name) {
return PromiseA.resolve().then(function () {
@ -154,7 +233,7 @@ DB.domains._add = function (acc, opts) {
parts.shift();
parts.pop();
if (parts.some(function (part) {
if (DB._byDomain[part]) {
if (DB._byDomain[part] && DB._byDomain[part].wildcard) {
pdomain = part;
return true;
}
@ -175,6 +254,7 @@ DB.domains._add = function (acc, opts) {
, domain: domain
};
acc.domains.push(domain);
DB.save();
});
};
DB.ports = {};

View File

@ -19,6 +19,15 @@ var requestAsync = util.promisify(require('@coolaj86/urequest'));
var mkdirpAsync = util.promisify(require('mkdirp'));
var TRUSTED_ISSUERS = [ 'oauth3.org' ];
var DB = require('./db.js');
var Claims = {};
Claims.publicize = function publicizeClaim(claim) {
if (!claim) {
return null;
}
var result = { type: claim.type, value: claim.value, verifiedAt: claim.verifiedAt, createdAt: claim.createdAt };
if ('dns' === claim.type) { result.challenge = claim.challenge; }
return result;
};
var _auths = module.exports._auths = {};
var Auths = {};
@ -763,6 +772,7 @@ app.get('/api/telebit.cloud/account', function (req, res) {
//console.log(grants);
var domainsMap = {};
var portsMap = {};
var claimsMap = {};
var result = JSON.parse(JSON.stringify(acc));
result.domains.length = 0;
result.ports.length = 0;
@ -775,6 +785,11 @@ app.get('/api/telebit.cloud/account', function (req, res) {
account.ports.forEach(function (p) {
portsMap[p.number] = p;
});
DB.authorizations.checkAll({ id: account.id }).filter(function (claim) {
return !claim.verifiedAt;
}).forEach(function (claim) {
claimsMap[claim.challenge] = claim;
});
});
result.domains = Object.keys(domainsMap).map(function (k) {
return domainsMap[k];
@ -782,6 +797,9 @@ app.get('/api/telebit.cloud/account', function (req, res) {
result.ports = Object.keys(portsMap).map(function (k) {
return portsMap[k];
});
result.claims = Object.keys(claimsMap).map(function (k) {
return Claims.publicize(claimsMap[k]);
});
return result;
});
}
@ -821,24 +839,121 @@ app.post('/api/telebit.cloud/account', function (req, res) {
// Challenge Nodes / Email, Domains / DNS
app.post('/api/telebit.cloud/account/authorizations/new', function (req, res) {
// Send email via SMTP, confirm client's chosen pin
res.statusCode = 500;
res.send({ error: { code: "E_NO_IMPL", message: "not implemented" } });
var accId = Accounts._getTokenId(req.auth);
var typ = req.body.type;
var val = req.body.value;
var wild = req.body.wildcard;
var claim;
if ('dns' === typ && /^[a-z0-9\-\.]+.[a-z]+$/i.test(val)) {
claim = DB.authorizations.create({ id: accId }, { type: typ, value: val, wildcard: wild });
// MUST RETURN PUBLIC VALUES ONLY!
// (challenge is public with dns because the verification is internal)
res.send({ success: true, claim: claim });
} else if ('email' === typ) {
//claim = DB.authorizations.create({ type: dns, claim: claim });
// MUST RETURN PUBLIC VALUES ONLY!
// (challenge is private with email because the verification is external)
//claim.challenge = undefined;;
// TODO send email
res.statusCode = 501;
res.send({ error: { code: "E_NO_IMPL", message: "authz '" + typ + "' understood but not implemented" } });
} else {
res.statusCode = 501;
res.send({ error: { code: "E_NO_IMPL", message: "unknown authz type '" + typ + "'" } });
}
});
app.get('/api/telebit.cloud/account/authorizations/status/:id', function (req, res) {
app.get('/api/telebit.cloud/account/authorizations/status/:value?', function (req, res) {
// For client to check on status
res.statusCode = 500;
res.send({ error: { code: "E_NO_IMPL", message: "not implemented" } });
var accId = Accounts._getTokenId(req.auth);
var val = req.params.value;
var result;
if (val) {
result = Claims.publicize(DB.authorizations.check({ id: accId }, { value: val }));
// MUST RETURN PUBLIC VALUES ONLY!
res.send({ success: true, claim: result });
} else {
result = DB.authorizations.checkAll({ id: accId }).map(Claims.publicize);
// MUST RETURN PUBLIC VALUES ONLY!
res.send({ success: true, claims: result });
}
});
app.get('/api/telebit.cloud/account/authorizations/meta/:secret', function (req, res) {
// For agent to retrieve metadata
res.statusCode = 500;
res.send({ error: { code: "E_NO_IMPL", message: "not implemented" } });
});
app.post('/api/telebit.cloud/account/authorizations/new/:magic/:pin', function (req, res) {
app.post('/api/telebit.cloud/account/authorizations/verify/:magic/:pin', function (req, res) {
// For agent to confirm user's intent
res.statusCode = 500;
res.send({ error: { code: "E_NO_IMPL", message: "not implemented" } });
});
app.post('/api/telebit.cloud/account/authorizations/new/:value/:challenge?', function (req, res) {
// For agent to confirm user's intent
var dns = require('dns');
var accId = Accounts._getTokenId(req.auth);
var val = req.params.value;
var ch = req.params.challenge;
var claim = DB.authorizations.check({ id: accId }, { challenge: ch, value: val });
function notFound() {
res.send({ error: {
code: "E_PENDING"
, message: "Did not find '" + claim.challenge + "' among records at '_claim-challenge." + claim.value + "'"
} });
}
function grantDnsClaim() {
return Accounts.getOrCreate(req).then(function (acc) {
return DB.domains._add(acc, { domain: claim.value, wildcard: claim.wildcard }).then(function (result) {
if (!DB.authorizations.verify({ id: accId }, claim)) {
var err = new Error("'_claim-challenge." + claim.value + "' matched, but final verification failed");
err.code = "E_UNKNOWN";
return PromiseA.reject(err);
}
return result;
});
});
}
function checkDns() {
dns.resolveTxt('_claim-challenge.' + claim.value, function (err, records) {
if (err) {
notFound();
return;
}
if (!records.some(function (txts) {
return txts.some(function (txt) {
console.log('TXT', txt);
return claim.challenge === txt;
});
})) {
notFound();
return;
}
grantDnsClaim().then(function () {
res.send({ success: true });
}).catch(function (err) {
res.send({ error: { code: err.code, message: err.toString(), _stack: err.stack } });
});
});
}
console.log('claim', claim);
if ('dns' === claim.type) {
checkDns();
} else if ('email' === claim.type) {
res.statusCode = 500;
res.send({ error: { code: "E_NO_IMPL", message: "'" + claim.type + "' not implemented yet" } });
} else {
res.statusCode = 500;
res.send({ error: { code: "E_NO_IMPL", message: "'" + claim.type + "' not understood" } });
}
});
// From Device (which knows id, but not secret)

View File

@ -11,6 +11,7 @@
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"jwk-to-pem": "^2.0.0",
"oauth3.js": "^1.2.5"
"oauth3.js": "^1.2.5",
"scmp": "^1.0.2"
}
}