Browse Source

v3.0.15: drastically reduce scope of manager

v3 v3.0.15
AJ ONeal 4 years ago
parent
commit
72ccd8f3b7
  1. 10
      accounts.js
  2. 41
      certificates.js
  3. 656
      greenlock.js
  4. 258
      manager-underlay.js
  5. 30
      package-lock.json
  6. 5
      package.json
  7. 51
      tests/index.js

10
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,

41
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) {

656
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;
}

258
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;
}

30
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",

5
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"
},

51
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);
});

Loading…
Cancel
Save