2019-10-22 00:15:34 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var Manage = module.exports;
|
|
|
|
var sfs = require('safe-replace').create({ tmp: 'tmp', bak: 'bak' });
|
|
|
|
var promisify = require('util').promisify;
|
|
|
|
var fs = require('fs');
|
|
|
|
var readFile = promisify(fs.readFile);
|
|
|
|
var statFile = promisify(fs.stat);
|
|
|
|
var chmodFile = promisify(fs.chmod);
|
|
|
|
var homedir = require('os').homedir();
|
|
|
|
var path = require('path');
|
|
|
|
var mkdirp = promisify(require('@root/mkdirp'));
|
|
|
|
|
|
|
|
Manage.create = function(opts) {
|
|
|
|
if (!opts) {
|
|
|
|
opts = {};
|
|
|
|
}
|
|
|
|
if (!opts.configFile) {
|
|
|
|
opts.configFile = '~/.config/greenlock/manager.json';
|
2019-10-28 08:23:23 +00:00
|
|
|
console.info('Greenlock Manager Config File: ' + opts.configFile);
|
2019-10-22 00:15:34 +00:00
|
|
|
}
|
|
|
|
opts.configFile = opts.configFile.replace('~/', homedir + '/');
|
|
|
|
|
|
|
|
var manage = {};
|
|
|
|
|
2019-10-28 08:23:23 +00:00
|
|
|
manage._txPromise = Promise.resolve();
|
2019-10-22 00:15:34 +00:00
|
|
|
|
2019-10-29 05:08:41 +00:00
|
|
|
manage.defaults = manage.config = function(conf) {
|
2019-10-22 00:15:34 +00:00
|
|
|
// get / set default site settings such as
|
|
|
|
// subscriberEmail, store, challenges, renewOffset, renewStagger
|
|
|
|
return Manage._getLatest(manage, opts).then(function(config) {
|
|
|
|
if (!conf) {
|
|
|
|
conf = JSON.parse(JSON.stringify(config));
|
|
|
|
delete conf.sites;
|
|
|
|
return conf;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO set initial sites
|
|
|
|
if (conf.sites) {
|
|
|
|
throw new Error('cannot set sites as global config');
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO whitelist rather than blacklist?
|
|
|
|
if (
|
|
|
|
[
|
|
|
|
'subject',
|
|
|
|
'altnames',
|
|
|
|
'lastAttemptAt',
|
|
|
|
'expiresAt',
|
|
|
|
'issuedAt',
|
|
|
|
'renewAt'
|
|
|
|
].some(function(k) {
|
|
|
|
if (k in conf) {
|
|
|
|
throw new Error(
|
|
|
|
'`' + k + '` not allowed as a default setting'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
) {
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.keys(conf).forEach(function(k) {
|
|
|
|
if (-1 !== ['sites', '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`"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (null === k) {
|
|
|
|
delete config[k];
|
|
|
|
}
|
|
|
|
|
|
|
|
config[k] = conf[k];
|
|
|
|
});
|
|
|
|
|
2019-10-28 08:23:23 +00:00
|
|
|
return manage._save(config);
|
2019-10-22 00:15:34 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
manage.add = function(args) {
|
|
|
|
manage._txPromise = manage._txPromise.then(function() {
|
|
|
|
// if the fs has changed since we last wrote, get the lastest from disk
|
|
|
|
return Manage._getLatest(manage, opts).then(function(config) {
|
|
|
|
// TODO move to Greenlock.add
|
2019-10-28 08:23:23 +00:00
|
|
|
var subscriberEmail = args.subscriberEmail;
|
2019-10-22 00:15:34 +00:00
|
|
|
var subject = args.subject || args.domain;
|
|
|
|
var primary = subject;
|
2019-10-29 05:08:41 +00:00
|
|
|
var altnames =
|
|
|
|
args.servernames || args.altnames || args.domains;
|
2019-10-22 00:15:34 +00:00
|
|
|
if ('string' !== typeof primary) {
|
|
|
|
if (!Array.isArray(altnames) || !altnames.length) {
|
|
|
|
throw new Error('there needs to be a subject');
|
|
|
|
}
|
|
|
|
primary = altnames.slice(0).sort()[0];
|
|
|
|
}
|
|
|
|
if (!Array.isArray(altnames) || !altnames.length) {
|
|
|
|
altnames = [primary];
|
|
|
|
}
|
|
|
|
primary = primary.toLowerCase();
|
|
|
|
altnames = altnames.map(function(name) {
|
|
|
|
return name.toLowerCase();
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!config.sites) {
|
|
|
|
config.sites = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
var site = config.sites[primary];
|
|
|
|
if (!site) {
|
|
|
|
site = config.sites[primary] = { altnames: [] };
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
// TODO added, removed, moved (duplicate), changed
|
2019-10-28 08:23:23 +00:00
|
|
|
if (subscriberEmail) {
|
|
|
|
site.subscriberEmail = subscriberEmail;
|
|
|
|
}
|
2019-10-22 00:15:34 +00:00
|
|
|
site.subject = subject;
|
|
|
|
site.altnames = altnames;
|
2019-10-28 08:23:23 +00:00
|
|
|
if (!site.issuedAt) {
|
|
|
|
site.issuedAt = 0;
|
|
|
|
}
|
2019-10-22 00:15:34 +00:00
|
|
|
site.expiresAt = site.expiresAt || 0;
|
|
|
|
site.lastAttemptAt = site.lastAttemptAt || 0;
|
|
|
|
// re-add if this was deleted
|
|
|
|
site.deletedAt = 0;
|
|
|
|
if (
|
|
|
|
site.altnames
|
|
|
|
.slice(0)
|
|
|
|
.sort()
|
|
|
|
.join() !==
|
|
|
|
altnames
|
|
|
|
.slice(0)
|
|
|
|
.sort()
|
|
|
|
.join()
|
|
|
|
) {
|
|
|
|
site.expiresAt = 0;
|
|
|
|
site.issuedAt = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// These should usually be empty, for most situations
|
2019-10-28 08:23:23 +00:00
|
|
|
if (args.customerEmail) {
|
|
|
|
site.customerEmail = args.customerEmail;
|
|
|
|
}
|
|
|
|
if (args.challenges) {
|
|
|
|
site.challenges = args.challenges;
|
|
|
|
}
|
|
|
|
if (args.store) {
|
|
|
|
site.store = args.store;
|
|
|
|
}
|
2019-10-22 00:15:34 +00:00
|
|
|
|
|
|
|
return manage._save(config).then(function() {
|
|
|
|
return JSON.parse(JSON.stringify(site));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return manage._txPromise;
|
|
|
|
};
|
|
|
|
|
|
|
|
manage.find = function(args) {
|
2019-10-29 05:08:41 +00:00
|
|
|
var some = _find(args);
|
|
|
|
if (!opts.find) {
|
|
|
|
return some;
|
|
|
|
}
|
|
|
|
// TODO function to always add
|
|
|
|
throw new Error('TODO: use the given find');
|
|
|
|
};
|
|
|
|
function _find(args) {
|
2019-10-22 00:15:34 +00:00
|
|
|
return Manage._getLatest(manage, opts).then(function(config) {
|
|
|
|
// i.e. find certs more than 30 days old
|
|
|
|
//args.issuedBefore = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
|
|
// i.e. find certs more that will expire in less than 45 days
|
|
|
|
//args.expiresBefore = Date.now() + 45 * 24 * 60 * 60 * 1000;
|
|
|
|
var issuedBefore = args.issuedBefore || 0;
|
2019-10-28 08:23:23 +00:00
|
|
|
var expiresBefore = args.expiresBefore || Infinity; //Date.now() + 21 * 24 * 60 * 60 * 1000;
|
|
|
|
|
|
|
|
var altnames = (args.altnames || args.domains || []).slice(0);
|
|
|
|
if (args.servername && !altnames.includes(args.servername)) {
|
|
|
|
altnames.push(args.servername);
|
|
|
|
}
|
|
|
|
if (args.subject && !altnames.includes(args.subject)) {
|
|
|
|
altnames.push(args.subject);
|
|
|
|
}
|
2019-10-22 00:15:34 +00:00
|
|
|
|
|
|
|
// TODO match ANY domain on any cert
|
2019-10-28 08:23:23 +00:00
|
|
|
var sites = Object.keys(config.sites || {})
|
2019-10-22 00:15:34 +00:00
|
|
|
.filter(function(sub) {
|
|
|
|
var site = config.sites[sub];
|
|
|
|
if (
|
|
|
|
!site.deletedAt ||
|
|
|
|
site.expiresAt < expiresBefore ||
|
|
|
|
site.issuedAt < issuedBefore
|
|
|
|
) {
|
2019-10-28 08:23:23 +00:00
|
|
|
return (site.altnames || []).some(function(name) {
|
|
|
|
return altnames.includes(name);
|
|
|
|
});
|
2019-10-22 00:15:34 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.map(function(name) {
|
|
|
|
var site = config.sites[name];
|
|
|
|
return {
|
|
|
|
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
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
return sites;
|
|
|
|
});
|
2019-10-29 05:08:41 +00:00
|
|
|
}
|
2019-10-22 00:15:34 +00:00
|
|
|
|
2019-10-29 05:08:41 +00:00
|
|
|
manage.notify = opts.notify || _notify;
|
|
|
|
function _notify(ev, args) {
|
2019-10-22 00:15:34 +00:00
|
|
|
if (!args) {
|
|
|
|
args = ev;
|
|
|
|
ev = args.event;
|
|
|
|
delete args.event;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO define message types
|
2019-10-28 08:23:23 +00:00
|
|
|
if (!manage._notify_notice) {
|
|
|
|
console.info(
|
|
|
|
'set greenlockOptions.notify to override the default logger'
|
|
|
|
);
|
|
|
|
manage._notify_notice = true;
|
|
|
|
}
|
|
|
|
switch (ev) {
|
|
|
|
case 'error':
|
|
|
|
/* falls through */
|
|
|
|
case 'warning':
|
|
|
|
console.error(
|
|
|
|
'Error%s:',
|
2019-10-29 05:08:41 +00:00
|
|
|
(' ' + (args.context || '')).trimRight()
|
2019-10-28 08:23:23 +00:00
|
|
|
);
|
|
|
|
console.error(args.message);
|
|
|
|
if (args.description) {
|
|
|
|
console.error(args.description);
|
|
|
|
}
|
|
|
|
if (args.code) {
|
|
|
|
console.error('code:', args.code);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
if (/status/.test(ev)) {
|
|
|
|
console.info(
|
|
|
|
ev,
|
|
|
|
args.altname || args.subject || '',
|
|
|
|
args.status || ''
|
|
|
|
);
|
|
|
|
if (!args.status) {
|
2019-10-29 05:08:41 +00:00
|
|
|
console.info(args);
|
2019-10-28 08:23:23 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
console.info(
|
|
|
|
ev,
|
|
|
|
'(more info available: ' + Object.keys(args).join(' ') + ')'
|
|
|
|
);
|
|
|
|
}
|
2019-10-29 05:08:41 +00:00
|
|
|
}
|
2019-10-22 00:15:34 +00:00
|
|
|
|
|
|
|
manage.update = function(args) {
|
|
|
|
manage._txPromise = manage.txPromise.then(function() {
|
|
|
|
return Manage._getLatest(manage, opts).then(function(config) {
|
|
|
|
var site = config.sites[args.subject];
|
|
|
|
site.issuedAt = args.issuedAt;
|
|
|
|
site.expiresAt = args.expiresAt;
|
|
|
|
site.renewAt = args.renewAt;
|
|
|
|
// foo
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return manage._txPromise;
|
|
|
|
};
|
|
|
|
|
2019-10-29 05:08:41 +00:00
|
|
|
manage.remove = function(args) {
|
|
|
|
if (!args.subject) {
|
|
|
|
throw new Error('should have a subject for sites to remove');
|
|
|
|
}
|
|
|
|
manage._txPromise = manage.txPromise.then(function() {
|
|
|
|
return Manage._getLatest(manage, opts).then(function(config) {
|
|
|
|
var site = config.sites[args.subject];
|
|
|
|
if (!site) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
site.deletedAt = Date.now();
|
|
|
|
|
|
|
|
return JSON.parse(JSON.stringify(site));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return manage._txPromise;
|
|
|
|
};
|
|
|
|
|
|
|
|
manage._lastStat = {
|
|
|
|
size: 0,
|
|
|
|
mtimeMs: 0
|
|
|
|
};
|
|
|
|
manage._config = {};
|
|
|
|
|
|
|
|
manage._save = function(config) {
|
|
|
|
return mkdirp(path.dirname(opts.configFile)).then(function() {
|
|
|
|
return sfs
|
|
|
|
.writeFileAsync(
|
|
|
|
opts.configFile,
|
|
|
|
// pretty-print the config file
|
|
|
|
JSON.stringify(config, null, 2),
|
|
|
|
'utf8'
|
|
|
|
)
|
|
|
|
.then(function() {
|
|
|
|
// this file may contain secrets, so keep it safe
|
|
|
|
return chmodFile(opts.configFile, parseInt('0600', 8))
|
|
|
|
.catch(function() {
|
|
|
|
/*ignore for Windows */
|
|
|
|
})
|
|
|
|
.then(function() {
|
|
|
|
return statFile(opts.configFile).then(function(
|
|
|
|
stat
|
|
|
|
) {
|
|
|
|
manage._lastStat.size = stat.size;
|
|
|
|
manage._lastStat.mtimeMs = stat.mtimeMs;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2019-10-22 00:15:34 +00:00
|
|
|
return manage;
|
|
|
|
};
|
|
|
|
|
|
|
|
Manage._getLatest = function(mng, opts) {
|
|
|
|
return statFile(opts.configFile)
|
|
|
|
.catch(function(err) {
|
|
|
|
if ('ENOENT' === err.code) {
|
|
|
|
return {
|
|
|
|
size: 0,
|
|
|
|
mtimeMs: 0
|
|
|
|
};
|
|
|
|
}
|
|
|
|
err.context = 'manager_read';
|
|
|
|
throw err;
|
|
|
|
})
|
|
|
|
.then(function(stat) {
|
|
|
|
if (
|
|
|
|
stat.size === mng._lastStat.size &&
|
|
|
|
stat.mtimeMs === mng._lastStat.mtimeMs
|
|
|
|
) {
|
|
|
|
return mng._config;
|
|
|
|
}
|
|
|
|
return readFile(opts.configFile, 'utf8').then(function(data) {
|
|
|
|
mng._lastStat = stat;
|
|
|
|
mng._config = JSON.parse(data);
|
|
|
|
return mng._config;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|