test: add non-internet resolveAny tests
This is a bit of a check to see how people feel about having this kind of test. Ref: https://github.com/nodejs/node/pull/13137 PR-URL: https://github.com/nodejs/node/pull/13883 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Refael Ackermann <refack@gmail.com>
This commit is contained in:
parent
e76c49de33
commit
69f653dff9
290
test/common/dns.js
Normal file
290
test/common/dns.js
Normal file
@ -0,0 +1,290 @@
|
||||
/* eslint-disable required-modules */
|
||||
'use strict';
|
||||
|
||||
// Naïve DNS parser/serializer.
|
||||
|
||||
const assert = require('assert');
|
||||
const os = require('os');
|
||||
|
||||
const types = {
|
||||
A: 1,
|
||||
AAAA: 28,
|
||||
NS: 2,
|
||||
CNAME: 5,
|
||||
SOA: 6,
|
||||
PTR: 12,
|
||||
MX: 15,
|
||||
TXT: 16,
|
||||
ANY: 255
|
||||
};
|
||||
|
||||
const classes = {
|
||||
IN: 1
|
||||
};
|
||||
|
||||
function readDomainFromPacket(buffer, offset) {
|
||||
assert.ok(offset < buffer.length);
|
||||
const length = buffer[offset];
|
||||
if (length === 0) {
|
||||
return { nread: 1, domain: '' };
|
||||
} else if ((length & 0xC0) === 0) {
|
||||
offset += 1;
|
||||
const chunk = buffer.toString('ascii', offset, offset + length);
|
||||
// Read the rest of the domain.
|
||||
const { nread, domain } = readDomainFromPacket(buffer, offset + length);
|
||||
return {
|
||||
nread: 1 + length + nread,
|
||||
domain: domain ? `${chunk}.${domain}` : chunk
|
||||
};
|
||||
} else {
|
||||
// Pointer to another part of the packet.
|
||||
assert.strictEqual(length & 0xC0, 0xC0);
|
||||
// eslint-disable-next-line
|
||||
const pointeeOffset = buffer.readUInt16BE(offset) &~ 0xC000;
|
||||
return {
|
||||
nread: 2,
|
||||
domain: readDomainFromPacket(buffer, pointeeOffset)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseDNSPacket(buffer) {
|
||||
assert.ok(buffer.length > 12);
|
||||
|
||||
const parsed = {
|
||||
id: buffer.readUInt16BE(0),
|
||||
flags: buffer.readUInt16BE(2),
|
||||
};
|
||||
|
||||
const counts = [
|
||||
['questions', buffer.readUInt16BE(4)],
|
||||
['answers', buffer.readUInt16BE(6)],
|
||||
['authorityAnswers', buffer.readUInt16BE(8)],
|
||||
['additionalRecords', buffer.readUInt16BE(10)]
|
||||
];
|
||||
|
||||
let offset = 12;
|
||||
for (const [ sectionName, count ] of counts) {
|
||||
parsed[sectionName] = [];
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const { nread, domain } = readDomainFromPacket(buffer, offset);
|
||||
offset += nread;
|
||||
|
||||
const type = buffer.readUInt16BE(offset);
|
||||
|
||||
const rr = {
|
||||
domain,
|
||||
cls: buffer.readUInt16BE(offset + 2),
|
||||
};
|
||||
offset += 4;
|
||||
|
||||
for (const name in types) {
|
||||
if (types[name] === type)
|
||||
rr.type = name;
|
||||
}
|
||||
|
||||
if (sectionName !== 'questions') {
|
||||
rr.ttl = buffer.readInt32BE(offset);
|
||||
const dataLength = buffer.readUInt16BE(offset);
|
||||
offset += 6;
|
||||
|
||||
switch (type) {
|
||||
case types.A:
|
||||
assert.strictEqual(dataLength, 4);
|
||||
rr.address = `${buffer[offset + 0]}.${buffer[offset + 1]}.` +
|
||||
`${buffer[offset + 2]}.${buffer[offset + 3]}`;
|
||||
break;
|
||||
case types.AAAA:
|
||||
assert.strictEqual(dataLength, 16);
|
||||
rr.address = buffer.toString('hex', offset, offset + 16)
|
||||
.replace(/(.{4}(?!$))/g, '$1:');
|
||||
break;
|
||||
case types.TXT:
|
||||
{
|
||||
let position = offset;
|
||||
rr.entries = [];
|
||||
while (position < offset + dataLength) {
|
||||
const txtLength = buffer[offset];
|
||||
rr.entries.push(buffer.toString('utf8',
|
||||
position + 1,
|
||||
position + 1 + txtLength));
|
||||
position += 1 + txtLength;
|
||||
}
|
||||
assert.strictEqual(position, offset + dataLength);
|
||||
break;
|
||||
}
|
||||
case types.MX:
|
||||
{
|
||||
rr.priority = buffer.readInt16BE(buffer, offset);
|
||||
offset += 2;
|
||||
const { nread, domain } = readDomainFromPacket(buffer, offset);
|
||||
rr.exchange = domain;
|
||||
assert.strictEqual(nread, dataLength);
|
||||
break;
|
||||
}
|
||||
case types.NS:
|
||||
case types.CNAME:
|
||||
case types.PTR:
|
||||
{
|
||||
const { nread, domain } = readDomainFromPacket(buffer, offset);
|
||||
rr.value = domain;
|
||||
assert.strictEqual(nread, dataLength);
|
||||
break;
|
||||
}
|
||||
case types.SOA:
|
||||
{
|
||||
const mname = readDomainFromPacket(buffer, offset);
|
||||
const rname = readDomainFromPacket(buffer, offset + mname.nread);
|
||||
rr.nsname = mname.domain;
|
||||
rr.hostmaster = rname.domain;
|
||||
const trailerOffset = offset + mname.nread + rname.nread;
|
||||
rr.serial = buffer.readUInt32BE(trailerOffset);
|
||||
rr.refresh = buffer.readUInt32BE(trailerOffset + 4);
|
||||
rr.retry = buffer.readUInt32BE(trailerOffset + 8);
|
||||
rr.expire = buffer.readUInt32BE(trailerOffset + 12);
|
||||
rr.minttl = buffer.readUInt32BE(trailerOffset + 16);
|
||||
|
||||
assert.strictEqual(trailerOffset + 20, dataLength);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown RR type ${rr.type}`);
|
||||
}
|
||||
offset += dataLength;
|
||||
}
|
||||
|
||||
parsed[sectionName].push(rr);
|
||||
|
||||
assert.ok(offset <= buffer.length);
|
||||
}
|
||||
}
|
||||
|
||||
assert.strictEqual(offset, buffer.length);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function writeIPv6(ip) {
|
||||
const parts = ip.replace(/^:|:$/g, '').split(':');
|
||||
const buf = Buffer.alloc(16);
|
||||
|
||||
let offset = 0;
|
||||
for (const part of parts) {
|
||||
if (part === '') {
|
||||
offset += 16 - 2 * (parts.length - 1);
|
||||
} else {
|
||||
buf.writeUInt16BE(parseInt(part, 16), offset);
|
||||
offset += 2;
|
||||
}
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
function writeDomainName(domain) {
|
||||
return Buffer.concat(domain.split('.').map((label) => {
|
||||
assert(label.length < 64);
|
||||
return Buffer.concat([
|
||||
Buffer.from([label.length]),
|
||||
Buffer.from(label, 'ascii')
|
||||
]);
|
||||
}).concat([Buffer.alloc(1)]));
|
||||
}
|
||||
|
||||
function writeDNSPacket(parsed) {
|
||||
const buffers = [];
|
||||
const kStandardResponseFlags = 0x8180;
|
||||
|
||||
buffers.push(new Uint16Array([
|
||||
parsed.id,
|
||||
parsed.flags === undefined ? kStandardResponseFlags : parsed.flags,
|
||||
parsed.questions && parsed.questions.length,
|
||||
parsed.answers && parsed.answers.length,
|
||||
parsed.authorityAnswers && parsed.authorityAnswers.length,
|
||||
parsed.additionalRecords && parsed.additionalRecords.length,
|
||||
]));
|
||||
|
||||
for (const q of parsed.questions) {
|
||||
assert(types[q.type]);
|
||||
buffers.push(writeDomainName(q.domain));
|
||||
buffers.push(new Uint16Array([
|
||||
types[q.type],
|
||||
q.cls === undefined ? classes.IN : q.cls
|
||||
]));
|
||||
}
|
||||
|
||||
for (const rr of [].concat(parsed.answers,
|
||||
parsed.authorityAnswers,
|
||||
parsed.additionalRecords)) {
|
||||
if (!rr) continue;
|
||||
|
||||
assert(types[rr.type]);
|
||||
buffers.push(writeDomainName(rr.domain));
|
||||
buffers.push(new Uint16Array([
|
||||
types[rr.type],
|
||||
rr.cls === undefined ? classes.IN : rr.cls
|
||||
]));
|
||||
buffers.push(new Int32Array([rr.ttl]));
|
||||
|
||||
const rdLengthBuf = new Uint16Array(1);
|
||||
buffers.push(rdLengthBuf);
|
||||
|
||||
switch (rr.type) {
|
||||
case 'A':
|
||||
rdLengthBuf[0] = 4;
|
||||
buffers.push(new Uint8Array(rr.address.split('.')));
|
||||
break;
|
||||
case 'AAAA':
|
||||
rdLengthBuf[0] = 16;
|
||||
buffers.push(writeIPv6(rr.address));
|
||||
break;
|
||||
case 'TXT':
|
||||
const total = rr.entries.map((s) => s.length).reduce((a, b) => a + b);
|
||||
// Total length of all strings + 1 byte each for their lengths.
|
||||
rdLengthBuf[0] = rr.entries.length + total;
|
||||
for (const txt of rr.entries) {
|
||||
buffers.push(new Uint8Array([Buffer.byteLength(txt)]));
|
||||
buffers.push(Buffer.from(txt));
|
||||
}
|
||||
break;
|
||||
case 'MX':
|
||||
rdLengthBuf[0] = 2;
|
||||
buffers.push(new Uint16Array([rr.priority]));
|
||||
// fall through
|
||||
case 'NS':
|
||||
case 'CNAME':
|
||||
case 'PTR':
|
||||
{
|
||||
const domain = writeDomainName(rr.exchange || rr.value);
|
||||
rdLengthBuf[0] += domain.length;
|
||||
buffers.push(domain);
|
||||
break;
|
||||
}
|
||||
case 'SOA':
|
||||
{
|
||||
const mname = writeDomainName(rr.nsname);
|
||||
const rname = writeDomainName(rr.hostmaster);
|
||||
rdLengthBuf[0] = mname.length + rname.length + 20;
|
||||
buffers.push(mname, rname);
|
||||
buffers.push(new Uint32Array([
|
||||
rr.serial, rr.refresh, rr.retry, rr.expire, rr.minttl
|
||||
]));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown RR type ${rr.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
return Buffer.concat(buffers.map((typedArray) => {
|
||||
const buf = Buffer.from(typedArray.buffer,
|
||||
typedArray.byteOffset,
|
||||
typedArray.byteLength);
|
||||
if (os.endianness() === 'LE') {
|
||||
if (typedArray.BYTES_PER_ELEMENT === 2) buf.swap16();
|
||||
if (typedArray.BYTES_PER_ELEMENT === 4) buf.swap32();
|
||||
}
|
||||
return buf;
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = { types, classes, writeDNSPacket, parseDNSPacket };
|
35
test/parallel/test-dns-resolveany-bad-ancount.js
Normal file
35
test/parallel/test-dns-resolveany-bad-ancount.js
Normal file
@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const dnstools = require('../common/dns');
|
||||
const dns = require('dns');
|
||||
const assert = require('assert');
|
||||
const dgram = require('dgram');
|
||||
|
||||
const server = dgram.createSocket('udp4');
|
||||
|
||||
server.on('message', common.mustCall((msg, { address, port }) => {
|
||||
const parsed = dnstools.parseDNSPacket(msg);
|
||||
const domain = parsed.questions[0].domain;
|
||||
assert.strictEqual(domain, 'example.org');
|
||||
|
||||
const buf = dnstools.writeDNSPacket({
|
||||
id: parsed.id,
|
||||
questions: parsed.questions,
|
||||
answers: { type: 'A', address: '1.2.3.4', ttl: 123, domain },
|
||||
});
|
||||
// Overwrite the # of answers with 2, which is incorrect.
|
||||
buf.writeUInt16LE(2, 6);
|
||||
server.send(buf, port, address);
|
||||
}));
|
||||
|
||||
server.bind(0, common.mustCall(() => {
|
||||
const address = server.address();
|
||||
dns.setServers([`127.0.0.1:${address.port}`]);
|
||||
|
||||
dns.resolveAny('example.org', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'EBADRESP');
|
||||
assert.strictEqual(err.syscall, 'queryAny');
|
||||
assert.strictEqual(err.hostname, 'example.org');
|
||||
server.close();
|
||||
}));
|
||||
}));
|
53
test/parallel/test-dns-resolveany.js
Normal file
53
test/parallel/test-dns-resolveany.js
Normal file
@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const dnstools = require('../common/dns');
|
||||
const dns = require('dns');
|
||||
const assert = require('assert');
|
||||
const dgram = require('dgram');
|
||||
|
||||
const answers = [
|
||||
{ type: 'A', address: '1.2.3.4', ttl: 123 },
|
||||
{ type: 'AAAA', address: '::42', ttl: 123 },
|
||||
{ type: 'MX', priority: 42, exchange: 'foobar.com', ttl: 124 },
|
||||
{ type: 'NS', value: 'foobar.org', ttl: 457 },
|
||||
{ type: 'TXT', entries: [ 'v=spf1 ~all', 'xyz' ] },
|
||||
{ type: 'PTR', value: 'baz.org', ttl: 987 },
|
||||
{
|
||||
type: 'SOA',
|
||||
nsname: 'ns1.example.com',
|
||||
hostmaster: 'admin.example.com',
|
||||
serial: 156696742,
|
||||
refresh: 900,
|
||||
retry: 900,
|
||||
expire: 1800,
|
||||
minttl: 60
|
||||
},
|
||||
];
|
||||
|
||||
const server = dgram.createSocket('udp4');
|
||||
|
||||
server.on('message', common.mustCall((msg, { address, port }) => {
|
||||
const parsed = dnstools.parseDNSPacket(msg);
|
||||
const domain = parsed.questions[0].domain;
|
||||
assert.strictEqual(domain, 'example.org');
|
||||
|
||||
server.send(dnstools.writeDNSPacket({
|
||||
id: parsed.id,
|
||||
questions: parsed.questions,
|
||||
answers: answers.map((answer) => Object.assign({ domain }, answer)),
|
||||
}), port, address);
|
||||
}));
|
||||
|
||||
server.bind(0, common.mustCall(() => {
|
||||
const address = server.address();
|
||||
dns.setServers([`127.0.0.1:${address.port}`]);
|
||||
|
||||
dns.resolveAny('example.org', common.mustCall((err, res) => {
|
||||
assert.ifError(err);
|
||||
// Compare copies with ttl removed, c-ares fiddles with that value.
|
||||
assert.deepStrictEqual(
|
||||
res.map((r) => Object.assign({}, r, { ttl: null })),
|
||||
answers.map((r) => Object.assign({}, r, { ttl: null })));
|
||||
server.close();
|
||||
}));
|
||||
}));
|
Loading…
x
Reference in New Issue
Block a user