zonefile 1.00

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+