diff --git a/MIGRATION_GUIDE_V2_V3.md b/MIGRATION_GUIDE_V2_V3.md index c3a92f8..08ca340 100644 --- a/MIGRATION_GUIDE_V2_V3.md +++ b/MIGRATION_GUIDE_V2_V3.md @@ -6,23 +6,48 @@ All options described for `Greenlock.create({...})` also apply to the Greenlock # Overview of Major Differences -## Greenlock JavaScript API greatly reduced +- Reduced API +- No code in the config + - (config is completely serializable) +- Manager callbacks replace `approveDomains` +- Greenlock Express does more, with less config + - cluster is supported out-of-the-box + - high-performance + - scalable +- ACME challenges are simplified + - init + - zones (dns-01) + - set + - get + - remove +- Store callbacks are simplified + - accounts + - checkKeypairs + - certificates + - checkKeypairs + - check + - set + +# Greenlock JavaScript API greatly reduced Whereas before there were many different methods with nuance differences, now there's just `create`, `get`, `renew`, and sometimes `add` (). - - Greenlock.create({ maintainerEmail, packageAgent, notify }) - - Greenlock.get({ servername, wildname, duplicate, force }) - - (just a convenience wrapper around renew) - - Greenlock.renew({ subject, altnames, issuedBefore, expiresAfter }) - - (retrieves, issues, renews, all-in-one) - - _optional_ Greenlock.add({ subject, altnames, subscriberEmail }) - - (partially replaces `approveDomains`) - - `domains` was often ambiguous and confusing, it has been replaced by: - - `subject` refers to the subject of a certificate - the primary domain - - `altnames` refers to the domains in the SAN (Subject Alternative Names) section of the certificate - - `servername` refers to the TLS (SSL) SNI (Server Name Indication) request for a cetificate - - `wildname` refers to the wildcard version of the servername (ex: `www.example.com => *.example.com`) +- Greenlock.create({ maintainerEmail, packageAgent, notify }) +- Greenlock.get({ servername, wildname, duplicate, force }) + - (just a convenience wrapper around renew) +- Greenlock.renew({ subject, altnames, issuedBefore, expiresAfter }) + - (retrieves, issues, renews, all-in-one) +- _optional_ Greenlock.add({ subject, altnames, subscriberEmail }) + - (partially replaces `approveDomains`) + +Also, some disambiguation on terms: + +- `domains` was often ambiguous and confusing, it has been replaced by: + - `subject` refers to the subject of a certificate - the primary domain + - `altnames` refers to the domains in the SAN (Subject Alternative Names) section of the certificate + - `servername` refers to the TLS (SSL) SNI (Server Name Indication) request for a cetificate + - `wildname` refers to the wildcard version of the servername (ex: `www.example.com => *.example.com`) When you create an instance of Greenlock, you only supply package and maintainer info. @@ -35,21 +60,21 @@ var pkg = require('./package.json'); var Greenlock = require('greenlock'); var greenlock = Greenlock.create({ - // used for the ACME client User-Agent string as per RFC 8555 and RFC 7231 - packageAgent: pkg.name + '/' + pkg.version, + // used for the ACME client User-Agent string as per RFC 8555 and RFC 7231 + packageAgent: pkg.name + '/' + pkg.version, - // used as the contact for critical bug and security notices - // should be the same as pkg.author.email - maintainerEmail: 'jon@example.com' + // used as the contact for critical bug and security notices + // should be the same as pkg.author.email + maintainerEmail: 'jon@example.com', - // used for logging background events and errors - notify: function (ev, args) { - if ('error' === ev || 'warning' === ev) { - console.error(ev, args); - return; + // used for logging background events and errors + notify: function(ev, args) { + if ('error' === ev || 'warning' === ev) { + console.error(ev, args); + return; + } + console.info(ev, args); } - console.info(ev, args); - } }); ``` @@ -64,25 +89,25 @@ When you want to get a single certificate, you use `get`, which will: ```js greenlock - .get({ servername: 'www.example.com' }) - .then(function(result) { - if (!result) { - // certificate is not on the approved list - return null; - } + .get({ servername: 'www.example.com' }) + .then(function(result) { + if (!result) { + // certificate is not on the approved list + return null; + } - var fullchain = result.pems.cert + '\n' + result.pems.chain + '\n'; - var privkey = result.pems.privkey; + var fullchain = result.pems.cert + '\n' + result.pems.chain + '\n'; + var privkey = result.pems.privkey; - return { - fullchain: fullchain, - privkey: privkey - }; - }) - .catch(function(e) { - // something went wrong in the renew process - console.error(e); - }); + return { + fullchain: fullchain, + privkey: privkey + }; + }) + .catch(function(e) { + // something went wrong in the renew process + console.error(e); + }); ``` By default **no certificates will be issued**. See the _manager_ section. @@ -95,20 +120,20 @@ When you want to renew certificates, _en masse_, you use `renew`, which will: ```js greenlock - .renew({}) - .then(function(results) { - if (!result.length) { - // no certificates found - return null; - } + .renew({}) + .then(function(results) { + if (!result.length) { + // no certificates found + return null; + } - // [{ site, error }] - return results; - }) - .catch(function(e) { - // an unexpected error, not related to renewal - console.error(e); - }); + // [{ site, error }] + return results; + }) + .catch(function(e) { + // an unexpected error, not related to renewal + console.error(e); + }); ``` Options: @@ -129,28 +154,39 @@ with a few extra that are specific to Greenlock Express: ```js require('@root/greenlock-express') - .init(function() { - return { - cluster: false, - packageAgent: pkg.name + '/' + pkg.version, - maintainerEmail: 'jon@example.com', - notify: function(ev, args) { - if ('error' === ev || 'warning' === ev) { - console.error(ev, args); - return; - } - console.info(ev, args); - } - }; - }) - .serve(function(glx) { - glx.serveApp(function(req, res) { - res.end('Hello, Encrypted World!'); - }); - }); + .init(function() { + // This object will be passed to Greenlock.create() + + var options = { + // some options, like cluster, are special to Greenlock Express + + cluster: false, + + // The rest are the same as for Greenlock + + packageAgent: pkg.name + '/' + pkg.version, + maintainerEmail: 'jon@example.com', + notify: function(ev, args) { + console.info(ev, args); + } + }; + + return options; + }) + .serve(function(glx) { + // will start servers on port 80 and 443 + + glx.serveApp(function(req, res) { + res.end('Hello, Encrypted World!'); + }); + + // you can get access to the raw server (i.e. for websockets) + + glx.httpsServer(); // returns raw server object + }); ``` -## _Manager_ replaces `approveDomains` +# _Manager_ replaces `approveDomains` `approveDomains` was always a little confusing. Most people didn't need it. @@ -167,56 +203,58 @@ The config file should look something like this: ```json { - "subscriberEmail": "jon@example.com", - "agreeToTerms": true, - "sites": { - "example.com": { - "subject": "example.com", - "altnames": ["example.com", "www.example.com"] - } - } + "subscriberEmail": "jon@example.com", + "agreeToTerms": true, + "sites": { + "example.com": { + "subject": "example.com", + "altnames": ["example.com", "www.example.com"] + } + } } ``` -You can specify a `acme-dns-01-*` or `acme-http-01-*` challenge plugin globally, or per-site. The same is true with `greenlock-store-*` plugins: +You can specify a `acme-dns-01-*` or `acme-http-01-*` challenge plugin globally, or per-site. ```json { - "subscriberEmail": "jon@example.com", - "agreeToTerms": true, - "sites": { - "example.com": { - "subject": "example.com", - "altnames": ["example.com", "www.example.com"] - } - }, - "store": { - "module": "greenlock-store-fs", - "basePath": "~/.config/greenlock" - } + "subscriberEmail": "jon@example.com", + "agreeToTerms": true, + "sites": { + "example.com": { + "subject": "example.com", + "altnames": ["example.com", "www.example.com"], + "challenges": { + "dns-01": { + "module": "acme-dns-01-digitalocean", + "token": "apikey-xxxxx" + } + } + } + } } ``` +The same is true with `greenlock-store-*` plugins: + ```json { - "subscriberEmail": "jon@example.com", - "agreeToTerms": true, - "sites": { - "example.com": { - "subject": "example.com", - "altnames": ["example.com", "www.example.com"], - "challenges": { - "dns-01": { - "module": "acme-dns-01-digitalocean", - "token": "apikey-xxxxx" - } - } - } - } + "subscriberEmail": "jon@example.com", + "agreeToTerms": true, + "sites": { + "example.com": { + "subject": "example.com", + "altnames": ["example.com", "www.example.com"] + } + }, + "store": { + "module": "greenlock-store-fs", + "basePath": "~/.config/greenlock" + } } ``` -### Customer Manager +### Customer Manager, the lazy way At the very least you have to implement `find({ servername })`. @@ -224,29 +262,29 @@ Since this is a very common use case, it's supported out of the box as part of t ```js var greenlock = Greenlock.create({ - packageAgent: pkg.name + '/' + pkg.version, - maintainerEmail: 'jon@example.com', - notify: notify, - find: find + packageAgent: pkg.name + '/' + pkg.version, + maintainerEmail: 'jon@example.com', + notify: notify, + find: find }); // In the simplest case you can ignore all incoming options // and return a single site config in the same format as the config file function find(options) { - var servername = options.servername; // www.example.com - var wildname = options.wildname; // *.example.com - return Promise.resolve([ - { subject: 'example.com', altnames: ['example.com', 'www.example.com'] } - ]); + var servername = options.servername; // www.example.com + var wildname = options.wildname; // *.example.com + return Promise.resolve([ + { subject: 'example.com', altnames: ['example.com', 'www.example.com'] } + ]); } function notify(ev, args) { - if ('error' === ev || 'warning' === ev) { - console.error(ev, args); - return; - } - console.info(ev, args); + if ('error' === ev || 'warning' === ev) { + console.error(ev, args); + return; + } + console.info(ev, args); } ``` @@ -254,20 +292,101 @@ If you want to use wildcards or local domains, you must specify the `dns-01` cha ```js function find(options) { - var servername = options.servername; // www.example.com - var wildname = options.wildname; // *.example.com - return Promise.resolve([ - { - subject: 'example.com', - altnames: ['example.com', 'www.example.com'], - challenges: { - 'dns-01': { module: 'acme-dns-01-namedotcom', apikey: 'xxxx' } - } - } - ]); + var subject = options.subject; + // may include wildcard + var altnames = options.altnames; + var wildname = options.wildname; // *.example.com + return Promise.resolve([ + { + subject: 'example.com', + altnames: ['example.com', 'www.example.com'], + challenges: { + 'dns-01': { module: 'acme-dns-01-namedotcom', apikey: 'xxxx' } + } + } + ]); } ``` +### Customer Manager, complete + +To use a fully custom manager, you give the npm package name, or absolute path to the file to load + +```js +Greenlock.create({ + // Greenlock Options + maintainerEmail: 'jon@example.com', + packageAgent: 'my-package/v2.1.1', + notify: notify, + + // file path or npm package name + manager: '/path/to/manager.js', + // options that get passed to the manager + myFooOption: 'whatever' +}); +``` + +The manager itself is, again relatively simple: + +- find(options) +- add(siteConfig) +- update(updates) +- remove(options) +- defaults(globalOptions) (as setter) +- defaults() => globalOptions (as getter) + +`/path/to/manager.js`: + +```js +'use strict'; + +module.exports.create = function() { + var manager = {}; + + manager.find = async function({ subject, altnames, renewBefore }) { + if (subject) { + return getSiteConfigBySubject(subject); + } + + if (altnames) { + // may include wildcards + return getSiteConfigByAnyAltname(altnames); + } + + if (renewBefore) { + return getSiteConfigsWhereRenewAtIsLessThan(renewBefore); + } + + return []; + }; + + manage.add = function({ subject, altnames }) { + return setSiteConfig(subject, { subject, altnames }); + }; + + manage.update = function({ subject, renewAt }) { + // update the `renewAt` date of the site by `subject` + return mergSiteConfig(subject, { renewAt }); + }; + + manage.remove = function({ subject, altname }) { + if (subject) { + return removeSiteConfig(subject); + } + + return removeFromSiteConfigAndResetRenewAtToZero(altname); + }; + + // set the global config + manage.defaults = function(options) { + if (!options) { + return getGlobalConfig(); + } + return mergeGlobalConfig(options); + }; +}; +``` + # ACME Challenge Plugins The ACME challenge plugins are just a few simple callbacks: @@ -309,64 +428,69 @@ If you are just implenting in-house and are not going to publish a module, you c `/path/to/project/my-hacky-store.js`: ```js +'use strict'; + module.exports.create = function(options) { - // ex: /path/to/account.ecdsa.jwk.json - var accountJwk = require(options.accountJwkPath); - // ex: /path/to/privkey.rsa.pem - var serverPem = fs.readFileSync(options.serverPemPath, 'ascii'); - var store = {}; + // ex: /path/to/account.ecdsa.jwk.json + var accountJwk = require(options.accountJwkPath); + // ex: /path/to/privkey.rsa.pem + var serverPem = fs.readFileSync(options.serverPemPath, 'ascii'); + var accounts = {}; + var certificates = {}; + var store = { accounts, certificates }; - // bare essential account callbacks - store.accounts = { - checkKeypair: function() { - // ignore all options and just return a single, global keypair - return Promise.resolve({ - privateKeyJwk: accountJwk - }); - }, - setKeypair: function() { - // this will never get called if checkKeypair always returns - return Promise.resolve({}); - } - }; + // bare essential account callbacks + accounts.checkKeypair = function() { + // ignore all options and just return a single, global keypair - // bare essential cert and key callbacks - store.certificates = { - checkKeypair: function() { - // ignore all options and just return a global server keypair - return { - privateKeyPem: serverPem - }; - }, - setKeypair: function() { - // never gets called if checkKeypair always returns an existing key - return Promise.resolve(null); - }, - check: function(args) { - var subject = args.subject; - // make a database call or whatever to get a certificate - return goGetCertBySubject(subject).then(function() { - return { - pems: { - chain: '', - cert: '' - } - }; - }); - }, - set: function(args) { - var subject = args.subject; - var cert = args.pems.cert; - var chain = args.pems.chain; + return Promise.resolve({ + privateKeyJwk: accountJwk + }); + }; + accounts.setKeypair = function() { + // this will never get called if checkKeypair always returns - // make a database call or whatever to get a certificate - return goSaveCert({ - subject, - cert, - chain - }); - } - }; + return Promise.resolve({}); + }; + + // bare essential cert and key callbacks + certificates.checkKeypair = function() { + // ignore all options and just return a global server keypair + + return { + privateKeyPem: serverPem + }; + }; + certificates.setKeypair = function() { + // never gets called if checkKeypair always returns an existing key + + return Promise.resolve(null); + }; + + certificates.check = function(args) { + var subject = args.subject; + // make a database call or whatever to get a certificate + return goGetCertBySubject(subject).then(function() { + return { + pems: { + chain: '', + cert: '' + } + }; + }); + }; + certificates.set = function(args) { + var subject = args.subject; + var cert = args.pems.cert; + var chain = args.pems.chain; + + // make a database call or whatever to get a certificate + return goSaveCert({ + subject, + cert, + chain + }); + }; }; ``` @@ -379,18 +503,18 @@ Then you could assign it as the default for all of your sites: ```json { - "subscriberEmail": "jon@example.com", - "agreeToTerms": true, - "sites": { - "example.com": { - "subject": "example.com", - "altnames": ["example.com", "www.example.com"] - } - }, - "store": { - "module": "/path/to/project/my-hacky-store.js", - "accountJwkPath": "/path/to/account.ecdsa.jwk.json", - "serverPemPath": "/path/to/privkey.rsa.pem" - } + "subscriberEmail": "jon@example.com", + "agreeToTerms": true, + "sites": { + "example.com": { + "subject": "example.com", + "altnames": ["example.com", "www.example.com"] + } + }, + "store": { + "module": "/path/to/project/my-hacky-store.js", + "accountJwkPath": "/path/to/account.ecdsa.jwk.json", + "serverPemPath": "/path/to/privkey.rsa.pem" + } } ```