This commit is contained in:
AJ ONeal 2015-12-12 13:11:05 +00:00
parent 82b10eda4c
commit 145dbad411
10 changed files with 323 additions and 251 deletions

190
README.md
View File

@ -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

59
bin/standalone.js Normal file
View File

@ -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();
});

View File

@ -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;
};

View File

@ -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());
});

View File

@ -32,5 +32,8 @@
"devDependencies": {
"express": "^4.13.3",
"localhost.daplie.com-certificates": "^1.1.2"
},
"dependencies": {
"letsencrypt-python": "^1.0.3"
}
}

View File

@ -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")
};

View File

@ -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();
};