Maciej Krüger
5 years ago
commit
c4ac31eccd
9 changed files with 351 additions and 0 deletions
@ -0,0 +1,2 @@ |
|||
*secret* |
|||
node_modules |
@ -0,0 +1,8 @@ |
|||
{ |
|||
"bracketSpacing": true, |
|||
"printWidth": 80, |
|||
"singleQuote": true, |
|||
"tabWidth": 2, |
|||
"trailingComma": "none", |
|||
"useTabs": true |
|||
} |
@ -0,0 +1 @@ |
|||
Maciej Krüger <mkg20001@gmail.com> (https://mkg20001.io/) |
@ -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 |
|||
``` |
@ -0,0 +1,3 @@ |
|||
'use strict' |
|||
|
|||
module.exports = require('./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?!
|
|||
} |
|||
} |
|||
} |
|||
} |
@ -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" |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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 <mkg20001@gmail.com> (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" |
|||
} |
|||
} |
@ -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) |
|||
}) |
Loading…
Reference in new issue