From eb3d395f622d4f650b3bfff4fa15b291af90bc1b Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 6 Jun 2019 23:11:33 -0600 Subject: [PATCH] initial commit --- .prettierrc | 8 +++ README.md | 10 +++ index.js | 3 + lib/index.js | 152 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 19 ++++++ package.json | 30 +++++++++ test.js | 23 +++++++ 7 files changed, 245 insertions(+) create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 index.js create mode 100644 lib/index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 test.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..420e082 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "bracketSpacing": true, + "printWidth": 80, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": true +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..888f3a6 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# [acme-dns-01-digitalocean](https://git.rootprojects.org/root/acme-dns-01-digitalocean) | a [Root](https://rootrpojects.org) project + +Digital Ocean DNS for Let's Encrypt / ACME dns-01 challenges with ACME.js and Greenlock.js (Node.js). + +# Tests + +``` +# node ./test.js domain-zone api-token +node ./test.js example.com xxxxxx +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..647221a --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./lib/index.js'); diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..2995a01 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,152 @@ +'use strict'; + +var request = require('@root/request'); +request = require('util').promisify(request); + +var defaults = { + baseUrl: 'https://api.digitalocean.com' +}; + +module.exports.create = function(config) { + // config = { baseUrl, token } + var baseUrl = config.baseUrl || defaults.baseUrl; + var authtoken = config.token; + return { + set: function(data) { + var ch = data.challenge; + var domainname = ch.identifier.value; + // PROBLEM: zone != domain + // example.com, foo.example.com, and bar.foo.example.com are all in the example.com zone!! + // Need to get list of "domains" (zones) and *then* set "subdomains" (domain records) + // https://developers.digitalocean.com/documentation/v2/#domains + var zone = domainname; + // PROBLEM: dnsPrefix != dnsHost.split('.')[0] + // _greenlock-dryrun-2277.bar.foo.example.com => _greenlock-dryrun-2277.bar.foo + var dnsPrefix = ch.dnsHost.replace(new RegExp('.' + zone + '$'), ''); + var txt = ch.dnsAuthorization; + // If the domain to be verified is + var url = baseUrl + '/v2/domains/' + zone + '/records'; + + console.log('adding txt', data); + return request({ + method: 'POST', + url: url, + headers: { + Authorization: 'Bearer ' + authtoken, + 'Content-Type': 'application/json' + }, + json: { + type: 'TXT', + name: dnsPrefix, + data: txt, + // PROBLEM (fixed) set a LOW ttl so that responses are not cached so long + ttl: 300 + } + }).then(function(resp) { + resp = resp.body; + console.log(resp); + if (resp && resp.domain_record && resp.domain_record.data === txt) { + return true; + } + throw new Error('record did not set. check subdomain, api key, etc'); + }); + }, + remove: function(data) { + var domainname = data.challenge.altname; + var zone = domainname; + // PROBLEM: domainname != zone + var url = baseUrl + '/v2/domains/' + zone + '/records'; + + // Digital ocean provides the api to remove records by ID. Since we do not have id, we fetch all the records, + // filter the required TXT record and pass its id to remove API + return request({ + method: 'GET', + url: url, + // PROBLEM (fixed): Remember to set json: true (not need to JSON.parse) + json: true, + headers: { + Authorization: 'Bearer ' + authtoken, + 'Content-Type': 'application/json' + } + }) + .then(function(resp) { + resp = resp.body; + var entries = + resp && + resp.domain_records && + resp.domain_records.filter(function(x) { + return x.type === 'TXT'; + }); + // PROBLEM entry[0] !== our entry + // (see solution for other entries, down below) + if (entries.length > 0) { + return entries[0].id; + } else { + throw new Error( + 'Couldnt remove record. check subdomain, api key, etc' + ); + } + }) + .then(function(recordId) { + var domainname = data.challenge.altname; + var zone = domainname; + var url = baseUrl + '/v2/domains/' + zone + '/records/' + recordId; + + return request({ + method: 'DELETE', + url: url, + headers: { + Authorization: 'Bearer ' + authtoken, + 'Content-Type': 'application/json' + } + }).then(function(resp) { + resp = resp.body; + console.log(resp); + return true; + }); + }); + }, + get: function(data) { + var ch = data.challenge; + var domainname = data.challenge.altname; + var zone = domainname; + // PROBLEM: domainname != zone + var url = baseUrl + '/v2/domains/' + zone + '/records'; + console.log('getting txt', data); + + // Digital ocean provides the api to fetch records by ID. Since we do not have id, we fetch all the records, + // filter the required TXT record + + return request({ + method: 'GET', + url: url, + json: true, + headers: { + Authorization: 'Bearer ' + authtoken, + 'Content-Type': 'application/json' + } + }).then(function(resp) { + resp = resp.body; + + var entries = + resp && + resp.domain_records && + resp.domain_records.filter(function(x) { + // PROBLEM should also check for prefix + return x.type === 'TXT'; + }); + // PROBLEM (fixed): entries[0] !== our entry + var entry = entries.filter(function(x) { + console.log('data', x.data); + console.log('dnsAuth', ch.dnsAuthorization, ch); + return x.data === ch.dnsAuthorization; + })[0]; + if (entry) { + return { dnsAuthorization: entry.data }; + } else { + return null; + } + }); + } + }; +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..def9282 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,19 @@ +{ + "name": "acme-dns-01-digitalocean", + "version": "3.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@root/request": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz", + "integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw==" + }, + "acme-challenge-test": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acme-challenge-test/-/acme-challenge-test-3.1.0.tgz", + "integrity": "sha512-k+2hNED8P245BdJh44+eJjmVr7oNcuxigMbfz2/7Emc5h4ZAI7iuRZ2M6t4Qui+q5YgiPPjnILDzCZTHLzb1Ag==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b66f221 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "acme-dns-01-digitalocean", + "version": "3.0.0", + "description": "Digital Ocean DNS for Let's Encrypt / ACME dns-01 challenges with ACME.js and Greenlock.js", + "main": "index.js", + "scripts": { + "test": "node ./test.js" + }, + "repository": { + "type": "git", + "url": "https://git.rootprojects.org/root/acme-dns-01-digitalocean.git" + }, + "keywords": [ + "digitalocean", + "digital-ocean", + "dns", + "dns-01", + "letsencrypt", + "acme", + "greenlock" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "MPL-2.0", + "dependencies": { + "@root/request": "^1.3.11" + }, + "devDependencies": { + "acme-challenge-test": "^3.1.0" + } +} diff --git a/test.js b/test.js new file mode 100755 index 0000000..8674fce --- /dev/null +++ b/test.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node +'use strict'; + +// See https://git.coolaj86.com/coolaj86/acme-challenge-test.js +var tester = require('acme-challenge-test'); + +// Usage: node ./test.js example.com xxxxxxxxx +var zone = process.argv[2]; +var challenger = require('./index.js').create({ + token: process.argv[3] +}); + +// The dry-run tests can pass on, literally, 'example.com' +// but the integration tests require that you have control over the domain +tester + .testZone('dns-01', zone, challenger) + .then(function() { + console.info('PASS', zone); + }) + .catch(function(e) { + console.error(e.message); + console.error(e.stack); + });