zonesync 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da2a32c21af6c64a37bdb9a998089318ba12cbf3e20e0699a757adafe23d6e29
4
- data.tar.gz: 6614061b9bc864e1f5a6219d25ac96d93f45cf5a98a9234c75bc7241e51eb909
3
+ metadata.gz: 9f636466fb4175eab983daffca93a4f3e09447bd49a4b50944078034c9067b46
4
+ data.tar.gz: 462dcac524149c882e25e8f1962c9b29cc376177448b5a8320241cb135a83b01
5
5
  SHA512:
6
- metadata.gz: c79134908c21f775a9aa58eccadd072684d0657b3e23bf9b953a050ef4c558cd557f9bbed9c57a522c7ab3e0521bd0a3254c19c8b2415a36c25e236be242a74c
7
- data.tar.gz: 17d1e8928f168ea9bd4e93a9add850c2c5e7e7c168d3982d133817bdef63a9c085ae4d2b108aa8b51f1d5cab692f597d8d035aece15c7b44fa2547ad7baa0f91
6
+ metadata.gz: '0628bc131c08e8432e82c7b1f9a94587224871434df282ad52adc3cc1fe04b755c2b73ba849ee707a4c9c3ab6dfb69cd99b1289e6cfde41df9c72b1ccc874485'
7
+ data.tar.gz: 4f55bace0450c64863dccbe394d160b00197db1294cc227e2fa119310c89415c5b6bf4ee52a9bd2d1e68a9aa362376e3079552947cc9703df3ee4e562d8e5fff
data/.rspec CHANGED
@@ -1,2 +1,2 @@
1
1
  --color
2
- --require debug
2
+ --require spec_helper
data/Gemfile CHANGED
@@ -4,3 +4,4 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem "debug"
7
+ gem "dns-zonefile", path: "../dns-zonefile"
data/README.md CHANGED
@@ -48,8 +48,11 @@ wwwtest CNAME www
48
48
  mail A 192.0.2.3
49
49
  mail2 A 192.0.2.4
50
50
  mail3 A 192.0.2.5
51
+ ignore A 192.0.2.6 ; zonesync: ignore
51
52
  ```
52
53
 
54
+ Note that records with a comment containing "zonesync: ignore" will not be touched during the sync. I'm considering inverting this from a blacklist to a whitelist in a future version, to avoid stomping on collaborators' records.
55
+
53
56
  ### DNS Host
54
57
 
55
58
  We need to tell `zonesync` about our DNS host by building a small YAML file. The structure of this file will depend on your DNS host, so here are some examples:
@@ -19,6 +19,7 @@ module Zonesync
19
19
  type: new_record[:type],
20
20
  ttl: new_record[:ttl],
21
21
  content: new_record[:rdata],
22
+ comment: new_record[:comment],
22
23
  })
23
24
  end
24
25
 
@@ -28,6 +29,7 @@ module Zonesync
28
29
  type: record[:type],
29
30
  ttl: record[:ttl],
30
31
  content: record[:rdata],
32
+ comment: record[:comment],
31
33
  })
32
34
  end
33
35
 
@@ -55,6 +57,7 @@ module Zonesync
55
57
  attrs["type"],
56
58
  attrs["ttl"].to_i,
57
59
  rdata,
60
+ attrs["comment"],
58
61
  ).to_h
59
62
  end
60
63
 
@@ -1,5 +1,5 @@
1
- require "dns/zonefile"
2
1
  require "zonesync/record"
2
+ require "zonesync/zonefile"
3
3
 
4
4
  module Zonesync
5
5
  class Provider < Struct.new(:credentials)
@@ -13,6 +13,8 @@ module Zonesync
13
13
  Record.from_dns_zonefile_record(record)
14
14
  end.select do |record|
15
15
  %w[A AAAA CNAME MX TXT SPF NAPTR PTR].include?(record.type)
16
+ end.reject do |record|
17
+ record.comment.to_s.downcase.include? "zonesync: ignore"
16
18
  end.sort
17
19
  end
18
20
 
@@ -21,7 +23,7 @@ module Zonesync
21
23
  if body !~ /\sSOA\s/ # insert dummy SOA to trick parser if needed
22
24
  body.sub!(/\n([^$])/, "\n@ 1 SOA example.com example.com ( 2000010101 1 1 1 1 )\n\\1")
23
25
  end
24
- DNS::Zonefile.load(body)
26
+ Zonefile.load(body)
25
27
  end
26
28
 
27
29
  def read record
@@ -1,5 +1,5 @@
1
1
  module Zonesync
2
- class Record < Struct.new(:name, :type, :ttl, :rdata)
2
+ class Record < Struct.new(:name, :type, :ttl, :rdata, :comment)
3
3
  def self.from_dns_zonefile_record record
4
4
  type = record.class.name.split("::").last
5
5
  rdata = case type
@@ -23,6 +23,7 @@ module Zonesync
23
23
  type,
24
24
  record.ttl,
25
25
  rdata,
26
+ record.comment,
26
27
  )
27
28
  end
28
29
 
@@ -35,7 +36,9 @@ module Zonesync
35
36
  end
36
37
 
37
38
  def to_s
38
- values.join(" ")
39
+ string = [name, type, ttl, rdata].join(" ")
40
+ string << " ; #{comment}" if comment
41
+ string
39
42
  end
40
43
  end
41
44
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zonesync
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -0,0 +1,318 @@
1
+ require "treetop"
2
+ Treetop.load File.join(__dir__, "zonefile")
3
+
4
+ module Zonesync
5
+ module Zonefile
6
+ class << self
7
+ def parse(zone_string)
8
+ parser = ZonefileParser.new
9
+ result = parser.parse(zone_string)
10
+ return result if result
11
+ raise ParsingError, parser.failure_reason
12
+ end
13
+
14
+ def load(zone_string, alternate_origin = nil)
15
+ Zone.new(parse(zone_string).entries, alternate_origin)
16
+ end
17
+ end
18
+
19
+ class ParsingError < RuntimeError; end
20
+ class UnknownRecordType < RuntimeError; end
21
+ class Zone
22
+ attr_reader :origin
23
+ attr_reader :records
24
+
25
+ def initialize(entries, alternate_origin = nil)
26
+ alternate_origin ||= "."
27
+ @records = []
28
+ @vars = {"origin" => alternate_origin, :last_host => "."}
29
+ entries.each do |e|
30
+ case e.parse_type
31
+ when :variable
32
+ key = e.name.text_value.downcase
33
+ @vars[key] = case key
34
+ when "ttl"
35
+ e.value.text_value.to_i
36
+ else
37
+ e.value.text_value
38
+ end
39
+ when :soa
40
+ @records << SOA.new(@vars, e)
41
+ when :record
42
+ case e.record_type
43
+ when "A" then @records << A.new(@vars, e)
44
+ when "AAAA" then @records << AAAA.new(@vars, e)
45
+ when "CAA" then @records << CAA.new(@vars, e)
46
+ when "CNAME" then @records << CNAME.new(@vars, e)
47
+ when "MX" then @records << MX.new(@vars, e)
48
+ when "NAPTR" then @records << NAPTR.new(@vars, e)
49
+ when "NS" then @records << NS.new(@vars, e)
50
+ when "PTR" then @records << PTR.new(@vars, e)
51
+ when "SRV" then @records << SRV.new(@vars, e)
52
+ when "SPF" then @records << SPF.new(@vars, e)
53
+ when "SSHFP" then @records << SSHFP.new(@vars, e)
54
+ when "TXT" then @records << TXT.new(@vars, e)
55
+ when "SOA" then
56
+ # No-op
57
+ else
58
+ raise UnknownRecordType, "Unknown record type: #{e.record_type}"
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def soa
65
+ records_of(SOA).first
66
+ end
67
+
68
+ def records_of(kl)
69
+ @records.select { |r| r.instance_of? kl }
70
+ end
71
+ end
72
+
73
+ class Record
74
+ # assign, with handling for global TTL
75
+ def self.writer_for_ttl(*attribs)
76
+ attribs.each do |attrib|
77
+ define_method "#{attrib}=" do |val|
78
+ instance_variable_set("@#{attrib}", val || @vars["ttl"])
79
+ end
80
+ end
81
+ end
82
+
83
+ attr_reader :ttl
84
+ attr_writer :klass
85
+ writer_for_ttl :ttl
86
+
87
+ def klass
88
+ @klass = nil if @klass == ""
89
+ @klass ||= "IN"
90
+ end
91
+
92
+ attr_accessor :comment
93
+
94
+ private
95
+
96
+ def qualify_host(host)
97
+ origin = vars["origin"]
98
+ host = vars[:last_host] if /^\s*$/.match?(host)
99
+ host = host.gsub(/@/, origin)
100
+ if /\.$/.match?(host)
101
+ host
102
+ elsif /^\./.match?(origin)
103
+ host + origin
104
+ else
105
+ host + "." + origin
106
+ end
107
+ end
108
+ attr_accessor :vars
109
+ end
110
+
111
+ class SOA < Record
112
+ attr_accessor :origin, :nameserver, :responsible_party, :serial, :refresh_time, :retry_time, :expiry_time, :nxttl
113
+
114
+ def initialize(vars, zonefile_soa = nil)
115
+ @vars = vars
116
+ if zonefile_soa
117
+ self.origin = qualify_host(zonefile_soa.origin.to_s)
118
+ @vars[:last_host] = origin
119
+ self.ttl = zonefile_soa.ttl.to_i
120
+ self.klass = zonefile_soa.klass.to_s
121
+ self.nameserver = qualify_host(zonefile_soa.ns.to_s)
122
+ self.responsible_party = qualify_host(zonefile_soa.rp.to_s)
123
+ self.serial = zonefile_soa.serial.to_i
124
+ self.refresh_time = zonefile_soa.refresh.to_i
125
+ self.retry_time = zonefile_soa.reretry.to_i
126
+ self.expiry_time = zonefile_soa.expiry.to_i
127
+ self.nxttl = zonefile_soa.nxttl.to_i
128
+ end
129
+ end
130
+ end
131
+
132
+ class A < Record
133
+ attr_accessor :host, :address
134
+
135
+ def initialize(vars, zonefile_record)
136
+ @vars = vars
137
+ if zonefile_record
138
+ self.host = qualify_host(zonefile_record.host.to_s)
139
+ @vars[:last_host] = host
140
+ self.ttl = zonefile_record.ttl.to_i
141
+ self.klass = zonefile_record.klass.to_s
142
+ self.address = zonefile_record.ip_address.to_s
143
+ self.comment = zonefile_record.comment&.to_s
144
+ end
145
+ end
146
+ end
147
+
148
+ class AAAA < A
149
+ end
150
+
151
+ class CAA < Record
152
+ attr_accessor :host, :flags, :tag, :value
153
+
154
+ def initialize(vars, zonefile_record)
155
+ @vars = vars
156
+ if zonefile_record
157
+ self.host = qualify_host(zonefile_record.host.to_s)
158
+ @vars[:last_host] = host
159
+ self.ttl = zonefile_record.ttl.to_i
160
+ self.klass = zonefile_record.klass.to_s
161
+ self.flags = zonefile_record.flags.to_i
162
+ self.tag = zonefile_record.tag.to_s
163
+ self.value = zonefile_record.value.to_s
164
+ self.comment = zonefile_record.comment&.to_s
165
+ end
166
+ end
167
+ end
168
+
169
+ class CNAME < Record
170
+ attr_accessor :host, :domainname
171
+
172
+ def initialize(vars, zonefile_record)
173
+ @vars = vars
174
+ if zonefile_record
175
+ self.host = qualify_host(zonefile_record.host.to_s)
176
+ @vars[:last_host] = host
177
+ self.ttl = zonefile_record.ttl.to_i
178
+ self.klass = zonefile_record.klass.to_s
179
+ self.domainname = qualify_host(zonefile_record.target.to_s)
180
+ self.comment = zonefile_record.comment&.to_s
181
+ end
182
+ end
183
+
184
+ alias target domainname
185
+ alias alias host
186
+ end
187
+
188
+ class MX < Record
189
+ attr_accessor :host, :priority, :domainname
190
+
191
+ def initialize(vars, zonefile_record)
192
+ @vars = vars
193
+ if zonefile_record
194
+ self.host = qualify_host(zonefile_record.host.to_s)
195
+ @vars[:last_host] = host
196
+ self.ttl = zonefile_record.ttl.to_i
197
+ self.klass = zonefile_record.klass.to_s
198
+ self.priority = zonefile_record.priority.to_i
199
+ self.domainname = qualify_host(zonefile_record.exchanger.to_s)
200
+ self.comment = zonefile_record.comment&.to_s
201
+ end
202
+ end
203
+
204
+ alias exchange domainname
205
+ alias exchanger domainname
206
+ end
207
+
208
+ class NAPTR < Record
209
+ attr_accessor :host, :data
210
+
211
+ def initialize(vars, zonefile_record)
212
+ @vars = vars
213
+ if zonefile_record
214
+ self.host = qualify_host(zonefile_record.host.to_s)
215
+ @vars[:last_host] = host
216
+ self.ttl = zonefile_record.ttl.to_i
217
+ self.klass = zonefile_record.klass.to_s
218
+ self.data = zonefile_record.data.to_s
219
+ self.comment = zonefile_record.comment&.to_s
220
+ end
221
+ end
222
+ end
223
+
224
+ class NS < Record
225
+ attr_accessor :host, :domainname
226
+
227
+ def initialize(vars, zonefile_record)
228
+ @vars = vars
229
+ if zonefile_record
230
+ self.host = qualify_host(zonefile_record.host.to_s)
231
+ @vars[:last_host] = host
232
+ self.ttl = zonefile_record.ttl.to_i
233
+ self.klass = zonefile_record.klass.to_s
234
+ self.domainname = qualify_host(zonefile_record.nameserver.to_s)
235
+ self.comment = zonefile_record.comment&.to_s
236
+ end
237
+ end
238
+
239
+ alias nameserver domainname
240
+ end
241
+
242
+ class PTR < Record
243
+ attr_accessor :host, :domainname
244
+
245
+ def initialize(vars, zonefile_record)
246
+ @vars = vars
247
+ if zonefile_record
248
+ self.host = qualify_host(zonefile_record.host.to_s)
249
+ @vars[:last_host] = host
250
+ self.ttl = zonefile_record.ttl.to_i
251
+ self.klass = zonefile_record.klass.to_s
252
+ self.domainname = qualify_host(zonefile_record.target.to_s)
253
+ self.comment = zonefile_record.comment&.to_s
254
+ end
255
+ end
256
+
257
+ alias target domainname
258
+ end
259
+
260
+ class SRV < Record
261
+ attr_accessor :host, :priority, :weight, :port, :domainname
262
+
263
+ def initialize(vars, zonefile_record)
264
+ @vars = vars
265
+ if zonefile_record
266
+ self.host = qualify_host(zonefile_record.host.to_s)
267
+ @vars[:last_host] = host
268
+ self.ttl = zonefile_record.ttl.to_i
269
+ self.klass = zonefile_record.klass.to_s
270
+ self.priority = zonefile_record.priority.to_i
271
+ self.weight = zonefile_record.weight.to_i
272
+ self.port = zonefile_record.port.to_i
273
+ self.domainname = qualify_host(zonefile_record.target.to_s)
274
+ self.comment = zonefile_record.comment&.to_s
275
+ end
276
+ end
277
+
278
+ alias target domainname
279
+ end
280
+
281
+ class SSHFP < Record
282
+ attr_accessor :host, :alg, :fptype, :fp
283
+
284
+ def initialize(vars, zonefile_record)
285
+ @vars = vars
286
+ if zonefile_record
287
+ self.host = qualify_host(zonefile_record.host.to_s)
288
+ @vars[:last_host] = host
289
+ self.ttl = zonefile_record.ttl.to_i
290
+ self.klass = zonefile_record.klass.to_s
291
+ self.alg = zonefile_record.alg.to_i
292
+ self.fptype = zonefile_record.fptype.to_i
293
+ self.fp = zonefile_record.fp.to_s
294
+ self.comment = zonefile_record.comment&.to_s
295
+ end
296
+ end
297
+ end
298
+
299
+ class TXT < Record
300
+ attr_accessor :host, :data
301
+
302
+ def initialize(vars, zonefile_record)
303
+ @vars = vars
304
+ if zonefile_record
305
+ self.host = qualify_host(zonefile_record.host.to_s)
306
+ @vars[:last_host] = host
307
+ self.ttl = zonefile_record.ttl.to_i
308
+ self.klass = zonefile_record.klass.to_s
309
+ self.data = zonefile_record.data.to_s
310
+ self.comment = zonefile_record.comment&.to_s
311
+ end
312
+ end
313
+ end
314
+
315
+ class SPF < TXT
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,586 @@
1
+ grammar Zonefile
2
+ rule zone
3
+ (variable / space_or_break / comment)* soa (resource_record / variable / comment / space / linebreak)* {
4
+ def variables
5
+ @variables ||= begin
6
+ raw = elements[0].elements.select { |e| e.to_s =~ /^\$/ }
7
+ variables = {}
8
+ raw.each do |e|
9
+ variables[e.name.text_value.to_s] = e.value.text_value.to_s
10
+ end
11
+ variables
12
+ end
13
+ end
14
+
15
+ def origin
16
+ soa.origin.host.to_s
17
+ end
18
+
19
+ def to_s
20
+ text_value
21
+ end
22
+
23
+ def rr
24
+ elements[-1].elements.select { |e| e.to_s !~ /\A\s*([;$].*)?\z|\A\z/; }
25
+ end
26
+
27
+ def entries
28
+ elements[0].elements.select { |e| e.to_s !~ /\A\s*(;.*)?\z|\A\z/; } +
29
+ [soa] +
30
+ elements[-1].elements.select { |e| e.to_s !~ /\A\s*(;.*)?\z|\A\z/; }
31
+ end
32
+ }
33
+ end
34
+
35
+ rule variable
36
+ "$" name:([a-zA-Z0-9]+) space value:([a-zA-Z0-9\.\-_]+) space? comment? {
37
+ def to_s
38
+ "$#{name.text_value.to_s} #{value.text_value.to_s}"
39
+ end
40
+
41
+ def parse_type ; :variable ; end
42
+ }
43
+ end
44
+
45
+ rule soa
46
+ (
47
+ origin space ttl klass "SOA" space ns space "("? multiline_comment* space_or_break* rp multiline_comment* space_or_break* "("? multiline_comment* space_or_break* serial multiline_comment* space_or_break refresh multiline_comment* space_or_break reretry multiline_comment* space_or_break expiry multiline_comment* space_or_break nxttl multiline_comment* space_or_break* ")"? /
48
+ origin space klass ttl "SOA" space ns space "("? multiline_comment* space_or_break* rp multiline_comment* space_or_break* "("? multiline_comment* space_or_break* serial multiline_comment* space_or_break refresh multiline_comment* space_or_break reretry multiline_comment* space_or_break expiry multiline_comment* space_or_break nxttl multiline_comment* space_or_break* ")"?
49
+ ) {
50
+ def to_s
51
+ "#{origin} #{ttl} #{klass} SOA #{ns} #{rp} (#{serial} #{refresh} #{reretry} #{expiry} #{nxttl})"
52
+ end
53
+
54
+ def parse_type ; :soa ; end
55
+ }
56
+ end
57
+
58
+ rule resource_record
59
+ record:(a_record / aaaa_record / caa_record / cname_record / mx_record / naptr_record / ns_record / ptr_record / srv_record / spf_record / sshfp_record / txt_record / soa_record) space* comment:comment? linebreak {
60
+ def zone
61
+ p = parent
62
+ while p.respond_to?(:parent) && p.parent
63
+ p = p.parent
64
+ end
65
+ p
66
+ end
67
+
68
+ def to_s
69
+ text_value
70
+ end
71
+
72
+ def record_type
73
+ record.record_type
74
+ end
75
+
76
+ def ttl
77
+ record.ttl || zone.variables['TTL'].to_i
78
+ end
79
+
80
+ def comment
81
+ return if super.empty?
82
+ super
83
+ end
84
+
85
+ def method_missing(method_name, *args)
86
+ if record.respond_to?(method_name)
87
+ record.send(method_name, *args)
88
+ end
89
+ end
90
+
91
+ def respond_to?(method_name)
92
+ super || record.respond_to?(method_name)
93
+ end
94
+
95
+ def parse_type ; :record ; end
96
+ }
97
+ end
98
+
99
+ rule a_record
100
+ (
101
+ host space ms_age ttl klass "A" space ip_address /
102
+ host space ms_age klass ttl "A" space ip_address
103
+ ) {
104
+ def to_s
105
+ "#{host} #{ms_age} #{ttl} #{klass} A #{ip_address}"
106
+ end
107
+
108
+ def record_type
109
+ "A"
110
+ end
111
+ }
112
+ end
113
+
114
+ rule ip_address
115
+ [\d]+ "." [\d]+ "." [\d]+ "." [\d]+ {
116
+ def to_s
117
+ text_value
118
+ end
119
+ }
120
+ end
121
+
122
+ rule aaaa_record
123
+ (
124
+ host space ms_age ttl klass "AAAA" space ip_address:ip6_address /
125
+ host space ms_age klass ttl "AAAA" space ip_address:ip6_address
126
+ ) {
127
+ def to_s
128
+ "#{host} #{ttl} #{klass} AAAA #{ip_address}"
129
+ end
130
+
131
+ def record_type
132
+ "AAAA"
133
+ end
134
+ }
135
+ end
136
+
137
+ rule ip6_address
138
+ [\da-fA-F:.] 2..39 {
139
+ def to_s
140
+ text_value.downcase
141
+ end
142
+ }
143
+ end
144
+
145
+ rule caa_record
146
+ (
147
+ host space ms_age ttl klass "CAA" space flags:integer space tag:unquoted_string space value:caa_value /
148
+ host space ms_age klass ttl "CAA" space flags:integer space tag:unquoted_string space value:caa_value
149
+ ) {
150
+ def to_s
151
+ "#{host} #{ttl} #{klass} CAA #{flags} #{tag} #{value}"
152
+ end
153
+
154
+ def record_type
155
+ "CAA"
156
+ end
157
+ }
158
+ end
159
+
160
+ rule caa_value
161
+ (quoted_string / unquoted_string) {
162
+ def to_s
163
+ text_value
164
+ end
165
+ }
166
+ end
167
+
168
+ rule cname_record
169
+ (
170
+ host space ms_age ttl klass "CNAME" space target:host /
171
+ host space klass ms_age ttl "CNAME" space target:host /
172
+ host space ms_age ttl "CNAME" space target:host /
173
+ host space klass "CNAME" space target:host
174
+ ) {
175
+ def to_s
176
+ "#{host} #{ttl} #{klass} CNAME #{target}"
177
+ end
178
+
179
+ def record_type
180
+ "CNAME"
181
+ end
182
+ }
183
+ end
184
+
185
+ rule mx_record
186
+ (
187
+ host space ttl klass "MX" space priority:integer space exchanger:host /
188
+ host space klass ttl "MX" space priority:integer space exchanger:host
189
+ ) {
190
+ def to_s
191
+ "#{host} #{ttl} #{klass} MX #{priority} #{exchanger}"
192
+ end
193
+
194
+ def record_type
195
+ "MX"
196
+ end
197
+ }
198
+ end
199
+
200
+ rule naptr_record
201
+ (
202
+ host space ms_age ttl klass "NAPTR" space data /
203
+ host space ms_age klass ttl "NAPTR" space data
204
+ ) {
205
+ def to_s
206
+ "#{host} #{ttl} #{klass} NAPTR #{data}"
207
+ end
208
+
209
+ def record_type
210
+ "NAPTR"
211
+ end
212
+ }
213
+ end
214
+
215
+ rule ns_record
216
+ (
217
+ host space ms_age ttl klass "NS" space nameserver:host /
218
+ host space ms_age klass ttl "NS" space nameserver:host
219
+ ) {
220
+ def to_s
221
+ "#{host} #{ttl} #{klass} NS #{nameserver}"
222
+ end
223
+
224
+ def record_type
225
+ "NS"
226
+ end
227
+ }
228
+ end
229
+
230
+ rule ptr_record
231
+ (
232
+ host space ms_age ttl klass "PTR" space target:host /
233
+ host space ms_age klass ttl "PTR" space target:host
234
+ ) {
235
+ def to_s
236
+ "#{host} #{ttl} #{klass} PTR #{target}"
237
+ end
238
+
239
+ def record_type
240
+ "PTR"
241
+ end
242
+ }
243
+ end
244
+
245
+ rule soa_record
246
+ (
247
+ origin space ms_age ttl klass "SOA" space ns space rp space data /
248
+ origin space ms_age klass ttl "SOA" space ns space rp space data
249
+ ) {
250
+ def to_s
251
+ "#{origin} #{ttl} #{klass} SOA #{ns} #{rp} (#{space})"
252
+ end
253
+
254
+ def record_type
255
+ "SOA"
256
+ end
257
+ }
258
+ end
259
+
260
+ rule srv_record
261
+ (
262
+ host space ms_age ttl klass "SRV" space priority:integer space weight:integer space port:integer space target:host /
263
+ host space klass ms_age ttl "SRV" space priority:integer space weight:integer space port:integer space target:host /
264
+ host space ms_age ttl "SRV" space priority:integer space weight:integer space port:integer space target:host /
265
+ host space klass "SRV" space priority:integer space weight:integer space port:integer space target:host
266
+ ) {
267
+ def to_s
268
+ "#{host} #{ttl} #{klass} SRV #{priority} #{weight} #{port} #{target}"
269
+ end
270
+
271
+ def record_type
272
+ "SRV"
273
+ end
274
+ }
275
+ end
276
+
277
+ rule spf_record
278
+ (
279
+ host space ms_age ttl klass "SPF" space data:txt_data /
280
+ host space ms_age klass ttl "SPF" space data:txt_data
281
+ ) {
282
+ def to_s
283
+ "#{host} #{ttl} #{klass} SPF #{data}"
284
+ end
285
+
286
+ def record_type
287
+ "SPF"
288
+ end
289
+ }
290
+ end
291
+
292
+ rule sshfp_record
293
+ (
294
+ host space ms_age ttl klass "SSHFP" space alg:integer space fptype:integer space fp:fingerprint /
295
+ host space ms_age klass ttl "SSHFP" space alg:integer space fptype:integer space fp:fingerprint
296
+ ) {
297
+ def to_s
298
+ "#{host} #{ttl} #{klass} SSHFP #{alg} #{fptype} #{fp}"
299
+ end
300
+
301
+ def record_type
302
+ "SSHFP"
303
+ end
304
+ }
305
+ end
306
+
307
+ rule txt_record
308
+ (
309
+ host space ms_age ttl klass "TXT" space data:ms_txt_data /
310
+ host space ms_age klass ttl "TXT" space data:ms_txt_data
311
+ ) {
312
+ def to_s
313
+ "#{host} #{ttl} #{klass} TXT #{data}"
314
+ end
315
+
316
+ def record_type
317
+ "TXT"
318
+ end
319
+ }
320
+ end
321
+
322
+ rule origin
323
+ host comment* {
324
+ def to_s
325
+ "#{host}"
326
+ end
327
+ }
328
+ end
329
+
330
+ rule multiline_comment
331
+ linebreak+ comment* {
332
+ def to_s
333
+ text_value.strip
334
+ end
335
+ }
336
+ end
337
+
338
+ rule space
339
+ [ \t]+ {
340
+ def to_s
341
+ text_value
342
+ end
343
+ }
344
+ end
345
+
346
+ rule linebreak
347
+ [\n\r]+ {
348
+ def to_s
349
+ ''
350
+ end
351
+ }
352
+ end
353
+
354
+ rule space_or_break
355
+ [\s]+ {
356
+ def to_s
357
+ text_value
358
+ end
359
+ }
360
+ end
361
+
362
+ rule klass
363
+ (("IN" space) / '') {
364
+ def to_s
365
+ text_value.strip
366
+ end
367
+ }
368
+ end
369
+
370
+ rule comment
371
+ space* ";" [^\n\r]* {
372
+ def to_s
373
+ text_value[2..].to_s.strip
374
+ end
375
+
376
+ def parse_type ; :comment ; end
377
+ }
378
+ end
379
+
380
+ rule ns
381
+ host comment* {
382
+ def to_s
383
+ "#{host}"
384
+ end
385
+ }
386
+ end
387
+
388
+ rule rp
389
+ rp_value comment* {
390
+ def to_s
391
+ "#{rp_value}"
392
+ end
393
+ }
394
+ end
395
+
396
+ rule rp_value
397
+ ("." / (("\\." / [a-zA-Z0-9\-)])+ "."?)+) {
398
+ def to_s
399
+ text_value
400
+ end
401
+ }
402
+ end
403
+
404
+ rule serial
405
+ integer comment* {
406
+ def to_i
407
+ integer.to_i
408
+ end
409
+ def to_s
410
+ "#{to_i}"
411
+ end
412
+ }
413
+ end
414
+
415
+ rule time_interval
416
+ integer time_multiplier {
417
+ def to_s
418
+ text_value
419
+ end
420
+
421
+ def to_i
422
+ time_multiplier.to_i * integer.to_i
423
+ end
424
+ }
425
+ end
426
+
427
+ rule refresh
428
+ time_interval comment* {
429
+ def to_i
430
+ time_interval.to_i
431
+ end
432
+ def to_s
433
+ time_interval.to_s
434
+ end
435
+ }
436
+ end
437
+
438
+ rule integer
439
+ [0-9]+ {
440
+ def to_i
441
+ text_value.to_i
442
+ end
443
+ def to_s
444
+ "#{to_i}"
445
+ end
446
+ }
447
+ end
448
+
449
+ rule time_multiplier
450
+ ( 's' / 'S' / 'm' / 'M' / 'h' / 'H' / 'd' / 'D' / 'w' / 'W' / '' ) {
451
+ def to_s
452
+ text_value
453
+ end
454
+ def to_i
455
+ case text_value.downcase
456
+ when 'm' then 60
457
+ when 'h' then 60 * 60
458
+ when 'd' then 60 * 60 * 24
459
+ when 'w' then 60 * 60 * 24 * 7
460
+ else
461
+ 1
462
+ end
463
+ end
464
+ }
465
+ end
466
+
467
+ rule reretry
468
+ time_interval comment* {
469
+ def to_i
470
+ time_interval.to_i
471
+ end
472
+ def to_s
473
+ time_interval.to_s
474
+ end
475
+ }
476
+ end
477
+
478
+ rule expiry
479
+ time_interval comment* {
480
+ def to_i
481
+ time_interval.to_i
482
+ end
483
+ def to_s
484
+ time_interval.to_s
485
+ end
486
+ }
487
+ end
488
+
489
+ rule nxttl
490
+ time_interval comment* {
491
+ def to_i
492
+ time_interval.to_i
493
+ end
494
+ def to_s
495
+ time_interval.to_s
496
+ end
497
+ }
498
+ end
499
+
500
+ rule ms_age
501
+ ( "[AGE:" [\d]+ "]" space / '' ) {
502
+ def to_s
503
+ text_value
504
+ end
505
+ }
506
+ end
507
+
508
+ rule ttl
509
+ ((time_interval space) / '') {
510
+ def to_i
511
+ respond_to?(:time_interval) ? time_interval.to_i : nil
512
+ end
513
+ def to_s
514
+ respond_to?(:time_interval) ? time_interval.to_s : ''
515
+ end
516
+ }
517
+ end
518
+
519
+ rule host
520
+ ( ([*a-zA-Z0-9\-\._]+) / "@" / ' ' / "\t" ) {
521
+ def to_s
522
+ text_value
523
+ end
524
+ }
525
+ end
526
+
527
+ rule fingerprint
528
+ [a-fA-Z0-9:]+ {
529
+ def to_s
530
+ text_value.strip
531
+ end
532
+ }
533
+ end
534
+
535
+ rule data
536
+ [^;\n\r]+ {
537
+ def to_s
538
+ text_value.strip
539
+ end
540
+ }
541
+ end
542
+
543
+ rule ms_txt_data
544
+ (
545
+ "(" space* data:txt_data space* ")" /
546
+ data:txt_data
547
+ ) {
548
+ def to_s
549
+ data.to_s
550
+ end
551
+ }
552
+ end
553
+
554
+ rule txt_data
555
+ txt_string (space txt_data)* {
556
+ def to_s
557
+ text_value
558
+ end
559
+ }
560
+ end
561
+
562
+ rule txt_string
563
+ (quoted_string / unquoted_string) {
564
+ def to_s
565
+ text_value
566
+ end
567
+ }
568
+ end
569
+
570
+ rule quoted_string
571
+ ( '"' ( '\"' / [^"] )* '"') {
572
+ def to_s
573
+ text_value
574
+ end
575
+ }
576
+ end
577
+
578
+ rule unquoted_string
579
+ '[a-zA-Z0-9=_\.\-\@\:\~]+'r {
580
+ def to_s
581
+ text_value
582
+ end
583
+ }
584
+ end
585
+
586
+ end
data/zonesync.gemspec CHANGED
@@ -27,9 +27,9 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- spec.add_dependency "dns-zonefile", "~>1.0"
31
30
  spec.add_dependency "diff-lcs", "~>1.4"
32
31
  spec.add_dependency "thor", "~>1.0"
32
+ spec.add_dependency "treetop", "~>1.6"
33
33
 
34
34
  spec.add_development_dependency "rake"
35
35
  spec.add_development_dependency "rspec"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zonesync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
@@ -9,50 +9,50 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2024-04-17 00:00:00.000000000 Z
12
+ date: 2024-08-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: dns-zonefile
15
+ name: diff-lcs
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: '1.0'
20
+ version: '1.4'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: '1.0'
27
+ version: '1.4'
28
28
  - !ruby/object:Gem::Dependency
29
- name: diff-lcs
29
+ name: thor
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: '1.4'
34
+ version: '1.0'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: '1.4'
41
+ version: '1.0'
42
42
  - !ruby/object:Gem::Dependency
43
- name: thor
43
+ name: treetop
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - "~>"
47
47
  - !ruby/object:Gem::Version
48
- version: '1.0'
48
+ version: '1.6'
49
49
  type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: '1.0'
55
+ version: '1.6'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: rake
58
58
  requirement: !ruby/object:Gem::Requirement
@@ -121,6 +121,8 @@ files:
121
121
  - lib/zonesync/rake.rb
122
122
  - lib/zonesync/record.rb
123
123
  - lib/zonesync/version.rb
124
+ - lib/zonesync/zonefile.rb
125
+ - lib/zonesync/zonefile.treetop
124
126
  - log/.keep
125
127
  - zonesync.gemspec
126
128
  homepage: https://github.com/botandrose/zonesync