dns_json 0.1.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.
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DNSJSON
4
+ module RData
5
+ FORMATS = YAML.safe_load_file(File.expand_path("../../data/rdata_formats.yml", __dir__)).transform_values { |v| v.fetch("name").freeze }.freeze
6
+ NAMES = [2, 5, 12, 39].freeze
7
+ BASE32 = "0123456789ABCDEFGHIJKLMNOPQRSTUV"
8
+ class Cursor
9
+ attr_reader :position
10
+
11
+ def initialize(bytes, offset, length, max_pointers)
12
+ @bytes = bytes
13
+ @position = offset
14
+ @ending = offset + length
15
+ @max_pointers = max_pointers
16
+ end
17
+
18
+ def remaining = @ending - @position
19
+
20
+ def take(size = remaining)
21
+ raise ConversionError, "truncated RDATA" if size.negative? || @position + size > @ending || @position + size > @bytes.bytesize
22
+
23
+ result = @bytes.byteslice(@position, size)
24
+ @position += size
25
+ result
26
+ end
27
+
28
+ def u8 = take(1).unpack1("C")
29
+ def u16 = take(2).unpack1("n")
30
+ def u32 = take(4).unpack1("N")
31
+
32
+ def name
33
+ labels, count = Wire::Name.read(@bytes, @position, max_pointers: @max_pointers)
34
+ take(count)
35
+ Wire::Name.display(labels)
36
+ end
37
+
38
+ def strings
39
+ result = []
40
+ result << take(u8) until remaining.zero?
41
+ result
42
+ end
43
+ end
44
+
45
+ def self.decode(type, bytes, offset, length, max_pointers: 128)
46
+ return unless FORMATS.key?(type)
47
+
48
+ c = Cursor.new(bytes, offset, length, max_pointers)
49
+ result = case type
50
+ when 1, 28
51
+ expected = type == 1 ? 4 : 16
52
+ raise ConversionError, "invalid address length" unless length == expected
53
+
54
+ IPAddr.new_ntoh(c.take).to_s
55
+ when *NAMES then c.name
56
+ when 16 then octets_text(c.strings.join)
57
+ when 99 then c.strings.map { |s| quote(s) }.join(" ")
58
+ when 15 then [c.u16, c.name].join(" ")
59
+ when 33 then [c.u16, c.u16, c.u16, c.name].join(" ")
60
+ when 25, 48, 60 then [c.u16, c.u8, c.u8, base64_encode(c.take)].join(" ")
61
+ when 43, 59 then [c.u16, c.u8, c.u8, DNSJSON.hex(c.take)].join(" ")
62
+ when 44 then [c.u8, c.u8, DNSJSON.hex(c.take)].join(" ")
63
+ when 52, 53 then [c.u8, c.u8, c.u8, DNSJSON.hex(c.take)].join(" ")
64
+ when 61 then base64_encode(c.take)
65
+ when 62 then [c.u32, c.u16, *bitmap_decode(c.take)].join(" ")
66
+ when 47 then [c.name, *bitmap_decode(c.take)].join(" ")
67
+ when 50, 51
68
+ parts = [c.u8, c.u8, c.u16]
69
+ salt = DNSJSON.hex(c.take(c.u8))
70
+ parts << (salt.empty? ? "-" : salt)
71
+ parts.push(base32_encode(c.take(c.u8)), *bitmap_decode(c.take)) if type == 50
72
+ parts.join(" ")
73
+ when 46
74
+ [Registry.type(c.u16), c.u8, c.u8, c.u32, timestamp(c.u32), timestamp(c.u32), c.u16, c.name,
75
+ base64_encode(c.take)].join(" ")
76
+ when 45
77
+ precedence = c.u8
78
+ gateway_type = c.u8
79
+ algorithm = c.u8
80
+ gateway = case gateway_type
81
+ when 0 then "."
82
+ when 1 then IPAddr.new_ntoh(c.take(4)).to_s
83
+ when 2 then IPAddr.new_ntoh(c.take(16)).to_s
84
+ when 3 then c.name
85
+ else raise ConversionError, "unknown IPSECKEY gateway type"
86
+ end
87
+ key = c.take
88
+ [precedence, gateway_type, algorithm, gateway, key.empty? ? "." : base64_encode(key)].join(" ")
89
+ when 55
90
+ hit_length = c.u8
91
+ algorithm = c.u8
92
+ key_length = c.u16
93
+ parts = [algorithm, DNSJSON.hex(c.take(hit_length)), base64_encode(c.take(key_length))]
94
+ parts << c.name until c.remaining.zero?
95
+ parts.join(" ")
96
+ end
97
+ raise ConversionError, "excess RDATA" unless c.remaining.zero?
98
+
99
+ result
100
+ rescue ArgumentError, EncodingError => e
101
+ raise ConversionError, e.message
102
+ end
103
+
104
+ def self.encode(type, text)
105
+ raise ConversionError, "unsupported structured RDATA type" unless FORMATS.key?(type)
106
+ raise ConversionError, "RDATA must be a string" unless text.is_a?(String)
107
+ return address(text, type == 1 ? 4 : 16) if [1, 28].include?(type)
108
+ return Wire::Name.encode(text) if NAMES.include?(type)
109
+ return strings_encode([text_octets(text)]) if type == 16
110
+ return strings_encode(parse_strings(text)) if type == 99
111
+
112
+ parts = text.split(/\s+/)
113
+ result = case type
114
+ when 15 then integer(parts.shift, 16) + Wire::Name.encode(required(parts.shift))
115
+ when 33 then numbers(parts, 16, 16, 16) + Wire::Name.encode(required(parts.shift))
116
+ when 25, 48, 60 then numbers(parts, 16, 8, 8) + b64(parts)
117
+ when 43, 59 then numbers(parts, 16, 8, 8) + hex_tail(parts)
118
+ when 44 then numbers(parts, 8, 8) + hex_tail(parts)
119
+ when 52, 53 then numbers(parts, 8, 8, 8) + hex_tail(parts)
120
+ when 61 then b64(parts)
121
+ when 62 then numbers(parts, 32, 16) + bitmap_encode(parts.shift(parts.length))
122
+ when 47 then Wire::Name.encode(required(parts.shift)) + bitmap_encode(parts.shift(parts.length))
123
+ when 50, 51
124
+ head = numbers(parts, 8, 8, 16)
125
+ salt_text = required(parts.shift)
126
+ salt = salt_text == "-" ? "".b : DNSJSON.unhex(salt_text.upcase)
127
+ head += counted(salt)
128
+ head += counted(base32_decode(required(parts.shift))) + bitmap_encode(parts.shift(parts.length)) if type == 50
129
+ head
130
+ when 46
131
+ head = integer(Registry.number(required(parts.shift)), 16) + numbers(parts, 8, 8, 32)
132
+ head += integer(parse_time(parts.shift), 32) + integer(parse_time(parts.shift), 32) + numbers(parts, 16)
133
+ head + Wire::Name.encode(required(parts.shift)) + b64(parts)
134
+ when 45
135
+ gateway_type = parts[1]&.to_i
136
+ head = numbers(parts, 8, 8, 8)
137
+ gateway = required(parts.shift)
138
+ head += case gateway_type
139
+ when 0
140
+ raise ConversionError, "gateway type 0 requires dot" unless gateway == "."
141
+
142
+ "".b
143
+ when 1 then address(gateway, 4)
144
+ when 2 then address(gateway, 16)
145
+ when 3 then Wire::Name.encode(gateway)
146
+ else raise ConversionError, "unknown IPSECKEY gateway type"
147
+ end
148
+ key = if parts == ["."]
149
+ (parts.clear
150
+ "".b)
151
+ else
152
+ b64(parts)
153
+ end
154
+ head + key
155
+ when 55
156
+ algorithm = integer(parts.shift, 8)
157
+ hit = DNSJSON.unhex(required(parts.shift).upcase)
158
+ key = base64_decode(required(parts.shift))
159
+ integer(hit.bytesize, 8) + algorithm + integer(key.bytesize, 16) + hit + key + parts.shift(parts.length).map { |n|
160
+ Wire::Name.encode(n)
161
+ }.join.b
162
+ end
163
+ raise ConversionError, "excess RDATA fields" unless parts.empty?
164
+ raise ConversionError, "RDATA exceeds 65535 bytes" if result.bytesize > 65_535
165
+
166
+ result
167
+ rescue ArgumentError, EncodingError => e
168
+ raise ConversionError, e.message
169
+ end
170
+
171
+ def self.base64_encode(bytes) = [bytes].pack("m0")
172
+ def self.base64_decode(text) = text.unpack1("m0")
173
+
174
+ def self.required(value)
175
+ raise ConversionError, "missing RDATA field" if value.nil?
176
+
177
+ value
178
+ end
179
+
180
+ def self.integer(value, bits)
181
+ raise ConversionError, "invalid unsigned integer" unless required(value).to_s.match?(/\A\d+\z/)
182
+
183
+ value = value.to_i
184
+ raise ConversionError, "integer out of range" unless value.between?(0, (1 << bits) - 1)
185
+
186
+ [value].pack({ 8 => "C", 16 => "n", 32 => "N" }.fetch(bits))
187
+ end
188
+
189
+ def self.numbers(parts, *widths) = widths.map { |bits| integer(parts.shift, bits) }.join.b
190
+ def self.b64(parts) = base64_decode(parts.shift(parts.length).join)
191
+ def self.hex_tail(parts) = DNSJSON.unhex(parts.shift(parts.length).join.upcase)
192
+ def self.counted(bytes) = integer(bytes.bytesize, 8) + bytes
193
+
194
+ def self.address(text, length)
195
+ ip = IPAddr.new(text)
196
+ raise ConversionError, "wrong IP family or prefix" if text.include?("/") || ip.hton.bytesize != length
197
+
198
+ ip.hton
199
+ end
200
+
201
+ def self.octets_text(bytes) = bytes.force_encoding(Encoding::ISO_8859_1).encode(Encoding::UTF_8)
202
+ def self.text_octets(text) = text.encode(Encoding::ISO_8859_1).b
203
+
204
+ def self.strings_encode(strings)
205
+ strings.flat_map { |s| s.empty? ? ["".b] : s.bytes.each_slice(255).map { |v| v.pack("C*") } }.map { |s| counted(s) }.join.b
206
+ end
207
+
208
+ def self.quote(bytes)
209
+ content = bytes.bytes.map do |b|
210
+ if b.between?(32, 126)
211
+ [34, 92].include?(b) ? "\\#{b.chr}" : b.chr
212
+ else
213
+ format("\\%03d", b)
214
+ end
215
+ end.join
216
+ "\"#{content}\""
217
+ end
218
+
219
+ def self.parse_strings(text)
220
+ strings = []
221
+ current = nil
222
+ index = 0
223
+ while index < text.bytesize
224
+ byte = text.getbyte(index)
225
+ if current.nil?
226
+ if byte == 34 then current = +"".b
227
+ elsif ![9, 10, 13, 32].include?(byte) then raise ConversionError, "SPF strings must be quoted"
228
+ end
229
+ elsif byte == 34
230
+ strings << current
231
+ current = nil
232
+ elsif byte == 92
233
+ index += 1
234
+ digits = text.byteslice(index, 3)
235
+ raise ConversionError, "unfinished string escape" unless digits && !digits.empty?
236
+
237
+ if digits.match?(/\A\d{3}\z/)
238
+ raise ConversionError, "octet escape out of range" if digits.to_i > 255
239
+
240
+ current << digits.to_i
241
+ index += 2
242
+ else
243
+ current << text.getbyte(index)
244
+ end
245
+ else
246
+ current << byte
247
+ end
248
+ index += 1
249
+ end
250
+ raise ConversionError, "unclosed or missing SPF string" if current || strings.empty?
251
+
252
+ strings
253
+ end
254
+
255
+ def self.timestamp(value) = Time.at(value).utc.strftime("%Y%m%d%H%M%S")
256
+
257
+ def self.parse_time(text)
258
+ text = required(text)
259
+ return text if text.match?(/\A\d{1,10}\z/)
260
+ raise ConversionError, "invalid DNSSEC timestamp" unless text.match?(/\A\d{14}\z/)
261
+
262
+ time = Time.strptime("#{text} +0000", "%Y%m%d%H%M%S %z").utc
263
+ raise ConversionError, "invalid DNSSEC timestamp" unless timestamp(time.to_i) == text
264
+
265
+ time.to_i
266
+ end
267
+
268
+ def self.bitmap_decode(bytes)
269
+ types = []
270
+ offset = 0
271
+ previous = -1
272
+ while offset < bytes.bytesize
273
+ window, length = bytes.byteslice(offset, 2).unpack("CC")
274
+ raise ConversionError, "invalid type bitmap" unless length&.between?(1, 32) && window > previous && offset + 2 + length <= bytes.bytesize
275
+
276
+ bitmap = bytes.byteslice(offset + 2, length)
277
+ raise ConversionError, "noncanonical type bitmap" if bitmap.getbyte(length - 1).zero?
278
+
279
+ bitmap.bytes.each_with_index do |value, index|
280
+ 8.times { |bit| types << Registry.type((window * 256) + (index * 8) + bit) unless value.nobits?(128 >> bit) }
281
+ end
282
+ previous = window
283
+ offset += length + 2
284
+ end
285
+ types
286
+ end
287
+
288
+ def self.bitmap_encode(names)
289
+ names.map { |n| Registry.number(n) }.uniq.sort.group_by { |n| n / 256 }.map do |window, values|
290
+ bitmap = Array.new(((values.last % 256) / 8) + 1, 0)
291
+ values.each { |n| bitmap[(n % 256) / 8] |= 128 >> (n % 8) }
292
+ [window, bitmap.length, *bitmap].pack("C*")
293
+ end.join.b
294
+ end
295
+
296
+ def self.base32_encode(bytes)
297
+ bits = bytes.unpack1("B*")
298
+ bits += "0" * ((5 - (bits.length % 5)) % 5)
299
+ bits.scan(/.{5}/).map { |b| BASE32[b.to_i(2)] }.join
300
+ end
301
+
302
+ def self.base32_decode(text)
303
+ raise ConversionError, "invalid base32hex" unless text.match?(/\A[0-9A-V]+\z/i)
304
+
305
+ bits = text.upcase.chars.map { |c| BASE32.index(c).to_s(2).rjust(5, "0") }.join
306
+ bytes = [bits[0, bits.length / 8 * 8]].pack("B*")
307
+ raise ConversionError, "noncanonical base32hex" unless base32_encode(bytes) == text.upcase
308
+
309
+ bytes
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DNSJSON
4
+ module Registry
5
+ TYPES = YAML.safe_load_file(File.expand_path("../../data/rr_types.yml", __dir__)).transform_values(&:freeze).freeze
6
+ CLASSES = { 1 => "IN", 3 => "CH", 4 => "HS" }.freeze
7
+ def self.type(number) = TYPES.fetch(number) { "TYPE#{number}" }
8
+ def self.klass(number) = CLASSES.fetch(number) { "CLASS#{number}" }
9
+
10
+ def self.number(text, kind = :type)
11
+ table, prefix = kind == :type ? [TYPES, "TYPE"] : [CLASSES, "CLASS"]
12
+ number = table.key(text) || (text.to_s.match?(/\A#{prefix}\d+\z/) && text.delete_prefix(prefix).to_i)
13
+ raise ConversionError, "invalid #{kind} name" unless number.is_a?(Integer) && number.between?(0, 65_535)
14
+
15
+ number
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DNSJSON
4
+ module Stream
5
+ class Writer
6
+ def initialize(io, profile: Profile::FULL)
7
+ @io = io
8
+ @profile = profile
9
+ end
10
+
11
+ def <<(message)
12
+ @io.write("\x1e#{message.to_json(profile: @profile)}\n")
13
+ self
14
+ end
15
+ end
16
+
17
+ class Reader
18
+ include Enumerable
19
+
20
+ attr_reader :problems, :dropped_problems
21
+
22
+ def initialize(io, strict: false, max_json_bytes: LIMITS[:max_json_bytes], max_problems: 1000, **options)
23
+ @io = io
24
+ @strict = strict
25
+ @max_bytes = max_json_bytes
26
+ @options = options
27
+ @problems = []
28
+ raise ArgumentError, "limits must be positive integers" unless [max_json_bytes, max_problems].all? { |n| n.is_a?(Integer) && n.positive? }
29
+
30
+ @max_problems = max_problems
31
+ @dropped_problems = 0
32
+ end
33
+
34
+ def each(&block)
35
+ return enum_for(:each) unless block_given?
36
+
37
+ buffer = +"".b
38
+ started = false
39
+ oversized = false
40
+ index = 0
41
+ while (chunk = @io.read(4096)) && !chunk.empty?
42
+ chunk.each_byte do |byte|
43
+ if byte == 30
44
+ consume(buffer, index, &block) if started && !buffer.strip.empty? && !oversized
45
+ index += 1 if started
46
+ buffer = +"".b
47
+ started = true
48
+ oversized = false
49
+ elsif started && !oversized
50
+ buffer << byte
51
+ if buffer.bytesize > @max_bytes
52
+ report(LimitError.new("sequence element exceeds max_json_bytes"), index)
53
+ buffer = +"".b
54
+ oversized = true
55
+ end
56
+ elsif !started && ![9, 10, 13, 32].include?(byte)
57
+ report(ParseError.new("content before first record separator"), index)
58
+ started = true
59
+ oversized = true
60
+ end
61
+ end
62
+ end
63
+ consume(buffer, index, &block) if started && !buffer.strip.empty? && !oversized
64
+ self
65
+ end
66
+
67
+ private
68
+
69
+ def consume(buffer, index)
70
+ message = begin
71
+ DNSJSON.parse(buffer, max_json_bytes: @max_bytes, **@options)
72
+ rescue Error => e
73
+ report(e, index)
74
+ nil
75
+ end
76
+ yield message if message
77
+ end
78
+
79
+ def report(error, index)
80
+ raise error if @strict
81
+
82
+ if @problems.length < @max_problems
83
+ @problems << Problem.new(offset: index, reason: error.message)
84
+ else
85
+ @dropped_problems += 1
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DnsJson
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DNSJSON
4
+ module Wire
5
+ module Name
6
+ def self.read(bytes, offset, max_pointers: 128)
7
+ labels = []
8
+ visited = {}
9
+ cursor = offset
10
+ consumed = 0
11
+ jumped = false
12
+ loop do
13
+ raise ConversionError, "truncated name at #{cursor}" if cursor >= bytes.bytesize
14
+ raise ConversionError, "compression loop at #{cursor}" if visited[cursor]
15
+
16
+ visited[cursor] = true
17
+ size = bytes.getbyte(cursor)
18
+ if size >= 192
19
+ raise ConversionError, "truncated compression pointer" if cursor + 1 >= bytes.bytesize
20
+
21
+ target = ((size & 63) << 8) | bytes.getbyte(cursor + 1)
22
+ raise ConversionError, "compression pointer must point backwards" if target >= cursor
23
+
24
+ max_pointers -= 1
25
+ raise ConversionError, "name exceeds max_pointers" if max_pointers.negative?
26
+
27
+ consumed += 2 unless jumped
28
+ jumped = true
29
+ cursor = target
30
+ elsif size >= 64
31
+ raise ConversionError, "unsupported label encoding"
32
+ else
33
+ consumed += size + 1 unless jumped
34
+ break if size.zero?
35
+ raise ConversionError, "truncated label" if cursor + size >= bytes.bytesize
36
+
37
+ labels << bytes.byteslice(cursor + 1, size)
38
+ raise ConversionError, "expanded name exceeds 255 bytes" if labels.sum(&:bytesize) + labels.length + 1 > 255
39
+
40
+ cursor += size + 1
41
+ end
42
+ end
43
+ [labels, consumed, jumped]
44
+ end
45
+
46
+ def self.display(labels)
47
+ return "." if labels.empty?
48
+
49
+ result = labels.map do |label|
50
+ label.bytes.map do |byte|
51
+ case byte
52
+ when 46, 92 then "\\#{byte.chr}"
53
+ when 33..126 then byte.chr
54
+ else format("\\%03d", byte)
55
+ end
56
+ end.join
57
+ end.join(".")
58
+ "#{result}."
59
+ end
60
+
61
+ def self.encode(text)
62
+ raise ConversionError, "name must be an ASCII string" unless text.is_a?(String) && text.ascii_only?
63
+ return "\0".b if text == "." || text.empty?
64
+
65
+ labels = []
66
+ label = +"".b
67
+ index = 0
68
+ while index < text.bytesize
69
+ byte = text.getbyte(index)
70
+ if byte == 92
71
+ index += 1
72
+ raise ConversionError, "unfinished name escape" if index == text.bytesize
73
+
74
+ digits = text.byteslice(index, 3)
75
+ if digits.match?(/\A\d{3}\z/)
76
+ raise ConversionError, "name escape exceeds one octet" if digits.to_i > 255
77
+
78
+ label << digits.to_i
79
+ index += 2
80
+ else
81
+ label << text.getbyte(index)
82
+ end
83
+ elsif byte == 46
84
+ raise ConversionError, "empty interior name label" if label.empty?
85
+
86
+ labels << label
87
+ label = +"".b
88
+ else
89
+ label << byte
90
+ end
91
+ index += 1
92
+ end
93
+ labels << label unless label.empty?
94
+ pack(labels)
95
+ end
96
+
97
+ def self.pack(labels)
98
+ raise ConversionError, "label exceeds 63 bytes" if labels.any? { |s| s.bytesize > 63 }
99
+
100
+ result = labels.map { |s| s.bytesize.chr.b + s.b }.join.b + "\0".b
101
+ raise ConversionError, "name exceeds 255 bytes" if result.bytesize > 255
102
+
103
+ result
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DNSJSON
4
+ module Wire
5
+ class Reader
6
+ def initialize(bytes, **limits)
7
+ raise ParseError, "wire input must be a String" unless bytes.is_a?(String)
8
+
9
+ @limits = DNSJSON.validate_limits(limits)
10
+ raise LimitError, "wire input exceeds max_bytes" if bytes.bytesize > @limits[:max_bytes]
11
+
12
+ @bytes = bytes.b
13
+ @offset = 0
14
+ @problems = []
15
+ @data = { "messageOctetsHEX" => DNSJSON.hex(@bytes), "headerOctetsHEX" => DNSJSON.hex(@bytes.byteslice(0, 12)) }
16
+ end
17
+
18
+ def read(date: nil)
19
+ header
20
+ if @bytes.bytesize >= 12
21
+ total = 0
22
+ SECTIONS.each_with_index do |section, index|
23
+ start = @offset
24
+ records = []
25
+ count = @data.fetch(COUNTS[index])
26
+ count.times do
27
+ if total >= @limits[:max_records]
28
+ problem("record count exceeds max_records")
29
+ break
30
+ end
31
+ total += 1
32
+ record = read_record(question: index.zero?)
33
+ break unless record
34
+
35
+ records << record
36
+ end
37
+ @data["#{section}RRs"] = records
38
+ @data["#{section}OctetsHEX"] = DNSJSON.hex(@bytes.byteslice(start, @offset - start))
39
+ next unless index.zero? && !records.empty?
40
+
41
+ %w[NAME NAMEHEX TYPE TYPEname CLASS CLASSname].each { |key| @data["Q#{key}"] = records.first[key] }
42
+ @data["compressedQNAME"] = records.first["compressedNAME"]
43
+ end
44
+ if @offset < @bytes.bytesize
45
+ problem("unclaimed trailing bytes")
46
+ @data["additionalOctetsHEX"] += DNSJSON.hex(@bytes.byteslice(@offset..))
47
+ end
48
+ end
49
+ if date
50
+ raise ArgumentError, "date must be a nonnegative Time" unless date.is_a?(Time) && date.to_f >= 0
51
+
52
+ @data["dateString"] = date.utc.iso8601(6)
53
+ @data["dateSeconds"] = date.to_f
54
+ end
55
+ Message.new(@data, problems: @problems, **@limits)
56
+ end
57
+
58
+ private
59
+
60
+ def problem(reason, offset = @offset) = @problems << Problem.new(offset: offset, reason: reason)
61
+
62
+ def header
63
+ @data["ID"] = @bytes.unpack1("n") if @bytes.bytesize >= 2
64
+ if @bytes.bytesize >= 4
65
+ flags = @bytes.byteslice(2, 2).unpack1("n")
66
+ BITS.each { |key, shift| @data[key] = (flags >> shift) & 1 }
67
+ @data["Opcode"] = (flags >> 11) & 15
68
+ @data["RCODE"] = flags & 15
69
+ end
70
+ COUNTS.each_with_index { |key, i| @data[key] = @bytes.byteslice(4 + (i * 2), 2).unpack1("n") if @bytes.bytesize >= 6 + (i * 2) }
71
+ problem("truncated DNS header", @bytes.bytesize) if @bytes.bytesize < 12
72
+ problem("zero questions in DNS message", 4) if @data["QDCOUNT"]&.zero?
73
+ @offset = [12, @bytes.bytesize].min
74
+ end
75
+
76
+ def read_record(question:)
77
+ start = @offset
78
+ labels, consumed, compressed = Name.read(@bytes, @offset, max_pointers: @limits[:max_pointers])
79
+ @offset += consumed
80
+ record = { "NAME" => Name.display(labels), "NAMEHEX" => DNSJSON.hex(Name.pack(labels)),
81
+ "compressedNAME" => { "isCompressed" => compressed ? 1 : 0, "length" => consumed } }
82
+ required = question ? 4 : 10
83
+ raise ConversionError, "truncated resource record header" if @offset + required > @bytes.bytesize
84
+
85
+ type, klass = @bytes.byteslice(@offset, 4).unpack("nn")
86
+ record.merge!("TYPE" => type, "TYPEname" => Registry.type(type), "CLASS" => klass, "CLASSname" => Registry.klass(klass))
87
+ @offset += 4
88
+ unless question
89
+ ttl, length = @bytes.byteslice(@offset, 6).unpack("l>n")
90
+ @offset += 6
91
+ record.merge!("TTL" => ttl, "RDLENGTH" => length)
92
+ actual = [length, @bytes.bytesize - @offset].min
93
+ record["RDATAHEX"] = DNSJSON.hex(@bytes.byteslice(@offset, actual))
94
+ if actual < length
95
+ problem("RDLENGTH exceeds available RDATA")
96
+ else
97
+ begin
98
+ value = RData.decode(type, @bytes, @offset, length, max_pointers: @limits[:max_pointers])
99
+ record["rdata#{RData::FORMATS[type]}"] = value unless value.nil?
100
+ rescue ConversionError => e
101
+ problem(e.message)
102
+ end
103
+ end
104
+ @offset += actual
105
+ end
106
+ record["rrOctetsHEX"] = DNSJSON.hex(@bytes.byteslice(start, @offset - start))
107
+ record
108
+ rescue ConversionError => e
109
+ problem(e.message, start)
110
+ @offset = @bytes.bytesize
111
+ nil
112
+ end
113
+ end
114
+ end
115
+ end