mirror of https://github.com/lukechilds/node.git
Browse Source
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>v6
Anna Henningsen
8 years ago
3 changed files with 378 additions and 0 deletions
@ -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 }; |
@ -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(); |
|||
})); |
|||
})); |
@ -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…
Reference in new issue