Files: 012e32d5ba7c8cc7ea4d15234b2715ffc1f0e154 / message.js
9791 bytesRaw
1 | // Copyright 2012 Iris Couch, all rights reserved. |
2 | // |
3 | // Test displaying DNS records |
4 | |
5 | var util = require('util') |
6 | |
7 | var parse = require('./parse') |
8 | var encode = require('./encode') |
9 | var constants = require('./constants') |
10 | |
11 | module.exports = DNSMessage |
12 | |
13 | var SECTIONS = ['question', 'answer', 'authority', 'additional'] |
14 | |
15 | // A DNS message. This is an easy-to-understand object representation of |
16 | // standard DNS queries and responses. |
17 | // |
18 | // Attributes: |
19 | // * id - a number representing the unique query ID |
20 | // * type - "request" or "response" |
21 | // * response - Number (server response code) |
22 | // * opcode - "query", "iquery", "status", "unassigned", "notify", "update" |
23 | // * authoritative - Boolean |
24 | // * truncated - Boolean |
25 | // * recursion_desired - Boolean |
26 | // * recursion_available - Boolean |
27 | // * authenticated - Boolean |
28 | // * checking_disabled - Boolean |
29 | // |
30 | // Optional attributes: |
31 | // * question (optional) - Array of the question section |
32 | // * answer (optional) - Array of the answer section |
33 | // * authority (optional) - Array of the authority section |
34 | // * additional (optional) - Array of the additional section |
35 | // |
36 | // Methods: |
37 | // * toString() - return a human-readable representation of this message |
38 | // * toJSON() - Return a JSON-friendly represenation of this message |
39 | // * toBinary() - Return a buffer of the encoded message |
40 | function DNSMessage (body) { |
41 | var self = this |
42 | |
43 | this.id = null |
44 | this.type = null |
45 | this.responseCode = null |
46 | this.opcode = null |
47 | this.authoritative = null |
48 | this.truncated = null |
49 | this.recursion_desired = null |
50 | this.recursion_available = null |
51 | this.authenticated = null |
52 | this.checking_disabled = null |
53 | |
54 | if(Buffer.isBuffer(body)) |
55 | this.parse(body) |
56 | else if(typeof body != 'object') |
57 | throw new Error('Must provide a buffer or object argument with message contents') |
58 | else { |
59 | Object.keys(body).forEach(function(key) { self[key] = body[key] }) |
60 | SECTIONS.forEach(function(section) { |
61 | if(self[section]) |
62 | self[section].forEach(function(record, i) { |
63 | self[section][i] = new DNSRecord(record) |
64 | }) |
65 | }) |
66 | } |
67 | |
68 | // EDNS processing. For now, just remove those records. |
69 | SECTIONS.forEach(function(section) { |
70 | if(self[section]) { |
71 | self[section] = self[section].filter(function(record) { return ! record.edns }) |
72 | if(self[section].length == 0) |
73 | delete self[section] |
74 | } |
75 | }) |
76 | } |
77 | |
78 | DNSMessage.prototype.parse = function(body) { |
79 | var self = this |
80 | |
81 | self.id = parse.id(body) |
82 | |
83 | var qr = parse.qr(body) |
84 | self.type = (qr == 0) ? 'request' : 'response' |
85 | |
86 | self.responseCode = parse.rcode(body) |
87 | |
88 | var opcode_names = ['query', 'iquery', 'status', null, 'notify', 'update'] |
89 | var opcode = parse.opcode(body) |
90 | self.opcode = opcode_names[opcode] || null |
91 | |
92 | self.authoritative = !! parse.aa(body) |
93 | self.truncated = !! parse.tc(body) |
94 | self.recursion_desired = !! parse.rd(body) |
95 | self.recursion_available = !! parse.ra(body) |
96 | self.authenticated = !! parse.ad(body) |
97 | self.checking_disabled = !! parse.cd(body) |
98 | |
99 | var sections_cache = parse.sections(body) |
100 | |
101 | SECTIONS.forEach(function(section) { |
102 | var count = parse.record_count(body, section) |
103 | if(count) { |
104 | self[section] = [] |
105 | for(var i = 0; i < count; i++) |
106 | self[section].push(new DNSRecord(body, section, i, sections_cache)) |
107 | } |
108 | }) |
109 | } |
110 | |
111 | DNSMessage.prototype.toBinary = function() { |
112 | // The encoder is picky, so make sure it gets a valid message. |
113 | var msg = JSON.parse(JSON.stringify(this)) |
114 | |
115 | SECTIONS.forEach(function(section) { |
116 | if(section == 'question') |
117 | return |
118 | |
119 | msg[section] = msg[section] || [] |
120 | msg[section].forEach(function(record) { |
121 | if(record.class != 'IN') |
122 | return |
123 | |
124 | // Make sure records promising data have data. |
125 | if(record.class == 'IN' && record.type == 'A') |
126 | record.data = record.data || '0.0.0.0' |
127 | |
128 | // Convert SOA email addresses back to the dotted notation. |
129 | if(record.class == 'IN' && record.type == 'SOA') |
130 | record.data.rname = record.data.rname.replace(/@/g, '.') |
131 | |
132 | // Normalize TXT records. |
133 | if(record.type == 'TXT' && typeof record.data == 'string') |
134 | record.data = [record.data] |
135 | }) |
136 | }) |
137 | |
138 | var state = new encode.State |
139 | state.message(msg) |
140 | return state.toBinary() |
141 | } |
142 | |
143 | DNSMessage.prototype.toString = function() { |
144 | var self = this |
145 | |
146 | var info = [ util.format('ID : %d', self.id) |
147 | , util.format("Type : %s", self.type) |
148 | , util.format("Opcode : %s", self.opcode) |
149 | , util.format("Authoritative : %s", self.authoritative) |
150 | , util.format("Truncated : %s", self.truncated) |
151 | , util.format("Recursion Desired : %s", self.recursion_desired) |
152 | , util.format("Recursion Available: %s", self.recursion_available) |
153 | , util.format("Response Code : %d", self.responseCode) |
154 | ] |
155 | |
156 | SECTIONS.forEach(function(section) { |
157 | if(self[section]) { |
158 | info.push(util.format(';; %s SECTION:', section.toUpperCase())) |
159 | self[section].forEach(function(record) { |
160 | info.push(record.toString()) |
161 | }) |
162 | } |
163 | }) |
164 | |
165 | return info.join('\n') |
166 | } |
167 | |
168 | |
169 | // An individual record from a DNS message |
170 | // |
171 | // Attributes: |
172 | // * name - Host name |
173 | // * type - Query type ('A', 'NS', 'CNAME', etc. or 'Unknown') |
174 | // * class - Network class ('IN', 'None' 'Unknown') |
175 | // * ttl - Time to live for the data in the record |
176 | // * data - The record data value, or null if not applicable |
177 | function DNSRecord (body, section_name, record_num, sections_cache) { |
178 | var self = this |
179 | |
180 | this.name = null |
181 | this.type = null |
182 | this.class = null |
183 | |
184 | // Leave these undefined for more consice and clear JSON serialization. |
185 | //this.ttl = null |
186 | //this.data = null |
187 | |
188 | if(Buffer.isBuffer(body)) |
189 | this.parse(body, section_name, record_num, sections_cache || body) |
190 | else if(typeof body != 'object') |
191 | throw new Error('Must provide a buffer or object argument with message contents') |
192 | else |
193 | Object.keys(body).forEach(function(key) { self[key] = body[key] }) |
194 | } |
195 | |
196 | DNSRecord.prototype.parse = function(body, section_name, record_num, sections) { |
197 | var self = this |
198 | |
199 | self.name = parse.record_name(sections, section_name, record_num) |
200 | |
201 | var type = parse.record_type(sections, section_name, record_num) |
202 | self.type = constants.type_to_label(type) |
203 | if(! self.type) |
204 | throw new Error('Record '+record_num+' in section "'+section_name+'" has unknown type: ' + type) |
205 | |
206 | if(section_name != 'additional' || self.type != 'OPT' || self.name != '') { |
207 | // Normal record |
208 | var clas = parse.record_class(sections, section_name, record_num) |
209 | self.class = constants.class_to_label(clas) |
210 | if(! self.class) |
211 | throw new Error('Record '+record_num+' in section "'+section_name+'" has unknown class: ' + type) |
212 | |
213 | if(section_name == 'question') |
214 | return |
215 | else |
216 | self.ttl = parse.record_ttl(sections, section_name, record_num) |
217 | } else { |
218 | // EDNS record |
219 | self.edns = true |
220 | delete self.name |
221 | delete self.class |
222 | //self.edns = parse.record_edns(sections, section_name, record_num) |
223 | } |
224 | |
225 | var rdata = parse.record_data(sections, section_name, record_num) |
226 | switch (self.kind()) { |
227 | case 'IN A': |
228 | if(rdata.length != 4) |
229 | throw new Error('Bad IN A data: ' + JSON.stringify(self)) |
230 | self.data = inet_ntoa(rdata) |
231 | break |
232 | case 'IN AAAA': |
233 | if(rdata.length != 16) |
234 | throw new Error('Bad IN AAAA data: ' + JSON.stringify(self)) |
235 | self.data = inet_ntoa6(rdata) |
236 | break |
237 | case 'IN NS': |
238 | case 'IN CNAME': |
239 | case 'IN PTR': |
240 | self.data = parse.uncompress(body, rdata) |
241 | break |
242 | case 'IN TXT': |
243 | self.data = parse.txt(body, rdata) |
244 | if(self.data.length === 0) |
245 | self.data = '' |
246 | else if(self.data.length === 1) |
247 | self.data = self.data[0] |
248 | break |
249 | case 'IN MX': |
250 | self.data = parse.mx(body, rdata) |
251 | break |
252 | case 'IN SRV': |
253 | self.data = parse.srv(body, rdata) |
254 | break |
255 | case 'IN SOA': |
256 | self.data = parse.soa(body, rdata) |
257 | self.data.rname = self.data.rname.replace(/\./, '@') |
258 | break |
259 | case 'IN DS': |
260 | self.data = { 'key_tag' : rdata[0] << 8 | rdata[1] |
261 | , 'algorithm' : rdata[2] |
262 | , 'digest_type': rdata[3] |
263 | , 'digest' : rdata.slice(4).toJSON() // Convert to a list of numbers. |
264 | } |
265 | break |
266 | case 'IN SSHFP': |
267 | self.data = parse.sshfp(body, rdata) |
268 | break |
269 | case 'NONE A': |
270 | self.data = [] |
271 | break |
272 | case 'EDNS': |
273 | self.data = rdata |
274 | break |
275 | default: |
276 | throw new Error('Unknown record '+self.kind()+': ' + JSON.stringify(self)) |
277 | } |
278 | } |
279 | |
280 | DNSRecord.prototype.kind = function() { |
281 | return this.edns |
282 | ? 'EDNS' |
283 | : this.class + ' ' + this.type |
284 | } |
285 | |
286 | DNSRecord.prototype.toString = function() { |
287 | var self = this |
288 | return [ width(23, self.name) |
289 | , width( 7, self.ttl || '') |
290 | , width( 7, self.class) |
291 | , width( 7, self.type) |
292 | , self.type == 'MX' && self.data |
293 | ? (width(3, self.data[0]) + ' ' + self.data[1]) |
294 | : Buffer.isBuffer(self.data) |
295 | ? self.data.toString('hex') |
296 | : self.data || '' |
297 | ].join(' ') |
298 | } |
299 | |
300 | // |
301 | // Utilities |
302 | // |
303 | |
304 | function width(str_len, str) { |
305 | str = '' + str |
306 | do { |
307 | var needed = str_len - str.length |
308 | if(needed > 0) |
309 | str = ' ' + str |
310 | } while(needed > 0) |
311 | |
312 | return str |
313 | } |
314 | |
315 | function inet_ntoa(buf) { |
316 | return buf[0] + '.' + buf[1] + '.' + buf[2] + '.' + buf[3] |
317 | } |
318 | |
319 | function inet_ntoa6(buf) { |
320 | var result = [] |
321 | for(var i = 0; i < 16; i += 2) |
322 | result.push(buf.slice(i, i+2).toString('hex')) |
323 | return result.join(':') |
324 | } |
325 |
Built with git-ssb-web