v2.6.4: simplify existing defaults, add default servername support

This commit is contained in:
AJ ONeal 2018-12-22 07:37:16 -07:00
parent b17805d1fb
commit 779ab234ac
3 changed files with 190 additions and 107 deletions

View File

@ -114,41 +114,37 @@ All you have to do is start the webserver and then visit it at its domain name.
'use strict'; 'use strict';
require('greenlock-express').create({ require('greenlock-express').create({
email: 'john.doe@example.com' // The email address of the ACME user / hosting provider
, agreeTos: true // You must accept the ToS as the host which handles the certs
, configDir: '~/.config/acme/' // Writable directory where certs will be saved
, communityMember: true // Join the community to get notified of important updates
, telemetry: true // Contribute telemetry data to the project
// Let's Encrypt v2 is ACME draft 11 // Using your express app:
version: 'draft-11' // simply export it as-is, then include it here
, app: require('./app.js')
// Note: If at first you don't succeed, switch to staging to debug //, debug: true
// https://acme-staging-v02.api.letsencrypt.org/directory }).listen(80, 443);
, server: 'https://acme-v02.api.letsencrypt.org/directory' ```
// Where the certs will be saved, MUST have write access `app.js`:
, configDir: '~/.config/acme/' ```js
'use strict';
// You MUST change this to a valid email address var express = require('express');
, email: 'john.doe@example.com' var app = express();
// You MUST change these to valid domains app.use('/', function (req, res) {
// NOTE: all domains will validated and listed on the certificate
, approvedDomains: [ 'example.com', 'www.example.com' ]
// You MUST NOT build clients that accept the ToS without asking the user
, agreeTos: true
, app: require('express')().use('/', function (req, res) {
res.setHeader('Content-Type', 'text/html; charset=utf-8') res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end('Hello, World!\n\n💚 🔒.js'); res.end('Hello, World!\n\n💚 🔒.js');
}) })
// Join the community to get notified of important updates // Don't do this:
, communityMember: true // app.listen(3000)
// Contribute telemetry data to the project // Do this instead:
, telemetry: true module.exports = app;
//, debug: true
}).listen(80, 443);
``` ```
### `communityMember` ### `communityMember`
@ -181,7 +177,6 @@ Double check the following:
* You MUST set `email` to a **valid address** * You MUST set `email` to a **valid address**
* MX records must validate (`dig MX example.com` for `'john@example.com'`) * MX records must validate (`dig MX example.com` for `'john@example.com'`)
* **valid DNS records** * **valid DNS records**
* You MUST set `approveDomains` to real domains
* Must have public DNS records (test with `dig +trace A example.com; dig +trace www.example.com` for `[ 'example.com', 'www.example.com' ]`) * Must have public DNS records (test with `dig +trace A example.com; dig +trace www.example.com` for `[ 'example.com', 'www.example.com' ]`)
* **write access** * **write access**
* You MUST set `configDir` to a writeable location (test with `touch ~/acme/etc/tmp.tmp`) * You MUST set `configDir` to a writeable location (test with `touch ~/acme/etc/tmp.tmp`)
@ -320,6 +315,10 @@ var glx = require('greenlock-express').create({
// Contribute telemetry data to the project // Contribute telemetry data to the project
, telemetry: true , telemetry: true
// the default servername to use when the client doesn't specify
// (because some IoT devices don't support servername indication)
, servername: 'example.com'
, approveDomains: approveDomains , approveDomains: approveDomains
}); });
@ -345,6 +344,10 @@ var http01 = require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challen
function approveDomains(opts, certs, cb) { function approveDomains(opts, certs, cb) {
// This is where you check your database and associated // This is where you check your database and associated
// email addresses with domains and agreements and such // email addresses with domains and agreements and such
// if (!isAllowed(opts.domains)) { return cb(new Error("not allowed")); }
// The domains being approved for the first time are listed in opts.domains
// Certs being renewed are listed in certs.altnames (if that's useful)
// Opt-in to submit stats and get important updates // Opt-in to submit stats and get important updates
opts.communityMember = true; opts.communityMember = true;
@ -352,11 +355,6 @@ function approveDomains(opts, certs, cb) {
// If you wish to replace the default challenge plugin, you may do so here // If you wish to replace the default challenge plugin, you may do so here
opts.challenges = { 'http-01': http01 }; opts.challenges = { 'http-01': http01 };
// The domains being approved for the first time are listed in opts.domains
// Certs being renewed are listed in certs.altnames
if (certs) {
opts.domains = [certs.subject].concat(certs.altnames);
}
opts.email = 'john.doe@example.com'; opts.email = 'john.doe@example.com';
opts.agreeTos = true; opts.agreeTos = true;
@ -388,11 +386,10 @@ require('https').createServer(glx.httpsOptions, app).listen(443, function () {
}); });
``` ```
**Security Warning**: **Security**:
If you don't do proper checks in `approveDomains(opts, certs, cb)` Greenlock will do a self-check on all domain registrations
an attacker will spoof SNI packets with bad hostnames and that will to prevent you from hitting rate limits.
cause you to be rate-limited and or blocked from the ACME server.
# API # API

130
index.js
View File

@ -30,7 +30,7 @@ module.exports.create = function (opts) {
console.error(e.code + ": '" + e.address + ":" + e.port + "'"); console.error(e.code + ": '" + e.address + ":" + e.port + "'");
} }
function _listen(plainPort, plain) { function _createPlain(plainPort) {
if (!plainPort) { plainPort = 80; } if (!plainPort) { plainPort = 80; }
var parts = String(plainPort).split(':'); var parts = String(plainPort).split(':');
@ -41,14 +41,80 @@ module.exports.create = function (opts) {
var server; var server;
var validHttpPort = (parseInt(p, 10) >= 0); var validHttpPort = (parseInt(p, 10) >= 0);
function tryPlain() { if (addr) { args[1] = addr; }
if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) {
console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe");
}
server = require('http').createServer( server = require('http').createServer(
greenlock.middleware.sanitizeHost(greenlock.middleware(require('redirect-https')())) greenlock.middleware.sanitizeHost(greenlock.middleware(require('redirect-https')()))
); );
httpType = 'http'; httpType = 'http';
return { server: server, listen: function () { return new Promise(function (resolve, reject) {
args[0] = p;
args.push(function () {
if (!greenlock.servername) {
if (Array.isArray(greenlock.approvedDomains) && greenlock.approvedDomains.length) {
greenlock.servername = greenlock.approvedDomains[0];
}
if (Array.isArray(greenlock.approveDomains) && greenlock.approvedDomains.length) {
greenlock.servername = greenlock.approvedDomains[0];
}
}
if (!greenlock.servername) {
resolve(null);
return;
}
return greenlock.check({ domains: [ greenlock.servername ] }).then(function (certs) {
if (certs) {
console.info("Using '%s' as default certificate", greenlock.servername);
return {
key: Buffer.from(certs.privkey, 'ascii')
, cert: Buffer.from(certs.cert + '\r\n' + certs.chain, 'ascii')
};
}
console.info("Fetching certificate for '%s' to use as default for HTTPS server...", greenlock.servername);
return new PromiseA(function (resolve, reject) {
// using SNICallback because all options will be set
greenlock.tlsOptions.SNICallback(greenlock.servername, function (err/*, secureContext*/) {
if (err) { reject(err); return; }
return greenlock.check({ domains: [ greenlock.servername ] }).then(function (certs) {
resolve({
key: Buffer.from(certs.privkey, 'ascii')
, cert: Buffer.from(certs.cert + '\r\n' + certs.chain, 'ascii')
});
}).catch(reject);
});
});
}).then(resolve).catch(reject);
});
server.listen.apply(server, args).on('error', function (e) {
if (server.listenerCount('error') < 2) {
console.warn("Did not successfully create http server and bind to port '" + p + "':");
explainError(e);
process.exit(41);
}
});
}); } };
}
function _create(port) {
if (!port) { port = 443; }
var parts = String(port).split(':');
var p = parts.pop();
var addr = parts.join(':').replace(/^\[/, '').replace(/\]$/, '');
var args = [];
var httpType;
var server;
var validHttpPort = (parseInt(p, 10) >= 0);
if (addr) { args[1] = addr; }
if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) {
console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe");
} }
function trySecure() {
var https; var https;
try { try {
https = require('spdy'); https = require('spdy');
@ -58,6 +124,31 @@ module.exports.create = function (opts) {
https = require('https'); https = require('https');
httpType = 'https'; httpType = 'https';
} }
var sniCallback = greenlock.tlsOptions.SNICallback;
greenlock.tlsOptions.SNICallback = function (domain, cb) {
sniCallback(domain, function (err, context) {
cb(err, context);
if (!context || server._hasDefaultSecureContext) {
return;
}
return greenlock.check({ domains: [ domain ] }).then(function (certs) {
// ignore the case that check doesn't have all the right args here
// to get the same certs that it just got (eventually the right ones will come in)
if (!certs) { return; }
console.info("Using '%s' as default certificate", domain);
server.setSecureContext({
key: Buffer.from(certs.privkey, 'ascii')
, cert: Buffer.from(certs.cert + '\r\n' + certs.chain, 'ascii')
});
server._hasDefaultSecureContext = true;
}).catch(function (/*e*/) {
// this may be that the test.example.com was requested, but it's listed
// on the cert for demo.example.com which is in its own directory, not the other
//console.warn("Unusual error: couldn't get newly authorized certificate:");
//console.warn(e.message);
});
});
};
server = https.createServer( server = https.createServer(
greenlock.tlsOptions greenlock.tlsOptions
, greenlock.middleware.sanitizeHost(function (req, res) { , greenlock.middleware.sanitizeHost(function (req, res) {
@ -77,17 +168,10 @@ module.exports.create = function (opts) {
}) })
); );
server.type = httpType; server.type = httpType;
}
if (addr) { args[1] = addr; } return { server: server, listen: function () { return new PromiseA(function (resolve) {
if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) {
console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe");
}
if (plain) { tryPlain(); } else { trySecure(); }
var promise = new PromiseA(function (resolve) {
args[0] = p; args[0] = p;
args.push(function () { resolve(server); }); args.push(function () { resolve(/*server*/); });
server.listen.apply(server, args).on('error', function (e) { server.listen.apply(server, args).on('error', function (e) {
if (server.listenerCount('error') < 2) { if (server.listenerCount('error') < 2) {
console.warn("Did not successfully create http server and bind to port '" + p + "':"); console.warn("Did not successfully create http server and bind to port '" + p + "':");
@ -95,10 +179,7 @@ module.exports.create = function (opts) {
process.exit(41); process.exit(41);
} }
}); });
}); }); } };
promise.server = server;
return promise;
} }
// NOTE: 'greenlock' is just 'opts' renamed // NOTE: 'greenlock' is just 'opts' renamed
@ -111,7 +192,6 @@ module.exports.create = function (opts) {
} }
opts.listen = function (plainPort, port, fnPlain, fn) { opts.listen = function (plainPort, port, fnPlain, fn) {
var promises = [];
var server; var server;
var plainServer; var plainServer;
@ -122,13 +202,18 @@ module.exports.create = function (opts) {
fnPlain = null; fnPlain = null;
} }
promises.push(_listen(plainPort, true)); var obj1 = _createPlain(plainPort, true);
promises.push(_listen(port, false)); var obj2 = _create(port, false);
server = promises[1].server; plainServer = obj1.server;
plainServer = promises[0].server; server = obj2.server;
PromiseA.all(promises).then(function () { server.then = obj1.listen().then(function (tlsOptions) {
if (tlsOptions) {
server.setSecureContext(tlsOptions);
server._hasDefaultSecureContext = true;
}
return obj2.listen().then(function () {
// Report plain http status // Report plain http status
if ('function' === typeof fnPlain) { if ('function' === typeof fnPlain) {
fnPlain.apply(plainServer); fnPlain.apply(plainServer);
@ -144,6 +229,7 @@ module.exports.create = function (opts) {
console.info('[:' + (server.address().port || server.address()) + "] Serving " + server.type); console.info('[:' + (server.address().port || server.address()) + "] Serving " + server.type);
} }
}); });
}).then;
server.unencrypted = plainServer; server.unencrypted = plainServer;
return server; return server;

View File

@ -1,6 +1,6 @@
{ {
"name": "greenlock-express", "name": "greenlock-express",
"version": "2.5.0", "version": "2.6.4",
"description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.", "description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.",
"main": "index.js", "main": "index.js",
"homepage": "https://git.coolaj86.com/coolaj86/greenlock-express.js", "homepage": "https://git.coolaj86.com/coolaj86/greenlock-express.js",
@ -8,7 +8,7 @@
"example": "examples" "example": "examples"
}, },
"dependencies": { "dependencies": {
"greenlock": "^2.5.0", "greenlock": "^2.6.7",
"le-challenge-fs": "^2.0.8", "le-challenge-fs": "^2.0.8",
"le-sni-auto": "^2.1.4", "le-sni-auto": "^2.1.4",
"le-store-certbot": "^2.1.0", "le-store-certbot": "^2.1.0",