dnsruby 1.73.1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +314 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/dnsruby.gemspec +6 -4
- data/lib/dnsruby/cache.rb +279 -49
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +77 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +664 -207
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +798 -341
- data/lib/dnsruby/resolver.rb +472 -176
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +21 -2
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/OPT.rb +2 -3
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +986 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +53 -2
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +34 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +127 -14
- data/.coveralls.yml +0 -2
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
|
|
3
|
+
module Dnsruby
|
|
4
|
+
class RR
|
|
5
|
+
# Class for DNS Service Binding (SVCB) resource records.
|
|
6
|
+
#
|
|
7
|
+
# RFC 9460
|
|
8
|
+
#
|
|
9
|
+
# The presentation format is:
|
|
10
|
+
# Name TTL Class SVCB SvcPriority TargetName SvcParams
|
|
11
|
+
#
|
|
12
|
+
# A SvcPriority of 0 is AliasMode, where TargetName aliases the service to
|
|
13
|
+
# another name; non-zero is ServiceMode, where the SvcParams describe the
|
|
14
|
+
# endpoint.
|
|
15
|
+
#
|
|
16
|
+
# RR.create also takes a Hash, whose :params values are in presentation
|
|
17
|
+
# format -- the text a zone file carries after the "=". #params_to_hash is
|
|
18
|
+
# the inverse of that Hash.
|
|
19
|
+
class SVCB < RR
|
|
20
|
+
ClassValue = nil #:nodoc: all
|
|
21
|
+
TypeValue = Types::SVCB #:nodoc: all
|
|
22
|
+
|
|
23
|
+
# The registered SvcParamKey mnemonics: 0-4 and 6 from RFC 9460, 5 (ech)
|
|
24
|
+
# from RFC 9848, 7 (dohpath) from RFC 9461, 8 (ohttp) from RFC 9540 sec 4
|
|
25
|
+
# and 10 (docpath) from RFC 9953 sec 3. Keys whose value format is not
|
|
26
|
+
# yet published as an RFC are left out, and read and write as keyNNNNN.
|
|
27
|
+
KEY_NAME_TO_NUM = {
|
|
28
|
+
'mandatory' => 0,
|
|
29
|
+
'alpn' => 1,
|
|
30
|
+
'no-default-alpn' => 2,
|
|
31
|
+
'port' => 3,
|
|
32
|
+
'ipv4hint' => 4,
|
|
33
|
+
'ech' => 5,
|
|
34
|
+
'ipv6hint' => 6,
|
|
35
|
+
'dohpath' => 7,
|
|
36
|
+
'ohttp' => 8,
|
|
37
|
+
'docpath' => 10,
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
# The same table by number, for naming a key in presentation format and
|
|
41
|
+
# in error messages. A number with no mnemonic here becomes keyNNNNN.
|
|
42
|
+
KEY_NUM_TO_NAME = KEY_NAME_TO_NUM.invert.freeze
|
|
43
|
+
|
|
44
|
+
# Keys whose presentation format requires a SvcParamValue. An empty
|
|
45
|
+
# docpath means the root path, and an unknown key may stand alone.
|
|
46
|
+
KEYS_REQUIRING_VALUE = [0, 1, 3, 4, 5, 6, 7].freeze
|
|
47
|
+
|
|
48
|
+
# Keys defined to carry no value at all: no-default-alpn and ohttp.
|
|
49
|
+
KEYS_FORBIDDING_VALUE = [2, 8].freeze
|
|
50
|
+
|
|
51
|
+
# RFC 9460 sections 7.2, 7.3 and 8, and RFC 9848 section 3, prohibit
|
|
52
|
+
# escapes in these named parameters. Numeric keyNNNNN values still
|
|
53
|
+
# use the generic byte format.
|
|
54
|
+
KEYS_FORBIDDING_ESCAPES = [0, 3, 4, 5, 6].freeze
|
|
55
|
+
|
|
56
|
+
# RFC 9460 sec 14.3.2 reserves 65535 as an invalid SvcParamKey.
|
|
57
|
+
INVALID_KEY = 0xffff
|
|
58
|
+
|
|
59
|
+
# The SvcPriority field (0-65535). Zero indicates AliasMode.
|
|
60
|
+
attr_reader :priority
|
|
61
|
+
|
|
62
|
+
# Sets the SvcPriority, raising DecodeError unless the value is an
|
|
63
|
+
# integer in 0..65535.
|
|
64
|
+
def priority=(value)
|
|
65
|
+
@priority = to_uint16(value, 'SvcPriority')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The TargetName field, a Dnsruby::Name.
|
|
69
|
+
attr_accessor :target
|
|
70
|
+
|
|
71
|
+
# The SvcParams, an ordered Hash mapping the numeric SvcParamKey to the
|
|
72
|
+
# SvcParamValue in wire format. See #params_to_hash for a readable view.
|
|
73
|
+
attr_accessor :params
|
|
74
|
+
|
|
75
|
+
# Keep the binary RDATA interface provided for TYPE64/TYPE65 in 1.74.0.
|
|
76
|
+
# Compute bytes on demand so they reflect changes to target or params.
|
|
77
|
+
def data
|
|
78
|
+
MessageEncoder.new { |encoder| encode_rdata(encoder) }.to_s
|
|
79
|
+
end
|
|
80
|
+
alias rdata data
|
|
81
|
+
|
|
82
|
+
# Parse before assigning, so an invalid replacement leaves the record
|
|
83
|
+
# unchanged. Raw RDATA is always interpreted as octets, not UTF-8 text.
|
|
84
|
+
def data=(bytes)
|
|
85
|
+
raise DecodeError, 'SVCB RDATA must be a binary String' unless bytes.is_a?(String)
|
|
86
|
+
raise DecodeError, 'SVCB RDATA exceeds 65535 octets' if bytes.bytesize > 0xffff
|
|
87
|
+
|
|
88
|
+
parsed = self.class.decode_rdata(MessageDecoder.new(bytes.b))
|
|
89
|
+
parsed.klass = klass if klass
|
|
90
|
+
parsed.validate_decoded_rdata
|
|
91
|
+
@priority, @target, @params = parsed.priority, parsed.target, parsed.params
|
|
92
|
+
end
|
|
93
|
+
alias rdata= data=
|
|
94
|
+
|
|
95
|
+
def rdlength
|
|
96
|
+
data.bytesize
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# RR's default comparator uses the construction-time @rdata ivar, which
|
|
100
|
+
# can be presentation text or a tuple. Compare the same current bytes
|
|
101
|
+
# that callers receive from #rdata, regardless of construction path.
|
|
102
|
+
def <=>(other)
|
|
103
|
+
if @name.canonical == other.name.canonical && @type.code == other.type.code
|
|
104
|
+
rdata <=> other.rdata
|
|
105
|
+
else
|
|
106
|
+
super
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# RFC 3597 section 6 compares names inside newer RDATA formats as
|
|
111
|
+
# octets. RR's normal equality already compares priority and params,
|
|
112
|
+
# but Name equality deliberately ignores case. Keep the usual RR
|
|
113
|
+
# behavior (including ignoring TTL), with this additional distinction.
|
|
114
|
+
# Comparing labels also works for unfinished records used by UPDATE.
|
|
115
|
+
def ==(other)
|
|
116
|
+
super && (@target.nil? ||
|
|
117
|
+
@target.labels.map(&:string) == other.target.labels.map(&:string))
|
|
118
|
+
end
|
|
119
|
+
alias eql? ==
|
|
120
|
+
|
|
121
|
+
def hash
|
|
122
|
+
super ^ (@target ? @target.labels.map(&:string).hash : 0)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def from_hash(hash) #:nodoc: all
|
|
126
|
+
if hash.key?(:data) && hash.key?(:rdata) && hash[:data] != hash[:rdata]
|
|
127
|
+
raise DecodeError, 'SVCB data and rdata must agree when both are supplied'
|
|
128
|
+
end
|
|
129
|
+
self.data = hash.fetch(:data) { hash[:rdata] } if hash.key?(:data) || hash.key?(:rdata)
|
|
130
|
+
self.priority = hash[:priority] if hash[:priority]
|
|
131
|
+
@target = self.class.create_target_name(hash[:target]) if hash[:target]
|
|
132
|
+
@params ||= {}
|
|
133
|
+
if hash[:params]
|
|
134
|
+
@params = {}
|
|
135
|
+
hash[:params].each do |key, value|
|
|
136
|
+
store_param(key.to_s, value.nil? ? nil : value.to_s)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
validate_svcparams(@priority, @params)
|
|
140
|
+
check_complete_pair
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def from_data(data) #:nodoc: all
|
|
144
|
+
# Generic constructors accepted a one-element Array of raw bytes.
|
|
145
|
+
# Retain that form alongside the parsed tuple used by this codec.
|
|
146
|
+
if data.length == 1 && data.first.is_a?(String)
|
|
147
|
+
self.data = data.first
|
|
148
|
+
else
|
|
149
|
+
@priority, @target, @params = data
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def from_string(input) #:nodoc: all
|
|
154
|
+
# Generic.new historically accepted raw bytes. Every complete SVCB
|
|
155
|
+
# TargetName has a NUL terminator, so try its wire form when present.
|
|
156
|
+
# A presentation value can itself contain NUL; retain the normal text
|
|
157
|
+
# parser if the string is not also a complete, valid wire record.
|
|
158
|
+
if input && input.include?("\0")
|
|
159
|
+
begin
|
|
160
|
+
self.data = input
|
|
161
|
+
return
|
|
162
|
+
rescue DecodeError
|
|
163
|
+
# Not wire-format RDATA; parse the presentation form below.
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
@params = {}
|
|
167
|
+
return if input.nil? || input.strip.empty?
|
|
168
|
+
|
|
169
|
+
tokens = split_svcparams(input.strip)
|
|
170
|
+
self.priority = tokens.shift
|
|
171
|
+
target = tokens.shift
|
|
172
|
+
# Name.create(nil) would raise ArgumentError, not DecodeError.
|
|
173
|
+
raise DecodeError.new('SVCB record expects a TargetName after the SvcPriority') if target.nil?
|
|
174
|
+
|
|
175
|
+
@target = self.class.create_target_name(target)
|
|
176
|
+
|
|
177
|
+
tokens.each do |token|
|
|
178
|
+
key, eq, value = token.partition('=')
|
|
179
|
+
store_param(key, eq.empty? ? nil : value)
|
|
180
|
+
end
|
|
181
|
+
validate_svcparams(@priority, @params)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def rdata_to_string #:nodoc: all
|
|
185
|
+
return '' if @priority.nil? || @target.nil?
|
|
186
|
+
|
|
187
|
+
params_to_hash.inject(+"#{@priority} #{@target.to_s(true)}") do |s, (name, value)|
|
|
188
|
+
s << (value.nil? ? " #{name}" : " #{name}=#{value}")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def encode_rdata(msg, canonical=false) #:nodoc: all
|
|
193
|
+
# An RRset delete (RFC 2136 sec 2.5.2) names the type with no RDATA at
|
|
194
|
+
# all, which is what Update#delete builds; domain_name.rb does the same.
|
|
195
|
+
if @priority.nil? && @target.nil?
|
|
196
|
+
return if [Classes::NONE, Classes::ANY].include?(klass)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
if @priority.nil? || @target.nil?
|
|
200
|
+
raise EncodeError.new('SVCB record needs both a SvcPriority and a ' \
|
|
201
|
+
"TargetName, got #{@priority.inspect} and #{@target.inspect}")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# params is publicly mutable, so construction-time checks alone are
|
|
205
|
+
# insufficient. Validate before pack('n') can truncate a wire length.
|
|
206
|
+
params = (@params || {}).to_a
|
|
207
|
+
length = 3 + @target.to_a.sum { |label| 1 + label.string.bytesize }
|
|
208
|
+
begin
|
|
209
|
+
params.each do |num, value|
|
|
210
|
+
unless num.is_a?(Integer) && (0...INVALID_KEY).cover?(num) && value.is_a?(String)
|
|
211
|
+
raise DecodeError.new('SvcParams must map valid numeric keys to wire-format strings')
|
|
212
|
+
end
|
|
213
|
+
validate_param_value(num, value)
|
|
214
|
+
length += 4 + value.bytesize
|
|
215
|
+
end
|
|
216
|
+
validate_svcparams(@priority, @params || {})
|
|
217
|
+
rescue DecodeError => e
|
|
218
|
+
raise EncodeError.new(e.message)
|
|
219
|
+
end
|
|
220
|
+
if length > 0xffff
|
|
221
|
+
raise EncodeError.new("SVCB RDATA exceeds 65535 octets: #{length}")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
msg.put_pack('n', @priority)
|
|
225
|
+
# TargetName is never compressed (RFC 9460 section 2.2). Its case is
|
|
226
|
+
# preserved in canonical RDATA for DNSSEC (RFC 3597 section 7).
|
|
227
|
+
msg.put_name(@target, true, false)
|
|
228
|
+
params.sort_by(&:first).each do |num, value|
|
|
229
|
+
msg.put_pack('n', num)
|
|
230
|
+
msg.put_pack('n', value.bytesize)
|
|
231
|
+
msg.put_bytes(value.b)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def self.decode_rdata(msg) #:nodoc: all
|
|
236
|
+
# The reading half of the empty record above.
|
|
237
|
+
unless msg.has_remaining?
|
|
238
|
+
record = new([nil, nil, {}])
|
|
239
|
+
record.instance_variable_set(:@rdata, ''.b)
|
|
240
|
+
return record
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
start = msg.index
|
|
244
|
+
priority, = msg.get_unpack('n')
|
|
245
|
+
target = decode_target_name(msg)
|
|
246
|
+
params = {}
|
|
247
|
+
last_key = nil
|
|
248
|
+
while msg.has_remaining?
|
|
249
|
+
key, length = msg.get_unpack('nn')
|
|
250
|
+
if last_key && key <= last_key
|
|
251
|
+
raise DecodeError.new('SvcParams must be in strictly increasing key order without duplicates')
|
|
252
|
+
end
|
|
253
|
+
last_key = key
|
|
254
|
+
# get_bytes returns what is there, so a length past the end would end
|
|
255
|
+
# this loop short. Message.decode catches that at the RDLENGTH
|
|
256
|
+
# boundary; RR.new_from_data, decoding RDATA alone, has none.
|
|
257
|
+
value = msg.get_bytes(length).to_s # nil past the end of the buffer
|
|
258
|
+
if value.bytesize != length
|
|
259
|
+
raise DecodeError.new("SvcParamValue for #{num_to_key(key)} is truncated: " \
|
|
260
|
+
"#{length} octets declared, #{value.bytesize} present")
|
|
261
|
+
end
|
|
262
|
+
validate_param_value(key, value)
|
|
263
|
+
params[key] = value
|
|
264
|
+
end
|
|
265
|
+
validate_svcparams(priority, params)
|
|
266
|
+
record = new([priority, target, params])
|
|
267
|
+
# Keep the original bytes in @rdata for callers such as JSONDNS that
|
|
268
|
+
# read it directly. The public data/rdata readers rebuild the bytes
|
|
269
|
+
# from the current fields.
|
|
270
|
+
record.instance_variable_set(:@rdata, msg.data.byteslice(start, msg.index - start).b)
|
|
271
|
+
record
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def self.create_target_name(value) #:nodoc: all
|
|
275
|
+
# IDNA conversion case-folds ASCII, but RFC 3597 sec 7 requires the
|
|
276
|
+
# original octets in SVCB/HTTPS canonical RDATA. DNS presentation
|
|
277
|
+
# strings (including escaped wire octets) are already ASCII; parse
|
|
278
|
+
# them directly. Retain Name.create's IDNA handling for Unicode input
|
|
279
|
+
# and its copying behavior for an existing Name.
|
|
280
|
+
if value.is_a?(String)
|
|
281
|
+
value = Name.punycode(value) unless value.ascii_only?
|
|
282
|
+
# Only an unescaped final dot denotes the root. A literal dot in
|
|
283
|
+
# the last label (foo\.) still needs the zone origin appended.
|
|
284
|
+
absolute = /(?<!\\)(?:\\\\)*\.\z/.match?(value)
|
|
285
|
+
Name.new(Name.split_escaped(value), absolute)
|
|
286
|
+
else
|
|
287
|
+
Name.create(value)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def validate_decoded_rdata #:nodoc: all
|
|
292
|
+
if @priority.nil? && ![Classes::ANY, Classes::NONE].include?(klass)
|
|
293
|
+
raise DecodeError.new('Empty SVCB RDATA is only valid for ANY/NONE UPDATE markers')
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# RFC 9460 requires an uncompressed, complete TargetName. The generic
|
|
298
|
+
# name decoder also supports pointers and tolerates end-of-buffer, so
|
|
299
|
+
# read this field explicitly rather than synthesizing a missing root.
|
|
300
|
+
def self.decode_target_name(msg) #:nodoc: all
|
|
301
|
+
labels = []
|
|
302
|
+
loop do
|
|
303
|
+
raise DecodeError.new('SVCB TargetName is truncated') unless msg.has_remaining?
|
|
304
|
+
length, = msg.get_unpack('C')
|
|
305
|
+
raise DecodeError.new('SVCB TargetName is truncated') if length.nil?
|
|
306
|
+
break if length.zero?
|
|
307
|
+
if length > 63
|
|
308
|
+
raise DecodeError.new('SVCB TargetName must contain uncompressed labels')
|
|
309
|
+
end
|
|
310
|
+
msg.assert_buffer_position_valid(msg.index + length)
|
|
311
|
+
bytes = msg.get_bytes(length)
|
|
312
|
+
if bytes.nil? || bytes.bytesize != length
|
|
313
|
+
raise DecodeError.new('SVCB TargetName label is truncated')
|
|
314
|
+
end
|
|
315
|
+
labels << Name::Label.new(bytes)
|
|
316
|
+
end
|
|
317
|
+
Name.new(labels)
|
|
318
|
+
rescue ResolvError => e
|
|
319
|
+
raise DecodeError.new(e.message)
|
|
320
|
+
end
|
|
321
|
+
private_class_method :decode_target_name
|
|
322
|
+
|
|
323
|
+
# Returns the SvcParams as a Hash mapping the mnemonic (or "keyNNNNN") to
|
|
324
|
+
# its presentation-format value, nil for a value-less key.
|
|
325
|
+
def params_to_hash
|
|
326
|
+
sorted_params.each_with_object({}) do |(num, value), result|
|
|
327
|
+
result[num_to_key(num)] = decode_param_value(num, value)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
private
|
|
332
|
+
|
|
333
|
+
# RFC 9460 sec 2.2: the SvcParams are written in increasing key order.
|
|
334
|
+
def sorted_params
|
|
335
|
+
(@params || {}).sort_by(&:first)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Neither field at all is the empty record above, which stays allowed.
|
|
339
|
+
def check_complete_pair
|
|
340
|
+
return if @priority.nil? == @target.nil?
|
|
341
|
+
|
|
342
|
+
given, missing = @priority.nil? ? %w[target priority] : %w[priority target]
|
|
343
|
+
raise DecodeError.new("SVCB record given a #{given} but no #{missing}; both are mandatory")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Both constructors supply presentation text. Check restrictions before
|
|
347
|
+
# character-string decoding can erase an otherwise forbidden escape.
|
|
348
|
+
def store_param(key, value)
|
|
349
|
+
num = key_to_num(key)
|
|
350
|
+
if @params.key?(num)
|
|
351
|
+
raise DecodeError.new("duplicate SvcParamKey: #{num_to_key(num)}")
|
|
352
|
+
end
|
|
353
|
+
generic = key.downcase.start_with?('key')
|
|
354
|
+
if !generic && KEYS_FORBIDDING_ESCAPES.include?(num) && value&.include?('\\')
|
|
355
|
+
raise DecodeError.new("SvcParamValue for #{num_to_key(num)} must not contain escape sequences")
|
|
356
|
+
end
|
|
357
|
+
value = decode_presentation_value(value) unless value.nil?
|
|
358
|
+
# RFC 9460 sec 2.1: keyNNNNN uses the generic character-string form,
|
|
359
|
+
# even when that number has a known mnemonic. Its decoded octets are
|
|
360
|
+
# already wire format, rather than input for the mnemonic's codec.
|
|
361
|
+
if generic
|
|
362
|
+
wire = value.to_s.b
|
|
363
|
+
validate_param_value(num, wire)
|
|
364
|
+
@params[num] = wire
|
|
365
|
+
else
|
|
366
|
+
@params[num] = encode_param_value(num, value)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# The presentation <-> wire codec for the SvcParams, needed by
|
|
371
|
+
# .decode_rdata and by the instance methods alike. Extended and included
|
|
372
|
+
# so that the one `private` above covers both scopes.
|
|
373
|
+
module Codec #:nodoc: all
|
|
374
|
+
private
|
|
375
|
+
|
|
376
|
+
# Translates a mnemonic or "keyNNNNN" token to its number. RFC 9460 sec
|
|
377
|
+
# 2.1 writes that number without leading zeros, so key01 is not alpn.
|
|
378
|
+
def key_to_num(key)
|
|
379
|
+
key = key.downcase
|
|
380
|
+
return KEY_NAME_TO_NUM[key] if KEY_NAME_TO_NUM.key?(key)
|
|
381
|
+
if (m = /\Akey(0|[1-9]\d*)\z/.match(key))
|
|
382
|
+
num = m[1].to_i
|
|
383
|
+
raise DecodeError.new("SvcParamKey out of range: #{key}") if num > 0xffff
|
|
384
|
+
if num == INVALID_KEY
|
|
385
|
+
raise DecodeError.new("SvcParamKey #{INVALID_KEY} is reserved as invalid")
|
|
386
|
+
end
|
|
387
|
+
return num
|
|
388
|
+
end
|
|
389
|
+
raise DecodeError.new("Unknown SvcParamKey: #{key.inspect}")
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def num_to_key(num)
|
|
393
|
+
KEY_NUM_TO_NAME[num] || "key#{num}"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# String#to_i and pack('n') fail silently: "99999" becomes 34463.
|
|
397
|
+
def to_uint16(value, field)
|
|
398
|
+
text = value.to_s.strip
|
|
399
|
+
unless /\A\d+\z/.match?(text) && text.to_i <= 0xffff
|
|
400
|
+
raise DecodeError.new("#{field} must be an integer in 0..65535, got #{value.inspect}")
|
|
401
|
+
end
|
|
402
|
+
text.to_i
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Splits the SvcParams into whitespace-separated tokens, keeping
|
|
406
|
+
# double-quoted sections (which may contain spaces) intact and treating
|
|
407
|
+
# a backslash as escaping the octet after it.
|
|
408
|
+
def split_svcparams(str)
|
|
409
|
+
# Checked first: on an unterminated quote the scan below silently
|
|
410
|
+
# splits the value at its spaces instead.
|
|
411
|
+
if unterminated_quote?(str)
|
|
412
|
+
raise DecodeError.new("unterminated quoted SvcParamValue in #{str.inspect}")
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
str.scan(/(?:"(?:\\.|[^"\\])*"|\\.|[^\s"])+/m)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Drops the escaped octets, then an odd number of quotes is left open.
|
|
419
|
+
def unterminated_quote?(str)
|
|
420
|
+
str.gsub(/\\./m, '').count('"').odd?
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Runs before the per-key encoding -- see the value-list note below.
|
|
424
|
+
def decode_presentation_value(value)
|
|
425
|
+
unescape_char_string(unquote(value))
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def unquote(value)
|
|
429
|
+
if value.length >= 2 && value.start_with?('"') && value.end_with?('"')
|
|
430
|
+
value[1...-1]
|
|
431
|
+
else
|
|
432
|
+
value
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Resolves the backslash escapes of a character-string (\X and \DDD
|
|
437
|
+
# decimal), in octets, so a multibyte character becomes the octets it
|
|
438
|
+
# stands for.
|
|
439
|
+
def unescape_char_string(str)
|
|
440
|
+
str = str.b
|
|
441
|
+
result = +''.b
|
|
442
|
+
i = 0
|
|
443
|
+
while i < str.length
|
|
444
|
+
c = str[i]
|
|
445
|
+
if c == '\\'
|
|
446
|
+
# RFC 1035 sec 5.1: the backslash escapes the octet after it.
|
|
447
|
+
raise DecodeError.new("escape at end of #{str.inspect} has nothing to escape") if
|
|
448
|
+
i + 1 >= str.length
|
|
449
|
+
|
|
450
|
+
nxt = str[i + 1]
|
|
451
|
+
if nxt =~ /\d/
|
|
452
|
+
# \DDD is exactly three digits, and stands for one octet.
|
|
453
|
+
unless str[i + 1, 3] =~ /\A\d{3}\z/
|
|
454
|
+
raise DecodeError.new("escape #{str[i, 4].inspect} must be three digits")
|
|
455
|
+
end
|
|
456
|
+
octet = str[i + 1, 3].to_i
|
|
457
|
+
if octet > 0xff
|
|
458
|
+
raise DecodeError.new("escape \\#{str[i + 1, 3]} is not an octet value")
|
|
459
|
+
end
|
|
460
|
+
result << octet.chr
|
|
461
|
+
i += 4
|
|
462
|
+
else
|
|
463
|
+
result << nxt
|
|
464
|
+
i += 2
|
|
465
|
+
end
|
|
466
|
+
elsif c == '"'
|
|
467
|
+
# The surrounding quotes came off in unquote, so this one is data.
|
|
468
|
+
raise DecodeError.new("unescaped double quote in SvcParamValue #{str.inspect}")
|
|
469
|
+
else
|
|
470
|
+
result << c
|
|
471
|
+
i += 1
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
result
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Escapes raw octets into a presentation character-string. Four printable
|
|
478
|
+
# characters need escaping in an unquoted value: the quote delimits a
|
|
479
|
+
# character-string, a semicolon starts a comment, and parentheses group
|
|
480
|
+
# a record across lines. The numeric form handles them all uniformly.
|
|
481
|
+
def escape_char_string(str)
|
|
482
|
+
result = +''
|
|
483
|
+
str.each_byte do |b|
|
|
484
|
+
if b == 0x5c # backslash
|
|
485
|
+
result << '\\\\'
|
|
486
|
+
elsif b <= 0x20 || b > 0x7e || # non-printable or space
|
|
487
|
+
b == 0x22 || b == 0x3b || # " and ;
|
|
488
|
+
b == 0x28 || b == 0x29 # ( and )
|
|
489
|
+
result << format('\\%03d', b)
|
|
490
|
+
else
|
|
491
|
+
result << b.chr
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
result
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# RFC 9460 Appendix A: value-list decoding follows character-string
|
|
498
|
+
# decoding. Only an escaped comma or backslash is valid at this layer.
|
|
499
|
+
|
|
500
|
+
def split_value_list(str)
|
|
501
|
+
str = str.b
|
|
502
|
+
items = []
|
|
503
|
+
current = +''.b
|
|
504
|
+
i = 0
|
|
505
|
+
while i < str.length
|
|
506
|
+
c = str[i]
|
|
507
|
+
if c == '\\'
|
|
508
|
+
escaped = str[i + 1]
|
|
509
|
+
unless escaped == ',' || escaped == '\\'
|
|
510
|
+
raise DecodeError.new("invalid value-list escape in #{str.inspect}: " \
|
|
511
|
+
'expected an escaped comma or backslash')
|
|
512
|
+
end
|
|
513
|
+
current << escaped
|
|
514
|
+
i += 2
|
|
515
|
+
elsif c == ','
|
|
516
|
+
items << current
|
|
517
|
+
current = +''.b
|
|
518
|
+
i += 1
|
|
519
|
+
else
|
|
520
|
+
current << c
|
|
521
|
+
i += 1
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
items << current
|
|
525
|
+
items
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Splits a value-list, rejecting the empty item a leading, trailing or
|
|
529
|
+
# doubled comma leaves behind. No SvcParamValue list admits one.
|
|
530
|
+
def value_list_items(num, value)
|
|
531
|
+
items = split_value_list(value)
|
|
532
|
+
if items.any?(&:empty?)
|
|
533
|
+
raise DecodeError.new(
|
|
534
|
+
"SvcParamValue for #{num_to_key(num)} must not contain an empty item")
|
|
535
|
+
end
|
|
536
|
+
items
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def join_value_list(items)
|
|
540
|
+
items.map do |item|
|
|
541
|
+
escaped = +''.b
|
|
542
|
+
item.to_s.each_byte do |b|
|
|
543
|
+
escaped << '\\' if b == 0x5c || b == 0x2c # backslash or comma
|
|
544
|
+
escaped << b.chr
|
|
545
|
+
end
|
|
546
|
+
escaped
|
|
547
|
+
end.join(',')
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Packs a value-list of address text into an address hint. IPv4.create
|
|
551
|
+
# and IPv6.create raise ArgumentError rather than DecodeError, and the
|
|
552
|
+
# key already names the family their message would.
|
|
553
|
+
def join_addresses(num, value, klass)
|
|
554
|
+
value_list_items(num, value).map do |text|
|
|
555
|
+
klass.create(text).address
|
|
556
|
+
rescue ArgumentError
|
|
557
|
+
raise DecodeError.new(
|
|
558
|
+
"SvcParamValue for #{num_to_key(num)} is not an address: #{text.inspect}")
|
|
559
|
+
end.join
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# True when single-octet-length-prefixed items exactly tile the value,
|
|
563
|
+
# none of them empty -- the format shared by alpn and docpath. An empty
|
|
564
|
+
# value tiles trivially.
|
|
565
|
+
def length_prefixed_tile?(bytes)
|
|
566
|
+
i = 0
|
|
567
|
+
while i < bytes.bytesize
|
|
568
|
+
len = bytes.getbyte(i)
|
|
569
|
+
return false if len.zero? || i + 1 + len > bytes.bytesize
|
|
570
|
+
i += 1 + len
|
|
571
|
+
end
|
|
572
|
+
true
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def pack_length_prefixed(num, items)
|
|
576
|
+
items.map do |item|
|
|
577
|
+
# pack('C') would truncate a longer item to a zero-length one and
|
|
578
|
+
# shift every item after it.
|
|
579
|
+
if item.bytesize > 0xff
|
|
580
|
+
raise DecodeError.new("#{num_to_key(num)} item must be at most 255 " \
|
|
581
|
+
"octets, got #{item.bytesize}")
|
|
582
|
+
end
|
|
583
|
+
[item.bytesize].pack('C') + item
|
|
584
|
+
end.join
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
# Counts in octets: see #decode_param_value.
|
|
588
|
+
def unpack_length_prefixed(bytes)
|
|
589
|
+
items = []
|
|
590
|
+
i = 0
|
|
591
|
+
while i < bytes.bytesize
|
|
592
|
+
len = bytes.getbyte(i)
|
|
593
|
+
items << bytes.byteslice(i + 1, len)
|
|
594
|
+
i += 1 + len
|
|
595
|
+
end
|
|
596
|
+
items
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# Rejects a value whose length does not fit the shape its key defines,
|
|
600
|
+
# which the decoders below would truncate or pad. Unlisted keys take
|
|
601
|
+
# any octets.
|
|
602
|
+
def validate_param_value(num, bytes)
|
|
603
|
+
if num == INVALID_KEY
|
|
604
|
+
raise DecodeError.new("SvcParamKey #{INVALID_KEY} is reserved as invalid")
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
size = bytes.bytesize
|
|
608
|
+
if size > 0xffff
|
|
609
|
+
raise DecodeError.new("SvcParamValue for #{num_to_key(num)} exceeds 65535 octets: #{size}")
|
|
610
|
+
end
|
|
611
|
+
expected =
|
|
612
|
+
case num
|
|
613
|
+
when 0 # mandatory
|
|
614
|
+
'a non-empty, even number of octets' unless size.positive? && size.even?
|
|
615
|
+
when 1, 10 # alpn, docpath
|
|
616
|
+
# Zero docpath segments is the root path (RFC 9953 sec 3).
|
|
617
|
+
unless length_prefixed_tile?(bytes) && (num == 10 || size.positive?)
|
|
618
|
+
'a sequence of non-empty length-prefixed items'
|
|
619
|
+
end
|
|
620
|
+
when *KEYS_FORBIDDING_VALUE
|
|
621
|
+
'empty' unless size.zero?
|
|
622
|
+
when 3 # port
|
|
623
|
+
'exactly 2 octets' unless size == 2
|
|
624
|
+
when 4, 6 # ipv4hint, ipv6hint
|
|
625
|
+
width = num == 4 ? 4 : 16
|
|
626
|
+
"a non-empty multiple of #{width} octets" unless size.positive? && (size % width).zero?
|
|
627
|
+
when 5, 7 # ech, dohpath
|
|
628
|
+
'non-empty' unless size.positive?
|
|
629
|
+
end
|
|
630
|
+
return if expected.nil?
|
|
631
|
+
|
|
632
|
+
raise DecodeError.new(
|
|
633
|
+
"SvcParamValue for #{num_to_key(num)} must be #{expected}, got #{size} octets")
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def validate_svcparams(priority, params)
|
|
637
|
+
validate_mandatory_value(params)
|
|
638
|
+
validate_self_consistency(priority, params)
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# RFC 9460 sec 8 on the mandatory list itself: it names "valid"
|
|
642
|
+
# SvcParamKeys, must not name itself, must not repeat a key, and must be
|
|
643
|
+
# in strictly increasing wire order. These describe one SvcParamValue,
|
|
644
|
+
# so they hold in either mode.
|
|
645
|
+
def validate_mandatory_value(params)
|
|
646
|
+
bytes = params[0]
|
|
647
|
+
return if bytes.nil?
|
|
648
|
+
|
|
649
|
+
keys = bytes.unpack('n*')
|
|
650
|
+
raise DecodeError.new('mandatory must not list itself') if keys.include?(0)
|
|
651
|
+
if keys.include?(INVALID_KEY)
|
|
652
|
+
raise DecodeError.new("mandatory must not list key #{INVALID_KEY}, " \
|
|
653
|
+
'which sec 14.3.2 reserves as invalid')
|
|
654
|
+
end
|
|
655
|
+
unless keys.uniq.length == keys.length
|
|
656
|
+
raise DecodeError.new('mandatory must not list a key more than once')
|
|
657
|
+
end
|
|
658
|
+
# Mnemonic presentation format is unordered and the encoder sorts
|
|
659
|
+
# it. Wire input and generic key0 presentation must already be sorted.
|
|
660
|
+
unless keys == keys.sort
|
|
661
|
+
raise DecodeError.new('mandatory keys must be in strictly increasing order, got ' \
|
|
662
|
+
"#{keys.map { |k| num_to_key(k) }.join(', ')}")
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# RFC 9460 sec 2.4.3: a ServiceMode RR is self-consistent when its
|
|
667
|
+
# SvcParams meet each other's requirements. Sec 8 gives mandatory one,
|
|
668
|
+
# and sec 7.1.1 gives no-default-alpn another. Sec 2.4.2 has recipients
|
|
669
|
+
# ignore an AliasMode RR's SvcParams, so neither applies at priority 0.
|
|
670
|
+
def validate_self_consistency(priority, params)
|
|
671
|
+
return if priority.nil? || priority.zero?
|
|
672
|
+
|
|
673
|
+
if params.key?(2) && !params.key?(1)
|
|
674
|
+
raise DecodeError.new('no-default-alpn requires alpn')
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
keys = params[0]&.unpack('n*') || []
|
|
678
|
+
missing = keys.reject { |k| params.key?(k) }
|
|
679
|
+
return if missing.empty?
|
|
680
|
+
|
|
681
|
+
raise DecodeError.new("mandatory lists #{missing.map { |k| num_to_key(k) }.join(', ')}, " \
|
|
682
|
+
'which the record does not contain')
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# Converts a SvcParamValue whose character-string escapes are already
|
|
686
|
+
# resolved (a String, or nil for a value-less key) into wire-format
|
|
687
|
+
# octets.
|
|
688
|
+
def encode_param_value(num, value)
|
|
689
|
+
if KEYS_FORBIDDING_VALUE.include?(num)
|
|
690
|
+
# An explicitly empty value encodes to the same nothing.
|
|
691
|
+
unless value.nil? || value.empty?
|
|
692
|
+
raise DecodeError.new("SvcParamValue for #{num_to_key(num)} must be empty")
|
|
693
|
+
end
|
|
694
|
+
return ''.b
|
|
695
|
+
end
|
|
696
|
+
if value.nil? && KEYS_REQUIRING_VALUE.include?(num)
|
|
697
|
+
raise DecodeError.new("#{num_to_key(num)} requires a SvcParamValue")
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
wire =
|
|
701
|
+
case num
|
|
702
|
+
when 0 # mandatory
|
|
703
|
+
value_list_items(num, value).map { |k| key_to_num(k) }.sort.
|
|
704
|
+
map { |k| [k].pack('n') }.join
|
|
705
|
+
when 1, 10 # alpn, docpath
|
|
706
|
+
# The root path must not reach value_list_items, which rejects an
|
|
707
|
+
# empty item.
|
|
708
|
+
items = value.nil? || value.empty? ? [] : value_list_items(num, value)
|
|
709
|
+
pack_length_prefixed(num, items)
|
|
710
|
+
when 3 # port
|
|
711
|
+
[to_uint16(value, 'port')].pack('n')
|
|
712
|
+
when 4 # ipv4hint
|
|
713
|
+
join_addresses(num, value, IPv4)
|
|
714
|
+
when 5 # ech
|
|
715
|
+
# Base64.decode64 discards anything outside the alphabet.
|
|
716
|
+
begin
|
|
717
|
+
Base64.strict_decode64(value)
|
|
718
|
+
rescue ArgumentError
|
|
719
|
+
raise DecodeError.new(
|
|
720
|
+
"SvcParamValue for ech must be base64, got #{value.inspect}")
|
|
721
|
+
end
|
|
722
|
+
when 6 # ipv6hint
|
|
723
|
+
join_addresses(num, value, IPv6)
|
|
724
|
+
else # dohpath and unknown keys
|
|
725
|
+
value.to_s
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# dohpath and unknown keys hand the value straight back, and from_hash
|
|
729
|
+
# can pass in UTF-8, which MessageEncoder#put_bytes would reject.
|
|
730
|
+
wire = wire.b
|
|
731
|
+
|
|
732
|
+
# Applied here too, so a zone file cannot build a record that
|
|
733
|
+
# .decode_rdata would refuse.
|
|
734
|
+
validate_param_value(num, wire)
|
|
735
|
+
wire
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# Converts a SvcParamValue into presentation format, or nil for a key
|
|
739
|
+
# written without one. The value normally arrives from the wire, but
|
|
740
|
+
# #params is public, so it may be any String the caller assigned, which
|
|
741
|
+
# is why the branches below work in octets rather than characters.
|
|
742
|
+
def decode_param_value(num, bytes)
|
|
743
|
+
case num
|
|
744
|
+
when 0 # mandatory
|
|
745
|
+
join_value_list(bytes.unpack('n*').map { |k| num_to_key(k) })
|
|
746
|
+
when 1, 10 # alpn, docpath
|
|
747
|
+
# Alone among the value-lists, these items are arbitrary octets, so
|
|
748
|
+
# they need the character-string layer. Empty is the docpath root
|
|
749
|
+
# path, and RFC 9460 Appendix A has no empty char-string, so it
|
|
750
|
+
# stands alone. alpn is never empty.
|
|
751
|
+
unless bytes.empty?
|
|
752
|
+
escape_char_string(join_value_list(unpack_length_prefixed(bytes)))
|
|
753
|
+
end
|
|
754
|
+
when *KEYS_FORBIDDING_VALUE
|
|
755
|
+
nil
|
|
756
|
+
when 3 # port
|
|
757
|
+
bytes.unpack1('n').to_s
|
|
758
|
+
when 4 # ipv4hint
|
|
759
|
+
# .b: String#scan and IPv4/IPv6::new count characters.
|
|
760
|
+
join_value_list(bytes.b.scan(/.{4}/m).map { |a| IPv4.new(a).to_s })
|
|
761
|
+
when 5 # ech
|
|
762
|
+
Base64.strict_encode64(bytes)
|
|
763
|
+
when 6 # ipv6hint
|
|
764
|
+
join_value_list(bytes.b.scan(/.{16}/m).map { |a| IPv6.new(a).to_s_canonical })
|
|
765
|
+
else # dohpath and unknown keys
|
|
766
|
+
bytes.empty? ? nil : escape_char_string(bytes)
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
private_constant :Codec
|
|
771
|
+
extend Codec
|
|
772
|
+
include Codec
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
end
|