AJ ONeal
5 years ago
5 changed files with 206 additions and 155 deletions
@ -1,68 +1,137 @@ |
|||
'use strict'; |
|||
/*global Promise*/ |
|||
|
|||
var Challenge = module.exports; |
|||
|
|||
Challenge.create = function (defaults) { |
|||
// if you need special options that apply to all domains, you could set them here.
|
|||
return { |
|||
options: defaults |
|||
, set: Challenge.set |
|||
, get: Challenge.get |
|||
, remove: Challenge.remove |
|||
// If your implementation needs config options, set them. Otherwise, don't bother (duh).
|
|||
Challenge.create = function (config) { |
|||
|
|||
var challenger = {}; |
|||
|
|||
// Note: normally you'd these right in the method body, but for the sake of
|
|||
// "Table of Contents"-style documentation, I've pulled them out.
|
|||
|
|||
// Note: All of these methods can be synchronous, async, Promise, and callback-style
|
|||
// (the calling functions check function.length and then Promisify accordingly)
|
|||
|
|||
// Called when it's tiem to set the challenge
|
|||
challenger.set = function (opts, cb) { |
|||
return Challenge._setDns(opts, cb); |
|||
}; |
|||
|
|||
// Called when it's time to remove the challenge
|
|||
challenger.remove = function (opts) { |
|||
return Challenge._removeDns(opts); |
|||
}; |
|||
|
|||
// Optional (only really useful for http)
|
|||
// Called when the challenge needs to be retrieved
|
|||
challenger.get = function (opts) { |
|||
return Challenge._getDns(opts); |
|||
}; |
|||
|
|||
// Whatever you assign to 'options' will be merged into the incoming 'opts' beforehand
|
|||
// (for convenience, so you don't have to do the if (!x) { x = y; } dance)
|
|||
// (also, some defaults are layered, so it's good to set it any that you have)
|
|||
challenger.options = { debug: config.debug }; |
|||
|
|||
return challenger; |
|||
}; |
|||
|
|||
|
|||
// Show the user the token and key and wait for them to be ready to continue
|
|||
Challenge.set = function (args, cb) { |
|||
Challenge._setDns = function (args, cb) { |
|||
// if you need per-run / per-domain options set them in approveDomains() and they'll be on 'args' here.
|
|||
if (!args.challenge) { |
|||
console.error("please update to greenlock v2.7+"); |
|||
console.error("You must be using Greenlock v2.7+ to use le-challenge-dns v3+"); |
|||
process.exit(); |
|||
} |
|||
var opts = args.challenge; |
|||
|
|||
if (this.leDnsResponse) { |
|||
this.leDnsResponse(opts.token, opts.keyAuthorization, opts.dnsAuthorization, opts.dnsHost, opts.altname) |
|||
.then(function (/*successMessage*/) { |
|||
cb(null); |
|||
}); |
|||
} else { |
|||
console.info(""); |
|||
console.info("We now present (for your copy-and-paste pleasure)..."); |
|||
console.info("DNS-01 ACME (Let's Encrypt) Challenge for '" + opts.altname + "'"); |
|||
console.info(""); |
|||
console.info(opts.dnsHost + "\tTXT " + opts.dnsAuthorization + "\tTTL 60"); |
|||
var ch = args.challenge; |
|||
|
|||
console.info(""); |
|||
console.info("[ACME dns-01 '" + ch.altname + "' CHALLENGE]"); |
|||
console.info("You're about to receive the following DNS query:"); |
|||
console.info(""); |
|||
console.info("\tTXT\t" + ch.dnsHost + "\t" + ch.dnsAuthorization + "\tTTL 60"); |
|||
console.info(""); |
|||
if (ch.debug) { |
|||
console.info("Debug Info:"); |
|||
console.info(""); |
|||
console.info(JSON.stringify({ |
|||
identifier: opts.identifier |
|||
, wildcard: opts.wildcard |
|||
, altname: opts.altname |
|||
, type: opts.type |
|||
, token: opts.token |
|||
, keyAuthorization: opts.keyAuthorization |
|||
, dnsHost: opts.dnsHost |
|||
, dnsAuthorization: opts.dnsAuthorization |
|||
, expires: opts.expires |
|||
}, null, ' ').replace(/^/gm, '\t')); |
|||
console.info(JSON.stringify(dnsChallengeToJson(ch), null, ' ').replace(/^/gm, '\t')); |
|||
console.info(""); |
|||
console.info("Insert quarter, er... I mean hit the any key to continue..."); |
|||
process.stdin.resume(); |
|||
process.stdin.on('data', function () { |
|||
process.stdin.pause(); |
|||
cb(null); |
|||
}); |
|||
} |
|||
console.info("Go set that DNS record, wait a few seconds for it to propagate, and then continue when ready"); |
|||
console.info("[Press the ANY key to continue...]"); |
|||
process.stdin.resume(); |
|||
process.stdin.once('data', function () { |
|||
process.stdin.pause(); |
|||
cb(null); |
|||
}); |
|||
}; |
|||
|
|||
// nothing to do here (that's the dns server's job), that's why it's manual
|
|||
Challenge.get = function (defaults, cb) { |
|||
// defaults.challenge
|
|||
cb(null); |
|||
// might as well tell the user that whatever they were setting up has been checked
|
|||
Challenge._removeDns = function (args) { |
|||
var ch = args.challenge; |
|||
console.info(""); |
|||
console.info("[ACME http-01 '" + ch.altname + "' COMPLETE]: " + ch.status); |
|||
console.info("Challenge complete. You may now remove the DNS-01 challenge record:"); |
|||
console.info(""); |
|||
console.info("\tTXT\t" + args.challenge.altname + "\t" + args.challenge.dnsAuthorization); |
|||
console.info(""); |
|||
|
|||
return null; |
|||
}; |
|||
|
|||
// might as well tell the user that whatever they were setting up has been checked
|
|||
Challenge.remove = function (args, cb) { |
|||
console.info("Success. You may now remove the DNS-01 challenge record:"); |
|||
console.info("\t" + args.challenge.altname + "\tTXT\t" + args.challenge.dnsAuthorization); |
|||
cb(null); |
|||
// This is implemented here for completeness (and perhaps some possible use in testing),
|
|||
// but it's not something you would implement because the Greenlock server isn't the NameServer.
|
|||
Challenge._getDns = function (args) { |
|||
var ch = args.challenge; |
|||
|
|||
if (!Challenge._getCache[ch.altname + ':' + ch.token]) { |
|||
Challenge._getCache[ch.altname + ':' + ch.token] = true; |
|||
console.info(""); |
|||
console.info("[ACME " + ch.type + " '" + ch.altname + "' REQUEST]: " + ch.status); |
|||
console.info("The '" + ch.type + "' challenge request has arrived!"); |
|||
console.info('dig TXT ' + ch.dnsHost); |
|||
console.info("(paste in the \"DNS Authorization\" you received a moment ago to respond)"); |
|||
process.stdout.write("> "); |
|||
} |
|||
|
|||
return new Promise(function (resolve, reject) { |
|||
process.stdin.resume(); |
|||
process.stdin.once('error', reject); |
|||
process.stdin.once('data', function (chunk) { |
|||
process.stdin.pause(); |
|||
|
|||
var result = chunk.toString('utf8'); |
|||
try { |
|||
result = JSON.parse(result); |
|||
} catch(e) { |
|||
args.challenge.dnsAuthorization = result; |
|||
result = args.challenge; |
|||
} |
|||
if (result.dnsAuthorization) { |
|||
resolve(result); |
|||
return; |
|||
} |
|||
|
|||
// The return value will checked. It must not be 'undefined'.
|
|||
resolve(null); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
function dnsChallengeToJson(ch) { |
|||
return { |
|||
type: ch.type |
|||
, altname: ch.altname |
|||
, identifier: ch.identifier |
|||
, wildcard: ch.wildcard |
|||
, expires: ch.expires |
|||
, token: ch.token |
|||
, thumbprint: ch.thumbprint |
|||
, keyAuthorization: ch.keyAuthorization |
|||
, dnsHost: ch.dnsHost |
|||
, dnsAuthorization: ch.dnsAuthorization |
|||
}; |
|||
} |
|||
|
@ -0,0 +1,5 @@ |
|||
{ |
|||
"name": "le-challenge-dns", |
|||
"version": "3.0.3", |
|||
"lockfileVersion": 1 |
|||
} |
@ -1,63 +1,36 @@ |
|||
'use strict'; |
|||
/*global Promise*/ |
|||
|
|||
var PromiseA = require('bluebird'); |
|||
var resolveTxtAsync = PromiseA.promisify(require('dns').resolveTxt); |
|||
var Challenge = require('./'); |
|||
var leChallengeDns = Challenge.create({ }); |
|||
var opts = leChallengeDns.getOptions(); |
|||
var domain = 'test.example.com'; |
|||
var challenge = 'xxx-acme-challenge-xxx'; |
|||
var keyAuthorization = 'xxx-acme-challenge-xxx.xxx-acme-authorization-xxx'; |
|||
var challenge = require('./').create({}); |
|||
|
|||
Challenge.loopback = function (defaults, domain, challenge, done) { |
|||
var challengeDomain = (defaults.test || '') + defaults.acmeChallengeDns + domain; |
|||
console.log("dig TXT +noall +answer @8.8.8.8 '" + challengeDomain + "' # " + challenge); |
|||
resolveTxtAsync(challengeDomain).then(function (x) { done(null, x); }, done); |
|||
}; |
|||
|
|||
Challenge.test = function (args, domain, challenge, keyAuthorization, done) { |
|||
var me = this; |
|||
|
|||
args.test = args.test || '_test.'; |
|||
//defaults.test = args.test;
|
|||
|
|||
me.set(args, domain, challenge, keyAuthorization || challenge, function (err, k) { |
|||
if (err) { done(err); return; } |
|||
|
|||
me.loopback(/*defaults*/args, domain, challenge, function (err, arr) { |
|||
if (err) { done(err); return; } |
|||
|
|||
if (!arr.some(function (a) { |
|||
return a.some(function (keyAuthDigest) { |
|||
return keyAuthDigest === k; |
|||
}); |
|||
})) { |
|||
err = new Error("txt record '" + challenge + "' doesn't match '" + k + "'"); |
|||
} |
|||
var opts = challenge.getOptions && challenge.getOptions() || challenge.options; |
|||
|
|||
me.remove(/*defaults*/args, domain, challenge, function (_err) { |
|||
if (_err) { done(_err); return; } |
|||
|
|||
// TODO needs to use native-dns so that specific nameservers can be used
|
|||
// (otherwise the cache will still have the old answer)
|
|||
done(err || null); |
|||
/* |
|||
me.loopback(defaults, domain, challenge, function (err) { |
|||
if (err) { done(err); return; } |
|||
|
|||
done(); |
|||
}); |
|||
*/ |
|||
}); |
|||
function run() { |
|||
// this will cause the prompt to appear
|
|||
return new Promise(function (resolve, reject) { |
|||
challenge.set(opts, function () { |
|||
// this will cause the final completion message to appear
|
|||
return Promise.resolve(challenge.remove(opts)).then(resolve).catch(reject); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
opts.challenge = { |
|||
type: 'http-01' |
|||
, identifier: { type: 'dns', value: 'example.com' } |
|||
, wildcard: false |
|||
, expires: '2012-01-01T12:00:00.000Z' |
|||
, token: 'abc123' |
|||
, thumbprint: '<<account key thumbprint>>' |
|||
, keyAuthorization: 'abc123.xxxx' |
|||
, dnsHost: '_acme-challenge.example.com' |
|||
, dnsAuthorization: 'yyyy' |
|||
, altname: 'example.com' |
|||
}; |
|||
|
|||
setTimeout(function () { |
|||
leChallengeDns.test(opts, domain, challenge, keyAuthorization, function (err) { |
|||
// if there's an error, there's a problem
|
|||
if (err) { throw err; } |
|||
|
|||
console.log('test passed'); |
|||
}); |
|||
}, 300); |
|||
run(opts).then(function () { |
|||
console.info("PASS"); |
|||
}).catch(function (err) { |
|||
console.error("FAIL"); |
|||
console.error(err); |
|||
process.exit(18); |
|||
}); |
|||
|
Loading…
Reference in new issue