Files: 012e32d5ba7c8cc7ea4d15234b2715ffc1f0e154 / parse.js
7806 bytesRaw
1 | // Copyright 2012 Iris Couch, all rights reserved. |
2 | // |
3 | // Parse DNS messages |
4 | |
5 | var util = require('util') |
6 | |
7 | var constants = require('./constants') |
8 | |
9 | module.exports = { 'id': id |
10 | , 'qr': qr |
11 | , 'aa': aa |
12 | , 'tc': tc |
13 | , 'rd': rd |
14 | , 'ra': ra |
15 | , 'ad': ad |
16 | , 'cd': cd |
17 | , 'rcode': rcode |
18 | , 'opcode': opcode |
19 | , 'record_count': record_count |
20 | , 'record_name' : record_name |
21 | , 'record_class': record_class |
22 | , 'record_ttl' : record_ttl |
23 | , 'record_type' : record_type |
24 | , 'record_data' : record_data |
25 | , 'uncompress' : uncompress |
26 | , 'sections' : sections |
27 | , 'mx': mx |
28 | , 'srv': srv |
29 | , 'soa': soa |
30 | , 'txt': txt |
31 | , 'sshfp': sshfp |
32 | } |
33 | |
34 | |
35 | function id(msg) { |
36 | return msg.readUInt16BE(0) |
37 | } |
38 | |
39 | function qr(msg) { |
40 | return msg.readUInt8(2) >> 7 |
41 | } |
42 | |
43 | function opcode(msg) { |
44 | return (msg.readUInt8(2) >> 3) & 0x0f |
45 | } |
46 | |
47 | function aa(msg) { |
48 | return (msg.readUInt8(2) >> 2) & 0x01 |
49 | } |
50 | |
51 | function tc(msg) { |
52 | return (msg.readUInt8(2) >> 1) & 0x01 |
53 | } |
54 | |
55 | function rd(msg) { |
56 | return msg.readUInt8(2) & 0x01 |
57 | } |
58 | |
59 | function ra(msg) { |
60 | return msg.readUInt8(3) >> 7 |
61 | } |
62 | |
63 | function ad(msg) { |
64 | return msg.readUInt8(3) >> 5 & 0x01 |
65 | } |
66 | |
67 | function cd(msg) { |
68 | return msg.readUInt8(3) >> 4 & 0x01 |
69 | } |
70 | |
71 | function rcode(msg) { |
72 | return msg.readUInt8(3) & 0x0f |
73 | } |
74 | |
75 | function record_count(msg, name) { |
76 | if(name == 'question') |
77 | return msg.readUInt16BE(4) |
78 | else if(name == 'answer') |
79 | return msg.readUInt16BE(6) |
80 | else if(name == 'authority') |
81 | return msg.readUInt16BE(8) |
82 | else if(name == 'additional') |
83 | return msg.readUInt16BE(10) |
84 | else |
85 | throw new Error('Unknown section name: ' + name) |
86 | } |
87 | |
88 | function record_name(msg, section_name, offset) { |
89 | var rec = record(msg, section_name, offset) |
90 | return rec.name |
91 | } |
92 | |
93 | function record_class(msg, section_name, offset) { |
94 | var rec = record(msg, section_name, offset) |
95 | return rec.class |
96 | } |
97 | |
98 | function record_type(msg, section_name, offset) { |
99 | var rec = record(msg, section_name, offset) |
100 | return rec.type |
101 | } |
102 | |
103 | function record_ttl(msg, section_name, offset) { |
104 | var rec = record(msg, section_name, offset) |
105 | return rec.ttl |
106 | } |
107 | |
108 | function record_data(msg, section_name, offset) { |
109 | var rec = record(msg, section_name, offset) |
110 | return rec.data |
111 | } |
112 | |
113 | function record_class(msg, section_name, offset) { |
114 | var rec = record(msg, section_name, offset) |
115 | return rec.class |
116 | } |
117 | |
118 | function record(msg, section_name, offset) { |
119 | if(typeof offset != 'number' || isNaN(offset) || offset < 0) |
120 | throw new Error('Offset must be a natural number') |
121 | |
122 | // Support msg being a previously-parsed sections object. |
123 | var sects = Buffer.isBuffer(msg) |
124 | ? sections(msg) |
125 | : msg |
126 | |
127 | var records = sects[section_name] |
128 | if(!records) |
129 | throw new Error('No such section: "'+section_name+'"') |
130 | |
131 | var rec = records[offset] |
132 | if(!rec) |
133 | throw new Error('Bad offset for section "'+section_name+'": ' + offset) |
134 | |
135 | return rec |
136 | } |
137 | |
138 | function sections(msg) { |
139 | // Count the times this message has been parsed, for debugging and testing purposes. |
140 | if('__parsed' in msg) |
141 | msg.__parsed += 1 |
142 | |
143 | var position = 12 // First byte of the first section |
144 | , result = {'question':[], 'answer':[], 'authority':[], 'additional':[]} |
145 | , need = { 'question' : record_count(msg, 'question') |
146 | , 'answer' : record_count(msg, 'answer') |
147 | , 'authority' : record_count(msg, 'authority') |
148 | , 'additional': record_count(msg, 'additional') |
149 | } |
150 | |
151 | var states = ['question', 'answer', 'authority', 'additional', 'done'] |
152 | , state = states.shift() |
153 | |
154 | while(true) { |
155 | if(state == 'done') |
156 | return result |
157 | else if(result[state].length == need[state]) |
158 | state = states.shift() |
159 | else if(!state) |
160 | throw new Error('Unknown parsing state at position '+position+': '+JSON.stringify(state)) |
161 | else |
162 | add_record() |
163 | } |
164 | |
165 | function add_record() { |
166 | var record = {} |
167 | |
168 | var data = domain_parts(msg, position) |
169 | record.name = data.parts.join('.') |
170 | position += data.length |
171 | |
172 | record.type = msg.readUInt16BE(position + 0) |
173 | record.class = msg.readUInt16BE(position + 2) |
174 | position += 4 |
175 | |
176 | if(state != 'question') { |
177 | record.ttl = msg.readUInt32BE(position + 0) |
178 | var rdata_len = msg.readUInt16BE(position + 4) |
179 | |
180 | position += 6 |
181 | record.data = msg.slice(position, position + rdata_len) |
182 | |
183 | position += rdata_len |
184 | |
185 | if(constants.type(record.type) === 'OPT') { |
186 | // EDNS |
187 | if(record.name !== '') |
188 | throw new Error('EDNS record option for non-root domain: ' + record.name) |
189 | |
190 | record.udp_size = record.class |
191 | delete record.class |
192 | |
193 | record.extended_rcode = (record.ttl >> 24) |
194 | record.edns_version = (record.ttl >> 16) & 0xff |
195 | record.zero = (record.ttl >> 8) |
196 | delete record.ttl |
197 | |
198 | record.data = Array.prototype.slice.call(record.data) |
199 | } |
200 | } |
201 | |
202 | result[state] = result[state] || [] |
203 | result[state].push(record) |
204 | } |
205 | } |
206 | |
207 | function mx(msg, data) { |
208 | return [ data.readUInt16BE(0) |
209 | , uncompress(msg, data.slice(2)) |
210 | ] |
211 | } |
212 | |
213 | function srv(msg, data) { |
214 | return { 'priority': data.readUInt16BE(0) |
215 | , 'weight' : data.readUInt16BE(2) |
216 | , 'port' : data.readUInt16BE(4) |
217 | , 'target' : uncompress(msg, data.slice(6)) // Techncially compression is not allowed in RFC 2782. |
218 | } |
219 | } |
220 | |
221 | function soa(msg, data) { |
222 | var result = domain_parts(msg, data) |
223 | , offset = result.length |
224 | , mname = result.parts.join('.') |
225 | |
226 | result = domain_parts(msg, data.slice(offset)) |
227 | var rname = result.parts.join('.') |
228 | offset += result.length |
229 | |
230 | return { 'mname' : mname |
231 | , 'rname' : rname //.replace(/\./, '@') |
232 | , 'serial' : data.readUInt32BE(offset + 0) |
233 | , 'refresh': data.readUInt32BE(offset + 4) |
234 | , 'retry' : data.readUInt32BE(offset + 8) |
235 | , 'expire' : data.readUInt32BE(offset + 12) |
236 | , 'ttl' : data.readUInt32BE(offset + 16) |
237 | } |
238 | } |
239 | |
240 | function txt(msg, data) { |
241 | var parts = [] |
242 | while(data.length) { |
243 | var len = data.readUInt8(0) |
244 | parts.push(data.slice(1, 1+len).toString('ascii')) |
245 | data = data.slice(1+len) |
246 | } |
247 | |
248 | return parts |
249 | } |
250 | |
251 | function sshfp(msg, data) { |
252 | return { 'algorithm': data.readUInt8(0) |
253 | , 'fp_type' : data.readUInt8(1) |
254 | , 'fingerprint' : data.slice(2) |
255 | } |
256 | } |
257 | |
258 | function uncompress(msg, offset) { |
259 | var data = domain_parts(msg, offset) |
260 | return data.parts.join('.') |
261 | } |
262 | |
263 | function domain_parts(msg, offset) { |
264 | if(Buffer.isBuffer(offset)) { |
265 | var full_message = msg |
266 | msg = offset |
267 | offset = 0 |
268 | } |
269 | |
270 | if(typeof offset != 'number' || isNaN(offset) || offset < 0 || offset > msg.length) |
271 | throw new Error('Bad offset: ' + offset) |
272 | |
273 | var parts = [] |
274 | , real_length = 0 |
275 | , jumped = false |
276 | |
277 | var i = 0 |
278 | while(true) { |
279 | if(++i >= 100) |
280 | throw new Error('Too many iterations uncompressing name') |
281 | |
282 | var byte = msg.readUInt8(offset) |
283 | , flags = byte >> 6 |
284 | , len = byte & 0x3f // 0 - 63 |
285 | |
286 | offset += 1 |
287 | add_length(1) |
288 | |
289 | if(flags === 0x03) { |
290 | offset = (len << 8) + msg.readUInt8(offset) |
291 | add_length(1) |
292 | jumped = true |
293 | |
294 | // If processing so far has just been on some given fragment, begin using the full message now. |
295 | msg = full_message || msg |
296 | } |
297 | |
298 | else if(len == 0) |
299 | return {'parts':parts, 'length':real_length} |
300 | |
301 | else { |
302 | parts.push(msg.toString('ascii', offset, offset + len)) |
303 | |
304 | offset += len |
305 | add_length(len) |
306 | } |
307 | } |
308 | |
309 | function add_length(amount) { |
310 | if(! jumped) |
311 | real_length += amount |
312 | } |
313 | } |
314 |
Built with git-ssb-web