// Copyright 2011 Timothy J Fontaine // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the 'Software'), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE // TODO: change the default UDP packet size that node-dns sends // from 4096 to conform to these: // - [requestor's payload size](https://tools.ietf.org/html/rfc6891#section-6.2.3) // - [responders's payload size](https://tools.ietf.org/html/rfc6891#section-6.2.4) 'use strict'; var consts = require('./consts'), BufferCursor = require('buffercursor'), BufferCursorOverflow = BufferCursor.BufferCursorOverflow, ipaddr = require('ipaddr.js'), assert = require('assert'), util = require('util'); function assertUndefined(val, msg) { assert(typeof val != 'undefined', msg); } var Packet = module.exports = function() { this.header = { id: 0, qr: 0, opcode: 0, aa: 0, tc: 0, rd: 1, ra: 0, res1: 0, res2: 0, res3: 0, rcode: 0 }; this.question = []; this.answer = []; this.authority = []; this.additional = []; this.edns_options = []; // TODO: DEPRECATED! Use `.edns.options` instead! this.payload = undefined; // TODO: DEPRECATED! Use `.edns.payload` instead! }; var LABEL_POINTER = 0xC0; var isPointer = function(len) { return (len & LABEL_POINTER) === LABEL_POINTER; }; function nameUnpack(buff) { var len, comp, end, pos, part, combine = ''; len = buff.readUInt8(); comp = false; end = buff.tell(); while (len !== 0) { if (isPointer(len)) { len -= LABEL_POINTER; len = len << 8; pos = len + buff.readUInt8(); if (!comp) end = buff.tell(); buff.seek(pos); len = buff.readUInt8(); comp = true; continue; } part = buff.toString('ascii', len); if (combine.length) combine = combine + '.' + part; else combine = part; len = buff.readUInt8(); if (!comp) end = buff.tell(); } buff.seek(end); return combine; } function namePack(str, buff, index) { var offset, dot, part; while (str) { if (index[str]) { offset = (LABEL_POINTER << 8) + index[str]; buff.writeUInt16BE(offset); break; } else { index[str] = buff.tell(); dot = str.indexOf('.'); if (dot > -1) { part = str.slice(0, dot); str = str.slice(dot + 1); } else { part = str; str = undefined; } buff.writeUInt8(part.length); buff.write(part, part.length, 'ascii'); } } if (!str) { buff.writeUInt8(0); } } var WRITE_HEADER = 100001, WRITE_TRUNCATE = 100002, WRITE_QUESTION = 100003, WRITE_RESOURCE_RECORD = 100004, WRITE_RESOURCE_WRITE = 100005, WRITE_RESOURCE_DONE = 100006, WRITE_RESOURCE_END = 100007, WRITE_EDNS = 100008, WRITE_END = 100009, WRITE_A = consts.NAME_TO_QTYPE.A, WRITE_AAAA = consts.NAME_TO_QTYPE.AAAA, WRITE_NS = consts.NAME_TO_QTYPE.NS, WRITE_CNAME = consts.NAME_TO_QTYPE.CNAME, WRITE_PTR = consts.NAME_TO_QTYPE.PTR, WRITE_SPF = consts.NAME_TO_QTYPE.SPF, WRITE_MX = consts.NAME_TO_QTYPE.MX, WRITE_SRV = consts.NAME_TO_QTYPE.SRV, WRITE_TXT = consts.NAME_TO_QTYPE.TXT, WRITE_SOA = consts.NAME_TO_QTYPE.SOA, WRITE_OPT = consts.NAME_TO_QTYPE.OPT, WRITE_NAPTR = consts.NAME_TO_QTYPE.NAPTR, WRITE_TLSA = consts.NAME_TO_QTYPE.TLSA; function writeHeader(buff, packet) { assert(packet.header, 'Packet requires "header"'); buff.writeUInt16BE(packet.header.id & 0xFFFF); var val = 0; val += (packet.header.qr << 15) & 0x8000; val += (packet.header.opcode << 11) & 0x7800; val += (packet.header.aa << 10) & 0x400; val += (packet.header.tc << 9) & 0x200; val += (packet.header.rd << 8) & 0x100; val += (packet.header.ra << 7) & 0x80; val += (packet.header.res1 << 6) & 0x40; val += (packet.header.res2 << 5) & 0x20; val += (packet.header.res3 << 4) & 0x10; val += packet.header.rcode & 0xF; buff.writeUInt16BE(val & 0xFFFF); assert(packet.question.length == 1, 'DNS requires one question'); // aren't used buff.writeUInt16BE(1); // answer offset 6 buff.writeUInt16BE(packet.answer.length & 0xFFFF); // authority offset 8 buff.writeUInt16BE(packet.authority.length & 0xFFFF); // additional offset 10 buff.writeUInt16BE(packet.additional.length & 0xFFFF); return WRITE_QUESTION; } function writeTruncate(buff, packet, section, val) { // XXX FIXME TODO truncation is currently done wrong. // Quote rfc2181 section 9 // The TC bit should not be set merely because some extra information // could have been included, but there was insufficient room. This // includes the results of additional section processing. In such cases // the entire RRSet that will not fit in the response should be omitted, // and the reply sent as is, with the TC bit clear. If the recipient of // the reply needs the omitted data, it can construct a query for that // data and send that separately. // // TODO IOW only set TC if we hit it in ANSWERS otherwise make sure an // entire RRSet is removed during a truncation. var pos; buff.seek(2); val = buff.readUInt16BE(); val |= (1 << 9) & 0x200; buff.seek(2); buff.writeUInt16BE(val); switch (section) { case 'answer': pos = 6; // seek to authority and clear it and additional out buff.seek(8); buff.writeUInt16BE(0); buff.writeUInt16BE(0); break; case 'authority': pos = 8; // seek to additional and clear it out buff.seek(10); buff.writeUInt16BE(0); break; case 'additional': pos = 10; break; } buff.seek(pos); buff.writeUInt16BE(count - 1); // TODO: count not defined! buff.seek(last_resource); // TODO: last_resource not defined! return WRITE_END; } function writeQuestion(buff, val, label_index) { assert(val, 'Packet requires a question'); assertUndefined(val.name, 'Question requires a "name"'); assertUndefined(val.type, 'Question requires a "type"'); assertUndefined(val.class, 'Questionn requires a "class"'); namePack(val.name, buff, label_index); buff.writeUInt16BE(val.type & 0xFFFF); buff.writeUInt16BE(val.class & 0xFFFF); return WRITE_RESOURCE_RECORD; } function writeResource(buff, val, label_index, rdata) { assert(val, 'Resource must be defined'); assertUndefined(val.name, 'Resource record requires "name"'); assertUndefined(val.type, 'Resource record requires "type"'); assertUndefined(val.class, 'Resource record requires "class"'); assertUndefined(val.ttl, 'Resource record requires "ttl"'); namePack(val.name, buff, label_index); buff.writeUInt16BE(val.type & 0xFFFF); buff.writeUInt16BE(val.class & 0xFFFF); buff.writeUInt32BE(val.ttl & 0xFFFFFFFF); rdata.pos = buff.tell(); buff.writeUInt16BE(0); // if there is rdata, then this value will be updated // to the correct value by 'writeResourceDone' return val.type; } function writeResourceDone(buff, rdata) { var pos = buff.tell(); buff.seek(rdata.pos); buff.writeUInt16BE(pos - rdata.pos - 2); buff.seek(pos); return WRITE_RESOURCE_RECORD; } function writeIp(buff, val) { //TODO XXX FIXME -- assert that address is of proper type assertUndefined(val.address, 'A/AAAA record requires "address"'); val = ipaddr.parse(val.address).toByteArray(); val.forEach(function(b) { buff.writeUInt8(b); }); return WRITE_RESOURCE_DONE; } function writeCname(buff, val, label_index) { assertUndefined(val.data, 'NS/CNAME/PTR record requires "data"'); namePack(val.data, buff, label_index); return WRITE_RESOURCE_DONE; } // For see: http://tools.ietf.org/html/rfc1035#section-3.3 // For TXT: http://tools.ietf.org/html/rfc1035#section-3.3.14 function writeTxt(buff, val) { //TODO XXX FIXME -- split on max char string and loop assertUndefined(val.data, 'TXT record requires "data"'); for (var i=0,len=val.data.length; i> 15; packet.header.opcode = (val & 0x7800) >> 11; packet.header.aa = (val & 0x400) >> 10; packet.header.tc = (val & 0x200) >> 9; packet.header.rd = (val & 0x100) >> 8; packet.header.ra = (val & 0x80) >> 7; packet.header.res1 = (val & 0x40) >> 6; packet.header.res2 = (val & 0x20) >> 5; packet.header.res3 = (val & 0x10) >> 4; packet.header.rcode = (val & 0xF); packet.question = new Array(msg.readUInt16BE()); packet.answer = new Array(msg.readUInt16BE()); packet.authority = new Array(msg.readUInt16BE()); packet.additional = new Array(msg.readUInt16BE()); return PARSE_QUESTION; } function parseQuestion(msg, packet) { var val = {}; val.name = nameUnpack(msg); val.type = msg.readUInt16BE(); val.class = msg.readUInt16BE(); packet.question[0] = val; assert(packet.question.length === 1); // TODO handle qdcount > 1 in practice no one sends this return PARSE_RESOURCE_RECORD; } function parseRR(msg, val, rdata) { val.name = nameUnpack(msg); val.type = msg.readUInt16BE(); val.class = msg.readUInt16BE(); val.ttl = msg.readUInt32BE(); rdata.len = msg.readUInt16BE(); return val.type; } function parseA(val, msg) { var address = '' + msg.readUInt8() + '.' + msg.readUInt8() + '.' + msg.readUInt8() + '.' + msg.readUInt8(); val.address = address; return PARSE_RESOURCE_DONE; } function parseAAAA(val, msg) { var address = ''; var compressed = false; for (var i = 0; i < 8; i++) { if (i > 0) address += ':'; // TODO zero compression address += msg.readUInt16BE().toString(16); } val.address = address; return PARSE_RESOURCE_DONE; } function parseCname(val, msg) { val.data = nameUnpack(msg); return PARSE_RESOURCE_DONE; } function parseTxt(val, msg, rdata) { val.data = []; var end = msg.tell() + rdata.len; while (msg.tell() != end) { var len = msg.readUInt8(); val.data.push(msg.toString('utf8', len)); } return PARSE_RESOURCE_DONE; } function parseMx(val, msg, rdata) { val.priority = msg.readUInt16BE(); val.exchange = nameUnpack(msg); return PARSE_RESOURCE_DONE; } // TODO: SRV fixture failing for '_xmpp-server._tcp.gmail.com.srv.js' // https://tools.ietf.org/html/rfc2782 function parseSrv(val, msg) { val.priority = msg.readUInt16BE(); val.weight = msg.readUInt16BE(); val.port = msg.readUInt16BE(); val.target = nameUnpack(msg); return PARSE_RESOURCE_DONE; } function parseSoa(val, msg) { val.primary = nameUnpack(msg); val.admin = nameUnpack(msg); val.serial = msg.readUInt32BE(); val.refresh = msg.readInt32BE(); val.retry = msg.readInt32BE(); val.expiration = msg.readInt32BE(); val.minimum = msg.readInt32BE(); return PARSE_RESOURCE_DONE; } // http://tools.ietf.org/html/rfc3403#section-4.1 function parseNaptr(val, msg) { val.order = msg.readUInt16BE(); val.preference = msg.readUInt16BE(); var len = msg.readUInt8(); val.flags = msg.toString('ascii', len); len = msg.readUInt8(); val.service = msg.toString('ascii', len); len = msg.readUInt8(); val.regexp = msg.toString('ascii', len); val.replacement = nameUnpack(msg); return PARSE_RESOURCE_DONE; } function parseTlsa(val, msg, rdata) { val.usage = msg.readUInt8(); val.selector = msg.readUInt8(); val.matchingtype = msg.readUInt8(); val.buff = msg.slice(rdata.len - 3).buffer; // 3 because of the 3 UInt8s above. return PARSE_RESOURCE_DONE; } // https://tools.ietf.org/html/rfc6891#section-6.1.2 // https://tools.ietf.org/html/rfc2671#section-4.4 // - [payload size selection](https://tools.ietf.org/html/rfc6891#section-6.2.5) function parseOpt(val, msg, rdata, packet) { // assert first entry in additional rdata.buf = msg.slice(rdata.len); val.rcode = ((val.ttl & 0xFF000000) >> 20) + packet.header.rcode; val.version = (val.ttl >> 16) & 0xFF; val.do = (val.ttl >> 15) & 1; val.z = val.ttl & 0x7F; val.options = []; packet.edns = val; packet.edns_version = val.version; // TODO: return BADVERS for unsupported version! (Section 6.1.3) // !! BEGIN DEPRECATION NOTICE !! // THESE FIELDS MAY BE REMOVED IN THE FUTURE! packet.edns_options = val.options; packet.payload = val.class; // !! END DEPRECATION NOTICE !! while (!rdata.buf.eof()) { val.options.push({ code: rdata.buf.readUInt16BE(), data: rdata.buf.slice(rdata.buf.readUInt16BE()).buffer }); } return PARSE_RESOURCE_DONE; } var PARSE_HEADER = 100000, PARSE_QUESTION = 100001, PARSE_RESOURCE_RECORD = 100002, PARSE_RR_UNPACK = 100003, PARSE_RESOURCE_DONE = 100004, PARSE_END = 100005, PARSE_A = consts.NAME_TO_QTYPE.A, PARSE_NS = consts.NAME_TO_QTYPE.NS, PARSE_CNAME = consts.NAME_TO_QTYPE.CNAME, PARSE_SOA = consts.NAME_TO_QTYPE.SOA, PARSE_PTR = consts.NAME_TO_QTYPE.PTR, PARSE_MX = consts.NAME_TO_QTYPE.MX, PARSE_TXT = consts.NAME_TO_QTYPE.TXT, PARSE_AAAA = consts.NAME_TO_QTYPE.AAAA, PARSE_SRV = consts.NAME_TO_QTYPE.SRV, PARSE_NAPTR = consts.NAME_TO_QTYPE.NAPTR, PARSE_OPT = consts.NAME_TO_QTYPE.OPT, PARSE_SPF = consts.NAME_TO_QTYPE.SPF, PARSE_TLSA = consts.NAME_TO_QTYPE.TLSA; Packet.parse = function(msg) { var state, pos, val, rdata, section, count; var packet = new Packet(); pos = 0; state = PARSE_HEADER; msg = new BufferCursor(msg); while (true) { switch (state) { case PARSE_HEADER: state = parseHeader(msg, packet); break; case PARSE_QUESTION: state = parseQuestion(msg, packet); section = 'answer'; count = 0; break; case PARSE_RESOURCE_RECORD: // console.log('PARSE_RESOURCE_RECORD: count = %d, %s.len = %d', count, section, packet[section].length); if (count === packet[section].length) { switch (section) { case 'answer': section = 'authority'; count = 0; break; case 'authority': section = 'additional'; count = 0; break; case 'additional': state = PARSE_END; break; } } else { state = PARSE_RR_UNPACK; } break; case PARSE_RR_UNPACK: val = {}; rdata = {}; state = parseRR(msg, val, rdata); break; case PARSE_RESOURCE_DONE: packet[section][count++] = val; state = PARSE_RESOURCE_RECORD; break; case PARSE_A: state = parseA(val, msg); break; case PARSE_AAAA: state = parseAAAA(val, msg); break; case PARSE_NS: case PARSE_CNAME: case PARSE_PTR: state = parseCname(val, msg); break; case PARSE_SPF: case PARSE_TXT: state = parseTxt(val, msg, rdata); break; case PARSE_MX: state = parseMx(val, msg); break; case PARSE_SRV: state = parseSrv(val, msg); break; case PARSE_SOA: state = parseSoa(val, msg); break; case PARSE_OPT: state = parseOpt(val, msg, rdata, packet); break; case PARSE_NAPTR: state = parseNaptr(val, msg); break; case PARSE_TLSA: state = parseTlsa(val, msg, rdata); break; case PARSE_END: return packet; default: //console.log(state, val); val.data = msg.slice(rdata.len); state = PARSE_RESOURCE_DONE; break; } } };