From 695ecf1f5e74de7207718580d1dd51454bdaf506 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 10 Aug 2016 01:32:23 -0600 Subject: [PATCH 01/21] Initial commit --- .gitignore | 37 +++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++++++++++++++++ README.md | 2 ++ 3 files changed, 60 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5148e52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a7782bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Daplie, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..280a84e --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# le-challenge-dns +A dns-based strategy for node-letsencrypt for setting, and clearing ACME DNS-01 challenges issued by the ACME server. From 11212d3a5600946d65706f6acf2d91519314bbbc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 12 Aug 2016 00:17:01 -0600 Subject: [PATCH 02/21] Update README.md --- README.md | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 280a84e..e4e61d7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,43 @@ -# le-challenge-dns +le-challenge-dns +================ + A dns-based strategy for node-letsencrypt for setting, and clearing ACME DNS-01 challenges issued by the ACME server. + +DRAFT +----- + +This details how any dns-based challenge will work with node-letsencrypt, but is not yet implemented specifically (though it is in the pipeline at present, obviously). + +Usage +----- + +```bash +var leChallenge = require('le-challenge-dns').create({ + ttl: 600 +, debug: false +}); + +var LE = require('letsencrypt'); + +LE.create({ + server: LE.stagingServerUrl // Change to LE.productionServerUrl in production +, challenge: leChallenge +}); +``` + +NOTE: If you request a certificate with 6 domains listed, +it will require 6 individual challenges. + +Exposed Methods +--------------- + +For ACME Challenge: + +* `set(opts, domain, key, val, done)` +* `get(defaults, domain, key, done)` +* `remove(defaults, domain, key, done)` + +For node-letsencrypt internals: + +* `getOptions()` returns the internal defaults merged with the user-supplied options +* `loopback(defaults, domain, key, value, done)` should test, by external means, that the ACME server's challenge server will succeed From b41f09df11237f8028737cc4112e8b2d404ae778 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 7 Sep 2016 14:58:25 -0600 Subject: [PATCH 03/21] getting there... --- README.md | 55 ++++++++++++++---- index.js | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 36 ++++++++++++ 3 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 index.js create mode 100644 package.json diff --git a/README.md b/README.md index e4e61d7..f4a15e1 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,44 @@ +[![Join the chat at https://gitter.im/Daplie/letsencrypt-express](https://badges.gitter.im/Daplie/letsencrypt-express.svg)](https://gitter.im/Daplie/letsencrypt-express?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +| [letsencrypt](https://github.com/Daplie/node-letsencrypt) (library) +| [letsencrypt-cli](https://github.com/Daplie/letsencrypt-cli) +| [letsencrypt-express](https://github.com/Daplie/letsencrypt-express) +| [letsencrypt-koa](https://github.com/Daplie/letsencrypt-koa) +| [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi) +| + le-challenge-dns ================ -A dns-based strategy for node-letsencrypt for setting, and clearing ACME DNS-01 challenges issued by the ACME server. +A dns-based strategy for node-letsencrypt for setting, retrieving, +and clearing ACME DNS-01 challenges issued by the ACME server -DRAFT ------ +It creates a subdomain record for `_acme-challenge` wich `challenge` +to be tested by the ACME server. -This details how any dns-based challenge will work with node-letsencrypt, but is not yet implemented specifically (though it is in the pipeline at present, obviously). +``` +_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-dns@2.x +``` Usage ----- ```bash -var leChallenge = require('le-challenge-dns').create({ - ttl: 600 +var leChallengeDns = require('le-challenge-dns').create({ + email: 'john.doe@example.com' +, refreshToken: '...' +, ttl: 60 + , debug: false }); @@ -21,7 +46,11 @@ var LE = require('letsencrypt'); LE.create({ server: LE.stagingServerUrl // Change to LE.productionServerUrl in production -, challenge: leChallenge +, challengeType: 'dns-01' +, challenges: { + 'dns-01': leChallengeDns + } +, approvedDomains: [ 'example.com' ] }); ``` @@ -33,11 +62,15 @@ Exposed Methods For ACME Challenge: -* `set(opts, domain, key, val, done)` -* `get(defaults, domain, key, done)` -* `remove(defaults, domain, key, done)` +* `set(opts, domain, challange, keyAuthorization, done)` +* `get(defaults, domain, challenge, done)` +* `remove(defaults, domain, challenge, done)` + +Note: `get()` is a no-op for `dns-01` and although `dns-01` does not use `keyAuthorization`, +it must be passed in as `null` to keep the correct method signature. For node-letsencrypt internals: * `getOptions()` returns the internal defaults merged with the user-supplied options -* `loopback(defaults, domain, key, value, done)` should test, by external means, that the ACME server's challenge server will succeed +* `loopback(defaults, domain, challange, keyAuthorization, done)` should test, by external means, that the ACME server's challenge server will succeed +* `test(opts, domain, challange, keyAuthorization, done)` runs set, loopback, remove, loopback diff --git a/index.js b/index.js new file mode 100644 index 0000000..a38e9ea --- /dev/null +++ b/index.js @@ -0,0 +1,159 @@ +'use strict'; + +// See 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 fs = require('fs'); +var path = require('path'); + +var cluster = require('cluster'); +var numCores = require('os').cpus().length; +var defaults = { + oauth3: 'oauth3.org' +, debug: false +, acmeChallengeDns: '_acme-challenge.' // _acme-challenge.example.com TXT xxxxxxxxxxxxxxxx +, memstoreConfig: { + sock: '/tmp/memstore.sock' + + // If left 'null' or 'undefined' this defaults to a similar memstore + // with no special logic for 'cookie' or 'expires' + , store: null + + // a good default to use for instances where you might want + // to cluster or to run standalone, but with the same API + , serve: cluster.isMaster + , connect: cluster.isWorker + , standalone: (1 === numCores) // overrides serve and connect + } +}; + +var Challenge = module.exports; + +Challenge.create = function (options) { + var store = require('memstore-cluster'); + 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]; + } + }); + results._options = options; + + results.getOptions = function () { + return results._options; + }; + + // 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) { + // Note: keyAuthorization is not used for dns-01 + + this._memstore.set(domain, { + email: args.email + , refreshToken: args.refreshToken + }, function () { + + return DDNS.run({ + email: args.email + , refreshToken: args.refreshToken + + , name: args.test + args.acmeChallengeDns + '.' + domain + , type: "TXT" + , value: challenge + , ttl: 60 + }).then(function () { done(null); }, done); + }); +}; + + +// +// 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) { + throw new Error("Challenge.get() does not need an implementation for dns-01. (did you mean Challenge.loopback?)"); +}; + +Challenge.remove = function (defaults, domain, challenge, done) { + this._memstore.get(domain, function (data) { + return DDNS.run({ + email: data.email + , refreshToken: data.refreshToken + + , name: defaults.test + defaults.acmeChallengeDns + '.' + domain + , type: "TXT" + , value: challenge + , ttl: 60 + + , remove: true + }).then(function () { + + done(null); + }, done).then(function () { + this._memstore.remove(domain); + }); + }); +}; + +// same as get, but external +Challenge.loopback = function (defaults, domain, challenge, done) { + var subdomain = defaults.test + defaults.acmeChallengeDns + '.' + domain; + dns.resolveAsync(subdomain).then(function () { done(null); }, done); +}; + +Challenge.test = function (args, domain, challenge, keyAuthorization, done) { + // Note: keyAuthorization is not used for dns-01 + + args.test = '_test.'; + + Challenge.set(args, domain, challenge, keyAuthorization, function (err) { + if (err) { done(err); return; } + + Challenge.loopback(defaults, domain, challenge, function (err) { + if (err) { done(err); return; } + + Challenge.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(); + /* + Challenge.loopback(defaults, domain, challenge, function (err) { + if (err) { done(err); return; } + + done(); + }); + */ + }); + }); + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..35cc9f0 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "le-challenge-dns", + "version": "2.0.0", + "description": "A 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-dns.git" + }, + "keywords": [ + "le", + "letsencrypt", + "le-challenge", + "le-challenge-", + "le-challenge-dns", + "acme", + "challenge", + "dns", + "cluster", + "ephemeral" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "(MIT OR Apache-2.0)", + "bugs": { + "url": "https://github.com/Daplie/le-challenge-dns/issues" + }, + "homepage": "https://github.com/Daplie/le-challenge-dns#readme", + "dependencies": { + "daplie-dns": "git+https://github.com/Daplie/daplie-cli-dns.git#master", + "daplie-domains": "git+https://github.com/Daplie/daplie-cli-domains.git#master", + "oauth3-cli": "git+https://github.com/OAuth3/oauth3-cli.git#master" + } +} From d1814341a5fe71e6a9463477bccd1fa73521eac9 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 7 Sep 2016 17:10:04 -0600 Subject: [PATCH 04/21] make cluster safe --- index.js | 57 ++++++++++++++++++++++++++++++++++++---------------- package.json | 1 + 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index a38e9ea..b1e19dc 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,8 @@ var PromiseA = require('bluebird'); var dns = PromiseA.promisifyAll(require('dns')); -var DDNS = require('ddns-cli'); +var DDNS = require('/Users/aj/Dropbox/Code/ddns-cli'); +//var DDNS = require('ddns-cli'); var fs = require('fs'); var path = require('path'); @@ -73,22 +74,34 @@ Challenge.create = function (options) { // if you need access to them. // Challenge.set = function (args, domain, challenge, keyAuthorization, done) { + var me = this; // Note: keyAuthorization is not used for dns-01 - this._memstore.set(domain, { + me._memstore.set(domain, { email: args.email , refreshToken: args.refreshToken - }, function () { + }, function (err) { + if (err) { done(err); return; } - return DDNS.run({ + var challengeDomain = args.test + args.acmeChallengeDns + domain; + + return DDNS.update({ email: args.email , refreshToken: args.refreshToken - , name: args.test + args.acmeChallengeDns + '.' + domain + , name: challengeDomain , type: "TXT" , value: challenge , ttl: 60 - }).then(function () { done(null); }, done); + }, { + //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); + }, done); }); }; @@ -103,51 +116,61 @@ Challenge.get = function (defaults, domain, challenge, done) { }; Challenge.remove = function (defaults, domain, challenge, done) { - this._memstore.get(domain, function (data) { - return DDNS.run({ + var me = this; + + me._memstore.get(domain, function (err, data) { + if (err) { done(err); return; } + + var challengeDomain = defaults.test + defaults.acmeChallengeDns + domain; + + return DDNS.update({ email: data.email , refreshToken: data.refreshToken - , name: defaults.test + defaults.acmeChallengeDns + '.' + domain + , name: challengeDomain , type: "TXT" , value: challenge , ttl: 60 , remove: true + }, { + //debug: true }).then(function () { done(null); }, done).then(function () { - this._memstore.remove(domain); + me._memstore.destroy(domain); }); }); }; // same as get, but external Challenge.loopback = function (defaults, domain, challenge, done) { - var subdomain = defaults.test + defaults.acmeChallengeDns + '.' + domain; - dns.resolveAsync(subdomain).then(function () { done(null); }, done); + var challengeDomain = defaults.test + defaults.acmeChallengeDns + domain; + dns.resolveTxtAsync(challengeDomain).then(function () { done(null); }, done); }; Challenge.test = function (args, domain, challenge, keyAuthorization, done) { + var me = this; // Note: keyAuthorization is not used for dns-01 - args.test = '_test.'; + args.test = args.test || '_test.'; + defaults.test = args.test; - Challenge.set(args, domain, challenge, keyAuthorization, function (err) { + me.set(args, domain, challenge, null, function (err) { if (err) { done(err); return; } - Challenge.loopback(defaults, domain, challenge, function (err) { + me.loopback(defaults, domain, challenge, function (err) { if (err) { done(err); return; } - Challenge.remove(defaults, domain, challenge, function (err) { + 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(); /* - Challenge.loopback(defaults, domain, challenge, function (err) { + me.loopback(defaults, domain, challenge, function (err) { if (err) { done(err); return; } done(); diff --git a/package.json b/package.json index 35cc9f0..6095059 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dependencies": { "daplie-dns": "git+https://github.com/Daplie/daplie-cli-dns.git#master", "daplie-domains": "git+https://github.com/Daplie/daplie-cli-domains.git#master", + "memstore-cluster": "^1.0.0", "oauth3-cli": "git+https://github.com/OAuth3/oauth3-cli.git#master" } } From f89e50baa3378e81084e4661ea9aa0727bc703e1 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 8 Sep 2016 18:56:58 -0600 Subject: [PATCH 05/21] memstore-cluster -> cluster-store --- index.js | 16 ++++------------ package.json | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index b1e19dc..85067c2 100644 --- a/index.js +++ b/index.js @@ -11,29 +11,21 @@ var path = require('path'); var cluster = require('cluster'); var numCores = require('os').cpus().length; +//var count = 0; var defaults = { oauth3: 'oauth3.org' , debug: false , acmeChallengeDns: '_acme-challenge.' // _acme-challenge.example.com TXT xxxxxxxxxxxxxxxx , memstoreConfig: { - sock: '/tmp/memstore.sock' - - // If left 'null' or 'undefined' this defaults to a similar memstore - // with no special logic for 'cookie' or 'expires' - , store: null - - // a good default to use for instances where you might want - // to cluster or to run standalone, but with the same API - , serve: cluster.isMaster - , connect: cluster.isWorker - , standalone: (1 === numCores) // overrides serve and connect + name: 'le-dns' } }; var Challenge = module.exports; Challenge.create = function (options) { - var store = require('memstore-cluster'); + // count += 1; + var store = require('cluster-store'); var results = {}; Object.keys(Challenge).forEach(function (key) { diff --git a/package.json b/package.json index 6095059..8a6445b 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ }, "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", - "memstore-cluster": "^1.0.0", "oauth3-cli": "git+https://github.com/OAuth3/oauth3-cli.git#master" } } From 616538ed231c269f735a9ff80f8f1b9281e31486 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 8 Sep 2016 18:57:51 -0600 Subject: [PATCH 06/21] fix require --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 85067c2..2896700 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,8 @@ var PromiseA = require('bluebird'); var dns = PromiseA.promisifyAll(require('dns')); -var DDNS = require('/Users/aj/Dropbox/Code/ddns-cli'); -//var DDNS = require('ddns-cli'); +//var DDNS = require('/Users/aj/Code/ddns-cli'); +var DDNS = require('ddns-cli'); var fs = require('fs'); var path = require('path'); From 7ae8e66815bd5615033a43a23956d8792d0fe8eb Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 8 Sep 2016 19:01:03 -0600 Subject: [PATCH 07/21] seems to pass tests --- index.js | 6 ++++-- test.js | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 test.js diff --git a/index.js b/index.js index 2896700..ce633e6 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,8 @@ var PromiseA = require('bluebird'); var dns = PromiseA.promisifyAll(require('dns')); -//var DDNS = require('/Users/aj/Code/ddns-cli'); -var DDNS = require('ddns-cli'); +var DDNS = require('/Users/aj/Code/ddns-cli'); +//var DDNS = require('ddns-cli'); var fs = require('fs'); var path = require('path'); @@ -80,6 +80,7 @@ Challenge.set = function (args, domain, challenge, keyAuthorization, done) { return DDNS.update({ email: args.email , refreshToken: args.refreshToken + , silent: true , name: challengeDomain , type: "TXT" @@ -118,6 +119,7 @@ Challenge.remove = function (defaults, domain, challenge, done) { return DDNS.update({ email: data.email , refreshToken: data.refreshToken + , silent: true , name: challengeDomain , type: "TXT" diff --git a/test.js b/test.js new file mode 100644 index 0000000..deb3802 --- /dev/null +++ b/test.js @@ -0,0 +1,23 @@ +'use strict'; + +var leChallengeDns = require('./').create({ + + test: '_test_01' +, email: 'test@daplie.com' +, refreshToken: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjY2VmMjNlMGNjYWE1MDRlNDY0ZGEwZWQ4YTI3NmRjNSIsImlhdCI6MTQ3MzI3NTQwOSwiaXNzIjoib2F1dGgzLm9yZyIsImF1ZCI6Im9hdXRoMy5vcmciLCJhenAiOiJvYXV0aDMub3JnIiwic3ViIjoiIiwia2lkIjoib2F1dGgzLm9yZyIsInNjcCI6IiIsImFzIjoibG9naW4iLCJncnQiOiJwYXNzd29yZCIsInNydiI6ZmFsc2UsImsiOiJvYXV0aDMub3JnIiwiYXBwIjoib2F1dGgzLm9yZyIsImF4cyI6W10sInVzciI6IjFlMzAxOTBjZGJiMWM4Yjg4MmJiNTg0OTQ1OGNlZWEzYTk1NTI4ZjIiLCJhY3MiOltdLCJpZHgiOiJxTFNOVHYwTG11YkFnSTc4eEo3d2FlOHVNc1FORFhWVDFsVWRGdHdVbHNpN1hiRnY3OTFVSFlhNE81RkNaeGtDIiwicmVmcmVzaCI6dHJ1ZX0.q2AgyzclADm8LBIbkazbr9Ji_6lj0dS-OhOwHBKimbc6gNlJUpSAlUEKMhEPswYkIIw9oIzOdf2-13FRpk6ZSa7NxRcZ37B6TBMpVzmHojnyXa025uht3CX7UdBtXMsxOSNSEv-m2CLLfq89j2Zr0kwdiUvpb9oo2IwxWPJMgmc' + +//, debug: true +}); + +var opts = leChallengeDns.getOptions(); +var domain = 'test.daplie.me'; +var challenge = 'xxx-acme-challenge-xxx'; + +setTimeout(function () { + leChallengeDns.test(opts, domain, challenge, null, function (err) { + // if there's an error, there's a problem + if (err) { throw err; } + + console.log('test passed'); + }); +}, 300); From 8f3e758714d6c53a5c662890dbca56cfef67e308 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 8 Sep 2016 19:10:50 -0600 Subject: [PATCH 08/21] add missing deps --- index.js | 3 +-- package.json | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index ce633e6..064bb33 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,7 @@ var PromiseA = require('bluebird'); var dns = PromiseA.promisifyAll(require('dns')); -var DDNS = require('/Users/aj/Code/ddns-cli'); -//var DDNS = require('ddns-cli'); +var DDNS = require('ddns-cli'); var fs = require('fs'); var path = require('path'); diff --git a/package.json b/package.json index 8a6445b..16cf756 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "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" } } From 715c7594234c0213443d7355bd04d6bc4bd375d1 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 8 Sep 2016 19:11:01 -0600 Subject: [PATCH 09/21] v2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 16cf756..2ab1637 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "le-challenge-dns", - "version": "2.0.0", + "version": "2.0.1", "description": "A dns-based strategy for node-letsencrypt for setting, retrieving, and clearing ACME DNS-01 challenges issued by the ACME server", "main": "index.js", "scripts": { From b49f4a1b657a7e1532aa3d1a21dc0bbaecf9f034 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 15 Sep 2016 00:51:29 -0600 Subject: [PATCH 10/21] use sha256sum of keyAuthorization as per spec --- index.js | 54 +++++++++++++++++++++++++++++++++--------------------- test.js | 1 + 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 064bb33..660ca52 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,12 @@ 'use strict'; -// See https://gitlab.com/pushrocks/cert/blob/master/ts/cert.hook.ts +// 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 fs = require('fs'); -var path = require('path'); -var cluster = require('cluster'); -var numCores = require('os').cpus().length; //var count = 0; var defaults = { oauth3: 'oauth3.org' @@ -66,26 +63,37 @@ Challenge.create = function (options) { // Challenge.set = function (args, domain, challenge, keyAuthorization, done) { var me = this; - // Note: keyAuthorization is not used for dns-01 + // TODO use base64url module + var keyAuthDigest = require('crypto').createHash('sha256').update(keyAuthorization||'').digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, '') + ; - me._memstore.set(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 + , keyAuthDigest: keyAuthDigest }, function (err) { if (err) { done(err); return; } - var challengeDomain = args.test + args.acmeChallengeDns + domain; - - return DDNS.update({ + var challengeDomain = (args.test || '') + args.acmeChallengeDns + domain; + var update = { email: args.email , refreshToken: args.refreshToken , silent: true , name: challengeDomain , type: "TXT" - , value: challenge - , ttl: 60 - }, { + , value: keyAuthDigest || challenge + , ttl: args.ttl || 0 + }; + + return DDNS.update(update, { //debug: true }).then(function () { if (args.debug) { @@ -93,7 +101,11 @@ Challenge.set = function (args, domain, challenge, keyAuthorization, done) { console.log("dig TXT +noall +answer @ns1.redirect-www.org '" + challengeDomain + "' # " + challenge); } done(null); - }, done); + }, function (err) { + console.error(err); + done(err); + return PromiseA.reject(err); + }); }); }; @@ -104,16 +116,17 @@ Challenge.set = function (args, domain, challenge, keyAuthorization, done) { // 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?)"); }; Challenge.remove = function (defaults, domain, challenge, done) { var me = this; - me._memstore.get(domain, function (err, data) { + return me._memstore.get(domain, function (err, data) { if (err) { done(err); return; } - var challengeDomain = defaults.test + defaults.acmeChallengeDns + domain; + var challengeDomain = (defaults.test || '') + defaults.acmeChallengeDns + domain; return DDNS.update({ email: data.email @@ -122,8 +135,8 @@ Challenge.remove = function (defaults, domain, challenge, done) { , name: challengeDomain , type: "TXT" - , value: challenge - , ttl: 60 + , value: data.keyAuthDigest || challenge + , ttl: defaults.ttl || 0 , remove: true }, { @@ -139,18 +152,17 @@ Challenge.remove = function (defaults, domain, challenge, done) { // same as get, but external Challenge.loopback = function (defaults, domain, challenge, done) { - var challengeDomain = defaults.test + defaults.acmeChallengeDns + domain; + var challengeDomain = (defaults.test || '') + defaults.acmeChallengeDns + domain; dns.resolveTxtAsync(challengeDomain).then(function () { done(null); }, done); }; Challenge.test = function (args, domain, challenge, keyAuthorization, done) { var me = this; - // Note: keyAuthorization is not used for dns-01 args.test = args.test || '_test.'; defaults.test = args.test; - me.set(args, domain, challenge, null, function (err) { + me.set(args, domain, challenge, keyAuthorization || challenge, function (err) { if (err) { done(err); return; } me.loopback(defaults, domain, challenge, function (err) { diff --git a/test.js b/test.js index deb3802..739ef11 100644 --- a/test.js +++ b/test.js @@ -12,6 +12,7 @@ var leChallengeDns = require('./').create({ var opts = leChallengeDns.getOptions(); var domain = 'test.daplie.me'; var challenge = 'xxx-acme-challenge-xxx'; +var keyAuthorization = 'xxx-acme-challenge-xxx.xxx-acme-authorization-xxx'; setTimeout(function () { leChallengeDns.test(opts, domain, challenge, null, function (err) { From 714027f88b9acf24955b2058dc7bc9128d32f3e8 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 12 Oct 2016 15:04:25 -0600 Subject: [PATCH 11/21] actually check txt record --- index.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 660ca52..2d78b2c 100644 --- a/index.js +++ b/index.js @@ -100,7 +100,7 @@ Challenge.set = function (args, domain, challenge, keyAuthorization, done) { console.log("Test DNS Record:"); console.log("dig TXT +noall +answer @ns1.redirect-www.org '" + challengeDomain + "' # " + challenge); } - done(null); + done(null, keyAuthDigest); }, function (err) { console.error(err); done(err); @@ -125,6 +125,11 @@ Challenge.remove = function (defaults, domain, challenge, done) { 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; @@ -153,7 +158,7 @@ Challenge.remove = function (defaults, domain, challenge, done) { // same as get, but external Challenge.loopback = function (defaults, domain, challenge, done) { var challengeDomain = (defaults.test || '') + defaults.acmeChallengeDns + domain; - dns.resolveTxtAsync(challengeDomain).then(function () { done(null); }, done); + dns.resolveTxtAsync(challengeDomain).then(function (x) { done(null, x); }, done); }; Challenge.test = function (args, domain, challenge, keyAuthorization, done) { @@ -162,18 +167,26 @@ Challenge.test = function (args, domain, challenge, keyAuthorization, done) { args.test = args.test || '_test.'; defaults.test = args.test; - me.set(args, domain, challenge, keyAuthorization || challenge, function (err) { + me.set(args, domain, challenge, keyAuthorization || challenge, function (err, k) { if (err) { done(err); return; } - me.loopback(defaults, domain, challenge, function (err) { + 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(); + done(err || null); /* me.loopback(defaults, domain, challenge, function (err) { if (err) { done(err); return; } From 26dcf0b6d1751ecdb2af7af5387f9e7bab80da7e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 12 Oct 2016 17:12:29 -0600 Subject: [PATCH 12/21] fix error --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 2d78b2c..8ddfa5a 100644 --- a/index.js +++ b/index.js @@ -181,8 +181,8 @@ Challenge.test = function (args, domain, challenge, keyAuthorization, done) { err = new Error("txt record '" + challenge + "' doesn't match '" + k + "'"); } - me.remove(defaults, domain, challenge, function (err) { - if (err) { done(err); return; } + 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) From 8398af7d9f28ce42889893fd7418252e2899a819 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 12 Oct 2016 17:28:45 -0600 Subject: [PATCH 13/21] v2.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ab1637..86caa09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "le-challenge-dns", - "version": "2.0.1", + "version": "2.0.2", "description": "A dns-based strategy for node-letsencrypt for setting, retrieving, and clearing ACME DNS-01 challenges issued by the ACME server", "main": "index.js", "scripts": { From e5a4b97c3f78c55062f12211da93c4f23b0ec300 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 14 Oct 2016 12:32:46 -0600 Subject: [PATCH 14/21] le-challenge-dns -> le-challenge-ddns --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f4a15e1..06473e3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ | [letsencrypt-hapi](https://github.com/Daplie/letsencrypt-hapi) | -le-challenge-dns +le-challenge-ddns ================ A dns-based strategy for node-letsencrypt for setting, retrieving, @@ -27,14 +27,14 @@ Install ------- ```bash -npm install --save le-challenge-dns@2.x +npm install --save le-challenge-ddns@2.x ``` Usage ----- ```bash -var leChallengeDns = require('le-challenge-dns').create({ +var leChallengeDdns = require('le-challenge-ddns').create({ email: 'john.doe@example.com' , refreshToken: '...' , ttl: 60 @@ -48,7 +48,7 @@ LE.create({ server: LE.stagingServerUrl // Change to LE.productionServerUrl in production , challengeType: 'dns-01' , challenges: { - 'dns-01': leChallengeDns + 'dns-01': leChallengeDdns } , approvedDomains: [ 'example.com' ] }); From 5a8a30c7058a9af4cad7f75ba807731b2551e0ca Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 14 Oct 2016 12:34:18 -0600 Subject: [PATCH 15/21] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 06473e3..b02d982 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,7 @@ For ACME Challenge: * `get(defaults, domain, challenge, done)` * `remove(defaults, domain, challenge, done)` -Note: `get()` is a no-op for `dns-01` and although `dns-01` does not use `keyAuthorization`, -it must be passed in as `null` to keep the correct method signature. +Note: `get()` is a no-op for `dns-01`. For node-letsencrypt internals: From cfeef0d981783afae31838f1057a9dc5e08fea73 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 14 Oct 2016 12:35:26 -0600 Subject: [PATCH 16/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b02d982..cbcaf3b 100644 --- a/README.md +++ b/README.md @@ -71,5 +71,5 @@ Note: `get()` is a no-op for `dns-01`. For node-letsencrypt internals: * `getOptions()` returns the internal defaults merged with the user-supplied options -* `loopback(defaults, domain, challange, keyAuthorization, done)` should test, by external means, that the ACME server's challenge server will succeed +* `loopback(defaults, domain, challange, done)` performs a dns lookup of the txt record * `test(opts, domain, challange, keyAuthorization, done)` runs set, loopback, remove, loopback From 8d3279d537fc9d4aa5c6032319d867a56e00527d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 14 Oct 2016 12:37:01 -0600 Subject: [PATCH 17/21] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86caa09..dbbff60 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "le-challenge-dns", + "name": "le-challenge-ddns", "version": "2.0.2", "description": "A dns-based strategy for node-letsencrypt for setting, retrieving, and clearing ACME DNS-01 challenges issued by the ACME server", "main": "index.js", From 1a5561dc8bcd4a82a6319f928560555f539761a0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 14 Oct 2016 12:45:35 -0600 Subject: [PATCH 18/21] dns -> ddns --- index.js | 2 +- package.json | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 8ddfa5a..f125a1e 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ var defaults = { , debug: false , acmeChallengeDns: '_acme-challenge.' // _acme-challenge.example.com TXT xxxxxxxxxxxxxxxx , memstoreConfig: { - name: 'le-dns' + name: 'le-ddns' } }; diff --git a/package.json b/package.json index dbbff60..0de502d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Daplie/le-challenge-dns.git" + "url": "git+https://github.com/Daplie/le-challenge-ddns.git" }, "keywords": [ "le", @@ -16,18 +16,20 @@ "le-challenge", "le-challenge-", "le-challenge-dns", + "le-challenge-ddns", "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-dns/issues" + "url": "https://github.com/Daplie/le-challenge-ddns/issues" }, - "homepage": "https://github.com/Daplie/le-challenge-dns#readme", + "homepage": "https://github.com/Daplie/le-challenge-ddns#readme", "dependencies": { "cluster-store": "^2.0.4", "daplie-dns": "git+https://github.com/Daplie/daplie-cli-dns.git#master", From 937da00a42b4a9596401dd3190178e60647584f1 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 14 Oct 2016 13:17:44 -0600 Subject: [PATCH 19/21] typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbcaf3b..64ec1fb 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ le-challenge-ddns A 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` wich `challenge` +It creates a subdomain record for `_acme-challenge` with `keyAuthDigest` to be tested by the ACME server. ``` From 05950414eac4a2e037daab49c777da1939c9925f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 14 Oct 2016 13:17:51 -0600 Subject: [PATCH 20/21] v2.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0de502d..ec2a247 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "le-challenge-ddns", - "version": "2.0.2", + "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", "main": "index.js", "scripts": { From b38ad55e8da68a6c22d228d971d5a726a205bf6e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 14 Oct 2016 13:30:18 -0600 Subject: [PATCH 21/21] typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64ec1fb..629ac55 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ LE.create({ , challenges: { 'dns-01': leChallengeDdns } -, approvedDomains: [ 'example.com' ] +, approveDomains: [ 'example.com' ] }); ```