Class: Dnsruby::ZoneReader

Inherits:
Object
  • Object
show all
Defined in:
lib/dnsruby/zone_reader.rb

Defined Under Namespace

Classes: ParseException

Instance Method Summary collapse

Constructor Details

#initialize(origin, soa_minimum = nil, soa_ttl = nil) ⇒ ZoneReader

Create a new ZoneReader. The zone origin is required. If the desired SOA minimum

and TTL are passed in, then they are used as default values.


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/dnsruby/zone_reader.rb', line 27

def initialize(origin, soa_minimum = nil, soa_ttl = nil)
  @origin = origin.to_s

  if (!Name.create(@origin).absolute?)
    @origin = @origin.to_s + "."
  end
  @soa_ttl = soa_ttl
  if (soa_minimum && !@last_explicit_ttl)
    @last_explicit_ttl = soa_minimum
  else
    @last_explicit_ttl = 0
  end
  @last_explicit_class = Classes.new("IN")
  @last_name = nil
  @continued_line = nil
  @in_quoted_section = false
end

Instance Method Details

#get_ttl(ttl_text_in) ⇒ Object

Get the TTL in seconds from the m, h, d, w format



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/dnsruby/zone_reader.rb', line 397

def get_ttl(ttl_text_in)
  #  If no letter afterwards, then in seconds already
  #  Could be e.g. "3d4h12m" - unclear if "4h5w" is legal - best assume it is
  #  So, search out each letter in the string, and get the number before it.
  ttl_text = ttl_text_in.downcase
  index = ttl_text.index(/[whdms]/)
  if (!index)
    return ttl_text.to_i
  end
  last_index = -1
  total = 0
  while (index)
    letter = ttl_text[index]
    number = ttl_text[last_index + 1, index-last_index-1].to_i
    new_number = 0
    case letter
    when 115 then # "s"
      new_number = number
    when 109 then # "m"
      new_number = number * 60
    when 104 then # "h"
      new_number = number * 3600
    when 100 then # "d"
      new_number = number * 86400
    when 119 then # "w"
      new_number = number * 604800
    end
    total += new_number

    last_index = index
    index = ttl_text.index(/[whdms]/, last_index + 1)
  end
  return total
end

#normalise_line(line, do_prefix_hack = false) ⇒ Object

Take a line from the input zone file, and return the normalised form

do_prefix_hack should always be false


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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/dnsruby/zone_reader.rb', line 211

def normalise_line(line, do_prefix_hack = false)
  #  Note that a freestanding "@" is used to denote the current origin - we can simply replace that straight away
  #  Remove the ( and )
  #  Note that no domain name may be specified in the RR - in that case, last_name should be used. How do we tell? Tab or space at start of line.

  #  If we have text in the record, then ignore that in the parsing, and stick it on again at the end
  stored_line = "";
  if (line.index('"') != nil)
      stored_line = line[line.index('"'), line.length];
      line = line [0, line.index('"')]
  end
  if ((line[0,1] == " ") || (line[0,1] == "\t"))
    line = @last_name + " " + line
  end
  line.chomp!
  line.sub!(/\s+@$/, " #{@origin}") # IN CNAME @
  line.sub!(/^@\s+/, "#{@origin} ") # IN CNAME @
  line.sub!(/\s+@\s+/, " #{@origin} ")
  line.strip!


  #  o We need to identify the domain name in the record, and then
  split = line.split(' ') # split on whitespace
  name = split[0].strip
  if (name.index"\\")

    ls =[]
    Name.create(name).labels.each {|el| ls.push(Name.decode(el.to_s))}
    new_name = ls.join('.')


    if (!(/\.\z/ =~ name))
      new_name += "." + @origin
    else
      new_name += "."
    end
    line = new_name + " "
    (split.length - 1).times {|i| line += "#{split[i+1]} "}
    line += "\n"
    name = new_name
    split = line.split
    #  o add $ORIGIN to it if it is not absolute
  elsif !(/\.\z/ =~ name)
    new_name = name + "." + @origin
    line.sub!(name, new_name)
    name = new_name
    split = line.split
  end

  #  If the second field is not a number, then we should add the TTL to the line
  #  Remember we can get "m" "w" "y" here! So need to check for appropriate regexp...
  found_ttl_regexp = (split[1]=~/^[0-9]+[smhdwSMHDW]/)
  if (found_ttl_regexp == 0)
    #  Replace the formatted ttl with an actual number
    ttl = get_ttl(split[1])
    line = name + " #{ttl} "
    @last_explicit_ttl = ttl
    (split.length - 2).times {|i| line += "#{split[i+2]} "}
    line += "\n"
    split = line.split
  elsif (((split[1]).to_i == 0) && (split[1] != "0"))
    #  Add the TTL
    if (!@last_explicit_ttl)
      #  If this is the SOA record, and no @last_explicit_ttl is defined,
      #  then we need to try the SOA TTL element from the config. Otherwise,
      #  find the SOA Minimum field, and use that.
      #  We should also generate a warning to that effect
      #  How do we know if it is an SOA record at this stage? It must be, or
      #  else @last_explicit_ttl should be defined
      #  We could put a marker in the RR for now - and replace it once we know
      #  the actual type. If the type is not SOA then, then we can raise an error
      line = name + " %MISSING_TTL% "
    else
      line = name + " #{@last_explicit_ttl} "
    end
    (split.length - 1).times {|i| line += "#{split[i+1]} "}
    line += "\n"
    split = line.split
  else
    @last_explicit_ttl = split[1].to_i
  end

  #  Now see if the clas is included. If not, then we should default to the last class used.
  begin
    klass = Classes.new(split[2])
    @last_explicit_class = klass
  rescue ArgumentError
    #  Wasn't a CLASS
    #  So add the last explicit class in
    line = ""
    (2).times {|i| line += "#{split[i]} "}
    line += " #{@last_explicit_class} "
    (split.length - 2).times {|i| line += "#{split[i+2]} "}
    line += "\n"
    split = line.split
  rescue Error
  end

  #  Add the type so we can load the zone one RRSet at a time.
  type = Types.new(split[3].strip)
  is_soa = (type == Types::SOA)
  type_was = type
  if (type == Types.RRSIG)
    #  If this is an RRSIG record, then add the TYPE COVERED rather than the type - this allows us to load a complete RRSet at a time
    type = Types.new(split[4].strip)
  end

  type_string=prefix_for_rrset_order(type, type_was)
  @last_name = name

  if !([Types::NAPTR, Types::TXT].include?type_was)
    line.sub!("(", "")
    line.sub!(")", "")
  end

  if (is_soa)
    if (@soa_ttl)
      #  Replace the %MISSING_TTL% text with the SOA TTL from the config
      line.sub!(" %MISSING_TTL% ", " #{@soa_ttl} ")
    else
      #  Can we try the @last_explicit_ttl?
      if (@last_explicit_ttl)
        line.sub!(" %MISSING_TTL% ", " #{@last_explicit_ttl} ")
      end
    end
    line = replace_soa_ttl_fields(line)
    if (!@last_explicit_ttl)
      soa_rr = Dnsruby::RR.create(line)
      @last_explicit_ttl = soa_rr.minimum
    end
  end

  line = line.strip

  if (stored_line && stored_line != "")
    line += " " + stored_line.strip
  end

  #  We need to fix up any non-absolute names in the RR
  #  Some RRs have a single name, at the end of the string -
  #    to do these, we can just check the last character for "." and add the
  #    "." + origin string if necessary
  if ([Types::MX, Types::NS, Types::AFSDB, Types::NAPTR, Types::RT,
        Types::SRV, Types::CNAME, Types::MB, Types::MG, Types::MR,
        Types::PTR, Types::DNAME].include?type_was)
    #         if (line[line.length-1, 1] != ".")
    if (!(/\.\z/ =~ line))
      line = line + "." + @origin.to_s
    end
  end
  #  Other RRs have several names. These should be parsed by Dnsruby,
  #    and the names adjusted there.
  if ([Types::MINFO, Types::PX, Types::RP].include?type_was)
    parsed_rr = Dnsruby::RR.create(line)
    case parsed_rr.type
    when Types::MINFO
      if (!parsed_rr.rmailbx.absolute?)
        parsed_rr.rmailbx = parsed_rr.rmailbx.to_s + "." + @origin.to_s
      end
      if (!parsed_rr.emailbx.absolute?)
        parsed_rr.emailbx = parsed_rr.emailbx.to_s + "." + @origin.to_s
      end
    when Types::PX
      if (!parsed_rr.map822.absolute?)
        parsed_rr.map822 = parsed_rr.map822.to_s + "." + @origin.to_s
      end
      if (!parsed_rr.mapx400.absolute?)
        parsed_rr.mapx400 = parsed_rr.mapx400.to_s + "." + @origin.to_s
      end
    when Types::RP
      if (!parsed_rr.mailbox.absolute?)
        parsed_rr.mailbox = parsed_rr.mailbox.to_s + "." + @origin.to_s
      end
      if (!parsed_rr.txtdomain.absolute?)
        parsed_rr.txtdomain = parsed_rr.txtdomain.to_s + "." + @origin.to_s
      end
    end
    line = parsed_rr.to_s
  end
  if (do_prefix_hack)
    return line + "\n", type_string, @last_name
  end
  return line+"\n"
end

#prefix_for_rrset_order(type, type_was) ⇒ Object

This method is included only for OpenDNSSEC support. It should not be

used otherwise.
Frig the RR type so that NSEC records appear last in the RRSets.
Also make sure that DNSKEYs come first (so we have a key to verify
the RRSet with!).


448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/dnsruby/zone_reader.rb', line 448

def prefix_for_rrset_order(type, type_was) # :nodoc: all
  #  Now make sure that NSEC(3) RRs go to the back of the list
  if ['NSEC', 'NSEC3'].include?type.string
    if (type_was == Types::RRSIG)
      #  Get the RRSIG first
      type_string = "ZZ" + type.string
    else
      type_string = "ZZZ" + type.string
    end
  elsif type == Types::DNSKEY
    type_string = "0" + type.string
  elsif type == Types::NS
    #  Make sure that we see the NS records first so we know the delegation status
    type_string = "1" + type.string
  else
    type_string = type.string
  end
  return type_string
end

#process_file(source) ⇒ Object

Takes a filename string, or any type of IO object, and attempts to load a zone.

Returns a list of RRs if successful, nil otherwise.


47
48
49
50
51
52
53
54
55
# File 'lib/dnsruby/zone_reader.rb', line 47

def process_file(source)
  if source.is_a?(String)
    File.open(source) do |file|
      process_io(file)
    end
  else
    process_io(source)
  end
end

#process_io(io) ⇒ Object

Iterate over each line in a IO object, and process it.

Returns a list of RRs if successful, nil otherwise.


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/dnsruby/zone_reader.rb', line 59

def process_io(io)
  zone = nil
  io.each do |line|
    begin
      ret = process_line(line)
      if (ret)
        rr = RR.create(ret)
        if (!zone)
          zone = []
        end
        zone.push(rr)
      end
    rescue Exception
      raise ParseException.new("Error reading line #{io.lineno} of #{io.inspect} : [#{line}]")
    end
  end
  return zone
end

#process_line(line, do_prefix_hack = false) ⇒ Object

Process the next line of the file

Returns a string representing the normalised line.


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
# File 'lib/dnsruby/zone_reader.rb', line 80

def process_line(line, do_prefix_hack = false)
  return nil if (line[0,1] == ";")
  line = strip_comments(line)
  return nil if (line.strip.length == 0)
  return nil if (!line || (line.length == 0))
  @in_quoted_section = false if !@continued_line

  if (line.index("$ORIGIN") == 0)
    @origin = line.split()[1].strip #  $ORIGIN <domain-name> [<comment>]
    #                 print "Setting $ORIGIN to #{@origin}\n"
    return nil
  end
  if (line.index("$TTL") == 0)
    @last_explicit_ttl = get_ttl(line.split()[1].strip) #  $TTL <ttl>
    #                 print "Setting $TTL to #{ttl}\n"
    return nil
  end
  if (@continued_line)
    #  Add the next line until we see a ")"
    #  REMEMBER TO STRIP OFF COMMENTS!!!
    @continued_line = strip_comments(@continued_line)
    line = @continued_line.rstrip.chomp + " " + line
    if (line.index(")"))
      #  OK
      @continued_line = false
    end
  end
  open_bracket = line.index("(")
  if (open_bracket)
    #  Keep going until we see ")"
    index = line.index(")")
    if (index && (index > open_bracket))
      #  OK
      @continued_line = false
    else
      @continued_line = line
    end
  end
  return nil if @continued_line

  line = strip_comments(line) + "\n"

  #  If SOA, then replace "3h" etc. with expanded seconds
  #       begin
  return normalise_line(line, do_prefix_hack)
  #       rescue Exception => e
  #         print "ERROR parsing line #{@line_num} : #{line}\n"
  #         return "\n", Types::ANY
  #       end
end

#process_quotes(section) ⇒ Object



199
200
201
202
203
204
205
206
207
# File 'lib/dnsruby/zone_reader.rb', line 199

def process_quotes(section)
  #  Look through the section of text and set the @in_quoted_section
  #  as it should be at the end of the given section
  last_index = 0
  while (next_index = section.index("\"", last_index + 1))
    @in_quoted_section = !@in_quoted_section
    last_index = next_index
  end
end

#replace_soa_ttl_fields(line) ⇒ Object



432
433
434
435
436
437
438
439
440
441
# File 'lib/dnsruby/zone_reader.rb', line 432

def replace_soa_ttl_fields(line)
  #  Replace any fields which evaluate to 0
  split = line.split
  4.times {|i|
    x = i + 7
    split[x].strip!
    split[x] = get_ttl(split[x]).to_s
  }
  return split.join(" ") + "\n"
end

#strip_comments(line) ⇒ Object



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
# File 'lib/dnsruby/zone_reader.rb', line 131

def strip_comments(line)
  last_index = 0
  #  Are we currently in a quoted section?
  #  Does a quoted section begin or end in this line?
  #  Are there any semi-colons?
  #  Ary any of the semi-colons inside a quoted section?
  #  Handle escape characters
  if (line.index"\\")
    return strip_comments_meticulously(line)
  end
  while (next_index = line.index(";", last_index + 1))
    #  Have there been any quotes since we last looked?
    process_quotes(line[last_index, next_index - last_index])

    #  Now use @in_quoted_section to work out if the ';' terminates the line
    if (!@in_quoted_section)
      return line[0,next_index]
    end

    last_index = next_index
  end
  #  Check out the quote situation to the end of the line
  process_quotes(line[last_index, line.length-1])

  return line
end

#strip_comments_meticulously(line) ⇒ Object



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
# File 'lib/dnsruby/zone_reader.rb', line 158

def strip_comments_meticulously(line)
  #  We have escape characters in the text. Go through it character by
  #  character and work out what's escaped and quoted and what's not
  escaped = false
  quoted = false
  pos = 0
  line.each_char {|c|
    if (c == "\\")
      if (!escaped)
        escaped = true
      else
        escaped = false
      end
    else
      if (escaped)
        if (c >= "0" && c <= "9") # rfc 1035 5.1 \DDD
          pos = pos + 2
        end
        escaped = false
        next
      else
        if (c == "\"")
          if (quoted)
            quoted = false
          else
            quoted = true
          end
        else
          if (c == ";")
            if (!quoted)
              return line[0, pos+1]
            end
          end
        end
      end
    end
    pos +=1
  }
  return line
end