'use strict'; /*global Promise*/ var Challenge = module.exports; // IMPORTANT // // These are all PROMISIFIED by Greenlock in such a way that // it doesn't matter whether you return synchronously, asynchronously, // or even node-style callback thunk. // // Typically you should be using a promise or async function, // but choose whichever makes sense for you. Challenge.create = function (config) { // If your implementation needs config options, set them. Otherwise, don't bother (duh). var challenger = {}; // Note: normally you'd implement these right here, but for the sake of // documentation I've abstracted them out "Table of Contents"-style. // call out to set the challenge, wherever challenger.set = function (opts, cb) { var ch = opts.challenge; if ('http-01' === ch.type) { return Challenge._setHttp(opts, cb); } else if ('dns-01' === ch.type) { return Challenge._setDns(opts, cb); } else { return Challenge._setAny(opts, cb); } }; // call out to remove the challenge, wherever challenger.remove = function (opts) { var ch = opts.challenge; if ('http-01' === ch.type) { return Challenge._removeHttp(opts); } else if ('dns-01' === ch.type) { return Challenge._removeDns(opts); } else { return Challenge._removeAny(opts); } }; // only really useful for http, // but probably not so much in this context... // (though you can test it and it'll work) challenger.get = function (opts) { var ch = opts.challenge; if ('http-01' === ch.type) { return Challenge._getHttp(opts); } else if ('dns-01' === ch.type) { return Challenge._getDns(opts); } else { return Challenge._getAny(opts); } }; // Whatever you set to 'options' will be merged into 'opts' just before each call // (for convenience, so you don't have to merge it yourself). challenger.options = { debug: config.debug }; return challenger; }; // Show the user the token and key and wait for them to be ready to continue Challenge._setHttp = function (args, cb) { // Using a node-style callback "thunk" in this example, because that makes var ch = args.challenge; console.info("[ACME http-01 '" + ch.altname + "' CHALLENGE]"); console.info("Your mission (since you chose to accept it):"); console.info("First, you must create a file with the following name and contents."); console.info("Then, by any means necessary, you cause that file to appear at the specified URL."); console.info(""); console.info("\tFilename: " + ch.token); console.info("\tContents: " + ch.keyAuthorization); // TODO let acme-v2 handle generating this url console.info('\tURL: http://' + ch.altname + '/.well-known/acme-challenge/' + ch.token); console.info(""); console.info("And, if you need additional information for debugging:"); console.info(""); console.info(JSON.stringify(httpChallengeToJson(ch), null, 2).replace(/^/gm, '\t')); console.info(""); console.info("This message won't self-destruct, but you may press hit the any as soon as you're ready to continue..."); console.info(""); console.info("[Press the ANY key to continue...]"); process.stdin.resume(); process.stdin.once('data', function () { process.stdin.pause(); cb(null); }); }; Challenge._setDns = function (args, cb) { // Using a node-style callback "thunk" in this example, because that makes var ch = args.challenge; console.info("[ACME dns-01 '" + ch.altname + "' CHALLENGE]"); console.info("Your mission (since you chose to accept it):"); console.info("First, you must create a DNS record with the following parameters:"); console.info(""); console.info(ch.dnsHost + "\tTXT\t" + ch.dnsKeyAuthorization + "\tTTL 60"); console.info(""); console.info("Next, wait, no... there is no next. That's it - but here's some stuff anyway:"); console.info(""); console.info(JSON.stringify(dnsChallengeToJson(ch), null, 2).replace(/^/gm, '\t')); console.info(""); console.info("[Press the ANY key to continue...]"); process.stdin.resume(); process.stdin.once('data', function () { process.stdin.pause(); cb(null); }); }; Challenge._setAny = function (args, cb) { var ch = args.challenge; console.info("[ACME " + ch.type + " '" + ch.altname + "' CHALLENGE]"); console.info("There's no quippy pre-programmed response for this type of challenge."); console.info("I have no idea what you intend to do, but I'll tell you everything I know:"); console.info(""); console.info(JSON.stringify(ch, null, 2).replace(/^/gm, '\t')); console.info(""); console.info("[Press the ANY key to continue...]"); process.stdin.resume(); process.stdin.on('data', function () { process.stdin.pause(); cb(null); }); }; // might as well tell the user that whatever they were setting up has been checked Challenge._removeHttp = function (args) { var ch = args.challenge; console.info(""); console.info("Challenge for '" + ch.altname + "' complete. You can delete this file now:"); console.info('\thttp://' + ch.altname + '/.well-known/acme-challenge/' + ch.token); console.info(""); // this can return null or a Promise null // (or callback null, just like the set() above) return null; }; Challenge._removeDns = function (args) { var ch = args.challenge; console.info(""); console.info("Challenge for '" + ch.altname + "' complete. You can remove this record now:"); console.info("\t" + ch.dnsHost + "\tTXT\t" + ch.dnsKeyAuthorization + "\tTTL 60"); console.info(""); // this can return null or a Promise null // (or callback null, just like the set() above) return null; }; Challenge._removeAny = function (args) { var ch = args.challenge; console.info(""); console.info("Challenge for '" + ch.altname + "' complete. You can now undo what you did."); console.info(""); // this can return null or a Promise null // (or callback null, just like set() above) return null; }; // nothing to do here, that's why it's manual Challenge._get = function (args, cb) { console.info(""); console.info("Woah! Hey, guess what!? That's right you guessed it:"); console.info("It's time to painstakingly type out the ACME challenge response with your bear hands. Yes. Your bear hands."); process.stdout.write("> "); // Using a promise here just to show that Promises are support // (in fact, they're the default) return new Promise(function (resolve, reject) { process.stdin.resume(); process.stdin.on('error', reject); process.stdin.on('data', function (chunk) { process.stdin.pause(); var result = chunk.toString(); try { result = JSON.parse(result); } catch(e) { args.keyAuthorization = result; } cb(null); }); }); }; function httpChallengeToJson(ch) { return { type: ch.type , altname: ch.altname , identifier: ch.identifier , wildcard: false , expires: ch.expires , token: ch.token , thumbprint: ch.thumbprint , keyAuthorization: ch.keyAuthorization }; } function dnsChallengeToJson(ch) { return { type: ch.type , altname: '*.example.com' , identifier: ch.identifier , wildcard: ch.wildcard , expires: ch.expires , token: ch.token , thumbprint: ch.thumbprint , keyAuthorization: ch.keyAuthorization , dnsHost: ch.dnsHost , dnsAuthorization: ch.dnsAuthorization }; }