🔐 Free SSL, Free Wildcard SSL, and Fully Automated HTTPS for node.js, issued by Let's Encrypt v2 via ACME. Issues and PRs on Github.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

870 lines
23 KiB

'use strict';
/*global Promise*/
require('./lib/compat.js');
// I hate this code so much.
// Soooo many shims for backwards compatibility (some stuff dating back to v1)
// v3 will be a clean break and I'll delete half of the code...
var DAY = 24 * 60 * 60 * 1000;
//var MIN = 60 * 1000;
var ACME = require('acme-v2/compat').ACME;
var pkg = require('./package.json');
var util = require('util');
function promisifyAllSelf(obj) {
if (obj.__promisified) {
return obj;
}
Object.keys(obj).forEach(function(key) {
if ('function' === typeof obj[key] && !/Async$/.test(key)) {
obj[key + 'Async'] = util.promisify(obj[key]);
}
});
obj.__promisified = true;
return obj;
}
function promisifyAllStore(obj) {
Object.keys(obj).forEach(function(key) {
if ('function' !== typeof obj[key] || /Async$/.test(key)) {
return;
}
var p;
if (0 === obj[key].length || 1 === obj[key].length) {
// wrap just in case it's synchronous (or improperly throws)
p = function(opts) {
return Promise.resolve().then(function() {
return obj[key](opts);
});
};
} else {
p = util.promisify(obj[key]);
}
// internal backwards compat
obj[key + 'Async'] = p;
});
obj.__promisified = true;
return obj;
}
var Greenlock = module.exports;
Greenlock.Greenlock = Greenlock;
Greenlock.LE = Greenlock;
// in-process cache, shared between all instances
var ipc = {};
function _log(debug) {
if (debug) {
var args = Array.prototype.slice.call(arguments);
args.shift();
args.unshift('[gl/index.js]');
console.log.apply(console, args);
}
}
Greenlock.defaults = {
productionServerUrl: 'https://acme-v01.api.letsencrypt.org/directory',
stagingServerUrl: 'https://acme-staging.api.letsencrypt.org/directory',
rsaKeySize: ACME.rsaKeySize || 2048,
challengeType: ACME.challengeType || 'http-01',
challengeTypes: ACME.challengeTypes || ['http-01', 'dns-01'],
acmeChallengePrefix: ACME.acmeChallengePrefix
};
// backwards compat
Object.keys(Greenlock.defaults).forEach(function(key) {
Greenlock[key] = Greenlock.defaults[key];
});
// show all possible options
var u; // undefined
Greenlock._undefined = {
acme: u,
store: u,
//, challenge: u
challenges: u,
sni: u,
tlsOptions: u,
register: u,
check: u,
renewWithin: u, // le-auto-sni and core
//, renewBy: u // le-auto-sni
acmeChallengePrefix: u,
rsaKeySize: u,
challengeType: u,
server: u,
version: u,
agreeToTerms: u,
_ipc: u,
duplicate: u,
_acmeUrls: u
};
Greenlock._undefine = function(gl) {
Object.keys(Greenlock._undefined).forEach(function(key) {
if (!(key in gl)) {
gl[key] = u;
}
});
return gl;
};
Greenlock.create = function(gl) {
if (!gl.store) {
console.warn(
"Deprecation Notice: You're haven't chosen a storage strategy." +
" The old default is 'le-store-certbot', but the new default will be 'greenlock-store-fs'." +
" Please `npm install greenlock-store-fs@3` and explicitly set `{ store: require('greenlock-store-fs') }`."
);
gl.store = require('le-store-certbot').create({
debug: gl.debug,
configDir: gl.configDir,
logsDir: gl.logsDir,
webrootPath: gl.webrootPath
});
}
gl.core = require('./lib/core');
var log = gl.log || _log;
if (!gl.challenges) {
gl.challenges = {};
}
if (!gl.challenges['http-01']) {
gl.challenges['http-01'] = require('le-challenge-fs').create({
debug: gl.debug,
webrootPath: gl.webrootPath
});
}
if (!gl.challenges['dns-01']) {
try {
gl.challenges['dns-01'] = require('le-challenge-ddns').create({
debug: gl.debug
});
} catch (e) {
try {
gl.challenges['dns-01'] = require('le-challenge-dns').create({
debug: gl.debug
});
} catch (e) {
// not yet implemented
}
}
}
gl = Greenlock._undefine(gl);
gl.acmeChallengePrefix = Greenlock.acmeChallengePrefix;
gl.rsaKeySize = gl.rsaKeySize || Greenlock.rsaKeySize;
gl.challengeType = gl.challengeType || Greenlock.challengeType;
gl._ipc = ipc;
gl._communityPackage = gl._communityPackage || 'greenlock.js';
if ('greenlock.js' === gl._communityPackage) {
gl._communityPackageVersion = pkg.version;
} else {
gl._communityPackageVersion =
gl._communityPackageVersion || 'greenlock.js-' + pkg.version;
}
gl.agreeToTerms =
gl.agreeToTerms ||
function(args, agreeCb) {
agreeCb(
new Error(
"'agreeToTerms' was not supplied to Greenlock and 'agreeTos' was not supplied to Greenlock.register"
)
);
};
if (!gl.renewWithin) {
gl.renewWithin = 14 * DAY;
}
// renewBy has a default in le-sni-auto
///////////////////////////
// BEGIN VERSION MADNESS //
///////////////////////////
gl.version = gl.version || 'draft-11';
gl.server = gl.server || 'https://acme-v02.api.letsencrypt.org/directory';
if (!gl.version) {
//console.warn("Please specify version: 'v01' (Let's Encrypt v1) or 'draft-12' (Let's Encrypt v2 / ACME draft 12)");
console.warn('');
console.warn('');
console.warn('');
console.warn(
'=========================================================='
);
console.warn(
'== greenlock.js (v2.2.0+) =='
);
console.warn(
'=========================================================='
);
console.warn('');
console.warn("Please specify 'version' option:");
console.warn('');
console.warn(
" 'draft-12' for Let's Encrypt v2 and ACME draft 12"
);
console.warn(" ('v02' is an alias of 'draft-12'");
console.warn('');
console.warn('or');
console.warn('');
console.warn(" 'v01' for Let's Encrypt v1 (deprecated)");
console.warn(
" (also 'npm install --save le-acme-core' as this legacy dependency will soon be removed)"
);
console.warn('');
console.warn('This will be required in versions v2.3+');
console.warn('');
console.warn('');
} else if ('v02' === gl.version) {
gl.version = 'draft-11';
} else if ('draft-12' === gl.version) {
gl.version = 'draft-11';
} else if ('draft-11' === gl.version) {
// no-op
} else if ('v01' !== gl.version) {
throw new Error("Unrecognized version '" + gl.version + "'");
}
if (!gl.server) {
throw new Error(
"opts.server must specify an ACME directory URL, such as 'https://acme-staging-v02.api.letsencrypt.org/directory'"
);
}
if ('staging' === gl.server || 'production' === gl.server) {
if ('staging' === gl.server) {
gl.server = 'https://acme-staging.api.letsencrypt.org/directory';
gl.version = 'v01';
gl._deprecatedServerName = 'staging';
} else if ('production' === gl.server) {
gl.server = 'https://acme-v01.api.letsencrypt.org/directory';
gl.version = 'v01';
gl._deprecatedServerName = 'production';
}
console.warn('');
console.warn('');
console.warn('=== WARNING ===');
console.warn('');
console.warn(
"Due to versioning issues the '" +
gl._deprecatedServerName +
"' option is deprecated."
);
console.warn('Please specify the full url and version.');
console.warn('');
console.warn('For APIs add:');
console.warn('\t, "version": "' + gl.version + '"');
console.warn('\t, "server": "' + gl.server + '"');
console.warn('');
console.warn('For the CLI add:');
console.warn("\t--acme-url '" + gl.server + "' \\");
console.warn("\t--acme-version '" + gl.version + "' \\");
console.warn('');
console.warn('');
}
function loadLeV01() {
console.warn('');
console.warn('=== WARNING ===');
console.warn('');
console.warn("Let's Encrypt v1 is deprecated.");
console.warn("Please update to Let's Encrypt v2 (ACME draft 12)");
console.warn('');
try {
return require('le-acme-core').ACME;
} catch (e) {
console.error('');
console.error('=== Error (easy-to-fix) ===');
console.error('');
console.error(
"Hey, this isn't a big deal, but you need to manually add v1 support:"
);
console.error('');
console.error(' npm install --save le-acme-core');
console.error('');
console.error(
'Just run that real quick, restart, and everything will work great.'
);
console.error('');
console.error('');
process.exit(e.code || 13);
}
}
if (
-1 !==
[
'https://acme-v02.api.letsencrypt.org/directory',
'https://acme-staging-v02.api.letsencrypt.org/directory'
].indexOf(gl.server)
) {
if ('draft-11' !== gl.version) {
console.warn(
"Detected Let's Encrypt v02 URL. Changing version to draft-12."
);
gl.version = 'draft-11';
}
} else if (
-1 !==
[
'https://acme-v01.api.letsencrypt.org/directory',
'https://acme-staging.api.letsencrypt.org/directory'
].indexOf(gl.server) ||
'v01' === gl.version
) {
if ('v01' !== gl.version) {
console.warn(
"Detected Let's Encrypt v01 URL (deprecated). Changing version to v01."
);
gl.version = 'v01';
}
}
if ('v01' === gl.version) {
ACME = loadLeV01();
}
/////////////////////////
// END VERSION MADNESS //
/////////////////////////
gl.acme =
gl.acme ||
ACME.create({
debug: gl.debug,
skipChallengeTest: gl.skipChallengeTest,
skipDryRun: gl.skipDryRun
});
if (gl.acme.create) {
gl.acme = gl.acme.create(gl);
}
gl.acme = promisifyAllSelf(gl.acme);
gl._acmeOpts =
(gl.acme.getOptions && gl.acme.getOptions()) || gl.acme.options || {};
Object.keys(gl._acmeOpts).forEach(function(key) {
if (!(key in gl)) {
gl[key] = gl._acmeOpts[key];
}
});
try {
if (gl.store.create) {
gl.store = gl.store.create(gl);
}
gl.store = promisifyAllSelf(gl.store);
gl.store.accounts = promisifyAllStore(gl.store.accounts);
gl.store.certificates = promisifyAllStore(gl.store.certificates);
gl._storeOpts =
(gl.store.getOptions && gl.store.getOptions()) ||
gl.store.options ||
{};
} catch (e) {
console.error(e);
console.error(
'\nPROBABLE CAUSE:\n' +
'\tYour greenlock-store module should have a create function and return { options, accounts, certificates }\n'
);
process.exit(18);
return;
}
Object.keys(gl._storeOpts).forEach(function(key) {
if (!(key in gl)) {
gl[key] = gl._storeOpts[key];
}
});
//
// Backwards compat for <= v2.1.7
//
if (gl.challenge) {
console.warn(
"Deprecated use of gl.challenge. Use gl.challenges['" +
Greenlock.challengeType +
"'] instead."
);
gl.challenges[gl.challengeType] = gl.challenge;
gl.challenge = undefined;
}
Object.keys(gl.challenges || {}).forEach(function(challengeType) {
var challenger = gl.challenges[challengeType];
if (challenger.create) {
challenger = gl.challenges[challengeType] = challenger.create(gl);
}
challenger = gl.challenges[challengeType] = promisifyAllSelf(
challenger
);
gl['_challengeOpts_' + challengeType] =
(challenger.getOptions && challenger.getOptions()) ||
challenger.options ||
{};
Object.keys(gl['_challengeOpts_' + challengeType]).forEach(function(
key
) {
if (!(key in gl)) {
gl[key] = gl['_challengeOpts_' + challengeType][key];
}
});
// TODO wrap these here and now with tplCopy?
if (!challenger.set || ![5, 2, 1].includes(challenger.set.length)) {
throw new Error(
'gl.challenges[' +
challengeType +
'].set receives the wrong number of arguments.' +
' You must define setChallenge as function (opts) { return Promise.resolve(); }'
);
}
if (challenger.get && ![4, 2, 1].includes(challenger.get.length)) {
throw new Error(
'gl.challenges[' +
challengeType +
'].get receives the wrong number of arguments.' +
' You must define getChallenge as function (opts) { return Promise.resolve(); }'
);
}
if (
!challenger.remove ||
![4, 2, 1].includes(challenger.remove.length)
) {
throw new Error(
'gl.challenges[' +
challengeType +
'].remove receives the wrong number of arguments.' +
' You must define removeChallenge as function (opts) { return Promise.resolve(); }'
);
}
/*
if (!gl._challengeWarn && (!challenger.loopback || 4 !== challenger.loopback.length)) {
gl._challengeWarn = true;
console.warn("gl.challenges[" + challengeType + "].loopback should be defined as function (opts, domain, token, cb) { ... } and should prove (by external means) that the ACME server challenge '" + challengeType + "' will succeed");
}
else if (!gl._challengeWarn && (!challenger.test || 5 !== challenger.test.length)) {
gl._challengeWarn = true;
console.warn("gl.challenges[" + challengeType + "].test should be defined as function (opts, domain, token, keyAuthorization, cb) { ... } and should prove (by external means) that the ACME server challenge '" + challengeType + "' will succeed");
}
*/
});
gl.sni = gl.sni || null;
gl.tlsOptions = gl.tlsOptions || gl.httpsOptions || {};
// Workaround for https://github.com/nodejs/node/issues/22389
gl._updateServernames = function(cert) {
if (!gl._certnames) {
gl._certnames = {};
}
// Note: Any given domain could exist on multiple certs
// (especially during renewal where some may be added)
// hence we use a separate object for each domain and list each domain on it
// to get the minimal full set associated with each cert and domain
var allDomains = [cert.subject].concat(cert.altnames.slice(0));
allDomains.forEach(function(name) {
name = name.toLowerCase();
if (!gl._certnames[name]) {
gl._certnames[name] = {};
}
allDomains.forEach(function(name2) {
name2 = name2.toLowerCase();
gl._certnames[name][name2] = true;
});
});
};
gl._checkServername = function(safeHost, servername) {
// odd, but acceptable
if (!safeHost || !servername) {
return true;
}
if (safeHost === servername) {
return true;
}
// connection established with servername and session is re-used for allowed name
if (gl._certnames[servername] && gl._certnames[servername][safeHost]) {
return true;
}
return false;
};
if (!gl.tlsOptions.SNICallback) {
if (!gl.getCertificatesAsync && !gl.getCertificates) {
if (Array.isArray(gl.approveDomains)) {
gl.approvedDomains = gl.approveDomains;
gl.approveDomains = null;
}
if (!gl.approveDomains) {
gl.approveDomains = function(lexOpts, cb) {
var err;
var emsg;
if (!gl.email) {
throw new Error(
'le-sni-auto is not properly configured. Missing email'
);
}
if (!gl.agreeTos) {
throw new Error(
'le-sni-auto is not properly configured. Missing agreeTos'
);
}
if (!/[a-z]/i.test(lexOpts.domain)) {
cb(
new Error(
'le-sni-auto does not allow IP addresses in SNI'
)
);
return;
}
if (!Array.isArray(gl.approvedDomains)) {
// The acme-v2 package uses pre-flight test challenges to
// verify that each requested domain is hosted by the server
// these checks are sufficient for most use cases
return cb(null, lexOpts);
}
if (
lexOpts.domains.every(function(domain) {
return -1 !== gl.approvedDomains.indexOf(domain);
})
) {
// commented this out because people expect to be able to edit the list of domains
// lexOpts.domains = gl.approvedDomains.slice(0);
lexOpts.email = gl.email;
lexOpts.agreeTos = gl.agreeTos;
lexOpts.communityMember = gl.communityMember;
lexOpts.telemetry = gl.telemetry;
return cb(null, lexOpts);
}
emsg =
"tls SNI for '" +
lexOpts.domains.join(',') +
"' rejected: not in list '" +
gl.approvedDomains +
"'";
log(gl.debug, emsg, lexOpts.domains, gl.approvedDomains);
err = new Error(emsg);
err.code = 'E_REJECT_SNI';
cb(err);
};
}
gl.getCertificates = function(domain, certs, cb) {
// certs come from current in-memory cache, not lookup
log(
gl.debug,
'gl.getCertificates called for',
domain,
'with certs for',
(certs && certs.altnames) || 'NONE'
);
var opts = {
domain: domain,
domains: (certs && certs.altnames) || [domain],
certs: certs,
certificate: {},
account: {}
};
opts.wildname =
'*.' +
(domain || '')
.split('.')
.slice(1)
.join('.');
function cb2(results) {
log(
gl.debug,
'gl.approveDomains called with certs for',
(results.certs && results.certs.altnames) || 'NONE',
'and options:'
);
log(gl.debug, results.options || results);
var err;
if (!results) {
err = new Error('E_REJECT_SNI');
err.code = 'E_REJECT_SNI';
eb2(err);
return;
}
var options = results.options || results;
if (opts !== options) {
Object.keys(options).forEach(function(key) {
if (
'undefined' !== typeof options[key] &&
'domain' !== key
) {
opts[key] = options[key];
}
});
options = opts;
}
if (
Array.isArray(options.altnames) &&
options.altnames.length
) {
options.domains = options.altnames;
}
options.altnames = options.domains;
// just in case we get a completely different object from the one we originally created
if (!options.account) {
options.account = {};
}
if (!options.certificate) {
options.certificate = {};
}
if (results.certs) {
log(gl.debug, 'gl renewing');
return gl.core.certificates
.renewAsync(options, results.certs)
.then(
function(certs) {
// Workaround for https://github.com/nodejs/node/issues/22389
gl._updateServernames(certs);
cb(null, certs);
},
function(e) {
console.debug(
"Error renewing certificate for '" +
domain +
"':"
);
console.debug(e);
console.error('');
cb(e);
}
);
} else {
log(
gl.debug,
'gl getting from disk or registering new'
);
return gl.core.certificates.getAsync(options).then(
function(certs) {
// Workaround for https://github.com/nodejs/node/issues/22389
gl._updateServernames(certs);
cb(null, certs);
},
function(e) {
console.debug(
"Error loading/registering certificate for '" +
domain +
"':"
);
console.debug(e);
console.error('');
cb(e);
}
);
}
}
function eb2(_err) {
if (false !== gl.logRejectedDomains) {
console.error(
"[Error] approveDomains rejected tls sni '" +
domain +
"'"
);
console.error(
'[Error] (see https://git.coolaj86.com/coolaj86/greenlock.js/issues/11)'
);
if ('E_REJECT_SNI' !== _err.code) {
console.error(
'[Error] This is the rejection message:'
);
console.error(_err.message);
}
console.error('');
}
cb(_err);
return;
}
function mb2(_err, results) {
if (_err) {
eb2(_err);
return;
}
cb2(results);
}
try {
if (1 === gl.approveDomains.length) {
Promise.resolve(gl.approveDomains(opts))
.then(cb2)
.catch(eb2);
} else if (2 === gl.approveDomains.length) {
gl.approveDomains(opts, mb2);
} else {
gl.approveDomains(opts, certs, mb2);
}
} catch (e) {
console.error(
'[ERROR] Something went wrong in approveDomains:'
);
console.error(e);
console.error(
"BUT WAIT! Good news: It's probably your fault, so you can probably fix it."
);
}
};
}
gl.sni = gl.sni || require('le-sni-auto');
if (gl.sni.create) {
gl.sni = gl.sni.create(gl);
}
gl.tlsOptions.SNICallback = function(_domain, cb) {
// format and (lightly) sanitize sni so that users can be naive
// and not have to worry about SQL injection or fs discovery
var domain = (_domain || '').toLowerCase();
// hostname labels allow a-z, 0-9, -, and are separated by dots
// _ is sometimes allowed
// REGEX // https://www.codeproject.com/Questions/1063023/alphanumeric-validation-javascript-without-regex
if (
!gl.__sni_allow_dangerous_names &&
(!/^[a-z0-9_\.\-]+$/i.test(domain) ||
-1 !== domain.indexOf('..'))
) {
log(gl.debug, "invalid sni '" + domain + "'");
cb(new Error('invalid SNI'));
return;
}
try {
gl.sni.sniCallback(
(gl.__sni_preserve_case && _domain) || domain,
cb
);
} catch (e) {
console.error(
'[ERROR] Something went wrong in the SNICallback:'
);
console.error(e);
cb(e);
}
};
}
// We want to move to using tlsOptions instead of httpsOptions, but we also need to make
// sure anything that uses this object will still work if looking for httpsOptions.
gl.httpsOptions = gl.tlsOptions;
if (gl.core.create) {
gl.core = gl.core.create(gl);
}
gl.renew = function(args, certs) {
return gl.core.certificates.renewAsync(args, certs);
};
gl.register = function(args) {
return gl.core.certificates.getAsync(args);
};
gl.check = function(args) {
// TODO must return email, domains, tos, pems
return gl.core.certificates.checkAsync(args);
};
gl.middleware = gl.middleware || require('./lib/middleware');
if (gl.middleware.create) {
gl.middleware = gl.middleware.create(gl);
}
//var SERVERNAME_RE = /^[a-z0-9\.\-_]+$/;
var SERVERNAME_G = /[^a-z0-9\.\-_]/;
gl.middleware.sanitizeHost = function(app) {
return function(req, res, next) {
function realNext() {
if ('function' === typeof app) {
app(req, res);
} else if ('function' === typeof next) {
next();
} else {
res.statusCode = 500;
res.end('Error: no middleware assigned');
}
}
// Get the host:port combo, if it exists
var host = (req.headers.host || '').split(':');
// if not, move along
if (!host[0]) {
realNext();
return;
}
// if so, remove non-allowed characters
var safehost = host[0].toLowerCase().replace(SERVERNAME_G, '');
// if there were unallowed characters, complain
if (
!gl.__sni_allow_dangerous_names &&
safehost.length !== host[0].length
) {
res.statusCode = 400;
res.end("Malformed HTTP Header: 'Host: " + host[0] + "'");
return;
}
// make lowercase
if (!gl.__sni_preserve_case) {
host[0] = safehost;
req.headers.host = host.join(':');
}
// Note: This sanitize function is also called on plain sockets, which don't need Domain Fronting checks
if (req.socket.encrypted && !gl.__sni_allow_domain_fronting) {
if (req.socket && 'string' === typeof req.socket.servername) {
// Workaround for https://github.com/nodejs/node/issues/22389
if (
!gl._checkServername(
safehost,
req.socket.servername.toLowerCase()
)
) {
res.statusCode = 400;
res.setHeader(
'Content-Type',
'text/html; charset=utf-8'
);
res.end(
'<h1>Domain Fronting Error</h1>' +
"<p>This connection was secured using TLS/SSL for '" +
req.socket.servername.toLowerCase() +
"'</p>" +
"<p>The HTTP request specified 'Host: " +
safehost +
"', which is (obviously) different.</p>" +
'<p>Because this looks like a domain fronting attack, the connection has been terminated.</p>'
);
return;
}
} else if (
safehost &&
!gl.middleware.sanitizeHost._skip_fronting_check
) {
// TODO how to handle wrapped sockets, as with telebit?
console.warn(
'\n\n\n[greenlock] WARN: no string for req.socket.servername,' +
" skipping fronting check for '" +
safehost +
"'\n\n\n"
);
gl.middleware.sanitizeHost._skip_fronting_check = true;
}
}
// carry on
realNext();
};
};
gl.middleware.sanitizeHost._skip_fronting_check = false;
return gl;
};