Added support for authentication tokens + fix for #1 #2

Open
Ghost wants to merge 2 commits from (deleted):master into master
3 changed files with 67 additions and 27 deletions
Showing only changes of commit eebb2cb219 - Show all commits

View File

@ -13,7 +13,7 @@ npm install --save acme-dns-01-cloudflare@3.x
# Usage # Usage
First you create an instance with your credentials: First you create an instance with your account credentials:
```js ```js
var dns01 = require('acme-dns-01-cloudflare').create({ var dns01 = require('acme-dns-01-cloudflare').create({
@ -22,6 +22,18 @@ var dns01 = require('acme-dns-01-cloudflare').create({
}); });
``` ```
or token credentials:
```js
var dns01 = require('acme-dns-01-cloudflare').create({
bearerTokens: {
list: '123yourListToken', // This token needs to be able to list all of your zones
zone: '456yourZoneToken' // This token needs to have full control over the targeted DNS zone(s)
},
authEmail: 'you@example.com'
});
```
Then you can use it with any compatible ACME module, Then you can use it with any compatible ACME module,
such as Greenlock.js or ACME.js. such as Greenlock.js or ACME.js.
@ -72,6 +84,8 @@ for more implementation details.
# Tests # Tests
```bash ```bash
# node ./test.js domain-zone auth-key auth-email # node ./test.js domain-zone auth-email auth-type auth-credential (aux-credential?)
node ./test.js example.com xxxxxx you@example.com node ./test.js example.com you@example.com key YourApiKey
node ./test.js example.com you@example.com token YourApiTokenWithFullRights
node ./test.js example.com you@example.com token YourApiTokenWithListRights YourApiTokenWithEditRightsForTheZone
``` ```

View File

@ -6,10 +6,16 @@ request = require('util').promisify(request)
const Joi = require('@hapi/joi') const Joi = require('@hapi/joi')
const schema = Joi.object().keys({ const schema = Joi.object().keys({
authKey: Joi.string().alphanum().required(), authKey: Joi.string().alphanum(),
bearerTokens: Joi.object({
list: Joi.string().alphanum().required(),
zone: Joi.string().alphanum().required()
}),
authEmail: Joi.string().email({ minDomainSegments: 2 }).required(), authEmail: Joi.string().email({ minDomainSegments: 2 }).required(),
baseUrl: Joi.string() baseUrl: Joi.string()
}).with('username', 'birthyear').without('password', 'access_token') }).with('username', 'birthyear').without('password', 'access_token').nand(
'authKey', 'bearerTokens'
)
function formatError (msg, resp) { function formatError (msg, resp) {
const e = new Error(`${resp.statusCode}: ${msg}! (Check the Credentials)`) const e = new Error(`${resp.statusCode}: ${msg}! (Check the Credentials)`)
@ -27,25 +33,35 @@ var defaults = {
} }
module.exports.create = function (config) { module.exports.create = function (config) {
const baseUrl = (config.baseUrl || defaults.baseUrl).replace(/\/$/, '')
Joi.validate(config, schema) Joi.validate(config, schema)
const baseUrl = (config.baseUrl || defaults.baseUrl).replace(/\/$/, '')
function api (method, path, body) { const defaultHeaders = {
'Content-Type': 'application/json',
'X-Auth-Email': config.authEmail
}
if (config.authKey) {
defaultHeaders['X-Auth-Key'] = config.authKey
}
function api (method, path, body, tokenType) {
const headers = defaultHeaders;
if (tokenType && config.bearerTokens) {
if (!(tokenType in config.bearerTokens)) {
throw new Error('Unrecognized token type');
}
headers['Authorization'] = 'Bearer ' + config.bearerTokens[tokenType];
}
return request({ return request({
method: method,
url: baseUrl + path, url: baseUrl + path,
headers: {
'Content-Type': 'application/json',
'X-Auth-Key': config.authKey,
'X-Auth-Email': config.authEmail
},
json: true, json: true,
method,
headers,
body body
}) })
} }
async function zones (domain) { async function zones (domain) {
const resp = await api('GET', '/zones?per_page=1000' + (domain ? '&name=' + domain : '')) // TODO: use proper pagination?! const resp = await api('GET', '/zones?per_page=1000' + (domain ? '&name=' + domain : ''), undefined, 'list') // TODO: use proper pagination?!
if (resp.statusCode !== 200) { if (resp.statusCode !== 200) {
formatError('Could not get list of Zones', resp) formatError('Could not get list of Zones', resp)
} }
@ -70,14 +86,13 @@ module.exports.create = function (config) {
dnsPrefix dnsPrefix
} }
} = data } = data
console.log(data)
const zone = await getZone(domain) const zone = await getZone(domain)
if (zone.permissions.indexOf('#zone:edit') === -1) { if (zone.permissions.indexOf('#zone:edit') === -1) {
throw new Error('Can not edit zone ' + JSON.stringify(domain) + ' from this account') 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: txtRecord, ttl: 300}) const resp = await api('POST', `/zones/${zone.id}/dns_records`, {type: 'TXT', name: dnsPrefix, content: txtRecord, ttl: 300}, 'zone')
if (resp.statusCode !== 200) { if (resp.statusCode !== 200) {
formatError('Could not add record', resp) formatError('Could not add record', resp)
} }
@ -88,6 +103,7 @@ module.exports.create = function (config) {
const { const {
challenge: { challenge: {
dnsZone: domain, dnsZone: domain,
dnsAuthorization: txtRecord,
dnsPrefix dnsPrefix
} }
} = data } = data
@ -97,28 +113,28 @@ module.exports.create = function (config) {
throw new Error('Can not edit zone ' + JSON.stringify(domain) + ' from this account') 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)}`) const resp = await api('GET', `/zones/${zone.id}/dns_records?name=${encodeURI(dnsPrefix + '.' + domain)}`, undefined,'zone')
if (resp.statusCode !== 200) { if (resp.statusCode !== 200) {
formatError('Could not read record', resp) formatError('Could not read record', resp)
} }
let {result} = resp.body let {result} = resp.body
let record = result.filter(record => (record.type === 'TXT'))[0] let record = result.filter(record => (record.type === 'TXT' && record.content === txtRecord))[0]
if (record) { if (record) {
const resp = await api('DELETE', `/zones/${zone.id}/dns_records/${record.id}`) const resp = await api('DELETE', `/zones/${zone.id}/dns_records/${record.id}`, undefined, 'zone')
if (resp.statusCode !== 200) { if (resp.statusCode !== 200) {
formatError('Could not delete record', resp) formatError('Could not delete record', resp)
} }
} else {
return null // TODO: not found. should this throw?!
} }
return null // TODO: not found. should this throw?!
}, },
get: async function (data) { get: async function (data) {
const { const {
challenge: { challenge: {
dnsZone: domain, dnsZone: domain,
dnsAuthorization: txtRecord,
dnsPrefix dnsPrefix
} }
} = data } = data
@ -128,14 +144,14 @@ module.exports.create = function (config) {
throw new Error('Can not read zone ' + JSON.stringify(domain) + ' from this account') 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)}`) const resp = await api('GET', `/zones/${zone.id}/dns_records?name=${encodeURI(dnsPrefix + '.' + domain)}`, undefined, 'zone')
if (resp.statusCode !== 200) { if (resp.statusCode !== 200) {
formatError('Could not read record', resp) formatError('Could not read record', resp)
} }
let {result} = resp.body let {result} = resp.body
let record = result.filter(record => (record.type === 'TXT'))[0] let record = result.filter(record => (record.type === 'TXT' && record.content === txtRecord))[0]
if (record) { if (record) {
return {dnsAuthorization: record.content} return {dnsAuthorization: record.content}

18
test.js
View File

@ -2,11 +2,21 @@
'use strict' 'use strict'
// https://git.rootprojects.org/root/acme-dns-01-test.js // https://git.rootprojects.org/root/acme-dns-01-test.js
var tester = require('acme-dns-01-test') const tester = require('acme-dns-01-test')
// Usage: node ./test.js example.com xxxxxxxxx // Usage: node ./test.js example.com you@example.com key xxxxxxxxx
let [zone, authKey, authEmail] = process.argv.slice(2) // Usage: node ./test.js example.com you@example.com token xxxxxxxxx
var challenger = require('./index.js').create({ authKey, authEmail }) // Usage: node ./test.js example.com you@example.com token xxxxxxxxx yyyyyyy
const [zone, authEmail, authType, credential, zoneToken] = process.argv.slice(2)
const config = { authEmail }
switch (authType) {
case 'token':
config.bearerTokens = {list: credential, zone: zoneToken || credential}
break
default:
config.authKey = credential
}
const challenger = require('./index.js').create(config)
// The dry-run tests can pass on, literally, 'example.com' // The dry-run tests can pass on, literally, 'example.com'
// but the integration tests require that you have control over the domain // but the integration tests require that you have control over the domain