letsencrypt-express v2.x
This commit is contained in:
parent
f9e75faba7
commit
c68f748001
273
README.md
273
README.md
|
@ -1,202 +1,175 @@
|
|||
letsencrypt-cluster
|
||||
[![Join the chat at https://gitter.im/Daplie/letsencrypt-express](https://badges.gitter.im/Daplie/letsencrypt-express.svg)](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
|
||||
* Process certificates in master
|
||||
* Serve https from multiple workers
|
||||
* Can work with any clustering strategy [#1](https://github.com/Daplie/letsencrypt-cluster/issues/1)
|
||||
* Automatic Registration via SNI (`httpsOptions.SNICallback`)
|
||||
* **registrations** require an **approval callback** in *production*
|
||||
* Automatic Renewal (around 80 days)
|
||||
* **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
|
||||
=======
|
||||
|
||||
```bash
|
||||
npm install --save letsencrypt-cluster@2.x
|
||||
npm install --save letsencrypt-express@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.
|
||||
QuickStart
|
||||
----------
|
||||
|
||||
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:
|
||||
Here's a completely working (but terribly oversimplified) example that will get you started:
|
||||
|
||||
`boot.js`:
|
||||
`app.js`:
|
||||
```javascript
|
||||
'use strict';
|
||||
|
||||
var cluster = require('cluster');
|
||||
var path = require('path');
|
||||
var os = require('os');
|
||||
require('letsencrypt-express').create({
|
||||
|
||||
var main;
|
||||
var sharedOptions = {
|
||||
webrootPath: path.join(os.tmpdir(), 'acme-challenge') // /tmp/acme-challenge
|
||||
// used by le-challenge-fs, the default plugin
|
||||
server: 'staging'
|
||||
|
||||
, renewWithin: 10 * 24 * 60 * 60 * 1000 // 10 days before expiration
|
||||
, email: 'john.doe@example.com'
|
||||
|
||||
, debug: true
|
||||
};
|
||||
, agreeTos: true
|
||||
|
||||
if (cluster.isMaster) {
|
||||
main = require('./master');
|
||||
}
|
||||
else {
|
||||
main = require('./worker');
|
||||
}
|
||||
, app: require('express')().use('/', function (req, res) {
|
||||
res.end('Hello, World!');
|
||||
})
|
||||
|
||||
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.
|
||||
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.
|
||||
**Important**:
|
||||
|
||||
The main implementation detail here is `approveDomains(options, certs, cb)` for new domain certificates
|
||||
and potentially `agreeToTerms(opts, cb)` for new accounts.
|
||||
You must set `server` to `https://acme-v01.api.letsencrypt.org/directory` **after**
|
||||
you have tested that your setup works.
|
||||
|
||||
The master takes **the same arguments** as `node-letsencrypt` (`challenge`, `store`, etc),
|
||||
plus a few extra (`approveDomains`... okay, just one extra):
|
||||
**Security Warning**:
|
||||
|
||||
`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
|
||||
'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) {
|
||||
var cores = require('os').cpus();
|
||||
var leMaster = require('letsencrypt-cluster/master').create({
|
||||
debug: sharedOpts.debug
|
||||
// If you wish to replace the default plugins, you may do so here
|
||||
//
|
||||
//, challenges: { 'http-01:' require('le-challenge-fs').create({}) }
|
||||
//, 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);
|
||||
// The domains being approved for the first time are listed in opts.domains
|
||||
// Certs being renewed are listed in certs.altnames
|
||||
if (certs) {
|
||||
opts.domains = certs.altnames;
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
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);
|
||||
else {
|
||||
opts.email = 'john.doe@example.com';
|
||||
opts.agreeTos = true;
|
||||
}
|
||||
});
|
||||
|
||||
function app(req, res) {
|
||||
res.end("Hello, World!");
|
||||
cb(null, opts);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// handles acme-challenge and redirects to https
|
||||
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
|
||||
|
||||
`node-letsencrypt` is **not used** directly by the worker,
|
||||
but certain options are shared because certain logic is duplicated.
|
||||
All options are passed directly to `node-letsencrypt`,
|
||||
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.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`
|
||||
* `leWorker.httpsOptions` has a default localhost certificate and the `SNICallback`.
|
||||
* `lex.middleware(nextApp)` uses `letsencrypt/middleware` for GET-ing `http-01`, hence `sharedOptions.webrootPath`
|
||||
* `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
|
||||
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…
Reference in New Issue