zonefile 1.00

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,263 @@
1
+ #
2
+ # = Ruby Zonefile - Parse and manipulate DNS Zone Files.
3
+ #
4
+ # == Description
5
+ # This class can read, manipulate and create DNS zone files. It supports A, AAAA, MX, NS, SOA,
6
+ # TXT, CNAME and SRV records. The data can be accessed by the instance method of the same
7
+ # name. All except SOA return an array of hashes containing the named data. SOA directly returns the
8
+ # hash since there can only be one SOA information.
9
+ #
10
+ # The following hash keys are returned per record type:
11
+ #
12
+ # * SOA
13
+ # - :ttl, :primary, :email, :serial, :refresh, :retry, :expire, :minimumTTL
14
+ # * A
15
+ # - :name, :ttl, :class, :host
16
+ # * MX
17
+ # - :name, :ttl, :class, :pri, :host
18
+ # * NS
19
+ # - :name, :ttl, :class, :host
20
+ # * CNAME
21
+ # - :name, :ttl, :class, :host
22
+ # * TXT
23
+ # - :name, :ttl, :class, :text
24
+ # * A4 (AAAA)
25
+ # - :name, :ttl, :class, :host
26
+ # * SRV
27
+ # - :name, :ttl, :class, :pri, :weight, :port, :host
28
+ #
29
+ # == Examples
30
+ #
31
+ # === Read a Zonefile
32
+ #
33
+ # zf = Zonefile.from_file('/path/to/zonefile.db')
34
+ #
35
+ # # Display MX-Records
36
+ # zf.mx.each do |mx_record|
37
+ # puts "Mail Exchagne with priority: #{mx_record[:pri]} --> #{mx_record[:host]}"
38
+ # end
39
+ #
40
+ # # Show SOA TTL
41
+ # puts "Record Time To Live: #{zf.soa[:ttl]}"
42
+ #
43
+ # # Show A-Records
44
+ # zf.a.each do |a_record|
45
+ # puts "#{a_record[:name]} --> #{a_record[:host]}"
46
+ # end
47
+ #
48
+ #
49
+ # ==== Manipulate a Zonefile
50
+ #
51
+ # zf = Zonefile.from_file('/path/to/zonefile.db')
52
+ #
53
+ # # Change TTL and add an A-Record
54
+ #
55
+ # zf.soa[:ttl] = '123123' # Change the SOA ttl
56
+ # zf.a << { :class => 'IN', :name => 'www', :host => '192.168.100.1', :ttl => 3600 } # add A-Record
57
+ #
58
+ # # Increase Serial Number
59
+ # zf.new_serial
60
+ #
61
+ # # Print new zonefile
62
+ # puts "New Zonefile: \n#{zf.output}"
63
+ #
64
+ # == Author
65
+ #
66
+ # Martin Boese, based on Simon Flack Perl library DNS::ZoneParse
67
+ #
68
+
69
+ class Zonefile
70
+
71
+ RECORDS = %w{ mx a a4 ns cname txt ptr srv soa }
72
+ attr :records
73
+ attr :soa
74
+ attr :data
75
+
76
+ def method_missing(m)
77
+ return super unless RECORDS.include?(m.to_s)
78
+ @records[m]
79
+ end
80
+
81
+
82
+ # Compact a zonefile content - removes empty lines, comments,
83
+ # converts tabs into spaces etc...
84
+ def self.simplify(zf)
85
+ # concatenate everything split over multiple lines in parentheses - remove ;-comments in block
86
+ zf = zf.gsub(/(\([^\)]*?\))/) { |m| m.split(/\n/).map { |l| l.gsub(/\;.*$/, '') }.join("\n").gsub(/[\r\n]/, '') }
87
+
88
+ zf.split(/\n/).map do |line|
89
+ r = line.gsub(/\t/, ' ')
90
+ r = r.gsub(/\s+/, ' ')
91
+ r = r.gsub(/\;.*$/, '')
92
+ end.delete_if { |line| line.empty? || line[0].chr == ';'}.join("\n")
93
+ end
94
+
95
+
96
+ # create a new zonefile object by passing the content of the zonefile
97
+ def initialize(zonefile = '', file_name= nil, origin= nil)
98
+ @data = zonefile
99
+ @filename = file_name
100
+ @origin = origin
101
+
102
+ @records = {}
103
+ @soa = {}
104
+ RECORDS.each { |r| @records[r.intern] = [] }
105
+ parse
106
+ end
107
+
108
+ # Create a new object by reading the content of a file
109
+ def self.from_file(file_name, origin = nil)
110
+ Zonefile.new(File.read(file_name), file_name.split('/').last, origin)
111
+ end
112
+
113
+ def add_record(type, data= {})
114
+ @records[type.downcase.intern] << data
115
+ end
116
+
117
+ # Generates a new serial number in the format of YYYYMMDDII if possible
118
+ def new_serial
119
+ base = "%04d%02d%02d" % [Time.now.year, Time.now.month, Time.now.day ]
120
+
121
+ if ((self.soa[:serial].to_i / 100) > base.to_i) then
122
+ ns = self.soa[:serial].to_i + 1
123
+ self.soa[:serial] = ns.to_s
124
+ return ns.to_s
125
+ end
126
+
127
+ ii = 0
128
+ while (("#{base}%02d" % ii).to_i <= self.soa[:serial].to_i) do
129
+ ii += 1
130
+ end
131
+ self.soa[:serial] = "#{base}%02d" % ii
132
+ end
133
+
134
+ def parse_line(line)
135
+ valid_name = /[\@a-z_\-\.0-9\*]+/i
136
+ valid_ip6 = /[\@a-z_\-\.0-9\*:]+/i
137
+ rr_class = /\b(?:IN|HS|CH)\b/i
138
+ rr_type = /\b(?:NS|A|CNAME)\b/i
139
+ rr_ttl = /(?:\d+[wdhms]?)+/i
140
+ ttl_cls = Regexp.new("(?:(#{rr_ttl})\s)?(?:(#{rr_class})\s)?")
141
+
142
+ data = {}
143
+ if line =~ /^(#{valid_name})? \s*
144
+ #{ttl_cls}
145
+ (#{rr_type}) \s
146
+ (#{valid_name})
147
+ /ix then
148
+ (name, ttl, dclass, type, host) = [$1, $2, $3, $4, $5]
149
+ add_record($4, :name => $1, :ttl => $2, :class => $3, :host => $5)
150
+ elsif line=~/^(#{valid_name})? \s*
151
+ #{ttl_cls}
152
+ AAAA \s
153
+ (#{valid_ip6})
154
+ /x then
155
+ add_record('a4', :name => $1, :ttl => $2, :class => $3, :host => $4)
156
+ elsif line=~/^(#{valid_name})? \s*
157
+ #{ttl_cls}
158
+ MX \s
159
+ (\d+) \s
160
+ (#{valid_name})
161
+ /ix then
162
+ add_record('mx', :name => $1, :ttl => $2, :class => $3, :pri => $4.to_i, :host => $5)
163
+ elsif line=~/^(#{valid_name})? \s*
164
+ #{ttl_cls}
165
+ SRV \s
166
+ (\d+) \s
167
+ (\d+) \s
168
+ (\d+) \s
169
+ (#{valid_name})
170
+ /ix
171
+ add_record('srv', :name => $1, :ttl => $2, :class => $3, :pri => $4, :weight => $5,
172
+ :port => $6, :host => $7)
173
+ elsif line=~/^(#{valid_name}) \s+
174
+ #{ttl_cls}
175
+ SOA \s+
176
+ (#{valid_name}) \s+
177
+ (#{valid_name}) \s*
178
+ \(?\s*
179
+ (#{rr_ttl}) \s+
180
+ (#{rr_ttl}) \s+
181
+ (#{rr_ttl}) \s+
182
+ (#{rr_ttl}) \s+
183
+ (#{rr_ttl}) \s*
184
+ \)?
185
+ /ix
186
+ ttl = @soa[:ttl] || $2 || ''
187
+ @soa[:origin] = $1
188
+ @soa[:ttl] = ttl
189
+ @soa[:primary] = $4
190
+ @soa[:email] = $5
191
+ @soa[:serial] = $6
192
+ @soa[:refresh] = $7
193
+ @soa[:retry] = $8
194
+ @soa[:expire] = $9
195
+ @soa[:minimumTTL] = $10
196
+
197
+ elsif line=~ /^(#{valid_name})? \s*
198
+ #{ttl_cls}
199
+ PTR \s+
200
+ (#{valid_name})
201
+ /ix
202
+ add_record('ptr', :name => $1, :class => $3, :ttl => $2, :host => $4)
203
+ elsif line =~ /(#{valid_name})? \s #{ttl_cls} TXT \s \"([^\"]*)\"/ix
204
+ add_record('txt', :name => $1, :ttl => $2, :class => $3, :text => $4)
205
+ elsif line =~ /\$TTL\s+(#{rr_ttl})/i
206
+ @soa[:ttl] = $1
207
+ end
208
+ end
209
+
210
+ def parse
211
+ Zonefile.simplify(@data).each_line do |line|
212
+ parse_line(line)
213
+ end
214
+ end
215
+
216
+
217
+ # Build a new nicely formatted Zonefile
218
+ #
219
+ def output
220
+ out =<<-ENDH
221
+ ;
222
+ ; Database file #{@filename || 'unknown'} for #{@origin || 'unknown'} zone.
223
+ ; Zone version: #{self.soa[:serial]}
224
+ ;
225
+ #{self.soa[:ttl] ? "$TTL #{self.soa[:ttl]}" : ''}
226
+ #{self.soa[:origin]} #{self.soa[:ttl]} IN SOA #{self.soa[:primary]} #{self.soa[:email]} (
227
+ #{self.soa[:serial]} ; serial number
228
+ #{self.soa[:refresh]} ; refresh
229
+ #{self.soa[:retry]} ; retry
230
+ #{self.soa[:expire]} ; expire
231
+ #{self.soa[:minimumTTL]} ; minimum TTL
232
+ )
233
+ ; Zone NS Records
234
+ ENDH
235
+ self.ns.each do |ns|
236
+ out << "#{ns[:name]} #{ns[:ttl]} #{ns[:class]} NS #{ns[:host]}\n"
237
+ end
238
+ out << "\n; Zone MX Records\n" unless self.mx.empty?
239
+ self.mx.each do |mx|
240
+ out << "#{mx[:name]} #{mx[:ttl]} #{mx[:class]} MX #{mx[:pri]} #{mx[:host]}\n"
241
+ end
242
+
243
+ self.a.each do |a|
244
+ out << "#{a[:name]} #{a[:ttl]} #{a[:class]} A #{a[:host]}\n"
245
+ end
246
+ self.cname.each do |cn|
247
+ out << "#{cn[:name]} #{cn[:ttl]} #{cn[:class]} CNAME #{cn[:host]}\n"
248
+ end
249
+ self.a4.each do |a4|
250
+ out << "#{a4[:name]} #{a4[:ttl]} #{a4[:class]} AAAA #{a4[:host]}\n"
251
+ end
252
+ self.txt.each do |tx|
253
+ out << "#{tx[:name]} #{tx[:ttl]} #{tx[:class]} TXT \"#{tx[:text]}\"\n"
254
+ end
255
+ self.srv.each do |srv|
256
+ out << "#{srv[:name]} #{srv[:ttl]} #{srv[:class]} SRV #{srv[:pri]} #{srv[:weight]} #{srv[:port]} #{srv[:host]}\n"
257
+ end
258
+
259
+ out
260
+ end
261
+
262
+ end
263
+
data/lib/zonefile.rb ADDED
@@ -0,0 +1 @@
1
+ require 'zonefile/zonefile'
@@ -0,0 +1,32 @@
1
+ ; Database file dns-zoneparse-test.net.dns for dns-zoneparse-test.net zone.
2
+ ; Zone version: 2000100501
3
+ $TTL 1H
4
+ @ 3600 IN SOA ns0.dns-zoneparse-test.net. support.dns-zoneparse-test.net. (
5
+ 2000100501 ; serial number
6
+ 10800 ; refresh
7
+ 3600 ; retry
8
+ 691200 ; expire
9
+ 86400 ) ; minimum TTL
10
+
11
+ 43200 IN NS ns0.dns-zoneparse-test.net. ; ( A multi line
12
+ comment )
13
+ @ IN NS ns1.dns-zoneparse-test.net.
14
+
15
+ @ IN A 127.0.0.1
16
+ @ IN MX 10 mail
17
+ ftp IN CNAME www
18
+ localhost IN A 127.0.0.1
19
+ mail IN A 127.0.0.1
20
+ www IN A 127.0.0.1
21
+ in a 10.0.0.2
22
+ 43200 IN A 10.0.0.3
23
+ IN MX 10 10.0.0.4
24
+ A 10.0.0.5
25
+ TXT "web server"
26
+ foo IN A 10.0.0.6
27
+ mini A 10.0.0.7
28
+ icarus IN AAAA fe80::0260:83ff:fe7c:3a2a
29
+ soup IN TXT "This is a text message"
30
+ txta TXT "This is another text message"
31
+ _sip._tcp.example.com. 86400 IN SRV 0 5 5060 sipserver.example.com.
32
+
data/tests/zonefile.rb ADDED
@@ -0,0 +1,141 @@
1
+ require 'test/unit'
2
+
3
+
4
+ $: << File.expand_path(File.dirname(__FILE__) + '/../lib')
5
+
6
+ require 'zonefile'
7
+
8
+ $zonefile = ARGV[0] || 'test-zone.db'
9
+
10
+ class TC_Zonefile < Test::Unit::TestCase
11
+
12
+ def setup
13
+ @zf = Zonefile.from_file(File.dirname(__FILE__) + '/'+$zonefile, 'test-origin')
14
+ end
15
+
16
+ def swap # generate output and re-read @zf from it
17
+ @zf = Zonefile.new(@zf.output, 'test-origin')
18
+ end
19
+
20
+ def test_soa
21
+ assert_equal '86400', @zf.soa[:minimumTTL]
22
+ assert_equal '691200', @zf.soa[:expire]
23
+ assert_equal '3600', @zf.soa[:retry]
24
+ assert_equal '10800', @zf.soa[:refresh]
25
+ assert_equal '2000100501', @zf.soa[:serial]
26
+ assert_equal 'support.dns-zoneparse-test.net.', @zf.soa[:email]
27
+ assert_equal 'ns0.dns-zoneparse-test.net.', @zf.soa[:primary]
28
+
29
+ begin
30
+ @swap_soa = true
31
+ swap
32
+ test_soa
33
+ end unless @swap_soa
34
+ end
35
+
36
+ def test_a
37
+ assert_equal 9, @zf.a.size
38
+ assert_equal 'mini', @zf.a.last[:name]
39
+ assert_equal '10.0.0.7', @zf.a.last[:host]
40
+ assert_equal '127.0.0.1', @zf.a.first[:host]
41
+
42
+ a = @zf.a.find { |a| a[:host] == '10.0.0.3'}
43
+ assert_equal '43200', a[:ttl]
44
+ assert_equal '', a[:name].to_s
45
+
46
+ begin
47
+ @swap_a = true
48
+ swap
49
+ test_a
50
+ end unless @swap_a
51
+ end
52
+
53
+ def test_mx
54
+ assert_equal 2, @zf.mx.size
55
+ assert_equal 10, @zf.mx.first[:pri]
56
+ begin
57
+ @swap_mx = true
58
+ swap
59
+ test_mx
60
+ end unless @swap_mx
61
+ end
62
+
63
+ def test_cname
64
+ assert !!@zf.cname.find { |e| e[:host] == 'www' }
65
+ begin
66
+ @swap_cname = true
67
+ swap
68
+ test_cname
69
+ end unless @swap_cname
70
+ end
71
+
72
+ def test_ns
73
+ assert_equal 'ns0.dns-zoneparse-test.net.', @zf.ns[0][:host]
74
+ assert_equal 'ns1.dns-zoneparse-test.net.', @zf.ns[1][:host]
75
+ begin
76
+ @swap_ns = true
77
+ swap
78
+ test_ns
79
+ end unless @swap_ns
80
+ end
81
+
82
+ def test_txt
83
+ assert_equal 'web server', @zf.txt[0][:text]
84
+ assert_equal 'IN', @zf.txt[1][:class]
85
+ assert_equal 'soup', @zf.txt[1][:name]
86
+ assert_equal 'txta', @zf.txt[2][:name]
87
+ assert_equal 3, @zf.txt.size
88
+ begin
89
+ @swap_txt = true
90
+ swap
91
+ test_txt
92
+ end unless @swap_txt
93
+ end
94
+
95
+ def test_a4
96
+ assert_equal 'icarus', @zf.a4[0][:name]
97
+ assert_equal 'IN', @zf.a4[0][:class]
98
+ assert_equal 1, @zf.a4.size
99
+ assert_equal 'fe80::0260:83ff:fe7c:3a2a', @zf.a4[0][:host]
100
+ begin
101
+ @swap_a4 = true
102
+ swap
103
+ test_a4
104
+ end unless @swap_a4
105
+ end
106
+
107
+ def test_srv
108
+ assert_equal '_sip._tcp.example.com.', @zf.srv[0][:name]
109
+ assert_equal '86400', @zf.srv[0][:ttl]
110
+ assert_equal '0', @zf.srv[0][:pri]
111
+ assert_equal '5', @zf.srv[0][:weight]
112
+ assert_equal '5060', @zf.srv[0][:port]
113
+ assert_equal 'sipserver.example.com.', @zf.srv[0][:host]
114
+ begin
115
+ @swap_srv = true
116
+ swap
117
+ test_srv
118
+ end unless @swap_srv
119
+ end
120
+
121
+ def test_serial_generator
122
+ old = @zf.soa[:serial]
123
+ new = @zf.new_serial
124
+ assert new.to_i > old.to_i
125
+ newer = @zf.new_serial
126
+ assert newer.to_i - 1, new
127
+
128
+ @zf.soa[:serial] = '9999889901'
129
+ @zf.new_serial
130
+ assert_equal '9999889902', @zf.soa[:serial]
131
+
132
+ end
133
+
134
+
135
+ def test_output
136
+ # puts @zf.data
137
+ # puts '==================='
138
+ # puts @zf.output
139
+ end
140
+
141
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zonefile
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.00"
5
+ platform: ruby
6
+ authors:
7
+ - Martin Boese
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-04 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: martin@internet.ao
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/zonefile
26
+ - lib/zonefile/zonefile.rb
27
+ - lib/zonefile.rb
28
+ - tests/test-zone.db
29
+ - tests/zonefile.rb
30
+ has_rdoc: true
31
+ homepage: http://zonefile.rubyforge.org/
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project: zonefile
52
+ rubygems_version: 1.2.0
53
+ signing_key:
54
+ specification_version: 2
55
+ summary: BIND 8/9 Zonefile Reader and Writer
56
+ test_files: []
57
+