inet 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 441ae2bd4745812fbc1181d5a430b3eb065225670418c41f252e25907efe4117
4
- data.tar.gz: 1831047c063ad8e090b1a184b4bd8dd0e530e1302f711b0ba5abf26627bdf6ee
3
+ metadata.gz: d1b022216eda75fb5d2cf23f4b69b0e225069b7ec53b16400709fd91b7a804b2
4
+ data.tar.gz: c3d2eb967e3cb4a3abef18ba46e99c3fdbb69d1cf8c8f4dab534f3a3e4b5d25a
5
5
  SHA512:
6
- metadata.gz: 2e2f7f9e1f5904cfa0db29842c513e3b7b01a1a987b672b19cea5e91bb6d47e3e15a344c93cde983bc8da2bd447938e0cc674b1823dea27d2cb391ddd734d6e4
7
- data.tar.gz: c0d4867be92ee239c51666d27f442b56ca6071469cb36d1810f525acc28f317817ba29edb42dda798ad2881684c70eaaa22d0ce9a16a07f86473887412ee26a7
6
+ metadata.gz: 54fb104ff7fb73fe2291fe583fa1ad11b14bcff967e4d3b591e3993aed508685958bd6b69932065b0e9ec7607650f7f7f3b76b4e831cfc29473afa781f01af7c
7
+ data.tar.gz: 401fa85bb5d0461dd6646facd4313e539b3a722e7a95aef16054e6e29d05f7306d8e8b13cbbe8a9fce3cbddccb324f70e6643f93e78cdcdf2a73825273bf7f08
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.3.0
4
+
5
+ 1. **域名与 DNS**:从 algosec-rb 迁入 `Inet::DomainName` 和 `Inet::DnsResolver`,提供完整域名校验、左侧通配匹配、A/CNAME 解析与 TTL 缓存。
6
+ 2. **通用上下文**:解析器以 `context:` 隔离调用方缓存,默认使用 `Time.now`,不依赖 ActiveSupport;声明所需标准库 gem 依赖。
7
+
3
8
  ## 0.2.0
4
9
 
5
10
  1. **地址与服务运算**:从 algosec-rb 迁入 IPv4、IPv6 地址规范化、范围解析、CIDR 拆分、端口及协议服务编码,公共入口为 `Inet::IPAddr`、`Inet::IPv6`。
data/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  [![Release](https://github.com/gatework/inet/actions/workflows/release.yml/badge.svg)](https://github.com/gatework/inet/actions/workflows/release.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/inet.svg)](https://rubygems.org/gems/inet)
5
5
 
6
- `inet` 提供 IPv4、IPv6 地址与服务范围运算,以及不可变整数区间集合。加载时不发起网络请求或启动后台任务。
6
+ `inet` 提供 IPv4、IPv6 地址与服务范围运算,域名匹配、带 TTL 缓存的 DNS 解析,以及不可变整数区间集合。加载时不发起网络请求或启动后台任务。
7
7
 
8
- 要求 Ruby 3.4 或更高版本,无运行时依赖,采用 MIT 许可证。
8
+ 要求 Ruby 3.4 或更高版本,只依赖标准库 gem,采用 MIT 许可证。
9
9
 
10
10
  ## 安装与使用
11
11
 
@@ -16,7 +16,7 @@ gem install inet
16
16
  也可以在 Gemfile 中添加:
17
17
 
18
18
  ```ruby
19
- gem "inet", "~> 0.2"
19
+ gem "inet", "~> 0.3"
20
20
  ```
21
21
 
22
22
  ```ruby
@@ -48,6 +48,34 @@ allowed.cardinality # => 5
48
48
 
49
49
  这三个模块迁自 Gatework 的 algosec-rb,保留其公共方法与运算行为;最低 Ruby 版本自带的 `Range#overlap?` 替代原 ActiveSupport 加载。源代码采用 MIT 许可证,版权声明见 [LICENSE](LICENSE)。
50
50
 
51
+ ## 域名与 DNS
52
+
53
+ ```ruby
54
+ Inet::DomainName.normalize("WWW.Example.test.") # => "www.example.test"
55
+ Inet::DomainName.exact?("example.test") # => true
56
+ Inet::DomainName.matches?("*.example.test", "www.example.test") # => true
57
+
58
+ resolver = Inet::DnsResolver.new(context: { "device_id" => 42 }, servers: ["192.0.2.53"])
59
+ answer = resolver.resolve("www.example.test") # 此时才查询 DNS
60
+ answer.fetch("status") # => "resolved"、"unresolved" 或 "unsupported"
61
+ answer.fetch("addresses") # => IPv4 地址数组
62
+ ```
63
+
64
+ 域名只接受至少两个 ASCII 标签的完整名称,单个尾点会被移除;不支持 IDNA 转码和搜索域。
65
+ 通配仅支持 `*.example.test`,匹配一层或多层子域,不匹配根名称 `example.test`。
66
+ `require "inet/domain_name"` 和 `require "inet/dns_resolver"` 也可独立加载。
67
+
68
+ 解析器只查询 A 记录,跟随最多 8 个 CNAME,使用整条链中最短的 TTL;每次等待为 1 秒。
69
+ `servers:` 最多指定三个 DNS 服务器,省略时使用系统 DNS。`context:` 为字符串键的 JSON 对象,
70
+ 由解析器复制并冻结,供调用方隔离租户或设备缓存。返回的 `source` 保留上下文,追加
71
+ `source`(系统 DNS 为 `system`,显式 DNS 为 `device`)和 `servers`,这两个键由解析器管理。
72
+
73
+ 返回值及嵌套内容不可变,包含 `status`、`addresses`、`resolved_at`、`expires_at`、`source`、`error`。
74
+ 失败或没有 A 记录返回 `unresolved`,不支持的域名返回 `unsupported`,都缓存 15 秒;不会把失败当成成功的空集合。
75
+ 可选 `store:` 接收实现 `read(key)` 与 `write(key, value, expires_in:)` 的缓存,实例内也按 TTL 复用结果。
76
+ `clock:` 默认 `Time.now`;测试可注入 `transport:`(接收域名及显式服务器数组,返回 `[地址数组, TTL秒数]`)
77
+ 或 `dns_class:`。加载 gem 不发起 DNS 查询。
78
+
51
79
  ## 开发
52
80
 
53
81
  ```sh
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+ require "digest"
5
+ require "json"
6
+ require "time"
7
+ require_relative "ipaddr"
8
+ require_relative "domain_name"
9
+
10
+ module Inet
11
+ # 按调用方上下文和 DNS 视角缓存有期限的证据;不把 DNS 失败解释为空地址。
12
+ class DnsResolver
13
+ FAILURE_TTL = 15
14
+ MAX_CNAME_DEPTH = 8
15
+ attr_reader :context
16
+
17
+ def initialize(context: {}, servers: nil, store: nil, clock: -> { Time.now }, transport: nil, dns_class: Resolv::DNS)
18
+ servers = Array(servers)
19
+ raise ArgumentError, "DNS servers must contain at most three IP addresses" if servers.size > 3
20
+
21
+ servers.each { |server| ::IPAddr.new(server) }
22
+ @servers = servers.map { |server| server.to_s.dup.freeze }.freeze
23
+ @store = store
24
+ @clock = clock
25
+ @transport = transport
26
+ @dns_class = dns_class
27
+ effective = if @servers.empty?
28
+ Resolv::DNS::Config.default_config_hash.fetch(:nameserver).map do |server|
29
+ server.dup.freeze
30
+ end.freeze
31
+ else
32
+ @servers
33
+ end
34
+ @context = immutable_answer(context.merge("source" => @servers.empty? ? "system" : "device",
35
+ "servers" => effective))
36
+ @scope = Digest::SHA256.hexdigest(JSON.generate(@context))
37
+ @answers = {}
38
+ end
39
+
40
+ # 同次解析只查一次;跨次操作复用 TTL 内缓存,失败缓存不被视作成功。
41
+ def resolve(domain)
42
+ name = DomainName.normalize(domain)
43
+ key = ["inet", "dns-v1", @scope, name]
44
+ now = @clock.call
45
+ cached = @answers[name] || @store&.read(key)
46
+ return @answers[name] ||= immutable_answer(cached) if cached && Time.iso8601(cached.fetch("expires_at")) > now
47
+
48
+ answer = lookup(name, now)
49
+ @store&.write(key, answer, expires_in: [Time.iso8601(answer.fetch("expires_at")) - now, 0].max)
50
+ @answers[name] = answer
51
+ end
52
+
53
+ private
54
+
55
+ # 缓存编码器不保留冻结状态;复制恢复每层 JSON 值,避免调用方改写 DNS 证据。
56
+ def immutable_answer(value)
57
+ case value
58
+ when Hash then value.to_h { |key, item| [key, immutable_answer(item)] }.freeze
59
+ when Array then value.map { |item| immutable_answer(item) }.freeze
60
+ else value.dup.freeze
61
+ end
62
+ end
63
+
64
+ # 仅接受完整域名;通配和单标签主机名不借助本机搜索域猜测。
65
+ def lookup(name, now)
66
+ return record("unsupported", [], FAILURE_TTL, now, "Unsupported domain expression") unless DomainName.exact?(name)
67
+
68
+ addresses, ttl = @transport ? @transport.call(name, @servers) : query(name)
69
+ addresses = addresses.map { |ip| Inet::IPAddr.from_i(Inet::IPAddr.to_i(ip)) }.uniq.sort
70
+ return record("unresolved", [], FAILURE_TTL, now, "No A records") if addresses.empty?
71
+
72
+ record("resolved", addresses, Integer(ttl), now)
73
+ rescue Resolv::ResolvError, Resolv::ResolvTimeout, IOError, SystemCallError, ArgumentError => error
74
+ record("unresolved", [], FAILURE_TTL, now, error.message)
75
+ end
76
+
77
+ # CNAME 与 A 的 TTL 共同决定有效期,限制跳数和每次等待,防止循环和长时间阻塞。
78
+ def query(name)
79
+ servers = context.fetch("servers")
80
+ @dns_class.open(nameserver: servers, search: [], ndots: 1, raise_timeout_errors: true) do |dns|
81
+ dns.timeouts = 1
82
+ visited = []
83
+ ttls = []
84
+ MAX_CNAME_DEPTH.times do
85
+ raise Resolv::ResolvError, "CNAME cycle" if visited.include?(name)
86
+
87
+ visited << name
88
+ aliases = dns.getresources("#{name}.", Resolv::DNS::Resource::IN::CNAME)
89
+ if aliases.empty?
90
+ records = dns.getresources("#{name}.", Resolv::DNS::Resource::IN::A)
91
+ ttls.concat(records.map(&:ttl))
92
+ return [records.map { |rr| rr.address.to_s }, ttls.min || 0]
93
+ end
94
+ ttls.concat(aliases.map(&:ttl))
95
+ name = aliases.first.name.to_s.downcase.delete_suffix(".")
96
+ end
97
+ raise Resolv::ResolvError, "CNAME chain exceeds #{MAX_CNAME_DEPTH}"
98
+ end
99
+ end
100
+
101
+ # 使用普通 JSON 值保存证据,配置缓存不持有网络连接或解析器对象。
102
+ def record(status, addresses, ttl, now, error = nil)
103
+ { "status" => status, "addresses" => addresses.map(&:freeze).freeze, "resolved_at" => now.iso8601(6).freeze,
104
+ "expires_at" => (now + [ttl, 0].max).iso8601(6).freeze, "source" => context,
105
+ "error" => error&.dup&.freeze }.freeze
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inet
4
+ # 域名表达式只支持完整名称或最左侧标签通配,不使用子串匹配。
5
+ module DomainName
6
+ module_function
7
+
8
+ def normalize(value)
9
+ name = value.to_s.downcase
10
+ # 一个尾点表示根域;多个尾点是空标签,跨层重复归一化也不能把它变成合法名称。
11
+ name.end_with?("..") ? name : name.delete_suffix(".")
12
+ end
13
+
14
+ def exact?(value)
15
+ name = normalize(value)
16
+ name.bytesize <= 253 && name.include?(".") &&
17
+ name.split(".", -1).all? { |label| /\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/.match?(label) }
18
+ end
19
+
20
+ def wildcard?(value)
21
+ name = normalize(value)
22
+ name.start_with?("*.") && exact?(name.delete_prefix("*."))
23
+ end
24
+
25
+ def matches?(expression, domain)
26
+ pattern, name = [expression, domain].map { |value| normalize(value) }
27
+ return false unless exact?(name)
28
+
29
+ wildcard?(pattern) ? name.end_with?(pattern.delete_prefix("*")) : exact?(pattern) && pattern == name
30
+ end
31
+ end
32
+ end
data/lib/inet/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Inet
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/inet.rb CHANGED
@@ -5,6 +5,9 @@ require_relative "inet/set"
5
5
  require_relative "inet/ipaddr"
6
6
  require_relative "inet/ipv6"
7
7
 
8
- # IP 地址、服务编码与不可变整数区间集合的公共命名空间。
8
+ require_relative "inet/domain_name"
9
+ require_relative "inet/dns_resolver"
10
+
11
+ # IP 地址、域名、DNS 解析、服务编码与不可变整数区间集合的公共命名空间。
9
12
  module Inet
10
13
  end
metadata CHANGED
@@ -1,16 +1,86 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - inet contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-23 00:00:00.000000000 Z
11
- dependencies: []
12
- description: IP address normalization, address ranges, service encoding and immutable
13
- integer interval sets.
10
+ date: 2026-10-24 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: digest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ipaddr
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: json
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: resolv
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: time
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: Domain matching, cached DNS, IP address ranges, service encoding and
83
+ immutable interval sets.
14
84
  executables: []
15
85
  extensions: []
16
86
  extra_rdoc_files: []
@@ -20,6 +90,8 @@ files:
20
90
  - README.md
21
91
  - docs/releasing.md
22
92
  - lib/inet.rb
93
+ - lib/inet/dns_resolver.rb
94
+ - lib/inet/domain_name.rb
23
95
  - lib/inet/ipaddr.rb
24
96
  - lib/inet/ipv6.rb
25
97
  - lib/inet/set.rb
@@ -49,5 +121,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
121
  requirements: []
50
122
  rubygems_version: 4.0.20
51
123
  specification_version: 4
52
- summary: IPv4, IPv6 and immutable interval sets
124
+ summary: IP addresses, domain names, DNS and immutable interval sets
53
125
  test_files: []