dnsruby 1.74.0 → 2.0.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.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +302 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/demo/rubydig.rb +0 -2
  10. data/dnsruby.gemspec +5 -4
  11. data/lib/dnsruby/cache.rb +279 -47
  12. data/lib/dnsruby/code_mapper.rb +35 -8
  13. data/lib/dnsruby/code_mappers.rb +63 -49
  14. data/lib/dnsruby/config.rb +75 -4
  15. data/lib/dnsruby/deadline_queue.rb +75 -0
  16. data/lib/dnsruby/dnssec.rb +148 -45
  17. data/lib/dnsruby/ipv6.rb +45 -5
  18. data/lib/dnsruby/key_cache.rb +202 -35
  19. data/lib/dnsruby/message/decoder.rb +85 -19
  20. data/lib/dnsruby/message/encoder.rb +27 -16
  21. data/lib/dnsruby/message/header.rb +8 -12
  22. data/lib/dnsruby/message/message.rb +53 -22
  23. data/lib/dnsruby/message/section.rb +12 -34
  24. data/lib/dnsruby/name.rb +5 -2
  25. data/lib/dnsruby/packet_sender.rb +645 -203
  26. data/lib/dnsruby/presentation.rb +147 -0
  27. data/lib/dnsruby/query_control.rb +132 -0
  28. data/lib/dnsruby/recursor.rb +795 -339
  29. data/lib/dnsruby/resolver.rb +468 -186
  30. data/lib/dnsruby/resource/CAA.rb +67 -16
  31. data/lib/dnsruby/resource/DLV.rb +6 -0
  32. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  33. data/lib/dnsruby/resource/DS.rb +27 -28
  34. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  35. data/lib/dnsruby/resource/IN.rb +19 -0
  36. data/lib/dnsruby/resource/MX.rb +2 -2
  37. data/lib/dnsruby/resource/NSEC.rb +6 -10
  38. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  39. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +979 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +42 -20
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +21 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +105 -10
  167. data/.coveralls.yml +0 -2
@@ -25,12 +25,10 @@ module Dnsruby
25
25
  # Create a new ZoneReader. The zone origin is required. If the desired SOA minimum
26
26
  # and TTL are passed in, then they are used as default values.
27
27
  def initialize(origin, soa_minimum = nil, soa_ttl = nil)
28
- @origin = origin.to_s
29
-
30
- if (!Name.create(@origin).absolute?)
31
- @origin = @origin.to_s + "."
32
- end
28
+ origin_name = parse_zone_name(origin)
29
+ @origin = Name.new(origin_name.labels, true).to_s(true)
33
30
  @soa_ttl = soa_ttl
31
+ @default_ttl = nil
34
32
  if (soa_minimum && !@last_explicit_ttl)
35
33
  @last_explicit_ttl = soa_minimum
36
34
  else
@@ -39,6 +37,7 @@ module Dnsruby
39
37
  @last_explicit_class = Classes.new("IN")
40
38
  @last_name = nil
41
39
  @continued_line = nil
40
+ @group_depth = 0
42
41
  @in_quoted_section = false
43
42
  end
44
43
 
@@ -72,60 +71,62 @@ module Dnsruby
72
71
  raise ParseException.new("Error reading line #{io.lineno} of #{io.inspect} : [#{line}]")
73
72
  end
74
73
  end
74
+ if @continued_line
75
+ raise ParseException.new("Unclosed parenthesis at end of #{io.inspect}")
76
+ end
75
77
  return zone
76
78
  end
77
79
 
78
80
  # Process the next line of the file
79
81
  # Returns a string representing the normalised line.
80
82
  def process_line(line, do_prefix_hack = false)
81
- return nil if (line[0,1] == ";")
82
- line = strip_comments(line)
83
- return nil if (line.strip.length == 0)
84
- return nil if (!line || (line.length == 0))
85
- @in_quoted_section = false if !@continued_line
83
+ if !@continued_line
84
+ @in_quoted_section = false
85
+ @single_quoted_start = if /['(]/.match?(line)
86
+ Presentation.single_quoted_start(line, owner_omitted: line.start_with?(' ', "\t"))
87
+ end
88
+ end
89
+ previous_quote = @in_quoted_section
90
+ line, @group_depth, @in_quoted_section =
91
+ Presentation.scan(line, @group_depth, @in_quoted_section,
92
+ single_quoted_from: @continued_line && @single_quoted_start ? 0 : @single_quoted_start)
93
+
94
+ if @continued_line
95
+ # The scanner keeps literal newlines inside quotes and turns layout
96
+ # newlines into spaces. Do not strip trailing quoted data. Callers of
97
+ # process_line may also supply lines without a terminating newline.
98
+ separator = previous_quote ? (@continued_line.end_with?("\n") ? '' : "\n") : ' '
99
+ line = @continued_line + separator + line
100
+ end
101
+ if @group_depth.positive?
102
+ @continued_line = line
103
+ return nil
104
+ end
105
+ @continued_line = nil
106
+ return nil if line.strip.empty?
86
107
 
87
108
  if (line.index("$ORIGIN") == 0)
88
- @origin = line.split()[1].strip # $ORIGIN <domain-name> [<comment>]
109
+ @origin = absolute_name(Presentation.tokens(line)[1]).to_s(true) # $ORIGIN <domain-name>
89
110
  # print "Setting $ORIGIN to #{@origin}\n"
90
111
  return nil
91
112
  end
92
113
  if (line.index("$TTL") == 0)
93
- @last_explicit_ttl = get_ttl(line.split()[1].strip) # $TTL <ttl>
114
+ @default_ttl = get_ttl(line.split()[1].strip) # $TTL <ttl>
94
115
  # print "Setting $TTL to #{ttl}\n"
95
116
  return nil
96
117
  end
97
- if (@continued_line)
98
- # Add the next line until we see a ")"
99
- # REMEMBER TO STRIP OFF COMMENTS!!!
100
- @continued_line = strip_comments(@continued_line)
101
- line = @continued_line.rstrip.chomp + " " + line
102
- if (line.index(")"))
103
- # OK
104
- @continued_line = false
105
- end
106
- end
107
- open_bracket = line.index("(")
108
- if (open_bracket)
109
- # Keep going until we see ")"
110
- index = line.index(")")
111
- if (index && (index > open_bracket))
112
- # OK
113
- @continued_line = false
114
- else
115
- @continued_line = line
116
- end
117
- end
118
- return nil if @continued_line
118
+ line += "\n"
119
119
 
120
- line = strip_comments(line) + "\n"
121
-
122
- # If SOA, then replace "3h" etc. with expanded seconds
123
- # begin
124
- return normalise_line(line, do_prefix_hack)
125
- # rescue Exception => e
126
- # print "ERROR parsing line #{@line_num} : #{line}\n"
127
- # return "\n", Types::ANY
128
- # end
120
+ # Preserve the public normalization hook for subclasses and prepends.
121
+ # Only unchanged input has already been scanned. Overrides may replace
122
+ # or mutate the String before calling super, introducing new syntax.
123
+ previous_scanned_line = @scanned_line
124
+ begin
125
+ @scanned_line = [line, line.dup]
126
+ normalise_line(line, do_prefix_hack)
127
+ ensure
128
+ @scanned_line = previous_scanned_line
129
+ end
129
130
  end
130
131
 
131
132
  def strip_comments(line)
@@ -209,52 +210,68 @@ module Dnsruby
209
210
  # Take a line from the input zone file, and return the normalised form
210
211
  # do_prefix_hack should always be false
211
212
  def normalise_line(line, do_prefix_hack = false)
213
+ # Direct callers historically accepted unmatched grouping, just like
214
+ # RR.create. process_line has already scanned and checked its input.
215
+ unless @scanned_line && line.equal?(@scanned_line[0]) && line == @scanned_line[1]
216
+ single_quoted_start = if line.include?("'")
217
+ Presentation.single_quoted_start(line, owner_omitted: line.start_with?(' ', "\t"))
218
+ end
219
+ line, = Presentation.scan(line, allow_unbalanced: true, single_quoted_from: single_quoted_start)
220
+ end
221
+ normalise_scanned_line(line, do_prefix_hack)
222
+ end
223
+
224
+ def normalise_scanned_line(line, do_prefix_hack = false)
225
+
226
+ line = Presentation.escape_whitespace(line) if line.include?('\\')
227
+
212
228
  # Note that a freestanding "@" is used to denote the current origin - we can simply replace that straight away
213
- # Remove the ( and )
214
229
  # Note that no domain name may be specified in the RR - in that case, last_name should be used. How do we tell? Tab or space at start of line.
215
230
 
216
231
  # If we have text in the record, then ignore that in the parsing, and stick it on again at the end
217
232
  stored_line = "";
218
- if (line.index('"') != nil)
219
- stored_line = line[line.index('"'), line.length];
220
- line = line [0, line.index('"')]
233
+ # TXT quotes a whole token, but SVCB and HTTPS quote mid-token
234
+ # (alpn="h2,h3"), so remember which to put the text back as it came.
235
+ # A parenthesis separates too, and the strip below may remove it first.
236
+ stored_line_was_separate = true
237
+ quote_index = Presentation.unescaped_quote(line)
238
+ if line.include?("'") && (rdata_start = Presentation.single_quoted_start(line,
239
+ owner_omitted: line.start_with?(' ', "\t")))
240
+ quote_index = [quote_index, line.index(/(?<=[\s(])'/, rdata_start)].compact.min
241
+ end
242
+ if quote_index
243
+ stored_line_was_separate = (quote_index == 0) || !(/[\s()]/ =~ line[quote_index - 1]).nil?
244
+ stored_line = line[quote_index, line.length];
245
+ line = line [0, quote_index]
221
246
  end
222
247
  if ((line[0,1] == " ") || (line[0,1] == "\t"))
223
248
  line = @last_name + " " + line
224
249
  end
225
250
  line.chomp!
226
- line.sub!(/\s+@$/, " #{@origin}") # IN CNAME @
227
- line.sub!(/^@\s+/, "#{@origin} ") # IN CNAME @
228
- line.sub!(/\s+@\s+/, " #{@origin} ")
251
+ line.sub!(/\s+@$/) { " #{@origin}" } # IN CNAME @
252
+ line.sub!(/^@\s+/) { "#{@origin} " } # IN CNAME @
253
+ line.sub!(/\s+@\s+/) { " #{@origin} " }
229
254
  line.strip!
230
255
 
231
256
 
232
257
  # o We need to identify the domain name in the record, and then
233
- split = line.split(' ') # split on whitespace
234
- name = split[0].strip
235
- if (name.index"\\")
236
-
237
- ls =[]
238
- Name.create(name).labels.each {|el| ls.push(Name.decode(el.to_s))}
239
- new_name = ls.join('.')
240
-
258
+ split = Presentation.tokens(line)
259
+ name = split[0]
260
+ new_name = absolute_name(name).to_s(true)
261
+ line.sub!(name) { new_name }
262
+ name = new_name
263
+ split = line.split
241
264
 
242
- if (!(/\.\z/ =~ name))
243
- new_name += "." + @origin
244
- else
245
- new_name += "."
265
+ # RFC 1035 section 5.1 permits either TTL/class or class/TTL.
266
+ # Canonicalize the latter before applying the inherited defaults below.
267
+ if split[2] && /\A(?:[0-9]+[smhdwSMHDW])*[0-9]*\z/.match?(split[2]) && !split[2].empty?
268
+ begin
269
+ Classes.new(split[1])
270
+ split[1], split[2] = split[2], split[1]
271
+ line = split.join(' ') + "\n"
272
+ rescue ArgumentError
273
+ # The second token is a TTL or record type, not a class.
246
274
  end
247
- line = new_name + " "
248
- (split.length - 1).times {|i| line += "#{split[i+1]} "}
249
- line += "\n"
250
- name = new_name
251
- split = line.split
252
- # o add $ORIGIN to it if it is not absolute
253
- elsif !(/\.\z/ =~ name)
254
- new_name = name + "." + @origin
255
- line.sub!(name, new_name)
256
- name = new_name
257
- split = line.split
258
275
  end
259
276
 
260
277
  # If the second field is not a number, then we should add the TTL to the line
@@ -270,7 +287,8 @@ module Dnsruby
270
287
  split = line.split
271
288
  elsif (((split[1]).to_i == 0) && (split[1] != "0"))
272
289
  # Add the TTL
273
- if (!@last_explicit_ttl)
290
+ inherited_ttl = @default_ttl || @last_explicit_ttl
291
+ if (!inherited_ttl)
274
292
  # If this is the SOA record, and no @last_explicit_ttl is defined,
275
293
  # then we need to try the SOA TTL element from the config. Otherwise,
276
294
  # find the SOA Minimum field, and use that.
@@ -281,7 +299,7 @@ module Dnsruby
281
299
  # the actual type. If the type is not SOA then, then we can raise an error
282
300
  line = name + " %MISSING_TTL% "
283
301
  else
284
- line = name + " #{@last_explicit_ttl} "
302
+ line = name + " #{inherited_ttl} "
285
303
  end
286
304
  (split.length - 1).times {|i| line += "#{split[i+1]} "}
287
305
  line += "\n"
@@ -292,7 +310,11 @@ module Dnsruby
292
310
 
293
311
  # Now see if the clas is included. If not, then we should default to the last class used.
294
312
  begin
295
- klass = Classes.new(split[2])
313
+ class_text = split[2]
314
+ if /\ACLASS\d+\z/.match?(class_text)
315
+ class_text = Classes.classesbyval(Classes.classesbyname(class_text))
316
+ end
317
+ klass = Classes.new(class_text)
296
318
  @last_explicit_class = klass
297
319
  rescue ArgumentError
298
320
  # Wasn't a CLASS
@@ -307,22 +329,28 @@ module Dnsruby
307
329
  end
308
330
 
309
331
  # Add the type so we can load the zone one RRSet at a time.
310
- type = Types.new(split[3].strip)
332
+ type_text = split[3].strip
333
+ if /\ATYPE\d+\z/.match?(type_text)
334
+ type_text = Types.typesbyval(Types.typesbyname(type_text))
335
+ end
336
+ type = Types.new(type_text)
311
337
  is_soa = (type == Types::SOA)
312
338
  type_was = type
339
+ # RFC 3597 RDATA is already encoded wire data, even for known types.
340
+ # Its tokens must not be treated as textual names or SOA intervals.
341
+ generic_rdata = split[4]&.start_with?('\\#')
313
342
  if (type == Types.RRSIG)
314
343
  # If this is an RRSIG record, then add the TYPE COVERED rather than the type - this allows us to load a complete RRSet at a time
315
- type = Types.new(split[4].strip)
344
+ if generic_rdata
345
+ type = Dnsruby::RR.send(:new_from_normalised_string, line).type_covered if do_prefix_hack
346
+ else
347
+ type = Types.new(split[4].strip)
348
+ end
316
349
  end
317
350
 
318
351
  type_string=prefix_for_rrset_order(type, type_was)
319
352
  @last_name = name
320
353
 
321
- if !([Types::NAPTR, Types::TXT].include?type_was)
322
- line.sub!("(", "")
323
- line.sub!(")", "")
324
- end
325
-
326
354
  if (is_soa)
327
355
  if (@soa_ttl)
328
356
  # Replace the %MISSING_TTL% text with the SOA TTL from the config
@@ -333,9 +361,9 @@ module Dnsruby
333
361
  line.sub!(" %MISSING_TTL% ", " #{@last_explicit_ttl} ")
334
362
  end
335
363
  end
336
- line = replace_soa_ttl_fields(line)
364
+ line = replace_soa_ttl_fields(line) unless generic_rdata
337
365
  if (!@last_explicit_ttl)
338
- soa_rr = Dnsruby::RR.create(line)
366
+ soa_rr = Dnsruby::RR.send(:new_from_normalised_string, line)
339
367
  @last_explicit_ttl = soa_rr.minimum
340
368
  end
341
369
  end
@@ -343,47 +371,66 @@ module Dnsruby
343
371
  line = line.strip
344
372
 
345
373
  if (stored_line && stored_line != "")
346
- line += " " + stored_line.strip
347
- end
348
-
349
- # We need to fix up any non-absolute names in the RR
350
- # Some RRs have a single name, at the end of the string -
351
- # to do these, we can just check the last character for "." and add the
352
- # "." + origin string if necessary
353
- if ([Types::MX, Types::NS, Types::AFSDB, Types::NAPTR, Types::RT,
354
- Types::SRV, Types::CNAME, Types::MB, Types::MG, Types::MR,
355
- Types::PTR, Types::DNAME].include?type_was)
356
- # if (line[line.length-1, 1] != ".")
357
- if (!(/\.\z/ =~ line))
358
- line = line + "." + @origin.to_s
374
+ line += " " if stored_line_was_separate
375
+ line += stored_line.strip
376
+ end
377
+
378
+ if generic_rdata
379
+ return line + "\n", type_string, @last_name if do_prefix_hack
380
+ return line + "\n"
381
+ end
382
+
383
+ if [Types::TXT, Types::SPF].include?(type_was) && line.include?('\\')
384
+ header_and_data = line.split(/\s+/, 5)
385
+ if header_and_data[4]
386
+ header_and_data[4] = Presentation.character_string_escapes(header_and_data[4])
387
+ line = header_and_data.join(' ')
359
388
  end
360
389
  end
361
- # Other RRs have several names. These should be parsed by Dnsruby,
362
- # and the names adjusted there.
363
- if ([Types::MINFO, Types::PX, Types::RP].include?type_was)
364
- parsed_rr = Dnsruby::RR.create(line)
365
- case parsed_rr.type
366
- when Types::MINFO
367
- if (!parsed_rr.rmailbx.absolute?)
368
- parsed_rr.rmailbx = parsed_rr.rmailbx.to_s + "." + @origin.to_s
369
- end
370
- if (!parsed_rr.emailbx.absolute?)
371
- parsed_rr.emailbx = parsed_rr.emailbx.to_s + "." + @origin.to_s
372
- end
373
- when Types::PX
374
- if (!parsed_rr.map822.absolute?)
375
- parsed_rr.map822 = parsed_rr.map822.to_s + "." + @origin.to_s
376
- end
377
- if (!parsed_rr.mapx400.absolute?)
378
- parsed_rr.mapx400 = parsed_rr.mapx400.to_s + "." + @origin.to_s
379
- end
380
- when Types::RP
381
- if (!parsed_rr.mailbox.absolute?)
382
- parsed_rr.mailbox = parsed_rr.mailbox.to_s + "." + @origin.to_s
383
- end
384
- if (!parsed_rr.txtdomain.absolute?)
385
- parsed_rr.txtdomain = parsed_rr.txtdomain.to_s + "." + @origin.to_s
386
- end
390
+
391
+ # Qualify parsed labels so an escaped final dot stays inside its label.
392
+ if [Types::MX, Types::KX, Types::NS, Types::AFSDB, Types::NAPTR, Types::RT,
393
+ Types::SRV, Types::CNAME, Types::MB, Types::MG, Types::MR,
394
+ Types::PTR, Types::DNAME].include?(type_was)
395
+ line = line.sub(/\S+\z/) { |target| absolute_name(target).to_s(true) }
396
+ end
397
+ name_fields = case type_was
398
+ when Types::SOA then [:mname, :rname]
399
+ when Types::MINFO then [:rmailbx, :emailbx]
400
+ when Types::PX then [:map822, :mapx400]
401
+ when Types::RP then [:mailbox, :txtdomain]
402
+ end
403
+ if name_fields
404
+ parsed_rr = Dnsruby::RR.send(:new_from_normalised_string, line)
405
+ name_fields.each do |field|
406
+ parsed_rr.public_send("#{field}=", absolute_name(parsed_rr.public_send(field)))
407
+ end
408
+ line = parsed_rr.to_s
409
+ end
410
+ # These name fields precede other RDATA. Qualify their presentation
411
+ # tokens directly, preserving the remaining signature/key text and
412
+ # avoiding record serializers which omit a name's trailing dot.
413
+ case type_was
414
+ when Types::NSEC, Types::NXT, Types::RRSIG, Types::IPSECKEY, Types::HIP
415
+ fields = line.split
416
+ domain_fields = case type_was
417
+ when Types::NSEC, Types::NXT then [4]
418
+ when Types::RRSIG then [11]
419
+ when Types::IPSECKEY then fields[5].to_i == 3 ? [7] : []
420
+ when Types::HIP then (7...fields.length).to_a
421
+ end
422
+ domain_fields.each { |index| fields[index] = absolute_name(fields.fetch(index)).to_s(true) }
423
+ line = fields.join(' ')
424
+ end
425
+ # SVCB and HTTPS carry their TargetName in the middle of the RDATA
426
+ # (before the SvcParams), so the trailing-name logic above cannot reach
427
+ # it. Parse the record and qualify a relative TargetName against the
428
+ # origin; the root (".") is absolute, so the absolute? guard skips it.
429
+ if ([Types::SVCB, Types::HTTPS].include?type_was)
430
+ parsed_rr = Dnsruby::RR.send(:new_from_normalised_string, line)
431
+ if parsed_rr.is_a?(RR::SVCB) && parsed_rr.target && !parsed_rr.target.absolute?
432
+ origin = RR::SVCB.create_target_name(@origin.to_s)
433
+ parsed_rr.target = Name.new(parsed_rr.target.labels + origin.labels, true)
387
434
  end
388
435
  line = parsed_rr.to_s
389
436
  end
@@ -392,6 +439,25 @@ module Dnsruby
392
439
  end
393
440
  return line+"\n"
394
441
  end
442
+ private :normalise_scanned_line
443
+
444
+ def absolute_name(value)
445
+ name = parse_zone_name(value)
446
+ return name if name.absolute?
447
+ Name.new(name.labels + parse_zone_name(@origin).labels, true)
448
+ end
449
+ private :absolute_name
450
+
451
+ # Zone-file ASCII already represents wire octets. Preserve its case for
452
+ # record types such as SVCB whose signed TargetName is not case-folded.
453
+ def parse_zone_name(value)
454
+ if value.is_a?(String) && value.ascii_only?
455
+ Name.new(Name.split_escaped(value), /(?<!\\)(?:\\\\)*\.\z/.match?(value))
456
+ else
457
+ Name.create(value)
458
+ end
459
+ end
460
+ private :parse_zone_name
395
461
 
396
462
  # Get the TTL in seconds from the m, h, d, w format
397
463
  def get_ttl(ttl_text_in)
@@ -410,15 +476,15 @@ module Dnsruby
410
476
  number = ttl_text[last_index + 1, index-last_index-1].to_i
411
477
  new_number = 0
412
478
  case letter
413
- when 115 then # "s"
479
+ when "s"
414
480
  new_number = number
415
- when 109 then # "m"
481
+ when "m"
416
482
  new_number = number * 60
417
- when 104 then # "h"
483
+ when "h"
418
484
  new_number = number * 3600
419
- when 100 then # "d"
485
+ when "d"
420
486
  new_number = number * 86400
421
- when 119 then # "w"
487
+ when "w"
422
488
  new_number = number * 604800
423
489
  end
424
490
  total += new_number
@@ -426,6 +492,7 @@ module Dnsruby
426
492
  last_index = index
427
493
  index = ttl_text.index(/[whdms]/, last_index + 1)
428
494
  end
495
+ total += ttl_text[last_index + 1..].to_i
429
496
  return total
430
497
  end
431
498