v2.6.4: simplify existing defaults, add default servername support
This commit is contained in:
parent
b17805d1fb
commit
779ab234ac
79
README.md
79
README.md
|
@ -114,43 +114,39 @@ 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
|
|
||||||
// https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
, server: 'https://acme-v02.api.letsencrypt.org/directory'
|
|
||||||
|
|
||||||
// Where the certs will be saved, MUST have write access
|
|
||||||
, configDir: '~/.config/acme/'
|
|
||||||
|
|
||||||
// You MUST change this to a valid email address
|
|
||||||
, email: 'john.doe@example.com'
|
|
||||||
|
|
||||||
// You MUST change these to valid domains
|
|
||||||
// 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.end('Hello, World!\n\n💚 🔒.js');
|
|
||||||
})
|
|
||||||
|
|
||||||
// Join the community to get notified of important updates
|
|
||||||
, communityMember: true
|
|
||||||
|
|
||||||
// Contribute telemetry data to the project
|
|
||||||
, telemetry: true
|
|
||||||
|
|
||||||
//, debug: true
|
//, debug: true
|
||||||
|
|
||||||
}).listen(80, 443);
|
}).listen(80, 443);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`app.js`:
|
||||||
|
```js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var express = require('express');
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
app.use('/', function (req, res) {
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
|
res.end('Hello, World!\n\n💚 🔒.js');
|
||||||
|
})
|
||||||
|
|
||||||
|
// Don't do this:
|
||||||
|
// app.listen(3000)
|
||||||
|
|
||||||
|
// Do this instead:
|
||||||
|
module.exports = app;
|
||||||
|
```
|
||||||
|
|
||||||
### `communityMember`
|
### `communityMember`
|
||||||
|
|
||||||
If you're the kind of person that likes the kinds of stuff that I do,
|
If you're the kind of person that likes the kinds of stuff that I do,
|
||||||
|
@ -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
|
||||||
|
|
214
index.js
214
index.js
|
@ -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,53 +41,54 @@ module.exports.create = function (opts) {
|
||||||
var server;
|
var server;
|
||||||
var validHttpPort = (parseInt(p, 10) >= 0);
|
var validHttpPort = (parseInt(p, 10) >= 0);
|
||||||
|
|
||||||
function tryPlain() {
|
|
||||||
server = require('http').createServer(
|
|
||||||
greenlock.middleware.sanitizeHost(greenlock.middleware(require('redirect-https')()))
|
|
||||||
);
|
|
||||||
httpType = 'http';
|
|
||||||
}
|
|
||||||
|
|
||||||
function trySecure() {
|
|
||||||
var https;
|
|
||||||
try {
|
|
||||||
https = require('spdy');
|
|
||||||
greenlock.tlsOptions.spdy = { protocols: [ 'h2', 'http/1.1' ], plain: false };
|
|
||||||
httpType = 'http2 (spdy/h2)';
|
|
||||||
} catch(e) {
|
|
||||||
https = require('https');
|
|
||||||
httpType = 'https';
|
|
||||||
}
|
|
||||||
server = https.createServer(
|
|
||||||
greenlock.tlsOptions
|
|
||||||
, greenlock.middleware.sanitizeHost(function (req, res) {
|
|
||||||
try {
|
|
||||||
greenlock.app(req, res);
|
|
||||||
} catch(e) {
|
|
||||||
console.error("[error] [greenlock.app] Your HTTP handler had an uncaught error:");
|
|
||||||
console.error(e);
|
|
||||||
try {
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end("Internal Server Error: [Greenlock] HTTP exception logged for user-provided handler.");
|
|
||||||
} catch(e) {
|
|
||||||
// ignore
|
|
||||||
// (headers may have already been sent, etc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
server.type = httpType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addr) { args[1] = addr; }
|
if (addr) { args[1] = addr; }
|
||||||
if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) {
|
if (!validHttpPort && !/(\/)|(\\\\)/.test(p)) {
|
||||||
console.warn("'" + p + "' doesn't seem to be a valid port number, socket path, or pipe");
|
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) {
|
server = require('http').createServer(
|
||||||
|
greenlock.middleware.sanitizeHost(greenlock.middleware(require('redirect-https')()))
|
||||||
|
);
|
||||||
|
httpType = 'http';
|
||||||
|
|
||||||
|
return { server: server, listen: function () { return new Promise(function (resolve, reject) {
|
||||||
args[0] = p;
|
args[0] = p;
|
||||||
args.push(function () { resolve(server); });
|
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) {
|
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 +96,90 @@ module.exports.create = function (opts) {
|
||||||
process.exit(41);
|
process.exit(41);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}); } };
|
||||||
|
}
|
||||||
|
|
||||||
promise.server = server;
|
function _create(port) {
|
||||||
return promise;
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
var https;
|
||||||
|
try {
|
||||||
|
https = require('spdy');
|
||||||
|
greenlock.tlsOptions.spdy = { protocols: [ 'h2', 'http/1.1' ], plain: false };
|
||||||
|
httpType = 'http2 (spdy/h2)';
|
||||||
|
} catch(e) {
|
||||||
|
https = require('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(
|
||||||
|
greenlock.tlsOptions
|
||||||
|
, greenlock.middleware.sanitizeHost(function (req, res) {
|
||||||
|
try {
|
||||||
|
greenlock.app(req, res);
|
||||||
|
} catch(e) {
|
||||||
|
console.error("[error] [greenlock.app] Your HTTP handler had an uncaught error:");
|
||||||
|
console.error(e);
|
||||||
|
try {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end("Internal Server Error: [Greenlock] HTTP exception logged for user-provided handler.");
|
||||||
|
} catch(e) {
|
||||||
|
// ignore
|
||||||
|
// (headers may have already been sent, etc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
server.type = httpType;
|
||||||
|
|
||||||
|
return { server: server, listen: function () { return new PromiseA(function (resolve) {
|
||||||
|
args[0] = p;
|
||||||
|
args.push(function () { resolve(/*server*/); });
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}); } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,28 +202,34 @@ 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) {
|
||||||
// Report plain http status
|
if (tlsOptions) {
|
||||||
if ('function' === typeof fnPlain) {
|
server.setSecureContext(tlsOptions);
|
||||||
fnPlain.apply(plainServer);
|
server._hasDefaultSecureContext = true;
|
||||||
} else if (!fn && !plainServer.listenerCount('listening') && !server.listenerCount('listening')) {
|
|
||||||
console.info('[:' + (plainServer.address().port || plainServer.address())
|
|
||||||
+ "] Handling ACME challenges and redirecting to " + server.type);
|
|
||||||
}
|
}
|
||||||
|
return obj2.listen().then(function () {
|
||||||
|
// Report plain http status
|
||||||
|
if ('function' === typeof fnPlain) {
|
||||||
|
fnPlain.apply(plainServer);
|
||||||
|
} else if (!fn && !plainServer.listenerCount('listening') && !server.listenerCount('listening')) {
|
||||||
|
console.info('[:' + (plainServer.address().port || plainServer.address())
|
||||||
|
+ "] Handling ACME challenges and redirecting to " + server.type);
|
||||||
|
}
|
||||||
|
|
||||||
// Report h2/https status
|
// Report h2/https status
|
||||||
if ('function' === typeof fn) {
|
if ('function' === typeof fn) {
|
||||||
fn.apply(server);
|
fn.apply(server);
|
||||||
} else if (!server.listenerCount('listening')) {
|
} else if (!server.listenerCount('listening')) {
|
||||||
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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue