| 'use strict'; |
| |
| 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, |
| CAA: 257 |
| }; |
| |
| const classes = { |
| IN: 1 |
| }; |
| |
| // Naïve DNS parser/serializer. |
| |
| 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 |
| }; |
| } |
| // Pointer to another part of the packet. |
| assert.strictEqual(length & 0xC0, 0xC0); |
| // eslint-disable-next-line space-infix-ops, space-unary-ops |
| 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; |
| } |
| case 'CAA': |
| { |
| rdLengthBuf[0] = 5 + rr.issue.length + 2; |
| buffers.push(Buffer.from([Number(rr.critical)])); |
| buffers.push(Buffer.from([Number(5)])); |
| buffers.push(Buffer.from('issue' + rr.issue)); |
| 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; |
| })); |
| } |
| |
| const mockedErrorCode = 'ENOTFOUND'; |
| const mockedSysCall = 'getaddrinfo'; |
| |
| function errorLookupMock(code = mockedErrorCode, syscall = mockedSysCall) { |
| return function lookupWithError(hostname, dnsopts, cb) { |
| const err = new Error(`${syscall} ${code} ${hostname}`); |
| err.code = code; |
| err.errno = code; |
| err.syscall = syscall; |
| err.hostname = hostname; |
| cb(err); |
| }; |
| } |
| |
| module.exports = { |
| types, |
| classes, |
| writeDNSPacket, |
| parseDNSPacket, |
| errorLookupMock, |
| mockedErrorCode, |
| mockedSysCall |
| }; |