net-dns 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ module Net # :nodoc:
2
+ module DNS
3
+
4
+ module Names # :nodoc: all
5
+
6
+ INT16SZ = 2
7
+
8
+ # Expand a compressed name in a DNS Packet object. Please
9
+ # see RFC1025 for an explanation of how the compression
10
+ # in DNS packets works, how may it be useful and how should
11
+ # be handled.
12
+ #
13
+ # This method accept two parameters: a raw packet data and an
14
+ # offset, which indicates the point in the packet in which the
15
+ # parsing has arrived.
16
+ #
17
+ def dn_expand(packet,offset)
18
+ name = ""
19
+ packetlen = packet.size
20
+ while true
21
+ raise ExpandError, "offset is greater than packet lenght!" if packetlen < (offset+1)
22
+ len = packet.unpack("@#{offset} C")[0]
23
+
24
+ if len == 0
25
+ offset += 1
26
+ break
27
+ elsif (len & 0xC0) == 0xC0
28
+ raise ExpandError, "Packet ended before offset expand" if packetlen < (offset+INT16SZ)
29
+ ptr = packet.unpack("@#{offset} n")[0]
30
+ ptr &= 0x3FFF
31
+ name2 = dn_expand(packet,ptr)[0]
32
+ raise ExpandError, "Packet is malformed!" if name2 == nil
33
+ name += name2
34
+ offset += INT16SZ
35
+ break
36
+ else
37
+ offset += 1
38
+ raise ExpandError, "No expansion found" if packetlen < (offset+len)
39
+ elem = packet[offset..offset+len-1]
40
+ name += "#{elem}."
41
+ offset += len
42
+ end
43
+ end
44
+ return [name,offset] # name.chomp(".") if trailing dot has to be omitted
45
+ end
46
+
47
+ def pack_name(name)
48
+ if name.size > 63
49
+ raise ArgumentError, "Label data cannot exceed 63 chars"
50
+ end
51
+ arr = name.split(".")
52
+ str = ""
53
+ arr.each do |elem|
54
+ str += [elem.size,elem].pack("Ca*")
55
+ end
56
+ str += [0].pack("C")
57
+ str
58
+ end
59
+
60
+ def names_array(name)
61
+ arr = name.split(".")
62
+ ar = []
63
+ string = ""
64
+ arr.size.times do |i|
65
+ x = i+1
66
+ elem = arr[-x]
67
+ len = elem.size
68
+ string = ((string.reverse)+([len,elem].pack("Ca*")).reverse).reverse
69
+ ar.unshift(string)
70
+ end
71
+ return ar
72
+ end
73
+
74
+ def dn_comp(name,offset,compnames)
75
+ names = {}
76
+ ptr = 0
77
+ str = ""
78
+ arr = names_array(name)
79
+ arr.each do |entry|
80
+ if compnames.has_key?(entry)
81
+ ptr = 0xC000 | compnames[entry]
82
+ str += [ptr].pack("n")
83
+ offset += INT16SZ
84
+ break
85
+ else
86
+ len = entry.unpack("C")[0]
87
+ elem = entry[1..len]
88
+ str += [len,elem].pack("Ca*")
89
+ names.update({"#{entry}" => offset})
90
+ offset += len
91
+ end
92
+ end
93
+ return str,offset,names
94
+ end
95
+
96
+ def valid?(name)
97
+ if name =~ /^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+((a[cdefgilmnoqrstuwxz]|aero|arpa)|(b[abdefghijmnorstvwyz]|biz)|(c[acdfghiklmnorsuvxyz]|cat|com|coop)|d[ejkmoz]|(e[ceghrstu]|edu)|f[ijkmor]|(g[abdefghilmnpqrstuwy]|gov)|h[kmnrtu]|(i[delmnoqrst]|info|int)|(j[emop]|jobs)|k[eghimnprwyz]|l[abcikrstuvy]|(m[acdghklmnopqrstuvwxyz]|mil|mobi|museum)|(n[acefgilopruz]|name|net)|(om|org)|(p[aefghklmnrstwy]|pro)|qa|r[eouw]|s[abcdeghijklmnortvyz]|(t[cdfghjklmnoprtvwz]|travel)|u[agkmsyz]|v[aceginu]|w[fs]|y[etu]|z[amw])$/i
98
+ return name
99
+ else
100
+ raise ArgumentError, "Invalid FQDN: #{name}"
101
+ end
102
+ end
103
+
104
+ end # module Names
105
+ end # module DNS
106
+ end # module Net
107
+
108
+ class ExpandError < StandardError # :nodoc:
109
+ end
@@ -0,0 +1,549 @@
1
+ require 'logger'
2
+ require 'net/dns/names/names'
3
+ require 'net/dns/dns'
4
+ require 'net/dns/header'
5
+ require 'net/dns/question'
6
+ require 'net/dns/rr'
7
+
8
+ module Net # :nodoc:
9
+ module DNS
10
+
11
+ # =Name
12
+ #
13
+ # Net::DNS::Packet - DNS packet object class
14
+ #
15
+ # =Synopsis
16
+ #
17
+ # require 'net/dns/packet'
18
+ #
19
+ # =Description
20
+ #
21
+ # The Net::DNS::Packet class represents an entire DNS packet,
22
+ # divided in his main section:
23
+ #
24
+ # * Header (instance of Net::DNS::Header)
25
+ # * Question (array of Net::DNS::Question objects)
26
+ # * Answer, Authority, Additional (each formed by an array of Net::DNS::RR
27
+ # objects)
28
+ #
29
+ # You can use this class whenever you need to create a DNS packet, whether
30
+ # in an user application, in a resolver instance (have a look, for instance,
31
+ # at the Net::DNS::Resolver#send method) or for a nameserver.
32
+ #
33
+ # Some example:
34
+ #
35
+ # # Create a packet
36
+ # packet = Net::DNS::Packet.new("www.example.com")
37
+ # mx = Net::DNS::Packet.new("example.com", Net::DNS::MX)
38
+ #
39
+ # # Getting packet binary data, suitable for network transmission
40
+ # data = packet.data
41
+ #
42
+ # A packet object can be created from binary data too, like an
43
+ # answer packet just received from a network stream:
44
+ #
45
+ # packet = Net::DNS::Packet::parse(data)
46
+ #
47
+ # Each part of a packet can be gotten by the right accessors:
48
+ #
49
+ # header = packet.header # Instance of Net::DNS::Header class
50
+ # question = packet.question # Instance of Net::DNS::Question class
51
+ #
52
+ # # Iterate over additional RRs
53
+ # packet.additional.each do |rr|
54
+ # puts "Got an #{rr.type} record"
55
+ # end
56
+ #
57
+ # Some iterators have been written to easy the access of those RRs,
58
+ # which are often the most important. So instead of doing:
59
+ #
60
+ # packet.answer.each do |rr|
61
+ # if rr.type == Net::DNS::RR::Types::A
62
+ # # do something with +rr.address+
63
+ # end
64
+ # end
65
+ #
66
+ # we can do:
67
+ #
68
+ # packet.each_address do |ip|
69
+ # # do something with +ip+
70
+ # end
71
+ #
72
+ # Be sure you don't miss all the iterators in the class documentation.
73
+ #
74
+ # =Logging facility
75
+ #
76
+ # As Net::DNS::Resolver class, Net::DNS::Packet class has its own logging
77
+ # facility too. It work in the same way the other one do, so you can
78
+ # maybe want to override it or change the file descriptor.
79
+ #
80
+ # packet = Net::DNS::Packet.new("www.example.com")
81
+ # packet.logger = $stderr
82
+ #
83
+ # # or even
84
+ # packet.logger = Logger.new("/tmp/packet.log")
85
+ #
86
+ # If the Net::DNS::Packet class is directly instantiated by the Net::DNS::Resolver
87
+ # class, like the great majority of the time, it will use the same logger facility.
88
+ #
89
+ # Logger level will be set to Logger::Debug if $DEBUG variable is set.
90
+ #
91
+ # =Error classes
92
+ #
93
+ # Some error classes has been defined for the Net::DNS::Packet class,
94
+ # which are listed here to keep a light and browsable main documentation.
95
+ # We have:
96
+ #
97
+ # * PacketArgumentError: Generic argument error for class Net::DNS::Packet
98
+ # * PacketError: Generic Packet error
99
+ #
100
+ # =Copyright
101
+ #
102
+ # Copyright (c) 2006 Marco Ceresa
103
+ #
104
+ # All rights reserved. This program is free software; you may redistribute
105
+ # it and/or modify it under the same terms as Ruby itself.
106
+ #
107
+ class Packet
108
+
109
+ include Names
110
+
111
+ attr_reader :header, :question, :answer, :authority, :additional
112
+ attr_reader :answerfrom, :answersize
113
+
114
+ # Create a new instance of Net::DNS::Packet class. Arguments are the
115
+ # canonical name of the resourse, an optional type field and an optional
116
+ # class field. The record type and class can be omitted; they default
117
+ # to +A+ and +IN+.
118
+ #
119
+ # packet = Net::DNS::Packet.new("www.example.com")
120
+ # packet = Net::DNS::Packet.new("example.com", Net::DNS::MX)
121
+ # packet = Net::DNS::Packet.new("example.com",Net::DNS::TXT,Net::DNS::CH)
122
+ #
123
+ # This class no longer instantiate object from binary data coming from
124
+ # network streams. Please use Net::DNS::Packet.new_from_data instead.
125
+ #
126
+ def initialize(name,type=Net::DNS::A,cls=Net::DNS::IN)
127
+ @header = Net::DNS::Header.new(:qdCount => 1)
128
+ @question = [Net::DNS::Question.new(name,type,cls)]
129
+ @answer = []
130
+ @authority = []
131
+ @additional = []
132
+ @logger = Logger.new $stdout
133
+ @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
134
+ end
135
+
136
+ # Create a new instance of Net::DNS::Packet class from binary data, taken
137
+ # out by a network stream. For example:
138
+ #
139
+ # # udp_socket is an UDPSocket waiting for a response
140
+ # ans = udp_socket.recvfrom(1500)
141
+ # packet = Net::DNS::Packet::parse(ans)
142
+ #
143
+ # An optional +from+ argument can be used to specify the information
144
+ # of the sender. If data is passed as is from a Socket#recvfrom call,
145
+ # the method will accept it.
146
+ #
147
+ # Be sure that your network data is clean from any UDP/TCP header,
148
+ # expecially when using RAW sockets.
149
+ #
150
+ def Packet.parse(*args)
151
+ o = allocate
152
+ o.send(:new_from_data, *args)
153
+ o
154
+ end
155
+
156
+
157
+ # Checks if the packet is a QUERY packet
158
+ def query?
159
+ @header.opCode == Net::DNS::Header::QUERY
160
+ end
161
+
162
+ # Return the packet object in binary data, suitable
163
+ # for sending across a network stream.
164
+ #
165
+ # packet_data = packet.data
166
+ # puts "Packet is #{packet_data.size} bytes long"
167
+ #
168
+ def data
169
+ qdcount=ancount=nscount=arcount=0
170
+ data = @header.data
171
+ headerlength = data.length
172
+
173
+ @question.each do |question|
174
+ data += question.data
175
+ qdcount += 1
176
+ end
177
+ @answer.each do |rr|
178
+ data += rr.data#(data.length)
179
+ ancount += 1
180
+ end
181
+ @authority.each do |rr|
182
+ data += rr.data#(data.length)
183
+ nscount += 1
184
+ end
185
+ @additional.each do |rr|
186
+ data += rr.data#(data.length)
187
+ arcount += 1
188
+ end
189
+
190
+ @header.qdCount = qdcount
191
+ @header.anCount = ancount
192
+ @header.nsCount = nscount
193
+ @header.arCount = arcount
194
+
195
+ @header.data + data[Net::DNS::HFIXEDSZ..data.size]
196
+ end
197
+
198
+ # Same as Net::DNS::Packet#data, but implements name compression
199
+ # (see RFC1025) for a considerable save of bytes.
200
+ #
201
+ # packet = Net::DNS::Packet.new("www.example.com")
202
+ # puts "Size normal is #{packet.data.size} bytes"
203
+ # puts "Size compressed is #{packet.data_comp.size} bytes"
204
+ #
205
+ def data_comp
206
+ offset = 0
207
+ compnames = {}
208
+ qdcount=ancount=nscount=arcount=0
209
+ data = @header.data
210
+ headerlength = data.length
211
+
212
+ @question.each do |question|
213
+ str,offset,names = question.data
214
+ data += str
215
+ compnames.update(names)
216
+ qdcount += 1
217
+ end
218
+
219
+ @answer.each do |rr|
220
+ str,offset,names = rr.data(offset,compnames)
221
+ data += str
222
+ compnames.update(names)
223
+ ancount += 1
224
+ end
225
+
226
+ @authority.each do |rr|
227
+ str,offset,names = rr.data(offset,compnames)
228
+ data += str
229
+ compnames.update(names)
230
+ nscount += 1
231
+ end
232
+
233
+ @additional.each do |rr|
234
+ str,offset,names = rr.data(offset,compnames)
235
+ data += str
236
+ compnames.update(names)
237
+ arcount += 1
238
+ end
239
+
240
+ @header.qdCount = qdcount
241
+ @header.anCount = ancount
242
+ @header.nsCount = nscount
243
+ @header.arCount = arcount
244
+
245
+ @header.data + data[Net::DNS::HFIXEDSZ..data.size]
246
+ end
247
+
248
+ # Inspect method
249
+ def inspect
250
+ retval = ""
251
+ if @answerfrom != "0.0.0.0:0" and @answerfrom
252
+ retval += ";; Answer received from #@answerfrom (#{@answersize} bytes)\n;;\n"
253
+ end
254
+
255
+ retval += ";; HEADER SECTION\n"
256
+ retval += @header.inspect
257
+
258
+ retval += "\n"
259
+ section = (@header.opCode == "UPDATE") ? "ZONE" : "QUESTION"
260
+ retval += ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '' : 's'}):\n"
261
+ @question.each do |qr|
262
+ retval += ";; " + qr.inspect + "\n"
263
+ end
264
+
265
+ unless @answer.size == 0
266
+ retval += "\n"
267
+ section = (@header.opCode == "UPDATE") ? "PREREQUISITE" : "ANSWER"
268
+ retval += ";; #{section} SECTION (#{@header.anCount} record#{@header.anCount == 1 ? '' : 's'}):\n"
269
+ @answer.each do |rr|
270
+ retval += rr.inspect + "\n"
271
+ end
272
+ end
273
+
274
+ unless @authority.size == 0
275
+ retval += "\n"
276
+ section = (@header.opCode == "UPDATE") ? "UPDATE" : "AUTHORITY"
277
+ retval += ";; #{section} SECTION (#{@header.nsCount} record#{@header.nsCount == 1 ? '' : 's'}):\n"
278
+ @authority.each do |rr|
279
+ retval += rr.inspect + "\n"
280
+ end
281
+ end
282
+
283
+ unless @additional.size == 0
284
+ retval += "\n"
285
+ retval += ";; ADDITIONAL SECTION (#{@header.arCount} record#{@header.arCount == 1 ? '' : 's'}):\n"
286
+ @additional.each do |rr|
287
+ retval += rr.inspect + "\n"
288
+ end
289
+ end
290
+
291
+ retval
292
+ end
293
+
294
+ # Assing a Net::DNS::Header object to a Net::DNS::Packet
295
+ # instance.
296
+ #
297
+ def header=(object)
298
+ if object.kind_of? Net::DNS::Header
299
+ @header = object
300
+ else
301
+ raise PacketArgumentError, "Argument must be a Net::DNS::Header object"
302
+ end
303
+ end
304
+
305
+ # Assign a Net::DNS::Question object, or an array of
306
+ # Questions objects, to a Net::DNS::Packet instance.
307
+ #
308
+ def question=(object)
309
+ case object
310
+ when Array
311
+ if object.all? {|x| x.kind_of? Net::DNS::Question}
312
+ @question = object
313
+ else
314
+ raise PacketArgumentError, "Some of the elements is not an Net::DNS::Question object"
315
+ end
316
+ when Net::DNS::Question
317
+ @question = [object]
318
+ else
319
+ raise PacketArgumentError, "Invalid argument, not a Question object nor an array of objects"
320
+ end
321
+ end
322
+
323
+ # Assign a Net::DNS::RR object, or an array of
324
+ # RR objects, to a Net::DNS::Packet instance answer
325
+ # section.
326
+ #
327
+ def answer=(object)
328
+ case object
329
+ when Array
330
+ if object.all? {|x| x.kind_of? Net::DNS::RR}
331
+ @answer = object
332
+ else
333
+ raise PacketArgumentError, "Some of the elements is not an Net::DNS::RR object"
334
+ end
335
+ when Net::DNS::RR
336
+ @answer = [object]
337
+ else
338
+ raise PacketArgumentError, "Invalid argument, not a RR object nor an array of objects"
339
+ end
340
+ end
341
+
342
+ # Assign a Net::DNS::RR object, or an array of
343
+ # RR objects, to a Net::DNS::Packet instance additional
344
+ # section.
345
+ #
346
+ def additional=(object)
347
+ case object
348
+ when Array
349
+ if object.all? {|x| x.kind_of? Net::DNS::RR}
350
+ @additional = object
351
+ else
352
+ raise PacketArgumentError, "Some of the elements is not an Net::DNS::RR object"
353
+ end
354
+ when Net::DNS::RR
355
+ @additional = [object]
356
+ else
357
+ raise PacketArgumentError, "Invalid argument, not a RR object nor an array of objects"
358
+ end
359
+ end
360
+
361
+ # Assign a Net::DNS::RR object, or an array of
362
+ # RR objects, to a Net::DNS::Packet instance authority
363
+ # section.
364
+ #
365
+ def authority=(object)
366
+ case object
367
+ when Array
368
+ if object.all? {|x| x.kind_of? Net::DNS::RR}
369
+ @authority = object
370
+ else
371
+ raise PacketArgumentError, "Some of the elements is not an Net::DNS::RR object"
372
+ end
373
+ when Net::DNS::RR
374
+ @authority = [object]
375
+ else
376
+ raise PacketArgumentError, "Invalid argument, not a RR object nor an array of objects"
377
+ end
378
+ end
379
+
380
+ # Iterate for every address in the +answer+ section of a
381
+ # Net::DNS::Packet object.
382
+ #
383
+ # packet.each_address do |ip|
384
+ # ping ip.to_s
385
+ # end
386
+ #
387
+ # As you can see in the documentation for Net::DNS::RR::A class,
388
+ # the address returned is an instance of IPAddr class.
389
+ #
390
+ def each_address
391
+ @answer.each do |elem|
392
+ next unless elem.class == Net::DNS::RR::A
393
+ yield elem.address
394
+ end
395
+ end
396
+
397
+ # Iterate for every nameserver in the +answer+ section of a
398
+ # Net::DNS::Packet object.
399
+ #
400
+ # packet.each_nameserver do |ns|
401
+ # puts "Nameserver found: #{ns}"
402
+ # end
403
+ #
404
+ def each_nameserver
405
+ @answer.each do |elem|
406
+ next unless elem.class == Net::DNS::RR::NS
407
+ yield elem.nsdname
408
+ end
409
+ end
410
+
411
+ # Iterate for every exchange record in the +answer+ section
412
+ # of a Net::DNS::Packet object.
413
+ #
414
+ # packet.each_mx do |pref,name|
415
+ # puts "Mail exchange #{name} has preference #{pref}"
416
+ # end
417
+ #
418
+ def each_mx
419
+ @answer.each do |elem|
420
+ next unless elem.class == Net::DNS::RR::MX
421
+ yield elem.preference,elem.exchange
422
+ end
423
+ end
424
+
425
+ # Iterate for every canonical name in the +answer+ section
426
+ # of a Net::DNS::Packet object.
427
+ #
428
+ # packet.each_cname do |cname|
429
+ # puts "Canonical name: #{cname}"
430
+ # end
431
+ #
432
+ def each_cname
433
+ @answer.each do |elem|
434
+ next unless elem.class == Net::DNS::RR::CNAME
435
+ yield elem.cname
436
+ end
437
+ end
438
+
439
+ # Iterate for every pointer in the +answer+ section of a
440
+ # Net::DNS::Packet object.
441
+ #
442
+ # packet.each_ptr do |ptr|
443
+ # puts "Pointer for resource: #{ptr}"
444
+ # end
445
+ #
446
+ def each_ptr
447
+ @answer.each do |elem|
448
+ next unless elem.class == Net::DNS::RR::PTR
449
+ yield elem.ptrdname
450
+ end
451
+ end
452
+
453
+ private
454
+
455
+ # New packet from binary data
456
+ def new_from_data(data, from = nil)
457
+ unless from
458
+ if data.kind_of? Array
459
+ data,from = data
460
+ else
461
+ from = [0,0,"0.0.0.0","unknown"]
462
+ end
463
+ end
464
+
465
+ @answerfrom = from[2] + ":" + from[1].to_s
466
+ @answersize = data.size
467
+ @logger = Logger.new $stdout
468
+ @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
469
+
470
+ #------------------------------------------------------------
471
+ # Header section
472
+ #------------------------------------------------------------
473
+ offset = Net::DNS::HFIXEDSZ
474
+ @header = Net::DNS::Header.parse(data[0..offset-1])
475
+
476
+ @logger.debug ";; HEADER SECTION"
477
+ @logger.debug @header.inspect
478
+
479
+ #------------------------------------------------------------
480
+ # Question section
481
+ #------------------------------------------------------------
482
+ section = @header.opCode == "UPDATE" ? "ZONE" : "QUESTION"
483
+ @logger.debug ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '': 's'})"
484
+
485
+ @question = []
486
+ @header.qdCount.times do
487
+ qobj,offset = parse_question(data,offset)
488
+ @question << qobj
489
+ @logger.debug ";; #{qobj.inspect}"
490
+ end
491
+
492
+ #------------------------------------------------------------
493
+ # Answer/prerequisite section
494
+ #------------------------------------------------------------
495
+ section = @header.opCode == "UPDATE" ? "PREREQUISITE" : "ANSWER"
496
+ @logger.debug ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '': 's'})"
497
+
498
+ @answer = []
499
+ @header.anCount.times do
500
+ rrobj,offset = Net::DNS::RR.parse_packet(data,offset)
501
+ @answer << rrobj
502
+ @logger.debug rrobj.inspect
503
+ end
504
+
505
+ #------------------------------------------------------------
506
+ # Authority/update section
507
+ #------------------------------------------------------------
508
+ section = @header.opCode == "UPDATE" ? "UPDATE" : "AUTHORITY"
509
+ @logger.debug ";; #{section} SECTION (#{@header.nsCount} record#{@header.nsCount == 1 ? '': 's'})"
510
+
511
+ @authority = []
512
+ @header.nsCount.times do
513
+ rrobj,offset = Net::DNS::RR.parse_packet(data,offset)
514
+ @authority << rrobj
515
+ @logger.debug rrobj.inspect
516
+ end
517
+
518
+ #------------------------------------------------------------
519
+ # Additional section
520
+ #------------------------------------------------------------
521
+ @logger.debug ";; ADDITIONAL SECTION (#{@header.arCount} record#{@header.arCount == 1 ? '': 's'})"
522
+
523
+ @additional = []
524
+ @header.arCount.times do
525
+ rrobj,offset = Net::DNS::RR.parse_packet(data,offset)
526
+ @additional << rrobj
527
+ @logger.debug rrobj.inspect
528
+ end
529
+
530
+ end # new_from_data
531
+
532
+
533
+ # Parse question section
534
+ def parse_question(data,offset)
535
+ size = (dn_expand(data,offset)[1]-offset) + 2*Net::DNS::INT16SZ
536
+ return [Net::DNS::Question.parse(data[offset,size]), offset+size]
537
+ rescue StandardError => err
538
+ raise PacketError, "Caught exception, maybe packet malformed => #{err}"
539
+ end
540
+
541
+ end # class Packet
542
+
543
+ end # module DNS
544
+ end # module Net
545
+
546
+ class PacketError < StandardError # :nodoc:
547
+ end
548
+ class PacketArgumentError < ArgumentError # :nodoc:
549
+ end