add greenlock cli add
This commit is contained in:
parent
ca60e16413
commit
bc3d36a94a
|
@ -1,6 +1,7 @@
|
||||||
TODO.txt
|
TODO.txt
|
||||||
link.sh
|
link.sh
|
||||||
.env
|
.env
|
||||||
|
.greenlockrc
|
||||||
|
|
||||||
# ---> Node
|
# ---> Node
|
||||||
# Logs
|
# Logs
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
'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');
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete options.forceSave;
|
||||||
|
|
||||||
|
/*
|
||||||
|
console.log('manager conf:');
|
||||||
|
console.log(mconf);
|
||||||
|
console.log('cli options:');
|
||||||
|
console.log(options);
|
||||||
|
*/
|
||||||
|
|
||||||
|
greenlock.add(options).catch(function(err) {
|
||||||
|
console.error();
|
||||||
|
console.error('error:', err.message);
|
||||||
|
console.error();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.main(main, process.argv.slice(3));
|
||||||
|
});
|
|
@ -2,12 +2,15 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var args = process.argv.slice(2);
|
var args = process.argv.slice(2);
|
||||||
|
var arg0 = args[0];
|
||||||
//console.log(args);
|
//console.log(args);
|
||||||
//['certonly', 'add', 'config', 'defaults', 'remove']
|
|
||||||
if ('certonly' === args[0]) {
|
|
||||||
require('./certonly.js');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("command not yet implemented");
|
['certonly', 'add', 'config', 'defaults', 'remove'].some(function(k) {
|
||||||
|
if (k === arg0) {
|
||||||
|
require('./' + k);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error(arg0 + 'command not yet implemented');
|
||||||
process.exit();
|
process.exit();
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// TODO how to handle path differences when run from npx vs when required by greenlock?
|
||||||
|
|
||||||
|
var promisify = require('util').promisify;
|
||||||
|
var fs = require('fs');
|
||||||
|
var readFile = promisify(fs.readFile);
|
||||||
|
var writeFile = promisify(fs.writeFile);
|
||||||
|
var chmodFile = promisify(fs.chmod);
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
function saveFile(rcpath, data, enc) {
|
||||||
|
// because this may have a database url or some such
|
||||||
|
return writeFile(rcpath, data, enc).then(function() {
|
||||||
|
return chmodFile(rcpath, parseInt('0600', 8));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async function(pkgpath, manager, rc) {
|
||||||
|
// TODO when run from package
|
||||||
|
// Run from the package root (assumed) or exit
|
||||||
|
var pkgdir = path.dirname(pkgpath);
|
||||||
|
var rcpath = path.join(pkgdir, '.greenlockrc');
|
||||||
|
var created = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
require(pkgpath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
'npx greenlock must be run from the package root (where package.json is)'
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manager) {
|
||||||
|
if ('.' === manager[0]) {
|
||||||
|
manager = path.resolve(pkgdir, manager);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
require(manager);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('npx greenlock must be run from the package root');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _data = await readFile(rcpath, 'utf8').catch(function(err) {
|
||||||
|
if ('ENOENT' !== err.code) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
console.info('Creating ' + rcpath);
|
||||||
|
created = true;
|
||||||
|
var data = '{}';
|
||||||
|
return saveFile(rcpath, data, 'utf8').then(function() {
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var changed;
|
||||||
|
var _rc;
|
||||||
|
try {
|
||||||
|
_rc = JSON.parse(_data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("couldn't parse " + rcpath, _data);
|
||||||
|
console.error('(perhaps you should just delete it and try again?)');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manager) {
|
||||||
|
if (!_rc.manager) {
|
||||||
|
_rc.manager = manager;
|
||||||
|
}
|
||||||
|
if (_rc.manager !== manager) {
|
||||||
|
console.info('Switching manager:');
|
||||||
|
var older = _rc.manager;
|
||||||
|
var newer = manager;
|
||||||
|
if ('/' === older[0]) {
|
||||||
|
older = path.relative(pkgdir, older);
|
||||||
|
}
|
||||||
|
if ('/' === newer[0]) {
|
||||||
|
newer = path.relative(pkgdir, newer);
|
||||||
|
}
|
||||||
|
console.info('\told: ' + older);
|
||||||
|
console.info('\tnew: ' + newer);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc) {
|
||||||
|
changed = true;
|
||||||
|
Object.keys(rc).forEach(function(k) {
|
||||||
|
_rc[k] = rc;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_rc.manager) {
|
||||||
|
changed = true;
|
||||||
|
_rc.manager = 'greenlock-manager-fs';
|
||||||
|
console.info('Using default manager ' + _rc.manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
return _rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = JSON.stringify(_rc, null, 2);
|
||||||
|
if (created) {
|
||||||
|
console.info('Wrote ' + rcpath);
|
||||||
|
}
|
||||||
|
return saveFile(rcpath, data, 'utf8').then(function() {
|
||||||
|
return _rc;
|
||||||
|
});
|
||||||
|
};
|
|
@ -26,6 +26,7 @@ G.create = function(gconf) {
|
||||||
var manager;
|
var manager;
|
||||||
|
|
||||||
greenlock._create = function() {
|
greenlock._create = function() {
|
||||||
|
if (!gconf._bin_mode) {
|
||||||
if (!gconf.maintainerEmail) {
|
if (!gconf.maintainerEmail) {
|
||||||
throw E.NO_MAINTAINER('create');
|
throw E.NO_MAINTAINER('create');
|
||||||
}
|
}
|
||||||
|
@ -40,6 +41,7 @@ G.create = function(gconf) {
|
||||||
// maybe move this to init and don't exit the process, just in case
|
// maybe move this to init and don't exit the process, just in case
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ('function' === typeof gconf.notify) {
|
if ('function' === typeof gconf.notify) {
|
||||||
gdefaults.notify = gconf.notify;
|
gdefaults.notify = gconf.notify;
|
||||||
|
@ -84,6 +86,7 @@ G.create = function(gconf) {
|
||||||
greenlock._defaults = gdefaults;
|
greenlock._defaults = gdefaults;
|
||||||
greenlock._defaults.debug = gconf.debug;
|
greenlock._defaults.debug = gconf.debug;
|
||||||
|
|
||||||
|
if (!gconf._bin_mode) {
|
||||||
// renew every 90-ish minutes (random for staggering)
|
// renew every 90-ish minutes (random for staggering)
|
||||||
// the weak setTimeout (unref) means that when run as a CLI process this
|
// the weak setTimeout (unref) means that when run as a CLI process this
|
||||||
// will still finish as expected, and not wait on the timeout
|
// will still finish as expected, and not wait on the timeout
|
||||||
|
@ -93,6 +96,7 @@ G.create = function(gconf) {
|
||||||
renew();
|
renew();
|
||||||
}, Math.PI * 30 * 60 * 1000).unref();
|
}, Math.PI * 30 * 60 * 1000).unref();
|
||||||
})();
|
})();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The purpose of init is to make MCONF the source of truth
|
// The purpose of init is to make MCONF the source of truth
|
||||||
|
|
|
@ -5,7 +5,7 @@ var E = require('./errors.js');
|
||||||
|
|
||||||
var warned = {};
|
var warned = {};
|
||||||
|
|
||||||
module.exports.wrap = function(greenlock, manager) {
|
module.exports.wrap = function(greenlock, manager, gconf) {
|
||||||
greenlock.manager = {};
|
greenlock.manager = {};
|
||||||
greenlock.sites = {};
|
greenlock.sites = {};
|
||||||
//greenlock.accounts = {};
|
//greenlock.accounts = {};
|
||||||
|
@ -143,12 +143,14 @@ module.exports.wrap = function(greenlock, manager) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return manager.set(args).then(function(result) {
|
return manager.set(args).then(function(result) {
|
||||||
|
if (!gconf._bin_mode) {
|
||||||
greenlock.renew({}).catch(function(err) {
|
greenlock.renew({}).catch(function(err) {
|
||||||
if (!err.context) {
|
if (!err.context) {
|
||||||
err.contxt = 'renew';
|
err.contxt = 'renew';
|
||||||
}
|
}
|
||||||
greenlock._notify('error', err);
|
greenlock._notify('error', err);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -222,25 +224,15 @@ function checkAltnames(subject, args) {
|
||||||
return String(name || '').toLowerCase();
|
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
|
// punycode BEFORE validation
|
||||||
// (set, find, remove)
|
// (set, find, remove)
|
||||||
|
if (altnames.join() !== args.altnames.join()) {
|
||||||
|
console.warn(
|
||||||
|
'all domains in `altnames` must be lowercase:',
|
||||||
|
args.altnames
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
args.altnames = args.altnames.map(U._encodeName);
|
args.altnames = args.altnames.map(U._encodeName);
|
||||||
if (
|
if (
|
||||||
!args.altnames.every(function(d) {
|
!args.altnames.every(function(d) {
|
||||||
|
@ -250,9 +242,21 @@ function checkAltnames(subject, args) {
|
||||||
throw E.INVALID_HOSTNAME('add', "'" + args.altnames.join("' '") + "'");
|
throw E.INVALID_HOSTNAME('add', "'" + args.altnames.join("' '") + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (altnames.join() !== args.altnames.join()) {
|
if (subject && subject !== args.altnames[0]) {
|
||||||
console.warn('all domains in `altnames` must be lowercase', altnames);
|
throw E.BAD_ORDER(
|
||||||
|
'add',
|
||||||
|
'(' + args.subject + ") '" + args.altnames.join("' '") + "'"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
if (subject && subject !== altnames[0]) {
|
||||||
|
throw new Error(
|
||||||
|
'`subject` must be the first domain in `altnames`',
|
||||||
|
args.subject,
|
||||||
|
altnames.join(' ')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return altnames;
|
return altnames;
|
||||||
}
|
}
|
||||||
|
|
2
utils.js
2
utils.js
|
@ -59,7 +59,7 @@ U._validName = function(str) {
|
||||||
// Note: _ (underscore) is only allowed for "domain names", not "hostnames"
|
// Note: _ (underscore) is only allowed for "domain names", not "hostnames"
|
||||||
// Note: - (hyphen) is not allowed as a first character (but a number is)
|
// Note: - (hyphen) is not allowed as a first character (but a number is)
|
||||||
return (
|
return (
|
||||||
/^(\*\.)?[a-z0-9_\.\-]+$/.test(str) &&
|
/^(\*\.)?[a-z0-9_\.\-]+\.[a-z0-9_\.\-]+$/.test(str) &&
|
||||||
str.length < 254 &&
|
str.length < 254 &&
|
||||||
str.split('.').every(function(label) {
|
str.split('.').every(function(label) {
|
||||||
return label.length > 0 && label.length < 64;
|
return label.length > 0 && label.length < 64;
|
||||||
|
|
Loading…
Reference in New Issue