From 341347ba3e64db3dae4d8184c99251fd900ab0fc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 3 Nov 2019 02:51:32 -0700 Subject: [PATCH] CLI: implement init and bugfix .greenlockrc --- bin/greenlock.js | 2 +- bin/init.js | 135 +++++++++++++++++++ bin/lib/cli.js | 2 +- bin/lib/flags.js | 278 ++++++++++++++++++++++------------------ bin/lib/greenlockrc.js | 2 +- bin/tmpl/app.tmpl.js | 9 ++ bin/tmpl/server.tmpl.js | 36 ++++++ plugins.js | 5 + tests/cli.sh | 2 +- 9 files changed, 342 insertions(+), 129 deletions(-) create mode 100644 bin/init.js create mode 100644 bin/tmpl/app.tmpl.js create mode 100644 bin/tmpl/server.tmpl.js diff --git a/bin/greenlock.js b/bin/greenlock.js index c947217..04cb506 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', 'update', 'config', 'defaults', 'remove'].some( +var found = ['certonly', 'add', 'update', 'config', 'defaults', 'remove', 'init'].some( function(k) { if (k === arg0) { require('./' + k); diff --git a/bin/init.js b/bin/init.js new file mode 100644 index 0000000..3cae478 --- /dev/null +++ b/bin/init.js @@ -0,0 +1,135 @@ +'use strict'; + +var P = require('../plugins.js'); +var args = process.argv.slice(3); +var cli = require('./lib/cli.js'); +//var path = require('path'); +//var pkgpath = path.join(__dirname, '..', 'package.json'); +//var pkgpath = path.join(process.cwd(), 'package.json'); + +var Flags = require('./lib/flags.js'); + +var flagOptions = Flags.flags(); +var myFlags = {}; +['maintainer-email', 'cluster', 'manager', 'manager-xxxx'].forEach(function(k) { + myFlags[k] = flagOptions[k]; +}); + +cli.parse(myFlags); +cli.main(async function(argList, flags) { + var path = require('path'); + var pkgpath = path.join(process.cwd(), 'package.json'); + var pkgdir = path.dirname(pkgpath); + //var rcpath = path.join(pkgpath, '.greenlockrc'); + var configFile = path.join(pkgdir, 'greenlock.d/manager.json'); + var manager = flags.manager; + + // TODO move to bin/lib/greenlockrc.js + if (!manager) { + manager = 'greenlock-cloud-fs'; + if (!flags.managerOpts.configFile) { + flags.managerOpts.configFile = configFile; + } + } + if (['fs', 'cloud'].includes(manager)) { + // TODO publish the 1st party modules under a secure namespace + flags.manager = '@greenlock/manager-' + flags.manager; + } + flags.manager = flags.managerOpts; + delete flags.managerOpts; + flags.manager.manager = manager; + + try { + P._loadSync(manager); + } catch (e) { + try { + P._installSync(manager); + } catch (e) { + console.error( + 'error:', + JSON.stringify(manager), + 'could not be loaded, and could not be installed.' + ); + process.exit(1); + } + } + + var GreenlockRc = require('./lib/greenlockrc.js'); + //var rc = await GreenlockRc(pkgpath, manager, flags.manager); + await GreenlockRc(pkgpath, manager, flags.manager); + writeServerJs(pkgdir, flags); + writeAppJs(pkgdir); + + /* + rc._bin_mode = true; + var Greenlock = require('../'); + // this is a copy, so it's safe to modify + var greenlock = Greenlock.create(rc); + var mconf = await greenlock.manager.defaults(); + var flagOptions = Flags.flags(mconf, myOpts); + */ +}, args); + +function writeServerJs(pkgdir, flags) { + var serverJs = 'server.js'; + var bakTmpl = 'server-greenlock-tmpl.js'; + var fs = require('fs'); + var path = require('path'); + var tmpl = fs.readFileSync( + path.join(__dirname, 'tmpl/server.tmpl.js'), + 'utf8' + ); + + try { + fs.accessSync(path.join(pkgdir, serverJs)); + console.warn( + JSON.stringify(serverJs), + ' exists, writing to ', + JSON.stringify(bakTmpl), + 'instead' + ); + serverJs = bakTmpl; + } catch (e) { + // continue + } + + if (flags.cluster) { + tmpl = tmpl.replace( + /options.cluster = false/g, + 'options.cluster = true' + ); + } + if (flags.maintainerEmail) { + tmpl = tmpl.replace( + /pkg.author/g, + JSON.stringify(flags.maintainerEmail) + ); + } + fs.writeFileSync(path.join(pkgdir, serverJs), tmpl); +} + +function writeAppJs(pkgdir) { + var bakTmpl = 'app-greenlock-tmpl.js'; + var appJs = 'app.js'; + var fs = require('fs'); + var path = require('path'); + var tmpl = fs.readFileSync( + path.join(__dirname, 'tmpl/app.tmpl.js'), + 'utf8' + ); + + try { + fs.accessSync(path.join(pkgdir, appJs)); + console.warn( + JSON.stringify(appJs), + ' exists, writing to ', + JSON.stringify(bakTmpl), + 'instead' + ); + appJs = bakTmpl; + } catch (e) { + // continue + } + + fs.writeFileSync(path.join(pkgdir, appJs), tmpl); +} diff --git a/bin/lib/cli.js b/bin/lib/cli.js index e544b31..789c2e7 100644 --- a/bin/lib/cli.js +++ b/bin/lib/cli.js @@ -14,7 +14,7 @@ CLI.parse = function(conf) { var v = conf[k]; if (!v) { console.error( - 'Developer Error: missing config value for', + 'Developer Error: missing cli flag definition for', JSON.stringify(k) ); process.exit(1); diff --git a/bin/lib/flags.js b/bin/lib/flags.js index cc1afff..c2e8907 100644 --- a/bin/lib/flags.js +++ b/bin/lib/flags.js @@ -7,142 +7,170 @@ var path = require('path'); var pkgpath = path.join(process.cwd(), 'package.json'); var GreenlockRc = require('./greenlockrc.js'); -Flags.init = function(myOpts) { +// These are ALL options +// The individual CLI files each select a subset of them +Flags.flags = function(mconf, myOpts) { + // Current Manager Config + if (!mconf) { + mconf = {}; + } + + // Extra Override Options if (!myOpts) { myOpts = {}; } - 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 - ], - 'customer-email': [ - false, - "the email address of the owner of the domain or site (not necessarily the Let's Encrypt or ACME subscriber)", - 'string' - ], - 'subscriber-email': [ - false, - "the email address of the Let's Encrypt or ACME Account subscriber (not necessarily the domain owner)", - 'string' - ], - 'account-key-type': [ - false, - "either 'P-256' (ECDSA) or 'RSA-2048' - although other values are technically supported, they don't make sense and won't work with many services (More bits != More security)", - 'string', - mconf.accountKeyType - ], - '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 - ], - store: [ - false, - 'the module name or file path of the store module to use', - 'string' - //mconf.store.module - ], - 'store-xxxx': [ - false, - 'an option for the chosen store module, such as --store-apikey or --store-bucket', - 'bag' - ], - challenge: [ - false, - 'the module name or file path of the HTTP-01, DNS-01, or TLS-ALPN-01 challenge module to use', - 'string', - '' - /* + return { + 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' + ], + cluster: [false, 'initialize with cluster mode on', 'boolean', false], + '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 + ], + 'customer-email': [ + false, + "the email address of the owner of the domain or site (not necessarily the Let's Encrypt or ACME subscriber)", + 'string' + ], + 'subscriber-email': [ + false, + "the email address of the Let's Encrypt or ACME Account subscriber (not necessarily the domain owner)", + 'string' + ], + 'maintainer-email': [ + false, + 'the maintainance contact for security and critical bug notices', + 'string' + ], + 'account-key-type': [ + false, + "either 'P-256' (ECDSA) or 'RSA-2048' - although other values are technically supported, they don't make sense and won't work with many services (More bits != More security)", + 'string', + mconf.accountKeyType + ], + '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 + ], + store: [ + false, + 'the module name or file path of the store module to use', + 'string' + //mconf.store.module + ], + 'store-xxxx': [ + false, + 'an option for the chosen store module, such as --store-apikey or --store-bucket', + 'bag' + ], + manager: [ + false, + 'the module name or file path of the manager module to use', + 'string', + 'greenlock-manager-fs' + ], + 'manager-xxxx': [ + false, + 'an option for the chosen manager module, such as --manager-apikey or --manager-dburl', + 'bag' + ], + challenge: [ + false, + 'the module name or 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', - '{}' - ], - 'challenge-http-01': [ - false, - 'the module name or file path of the HTTP-01 to add', - 'string' - //(mconf.challenges['http-01'] || {}).module - ], - 'challenge-http-01-xxxx': [ - false, - 'an option for the chosen challenge module, such as --challenge-http-01-apikey or --challenge-http-01-bucket', - 'bag' - ], - 'challenge-dns-01': [ - false, - 'the module name or file path of the DNS-01 to add', - 'string' - //(mconf.challenges['dns-01'] || {}).module - ], - 'challenge-dns-01-xxxx': [ - false, - 'an option for the chosen challenge module, such as --challenge-dns-01-apikey or --challenge-dns-01-bucket', - 'bag' - ], - 'challenge-tls-alpn-01': [ - false, - 'the module name or file path of the DNS-01 to add', - 'string' - //(mconf.challenges['tls-alpn-01'] || {}).module - ], - 'challenge-tls-alpn-01-xxxx': [ - false, - 'an option for the chosen challenge module, such as --challenge-tls-alpn-01-apikey or --challenge-tls-alpn-01-bucket', - 'bag' - ], - 'force-save': [ - false, - "save all options for this site, even if it's the same as the defaults", - 'boolean', - myOpts.forceSave || false - ] - }; + ], + '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', + '{}' + ], + 'challenge-http-01': [ + false, + 'the module name or file path of the HTTP-01 to add', + 'string' + //(mconf.challenges['http-01'] || {}).module + ], + 'challenge-http-01-xxxx': [ + false, + 'an option for the chosen challenge module, such as --challenge-http-01-apikey or --challenge-http-01-bucket', + 'bag' + ], + 'challenge-dns-01': [ + false, + 'the module name or file path of the DNS-01 to add', + 'string' + //(mconf.challenges['dns-01'] || {}).module + ], + 'challenge-dns-01-xxxx': [ + false, + 'an option for the chosen challenge module, such as --challenge-dns-01-apikey or --challenge-dns-01-bucket', + 'bag' + ], + 'challenge-tls-alpn-01': [ + false, + 'the module name or file path of the DNS-01 to add', + 'string' + //(mconf.challenges['tls-alpn-01'] || {}).module + ], + 'challenge-tls-alpn-01-xxxx': [ + false, + 'an option for the chosen challenge module, such as --challenge-tls-alpn-01-apikey or --challenge-tls-alpn-01-bucket', + 'bag' + ], + 'force-save': [ + false, + "save all options for this site, even if it's the same as the defaults", + 'boolean', + myOpts.forceSave || false + ] + }; +}; +Flags.init = function(myOpts) { + return GreenlockRc(pkgpath).then(async function(rc) { + rc._bin_mode = true; + var Greenlock = require('../../'); + // this is a copy, so it's safe to modify + var greenlock = Greenlock.create(rc); + var mconf = await greenlock.manager.defaults(); + var flagOptions = Flags.flags(mconf, myOpts); return { flagOptions, rc, diff --git a/bin/lib/greenlockrc.js b/bin/lib/greenlockrc.js index 4973502..f8e22fe 100644 --- a/bin/lib/greenlockrc.js +++ b/bin/lib/greenlockrc.js @@ -89,7 +89,7 @@ module.exports = async function(pkgpath, manager, rc) { if (rc) { changed = true; Object.keys(rc).forEach(function(k) { - _rc[k] = rc; + _rc[k] = rc[k]; }); } diff --git a/bin/tmpl/app.tmpl.js b/bin/tmpl/app.tmpl.js new file mode 100644 index 0000000..fd6aac7 --- /dev/null +++ b/bin/tmpl/app.tmpl.js @@ -0,0 +1,9 @@ +'use strict'; + +// Here's a vanilla HTTP app to start, +// but feel free to replace it with Express, Koa, etc +var app = function(req, res) { + res.end('Hello, Encrypted World!'); +}; + +module.exports = app; diff --git a/bin/tmpl/server.tmpl.js b/bin/tmpl/server.tmpl.js new file mode 100644 index 0000000..5c0e24c --- /dev/null +++ b/bin/tmpl/server.tmpl.js @@ -0,0 +1,36 @@ +'use strict'; + +require('greenlock-express') + .init(function() { + // .greenlockrc defines which manager to use + // (i.e. greenlock-manager-fs or greenlock-manager-cloud) + var options = getGreenlockRc() || {}; + + // name & version for ACME client user agent + var pkg = require('./package.json'); + options.packageAgent = pkg.name + '/' + pkg.version; + + // contact for security and critical bug notices + options.maintainerEmail = pkg.author; + + // whether or not to run at cloudscale + options.cluster = false; + + return options; + }) + .ready(function(glx) { + var app = require('./app.js'); + + // Serves on 80 and 443 + // Get's SSL certificates magically! + glx.serveApp(app); + }); + +function getGreenlockRc() { + // The RC file is also used by the (optional) CLI and (optional) Web GUI. + // You are free to forego CLI and GUI support. + var fs = require('fs'); + var rcPath = '.greenlockrc'; + var rc = fs.readFileSync(rcPath, 'utf8'); + return JSON.parse(rc); +} diff --git a/plugins.js b/plugins.js index f2dea3e..4d3c9f9 100644 --- a/plugins.js +++ b/plugins.js @@ -208,6 +208,11 @@ P._loadSync = function(modname) { }; P._installSync = function(moduleName) { + try { + return require(moduleName); + } catch (e) { + // continue + } var npm = 'npm'; var args = ['install', '--save', moduleName]; var out = ''; diff --git a/tests/cli.sh b/tests/cli.sh index 8206f83..230b41c 100644 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -14,13 +14,13 @@ node bin/greenlock.js defaults node bin/greenlock.js defaults --challenge-dns-01 foo-http-01-bar --challenge-dns-01-token BIG_TOKEN # using --challenge is exclusive (will delete things not mentioned) node bin/greenlock.js defaults --challenge acme-http-01-standalone -node bin/greenlock.js remove --subject example.com # should delete all and add just this one anew node bin/greenlock.js update --subject example.com --challenge bar-http-01-baz # should add, leaving the existing node bin/greenlock.js update --subject example.com --challenge-dns-01 baz-dns-01-qux --challenge-dns-01-token BIG_TOKEN # should delete all and add just this one anew node bin/greenlock.js update --subject example.com --challenge bar-http-01-baz +node bin/greenlock.js remove --subject example.com # TODO test for failure # node bin/greenlock.js add --subject example.com