diff --git a/README.md b/README.md index 629ac55..06dcc78 100644 --- a/README.md +++ b/README.md @@ -7,39 +7,37 @@ | [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi) | -le-challenge-ddns +le-challenge-dns ================ -A dns-based strategy for node-letsencrypt for setting, retrieving, +**For production** use [`le-challenge-ddns`](https://github.com/Daplie/le-challenge-ddns) (or a similar ddns tool) + +A manual (interactive CLI) dns-based strategy for node-letsencrypt for setting, retrieving, and clearing ACME DNS-01 challenges issued by the ACME server -It creates a subdomain record for `_acme-challenge` with `keyAuthDigest` +Prints out a subdomain record for `_acme-challenge` with `keyAuthDigest` to be tested by the ACME server. +You can then update your DNS manually by whichever method you use and then +press [enter] to continue the process. + ``` _acme-challenge.example.com TXT xxxxxxxxxxxxxxxx TTL 60 ``` -* Safe to use with node cluster -* Safe to use with ephemeral services (Heroku, Joyent, etc) - Install ------- ```bash -npm install --save le-challenge-ddns@2.x +npm install --save le-challenge-dns@2.x ``` Usage ----- ```bash -var leChallengeDdns = require('le-challenge-ddns').create({ - email: 'john.doe@example.com' -, refreshToken: '...' -, ttl: 60 - -, debug: false +var leChallengeDns = require('le-challenge-dns').create({ + debug: false }); var LE = require('letsencrypt'); @@ -48,7 +46,7 @@ LE.create({ server: LE.stagingServerUrl // Change to LE.productionServerUrl in production , challengeType: 'dns-01' , challenges: { - 'dns-01': leChallengeDdns + 'dns-01': leChallengeDns } , approveDomains: [ 'example.com' ] }); diff --git a/index.js b/index.js index f125a1e..d1ba9bb 100644 --- a/index.js +++ b/index.js @@ -1,200 +1,76 @@ 'use strict'; -// See https://tools.ietf.org/html/draft-ietf-acme-acme-01 -// also https://gitlab.com/pushrocks/cert/blob/master/ts/cert.hook.ts - var PromiseA = require('bluebird'); var dns = PromiseA.promisifyAll(require('dns')); -var DDNS = require('ddns-cli'); - -//var count = 0; -var defaults = { - oauth3: 'oauth3.org' -, debug: false -, acmeChallengeDns: '_acme-challenge.' // _acme-challenge.example.com TXT xxxxxxxxxxxxxxxx -, memstoreConfig: { - name: 'le-ddns' - } -}; - var Challenge = module.exports; -Challenge.create = function (options) { - // count += 1; - var store = require('cluster-store'); - var results = {}; - - Object.keys(Challenge).forEach(function (key) { - results[key] = Challenge[key]; - }); - results.create = undefined; - - Object.keys(defaults).forEach(function (key) { - if (!(key in options)) { - options[key] = defaults[key]; +Challenge.create = function (defaults) { + return { + getOptions: function () { + return defaults || {}; } - }); - results._options = options; - - results.getOptions = function () { - return results._options; + , set: Challenge.set + , get: Challenge.get + , remove: Challenge.remove + , loopback: Challenge.loopback + , test: Challenge.test }; - - // TODO fix race condition at startup - results._memstore = options.memstore; - - if (!results._memstore) { - store.create(options.memstoreConfig).then(function (store) { - // same api as new sqlite3.Database(options.filename) - - results._memstore = store; - - // app.use(expressSession({ secret: 'keyboard cat', store: store })); - }); - } - - return results; }; -// -// NOTE: the "args" here in `set()` are NOT accessible to `get()` and `remove()` -// They are provided so that you can store them in an implementation-specific way -// if you need access to them. -// -Challenge.set = function (args, domain, challenge, keyAuthorization, done) { - var me = this; - // TODO use base64url module +// Show the user the token and key and wait for them to be ready to continue +Challenge.set = function (args, domain, challenge, keyAuthorization, cb) { var keyAuthDigest = require('crypto').createHash('sha256').update(keyAuthorization||'').digest('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, '') ; + var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain; - if (!challenge || !keyAuthorization) { - console.warn("SANITY FAIL: missing challenge or keyAuthorization", domain, challenge, keyAuthorization); - } - - return me._memstore.set(domain, { - email: args.email - , refreshToken: args.refreshToken + console.info(""); + console.info("Challenge for '" + domain + "'"); + console.info(""); + console.info("We now present (for you copy-and-paste pleasure) your ACME Challenge"); + console.info("public Challenge and secret KeyAuthorization and Digest, in that order, respectively:"); + console.info(challenge); + console.info(keyAuthorization); + console.info(keyAuthDigest); + console.info(""); + console.info(challengeDomain + "\tTXT " + keyAuthDigest + "\tTTL 60"); + console.info(""); + console.info(JSON.stringify({ + domain: domain + , challenge: challenge + , keyAuthorization: keyAuthorization , keyAuthDigest: keyAuthDigest - }, function (err) { - if (err) { done(err); return; } - - var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain; - var update = { - email: args.email - , refreshToken: args.refreshToken - , silent: true - - , name: challengeDomain - , type: "TXT" - , value: keyAuthDigest || challenge - , ttl: args.ttl || 0 - }; - - return DDNS.update(update, { - //debug: true - }).then(function () { - if (args.debug) { - console.log("Test DNS Record:"); - console.log("dig TXT +noall +answer @ns1.redirect-www.org '" + challengeDomain + "' # " + challenge); - } - done(null, keyAuthDigest); - }, function (err) { - console.error(err); - done(err); - return PromiseA.reject(err); - }); + }, null, ' ').replace(/^/gm, '\t')); + console.info(""); + console.info("hit enter to continue..."); + process.stdin.resume(); + process.stdin.on('data', function () { + process.stdin.pause(); + cb(null); }); }; - -// -// NOTE: the "defaults" here are still merged and templated, just like "args" would be, -// but if you specifically need "args" you must retrieve them from some storage mechanism -// based on domain and key -// -Challenge.get = function (defaults, domain, challenge, done) { - done = null; // nix linter error for unused vars - throw new Error("Challenge.get() does not need an implementation for dns-01. (did you mean Challenge.loopback?)"); +// nothing to do here, that's why it's manual +Challenge.get = function (defaults, domain, challenge, cb) { + cb(null); }; -Challenge.remove = function (defaults, domain, challenge, done) { - var me = this; - - return me._memstore.get(domain, function (err, data) { - if (err) { done(err); return; } - if (!data) { - console.warn("[warning] could not remove '" + domain + "': already removed"); - done(null); - return; - } - - var challengeDomain = (defaults.test || '') + defaults.acmeChallengeDns + domain; - - return DDNS.update({ - email: data.email - , refreshToken: data.refreshToken - , silent: true - - , name: challengeDomain - , type: "TXT" - , value: data.keyAuthDigest || challenge - , ttl: defaults.ttl || 0 - - , remove: true - }, { - //debug: true - }).then(function () { - - done(null); - }, done).then(function () { - me._memstore.destroy(domain); - }); - }); +// might as well tell the user that whatever they were setting up has been checked +Challenge.remove = function (args, domain, challenge, cb) { + console.info("Challenge for '" + domain + "' complete. You may remove it."); + console.info(""); + //console.info("hit enter to continue..."); + //process.stdin.resume(); + //process.stdin.on('data', function () { + // process.stdin.pause(); + cb(null); + //}); }; -// same as get, but external 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); dns.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, 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 + "'"); - } - - me.remove(defaults, 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(); - }); - */ - }); - }); - }); -}; diff --git a/package.json b/package.json index ec2a247..6d9428f 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "le-challenge-ddns", - "version": "2.0.3", - "description": "A dns-based strategy for node-letsencrypt for setting, retrieving, and clearing ACME DNS-01 challenges issued by the ACME server", + "name": "le-challenge-dns", + "version": "2.1.0", + "description": "A manual (interactive CLI) dns-based strategy for node-letsencrypt for setting, retrieving, and clearing ACME DNS-01 challenges issued by the ACME server", "main": "index.js", "scripts": { "test": "node test.js" }, "repository": { "type": "git", - "url": "git+https://github.com/Daplie/le-challenge-ddns.git" + "url": "git+https://github.com/Daplie/le-challenge-dns.git" }, "keywords": [ "le", @@ -16,25 +16,21 @@ "le-challenge", "le-challenge-", "le-challenge-dns", - "le-challenge-ddns", + "manual", + "interactive", + "cli", "acme", "challenge", "dns", - "ddns", "cluster", "ephemeral" ], "author": "AJ ONeal (https://coolaj86.com/)", "license": "(MIT OR Apache-2.0)", "bugs": { - "url": "https://github.com/Daplie/le-challenge-ddns/issues" + "url": "https://github.com/Daplie/le-challenge-dns/issues" }, - "homepage": "https://github.com/Daplie/le-challenge-ddns#readme", + "homepage": "https://github.com/Daplie/le-challenge-dns#readme", "dependencies": { - "cluster-store": "^2.0.4", - "daplie-dns": "git+https://github.com/Daplie/daplie-cli-dns.git#master", - "daplie-domains": "git+https://github.com/Daplie/daplie-cli-domains.git#master", - "ddns-cli": "git+https://github.com/Daplie/node-ddns-client.git#master", - "oauth3-cli": "git+https://github.com/OAuth3/oauth3-cli.git#master" } }