forked from jhs/dnsd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
encode.js
287 lines (238 loc) · 7.86 KB
/
encode.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// 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')
}