2015-12-17 10:00:30 +00:00
|
|
|
# LetsEncrypt Express
|
2015-12-17 00:51:37 +00:00
|
|
|
|
2015-12-17 10:34:05 +00:00
|
|
|
Free SSL and managed or automatic HTTPS for node.js with Express, Connect, and other middleware systems.
|
2015-12-17 10:27:13 +00:00
|
|
|
|
2015-12-17 10:29:00 +00:00
|
|
|
## Install
|
|
|
|
|
|
|
|
```
|
|
|
|
npm install --save letsencrypt-express
|
|
|
|
```
|
|
|
|
|
|
|
|
## Usage
|
|
|
|
|
|
|
|
**Minimal**
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// Note: using staging server url, remove .testing() for production
|
|
|
|
var lex = require('letsencrypt-express').testing();
|
2015-12-17 10:35:40 +00:00
|
|
|
|
2015-12-17 10:29:00 +00:00
|
|
|
var express = require('express');
|
|
|
|
var app = express();
|
|
|
|
|
2015-12-17 10:35:40 +00:00
|
|
|
|
|
|
|
// A happy little express app
|
|
|
|
app.use(function (req, res) {
|
2015-12-17 10:29:00 +00:00
|
|
|
res.send({ success: true });
|
|
|
|
});
|
|
|
|
|
2015-12-17 10:35:40 +00:00
|
|
|
|
|
|
|
// assumes ~/letsencrypt/etc as the configDir and ports 80, 443, and 5001 by default
|
2015-12-17 10:29:00 +00:00
|
|
|
lex.create(app).listen();
|
|
|
|
```
|
|
|
|
|
2015-12-17 10:27:13 +00:00
|
|
|
## How Automatic?
|
|
|
|
|
|
|
|
**Extremely**.
|
|
|
|
|
|
|
|
* **renewals** are *fully automatic* and happen in the *background*, with **no downtime**
|
|
|
|
* **registrations** are automatic in *testing*, but require a **approval callback** in *production*
|
|
|
|
|
|
|
|
**testing mode**
|
|
|
|
|
|
|
|
All you have to do is start the webserver and then visit it at it's domain name.
|
|
|
|
The certificate will be retrieved automatically. Renewals and Registrations are automatic.
|
|
|
|
|
|
|
|
**production mode**
|
|
|
|
|
|
|
|
You can run **registration** manually:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
npm install -g letsencrypt-cli
|
|
|
|
|
2015-12-17 10:39:54 +00:00
|
|
|
letsencrypt certonly --standalone \
|
|
|
|
--config-dir ~/letsencrypt/etc \
|
|
|
|
--agree-tos --domains example.com --email user@example.com
|
2015-12-17 10:27:13 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
(note that the `--webrootPath` option is also available if you don't want to shut down your webserver to get the cert)
|
|
|
|
|
|
|
|
Or you can approve registrations with the `opts.approveRegistration(domain, cb)`callback:
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
{ configDir: '...'
|
|
|
|
// ...
|
|
|
|
, approveRegistration: function (hostname, cb) {
|
|
|
|
// check a database or something, get the user
|
|
|
|
// show them the agreement that you've already downloaded
|
|
|
|
cb(null, {
|
|
|
|
domains: [hostname]
|
|
|
|
, email: 'user@example.com'
|
|
|
|
, agreeTos: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2015-12-17 10:29:00 +00:00
|
|
|
(if you don't check and simply complete the callback, 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)
|
2015-12-17 00:51:37 +00:00
|
|
|
|
|
|
|
## Examples
|
|
|
|
|
2015-12-17 10:29:00 +00:00
|
|
|
### < 140 Characters
|
2015-12-17 09:37:42 +00:00
|
|
|
|
|
|
|
Let's Encrypt in 128 characters, with spaces!
|
|
|
|
|
|
|
|
```
|
2015-12-30 14:25:14 +00:00
|
|
|
node -e 'require("letsencrypt-express").testing().create( require("express")().use(function (_, r) { r.end("Hi!") }) ).listen()'
|
2015-12-17 09:37:42 +00:00
|
|
|
```
|
|
|
|
|
2015-12-17 10:27:13 +00:00
|
|
|
### More realistic
|
2015-12-17 08:44:55 +00:00
|
|
|
|
|
|
|
```javascript
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// Note: using staging server url, remove .testing() for production
|
2016-02-13 01:04:40 +00:00
|
|
|
var LEX = require('letsencrypt-express').testing();
|
2015-12-17 08:44:55 +00:00
|
|
|
var express = require('express');
|
|
|
|
var app = express();
|
|
|
|
|
|
|
|
app.use('/', function (req, res) {
|
|
|
|
res.send({ success: true });
|
|
|
|
});
|
|
|
|
|
2016-02-13 01:04:40 +00:00
|
|
|
LEX.create({
|
2015-12-17 10:27:13 +00:00
|
|
|
configDir: './letsencrypt.config' // ~/letsencrypt, /etc/letsencrypt, whatever you want
|
2016-02-13 01:04:40 +00:00
|
|
|
|
2015-12-17 10:27:13 +00:00
|
|
|
, onRequest: app // your express app (or plain node http app)
|
|
|
|
|
|
|
|
, letsencrypt: null // you can provide you own instance of letsencrypt
|
|
|
|
// if you need to configure it (with an agreeToTerms
|
|
|
|
// callback, for example)
|
2016-02-13 01:04:40 +00:00
|
|
|
|
2015-12-17 10:27:13 +00:00
|
|
|
, approveRegistration: function (hostname, cb) { // PRODUCTION MODE needs this function, but only if you want
|
|
|
|
// automatic registration (usually not necessary)
|
|
|
|
// renewals for registered domains will still be automatic
|
|
|
|
cb(null, {
|
|
|
|
domains: [hostname]
|
|
|
|
, email: 'user@example.com'
|
2016-02-13 01:04:40 +00:00
|
|
|
, agreeTos: true // you
|
2015-12-17 10:27:13 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}).listen([80], [443, 5001], function () {
|
2015-12-17 01:20:56 +00:00
|
|
|
console.log("ENCRYPT __ALL__ THE DOMAINS!");
|
2015-12-17 00:51:37 +00:00
|
|
|
});
|
|
|
|
```
|
|
|
|
|
2016-02-13 02:38:04 +00:00
|
|
|
### Use with raw http / https modules
|
|
|
|
|
|
|
|
Let's say you want to redirect all http to https.
|
|
|
|
|
|
|
|
```
|
|
|
|
var LEX = require('letsencrypt-express');
|
|
|
|
var lex = LEX.create({
|
|
|
|
configDir: __dirname + '/letsencrypt.config'
|
|
|
|
, approveRegistration: function (hostname, cb) {
|
|
|
|
cb(null, {
|
|
|
|
domains: [hostname]
|
|
|
|
, email: 'CHANGE_ME' // 'user@example.com'
|
|
|
|
, agreeTos: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var http = require('http');
|
|
|
|
http.createServer(LEX.createAcmeResponder(lex, function redirectHttps(req, res) {
|
|
|
|
res.setHeader('Location', 'https://' + req.headers.host + req.url);
|
|
|
|
res.end('<!-- Hello Developer Person! Please use HTTPS instead -->');
|
|
|
|
})).listen(80);
|
|
|
|
|
|
|
|
|
|
|
|
var app = require('express')();
|
|
|
|
app.use('/', function (req, res) {
|
|
|
|
res.end('Hello!');
|
|
|
|
});
|
|
|
|
|
|
|
|
var https = require('https');
|
|
|
|
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app)).listen(443);
|
|
|
|
```
|
|
|
|
|
|
|
|
In short these are the only functions you need to be aware of:
|
|
|
|
|
|
|
|
* `LEX.create(opts)`
|
|
|
|
* `{ configDir: pathname, approveRegistration: func }`
|
|
|
|
* `LEX.createAcmeResponder(lex, onRequest)`
|
|
|
|
|
|
|
|
|
2015-12-17 00:51:37 +00:00
|
|
|
### More Options Exposed
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
'use strict';
|
|
|
|
|
2015-12-17 03:03:07 +00:00
|
|
|
var lex = require('letsencrypt-express');
|
2015-12-17 00:51:37 +00:00
|
|
|
var express = require('express');
|
|
|
|
var app = express();
|
|
|
|
|
|
|
|
app.use('/', function (req, res) {
|
|
|
|
res.send({ success: true });
|
|
|
|
});
|
|
|
|
|
2015-12-17 03:03:07 +00:00
|
|
|
var results = lex.create({
|
2015-12-17 00:51:37 +00:00
|
|
|
configDir: '/etc/letsencrypt'
|
|
|
|
, onRequest: app
|
2015-12-17 01:20:56 +00:00
|
|
|
, server: require('letsencrypt').productionServerUrl
|
2015-12-17 00:51:37 +00:00
|
|
|
}).listen(
|
|
|
|
|
|
|
|
// you can give just the port, or expand out to the full options
|
|
|
|
[80, { port: 8080, address: 'localhost', onListening: function () { console.log('http://localhost'); } }]
|
|
|
|
|
|
|
|
// you can give just the port, or expand out to the full options
|
|
|
|
, [443, 5001, { port: 8443, address: 'localhost' }]
|
|
|
|
|
|
|
|
// this is pretty much the default onListening handler
|
|
|
|
, function onListening() {
|
|
|
|
var server = this;
|
|
|
|
var protocol = ('requestCert' in server) ? 'https': 'http';
|
|
|
|
console.log("Listening at " + protocol + '://localhost:' + this.address().port);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// In case you need access to the raw servers (i.e. using websockets)
|
|
|
|
console.log(results.plainServers);
|
|
|
|
console.log(results.tlsServers);
|
|
|
|
```
|
|
|
|
|
|
|
|
### WebSockets with Let's Encrypt
|
|
|
|
|
|
|
|
Note: you don't need to create websockets for the plain ports.
|
|
|
|
|
2015-12-17 10:32:43 +00:00
|
|
|
```javascript
|
|
|
|
var WebSocketServer = require('ws').Server;
|
|
|
|
|
2015-12-17 00:51:37 +00:00
|
|
|
results.tlsServers.forEach(function (server) {
|
2015-12-17 10:32:43 +00:00
|
|
|
var wss = new WebSocketServer({ server: server });
|
|
|
|
wss.on('connection', onConnection);
|
2015-12-17 00:51:37 +00:00
|
|
|
});
|
2015-12-17 10:32:43 +00:00
|
|
|
|
|
|
|
function onConnection(ws) {
|
|
|
|
var location = url.parse(ws.upgradeReq.url, true);
|
|
|
|
// you might use location.query.access_token to authenticate or share sessions
|
|
|
|
// or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312)
|
2016-02-13 01:04:40 +00:00
|
|
|
|
2015-12-17 10:32:43 +00:00
|
|
|
ws.on('message', function incoming(message) {
|
|
|
|
console.log('received: %s', message);
|
|
|
|
});
|
|
|
|
|
|
|
|
ws.send('something');
|
|
|
|
}
|
2015-12-17 00:51:37 +00:00
|
|
|
```
|
|
|
|
|
2015-12-17 03:03:07 +00:00
|
|
|
## API
|
|
|
|
|
|
|
|
```
|
2015-12-17 03:15:20 +00:00
|
|
|
// checks options and sets up defaults. returns object with `listen`
|
|
|
|
LEX.create(options) // (it was really just done this way to appeal to what people are used to seeing)
|
2015-12-17 03:03:07 +00:00
|
|
|
|
|
|
|
lex.listen(plain, tls, fn) // actually creates the servers and causes them to listen
|
|
|
|
|
|
|
|
|
2015-12-17 03:15:20 +00:00
|
|
|
// receives an instance of letsencrypt, returns an SNICallback handler for https.createServer()
|
|
|
|
LEX.createSniCallback(opts) // this will call letsencrypt.renew and letsencrypt.register as appropriate
|
|
|
|
// it will randomly stagger renewals such that they don't all happen at once on boot
|
|
|
|
// or at any other time. registrations will be handled as per `handleRegistration`
|
|
|
|
opts = {
|
|
|
|
letsencrypt: <obj> // letsencrypt instance
|
|
|
|
, memorizeFor: <1 day> // how long to wait before checking the disk for updated certificates
|
|
|
|
, renewWithin: <3 days> // the first possible moment the certificate staggering should begin
|
|
|
|
, failedWait: <5 minutes> // how long to wait before trying again if the certificate registration failed
|
2015-12-17 05:08:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
// registrations are NOT approved automatically by default due to security concerns
|
|
|
|
, approveRegistration: func // (someone can spoof servername indication to your server and cause you to be rate-limited)
|
|
|
|
// but you can implement handling of them if you wish
|
|
|
|
// (note that you should probably call the callback immediately with a tlsContext)
|
|
|
|
//
|
|
|
|
// default function (hostname, cb) { cb(null, null); }
|
|
|
|
//
|
|
|
|
// example function (hostname, cb) {
|
|
|
|
// cb(null, { domains: [hostname], agreeTos: true, email: 'user@example.com' });
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
, handleRenewFailure: func // renewals are automatic, but sometimes they may fail. If that happens, you should handle it
|
|
|
|
// (note that renewals happen in the background)
|
|
|
|
//
|
|
|
|
// default function (err, letsencrypt, hostname, certInfo) {}
|
2015-12-17 03:15:20 +00:00
|
|
|
}
|
2015-12-17 03:03:07 +00:00
|
|
|
|
|
|
|
|
2015-12-17 03:15:20 +00:00
|
|
|
// uses `opts.webrootPath` to read from the filesystem
|
2016-02-13 01:04:40 +00:00
|
|
|
LEX.getChallenge(opts, hostname, key cb)
|
|
|
|
|
|
|
|
LEX.createAcmeResponder(opts, fn) // this will return the necessary request handler for /.well-known/acme-challenges
|
|
|
|
// which then calls `fn` (such as express app) to complete the request
|
|
|
|
//
|
|
|
|
// opts lex instance created with LEX.create(opts)
|
|
|
|
// more generally, any object with a compatible `getChallenge` will work:
|
|
|
|
// `lex.getChallenge(opts, domain, key, function (err, val) {})`
|
|
|
|
//
|
|
|
|
// fn function (req, res) {
|
|
|
|
// console.log(req.method, req.url);
|
|
|
|
//
|
|
|
|
// res.end('Hello!');
|
|
|
|
// }
|
2015-12-17 03:03:07 +00:00
|
|
|
```
|
|
|
|
|
2015-12-16 23:06:00 +00:00
|
|
|
## Options
|
|
|
|
|
|
|
|
If any of these values are `undefined` or `null` the will assume use reasonable defaults.
|
|
|
|
|
|
|
|
Partially defined values will be merged with the defaults.
|
|
|
|
|
|
|
|
Setting the value to `false` will, in many cases (as documented), disable the defaults.
|
|
|
|
|
|
|
|
```
|
2015-12-17 01:20:56 +00:00
|
|
|
configDir: string // string the letsencrypt configuration path (de facto /etc/letsencrypt)
|
|
|
|
//
|
|
|
|
// default os.homedir() + '/letsencrypt/etc'
|
2015-12-17 00:51:37 +00:00
|
|
|
|
|
|
|
|
2015-12-16 23:06:00 +00:00
|
|
|
webrootPath: string // string a path to a folder where temporary challenge files will be stored and read
|
2015-12-17 01:20:56 +00:00
|
|
|
//
|
|
|
|
// default os.tmpdir() + '/acme-challenge'
|
2015-12-16 23:06:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
getChallenge: func | false // false do not handle getChallenge
|
|
|
|
//
|
|
|
|
// func Example:
|
|
|
|
//
|
|
|
|
// default function (defaults, hostname, key, cb) {
|
|
|
|
// var filename = path.join(defaults.webrootPath.replace(':hostname', hostname), key);
|
|
|
|
// fs.readFile(filename, 'ascii', function (cb, text) {
|
|
|
|
// cb(null, text);
|
|
|
|
// })
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
httpsOptions: object // object will be merged with internal defaults and passed to https.createServer()
|
|
|
|
// { pfx, key, cert, passphrase, ca, ciphers, rejectUnauthorized, secureProtocol }
|
|
|
|
// See https://nodejs.org/api/https.html
|
|
|
|
// Note: if SNICallback is specified, it will be run *before*
|
|
|
|
// the internal SNICallback that manages automated certificates
|
|
|
|
//
|
|
|
|
// default uses a localhost cert and key to prevent https.createServer() from throwing an error
|
|
|
|
// and also uses our SNICallback, which manages certificates
|
|
|
|
|
|
|
|
|
|
|
|
sniCallback: func // func replace the default sniCallback handler (which manages certificates) with your own
|
2015-12-17 00:51:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
letsencrypt: object // object configure the letsencrypt object yourself and pass it in directly
|
|
|
|
//
|
|
|
|
// default we create the letsencrypt object using parameters you specify
|
2015-12-17 01:20:56 +00:00
|
|
|
|
|
|
|
server: url // url use letsencrypt.productionServerUrl (i.e. https://acme-v01.api.letsencrypt.org/directory)
|
|
|
|
// or letsencrypt.stagingServerUrl (i.e. https://acme-staging.api.letsencrypt.org/directory)
|
|
|
|
//
|
|
|
|
// default production
|
2015-12-16 23:06:00 +00:00
|
|
|
```
|
2015-12-17 00:51:37 +00:00
|
|
|
|
2016-02-13 01:04:40 +00:00
|
|
|
### Fullest Example Ever
|
|
|
|
|
|
|
|
Here's absolutely every option and function exposed
|
|
|
|
|
|
|
|
```
|
|
|
|
var http = require('http');
|
|
|
|
var https = require('https');
|
|
|
|
var LEX = require('letsencrypt-express');
|
|
|
|
var LE = require('letsencrypt');
|
|
|
|
var lex;
|
|
|
|
|
|
|
|
lex = LEX.create({
|
|
|
|
webrootPath: '/tmp/.well-known/acme-challenge'
|
|
|
|
|
|
|
|
, lifetime: 90 * 24 * 60 * 60 * 1000 // expect certificates to last 90 days
|
|
|
|
, failedWait: 5 * 60 * 1000 // if registering fails wait 5 minutes before trying again
|
|
|
|
, renewWithin: 3 * 24 * 60 * 60 * 1000 // renew at least 3 days before expiration
|
|
|
|
, memorizeFor: 1 * 24 * 60 * 60 * 1000 // keep certificates in memory for 1 day
|
|
|
|
|
|
|
|
, approveRegistration: function (hostname, cb) {
|
|
|
|
cb(null, {
|
|
|
|
domains: [hostname]
|
|
|
|
, email: 'user@example.com'
|
|
|
|
, agreeTos: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
, handleRenewFailure: function (err, hostname, certInfo) {
|
|
|
|
console.error("ERROR: Failed to renew domain '", hostname, "':");
|
|
|
|
if (err) {
|
|
|
|
console.error(err.stack || err);
|
|
|
|
}
|
|
|
|
if (certInfo) {
|
|
|
|
console.error(certInfo);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
, letsencrypt: LE.create(
|
|
|
|
// options
|
|
|
|
{ configDir: './letsencrypt.config'
|
2016-02-13 02:28:23 +00:00
|
|
|
, manual: true
|
2016-02-13 01:04:40 +00:00
|
|
|
|
|
|
|
, server: LE.productionServerUrl
|
|
|
|
, privkeyPath: LE.privkeyPath
|
|
|
|
, fullchainPath: LE.fullchainPath
|
|
|
|
, certPath: LE.certPath
|
|
|
|
, chainPath: LE.chainPath
|
|
|
|
, renewalPath: LE.renewalPath
|
|
|
|
, accountsDir: LE.accountsDir
|
|
|
|
|
|
|
|
, debug: false
|
|
|
|
}
|
|
|
|
|
|
|
|
// handlers
|
|
|
|
, { setChallenge: LEX.setChallenge
|
|
|
|
, removeChallenge: LEX.removeChallenge
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
, debug: false
|
|
|
|
});
|
|
|
|
|
|
|
|
http.createServer(LEX.createAcmeResponder(lex, function (req, res) {
|
|
|
|
res.setHeader('Location', 'https://' + req.headers.host + req.url);
|
|
|
|
res.end('<!-- Hello Mr Developer! Please use HTTPS instead -->');
|
|
|
|
}));
|
|
|
|
|
|
|
|
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, function (req, res) {
|
|
|
|
res.end('Hello!');
|
|
|
|
}));
|
|
|
|
```
|
|
|
|
|
2015-12-17 00:51:37 +00:00
|
|
|
## Heroku?
|
|
|
|
|
|
|
|
This doesn't work on heroku because heroku uses a proxy with built-in https
|
|
|
|
(which is a smart thing to do) and besides, they want you to pay big bucks
|
|
|
|
for https. (hopefully not for long?...)
|