diff --git a/README.md b/README.md index e980688..5b74bba 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ npm install --save acme-dns-01-cloudflare@3.x # Usage -First you create an instance with your credentials: +First you create an instance with your account credentials: ```js 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, such as Greenlock.js or ACME.js. @@ -72,6 +84,8 @@ for more implementation details. # Tests ```bash -# node ./test.js domain-zone auth-key auth-email -node ./test.js example.com xxxxxx you@example.com +# node ./test.js domain-zone auth-email auth-type auth-credential (aux-credential?) +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 ``` diff --git a/lib/index.js b/lib/index.js index 0e9959a..5998bf5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,10 +6,16 @@ request = require('util').promisify(request) const Joi = require('@hapi/joi') 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(), baseUrl: Joi.string() -}).with('username', 'birthyear').without('password', 'access_token') +}).with('username', 'birthyear').without('password', 'access_token').nand( + 'authKey', 'bearerTokens' +) function formatError (msg, resp) { const e = new Error(`${resp.statusCode}: ${msg}! (Check the Credentials)`) @@ -27,25 +33,35 @@ var defaults = { } module.exports.create = function (config) { - const baseUrl = (config.baseUrl || defaults.baseUrl).replace(/\/$/, '') 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({ - method: method, url: baseUrl + path, - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Key': config.authKey, - 'X-Auth-Email': config.authEmail - }, json: true, + method, + headers, body }) } 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) { formatError('Could not get list of Zones', resp) } @@ -70,14 +86,13 @@ module.exports.create = function (config) { dnsPrefix } } = data - console.log(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: 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) { formatError('Could not add record', resp) } @@ -88,6 +103,7 @@ module.exports.create = function (config) { const { challenge: { dnsZone: domain, + dnsAuthorization: txtRecord, dnsPrefix } } = data @@ -97,28 +113,28 @@ module.exports.create = function (config) { 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) { formatError('Could not read record', resp) } 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) { - 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) { 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) { const { challenge: { dnsZone: domain, + dnsAuthorization: txtRecord, dnsPrefix } } = data @@ -128,14 +144,14 @@ module.exports.create = function (config) { 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) { formatError('Could not read record', resp) } 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) { return {dnsAuthorization: record.content} diff --git a/test.js b/test.js index 87f84d2..9a5c080 100644 --- a/test.js +++ b/test.js @@ -2,11 +2,21 @@ 'use strict' // 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 -let [zone, authKey, authEmail] = process.argv.slice(2) -var challenger = require('./index.js').create({ authKey, authEmail }) +// Usage: node ./test.js example.com you@example.com key xxxxxxxxx +// Usage: node ./test.js example.com you@example.com token xxxxxxxxx +// 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' // but the integration tests require that you have control over the domain