250 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			250 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Replaced: Use Greenlock Express v3
 | |
| 
 | |
| See https://git.rootprojects.org/root/greenlock-express.js
 | |
| 
 | |
| ```js
 | |
| "use strict";
 | |
| 
 | |
| var pkg = require("./package.json");
 | |
| require("greenlock-express")
 | |
|     .init(function getConfig() {
 | |
|         // Greenlock Config
 | |
| 
 | |
|         return {
 | |
|             package: { name: pkg.name, version: pkg.version },
 | |
|             maintainerEmail: pkg.author,
 | |
| 
 | |
|             // put cluster on full throttle!
 | |
|             cloudnative: true
 | |
|             webscale: true
 | |
|             cluster: true
 | |
|         };
 | |
|     })
 | |
|     .serve(httpsWorker);
 | |
| 
 | |
| function httpsWorker(glx) {
 | |
|     // Serves on 80 and 443
 | |
|     // Get's SSL certificates magically!
 | |
| 
 | |
|     glx.serveApp(function(req, res) {
 | |
|         res.end("Hello, Encrypted World!");
 | |
|     });
 | |
| }
 | |
| ```
 | |
| 
 | |
| # OLD STUFF BELOW
 | |
| 
 | |
| # (Preserved for historical reference)
 | |
| 
 | |
| | A [Root](https://therootcompany.com) Project
 | |
| | [greenlock (lib)](https://git.coolaj86.com/coolaj86/greenlock.js)
 | |
| | [greenlock-cli](https://git.coolaj86.com/coolaj86/greenlock-cli.js)
 | |
| | [greenlock-express](https://git.coolaj86.com/coolaj86/greenlock-express.js)
 | |
| | **greenlock-cluster**
 | |
| | [greenlock-koa](https://git.coolaj86.com/coolaj86/greenlock-koa.js)
 | |
| | [greenlock-hapi](https://git.coolaj86.com/coolaj86/greenlock-hapi.js)
 | |
| |
 | |
| 
 | |
| # greenlock-cluster
 | |
| 
 | |
| (previously letsencrypt-cluster)
 | |
| 
 | |
| Use automatic letsencrypt with node on multiple cores or even multiple machines.
 | |
| 
 | |
| -   Take advantage of multi-core computing
 | |
| -   Process certificates in master
 | |
| -   Serve https from multiple workers
 | |
| -   Can work with any clustering strategy [#1](https://github.com/Daplie/letsencrypt-cluster/issues/1)
 | |
| 
 | |
| # Install
 | |
| 
 | |
| ```bash
 | |
| npm install --save greenlock-cluster@2.x
 | |
| ```
 | |
| 
 | |
| # Usage
 | |
| 
 | |
| In a cluster environment you have some main file that boots your app
 | |
| and then conditionally loads certain code based on whether that fork
 | |
| is the master or just a worker.
 | |
| 
 | |
| In such a file you might want to define some of the options that need
 | |
| to be shared between both the master and the worker, like this:
 | |
| 
 | |
| `boot.js`:
 | |
| 
 | |
| ```javascript
 | |
| "use strict";
 | |
| 
 | |
| var cluster = require("cluster");
 | |
| var path = require("path");
 | |
| var os = require("os");
 | |
| 
 | |
| var main;
 | |
| var sharedOptions = {
 | |
|     webrootPath: path.join(os.tmpdir(), "acme-challenge"), // /tmp/acme-challenge
 | |
|     // used by le-challenge-fs, the default plugin
 | |
| 
 | |
|     renewWithin: 14 * 24 * 60 * 60 * 1000, // 10 days before expiration
 | |
| 
 | |
|     debug: true
 | |
| };
 | |
| 
 | |
| if (cluster.isMaster) {
 | |
|     main = require("./master");
 | |
| } else {
 | |
|     main = require("./worker");
 | |
| }
 | |
| 
 | |
| main.init(sharedOptions);
 | |
| ```
 | |
| 
 | |
| ## Master
 | |
| 
 | |
| We think it makes the most sense to load greenlock in master.
 | |
| This can prevent race conditions (see [node-letsencrypt#45](https://github.com/Daplie/node-letsencrypt/issues/45))
 | |
| as only one process is writing the to file system or database at a time.
 | |
| 
 | |
| The main implementation detail here is `approveDomains(options, certs, cb)` for new domain certificates
 | |
| and potentially `agreeToTerms(opts, cb)` for new accounts.
 | |
| 
 | |
| The master takes **the same arguments** as `node-greenlock` (`challenge`, `store`, etc),
 | |
| plus a few extra (`approveDomains`... okay, just one extra):
 | |
| 
 | |
| `master.js`:
 | |
| 
 | |
| ```javascript
 | |
| 'use strict';
 | |
| 
 | |
| var cluster = require('cluster');
 | |
| 
 | |
| module.exports.init = function (sharedOpts) {
 | |
|   var cores = require('os').cpus();
 | |
|   var leMaster = require('greenlock-cluster/master').create({
 | |
|     debug: sharedOpts.debug
 | |
| 
 | |
|     // You MUST change this to 'https://acme-v02.api.letsencrypt.org/directory' in production
 | |
|     server: 'https://acme-staging-v02.api.letsencrypt.org/directory'
 | |
|   , version: 'draft-11' // Let's Encrypt v2
 | |
| 
 | |
|   , renewWithin: sharedOpts.renewWithin
 | |
| 
 | |
|   , webrootPath: sharedOpts.webrootPath
 | |
| 
 | |
|   , approveDomains: function (masterOptions, certs, cb) {
 | |
|       // Do any work that must be done by master to approve this domain
 | |
|       // (in this example, it's assumed to be done by the worker)
 | |
| 
 | |
|       var results = { domain: masterOptions.domain                          // required
 | |
|                     , options: masterOptions                                // domains, email, agreeTos
 | |
|                     , certs: certs };                                       // altnames, privkey, cert
 | |
|       cb(null, results);
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   cores.forEach(function () {
 | |
|     var worker = cluster.fork();
 | |
|     leMaster.addWorker(worker);
 | |
|   });
 | |
| };
 | |
| ```
 | |
| 
 | |
| ### API
 | |
| 
 | |
| All options are passed directly to `node-greenlock`
 | |
| (in other works, `leMaster` is a `greenlock` instance),
 | |
| but a few are only actually used by `greenlock-cluster`.
 | |
| 
 | |
| -   `leOptions.approveDomains(options, certs, cb)` is special for `greenlock-cluster`, but will probably be included in `node-greenlock` in the future (no API change).
 | |
| 
 | |
| -   `leMaster.addWorker(worker)` is added by `greenlock-cluster` and **must be called** for each new worker.
 | |
| 
 | |
| ## Worker
 | |
| 
 | |
| The worker takes _similar_ arguments to `node-greenlock`,
 | |
| but only ones that are useful for determining certificate
 | |
| renewal and for `le.challenge.get`.
 | |
| 
 | |
| If you want to a non-default `le.challenge`
 | |
| 
 | |
| `worker.js`:
 | |
| 
 | |
| ```javascript
 | |
| "use strict";
 | |
| 
 | |
| module.exports.init = function(sharedOpts) {
 | |
|     var leWorker = require("greenlock-cluster/worker").create({
 | |
|         debug: sharedOpts.debug,
 | |
| 
 | |
|         renewWithin: sharedOpts.renewWithin,
 | |
| 
 | |
|         webrootPath: sharedOpts.webrootPath,
 | |
| 
 | |
|         // , challenge: require('le-challenge-fs').create({ webrootPath: '...', ... })
 | |
| 
 | |
|         approveDomains: function(workerOptions, certs, cb) {
 | |
|             // opts = { domains, email, agreeTos, tosUrl }
 | |
|             // certs = { subject, altnames, expiresAt, issuedAt }
 | |
| 
 | |
|             var results = {
 | |
|                 domain: workerOptions.domains[0],
 | |
|                 options: {
 | |
|                     domains: workerOptions.domains
 | |
|                 },
 | |
|                 certs: certs
 | |
|             };
 | |
| 
 | |
|             if (certs) {
 | |
|                 // modify opts.domains to match the original request
 | |
|                 // email is not necessary, because the account already exists
 | |
|                 // this will only fail if the account has become corrupt
 | |
|                 results.options.domains = certs.altnames;
 | |
|                 cb(null, results);
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // This is where one would check one's application-specific database:
 | |
|             //   1. Lookup the domain to see which email it belongs to
 | |
|             //   2. Assign a default email if it isn't in the system
 | |
|             //   3. If the email has no le account, `agreeToTerms` will fire unless `agreeTos` is preset
 | |
| 
 | |
|             results.options.email = "john.doe@example.com";
 | |
|             results.options.agreeTos = true; // causes agreeToTerms to be skipped
 | |
|             cb(null, results);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     function app(req, res) {
 | |
|         res.end("Hello, World!");
 | |
|     }
 | |
| 
 | |
|     var redirectHttps = require("redirect-https")();
 | |
|     var plainServer = require("http").createServer(leWorker.middleware(redirectHttps));
 | |
|     plainServer.listen(80);
 | |
| 
 | |
|     var server = require("https").createServer(leWorker.httpsOptions, leWorker.middleware(app));
 | |
|     server.listen(443);
 | |
| };
 | |
| ```
 | |
| 
 | |
| ### API
 | |
| 
 | |
| `node-greenlock` is **not used** directly by the worker,
 | |
| but certain options are shared because certain logic is duplicated.
 | |
| 
 | |
| -   `leOptions.renewWithin` is shared so that the worker knows how earlier to request a new cert
 | |
| -   `leOptions.renewBy` is passed to `le-sni-auto` so that it staggers renewals between `renewWithin` (latest) and `renewBy` (earlier)
 | |
| -   `leWorker.middleware(nextApp)` uses `greenlock/middleware` for GET-ing `http-01`, hence `sharedOptions.webrootPath`
 | |
| -   `leWorker.httpsOptions` has a default localhost certificate and the `SNICallback`.
 | |
| 
 | |
| There are a few options that aren't shown in these examples, so if you need to change something
 | |
| that isn't shown here, look at the code (it's not that much) or open an issue.
 | |
| 
 | |
| ## Message Passing
 | |
| 
 | |
| The master and workers will communicate through `process.on('message', fn)`, `process.send({})`,
 | |
| `worker.on('message', fn)`and `worker.send({})`.
 | |
| 
 | |
| All messages have a `type` property which is a string and begins with `LE_`.
 | |
| All other messages are ignored.
 |