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
|
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
|
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": {
|
"devDependencies": {
|
||||||
"express": "^4.13.3",
|
"express": "^4.13.3",
|
||||||
"localhost.daplie.com-certificates": "^1.1.2"
|
"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")
|
, webrootPath: path.join(__dirname, "acme-challenge")
|
||||||
, configDir: path.join(__dirname, "letsencrypt.config")
|
, configDir: path.join(__dirname, "letsencrypt.config")
|
||||||
, workDir: path.join(__dirname, "letsencrypt.work")
|
, workDir: path.join(__dirname, "letsencrypt.work")
|
||||||
, logDir: path.join(__dirname, "letsencrypt.log")
|
, logsDir: path.join(__dirname, "letsencrypt.logs")
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,17 +7,14 @@ var http = require('http');
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
var config = require('./config');
|
module.exports.create = function (opts) {
|
||||||
|
|
||||||
|
|
||||||
function getSecureContext(domainname, opts, cb) {
|
function getSecureContext(domainname, opts, cb) {
|
||||||
var letsetc = '/etc/letsencrypt/live/';
|
|
||||||
|
|
||||||
if (!opts) { opts = {}; }
|
if (!opts) { opts = {}; }
|
||||||
|
|
||||||
opts.key = fs.readFileSync(path.join(letsetc, domainname, 'privkey.pem'));
|
opts.key = fs.readFileSync(path.join(opts.configDir, 'live', domainname, 'privkey.pem'));
|
||||||
opts.cert = fs.readFileSync(path.join(letsetc, domainname, 'cert.pem'));
|
opts.cert = fs.readFileSync(path.join(opts.configDir, 'live', domainname, 'cert.pem'));
|
||||||
opts.ca = fs.readFileSync(path.join(letsetc, domainname, 'chain.pem'), 'ascii')
|
opts.ca = fs.readFileSync(path.join(opts.configDir, 'live', domainname, 'chain.pem'), 'ascii')
|
||||||
.split('-----END CERTIFICATE-----')
|
.split('-----END CERTIFICATE-----')
|
||||||
.filter(function (ca) {
|
.filter(function (ca) {
|
||||||
return ca.trim();
|
return ca.trim();
|
||||||
|
@ -37,10 +34,9 @@ app.use('/', function (req, res, next) {
|
||||||
// handle static requests to /.well-known/acme-challenge
|
// handle static requests to /.well-known/acme-challenge
|
||||||
app.use(
|
app.use(
|
||||||
'/.well-known/acme-challenge'
|
'/.well-known/acme-challenge'
|
||||||
, express.static(config.webrootPath, { dotfiles: undefined })
|
, express.static(opts.webrootPath, { dotfiles: undefined })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
function serveHttps() {
|
function serveHttps() {
|
||||||
//
|
//
|
||||||
// SSL Certificates
|
// SSL Certificates
|
||||||
|
@ -70,7 +66,7 @@ function serveHttps() {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
server.on('request', app);
|
server.on('request', app);
|
||||||
server.listen(config.tlsSni01Port, function () {
|
server.listen(opts.tlsSni01Port, function () {
|
||||||
console.log('[https] Listening', server.address());
|
console.log('[https] Listening', server.address());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -83,7 +79,7 @@ function serveHttp() {
|
||||||
});
|
});
|
||||||
// note that request handler must be attached *before* and handle comes in
|
// note that request handler must be attached *before* and handle comes in
|
||||||
insecureServer.on('request', app);
|
insecureServer.on('request', app);
|
||||||
insecureServer.listen(config.http01Port, function () {
|
insecureServer.listen(opts.http01Port, function () {
|
||||||
console.log('[http] Listening', insecureServer.address());
|
console.log('[http] Listening', insecureServer.address());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -91,3 +87,4 @@ function serveHttp() {
|
||||||
|
|
||||||
serveHttps();
|
serveHttps();
|
||||||
serveHttp();
|
serveHttp();
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue