dns check complete
This commit is contained in:
parent
2819117f10
commit
2564b750e6
|
@ -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 }}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}());
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue