WIP de-nest and restructure
This commit is contained in:
parent
2a28dca257
commit
2301966f9f
|
@ -1,12 +1,14 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
/*global Promise*/
|
||||||
|
|
||||||
var pkg = require('../package.json');
|
var pkg = require('../package.json');
|
||||||
var os = require('os');
|
var os = require('os');
|
||||||
|
|
||||||
//var url = require('url');
|
//var url = require('url');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var util = require('util');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
//var https = require('https');
|
//var https = require('https');
|
||||||
var YAML = require('js-yaml');
|
var YAML = require('js-yaml');
|
||||||
|
@ -104,7 +106,9 @@ if (!confpath || /^--/.test(confpath)) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function askForConfig(state, mainCb) {
|
var Console = {};
|
||||||
|
Console.setup = function (state) {
|
||||||
|
if (Console.rl) { return; }
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var ttyname = '/dev/tty';
|
var ttyname = '/dev/tty';
|
||||||
var stdin = useTty ? fs.createReadStream(ttyname, {
|
var stdin = useTty ? fs.createReadStream(ttyname, {
|
||||||
|
@ -119,6 +123,34 @@ function askForConfig(state, mainCb) {
|
||||||
, terminal: !/^win/i.test(os.platform()) && !useTty
|
, terminal: !/^win/i.test(os.platform()) && !useTty
|
||||||
});
|
});
|
||||||
state._useTty = useTty;
|
state._useTty = useTty;
|
||||||
|
Console.rl = rl;
|
||||||
|
};
|
||||||
|
Console.teardown = function () {
|
||||||
|
// https://github.com/nodejs/node/issues/21319
|
||||||
|
if (useTty) { try { Console.stdin.push(null); } catch(e) { /*ignore*/ } }
|
||||||
|
Console.rl.close();
|
||||||
|
if (useTty) { try { Console.stdin.close(); } catch(e) { /*ignore*/ } }
|
||||||
|
Console.rl = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function askEmail(cb) {
|
||||||
|
Console.setup();
|
||||||
|
if (state.config.email) { cb(); return; }
|
||||||
|
console.info(TPLS.remote.setup.email);
|
||||||
|
// TODO attempt to read email from npmrc or the like?
|
||||||
|
Console.rl.question('email: ', function (email) {
|
||||||
|
// TODO validate email domain
|
||||||
|
email = /@/.test(email) && email.trim();
|
||||||
|
if (!email) { askEmail(cb); return; }
|
||||||
|
state.config.email = email.trim();
|
||||||
|
state.config.agreeTos = true;
|
||||||
|
console.info("");
|
||||||
|
setTimeout(cb, 250);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function askForConfig(state, mainCb) {
|
||||||
|
Console.setup(state);
|
||||||
|
|
||||||
// NOTE: Use of setTimeout
|
// NOTE: Use of setTimeout
|
||||||
// We're using setTimeout just to make the user experience a little
|
// We're using setTimeout just to make the user experience a little
|
||||||
|
@ -128,19 +160,7 @@ function askForConfig(state, mainCb) {
|
||||||
// <= 100ms is shorter than normal human reaction time (ability to place events chronologically, which happened first)
|
// <= 100ms is shorter than normal human reaction time (ability to place events chronologically, which happened first)
|
||||||
// ~ 150-250ms is the sweet spot for most humans (long enough to notice change and not be jarred, but stay on task)
|
// ~ 150-250ms is the sweet spot for most humans (long enough to notice change and not be jarred, but stay on task)
|
||||||
var firstSet = [
|
var firstSet = [
|
||||||
function askEmail(cb) {
|
askEmail
|
||||||
if (state.config.email) { cb(); return; }
|
|
||||||
console.info(TPLS.remote.setup.email);
|
|
||||||
// TODO attempt to read email from npmrc or the like?
|
|
||||||
rl.question('email: ', function (email) {
|
|
||||||
email = /@/.test(email) && email.trim();
|
|
||||||
if (!email) { askEmail(cb); return; }
|
|
||||||
state.config.email = email.trim();
|
|
||||||
state.config.agreeTos = true;
|
|
||||||
console.info("");
|
|
||||||
setTimeout(cb, 250);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
, function askRelay(cb) {
|
, function askRelay(cb) {
|
||||||
function checkRelay(relay) {
|
function checkRelay(relay) {
|
||||||
// TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json
|
// TODO parse and check https://{{relay}}/.well-known/telebit.cloud/directives.json
|
||||||
|
@ -173,7 +193,7 @@ function askForConfig(state, mainCb) {
|
||||||
console.info("");
|
console.info("");
|
||||||
console.info("What relay will you be using? (press enter for default)");
|
console.info("What relay will you be using? (press enter for default)");
|
||||||
console.info("");
|
console.info("");
|
||||||
rl.question('relay [default: telebit.cloud]: ', checkRelay);
|
Console.rl.question('relay [default: telebit.cloud]: ', checkRelay);
|
||||||
}
|
}
|
||||||
, function checkRelay(cb) {
|
, function checkRelay(cb) {
|
||||||
nextSet = [];
|
nextSet = [];
|
||||||
|
@ -201,7 +221,7 @@ function askForConfig(state, mainCb) {
|
||||||
console.info("");
|
console.info("");
|
||||||
console.info("Type 'y' or 'yes' to accept these Terms of Service.");
|
console.info("Type 'y' or 'yes' to accept these Terms of Service.");
|
||||||
console.info("");
|
console.info("");
|
||||||
rl.question('agree to all? [y/N]: ', function (resp) {
|
Console.rl.question('agree to all? [y/N]: ', function (resp) {
|
||||||
resp = resp.trim();
|
resp = resp.trim();
|
||||||
if (!/^y(es)?$/i.test(resp) && 'true' !== resp) {
|
if (!/^y(es)?$/i.test(resp) && 'true' !== resp) {
|
||||||
throw new Error("You didn't accept the Terms of Service... not sure what to do...");
|
throw new Error("You didn't accept the Terms of Service... not sure what to do...");
|
||||||
|
@ -219,7 +239,7 @@ function askForConfig(state, mainCb) {
|
||||||
console.info("");
|
console.info("");
|
||||||
console.info("What updates would you like to receive? (" + options.join(',') + ")");
|
console.info("What updates would you like to receive? (" + options.join(',') + ")");
|
||||||
console.info("");
|
console.info("");
|
||||||
rl.question('messages (default: important): ', function (updates) {
|
Console.rl.question('messages (default: important): ', function (updates) {
|
||||||
state._updates = (updates || '').trim().toLowerCase();
|
state._updates = (updates || '').trim().toLowerCase();
|
||||||
if (!state._updates) { state._updates = 'important'; }
|
if (!state._updates) { state._updates = 'important'; }
|
||||||
if (-1 === options.indexOf(state._updates)) { askUpdates(cb); return; }
|
if (-1 === options.indexOf(state._updates)) { askUpdates(cb); return; }
|
||||||
|
@ -240,7 +260,7 @@ function askForConfig(state, mainCb) {
|
||||||
console.info("");
|
console.info("");
|
||||||
console.info("Contribute project telemetry data? (press enter for default [yes])");
|
console.info("Contribute project telemetry data? (press enter for default [yes])");
|
||||||
console.info("");
|
console.info("");
|
||||||
rl.question('telemetry [Y/n]: ', function (telemetry) {
|
Console.rl.question('telemetry [Y/n]: ', function (telemetry) {
|
||||||
if (!telemetry || /^y(es)?$/i.test(telemetry)) {
|
if (!telemetry || /^y(es)?$/i.test(telemetry)) {
|
||||||
state.config.telemetry = true;
|
state.config.telemetry = true;
|
||||||
}
|
}
|
||||||
|
@ -263,7 +283,7 @@ function askForConfig(state, mainCb) {
|
||||||
console.info("\tShared Secret (HMAC hex)");
|
console.info("\tShared Secret (HMAC hex)");
|
||||||
//console.info("\tPrivate key (hex)");
|
//console.info("\tPrivate key (hex)");
|
||||||
console.info("");
|
console.info("");
|
||||||
rl.question('auth: ', function (resp) {
|
Console.rl.question('auth: ', function (resp) {
|
||||||
resp = (resp || '').trim();
|
resp = (resp || '').trim();
|
||||||
try {
|
try {
|
||||||
JWT.decode(resp);
|
JWT.decode(resp);
|
||||||
|
@ -291,7 +311,7 @@ function askForConfig(state, mainCb) {
|
||||||
console.info("What servername(s) will you be relaying here?");
|
console.info("What servername(s) will you be relaying here?");
|
||||||
console.info("(use a comma-separated list such as example.com,example.net)");
|
console.info("(use a comma-separated list such as example.com,example.net)");
|
||||||
console.info("");
|
console.info("");
|
||||||
rl.question('domain(s): ', function (resp) {
|
Console.rl.question('domain(s): ', function (resp) {
|
||||||
resp = (resp || '').trim().split(/,/g);
|
resp = (resp || '').trim().split(/,/g);
|
||||||
if (!resp.length) { askServernames(); return; }
|
if (!resp.length) { askServernames(); return; }
|
||||||
// TODO validate the domains
|
// TODO validate the domains
|
||||||
|
@ -306,7 +326,7 @@ function askForConfig(state, mainCb) {
|
||||||
console.info("What tcp port(s) will you be relaying here?");
|
console.info("What tcp port(s) will you be relaying here?");
|
||||||
console.info("(use a comma-separated list such as 2222,5050)");
|
console.info("(use a comma-separated list such as 2222,5050)");
|
||||||
console.info("");
|
console.info("");
|
||||||
rl.question('port(s) [default:none]: ', function (resp) {
|
Console.rl.question('port(s) [default:none]: ', function (resp) {
|
||||||
resp = (resp || '').trim().split(/,/g);
|
resp = (resp || '').trim().split(/,/g);
|
||||||
if (!resp.length) { askPorts(); return; }
|
if (!resp.length) { askPorts(); return; }
|
||||||
// TODO validate the domains
|
// TODO validate the domains
|
||||||
|
@ -320,10 +340,7 @@ function askForConfig(state, mainCb) {
|
||||||
function next() {
|
function next() {
|
||||||
var q = nextSet.shift();
|
var q = nextSet.shift();
|
||||||
if (!q) {
|
if (!q) {
|
||||||
// https://github.com/nodejs/node/issues/21319
|
Console.teardown();
|
||||||
if (useTty) { try { stdin.push(null); } catch(e) { /*ignore*/ } }
|
|
||||||
rl.close();
|
|
||||||
if (useTty) { try { stdin.close(); } catch(e) { /*ignore*/ } }
|
|
||||||
mainCb(null, state);
|
mainCb(null, state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -335,8 +352,76 @@ function askForConfig(state, mainCb) {
|
||||||
|
|
||||||
var RC;
|
var RC;
|
||||||
|
|
||||||
function parseConfig(err, text) {
|
function bootstrap(opts) {
|
||||||
|
state.key = opts.key;
|
||||||
|
// Create / retrieve account (sign-in, more or less)
|
||||||
|
// TODO hit directory resource /.well-known/openid-configuration -> acme_uri (?)
|
||||||
|
// Occassionally rotate the key just for the sake of testing the key rotation
|
||||||
|
return urequestAsync({
|
||||||
|
method: 'HEAD'
|
||||||
|
, url: RC.resolve('/acme/new-nonce')
|
||||||
|
, headers: { "User-Agent": 'Telebit/' + pkg.version }
|
||||||
|
}).then(function (resp) {
|
||||||
|
var nonce = resp.headers['replay-nonce'];
|
||||||
|
var newAccountUrl = RC.resolve('/acme/new-acct');
|
||||||
|
var contact = [];
|
||||||
|
if (opts.email) {
|
||||||
|
contact.push("mailto:" + opts.email);
|
||||||
|
}
|
||||||
|
return keypairs.signJws({
|
||||||
|
jwk: state.key
|
||||||
|
, protected: {
|
||||||
|
// alg will be filled out automatically
|
||||||
|
jwk: state.pub
|
||||||
|
, kid: false
|
||||||
|
, nonce: nonce
|
||||||
|
, url: newAccountUrl
|
||||||
|
}
|
||||||
|
, payload: JSON.stringify({
|
||||||
|
// We can auto-agree here because the client is the user agent of the primary user
|
||||||
|
termsOfServiceAgreed: true
|
||||||
|
, contact: contact // I don't think we have email yet...
|
||||||
|
, onlyReturnExisting: opts.onlyReturnExisting || !opts.email
|
||||||
|
//, externalAccountBinding: null
|
||||||
|
})
|
||||||
|
}).then(function (jws) {
|
||||||
|
return urequestAsync({
|
||||||
|
url: newAccountUrl
|
||||||
|
, method: 'POST'
|
||||||
|
, json: jws // TODO default to post when body is present
|
||||||
|
, headers: {
|
||||||
|
"Content-Type": 'application/jose+json'
|
||||||
|
, "User-Agent": 'Telebit/' + pkg.version
|
||||||
|
}
|
||||||
|
}).then(function (resp) {
|
||||||
|
//nonce = resp.headers['replay-nonce'];
|
||||||
|
if (!resp.body || 'valid' !== resp.body.status) {
|
||||||
|
console.error('request jws:', jws);
|
||||||
|
console.error('response:');
|
||||||
|
console.error(resp.headers);
|
||||||
|
console.error(resp.body);
|
||||||
|
throw new Error("did not successfully create or restore account");
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(RC.createRelauncher(bootstrap._replay(opts), bootstrap._bootstate)).catch(function (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(17);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
bootstrap._bootstate = {};
|
||||||
|
bootstrap._replay = function (_opts) {
|
||||||
|
return function (opts) {
|
||||||
|
// supply opts to match reverse signature (.length checking)
|
||||||
|
opts = _opts;
|
||||||
|
return bootstrap(opts);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
function handleConfig(config) {
|
function handleConfig(config) {
|
||||||
|
var _config = state.config || {};
|
||||||
|
|
||||||
state.config = config;
|
state.config = config;
|
||||||
var verstrd = [ pkg.name + ' daemon v' + state.config.version ];
|
var verstrd = [ pkg.name + ' daemon v' + state.config.version ];
|
||||||
if (state.config.version && state.config.version !== pkg.version) {
|
if (state.config.version && state.config.version !== pkg.version) {
|
||||||
|
@ -345,6 +430,10 @@ function parseConfig(err, text) {
|
||||||
console.info(verstr.join(' '));
|
console.info(verstr.join(' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.config.email && _config) {
|
||||||
|
state.config.email = _config.email;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// check for init first, before anything else
|
// check for init first, before anything else
|
||||||
// because it has arguments that may help in
|
// because it has arguments that may help in
|
||||||
|
@ -463,39 +552,6 @@ function parseConfig(err, text) {
|
||||||
help();
|
help();
|
||||||
process.exit(11);
|
process.exit(11);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
state._clientConfig = JSON.parse(text || '{}');
|
|
||||||
} catch(e1) {
|
|
||||||
try {
|
|
||||||
state._clientConfig = YAML.safeLoad(text || '{}');
|
|
||||||
} catch(e2) {
|
|
||||||
try {
|
|
||||||
state._clientConfig = TOML.parse(text || '');
|
|
||||||
} catch(e3) {
|
|
||||||
console.error(e1.message);
|
|
||||||
console.error(e2.message);
|
|
||||||
process.exit(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state._clientConfig = camelCopy(state._clientConfig || {}) || {};
|
|
||||||
RC = require('../lib/rc/index.js').create(state);
|
|
||||||
RC.requestAsync = require('util').promisify(RC.request);
|
|
||||||
|
|
||||||
if (!Object.keys(state._clientConfig).length) {
|
|
||||||
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
|
|
||||||
console.info("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((err && 'ENOENT' === err.code) || !Object.keys(state._clientConfig).length) {
|
|
||||||
if (!err || 'ENOENT' === err.code) {
|
|
||||||
//console.warn("Empty config file. Run 'telebit init' to configure.\n");
|
|
||||||
} else {
|
|
||||||
console.warn("Couldn't load config:\n\n\t" + err.message + "\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRemoteRequest(service, fn) {
|
function handleRemoteRequest(service, fn) {
|
||||||
return function (err, body) {
|
return function (err, body) {
|
||||||
|
@ -673,75 +729,26 @@ function parseConfig(err, text) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var bootState = {};
|
function parseConfig(text) {
|
||||||
function bootstrap() {
|
var _clientConfig;
|
||||||
// Create / retrieve account (sign-in, more or less)
|
try {
|
||||||
// TODO hit directory resource /.well-known/openid-configuration -> acme_uri (?)
|
_clientConfig = JSON.parse(text || '{}');
|
||||||
// Occassionally rotate the key just for the sake of testing the key rotation
|
} catch(e1) {
|
||||||
return urequestAsync({
|
try {
|
||||||
method: 'HEAD'
|
_clientConfig = YAML.safeLoad(text || '{}');
|
||||||
, url: RC.resolve('/acme/new-nonce')
|
} catch(e2) {
|
||||||
, headers: { "User-Agent": 'Telebit/' + pkg.version }
|
try {
|
||||||
}).then(function (resp) {
|
_clientConfig = TOML.parse(text || '');
|
||||||
var nonce = resp.headers['replay-nonce'];
|
} catch(e3) {
|
||||||
var newAccountUrl = RC.resolve('/acme/new-acct');
|
console.error(e1.message);
|
||||||
return keypairs.signJws({
|
console.error(e2.message);
|
||||||
jwk: state.key
|
process.exit(1);
|
||||||
, protected: {
|
|
||||||
// alg will be filled out automatically
|
|
||||||
jwk: state.pub
|
|
||||||
, kid: false
|
|
||||||
, nonce: nonce
|
|
||||||
, url: newAccountUrl
|
|
||||||
}
|
|
||||||
, payload: JSON.stringify({
|
|
||||||
// We can auto-agree here because the client is the user agent of the primary user
|
|
||||||
termsOfServiceAgreed: true
|
|
||||||
, contact: [] // I don't think we have email yet...
|
|
||||||
//, externalAccountBinding: null
|
|
||||||
})
|
|
||||||
}).then(function (jws) {
|
|
||||||
return urequestAsync({
|
|
||||||
url: newAccountUrl
|
|
||||||
, method: 'POST'
|
|
||||||
, json: jws // TODO default to post when body is present
|
|
||||||
, headers: {
|
|
||||||
"Content-Type": 'application/jose+json'
|
|
||||||
, "User-Agent": 'Telebit/' + pkg.version
|
|
||||||
}
|
|
||||||
}).then(function (resp) {
|
|
||||||
//nonce = resp.headers['replay-nonce'];
|
|
||||||
if (!resp.body || 'valid' !== resp.body.status) {
|
|
||||||
console.error('request jws:', jws);
|
|
||||||
console.error('response:');
|
|
||||||
console.error(resp.headers);
|
|
||||||
console.error(resp.body);
|
|
||||||
throw new Error("did not successfully create or restore account");
|
|
||||||
}
|
|
||||||
return RC.requestAsync({ service: 'config', method: 'GET' }).catch(function (err) {
|
|
||||||
if (err) {
|
|
||||||
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
|
|
||||||
console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to.");
|
|
||||||
console.error(err);
|
|
||||||
} else if ('ENOTSOCK' === err.code) {
|
|
||||||
console.error(err);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
process.exit(101);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}).then(handleConfig);
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}).catch(RC.createErrorHandler(bootstrap, bootState, function (err) {
|
|
||||||
console.error(err);
|
|
||||||
process.exit(17);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
return camelCopy(_clientConfig || {}) || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsers = {
|
var parsers = {
|
||||||
|
@ -821,12 +828,30 @@ var parsers = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Start by reading the config file, before all else
|
||||||
|
//
|
||||||
|
util.promisify(fs.readFile)(confpath, 'utf8').catch(function (err) {
|
||||||
|
if (err && 'ENOENT' !== err.code) {
|
||||||
|
console.warn("Couldn't load config:\n\n\t" + err.message + "\n");
|
||||||
|
}
|
||||||
|
}).then(function (text) {
|
||||||
|
state._clientConfig = parseConfig(text);
|
||||||
|
RC = require('../lib/rc/index.js').create(state); // adds state._ipc
|
||||||
|
if (!Object.keys(state._clientConfig).length) {
|
||||||
|
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
|
||||||
|
console.info("");
|
||||||
|
}
|
||||||
|
RC.requestAsync = require('util').promisify(RC.request);
|
||||||
|
}).then(function () {
|
||||||
var keystore = require('../lib/keystore.js').create(state);
|
var keystore = require('../lib/keystore.js').create(state);
|
||||||
state.keystore = keystore;
|
state.keystore = keystore;
|
||||||
state.keystoreSecure = !keystore.insecure;
|
state.keystoreSecure = !keystore.insecure;
|
||||||
keystore.all().then(function (list) {
|
keystore.all().then(function (list) {
|
||||||
var keyext = '.key.jwk.json';
|
var keyext = '.key.jwk.json';
|
||||||
var key;
|
var key;
|
||||||
|
var p;
|
||||||
|
|
||||||
// TODO create map by account and index into that map to get the master key
|
// TODO create map by account and index into that map to get the master key
|
||||||
// and sort keys in the process
|
// and sort keys in the process
|
||||||
list.some(function (el) {
|
list.some(function (el) {
|
||||||
|
@ -838,21 +863,65 @@ keystore.all().then(function (list) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (key) {
|
if (key) {
|
||||||
state.key = key;
|
p = Promise.resolve(key);
|
||||||
state.pub = keypairs.neuter({ jwk: key });
|
} else {
|
||||||
fs.readFile(confpath, 'utf8', parseConfig);
|
p = keypairs.generate().then(function (pair) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return keypairs.generate().then(function (pair) {
|
|
||||||
var jwk = pair.private;
|
var jwk = pair.private;
|
||||||
return keypairs.thumbprint({ jwk: jwk }).then(function (kid) {
|
return keypairs.thumbprint({ jwk: jwk }).then(function (kid) {
|
||||||
jwk.kid = kid;
|
|
||||||
return keystore.set(kid + keyext, jwk).then(function () {
|
|
||||||
var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8);
|
var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8);
|
||||||
|
jwk.kid = kid;
|
||||||
console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid);
|
console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid);
|
||||||
state.key = jwk;
|
return keystore.set(kid + keyext, jwk).then(function () {
|
||||||
fs.readFile(confpath, 'utf8', parseConfig);
|
return jwk;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.then(function (key) {
|
||||||
|
state.key = key;
|
||||||
|
state.pub = keypairs.neuter({ jwk: key });
|
||||||
|
// we don't have config yet
|
||||||
|
state.config = {};
|
||||||
|
return bootstrap({ key: state.key, onlyReturnExisting: true }).catch(function (err) {
|
||||||
|
console.error("[DEBUG] local account not created?");
|
||||||
|
console.error(err);
|
||||||
|
// Ask for email address. The prior email may have been bad
|
||||||
|
return require('util').promisify(askEmail).then(function (email) {
|
||||||
|
return bootstrap({ key: state.key, email: email });
|
||||||
|
});
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error(err);
|
||||||
|
console.error("You may need to go into the web interface and allow Telebit Client by ID '" + key.kid + "'");
|
||||||
|
process.exit(10);
|
||||||
|
}).then(function (result) {
|
||||||
|
//#console.log("Telebit Account Bootstrap result:");
|
||||||
|
//#console.log(result.body);
|
||||||
|
state.config.email = (result.body.contact[0]||'').replace(/mailto:/, '');
|
||||||
|
var p2;
|
||||||
|
if (state.key.sub === state.config.email) {
|
||||||
|
p2 = Promise.resolve(state.key);
|
||||||
|
} else {
|
||||||
|
state.key.sub = state.config.email;
|
||||||
|
p2 = keystore.set(state.key.kid + keyext, state.key);
|
||||||
|
}
|
||||||
|
return p2.then(function () {
|
||||||
|
return RC.requestAsync({ service: 'config', method: 'GET' }).catch(function (err) {
|
||||||
|
if (err) {
|
||||||
|
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
|
||||||
|
console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to.");
|
||||||
|
console.error(err);
|
||||||
|
} else if ('ENOTSOCK' === err.code) {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
process.exit(101);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}).then(handleConfig);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -525,6 +525,27 @@ controllers.newAccount = function (req, res) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
controllers.acmeAccounts = function (req, res) {
|
||||||
|
if (!req.jws || !req.jws.verified) {
|
||||||
|
res.statusCode = 400;
|
||||||
|
res.send({"error":{"message": "this type of requests must be encoded as a jws payload"
|
||||||
|
+ " and signed by a known account holder"}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var account;
|
||||||
|
var accountId = req.params[0];
|
||||||
|
DB.accounts.some(function (acc) {
|
||||||
|
// TODO calculate thumbprint from jwk
|
||||||
|
// find a key with matching jwk
|
||||||
|
if (acc._id === accountId) {
|
||||||
|
account = acc;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// TODO check that the JWS matches the accountI
|
||||||
|
console.warn("[warn] account ID still acts as secret, should use JWS kid for verification");
|
||||||
|
res.send(account);
|
||||||
|
};
|
||||||
|
|
||||||
function jsonEggspress(req, res, next) {
|
function jsonEggspress(req, res, next) {
|
||||||
/*
|
/*
|
||||||
|
@ -1064,6 +1085,10 @@ function handleApi() {
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
// TODO convert /acme/accounts/:account_id into a regex
|
||||||
|
app.get(/^\/acme\/accounts\/([\w]+)/, controllers.acmeAccounts);
|
||||||
|
// POST-as-GET
|
||||||
|
app.post(/^\/acme\/accounts\/([\w]+)/, controllers.acmeAccounts);
|
||||||
app.use(/\b(relay)\b/, mustTrust, controllers.relay);
|
app.use(/\b(relay)\b/, mustTrust, controllers.relay);
|
||||||
app.get(/\b(config)\b/, mustTrust, getConfigOnly);
|
app.get(/\b(config)\b/, mustTrust, getConfigOnly);
|
||||||
app.use(/\b(init|config)\b/, mustTrust, initOrConfig);
|
app.use(/\b(init|config)\b/, mustTrust, initOrConfig);
|
||||||
|
|
|
@ -186,6 +186,7 @@ var telebitState = {};
|
||||||
var appMethods = {
|
var appMethods = {
|
||||||
initialize: function () {
|
initialize: function () {
|
||||||
console.log("call initialize");
|
console.log("call initialize");
|
||||||
|
return requestAccountHelper().then(function (/*key*/) {
|
||||||
if (!appData.init.relay) {
|
if (!appData.init.relay) {
|
||||||
appData.init.relay = DEFAULT_RELAY;
|
appData.init.relay = DEFAULT_RELAY;
|
||||||
}
|
}
|
||||||
|
@ -210,6 +211,7 @@ var appMethods = {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2)));
|
window.alert("Error: [initialize] " + (err.message || JSON.stringify(err, null, 2)));
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
, advance: function () {
|
, advance: function () {
|
||||||
return doConfigure();
|
return doConfigure();
|
||||||
|
@ -473,14 +475,43 @@ new Vue({
|
||||||
, methods: appMethods
|
, methods: appMethods
|
||||||
});
|
});
|
||||||
|
|
||||||
function run(key) {
|
function requestAccountHelper() {
|
||||||
|
function reset() {
|
||||||
|
changeState('setup');
|
||||||
|
setState();
|
||||||
|
}
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
appData.init.email = localStorage.getItem('email');
|
||||||
|
if (!appData.init.email) {
|
||||||
|
// don't resolve
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return requestAccount(appData.init.email).then(function (key) {
|
||||||
|
if (!key) { throw new Error("[SANITY] Error: completed without key"); }
|
||||||
|
resolve(key);
|
||||||
|
}).catch(function (err) {
|
||||||
|
appData.init.email = "";
|
||||||
|
localStorage.removeItem('email');
|
||||||
|
console.error(err);
|
||||||
|
window.alert("something went wrong");
|
||||||
|
// don't resolve
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
return requestAccountHelper().then(function (key) {
|
||||||
api._key = key;
|
api._key = key;
|
||||||
|
// TODO create session instance of Telebit
|
||||||
|
Telebit._key = key;
|
||||||
// 😁 1. Get ACME directory
|
// 😁 1. Get ACME directory
|
||||||
// 😁 2. Fetch ACME account
|
// 😁 2. Fetch ACME account
|
||||||
// 3. Test if account has access
|
// 3. Test if account has access
|
||||||
// 4. Show command line auth instructions to auth
|
// 4. Show command line auth instructions to auth
|
||||||
// 5. Sign requests / use JWT
|
// 😁 5. Sign requests / use JWT
|
||||||
// 6. Enforce token required for config, status, etc
|
// 😁 6. Enforce token required for config, status, etc
|
||||||
// 7. Move admin interface to standard ports (admin.foo-bar-123.telebit.xyz)
|
// 7. Move admin interface to standard ports (admin.foo-bar-123.telebit.xyz)
|
||||||
api.config().then(function (config) {
|
api.config().then(function (config) {
|
||||||
telebitState.config = config;
|
telebitState.config = config;
|
||||||
|
@ -522,6 +553,7 @@ function run(key) {
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
appData.views.flash.error = err.message || JSON.stringify(err, null, 2);
|
appData.views.flash.error = err.message || JSON.stringify(err, null, 2);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -543,19 +575,8 @@ function getKey() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEmail() {
|
function requestAccount(email) {
|
||||||
return Promise.resolve().then(function () {
|
|
||||||
var email = localStorage.getItem('email');
|
|
||||||
if (email) { return email; }
|
|
||||||
while (!email) {
|
|
||||||
email = window.prompt("Email address (device owner)?");
|
|
||||||
}
|
|
||||||
return email;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function requestAccount() {
|
|
||||||
return getKey().then(function (jwk) {
|
return getKey().then(function (jwk) {
|
||||||
return getEmail().then(function(email) {
|
|
||||||
// creates new or returns existing
|
// creates new or returns existing
|
||||||
var acme = ACME.create({});
|
var acme = ACME.create({});
|
||||||
var url = window.location.protocol + '//' + window.location.host + '/acme/directory';
|
var url = window.location.protocol + '//' + window.location.host + '/acme/directory';
|
||||||
|
@ -574,15 +595,14 @@ function requestAccount() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.api = api;
|
window.api = api;
|
||||||
requestAccount().then(function (jwk) {
|
run();
|
||||||
run(jwk);
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
document.body.hidden = false;
|
document.body.hidden = false;
|
||||||
}, 50);
|
}, 50);
|
||||||
});
|
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
window.changeState = changeState;
|
||||||
}());
|
}());
|
||||||
|
|
|
@ -34,11 +34,12 @@ module.exports = function eggspress() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var urlstr = (req.url.replace(/\/$/, '') + '/');
|
var urlstr = (req.url.replace(/\/$/, '') + '/');
|
||||||
if (!urlstr.match(todo[0])) {
|
var match = urlstr.match(todo[0]);
|
||||||
|
if (!match) {
|
||||||
//console.log("[eggspress] pattern doesn't match", todo[0], req.url);
|
//console.log("[eggspress] pattern doesn't match", todo[0], req.url);
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
} else if ('string' === typeof todo[0] && 0 !== urlstr.match(todo[0]).index) {
|
} else if ('string' === typeof todo[0] && 0 !== match.index) {
|
||||||
//console.log("[eggspress] string pattern is not the start", todo[0], req.url);
|
//console.log("[eggspress] string pattern is not the start", todo[0], req.url);
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
|
@ -58,6 +59,7 @@ module.exports = function eggspress() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var fns = todo[1].slice(0);
|
var fns = todo[1].slice(0);
|
||||||
|
req.params = match.slice(1);
|
||||||
|
|
||||||
function nextTodo(err) {
|
function nextTodo(err) {
|
||||||
if (err) { fail(err); return; }
|
if (err) { fail(err); return; }
|
||||||
|
|
|
@ -93,26 +93,45 @@ module.exports.create = function (state) {
|
||||||
}
|
}
|
||||||
return reqOpts;
|
return reqOpts;
|
||||||
};
|
};
|
||||||
RC.createErrorHandler = function (replay, opts, cb) {
|
RC.createRelauncher = function (replay, opts, cb) {
|
||||||
return function (err) {
|
return function (err) {
|
||||||
|
/*global Promise*/
|
||||||
|
var p = new Promise(function (resolve, reject) {
|
||||||
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
|
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
|
||||||
// ECONNREFUSED - leftover socket just needs to be restarted
|
// ECONNREFUSED - leftover socket just needs to be restarted
|
||||||
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
|
if ('ENOENT' !== err.code && 'ECONNREFUSED' !== err.code) {
|
||||||
if (opts._taketwo) {
|
reject(err);
|
||||||
cb(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
|
|
||||||
if (err) { cb(err); return; }
|
|
||||||
opts._taketwo = true;
|
|
||||||
setTimeout(function () {
|
|
||||||
replay(opts, cb);
|
|
||||||
}, 2500);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(err);
|
// retried and failed again: quit
|
||||||
|
if (opts._taketwo) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
|
||||||
|
if (err) { reject(err); return; }
|
||||||
|
opts._taketwo = true;
|
||||||
|
setTimeout(function () {
|
||||||
|
if (replay.length <= 1) {
|
||||||
|
replay(opts).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
replay(opts, function (err, res) {
|
||||||
|
if (err) { reject(err); }
|
||||||
|
else { resolve(res); }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, 2500);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
if (cb) {
|
||||||
|
p.then(function () { cb(null); }).catch(function (err) { cb(err); });
|
||||||
|
}
|
||||||
|
return p;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
RC.request = function request(opts, fn) {
|
RC.request = function request(opts, fn) {
|
||||||
|
@ -141,7 +160,8 @@ module.exports.create = function (state) {
|
||||||
makeResponder(service, resp, fn);
|
makeResponder(service, resp, fn);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', RC.createErrorHandler(RC.request, opts, fn));
|
var errHandler = RC.createRelauncher(RC.request, opts, fn);
|
||||||
|
req.on('error', errHandler);
|
||||||
|
|
||||||
// Simple GET
|
// Simple GET
|
||||||
if ('POST' !== method || !opts.data) {
|
if ('POST' !== method || !opts.data) {
|
||||||
|
|
Loading…
Reference in New Issue