• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const assert = require('assert');
4const os = require('os');
5
6const types = {
7  A: 1,
8  AAAA: 28,
9  NS: 2,
10  CNAME: 5,
11  SOA: 6,
12  PTR: 12,
13  MX: 15,
14  TXT: 16,
15  ANY: 255,
16  CAA: 257
17};
18
19const classes = {
20  IN: 1
21};
22
23// Naïve DNS parser/serializer.
24
25function readDomainFromPacket(buffer, offset) {
26  assert.ok(offset < buffer.length);
27  const length = buffer[offset];
28  if (length === 0) {
29    return { nread: 1, domain: '' };
30  } else if ((length & 0xC0) === 0) {
31    offset += 1;
32    const chunk = buffer.toString('ascii', offset, offset + length);
33    // Read the rest of the domain.
34    const { nread, domain } = readDomainFromPacket(buffer, offset + length);
35    return {
36      nread: 1 + length + nread,
37      domain: domain ? `${chunk}.${domain}` : chunk
38    };
39  }
40  // Pointer to another part of the packet.
41  assert.strictEqual(length & 0xC0, 0xC0);
42  // eslint-disable-next-line space-infix-ops, space-unary-ops
43  const pointeeOffset = buffer.readUInt16BE(offset) &~ 0xC000;
44  return {
45    nread: 2,
46    domain: readDomainFromPacket(buffer, pointeeOffset)
47  };
48}
49
50function parseDNSPacket(buffer) {
51  assert.ok(buffer.length > 12);
52
53  const parsed = {
54    id: buffer.readUInt16BE(0),
55    flags: buffer.readUInt16BE(2),
56  };
57
58  const counts = [
59    ['questions', buffer.readUInt16BE(4)],
60    ['answers', buffer.readUInt16BE(6)],
61    ['authorityAnswers', buffer.readUInt16BE(8)],
62    ['additionalRecords', buffer.readUInt16BE(10)],
63  ];
64
65  let offset = 12;
66  for (const [ sectionName, count ] of counts) {
67    parsed[sectionName] = [];
68    for (let i = 0; i < count; ++i) {
69      const { nread, domain } = readDomainFromPacket(buffer, offset);
70      offset += nread;
71
72      const type = buffer.readUInt16BE(offset);
73
74      const rr = {
75        domain,
76        cls: buffer.readUInt16BE(offset + 2),
77      };
78      offset += 4;
79
80      for (const name in types) {
81        if (types[name] === type)
82          rr.type = name;
83      }
84
85      if (sectionName !== 'questions') {
86        rr.ttl = buffer.readInt32BE(offset);
87        const dataLength = buffer.readUInt16BE(offset);
88        offset += 6;
89
90        switch (type) {
91          case types.A:
92            assert.strictEqual(dataLength, 4);
93            rr.address = `${buffer[offset + 0]}.${buffer[offset + 1]}.` +
94                         `${buffer[offset + 2]}.${buffer[offset + 3]}`;
95            break;
96          case types.AAAA:
97            assert.strictEqual(dataLength, 16);
98            rr.address = buffer.toString('hex', offset, offset + 16)
99                               .replace(/(.{4}(?!$))/g, '$1:');
100            break;
101          case types.TXT:
102          {
103            let position = offset;
104            rr.entries = [];
105            while (position < offset + dataLength) {
106              const txtLength = buffer[offset];
107              rr.entries.push(buffer.toString('utf8',
108                                              position + 1,
109                                              position + 1 + txtLength));
110              position += 1 + txtLength;
111            }
112            assert.strictEqual(position, offset + dataLength);
113            break;
114          }
115          case types.MX:
116          {
117            rr.priority = buffer.readInt16BE(buffer, offset);
118            offset += 2;
119            const { nread, domain } = readDomainFromPacket(buffer, offset);
120            rr.exchange = domain;
121            assert.strictEqual(nread, dataLength);
122            break;
123          }
124          case types.NS:
125          case types.CNAME:
126          case types.PTR:
127          {
128            const { nread, domain } = readDomainFromPacket(buffer, offset);
129            rr.value = domain;
130            assert.strictEqual(nread, dataLength);
131            break;
132          }
133          case types.SOA:
134          {
135            const mname = readDomainFromPacket(buffer, offset);
136            const rname = readDomainFromPacket(buffer, offset + mname.nread);
137            rr.nsname = mname.domain;
138            rr.hostmaster = rname.domain;
139            const trailerOffset = offset + mname.nread + rname.nread;
140            rr.serial = buffer.readUInt32BE(trailerOffset);
141            rr.refresh = buffer.readUInt32BE(trailerOffset + 4);
142            rr.retry = buffer.readUInt32BE(trailerOffset + 8);
143            rr.expire = buffer.readUInt32BE(trailerOffset + 12);
144            rr.minttl = buffer.readUInt32BE(trailerOffset + 16);
145
146            assert.strictEqual(trailerOffset + 20, dataLength);
147            break;
148          }
149          default:
150            throw new Error(`Unknown RR type ${rr.type}`);
151        }
152        offset += dataLength;
153      }
154
155      parsed[sectionName].push(rr);
156
157      assert.ok(offset <= buffer.length);
158    }
159  }
160
161  assert.strictEqual(offset, buffer.length);
162  return parsed;
163}
164
165function writeIPv6(ip) {
166  const parts = ip.replace(/^:|:$/g, '').split(':');
167  const buf = Buffer.alloc(16);
168
169  let offset = 0;
170  for (const part of parts) {
171    if (part === '') {
172      offset += 16 - 2 * (parts.length - 1);
173    } else {
174      buf.writeUInt16BE(parseInt(part, 16), offset);
175      offset += 2;
176    }
177  }
178
179  return buf;
180}
181
182function writeDomainName(domain) {
183  return Buffer.concat(domain.split('.').map((label) => {
184    assert(label.length < 64);
185    return Buffer.concat([
186      Buffer.from([label.length]),
187      Buffer.from(label, 'ascii'),
188    ]);
189  }).concat([Buffer.alloc(1)]));
190}
191
192function writeDNSPacket(parsed) {
193  const buffers = [];
194  const kStandardResponseFlags = 0x8180;
195
196  buffers.push(new Uint16Array([
197    parsed.id,
198    parsed.flags === undefined ? kStandardResponseFlags : parsed.flags,
199    parsed.questions && parsed.questions.length,
200    parsed.answers && parsed.answers.length,
201    parsed.authorityAnswers && parsed.authorityAnswers.length,
202    parsed.additionalRecords && parsed.additionalRecords.length,
203  ]));
204
205  for (const q of parsed.questions) {
206    assert(types[q.type]);
207    buffers.push(writeDomainName(q.domain));
208    buffers.push(new Uint16Array([
209      types[q.type],
210      q.cls === undefined ? classes.IN : q.cls,
211    ]));
212  }
213
214  for (const rr of [].concat(parsed.answers,
215                             parsed.authorityAnswers,
216                             parsed.additionalRecords)) {
217    if (!rr) continue;
218
219    assert(types[rr.type]);
220    buffers.push(writeDomainName(rr.domain));
221    buffers.push(new Uint16Array([
222      types[rr.type],
223      rr.cls === undefined ? classes.IN : rr.cls,
224    ]));
225    buffers.push(new Int32Array([rr.ttl]));
226
227    const rdLengthBuf = new Uint16Array(1);
228    buffers.push(rdLengthBuf);
229
230    switch (rr.type) {
231      case 'A':
232        rdLengthBuf[0] = 4;
233        buffers.push(new Uint8Array(rr.address.split('.')));
234        break;
235      case 'AAAA':
236        rdLengthBuf[0] = 16;
237        buffers.push(writeIPv6(rr.address));
238        break;
239      case 'TXT':
240        const total = rr.entries.map((s) => s.length).reduce((a, b) => a + b);
241        // Total length of all strings + 1 byte each for their lengths.
242        rdLengthBuf[0] = rr.entries.length + total;
243        for (const txt of rr.entries) {
244          buffers.push(new Uint8Array([Buffer.byteLength(txt)]));
245          buffers.push(Buffer.from(txt));
246        }
247        break;
248      case 'MX':
249        rdLengthBuf[0] = 2;
250        buffers.push(new Uint16Array([rr.priority]));
251        // fall through
252      case 'NS':
253      case 'CNAME':
254      case 'PTR':
255      {
256        const domain = writeDomainName(rr.exchange || rr.value);
257        rdLengthBuf[0] += domain.length;
258        buffers.push(domain);
259        break;
260      }
261      case 'SOA':
262      {
263        const mname = writeDomainName(rr.nsname);
264        const rname = writeDomainName(rr.hostmaster);
265        rdLengthBuf[0] = mname.length + rname.length + 20;
266        buffers.push(mname, rname);
267        buffers.push(new Uint32Array([
268          rr.serial, rr.refresh, rr.retry, rr.expire, rr.minttl,
269        ]));
270        break;
271      }
272      case 'CAA':
273      {
274        rdLengthBuf[0] = 5 + rr.issue.length + 2;
275        buffers.push(Buffer.from([Number(rr.critical)]));
276        buffers.push(Buffer.from([Number(5)]));
277        buffers.push(Buffer.from('issue' + rr.issue));
278        break;
279      }
280      default:
281        throw new Error(`Unknown RR type ${rr.type}`);
282    }
283  }
284
285  return Buffer.concat(buffers.map((typedArray) => {
286    const buf = Buffer.from(typedArray.buffer,
287                            typedArray.byteOffset,
288                            typedArray.byteLength);
289    if (os.endianness() === 'LE') {
290      if (typedArray.BYTES_PER_ELEMENT === 2) buf.swap16();
291      if (typedArray.BYTES_PER_ELEMENT === 4) buf.swap32();
292    }
293    return buf;
294  }));
295}
296
297const mockedErrorCode = 'ENOTFOUND';
298const mockedSysCall = 'getaddrinfo';
299
300function errorLookupMock(code = mockedErrorCode, syscall = mockedSysCall) {
301  return function lookupWithError(hostname, dnsopts, cb) {
302    const err = new Error(`${syscall} ${code} ${hostname}`);
303    err.code = code;
304    err.errno = code;
305    err.syscall = syscall;
306    err.hostname = hostname;
307    cb(err);
308  };
309}
310
311module.exports = {
312  types,
313  classes,
314  writeDNSPacket,
315  parseDNSPacket,
316  errorLookupMock,
317  mockedErrorCode,
318  mockedSysCall
319};
320