From 2dbd61158f854cd7b612ee53a53bb4b58fc7897c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 2 Nov 2019 19:33:12 -0600 Subject: [PATCH] cli: add, update, config --- bin/add.js | 188 ++++++++++++++++++----------------------------- bin/config.js | 70 ++++++++++++++++++ bin/defaults.js | 17 +++++ bin/flags.js | 138 ++++++++++++++++++++++++++++++++++ bin/greenlock.js | 2 +- bin/update.js | 72 ++++++++++++++++++ greenlock.js | 26 +++++-- plugins.js | 2 + 8 files changed, 391 insertions(+), 124 deletions(-) create mode 100644 bin/config.js create mode 100644 bin/defaults.js create mode 100644 bin/flags.js create mode 100644 bin/update.js diff --git a/bin/add.js b/bin/add.js index 9d87123..4a06dfc 100644 --- a/bin/add.js +++ b/bin/add.js @@ -2,133 +2,85 @@ var args = process.argv.slice(3); var cli = require('./cli.js'); -var path = require('path'); +//var path = require('path'); //var pkgpath = path.join(__dirname, '..', 'package.json'); -var pkgpath = path.join(process.cwd(), 'package.json'); +//var pkgpath = path.join(process.cwd(), 'package.json'); -require('./greenlockrc')(pkgpath).then(async function(rc) { - var Greenlock = require('../'); - // this is a copy, so it's safe to modify - rc._bin_mode = true; - var greenlock = Greenlock.create(rc); - var mconf = await greenlock.manager.defaults(); +var Flags = require('./flags.js'); - cli.parse({ - subject: [ - false, - 'the "subject" (primary domain) of the certificate', - 'string' - ], - altnames: [ - false, - 'the "subject alternative names" (additional domains) on the certificate, the first of which MUST be the subject', - 'string' - ], - 'renew-offset': [ - false, - "time to wait until renewing the cert such as '45d' (45 days after being issued) or '-3w' (3 weeks before expiration date)", - 'string', - mconf.renewOffset - ], - 'server-key-type': [ - false, - "either 'RSA-2048' or 'P-256' (ECDSA) - although other values are technically supported, they don't make sense and won't work with many services (More bits != More security)", - 'string', - mconf.serverKeyType - ], - challenge: [ - false, - 'the name name of file path of the HTTP-01, DNS-01, or TLS-ALPN-01 challenge module to use', - 'string', - Object.keys(mconf.challenges) - .map(function(typ) { - return mconf.challenges[typ].module; - }) - .join(',') - ], - 'challenge-xxxx': [ - false, - 'an option for the chosen challenge module, such as --challenge-apikey or --challenge-bucket', - 'bag' - ], - 'challenge-json': [ - false, - 'a JSON string containing all option for the chosen challenge module (instead of --challenge-xxxx)', - 'json', - '{}' - ], - 'force-save': [ - false, - "save all options for this site, even if it's the same as the defaults", - 'boolean', - false - ] +Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) { + var myFlags = {}; + [ + 'subject', + 'altnames', + 'renew-offset', + 'server-key-type', + 'challenge', + 'challenge-xxxx', + 'challenge-json', + 'force-save' + ].forEach(function(k) { + myFlags[k] = flagOptions[k]; }); - // ignore certonly and extraneous arguments - async function main(_, options) { - if (!options.subject || !options.altnames) { - console.error( - '--subject and --altnames must be provided and should be valid domains' - ); - process.exit(1); - return; - } - options.altnames = options.altnames.split(/[,\s]+/); - - Object.keys(options).forEach(function(k) { - if (options[k] === mconf[k] && !options.forceSave) { - delete options[k]; - } - }); - - var typ; - var challenge; - if (options.challenge) { - if (/http-01/.test(options.challenge)) { - typ = 'http-01'; - } else if (/dns-01/.test(options.challenge)) { - typ = 'dns-01'; - } else if (/tls-alpn-01/.test(options.challenge)) { - typ = 'tls-alpn-01'; - } - - challenge = options.challengeOpts; - challenge.module = options.challenge; - options.challenges = {}; - options.challenges[typ] = challenge; - delete options.challengeOpts; - delete options.challenge; - - var chall = mconf.challenges[typ]; - if (challenge.module === chall.module) { - var keys = Object.keys(challenge); - var same = - !keys.length || - keys.every(function(k) { - return chall[k] === challenge[k]; - }); - if (same && !options.forceSave) { - delete options.challenges; - } - } - } + cli.parse(myFlags); + cli.main(function(argList, flags) { + main(argList, flags, rc, greenlock, mconf); + }, args); +}); - delete options.forceSave; +async function main(_, flags, rc, greenlock, mconf) { + if (!flags.subject) { + console.error( + '--subject must be provided as the id of the site/certificate' + ); + process.exit(1); + return; + } - /* - console.log('manager conf:'); - console.log(mconf); - console.log('cli options:'); - console.log(options); - */ + Flags.mangleFlags(flags, mconf); - greenlock.add(options).catch(function(err) { + greenlock + .add(flags) + .catch(function(err) { console.error(); console.error('error:', err.message); console.error(); - }); - } + }) + .then(function() { + return greenlock + ._config({ + servername: + flags.altnames[ + Math.floor(Math.random() * flags.altnames.length) + ] + }) + .then(function(site) { + if (!site) { + console.info(); + console.info( + 'Internal bug or configuration mismatch: No config found.' + ); + console.info(); + process.exit(1); + return; + } + console.info(); + console.info('Created config!'); + console.info(); - cli.main(main, process.argv.slice(3)); -}); + Object.keys(site).forEach(function(k) { + if ('defaults' === k) { + console.info(k + ':'); + Object.keys(site.defaults).forEach(function(key) { + var value = JSON.stringify(site.defaults[key]); + console.info('\t' + key + ':' + value); + }); + } else { + console.info(k + ': ' + JSON.stringify(site[k])); + } + }); + console.info(); + }); + }); +} diff --git a/bin/config.js b/bin/config.js new file mode 100644 index 0000000..aaf2147 --- /dev/null +++ b/bin/config.js @@ -0,0 +1,70 @@ +'use strict'; + +var args = process.argv.slice(3); +var cli = require('./cli.js'); +//var path = require('path'); +//var pkgpath = path.join(__dirname, '..', 'package.json'); +//var pkgpath = path.join(process.cwd(), 'package.json'); + +var Flags = require('./flags.js'); + +Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) { + var myFlags = {}; + ['subject', 'servername' /*, 'servernames', 'altnames'*/].forEach(function( + k + ) { + myFlags[k] = flagOptions[k]; + }); + + cli.parse(myFlags); + cli.main(function(argList, flags) { + Flags.mangleFlags(flags, mconf); + main(argList, flags, rc, greenlock, mconf); + }, args); +}); + +async function main(_, flags, rc, greenlock /*, mconf */) { + var servernames = [flags.subject] + .concat([flags.servername]) + //.concat(flags.servernames) + //.concat(flags.altnames) + .filter(Boolean); + delete flags.subject; + delete flags.altnames; + flags.servernames = servernames; + if (flags.servernames.length > 1) { + console.error('Error: should only have one servername'); + process.exit(1); + return; + } else if (flags.servernames.length !== 1) { + console.error('Error: need a servername to check'); + process.exit(1); + return; + } + flags.servername = flags.servernames[0]; + delete flags.servernames; + + greenlock + ._config(flags) + .catch(function(err) { + console.error(); + console.error('error:', err.message); + console.log(err.stack); + console.error(); + }) + .then(function(site) { + if (!site) { + console.info(); + console.info('No config found for '); + console.info(); + process.exit(1); + return; + } + console.info(); + console.info( + 'Config for ' + JSON.stringify(flags.servername) + ':' + ); + console.info(JSON.stringify(site, null, 2)); + console.info(); + }); +} diff --git a/bin/defaults.js b/bin/defaults.js new file mode 100644 index 0000000..d61f6fa --- /dev/null +++ b/bin/defaults.js @@ -0,0 +1,17 @@ + store: [ + false, + 'the npm name or path of the storage module or callbacks file', + 'string', + 'greenlock-store-fs' + ], + 'store-xxxx': [ + false, + 'an option for the chosen storage module, such as --store-apikey or --store-bucket', + 'bag' + ], + 'store-json': [ + false, + 'a JSON string containing all option for the chosen store module (instead of --store-xxxx)', + 'json', + '{}' + ], diff --git a/bin/flags.js b/bin/flags.js new file mode 100644 index 0000000..ea82afe --- /dev/null +++ b/bin/flags.js @@ -0,0 +1,138 @@ +'use strict'; + +var Flags = module.exports; + +var path = require('path'); +//var pkgpath = path.join(__dirname, '..', 'package.json'); +var pkgpath = path.join(process.cwd(), 'package.json'); +var GreenlockRc = require('./greenlockrc.js'); + +Flags.init = function() { + return GreenlockRc(pkgpath).then(async function(rc) { + var Greenlock = require('../'); + // this is a copy, so it's safe to modify + rc._bin_mode = true; + var greenlock = Greenlock.create(rc); + var mconf = await greenlock.manager.defaults(); + + var flagOptions = { + subject: [ + false, + 'the "subject" (primary domain) of the certificate', + 'string' + ], + altnames: [ + false, + 'the "subject alternative names" (additional domains) on the certificate, the first of which MUST be the subject', + 'string' + ], + servername: [ + false, + 'a name that matches a subject or altname', + 'string' + ], + servernames: [ + false, + 'a list of names that matches a subject or altname', + 'string' + ], + 'renew-offset': [ + false, + "time to wait until renewing the cert such as '45d' (45 days after being issued) or '-3w' (3 weeks before expiration date)", + 'string', + mconf.renewOffset + ], + 'server-key-type': [ + false, + "either 'RSA-2048' or 'P-256' (ECDSA) - although other values are technically supported, they don't make sense and won't work with many services (More bits != More security)", + 'string', + mconf.serverKeyType + ], + challenge: [ + false, + 'the name name of file path of the HTTP-01, DNS-01, or TLS-ALPN-01 challenge module to use', + 'string', + Object.keys(mconf.challenges) + .map(function(typ) { + return mconf.challenges[typ].module; + }) + .join(',') + ], + 'challenge-xxxx': [ + false, + 'an option for the chosen challenge module, such as --challenge-apikey or --challenge-bucket', + 'bag' + ], + 'challenge-json': [ + false, + 'a JSON string containing all option for the chosen challenge module (instead of --challenge-xxxx)', + 'json', + '{}' + ], + 'force-save': [ + false, + "save all options for this site, even if it's the same as the defaults", + 'boolean', + false + ] + }; + + return { + flagOptions, + rc, + greenlock, + mconf + }; + }); +}; + +Flags.mangleFlags = function(flags, mconf) { + if ('altnames' in flags) { + flags.altnames = (flags.altnames || '').split(/[,\s]+/).filter(Boolean); + } + if ('servernames' in flags) { + flags.servernames = (flags.servernames || '') + .split(/[,\s]+/) + .filter(Boolean); + } + + Object.keys(flags).forEach(function(k) { + if (flags[k] === mconf[k] && !flags.forceSave) { + delete flags[k]; + } + }); + + var typ; + var challenge; + if (flags.challenge) { + if (/http-01/.test(flags.challenge)) { + typ = 'http-01'; + } else if (/dns-01/.test(flags.challenge)) { + typ = 'dns-01'; + } else if (/tls-alpn-01/.test(flags.challenge)) { + typ = 'tls-alpn-01'; + } + + challenge = flags.challengeOpts; + challenge.module = flags.challenge; + flags.challenges = {}; + flags.challenges[typ] = challenge; + delete flags.challengeOpts; + delete flags.challenge; + + var chall = mconf.challenges[typ]; + if (challenge.module === chall.module) { + var keys = Object.keys(challenge); + var same = + !keys.length || + keys.every(function(k) { + return chall[k] === challenge[k]; + }); + if (same && !flags.forceSave) { + delete flags.challenges; + } + } + } + + delete flags.forceSave; +}; diff --git a/bin/greenlock.js b/bin/greenlock.js index 4254c19..282f5eb 100755 --- a/bin/greenlock.js +++ b/bin/greenlock.js @@ -5,7 +5,7 @@ var args = process.argv.slice(2); var arg0 = args[0]; //console.log(args); -var found = ['certonly', 'add', 'config', 'defaults', 'remove'].some(function( +var found = ['certonly', 'add', 'update', 'config', 'defaults', 'remove'].some(function( k ) { if (k === arg0) { diff --git a/bin/update.js b/bin/update.js new file mode 100644 index 0000000..fcbffc4 --- /dev/null +++ b/bin/update.js @@ -0,0 +1,72 @@ +'use strict'; + +var args = process.argv.slice(3); +var cli = require('./cli.js'); +var Flags = require('./flags.js'); + +Flags.init().then(function({ flagOptions, rc, greenlock, mconf }) { + var myFlags = {}; + [ + 'subject', + 'altnames', + 'renew-offset', + 'server-key-type', + 'challenge', + 'challenge-xxxx', + 'challenge-json', + 'force-save' + ].forEach(function(k) { + myFlags[k] = flagOptions[k]; + }); + + cli.parse(myFlags); + cli.main(function(argList, flags) { + main(argList, flags, rc, greenlock, mconf); + }, args); +}); + +async function main(_, flags, rc, greenlock, mconf) { + if (!flags.subject || !flags.altnames) { + console.error( + '--subject and --altnames must be provided and should be valid domains' + ); + process.exit(1); + return; + } + + Flags.mangleFlags(flags, mconf); + + greenlock.update(flags).catch(function(err) { + console.error(); + console.error('error:', err.message); + console.error(); + }) .then(function() { + return greenlock + ._config({ servername: flags.subject }) + .then(function(site) { + if (!site) { + console.info(); + console.info('No config found for '); + console.info(); + process.exit(1); + return; + } + console.info(); + console.info("Updated config!"); + console.info(); + + Object.keys(site).forEach(function(k) { + if ('defaults' === k) { + console.info(k + ':'); + Object.keys(site.defaults).forEach(function(key) { + var value = JSON.stringify(site.defaults[key]); + console.info('\t' + key + ':' + value); + }); + } else { + console.info(k + ': ' + JSON.stringify(site[k])); + } + }); + console.info(); + }); + }); +} diff --git a/greenlock.js b/greenlock.js index f01ea79..18bff3f 100644 --- a/greenlock.js +++ b/greenlock.js @@ -128,9 +128,13 @@ G.create = function(gconf) { }); }) .catch(function(err) { - console.error('Fatal error during greenlock init:'); - console.error(err); - process.exit(1); + if ('load_plugin' !== err.context) { + console.error('Fatal error during greenlock init:'); + console.error(err.message); + } + if (!gconf._bin_mode) { + process.exit(1); + } }); return p; }; @@ -247,8 +251,14 @@ G.create = function(gconf) { .split('.') .slice(1) .join('.'); + if (args.wildname.split('.').length < 3) { + // No '*.com' + args.wildname = ''; + } if ( args.servernames || + //TODO I think we need to block altnames as well, but I don't want to break anything + //args.altnames || args.subject || args.renewBefore || args.issueBefore || @@ -279,12 +289,16 @@ G.create = function(gconf) { if (site.store && site.challenges) { return site; } + var dconf = site; + if (gconf._bin_mode) { + dconf = site.defaults = {}; + } return manager.defaults().then(function(mconf) { if (!site.store) { - site.store = mconf.store; + dconf.store = mconf.store; } if (!site.challenges) { - site.challenges = mconf.challenges; + dconf.challenges = mconf.challenges; } return site; }); @@ -483,6 +497,7 @@ function normalizeManager(gconf) { // wrap this to be safe for greenlock-manager-fs m = require(gconf.manager).create(gconf); } catch (e) { + console.error('Error loading manager:'); console.error(e.code); console.error(e.message); } @@ -592,6 +607,7 @@ function mergeDefaults(MCONF, gconf) { }; } } + // just to test that it loads P._loadSync(MCONF.store.module); diff --git a/plugins.js b/plugins.js index 118b19a..f2dea3e 100644 --- a/plugins.js +++ b/plugins.js @@ -31,6 +31,7 @@ P._loadHelper = function(modname) { console.error("Could not load '%s'", modname); console.error('Did you install it?'); console.error('\tnpm install --save %s', modname); + e.context = 'load_plugin'; throw e; // Fun experiment, bad idea @@ -193,6 +194,7 @@ P._loadSync = function(modname) { console.error("Could not load '%s'", modname); console.error('Did you install it?'); console.error('\tnpm install --save %s', modname); + e.context = 'load_plugin'; throw e; } /*