diff --git a/index.js b/index.js index 5c708fe..5fb9339 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,6 @@ -'use strict'; +"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) @@ -6,43 +8,44 @@ var DAY = 24 * 60 * 60 * 1000; //var MIN = 60 * 1000; -var ACME = require('acme-v2/compat').ACME; -var pkg = require('./package.json'); -var PromiseA; -try { - PromiseA = require('bluebird'); -} catch(e) { - PromiseA = global.Promise; -} -if (!PromiseA.promisify) { - PromiseA.promisify = require('util').promisify; -} +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'] = PromiseA.promisify(obj[key]); - } - }); - obj.__promisified = true; - return 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; } + 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 PromiseA.resolve().then(function () { return obj[key](opts); }); }; - } else { - p = PromiseA.promisify(obj[key]); - } - // internal backwards compat - obj[key + 'Async'] = p; - }); - obj.__promisified = true; - return obj; + 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; @@ -52,311 +55,373 @@ Greenlock.LE = Greenlock; 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); - } + 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' + 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' ] + rsaKeySize: ACME.rsaKeySize || 2048, + challengeType: ACME.challengeType || "http-01", + challengeTypes: ACME.challengeTypes || ["http-01", "dns-01"], -, acmeChallengePrefix: ACME.acmeChallengePrefix + acmeChallengePrefix: ACME.acmeChallengePrefix }; // backwards compat -Object.keys(Greenlock.defaults).forEach(function (key) { - Greenlock[key] = Greenlock.defaults[key]; +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 + acme: u, + store: u, + //, challenge: u + challenges: u, + sni: u, + tlsOptions: u, -, register: u -, check: 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 + 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; - } - }); +Greenlock._undefine = function(gl) { + Object.keys(Greenlock._undefined).forEach(function(key) { + if (!(key in gl)) { + gl[key] = u; + } + }); - return gl; + 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; +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 - } - } - } + 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")); - }; + 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 + 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 + "'"); + } - /////////////////////////// - // BEGIN VERSION MADNESS // - /////////////////////////// + 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(""); + } - 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 + "'"); - } + 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 (!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(""); - } + 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 // + ///////////////////////// - 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); - } - } + 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]; + } + }); - 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 // - ///////////////////////// + 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]; - gl.acme = gl.acme || ACME.create({ debug: gl.debug }); - 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]; - } - }); + 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]; + } + }); - 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]; - } - }); + // 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(); }" + ); + } - - // - // 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"); @@ -366,309 +431,386 @@ Greenlock.create = function (gl) { 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 || {}; + 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 = {}; } + // 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; - }; + // 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.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 (!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 (!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); - } + 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); - }; - } + 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('.'); + 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); - var err; - if (!results) { - err = new Error('E_REJECT_SNI'); - err.code = 'E_REJECT_SNI'; - eb2(err); - return; - } + 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); - } + 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) { - PromiseA.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 { + 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); - } - }; - } + 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; + // 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); - } + if (gl.core.create) { + gl.core = gl.core.create(gl); + } - gl.renew = function (args, certs) { - return gl.core.certificates.renewAsync(args, certs); - }; + gl.renew = function(args, certs) { + return gl.core.certificates.renewAsync(args, certs); + }; - gl.register = function (args) { - return gl.core.certificates.getAsync(args); - }; + 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.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); - } + 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(':'); + //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 not, move along + if (!host[0]) { + realNext(); + return; + } - // if so, remove non-allowed characters - var safehost = host[0].toLowerCase().replace(SERVERNAME_G, ''); + // 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; - } + // 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(':'); - } + // 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( - "

Domain Fronting Error

" - + "

This connection was secured using TLS/SSL for '" + req.socket.servername.toLowerCase() + "'

" - + "

The HTTP request specified 'Host: " + safehost + "', which is (obviously) different.

" - + "

Because this looks like a domain fronting attack, the connection has been terminated.

" - ); - 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; - } - } + // 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( + "

Domain Fronting Error

" + + "

This connection was secured using TLS/SSL for '" + + req.socket.servername.toLowerCase() + + "'

" + + "

The HTTP request specified 'Host: " + + safehost + + "', which is (obviously) different.

" + + "

Because this looks like a domain fronting attack, the connection has been terminated.

" + ); + 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; + // carry on + realNext(); + }; + }; + gl.middleware.sanitizeHost._skip_fronting_check = false; - return gl; + return gl; }; diff --git a/lib/compat.js b/lib/compat.js new file mode 100644 index 0000000..0f46780 --- /dev/null +++ b/lib/compat.js @@ -0,0 +1,21 @@ +'use strict'; + +function requireBluebird() { + try { + return require('bluebird'); + } catch(e) { + console.error(""); + console.error("DON'T PANIC. You're running an old version of node with incomplete Promise support."); + console.error("EASY FIX: `npm install --save bluebird`"); + console.error(""); + throw e; + } +} + +if ('undefined' === typeof Promise) { + global.Promise = requireBluebird(); +} + +if ('function' !== typeof require('util').promisify) { + require('util').promisify = requireBluebird().promisify; +} diff --git a/lib/core.js b/lib/core.js index c938e4e..f1d4956 100644 --- a/lib/core.js +++ b/lib/core.js @@ -1,11 +1,7 @@ 'use strict'; +/*global Promise*/ +require('./compat.js'); -var PromiseA; -try { - PromiseA = require('bluebird'); -} catch(e) { - PromiseA = global.Promise; -} var util = require('util'); function promisifyAll(obj) { var aobj = {}; @@ -42,7 +38,7 @@ module.exports.create = function (gl) { // TODO check response header on request for cache time if ((now - gl._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { - return PromiseA.resolve(gl._ipc.acmeUrls); + return Promise.resolve(gl._ipc.acmeUrls); } // TODO acme-v2/nocompat @@ -79,7 +75,7 @@ module.exports.create = function (gl) { + " and 'rsaKeySize' must be 2048 or greater." ); err.code = 'E_ARGS'; - return PromiseA.reject(err); + return Promise.reject(err); } return utils.testEmail(args.email).then(function () { @@ -156,9 +152,9 @@ module.exports.create = function (gl) { if (newAccountKeypair) { accountKeypairPromise = gl.store.accounts.setKeypairAsync(args, keypair); } - return PromiseA.resolve(accountKeypairPromise).then(function () { + return Promise.resolve(accountKeypairPromise).then(function () { // TODO move templating of arguments to right here? - if (!gl.store.accounts.setAsync) { return PromiseA.resolve({ keypair: keypair }); } + if (!gl.store.accounts.setAsync) { return Promise.resolve({ keypair: keypair }); } return gl.store.accounts.setAsync(args, reg).then(function (account) { if (account && 'object' !== typeof account) { throw new Error("store.accounts.setAsync should either return 'null' or an object with at least a string 'id'"); @@ -181,7 +177,7 @@ module.exports.create = function (gl) { if (gl.store.accounts.checkAsync) { accountPromise = core.accounts.checkAsync(args); } - return PromiseA.resolve(accountPromise).then(function (account) { + return Promise.resolve(accountPromise).then(function (account) { if (!account) { return core.accounts.registerAsync(args); } if (account.keypair) { return account; } @@ -201,7 +197,7 @@ module.exports.create = function (gl) { var requiredArgs = ['accountId', 'email', 'domains', 'domain']; if (!(args.account && (args.account.id || args.account.kid)) && !requiredArgs.some(function (key) { return -1 !== Object.keys(args).indexOf(key); })) { - return PromiseA.reject(new Error( + return Promise.reject(new Error( "In order to register or retrieve an account one of '" + requiredArgs.join("', '") + "' must be present" )); } @@ -213,7 +209,7 @@ module.exports.create = function (gl) { // we can re-register the same account until we're blue in the face and it's all the same // of course, we can also skip the lookup if we do store the account, but whatever - if (!gl.store.accounts.checkAsync) { return PromiseA.resolve(null); } + if (!gl.store.accounts.checkAsync) { return Promise.resolve(null); } return gl.store.accounts.checkAsync(args).then(function (account) { if (!account) { @@ -240,7 +236,7 @@ module.exports.create = function (gl) { args = utils.tplCopy(copy); if (!Array.isArray(args.domains)) { - return PromiseA.reject(new Error('args.domains should be an array of domains')); + return Promise.reject(new Error('args.domains should be an array of domains')); } //if (-1 === args.domains.indexOf(args.subject)) // TODO relax the constraint once acme-v2 handles subject? if (args.subject !== args.domains[0]) { @@ -250,7 +246,7 @@ module.exports.create = function (gl) { console.warn('\topts.domains: (set by you in approveDomains()) ' + args.domains.join(',')); console.warn("Updating your code will prevent weird, random, hard-to-repro bugs during renewals"); console.warn("(also this will be required in the next major version of greenlock)"); - //return PromiseA.reject(new Error('certificate subject (primary domain) must be the first in opts.domains')); + //return Promise.reject(new Error('certificate subject (primary domain) must be the first in opts.domains')); } if (!(args.domains.length && args.domains.every(utils.isValidDomain))) { // NOTE: this library can't assume to handle the http loopback @@ -258,7 +254,7 @@ module.exports.create = function (gl) { // so we do not check dns records or attempt a loopback here err = new Error("invalid domain name(s): '(" + args.subject + ') ' + args.domains.join(',') + "'"); err.code = "INVALID_DOMAIN"; - return PromiseA.reject(err); + return Promise.reject(err); } // If a previous request to (re)register a certificate is already underway we need @@ -384,6 +380,9 @@ module.exports.create = function (gl) { Object.keys(challenge).forEach(function (key) { done[key] = challenge[key]; }); + // regression bugfix for le-challenge-cloudflare + // (_acme-challege => _greenlock-dryrun-XXXX) + copy.acmePrefix = (challenge.dnsHost||'').replace(/\.*/, '') || copy.acmePrefix; gl.challenges[challenge.type].set(copy, challenge.altname, challenge.token, challenge.keyAuthorization, done); } }; @@ -429,7 +428,7 @@ module.exports.create = function (gl) { args.keypair = domainKeypair; promise = gl.store.certificates.setKeypairAsync(args, domainKeypair); } - return PromiseA.resolve(promise).then(function () { + return Promise.resolve(promise).then(function () { return gl.store.certificates.setAsync(args).then(function () { return results; }); @@ -455,7 +454,7 @@ module.exports.create = function (gl) { + new Date(renewableAt).toISOString() + "'. Set { duplicate: true } to force." ); err.code = 'E_NOT_RENEWABLE'; - return PromiseA.reject(err); + return Promise.reject(err); } // Either the cert has entered its renewal period diff --git a/lib/utils.js b/lib/utils.js index 36acb02..caa3fb1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,12 +1,12 @@ 'use strict'; +require('./compat.js'); var path = require('path'); var homeRe = new RegExp("^~(\\/|\\\\|\\" + path.sep + ")"); // very basic check. Allows *.example.com. var re = /^(\*\.)?[a-zA-Z0-9\.\-]+$/; var punycode = require('punycode'); -var promisify = (require('util').promisify || require('bluebird').promisify); -var dnsResolveMxAsync = promisify(require('dns').resolveMx); +var dnsResolveMxAsync = require('util').promisify(require('dns').resolveMx); module.exports.attachCertInfo = function (results) { var certInfo = require('cert-info').info(results.cert); @@ -54,12 +54,24 @@ module.exports.merge = function (/*defaults, args*/) { allDefaults.forEach(function (defaults) { Object.keys(defaults).forEach(function (key) { - copy[key] = defaults[key]; + if ('challenges' === key && copy[key] && defaults[key]) { + Object.keys(defaults[key]).forEach(function (k) { + copy[key][k] = defaults[key][k]; + }); + } else { + copy[key] = defaults[key]; + } }); }); Object.keys(args).forEach(function (key) { - copy[key] = args[key]; + if ('challenges' === key && copy[key] && args[key]) { + Object.keys(args[key]).forEach(function (k) { + copy[key][k] = args[key][k]; + }); + } else { + copy[key] = args[key]; + } }); return copy; diff --git a/package.json b/package.json index 5966c84..2f96add 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "greenlock", - "version": "2.7.22", + "version": "2.7.23", "description": "Greenlock is Let's Encrypt (ACME) client for node.js", "homepage": "https://greenlock.domains/", "main": "index.js", diff --git a/tests/challenge-middleware.js b/tests/challenge-middleware.js deleted file mode 100644 index f97d3ee..0000000 --- a/tests/challenge-middleware.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict'; - -var PromiseA = require('bluebird'); -var path = require('path'); -var requestAsync = PromiseA.promisify(require('@coolaj86/request')); -var LE = require('../').LE; -var le = LE.create({ - server: 'staging' -, acme: require('le-acme-core').ACME.create() -, store: require('le-store-certbot').create({ - configDir: '~/letsencrypt.test/etc'.split('/').join(path.sep) - , webrootPath: '~/letsencrypt.test/var/:hostname'.split('/').join(path.sep) - }) -, challenge: require('le-challenge-fs').create({ - webrootPath: '~/letsencrypt.test/var/:hostname'.split('/').join(path.sep) - }) -, debug: true -}); -var utils = require('../lib/utils'); - -if ('/.well-known/acme-challenge/' !== LE.acmeChallengePrefix) { - throw new Error("Bad constant 'acmeChallengePrefix'"); -} - -var baseUrl; -// could use localhost as well, but for the sake of an FQDN for testing, we use this -// also, example.com is just a junk domain to make sure that it is ignored -// (even though it should always be an array of only one element in lib/core.js) -var domains = [ 'localhost.daplie.com', 'example.com' ]; // or just localhost -var token = 'token-id'; -var secret = 'key-secret'; - -var tests = [ - function () { - console.log('Test Url:', baseUrl + token); - return requestAsync({ url: baseUrl + token }).then(function (req) { - if (404 !== req.statusCode) { - console.log(req.statusCode); - throw new Error("Should be status 404"); - } - }); - } - -, function () { - var copy = utils.merge({ domains: domains }, le); - copy = utils.tplCopy(copy); - return PromiseA.promisify(le.challenge.set)(copy, domains[0], token, secret); - } - -, function () { - return requestAsync(baseUrl + token).then(function (req) { - if (200 !== req.statusCode) { - console.log(req.statusCode, req.body); - throw new Error("Should be status 200"); - } - - if (req.body !== secret) { - console.error(token, secret, req.body); - throw new Error("req.body should be secret"); - } - }); - } - -, function () { - var copy = utils.merge({ domains: domains }, le); - copy = utils.tplCopy(copy); - return PromiseA.promisify(le.challenge.remove)(copy, domains[0], token); - } - -, function () { - return requestAsync(baseUrl + token).then(function (req) { - if (404 !== req.statusCode) { - console.log(req.statusCode); - throw new Error("Should be status 404"); - } - }); - } -]; - -function run() { - //var express = require(express); - var server = require('http').createServer(le.middleware()); - server.listen(0, function () { - console.log('Server running, proceeding to test.'); - baseUrl = 'http://' + domains[0] + ':' + server.address().port + LE.acmeChallengePrefix; - - function next() { - var test = tests.shift(); - if (!test) { - console.info('All tests passed'); - server.close(); - return; - } - - test().then(next, function (err) { - console.error('ERROR'); - console.error(err.stack); - server.close(); - }); - } - - next(); - }); -} - -run();