From b41f09df11237f8028737cc4112e8b2d404ae778 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 7 Sep 2016 14:58:25 -0600 Subject: [PATCH] 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" + } +}