321 lines
9.3 KiB
JavaScript
321 lines
9.3 KiB
JavaScript
'use strict';
|
|
|
|
var path = require('path');
|
|
var challengeStore = require('./challenge-handlers');
|
|
var createSniCallback = require('./sni-callback').create;
|
|
var LE = require('letsencrypt');
|
|
|
|
function lexHelper(obj, app) {
|
|
var defaultPems = require('localhost.daplie.com-certificates');
|
|
|
|
if (!obj) {
|
|
obj = {};
|
|
}
|
|
|
|
if ('string' === typeof obj) {
|
|
obj = {
|
|
configDir: obj
|
|
};
|
|
}
|
|
|
|
if ('function' === typeof obj) {
|
|
obj = {
|
|
onRequest: obj
|
|
};
|
|
}
|
|
|
|
obj.debug = LEX.debug;
|
|
|
|
if ('function' === typeof app) {
|
|
obj.onRequest = obj.onRequest || app;
|
|
}
|
|
|
|
if (!obj.getChallenge) {
|
|
if (false !== obj.getChallenge) {
|
|
obj.getChallenge = challengeStore.get;
|
|
}
|
|
if (!obj.webrootPath) {
|
|
obj.webrootPath = path.join(require('os').tmpdir(), 'acme-challenge');
|
|
}
|
|
}
|
|
|
|
if (!obj.onRequest && false !== obj.onRequest) {
|
|
console.warn("You should either do args.onRequest = app or server.on('request', app),"
|
|
+ " otherwise only acme-challenge requests will be handled (and the rest will hang)");
|
|
console.warn("You can silence this warning by setting args.onRequest = false");
|
|
}
|
|
|
|
if (!obj.configDir) {
|
|
obj.configDir = path.join(require('homedir')(), '/letsencrypt/etc');
|
|
}
|
|
if (!obj.privkeyPath) {
|
|
obj.privkeyPath = ':config/live/:hostname/privkey.pem';
|
|
}
|
|
if (!obj.fullchainPath) {
|
|
obj.fullchainPath = ':config/live/:hostname/fullchain.pem';
|
|
}
|
|
if (!obj.certPath) {
|
|
obj.certPath = ':config/live/:hostname/cert.pem';
|
|
}
|
|
if (!obj.chainPath) {
|
|
obj.chainPath = ':config/live/:hostname/chain.pem';
|
|
}
|
|
|
|
if (!obj.server) {
|
|
obj.server = LEX.defaultServerUrl;
|
|
}
|
|
|
|
if (!obj.letsencrypt) {
|
|
//LE.merge(obj, );
|
|
// { configDir, webrootPath, server }
|
|
obj.letsencrypt = LE.create(obj, {
|
|
setChallenge: challengeStore.set
|
|
, removeChallenge: challengeStore.remove
|
|
});
|
|
}
|
|
|
|
var httpsOptions = obj.httpsOptions || {};
|
|
var sniCallback = httpsOptions.SNICallback;
|
|
|
|
// https://nodejs.org/api/https.html
|
|
// pfx, key, cert, passphrase, ca, ciphers, rejectUnauthorized, secureProtocol
|
|
if (!httpsOptions.pfx) {
|
|
if (!(httpsOptions.cert || httpsOptions.key)) {
|
|
httpsOptions.key = defaultPems.key;
|
|
httpsOptions.cert = defaultPems.cert;
|
|
}
|
|
else if (!(httpsOptions.cert && httpsOptions.key)) {
|
|
if (!httpsOptions.cert) {
|
|
console.warn("You specified httpsOptions.cert, but not httpsOptions.key");
|
|
}
|
|
if (!httpsOptions.key) {
|
|
console.warn("You specified httpsOptions.key, but not httpsOptions.cert");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!obj.approveRegistration && LEX.defaultApproveRegistration) {
|
|
obj.approveRegistration = function (domain, cb) {
|
|
if (LEX.debug) {
|
|
console.debug('[LEX] auto register against staging server');
|
|
}
|
|
cb(null, {
|
|
email: 'example@gmail.com'
|
|
, domains: [domain]
|
|
, agreeTos: true
|
|
, server: LEX.stagingServerUrl
|
|
});
|
|
};
|
|
}
|
|
|
|
if (obj.sniCallback) {
|
|
if (sniCallback) {
|
|
console.warn("You specified both args.sniCallback and args.httpsOptions.SNICallback,"
|
|
+ " but only args.sniCallback will be used.");
|
|
}
|
|
httpsOptions.SNICallback = obj.sniCallback;
|
|
}
|
|
else if (sniCallback) {
|
|
obj._sniCallback = createSniCallback(obj);
|
|
httpsOptions.SNICallback = function (domain, cb) {
|
|
sniCallback(domain, function (err, context) {
|
|
if (context) {
|
|
cb(err, context);
|
|
return;
|
|
}
|
|
|
|
obj._sniCallback(domain, cb);
|
|
});
|
|
};
|
|
}
|
|
else {
|
|
httpsOptions.SNICallback = createSniCallback(obj);
|
|
}
|
|
|
|
function createAcmeResponder(onRequest) {
|
|
|
|
function httpAcmeResponder(req, res) {
|
|
if (LEX.debug) {
|
|
console.debug('[LEX] ', req.method, req.headers.host, req.url);
|
|
}
|
|
var acmeChallengePrefix = '/.well-known/acme-challenge/';
|
|
|
|
if (0 !== req.url.indexOf(acmeChallengePrefix)) {
|
|
onRequest(req, res);
|
|
return;
|
|
}
|
|
|
|
var key = req.url.slice(acmeChallengePrefix.length);
|
|
|
|
obj.getChallenge(obj, req.headers.host, key, function (err, val) {
|
|
if (LEX.debug) {
|
|
console.debug('[LEX] GET challenge, response:');
|
|
console.debug('challenge:', key);
|
|
console.debug('response:', val);
|
|
if (err) {
|
|
console.debug(err.stack);
|
|
}
|
|
}
|
|
res.end(val || '_');
|
|
});
|
|
}
|
|
|
|
return httpAcmeResponder;
|
|
}
|
|
|
|
obj.httpAcmeResponder = createAcmeResponder(obj.onHttpRequest||obj.onRequest);
|
|
obj.httpsAcmeResponder = createAcmeResponder(obj.onHttpsRequest||obj.onRequest);
|
|
obj.httpsOptions = httpsOptions;
|
|
|
|
return obj;
|
|
}
|
|
|
|
function LEX(obj, app) {
|
|
var https = require('https');
|
|
var http = require('http');
|
|
|
|
function listen(plainPorts, tlsPorts, onListening) {
|
|
if (plainPorts && (!Array.isArray(plainPorts) || !Array.isArray(tlsPorts))) {
|
|
throw new Error(".listen() must be used with plain and tls port arrays, like this: `.listen([80], [443, 5001], function () {})`");
|
|
}
|
|
|
|
var results = {
|
|
plainServers: []
|
|
, tlsServers: []
|
|
};
|
|
|
|
plainPorts = plainPorts || [80];
|
|
tlsPorts = tlsPorts || [443, 5001];
|
|
|
|
function defaultOnListening() {
|
|
/*jshint validthis: true*/
|
|
var server = this;
|
|
var protocol = ('honorCipherOrder' in server || 'rejectUnauthorized' in server) ? 'https': 'http';
|
|
var addr = server.address();
|
|
var port;
|
|
|
|
if (80 === addr.port || 443 === addr.port) {
|
|
port = '';
|
|
} else {
|
|
port = ':' + addr.port;
|
|
}
|
|
console.info('Listening ' + protocol + '://' + addr.address + port + '/');
|
|
}
|
|
|
|
plainPorts.forEach(function (addr) {
|
|
var port = addr.port || addr;
|
|
var address = addr.address || '';
|
|
var server = http.createServer(obj.httpAcmeResponder);
|
|
|
|
server.__le_onListening = addr.onListen;
|
|
server.__le_port = port;
|
|
server.__le_address = address;
|
|
|
|
results.plainServers.push(server);
|
|
});
|
|
|
|
tlsPorts.forEach(function (addr) {
|
|
var port = addr.port || addr;
|
|
var address = addr.address || '';
|
|
var options = addr.httpsOptions || obj.httpsOptions;
|
|
var server = https.createServer(options, obj.httpsAcmeResponder);
|
|
|
|
server.__le_onListen = addr.onListen;
|
|
server.__le_port = port;
|
|
server.__le_address = address;
|
|
|
|
results.tlsServers.push(server);
|
|
});
|
|
|
|
results.plainServers.forEach(function (server) {
|
|
var listen = server.__le_onListening || onListening || defaultOnListening;
|
|
server.listen(server.__le_port, server.__le_address, listen);
|
|
});
|
|
|
|
results.tlsServers.forEach(function (server) {
|
|
var listen = server.__le_onListening || onListening || defaultOnListening;
|
|
server.listen(server.__le_port, server.__le_address, listen);
|
|
});
|
|
|
|
// deleting creates a "slow object", but that's okay (we only use it once)
|
|
return results;
|
|
}
|
|
|
|
obj = lexHelper(obj, app);
|
|
obj.listen = listen;
|
|
|
|
return obj;
|
|
}
|
|
|
|
module.exports = LEX;
|
|
|
|
LEX.create = LEX;
|
|
LEX.setChallenge = challengeStore.set;
|
|
LEX.getChallenge = challengeStore.get;
|
|
LEX.removeChallenge = challengeStore.remove;
|
|
LEX.createSniCallback = createSniCallback;
|
|
// TODO not sure how well this works
|
|
LEX.middleware = function (defaults) {
|
|
var leCore = require('letiny-core');
|
|
var merge = require('letsencrypt/common').merge;
|
|
var tplConfigDir = require('letsencrypt/common').tplConfigDir;
|
|
var tplHostname = require('letsencrypt/common').tplHostname;
|
|
var prefix = leCore.acmeChallengePrefix;
|
|
|
|
tplConfigDir(defaults.configDir || '', defaults);
|
|
|
|
return function (req, res, next) {
|
|
if (LEX.debug) {
|
|
console.debug('[LEX middleware]:', req.hostname, req.url, req.url.slice(prefix.length));
|
|
}
|
|
|
|
if (0 !== req.url.indexOf(prefix)) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
function done(err, token) {
|
|
if (err) {
|
|
res.send("Error: These aren't the tokens you're looking for. Move along.");
|
|
return;
|
|
}
|
|
|
|
res.send(token);
|
|
}
|
|
|
|
var copy = merge(defaults, { domains: [req.hostname] });
|
|
tplHostname(req.hostname, copy);
|
|
|
|
LEX.getChallenge(copy, req.hostname, req.url.slice(prefix.length), done);
|
|
};
|
|
};
|
|
|
|
LEX.stagingServerUrl = LE.stagingServerUrl;
|
|
LEX.productionServerUrl = LE.productionServerUrl || LE.liveServerUrl;
|
|
LEX.defaultServerUrl = LEX.productionServerUrl;
|
|
LEX.testing = function () {
|
|
LEX.debug = true;
|
|
LEX.defaultServerUrl = LEX.stagingServerUrl;
|
|
LEX.defaultApproveRegistration = true;
|
|
console.debug = console.log;
|
|
console.debug('[LEX] testing mode turned on');
|
|
console.debug('[LEX] default server: ' + LEX.defaultServerUrl);
|
|
console.debug('\n');
|
|
console.debug('###################################################');
|
|
console.debug('# #');
|
|
console.debug('# Open up a browser and visit this server #');
|
|
console.debug('# at its domain name. #');
|
|
console.debug('# #');
|
|
console.debug('# ENJOY! #');
|
|
console.debug('# #');
|
|
console.debug('###################################################');
|
|
console.debug('\n');
|
|
console.debug('Note: testing certs will be installed because .testing() was called.');
|
|
console.debug(' remove .testing() to get live certs.');
|
|
console.debug('\n');
|
|
console.debug('[LEX] automatic registration handling turned on for testing.');
|
|
console.debug('\n');
|
|
|
|
return module.exports;
|
|
};
|