mirror of
				https://git.coolaj86.com/coolaj86/greenlock-manager-fs.js.git
				synced 2025-11-04 02:22:48 +00:00 
			
		
		
		
	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';
 | 
			
		||||
 | 
			
		||||
var Manage = module.exports;
 | 
			
		||||
var doctor = {};
 | 
			
		||||
 | 
			
		||||
var sfs = require('safe-replace').create({ tmp: 'tmp', bak: 'bak' });
 | 
			
		||||
var promisify = require('util').promisify;
 | 
			
		||||
var fs = require('fs');
 | 
			
		||||
@ -11,6 +13,15 @@ var homedir = require('os').homedir();
 | 
			
		||||
var path = require('path');
 | 
			
		||||
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) {
 | 
			
		||||
	if (!CONF) {
 | 
			
		||||
		CONF = {};
 | 
			
		||||
@ -25,264 +36,105 @@ Manage.create = function(CONF) {
 | 
			
		||||
 | 
			
		||||
	manage._txPromise = Promise.resolve();
 | 
			
		||||
 | 
			
		||||
	manage.defaults = manage.config = function(conf) {
 | 
			
		||||
		// get / set default site settings such as
 | 
			
		||||
		// subscriberEmail, store, challenges, renewOffset, renewStagger
 | 
			
		||||
		return Manage._getLatest(manage, CONF).then(function(config) {
 | 
			
		||||
	// Note: all of these top-level methods are effectively mutexed
 | 
			
		||||
	// You cannot call them from each other or they will deadlock
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
				conf = JSON.parse(JSON.stringify(config));
 | 
			
		||||
				delete conf.sites;
 | 
			
		||||
				conf = JSON.parse(JSON.stringify(config.defaults));
 | 
			
		||||
				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'
 | 
			
		||||
						);
 | 
			
		||||
					}
 | 
			
		||||
				})
 | 
			
		||||
			) {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// act as a setter
 | 
			
		||||
			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];
 | 
			
		||||
				config.defaults[k] = conf[k];
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			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;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	manage.find = function(args) {
 | 
			
		||||
		return _find(args).then(function(existing) {
 | 
			
		||||
			if (!CONF.find) {
 | 
			
		||||
				return existing;
 | 
			
		||||
			}
 | 
			
		||||
	manage.set = async function(args) {
 | 
			
		||||
		manage._txPromise = manage._txPromise.then(async function() {
 | 
			
		||||
			var config = await Manage._getLatest(manage, CONF);
 | 
			
		||||
 | 
			
		||||
			return Promise.resolve(CONF.find(args)).then(function(results) {
 | 
			
		||||
				// 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);
 | 
			
		||||
					}
 | 
			
		||||
			manage._merge(config, config.sites[args.subject], args);
 | 
			
		||||
 | 
			
		||||
					existing.some(function(_older) {
 | 
			
		||||
						if (subject !== (_older.subject || '').toLowerCase()) {
 | 
			
		||||
							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;
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
			await manage._save(config);
 | 
			
		||||
			return JSON.parse(JSON.stringify(config.sites[args.subject]));
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		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) {
 | 
			
		||||
		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
 | 
			
		||||
			//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 || Infinity;
 | 
			
		||||
			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 nothing to match, return everything
 | 
			
		||||
			var nameKeys = ['subject', 'altnames'];
 | 
			
		||||
			var matchAll = !nameKeys.some(function(k) {
 | 
			
		||||
				return k in args;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			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) {
 | 
			
		||||
					var site = doctor.site(config.sites, subject);
 | 
			
		||||
					var site = config.sites[subject];
 | 
			
		||||
					if (site.deletedAt) {
 | 
			
		||||
						return false;
 | 
			
		||||
					}
 | 
			
		||||
@ -309,172 +161,163 @@ Manage.create = function(CONF) {
 | 
			
		||||
					});
 | 
			
		||||
				})
 | 
			
		||||
				.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 doctor.site(config.sites, name);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			return sites;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return manage._txPromise;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	manage.notify = CONF.notify || _notify;
 | 
			
		||||
	function _notify(ev, args) {
 | 
			
		||||
		if (!args) {
 | 
			
		||||
			args = ev;
 | 
			
		||||
			ev = args.event;
 | 
			
		||||
			delete args.event;
 | 
			
		||||
		}
 | 
			
		||||
	function _mergeFind(config, ours, theirs) {
 | 
			
		||||
		theirs.forEach(function(_newer) {
 | 
			
		||||
			ours.some(function(_older) {
 | 
			
		||||
				if (_newer.subject !== _older.subject) {
 | 
			
		||||
					return false;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
		// TODO define message types
 | 
			
		||||
		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:',
 | 
			
		||||
					(' ' + (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(' ') + ')'
 | 
			
		||||
				);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
				// BE SURE TO SET THIS UNDEFINED AFTERWARDS
 | 
			
		||||
				_older._exists = true;
 | 
			
		||||
 | 
			
		||||
	manage.update = function(args) {
 | 
			
		||||
		manage._txPromise = manage._txPromise.then(function() {
 | 
			
		||||
			return Manage._getLatest(manage, CONF).then(function(config) {
 | 
			
		||||
				var site = config.sites[args.subject];
 | 
			
		||||
				//site.issuedAt = args.issuedAt;
 | 
			
		||||
				//site.expiresAt = args.expiresAt;
 | 
			
		||||
				site.renewAt = args.renewAt;
 | 
			
		||||
				return manage._save(config);
 | 
			
		||||
				manage._merge(config, _older, _newer);
 | 
			
		||||
				_newer = config.sites[_older.subject];
 | 
			
		||||
 | 
			
		||||
				// handled the (only) match
 | 
			
		||||
				return true;
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// 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;
 | 
			
		||||
	};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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, CONF).then(function(config) {
 | 
			
		||||
				var site = config.sites[args.subject];
 | 
			
		||||
				if (!site) {
 | 
			
		||||
					return {};
 | 
			
		||||
				}
 | 
			
		||||
				site.deletedAt = Date.now();
 | 
			
		||||
 | 
			
		||||
				return JSON.parse(JSON.stringify(site));
 | 
			
		||||
			});
 | 
			
		||||
		manage._txPromise = manage._txPromise.then(async function() {
 | 
			
		||||
			var config = await Manage._getLatest(manage, CONF);
 | 
			
		||||
			var site = config.sites[args.subject];
 | 
			
		||||
			if (!site) {
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
			site.deletedAt = Date.now();
 | 
			
		||||
			await manage._save(config);
 | 
			
		||||
			return JSON.parse(JSON.stringify(site));
 | 
			
		||||
		});
 | 
			
		||||
		return manage._txPromise;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	manage._lastStat = {
 | 
			
		||||
		size: 0,
 | 
			
		||||
		mtimeMs: 0
 | 
			
		||||
	};
 | 
			
		||||
	manage._config = {};
 | 
			
		||||
	// (wrong type #1) specifically the wrong type (null)
 | 
			
		||||
	manage._lastStat = { size: null, mtimeMs: null };
 | 
			
		||||
 | 
			
		||||
	manage._save = function(config) {
 | 
			
		||||
		return mkdirp(path.dirname(CONF.configFile)).then(function() {
 | 
			
		||||
			return sfs
 | 
			
		||||
				.writeFileAsync(
 | 
			
		||||
					CONF.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(CONF.configFile, parseInt('0600', 8))
 | 
			
		||||
						.catch(function() {
 | 
			
		||||
							/*ignore for Windows */
 | 
			
		||||
						})
 | 
			
		||||
						.then(function() {
 | 
			
		||||
							return statFile(CONF.configFile).then(function(
 | 
			
		||||
								stat
 | 
			
		||||
							) {
 | 
			
		||||
								manage._lastStat.size = stat.size;
 | 
			
		||||
								manage._lastStat.mtimeMs = stat.mtimeMs;
 | 
			
		||||
							});
 | 
			
		||||
						});
 | 
			
		||||
				});
 | 
			
		||||
		});
 | 
			
		||||
	manage._save = async function(config) {
 | 
			
		||||
		await mkdirp(path.dirname(CONF.configFile));
 | 
			
		||||
		// pretty-print the config file
 | 
			
		||||
		var data = JSON.stringify(config, null, 2);
 | 
			
		||||
		await sfs.writeFileAsync(CONF.configFile, data, 'utf8');
 | 
			
		||||
 | 
			
		||||
		// this file may contain secrets, so keep it safe
 | 
			
		||||
		return chmodFile(CONF.configFile, parseInt('0600', 8))
 | 
			
		||||
			.catch(function() {
 | 
			
		||||
				/*ignore for Windows */
 | 
			
		||||
			})
 | 
			
		||||
			.then(async function() {
 | 
			
		||||
				var stat = await statFile(CONF.configFile);
 | 
			
		||||
				manage._lastStat.size = stat.size;
 | 
			
		||||
				manage._lastStat.mtimeMs = stat.mtimeMs;
 | 
			
		||||
			});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	manage.init = async function(deps) {
 | 
			
		||||
		var request = deps.request;
 | 
			
		||||
		// how nice...
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	return manage;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Manage._getLatest = function(mng, CONF) {
 | 
			
		||||
Manage._getLatest = function(MNG, CONF) {
 | 
			
		||||
	return statFile(CONF.configFile)
 | 
			
		||||
		.catch(function(err) {
 | 
			
		||||
			if ('ENOENT' === err.code) {
 | 
			
		||||
				return {
 | 
			
		||||
					size: 0,
 | 
			
		||||
					mtimeMs: 0
 | 
			
		||||
				};
 | 
			
		||||
		.catch(async function(err) {
 | 
			
		||||
			if ('ENOENT' !== err.code) {
 | 
			
		||||
				err.context = 'manager_read';
 | 
			
		||||
				throw err;
 | 
			
		||||
			}
 | 
			
		||||
			err.context = 'manager_read';
 | 
			
		||||
			throw err;
 | 
			
		||||
			await MNG._save(doctor.config());
 | 
			
		||||
			// (wrong type #2) specifically the wrong type (bool)
 | 
			
		||||
			return { size: false, mtimeMs: false };
 | 
			
		||||
		})
 | 
			
		||||
		.then(function(stat) {
 | 
			
		||||
		.then(async function(stat) {
 | 
			
		||||
			if (
 | 
			
		||||
				stat.size === mng._lastStat.size &&
 | 
			
		||||
				stat.mtimeMs === mng._lastStat.mtimeMs
 | 
			
		||||
				stat.size === MNG._lastStat.size &&
 | 
			
		||||
				stat.mtimeMs === MNG._lastStat.mtimeMs
 | 
			
		||||
			) {
 | 
			
		||||
				return mng._config;
 | 
			
		||||
				return MNG._config;
 | 
			
		||||
			}
 | 
			
		||||
			return readFile(CONF.configFile, 'utf8').then(function(data) {
 | 
			
		||||
				mng._lastStat = stat;
 | 
			
		||||
				mng._config = JSON.parse(data);
 | 
			
		||||
				return mng._config;
 | 
			
		||||
			});
 | 
			
		||||
			var data = await readFile(CONF.configFile, 'utf8');
 | 
			
		||||
			MNG._lastStat = stat;
 | 
			
		||||
			MNG._config = JSON.parse(data);
 | 
			
		||||
			return doctor.config(MNG._config);
 | 
			
		||||
		});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
var doctor = {};
 | 
			
		||||
// 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) {
 | 
			
		||||
	var site = sconfs[subject];
 | 
			
		||||
	if (!site) {
 | 
			
		||||
@ -482,8 +325,9 @@ doctor.site = function(sconfs, subject) {
 | 
			
		||||
		site = {};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO notify on any changes
 | 
			
		||||
	if ('string' !== typeof site.subject) {
 | 
			
		||||
		console.warn('warning: deleted malformed site from config file:');
 | 
			
		||||
		console.warn(JSON.stringify(site));
 | 
			
		||||
		delete sconfs[subject];
 | 
			
		||||
		site.subject = 'greenlock-error.example.com';
 | 
			
		||||
	}
 | 
			
		||||
@ -496,3 +340,46 @@ doctor.site = function(sconfs, subject) {
 | 
			
		||||
 | 
			
		||||
	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",
 | 
			
		||||
	"version": "0.7.0",
 | 
			
		||||
	"version": "3.0.0",
 | 
			
		||||
	"description": "A simple file-based management strategy for Greenlock",
 | 
			
		||||
	"main": "manager.js",
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"test": "node tests"
 | 
			
		||||
	},
 | 
			
		||||
	"files": [
 | 
			
		||||
		"*.js",
 | 
			
		||||
		"lib"
 | 
			
		||||
	],
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"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": [
 | 
			
		||||
		"Greenlock",
 | 
			
		||||
@ -20,6 +24,7 @@
 | 
			
		||||
	"license": "MPL-2.0",
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@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();
 | 
			
		||||
							
								
								
									
										19
									
								
								tests/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								tests/index.js
									
									
									
									
									
										Normal file
									
								
							@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user