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,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DNSJSON
|
|
4
|
+
module Wire
|
|
5
|
+
class Writer
|
|
6
|
+
def initialize(data, **limits)
|
|
7
|
+
@data = data
|
|
8
|
+
@limits = DNSJSON.validate_limits(limits)
|
|
9
|
+
@record_count = 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def write
|
|
13
|
+
Message.new(@data, **@limits)
|
|
14
|
+
return bounded(DNSJSON.unhex(@data["messageOctetsHEX"])) if @data.key?("messageOctetsHEX")
|
|
15
|
+
|
|
16
|
+
sections = SECTIONS.map { |section| expand(@data.fetch("#{section}RRs", [])) }
|
|
17
|
+
if sections[0].empty? && (@data.key?("QNAME") || @data.key?("QNAMEHEX"))
|
|
18
|
+
sections[0] = [@data.each_with_object({}) { |(k, v), h| h[k.delete_prefix("Q")] = v if k.start_with?("Q") && k != "QDCOUNT" }]
|
|
19
|
+
end
|
|
20
|
+
raise LimitError, "record count exceeds max_records" if sections.sum(&:length) > @limits[:max_records]
|
|
21
|
+
|
|
22
|
+
id = required(@data, "ID")
|
|
23
|
+
flags = BITS.sum { |key, shift| [true, 1].include?(@data.fetch(key, 0)) ? 1 << shift : 0 }
|
|
24
|
+
flags |= @data.fetch("Opcode", 0) << 11
|
|
25
|
+
flags |= @data.fetch("RCODE", 0)
|
|
26
|
+
counts = COUNTS.each_with_index.map { |key, i| @data.fetch(key, sections[i].length) }
|
|
27
|
+
result = [id, flags, *counts].pack("n6")
|
|
28
|
+
sections.each_with_index do |records, index|
|
|
29
|
+
records.each do |record|
|
|
30
|
+
result << encode_record(record, question: index.zero?)
|
|
31
|
+
bounded(result)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
bounded(result)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def bounded(bytes)
|
|
40
|
+
raise LimitError, "wire output exceeds max_bytes" if bytes.bytesize > @limits[:max_bytes]
|
|
41
|
+
|
|
42
|
+
bytes
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def required(data, key)
|
|
46
|
+
raise ConversionError, "missing #{key}" unless data.key?(key)
|
|
47
|
+
|
|
48
|
+
data[key]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def expand(records)
|
|
52
|
+
records.flat_map do |record|
|
|
53
|
+
count = record.key?("rrSet") ? record["rrSet"].length : 1
|
|
54
|
+
@record_count += count
|
|
55
|
+
raise LimitError, "record count exceeds max_records" if @record_count > @limits[:max_records]
|
|
56
|
+
|
|
57
|
+
record.key?("rrSet") ? record["rrSet"].map { |entry| record.except("rrSet").merge(entry) } : [record]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def code(record, key, kind)
|
|
62
|
+
record.key?(key) ? record[key] : Registry.number(required(record, "#{key}name"), kind)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def encode_record(record, question:)
|
|
66
|
+
name = if record.key?("NAMEHEX")
|
|
67
|
+
bytes = DNSJSON.unhex(record["NAMEHEX"])
|
|
68
|
+
labels, size = Name.read(bytes, 0, max_pointers: 0)
|
|
69
|
+
raise ConversionError, "excess NAMEHEX data" unless size == bytes.bytesize
|
|
70
|
+
|
|
71
|
+
Name.pack(labels)
|
|
72
|
+
else
|
|
73
|
+
Name.encode(required(record, "NAME"))
|
|
74
|
+
end
|
|
75
|
+
type = code(record, "TYPE", :type)
|
|
76
|
+
klass = code(record, "CLASS", :class)
|
|
77
|
+
result = name + [type, klass].pack("nn")
|
|
78
|
+
return result if question
|
|
79
|
+
|
|
80
|
+
ttl = required(record, "TTL")
|
|
81
|
+
# Prefer structured RDATA on reconstruction: captured RDATAHEX can contain obsolete compression offsets.
|
|
82
|
+
member = "rdata#{RData::FORMATS[type]}"
|
|
83
|
+
rdata = if record.key?(member)
|
|
84
|
+
RData.encode(type, record[member])
|
|
85
|
+
else
|
|
86
|
+
DNSJSON.unhex(required(record, "RDATAHEX"))
|
|
87
|
+
end
|
|
88
|
+
raise ConversionError, "RDATA exceeds 65535 bytes" if rdata.bytesize > 65_535
|
|
89
|
+
|
|
90
|
+
result + [ttl, record.key?(member) ? rdata.bytesize : record.fetch("RDLENGTH", rdata.bytesize)].pack("l>n") + rdata
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/dns_json.rb
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "time"
|
|
6
|
+
require "date"
|
|
7
|
+
require "ipaddr"
|
|
8
|
+
|
|
9
|
+
require_relative "dns_json/version"
|
|
10
|
+
|
|
11
|
+
module DNSJSON
|
|
12
|
+
VERSION = DnsJson::VERSION
|
|
13
|
+
MEDIA_TYPE = "application/dns+json"
|
|
14
|
+
LIMITS = { max_bytes: 65_535, max_records: 4096, max_pointers: 128, max_json_bytes: 4_194_304, max_depth: 32 }.freeze
|
|
15
|
+
BITS = { "QR" => 15, "AA" => 10, "TC" => 9, "RD" => 8, "RA" => 7, "AD" => 5, "CD" => 4 }.freeze
|
|
16
|
+
SECTIONS = %w[question answer authority additional].freeze
|
|
17
|
+
COUNTS = %w[QDCOUNT ANCOUNT NSCOUNT ARCOUNT].freeze
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
class ParseError < Error; end
|
|
20
|
+
class ConversionError < Error; end
|
|
21
|
+
class LimitError < ParseError; end
|
|
22
|
+
|
|
23
|
+
class ValidationError < Error
|
|
24
|
+
attr_reader :pointer
|
|
25
|
+
|
|
26
|
+
def initialize(message, pointer: "")
|
|
27
|
+
@pointer = pointer
|
|
28
|
+
super("#{pointer.empty? ? "/" : pointer}: #{message}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
Problem = Data.define(:offset, :reason)
|
|
32
|
+
class DecimalToken
|
|
33
|
+
attr_reader :source
|
|
34
|
+
|
|
35
|
+
def initialize(source)
|
|
36
|
+
@source = source
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.validate_limits(options)
|
|
41
|
+
raise ArgumentError, "unknown limit" unless (options.keys - LIMITS.keys).empty?
|
|
42
|
+
|
|
43
|
+
limits = LIMITS.merge(options)
|
|
44
|
+
limits.each do |name, value|
|
|
45
|
+
minimum = name == :max_depth ? 1 : 0
|
|
46
|
+
raise ArgumentError, "#{name} must be an Integer >= #{minimum}" unless value.is_a?(Integer) && value >= minimum
|
|
47
|
+
end
|
|
48
|
+
limits
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.normalize_decimals(value, errors, pointer = "", member = nil, known: true)
|
|
52
|
+
case value
|
|
53
|
+
when Hash
|
|
54
|
+
value.to_h do |key, item|
|
|
55
|
+
child_known = known && (%w[queryMessage responseMessage compressedNAME compressedQNAME rrSet].include?(key) || SECTIONS.any? { |s| key == "#{s}RRs" })
|
|
56
|
+
path = "#{pointer}/#{key.gsub("~", "~0").gsub("/", "~1")}"
|
|
57
|
+
[key, normalize_decimals(item, errors, path, known ? key : nil, known: child_known)]
|
|
58
|
+
end
|
|
59
|
+
when Array
|
|
60
|
+
value.each_with_index.map { |item, index| normalize_decimals(item, errors, "#{pointer}/#{index}", known: known) }
|
|
61
|
+
when DecimalToken
|
|
62
|
+
errors << ValidationError.new("dateSeconds forbids signs and exponents", pointer: pointer) if member == "dateSeconds" && value.source.match?(/[eE-]/)
|
|
63
|
+
Float(value.source)
|
|
64
|
+
else value
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.hex(bytes) = bytes.unpack1("H*").upcase
|
|
69
|
+
|
|
70
|
+
def self.unhex(text)
|
|
71
|
+
raise ConversionError, "expected uppercase even-length hexadecimal" unless text.is_a?(String) && text.match?(/\A(?:[0-9A-F]{2})*\z/)
|
|
72
|
+
|
|
73
|
+
[text].pack("H*")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.copy(value, depth = 0)
|
|
77
|
+
raise LimitError, "object copy exceeds safe depth" if depth > 128
|
|
78
|
+
|
|
79
|
+
case value
|
|
80
|
+
when Hash then value.to_h { |k, v| [k.dup, copy(v, depth + 1)] }
|
|
81
|
+
when Array then value.map { |v| copy(v, depth + 1) }
|
|
82
|
+
when String then value.dup
|
|
83
|
+
else value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.parse(source, strict: true, profile: nil, **limits)
|
|
88
|
+
raise ParseError, "JSON source must be a String" unless source.is_a?(String)
|
|
89
|
+
|
|
90
|
+
limits = validate_limits(limits)
|
|
91
|
+
raise LimitError, "JSON exceeds max_json_bytes" if source.bytesize > limits[:max_json_bytes]
|
|
92
|
+
raise ParseError, "JSON source must contain only ASCII characters" unless source.ascii_only?
|
|
93
|
+
|
|
94
|
+
data = JSON.parse(source, allow_duplicate_key: false, decimal_class: DecimalToken, max_nesting: limits[:max_depth])
|
|
95
|
+
lexical_errors = []
|
|
96
|
+
data = normalize_decimals(data, lexical_errors)
|
|
97
|
+
raise ParseError, "expected a JSON object" unless data.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
type = data.key?("queryMessage") || data.key?("responseMessage") ? Pair : Message
|
|
100
|
+
raise lexical_errors.first if strict && !lexical_errors.empty?
|
|
101
|
+
|
|
102
|
+
result = type.new(data, strict: strict, profile: profile, **limits)
|
|
103
|
+
result.warnings.concat(lexical_errors)
|
|
104
|
+
result
|
|
105
|
+
rescue JSON::ParserError, EncodingError => e
|
|
106
|
+
raise ParseError, e.message
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.from_wire(bytes, date: nil, **limits)
|
|
110
|
+
limits = validate_limits(limits)
|
|
111
|
+
Wire::Reader.new(bytes, **limits).read(date: date)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
require_relative "dns_json/registry"
|
|
116
|
+
require_relative "dns_json/wire/name"
|
|
117
|
+
require_relative "dns_json/rdata"
|
|
118
|
+
require_relative "dns_json/model"
|
|
119
|
+
require_relative "dns_json/profile"
|
|
120
|
+
require_relative "dns_json/wire/reader"
|
|
121
|
+
require_relative "dns_json/wire/writer"
|
|
122
|
+
require_relative "dns_json/stream"
|
data/sig/dns_json.rbs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module DnsJson
|
|
2
|
+
VERSION: String
|
|
3
|
+
end
|
|
4
|
+
module DNSJSON
|
|
5
|
+
VERSION: String
|
|
6
|
+
MEDIA_TYPE: String
|
|
7
|
+
type json = nil | bool | Integer | Float | String | Array[json] | Hash[String, json]
|
|
8
|
+
type object = Hash[String, json]
|
|
9
|
+
class Error < StandardError
|
|
10
|
+
end
|
|
11
|
+
class ParseError < Error
|
|
12
|
+
end
|
|
13
|
+
class ConversionError < Error
|
|
14
|
+
end
|
|
15
|
+
class LimitError < ParseError
|
|
16
|
+
end
|
|
17
|
+
class ValidationError < Error
|
|
18
|
+
attr_reader pointer: String
|
|
19
|
+
def initialize: (String, ?pointer: String) -> void
|
|
20
|
+
end
|
|
21
|
+
class Problem < Data
|
|
22
|
+
attr_reader offset: Integer
|
|
23
|
+
attr_reader reason: String
|
|
24
|
+
end
|
|
25
|
+
def self.parse: (String, ?strict: bool, ?profile: Profile?, **Integer) -> Message
|
|
26
|
+
def self.from_wire: (String, ?date: Time?, **Integer) -> Message
|
|
27
|
+
class Message
|
|
28
|
+
attr_reader problems: Array[Problem]
|
|
29
|
+
attr_reader warnings: Array[ValidationError]
|
|
30
|
+
def initialize: (?object, ?strict: bool, ?profile: Profile?, ?problems: Array[Problem], **Integer) -> void
|
|
31
|
+
def []: (String | Symbol) -> json
|
|
32
|
+
def to_h: () -> object
|
|
33
|
+
def to_json: (*untyped, ?profile: Profile) -> String
|
|
34
|
+
def to_wire: () -> String
|
|
35
|
+
def malformed?: () -> bool
|
|
36
|
+
def validate: () -> Array[ValidationError]
|
|
37
|
+
def id: () -> Integer?
|
|
38
|
+
def qname: () -> String?
|
|
39
|
+
def qtype: () -> Integer?
|
|
40
|
+
def qclass: () -> Integer?
|
|
41
|
+
def name: () -> String?
|
|
42
|
+
def type: () -> Integer?
|
|
43
|
+
def klass: () -> Integer?
|
|
44
|
+
def ttl: () -> Integer?
|
|
45
|
+
def rdlength: () -> Integer?
|
|
46
|
+
def opcode: () -> Integer?
|
|
47
|
+
def rcode: () -> Integer?
|
|
48
|
+
def qdcount: () -> Integer?
|
|
49
|
+
def ancount: () -> Integer?
|
|
50
|
+
def nscount: () -> Integer?
|
|
51
|
+
def arcount: () -> Integer?
|
|
52
|
+
def length: () -> Integer?
|
|
53
|
+
end
|
|
54
|
+
class RR < Message
|
|
55
|
+
end
|
|
56
|
+
class Pair < Message
|
|
57
|
+
def initialize: (?object, ?query: Message?, ?response: Message?, **untyped) -> void
|
|
58
|
+
def query: () -> Message?
|
|
59
|
+
def response: () -> Message?
|
|
60
|
+
def to_wire: () -> bot
|
|
61
|
+
end
|
|
62
|
+
class Profile
|
|
63
|
+
FULL: Profile
|
|
64
|
+
LOGGING: Profile
|
|
65
|
+
PRIVACY: Profile
|
|
66
|
+
def initialize: (?include: Array[Symbol], ?required: Array[String | Symbol], ?prohibited: Array[String | Symbol], ?one_bit_as: Symbol, ?privacy: bool) -> void
|
|
67
|
+
def apply: (object) -> object
|
|
68
|
+
def validate: (object) -> Array[ValidationError]
|
|
69
|
+
end
|
|
70
|
+
module Stream
|
|
71
|
+
class Writer
|
|
72
|
+
def initialize: (untyped, ?profile: Profile) -> void
|
|
73
|
+
def <<: (Message) -> Writer
|
|
74
|
+
end
|
|
75
|
+
class Reader
|
|
76
|
+
include Enumerable[Message]
|
|
77
|
+
attr_reader problems: Array[Problem]
|
|
78
|
+
attr_reader dropped_problems: Integer
|
|
79
|
+
def initialize: (untyped, ?strict: bool, ?max_json_bytes: Integer, ?max_problems: Integer, **untyped) -> void
|
|
80
|
+
def each: () { (Message) -> void } -> Reader
|
|
81
|
+
| () -> Enumerator[Message, Reader]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
module Registry
|
|
85
|
+
def self.type: (Integer) -> String
|
|
86
|
+
def self.klass: (Integer) -> String
|
|
87
|
+
def self.number: (String, ?Symbol) -> Integer
|
|
88
|
+
end
|
|
89
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dns_json
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 2.19.4
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 2.19.4
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4'
|
|
32
|
+
description: Bounded tolerant DNS wire parsing, RFC 8427 JSON conversion, RDATA codecs,
|
|
33
|
+
profiles, and JSON text sequences.
|
|
34
|
+
email:
|
|
35
|
+
- t.yudai92@gmail.com
|
|
36
|
+
executables: []
|
|
37
|
+
extensions: []
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
files:
|
|
40
|
+
- CHANGELOG.md
|
|
41
|
+
- LICENSE.txt
|
|
42
|
+
- README.md
|
|
43
|
+
- SECURITY.md
|
|
44
|
+
- data/properties.yml
|
|
45
|
+
- data/rdata_formats.yml
|
|
46
|
+
- data/rr_types.yml
|
|
47
|
+
- lib/dns_json.rb
|
|
48
|
+
- lib/dns_json/model.rb
|
|
49
|
+
- lib/dns_json/profile.rb
|
|
50
|
+
- lib/dns_json/rdata.rb
|
|
51
|
+
- lib/dns_json/registry.rb
|
|
52
|
+
- lib/dns_json/stream.rb
|
|
53
|
+
- lib/dns_json/version.rb
|
|
54
|
+
- lib/dns_json/wire/name.rb
|
|
55
|
+
- lib/dns_json/wire/reader.rb
|
|
56
|
+
- lib/dns_json/wire/writer.rb
|
|
57
|
+
- sig/dns_json.rbs
|
|
58
|
+
homepage: https://github.com/ydah/dns_json
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata:
|
|
62
|
+
allowed_push_host: https://rubygems.org
|
|
63
|
+
source_code_uri: https://github.com/ydah/dns_json
|
|
64
|
+
changelog_uri: https://github.com/ydah/dns_json/blob/main/CHANGELOG.md
|
|
65
|
+
rubygems_mfa_required: 'true'
|
|
66
|
+
rdoc_options: []
|
|
67
|
+
require_paths:
|
|
68
|
+
- lib
|
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 3.3.0
|
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubygems_version: 4.0.16
|
|
81
|
+
specification_version: 4
|
|
82
|
+
summary: Read and write DNS messages as RFC 8427 JSON
|
|
83
|
+
test_files: []
|