v3.1.0: reduce scope of manager API

This commit is contained in:
AJ ONeal 2019-11-04 21:03:21 -07:00
parent 19b571f088
commit 30884601c6
9 changed files with 856 additions and 324 deletions

View File

@ -1,7 +1,7 @@
{ {
"bracketSpacing": true, "bracketSpacing": true,
"printWidth": 80, "printWidth": 80,
"singleQuote": true, "singleQuote": false,
"tabWidth": 4, "tabWidth": 4,
"trailingComma": "none", "trailingComma": "none",
"useTabs": false "useTabs": false

357
README.md
View File

@ -1,113 +1,246 @@
# [greenlock-manager-test.js](https://git.rootprojects.org/root/greenlock-manager-test.js) # [greenlock-manager-test.js](https://git.rootprojects.org/root/greenlock-manager-test.js)
A simple test suite for Greenlock manager plugins. A simple test suite for Greenlock v3 manager plugins.
# Greenlock Manager # Greenlock Manager
A greenlock manager is just a set of a few callbacks to keeps track of: A Greenlock Manager is responsible for tracking which domains
belong on a certificate, when they are scheduled for renewal,
and if they have been deleted.
- **Default settings** that apply to all sites such as It consists of two required functions:
- `subscriberEmail`
- `agreeToTerms`
- `store` (the account key and ssl certificate store)
- **Site settings** such as
- `subject` (ex: example.com)
- `altnames` (ex: example.com,www.example.com)
- `renewAt` (ex: '45d')
- `challenges` (plugins for 'http-01', 'dns-01', etc)
The **callbacks** are:
- `set({ subject, altnames, renewAt })` to save site details
- `find({ subject, altnames, renewBefore })` which returns a list of matching sites (perhaps all sites)
- `remove({ subject })` which marks a site as deleted
- `defaults()` which either **gets** or **sets** the global configs that apply to all sites
When do they get called? Well, whenever they need to.
# Some Terminology
- `subject` refers to the **primary domain** on an SSL certificate
- `altnames` refers to the list of **domain names** on the certificate (including the subject)
- `renewAt` is a pre-calculated value based on `expiresAt` or `issuedAt` on the certificate
Those are the only values you really have to worry about.
The rest you can make up for your own needs, or they're just opaque values you'll get from Greenlock.
# Do you want to build a plugin?
You can start _really_ simple: just make a file that exports a `create()` function:
## A great first, failing plugin:
`my-plugin.js`:
```js ```js
'use strict'; set({ subject, altnames, renewAt, deletedAt });
var MyManager = module.exports;
MyManager.create = function(options) {
console.log('The tests will make me stronger');
return {};
};
``` ```
## The test suite from heaven ```js
get({ servername });
You write your test file, run it,
and then you get a play-by-play of what to do.
``` ```
However, if you implement `find({ subject, servernames, renewBefore })` (optional),
you don't have to implement `get()`.
<details>
<summary>Usage Details</summary>
# How to use your plugin
The **Right Way**:
```bash
npm install --save greenlack
npx greenlock init --manager ./path-or-npm-name.js --manager-xxxx 'sets xxxx' --manager-yyyy 'set yyyy'
```
That creates a `.greenlockrc`, which is essentially the same as doing this:
```js
var Greenlock = require("greenlock");
var greenlock = Greenlock.create({
// ...
manager: "./path-or-npm-name.js",
xxxx: "sets xxxx",
yyyy: "sets yyyy",
packageRoot: __dirname
});
```
## Why no require?
Okay, so you **expect** it to look like this:
```js
var Greenlock = require("greenlock");
var greenlock = Greenlock.create({
// WRONG!!
manager: require("./path-or-npm-name.js").create({
someOptionYouWant: true
})
});
```
**NOPE**!
Greenlock is designed to so that the CLI tools, Web API, and JavaScript API
can all work interdepedently, indpendently.
Therefore the configuration has to go into serializable JSON rather than
executable JavaScript.
</details>
# Quick Start
If you want to write a manager,
the best way to start is by using one of the provided templates.
```bash
npm install --save-dev greenlock-manager-test npm install --save-dev greenlock-manager-test
npx greenlock-manager-init
``` ```
`test.js`: It will generate a bare bones manager that passes the tests,
(skipping all optional features), and a test file:
<details>
<summary>manager.js</summary>
```js ```js
'use strict'; "use strict";
var Manager = module.exports;
var db = {};
Manager.create = function(opts) {
var manager = {};
//
// REQUIRED (basic issuance)
//
// Get
manager.get = async function({ servername, wildname }) {
// Required: find the certificate with the subject of `servername`
// Optional (multi-domain certs support): find a certificate with `servername` as an altname
// Optional (wildcard support): find a certificate with `wildname` as an altname
// { subject, altnames, renewAt, deletedAt, challenges, ... }
return db[servername] || db[wildname];
};
// Set
manager.set = async function(opts) {
// { subject, altnames, renewAt, deletedAt }
// Required: updated `renewAt` and `deletedAt` for certificate matching `subject`
var site = db[opts.subject] || {};
db[opts.subject] = Object.assign(site, opts);
return null;
};
//
// Optional (Fully Automatic Renewal)
//
/*
manager.find = async function(opts) {
// { subject, servernames, altnames, renewBefore }
return [{ subject, altnames, renewAt, deletedAt }];
};
//*/
//
// Optional (Special Remove Functionality)
// The default behavior is to set `deletedAt`
//
/*
manager.remove = async function(opts) {
return mfs.remove(opts);
};
//*/
//
// Optional (special settings save)
// Implemented here because this module IS the fallback
//
/*
manager.defaults = async function(opts) {
if (opts) {
return setDefaults(opts);
}
return getDefaults();
};
//*/
//
// Optional (for common deps and/or async initialization)
//
/*
manager.init = async function(deps) {
manager.request = deps.request;
return null;
};
//*/
return manager;
var Tester = require('greenlock-manager-test');
var MyManager = require('./');
var myConfigOptions = {
someApiTokenForMyManager: 'xxx'
}; };
Tester.test(MyManager, myConfigOptions) ````
.then(function() { </details>
console.log('All Tests Passed'); <details>
<summary>manager.test.js</summary>
```js
"use strict";
var Tester = require("greenlock-manager-test");
var Manager = require("./manager.js");
var config = {
configFile: "greenlock-manager-test.delete-me.json"
};
Tester.test(Manager, config)
.then(function(features) {
console.info("PASS");
console.info();
console.info("Optional Feature Support:");
features.forEach(function(feature) {
console.info(feature.supported ? "✓ (YES)" : "✘ (NO) ", feature.description);
});
console.info();
}) })
.catch(function(err) { .catch(function(err) {
console.error('Oops... something bad happened:'); console.error("Oops, you broke it. Here are the details:");
console.error(err); console.error(err.stack);
console.error();
console.error("That's all I know.");
}); });
````
</details>
```bash
node manager.test.js
``` ```
You just follow the error messages and, which a little help from this README, ```txt
bam!, you get a working plugin. It's insane! PASS: get({ servername, wildname })
PASS: set({ subject })
# The lazy, hacky way. Optional Feature Support:
✘ (NO) Multiple Domains per Certificate
✘ (NO) Wildcard Certificates
✘ (NO) Fully Automatic Renewal
```
If you're going to publish a module, you should pass the full test suite. # Optional Features
If not, eh, you can be lazy. If you're publishing a module to the community,
you should implement the full test suite (and it's not that hard).
## Bare minimum... If you're only halfway through, you should note
which features are supported and which aren't.
At a bare minimum, you must implement `find()` to return an array of `{ subject, altnames }`.
For example:
```js ```js
function find(argsToIgnore) { find({ subject, servernames, renewBefore });
return Promise.resolve([ defaults({ subscriberEmail, agreeToTerms, challenges, store, ... });
{ subject: 'example.com', altnames: ['example.com', 'www.example.com'] } defaults(); // as getter
]);
}
``` ```
If that's absolutely all that you do, all of the other methods will be implemented around `greenlock-manager-fs`. - `find()` is used to get the full list of sites, for continuous fully automatic renewal.
- `defaults()` exists so that the global config can be saved in the same place as the per-site config.
- a proper `get()` should be able to search not just primary domains, but altnames as well.
Additionally, you're manager may need an init or a _real_ delete - rather than just using `set({ deletedAt })`:
```js
init({ request });
remove({ subject });
```
<details>
<summary>Full Implementation</summary>
# The Right Way™ # The Right Way™
@ -122,7 +255,7 @@ module.exports.create = function(options) {
manager.set = async function(siteConfig) { manager.set = async function(siteConfig) {
// You can see in the tests a sample of common values, // You can see in the tests a sample of common values,
// but you don't really need to worry about it. // but you don't really need to worry about it.
var subject = siteConfig; var subject = siteConfig.subject;
// Cherry pick what you like for indexing / search, and JSONify the rest // Cherry pick what you like for indexing / search, and JSONify the rest
return mergeOrCreateSite(subject, siteConfig); return mergeOrCreateSite(subject, siteConfig);
@ -130,38 +263,31 @@ module.exports.create = function(options) {
// find the things you've saved before // find the things you've saved before
manager.find = async function({ subject, altnames, renewBefore }) { manager.get = async function({ servername }) {
return getSiteByAltname(servername);
}
manager.find = async function({ subject, servernames, renewBefore }) {
var results = []; var results = [];
var gotten = {}; var gotten = {};
if (subject) { if (subject) {
var site = await getSiteBySubject(subject); var site = await getSiteBySubject(subject);
if (site) { if (site && site.subject === subject) {
results.push(site); return [site];
gotten[site.subject] = true;
} }
} }
if (altnames) { if (severnames) {
var sites = await getSiteByAltnames(subject); return await Promise.all(servernames.map(function (altname) {
sites.forEach(function() {}); var site = getSiteByAltname(subject);
if (site) { if (site && !gotten[site.subject]) {
if (!gotten[site.subject]) {
results.push(site);
gotten[site.subject] = true; gotten[site.subject] = true;
return site;
} }
} });
} }
if (subject || altnames) { return getSitesThatShouldBeRenewedBefore(renewBefore || Infinity);
return results;
}
if (renewBefore) {
return getSitesThatShouldBeRenewedBefore(renewBefore);
}
return getAllSites();
}; };
// delete a site config // delete a site config
@ -198,35 +324,4 @@ module.exports.create = function(options) {
}; };
``` ```
# How to use your plugin </details>
The **Right Way**:
```js
var Greenlock = require('greenlock');
var greenlock = Greenlock.create({
manager: '/absolute/path/to/manager'
someOptionYouWant: true,
});
```
## Why no require?
Okay, so you **expect** it to look like this:
```js
var Greenlock = require('greenlock');
var greenlock = Greenlock.create({
// WRONG!!
manager: require('./relative/path/to/manager').create({
someOptionYouWant: true
})
});
```
**NOPE**!
It just has to do with some plugin architecture decisions around making the configuration
serializable.
I may go back and add the other way, but this is how it is right now.

35
bin/init.js Normal file
View File

@ -0,0 +1,35 @@
"use strict";
var fs = require("fs");
var path = require("path");
function tmpl() {
var src = path.join(__dirname, "tmpl/manager.tmpl.js");
var dst = path.join(process.cwd(), "./manager.js");
try {
fs.accessSync(dst);
console.warn("skip 'manager.js': already exists");
return;
} catch (e) {
fs.writeFileSync(dst, fs.readFileSync(src, "utf8"), "utf8");
console.info("wrote 'manager.js'");
}
}
function tmplTest() {
var srcTest = path.join(__dirname, "tmpl/manager.test.tmpl.js");
var dstTest = path.join(process.cwd(), "./manager.test.js");
try {
fs.accessSync(dstTest);
console.warn("skip 'manager.test.js': already exists");
return;
} catch (e) {
fs.writeFileSync(dstTest, fs.readFileSync(srcTest, "utf8"), "utf8");
console.info("wrote 'manager.test.js'");
}
}
tmpl();
tmplTest();

View File

@ -0,0 +1,25 @@
"use strict";
var Tester = require("greenlock-manager-test");
var Manager = require("./manager.js");
var config = {
configFile: "greenlock-manager-test.delete-me.json"
};
Tester.test(Manager, config)
.then(function(features) {
console.info("PASS");
console.info();
console.info("Optional Feature Support:");
features.forEach(function(feature) {
console.info(feature.supported ? "✓ (YES)" : "✘ (NO) ", feature.description);
});
console.info();
})
.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.");
});

78
bin/tmpl/manager.tmpl.js Normal file
View File

@ -0,0 +1,78 @@
"use strict";
var Manager = module.exports;
var db = {};
Manager.create = function(opts) {
var manager = {};
//
// REQUIRED (basic issuance)
//
// Get
manager.get = async function({ servername, wildname }) {
// Required: find the certificate with the subject of `servername`
// Optional (multi-domain certs support): find a certificate with `servername` as an altname
// Optional (wildcard support): find a certificate with `wildname` as an altname
// { subject, altnames, renewAt, deletedAt, challenges, ... }
return db[servername] || db[wildname];
};
// Set
manager.set = async function(opts) {
// { subject, altnames, renewAt, deletedAt }
// Required: updated `renewAt` and `deletedAt` for certificate matching `subject`
var site = db[opts.subject] || {};
db[opts.subject] = Object.assign(site, opts);
return null;
};
//
// Optional (Fully Automatic Renewal)
//
/*
manager.find = async function(opts) {
// { subject, servernames, altnames, renewBefore }
return [{ subject, altnames, renewAt, deletedAt }];
};
//*/
//
// Optional (Special Remove Functionality)
// The default behavior is to set `deletedAt`
//
/*
manager.remove = async function(opts) {
return mfs.remove(opts);
};
//*/
//
// Optional (special settings save)
// Implemented here because this module IS the fallback
//
/*
manager.defaults = async function(opts) {
if (opts) {
return setDefaults(opts);
}
return getDefaults();
};
//*/
//
// Optional (for common deps and/or async initialization)
//
/*
manager.init = async function(deps) {
manager.request = deps.request;
return null;
};
//*/
return manager;
};

39
package-lock.json generated
View File

@ -1,13 +1,32 @@
{ {
"name": "greenlock-manager-test", "name": "greenlock-manager-test",
"version": "3.0.0", "version": "3.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@root/request": { "@root/mkdirp": {
"version": "1.4.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.1.tgz", "resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz",
"integrity": "sha512-2zSP1v9VhJ3gvm4oph0C4BYCoM3Sj84/Wx4iKdt0IbqbJzfON04EodBq5dsV65UxO/aHZciUBwY2GCZcHqaTYg==" "integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA=="
},
"@root/request": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.1.tgz",
"integrity": "sha512-2zSP1v9VhJ3gvm4oph0C4BYCoM3Sj84/Wx4iKdt0IbqbJzfON04EodBq5dsV65UxO/aHZciUBwY2GCZcHqaTYg=="
},
"greenlock-manager-fs": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-3.0.5.tgz",
"integrity": "sha512-r/q+tEFuDwklfzPfiGhcIrHuJxMrppC+EseESpu5f0DMokh+1iZVm9nGC/VE7/7GETdOYfEYhhQkmspsi8Gr/A==",
"requires": {
"@root/mkdirp": "^1.0.0",
"safe-replace": "^1.1.0"
}
},
"safe-replace": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz",
"integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw=="
}
} }
}
} }

View File

@ -1,28 +1,32 @@
{ {
"name": "greenlock-manager-test", "name": "greenlock-manager-test",
"version": "3.0.0", "version": "3.1.0",
"description": "A simple test suite for Greenlock manager plugins.", "description": "A simple test suite for Greenlock manager plugins.",
"main": "tester.js", "main": "tester.js",
"scripts": { "scripts": {
"test": "node tests" "test": "node tests"
}, },
"files": [ "bin": {
"*.js", "greenlock-manager-init": "bin/init.js"
"lib" },
], "files": [
"repository": { "*.js",
"type": "git", "bin",
"url": "https://git.rootprojects.org/root/greenlock-manager-test.js" "lib"
}, ],
"keywords": [ "repository": {
"Greenlock", "type": "git",
"manager", "url": "https://git.rootprojects.org/root/greenlock-manager-test.js"
"plugin" },
], "keywords": [
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "Greenlock",
"license": "MPL-2.0", "manager",
"dependencies": { "plugin"
"@root/request": "^1.4.1", ],
"greenlock-manager-fs": "^3.0.0" "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
} "license": "MPL-2.0",
"dependencies": {
"@root/request": "^1.4.1",
"greenlock-manager-fs": "^3.0.0"
}
} }

562
tester.js
View File

@ -1,165 +1,441 @@
'use strict'; "use strict";
var request = require('@root/request'); var request = require("@root/request");
// For most tests
var siteSubject = "xx.com";
var siteAltname = "www.foo.xx.com";
var siteWildname = "*.xx.com";
var siteMatch = "foo.xx.com";
var domains = [siteSubject, siteAltname, siteWildname];
// Similar, but non-matching subjects
var noExistWild = "*.foo.xx.com";
var noExistAlt = "bar.xx.com";
// For wildcard-as-subject test
var siteWildnameNet = "*.xx.net";
var siteMatchNet = "foo.xx.net";
var domains = ['example.com', 'www.example.com'];
module.exports.test = async function(pkg, config) { module.exports.test = async function(pkg, config) {
if ('function' !== typeof pkg.create) { if ("function" !== typeof pkg.create) {
throw new Error( throw new Error(
'must have a create function that accepts a single options object' "must have a create function that accepts a single options object"
); );
} }
var manager = pkg.create(config); var features = {
altnames: false,
wildcard: false,
renewal: false
};
var manager = pkg.create(config);
var initVal;
if (manager.init) { if (manager.init) {
await manager.init({ initVal = await manager.init({
request: request request: request
}); });
} else { if (!initVal && initVal !== null) {
console.warn( console.warn(
'WARN: should have an init(deps) function which returns a promise' "WARN: `init()` returned `undefined`, but should return `null`"
); );
} }
}
console.info("PASS: init(deps)");
await manager.set({ await manager.set({
subject: domains[0], subject: siteSubject,
altnames: domains altnames: domains
}); });
var site = await manager.get({
servername: siteSubject
// *.com is an invalid wildname
});
if (!site || site.subject !== siteSubject) {
throw new Error(
"set({ subject: '" +
siteSubject +
"'}), but could not `get()` or `find()` it"
);
}
await manager.find({}).then(function(results) { //
if (!results.length) { // Test for altname support
console.log(results); //
throw new Error('should have found all managed sites'); site = await get({
} servername: siteAltname,
}); wildname: untame(siteAltname)
console.log('PASS: set'); });
if (site) {
if (site.subject !== siteSubject) {
throw new Error("found incorrect site");
}
features.altnames = true;
} else {
console.warn("WARN: Does not support altnames.");
console.warn(
" (searched for %s but did not find site '%s')",
siteAltname,
domains.join(" ")
);
}
await manager.find({ subject: 'www.example.com' }).then(function(results) { //
if (results.length) { // Test for wildcard support
console.log(results); //
throw new Error( if (features.altnames) {
"shouldn't find what doesn't exist, exactly, by subject" // Set the wildcard as an altname
); site = await get({
} servername: siteMatch,
}); wildname: siteWildname
});
if (site) {
if (site.subject !== siteSubject) {
throw new Error(
"found %s when looking for %s",
site.subject,
siteSubject
);
}
features.wildcard = true;
} else {
console.warn("WARN: Does not support wildcard domains.");
console.warn(
" (searched for %s but did not find site %s)",
siteMatch,
siteSubject
);
}
}
// Set the wildcard as the subject
await manager.set({
subject: siteWildnameNet,
altnames: [siteWildnameNet]
});
site = await get({
servername: siteMatchNet,
wildname: siteWildnameNet
});
if (site) {
if (site.subject !== siteWildnameNet) {
throw new Error("found incorrect site");
}
features.wildcard = true;
} else {
if (features.wildcard) {
throw new Error(
"searched for wildcard subject " +
siteWildnameNet +
" but did not find it"
);
}
if (!features.altnames) {
console.warn(
"WARN: Does not support wildcard domains as certificate subjects."
);
console.warn(
" (searched for %s as %s but did not find site %s)",
siteMatchNet,
siteWildnameNet,
siteWildnameNet
);
}
}
await remove({ subject: siteWildnameNet });
await manager var wasSet = false;
.find({ altnames: ['www.example.com'] }) if (manager.find) {
.then(function(results) { await manager.find({}).then(function(results) {
if (!results.length) { if (!results.length) {
console.log(results); //console.error(results);
throw new Error('should have found sites matching altname'); throw new Error("should have found all managed sites");
} }
}); wasSet = results.some(function(site) {
return site.subject === siteSubject;
});
if (!wasSet) {
throw new Error("should have found " + siteSubject);
}
});
}
await manager.find({ altnames: ['*.example.com'] }).then(function(results) { if (manager.get) {
if (results.length) { await manager.get({ servername: siteSubject }).then(function(site) {
console.log(results); if (!site || site.subject !== siteSubject) {
throw new Error( throw new Error("should have found " + siteSubject);
'should only find an exact (literal) wildcard match' }
); wasSet = true;
} });
}); if (features.altnames) {
console.log('PASS: find'); wasSet = false;
await manager.get({ servername: siteAltname }).then(function(site) {
if (!site || site.subject !== siteSubject) {
throw new Error("should have found " + siteAltname);
}
});
await manager
.get({ servername: siteMatch, wildname: siteWildname })
.then(function(site) {
if (!site || site.subject !== siteSubject) {
throw new Error(
"did not find " +
siteMatch +
", which matches " +
siteWildname
);
}
wasSet = true;
});
}
console.info("PASS: get({ servername, wildname })");
} else {
console.info("[skip] get({ servername, wildname }) not implemented");
}
await manager.remove({ subject: '*.example.com' }).then(function(result) { if (wasSet) {
if (result) { console.info("PASS: set({ subject })");
throw new Error( } else {
'should not return prior object when deleting non-existing site' throw new Error("neither `get()` nor `find()` was implemented");
); }
}
});
await manager.remove({ subject: 'www.example.com' }).then(function(result) { if (manager.find) {
if (result) { await manager.find({ subject: siteAltname }).then(function(results) {
throw new Error( if (results.length) {
'should not return prior object when deleting non-existing site' console.error(results);
); throw new Error(
} "shouldn't find what doesn't exist, exactly, by subject"
}); );
}
});
await manager.remove({ subject: 'example.com' }).then(function(result) { await manager
if (!result || !result.subject || !result.altnames) { .find({ servernames: [siteAltname], altnames: [siteAltname] })
throw new Error('should return prior object when deleting site'); .then(function(results) {
} if (!results.length) {
}); console.error(results);
throw new Error("should have found sites matching altname");
}
});
console.info("PASS: find({ servernames, renewBefore })");
} else {
console.info(
"[skip] find({ servernames, renewBefore }) not implemented"
);
}
await manager await remove({ subject: noExistWild }).then(function(result) {
.find({ altnames: ['example.com', 'www.example.com'] }) if (result) {
.then(function(results) { console.error(siteWildname, result);
if (results.length) { throw new Error(
console.log(results); "should not return prior object when deleting non-existing wildcard domain: " +
throw new Error('should not find deleted sites'); noExistWild
} );
}); }
console.log('PASS: remove'); });
var originalInput = { await remove({ subject: noExistAlt }).then(function(result) {
serverKeyType: 'RSA-2048', if (result) {
accountKeyType: 'P-256', throw new Error(
subscriberEmail: 'jon@example.com', "should not return prior object when deleting non-existing site: " +
agreeToTerms: true, noExistAlt
store: { module: '/path/to/store-module', foo: 'foo' }, );
challenges: { }
'http-01': { module: '/path/to/http-01-module', bar: 'bar' }, });
'dns-01': { module: '/path/to/dns-01-module', baz: 'baz' },
'tls-alpn-01': {
module: '/path/to/tls-alpn-01-module',
qux: 'quux'
}
},
customerEmail: 'jane@example.com'
};
//var backup = JSON.parse(JSON.stringify(originalInput));
var configUpdate = {
renewOffset: '45d',
renewStagger: '12h',
subscriberEmail: 'pat@example.com'
};
var internalConfig; await remove({ subject: siteWildname }).then(function(result) {
await manager.defaults().then(function(result) { if (result) {
internalConfig = result; throw new Error("should not delete by wildname: " + siteWildname);
if (!result) { }
throw new Error( });
'should at least return an empty object, perhaps one with some defaults set'
);
}
});
await manager.defaults(originalInput).then(function(result) { await remove({ subject: siteAltname }).then(function(result) {
// can't say much... what _should_ this return? if (result) {
// probably nothing? or maybe the full config object? throw new Error("should not delete by altname: " + siteAltname);
if (internalConfig === result) { }
console.warn( });
'WARN: should return a new copy, not the same internal object'
);
}
if (originalInput === result) {
console.warn(
'WARN: should probably return a copy, not the original input'
);
}
});
await manager.defaults().then(function(result) { await remove({ subject: siteSubject }).then(function(result) {
if (originalInput === result) { if (!result || !result.subject || !result.altnames) {
console.warn('WARN: should probably return a copy, not the prior input'); throw new Error("should return prior object when deleting site");
} }
}); });
if (!manager.remove) {
console.info(
"[skip] remove() not implemented - using set({ deletedAt }) instead"
);
}
await manager.defaults(configUpdate).then(function() { await manager.set({ subject: siteSubject, altnames: domains.slice(0, 2) });
if (originalInput.renewOffset) { if (manager.find) {
console.warn('WARN: should probably modify the prior input'); await manager
} .find({ servernames: [noExistWild], altnames: [noExistWild] })
}); .then(function(results) {
if (results.length) {
console.error(results);
throw new Error(
"should only find an exact (literal) wildcard match"
);
}
});
}
await remove({ subject: siteSubject }).then(function(result) {
if (!result || !result.subject || !result.altnames) {
console.error(
"Could not find",
siteSubject,
"to delete it:",
result
);
throw new Error("should return prior object when deleting site");
}
});
await manager.defaults().then(function(result) { if (manager.find) {
if (!result.subscriberEmail || !result.renewOffset) { await manager
throw new Error('should merge config values together'); .find({ servernames: domains, altnames: domains })
} .then(function(results) {
}); if (results.length) {
console.error(results);
throw new Error("should not find() deleted sites");
}
});
} else {
await get({ servername: siteAltname }).then(function(result) {
if (result) {
console.error(result);
throw new Error("should not get() deleted sites");
}
});
}
console.info("PASS: remove({ subject })");
console.log('PASS: defaults'); var originalInput = {
serverKeyType: "RSA-2048",
accountKeyType: "P-256",
subscriberEmail: "jon@example.com",
agreeToTerms: true,
store: { module: "/path/to/store-module", foo: "foo" },
challenges: {
"http-01": { module: "/path/to/http-01-module", bar: "bar" },
"dns-01": { module: "/path/to/dns-01-module", baz: "baz" },
"tls-alpn-01": {
module: "/path/to/tls-alpn-01-module",
qux: "quux"
}
},
customerEmail: "jane@example.com"
};
//var backup = JSON.parse(JSON.stringify(originalInput));
var configUpdate = {
renewOffset: "45d",
renewStagger: "12h",
subscriberEmail: "pat@example.com"
};
var internalConfig;
if (manager.defaults) {
await manager.defaults().then(function(result) {
internalConfig = result;
if (!result) {
throw new Error(
"should at least return an empty object, perhaps one with some defaults set"
);
}
});
await manager.defaults(originalInput).then(function(result) {
// can't say much... what _should_ this return?
// probably nothing? or maybe the full config object?
if (internalConfig === result) {
console.warn(
"WARN: should return a new copy, not the same internal object"
);
}
if (originalInput === result) {
console.warn(
"WARN: should probably return a copy, not the original input"
);
}
});
await manager.defaults().then(function(result) {
if (originalInput === result) {
console.warn(
"WARN: should probably return a copy, not the prior input"
);
}
});
await manager.defaults(configUpdate).then(function() {
if (originalInput.renewOffset) {
console.warn("WARN: should probably modify the prior input");
}
});
console.info("PASS: defaults(conf)");
await manager.defaults().then(function(result) {
if (!result.subscriberEmail || !result.renewOffset) {
throw new Error("should merge config values together");
}
});
console.info("PASS: defaults()");
} else {
console.info(
"[skip] defaults({ store, challenges, ... }) not implemented"
);
}
features.renewal = !!manager.find;
var featureNames = {
altnames: "Multiple Domains per Certificate",
wildcard:
"Wildcard Certificates" +
(features.altnames ? "" : " (subject only)"),
renewal: "Fully Automatic Renewal"
};
return Object.keys(features).map(function(k) {
return {
name: k,
description: featureNames[k],
supported: features[k]
};
});
function get(opts) {
if (manager.get) {
opts.servername = opts.servername || opts.subject;
delete opts.subject;
return manager.get(opts);
} else {
return manager.find(opts);
}
}
function remove(opts) {
if (manager.remove) {
return manager.remove(opts);
} else {
return get(opts).then(function(site) {
// get matches servername, but remove should only match subject
if (site && site.subject === opts.servername) {
site.deletedAt = Date.now();
return manager.set(site).then(function() {
return site;
});
}
return null;
});
}
}
function untame(str) {
return (
"*." +
str
.split(".")
.slice(1)
.join(".")
);
}
}; };

View File

@ -1,19 +1,19 @@
'use strict'; "use strict";
var Tester = require('../'); var Tester = require("../");
var Manager = require('greenlock-manager-fs'); var Manager = require("greenlock-manager-fs");
var config = { var config = {
configFile: 'greenlock-manager-test.delete-me.json' configFile: "greenlock-manager-test.delete-me.json"
}; };
Tester.test(Manager, config) Tester.test(Manager, config)
.then(function() { .then(function() {
console.log('PASS: Known-good test module passes'); console.log("PASS: Known-good test module passes");
}) })
.catch(function(err) { .catch(function(err) {
console.error('Oops, you broke it. Here are the details:'); console.error("Oops, you broke it. Here are the details:");
console.error(err.stack); console.error(err.stack);
console.error(); console.error();
console.error("That's all I know."); console.error("That's all I know.");
}); });