Added support for authentication tokens + fix for #1 #2
20
README.md
20
README.md
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
62
lib/index.js
62
lib/index.js
|
@ -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
|
const { result } = resp.body
|
||||||
|
|
||||||
let record = result.filter(record => (record.type === 'TXT'))[0]
|
const 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,17 +144,17 @@ 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
|
const { result } = resp.body
|
||||||
|
|
||||||
let record = result.filter(record => (record.type === 'TXT'))[0]
|
const record = result.filter(record => (record.type === 'TXT' && record.content === txtRecord))[0]
|
||||||
|
|
||||||
if (record) {
|
if (record) {
|
||||||
return {dnsAuthorization: record.content}
|
return { dnsAuthorization: record.content }
|
||||||
} else {
|
} else {
|
||||||
return null // TODO: not found. should this throw?!
|
return null // TODO: not found. should this throw?!
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -25,6 +25,13 @@
|
||||||
"@root/request": "^1.3.11"
|
"@root/request": "^1.3.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"acme-dns-01-test": "^3.2.1"
|
"acme-dns-01-test": "^3.2.1",
|
||||||
|
"standard": "^14.1.0",
|
||||||
|
"husky": "^3.0.4"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "npx standard --fix && git update-index --again"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
test.js
18
test.js
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue