diff --git a/README.md b/README.md index bc1efdb..87564bc 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Handles ACME dns-01 challenges. Compatible with ACME.js and Greenlock.js. Passes - Compatible - Let’s Encrypt v2.1 / ACME draft 18 (2019) - - Name.com API + - Name.com v4 API - ACME.js, Greenlock.js, and others - Quality - node v6 compatible VanillaJS @@ -21,10 +21,22 @@ Handles ACME dns-01 challenges. Compatible with ACME.js and Greenlock.js. Passes npm install --save acme-dns-01-namedotcom ``` -Name.com Token: +## Name.com API Token -- Login to your account at: {{ Service URL }} -- {{ Instructions to generate token }} +- Login to your account at: +- Generate a token at +- **Important** Enable API Access, _again_ at + +The following error is what you may get if you have Two-Factor Auth and don't _Enable API Access_ the second time: + +```json +{ + "message": "Permission Denied", + "details": "Authentication Error - Account Has Namesafe Enabled" +} +``` + +If you're using the Whitelist IPs feature, don't forget to add your test environment! # Usage @@ -32,7 +44,8 @@ First you create an instance with your credentials: ```js var dns01 = require('acme-dns-01-namedotcom').create({ - baseUrl: '{{ api url }}', // default + baseUrl: 'http://api.name.com/v4/', // default + username: 'johndoe', token: 'xxxx' }); ``` @@ -51,7 +64,8 @@ var greenlock = Greenlock.create({ }); ``` -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. +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 @@ -93,8 +107,8 @@ See acme-dns-01-test for more implementation details. # Tests ```bash -# node ./test.js domain-zone api-token -node ./test.js example.com xxxxxx +# node ./test.js domain-zone username api-token +node ./test.js example.com me xxxxxx ``` # Authors diff --git a/example.env b/example.env index 48ceb39..4feaac3 100644 --- a/example.env +++ b/example.env @@ -1,2 +1,4 @@ ZONE=example.co.uk -TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# Used for HTTP Basic Auth +USERNAME=johndoe +TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/lib/index.js b/lib/index.js index e25856a..8cb2b83 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,29 +1,137 @@ 'use strict'; var request; -var defaults = {}; +var defaults = { + baseUrl: 'https://api.name.com/v4/' +}; module.exports.create = function(config) { - return { + var baseUrl = (config.baseUrl || defaults.baseUrl).replace(/\/$/, ''); + var token = config.token; + var username = config.username; + + var plugin = { init: function(opts) { request = opts.request; return null; }, + + // We must list all zones (domains) in the account zones: function(data) { - //console.info('List Zones', data); - throw Error('listing zones not implemented'); + return api({ + url: baseUrl + '/domains' + }).then(function(resp) { + return resp.body.domains.map(function(d) { + //#console.log("Domain Name:", d.domainName); + return d.domainName; + }); + }); }, + + // We must set each record as required set: function(data) { - // console.info('Add TXT', data); - throw Error('setting TXT not implemented'); - }, - remove: function(data) { - // console.info('Remove TXT', data); - throw Error('removing TXT not implemented'); + // console.log('Add TXT', data); + var ch = data.challenge; + if (!ch.dnsZone) { + throw new Error( + '[Name.com Plugin] Unknown domain: ', + data.domain || data.dnsHost + ); + } + return api({ + method: 'POST', + url: baseUrl + '/domains/' + ch.dnsZone + '/records', + json: { + host: ch.dnsPrefix, + type: 'TXT', + answer: ch.dnsAuthorization, + ttl: 300 // minimum allowed value + } + }).then(function(resp) { + if (!resp.body.id) { + throw Error('[Name.com API] [set] ' + resp.body); + } + return null; + }); }, + + // We must be able to confirm that the appropriate records were set get: function(data) { - // console.info('List TXT', data); - throw Error('listing TXTs not implemented'); + // console.log('List TXT', data); + var ch = data.challenge; + if (!ch.dnsZone) { + throw new Error( + '[Name.com Plugin] Unknown domain: ', + data.domain || data.dnsHost + ); + } + return api({ + url: baseUrl + '/domains/' + ch.dnsZone + '/records' + }).then(function(resp) { + var value = resp.body.records.filter(function(r) { + return ( + r.host === ch.dnsPrefix && + 'TXT' === r.type && + ch.dnsAuthorization === r.answer + ); + })[0]; + if (!value) { + return null; + } + // adding id to make re-usable for remove + return { id: value.id, dnsAuthorization: value.answer }; + }); + }, + + // We must delete junk records once we're done + remove: function(data) { + // console.log('Remove TXT', data); + var ch = data.challenge; + return plugin.get(data).then(function(r) { + if (!r.id) { + throw new Error( + '[Name.com Plugin] [del] Did not find TXT record for ' + + ch.dnsHost + ); + } + return api({ + method: 'DELETE', + url: baseUrl + '/domains/' + ch.dnsZone + '/records/' + r.id + }).then(function(resp) { + return null; + }); + }); } }; + + // Authentication and Error handling here + function api(opts) { + opts.auth = { + user: username, + pass: token, + sendImmediately: true + }; + if (!opts.json) { + opts.json = true; + } + return request(opts).then(function(resp) { + if (!resp.body.message) { + return resp; + } + + console.error(opts.method + ' ' + opts.url); + console.error(resp.headers); + console.error(resp.body); + throw new Error( + '[Name.com API] ' + + (opts.method || 'GET') + + ' ' + + opts.url + + ' : ' + + resp.body.message + ); + }); + } + + return plugin; }; diff --git a/package.json b/package.json index d4cdd44..b101697 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-dns-01-namedotcom", - "version": "0.0.1", + "version": "3.0.0", "description": "Name.com + Let's Encrypt for Node.js - ACME dns-01 challenges w/ ACME.js and Greenlock.js", "main": "index.js", "files": [ @@ -26,9 +26,9 @@ "author": "AJ ONeal (https://coolaj86.com/)", "license": "MPL-2.0", "devDependencies": { + "acme-challenge-test": "^3.3.2", "dotenv": "^8.0.0" }, "dependencies": { - "acme-challenge-test": "^3.3.2" } } diff --git a/test.js b/test.js index d8b9412..04258f6 100755 --- a/test.js +++ b/test.js @@ -5,10 +5,12 @@ var tester = require('acme-challenge-test'); require('dotenv').config(); -// Usage: node ./test.js example.com xxxxxxxxx +// Usage: node ./test.js example.com username xxxxxxxxx var zone = process.argv[2] || process.env.ZONE; var challenger = require('./index.js').create({ - token: process.argv[3] || process.env.TOKEN + //baseUrl: 'http://api.dev.name.com/v4/', // sandbox url + username: process.argv[3] || process.env.USERNAME, + token: process.argv[4] || process.env.TOKEN }); // The dry-run tests can pass on, literally, 'example.com' diff --git a/token-test.sh b/token-test.sh new file mode 100644 index 0000000..dafe22d --- /dev/null +++ b/token-test.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +# Before assuming there's something wrong with this plugin, +# you should test that you can use your token with curl first. + +echo "USERNAME: '$USERNAME'" +echo "TOKEN: '$TOKEN'" +curl -f -u "$USERNAME:$TOKEN" https://api.name.com/v4/domains