243 lines
6.4 KiB
Markdown
243 lines
6.4 KiB
Markdown
letsencrypt
|
|
===========
|
|
|
|
Let's Encrypt for node.js
|
|
|
|
This allows you to get Free SSL Certificates for Automatic HTTPS.
|
|
|
|
NOT YET PUBLISHED
|
|
============
|
|
|
|
Dec 12 2015: gettin' really close
|
|
Dec 11 2015: almost done
|
|
|
|
Install
|
|
=======
|
|
|
|
```bash
|
|
npm install --save letsencrypt
|
|
```
|
|
|
|
Right now this uses [`letsencrypt-python`](https://github.com/Daplie/node-letsencrypt-python),
|
|
but it's built to be able to use a pure javasript version.
|
|
|
|
```bash
|
|
# install the python client (takes 2 minutes normally, 20 on a rasberry pi)
|
|
git clone https://github.com/letsencrypt/letsencrypt
|
|
pushd letsencrypt
|
|
|
|
./letsencrypt-auto
|
|
```
|
|
|
|
Usage
|
|
=====
|
|
|
|
* `Letsencrypt.create(backend, bkDefaults);`
|
|
* { webrootPath, configDir, fullchainTpl, privkeyTpl }
|
|
* `le.middleware();`
|
|
* `le.sniCallback(hostname, function (err, tlsContext) {});`
|
|
* `le.register({ domains, email, agreeTos, ... })` returns promise
|
|
<!-- * `le.validate(args)` -->
|
|
<!-- * `le.fetch(args, cb)` -->
|
|
|
|
```javascript
|
|
var leBinPath = '/home/user/.local/share/letsencrypt/bin/letsencrypt';
|
|
var lep = require('letsencrypt-python').create(leBinPath);
|
|
|
|
// backend-specific defaults
|
|
// Note: For legal reasons you should NOT set email or agreeTos as a default
|
|
var bkDefaults = {
|
|
webroot: true
|
|
, webrootPath: __dirname, '/acme-challenge'
|
|
, fullchainTpl: '/live/:hostname/fullchain.pem'
|
|
, privkeyTpl: '/live/:hostname/fullchain.pem'
|
|
, configDir: '/etc/letsencrypt'
|
|
, logsDir: '/var/log/letsencrypt'
|
|
, workDir: '/var/lib/letsencrypt'
|
|
, text: true
|
|
};
|
|
var leConfig = {
|
|
, webrootPath: __dirname, '/acme-challenge'
|
|
, configDir: '/etc/letsencrypt'
|
|
};
|
|
var le = require('letsencrypt').create(le, bkDefaults, leConfig);
|
|
|
|
|
|
```
|
|
|
|
```javascript
|
|
var leBinPath = '/home/user/.local/share/letsencrypt/bin/letsencrypt';
|
|
var lep = require('letsencrypt-python').create(leBinPath);
|
|
|
|
// backend-specific defaults
|
|
// Note: For legal reasons you should NOT set email or agreeTos as a default
|
|
var bkDefaults = {
|
|
webroot: true
|
|
, webrootPath: __dirname, '/acme-challenge'
|
|
, fullchainTpl: '/live/:hostname/fullchain.pem'
|
|
, privkeyTpl: '/live/:hostname/fullchain.pem'
|
|
, configDir: '/etc/letsencrypt'
|
|
, logsDir: '/var/log/letsencrypt'
|
|
, workDir: '/var/lib/letsencrypt'
|
|
, text: true
|
|
};
|
|
var leConfig = {
|
|
, webrootPath: __dirname, '/acme-challenge'
|
|
, configDir: '/etc/letsencrypt'
|
|
};
|
|
var le = require('letsencrypt').create(le, bkDefaults, leConfig);
|
|
|
|
var localCerts = require('localhost.daplie.com-certificates');
|
|
var express = require('express');
|
|
var app = express();
|
|
|
|
app.use(le.middleware);
|
|
|
|
server = require('http').createServer();
|
|
server.on('request', app);
|
|
server.listen(80, function () {
|
|
console.log('Listening http', server.address());
|
|
});
|
|
|
|
tlsServer = require('https').createServer({
|
|
key: localCerts.key
|
|
, cert: localCerts.cert
|
|
, SNICallback: le.SNICallback
|
|
});
|
|
tlsServer.on('request', app);
|
|
tlsServer.listen(443, function () {
|
|
console.log('Listening http', server.address());
|
|
});
|
|
|
|
le.register({
|
|
, domains: ['example.com']
|
|
, agreeTos: true
|
|
, email: 'user@example.com'
|
|
}).then(function () {
|
|
server.close();
|
|
tlsServer.close();
|
|
});
|
|
```
|
|
|
|
```
|
|
lep.register('certonly', {
|
|
, domains: ['example.com']
|
|
, agreeTos: true
|
|
, email: 'user@example.com'
|
|
|
|
, configDir: '/etc/letsencrypt'
|
|
, logsDir: '/var/log/letsencrypt'
|
|
, workDir: '/var/lib/letsencrypt'
|
|
, text: true
|
|
});
|
|
```
|
|
|
|
```
|
|
// if you would like to register new domains on-the-fly
|
|
// you can use this function to return the user to which
|
|
// it should be registered (or null if none)
|
|
, needsRegistration: function (hostname, cb) {
|
|
cb(null, {
|
|
agreeTos: true
|
|
, email: 'user@example.com'
|
|
});
|
|
}
|
|
```
|
|
|
|
Backends
|
|
--------
|
|
|
|
* [`letsencrypt-python`](https://github.com/Daplie/node-letsencrypt-python) (complete)
|
|
* [`lejs`](https://github.com/Daplie/node-lejs) (in progress)
|
|
|
|
#### How to write a backend
|
|
|
|
A backend must implement (or be wrapped to implement) this API:
|
|
|
|
* fetch(hostname, cb) will cb(err, certs) will get registered certs or null unless there is an error
|
|
* register(args, challengeCb, done) will register and or renew a cert
|
|
* args = `{ domains, email, agreeTos }` MUST check that agreeTos === true
|
|
* challengeCb = `function (challenge, cb) { }` handle challenge as needed, call cb()
|
|
|
|
This is what `args` looks like:
|
|
|
|
```javascript
|
|
{ domains: ['example.com', 'www.example.com']
|
|
, email: 'user@email.com'
|
|
, agreeTos: true
|
|
, configDir: '/etc/letsencrypt'
|
|
, fullchainTpl: '/live/:hostname/fullchain.pem' // :hostname will be replaced with the domainname
|
|
, privkeyTpl: '/live/:hostname/privkey.pem' // :hostname
|
|
}
|
|
```
|
|
|
|
This is what the implementation should look like:
|
|
|
|
(it's expected that the client will follow the same conventions as
|
|
the python client, but it's not necessary)
|
|
|
|
```javascript
|
|
return {
|
|
fetch: function (args, cb) {
|
|
// NOTE: should return an error if args.domains cannot be satisfied with a single cert
|
|
// (usually example.com and www.example.com will be handled on the same cert, for example)
|
|
if (errorHappens) {
|
|
// return an error if there is an actual error (db, etc)
|
|
cb(err);
|
|
return;
|
|
}
|
|
// return null if there is no error, nor a certificate
|
|
else if (!cert) {
|
|
cb(null, null);
|
|
return;
|
|
}
|
|
|
|
// NOTE: if the certificate is available but expired it should be
|
|
// returned and the calling application will decide to renew when
|
|
// it is convenient
|
|
|
|
// NOTE: the application should handle caching, not the library
|
|
|
|
// return the cert with metadata
|
|
cb(null, {
|
|
cert: "/*contcatonated certs in pem format: cert + intermediate*/"
|
|
, key: "/*private keypair in pem format*/"
|
|
, renewedAt: new Date() // fs.stat cert.pem should also work
|
|
, expiresIn: 90 * 60 // assumes 90-days unless specified
|
|
});
|
|
}
|
|
, register: function (args, challengeCallback, completeCallback) {
|
|
// **MUST** reject if args.agreeTos is not true
|
|
|
|
// once you're ready for the caller to know the challenge
|
|
if (challengeCallback) {
|
|
challengeCallback(challenge, function () {
|
|
continueRegistration();
|
|
})
|
|
} else {
|
|
continueRegistration();
|
|
}
|
|
|
|
function continueRegistration() {
|
|
// it is not neccessary to to return the certificates here
|
|
// the client will call fetch() when it needs them
|
|
completeCallback(err);
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
See Also
|
|
========
|
|
|
|
* [Let's Encrypt in (exactly) 90 seconds with Caddy](https://daplie.com/articles/lets-encrypt-in-literally-90-seconds/)
|
|
* Let's Encrypt for golang [lego](https://github.com/xenolf/lego)
|
|
|
|
|
|
LICENSE
|
|
=======
|
|
|
|
Dual-licensed MIT and Apache-2.0
|
|
|
|
See LICENSE
|