zonesync 0.6.1 → 0.7.0

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.
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