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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +87 -0
- data/SECURITY.md +9 -0
- data/data/properties.yml +17 -0
- data/data/rdata_formats.yml +105 -0
- data/data/rr_types.yml +99 -0
- data/lib/dns_json/model.rb +143 -0
- data/lib/dns_json/profile.rb +68 -0
- data/lib/dns_json/rdata.rb +312 -0
- data/lib/dns_json/registry.rb +18 -0
- data/lib/dns_json/stream.rb +90 -0
- data/lib/dns_json/version.rb +5 -0
- data/lib/dns_json/wire/name.rb +107 -0
- data/lib/dns_json/wire/reader.rb +115 -0
- data/lib/dns_json/wire/writer.rb +94 -0
- data/lib/dns_json.rb +122 -0
- data/sig/dns_json.rbs +89 -0
- metadata +83 -0
|
@@ -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,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
|