local account registration from CLI! w00t!
This commit is contained in:
parent
a5c448902e
commit
38123793c4
|
@ -25,6 +25,7 @@ var camelCopy = recase.camelCopy.bind(recase);
|
||||||
//var snakeCopy = recase.snakeCopy.bind(recase);
|
//var snakeCopy = recase.snakeCopy.bind(recase);
|
||||||
|
|
||||||
var urequest = require('@coolaj86/urequest');
|
var urequest = require('@coolaj86/urequest');
|
||||||
|
var urequestAsync = require('util').promisify(urequest);
|
||||||
var common = require('../lib/cli-common.js');
|
var common = require('../lib/cli-common.js');
|
||||||
|
|
||||||
var defaultConfPath = path.join(os.homedir(), '.config/telebit');
|
var defaultConfPath = path.join(os.homedir(), '.config/telebit');
|
||||||
|
@ -335,9 +336,7 @@ function askForConfig(state, mainCb) {
|
||||||
var RC;
|
var RC;
|
||||||
|
|
||||||
function parseConfig(err, text) {
|
function parseConfig(err, text) {
|
||||||
function handleConfig(err, config) {
|
function handleConfig(config) {
|
||||||
if (err) { throw err; }
|
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -346,20 +345,6 @@ function parseConfig(err, text) {
|
||||||
console.info(verstr.join(' '));
|
console.info(verstr.join(' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// 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
|
||||||
|
@ -492,6 +477,7 @@ function parseConfig(err, text) {
|
||||||
|
|
||||||
state._clientConfig = camelCopy(state._clientConfig || {}) || {};
|
state._clientConfig = camelCopy(state._clientConfig || {}) || {};
|
||||||
RC = require('../lib/rc/index.js').create(state);
|
RC = require('../lib/rc/index.js').create(state);
|
||||||
|
RC.requestAsync = require('util').promisify(RC.request);
|
||||||
|
|
||||||
if (!Object.keys(state._clientConfig).length) {
|
if (!Object.keys(state._clientConfig).length) {
|
||||||
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
|
console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')');
|
||||||
|
@ -682,7 +668,63 @@ function parseConfig(err, text) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
RC.request({ service: 'config', method: 'GET' }, handleConfig);
|
var bootState = {};
|
||||||
|
function bootstrap() {
|
||||||
|
// 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') }).then(function (resp) {
|
||||||
|
var nonce = resp.headers['replay-nonce'];
|
||||||
|
var newAccountUrl = RC.resolve('/acme/new-acct');
|
||||||
|
return keypairs.signJws({
|
||||||
|
jwk: state.key
|
||||||
|
, protected: {
|
||||||
|
// alg will be filled out automatically
|
||||||
|
jwk: state.pub
|
||||||
|
, 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' }
|
||||||
|
}).then(function (resp) {
|
||||||
|
//nonce = resp.headers['replay-nonce'];
|
||||||
|
if (!resp.body || 'valid' !== resp.body.status) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}).then(handleConfig);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(RC.createErrorHandler(bootstrap, bootState, function (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(17);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsers = {
|
var parsers = {
|
||||||
|
|
204
bin/telebitd.js
204
bin/telebitd.js
|
@ -16,6 +16,7 @@ var crypto = require('crypto');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var os = require('os');
|
var os = require('os');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var fsp = fs.promises;
|
||||||
var urequest = require('@coolaj86/urequest');
|
var urequest = require('@coolaj86/urequest');
|
||||||
var urequestAsync = require('util').promisify(urequest);
|
var urequestAsync = require('util').promisify(urequest);
|
||||||
var common = require('../lib/cli-common.js');
|
var common = require('../lib/cli-common.js');
|
||||||
|
@ -110,8 +111,20 @@ function getServername(servernames, sub) {
|
||||||
})[0];
|
})[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*global Promise*/
|
||||||
|
var _savingConfig = Promise.resolve();
|
||||||
function saveConfig(cb) {
|
function saveConfig(cb) {
|
||||||
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), cb);
|
// simple sequencing chain so that write corruption is not possible
|
||||||
|
_savingConfig = _savingConfig.then(function () {
|
||||||
|
return fsp.writeFile(confpath, YAML.safeDump(snakeCopy(state.config))).then(function () {
|
||||||
|
try {
|
||||||
|
cb();
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e.stack);
|
||||||
|
process.exit(47);
|
||||||
|
}
|
||||||
|
}).catch(cb);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
var controllers = {};
|
var controllers = {};
|
||||||
controllers.http = function (req, res) {
|
controllers.http = function (req, res) {
|
||||||
|
@ -366,7 +379,7 @@ controllers.relay = function (req, res) {
|
||||||
};
|
};
|
||||||
controllers._nonces = {};
|
controllers._nonces = {};
|
||||||
controllers._requireNonce = function (req, res, next) {
|
controllers._requireNonce = function (req, res, next) {
|
||||||
var nonce = req.jws && req.jws.protected && req.jws.protected.nonce;
|
var nonce = req.jws && req.jws.header && req.jws.header.nonce;
|
||||||
var active = (Date.now() - controllers._nonces[nonce]) < (4 * 60 * 60 * 1000);
|
var active = (Date.now() - controllers._nonces[nonce]) < (4 * 60 * 60 * 1000);
|
||||||
if (!active) {
|
if (!active) {
|
||||||
// TODO proper headers and error message
|
// TODO proper headers and error message
|
||||||
|
@ -381,31 +394,133 @@ controllers._issueNonce = function (req, res) {
|
||||||
var nonce = toUrlSafe(crypto.randomBytes(16).toString('base64'));
|
var nonce = toUrlSafe(crypto.randomBytes(16).toString('base64'));
|
||||||
// TODO associate with a TLS session
|
// TODO associate with a TLS session
|
||||||
controllers._nonces[nonce] = Date.now();
|
controllers._nonces[nonce] = Date.now();
|
||||||
res.headers.set("Replay-Nonce", nonce);
|
res.setHeader("Replay-Nonce", nonce);
|
||||||
return nonce;
|
return nonce;
|
||||||
};
|
};
|
||||||
controllers.newNonce = function (req, res) {
|
controllers.newNonce = function (req, res) {
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.headers.set("Cache-Control", "max-age=0, no-cache, no-store");
|
res.setHeader("Cache-Control", "max-age=0, no-cache, no-store");
|
||||||
// TODO
|
// TODO
|
||||||
//res.headers.set("Date", "Sun, 10 Mar 2019 08:04:45 GMT");
|
//res.setHeader("Date", "Sun, 10 Mar 2019 08:04:45 GMT");
|
||||||
// is this the expiration of the nonce itself? methinks maybe so
|
// is this the expiration of the nonce itself? methinks maybe so
|
||||||
//res.headers.set("Expires", "Sun, 10 Mar 2019 08:04:45 GMT");
|
//res.setHeader("Expires", "Sun, 10 Mar 2019 08:04:45 GMT");
|
||||||
// TODO use one of the registered domains
|
// TODO use one of the registered domains
|
||||||
//var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index"
|
//var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index"
|
||||||
var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined);
|
var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined);
|
||||||
var indexUrl = "http://localhost:" + port + "/index";
|
var indexUrl = "http://localhost:" + port + "/index";
|
||||||
res.headers.set("Link", "Link: <" + indexUrl + ">;rel=\"index\"");
|
res.setHeader("Link", "<" + indexUrl + ">;rel=\"index\"");
|
||||||
res.headers.set("Pragma", "no-cache");
|
res.setHeader("Cache-Control", "max-age=0, no-cache, no-store");
|
||||||
//res.headers.set("Strict-Transport-Security", "max-age=604800");
|
res.setHeader("Pragma", "no-cache");
|
||||||
res.headers.set("X-Frame-Options", "DENY");
|
//res.setHeader("Strict-Transport-Security", "max-age=604800");
|
||||||
|
res.setHeader("X-Frame-Options", "DENY");
|
||||||
|
|
||||||
|
controllers._issueNonce(req, res);
|
||||||
res.end("");
|
res.end("");
|
||||||
};
|
};
|
||||||
controllers.newAccount = function (req, res) {
|
controllers.newAccount = function (req, res) {
|
||||||
controllers._requireNonce(req, res, function () {
|
controllers._requireNonce(req, res, function () {
|
||||||
res.statusCode = 500;
|
// TODO clean up error messages to be similar to ACME
|
||||||
res.end("not implemented yet");
|
|
||||||
|
// check if there's a public key
|
||||||
|
if (!req.jws || !req.jws.header.kid || !req.jws.header.jwk) {
|
||||||
|
res.statusCode = 422;
|
||||||
|
res.send({ error: { message: "jws body was not present or could not be validated" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO mx record email validation
|
||||||
|
if (!Array.isArray(req.body.contact) || !req.body.contact.length && '127.0.0.1' !== req.connection.remoteAddress) {
|
||||||
|
// req.body.contact: [ 'mailto:email' ]
|
||||||
|
res.statusCode = 422;
|
||||||
|
res.send({ error: { message: "jws signed payload should contain a valid mailto:email in the contact array" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!req.body.termsOfServiceAgreed) {
|
||||||
|
// req.body.termsOfServiceAgreed: true
|
||||||
|
res.statusCode = 422;
|
||||||
|
res.send({ error: { message: "jws signed payload should have termsOfServiceAgreed: true" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We verify here regardless of whether or not it was verified before,
|
||||||
|
// because it needs to be signed by the presenter of the public key,
|
||||||
|
// not just a trusted key
|
||||||
|
return verifyJws(req.jws.header.jwk, req.jws).then(function (verified) {
|
||||||
|
if (!verified) {
|
||||||
|
res.statusCode = 422;
|
||||||
|
res.send({ error: { message: "jws body was not present or could not be validated" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var jwk = req.jws.header.jwk;
|
||||||
|
return keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
|
||||||
|
// Note: we can get any number of account requests
|
||||||
|
// and these need to be stored for some space of time
|
||||||
|
// to await verification.
|
||||||
|
// we'll have to expire them somehow and prevent DoS
|
||||||
|
|
||||||
|
// check if this account already exists
|
||||||
|
var account;
|
||||||
|
DB.accounts.some(function (acc) {
|
||||||
|
// TODO calculate thumbprint from jwk
|
||||||
|
// find a key with matching jwk
|
||||||
|
if (acc.thumb === thumb) {
|
||||||
|
account = acc;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// TODO ACME requires kid to be the account URL (STUPID!!!)
|
||||||
|
// rather than the key id (as decided by the key issuer)
|
||||||
|
// not sure if it's necessary to handle it that way though
|
||||||
|
});
|
||||||
|
|
||||||
|
var myBaseUrl = (req.connection.encrypted ? 'https' : 'http') + '://' + req.headers.host;
|
||||||
|
if (!account) {
|
||||||
|
// fail if onlyReturnExisting is not false
|
||||||
|
if (req.body.onlyReturnExisting) {
|
||||||
|
res.statusCode = 422;
|
||||||
|
res.send({ error: { message: "onlyReturnExisting is set, so there's nothing to do" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.statusCode = 201;
|
||||||
|
account = {};
|
||||||
|
account._id = crypto.randomBytes(16).toString('base64');
|
||||||
|
// TODO be better about this
|
||||||
|
account.location = myBaseUrl + '/acme/accounts/' + account._id;
|
||||||
|
account.thumb = thumb;
|
||||||
|
account.pub = jwk;
|
||||||
|
account.contact = req.body.contact;
|
||||||
|
DB.accounts.push(account);
|
||||||
|
state.config.accounts = DB.accounts;
|
||||||
|
saveConfig(function () {});
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = {
|
||||||
|
status: 'valid'
|
||||||
|
, contact: account.contact // [ "mailto:john.doe@gmail.com" ],
|
||||||
|
, orders: account.location + '/orders'
|
||||||
|
// optional / off-spec
|
||||||
|
, id: account._id
|
||||||
|
, jwk: account.pub
|
||||||
|
/*
|
||||||
|
// I'm not sure if we have the real IP through telebit's network wrapper at this point
|
||||||
|
// TODO we also need to set X-Forwarded-Addr as a proxy
|
||||||
|
"initialIp": req.connection.remoteAddress, //"128.187.116.28",
|
||||||
|
"createdAt": (new Date()).toISOString(), // "2018-04-17T21:29:10.833305103Z",
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
res.setHeader('Location', account.location);
|
||||||
|
res.send(result);
|
||||||
|
/*
|
||||||
|
Cache-Control: max-age=0, no-cache, no-store
|
||||||
|
Content-Type: application/json
|
||||||
|
Expires: Tue, 17 Apr 2018 21:29:10 GMT
|
||||||
|
Link: <https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;rel="terms-of-service"
|
||||||
|
Location: https://acme-staging-v02.api.letsencrypt.org/acme/acct/5937234
|
||||||
|
Pragma: no-cache
|
||||||
|
Replay-nonce: DKxX61imF38y_qkKvVcnWyo9oxQlHll0t9dMwGbkcxw
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -472,6 +587,7 @@ function jwtEggspress(req, res, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO verify if possible
|
// TODO verify if possible
|
||||||
|
console.warn("[warn] JWT is not verified yet");
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,7 +595,7 @@ function verifyJws(jwk, jws) {
|
||||||
return keypairs.export({ jwk: jwk }).then(function (pem) {
|
return keypairs.export({ jwk: jwk }).then(function (pem) {
|
||||||
var alg = 'SHA' + jws.header.alg.replace(/[^\d]+/i, '');
|
var alg = 'SHA' + jws.header.alg.replace(/[^\d]+/i, '');
|
||||||
var sig = ecdsaAsn1SigToJwtSig(jws.header.alg, jws.signature);
|
var sig = ecdsaAsn1SigToJwtSig(jws.header.alg, jws.signature);
|
||||||
return require('crypto')
|
return crypto
|
||||||
.createVerify(alg)
|
.createVerify(alg)
|
||||||
.update(jws.protected + '.' + jws.payload)
|
.update(jws.protected + '.' + jws.payload)
|
||||||
.verify(pem, sig, 'base64');
|
.verify(pem, sig, 'base64');
|
||||||
|
@ -487,11 +603,14 @@ function verifyJws(jwk, jws) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function jwsEggspress(req, res, next) {
|
function jwsEggspress(req, res, next) {
|
||||||
|
// Check to see if this looks like a JWS
|
||||||
// TODO check header application/jose+json ??
|
// TODO check header application/jose+json ??
|
||||||
if (!req.body || !(req.body.protected && req.body.payload && req.body.signature)) {
|
if (!req.body || !(req.body.protected && req.body.payload && req.body.signature)) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode it a bit
|
||||||
req.jws = req.body;
|
req.jws = req.body;
|
||||||
req.jws.header = JSON.parse(Buffer.from(req.jws.protected, 'base64'));
|
req.jws.header = JSON.parse(Buffer.from(req.jws.protected, 'base64'));
|
||||||
req.body = Buffer.from(req.jws.payload, 'base64');
|
req.body = Buffer.from(req.jws.payload, 'base64');
|
||||||
|
@ -499,27 +618,40 @@ function jwsEggspress(req, res, next) {
|
||||||
req.body = JSON.parse(req.body);
|
req.body = JSON.parse(req.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a key we already trust
|
||||||
var vjwk;
|
var vjwk;
|
||||||
DB.pubs.some(function (jwk) {
|
DB.pubs.some(function (jwk) {
|
||||||
if (jwk.kid === req.jws.header.kid) {
|
if (jwk.kid === req.jws.header.kid) {
|
||||||
vjwk = jwk;
|
vjwk = jwk;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if there aren't any keys that we trust
|
||||||
|
// and this has signed itself, then make it a key we trust
|
||||||
|
// (TODO: move this all to the new account function)
|
||||||
if ((0 === DB.pubs.length && req.jws.header.jwk)) {
|
if ((0 === DB.pubs.length && req.jws.header.jwk)) {
|
||||||
vjwk = req.jws.header.jwk;
|
vjwk = req.jws.header.jwk;
|
||||||
if (!vjwk.kid) { throw Error("Impossible: no key id"); }
|
if (!vjwk.kid) { throw Error("Impossible: no key id"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't verify if it can't be verified
|
||||||
|
if (!vjwk) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the verification
|
||||||
return verifyJws(vjwk, req.jws).then(function (verified) {
|
return verifyJws(vjwk, req.jws).then(function (verified) {
|
||||||
if (true !== verified) {
|
if (true !== verified) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Mark as verified
|
||||||
req.jws.verified = verified;
|
req.jws.verified = verified;
|
||||||
|
|
||||||
if (0 !== DB.pubs.length) {
|
// (double check) DO NOT save if there are existing pubs
|
||||||
return;
|
if (0 !== DB.pubs.length) { return; }
|
||||||
}
|
|
||||||
return keystore.set(vjwk.kid + '.pub.jwk.json', vjwk);
|
return keystore.set(vjwk.kid + PUBEXT, vjwk);
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
@ -828,16 +960,35 @@ function handleApi() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO turn strings into regexes to match beginnings
|
// TODO turn strings into regexes to match beginnings
|
||||||
|
app.get('/.well-known/openid-configuration', function (req, res) {
|
||||||
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
res.setHeader("Access-Control-Expose-Headers", "Link, Replay-Nonce, Location");
|
||||||
|
res.setHeader("Access-Control-Max-Age", "86400");
|
||||||
|
if ('OPTIONS' === req.method) { res.end(); return; }
|
||||||
|
res.send({
|
||||||
|
jwks_uri: 'http://localhost/.well-known/jwks.json'
|
||||||
|
, acme_uri: 'http://localhost/acme/directory'
|
||||||
|
});
|
||||||
|
});
|
||||||
app.use('/acme', function acmeCors(req, res, next) {
|
app.use('/acme', function acmeCors(req, res, next) {
|
||||||
// Taken from New-Nonce
|
// Taken from New-Nonce
|
||||||
res.headers.set("Access-Control-Allow-Headers", "Content-Type");
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||||
res.headers.set("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
res.headers.set("Access-Control-Expose-Headers", "Link, Replay-Nonce, Location");
|
res.setHeader("Access-Control-Expose-Headers", "Link, Replay-Nonce, Location");
|
||||||
res.headers.set("Access-Control-Max-Age", "86400");
|
res.setHeader("Access-Control-Max-Age", "86400");
|
||||||
|
if ('OPTIONS' === req.method) { res.end(); return; }
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use('/acme/new-nonce', controllers.newNonce);
|
app.get('/acme/directory', function (req, res) {
|
||||||
app.use('/acme/new-acct', controllers.newAccount);
|
res.send({
|
||||||
|
'new-nonce': '/acme/new-nonce'
|
||||||
|
, 'new-account': '/acme/new-acct'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
app.head('/acme/new-nonce', controllers.newNonce);
|
||||||
|
app.get('/acme/new-nonce', controllers.newNonce);
|
||||||
|
app.post('/acme/new-acct', controllers.newAccount);
|
||||||
app.use(/\b(relay)\b/, controllers.relay);
|
app.use(/\b(relay)\b/, controllers.relay);
|
||||||
app.get(/\b(config)\b/, getConfigOnly);
|
app.get(/\b(config)\b/, getConfigOnly);
|
||||||
app.use(/\b(init|config)\b/, initOrConfig);
|
app.use(/\b(init|config)\b/, initOrConfig);
|
||||||
|
@ -872,6 +1023,7 @@ function serveControlsHelper() {
|
||||||
|
|
||||||
app.use('/rpc/', apiHandler);
|
app.use('/rpc/', apiHandler);
|
||||||
app.use('/api/', apiHandler);
|
app.use('/api/', apiHandler);
|
||||||
|
app.use('/acme/', apiHandler);
|
||||||
app.use('/', serveStatic);
|
app.use('/', serveStatic);
|
||||||
|
|
||||||
controlServer = http.createServer(app);
|
controlServer = http.createServer(app);
|
||||||
|
@ -1011,6 +1163,7 @@ function parseConfig(err, text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.config = camelCopy(state.config || {}) || {};
|
state.config = camelCopy(state.config || {}) || {};
|
||||||
|
DB.accounts = state.config.accounts || [];
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
||||||
|
@ -1414,6 +1567,7 @@ state.net = state.net || {
|
||||||
|
|
||||||
var DB = {};
|
var DB = {};
|
||||||
DB.pubs = [];
|
DB.pubs = [];
|
||||||
|
DB.accounts = [];
|
||||||
var token;
|
var token;
|
||||||
var tokenname = "access_token.jwt";
|
var tokenname = "access_token.jwt";
|
||||||
try {
|
try {
|
||||||
|
@ -1512,8 +1666,8 @@ function ecdsaAsn1SigToJwtSig(alg, b64sig) {
|
||||||
|
|
||||||
function toUrlSafe(b64) {
|
function toUrlSafe(b64) {
|
||||||
return b64
|
return b64
|
||||||
.replace(/-/g, '+')
|
.replace(/\+/g, '-')
|
||||||
.replace(/_/g, '/')
|
.replace(/\//g, '_')
|
||||||
.replace(/=/g, '')
|
.replace(/=/g, '')
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,12 @@ module.exports = function eggspress() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.url.match(todo[0])) {
|
var urlstr = (req.url.replace(/\/$/, '') + '/');
|
||||||
|
if (!urlstr.match(todo[0])) {
|
||||||
//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 !== req.url.match(todo[0]).index) {
|
} else if ('string' === typeof todo[0] && 0 !== urlstr.match(todo[0]).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;
|
||||||
|
@ -70,7 +71,7 @@ module.exports = function eggspress() {
|
||||||
app.use = function (pattern, fn) {
|
app.use = function (pattern, fn) {
|
||||||
return app._use('', pattern, fn);
|
return app._use('', pattern, fn);
|
||||||
};
|
};
|
||||||
[ 'GET', 'POST', 'DELETE' ].forEach(function (method) {
|
[ 'HEAD', 'GET', 'POST', 'DELETE' ].forEach(function (method) {
|
||||||
app[method.toLowerCase()] = function (pattern, fn) {
|
app[method.toLowerCase()] = function (pattern, fn) {
|
||||||
return app._use(method, pattern, fn);
|
return app._use(method, pattern, fn);
|
||||||
};
|
};
|
||||||
|
|
|
@ -72,6 +72,49 @@ module.exports.create = function (state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var RC = {};
|
var RC = {};
|
||||||
|
RC.resolve = function (pathstr) {
|
||||||
|
// TODO use real hostname and return reqOpts rather than string?
|
||||||
|
return 'http://localhost:' + (RC.port({}).port||'1').toString() + '/' + pathstr.replace(/^\//, '');
|
||||||
|
};
|
||||||
|
RC.port = function (reqOpts) {
|
||||||
|
var fs = require('fs');
|
||||||
|
var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port');
|
||||||
|
if (fs.existsSync(portFile)) {
|
||||||
|
reqOpts.host = 'localhost';
|
||||||
|
reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10);
|
||||||
|
if (!state.ipc) {
|
||||||
|
state.ipc = {};
|
||||||
|
}
|
||||||
|
state.ipc.type = 'port';
|
||||||
|
state.ipc.path = path.dirname(state._ipc.path);
|
||||||
|
state.ipc.port = reqOpts.port;
|
||||||
|
} else {
|
||||||
|
reqOpts.socketPath = state._ipc.path;
|
||||||
|
}
|
||||||
|
return reqOpts;
|
||||||
|
};
|
||||||
|
RC.createErrorHandler = function (replay, opts, cb) {
|
||||||
|
return function (err) {
|
||||||
|
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
|
||||||
|
// ECONNREFUSED - leftover socket just needs to be restarted
|
||||||
|
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
|
||||||
|
if (opts._taketwo) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(err);
|
||||||
|
};
|
||||||
|
};
|
||||||
RC.request = function request(opts, fn) {
|
RC.request = function request(opts, fn) {
|
||||||
if (!opts) { opts = {}; }
|
if (!opts) { opts = {}; }
|
||||||
var service = opts.service || 'config';
|
var service = opts.service || 'config';
|
||||||
|
@ -93,44 +136,12 @@ module.exports.create = function (state) {
|
||||||
method: method
|
method: method
|
||||||
, path: url
|
, path: url
|
||||||
};
|
};
|
||||||
var fs = require('fs');
|
reqOpts = RC.port(reqOpts);
|
||||||
var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port');
|
|
||||||
if (fs.existsSync(portFile)) {
|
|
||||||
reqOpts.host = 'localhost';
|
|
||||||
reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10);
|
|
||||||
if (!state.ipc) {
|
|
||||||
state.ipc = {};
|
|
||||||
}
|
|
||||||
state.ipc.type = 'port';
|
|
||||||
state.ipc.path = path.dirname(state._ipc.path);
|
|
||||||
state.ipc.port = reqOpts.port;
|
|
||||||
} else {
|
|
||||||
reqOpts.socketPath = state._ipc.path;
|
|
||||||
}
|
|
||||||
var req = http.request(reqOpts, function (resp) {
|
var req = http.request(reqOpts, function (resp) {
|
||||||
makeResponder(service, resp, fn);
|
makeResponder(service, resp, fn);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', function (err) {
|
req.on('error', RC.createErrorHandler(RC.request, opts, fn));
|
||||||
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
|
|
||||||
// ECONNREFUSED - leftover socket just needs to be restarted
|
|
||||||
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
|
|
||||||
if (opts._taketwo) {
|
|
||||||
fn(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
|
|
||||||
if (err) { fn(err); return; }
|
|
||||||
opts._taketwo = true;
|
|
||||||
setTimeout(function () {
|
|
||||||
RC.request(opts, fn);
|
|
||||||
}, 2500);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simple GET
|
// Simple GET
|
||||||
if ('POST' !== method || !opts.data) {
|
if ('POST' !== method || !opts.data) {
|
||||||
|
@ -150,7 +161,8 @@ module.exports.create = function (state) {
|
||||||
// alg will be filled out automatically
|
// alg will be filled out automatically
|
||||||
jwk: state.pub
|
jwk: state.pub
|
||||||
, nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
|
, nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
|
||||||
, url: 'https://' + reqOpts.host + reqOpts.path
|
// TODO make localhost exceptional
|
||||||
|
, url: RC.resolve(reqOpts.path)
|
||||||
}
|
}
|
||||||
, payload: JSON.stringify(opts.data)
|
, payload: JSON.stringify(opts.data)
|
||||||
}).then(function (jws) {
|
}).then(function (jws) {
|
||||||
|
|
|
@ -435,9 +435,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keypairs": {
|
"keypairs": {
|
||||||
"version": "1.2.12",
|
"version": "1.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz",
|
||||||
"integrity": "sha512-zYjYdDvo7G4AIkkZVM3WEJBTRUIrFzYswYNqCxcCPHUsgbBBdewSHAH1CiaQ+VA6Yb7BLEPIv8gFrRz5wJrgsw==",
|
"integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"eckles": "^1.4.1",
|
"eckles": "^1.4.1",
|
||||||
"rasha": "^1.2.4"
|
"rasha": "^1.2.4"
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
"greenlock": "^2.6.7",
|
"greenlock": "^2.6.7",
|
||||||
"js-yaml": "^3.11.0",
|
"js-yaml": "^3.11.0",
|
||||||
"keyfetch": "^1.1.8",
|
"keyfetch": "^1.1.8",
|
||||||
"keypairs": "^1.2.12",
|
"keypairs": "^1.2.14",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"proxy-packer": "^2.0.2",
|
"proxy-packer": "^2.0.2",
|
||||||
"ps-list": "^5.0.0",
|
"ps-list": "^5.0.0",
|
||||||
|
|
Loading…
Reference in New Issue