From c4ac31eccd9b5c04574bd51b21a33f1ad239034b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Sun, 23 Jun 2019 20:21:40 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 2 + .prettierrc | 8 +++ AUTHORS | 1 + README.md | 77 ++++++++++++++++++++++++ index.js | 3 + lib/index.js | 146 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 62 ++++++++++++++++++++ package.json | 30 ++++++++++ test.js | 22 +++++++ 9 files changed, 351 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 AUTHORS 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 100644 test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..407a14c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*secret* +node_modules 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/AUTHORS b/AUTHORS new file mode 100644 index 0000000..c6eed3f --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Maciej Krüger (https://mkg20001.io/) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e980688 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# acme-dns-01-cloudflare + +Cloudflare DNS + Let's Encrypt for Node.js + +This handles ACME dns-01 challenges, compatible with ACME.js and Greenlock.js. +Passes [acme-dns-01-test](https://git.rootprojects.org/root/acme-dns-01-test.js). + +# Install + +```bash +npm install --save acme-dns-01-cloudflare@3.x +``` + +# Usage + +First you create an instance with your credentials: + +```js +var dns01 = require('acme-dns-01-cloudflare').create({ + authKey: '123yourkey', + authEmail: 'you@example.com' +}); +``` + +Then you can use it with any compatible ACME module, +such as Greenlock.js or ACME.js. + +### Greenlock.js + +```js +var Greenlock = require('greenlock-express'); +var greenlock = Greenlock.create({ + challenges: { + 'dns-01': dns01 + // ... + } +}); +``` + +See [Greenlock™ Express](https://git.rootprojects.org/root/greenlock-express.js) +and/or [Greenlock.js](https://git.rootprojects.org/root/greenlock.js) documentation for more details. + +### ACME.js + +```js +// TODO +``` + +See the [ACME.js](https://git.rootprojects.org/root/acme-v2.js) for more details. + +### Build your own + +```js +dns01 + .set({ + identifier: { value: 'foo.example.com' }, + wildcard: false, + dnsHost: '_acme-challenge.foo.example.com', + dnsAuthorization: 'xxx_secret_xxx' + }) + .then(function () { + console.log("TXT record set"); + }) + .catch(function () { + console.log("Failed to set TXT record"); + }); +``` + +See [acme-dns-01-test](https://git.rootprojects.org/root/acme-dns-01-test.js) +for more implementation details. + +# Tests + +```bash +# node ./test.js domain-zone auth-key auth-email +node ./test.js example.com xxxxxx you@example.com +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..a4f3d55 --- /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..5f5f885 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,146 @@ +'use strict' + +let request = require('@root/request') +request = require('util').promisify(request) + +const Joi = require('@hapi/joi') + +const schema = Joi.object().keys({ + authKey: Joi.string().alphanum().required(), + authEmail: Joi.string().email({ minDomainSegments: 2 }).required(), + baseUrl: Joi.string() +}).with('username', 'birthyear').without('password', 'access_token') + +function formatError (msg, resp) { + const e = new Error(`${resp.statusCode}: ${msg}! (Check the Credentials)`) + e.body = resp.body + if (resp.body && resp.body.errors) { + e.errors = resp.body.errors + console.log(e.errors[0].error_chain) + } + e.statusCode = resp.statusCode + throw e +} + +var defaults = { + baseUrl: 'https://api.cloudflare.com/client/v4/' +} + +module.exports.create = function (config) { + const baseUrl = (config.baseUrl || defaults.baseUrl).replace(/\/$/, '') + Joi.validate(config, schema) + + function api (method, path, body) { + return request({ + method: method, + url: baseUrl + path, + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Key': config.authKey, + 'X-Auth-Email': config.authEmail + }, + json: true, + body + }) + } + + async function zones (domain) { + const resp = await api('GET', '/zones?per_page=1000' + (domain ? '&name=' + domain : '')) // TODO: use proper pagination?! + if (resp.statusCode !== 200) { + formatError('Could not get list of Zones', resp) + } + return resp + } + + async function getZone (domain) { + const res = await zones(domain) + return res.body.result.filter(zone => (zone.name === domain))[0] + } + + return { + zones: async function (data) { + const resp = await zones() + return resp.body.result.map(x => x.name) + }, + set: async function (data) { + const { + challenge: { + dnsZone: domain, + dnsAuthorization: txtRecord, + dnsPrefix + } + } = data + + const zone = await getZone(domain) + if (zone.permissions.indexOf('#zone:edit') === -1) { + throw new Error('Can not edit zone ' + JSON.stringify(domain) + ' from this account') + } + + const resp = await api('POST', `/zones/${zone.id}/dns_records`, {type: 'TXT', name: dnsPrefix, content: JSON.stringify(txtRecord), ttl: 300}) + if (resp.statusCode !== 200) { + formatError('Could not add record', resp) + } + + return true + }, + remove: async function (data) { + const { + challenge: { + dnsZone: domain, + dnsPrefix + } + } = data + + const zone = await getZone(domain) + if (zone.permissions.indexOf('#zone:edit') === -1) { + throw new Error('Can not edit zone ' + JSON.stringify(domain) + ' from this account') + } + + const resp = await api('GET', `/zones/${zone.id}/dns_records?name=${encodeURI(dnsPrefix + '.' + domain)}`) + if (resp.statusCode !== 200) { + formatError('Could not read record', resp) + } + + let {result} = resp.body + + let record = result.filter(record => (record.type === 'TXT'))[0] + + if (record) { + const resp = await api('DELETE', `/zones/${zone.id}/dns_records/${record.id}`) + if (resp.statusCode !== 200) { + formatError('Could not delete record', resp) + } + } else { + return null // TODO: not found. should this throw?! + } + }, + get: async function (data) { + const { + challenge: { + dnsZone: domain, + dnsPrefix + } + } = data + + const zone = await getZone(domain) + if (zone.permissions.indexOf('#zone:read') === -1) { + throw new Error('Can not read zone ' + JSON.stringify(domain) + ' from this account') + } + + const resp = await api('GET', `/zones/${zone.id}/dns_records?name=${encodeURI(dnsPrefix + '.' + domain)}`) + if (resp.statusCode !== 200) { + formatError('Could not read record', resp) + } + + let {result} = resp.body + + let record = result.filter(record => (record.type === 'TXT'))[0] + + if (record) { + return {dnsAuthorization: JSON.parse(record.content)} + } else { + return null // TODO: not found. should this throw?! + } + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b92c198 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,62 @@ +{ + "name": "acme-dns-01-cloudflare", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@hapi/address": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz", + "integrity": "sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw==" + }, + "@hapi/hoek": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.4.tgz", + "integrity": "sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A==" + }, + "@hapi/joi": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.0.tgz", + "integrity": "sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ==", + "requires": { + "@hapi/address": "2.x.x", + "@hapi/hoek": "6.x.x", + "@hapi/marker": "1.x.x", + "@hapi/topo": "3.x.x" + } + }, + "@hapi/marker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@hapi/marker/-/marker-1.0.0.tgz", + "integrity": "sha512-JOfdekTXnJexfE8PyhZFyHvHjt81rBFSAbTIRAhF2vv/2Y1JzoKsGqxH/GpZJoF7aEfYok8JVcAHmSz1gkBieA==" + }, + "@hapi/topo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.0.tgz", + "integrity": "sha512-gZDI/eXOIk8kP2PkUKjWu9RW8GGVd2Hkgjxyr/S7Z+JF+0mr7bAlbw+DkTRxnD580o8Kqxlnba9wvqp5aOHBww==", + "requires": { + "@hapi/hoek": "6.x.x" + } + }, + "@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.2.1", + "resolved": "https://registry.npmjs.org/acme-challenge-test/-/acme-challenge-test-3.2.1.tgz", + "integrity": "sha512-8MwL2oWx7vM/SBIeEQfeoRyW0kYCtLFS4FfgIx3lsQmSKhbDo9J88Ud6DejdupRp2T+DlEkWIBVI3qOCVViUaQ==", + "dev": true + }, + "acme-dns-01-test": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/acme-dns-01-test/-/acme-dns-01-test-3.2.1.tgz", + "integrity": "sha512-m4UxltZzTNbQTK30iQQ6BQ99oRJA9p69+eg/u/Plxiw10bD+qsmRZR9rsqZuiSc62wfng/upGvXWMQZ/csn3lA==", + "dev": true, + "requires": { + "acme-challenge-test": "^3.2.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d3d54cd --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "acme-dns-01-cloudflare", + "version": "0.1.0", + "description": "Cloudflare 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.coolaj86.com/mkg20001/acme-dns-01-cloudflare.git" + }, + "keywords": [ + "cloudflare", + "dns", + "dns-01", + "letsencrypt", + "acme", + "greenlock" + ], + "author": "Maciej Krüger (https://mkg20001.io/)", + "license": "MPL-2.0", + "dependencies": { + "@hapi/joi": "^15.1.0", + "@root/request": "^1.3.11" + }, + "devDependencies": { + "acme-dns-01-test": "^3.2.1" + } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..87f84d2 --- /dev/null +++ b/test.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node +'use strict' + +// https://git.rootprojects.org/root/acme-dns-01-test.js +var tester = require('acme-dns-01-test') + +// Usage: node ./test.js example.com xxxxxxxxx +let [zone, authKey, authEmail] = process.argv.slice(2) +var challenger = require('./index.js').create({ authKey, authEmail }) + +// 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.info('FAIL', zone) + console.error(e.message) + console.error(e.stack) + })