updates
This commit is contained in:
parent
82b10eda4c
commit
145dbad411
190
README.md
190
README.md
|
@ -3,11 +3,195 @@ letsencrypt
|
|||
|
||||
Let's Encrypt for node.js
|
||||
|
||||
### Update: Fri, Dec 11
|
||||
This allows you to get Free SSL Certificates for Automatic HTTPS.
|
||||
|
||||
Committing some stub code.
|
||||
NOT YET PUBLISHED
|
||||
============
|
||||
|
||||
Expect something workable by Tuesday or Wednesday.
|
||||
Dec 12 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
|
||||
=====
|
||||
|
||||
```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('certonly', {
|
||||
, 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
LICENSE
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
'use strict';
|
||||
|
||||
var homedir = require('homedir');
|
||||
var leBinPath = homedir() + '/.local/share/letsencrypt/bin/letsencrypt';
|
||||
var lep = require('letsencrypt-python').create(leBinPath);
|
||||
var conf = {
|
||||
domains: process.argv[2]
|
||||
, email: process.argv[3]
|
||||
, agree: process.argv[4]
|
||||
};
|
||||
|
||||
// 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 le = require('letsencrypt').create(lep, bkDefaults);
|
||||
|
||||
var localCerts = require('localhost.daplie.com-certificates');
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
|
||||
app.use(le.middleware);
|
||||
|
||||
var server = require('http').createServer();
|
||||
server.on('request', app);
|
||||
server.listen(80, function () {
|
||||
console.log('Listening http', server.address());
|
||||
});
|
||||
|
||||
var 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', tlsServer.address());
|
||||
});
|
||||
|
||||
le.register('certonly', {
|
||||
agreeTos: 'agree' === conf.agree
|
||||
, domains: conf.domains.split(',')
|
||||
, email: conf.email
|
||||
}).then(function () {
|
||||
console.log('success');
|
||||
}, function (err) {
|
||||
console.error(err.stack);
|
||||
}).then(function () {
|
||||
server.close();
|
||||
tlsServer.close();
|
||||
});
|
|
@ -1,131 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
var spawn = require('child_process').spawn;
|
||||
|
||||
var letsencrypt = module.exports;
|
||||
|
||||
letsencrypt.parseOptions = function (text) {
|
||||
var options = {};
|
||||
var re = /--([a-z0-9\-]+)/g;
|
||||
var m;
|
||||
|
||||
function uc(match, c) {
|
||||
return c.toUpperCase();
|
||||
}
|
||||
|
||||
while ((m = re.exec(text))) {
|
||||
var key = m[1].replace(/-([a-z0-9])/g, uc);
|
||||
|
||||
options[key] = true;
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
letsencrypt.opts = function (lebinpath, cb) {
|
||||
letsencrypt.exec(lebinpath, ['--help', 'all'], function (err, text) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(null, Object.keys(letsencrypt.parseOptions(text)));
|
||||
});
|
||||
};
|
||||
|
||||
letsencrypt.exec = function (lebinpath, args, opts, cb) {
|
||||
// TODO create and watch the directory for challenge callback
|
||||
if (opts.challengeCallback) {
|
||||
return PromiseA.reject({
|
||||
message: "challengeCallback not yet supported"
|
||||
});
|
||||
}
|
||||
|
||||
var le = spawn(lebinpath, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
var text = '';
|
||||
var errtext = '';
|
||||
var err;
|
||||
|
||||
le.on('error', function (error) {
|
||||
err = error;
|
||||
});
|
||||
|
||||
le.stdout.on('data', function (chunk) {
|
||||
text += chunk.toString('ascii');
|
||||
});
|
||||
|
||||
le.stderr.on('data', function (chunk) {
|
||||
errtext += chunk.toString('ascii');
|
||||
});
|
||||
|
||||
le.on('close', function (code, signal) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errtext) {
|
||||
err = new Error(errtext);
|
||||
err.code = code;
|
||||
err.signal = signal;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (0 !== code) {
|
||||
err = new Error("exited with code '" + code + "'");
|
||||
err.code = code;
|
||||
err.signal = signal;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(null, text);
|
||||
});
|
||||
};
|
||||
|
||||
letsencrypt.objToArr = function (params, opts) {
|
||||
var args = {};
|
||||
var arr = [];
|
||||
|
||||
Object.keys(opts).forEach(function (key) {
|
||||
var val = opts[key];
|
||||
|
||||
if (!val && 0 !== val) {
|
||||
// non-zero value which is false, null, or otherwise falsey
|
||||
// falsey values should not be passed
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params.indexOf(key)) {
|
||||
// key is not recognized by the python client
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
args[key] = opts[key].join(',');
|
||||
} else {
|
||||
args[key] = opts[key];
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(args).forEach(function (key) {
|
||||
if ('tlsSni01Port' === key) {
|
||||
arr.push('--tls-sni-01-port');
|
||||
}
|
||||
else if ('http01Port' === key) {
|
||||
arr.push('--http-01-port');
|
||||
}
|
||||
else {
|
||||
arr.push('--' + key.replace(/([A-Z])/g, '-$1').toLowerCase());
|
||||
}
|
||||
|
||||
if (true !== opts[key]) {
|
||||
// value is truthy, but not true (and falsies were weeded out above)
|
||||
arr.push(opts[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return arr;
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
cacheIpAddresses
|
||||
|
||||
var https = require('https');
|
||||
var http = require('http');
|
||||
var letsencrypt = require('letsencrypt');
|
||||
var localCerts = require('localhost.daplie.com-certificates');
|
||||
var insecureServer;
|
||||
var server;
|
||||
|
||||
letsencrypt.create(
|
||||
'/home/user/.local/share/letsencrypt/bin/letsencrypt'
|
||||
// set some defaults
|
||||
, { "": ""
|
||||
}
|
||||
).then(function (le) {
|
||||
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var getSecureContext = require('./le-standalone').getSecureContext;
|
||||
|
||||
insecureServer = http.createServer();
|
||||
localCerts.sniCallback = function (hostname, cb) {
|
||||
getSecureContext(le, hostname, cb);
|
||||
};
|
||||
server = https.createServer(localCerts);
|
||||
|
||||
insecureServer.on('request', app);
|
||||
|
||||
server.on('request', app);
|
||||
});
|
||||
|
||||
insecureServer.listen(80, function () {
|
||||
console.log('http server listening', insecureServer.address());
|
||||
});
|
||||
|
||||
server.listen(443, function () {
|
||||
console.log('https server listening', server.address());
|
||||
});
|
|
@ -32,5 +32,8 @@
|
|||
"devDependencies": {
|
||||
"express": "^4.13.3",
|
||||
"localhost.daplie.com-certificates": "^1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"letsencrypt-python": "^1.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,5 @@ module.exports = {
|
|||
, webrootPath: path.join(__dirname, "acme-challenge")
|
||||
, configDir: path.join(__dirname, "letsencrypt.config")
|
||||
, workDir: path.join(__dirname, "letsencrypt.work")
|
||||
, logDir: path.join(__dirname, "letsencrypt.log")
|
||||
, logsDir: path.join(__dirname, "letsencrypt.logs")
|
||||
};
|
||||
|
|
|
@ -7,87 +7,84 @@ var http = require('http');
|
|||
var express = require('express');
|
||||
var app = express();
|
||||
|
||||
var config = require('./config');
|
||||
module.exports.create = function (opts) {
|
||||
function getSecureContext(domainname, opts, cb) {
|
||||
|
||||
if (!opts) { opts = {}; }
|
||||
|
||||
opts.key = fs.readFileSync(path.join(opts.configDir, 'live', domainname, 'privkey.pem'));
|
||||
opts.cert = fs.readFileSync(path.join(opts.configDir, 'live', domainname, 'cert.pem'));
|
||||
opts.ca = fs.readFileSync(path.join(opts.configDir, 'live', domainname, 'chain.pem'), 'ascii')
|
||||
.split('-----END CERTIFICATE-----')
|
||||
.filter(function (ca) {
|
||||
return ca.trim();
|
||||
}).map(function (ca) {
|
||||
return (ca + '-----END CERTIFICATE-----').trim();
|
||||
});
|
||||
|
||||
cb(null, require('tls').createSecureContext(opts));
|
||||
}
|
||||
|
||||
|
||||
function getSecureContext(domainname, opts, cb) {
|
||||
var letsetc = '/etc/letsencrypt/live/';
|
||||
// log the requests
|
||||
app.use('/', function (req, res, next) {
|
||||
console.log('[' + req.ip + ']', req.method + ' ' + req.headers.host, req.protocol + req.url);
|
||||
next();
|
||||
});
|
||||
// handle static requests to /.well-known/acme-challenge
|
||||
app.use(
|
||||
'/.well-known/acme-challenge'
|
||||
, express.static(opts.webrootPath, { dotfiles: undefined })
|
||||
);
|
||||
|
||||
if (!opts) { opts = {}; }
|
||||
function serveHttps() {
|
||||
//
|
||||
// SSL Certificates
|
||||
//
|
||||
var server;
|
||||
var localCerts = require('localhost.daplie.com-certificates');
|
||||
var options = {
|
||||
requestCert: false
|
||||
, rejectUnauthorized: true
|
||||
|
||||
opts.key = fs.readFileSync(path.join(letsetc, domainname, 'privkey.pem'));
|
||||
opts.cert = fs.readFileSync(path.join(letsetc, domainname, 'cert.pem'));
|
||||
opts.ca = fs.readFileSync(path.join(letsetc, domainname, 'chain.pem'), 'ascii')
|
||||
.split('-----END CERTIFICATE-----')
|
||||
.filter(function (ca) {
|
||||
return ca.trim();
|
||||
}).map(function (ca) {
|
||||
return (ca + '-----END CERTIFICATE-----').trim();
|
||||
// If you need to use SNICallback you should be using io.js >= 1.x (possibly node >= 0.12)
|
||||
, SNICallback: function (domainname, cb) {
|
||||
var secureContext = getSecureContext(domainname);
|
||||
cb(null, secureContext);
|
||||
}
|
||||
// If you need to support HTTP2 this is what you need to work with
|
||||
//, NPNProtocols: ['http/2.0', 'http/1.1', 'http/1.0']
|
||||
//, NPNProtocols: ['http/1.1']
|
||||
, key: localCerts.key
|
||||
, cert: localCerts.cert
|
||||
//, ca: null
|
||||
};
|
||||
|
||||
// Start the tls sni server4
|
||||
server = https.createServer(options);
|
||||
server.on('error', function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
server.on('request', app);
|
||||
server.listen(opts.tlsSni01Port, function () {
|
||||
console.log('[https] Listening', server.address());
|
||||
});
|
||||
}
|
||||
|
||||
cb(null, require('tls').createSecureContext(opts));
|
||||
}
|
||||
function serveHttp() {
|
||||
// Start the http server4
|
||||
var insecureServer = http.createServer();
|
||||
insecureServer.on('error', function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
// note that request handler must be attached *before* and handle comes in
|
||||
insecureServer.on('request', app);
|
||||
insecureServer.listen(opts.http01Port, function () {
|
||||
console.log('[http] Listening', insecureServer.address());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// log the requests
|
||||
app.use('/', function (req, res, next) {
|
||||
console.log('[' + req.ip + ']', req.method + ' ' + req.headers.host, req.protocol + req.url);
|
||||
next();
|
||||
});
|
||||
// handle static requests to /.well-known/acme-challenge
|
||||
app.use(
|
||||
'/.well-known/acme-challenge'
|
||||
, express.static(config.webrootPath, { dotfiles: undefined })
|
||||
);
|
||||
|
||||
|
||||
function serveHttps() {
|
||||
//
|
||||
// SSL Certificates
|
||||
//
|
||||
var server;
|
||||
var localCerts = require('localhost.daplie.com-certificates');
|
||||
var options = {
|
||||
requestCert: false
|
||||
, rejectUnauthorized: true
|
||||
|
||||
// If you need to use SNICallback you should be using io.js >= 1.x (possibly node >= 0.12)
|
||||
, SNICallback: function (domainname, cb) {
|
||||
var secureContext = getSecureContext(domainname);
|
||||
cb(null, secureContext);
|
||||
}
|
||||
// If you need to support HTTP2 this is what you need to work with
|
||||
//, NPNProtocols: ['http/2.0', 'http/1.1', 'http/1.0']
|
||||
//, NPNProtocols: ['http/1.1']
|
||||
, key: localCerts.key
|
||||
, cert: localCerts.cert
|
||||
//, ca: null
|
||||
};
|
||||
|
||||
// Start the tls sni server4
|
||||
server = https.createServer(options);
|
||||
server.on('error', function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
server.on('request', app);
|
||||
server.listen(config.tlsSni01Port, function () {
|
||||
console.log('[https] Listening', server.address());
|
||||
});
|
||||
}
|
||||
|
||||
function serveHttp() {
|
||||
// Start the http server4
|
||||
var insecureServer = http.createServer();
|
||||
insecureServer.on('error', function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
// note that request handler must be attached *before* and handle comes in
|
||||
insecureServer.on('request', app);
|
||||
insecureServer.listen(config.http01Port, function () {
|
||||
console.log('[http] Listening', insecureServer.address());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
serveHttps();
|
||||
serveHttp();
|
||||
serveHttps();
|
||||
serveHttp();
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue