v3.0.0: A file-based site manager for greenlock
This commit is contained in:
parent
116b4925e7
commit
cdd1201bc6
59
README.md
59
README.md
|
@ -1,3 +1,58 @@
|
||||||
# greenlock-manager-fs.js
|
# [greenlock-manager-fs.js](https://git.rootprojects.org/root/greenlock-manager-fs.js)
|
||||||
|
|
||||||
A simple file-based management strategy for greenlock
|
A simple file-based management strategy for Greenlock v3
|
||||||
|
|
||||||
|
(to manage SSL certificates for sites)
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```js
|
||||||
|
npm install --save greenlock-manager-fs@v3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use with Greenlock
|
||||||
|
|
||||||
|
```js
|
||||||
|
var greenlock = require('greenlock').create({
|
||||||
|
// ...
|
||||||
|
|
||||||
|
manager: 'greenlock-manager-fs',
|
||||||
|
configFile: '~/.config/greenlock/manager.json'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example config file
|
||||||
|
|
||||||
|
You might start your config file like this:
|
||||||
|
|
||||||
|
`~/.config/greenlock/manager.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"subscriberEmail": "jon@example.com",
|
||||||
|
"agreeToTerms": true,
|
||||||
|
"sites": [
|
||||||
|
{
|
||||||
|
"subject": "example.com",
|
||||||
|
"altnames": ["example.com", "*.example.com"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Management (coming soon)
|
||||||
|
|
||||||
|
We're going to be adding some tools to greenlock so that you can do
|
||||||
|
something like this to manage your sites and SSL certificates:
|
||||||
|
|
||||||
|
```js
|
||||||
|
npx greenlock defaults --subscriber-email jon@example.com --agree-to-terms true
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
npx greenlock add --subject example.com --altnames example.com,*.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
npx greenlock renew --all
|
||||||
|
```
|
||||||
|
|
593
manager.js
593
manager.js
|
@ -1,6 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var Manage = module.exports;
|
var Manage = module.exports;
|
||||||
|
var doctor = {};
|
||||||
|
|
||||||
var sfs = require('safe-replace').create({ tmp: 'tmp', bak: 'bak' });
|
var sfs = require('safe-replace').create({ tmp: 'tmp', bak: 'bak' });
|
||||||
var promisify = require('util').promisify;
|
var promisify = require('util').promisify;
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
@ -11,6 +13,15 @@ var homedir = require('os').homedir();
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var mkdirp = promisify(require('@root/mkdirp'));
|
var mkdirp = promisify(require('@root/mkdirp'));
|
||||||
|
|
||||||
|
// NOTE
|
||||||
|
// this is over-complicated to account for people
|
||||||
|
// doing weird things, and this just being a file system
|
||||||
|
// and wanting to be fairly sure it works and produces
|
||||||
|
// meaningful errors
|
||||||
|
|
||||||
|
// For your use case you'll probably find a better example
|
||||||
|
// in greenlock-manager-test
|
||||||
|
|
||||||
Manage.create = function(CONF) {
|
Manage.create = function(CONF) {
|
||||||
if (!CONF) {
|
if (!CONF) {
|
||||||
CONF = {};
|
CONF = {};
|
||||||
|
@ -25,264 +36,105 @@ Manage.create = function(CONF) {
|
||||||
|
|
||||||
manage._txPromise = Promise.resolve();
|
manage._txPromise = Promise.resolve();
|
||||||
|
|
||||||
manage.defaults = manage.config = function(conf) {
|
// Note: all of these top-level methods are effectively mutexed
|
||||||
// get / set default site settings such as
|
// You cannot call them from each other or they will deadlock
|
||||||
// subscriberEmail, store, challenges, renewOffset, renewStagger
|
|
||||||
return Manage._getLatest(manage, CONF).then(function(config) {
|
manage.defaults = manage.config = async function(conf) {
|
||||||
|
manage._txPromise = manage._txPromise.then(async function() {
|
||||||
|
var config = await Manage._getLatest(manage, CONF);
|
||||||
|
|
||||||
|
// act as a getter
|
||||||
if (!conf) {
|
if (!conf) {
|
||||||
conf = JSON.parse(JSON.stringify(config));
|
conf = JSON.parse(JSON.stringify(config.defaults));
|
||||||
delete conf.sites;
|
|
||||||
return conf;
|
return conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO set initial sites
|
// act as a setter
|
||||||
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) {
|
Object.keys(conf).forEach(function(k) {
|
||||||
if (-1 !== ['sites', 'module', 'manager'].indexOf(k)) {
|
config.defaults[k] = conf[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];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return manage._save(config);
|
return manage._save(config);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
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, CONF).then(function(config) {
|
|
||||||
// TODO move to Greenlock.add
|
|
||||||
var subscriberEmail = args.subscriberEmail;
|
|
||||||
var subject = args.subject || args.domain;
|
|
||||||
var primary = subject;
|
|
||||||
var altnames =
|
|
||||||
args.servernames || args.altnames || args.domains;
|
|
||||||
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 existing = config.sites[primary];
|
|
||||||
var site = existing;
|
|
||||||
if (!existing) {
|
|
||||||
site = config.sites[primary] = { altnames: [primary] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
if (subscriberEmail) {
|
|
||||||
site.subscriberEmail = subscriberEmail;
|
|
||||||
}
|
|
||||||
site.subject = subject;
|
|
||||||
site.renewAt = args.renewAt || site.renewAt || 0;
|
|
||||||
if (
|
|
||||||
altnames
|
|
||||||
.slice(0)
|
|
||||||
.sort()
|
|
||||||
.join(' ') !==
|
|
||||||
site.altnames
|
|
||||||
.slice(0)
|
|
||||||
.sort()
|
|
||||||
.join(' ')
|
|
||||||
) {
|
|
||||||
// TODO signal to wait for renewal?
|
|
||||||
// it will definitely be renewed on the first request anyway
|
|
||||||
site.renewAt = 0;
|
|
||||||
}
|
|
||||||
site.altnames = altnames;
|
|
||||||
if (!site.issuedAt) {
|
|
||||||
site.issuedAt = 0;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
if (args.customerEmail) {
|
|
||||||
site.customerEmail = args.customerEmail;
|
|
||||||
}
|
|
||||||
if (args.challenges) {
|
|
||||||
site.challenges = args.challenges;
|
|
||||||
}
|
|
||||||
if (args.store) {
|
|
||||||
site.store = args.store;
|
|
||||||
}
|
|
||||||
|
|
||||||
return manage._save(config).then(function() {
|
|
||||||
return JSON.parse(JSON.stringify(site));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return manage._txPromise;
|
return manage._txPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
manage.find = function(args) {
|
manage.set = async function(args) {
|
||||||
return _find(args).then(function(existing) {
|
manage._txPromise = manage._txPromise.then(async function() {
|
||||||
if (!CONF.find) {
|
var config = await Manage._getLatest(manage, CONF);
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(CONF.find(args)).then(function(results) {
|
manage._merge(config, config.sites[args.subject], args);
|
||||||
// TODO also detect and delete stale (just ignoring them for now)
|
|
||||||
var changed = [];
|
|
||||||
var same = [];
|
|
||||||
results.forEach(function(_newer) {
|
|
||||||
// Check lowercase subject names
|
|
||||||
var subject = (_newer.subject || '').toLowerCase();
|
|
||||||
// Set the default altnames to the subject, just in case
|
|
||||||
var altnames = (_newer.altnames || []).slice(0);
|
|
||||||
if (!altnames.includes(subject)) {
|
|
||||||
console.warn(
|
|
||||||
"all site configs should include 'subject' and 'altnames': " +
|
|
||||||
subject
|
|
||||||
);
|
|
||||||
altnames.push(subject);
|
|
||||||
}
|
|
||||||
|
|
||||||
existing.some(function(_older) {
|
await manage._save(config);
|
||||||
if (subject !== (_older.subject || '').toLowerCase()) {
|
return JSON.parse(JSON.stringify(config.sites[args.subject]));
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_newer._exists = true;
|
|
||||||
|
|
||||||
// Compare the altnames and update if needed
|
|
||||||
if (
|
|
||||||
altnames
|
|
||||||
.slice(0)
|
|
||||||
.sort()
|
|
||||||
.join(' ') !==
|
|
||||||
(_older.altnames || [])
|
|
||||||
.slice(0)
|
|
||||||
.sort()
|
|
||||||
.join(' ')
|
|
||||||
) {
|
|
||||||
_older.renewAt = 0;
|
|
||||||
_older.altnames = altnames;
|
|
||||||
changed.push(_older);
|
|
||||||
} else {
|
|
||||||
same.push(_older);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!_newer._exists) {
|
|
||||||
changed.push({
|
|
||||||
subject: subject,
|
|
||||||
altnames: altnames,
|
|
||||||
renewAt: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!changed.length) {
|
|
||||||
return same;
|
|
||||||
}
|
|
||||||
|
|
||||||
// kinda redundant to pull again, but whatever...
|
|
||||||
return Manage._getLatest(manage, CONF).then(function(config) {
|
|
||||||
changed.forEach(function(site) {
|
|
||||||
config.sites[site.subject] = site;
|
|
||||||
});
|
|
||||||
return manage._save(config).then(function() {
|
|
||||||
// everything was either added, updated, or not different
|
|
||||||
// hence, this is everything
|
|
||||||
var all = changed.concat(same);
|
|
||||||
return all;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return manage._txPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
manage._merge = function(config, current, args) {
|
||||||
|
if (!current || current.deletedAt) {
|
||||||
|
current = config.sites[args.subject] = {
|
||||||
|
subject: args.subject,
|
||||||
|
altnames: [],
|
||||||
|
renewAt: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
current.renewAt = parseInt(args.renewAt || current.renewAt, 10) || 1;
|
||||||
|
var oldAlts;
|
||||||
|
var newAlts;
|
||||||
|
if (args.altnames) {
|
||||||
|
// copy as to not disturb order, which matters
|
||||||
|
oldAlts = current.altnames.slice(0).sort();
|
||||||
|
newAlts = args.altnames.slice(0).sort();
|
||||||
|
|
||||||
|
if (newAlts.join() !== oldAlts.join()) {
|
||||||
|
// this will cause immediate renewal
|
||||||
|
args.renewAt = 1;
|
||||||
|
current.renewAt = 1;
|
||||||
|
current.altnames = args.altnames.slice(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// no transaction promise here because it calls set
|
||||||
|
manage.find = async function(args) {
|
||||||
|
var ours = await _find(args);
|
||||||
|
if (!CONF.find) {
|
||||||
|
return ours;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the user has an overlay find function we'll do a diff
|
||||||
|
// between the managed state and the overlay, and choose
|
||||||
|
// what was found.
|
||||||
|
var theirs = await CONF.find(args);
|
||||||
|
return _mergeFind(ours, theirs);
|
||||||
};
|
};
|
||||||
|
|
||||||
function _find(args) {
|
function _find(args) {
|
||||||
return Manage._getLatest(manage, CONF).then(function(config) {
|
manage._txPromise = manage._txPromise.then(async function() {
|
||||||
|
var config = await Manage._getLatest(manage, CONF);
|
||||||
// i.e. find certs more than 30 days old
|
// i.e. find certs more than 30 days old
|
||||||
//args.issuedBefore = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
//args.issuedBefore = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
||||||
// i.e. find certs more that will expire in less than 45 days
|
// i.e. find certs more that will expire in less than 45 days
|
||||||
//args.expiresBefore = Date.now() + 45 * 24 * 60 * 60 * 1000;
|
//args.expiresBefore = Date.now() + 45 * 24 * 60 * 60 * 1000;
|
||||||
var issuedBefore = args.issuedBefore || Infinity;
|
var issuedBefore = args.issuedBefore || Infinity;
|
||||||
var expiresBefore = args.expiresBefore || Infinity; //Date.now() + 21 * 24 * 60 * 60 * 1000;
|
var expiresBefore = args.expiresBefore || Infinity; //Date.now() + 21 * 24 * 60 * 60 * 1000;
|
||||||
var nameKeys = ['subject', 'altnames'];
|
|
||||||
|
|
||||||
// if there's anything to match, only return matches
|
// if there's anything to match, only return matches
|
||||||
// if there's nothing to match, return everything
|
// if there's nothing to match, return everything
|
||||||
|
var nameKeys = ['subject', 'altnames'];
|
||||||
var matchAll = !nameKeys.some(function(k) {
|
var matchAll = !nameKeys.some(function(k) {
|
||||||
return k in args;
|
return k in args;
|
||||||
});
|
});
|
||||||
|
|
||||||
var querynames = (args.altnames || []).slice(0);
|
var querynames = (args.altnames || []).slice(0);
|
||||||
|
|
||||||
// TODO match ANY domain on any cert
|
var sites = Object.keys(config.sites)
|
||||||
var sites = Object.keys(config.sites || {})
|
|
||||||
.filter(function(subject) {
|
.filter(function(subject) {
|
||||||
var site = doctor.site(config.sites, subject);
|
var site = config.sites[subject];
|
||||||
if (site.deletedAt) {
|
if (site.deletedAt) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -309,172 +161,163 @@ Manage.create = function(CONF) {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.map(function(name) {
|
.map(function(name) {
|
||||||
var site = config.sites[name];
|
return doctor.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;
|
return sites;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return manage._txPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
manage.notify = CONF.notify || _notify;
|
function _mergeFind(config, ours, theirs) {
|
||||||
function _notify(ev, args) {
|
theirs.forEach(function(_newer) {
|
||||||
if (!args) {
|
ours.some(function(_older) {
|
||||||
args = ev;
|
if (_newer.subject !== _older.subject) {
|
||||||
ev = args.event;
|
return false;
|
||||||
delete args.event;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// TODO define message types
|
// BE SURE TO SET THIS UNDEFINED AFTERWARDS
|
||||||
if (!manage._notify_notice) {
|
_older._exists = true;
|
||||||
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:',
|
|
||||||
(' ' + (args.context || '')).trimRight()
|
|
||||||
);
|
|
||||||
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) {
|
|
||||||
console.info(args);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.info(
|
|
||||||
ev,
|
|
||||||
'(more info available: ' + Object.keys(args).join(' ') + ')'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
manage.update = function(args) {
|
manage._merge(config, _older, _newer);
|
||||||
manage._txPromise = manage._txPromise.then(function() {
|
_newer = config.sites[_older.subject];
|
||||||
return Manage._getLatest(manage, CONF).then(function(config) {
|
|
||||||
var site = config.sites[args.subject];
|
// handled the (only) match
|
||||||
//site.issuedAt = args.issuedAt;
|
return true;
|
||||||
//site.expiresAt = args.expiresAt;
|
|
||||||
site.renewAt = args.renewAt;
|
|
||||||
return manage._save(config);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// delete the things that are gone
|
||||||
|
ours.forEach(function(_older) {
|
||||||
|
if (!_older._exists) {
|
||||||
|
delete config.sites[_older.subject];
|
||||||
|
}
|
||||||
|
_older._exists = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
manage._txPromise = manage._txPromise.then(async function() {
|
||||||
|
// kinda redundant to pull again, but whatever...
|
||||||
|
var config = await Manage._getLatest(manage, CONF);
|
||||||
|
await manage._save(config);
|
||||||
|
// everything was either added, updated, or not different
|
||||||
|
// hence, this is everything
|
||||||
|
return JSON.parse(JSON.stringify(config.sites));
|
||||||
|
});
|
||||||
|
|
||||||
return manage._txPromise;
|
return manage._txPromise;
|
||||||
};
|
}
|
||||||
|
|
||||||
manage.remove = function(args) {
|
manage.remove = function(args) {
|
||||||
if (!args.subject) {
|
if (!args.subject) {
|
||||||
throw new Error('should have a subject for sites to remove');
|
throw new Error('should have a subject for sites to remove');
|
||||||
}
|
}
|
||||||
manage._txPromise = manage._txPromise.then(function() {
|
manage._txPromise = manage._txPromise.then(async function() {
|
||||||
return Manage._getLatest(manage, CONF).then(function(config) {
|
var config = await Manage._getLatest(manage, CONF);
|
||||||
var site = config.sites[args.subject];
|
var site = config.sites[args.subject];
|
||||||
if (!site) {
|
if (!site) {
|
||||||
return {};
|
return null;
|
||||||
}
|
}
|
||||||
site.deletedAt = Date.now();
|
site.deletedAt = Date.now();
|
||||||
|
await manage._save(config);
|
||||||
return JSON.parse(JSON.stringify(site));
|
return JSON.parse(JSON.stringify(site));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
return manage._txPromise;
|
return manage._txPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
manage._lastStat = {
|
|
||||||
size: 0,
|
|
||||||
mtimeMs: 0
|
|
||||||
};
|
|
||||||
manage._config = {};
|
manage._config = {};
|
||||||
|
// (wrong type #1) specifically the wrong type (null)
|
||||||
|
manage._lastStat = { size: null, mtimeMs: null };
|
||||||
|
|
||||||
manage._save = function(config) {
|
manage._save = async function(config) {
|
||||||
return mkdirp(path.dirname(CONF.configFile)).then(function() {
|
await mkdirp(path.dirname(CONF.configFile));
|
||||||
return sfs
|
// pretty-print the config file
|
||||||
.writeFileAsync(
|
var data = JSON.stringify(config, null, 2);
|
||||||
CONF.configFile,
|
await sfs.writeFileAsync(CONF.configFile, data, 'utf8');
|
||||||
// pretty-print the config file
|
|
||||||
JSON.stringify(config, null, 2),
|
// this file may contain secrets, so keep it safe
|
||||||
'utf8'
|
return chmodFile(CONF.configFile, parseInt('0600', 8))
|
||||||
)
|
.catch(function() {
|
||||||
.then(function() {
|
/*ignore for Windows */
|
||||||
// this file may contain secrets, so keep it safe
|
})
|
||||||
return chmodFile(CONF.configFile, parseInt('0600', 8))
|
.then(async function() {
|
||||||
.catch(function() {
|
var stat = await statFile(CONF.configFile);
|
||||||
/*ignore for Windows */
|
manage._lastStat.size = stat.size;
|
||||||
})
|
manage._lastStat.mtimeMs = stat.mtimeMs;
|
||||||
.then(function() {
|
});
|
||||||
return statFile(CONF.configFile).then(function(
|
};
|
||||||
stat
|
|
||||||
) {
|
manage.init = async function(deps) {
|
||||||
manage._lastStat.size = stat.size;
|
var request = deps.request;
|
||||||
manage._lastStat.mtimeMs = stat.mtimeMs;
|
// how nice...
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return manage;
|
return manage;
|
||||||
};
|
};
|
||||||
|
|
||||||
Manage._getLatest = function(mng, CONF) {
|
Manage._getLatest = function(MNG, CONF) {
|
||||||
return statFile(CONF.configFile)
|
return statFile(CONF.configFile)
|
||||||
.catch(function(err) {
|
.catch(async function(err) {
|
||||||
if ('ENOENT' === err.code) {
|
if ('ENOENT' !== err.code) {
|
||||||
return {
|
err.context = 'manager_read';
|
||||||
size: 0,
|
throw err;
|
||||||
mtimeMs: 0
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
err.context = 'manager_read';
|
await MNG._save(doctor.config());
|
||||||
throw err;
|
// (wrong type #2) specifically the wrong type (bool)
|
||||||
|
return { size: false, mtimeMs: false };
|
||||||
})
|
})
|
||||||
.then(function(stat) {
|
.then(async function(stat) {
|
||||||
if (
|
if (
|
||||||
stat.size === mng._lastStat.size &&
|
stat.size === MNG._lastStat.size &&
|
||||||
stat.mtimeMs === mng._lastStat.mtimeMs
|
stat.mtimeMs === MNG._lastStat.mtimeMs
|
||||||
) {
|
) {
|
||||||
return mng._config;
|
return MNG._config;
|
||||||
}
|
}
|
||||||
return readFile(CONF.configFile, 'utf8').then(function(data) {
|
var data = await readFile(CONF.configFile, 'utf8');
|
||||||
mng._lastStat = stat;
|
MNG._lastStat = stat;
|
||||||
mng._config = JSON.parse(data);
|
MNG._config = JSON.parse(data);
|
||||||
return mng._config;
|
return doctor.config(MNG._config);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var doctor = {};
|
|
||||||
// users muck up config files, so we try to handle it gracefully.
|
// users muck up config files, so we try to handle it gracefully.
|
||||||
|
doctor.config = function(config) {
|
||||||
|
if (!config) {
|
||||||
|
config = {};
|
||||||
|
}
|
||||||
|
if (!config.defaults) {
|
||||||
|
config.defaults = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
doctor.sites(config);
|
||||||
|
|
||||||
|
Object.keys(config).forEach(function(key) {
|
||||||
|
if (['defaults', 'routes', 'sites'].includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.defaults[key] = config[key];
|
||||||
|
delete config[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
doctor.challenges(config.defaults);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
doctor.sites = function(config) {
|
||||||
|
var sites = config.sites;
|
||||||
|
if (!sites) {
|
||||||
|
sites = {};
|
||||||
|
}
|
||||||
|
if (Array.isArray(sites)) {
|
||||||
|
sites.forEach(function(site) {
|
||||||
|
sites[site.subject] = site;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Object.keys(sites).forEach(function(k) {
|
||||||
|
doctor.site(sites, k);
|
||||||
|
});
|
||||||
|
config.sites = sites;
|
||||||
|
};
|
||||||
doctor.site = function(sconfs, subject) {
|
doctor.site = function(sconfs, subject) {
|
||||||
var site = sconfs[subject];
|
var site = sconfs[subject];
|
||||||
if (!site) {
|
if (!site) {
|
||||||
|
@ -482,8 +325,9 @@ doctor.site = function(sconfs, subject) {
|
||||||
site = {};
|
site = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO notify on any changes
|
|
||||||
if ('string' !== typeof site.subject) {
|
if ('string' !== typeof site.subject) {
|
||||||
|
console.warn('warning: deleted malformed site from config file:');
|
||||||
|
console.warn(JSON.stringify(site));
|
||||||
delete sconfs[subject];
|
delete sconfs[subject];
|
||||||
site.subject = 'greenlock-error.example.com';
|
site.subject = 'greenlock-error.example.com';
|
||||||
}
|
}
|
||||||
|
@ -496,3 +340,46 @@ doctor.site = function(sconfs, subject) {
|
||||||
|
|
||||||
return site;
|
return site;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
doctor.challenges = function(defaults) {
|
||||||
|
var challenges = defaults.challenges;
|
||||||
|
if (!challenges) {
|
||||||
|
challenges = {};
|
||||||
|
}
|
||||||
|
if (Array.isArray(defaults.challenges)) {
|
||||||
|
defaults.challenges.forEach(function(challenge) {
|
||||||
|
var typ = doctor.challengeType(challenge);
|
||||||
|
challenges[typ] = challenge;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Object.keys(challenges).forEach(function(k) {
|
||||||
|
doctor.challenge(challenges, k);
|
||||||
|
});
|
||||||
|
defaults.challenges = challenges;
|
||||||
|
if (!Object.keys(defaults.challenges).length) {
|
||||||
|
delete defaults.challenges;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
doctor.challengeType = function(challenge) {
|
||||||
|
var typ = challenge.type;
|
||||||
|
if (!typ) {
|
||||||
|
if (/\bhttp-01\b/.test(challenge.module)) {
|
||||||
|
typ = 'http-01';
|
||||||
|
} else if (/\bdns-01\b/.test(challenge.module)) {
|
||||||
|
typ = 'dns-01';
|
||||||
|
} else if (/\btls-alpn-01\b/.test(challenge.module)) {
|
||||||
|
typ = 'tls-alpn-01';
|
||||||
|
} else {
|
||||||
|
typ = 'error-01';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete challenge.type;
|
||||||
|
return typ;
|
||||||
|
};
|
||||||
|
doctor.challenge = function(chconfs, typ) {
|
||||||
|
var ch = chconfs[typ];
|
||||||
|
if (!ch) {
|
||||||
|
delete chconfs[typ];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
11
package.json
11
package.json
|
@ -1,14 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "greenlock-manager-fs",
|
"name": "greenlock-manager-fs",
|
||||||
"version": "0.7.0",
|
"version": "3.0.0",
|
||||||
"description": "A simple file-based management strategy for Greenlock",
|
"description": "A simple file-based management strategy for Greenlock",
|
||||||
"main": "manager.js",
|
"main": "manager.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node tests"
|
"test": "node tests"
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"*.js",
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.coolaj86.com/coolaj86/greenlock-manager-fs.js.git"
|
"url": "https://git.rootprojects.org/root/greenlock-manager-fs.js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Greenlock",
|
"Greenlock",
|
||||||
|
@ -20,6 +24,7 @@
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@root/mkdirp": "^1.0.0",
|
"@root/mkdirp": "^1.0.0",
|
||||||
"safe-replace": "^1.1.0"
|
"safe-replace": "^1.1.0",
|
||||||
|
"greenlock-manager-test": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
52
test.js
52
test.js
|
@ -1,52 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var Manager = require('./');
|
|
||||||
var manager = Manager.create({
|
|
||||||
configFile: 'greenlock-manager-test.delete-me.json'
|
|
||||||
});
|
|
||||||
var domains = ['example.com', 'www.example.com'];
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
await manager.add({
|
|
||||||
subject: domains[0],
|
|
||||||
altnames: domains
|
|
||||||
});
|
|
||||||
|
|
||||||
await manager.find({}).then(function(results) {
|
|
||||||
if (!results.length) {
|
|
||||||
console.log(results);
|
|
||||||
throw new Error('should have found all managed sites');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await manager.find({ subject: 'www.example.com' }).then(function(results) {
|
|
||||||
if (results.length) {
|
|
||||||
console.log(results);
|
|
||||||
throw new Error(
|
|
||||||
"shouldn't find what doesn't exist, exactly, by subject"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await manager
|
|
||||||
.find({ altnames: ['www.example.com'] })
|
|
||||||
.then(function(results) {
|
|
||||||
if (!results.length) {
|
|
||||||
console.log(results);
|
|
||||||
throw new Error('should have found sites matching altname');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await manager.find({ altnames: ['*.example.com'] }).then(function(results) {
|
|
||||||
if (results.length) {
|
|
||||||
console.log(results);
|
|
||||||
throw new Error(
|
|
||||||
'should only find an exact (literal) wildcard match'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("PASS");
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Tester = require('greenlock-manager-test');
|
||||||
|
|
||||||
|
var Manager = require('../');
|
||||||
|
var config = {
|
||||||
|
configFile: 'greenlock-manager-test.delete-me.json'
|
||||||
|
};
|
||||||
|
|
||||||
|
Tester.test(Manager, config)
|
||||||
|
.then(function() {
|
||||||
|
console.log('PASS: Known-good test module passes');
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.error('Oops, you broke it. Here are the details:');
|
||||||
|
console.error(err.stack);
|
||||||
|
console.error();
|
||||||
|
console.error("That's all I know.");
|
||||||
|
});
|
Loading…
Reference in New Issue