This commit is contained in:
Hitesh 2019-08-19 14:40:43 -07:00
parent 9a54d2e3bc
commit 1ec51c635c
4 changed files with 330 additions and 254 deletions

View File

@ -1 +1,2 @@
AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/) AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)
Hitesh Walia <hiteshwar.walia@gmail.com>

108
README.md
View File

@ -1,3 +1,111 @@
# acme-dns-01-route53 # acme-dns-01-route53
Amazon AWS Route53 DNS + Let's Encrypt for Node.js - ACME dns-01 challenges w/ ACME.js and Greenlock.js Amazon AWS Route53 DNS + Let's Encrypt for Node.js - ACME dns-01 challenges w/ ACME.js and Greenlock.js
# Features
- Compatible
- Lets Encrypt v2.1 / ACME draft 18 (2019)
- DNSimple v2 API
- ACME.js, Greenlock.js, and others
- Quality
- node v6 compatible VanillaJS
# Install
```js
npm install --save acme-dns-01-route53
```
AWS API keys:
- Login to your account at: https://console.aws.amazon.com
- Go to Services > IAM > Users, create a new user and assign AmazonRoute53FullAccess policy.
- Click *Securty credentials* tab and then click *Create access key*.
# Usage
First you create an instance with your credentials:
```js
var dns01 = require('acme-dns-01-route53').create({
key: 'your_key',
secret: 'your_secret'
});
```
Then you can use it with any compatible ACME library, 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
There are only 5 methods:
- `init(config)`
- `zones(opts)`
- `set(opts)`
- `get(opts)`
- `remove(opts)`
```js
dns01
.set({
identifier: { value: 'foo.example.co.uk' },
wildcard: false,
dnsZone: 'example.co.uk',
dnsPrefix: '_acme-challenge.foo',
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 for more implementation details.
# Tests
```bash
# node ./test.js domain-zone key secret
node ./test.js example.com xxxxx yyyyy
```
# Authors
- AJ ONeal
- Hitesh Walia
See AUTHORS for contact info.
# Legal
[acme-dns-01-dnsimple.js](https://git.coolaj86.com/coolaj86/acme-dns-01-dnsimple.js) | MPL-2.0 | [Terms of Use](https://therootcompany.com/legal/#terms) | [Privacy Policy](https://therootcompany.com/legal/#privacy)
Copyright 2019 Hitesh Walia
Copyright 2019 AJ ONeal
Copyright 2019 The Root Group LLC

View File

@ -5,14 +5,66 @@ var xml2js = require('xml2js');
var parseString = xml2js.parseString; var parseString = xml2js.parseString;
parseString = require('util').promisify(parseString); parseString = require('util').promisify(parseString);
var builder = new xml2js.Builder(); var builder = new xml2js.Builder();
var request;
var request = require('@root/request');
//request.debug = false;
var defaults = { var defaults = {
baseUri: '/2013-04-01/' baseUri: '/2013-04-01/'
}; };
function setWeightedPolicy(XmlObject, isWildcard) {
if (isWildcard) {
XmlObject.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].SetIdentifier = [
'wildcard'
];
} else {
XmlObject.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].SetIdentifier = [
'subdomain'
];
}
XmlObject.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].Weight = [
1
];
}
// used to build XML payload
var XmlParameters = {
ChangeResourceRecordSetsRequest: {
$: {
xmlns: ''
},
ChangeBatch: [
{
Changes: [
{
Change: [
{
Action: [''],
ResourceRecordSet: [
{
Type: ['TXT'],
Name: [''],
ResourceRecords: [
{
ResourceRecord: [
{
Value: ['']
}
]
}
],
TTL: [300]
}
]
}
]
}
]
}
]
}
};
module.exports.create = function(config) { module.exports.create = function(config) {
var baseUri = defaults.baseUri.replace(/\/$/, ''); var baseUri = defaults.baseUri.replace(/\/$/, '');
var key = config.key; var key = config.key;
@ -26,28 +78,29 @@ module.exports.create = function(config) {
secret: secret, secret: secret,
query: query query: query
}; };
if(method === 'POST') {
options.headers = { 'Content-Type': 'application/xml' }
}
var url = v4.createPresignedURL(method, 'route53.amazonaws.com', path, 'route53', body, options);
//console.log('url: ' + url + '\n');
var parameters = { var parameters = {
method: method, method: method
url: url
}; };
if(method === 'POST') { if (method === 'POST') {
parameters.headers = { 'Content-Type': 'application/xml' } options.headers = { 'Content-Type': 'application/xml' };
parameters.headers = { 'Content-Type': 'application/xml' };
} }
if(body) { if (body) {
parameters.body = body; parameters.body = body;
} }
// console.log(method, path, query); var url = v4.createPresignedURL(
method,
'route53.amazonaws.com',
path,
'route53',
body,
options
);
parameters.url = url;
return request(parameters).then(function(response) { return request(parameters).then(function(response) {
if (response.statusCode < 200 || response.statusCode >= 300) { if (response.statusCode < 200 || response.statusCode >= 300) {
@ -67,263 +120,174 @@ module.exports.create = function(config) {
}); });
} }
/*
api('GET', baseUri + '/' + 'hostedzone').then(function(response){
console.log('######## RESPONSE #########');
});
*/
return { return {
init: function(options) { init: function(options) {
request = options.request; request = options.request;
return null; return null;
}, },
zones: function(data) { zones: function(data) {
//console.info('List Zones', data); return api('GET', baseUri + '/' + 'hostedzone').then(function(
return api('GET', baseUri + '/' + 'hostedzone').then(function(response){ response
return parseString(response.body).then(function(body){ ) {
var zones = body.ListHostedZonesResponse.HostedZones[0].HostedZone.map(function(element) { return parseString(response.body).then(function(body) {
return element.Name[0].replace(/\.$/, ''); // all route 53 domains terminate with a dot var zones = body.ListHostedZonesResponse.HostedZones[0].HostedZone.map(
}); function(element) {
return element.Name[0].replace(/\.$/, ''); // all route 53 domains terminate with a dot
}
);
return zones; return zones;
}); });
}); });
}, },
set: function(data) { set: function(data) {
// console.info('Add TXT', data);
// console.log('data: ' + JSON.stringify(data, null, 2));
var challenge = data.challenge; var challenge = data.challenge;
if (!challenge.dnsZone) { if (!challenge.dnsZone) {
throw new Error('No matching zone for ' + challenge.dnsHost); throw new Error('No matching zone for ' + challenge.dnsHost);
} }
return api('GET', baseUri + '/' + 'hostedzonesbyname', null, 'dnsname=' + challenge.dnsZone + '&maxitems=1') return api(
.then(function(response){ 'GET',
return parseString(response.body).then(function(body){ baseUri + '/' + 'hostedzonesbyname',
return body; null,
}); 'dnsname=' + challenge.dnsZone + '&maxitems=1'
}) )
.then(function(body){ .then(function(response) {
var zoneID = body.ListHostedZonesByNameResponse.HostedZones[0].HostedZone[0].Id[0]; return parseString(response.body).then(function(body) {
var xmlns = body.ListHostedZonesByNameResponse.$.xmlns; return body;
});
})
.then(function(body) {
var zoneID =
body.ListHostedZonesByNameResponse.HostedZones[0]
.HostedZone[0].Id[0];
var xmlns = body.ListHostedZonesByNameResponse.$.xmlns;
var parameters = { var parameters = JSON.parse(JSON.stringify(XmlParameters));
'ChangeResourceRecordSetsRequest': { parameters.ChangeResourceRecordSetsRequest.$.xmlns = xmlns;
'$': { parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].Action[0] =
'xmlns': xmlns 'CREATE';
}, parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].Name[0] =
'ChangeBatch': [ challenge.dnsHost; // AWS requires FQDN
{ parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].ResourceRecords[0].ResourceRecord[0].Value[0] =
'Changes': [ '"' + challenge.dnsAuthorization + '"'; // value must be surrounded by double quotes
{
'Change': [
{
'Action': [
'CREATE'
],
'ResourceRecordSet': [
{
'Type': [
'TXT'
],
'Name': [
challenge.dnsHost // AWS requires FQDN
],
'ResourceRecords': [
{
'ResourceRecord': [
{
'Value': [
'"' + challenge.dnsAuthorization + '"' // value must be surrounded by double quotes
]
}
]
}
],
'TTL': [
300
]
}
]
}
]
}
]
}
]
}
};
// By default AWS creates record sets with simple routing policy, so duplicates are not allowed.
// A workaround is put the record sets in different weighted policies.
if(challenge.identifier.value.startsWith('foo')) {
if(challenge.wildcard) {
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].Weight = [
1
];
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].SetIdentifier = [
'wildcard'
];
} else {
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].Weight = [
1
];
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].SetIdentifier = [
'subdomain'
];
}
}
var xml = builder.buildObject(parameters);
//console.log('xml: ' + xml);
return api('POST', baseUri + zoneID + '/rrset/', xml);
});
// By default AWS creates record sets with simple routing policy, so duplicates are not allowed.
// A workaround is put the record sets in different weighted policies.
if (challenge.identifier.value.startsWith('foo')) {
setWeightedPolicy(parameters, challenge.wildcard);
}
var xml = builder.buildObject(parameters);
return api('POST', baseUri + zoneID + '/rrset/', xml);
});
}, },
get: function(data) { get: function(data) {
// console.info('List TXT', data);
var challenge = data.challenge; var challenge = data.challenge;
return api('GET', baseUri + '/' + 'hostedzonesbyname', null, 'dnsname=' + challenge.dnsZone + '&maxitems=1') return api(
.then(function(response){ 'GET',
return parseString(response.body).then(function(body){ baseUri + '/' + 'hostedzonesbyname',
return body; null,
}); 'dnsname=' + challenge.dnsZone + '&maxitems=1'
}) )
.then(function(body){ .then(function(response) {
var zoneID = body.ListHostedZonesByNameResponse.HostedZones[0].HostedZone[0].Id[0]; return parseString(response.body).then(function(body) {
// console.log('zoneID: ' + zoneID); return body;
return zoneID; });
}) })
.then(function(zoneID){ .then(function(body) {
// GET /2013-04-01/hostedzone/Id/rrset?identifier=StartRecordIdentifier&maxitems=MaxItems&name=StartRecordName&type=StartRecordType HTTP/1.1 var zoneID =
body.ListHostedZonesByNameResponse.HostedZones[0]
return api('GET', baseUri + '/' + zoneID + '/rrset', null, 'name=' + challenge.dnsPrefix + '.' + challenge.dnsZone + '.' + '&type=TXT') .HostedZone[0].Id[0];
.then(function(response){ return zoneID;
return parseString(response.body).then(function(body){ })
if (body.ListResourceRecordSetsResponse.ResourceRecordSets[0] === '') return null; .then(function(zoneID) {
return api(
var record = body.ListResourceRecordSetsResponse.ResourceRecordSets[0].ResourceRecordSet.filter(function(element){ 'GET',
return ( baseUri + '/' + zoneID + '/rrset',
element.ResourceRecords[0].ResourceRecord[0].Value[0].replace(/\"/g, '') === challenge.dnsAuthorization && null,
element.Name[0].includes(challenge.dnsPrefix) 'name=' +
); challenge.dnsPrefix +
})[0]; '.' +
challenge.dnsZone +
if (record) { '.' +
return { dnsAuthorization: record.ResourceRecords[0].ResourceRecord[0].Value[0].replace(/\"/g, '') }; '&type=TXT'
} ).then(function(response) {
return parseString(response.body).then(function(body) {
return null; if (
}); body.ListResourceRecordSetsResponse
}); .ResourceRecordSets[0] === ''
) {
}); return null;
},
remove: function(data) {
// console.info('Remove TXT', data);
var challenge = data.challenge;
return api('GET', baseUri + '/' + 'hostedzonesbyname', null, 'dnsname=' + challenge.dnsZone + '&maxitems=1')
.then(function(response){
return parseString(response.body).then(function(body){
return body;
});
})
.then(function(body){
var zoneID = body.ListHostedZonesByNameResponse.HostedZones[0].HostedZone[0].Id[0];
var xmlns = body.ListHostedZonesByNameResponse.$.xmlns;
var parameters = {
'ChangeResourceRecordSetsRequest': {
'$': {
'xmlns': xmlns
},
'ChangeBatch': [
{
'Changes': [
{
'Change': [
{
'Action': [
'DELETE'
],
'ResourceRecordSet': [
{
'Type': [
'TXT'
],
'Name': [
challenge.dnsHost // AWS requires FQDN
],
'ResourceRecords': [
{
'ResourceRecord': [
{
'Value': [
'"' + challenge.dnsAuthorization + '"' // value must be surrounded by double quotes
]
}
]
}
],
'TTL': [
300
]
}
]
}
]
}
]
}
]
}
};
if(challenge.identifier.value.startsWith('foo')) {
if(challenge.wildcard) {
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].Weight = [
1
];
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].SetIdentifier = [
'wildcard'
];
} else {
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].Weight = [
1
];
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].SetIdentifier = [
'subdomain'
];
}
} }
var xml = builder.buildObject(parameters); var record = body.ListResourceRecordSetsResponse.ResourceRecordSets[0].ResourceRecordSet.filter(
// console.log('\n', xml, '\n'); function(element) {
return (
element.ResourceRecords[0].ResourceRecord[0].Value[0].replace(
/\"/g,
''
) === challenge.dnsAuthorization &&
element.Name[0].includes(
challenge.dnsPrefix
)
);
}
)[0];
return api('POST', baseUri + zoneID + '/rrset/', xml); if (record) {
return {
dnsAuthorization: record.ResourceRecords[0].ResourceRecord[0].Value[0].replace(
/\"/g,
''
)
};
}
return null;
}); });
});
});
},
remove: function(data) {
var challenge = data.challenge;
return api(
'GET',
baseUri + '/' + 'hostedzonesbyname',
null,
'dnsname=' + challenge.dnsZone + '&maxitems=1'
)
.then(function(response) {
return parseString(response.body).then(function(body) {
return body;
});
})
.then(function(body) {
var zoneID =
body.ListHostedZonesByNameResponse.HostedZones[0]
.HostedZone[0].Id[0];
var xmlns = body.ListHostedZonesByNameResponse.$.xmlns;
var parameters = JSON.parse(JSON.stringify(XmlParameters));
parameters.ChangeResourceRecordSetsRequest.$.xmlns = xmlns;
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].Action[0] =
'DELETE';
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].Name[0] =
challenge.dnsHost; // AWS requires FQDN
parameters.ChangeResourceRecordSetsRequest.ChangeBatch[0].Changes[0].Change[0].ResourceRecordSet[0].ResourceRecords[0].ResourceRecord[0].Value[0] =
'"' + challenge.dnsAuthorization + '"'; // value must be surrounded by double quotes
// need to add weight and setidentifier to delete record set
if (challenge.identifier.value.startsWith('foo')) {
setWeightedPolicy(parameters, challenge.wildcard);
}
var xml = builder.buildObject(parameters);
return api('POST', baseUri + zoneID + '/rrset/', xml);
});
} }
};
}
}; };

View File

@ -19,7 +19,10 @@
"acme", "acme",
"greenlock" "greenlock"
], ],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "Hitesh Walia <hiteshwar.walia@gmail.com",
"contributors": [
"AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"dotenv": "^8.1.0" "dotenv": "^8.1.0"