Skip to content

Commit

Permalink
Added support for resolving DNS CAA records
Browse files Browse the repository at this point in the history
This adds support for DNS Certification Authority
Authorization (RFC 6844) to nodejs.

This closes nodejs#19239 and possibly affects nodejs#14713.
  • Loading branch information
lxdicted committed Oct 2, 2020
1 parent b5de9b4 commit 08d2e88
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 0 deletions.
17 changes: 17 additions & 0 deletions doc/api/dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ The following methods from the `dns` module are available:
* [`resolver.resolve4()`][`dns.resolve4()`]
* [`resolver.resolve6()`][`dns.resolve6()`]
* [`resolver.resolveAny()`][`dns.resolveAny()`]
* [`resolver.resolveCaa()`][`dns.resolveCaa()`]
* [`resolver.resolveCname()`][`dns.resolveCname()`]
* [`resolver.resolveMx()`][`dns.resolveMx()`]
* [`resolver.resolveNaptr()`][`dns.resolveNaptr()`]
Expand Down Expand Up @@ -290,6 +291,7 @@ records. The type and structure of individual results varies based on `rrtype`:
| `'AAAA'` | IPv6 addresses | {string} | [`dns.resolve6()`][] |
| `'ANY'` | any records | {Object} | [`dns.resolveAny()`][] |
| `'CNAME'` | canonical name records | {string} | [`dns.resolveCname()`][] |
| `'CAA'` | CA authorization | {Object} | [`dns.resolveCaa()`][] |
| `'MX'` | mail exchange records | {Object} | [`dns.resolveMx()`][] |
| `'NAPTR'` | name authority pointer records | {Object} | [`dns.resolveNaptr()`][] |
| `'NS'` | name server records | {string} | [`dns.resolveNs()`][] |
Expand Down Expand Up @@ -414,6 +416,21 @@ Uses the DNS protocol to resolve `CNAME` records for the `hostname`. The
will contain an array of canonical name records available for the `hostname`
(e.g. `['bar.example.com']`).

## `dns.resolveCaa(hostname, callback)`
<!-- YAML
added: v0.3.2
-->

* `hostname` {string}
* `callback` {Function}
* `err` {Error}
* `records` {object[]}

Uses the DNS protocol to resolve `CAA` records for the `hostname`. The
`addresses` argument passed to the `callback` function
will contain an array of certification authority authorization records
available for the `hostname` (e.g. `[{critial: 0, iodef: 'letsencrypt.org']`).

## `dns.resolveMx(hostname, callback)`
<!-- YAML
added: v0.1.27
Expand Down
1 change: 1 addition & 0 deletions lib/dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ const resolveMap = ObjectCreate(null);
Resolver.prototype.resolveAny = resolveMap.ANY = resolver('queryAny');
Resolver.prototype.resolve4 = resolveMap.A = resolver('queryA');
Resolver.prototype.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
Resolver.prototype.resolveCaa = resolveMap.CAA = resolver('queryCaa');
Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname');
Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx');
Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs');
Expand Down
1 change: 1 addition & 0 deletions lib/internal/dns/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ Resolver.prototype.setServers = CallbackResolver.prototype.setServers;
Resolver.prototype.resolveAny = resolveMap.ANY = resolver('queryAny');
Resolver.prototype.resolve4 = resolveMap.A = resolver('queryA');
Resolver.prototype.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
Resolver.prototype.resolveCaa = resolveMap.CAA = resolver('queryCaa');
Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname');
Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx');
Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs');
Expand Down
1 change: 1 addition & 0 deletions lib/internal/dns/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const resolverKeys = [
'resolve4',
'resolve6',
'resolveAny',
'resolveCaa',
'resolveCname',
'resolveMx',
'resolveNaptr',
Expand Down
68 changes: 68 additions & 0 deletions src/cares_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,43 @@ int ParseMxReply(Environment* env,
return ARES_SUCCESS;
}

int ParseCaaReply(Environment* env,
const unsigned char* buf,
int len,
Local<Array> ret,
bool need_type = false) {
HandleScope handle_scope(env->isolate());
auto context = env->context();

struct ares_caa_reply* caa_start;
int status = ares_parse_caa_reply(buf, len, &caa_start);
if (status != ARES_SUCCESS) {
return status;
}

uint32_t offset = ret->Length();
ares_caa_reply* current = caa_start;
for (uint32_t i = 0; current != nullptr; ++i, current = current->next) {
Local<Object> caa_record = Object::New(env->isolate());

caa_record->Set(context,
env->dns_critical_string(),
Integer::New(env->isolate(), current->critical)).Check();
caa_record->Set(context,
OneByteString(env->isolate(), current->property),
OneByteString(env->isolate(), current->value)).Check();
if (need_type)
caa_record->Set(context,
env->type_string(),
env->dns_caa_string()).Check();

ret->Set(context, i + offset, caa_record).Check();
}

ares_free_data(caa_start);
return ARES_SUCCESS;
}

int ParseTxtReply(Environment* env,
const unsigned char* buf,
int len,
Expand Down Expand Up @@ -1445,6 +1482,36 @@ class QueryAaaaWrap: public QueryWrap {
}
};

class QueryCaaWrap: public QueryWrap {
public:
QueryCaaWrap(ChannelWrap* channel, Local<Object> req_wrap_obj)
: QueryWrap(channel, req_wrap_obj, "resolve6") {
}

int Send(const char* name) override {
AresQuery(name, ns_c_in, ns_t_caa);
return 0;
}

SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(QueryAaaaWrap)
SET_SELF_SIZE(QueryAaaaWrap)

protected:
void Parse(unsigned char* buf, int len) override {
HandleScope handle_scope(env()->isolate());
Context::Scope context_scope(env()->context());

Local<Array> ret = Array::New(env()->isolate());
int status = ParseCaaReply(env(), buf, len, ret);
if (status != ARES_SUCCESS) {
ParseError(status);
return;
}

this->CallOnComplete(ret);
}
};

class QueryCnameWrap: public QueryWrap {
public:
Expand Down Expand Up @@ -2242,6 +2309,7 @@ void Initialize(Local<Object> target,
env->SetProtoMethod(channel_wrap, "queryAny", Query<QueryAnyWrap>);
env->SetProtoMethod(channel_wrap, "queryA", Query<QueryAWrap>);
env->SetProtoMethod(channel_wrap, "queryAaaa", Query<QueryAaaaWrap>);
env->SetProtoMethod(channel_wrap, "queryCaa", Query<QueryCaaWrap>);
env->SetProtoMethod(channel_wrap, "queryCname", Query<QueryCnameWrap>);
env->SetProtoMethod(channel_wrap, "queryMx", Query<QueryMxWrap>);
env->SetProtoMethod(channel_wrap, "queryNs", Query<QueryNsWrap>);
Expand Down
2 changes: 2 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ constexpr size_t kFsStatsBufferLength =
V(dh_string, "DH") \
V(dns_a_string, "A") \
V(dns_aaaa_string, "AAAA") \
V(dns_caa_string, "CAA") \
V(dns_critical_string, "critical") \
V(dns_cname_string, "CNAME") \
V(dns_mx_string, "MX") \
V(dns_naptr_string, "NAPTR") \
Expand Down
2 changes: 2 additions & 0 deletions test/common/internet.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const addresses = {
NAPTR_HOST: 'sip2sip.info',
// A host with SOA records registered
SOA_HOST: 'nodejs.org',
// A host with CAA record registred
CAA_HOST: 'google.com',
// A host with CNAME records registered
CNAME_HOST: 'blog.nodejs.org',
// A host with NS records registered
Expand Down
41 changes: 41 additions & 0 deletions test/internet/test-dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,47 @@ TEST(function test_resolveSoa_failure(done) {
checkWrap(req);
});

TEST(async function test_resolveCaa(done) {
function validateResult(result) {
assert.ok(Array.isArray(result[0]));
assert.strictEqual(result.length, 1);
assert.strictEqual(typeof result[0].critical, 'number');
assert.strictEqual(result[0].critical, 0);
assert.strictEqual(result[0].issue, 'pki.goog');
}

validateResult(await dnsPromises.resolveCaa(addresses.CAA_HOST));

const req = dns.resolveCaa(addresses.CAA_HOST, function(err, records) {
assert.ifError(err);
validateResult(records);
done();
});

checkWrap(req);
});

TEST(function test_resolveCaa_failure(done) {
dnsPromises.resolveTxt(addresses.INVALID_HOST)
.then(common.mustNotCall())
.catch(common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOTFOUND');
}));

const req = dns.resolveCaa(addresses.INVALID_HOST, function(err, result) {
assert.ok(err instanceof Error);
assert.strictEqual(err.code, 'ENOTFOUND');

assert.strictEqual(result, undefined);

done();
});

checkWrap(req);
});



TEST(async function test_resolveCname(done) {
function validateResult(result) {
assert.ok(result.length > 0);
Expand Down
1 change: 1 addition & 0 deletions test/internet/test-trace-events-dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const tests = {
'resolveAny': 'dns.resolveAny("example.com", (err, res) => {});',
'resolve4': 'dns.resolve4("example.com", (err, res) => {});',
'resolve6': 'dns.resolve6("example.com", (err, res) => {});',
'resolveCaa': 'dns.resolveCaa("example.com", (err, res) => {});',
'resolveCname': 'dns.resolveCname("example.com", (err, res) => {});',
'resolveMx': 'dns.resolveMx("example.com", (err, res) => {});',
'resolveNs': 'dns.resolveNs("example.com", (err, res) => {});',
Expand Down

0 comments on commit 08d2e88

Please sign in to comment.