@sintaxi/dnsd
Version:
Dynamic authoritative name server for Iris services
288 lines (238 loc) • 8.05 kB
JavaScript
// Copyright 2012 Iris Couch, all rights reserved.
//
// Encode DNS messages
var util = require('util')
var constants = require('./constants')
module.exports = { 'State': State
}
var SECTIONS = ['question', 'answer', 'authority', 'additional']
function State () {
var self = this
self.header = new Buffer(12)
self.position = 0
self.question = []
self.answer = []
self.authority = []
self.additional = []
self.domains = {} // The compression lookup table
}
State.prototype.toBinary = function() {
var self = this
var bufs = [self.header]
self.question .forEach(function(buf) { bufs.push(buf) })
self.answer .forEach(function(buf) { bufs.push(buf) })
self.authority .forEach(function(buf) { bufs.push(buf) })
self.additional.forEach(function(buf) { bufs.push(buf) })
return Buffer.concat(bufs)
}
State.prototype.message = function(msg) {
var self = this
// ID
self.header.writeUInt16BE(msg.id, 0)
// QR, opcode, AA, TC, RD
var byte = 0
byte |= msg.type == 'response' ? 0x80 : 0x00
byte |= msg.authoritative ? 0x04 : 0x00
byte |= msg.truncated ? 0x02 : 0x00
byte |= msg.recursion_desired ? 0x01 : 0x00
var opcode_names = ['query', 'iquery', 'status', null, 'notify', 'update']
, opcode = opcode_names.indexOf(msg.opcode)
if(opcode == -1 || typeof msg.opcode != 'string')
throw new Error('Unknown opcode: ' + msg.opcode)
else
byte |= (opcode << 3)
self.header.writeUInt8(byte, 2)
// RA, Z, AD, CD, Rcode
byte = 0
byte |= msg.recursion_available ? 0x80 : 0x00
byte |= msg.authenticated ? 0x20 : 0x00
byte |= msg.checking_disabled ? 0x10 : 0x00
byte |= (msg.responseCode & 0x0f)
self.header.writeUInt8(byte, 3)
self.position = 12 // the beginning of the sections
SECTIONS.forEach(function(section) {
var records = msg[section] || []
records.forEach(function(rec) {
self.record(section, rec)
})
})
// Write the section counts.
self.header.writeUInt16BE(self.question.length , 4)
self.header.writeUInt16BE(self.answer.length , 6)
self.header.writeUInt16BE(self.authority.length , 8)
self.header.writeUInt16BE(self.additional.length , 10)
}
State.prototype.record = function(section_name, record) {
var self = this
var body = []
, buf
// Write the record name.
buf = self.encode(record.name)
body.push(buf)
self.position += buf.length
var type = constants.type_to_number(record.type)
, clas = constants.class_to_number(record.class)
// Write the type.
buf = new Buffer(2)
buf.writeUInt16BE(type, 0)
body.push(buf)
self.position += 2
// Write the class.
buf = new Buffer(2)
buf.writeUInt16BE(clas, 0)
body.push(buf)
self.position += 2
if(section_name != 'question') {
// Write the TTL.
buf = new Buffer(4)
buf.writeUInt32BE(record.ttl || 0, 0)
body.push(buf)
self.position += 4
// Write the rdata. Update the position now (the rdata length value) in case self.encode() runs.
var match, rdata
switch (record.class + ' ' + record.type) {
case 'IN A':
rdata = record.data || ''
match = rdata.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)
if(!match)
throw new Error('Bad '+record.type+' record data: ' + JSON.stringify(record))
rdata = [ +match[1], +match[2], +match[3], +match[4] ]
break
case 'IN AAAA':
rdata = (record.data || '').split(/:/)
if(rdata.length != 8)
throw new Error('Bad '+record.type+' record data: ' + JSON.stringify(record))
rdata = rdata.map(pair_to_buf)
break
case 'IN MX':
var host = record.data[1]
rdata = [ buf16(record.data[0])
, self.encode(host, 2 + 2) // Adjust for the rdata length + preference values.
]
break
case 'IN SOA':
var mname = self.encode(record.data.mname, 2) // Adust for rdata length
, rname = self.encode(record.data.rname, 2 + mname.length)
rdata = [ mname
, rname
, buf32(record.data.serial)
, buf32(record.data.refresh)
, buf32(record.data.retry)
, buf32(record.data.expire)
, buf32(record.data.ttl)
]
break
case 'IN NS':
case 'IN PTR':
case 'IN CNAME':
rdata = self.encode(record.data, 2) // Adjust for the rdata length
break
case 'IN TXT':
rdata = record.data.map(function(part) {
part = new Buffer(part)
return [part.length, part]
})
break
case 'IN SRV':
rdata = [ buf16(record.data.priority)
, buf16(record.data.weight)
, buf16(record.data.port)
, self.encode(record.data.target, 2 + 6, 'nocompress') // Offset for rdata length + priority, weight, and port.
]
break
case 'IN DS':
rdata = [ buf16(record.data.key_tag)
, new Buffer([record.data.algorithm])
, new Buffer([record.data.digest_type])
, new Buffer(record.data.digest)
]
break
case 'NONE A':
// I think this is no data, from RFC 2136 S. 2.4.3.
rdata = []
break
default:
throw new Error('Unsupported record type: ' + JSON.stringify(record))
}
// Write the rdata length. (The position was already updated.)
rdata = flat(rdata)
buf = new Buffer(2)
buf.writeUInt16BE(rdata.length, 0)
body.push(buf)
self.position += 2
// Write the rdata.
self.position += rdata.length
if(rdata.length > 0)
body.push(new Buffer(rdata))
}
self[section_name].push(Buffer.concat(body))
}
State.prototype.encode = function(full_domain, position_offset, option) {
var self = this
var domain = full_domain
domain = domain.replace(/\.$/, '') // Strip the trailing dot.
position = self.position + (position_offset || 0)
var body = []
, bytes
var i = 0
var max_iterations = 40 // Enough for 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa
while(++i < max_iterations) {
if(domain == '') {
// Encode the root domain and be done.
body.push(new Buffer([0]))
return Buffer.concat(body)
}
else if(self.domains[domain] && option !== 'nocompress') {
// Encode a pointer and be done.
body.push(new Buffer([0xc0, self.domains[domain]]))
return Buffer.concat(body)
}
else {
// Encode the next part of the domain, saving its position in the lookup table for later.
self.domains[domain] = position
var parts = domain.split(/\./)
, car = parts[0]
domain = parts.slice(1).join('.')
// Write the first part of the domain, with a length prefix.
//var part = parts[0]
var buf = new Buffer(car.length + 1)
buf.write(car, 1, car.length, 'ascii')
buf.writeUInt8(car.length, 0)
body.push(buf)
position += buf.length
//bytes.unshift(bytes.length)
}
}
throw new Error('Too many iterations encoding domain: ' + full_domain)
}
//
// Utilities
//
function buf32(value) {
var buf = new Buffer(4)
buf.writeUInt32BE(value, 0)
return buf
}
function buf16(value) {
var buf = new Buffer(2)
buf.writeUInt16BE(value, 0)
return buf
}
function flat(data) {
return Buffer.isBuffer(data)
? Array.prototype.slice.call(data)
: Array.isArray(data)
? data.reduce(flatten, [])
: [data]
}
function flatten(state, element) {
return (Buffer.isBuffer(element) || Array.isArray(element))
? state.concat(flat(element))
: state.concat([element])
}
function pair_to_buf(pair) {
// Convert a string of two hex bytes, e.g. "89ab" to a buffer.
if(! pair.match(/^[0-9a-fA-F]{4}$/))
throw new Error('Bad '+record.type+' record data: ' + JSON.stringify(record))
return new Buffer(pair, 'hex')
}