whois-parser-next 0.3.1 → 0.4.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -6
  3. data/README.md +20 -15
  4. data/RELEASING.md +2 -2
  5. data/lib/whois/parser/version.rb +1 -1
  6. data/lib/whois/parsers/registry_response_safety.rb +7 -5
  7. data/lib/whois/parsers/whois.auda.org.au.rb +43 -1
  8. data/lib/whois/parsers/whois.cctld.uz.rb +6 -0
  9. data/lib/whois/parsers/whois.eu.rb +33 -7
  10. data/lib/whois/parsers/whois.id.rb +22 -2
  11. data/lib/whois/parsers/whois.nic.africa.rb +23 -0
  12. data/lib/whois/parsers/whois.nic.biz.rb +38 -0
  13. data/lib/whois/parsers/whois.nic.cl.rb +32 -8
  14. data/lib/whois/parsers/whois.nic.foundation.rb +4 -0
  15. data/lib/whois/parsers/whois.nic.ge.rb +6 -0
  16. data/lib/whois/parsers/whois.nic.gifts.rb +13 -0
  17. data/lib/whois/parsers/whois.nic.gives.rb +46 -0
  18. data/lib/whois/parsers/whois.nic.giving.rb +44 -0
  19. data/lib/whois/parsers/whois.nic.help.rb +12 -0
  20. data/lib/whois/parsers/whois.nic.homes.rb +12 -0
  21. data/lib/whois/parsers/whois.nic.icu.rb +11 -1
  22. data/lib/whois/parsers/whois.nic.lat.rb +15 -1
  23. data/lib/whois/parsers/whois.nic.love.rb +19 -0
  24. data/lib/whois/parsers/whois.nic.mn.rb +51 -0
  25. data/lib/whois/parsers/whois.nic.mx.rb +47 -7
  26. data/lib/whois/parsers/whois.nic.net.sa.rb +6 -0
  27. data/lib/whois/parsers/whois.nic.news.rb +2 -0
  28. data/lib/whois/parsers/whois.nic.one.rb +7 -1
  29. data/lib/whois/parsers/whois.nic.online.rb +25 -1
  30. data/lib/whois/parsers/whois.nic.open.rb +95 -0
  31. data/lib/whois/parsers/whois.nic.quest.rb +17 -0
  32. data/lib/whois/parsers/whois.nic.sbs.rb +28 -0
  33. data/lib/whois/parsers/whois.nic.store.rb +28 -0
  34. data/lib/whois/parsers/whois.nic.top.rb +25 -3
  35. data/lib/whois/parsers/whois.nic.tv.rb +21 -0
  36. data/lib/whois/parsers/whois.nic.uk.rb +84 -23
  37. data/lib/whois/parsers/whois.nic.vip.rb +21 -0
  38. data/lib/whois/parsers/whois.nic.wiki.rb +21 -0
  39. data/lib/whois/parsers/whois.nic.win.rb +27 -0
  40. data/lib/whois/parsers/whois.publicinterestregistry.org.rb +52 -0
  41. data/lib/whois/parsers/whois.punktum.dk.rb +53 -0
  42. data/lib/whois/parsers/whois.registry.click.rb +9 -0
  43. data/lib/whois/parsers/whois.registry.gift.rb +10 -0
  44. data/lib/whois/parsers/whois.sr.rb +44 -0
  45. data/lib/whois/parsers/whois.tcinet.ru.rb +83 -8
  46. data/lib/whois/parsers/whois.tld.mu.rb +20 -0
  47. data/lib/whois/parsers/whois.tonicregistry.to.rb +55 -1
  48. data/lib/whois/parsers/whois.uniregistry.net.rb +15 -2
  49. data/lib/whois/parsers/whois.weare.ie.rb +12 -0
  50. metadata +16 -1
@@ -8,6 +8,7 @@
8
8
 
9
9
 
10
10
  require_relative 'base'
11
+ require_relative 'registry_response_safety'
11
12
 
12
13
 
13
14
  module Whois
@@ -22,6 +23,22 @@ module Whois
22
23
  # @see http://www.nominet.org.uk/other/whois/detailedinstruct/
23
24
  #
24
25
  class WhoisNicUk < Base
26
+ include RegistryResponseSafety
27
+
28
+ DOMAIN_NAME_LINE = /^[ \t]*Domain name:[ \t]*(.*?)[ \t]*\r?$/i
29
+ NO_MATCH_LINE = /^[ \t]*No match for "([^"]+)"\.[ \t]*\r?$/i
30
+ NOT_REGISTERED_LINE = /^[ \t]*This domain name has not been registered\.[ \t]*\r?$/i
31
+ REGISTRATION_STATUS_LINE = /\A[ \t]*Registration status:[ \t]*(.*?)[ \t]*\r?\n?\z/i
32
+ REGISTRATION_SUPPORT_LINE = /^[ \t]*(?:Registered on|Expiry date|Last updated|Registrar):/i
33
+ REGISTRATION_EVIDENCE_LINE = /^[ \t]*(?:Domain name|Registration status|Registered on|Expiry date|Last updated|Registrar|Name servers):/i
34
+ REGISTRATION_STATUSES = {
35
+ 'registered until expiry date.' => :registered,
36
+ 'registration request being processed.' => :registered,
37
+ 'renewal request being processed.' => :registered,
38
+ 'no longer required' => :registered,
39
+ 'renewal required.' => :registered,
40
+ 'no registration status listed.' => :reserved,
41
+ }.freeze
25
42
 
26
43
  # == Values for Status
27
44
  #
@@ -29,37 +46,37 @@ module Whois
29
46
  # @see http://www.nominet.org.uk/registrants/maintain/renew/status/
30
47
  #
31
48
  property_supported :status do
32
- if content_for_scanner =~ /\s+Registration status:\s+(.+?)\n/
33
- case ::Regexp.last_match(1).downcase
34
- when "registered until expiry date."
35
- :registered
36
- when "registration request being processed."
37
- :registered
38
- when "renewal request being processed."
39
- :registered
40
- when "no longer required"
41
- :registered
42
- when "no registration status listed."
43
- :reserved
44
- # NEWSTATUS redemption (https://github.com/weppos/whois/issues/5)
45
- when "renewal required."
46
- :registered
47
- else
48
- Whois::Parser.bug!(ParserError, "Unknown status `#{::Regexp.last_match(1)}'.")
49
- end
50
- elsif invalid?
49
+ body = content_for_scanner
50
+ absence_markers = body.lines.count do |line|
51
+ line.match?(NO_MATCH_LINE) || line.match?(NOT_REGISTERED_LINE)
52
+ end
53
+
54
+ if absence_markers.positive?
55
+ authoritative_absence?(body) ? :available : :unknown
56
+ elsif invalid? && !registration_evidence?(body)
51
57
  :invalid
52
58
  else
53
- :available
59
+ domains = domain_name_values(body)
60
+ statuses = registration_status_values(body)
61
+ if domains.length != 1 || !domains.first.downcase.end_with?('.uk') || statuses.length != 1
62
+ :unknown
63
+ else
64
+ status = REGISTRATION_STATUSES[statuses.first.downcase]
65
+ if status == :registered && !body.match?(REGISTRATION_SUPPORT_LINE)
66
+ :unknown
67
+ else
68
+ status || :unknown
69
+ end
70
+ end
54
71
  end
55
72
  end
56
73
 
57
74
  property_supported :available? do
58
- !!(content_for_scanner =~ /This domain name has not been registered/)
75
+ status == :available
59
76
  end
60
77
 
61
78
  property_supported :registered? do
62
- !available?
79
+ status == :registered
63
80
  end
64
81
 
65
82
 
@@ -149,7 +166,51 @@ module Whois
149
166
  # and will be replenished in 50 seconds.
150
167
  #
151
168
  def response_throttled?
152
- !!(content_for_scanner =~ /The WHOIS query quota for .+ has been exceeded/)
169
+ content_for_scanner.match?(/^[ \t]*The WHOIS query quota for .+ has been exceeded\b/i) ||
170
+ RegistryResponseSafety.instance_method(:response_throttled?).bind_call(self)
171
+ end
172
+
173
+ def registration_status_values(body)
174
+ lines = body.lines
175
+ lines.each_with_index.filter_map do |line, index|
176
+ match = REGISTRATION_STATUS_LINE.match(line)
177
+ next unless match
178
+
179
+ value = match[1].strip
180
+ if value.empty?
181
+ next_index = index + 1
182
+ next_index += 1 while next_index < lines.length && lines[next_index].strip.empty?
183
+ value = lines[next_index].to_s.strip
184
+ end
185
+ value unless value.empty?
186
+ end
187
+ end
188
+
189
+ def domain_name_values(body)
190
+ lines = body.lines
191
+ lines.each_with_index.filter_map do |line, index|
192
+ match = DOMAIN_NAME_LINE.match(line)
193
+ next unless match
194
+
195
+ value = match[1].strip
196
+ if value.empty?
197
+ next_index = index + 1
198
+ next_index += 1 while next_index < lines.length && lines[next_index].strip.empty?
199
+ value = lines[next_index].to_s.strip.split(/\s+/, 2).first.to_s
200
+ end
201
+ value unless value.empty?
202
+ end
203
+ end
204
+
205
+ def authoritative_absence?(body)
206
+ no_match = body.lines.filter_map { |line| NO_MATCH_LINE.match(line)&.captures&.first }
207
+ not_registered = body.lines.count { |line| line.match?(NOT_REGISTERED_LINE) }
208
+ no_match.length == 1 && no_match.first.downcase.end_with?('.uk') &&
209
+ not_registered == 1 && !registration_evidence?(body) && !invalid?
210
+ end
211
+
212
+ def registration_evidence?(body)
213
+ body.match?(REGISTRATION_EVIDENCE_LINE)
153
214
  end
154
215
 
155
216
 
@@ -5,9 +5,30 @@ module Whois
5
5
  # Identity Digital's current .vip endpoint uses the ICANN key/value
6
6
  # layout and reports an absent name as `No Data Found`.
7
7
  class WhoisNicVip < BaseIcannCompliant
8
+ RECORD_EVIDENCE = /^(?:Domain Name|Registry Domain ID|Creation Date|Registrar|Domain Status):[ \t]*\S/i
9
+
8
10
  self.scanner = Scanners::BaseIcannCompliant, {
9
11
  pattern_available: /^No Data Found\s*$/,
10
12
  }
13
+
14
+ property_supported :available? do
15
+ raise ResponseIsThrottled if response_throttled?
16
+ raise ResponseIsUnavailable if response_unavailable?
17
+
18
+ super()
19
+ end
20
+
21
+ def response_unavailable?
22
+ body = content_for_scanner
23
+ ambiguous_absence = body.match?(/^No Data Found\s*$/i) && body.match?(RECORD_EVIDENCE)
24
+
25
+ super || body.match?(/^\s*access denied\b/i) ||
26
+ ambiguous_absence
27
+ end
28
+
29
+ def response_throttled?
30
+ super || content_for_scanner.match?(/^\s*too many requests\b/i)
31
+ end
11
32
  end
12
33
  end
13
34
  end
@@ -2,7 +2,28 @@ require_relative 'base_identity_digital'
2
2
  module Whois
3
3
  class Parsers
4
4
  class WhoisNicWiki < BaseIdentityDigital
5
+ RECORD_EVIDENCE = /^(?:Domain Name|Registry Domain ID|Creation Date|Registrar|Domain Status):[ \t]*\S/i
6
+
5
7
  self.scanner = Scanners::BaseIcannCompliant, { pattern_available: /^No Data Found\s*$/i }
8
+
9
+ property_supported :available? do
10
+ raise ResponseIsThrottled if response_throttled?
11
+ raise ResponseIsUnavailable if response_unavailable?
12
+
13
+ super()
14
+ end
15
+
16
+ def response_unavailable?
17
+ body = content_for_scanner
18
+ ambiguous_absence = body.match?(/^No Data Found\s*$/i) && body.match?(RECORD_EVIDENCE)
19
+
20
+ super || body.match?(/^\s*access denied\b/i) ||
21
+ ambiguous_absence
22
+ end
23
+
24
+ def response_throttled?
25
+ super || content_for_scanner.match?(/^\s*too many requests\b/i)
26
+ end
6
27
  end
7
28
  end
8
29
  end
@@ -5,6 +5,33 @@ module Whois
5
5
  # The current .win endpoint uses the same CentralNic/MarkMonitor layout
6
6
  # as .design, including the `No Data Found` absence response.
7
7
  class WhoisNicWin < WhoisNicDesign
8
+ RECORD_EVIDENCE = /^(?:Domain Name|Registry Domain ID|Creation Date|Registrar|Domain Status):[ \t]*\S/i
9
+
10
+ property_supported :status do
11
+ raise ResponseIsThrottled if response_throttled?
12
+ raise ResponseIsUnavailable if response_unavailable?
13
+
14
+ super()
15
+ end
16
+
17
+ property_supported :available? do
18
+ raise ResponseIsThrottled if response_throttled?
19
+ raise ResponseIsUnavailable if response_unavailable?
20
+
21
+ super()
22
+ end
23
+
24
+ def response_unavailable?
25
+ body = content_for_scanner
26
+ ambiguous_absence = body.match?(/^No Data Found\s*$/i) && body.match?(RECORD_EVIDENCE)
27
+
28
+ super || body.match?(/^\s*access denied\b/i) ||
29
+ ambiguous_absence
30
+ end
31
+
32
+ def response_throttled?
33
+ super || content_for_scanner.match?(/^\s*too many requests\b/i)
34
+ end
8
35
  end
9
36
  end
10
37
  end
@@ -0,0 +1,52 @@
1
+ require_relative 'whois.pir.org'
2
+ require_relative 'registry_response_safety'
3
+
4
+ module Whois
5
+ class Parsers
6
+ # IANA currently delegates .org WHOIS to whois.publicinterestregistry.org.
7
+ # Its field format matches the legacy whois.pir.org parser.
8
+ class WhoisPublicinterestregistryOrg < WhoisPirOrg
9
+ include RegistryResponseSafety
10
+
11
+ DOMAIN_LINE = /^Domain Name:[ \t]*(\S+)[ \t]*\r?$/i
12
+ ABSENCE_LINE = /\ADomain not found\.[ \t]*\z/i
13
+ REGISTERED_EVIDENCE = /^(Registry Domain ID|Creation Date|Updated Date|Registry Expiry Date|Registrar(?: IANA ID)?|Domain Status):[ \t]*\S/im
14
+ RECORD_EVIDENCE = /^(?:Domain Name|Registry Domain ID|Creation Date|Updated Date|Registry Expiry Date|Registrar(?: IANA ID)?|Domain Status|Name Server):[ \t]*\S/im
15
+ LEGACY_THROTTLE_LINE = /^[ \t]*WHOIS LIMIT EXCEEDED\b/i
16
+
17
+ # WhoisPirOrg has its own scanner-backed throttle check. Call the shared
18
+ # line guard directly as that inherited override parses the whole body.
19
+ # Preserve its established PIR marker without parsing unrelated lines.
20
+ def response_throttled?
21
+ body = content_for_scanner
22
+ body.match?(LEGACY_THROTTLE_LINE) ||
23
+ RegistryResponseSafety.instance_method(:response_throttled?).bind_call(self)
24
+ end
25
+
26
+ property_supported :status do
27
+ body = content_for_scanner
28
+ absence_count = body.lines.count { |line| line.chomp.match?(ABSENCE_LINE) }
29
+
30
+ if absence_count.positive?
31
+ absence_count == 1 && !body.match?(RECORD_EVIDENCE) ? :available : :unknown
32
+ else
33
+ domains = body.scan(DOMAIN_LINE).flatten
34
+ evidence_fields = body.scan(REGISTERED_EVIDENCE).flatten.map(&:downcase).uniq
35
+ if domains.length == 1 && domains.first.downcase.end_with?('.org') && evidence_fields.length >= 2
36
+ :registered
37
+ else
38
+ :unknown
39
+ end
40
+ end
41
+ end
42
+
43
+ property_supported :available? do
44
+ status == :available
45
+ end
46
+
47
+ property_supported :registered? do
48
+ status == :registered
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,53 @@
1
+ require_relative 'whois.dk-hostmaster.dk'
2
+ require_relative 'registry_response_safety'
3
+
4
+ module Whois
5
+ class Parsers
6
+ # Direct parser for IANA's current .dk WHOIS host. The older
7
+ # whois.dk-hostmaster.dk parser remains available for its legacy route.
8
+ class WhoisPunktumDk < WhoisDkHostmasterDk
9
+ include RegistryResponseSafety
10
+
11
+ DOMAIN_LINE = /^Domain:[ \t]*(\S+)[ \t]*\r?$/i
12
+ STATUS_LINE = /^Status:[ \t]*(\S+)[ \t]*\r?$/i
13
+ ABSENCE_LINE = /\ANo entries found for the selected source\.[ \t]*\z/i
14
+ REGISTERED_EVIDENCE = /^(Registered|Expires|Registrar|Hostname):[ \t]*\S/im
15
+ RECORD_EVIDENCE = /^(?:Domain|DNS|Registered|Expires|Registrar|Registration period|VID|DNSSEC|Status|Hostname):[ \t]*\S/im
16
+
17
+ property_supported :domain do
18
+ content_for_scanner.scan(DOMAIN_LINE).flatten.first&.downcase
19
+ end
20
+
21
+ property_supported :status do
22
+ body = content_for_scanner
23
+ absence_count = body.lines.count { |line| line.chomp.match?(ABSENCE_LINE) }
24
+
25
+ if absence_count.positive?
26
+ absence_count == 1 && !body.match?(RECORD_EVIDENCE) ? :available : :unknown
27
+ else
28
+ domains = body.scan(DOMAIN_LINE).flatten
29
+ statuses = body.scan(STATUS_LINE).flatten
30
+ evidence_fields = body.scan(REGISTERED_EVIDENCE).flatten.map(&:downcase).uniq
31
+ if domains.length == 1 && domains.first.downcase.end_with?('.dk') && statuses.length == 1 && evidence_fields.length >= 2
32
+ case statuses.first.downcase
33
+ when 'active' then :registered
34
+ when 'deactivated' then :expired
35
+ when 'reserved' then :reserved
36
+ else :unknown
37
+ end
38
+ else
39
+ :unknown
40
+ end
41
+ end
42
+ end
43
+
44
+ property_supported :available? do
45
+ status == :available
46
+ end
47
+
48
+ property_supported :registered? do
49
+ status == :registered
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'base_top1000_icann'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # Parser for IANA's current .click WHOIS host, whois.registry.click.
6
+ class WhoisRegistryClick < BaseTop1000Icann
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'whois.uniregistry.net'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # The current IANA-listed WHOIS host for .GIFT uses the Uniregistry
6
+ # response format.
7
+ class WhoisRegistryGift < WhoisUniregistryNet
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,44 @@
1
+ require_relative 'base_top1000_icann'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # Parser for the IANA-listed WHOIS server for .SR.
6
+ #
7
+ # IANA's delegation record and current server responses were checked on
8
+ # 2026-09-25. The generated-name response uses an exact Message marker;
9
+ # other incomplete responses must remain unknown.
10
+ class WhoisSr < BaseTop1000Icann
11
+ property_supported :domain do
12
+ node("Domain", &:downcase)
13
+ end
14
+
15
+ property_supported :status do
16
+ if reserved?
17
+ :reserved
18
+ elsif available?
19
+ :available
20
+ elsif !node("Domain").to_s.strip.empty? &&
21
+ node("Status").to_s.casecmp("active").zero? &&
22
+ !node("Creation Date").to_s.strip.empty?
23
+ :registered
24
+ else
25
+ :unknown
26
+ end
27
+ end
28
+
29
+ property_supported :available? do
30
+ content_for_scanner.match?(/^Message: No Object Found\s*$/i)
31
+ end
32
+
33
+ property_supported :registered? do
34
+ status == :registered
35
+ end
36
+
37
+ property_supported :nameservers do
38
+ Array.wrap(node("Name server")).reject(&:empty?).map do |name|
39
+ Parser::Nameserver.new(name: name.downcase)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -8,6 +8,7 @@
8
8
 
9
9
 
10
10
  require_relative 'base'
11
+ require_relative 'registry_response_safety'
11
12
 
12
13
 
13
14
  module Whois
@@ -23,30 +24,35 @@ module Whois
23
24
  # The Example parser for the list of all available methods.
24
25
  #
25
26
  class WhoisTcinetRu < Base
27
+ include RegistryResponseSafety
28
+
29
+ DOMAIN_PATTERN = /\A(?:[[:alnum:]](?:[[:alnum:]-]{0,61}[[:alnum:]])?\.)+[[:alnum:]](?:[[:alnum:]-]{0,61}[[:alnum:]])?\z/i
30
+ KNOWN_STATES = %w[REGISTERED DELEGATED NOT\ DELEGATED VERIFIED UNVERIFIED].freeze
26
31
 
27
32
  property_supported :domain do
28
- if content_for_scanner =~ /domain:\s+(.+?)\n/
29
- ::Regexp.last_match(1).downcase
30
- end
33
+ domain_value&.downcase
31
34
  end
32
35
 
33
36
  property_not_supported :domain_id
34
37
 
35
38
 
36
39
  property_supported :status do
37
- if content_for_scanner =~ /state:\s+(.+?)\n/
38
- ::Regexp.last_match(1).split(",").map(&:strip)
39
- else
40
+ case classify_status
41
+ when :registered
42
+ state_values
43
+ when :available
40
44
  []
45
+ else
46
+ :unknown
41
47
  end
42
48
  end
43
49
 
44
50
  property_supported :available? do
45
- !!(content_for_scanner =~ /No entries found/)
51
+ classify_status == :available
46
52
  end
47
53
 
48
54
  property_supported :registered? do
49
- !available?
55
+ classify_status == :registered
50
56
  end
51
57
 
52
58
 
@@ -108,6 +114,75 @@ module Whois
108
114
  end
109
115
  end
110
116
 
117
+ def response_unavailable?
118
+ super || content_for_scanner.match?(/^\s*access denied\b/i)
119
+ end
120
+
121
+ def response_throttled?
122
+ super || content_for_scanner.match?(/^\s*too many requests\b/i)
123
+ end
124
+
125
+
126
+ private
127
+
128
+ def classify_status
129
+ cached_properties_fetch(:classified_status) do
130
+ response = content_for_scanner
131
+ domains = domain_lines
132
+ state_rows = state_lines
133
+ states = state_values
134
+ normalized_states = states.map(&:upcase)
135
+ valid_states = states.any? && normalized_states.all? { |state| KNOWN_STATES.include?(state) }
136
+ conflicting_delegation = normalized_states.include?("DELEGATED") &&
137
+ normalized_states.include?("NOT DELEGATED")
138
+ conflicting_verification = normalized_states.include?("VERIFIED") &&
139
+ normalized_states.include?("UNVERIFIED")
140
+ safe_response = !response_unavailable? && !response_throttled?
141
+ absence_count = response.lines.count do |line|
142
+ line.chomp.match?(/\ANo entries found for the selected source\(s\)\.[ \t]*\z/i)
143
+ end
144
+
145
+ if safe_response && absence_count == 1 && domains.empty? && state_rows.empty?
146
+ :available
147
+ elsif safe_response && domains.one? && domain_value && state_rows.one? &&
148
+ normalized_states.include?("REGISTERED") && valid_states &&
149
+ !conflicting_delegation && !conflicting_verification && absence_count.zero?
150
+ :registered
151
+ else
152
+ :unknown
153
+ end
154
+ end
155
+ end
156
+
157
+ def domain_lines
158
+ content_for_scanner.lines.filter_map do |line|
159
+ line.chomp.match(/\Adomain:[ \t]*(.*?)[ \t]*\z/i)&.[](1)
160
+ end
161
+ end
162
+
163
+ def domain_value
164
+ domains = domain_lines
165
+ return unless domains.one? && domains.first.match?(DOMAIN_PATTERN)
166
+
167
+ domains.first
168
+ end
169
+
170
+ def state_lines
171
+ content_for_scanner.lines.filter_map do |line|
172
+ line.chomp.match(/\Astate:[ \t]*(.*?)[ \t]*\z/i)&.[](1)
173
+ end
174
+ end
175
+
176
+ def state_values
177
+ rows = state_lines
178
+ return [] unless rows.one?
179
+
180
+ values = rows.first.split(",", -1).map(&:strip)
181
+ return [] if values.any?(&:empty?)
182
+
183
+ values
184
+ end
185
+
111
186
  end
112
187
 
113
188
  end
@@ -0,0 +1,20 @@
1
+ require_relative 'base_icann_compliant'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # Parser for the IANA-listed .mu WHOIS server, whois.tld.mu.
6
+ #
7
+ # Captures from 2026-09-25 expose ICANN-style records and the exact
8
+ # `Domain not found.` absence marker. This host is distinct from the
9
+ # legacy whois.nic.mu mapping in whois 6.0.3.
10
+ class WhoisTldMu < BaseIcannCompliant
11
+ self.scanner = Scanners::BaseIcannCompliant, {
12
+ pattern_available: /^[ \t]*Domain not found\.[ \t]*$/i,
13
+ }
14
+
15
+ property_supported :expires_on do
16
+ node("Registry Expiry Date") { |value| parse_time(value) }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -4,9 +4,63 @@ module Whois
4
4
  class Parsers
5
5
  # Parser for the whois.tonicregistry.to server.
6
6
  class WhoisTonicregistryTo < BaseIcannCompliant
7
+ AVAILABLE_DOMAIN_LINE = /^>>> Domain (?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+to is available for registration$/i
8
+ DOMAIN_PATTERN = /\A(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+to\z/i
9
+ REGISTRATION_FIELD = /^[ \t]*(?:Domain Name|Registry Domain ID|Updated Date|Creation Date|Registry Expiry Date|Registrar(?: URL| IANA ID| Abuse Contact (?:Email|Phone))?|Domain Status|Name Server|DNSSEC):[ \t]*\S/i
10
+
7
11
  self.scanner = Scanners::BaseIcannCompliant, {
8
- pattern_available: /^>>> Domain .+ is available for registration\n/,
12
+ pattern_available: AVAILABLE_DOMAIN_LINE,
9
13
  }
14
+
15
+ property_supported :status do
16
+ if available_marker_count == 1
17
+ :available
18
+ elsif registered_record?
19
+ :registered
20
+ else
21
+ :unknown
22
+ end
23
+ end
24
+
25
+ property_supported :available? do
26
+ status == :available
27
+ end
28
+
29
+ property_supported :registered? do
30
+ status == :registered
31
+ end
32
+
33
+ def response_unavailable?
34
+ super || ambiguous_availability_response?
35
+ end
36
+
37
+ private
38
+
39
+ def available_marker_count
40
+ content_for_scanner.lines.count { |line| line.match?(AVAILABLE_DOMAIN_LINE) }
41
+ end
42
+
43
+ def ambiguous_availability_response?
44
+ marker_count = available_marker_count
45
+ marker_count.positive? && (
46
+ marker_count != 1 || content_for_scanner.match?(REGISTRATION_FIELD)
47
+ )
48
+ end
49
+
50
+ def registered_record?
51
+ response = content_for_scanner
52
+ domains = response.scan(/^[ \t]*Domain Name:[ \t]*(.*?)[ \t]*$/i).flatten
53
+ registry_ids = response.scan(/^[ \t]*Registry Domain ID:[ \t]*(.*?)[ \t]*$/i).flatten
54
+ creation_dates = response.scan(/^[ \t]*Creation Date:[ \t]*(.*?)[ \t]*$/i).flatten
55
+ registrars = response.scan(/^[ \t]*Registrar:[ \t]*(.*?)[ \t]*$/i).flatten
56
+ nameservers = response.scan(/^[ \t]*Name Server:[ \t]*(.*?)[ \t]*$/i).flatten
57
+
58
+ domains.one? && domains.first.match?(DOMAIN_PATTERN) &&
59
+ registry_ids.one? && !registry_ids.first.empty? &&
60
+ creation_dates.one? && parse_time(creation_dates.first) &&
61
+ registrars.one? && !registrars.first.empty? &&
62
+ nameservers.any? { |nameserver| !nameserver.empty? }
63
+ end
10
64
  end
11
65
  end
12
66
  end
@@ -19,8 +19,11 @@ module Whois
19
19
  # The Example parser for the list of all available methods.
20
20
  #
21
21
  class WhoisUniregistryNet < BaseIcannCompliant
22
+ AVAILABLE_DOMAIN_LINE = /^>>> Domain (?:"[^"\r\n]+"|[^\s\r\n]+) is available for registration[ \t]*$/
23
+ RECORD_EVIDENCE = /^[ \t]*(?:Domain Name|Domain ID|Registry Domain ID|Creation Date|Registrar|Sponsoring Registrar|Name Server):[ \t]*\S/i
24
+
22
25
  self.scanner = Scanners::BaseIcannCompliant, {
23
- pattern_available: />>> Domain ".+" is available/,
26
+ pattern_available: AVAILABLE_DOMAIN_LINE,
24
27
  }
25
28
 
26
29
 
@@ -52,12 +55,22 @@ module Whois
52
55
  def response_unavailable?
53
56
  super || content_for_scanner.match?(
54
57
  /^(?:>>> Tld not supported by this registry interface|TLD is not supported\.)$/i
55
- )
58
+ ) || ambiguous_availability_response?
56
59
  end
57
60
 
58
61
 
59
62
  private
60
63
 
64
+ def ambiguous_availability_response?
65
+ marker_count = content_for_scanner.lines.count do |line|
66
+ line.match?(AVAILABLE_DOMAIN_LINE)
67
+ end
68
+
69
+ marker_count.positive? && (
70
+ marker_count != 1 || content_for_scanner.match?(RECORD_EVIDENCE)
71
+ )
72
+ end
73
+
61
74
  def build_contact(element, type)
62
75
  if (contact = super)
63
76
  contact.id = node("#{element} ID")
@@ -0,0 +1,12 @@
1
+ require_relative 'base_icann_compliant'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # Parser for the current IANA-listed .IE WHOIS host, whois.weare.ie.
6
+ class WhoisWeareIe < BaseIcannCompliant
7
+ self.scanner = Scanners::BaseIcannCompliant, {
8
+ pattern_available: /^Not found: [a-z0-9-]+\.ie\s*$/i,
9
+ }
10
+ end
11
+ end
12
+ end