diff --git a/examples/README.md b/examples/README.md index 3ac9a10..fa7d8b0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,4 @@ -STOP -==== +# STOP **These aren't the droids you're looking for.** @@ -7,8 +6,7 @@ You probably don't want to use `greenlock` directly. Instead, look here: -Webservers ----------- +## Webservers For any type of webserver (express, hapi, koa, connect, https, spdy, etc), you're going to want to take a look at @@ -16,8 +14,7 @@ you're going to want to take a look at -CLIs ----- +## CLIs For any type of CLI (like what you want to use with bash, fish, zsh, cmd.exe, PowerShell, etc), you're going to want to take a look at @@ -25,8 +22,7 @@ you're going to want to take a look at -No, I wanted greenlock -====================== +# No, I wanted greenlock Well, take a look at the API in the main README and you can also check out the code in the repos above. diff --git a/examples/simple.js b/examples/simple.js index e689765..88b16b0 100644 --- a/examples/simple.js +++ b/examples/simple.js @@ -5,63 +5,73 @@ var Greenlock = require('../'); var db = {}; var config = { - server: 'https://acme-v02.api.letsencrypt.org/directory' -, version: 'draft-11' + server: 'https://acme-v02.api.letsencrypt.org/directory', + version: 'draft-11', -, configDir: require('os').homedir() + '/acme/etc' // or /etc/acme or wherever + configDir: require('os').homedir() + '/acme/etc', // or /etc/acme or wherever -, privkeyPath: ':config/live/:hostname/privkey.pem' // -, fullchainPath: ':config/live/:hostname/fullchain.pem' // Note: both that :config and :hostname -, certPath: ':config/live/:hostname/cert.pem' // will be templated as expected -, chainPath: ':config/live/:hostname/chain.pem' // + privkeyPath: ':config/live/:hostname/privkey.pem', // + fullchainPath: ':config/live/:hostname/fullchain.pem', // Note: both that :config and :hostname + certPath: ':config/live/:hostname/cert.pem', // will be templated as expected + chainPath: ':config/live/:hostname/chain.pem', // -, rsaKeySize: 2048 + rsaKeySize: 2048, -, debug: true + debug: true }; var handlers = { - setChallenge: function (opts, hostname, key, val, cb) { // called during the ACME server handshake, before validation - db[key] = { - hostname: hostname - , key: key - , val: val - }; + setChallenge: function(opts, hostname, key, val, cb) { + // called during the ACME server handshake, before validation + db[key] = { + hostname: hostname, + key: key, + val: val + }; - cb(null); - } -, removeChallenge: function (opts, hostname, key, cb) { // called after validation on both success and failure - db[key] = null; - cb(null); - } -, getChallenge: function (opts, hostname, key, cb) { // this is special because it is called by the webserver - cb(null, db[key].val); // (see greenlock-cli/bin & greenlock-express/standalone), - // not by the library itself - } -, agreeToTerms: function (tosUrl, cb) { // gives you an async way to expose the legal agreement - cb(null, tosUrl); // (terms of use) to your users before accepting - } + cb(null); + }, + removeChallenge: function(opts, hostname, key, cb) { + // called after validation on both success and failure + db[key] = null; + cb(null); + }, + getChallenge: function(opts, hostname, key, cb) { + // this is special because it is called by the webserver + cb(null, db[key].val); // (see greenlock-cli/bin & greenlock-express/standalone), + // not by the library itself + }, + agreeToTerms: function(tosUrl, cb) { + // gives you an async way to expose the legal agreement + cb(null, tosUrl); // (terms of use) to your users before accepting + } }; var greenlock = Greenlock.create(config, handlers); -console.error("CHANGE THE EMAIL, DOMAINS, AND AGREE TOS IN THE EXAMPLE BEFORE RUNNING IT"); +console.error( + 'CHANGE THE EMAIL, DOMAINS, AND AGREE TOS IN THE EXAMPLE BEFORE RUNNING IT' +); process.exit(1); - // checks :conf/renewal/:hostname.conf -greenlock.register({ // and either renews or registers - domains: ['example.com'] // CHANGE TO YOUR DOMAIN -, email: 'user@email.com' // CHANGE TO YOUR EMAIL -, agreeTos: false // set to true to automatically accept an agreement - // which you have pre-approved (not recommended) -, rsaKeySize: 2048 -}, function (err) { - if (err) { - // Note: you must have a webserver running - // and expose handlers.getChallenge to it - // in order to pass validation - // See greenlock-cli and or greenlock-express - console.error('[Error]: greenlock/examples/standalone'); - console.error(err.stack); - } else { - console.log('success'); - } -}); +// checks :conf/renewal/:hostname.conf +greenlock.register( + { + // and either renews or registers + domains: ['example.com'], // CHANGE TO YOUR DOMAIN + email: 'user@email.com', // CHANGE TO YOUR EMAIL + agreeTos: false, // set to true to automatically accept an agreement + // which you have pre-approved (not recommended) + rsaKeySize: 2048 + }, + function(err) { + if (err) { + // Note: you must have a webserver running + // and expose handlers.getChallenge to it + // in order to pass validation + // See greenlock-cli and or greenlock-express + console.error('[Error]: greenlock/examples/standalone'); + console.error(err.stack); + } else { + console.log('success'); + } + } +); diff --git a/index.js b/index.js index 5fb9339..dca4f05 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ -"use strict"; +'use strict'; /*global Promise*/ -require("./lib/compat.js"); +require('./lib/compat.js'); // I hate this code so much. // Soooo many shims for backwards compatibility (some stuff dating back to v1) @@ -8,17 +8,17 @@ require("./lib/compat.js"); 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"); +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]); + if ('function' === typeof obj[key] && !/Async$/.test(key)) { + obj[key + 'Async'] = util.promisify(obj[key]); } }); obj.__promisified = true; @@ -26,7 +26,7 @@ function promisifyAllSelf(obj) { } function promisifyAllStore(obj) { Object.keys(obj).forEach(function(key) { - if ("function" !== typeof obj[key] || /Async$/.test(key)) { + if ('function' !== typeof obj[key] || /Async$/.test(key)) { return; } @@ -42,7 +42,7 @@ function promisifyAllStore(obj) { p = util.promisify(obj[key]); } // internal backwards compat - obj[key + "Async"] = p; + obj[key + 'Async'] = p; }); obj.__promisified = true; return obj; @@ -58,18 +58,18 @@ function _log(debug) { if (debug) { var args = Array.prototype.slice.call(arguments); args.shift(); - args.unshift("[gl/index.js]"); + 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"], + challengeType: ACME.challengeType || 'http-01', + challengeTypes: ACME.challengeTypes || ['http-01', 'dns-01'], acmeChallengePrefix: ACME.acmeChallengePrefix }; @@ -120,33 +120,33 @@ Greenlock.create = function(gl) { " 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({ + gl.store = require('le-store-certbot').create({ debug: gl.debug, configDir: gl.configDir, logsDir: gl.logsDir, webrootPath: gl.webrootPath }); } - gl.core = require("./lib/core"); + 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({ + 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"]) { + if (!gl.challenges['dns-01']) { try { - gl.challenges["dns-01"] = require("le-challenge-ddns").create({ + gl.challenges['dns-01'] = require('le-challenge-ddns').create({ debug: gl.debug }); } catch (e) { try { - gl.challenges["dns-01"] = require("le-challenge-dns").create({ + gl.challenges['dns-01'] = require('le-challenge-dns').create({ debug: gl.debug }); } catch (e) { @@ -160,12 +160,12 @@ Greenlock.create = function(gl) { 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._communityPackage = gl._communityPackage || 'greenlock.js'; + if ('greenlock.js' === gl._communityPackage) { gl._communityPackageVersion = pkg.version; } else { gl._communityPackageVersion = - gl._communityPackageVersion || "greenlock.js-" + pkg.version; + gl._communityPackageVersion || 'greenlock.js-' + pkg.version; } gl.agreeToTerms = gl.agreeToTerms || @@ -186,39 +186,47 @@ Greenlock.create = function(gl) { // BEGIN VERSION MADNESS // /////////////////////////// - gl.version = gl.version || "draft-11"; - gl.server = gl.server || "https://acme-v02.api.letsencrypt.org/directory"; + 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(''); + 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(''); + 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(''); + 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) { + 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) { + } else if ('v01' !== gl.version) { throw new Error("Unrecognized version '" + gl.version + "'"); } @@ -227,62 +235,62 @@ Greenlock.create = function(gl) { "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"; + 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(''); + 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('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(''); + console.warn('For the CLI add:'); console.warn("\t--acme-url '" + gl.server + "' \\"); console.warn("\t--acme-version '" + gl.version + "' \\"); - console.warn(""); - console.warn(""); + console.warn(''); + console.warn(''); } function loadLeV01() { - console.warn(""); - console.warn("=== WARNING ==="); - console.warn(""); + 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(""); + console.warn(''); try { - return require("le-acme-core").ACME; + return require('le-acme-core').ACME; } catch (e) { - console.error(""); - console.error("=== Error (easy-to-fix) ==="); - console.error(""); + 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(''); + console.error(' npm install --save le-acme-core'); + console.error(''); console.error( - "Just run that real quick, restart, and everything will work great." + 'Just run that real quick, restart, and everything will work great.' ); - console.error(""); - console.error(""); + console.error(''); + console.error(''); process.exit(e.code || 13); } } @@ -290,32 +298,32 @@ Greenlock.create = function(gl) { if ( -1 !== [ - "https://acme-v02.api.letsencrypt.org/directory", - "https://acme-staging-v02.api.letsencrypt.org/directory" + 'https://acme-v02.api.letsencrypt.org/directory', + 'https://acme-staging-v02.api.letsencrypt.org/directory' ].indexOf(gl.server) ) { - if ("draft-11" !== gl.version) { + if ('draft-11' !== gl.version) { console.warn( "Detected Let's Encrypt v02 URL. Changing version to draft-12." ); - gl.version = "draft-11"; + gl.version = 'draft-11'; } } else if ( -1 !== [ - "https://acme-v01.api.letsencrypt.org/directory", - "https://acme-staging.api.letsencrypt.org/directory" + 'https://acme-v01.api.letsencrypt.org/directory', + 'https://acme-staging.api.letsencrypt.org/directory' ].indexOf(gl.server) || - "v01" === gl.version + 'v01' === gl.version ) { - if ("v01" !== gl.version) { + if ('v01' !== gl.version) { console.warn( "Detected Let's Encrypt v01 URL (deprecated). Changing version to v01." ); - gl.version = "v01"; + gl.version = 'v01'; } } - if ("v01" === gl.version) { + if ('v01' === gl.version) { ACME = loadLeV01(); } ///////////////////////// @@ -349,12 +357,14 @@ Greenlock.create = function(gl) { 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 || {}; + (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" + '\nPROBABLE CAUSE:\n' + + '\tYour greenlock-store module should have a create function and return { options, accounts, certificates }\n' ); process.exit(18); return; @@ -384,40 +394,47 @@ Greenlock.create = function(gl) { if (challenger.create) { challenger = gl.challenges[challengeType] = challenger.create(gl); } - challenger = gl.challenges[challengeType] = promisifyAllSelf(challenger); - gl["_challengeOpts_" + challengeType] = + challenger = gl.challenges[challengeType] = promisifyAllSelf( + challenger + ); + gl['_challengeOpts_' + challengeType] = (challenger.getOptions && challenger.getOptions()) || challenger.options || {}; - Object.keys(gl["_challengeOpts_" + challengeType]).forEach(function(key) { + Object.keys(gl['_challengeOpts_' + challengeType]).forEach(function( + key + ) { if (!(key in gl)) { - gl[key] = gl["_challengeOpts_" + challengeType][key]; + 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[" + + 'gl.challenges[' + challengeType + - "].set receives the wrong number of arguments." + - " You must define setChallenge as function (opts) { return Promise.resolve(); }" + '].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[" + + 'gl.challenges[' + challengeType + - "].get receives the wrong number of arguments." + - " You must define getChallenge as function (opts) { return Promise.resolve(); }" + '].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)) { + if ( + !challenger.remove || + ![4, 2, 1].includes(challenger.remove.length) + ) { throw new Error( - "gl.challenges[" + + 'gl.challenges[' + challengeType + - "].remove receives the wrong number of arguments." + - " You must define removeChallenge as function (opts) { return Promise.resolve(); }" + '].remove receives the wrong number of arguments.' + + ' You must define removeChallenge as function (opts) { return Promise.resolve(); }' ); } @@ -486,16 +503,20 @@ Greenlock.create = function(gl) { if (!gl.email) { throw new Error( - "le-sni-auto is not properly configured. Missing email" + 'le-sni-auto is not properly configured. Missing email' ); } if (!gl.agreeTos) { throw new Error( - "le-sni-auto is not properly configured. Missing agreeTos" + '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")); + cb( + new Error( + 'le-sni-auto does not allow IP addresses in SNI' + ) + ); return; } @@ -522,13 +543,13 @@ Greenlock.create = function(gl) { emsg = "tls SNI for '" + - lexOpts.domains.join(",") + + 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"; + err.code = 'E_REJECT_SNI'; cb(err); }; } @@ -537,10 +558,10 @@ Greenlock.create = function(gl) { // certs come from current in-memory cache, not lookup log( gl.debug, - "gl.getCertificates called for", + 'gl.getCertificates called for', domain, - "with certs for", - (certs && certs.altnames) || "NONE" + 'with certs for', + (certs && certs.altnames) || 'NONE' ); var opts = { domain: domain, @@ -550,24 +571,24 @@ Greenlock.create = function(gl) { account: {} }; opts.wildname = - "*." + - (domain || "") - .split(".") + '*.' + + (domain || '') + .split('.') .slice(1) - .join("."); + .join('.'); function cb2(results) { log( gl.debug, - "gl.approveDomains called with certs for", - (results.certs && results.certs.altnames) || "NONE", - "and options:" + '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"; + err = new Error('E_REJECT_SNI'); + err.code = 'E_REJECT_SNI'; eb2(err); return; } @@ -575,13 +596,19 @@ Greenlock.create = function(gl) { var options = results.options || results; if (opts !== options) { Object.keys(options).forEach(function(key) { - if ("undefined" !== typeof options[key] && "domain" !== key) { + if ( + 'undefined' !== typeof options[key] && + 'domain' !== key + ) { opts[key] = options[key]; } }); options = opts; } - if (Array.isArray(options.altnames) && options.altnames.length) { + if ( + Array.isArray(options.altnames) && + options.altnames.length + ) { options.domains = options.altnames; } options.altnames = options.domains; @@ -593,24 +620,31 @@ Greenlock.create = function(gl) { 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); - } - ); + 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"); + 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 @@ -619,10 +653,12 @@ Greenlock.create = function(gl) { }, function(e) { console.debug( - "Error loading/registering certificate for '" + domain + "':" + "Error loading/registering certificate for '" + + domain + + "':" ); console.debug(e); - console.error(""); + console.error(''); cb(e); } ); @@ -631,16 +667,20 @@ Greenlock.create = function(gl) { function eb2(_err) { if (false !== gl.logRejectedDomains) { console.error( - "[Error] approveDomains rejected tls sni '" + domain + "'" + "[Error] approveDomains rejected tls sni '" + + domain + + "'" ); console.error( - "[Error] (see https://git.coolaj86.com/coolaj86/greenlock.js/issues/11)" + '[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:"); + if ('E_REJECT_SNI' !== _err.code) { + console.error( + '[Error] This is the rejection message:' + ); console.error(_err.message); } - console.error(""); + console.error(''); } cb(_err); return; @@ -664,7 +704,9 @@ Greenlock.create = function(gl) { gl.approveDomains(opts, certs, mb2); } } catch (e) { - console.error("[ERROR] Something went wrong in approveDomains:"); + 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." @@ -672,30 +714,36 @@ Greenlock.create = function(gl) { } }; } - gl.sni = gl.sni || require("le-sni-auto"); + 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(); + 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("..")) + (!/^[a-z0-9_\.\-]+$/i.test(domain) || + -1 !== domain.indexOf('..')) ) { log(gl.debug, "invalid sni '" + domain + "'"); - cb(new Error("invalid SNI")); + cb(new Error('invalid SNI')); return; } try { - gl.sni.sniCallback((gl.__sni_preserve_case && _domain) || domain, cb); + gl.sni.sniCallback( + (gl.__sni_preserve_case && _domain) || domain, + cb + ); } catch (e) { - console.error("[ERROR] Something went wrong in the SNICallback:"); + console.error( + '[ERROR] Something went wrong in the SNICallback:' + ); console.error(e); cb(e); } @@ -723,7 +771,7 @@ Greenlock.create = function(gl) { return gl.core.certificates.checkAsync(args); }; - gl.middleware = gl.middleware || require("./lib/middleware"); + gl.middleware = gl.middleware || require('./lib/middleware'); if (gl.middleware.create) { gl.middleware = gl.middleware.create(gl); } @@ -733,17 +781,17 @@ Greenlock.create = function(gl) { gl.middleware.sanitizeHost = function(app) { return function(req, res, next) { function realNext() { - if ("function" === typeof app) { + if ('function' === typeof app) { app(req, res); - } else if ("function" === typeof next) { + } else if ('function' === typeof next) { next(); } else { res.statusCode = 500; - res.end("Error: no middleware assigned"); + res.end('Error: no middleware assigned'); } } // Get the host:port combo, if it exists - var host = (req.headers.host || "").split(":"); + var host = (req.headers.host || '').split(':'); // if not, move along if (!host[0]) { @@ -752,7 +800,7 @@ Greenlock.create = function(gl) { } // if so, remove non-allowed characters - var safehost = host[0].toLowerCase().replace(SERVERNAME_G, ""); + var safehost = host[0].toLowerCase().replace(SERVERNAME_G, ''); // if there were unallowed characters, complain if ( @@ -767,27 +815,33 @@ Greenlock.create = function(gl) { // make lowercase if (!gl.__sni_preserve_case) { host[0] = safehost; - req.headers.host = host.join(":"); + 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) { + 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()) + !gl._checkServername( + safehost, + req.socket.servername.toLowerCase() + ) ) { res.statusCode = 400; - res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.setHeader( + 'Content-Type', + 'text/html; charset=utf-8' + ); res.end( - "

Domain Fronting Error

" + + '

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.

" + '

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

' ); return; } @@ -797,7 +851,7 @@ Greenlock.create = function(gl) { ) { // TODO how to handle wrapped sockets, as with telebit? console.warn( - "\n\n\n[greenlock] WARN: no string for req.socket.servername," + + '\n\n\n[greenlock] WARN: no string for req.socket.servername,' + " skipping fronting check for '" + safehost + "'\n\n\n" diff --git a/lib/community.js b/lib/community.js index 12ed83c..d6eefe9 100644 --- a/lib/community.js +++ b/lib/community.js @@ -1,65 +1,80 @@ 'use strict'; function addCommunityMember(opts) { - // { name, version, email, domains, action, communityMember, telemetry } - var https = require('https'); - var req = https.request({ - hostname: 'api.ppl.family' - , port: 443 - , path: '/api/ppl.family/public/list' - , method: 'POST' - , headers: { - 'Content-Type': 'application/json' - } - }, function (err, resp) { - if (err) { return; } - resp.on('data', function () {}); - }); - req.on('error', function(error) { - /* ignore */ - }); - var os = require('os'); - var data = { - address: opts.email - // greenlock-security is transactional and security only - , list: opts.communityMember ? (opts.name + '@ppl.family') : 'greenlock-security@ppl.family' - , action: opts.action // reg | renew - , package: opts.name - // hashed for privacy, but so we can still get some telemetry and inform users - // if abnormal things are happening (like several registrations for the same domain each day) - , domain: (opts.domains||[]).map(function (d) { - return require('crypto').createHash('sha1').update(d).digest('base64') - .replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, ''); - }).join(',') - }; - if (false !== opts.telemetry) { - data.arch = process.arch || os.arch(); - data.platform = process.platform || os.platform(); - data.release = os.release(); - data.version = opts.version; - data.node = process.version; - } - req.write(JSON.stringify(data, 2, null)); - req.end(); + // { name, version, email, domains, action, communityMember, telemetry } + var https = require('https'); + var req = https.request( + { + hostname: 'api.ppl.family', + port: 443, + path: '/api/ppl.family/public/list', + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }, + function(err, resp) { + if (err) { + return; + } + resp.on('data', function() {}); + } + ); + req.on('error', function(error) { + /* ignore */ + }); + var os = require('os'); + var data = { + address: opts.email, + // greenlock-security is transactional and security only + list: opts.communityMember + ? opts.name + '@ppl.family' + : 'greenlock-security@ppl.family', + action: opts.action, // reg | renew + package: opts.name, + // hashed for privacy, but so we can still get some telemetry and inform users + // if abnormal things are happening (like several registrations for the same domain each day) + domain: (opts.domains || []) + .map(function(d) { + return require('crypto') + .createHash('sha1') + .update(d) + .digest('base64') + .replace(/\//g, '_') + .replace(/\+/g, '-') + .replace(/=/g, ''); + }) + .join(',') + }; + if (false !== opts.telemetry) { + data.arch = process.arch || os.arch(); + data.platform = process.platform || os.platform(); + data.release = os.release(); + data.version = opts.version; + data.node = process.version; + } + req.write(JSON.stringify(data, 2, null)); + req.end(); } function delay(ms) { - return new Promise(function (resolve) { - return setTimeout(resolve, ms); - }); + return new Promise(function(resolve) { + return setTimeout(resolve, ms); + }); } -module.exports.add = function (opts) { - return delay(50).then(() => { - return addCommunityMember(opts); - }) - .catch(function (ex) { - /* ignore */ - }) -} +module.exports.add = function(opts) { + return delay(50) + .then(() => { + return addCommunityMember(opts); + }) + .catch(function(ex) { + /* ignore */ + }); +}; if (require.main === module) { - //addCommunityMember('greenlock-express.js', 'reg', 'coolaj86+test42@gmail.com', ['coolaj86.com'], true); - //addCommunityMember('greenlock.js', 'reg', 'coolaj86+test37@gmail.com', ['oneal.im'], false); - //addCommunityMember('greenlock.js', 'reg', 'coolaj86+test11@gmail.com', ['ppl.family'], true); + //addCommunityMember('greenlock-express.js', 'reg', 'coolaj86+test42@gmail.com', ['coolaj86.com'], true); + //addCommunityMember('greenlock.js', 'reg', 'coolaj86+test37@gmail.com', ['oneal.im'], false); + //addCommunityMember('greenlock.js', 'reg', 'coolaj86+test11@gmail.com', ['ppl.family'], true); } diff --git a/lib/compat.js b/lib/compat.js index 0f46780..5cfbbf5 100644 --- a/lib/compat.js +++ b/lib/compat.js @@ -1,21 +1,23 @@ '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; - } + 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(); + global.Promise = requireBluebird(); } if ('function' !== typeof require('util').promisify) { - require('util').promisify = requireBluebird().promisify; + require('util').promisify = requireBluebird().promisify; } diff --git a/lib/core.js b/lib/core.js index adee10b..3ba2bdf 100644 --- a/lib/core.js +++ b/lib/core.js @@ -4,611 +4,955 @@ require('./compat.js'); var util = require('util'); function promisifyAll(obj) { - var aobj = {}; - Object.keys(obj).forEach(function (key) { - if ('function' === typeof obj[key]) { - aobj[key] = obj[key]; - aobj[key + 'Async'] = util.promisify(obj[key]); - } - }); - return aobj; + var aobj = {}; + Object.keys(obj).forEach(function(key) { + if ('function' === typeof obj[key]) { + aobj[key] = obj[key]; + aobj[key + 'Async'] = util.promisify(obj[key]); + } + }); + return aobj; } function _log(debug) { - if (debug) { - var args = Array.prototype.slice.call(arguments); - args.shift(); - args.unshift("[greenlock/lib/core.js]"); - console.log.apply(console, args); - } + if (debug) { + var args = Array.prototype.slice.call(arguments); + args.shift(); + args.unshift('[greenlock/lib/core.js]'); + console.log.apply(console, args); + } } -module.exports.create = function (gl) { - var utils = require('./utils'); - var RSA = promisifyAll(require('rsa-compat').RSA); - var log = gl.log || _log; // allow custom log - var pendingRegistrations = {}; - - var core = { - // - // Helpers - // - getAcmeUrlsAsync: function (args) { - var now = Date.now(); - - // TODO check response header on request for cache time - if ((now - gl._ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { - return Promise.resolve(gl._ipc.acmeUrls); - } - - // TODO acme-v2/nocompat - return gl.acme.getAcmeUrlsAsync(args.server).then(function (data) { - gl._ipc.acmeUrlsUpdatedAt = Date.now(); - gl._ipc.acmeUrls = data; - - return gl._ipc.acmeUrls; - }); - } - - - // - // The Main Enchilada - // - - // - // Accounts - // - , accounts: { - // Accounts - registerAsync: function (args) { - var err; - var copy = utils.merge(args, gl); - var disagreeTos; - args = utils.tplCopy(copy); - if (!args.account) { args.account = {}; } - if ('object' === typeof args.account && !args.account.id) { args.account.id = args.accountId || args.email || ''; } - - disagreeTos = (!args.agreeTos && 'undefined' !== typeof args.agreeTos); - if (!args.email || disagreeTos || (parseInt(args.rsaKeySize, 10) < 2048)) { - err = new Error( - "In order to register an account both 'email' and 'agreeTos' must be present" - + " and 'rsaKeySize' must be 2048 or greater." - ); - err.code = 'E_ARGS'; - return Promise.reject(err); - } - - return utils.testEmail(args.email).then(function () { - if (args.account && args.account.privkey && (args.account.privkey.jwk || args.account.privkey.pem)) { - // TODO import jwk or pem and return it here - console.warn("TODO: implement accounts.checkKeypairAsync skipping"); - } - var accountKeypair; - var newAccountKeypair = true; - var promise = gl.store.accounts.checkKeypairAsync(args).then(function (keypair) { - if (keypair) { - // TODO keypairs - newAccountKeypair = false; - accountKeypair = RSA.import(keypair); - return; - } - - if (args.accountKeypair) { - // TODO keypairs - accountKeypair = RSA.import(args.accountKeypair); - return; - } - - var keypairOpts = { bitlen: args.rsaKeySize, exp: 65537, public: true, pem: true }; - // TODO keypairs - return (args.generateKeypair||RSA.generateKeypairAsync)(keypairOpts).then(function (keypair) { - keypair.privateKeyPem = RSA.exportPrivatePem(keypair); - keypair.publicKeyPem = RSA.exportPublicPem(keypair); - keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); - accountKeypair = keypair; - }); - }).then(function () { - return accountKeypair; - }); - - return promise.then(function (keypair) { - // Note: the ACME urls are always fetched fresh on purpose - // TODO acme-v2/nocompat - return core.getAcmeUrlsAsync(args).then(function (urls) { - args._acmeUrls = urls; - - // TODO acme-v2/nocompat - return gl.acme.registerNewAccountAsync({ - email: args.email - , newRegUrl: args._acmeUrls.newReg - , newAuthzUrl: args._acmeUrls.newAuthz - , agreeToTerms: function (tosUrl, agreeCb) { - if (true === args.agreeTos || tosUrl === args.agreeTos || tosUrl === gl.agreeToTerms) { - agreeCb(null, tosUrl); - return; - } - - // args.email = email; // already there - // args.domains = domains // already there - args.tosUrl = tosUrl; - gl.agreeToTerms(args, agreeCb); - } - , accountKeypair: keypair - - , debug: gl.debug || args.debug - }).then(function (receipt) { - var reg = { - keypair: keypair - , receipt: receipt - , kid: receipt && receipt.key && (receipt.key.kid || receipt.kid) - , email: args.email - , newRegUrl: args._acmeUrls.newReg - , newAuthzUrl: args._acmeUrls.newAuthz - }; - - var accountKeypairPromise; - args.keypair = keypair; - args.receipt = receipt; - if (newAccountKeypair) { - accountKeypairPromise = gl.store.accounts.setKeypairAsync(args, keypair); - } - return Promise.resolve(accountKeypairPromise).then(function () { - // TODO move templating of arguments to right here? - 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'"); - } - if (!account) { account = {}; } - account.keypair = keypair; - return account; - }); - }); - }); - }); - }); - }); - } - - // Accounts - // (only used for keypair) - , getAsync: function (args) { - var accountPromise = null; - if (gl.store.accounts.checkAsync) { - accountPromise = core.accounts.checkAsync(args); - } - return Promise.resolve(accountPromise).then(function (account) { - if (!account) { return core.accounts.registerAsync(args); } - if (account.keypair) { return account; } - - if (!args.account) { args.account = {}; } - if ('object' === typeof args.account && !args.account.id) { args.account.id = args.accountId || args.email || ''; } - var copy = utils.merge(args, gl); - args = utils.tplCopy(copy); - return gl.store.accounts.checkKeypairAsync(args).then(function (keypair) { - if (keypair) { return { keypair: keypair }; } - return core.accounts.registerAsync(args); - }); - }); - } - - // Accounts - , checkAsync: function (args) { - 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 Promise.reject(new Error( - "In order to register or retrieve an account one of '" + requiredArgs.join("', '") + "' must be present" - )); - } - - var copy = utils.merge(args, gl); - args = utils.tplCopy(copy); - if (!args.account) { args.account = {}; } - if ('object' === typeof args.account && !args.account.id) { args.account.id = args.accountId || args.email || ''; } - - // 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 Promise.resolve(null); } - return gl.store.accounts.checkAsync(args).then(function (account) { - - if (!account) { - return null; - } - - args.account = account; - args.accountId = account.id; - - return account; - }); - } - } - - , certificates: { - // Certificates - registerAsync: function (args) { - var err; - var challengeDefaults = gl['_challengeOpts_' + (args.challengeType || gl.challengeType)] || {}; - var copy = utils.merge(args, challengeDefaults || {}); - copy = utils.merge(copy, gl); - if (!copy.subject) { copy.subject = copy.domains[0]; } - if (!copy.domain) { copy.domain = copy.domains[0]; } - args = utils.tplCopy(copy); - - if (!Array.isArray(args.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]) { - console.warn("The certificate's subject (primary domain) should be first in the list of opts.domains"); - console.warn('\topts.subject: (set by you approveDomains(), falling back to opts.domain) ' + args.subject); - console.warn('\topts.domain: (set by SNICallback()) ' + args.domain); - 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 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 - // (or dns-01 validation may be used) - // 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 Promise.reject(err); - } - - // If a previous request to (re)register a certificate is already underway we need - // to return the same promise created before rather than registering things twice. - // I'm not 100% sure how to properly handle the case where someone registers domain - // lists with some but not all elements common, nor am I sure that's even a case that - // is allowed to happen anyway. But for now we act like the list is completely the - // same if any elements are the same. - var promise; - args.domains.some(function (name) { - if (pendingRegistrations.hasOwnProperty(name)) { - promise = pendingRegistrations[name]; - return true; - } - }); - if (promise) { - return promise; - } - - promise = core.certificates._runRegistration(args); - - // Now that the registration is actually underway we need to make sure any subsequent - // registration attempts return the same promise until it is completed (but not after - // it is completed). - args.domains.forEach(function (name) { - pendingRegistrations[name] = promise; - }); - function clearPending() { - args.domains.forEach(function (name) { - delete pendingRegistrations[name]; - }); - } - promise.then(clearPending, clearPending); - - return promise; - } - , _runRegistration: function (args) { - // TODO renewal cb - // accountId and or email - return core.accounts.getAsync(args).then(function (account) { - args.account = account; - - - if (args.certificate && args.certificate.privkey && (args.certificate.privkey.jwk || args.certificate.privkey.pem)) { - // TODO import jwk or pem and return it here - console.warn("TODO: implement certificates.checkKeypairAsync skipping"); - } - var domainKeypair; - var newDomainKeypair = true; - // This has been done in the getAsync already, so we skip it here - // if approveDomains doesn't set subject, we set it here - //args.subject = args.subject || args.domains[0]; - var promise = gl.store.certificates.checkKeypairAsync(args).then(function (keypair) { - if (keypair) { - domainKeypair = RSA.import(keypair); - newDomainKeypair = false; - return; - } - - if (args.domainKeypair) { - domainKeypair = RSA.import(args.domainKeypair); - return; - } - - var keypairOpts = { bitlen: args.rsaKeySize, exp: 65537, public: true, pem: true }; - return (args.generateKeypair||RSA.generateKeypairAsync)(keypairOpts).then(function (keypair) { - keypair.privateKeyPem = RSA.exportPrivatePem(keypair); - keypair.publicKeyPem = RSA.exportPublicPem(keypair); - keypair.privateKeyJwk = RSA.exportPrivateJwk(keypair); - domainKeypair = keypair; - }); - }).then(function () { - return domainKeypair; - }); - - return promise.then(function (domainKeypair) { - args.domainKeypair = domainKeypair; - //args.registration = domainKey; - - // Note: the ACME urls are always fetched fresh on purpose - // TODO is this the right place for this? - return core.getAcmeUrlsAsync(args).then(function (urls) { - args._acmeUrls = urls; - - var certReq = { - debug: args.debug || gl.debug - - , newAuthzUrl: args._acmeUrls.newAuthz - , newCertUrl: args._acmeUrls.newCert - - , accountKeypair: RSA.import(account.keypair) - , domainKeypair: domainKeypair - , subject: args.subject // TODO handle this in acme-v2 - , domains: args.domains - , challengeTypes: Object.keys(args.challenges) - }; - - // - // IMPORTANT - // - // setChallenge and removeChallenge are handed defaults - // instead of args because getChallenge does not have - // access to args - // (args is per-request, defaults is per instance) - // - // Each of these fires individually for each domain, - // even though the certificate on the whole may have many domains - // - certReq.setChallenge = function (challenge, done) { - log(args.debug, "setChallenge called for '" + challenge.altname + "'"); - // NOTE: First arg takes precedence - var copy = utils.merge({ domains: [challenge.altname] }, args); - copy = utils.merge(copy, gl); - utils.tplCopy(copy); - copy.challenge = challenge; - - if (1 === copy.challenges[challenge.type].set.length) { - copy.challenges[challenge.type].set(copy).then(function (result) { - done(null, result); - }).catch(done); - } else if (2 === copy.challenges[challenge.type].set.length) { - copy.challenges[challenge.type].set(copy, done); - } else { - 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; - copy.challenges[challenge.type].set(copy, challenge.altname, challenge.token, challenge.keyAuthorization, done); - } - }; - certReq.removeChallenge = function (challenge, done) { - log(args.debug, "removeChallenge called for '" + challenge.altname + "'"); - var copy = utils.merge({ domains: [challenge.altname] }, args); - copy = utils.merge(copy, gl); - utils.tplCopy(copy); - copy.challenge = challenge; - - if (1 === copy.challenges[challenge.type].remove.length) { - copy.challenges[challenge.type].remove(copy).then(function (result) { - done(null, result); - }).catch(done); - } else if (2 === copy.challenges[challenge.type].remove.length) { - copy.challenges[challenge.type].remove(copy, done); - } else { - Object.keys(challenge).forEach(function (key) { - done[key] = challenge[key]; - }); - copy.challenges[challenge.type].remove(copy, challenge.altname, challenge.token, done); - } - }; - certReq.init = function (deps) { - var copy = utils.merge(deps, args); - copy = utils.merge(copy, gl); - utils.tplCopy(copy); - - Object.keys(copy.challenges).forEach(function (key) { - if ('function' === typeof copy.challenges[key].init) { - copy.challenges[key].init(copy); - } - }); - - return null; - }; - certReq.getZones = function (challenge) { - var copy = utils.merge({ dnsHosts: args.domains.map(function (x) { return 'xxxx.' + x; }) }, args); - copy = utils.merge(copy, gl); - utils.tplCopy(copy); - copy.challenge = challenge; - - if (!copy.challenges[challenge.type] || ('function' !== typeof copy.challenges[challenge.type].zones)) { - // may not be available, that's fine. - return Promise.resolve([]); - } - - return copy.challenges[challenge.type].zones(copy); - }; - - log(args.debug, 'calling greenlock.acme.getCertificateAsync', certReq.subject, certReq.domains); - - // TODO acme-v2/nocompat - return gl.acme.getCertificateAsync(certReq).then(utils.attachCertInfo); - }); - }).then(function (results) { - //var requested = {}; - //var issued = {}; - // { cert, chain, privkey /*TODO, subject, altnames, issuedAt, expiresAt */ } - - // args.certs.privkey = RSA.exportPrivatePem(options.domainKeypair); - args.certs = results; - // args.pems is deprecated - args.pems = results; - // This has been done in the getAsync already, so we skip it here - // if approveDomains doesn't set subject, we set it here - //args.subject = args.subject || args.domains[0]; - var promise; - if (newDomainKeypair) { - args.keypair = domainKeypair; - promise = gl.store.certificates.setKeypairAsync(args, domainKeypair); - } - return Promise.resolve(promise).then(function () { - return gl.store.certificates.setAsync(args).then(function () { - return results; - }); - }); - }); - }); - } - // Certificates - , renewAsync: function (args, certs) { - var renewableAt = core.certificates._getRenewableAt(args, certs); - var err; - //var halfLife = (certs.expiresAt - certs.issuedAt) / 2; - //var renewable = (Date.now() - certs.issuedAt) > halfLife; - - log(args.debug, "(Renew) Expires At", new Date(certs.expiresAt).toISOString()); - log(args.debug, "(Renew) Renewable At", new Date(renewableAt).toISOString()); - - if (!args.duplicate && Date.now() < renewableAt) { - err = new Error( - "[ERROR] Certificate issued at '" - + new Date(certs.issuedAt).toISOString() + "' and expires at '" - + new Date(certs.expiresAt).toISOString() + "'. Ignoring renewal attempt until '" - + new Date(renewableAt).toISOString() + "'. Set { duplicate: true } to force." - ); - err.code = 'E_NOT_RENEWABLE'; - return Promise.reject(err); - } - - // Either the cert has entered its renewal period - // or we're forcing a refresh via 'dupliate: true' - log(args.debug, "Renewing!"); - - if (!args.domains || !args.domains.length) { - args.domains = args.servernames || [certs.subject].concat(certs.altnames); - } - - return core.certificates.registerAsync(args); - } - // Certificates - , _isRenewable: function (args, certs) { - var renewableAt = core.certificates._getRenewableAt(args, certs); - - log(args.debug, "Check Expires At", new Date(certs.expiresAt).toISOString()); - log(args.debug, "Check Renewable At", new Date(renewableAt).toISOString()); - - if (args.duplicate || Date.now() >= renewableAt) { - log(args.debug, "certificates are renewable"); - return true; - } - - return false; - } - , _getRenewableAt: function (args, certs) { - return certs.expiresAt - (args.renewWithin || gl.renewWithin); - } - , checkAsync: function (args) { - var copy = utils.merge(args, gl); - // if approveDomains doesn't set subject, we set it here - if (!(copy.domains && copy.domains.length)) { copy.domains = [ copy.subject || copy.domain ].filter(Boolean); } - if (!copy.subject) { copy.subject = copy.domains[0]; } - if (!copy.domain) { copy.domain = copy.domains[0]; } - args = utils.tplCopy(copy); - - // returns pems - return gl.store.certificates.checkAsync(args).then(function (cert) { - if (!cert) { log(args.debug, 'checkAsync failed to find certificates'); return null; } - - cert = utils.attachCertInfo(cert); - if (utils.certHasDomain(cert, args.domain)) { - log(args.debug, 'checkAsync found existing certificates'); - - if (cert.privkey) { - return cert; - } else { - return gl.store.certificates.checkKeypairAsync(args).then(function (keypair) { - cert.privkey = keypair.privateKeyPem || RSA.exportPrivatePem(keypair); - return cert; - }); - } - } - log(args.debug, 'checkAsync found mismatched / incomplete certificates'); - }); - } - // Certificates - , getAsync: function (args) { - var copy = utils.merge(args, gl); - // if approveDomains doesn't set subject, we set it here - if (!(copy.domains && copy.domains.length)) { copy.domains = [ copy.subject || copy.domain ].filter(Boolean); } - if (!copy.subject) { copy.subject = copy.domains[0]; } - if (!copy.domain) { copy.domain = copy.domains[0]; } - args = utils.tplCopy(copy); - - if (args.certificate && args.certificate.privkey && args.certificate.cert && args.certificate.chain) { - // TODO skip fetching a certificate if it's fetched during approveDomains - console.warn("TODO: implement certificates.checkAsync skipping"); - } - return core.certificates.checkAsync(args).then(function (certs) { - if (certs) { certs = utils.attachCertInfo(certs); } - if (!certs || !utils.certHasDomain(certs, args.domain)) { - // There is no cert available - if (false !== args.securityUpdates && !args._communityMemberAdded) { - // We will notify all greenlock users of mandatory and security updates - // We'll keep track of versions and os so we can make sure things work well - // { name, version, email, domains, action, communityMember, telemetry } - require('./community').add({ - name: args._communityPackage - , version: args._communityPackageVersion - , email: args.email - , domains: args.domains || args.servernames - , action: 'reg' - , communityMember: args.communityMember - , telemetry: args.telemetry - }); - args._communityMemberAdded = true; - } - return core.certificates.registerAsync(args); - } - - if (core.certificates._isRenewable(args, certs)) { - // it's time to renew the available cert - if (false !== args.securityUpdates && !args._communityMemberAdded) { - // We will notify all greenlock users of mandatory and security updates - // We'll keep track of versions and os so we can make sure things work well - // { name, version, email, domains, action, communityMember, telemetry } - require('./community').add({ - name: args._communityPackage - , version: args._communityPackageVersion - , email: args.email - , domains: args.domains || args.servernames - , action: 'renew' - , communityMember: args.communityMember - , telemetry: args.telemetry - }); - args._communityMemberAdded = true; - } - certs.renewing = core.certificates.renewAsync(args, certs); - if (args.waitForRenewal) { - return certs.renewing; - } - } - - // return existing unexpired (although potentially stale) certificates when available - // there will be an additional .renewing property if the certs are being asynchronously renewed - return certs; - }).then(function (results) { - // returns pems - return results; - }); - } - } - - }; - - return core; +module.exports.create = function(gl) { + var utils = require('./utils'); + var RSA = promisifyAll(require('rsa-compat').RSA); + var log = gl.log || _log; // allow custom log + var pendingRegistrations = {}; + + var core = { + // + // Helpers + // + getAcmeUrlsAsync: function(args) { + var now = Date.now(); + + // TODO check response header on request for cache time + if (now - gl._ipc.acmeUrlsUpdatedAt < 10 * 60 * 1000) { + return Promise.resolve(gl._ipc.acmeUrls); + } + + // TODO acme-v2/nocompat + return gl.acme.getAcmeUrlsAsync(args.server).then(function(data) { + gl._ipc.acmeUrlsUpdatedAt = Date.now(); + gl._ipc.acmeUrls = data; + + return gl._ipc.acmeUrls; + }); + }, + + // + // The Main Enchilada + // + + // + // Accounts + // + accounts: { + // Accounts + registerAsync: function(args) { + var err; + var copy = utils.merge(args, gl); + var disagreeTos; + args = utils.tplCopy(copy); + if (!args.account) { + args.account = {}; + } + if ('object' === typeof args.account && !args.account.id) { + args.account.id = args.accountId || args.email || ''; + } + + disagreeTos = + !args.agreeTos && 'undefined' !== typeof args.agreeTos; + if ( + !args.email || + disagreeTos || + parseInt(args.rsaKeySize, 10) < 2048 + ) { + err = new Error( + "In order to register an account both 'email' and 'agreeTos' must be present" + + " and 'rsaKeySize' must be 2048 or greater." + ); + err.code = 'E_ARGS'; + return Promise.reject(err); + } + + return utils.testEmail(args.email).then(function() { + if ( + args.account && + args.account.privkey && + (args.account.privkey.jwk || args.account.privkey.pem) + ) { + // TODO import jwk or pem and return it here + console.warn( + 'TODO: implement accounts.checkKeypairAsync skipping' + ); + } + var accountKeypair; + var newAccountKeypair = true; + var promise = gl.store.accounts + .checkKeypairAsync(args) + .then(function(keypair) { + if (keypair) { + // TODO keypairs + newAccountKeypair = false; + accountKeypair = RSA.import(keypair); + return; + } + + if (args.accountKeypair) { + // TODO keypairs + accountKeypair = RSA.import( + args.accountKeypair + ); + return; + } + + var keypairOpts = { + bitlen: args.rsaKeySize, + exp: 65537, + public: true, + pem: true + }; + // TODO keypairs + return (args.generateKeypair || + RSA.generateKeypairAsync)(keypairOpts).then( + function(keypair) { + keypair.privateKeyPem = RSA.exportPrivatePem( + keypair + ); + keypair.publicKeyPem = RSA.exportPublicPem( + keypair + ); + keypair.privateKeyJwk = RSA.exportPrivateJwk( + keypair + ); + accountKeypair = keypair; + } + ); + }) + .then(function() { + return accountKeypair; + }); + + return promise.then(function(keypair) { + // Note: the ACME urls are always fetched fresh on purpose + // TODO acme-v2/nocompat + return core.getAcmeUrlsAsync(args).then(function(urls) { + args._acmeUrls = urls; + + // TODO acme-v2/nocompat + return gl.acme + .registerNewAccountAsync({ + email: args.email, + newRegUrl: args._acmeUrls.newReg, + newAuthzUrl: args._acmeUrls.newAuthz, + agreeToTerms: function(tosUrl, agreeCb) { + if ( + true === args.agreeTos || + tosUrl === args.agreeTos || + tosUrl === gl.agreeToTerms + ) { + agreeCb(null, tosUrl); + return; + } + + // args.email = email; // already there + // args.domains = domains // already there + args.tosUrl = tosUrl; + gl.agreeToTerms(args, agreeCb); + }, + accountKeypair: keypair, + + debug: gl.debug || args.debug + }) + .then(function(receipt) { + var reg = { + keypair: keypair, + receipt: receipt, + kid: + receipt && + receipt.key && + (receipt.key.kid || receipt.kid), + email: args.email, + newRegUrl: args._acmeUrls.newReg, + newAuthzUrl: args._acmeUrls.newAuthz + }; + + var accountKeypairPromise; + args.keypair = keypair; + args.receipt = receipt; + if (newAccountKeypair) { + accountKeypairPromise = gl.store.accounts.setKeypairAsync( + args, + keypair + ); + } + return Promise.resolve( + accountKeypairPromise + ).then(function() { + // TODO move templating of arguments to right here? + 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'" + ); + } + if (!account) { + account = {}; + } + account.keypair = keypair; + return account; + }); + }); + }); + }); + }); + }); + }, + + // Accounts + // (only used for keypair) + getAsync: function(args) { + var accountPromise = null; + if (gl.store.accounts.checkAsync) { + accountPromise = core.accounts.checkAsync(args); + } + return Promise.resolve(accountPromise).then(function(account) { + if (!account) { + return core.accounts.registerAsync(args); + } + if (account.keypair) { + return account; + } + + if (!args.account) { + args.account = {}; + } + if ('object' === typeof args.account && !args.account.id) { + args.account.id = args.accountId || args.email || ''; + } + var copy = utils.merge(args, gl); + args = utils.tplCopy(copy); + return gl.store.accounts + .checkKeypairAsync(args) + .then(function(keypair) { + if (keypair) { + return { keypair: keypair }; + } + return core.accounts.registerAsync(args); + }); + }); + }, + + // Accounts + checkAsync: function(args) { + 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 Promise.reject( + new Error( + "In order to register or retrieve an account one of '" + + requiredArgs.join("', '") + + "' must be present" + ) + ); + } + + var copy = utils.merge(args, gl); + args = utils.tplCopy(copy); + if (!args.account) { + args.account = {}; + } + if ('object' === typeof args.account && !args.account.id) { + args.account.id = args.accountId || args.email || ''; + } + + // 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 Promise.resolve(null); + } + return gl.store.accounts + .checkAsync(args) + .then(function(account) { + if (!account) { + return null; + } + + args.account = account; + args.accountId = account.id; + + return account; + }); + } + }, + + certificates: { + // Certificates + registerAsync: function(args) { + var err; + var challengeDefaults = + gl[ + '_challengeOpts_' + + (args.challengeType || gl.challengeType) + ] || {}; + var copy = utils.merge(args, challengeDefaults || {}); + copy = utils.merge(copy, gl); + if (!copy.subject) { + copy.subject = copy.domains[0]; + } + if (!copy.domain) { + copy.domain = copy.domains[0]; + } + args = utils.tplCopy(copy); + + if (!Array.isArray(args.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]) { + console.warn( + "The certificate's subject (primary domain) should be first in the list of opts.domains" + ); + console.warn( + '\topts.subject: (set by you approveDomains(), falling back to opts.domain) ' + + args.subject + ); + console.warn( + '\topts.domain: (set by SNICallback()) ' + args.domain + ); + 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 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 + // (or dns-01 validation may be used) + // 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 Promise.reject(err); + } + + // If a previous request to (re)register a certificate is already underway we need + // to return the same promise created before rather than registering things twice. + // I'm not 100% sure how to properly handle the case where someone registers domain + // lists with some but not all elements common, nor am I sure that's even a case that + // is allowed to happen anyway. But for now we act like the list is completely the + // same if any elements are the same. + var promise; + args.domains.some(function(name) { + if (pendingRegistrations.hasOwnProperty(name)) { + promise = pendingRegistrations[name]; + return true; + } + }); + if (promise) { + return promise; + } + + promise = core.certificates._runRegistration(args); + + // Now that the registration is actually underway we need to make sure any subsequent + // registration attempts return the same promise until it is completed (but not after + // it is completed). + args.domains.forEach(function(name) { + pendingRegistrations[name] = promise; + }); + function clearPending() { + args.domains.forEach(function(name) { + delete pendingRegistrations[name]; + }); + } + promise.then(clearPending, clearPending); + + return promise; + }, + _runRegistration: function(args) { + // TODO renewal cb + // accountId and or email + return core.accounts.getAsync(args).then(function(account) { + args.account = account; + + if ( + args.certificate && + args.certificate.privkey && + (args.certificate.privkey.jwk || + args.certificate.privkey.pem) + ) { + // TODO import jwk or pem and return it here + console.warn( + 'TODO: implement certificates.checkKeypairAsync skipping' + ); + } + var domainKeypair; + var newDomainKeypair = true; + // This has been done in the getAsync already, so we skip it here + // if approveDomains doesn't set subject, we set it here + //args.subject = args.subject || args.domains[0]; + var promise = gl.store.certificates + .checkKeypairAsync(args) + .then(function(keypair) { + if (keypair) { + domainKeypair = RSA.import(keypair); + newDomainKeypair = false; + return; + } + + if (args.domainKeypair) { + domainKeypair = RSA.import(args.domainKeypair); + return; + } + + var keypairOpts = { + bitlen: args.rsaKeySize, + exp: 65537, + public: true, + pem: true + }; + return (args.generateKeypair || + RSA.generateKeypairAsync)(keypairOpts).then( + function(keypair) { + keypair.privateKeyPem = RSA.exportPrivatePem( + keypair + ); + keypair.publicKeyPem = RSA.exportPublicPem( + keypair + ); + keypair.privateKeyJwk = RSA.exportPrivateJwk( + keypair + ); + domainKeypair = keypair; + } + ); + }) + .then(function() { + return domainKeypair; + }); + + return promise + .then(function(domainKeypair) { + args.domainKeypair = domainKeypair; + //args.registration = domainKey; + + // Note: the ACME urls are always fetched fresh on purpose + // TODO is this the right place for this? + return core + .getAcmeUrlsAsync(args) + .then(function(urls) { + args._acmeUrls = urls; + + var certReq = { + debug: args.debug || gl.debug, + + newAuthzUrl: args._acmeUrls.newAuthz, + newCertUrl: args._acmeUrls.newCert, + + accountKeypair: RSA.import( + account.keypair + ), + domainKeypair: domainKeypair, + subject: args.subject, // TODO handle this in acme-v2 + domains: args.domains, + challengeTypes: Object.keys( + args.challenges + ) + }; + + // + // IMPORTANT + // + // setChallenge and removeChallenge are handed defaults + // instead of args because getChallenge does not have + // access to args + // (args is per-request, defaults is per instance) + // + // Each of these fires individually for each domain, + // even though the certificate on the whole may have many domains + // + certReq.setChallenge = function( + challenge, + done + ) { + log( + args.debug, + "setChallenge called for '" + + challenge.altname + + "'" + ); + // NOTE: First arg takes precedence + var copy = utils.merge( + { domains: [challenge.altname] }, + args + ); + copy = utils.merge(copy, gl); + utils.tplCopy(copy); + copy.challenge = challenge; + + if ( + 1 === + copy.challenges[challenge.type].set + .length + ) { + copy.challenges[challenge.type] + .set(copy) + .then(function(result) { + done(null, result); + }) + .catch(done); + } else if ( + 2 === + copy.challenges[challenge.type].set + .length + ) { + copy.challenges[challenge.type].set( + copy, + done + ); + } else { + 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; + copy.challenges[challenge.type].set( + copy, + challenge.altname, + challenge.token, + challenge.keyAuthorization, + done + ); + } + }; + certReq.removeChallenge = function( + challenge, + done + ) { + log( + args.debug, + "removeChallenge called for '" + + challenge.altname + + "'" + ); + var copy = utils.merge( + { domains: [challenge.altname] }, + args + ); + copy = utils.merge(copy, gl); + utils.tplCopy(copy); + copy.challenge = challenge; + + if ( + 1 === + copy.challenges[challenge.type] + .remove.length + ) { + copy.challenges[challenge.type] + .remove(copy) + .then(function(result) { + done(null, result); + }) + .catch(done); + } else if ( + 2 === + copy.challenges[challenge.type] + .remove.length + ) { + copy.challenges[ + challenge.type + ].remove(copy, done); + } else { + Object.keys(challenge).forEach( + function(key) { + done[key] = challenge[key]; + } + ); + copy.challenges[ + challenge.type + ].remove( + copy, + challenge.altname, + challenge.token, + done + ); + } + }; + certReq.init = function(deps) { + var copy = utils.merge(deps, args); + copy = utils.merge(copy, gl); + utils.tplCopy(copy); + + Object.keys(copy.challenges).forEach( + function(key) { + if ( + 'function' === + typeof copy.challenges[key] + .init + ) { + copy.challenges[key].init( + copy + ); + } + } + ); + + return null; + }; + certReq.getZones = function(challenge) { + var copy = utils.merge( + { + dnsHosts: args.domains.map( + function(x) { + return 'xxxx.' + x; + } + ) + }, + args + ); + copy = utils.merge(copy, gl); + utils.tplCopy(copy); + copy.challenge = challenge; + + if ( + !copy.challenges[challenge.type] || + 'function' !== + typeof copy.challenges[ + challenge.type + ].zones + ) { + // may not be available, that's fine. + return Promise.resolve([]); + } + + return copy.challenges[ + challenge.type + ].zones(copy); + }; + + log( + args.debug, + 'calling greenlock.acme.getCertificateAsync', + certReq.subject, + certReq.domains + ); + + // TODO acme-v2/nocompat + return gl.acme + .getCertificateAsync(certReq) + .then(utils.attachCertInfo); + }); + }) + .then(function(results) { + //var requested = {}; + //var issued = {}; + // { cert, chain, privkey /*TODO, subject, altnames, issuedAt, expiresAt */ } + + // args.certs.privkey = RSA.exportPrivatePem(options.domainKeypair); + args.certs = results; + // args.pems is deprecated + args.pems = results; + // This has been done in the getAsync already, so we skip it here + // if approveDomains doesn't set subject, we set it here + //args.subject = args.subject || args.domains[0]; + var promise; + if (newDomainKeypair) { + args.keypair = domainKeypair; + promise = gl.store.certificates.setKeypairAsync( + args, + domainKeypair + ); + } + return Promise.resolve(promise).then(function() { + return gl.store.certificates + .setAsync(args) + .then(function() { + return results; + }); + }); + }); + }); + }, + // Certificates + renewAsync: function(args, certs) { + var renewableAt = core.certificates._getRenewableAt( + args, + certs + ); + var err; + //var halfLife = (certs.expiresAt - certs.issuedAt) / 2; + //var renewable = (Date.now() - certs.issuedAt) > halfLife; + + log( + args.debug, + '(Renew) Expires At', + new Date(certs.expiresAt).toISOString() + ); + log( + args.debug, + '(Renew) Renewable At', + new Date(renewableAt).toISOString() + ); + + if (!args.duplicate && Date.now() < renewableAt) { + err = new Error( + "[ERROR] Certificate issued at '" + + new Date(certs.issuedAt).toISOString() + + "' and expires at '" + + new Date(certs.expiresAt).toISOString() + + "'. Ignoring renewal attempt until '" + + new Date(renewableAt).toISOString() + + "'. Set { duplicate: true } to force." + ); + err.code = 'E_NOT_RENEWABLE'; + return Promise.reject(err); + } + + // Either the cert has entered its renewal period + // or we're forcing a refresh via 'dupliate: true' + log(args.debug, 'Renewing!'); + + if (!args.domains || !args.domains.length) { + args.domains = + args.servernames || + [certs.subject].concat(certs.altnames); + } + + return core.certificates.registerAsync(args); + }, + // Certificates + _isRenewable: function(args, certs) { + var renewableAt = core.certificates._getRenewableAt( + args, + certs + ); + + log( + args.debug, + 'Check Expires At', + new Date(certs.expiresAt).toISOString() + ); + log( + args.debug, + 'Check Renewable At', + new Date(renewableAt).toISOString() + ); + + if (args.duplicate || Date.now() >= renewableAt) { + log(args.debug, 'certificates are renewable'); + return true; + } + + return false; + }, + _getRenewableAt: function(args, certs) { + return certs.expiresAt - (args.renewWithin || gl.renewWithin); + }, + checkAsync: function(args) { + var copy = utils.merge(args, gl); + // if approveDomains doesn't set subject, we set it here + if (!(copy.domains && copy.domains.length)) { + copy.domains = [copy.subject || copy.domain].filter( + Boolean + ); + } + if (!copy.subject) { + copy.subject = copy.domains[0]; + } + if (!copy.domain) { + copy.domain = copy.domains[0]; + } + args = utils.tplCopy(copy); + + // returns pems + return gl.store.certificates + .checkAsync(args) + .then(function(cert) { + if (!cert) { + log( + args.debug, + 'checkAsync failed to find certificates' + ); + return null; + } + + cert = utils.attachCertInfo(cert); + if (utils.certHasDomain(cert, args.domain)) { + log( + args.debug, + 'checkAsync found existing certificates' + ); + + if (cert.privkey) { + return cert; + } else { + return gl.store.certificates + .checkKeypairAsync(args) + .then(function(keypair) { + cert.privkey = + keypair.privateKeyPem || + RSA.exportPrivatePem(keypair); + return cert; + }); + } + } + log( + args.debug, + 'checkAsync found mismatched / incomplete certificates' + ); + }); + }, + // Certificates + getAsync: function(args) { + var copy = utils.merge(args, gl); + // if approveDomains doesn't set subject, we set it here + if (!(copy.domains && copy.domains.length)) { + copy.domains = [copy.subject || copy.domain].filter( + Boolean + ); + } + if (!copy.subject) { + copy.subject = copy.domains[0]; + } + if (!copy.domain) { + copy.domain = copy.domains[0]; + } + args = utils.tplCopy(copy); + + if ( + args.certificate && + args.certificate.privkey && + args.certificate.cert && + args.certificate.chain + ) { + // TODO skip fetching a certificate if it's fetched during approveDomains + console.warn( + 'TODO: implement certificates.checkAsync skipping' + ); + } + return core.certificates + .checkAsync(args) + .then(function(certs) { + if (certs) { + certs = utils.attachCertInfo(certs); + } + if ( + !certs || + !utils.certHasDomain(certs, args.domain) + ) { + // There is no cert available + if ( + false !== args.securityUpdates && + !args._communityMemberAdded + ) { + // We will notify all greenlock users of mandatory and security updates + // We'll keep track of versions and os so we can make sure things work well + // { name, version, email, domains, action, communityMember, telemetry } + require('./community').add({ + name: args._communityPackage, + version: args._communityPackageVersion, + email: args.email, + domains: args.domains || args.servernames, + action: 'reg', + communityMember: args.communityMember, + telemetry: args.telemetry + }); + args._communityMemberAdded = true; + } + return core.certificates.registerAsync(args); + } + + if (core.certificates._isRenewable(args, certs)) { + // it's time to renew the available cert + if ( + false !== args.securityUpdates && + !args._communityMemberAdded + ) { + // We will notify all greenlock users of mandatory and security updates + // We'll keep track of versions and os so we can make sure things work well + // { name, version, email, domains, action, communityMember, telemetry } + require('./community').add({ + name: args._communityPackage, + version: args._communityPackageVersion, + email: args.email, + domains: args.domains || args.servernames, + action: 'renew', + communityMember: args.communityMember, + telemetry: args.telemetry + }); + args._communityMemberAdded = true; + } + certs.renewing = core.certificates.renewAsync( + args, + certs + ); + if (args.waitForRenewal) { + return certs.renewing; + } + } + + // return existing unexpired (although potentially stale) certificates when available + // there will be an additional .renewing property if the certs are being asynchronously renewed + return certs; + }) + .then(function(results) { + // returns pems + return results; + }); + } + } + }; + + return core; }; diff --git a/lib/middleware.js b/lib/middleware.js index 1535e53..2552b8b 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -3,92 +3,106 @@ var utils = require('./utils'); function _log(debug) { - if (debug) { - var args = Array.prototype.slice.call(arguments); - args.shift(); - args.unshift("[greenlock/lib/middleware.js]"); - console.log.apply(console, args); - } + if (debug) { + var args = Array.prototype.slice.call(arguments); + args.shift(); + args.unshift('[greenlock/lib/middleware.js]'); + console.log.apply(console, args); + } } -module.exports.create = function (gl) { - if (!gl.challenges['http-01'] || !gl.challenges['http-01'].get) { - throw new Error("middleware requires challenge plugin with get method"); - } - var log = gl.log || _log; +module.exports.create = function(gl) { + if (!gl.challenges['http-01'] || !gl.challenges['http-01'].get) { + throw new Error('middleware requires challenge plugin with get method'); + } + var log = gl.log || _log; - log(gl.debug, "created middleware"); - return function (_app) { - if (_app && 'function' !== typeof _app) { - throw new Error("use greenlock.middleware() or greenlock.middleware(function (req, res) {})"); - } - var prefix = gl.acmeChallengePrefix || '/.well-known/acme-challenge/'; + log(gl.debug, 'created middleware'); + return function(_app) { + if (_app && 'function' !== typeof _app) { + throw new Error( + 'use greenlock.middleware() or greenlock.middleware(function (req, res) {})' + ); + } + var prefix = gl.acmeChallengePrefix || '/.well-known/acme-challenge/'; - return function (req, res, next) { - if (0 !== req.url.indexOf(prefix)) { - log(gl.debug, "no match, skipping middleware"); - if ('function' === typeof _app) { - _app(req, res, next); - } - else if ('function' === typeof next) { - next(); - } - else { - res.statusCode = 500; - res.end("[500] Developer Error: app.use('/', greenlock.middleware()) or greenlock.middleware(app)"); - } - return; - } + return function(req, res, next) { + if (0 !== req.url.indexOf(prefix)) { + log(gl.debug, 'no match, skipping middleware'); + if ('function' === typeof _app) { + _app(req, res, next); + } else if ('function' === typeof next) { + next(); + } else { + res.statusCode = 500; + res.end( + "[500] Developer Error: app.use('/', greenlock.middleware()) or greenlock.middleware(app)" + ); + } + return; + } - log(gl.debug, "this must be tinder, 'cuz it's a match!"); + log(gl.debug, "this must be tinder, 'cuz it's a match!"); - var token = req.url.slice(prefix.length); - var hostname = req.hostname || (req.headers.host || '').toLowerCase().replace(/:.*/, ''); + var token = req.url.slice(prefix.length); + var hostname = + req.hostname || + (req.headers.host || '').toLowerCase().replace(/:.*/, ''); - log(gl.debug, "hostname", hostname, "token", token); + log(gl.debug, 'hostname', hostname, 'token', token); - var copy = utils.merge({ domains: [ hostname ] }, gl); - copy = utils.tplCopy(copy); - copy.challenge = {}; - copy.challenge.type = 'http-01'; // obviously... - copy.challenge.identifier = { type: 'dns', value: hostname }; - copy.challenge.wildcard = false; - copy.challenge.token = token; - copy.challenge.altname = hostname; + var copy = utils.merge({ domains: [hostname] }, gl); + copy = utils.tplCopy(copy); + copy.challenge = {}; + copy.challenge.type = 'http-01'; // obviously... + copy.challenge.identifier = { type: 'dns', value: hostname }; + copy.challenge.wildcard = false; + copy.challenge.token = token; + copy.challenge.altname = hostname; - function cb(opts) { - var secret = opts.keyAuthorization || opts; - if (secret && 'string' === typeof secret) { - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.end(secret); - return; - } - eb(new Error("couldn't retrieve keyAuthorization")); - return; - } - function eb(/*err*/) { - res.statusCode = 404; - res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.end('{ "error": { "message": "Error: These aren\'t the tokens you\'re looking for. Move along." } }'); - return; - } - function mb(err, result) { - if (err) { eb(err); return; } - cb(result); - } + function cb(opts) { + var secret = opts.keyAuthorization || opts; + if (secret && 'string' === typeof secret) { + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.end(secret); + return; + } + eb(new Error("couldn't retrieve keyAuthorization")); + return; + } + function eb(/*err*/) { + res.statusCode = 404; + res.setHeader( + 'Content-Type', + 'application/json; charset=utf-8' + ); + res.end( + '{ "error": { "message": "Error: These aren\'t the tokens you\'re looking for. Move along." } }' + ); + return; + } + function mb(err, result) { + if (err) { + eb(err); + return; + } + cb(result); + } - var challenger = gl.challenges['http-01'].get; - if (1 === challenger.length) { - /*global Promise*/ - return Promise.resolve().then(function () { - return gl.challenges['http-01'].get(copy); - }).then(cb).catch(eb); - } else if (2 === challenger.length) { - gl.challenges['http-01'].get(copy, mb); - } else { - gl.challenges['http-01'].get(copy, hostname, token, mb); - } - - }; - }; + var challenger = gl.challenges['http-01'].get; + if (1 === challenger.length) { + /*global Promise*/ + return Promise.resolve() + .then(function() { + return gl.challenges['http-01'].get(copy); + }) + .then(cb) + .catch(eb); + } else if (2 === challenger.length) { + gl.challenges['http-01'].get(copy, mb); + } else { + gl.challenges['http-01'].get(copy, hostname, token, mb); + } + }; + }; }; diff --git a/lib/utils-test.js b/lib/utils-test.js index 964d727..aefb4d8 100644 --- a/lib/utils-test.js +++ b/lib/utils-test.js @@ -1,24 +1,24 @@ 'use strict'; -var utils = require('./utils.js') -var cert = { subject: 'example.com', altnames: ['*.bar.com','foo.net'] }; +var utils = require('./utils.js'); +var cert = { subject: 'example.com', altnames: ['*.bar.com', 'foo.net'] }; if (utils.certHasDomain(cert, 'bad.com')) { - throw new Error("allowed bad domain"); + throw new Error('allowed bad domain'); } if (!utils.certHasDomain(cert, 'example.com')) { - throw new Error("missed subject"); + throw new Error('missed subject'); } if (utils.certHasDomain(cert, 'bar.com')) { - throw new Error("allowed bad (missing) sub"); + throw new Error('allowed bad (missing) sub'); } if (!utils.certHasDomain(cert, 'foo.bar.com')) { - throw new Error("didn't allow valid wildcarded-domain"); + throw new Error("didn't allow valid wildcarded-domain"); } if (utils.certHasDomain(cert, 'dub.foo.bar.com')) { - throw new Error("allowed sub-sub domain"); + throw new Error('allowed sub-sub domain'); } if (!utils.certHasDomain(cert, 'foo.net')) { - throw new Error("missed altname"); + throw new Error('missed altname'); } -console.info("PASSED"); +console.info('PASSED'); diff --git a/lib/utils.js b/lib/utils.js index 46324b5..d526cc1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,59 +2,62 @@ require('./compat.js'); var path = require('path'); -var homeRe = new RegExp("^~(\\/|\\\\|\\" + path.sep + ")"); +var homeRe = new RegExp('^~(\\/|\\\\|\\' + path.sep + ')'); // very basic check. Allows *.example.com. var re = /^(\*\.)?[a-zA-Z0-9\.\-]+$/; var punycode = require('punycode'); var dnsResolveMxAsync = require('util').promisify(require('dns').resolveMx); -module.exports.attachCertInfo = function (results) { - var certInfo = require('cert-info').info(results.cert); +module.exports.attachCertInfo = function(results) { + var certInfo = require('cert-info').info(results.cert); - // subject, altnames, issuedAt, expiresAt - Object.keys(certInfo).forEach(function (key) { - results[key] = certInfo[key]; - }); + // subject, altnames, issuedAt, expiresAt + Object.keys(certInfo).forEach(function(key) { + results[key] = certInfo[key]; + }); - return results; + return results; }; -module.exports.certHasDomain = function (certInfo, _domain) { - var names = (certInfo.altnames || []).slice(0); - names.push(certInfo.subject); - return names.some(function (name) { - var domain = _domain.toLowerCase(); - name = name.toLowerCase(); - if ('*.' === name.substr(0, 2)) { - name = name.substr(2); - domain = domain.split('.').slice(1).join('.'); - } - return name === domain; - }); +module.exports.certHasDomain = function(certInfo, _domain) { + var names = (certInfo.altnames || []).slice(0); + names.push(certInfo.subject); + return names.some(function(name) { + var domain = _domain.toLowerCase(); + name = name.toLowerCase(); + if ('*.' === name.substr(0, 2)) { + name = name.substr(2); + domain = domain + .split('.') + .slice(1) + .join('.'); + } + return name === domain; + }); }; -module.exports.isValidDomain = function (domain) { - if (re.test(domain)) { - return domain; - } +module.exports.isValidDomain = function(domain) { + if (re.test(domain)) { + return domain; + } - domain = punycode.toASCII(domain); + domain = punycode.toASCII(domain); - if (re.test(domain)) { - return domain; - } + if (re.test(domain)) { + return domain; + } - return ''; + return ''; }; -module.exports.merge = function (/*defaults, args*/) { - var allDefaults = Array.prototype.slice.apply(arguments); - var args = allDefaults.shift(); - var copy = {}; +module.exports.merge = function(/*defaults, args*/) { + var allDefaults = Array.prototype.slice.apply(arguments); + var args = allDefaults.shift(); + var copy = {}; - allDefaults.forEach(function (defaults) { - Object.keys(defaults).forEach(function (key) { - /* + allDefaults.forEach(function(defaults) { + Object.keys(defaults).forEach(function(key) { + /* if ('challenges' === key && copy[key] && defaults[key]) { Object.keys(defaults[key]).forEach(function (k) { copy[key][k] = defaults[key][k]; @@ -63,12 +66,12 @@ module.exports.merge = function (/*defaults, args*/) { copy[key] = defaults[key]; } */ - copy[key] = defaults[key]; - }); - }); + copy[key] = defaults[key]; + }); + }); - Object.keys(args).forEach(function (key) { - /* + Object.keys(args).forEach(function(key) { + /* if ('challenges' === key && copy[key] && args[key]) { Object.keys(args[key]).forEach(function (k) { copy[key][k] = args[key][k]; @@ -77,81 +80,86 @@ module.exports.merge = function (/*defaults, args*/) { copy[key] = args[key]; } */ - copy[key] = args[key]; - }); + copy[key] = args[key]; + }); - return copy; + return copy; }; -module.exports.tplCopy = function (copy) { - var homedir = require('os').homedir(); - var tplKeys; +module.exports.tplCopy = function(copy) { + var homedir = require('os').homedir(); + var tplKeys; - copy.hostnameGet = function (copy) { - return copy.subject || (copy.domains || [])[0] || copy.domain; - }; + copy.hostnameGet = function(copy) { + return copy.subject || (copy.domains || [])[0] || copy.domain; + }; - Object.keys(copy).forEach(function (key) { - var newName; - if (!/Get$/.test(key)) { - return; - } + Object.keys(copy).forEach(function(key) { + var newName; + if (!/Get$/.test(key)) { + return; + } - newName = key.replace(/Get$/, ''); - copy[newName] = copy[newName] || copy[key](copy); - }); + newName = key.replace(/Get$/, ''); + copy[newName] = copy[newName] || copy[key](copy); + }); - tplKeys = Object.keys(copy); - tplKeys.sort(function (a, b) { - return b.length - a.length; - }); + tplKeys = Object.keys(copy); + tplKeys.sort(function(a, b) { + return b.length - a.length; + }); - tplKeys.forEach(function (key) { - if ('string' !== typeof copy[key]) { - return; - } + tplKeys.forEach(function(key) { + if ('string' !== typeof copy[key]) { + return; + } - copy[key] = copy[key].replace(homeRe, homedir + path.sep); - }); + copy[key] = copy[key].replace(homeRe, homedir + path.sep); + }); - tplKeys.forEach(function (key) { - if ('string' !== typeof copy[key]) { - return; - } + tplKeys.forEach(function(key) { + if ('string' !== typeof copy[key]) { + return; + } - tplKeys.forEach(function (tplname) { - if (!copy[tplname]) { - // what can't be templated now may be templatable later - return; - } - copy[key] = copy[key].replace(':' + tplname, copy[tplname]); - }); - }); + tplKeys.forEach(function(tplname) { + if (!copy[tplname]) { + // what can't be templated now may be templatable later + return; + } + copy[key] = copy[key].replace(':' + tplname, copy[tplname]); + }); + }); - return copy; + return copy; }; -module.exports.testEmail = function (email) { - var parts = (email||'').split('@'); - var err; +module.exports.testEmail = function(email) { + var parts = (email || '').split('@'); + var err; - if (2 !== parts.length || !parts[0] || !parts[1]) { - err = new Error("malformed email address '" + email + "'"); - err.code = 'E_EMAIL'; - return Promise.reject(err); - } + if (2 !== parts.length || !parts[0] || !parts[1]) { + err = new Error("malformed email address '" + email + "'"); + err.code = 'E_EMAIL'; + return Promise.reject(err); + } - return dnsResolveMxAsync(parts[1]).then(function (records) { - // records only returns when there is data - if (!records.length) { - throw new Error("sanity check fail: success, but no MX records returned"); - } - return email; - }, function (err) { - if ('ENODATA' === err.code) { - err = new Error("no MX records found for '" + parts[1] + "'"); - err.code = 'E_EMAIL'; - return Promise.reject(err); - } - }); + return dnsResolveMxAsync(parts[1]).then( + function(records) { + // records only returns when there is data + if (!records.length) { + throw new Error( + 'sanity check fail: success, but no MX records returned' + ); + } + return email; + }, + function(err) { + if ('ENODATA' === err.code) { + err = new Error("no MX records found for '" + parts[1] + "'"); + err.code = 'E_EMAIL'; + return Promise.reject(err); + } + } + ); }; diff --git a/tests/check-account.js b/tests/check-account.js index 4dfaaa7..3ff7bb3 100644 --- a/tests/check-account.js +++ b/tests/check-account.js @@ -2,13 +2,13 @@ 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/' - , webrootPath: '~/letsencrypt.test/tmp/:hostname' - }) -, debug: true + server: 'staging', + acme: require('le-acme-core').ACME.create(), + store: require('le-store-certbot').create({ + configDir: '~/letsencrypt.test/etc/', + webrootPath: '~/letsencrypt.test/tmp/:hostname' + }), + debug: true }); // TODO test generateRsaKey code path separately @@ -20,37 +20,45 @@ var testEmail = 'coolaj86+le.' + testId + '@gmail.com'; var testAccountId = '939573edbf2506c92c9ab32131209d7b'; var tests = [ - function () { - return le.core.accounts.checkAsync({ - accountId: testAccountId - }).then(function (account) { - if (!account) { - throw new Error("Test account should exist when searched by account id."); - } - }); - } + function() { + return le.core.accounts + .checkAsync({ + accountId: testAccountId + }) + .then(function(account) { + if (!account) { + throw new Error( + 'Test account should exist when searched by account id.' + ); + } + }); + }, -, function () { - return le.core.accounts.checkAsync({ - email: testEmail - }).then(function (account) { - console.log('account.regr'); - console.log(account.regr); - if (!account) { - throw new Error("Test account should exist when searched by email."); - } - }); - } + function() { + return le.core.accounts + .checkAsync({ + email: testEmail + }) + .then(function(account) { + console.log('account.regr'); + console.log(account.regr); + if (!account) { + throw new Error( + 'Test account should exist when searched by email.' + ); + } + }); + } ]; function run() { - var test = tests.shift(); - if (!test) { - console.info('All tests passed'); - return; - } + var test = tests.shift(); + if (!test) { + console.info('All tests passed'); + return; + } - test().then(run); + test().then(run); } run(); diff --git a/tests/create-account.js b/tests/create-account.js index d648770..a7e6bc1 100644 --- a/tests/create-account.js +++ b/tests/create-account.js @@ -2,13 +2,13 @@ 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/' - , webrootPath: '~/letsencrypt.test/tmp/:hostname' - }) -, debug: true + server: 'staging', + acme: require('le-acme-core').ACME.create(), + store: require('le-store-certbot').create({ + configDir: '~/letsencrypt.test/etc/', + webrootPath: '~/letsencrypt.test/tmp/:hostname' + }), + debug: true }); //var testId = Math.round(Date.now() / 1000).toString(); @@ -18,88 +18,117 @@ var testEmail = 'coolaj86+le.' + testId + '@gmail.com'; var testAccount; var tests = [ - function () { - return le.core.accounts.checkAsync({ - email: testEmail - }).then(function (account) { - if (account) { - console.error(account); - throw new Error("Test account should not exist."); - } - }); - } -, function () { - return le.core.accounts.registerAsync({ - email: testEmail - , agreeTos: false - , rsaKeySize: 2048 - }).then(function (/*account*/) { - throw new Error("Should not register if 'agreeTos' is not truthy."); - }, function (err) { - if (err.code !== 'E_ARGS') { - throw err; - } - }); - } -, function () { - return le.core.accounts.registerAsync({ - email: testEmail - , agreeTos: true - , rsaKeySize: 1024 - }).then(function (/*account*/) { - throw new Error("Should not register if 'rsaKeySize' is less than 2048."); - }, function (err) { - if (err.code !== 'E_ARGS') { - throw err; - } - }); - } -, function () { - return le.core.accounts.registerAsync({ - email: fakeEmail - , agreeTos: true - , rsaKeySize: 2048 - }).then(function (/*account*/) { - // TODO test mx record - throw new Error("Registration should NOT succeed with a bad email address."); - }, function (err) { - if (err.code !== 'E_EMAIL') { - throw err; - } - }); - } -, function () { - return le.core.accounts.registerAsync({ - email: testEmail - , agreeTos: true - , rsaKeySize: 2048 - }).then(function (account) { - testAccount = account; + function() { + return le.core.accounts + .checkAsync({ + email: testEmail + }) + .then(function(account) { + if (account) { + console.error(account); + throw new Error('Test account should not exist.'); + } + }); + }, + function() { + return le.core.accounts + .registerAsync({ + email: testEmail, + agreeTos: false, + rsaKeySize: 2048 + }) + .then( + function(/*account*/) { + throw new Error( + "Should not register if 'agreeTos' is not truthy." + ); + }, + function(err) { + if (err.code !== 'E_ARGS') { + throw err; + } + } + ); + }, + function() { + return le.core.accounts + .registerAsync({ + email: testEmail, + agreeTos: true, + rsaKeySize: 1024 + }) + .then( + function(/*account*/) { + throw new Error( + "Should not register if 'rsaKeySize' is less than 2048." + ); + }, + function(err) { + if (err.code !== 'E_ARGS') { + throw err; + } + } + ); + }, + function() { + return le.core.accounts + .registerAsync({ + email: fakeEmail, + agreeTos: true, + rsaKeySize: 2048 + }) + .then( + function(/*account*/) { + // TODO test mx record + throw new Error( + 'Registration should NOT succeed with a bad email address.' + ); + }, + function(err) { + if (err.code !== 'E_EMAIL') { + throw err; + } + } + ); + }, + function() { + return le.core.accounts + .registerAsync({ + email: testEmail, + agreeTos: true, + rsaKeySize: 2048 + }) + .then(function(account) { + testAccount = account; - console.log(testEmail); - console.log(testAccount); + console.log(testEmail); + console.log(testAccount); - if (!account) { - throw new Error("Registration should always return a new account."); - } - if (!account.email) { - throw new Error("Registration should return the email."); - } - if (!account.id) { - throw new Error("Registration should return the account id."); - } - }); - } + if (!account) { + throw new Error( + 'Registration should always return a new account.' + ); + } + if (!account.email) { + throw new Error('Registration should return the email.'); + } + if (!account.id) { + throw new Error( + 'Registration should return the account id.' + ); + } + }); + } ]; function run() { - var test = tests.shift(); - if (!test) { - console.info('All tests passed'); - return; - } + var test = tests.shift(); + if (!test) { + console.info('All tests passed'); + return; + } - test().then(run); + test().then(run); } run(); diff --git a/tests/register-certificate.js b/tests/register-certificate.js index 381f97b..e390940 100644 --- a/tests/register-certificate.js +++ b/tests/register-certificate.js @@ -2,16 +2,16 @@ 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' - , webrootPath: '~/letsencrypt.test/var/:hostname' - }) -, challenge: require('le-challenge-fs').create({ - webrootPath: '~/letsencrypt.test/var/:hostname' - }) -, debug: true + server: 'staging', + acme: require('le-acme-core').ACME.create(), + store: require('le-store-certbot').create({ + configDir: '~/letsencrypt.test/etc', + webrootPath: '~/letsencrypt.test/var/:hostname' + }), + challenge: require('le-challenge-fs').create({ + webrootPath: '~/letsencrypt.test/var/:hostname' + }), + debug: true }); // TODO test generateRsaKey code path separately @@ -21,54 +21,62 @@ var le = LE.create({ var testId = 'test1000'; var testEmail = 'coolaj86+le.' + testId + '@gmail.com'; // TODO integrate with Daplie Domains for junk domains to test with -var testDomains = [ 'pokemap.hellabit.com', 'www.pokemap.hellabit.com' ]; +var testDomains = ['pokemap.hellabit.com', 'www.pokemap.hellabit.com']; var tests = [ - function () { - return le.core.certificates.checkAsync({ - domains: [ 'example.com', 'www.example.com' ] - }).then(function (cert) { - if (cert) { - throw new Error("Bogus domain should not have certificate."); - } - }); - } + function() { + return le.core.certificates + .checkAsync({ + domains: ['example.com', 'www.example.com'] + }) + .then(function(cert) { + if (cert) { + throw new Error( + 'Bogus domain should not have certificate.' + ); + } + }); + }, -, function () { - return le.core.certificates.getAsync({ - email: testEmail - , domains: testDomains - }).then(function (certs) { - if (!certs) { - throw new Error("Should have acquired certificate for domains."); - } - }); - } + function() { + return le.core.certificates + .getAsync({ + email: testEmail, + domains: testDomains + }) + .then(function(certs) { + if (!certs) { + throw new Error( + 'Should have acquired certificate for domains.' + ); + } + }); + } ]; function run() { - //var express = require(express); - var server = require('http').createServer(le.middleware()); - server.listen(80, function () { - console.log('Server running, proceeding to test.'); + //var express = require(express); + var server = require('http').createServer(le.middleware()); + server.listen(80, function() { + console.log('Server running, proceeding to test.'); - function next() { - var test = tests.shift(); - if (!test) { - server.close(); - console.info('All tests passed'); - return; - } + function next() { + var test = tests.shift(); + if (!test) { + server.close(); + console.info('All tests passed'); + return; + } - test().then(next, function (err) { - console.error('ERROR'); - console.error(err.stack); - server.close(); - }); - } + test().then(next, function(err) { + console.error('ERROR'); + console.error(err.stack); + server.close(); + }); + } - next(); - }); + next(); + }); } run(); diff --git a/tests/renew-certificate.js b/tests/renew-certificate.js index 58a4ae8..b64d67e 100644 --- a/tests/renew-certificate.js +++ b/tests/renew-certificate.js @@ -2,16 +2,16 @@ 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' - , webrootPath: '~/letsencrypt.test/var/:hostname' - }) -, challenge: require('le-challenge-fs').create({ - webrootPath: '~/letsencrypt.test/var/:hostname' - }) -, debug: true + server: 'staging', + acme: require('le-acme-core').ACME.create(), + store: require('le-store-certbot').create({ + configDir: '~/letsencrypt.test/etc', + webrootPath: '~/letsencrypt.test/var/:hostname' + }), + challenge: require('le-challenge-fs').create({ + webrootPath: '~/letsencrypt.test/var/:hostname' + }), + debug: true }); // TODO test generateRsaKey code path separately @@ -21,82 +21,117 @@ var le = LE.create({ var testId = 'test1000'; var testEmail = 'coolaj86+le.' + testId + '@gmail.com'; // TODO integrate with Daplie Domains for junk domains to test with -var testDomains = [ 'pokemap.hellabit.com', 'www.pokemap.hellabit.com' ]; +var testDomains = ['pokemap.hellabit.com', 'www.pokemap.hellabit.com']; var testCerts; var tests = [ - function () { - // TODO test that an altname also fetches the proper certificate - return le.core.certificates.checkAsync({ - domains: testDomains - }).then(function (certs) { - if (!certs) { - throw new Error("Either certificates.registerAsync (in previous test)" - + " or certificates.checkAsync (in this test) failed."); - } + function() { + // TODO test that an altname also fetches the proper certificate + return le.core.certificates + .checkAsync({ + domains: testDomains + }) + .then(function(certs) { + if (!certs) { + throw new Error( + 'Either certificates.registerAsync (in previous test)' + + ' or certificates.checkAsync (in this test) failed.' + ); + } - testCerts = certs; - console.log('Issued At', new Date(certs.issuedAt).toISOString()); - console.log('Expires At', new Date(certs.expiresAt).toISOString()); + testCerts = certs; + console.log( + 'Issued At', + new Date(certs.issuedAt).toISOString() + ); + console.log( + 'Expires At', + new Date(certs.expiresAt).toISOString() + ); - if (certs.expiresAt <= Date.now()) { - throw new Error("Certificates are already expired. They cannot be tested for duplicate or forced renewal."); - } - }); - } + if (certs.expiresAt <= Date.now()) { + throw new Error( + 'Certificates are already expired. They cannot be tested for duplicate or forced renewal.' + ); + } + }); + }, -, function () { - return le.core.certificates.renewAsync({ - email: testEmail - , domains: testDomains - }, testCerts).then(function () { - throw new Error("Should not have renewed non-expired certificates."); - }, function (err) { - if ('E_NOT_RENEWABLE' !== err.code) { - throw err; - } - }); - } + function() { + return le.core.certificates + .renewAsync( + { + email: testEmail, + domains: testDomains + }, + testCerts + ) + .then( + function() { + throw new Error( + 'Should not have renewed non-expired certificates.' + ); + }, + function(err) { + if ('E_NOT_RENEWABLE' !== err.code) { + throw err; + } + } + ); + }, -, function () { - return le.core.certificates.renewAsync({ - email: testEmail - , domains: testDomains - , renewWithin: 720 * 24 * 60 * 60 * 1000 - }, testCerts).then(function (certs) { - console.log('Issued At', new Date(certs.issuedAt).toISOString()); - console.log('Expires At', new Date(certs.expiresAt).toISOString()); + function() { + return le.core.certificates + .renewAsync( + { + email: testEmail, + domains: testDomains, + renewWithin: 720 * 24 * 60 * 60 * 1000 + }, + testCerts + ) + .then(function(certs) { + console.log( + 'Issued At', + new Date(certs.issuedAt).toISOString() + ); + console.log( + 'Expires At', + new Date(certs.expiresAt).toISOString() + ); - if (certs.issuedAt === testCerts.issuedAt) { - throw new Error("Should not have returned existing certificates."); - } - }); - } + if (certs.issuedAt === testCerts.issuedAt) { + throw new Error( + 'Should not have returned existing certificates.' + ); + } + }); + } ]; function run() { - //var express = require(express); - var server = require('http').createServer(le.middleware()); - server.listen(80, function () { - console.log('Server running, proceeding to test.'); + //var express = require(express); + var server = require('http').createServer(le.middleware()); + server.listen(80, function() { + console.log('Server running, proceeding to test.'); - function next() { - var test = tests.shift(); - if (!test) { - server.close(); - console.info('All tests passed'); - return; - } + function next() { + var test = tests.shift(); + if (!test) { + server.close(); + console.info('All tests passed'); + return; + } - test().then(next, function (err) { - console.error('ERROR'); - console.error(err.stack); - server.close(); - }); - } + test().then(next, function(err) { + console.error('ERROR'); + console.error(err.stack); + server.close(); + }); + } - next(); - }); + next(); + }); } run();