Initial Commit
This commit is contained in:
commit
c4ac31eccd
|
@ -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