Files: 012e32d5ba7c8cc7ea4d15234b2715ffc1f0e154 / encode.js
8298 bytesRaw
1 | // Copyright 2012 Iris Couch, all rights reserved. |
2 | // |
3 | // Encode DNS messages |
4 | |
5 | var util = require('util') |
6 | |
7 | var constants = require('./constants') |
8 | |
9 | module.exports = { 'State': State |
10 | } |
11 | |
12 | var SECTIONS = ['question', 'answer', 'authority', 'additional'] |
13 | |
14 | function State () { |
15 | var self = this |
16 | |
17 | self.header = new Buffer(12) |
18 | self.position = 0 |
19 | |
20 | self.question = [] |
21 | self.answer = [] |
22 | self.authority = [] |
23 | self.additional = [] |
24 | |
25 | self.domains = {} // The compression lookup table |
26 | } |
27 | |
28 | State.prototype.toBinary = function() { |
29 | var self = this |
30 | |
31 | var bufs = [self.header] |
32 | self.question .forEach(function(buf) { bufs.push(buf) }) |
33 | self.answer .forEach(function(buf) { bufs.push(buf) }) |
34 | self.authority .forEach(function(buf) { bufs.push(buf) }) |
35 | self.additional.forEach(function(buf) { bufs.push(buf) }) |
36 | |
37 | return Buffer.concat(bufs) |
38 | } |
39 | |
40 | State.prototype.message = function(msg) { |
41 | var self = this |
42 | |
43 | // ID |
44 | self.header.writeUInt16BE(msg.id, 0) |
45 | |
46 | // QR, opcode, AA, TC, RD |
47 | var byte = 0 |
48 | byte |= msg.type == 'response' ? 0x80 : 0x00 |
49 | byte |= msg.authoritative ? 0x04 : 0x00 |
50 | byte |= msg.truncated ? 0x02 : 0x00 |
51 | byte |= msg.recursion_desired ? 0x01 : 0x00 |
52 | |
53 | var opcode_names = ['query', 'iquery', 'status', null, 'notify', 'update'] |
54 | , opcode = opcode_names.indexOf(msg.opcode) |
55 | |
56 | if(opcode == -1 || typeof msg.opcode != 'string') |
57 | throw new Error('Unknown opcode: ' + msg.opcode) |
58 | else |
59 | byte |= (opcode << 3) |
60 | |
61 | self.header.writeUInt8(byte, 2) |
62 | |
63 | // RA, Z, AD, CD, Rcode |
64 | byte = 0 |
65 | byte |= msg.recursion_available ? 0x80 : 0x00 |
66 | byte |= msg.authenticated ? 0x20 : 0x00 |
67 | byte |= msg.checking_disabled ? 0x10 : 0x00 |
68 | byte |= (msg.responseCode & 0x0f) |
69 | |
70 | self.header.writeUInt8(byte, 3) |
71 | |
72 | self.position = 12 // the beginning of the sections |
73 | SECTIONS.forEach(function(section) { |
74 | var records = msg[section] || [] |
75 | records.forEach(function(rec) { |
76 | self.record(section, rec) |
77 | }) |
78 | }) |
79 | |
80 | // Write the section counts. |
81 | self.header.writeUInt16BE(self.question.length , 4) |
82 | self.header.writeUInt16BE(self.answer.length , 6) |
83 | self.header.writeUInt16BE(self.authority.length , 8) |
84 | self.header.writeUInt16BE(self.additional.length , 10) |
85 | } |
86 | |
87 | State.prototype.record = function(section_name, record) { |
88 | var self = this |
89 | |
90 | var body = [] |
91 | , buf |
92 | |
93 | // Write the record name. |
94 | buf = self.encode(record.name) |
95 | body.push(buf) |
96 | self.position += buf.length |
97 | |
98 | var type = constants.type_to_number(record.type) |
99 | , clas = constants.class_to_number(record.class) |
100 | |
101 | // Write the type. |
102 | buf = new Buffer(2) |
103 | buf.writeUInt16BE(type, 0) |
104 | body.push(buf) |
105 | self.position += 2 |
106 | |
107 | // Write the class. |
108 | buf = new Buffer(2) |
109 | buf.writeUInt16BE(clas, 0) |
110 | body.push(buf) |
111 | self.position += 2 |
112 | |
113 | if(section_name != 'question') { |
114 | // Write the TTL. |
115 | buf = new Buffer(4) |
116 | buf.writeUInt32BE(record.ttl || 0, 0) |
117 | body.push(buf) |
118 | self.position += 4 |
119 | |
120 | // Write the rdata. Update the position now (the rdata length value) in case self.encode() runs. |
121 | var match, rdata |
122 | switch (record.class + ' ' + record.type) { |
123 | case 'IN A': |
124 | rdata = record.data || '' |
125 | match = rdata.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) |
126 | if(!match) |
127 | throw new Error('Bad '+record.type+' record data: ' + JSON.stringify(record)) |
128 | rdata = [ +match[1], +match[2], +match[3], +match[4] ] |
129 | break |
130 | case 'IN AAAA': |
131 | rdata = (record.data || '').split(/:/) |
132 | if(rdata.length != 8) |
133 | throw new Error('Bad '+record.type+' record data: ' + JSON.stringify(record)) |
134 | rdata = rdata.map(pair_to_buf) |
135 | break |
136 | case 'IN MX': |
137 | var host = record.data[1] |
138 | rdata = [ buf16(record.data[0]) |
139 | , self.encode(host, 2 + 2) // Adjust for the rdata length + preference values. |
140 | ] |
141 | break |
142 | case 'IN SOA': |
143 | var mname = self.encode(record.data.mname, 2) // Adust for rdata length |
144 | , rname = self.encode(record.data.rname, 2 + mname.length) |
145 | rdata = [ mname |
146 | , rname |
147 | , buf32(record.data.serial) |
148 | , buf32(record.data.refresh) |
149 | , buf32(record.data.retry) |
150 | , buf32(record.data.expire) |
151 | , buf32(record.data.ttl) |
152 | ] |
153 | break |
154 | case 'IN NS': |
155 | case 'IN PTR': |
156 | case 'IN CNAME': |
157 | rdata = self.encode(record.data, 2) // Adjust for the rdata length |
158 | break |
159 | case 'IN TXT': |
160 | rdata = record.data.map(function(part) { |
161 | part = new Buffer(part) |
162 | return [part.length, part] |
163 | }) |
164 | break |
165 | case 'IN SRV': |
166 | rdata = [ buf16(record.data.priority) |
167 | , buf16(record.data.weight) |
168 | , buf16(record.data.port) |
169 | , self.encode(record.data.target, 2 + 6, 'nocompress') // Offset for rdata length + priority, weight, and port. |
170 | ] |
171 | break |
172 | case 'IN DS': |
173 | rdata = [ buf16(record.data.key_tag) |
174 | , new Buffer([record.data.algorithm]) |
175 | , new Buffer([record.data.digest_type]) |
176 | , new Buffer(record.data.digest) |
177 | ] |
178 | break |
179 | case 'IN SSHFP': |
180 | rdata = [ new Buffer([record.algorithm]) |
181 | , new Buffer([record.fp_type]) |
182 | , new Buffer(record.fingerprint) |
183 | ] |
184 | break |
185 | case 'NONE A': |
186 | // I think this is no data, from RFC 2136 S. 2.4.3. |
187 | rdata = [] |
188 | break |
189 | default: |
190 | throw new Error('Unsupported record type: ' + JSON.stringify(record)) |
191 | } |
192 | |
193 | // Write the rdata length. (The position was already updated.) |
194 | rdata = flat(rdata) |
195 | buf = new Buffer(2) |
196 | buf.writeUInt16BE(rdata.length, 0) |
197 | body.push(buf) |
198 | self.position += 2 |
199 | |
200 | // Write the rdata. |
201 | self.position += rdata.length |
202 | if(rdata.length > 0) |
203 | body.push(new Buffer(rdata)) |
204 | } |
205 | |
206 | self[section_name].push(Buffer.concat(body)) |
207 | } |
208 | |
209 | State.prototype.encode = function(full_domain, position_offset, option) { |
210 | var self = this |
211 | |
212 | var domain = full_domain |
213 | domain = domain.replace(/\.$/, '') // Strip the trailing dot. |
214 | position = self.position + (position_offset || 0) |
215 | |
216 | var body = [] |
217 | , bytes |
218 | |
219 | var i = 0 |
220 | var max_iterations = 127 // Theoretical upper limit for number of labels in a DNS name; see https://en.wikipedia.org/wiki/Subdomain#Overview |
221 | |
222 | while(++i < max_iterations) { |
223 | if(domain == '') { |
224 | // Encode the root domain and be done. |
225 | body.push(new Buffer([0])) |
226 | return Buffer.concat(body) |
227 | } |
228 | |
229 | /* FIXME |
230 | else if(self.domains[domain] && option !== 'nocompress') { |
231 | // Encode a pointer and be done. |
232 | body.push(new Buffer([0xc0, self.domains[domain]])) |
233 | return Buffer.concat(body) |
234 | } |
235 | */ |
236 | |
237 | else { |
238 | // Encode the next part of the domain, saving its position in the lookup table for later. |
239 | self.domains[domain] = position |
240 | |
241 | var parts = domain.split(/\./) |
242 | , car = parts[0] |
243 | domain = parts.slice(1).join('.') |
244 | |
245 | // Write the first part of the domain, with a length prefix. |
246 | //var part = parts[0] |
247 | var buf = new Buffer(car.length + 1) |
248 | buf.write(car, 1, car.length, 'ascii') |
249 | buf.writeUInt8(car.length, 0) |
250 | body.push(buf) |
251 | position += buf.length |
252 | //bytes.unshift(bytes.length) |
253 | } |
254 | } |
255 | |
256 | throw new Error('Too many iterations encoding domain: ' + full_domain) |
257 | } |
258 | |
259 | |
260 | // |
261 | // Utilities |
262 | // |
263 | |
264 | function buf32(value) { |
265 | var buf = new Buffer(4) |
266 | buf.writeUInt32BE(value, 0) |
267 | return buf |
268 | } |
269 | |
270 | function buf16(value) { |
271 | var buf = new Buffer(2) |
272 | buf.writeUInt16BE(value, 0) |
273 | return buf |
274 | } |
275 | |
276 | function flat(data) { |
277 | return Buffer.isBuffer(data) |
278 | ? Array.prototype.slice.call(data) |
279 | : Array.isArray(data) |
280 | ? data.reduce(flatten, []) |
281 | : [data] |
282 | } |
283 | |
284 | function flatten(state, element) { |
285 | return (Buffer.isBuffer(element) || Array.isArray(element)) |
286 | ? state.concat(flat(element)) |
287 | : state.concat([element]) |
288 | } |
289 | |
290 | function pair_to_buf(pair) { |
291 | // Convert a string of two hex bytes, e.g. "89ab" to a buffer. |
292 | if(! pair.match(/^[0-9a-fA-F]{4}$/)) |
293 | throw new Error('Bad '+record.type+' record data: ' + JSON.stringify(record)) |
294 | return new Buffer(pair, 'hex') |
295 | } |
296 |
Built with git-ssb-web