v3.1.0: reduce scope of manager API
This commit is contained in:
parent
19b571f088
commit
30884601c6
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 80,
|
||||
"singleQuote": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false
|
||||
|
|
357
README.md
357
README.md
|
@ -1,113 +1,246 @@
|
|||
# [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
|
||||
|
||||
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
|
||||
- `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`:
|
||||
It consists of two required functions:
|
||||
|
||||
```js
|
||||
'use strict';
|
||||
|
||||
var MyManager = module.exports;
|
||||
|
||||
MyManager.create = function(options) {
|
||||
console.log('The tests will make me stronger');
|
||||
return {};
|
||||
};
|
||||
set({ subject, altnames, renewAt, deletedAt });
|
||||
```
|
||||
|
||||
## The test suite from heaven
|
||||
|
||||
You write your test file, run it,
|
||||
and then you get a play-by-play of what to do.
|
||||
|
||||
```js
|
||||
get({ servername });
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
'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() {
|
||||
console.log('All Tests Passed');
|
||||
````
|
||||
</details>
|
||||
<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) {
|
||||
console.error('Oops... something bad happened:');
|
||||
console.error(err);
|
||||
console.error("Oops, you broke it. Here are the details:");
|
||||
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,
|
||||
bam!, you get a working plugin. It's insane!
|
||||
```txt
|
||||
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...
|
||||
|
||||
At a bare minimum, you must implement `find()` to return an array of `{ subject, altnames }`.
|
||||
|
||||
For example:
|
||||
If you're only halfway through, you should note
|
||||
which features are supported and which aren't.
|
||||
|
||||
```js
|
||||
function find(argsToIgnore) {
|
||||
return Promise.resolve([
|
||||
{ subject: 'example.com', altnames: ['example.com', 'www.example.com'] }
|
||||
]);
|
||||
}
|
||||
find({ subject, servernames, renewBefore });
|
||||
defaults({ subscriberEmail, agreeToTerms, challenges, store, ... });
|
||||
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™
|
||||
|
||||
|
@ -122,7 +255,7 @@ module.exports.create = function(options) {
|
|||
manager.set = async function(siteConfig) {
|
||||
// You can see in the tests a sample of common values,
|
||||
// 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
|
||||
return mergeOrCreateSite(subject, siteConfig);
|
||||
|
@ -130,38 +263,31 @@ module.exports.create = function(options) {
|
|||
|
||||
// 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 gotten = {};
|
||||
|
||||
if (subject) {
|
||||
var site = await getSiteBySubject(subject);
|
||||
if (site) {
|
||||
results.push(site);
|
||||
gotten[site.subject] = true;
|
||||
if (site && site.subject === subject) {
|
||||
return [site];
|
||||
}
|
||||
}
|
||||
|
||||
if (altnames) {
|
||||
var sites = await getSiteByAltnames(subject);
|
||||
sites.forEach(function() {});
|
||||
if (site) {
|
||||
if (!gotten[site.subject]) {
|
||||
results.push(site);
|
||||
if (severnames) {
|
||||
return await Promise.all(servernames.map(function (altname) {
|
||||
var site = getSiteByAltname(subject);
|
||||
if (site && !gotten[site.subject]) {
|
||||
gotten[site.subject] = true;
|
||||
return site;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (subject || altnames) {
|
||||
return results;
|
||||
}
|
||||
|
||||
if (renewBefore) {
|
||||
return getSitesThatShouldBeRenewedBefore(renewBefore);
|
||||
}
|
||||
|
||||
return getAllSites();
|
||||
return getSitesThatShouldBeRenewedBefore(renewBefore || Infinity);
|
||||
};
|
||||
|
||||
// delete a site config
|
||||
|
@ -198,35 +324,4 @@ module.exports.create = function(options) {
|
|||
};
|
||||
```
|
||||
|
||||
# How to use your plugin
|
||||
|
||||
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.
|
||||
</details>
|
||||
|
|
|
@ -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();
|
|
@ -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.");
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -1,13 +1,32 @@
|
|||
{
|
||||
"name": "greenlock-manager-test",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@root/request": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.1.tgz",
|
||||
"integrity": "sha512-2zSP1v9VhJ3gvm4oph0C4BYCoM3Sj84/Wx4iKdt0IbqbJzfON04EodBq5dsV65UxO/aHZciUBwY2GCZcHqaTYg=="
|
||||
"name": "greenlock-manager-test",
|
||||
"version": "3.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@root/mkdirp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz",
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
56
package.json
56
package.json
|
@ -1,28 +1,32 @@
|
|||
{
|
||||
"name": "greenlock-manager-test",
|
||||
"version": "3.0.0",
|
||||
"description": "A simple test suite for Greenlock manager plugins.",
|
||||
"main": "tester.js",
|
||||
"scripts": {
|
||||
"test": "node tests"
|
||||
},
|
||||
"files": [
|
||||
"*.js",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.rootprojects.org/root/greenlock-manager-test.js"
|
||||
},
|
||||
"keywords": [
|
||||
"Greenlock",
|
||||
"manager",
|
||||
"plugin"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
"name": "greenlock-manager-test",
|
||||
"version": "3.1.0",
|
||||
"description": "A simple test suite for Greenlock manager plugins.",
|
||||
"main": "tester.js",
|
||||
"scripts": {
|
||||
"test": "node tests"
|
||||
},
|
||||
"bin": {
|
||||
"greenlock-manager-init": "bin/init.js"
|
||||
},
|
||||
"files": [
|
||||
"*.js",
|
||||
"bin",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.rootprojects.org/root/greenlock-manager-test.js"
|
||||
},
|
||||
"keywords": [
|
||||
"Greenlock",
|
||||
"manager",
|
||||
"plugin"
|
||||
],
|
||||
"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
562
tester.js
|
@ -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) {
|
||||
if ('function' !== typeof pkg.create) {
|
||||
throw new Error(
|
||||
'must have a create function that accepts a single options object'
|
||||
);
|
||||
}
|
||||
if ("function" !== typeof pkg.create) {
|
||||
throw new Error(
|
||||
"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) {
|
||||
await manager.init({
|
||||
request: request
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
'WARN: should have an init(deps) function which returns a promise'
|
||||
);
|
||||
}
|
||||
if (manager.init) {
|
||||
initVal = await manager.init({
|
||||
request: request
|
||||
});
|
||||
if (!initVal && initVal !== null) {
|
||||
console.warn(
|
||||
"WARN: `init()` returned `undefined`, but should return `null`"
|
||||
);
|
||||
}
|
||||
}
|
||||
console.info("PASS: init(deps)");
|
||||
|
||||
await manager.set({
|
||||
subject: domains[0],
|
||||
altnames: domains
|
||||
});
|
||||
await manager.set({
|
||||
subject: siteSubject,
|
||||
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) {
|
||||
console.log(results);
|
||||
throw new Error('should have found all managed sites');
|
||||
}
|
||||
});
|
||||
console.log('PASS: set');
|
||||
//
|
||||
// Test for altname support
|
||||
//
|
||||
site = await get({
|
||||
servername: siteAltname,
|
||||
wildname: untame(siteAltname)
|
||||
});
|
||||
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) {
|
||||
console.log(results);
|
||||
throw new Error(
|
||||
"shouldn't find what doesn't exist, exactly, by subject"
|
||||
);
|
||||
}
|
||||
});
|
||||
//
|
||||
// Test for wildcard support
|
||||
//
|
||||
if (features.altnames) {
|
||||
// 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
|
||||
.find({ altnames: ['www.example.com'] })
|
||||
.then(function(results) {
|
||||
if (!results.length) {
|
||||
console.log(results);
|
||||
throw new Error('should have found sites matching altname');
|
||||
}
|
||||
});
|
||||
var wasSet = false;
|
||||
if (manager.find) {
|
||||
await manager.find({}).then(function(results) {
|
||||
if (!results.length) {
|
||||
//console.error(results);
|
||||
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 (results.length) {
|
||||
console.log(results);
|
||||
throw new Error(
|
||||
'should only find an exact (literal) wildcard match'
|
||||
);
|
||||
}
|
||||
});
|
||||
console.log('PASS: find');
|
||||
if (manager.get) {
|
||||
await manager.get({ servername: siteSubject }).then(function(site) {
|
||||
if (!site || site.subject !== siteSubject) {
|
||||
throw new Error("should have found " + siteSubject);
|
||||
}
|
||||
wasSet = true;
|
||||
});
|
||||
if (features.altnames) {
|
||||
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 (result) {
|
||||
throw new Error(
|
||||
'should not return prior object when deleting non-existing site'
|
||||
);
|
||||
}
|
||||
});
|
||||
if (wasSet) {
|
||||
console.info("PASS: set({ subject })");
|
||||
} else {
|
||||
throw new Error("neither `get()` nor `find()` was implemented");
|
||||
}
|
||||
|
||||
await manager.remove({ subject: 'www.example.com' }).then(function(result) {
|
||||
if (result) {
|
||||
throw new Error(
|
||||
'should not return prior object when deleting non-existing site'
|
||||
);
|
||||
}
|
||||
});
|
||||
if (manager.find) {
|
||||
await manager.find({ subject: siteAltname }).then(function(results) {
|
||||
if (results.length) {
|
||||
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) {
|
||||
if (!result || !result.subject || !result.altnames) {
|
||||
throw new Error('should return prior object when deleting site');
|
||||
}
|
||||
});
|
||||
await manager
|
||||
.find({ servernames: [siteAltname], altnames: [siteAltname] })
|
||||
.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
|
||||
.find({ altnames: ['example.com', 'www.example.com'] })
|
||||
.then(function(results) {
|
||||
if (results.length) {
|
||||
console.log(results);
|
||||
throw new Error('should not find deleted sites');
|
||||
}
|
||||
});
|
||||
console.log('PASS: remove');
|
||||
await remove({ subject: noExistWild }).then(function(result) {
|
||||
if (result) {
|
||||
console.error(siteWildname, result);
|
||||
throw new Error(
|
||||
"should not return prior object when deleting non-existing wildcard domain: " +
|
||||
noExistWild
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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'
|
||||
};
|
||||
await remove({ subject: noExistAlt }).then(function(result) {
|
||||
if (result) {
|
||||
throw new Error(
|
||||
"should not return prior object when deleting non-existing site: " +
|
||||
noExistAlt
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var internalConfig;
|
||||
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 remove({ subject: siteWildname }).then(function(result) {
|
||||
if (result) {
|
||||
throw new Error("should not delete by wildname: " + siteWildname);
|
||||
}
|
||||
});
|
||||
|
||||
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 remove({ subject: siteAltname }).then(function(result) {
|
||||
if (result) {
|
||||
throw new Error("should not delete by altname: " + siteAltname);
|
||||
}
|
||||
});
|
||||
|
||||
await manager.defaults().then(function(result) {
|
||||
if (originalInput === result) {
|
||||
console.warn('WARN: should probably return a copy, not the prior input');
|
||||
}
|
||||
});
|
||||
await remove({ subject: siteSubject }).then(function(result) {
|
||||
if (!result || !result.subject || !result.altnames) {
|
||||
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() {
|
||||
if (originalInput.renewOffset) {
|
||||
console.warn('WARN: should probably modify the prior input');
|
||||
}
|
||||
});
|
||||
await manager.set({ subject: siteSubject, altnames: domains.slice(0, 2) });
|
||||
if (manager.find) {
|
||||
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 (!result.subscriberEmail || !result.renewOffset) {
|
||||
throw new Error('should merge config values together');
|
||||
}
|
||||
});
|
||||
if (manager.find) {
|
||||
await manager
|
||||
.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(".")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
configFile: 'greenlock-manager-test.delete-me.json'
|
||||
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.");
|
||||
});
|
||||
.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