From 72ccd8f3b704f40c7b789d487263d30e535932de Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 31 Oct 2019 05:49:37 -0600 Subject: [PATCH] v3.0.15: drastically reduce scope of manager --- accounts.js | 10 +- certificates.js | 41 +-- greenlock.js | 656 ++++++++++++++++++++++---------------------- manager-underlay.js | 258 +++++++++++++++++ package-lock.json | 30 +- package.json | 5 +- tests/index.js | 51 ++-- 7 files changed, 668 insertions(+), 383 deletions(-) create mode 100644 manager-underlay.js diff --git a/accounts.js b/accounts.js index 7dd7568..738c03c 100644 --- a/accounts.js +++ b/accounts.js @@ -7,10 +7,7 @@ var E = require('./errors.js'); var pending = {}; A._getOrCreate = function(gnlck, mconf, db, acme, args) { - var email = - args.subscriberEmail || - mconf.subscriberEmail || - gnlck._defaults.subscriberEmail; + var email = args.subscriberEmail || mconf.subscriberEmail; if (!email) { throw E.NO_SUBSCRIBER('get account', args.subject); @@ -70,10 +67,7 @@ A._rawGetOrCreate = function(gnlck, mconf, db, acme, args, email) { }; A._newAccount = function(gnlck, mconf, db, acme, args, email, fullAccount) { - var keyType = - args.accountKeyType || - mconf.accountKeyType || - gnlck._defaults.accountKeyType; + var keyType = args.accountKeyType || mconf.accountKeyType; var query = { subject: args.subject, email: email, diff --git a/certificates.js b/certificates.js index ff8496a..2e17485 100644 --- a/certificates.js +++ b/certificates.js @@ -21,12 +21,12 @@ var rawPending = {}; // Certificates C._getOrOrder = function(gnlck, mconf, db, acme, chs, acc, args) { - var email = - args.subscriberEmail || - mconf.subscriberEmail || - gnlck._defaults.subscriberEmail; + var email = args.subscriberEmail || mconf.subscriberEmail; - var id = args.altnames.join(' '); + var id = args.altnames + .slice(0) + .sort() + .join(' '); if (pending[id]) { return pending[id]; } @@ -123,17 +123,11 @@ C._rawOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) { return rawPending[id]; } - var keyType = - args.serverKeyType || - mconf.serverKeyType || - gnlck._defaults.serverKeyType; + var keyType = args.serverKeyType || mconf.serverKeyType; var query = { subject: args.subject, certificate: args.certificate || {}, - directoryUrl: - args.directoryUrl || - mconf.directoryUrl || - gnlck._defaults.directoryUrl + directoryUrl: args.directoryUrl || gnlck._defaults.directoryUrl }; rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType) .then(function(kresult) { @@ -214,10 +208,7 @@ C._check = function(gnlck, mconf, db, args) { subject: args.subject, // may contain certificate.id certificate: args.certificate, - directoryUrl: - args.directoryUrl || - mconf.directoryUrl || - gnlck._defaults.directoryUrl + directoryUrl: args.directoryUrl || gnlck._defaults.directoryUrl }; return db.check(query).then(function(pems) { if (!pems) { @@ -275,13 +266,12 @@ C._renewWithStagger = function(gnlck, mconf, args, pems) { var renewStagger; try { renewStagger = U._parseDuration( - args.renewStagger || - mconf.renewStagger || - gnlck._defaults.renewStagger || - 0 + args.renewStagger || mconf.renewStagger || 0 ); } catch (e) { - renewStagger = U._parseDuration(gnlck._defaults.renewStagger); + renewStagger = U._parseDuration( + args.renewStagger || mconf.renewStagger + ); } // TODO check this beforehand @@ -301,12 +291,9 @@ C._renewWithStagger = function(gnlck, mconf, args, pems) { pems.expiresAt + renewOffset - Math.random() * renewStagger ); }; -C._renewOffset = function(gnlck, mconf, args, pems) { +C._renewOffset = function(gnlck, mconf, args /*, pems*/) { var renewOffset = U._parseDuration( - args.renewOffset || - mconf.renewOffset || - gnlck._defaults.renewOffset || - 0 + args.renewOffset || mconf.renewOffset || 0 ); var week = 1000 * 60 * 60 * 24 * 6; if (!args.force && Math.abs(renewOffset) < week) { diff --git a/greenlock.js b/greenlock.js index 9bb2f16..8409e2e 100644 --- a/greenlock.js +++ b/greenlock.js @@ -4,7 +4,7 @@ var pkg = require('./package.json'); var ACME = require('@root/acme'); var Greenlock = module.exports; -var homedir = require('os').homedir(); +var request = require('@root/request'); var G = Greenlock; var U = require('./utils.js'); @@ -19,6 +19,7 @@ var caches = {}; // { maintainerEmail, directoryUrl, subscriberEmail, store, challenges } G.create = function(gconf) { var greenlock = {}; + var gdefaults = {}; if (!gconf) { gconf = {}; } @@ -33,133 +34,36 @@ G.create = function(gconf) { 'invalid maintainer contact info:', gconf.maintainerEmail ); - // maybe a little harsh? + + // maybe move this to init and don't exit the process, just in case process.exit(1); }); - // TODO default servername is GLE only - - if (!gconf.manager) { - gconf.manager = 'greenlock-manager-fs'; + if ('function' === typeof gconf.notify) { + gdefaults.notify = gconf.notify; + } else { + gdefaults.notify = _notify; } - var Manager; - if ('string' === typeof gconf.manager) { - try { - Manager = require(gconf.manager); - } catch (e) { - if ('MODULE_NOT_FOUND' !== e.code) { - throw e; - } - console.error(e.code); - console.error(e.message); - console.error(gconf.manager); - P._installSync(gconf.manager); - Manager = require(gconf.manager); + if (gconf.directoryUrl) { + gdefaults = gconf.directoryUrl; + if (gconf.staging) { + throw new Error('supply `directoryUrl` or `staging`, but not both'); } + } else if (gconf.staging) { + gdefaults.directoryUrl = + 'https://acme-staging-v02.api.letsencrypt.org/directory'; + } else { + gdefaults.directoryUrl = + 'https://acme-v02.api.letsencrypt.org/directory'; } + console.info('ACME Directory URL:', gdefaults.directoryUrl); - // minimal modification to the original object - var defaults = G._defaults(gconf); - - greenlock.manager = Manager.create(defaults); + var manager = normalizeManager(gconf); + require('./manager-underlay.js').wrap(greenlock, manager, gconf); //console.log('debug greenlock.manager', Object.keys(greenlock.manager)); - greenlock._init = function() { - var p; - greenlock._init = function() { - return p; - }; - p = greenlock.manager.defaults().then(function(conf) { - var changed = false; - if (!conf.challenges) { - changed = true; - conf.challenges = defaults.challenges; - } - if (!conf.store) { - changed = true; - conf.store = defaults.store; - } - if (changed) { - return greenlock.manager.defaults(conf); - } - }); - return p; - }; - - // The goal here is to reduce boilerplate, such as error checking - // and duration parsing, that a manager must implement - greenlock.add = function(args) { - return greenlock._init().then(function() { - return greenlock._add(args).then(function(result) { - greenlock.renew({}).catch(function(err) { - if (!err.context) { - err.contxt = 'renew'; - } - greenlock.notify('error', err); - }); - return result; - }); - }); - }; - greenlock._add = function(args) { - return Promise.resolve().then(function() { - // durations - if (args.renewOffset) { - args.renewOffset = U._parseDuration(args.renewOffset); - } - if (args.renewStagger) { - args.renewStagger = U._parseDuration(args.renewStagger); - } - if (!args.subject) { - throw E.NO_SUBJECT('add'); - } - - if (!args.altnames) { - args.altnames = [args.subject]; - } - if ('string' === typeof args.altnames) { - args.altnames = args.altnames.split(/[,\s]+/); - } - if (args.subject !== args.altnames[0]) { - throw E.BAD_ORDER( - 'add', - '(' + args.subject + ") '" + args.altnames.join("' '") + "'" - ); - } - args.altnames = args.altnames.map(U._encodeName); - - if ( - !args.altnames.every(function(d) { - return U._validName(d); - }) - ) { - throw E.INVALID_HOSTNAME( - 'add', - "'" + args.altnames.join("' '") + "'" - ); - } - - // at this point we know that subject is the first of altnames - return Promise.all( - args.altnames.map(function(d) { - d = d.replace('*.', ''); - return U._validDomain(d); - }) - ).then(function() { - if (!U._uniqueNames(args.altnames)) { - throw E.NOT_UNIQUE( - 'add', - "'" + args.altnames.join("' '") + "'" - ); - } - - return greenlock.manager.add(args); - }); - }); - }; - - greenlock._notify = function(ev, params) { + greenlock.notify = greenlock._notify = function(ev, params) { var mng = greenlock.manager; if ('_' === String(ev)[0]) { @@ -181,38 +85,19 @@ G.create = function(gconf) { return; } - if (mng.notify || greenlock._defaults.notify) { - try { - var p = (mng.notify || greenlock._defaults.notify)(ev, params); - if (p && p.catch) { - p.catch(function(e) { - console.error( - "Promise Rejection on event '" + ev + "':" - ); - console.error(e); - }); - } - } catch (e) { - console.error("Thrown Exception on event '" + ev + "':"); - console.error(e); - } - } else { - if (/error/i.test(ev)) { - console.error("Error event '" + ev + "':"); - console.error(params); - console.error(params.stack); + try { + var p = greenlock._defaults.notify(ev, params); + if (p && p.catch) { + p.catch(function(e) { + console.error("Promise Rejection on event '" + ev + "':"); + console.error(e); + }); } + } catch (e) { + console.error("Thrown Exception on event '" + ev + "':"); + console.error(e); + console.error(params); } - /* - *'cert_issue', { - options: args, - subject: args.subject, - altnames: args.altnames, - account: account, - email: email, - pems: newPems - } - */ if (-1 !== ['cert_issue', 'cert_renewal'].indexOf(ev)) { // We will notify all greenlock users of mandatory and security updates @@ -221,49 +106,62 @@ G.create = function(gconf) { // TODO look at the other one UserEvents.notify({ /* - // maintainer should be only on pre-publish, or maybe install, I think - maintainerEmail: greenlock._defaults._maintainerEmail, - name: greenlock._defaults._packageAgent, - version: greenlock._defaults._maintainerPackageVersion, - //action: params.pems._type, - domains: params.altnames, - subscriberEmail: greenlock._defaults._subscriberEmail, - // TODO enable for Greenlock Pro - //customerEmail: args.customerEmail - telemetry: greenlock._defaults.telemetry - */ + // maintainer should be only on pre-publish, or maybe install, I think + maintainerEmail: greenlock._defaults._maintainerEmail, + name: greenlock._defaults._packageAgent, + version: greenlock._defaults._maintainerPackageVersion, + //action: params.pems._type, + domains: params.altnames, + subscriberEmail: greenlock._defaults._subscriberEmail, + // TODO enable for Greenlock Pro + //customerEmail: args.customerEmail + telemetry: greenlock._defaults.telemetry + */ }); } }; - greenlock._single = function(args) { - if ('string' !== typeof args.servername) { - return Promise.reject(new Error('no `servername` given')); - } - // www.example.com => *.example.com - args.wildname = - '*.' + - args.servername - .split('.') - .slice(1) - .join('.'); - if ( - args.servernames || - args.subject || - args.renewBefore || - args.issueBefore || - args.expiresBefore - ) { - return Promise.reject( - new Error( - 'bad arguments, did you mean to call greenlock.renew()?' - ) - ); + // The purpose of init is to make MCONF the source of truth + greenlock._init = function() { + var p; + greenlock._init = function() { + return p; + }; + + if (manager.init) { + // TODO punycode? + p = manager.init({ + request: request + //punycode: require('punycode') + }); + } else { + p = Promise.resolve(); } - // duplicate, force, and others still allowed - return Promise.resolve(args); + p = p + .then(function() { + return manager.defaults().then(function(MCONF) { + mergeDefaults(MCONF, gconf); + if (true === MCONF.agreeToTerms) { + gdefaults.agreeToTerms = function(tos) { + return Promise.resolve(tos); + }; + } + return manager.defaults(MCONF); + }); + }) + .catch(function(err) { + console.error('Fatal error during greenlock init:'); + console.error(err); + process.exit(1); + }); + return p; }; + // The goal here is to reduce boilerplate, such as error checking + // and duration parsing, that a manager must implement + greenlock.sites.add = greenlock.add = greenlock.manager.add; + + // certs.get greenlock.get = function(args) { return greenlock ._single(args) @@ -306,21 +204,32 @@ G.create = function(gconf) { }); }; - greenlock._find = function(args) { - var altnames = args.altnames || []; - - // servername, wildname, and altnames are all the same - ['wildname', 'servername'].forEach(function(k) { - var altname = args[k]; - if (altname && !altnames.includes(altname)) { - altnames.push(altname); - } - }); - if (altnames.length) { - args.altnames = altnames; + greenlock._single = function(args) { + if ('string' !== typeof args.servername) { + return Promise.reject(new Error('no `servername` given')); } - - return greenlock.manager.find(args); + // www.example.com => *.example.com + args.wildname = + '*.' + + args.servername + .split('.') + .slice(1) + .join('.'); + if ( + args.servernames || + args.subject || + args.renewBefore || + args.issueBefore || + args.expiresBefore + ) { + return Promise.reject( + new Error( + 'bad arguments, did you mean to call greenlock.renew()?' + ) + ); + } + // duplicate, force, and others still allowed + return Promise.resolve(args); }; greenlock._config = function(args) { @@ -338,13 +247,12 @@ G.create = function(gconf) { if (site.store && site.challenges) { return site; } - return greenlock.manager.defaults().then(function(mconf) { + return manager.defaults().then(function(mconf) { if (!site.store) { - site.store = mconf.store || greenlock._defaults.store; + site.store = mconf.store; } if (!site.challenges) { - site.challenges = - mconf.challenges || greenlock._defaults.challenges; + site.challenges = mconf.challenges; } return site; }); @@ -353,7 +261,7 @@ G.create = function(gconf) { // needs to get info about the renewal, such as which store and challenge(s) to use greenlock.renew = function(args) { - return greenlock.manager.defaults().then(function(mconf) { + return manager.defaults().then(function(mconf) { return greenlock._renew(mconf, args); }); }; @@ -362,26 +270,11 @@ G.create = function(gconf) { args = {}; } - // durations - if (args.renewOffset) { - args.renewOffset = U._parseDuration(args.renewOffset); - } - if (args.renewStagger) { - args.renewStagger = U._parseDuration(args.renewStagger); - } - - if (args.servername) { - // this doesn't have to be the subject, it can be anything - // however, not sure how useful this really is... - args.servername = args.servername.toLowerCase(); - } - + var renewedOrFailed = []; //console.log('greenlock._renew find', args); return greenlock._find(args).then(function(sites) { // Note: the manager must guaranteed that these are mutable copies - //console.log('greenlock._renew found', sites); - - var renewedOrFailed = []; + //console.log('greenlock._renew found', sites);; function next() { var site = sites.shift(); @@ -426,13 +319,13 @@ G.create = function(gconf) { }; greenlock._acme = function(args) { - var packageAgent = greenlock._defaults.packageAgent || ''; + var packageAgent = gconf.packageAgent || ''; // because Greenlock_Express/v3.x Greenlock/v3 is redundant if (!/greenlock/i.test(packageAgent)) { packageAgent = (packageAgent + ' Greenlock/' + pkg.version).trim(); } var acme = ACME.create({ - maintainerEmail: greenlock._defaults.maintainerEmail, + maintainerEmail: gconf.maintainerEmail, packageAgent: packageAgent, notify: greenlock._notify, debug: greenlock._defaults.debug || args.debug @@ -465,7 +358,7 @@ G.create = function(gconf) { greenlock.order = function(args) { return greenlock._init().then(function() { - return greenlock.manager.defaults().then(function(mconf) { + return manager.defaults().then(function(mconf) { return greenlock._order(mconf, args); }); }); @@ -473,7 +366,7 @@ G.create = function(gconf) { greenlock._order = function(mconf, args) { // packageAgent, maintainerEmail return greenlock._acme(args).then(function(acme) { - var storeConf = args.store || greenlock._defaults.store; + var storeConf = args.store || mconf.store; return P._loadStore(storeConf).then(function(store) { return A._getOrCreate( greenlock, @@ -482,10 +375,7 @@ G.create = function(gconf) { acme, args ).then(function(account) { - var challengeConfs = - args.challenges || - mconf.challenges || - greenlock._defaults.challenges; + var challengeConfs = args.challenges || mconf.challenges; return Promise.all( Object.keys(challengeConfs).map(function(typ01) { return P._loadChallenge(challengeConfs, typ01); @@ -504,6 +394,9 @@ G.create = function(gconf) { account, args ).then(function(pems) { + if (!pems) { + throw new Error('no order result'); + } if (!pems.privkey) { throw new Error( 'missing private key, which is kinda important' @@ -517,135 +410,254 @@ G.create = function(gconf) { }); }; - greenlock._options = gconf; - greenlock._defaults = defaults; + greenlock._defaults = gdefaults; + greenlock._defaults.debug = gconf.debug; - if (!gconf.onOrderFailure) { - gconf.onOrderFailure = function(err) { - G._onOrderFailure(gconf, err); - }; - } + // renew every 90-ish minutes (random for staggering) + // the weak setTimeout (unref) means that when run as a CLI process this + // will still finish as expected, and not wait on the timeout + (function renew() { + setTimeout(function() { + greenlock.renew({}); + renew(); + }, Math.PI * 30 * 60 * 1000).unref(); + })(); return greenlock; }; G._loadChallenge = P._loadChallenge; -G._defaults = function(opts) { - var defaults = {}; - - // [ 'store', 'challenges' ] - Object.keys(opts).forEach(function(k) { - // manage is the only thing that is, potentially, not plain-old JSON - if ('manage' === k && 'string' !== typeof opts[k]) { - return; - } - defaults[k] = opts[k]; +function errorToJSON(e) { + var error = {}; + Object.getOwnPropertyNames(e).forEach(function(k) { + error[k] = e[k]; }); + return error; +} - if ('function' === typeof opts.notify) { - defaults.notify = opts.notify; - } - if ('function' === typeof opts.find) { - defaults.find = opts.find; +function normalizeManager(gconf) { + var m; + // 1. Get the manager + // 2. Figure out if we need to wrap it + + if (!gconf.manager) { + gconf.manager = 'greenlock-manager-fs'; + if (gconf.find) { + // { manager: 'greenlock-manager-fs', find: function () { } } + warpFind(gconf); + } } - if (!defaults.directoryUrl) { - if (defaults.staging) { - defaults.directoryUrl = - 'https://acme-staging-v02.api.letsencrypt.org/directory'; - } else { - defaults.directoryUrl = - 'https://acme-v02.api.letsencrypt.org/directory'; + if ('string' === typeof gconf.manager) { + try { + // wrap this to be safe for greenlock-manager-fs + m = require(gconf.manager).create(gconf); + } catch (e) { + console.error(e.code); + console.error(e.message); } } else { - if (defaults.staging) { - throw new Error('supply `directoryUrl` or `staging`, but not both'); - } + m = gconf.manager; + } + + if (!m) { + console.error(); + console.error( + 'Failed to load manager plugin ', + JSON.stringify(gconf.manager) + ); + console.error(); + process.exit(1); + } + + if ( + ['set', 'remove', 'find', 'defaults'].every(function(k) { + return 'function' === typeof m[k]; + }) + ) { + return m; + } + + // { manager: { find: function () { } } } + if (m.find) { + warpFind(m); } - console.info('ACME Directory URL:', defaults.directoryUrl); + // m.configFile could also be set + m = require('greenlock-manager-fs').create(m); + if ('function' !== typeof m.find) { + console.error(); + console.error( + JSON.stringify(gconf.manager), + 'must implement `find()` and should implement `set()`, `remove()`, `defaults()`, and `init()`' + ); + console.error(); + process.exit(1); + } + + return m; +} + +function warpFind(gconf) { + gconf.__gl_find = gconf.find; + gconf.find = function(args) { + // the incoming args will be normalized by greenlock + return gconf.__gl_find(args).then(function(sites) { + // we also need to error check the incoming sites, + // as if they were being passed through `add()` or `set()` + // (effectively they are) because the manager assumes that + // they're not bad + sites.forEach(function(s) { + if (!s || 'string' !== typeof s.subject) { + throw new Error('missing subject'); + } + if ( + !Array.isArray(s.altnames) || + !s.altnames.length || + !s.altnames[0] || + s.altnames[0] !== s.subject + ) { + throw new Error('missing or malformed altnames'); + } + ['renewAt', 'issuedAt', 'expiresAt'].forEach(function(k) { + if (s[k]) { + throw new Error( + '`' + + k + + '` should be updated by `set()`, not by `find()`' + ); + } + }); + }); + }); + }; +} + +function mergeDefaults(MCONF, gconf) { + if ( + gconf.agreeToTerms === true || + MCONF.agreeToTerms === true || + // TODO deprecate + gconf.agreeTos === true || + MCONF.agreeTos === true + ) { + MCONF.agreeToTerms = true; + } + + if (!MCONF.subscriberEmail && gconf.subscriberEmail) { + MCONF.subscriberEmail = gconf.subscriberEmail; + } + + var homedir; // Load the default store module - if (!defaults.store) { - defaults.store = { - module: 'greenlock-store-fs', - basePath: homedir + '/.config/greenlock/' - }; + if (!MCONF.store) { + if (gconf.store) { + MCONF.store = gconf.store; + } else { + homedir = require('os').homedir(); + MCONF.store = { + module: 'greenlock-store-fs', + basePath: homedir + '/.config/greenlock/' + }; + } } - P._loadSync(defaults.store.module); - //defaults.store = store; + // just to test that it loads + P._loadSync(MCONF.store.module); // Load the default challenge modules - var challenges; - if (!defaults.challenges) { - defaults.challenges = {}; + var challenges = MCONF.challenges || gconf.challenges; + if (!challenges) { + challenges = {}; } - challenges = defaults.challenges; - - // TODO provide http-01 when http-01 and/or(?) dns-01 don't exist if (!challenges['http-01'] && !challenges['dns-01']) { - challenges['http-01'] = { - module: 'acme-http-01-standalone' - }; + challenges['http-01'] = { module: 'acme-http-01-standalone' }; } - if (challenges['http-01']) { - if ('string' === typeof challenges['http-01'].module) { - P._loadSync(challenges['http-01'].module); + if ('string' !== typeof challenges['http-01'].module) { + throw new Error( + 'bad challenge http-01 module config:' + + JSON.stringify(challenges['http-01']) + ); } + P._loadSync(challenges['http-01'].module); } - if (challenges['dns-01']) { - if ('string' === typeof challenges['dns-01'].module) { - P._loadSync(challenges['dns-01'].module); + if ('string' !== typeof challenges['dns-01'].module) { + throw new Error( + 'bad challenge dns-01 module config' + + JSON.stringify(challenges['dns-01']) + ); } + P._loadSync(challenges['dns-01'].module); } + MCONF.challenges = challenges; - if (defaults.agreeToTerms === true || defaults.agreeTos === true) { - defaults.agreeToTerms = function(tos) { - return Promise.resolve(tos); - }; + if (!MCONF.renewOffset) { + MCONF.renewOffset = gconf.renewOffset || '-45d'; + } + if (!MCONF.renewStagger) { + MCONF.renewStagger = gconf.renewStagger || '3d'; } - if (!defaults.renewOffset) { - defaults.renewOffset = '-45d'; + if (!MCONF.accountKeyType) { + MCONF.accountKeyType = gconf.accountKeyType || 'EC-P256'; } - if (!defaults.renewStagger) { - defaults.renewStagger = '3d'; + if (!MCONF.serverKeyType) { + MCONF.serverKeyType = gconf.serverKeyType || 'RSA-2048'; } +} - if (!defaults.accountKeyType) { - defaults.accountKeyType = 'EC-P256'; +function _notify(ev, args) { + if (!args) { + args = ev; + ev = args.event; + delete args.event; } - if (!defaults.serverKeyType) { - if (defaults.domainKeyType) { - console.warn('use serverKeyType instead of domainKeyType'); - defaults.serverKeyType = defaults.domainKeyType; - } else { - defaults.serverKeyType = 'RSA-2048'; - } + + // TODO define message types + if (!_notify._notice) { + console.info( + 'set greenlockOptions.notify to override the default logger' + ); + _notify._notice = true; } - if (defaults.domainKeypair) { - console.warn('use serverKeypair instead of domainKeypair'); - defaults.serverKeypair = - defaults.serverKeypair || defaults.domainKeypair; + var prefix = 'Warning'; + switch (ev) { + case 'error': + prefix = 'Error'; + /* falls through */ + case 'warning': + console.error( + prefix + '%s:', + (' ' + (args.context || '')).trimRight() + ); + console.error(args.message); + if (args.description) { + console.error(args.description); + } + if (args.code) { + console.error('code:', args.code); + } + if (args.stack) { + console.error(args.stack); + } + break; + default: + if (/status/.test(ev)) { + console.info( + ev, + args.altname || args.subject || '', + args.status || '' + ); + if (!args.status) { + console.info(args); + } + break; + } + console.info( + ev, + '(more info available: ' + Object.keys(args).join(' ') + ')' + ); } - - Object.defineProperty(defaults, 'domainKeypair', { - write: false, - get: function() { - console.warn('use serverKeypair instead of domainKeypair'); - return defaults.serverKeypair; - } - }); - - return defaults; -}; - -function errorToJSON(e) { - var error = {}; - Object.getOwnPropertyNames(e).forEach(function(k) { - error[k] = e[k]; - }); - return error; } diff --git a/manager-underlay.js b/manager-underlay.js new file mode 100644 index 0000000..33950b7 --- /dev/null +++ b/manager-underlay.js @@ -0,0 +1,258 @@ +'use strict'; + +var U = require('./utils.js'); +var E = require('./errors.js'); + +var warned = {}; + +module.exports.wrap = function(greenlock, manager) { + greenlock.manager = {}; + greenlock.sites = {}; + //greenlock.accounts = {}; + //greenlock.certs = {}; + + var allowed = [ + 'accountKeyType', //: ["P-256", "RSA-2048"], + 'serverKeyType', //: ["RSA-2048", "P-256"], + 'store', // : { module, specific opts }, + 'challenges', // : { "http-01", "dns-01", "tls-alpn-01" }, + 'subscriberEmail', + 'agreeToTerms', + 'agreeTos', + 'customerEmail', + 'renewOffset', + 'renewStagger', + 'module', // not allowed, just ignored + 'manager' + ]; + + // get / set default site settings such as + // subscriberEmail, store, challenges, renewOffset, renewStagger + greenlock.manager.defaults = function(conf) { + return greenlock._init().then(function() { + if (!conf) { + return manager.defaults(); + } + + if (conf.sites) { + throw new Error('cannot set sites as global config'); + } + if (conf.routes) { + throw new Error('cannot set routes as global config'); + } + + // disallow keys we know to be bad + [ + 'subject', + 'deletedAt', + 'altnames', + 'lastAttemptAt', + 'expiresAt', + 'issuedAt', + 'renewAt', + 'sites', + 'routes' + ].some(function(k) { + if (k in conf) { + throw new Error( + '`' + k + '` not allowed as a default setting' + ); + } + }); + Object.keys(conf).forEach(function(k) { + if (!allowed.includes(k) && !warned[k]) { + warned[k] = true; + console.warn( + k + + " isn't a known key. Please open an issue and let us know the use case." + ); + } + }); + + Object.keys(conf).forEach(function(k) { + if (-1 !== ['module', 'manager'].indexOf(k)) { + return; + } + + if ('undefined' === typeof k) { + throw new Error( + "'" + + k + + "' should be set to a value, or `null`, but not left `undefined`" + ); + } + }); + + return manager.defaults(conf); + }); + }; + + greenlock.add = greenlock.manager.add = function(args) { + if (!args || !Array.isArray(args.altnames) || !args.altnames.length) { + throw new Error( + 'you must specify `altnames` when adding a new site' + ); + } + if (args.renewAt) { + throw new Error( + 'you cannot specify `renewAt` when adding a new site' + ); + } + + return greenlock.manager.set(args); + }; + + // TODO agreeToTerms should be handled somewhere... maybe? + + // Add and update remains because I said I had locked the API + greenlock.manager.set = greenlock.manager.update = function(args) { + return greenlock._init().then(function() { + // The goal is to make this decently easy to manage by hand without mistakes + // but also reasonably easy to error check and correct + // and to make deterministic auto-corrections + + args.subject = checkSubject(args); + + //var subscriberEmail = args.subscriberEmail; + + // TODO shortcut the other array checks when not necessary + if (Array.isArray(args.altnames)) { + args.altnames = checkAltnames(args.subject, args); + } + + // at this point we know that subject is the first of altnames + return Promise.all( + (args.altnames || []).map(function(d) { + d = d.replace('*.', ''); + return U._validDomain(d); + }) + ).then(function() { + if (!U._uniqueNames(args.altnames || [])) { + throw E.NOT_UNIQUE( + 'add', + "'" + args.altnames.join("' '") + "'" + ); + } + + // durations + if (args.renewOffset) { + args.renewOffset = U._parseDuration(args.renewOffset); + } + if (args.renewStagger) { + args.renewStagger = U._parseDuration(args.renewStagger); + } + + return manager.set(args).then(function(result) { + greenlock.renew({}).catch(function(err) { + if (!err.context) { + err.contxt = 'renew'; + } + greenlock._notify('error', err); + }); + return result; + }); + }); + }); + }; + + greenlock.manager.remove = function(args) { + args.subject = checkSubject(args); + // TODO check no altnames + return manager.remove(args); + }; + + /* + { + subject: site.subject, + altnames: site.altnames, + //issuedAt: site.issuedAt, + //expiresAt: site.expiresAt, + renewOffset: site.renewOffset, + renewStagger: site.renewStagger, + renewAt: site.renewAt, + subscriberEmail: site.subscriberEmail, + customerEmail: site.customerEmail, + challenges: site.challenges, + store: site.store + }; + */ + + greenlock._find = function(args) { + var altnames = args.altnames || []; + + // servername, wildname, and altnames are all the same + ['wildname', 'servername'].forEach(function(k) { + var altname = args[k] || ''; + if (altname && !altnames.includes(altname)) { + altnames.push(altname); + } + }); + + if (altnames.length) { + args.altnames = altnames; + args.altnames = args.altnames.map(U._encodeName); + args.altnames = checkAltnames(false, args); + } + + return manager.find(args); + }; +}; + +function checkSubject(args) { + if (!args || !args.subject) { + throw new Error('you must specify `subject` when configuring a site'); + } + /* + if (!args.subject) { + throw E.NO_SUBJECT('add'); + } + */ + + var subject = (args.subject || '').toLowerCase(); + if (subject !== args.subject) { + console.warn('`subject` must be lowercase', args.subject); + } + + return U._encodeName(subject); +} + +function checkAltnames(subject, args) { + // the things we have to check and get right + var altnames = (args.altnames || []).map(function(name) { + return String(name || '').toLowerCase(); + }); + + if (subject && subject !== altnames[0]) { + throw new Error( + '`subject` must be the first domain in `altnames`', + args.subject, + altnames.join(' ') + ); + } + + /* + if (args.subject !== args.altnames[0]) { + throw E.BAD_ORDER( + 'add', + '(' + args.subject + ") '" + args.altnames.join("' '") + "'" + ); + } + */ + + // punycode BEFORE validation + // (set, find, remove) + args.altnames = args.altnames.map(U._encodeName); + if ( + !args.altnames.every(function(d) { + return U._validName(d); + }) + ) { + throw E.INVALID_HOSTNAME('add', "'" + args.altnames.join("' '") + "'"); + } + + if (altnames.join() !== args.altnames.join()) { + console.warn('all domains in `altnames` must be lowercase', altnames); + } + + return altnames; +} diff --git a/package-lock.json b/package-lock.json index ae83f62..6567db4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@root/greenlock", - "version": "3.0.12", + "version": "3.0.15", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -73,6 +73,11 @@ "@root/encoding": "^1.0.1" } }, + "acme-dns-01-digitalocean": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acme-dns-01-digitalocean/-/acme-dns-01-digitalocean-3.0.1.tgz", + "integrity": "sha512-LUdOGluDERQWJG4CwlC9HbzUai4mtKzCz8nzpVTirXup2WwH60iRFAcd81hRGaoWbd0Bc0m6RVjN9YFkXB84yA==" + }, "acme-http-01-standalone": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/acme-http-01-standalone/-/acme-http-01-standalone-3.0.5.tgz", @@ -90,14 +95,31 @@ "dev": true }, "greenlock-manager-fs": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-0.7.0.tgz", - "integrity": "sha512-cWmrfdSbT0ettDZzl6SXhZ47gVLj7saM/tdEP6sEfnsocJ3mRFRP3QUrJYyLVdCOCuVH6cclOKLembIrZjwDrQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-3.0.0.tgz", + "integrity": "sha512-+SXySDAKv4dgSQ4tHPIdOJrNfLmCA9h0N8G7N+FdYswXmA7zKGyqOBzhNCbyZ47/O3wzvszcpGV+wMS/6kG2NQ==", "requires": { "@root/mkdirp": "^1.0.0", + "greenlock-manager-test": "^3.0.0", "safe-replace": "^1.1.0" } }, + "greenlock-manager-test": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/greenlock-manager-test/-/greenlock-manager-test-3.0.0.tgz", + "integrity": "sha512-grqpUcxT7v5KzJ04r8wJWXjSVm7us1z/2QluCJRl9BZUM4CXzQuP1C0d2wdsV4NHwziEGJpA+4mFsntuh3f1YA==", + "requires": { + "@root/request": "^1.4.1", + "greenlock-manager-fs": "^3.0.0" + }, + "dependencies": { + "@root/request": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.1.tgz", + "integrity": "sha512-2zSP1v9VhJ3gvm4oph0C4BYCoM3Sj84/Wx4iKdt0IbqbJzfON04EodBq5dsV65UxO/aHZciUBwY2GCZcHqaTYg==" + } + } + }, "greenlock-store-fs": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/greenlock-store-fs/-/greenlock-store-fs-3.2.0.tgz", diff --git a/package.json b/package.json index 8d15735..cb6c099 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@root/greenlock", - "version": "3.0.12", + "version": "3.0.15", "description": "The easiest Let's Encrypt client for Node.js and Browsers", "homepage": "https://rootprojects.org/greenlock/", "main": "greenlock.js", @@ -40,9 +40,10 @@ "@root/keypairs": "^0.9.0", "@root/mkdirp": "^1.0.0", "@root/request": "^1.3.10", + "acme-dns-01-digitalocean": "^3.0.1", "acme-http-01-standalone": "^3.0.5", "cert-info": "^1.5.1", - "greenlock-manager-fs": "^0.7.0", + "greenlock-manager-fs": "^3.0.0", "greenlock-store-fs": "^3.2.0", "safe-replace": "^1.1.0" }, diff --git a/tests/index.js b/tests/index.js index 666b07c..516e876 100644 --- a/tests/index.js +++ b/tests/index.js @@ -2,7 +2,6 @@ require('dotenv').config(); -var path = require('path'); var Greenlock = require('../'); var subject = process.env.BASE_DOMAIN; @@ -12,32 +11,44 @@ var challenge = JSON.parse(process.env.CHALLENGE_OPTIONS); challenge.module = process.env.CHALLENGE_PLUGIN; var greenlock = Greenlock.create({ - agreeTos: true, + packageAgent: 'Greenlock_Test/v0', maintainerEmail: email, staging: true, - manager: path.join(__dirname, 'manager.js'), - challenges: { - 'dns-01': challenge - } - //configFile: '~/.config/greenlock/certs.json', - //challenges: challenges, - //store: args.storeOpts, - //renewOffset: args.renewOffset || '30d', - //renewStagger: '1d' + manager: require('greenlock-manager-fs').create({ + //configFile: '~/.config/greenlock/certs.json', + }) }); -greenlock - .add({ - subject: subject, - altnames: altnames, - subscriberEmail: email +greenlock.manager + .defaults({ + agreeToTerms: true, + subscriberEmail: email, + challenges: { + 'dns-01': challenge + } + //store: args.storeOpts, + //renewOffset: args.renewOffset || '30d', + //renewStagger: '1d' }) .then(function() { - return greenlock.renew().then(function(pems) { - console.info(pems); - }); + return greenlock + .add({ + subject: subject, + altnames: altnames, + subscriberEmail: email + }) + .then(function() { + return greenlock + .get({ servername: subject }) + .then(function(pems) { + if (pems && pems.privkey && pems.cert && pems.chain) { + console.info('Success'); + } + //console.log(pems); + }); + }); }) .catch(function(e) { - console.error('yo', e.code); + console.error('Big bad error:', e.code); console.error(e); });