letsencrypt-express v2.x
This commit is contained in:
		
							parent
							
								
									f9e75faba7
								
							
						
					
					
						commit
						c68f748001
					
				
							
								
								
									
										271
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										271
									
								
								README.md
									
									
									
									
									
								
							| @ -1,202 +1,175 @@ | |||||||
| letsencrypt-cluster | [](https://gitter.im/Daplie/letsencrypt-express?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||||||
|  | 
 | ||||||
|  | | [letsencrypt (library)](https://github.com/Daplie/node-letsencrypt) | ||||||
|  | | [letsencrypt-cli](https://github.com/Daplie/letsencrypt-cli) | ||||||
|  | | **letsencrypt-express** | ||||||
|  | | [letsencrypt-koa](https://github.com/Daplie/letsencrypt-koa) | ||||||
|  | | [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi) | ||||||
|  | | | ||||||
|  | 
 | ||||||
|  | letsencrypt-express | ||||||
| =================== | =================== | ||||||
| 
 | 
 | ||||||
| Use automatic letsencrypt with node on multiple cores or even multiple machines. | Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems. | ||||||
| 
 | 
 | ||||||
| * Take advantage of multi-core computing | * Automatic Registration via SNI (`httpsOptions.SNICallback`) | ||||||
| * Process certificates in master |   * **registrations** require an **approval callback** in *production* | ||||||
| * Serve https from multiple workers | * Automatic Renewal (around 80 days) | ||||||
| * Can work with any clustering strategy [#1](https://github.com/Daplie/letsencrypt-cluster/issues/1) |   * **renewals** are *fully automatic* and happen in the *background*, with **no downtime** | ||||||
|  | * Automatic vhost / virtual hosting | ||||||
|  | 
 | ||||||
|  | All you have to do is start the webserver and then visit it at it's domain name. | ||||||
|  | 
 | ||||||
|  | Help Wanted | ||||||
|  | ----------- | ||||||
|  | 
 | ||||||
|  | There are a number of easy-to-complete features that are up for grabs. | ||||||
|  | 
 | ||||||
|  | (mostly requiring either tracing some functions and doing some console.log-ing | ||||||
|  | or simply updating docs and getting tests to pass so that certain plugins accept | ||||||
|  | and return the right type of objects to complete the implementation | ||||||
|  | of certain plugins). | ||||||
|  | 
 | ||||||
|  | If you've got some free cycles to help, I can guide you through the process, | ||||||
|  | I'm just still too busy to do it all myself right now and nothing is breaking. | ||||||
|  | 
 | ||||||
|  | Email me <aj@daplie.com> if you want to help. | ||||||
| 
 | 
 | ||||||
| Install | Install | ||||||
| ======= | ======= | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| npm install --save letsencrypt-cluster@2.x | npm install --save letsencrypt-express@2.x | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Usage | Usage | ||||||
| ===== | ===== | ||||||
| 
 | 
 | ||||||
| In a cluster environment you have some main file that boots your app | QuickStart | ||||||
| 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 | Here's a completely working (but terribly oversimplified) example that will get you started: | ||||||
| to be shared between both the master and the worker, like this: |  | ||||||
| 
 | 
 | ||||||
| `boot.js`: | `app.js`: | ||||||
| ```javascript | ```javascript | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var cluster = require('cluster'); | require('letsencrypt-express').create({ | ||||||
| var path = require('path'); |  | ||||||
| var os = require('os'); |  | ||||||
| 
 | 
 | ||||||
| var main; |   server: 'staging' | ||||||
| var sharedOptions = { |  | ||||||
|   webrootPath: path.join(os.tmpdir(), 'acme-challenge')			// /tmp/acme-challenge |  | ||||||
|                                                             // used by le-challenge-fs, the default plugin |  | ||||||
| 
 | 
 | ||||||
| , renewWithin: 10 * 24 * 60 * 60 * 1000 										// 10 days before expiration | , email: 'john.doe@example.com' | ||||||
| 
 | 
 | ||||||
| , debug: true | , agreeTos: true | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| if (cluster.isMaster) { | , app: require('express')().use('/', function (req, res) { | ||||||
|   main = require('./master'); |     res.end('Hello, World!'); | ||||||
| } |   }) | ||||||
| else { |  | ||||||
|   main = require('./worker'); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| main.init(sharedOptions); | }).listen(80, 443); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Master | Certificates will be stored in `~/letsencrypt`. | ||||||
| ------ |  | ||||||
| 
 | 
 | ||||||
| We think it makes the most sense to load letsencrypt in master. | **Important**: | ||||||
| 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 | You must set `server` to `https://acme-v01.api.letsencrypt.org/directory` **after** | ||||||
| and potentially `agreeToTerms(opts, cb)` for new accounts. | you have tested that your setup works. | ||||||
| 
 | 
 | ||||||
| The master takes **the same arguments** as `node-letsencrypt` (`challenge`, `store`, etc), | **Security Warning**: | ||||||
| plus a few extra (`approveDomains`... okay, just one extra): |  | ||||||
| 
 | 
 | ||||||
| `master.js`: | If you don't do proper checks in `approveDomains(opts, certs, cb)` | ||||||
|  | an attacker will spoof SNI packets with bad hostnames and that will | ||||||
|  | cause you to be rate-limited and or blocked from the ACME server. | ||||||
|  | 
 | ||||||
|  | Why You Must Use 'staging' First | ||||||
|  | -------------------------------- | ||||||
|  | 
 | ||||||
|  | There are a number of common problems related to system configuration - | ||||||
|  | firewalls, ports, permissions, etc - that you are likely to run up against | ||||||
|  | when using letsencrypt for your first time. | ||||||
|  | 
 | ||||||
|  | In order to avoid being blocked by hitting rate limits with bad requests, | ||||||
|  | you should always test against the `'staging'` server | ||||||
|  | (`https://acme-staging.api.letsencrypt.org/directory`) first. | ||||||
|  | 
 | ||||||
|  | A more typical example | ||||||
|  | ---------------------- | ||||||
|  | 
 | ||||||
|  | The oversimplified example was the bait | ||||||
|  | (because everyone seems to want an example that fits in 3 lines, even if it's terribly bad practices), | ||||||
|  | now here's the switch: | ||||||
|  | 
 | ||||||
|  | `serve.js`: | ||||||
| ```javascript | ```javascript | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var cluster = require('cluster'); | // returns an instance of node-letsencrypt with additional helper methods | ||||||
|  | var lex = require('letsencrypt-express').create({ | ||||||
|  |   server: 'staging' | ||||||
| 
 | 
 | ||||||
| module.exports.init = function (sharedOpts) { | // If you wish to replace the default plugins, you may do so here | ||||||
|   var cores = require('os').cpus(); | // | ||||||
|   var leMaster = require('letsencrypt-cluster/master').create({ | //, challenges: { 'http-01:' require('le-challenge-fs').create({}) } | ||||||
|     debug: sharedOpts.debug | //, store: require('le-store-certbot').create({}) | ||||||
|  | //, sni: require('le-sni-auto').create({}) | ||||||
| 
 | 
 | ||||||
|   , server: 'staging'                                                       // CHANGE TO PRODUCTION | , approveDomains: function (opts, certs, cb) { | ||||||
|  |     // This is where you check your database and associated | ||||||
|  |     // email addresses with domains and agreements and such | ||||||
| 
 | 
 | ||||||
|   , 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-letsencrypt` |  | ||||||
| (in other works, `leMaster` is a `letsencrypt` instance), |  | ||||||
| but a few are only actually used by `letsencrypt-cluster`. |  | ||||||
| 
 |  | ||||||
| * `leOptions.approveDomains(options, certs, cb)` is special for `letsencrypt-cluster`, but will probably be included in `node-letsencrypt` in the future (no API change). |  | ||||||
| 
 |  | ||||||
| * `leMaster.addWorker(worker)` is added by `letsencrypt-cluster` and **must be called** for each new worker. |  | ||||||
| 
 |  | ||||||
| Worker |  | ||||||
| ------ |  | ||||||
| 
 |  | ||||||
| The worker takes *similar* arguments to `node-letsencrypt`, |  | ||||||
| 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('letsencrypt-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 |  | ||||||
|       }; |  | ||||||
| 
 | 
 | ||||||
|  |     // The domains being approved for the first time are listed in opts.domains | ||||||
|  |     // Certs being renewed are listed in certs.altnames | ||||||
|     if (certs) { |     if (certs) { | ||||||
|         // modify opts.domains to match the original request |       opts.domains = certs.altnames; | ||||||
|         // email is not necessary, because the account already exists |     } | ||||||
|         // this will only fail if the account has become corrupt |     else { | ||||||
|         results.options.domains = certs.altnames; |       opts.email = 'john.doe@example.com'; | ||||||
|         cb(null, results); |       opts.agreeTos = true; | ||||||
|         return; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|       // This is where one would check one's application-specific database: |     cb(null, opts); | ||||||
|       //   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)); | // handles acme-challenge and redirects to https | ||||||
|   server.listen(443); | require('http').createServer(lex.middleware()).listen(80, function () { | ||||||
| }; |   console.log("Listening for ACME http-01 challenges on", this.address()); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | var app = require('express')(); | ||||||
|  | app.use('/', function (req, res) { | ||||||
|  |   res.end('Hello, World!'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // handles your app | ||||||
|  | require('https').createServer(lex.httpsOptions, lex.middleware(app)).listen(443, function () { | ||||||
|  |   console.log("Listening for ACME tls-sni-01 challenges and serve app on", this.address()); | ||||||
|  | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### API | ### API | ||||||
| 
 | 
 | ||||||
| `node-letsencrypt` is **not used** directly by the worker, | All options are passed directly to `node-letsencrypt`, | ||||||
| but certain options are shared because certain logic is duplicated. | so `lex` is an instance of `letsencrypt`, but has a few | ||||||
|  | extra helper methods and options. | ||||||
| 
 | 
 | ||||||
|  | See [node-letsencrypt options](https://github.com/Daplie/node-letsencrypt) | ||||||
|  | 
 | ||||||
|  | * `lexOptions.approveDomains(options, certs, cb)` is special for `letsencrypt-express`, but will probably be included in `node-letsencrypt` in the future (no API change). | ||||||
|  | 
 | ||||||
|  | * `lexOptions.app` is just an elaborate ruse used for the Quickstart. It's sole purpose is to trim out 5 lines of code for setting http and https servers so that whiners won't whine. Real programmers don't use this. | ||||||
|  | * `leOptions.email` useful for simple sites where there is only one owner. Leave this `null` and use `approveDomains` otherwise. | ||||||
|  | * `leOptions.agreeTos` useful for simple sites where there is only one owner. Leave this `null` and use `approveDomains` otherwise. | ||||||
| * `leOptions.renewWithin` is shared so that the worker knows how earlier to request a new cert | * `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) | * `leOptions.renewBy` is passed to `le-sni-auto` so that it staggers renewals between `renewWithin` (latest) and `renewBy` (earlier) | ||||||
| * `leWorker.middleware(nextApp)` uses `letsencrypt/middleware` for GET-ing `http-01`, hence `sharedOptions.webrootPath` | * `lex.middleware(nextApp)` uses `letsencrypt/middleware` for GET-ing `http-01`, hence `sharedOptions.webrootPath` | ||||||
| * `leWorker.httpsOptions` has a default localhost certificate and the `SNICallback`. | * `lex.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 | 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. | 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. |  | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user