Class: DNS::Zone

Inherits:
Object
  • Object
show all
Defined in:
lib/dns/zone.rb,
lib/dns/zone/rr.rb,
lib/dns/zone/version.rb

Overview

Represents a 'whole' zone of many resource records (RRs).

This is also the primary namespace for the `dns-zone` gem.

Defined Under Namespace

Modules: RR Classes: TestCase

Constant Summary collapse

Version =

Version number (major.minor.tiny)

'0.3.1'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeZone

Create an empty instance of a DNS zone that you can drive programmatically.


22
23
24
25
26
27
28
29
30
31
# File 'lib/dns/zone.rb', line 22

def initialize
  @records = []
  soa = DNS::Zone::RR::SOA.new
  # set a couple of defaults on the SOA
  soa.serial = Time.now.utc.strftime("%Y%m%d01")    
  soa.refresh_ttl = '3h'
  soa.retry_ttl = '15m'
  soa.expiry_ttl = '4w'
  soa.minimum_ttl = '30m'
end

Instance Attribute Details

#originObject

The primary $ORIGIN (directive) of the zone.


15
16
17
# File 'lib/dns/zone.rb', line 15

def origin
  @origin
end

#recordsObject

Array of all the zones RRs (including the SOA).


17
18
19
# File 'lib/dns/zone.rb', line 17

def records
  @records
end

#ttlObject

The default $TTL (directive) of the zone.


13
14
15
# File 'lib/dns/zone.rb', line 13

def ttl
  @ttl
end

Class Method Details

.extract_entries(string) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Extract entries from a zone file that will be later parsed as RRs.


123
124
125
126
127
128
129
130
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
157
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
# File 'lib/dns/zone.rb', line 123

def self.extract_entries(string)
  # FROM RFC:
  #     The format of these files is a sequence of entries.  Entries are
  #     predominantly line-oriented, though parentheses can be used to continue
  #     a list of items across a line boundary, and text literals can contain
  #     CRLF within the text.  Any combination of tabs and spaces act as a
  #     delimiter between the separate items that make up an entry.  The end of
  #     any line in the master file can end with a comment.  The comment starts
  #     with a ";" (semicolon). 

  entries = []
  mode = :line
  entry = ''

  parentheses_ref_count = 0

  string.lines.each do |line|
    # strip comments unless escaped
    # strip comments, unless its escaped.
    # skip semicolons within "quote segments" (TXT records)
    line = line.gsub(/((?<!\\);)(?=(?:[^"]|"[^"]*")*$).*/o, "").chomp

    next if line.gsub(/\s+/, '').empty?

    # append to entry line
    entry << line

    quotes = entry.count('"')
    has_quotes = quotes > 0

    parentheses = entry.count('()')
    has_parentheses = parentheses > 0

    if has_quotes
      character_strings = entry.scan(/("(?:[^"\\]+|\\.)*")/).join(' ')
      without = entry.gsub(/"((?:[^"\\]+|\\.)*)"/, '')
      parentheses_ref_count = without.count('(') - without.count(')')
    else
      parentheses_ref_count = entry.count('(') - entry.count(')')
    end

    # are parentheses balanced?
    if parentheses_ref_count == 0
      if has_quotes
        without.gsub!(/[()]/, '')
        without.gsub!(/[ ]{2,}/, '  ')
        #entries << (without + character_strings)
        entry = (without + character_strings)
      else
        entry.gsub!(/[()]/, '')
        entry.gsub!(/[ ]{2,}/, '  ')
        entry.gsub!(/[ ]+$/, '')
        #entries << entry
      end
      entries << entry
      entry = ''
    end

  end

  return entries
end

.load(string, default_origin = "") ⇒ Object

Load the provided zone file data into a new DNS::Zone object.


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
# File 'lib/dns/zone.rb', line 84

def self.load(string, default_origin = "")
  # get entries
  entries = self.extract_entries(string)

  instance = self.new

  options = {}
  entries.each do |entry|
    # read in special statments like $TTL and $ORIGIN
    if entry =~ /\$(ORIGIN|TTL)\s+(.+)/
      instance.ttl    = $2 if $1 == 'TTL'
      if $1 == 'ORIGIN'
        instance.origin ||= $2
        options[:origin] ||= $2
        options[:last_origin] = $2
      end
      next
    end

    # parse each RR and create a Ruby object for it
    if entry =~ DNS::Zone::RR::REGEX_RR
      rec = DNS::Zone::RR.load(entry, options)
      next unless rec
      instance.records << rec
      options[:last_label] = rec.label
    end
  end

  # use default_origin if we didn't see a ORIGIN directive in the zone
  if instance.origin.to_s.empty? && !default_origin.empty?
    instance.origin = default_origin
  end

  return instance
end

Instance Method Details

#dumpObject

Generates output of the zone and its records.


55
56
57
58
59
60
61
62
63
# File 'lib/dns/zone.rb', line 55

def dump
  content = []

  @records.each do |rr|
    content << rr.dump
  end

  content.join("\n") << "\n"
end

#dump_prettyObject

Generates pretty output of the zone and its records.


68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/dns/zone.rb', line 68

def dump_pretty
  content = []

  last_type = "SOA"
  sorted_records.each do |rr|
    content << '' if last_type != rr.type
    content << rr.dump
    last_type = rr.type
  end

  content.join("\n") << "\n"
end

#soaObject

Helper method to access the zones SOA RR.


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/dns/zone.rb', line 36

def soa
  # return the first SOA we find in the records array.
  rr = @records.find { |rr| rr.type == "SOA" }
  return rr if rr
  # otherwise create a new SOA
  rr = DNS::Zone::RR::SOA.new
  rr.serial = Time.now.utc.strftime("%Y%m%d01")    
  rr.refresh_ttl = '3h'
  rr.retry_ttl = '15m'
  rr.expiry_ttl = '4w'
  rr.minimum_ttl = '30m'
  # store and return new SOA
  @records << rr
  return rr
end