whois-parser-next 0.3.2 → 0.4.1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +3 -3
  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.dns.lu.rb +14 -4
  10. data/lib/whois/parsers/whois.eu.rb +33 -7
  11. data/lib/whois/parsers/whois.id.rb +22 -2
  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.tcinet.ru.rb +83 -8
  45. data/lib/whois/parsers/whois.tonicregistry.to.rb +55 -1
  46. data/lib/whois/parsers/whois.uniregistry.net.rb +15 -2
  47. data/lib/whois/parsers/whois.weare.ie.rb +12 -0
  48. metadata +13 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4dd7f05a922301b74d2dccf983d9712ac9611198244bb84bf7da9d3106eb6985
4
- data.tar.gz: 474233b88fad6dad201ca87d0d5c91e5a15a8abab76904192710d644a74420db
3
+ metadata.gz: '09b27e56a5225ea22fadb05db7497d93757e87e811520604377c2dcaa14900f8'
4
+ data.tar.gz: 7db37727a691aad97de784fb680da596ad79f5918df671a9f862becae1a0d37e
5
5
  SHA512:
6
- metadata.gz: 0f041533d14ae1f57dfe6ea92e1967bb1ef5fdfb40379f4eea079c2bf37c58adadf53e8ed0685e0e55076ba9df44341b5a91f9115e59478a67f1c178d66fae76
7
- data.tar.gz: 762d431b3ec1aa9b967a68b74f346ed41fbb37adbbb4f5304fb8e615dec298308f5f276f54a06e57da5b8a1bc217f6dd4b5f57c0df5e62c6142f1202d1069c41
6
+ metadata.gz: e71cf9f12d9602ad01877a3ab8592dfd387bc3f9afe6a01976a55a3d44af2e931ddd70debe9d9a60042b439712a4773cb4b65f29f5199f91d0815624ce749a7e
7
+ data.tar.gz: 3671c9bd5513063ebde38e00f07b39e26b3b00f2ad605ba6233a402cf5002a43cf75f0c0ce52bc3dcce376c860309a438530764d3dfb6494d76006c507927ccb
data/CHANGELOG.md CHANGED
@@ -2,6 +2,53 @@
2
2
 
3
3
  This project uses [Semantic Versioning 2.0.0](http://semver.org/).
4
4
 
5
+ #### Release 0.4.1 (2026-10-17)
6
+
7
+ - SAFETY: `.lu` records are registered only when the response contains a
8
+ domain name and an exact `domaintype: ACTIVE` field. The exact existing
9
+ absence marker is accepted only without a record; mixed, incomplete, or
10
+ ambiguous replies remain unknown.
11
+ - SAFETY: `.lu` query-rate limits, including the registry's repeated-percent
12
+ `%% Maximum query rate reached` response, raise a throttled-response error
13
+ even when the reply also contains a domain record or absence marker.
14
+ - TESTED: The parser suite passes 7,016 examples with 0 failures and the
15
+ package smoke check passes on Ruby 3.2 and 3.4.5.
16
+ - EVIDENCE: The [2026-10-17 DomScan audit](docs/audits/domscan-whois-usage-2026-10-17.md)
17
+ documents the fixed false-availability path and explains why its fixture
18
+ overlap and bounded replay do not establish 99% live accuracy.
19
+
20
+ #### Release 0.4.0 (2026-09-29)
21
+
22
+ - ADDED: Expanded and tightened first-class parsing for current official WHOIS
23
+ hosts used by the pinned DomScan high-demand cohort. New parser examples
24
+ include `.biz`, `.click`, `.gift`, `.help`, `.homes`, `.ie`, `.org`, and
25
+ `.dk`; updated parser examples include `.mn`, `.cl`, `.eu`, `.giving`, `.id`,
26
+ `.mx`, `.love`, `.link`, `.icu`, `.lat`, `.to`, and `.open`. The audit tracks
27
+ all 153 suffixes with more than five observed endpoint requests (44,402
28
+ request items), with 94 registered/absence regression pairs and no
29
+ parser-pending rows. Restricted registry cases use minimal synthetic
30
+ fixtures. The audit keeps RDAP-only, retired, unsupported, and restricted
31
+ cases as separate outcomes; see the [full audit](docs/audits/domscan-whois-usage-2026-09-28.md)
32
+ for per-suffix evidence and route limitations.
33
+ - SAFETY: Registration and absence require exact registry evidence. Denied,
34
+ throttled, malformed, incomplete, or contradictory responses do not become
35
+ positive registration or availability results. Unknown and unsupported
36
+ responses remain distinct from authoritative absence.
37
+ - CLARIFIED: `.uk` remains a temporary WHOIS exception based on Nominet's
38
+ guidance that `whois.nic.uk` continues through 2027-02-09, despite IANA's
39
+ RDAP-only listing. The release tests current registered and absence response
40
+ shapes; DomScan's audited 30-day snapshot contained no traditional `.uk`
41
+ WHOIS calls.
42
+ - SAFETY: `.open` registration is restricted by the registry's
43
+ [registration policy](https://web.aexp-static.com/content/dam/amex/us/staticassets/pdf/nic/Registry-Policies-OPEN.pdf).
44
+ A synthetic complete-record test checks the exact `nic.open` registered
45
+ shape. `No Data Found` remains unknown, and this release makes no
46
+ public-availability claim for `.open`.
47
+ - TESTED: The integrated parser suite passes 7,001 examples with 0 failures;
48
+ the pinned-cohort coverage audit passes 3 examples with 0 failures. The
49
+ linked audit records the separate DomScan route checks and their local,
50
+ unreleased status.
51
+
5
52
  #### Release 0.3.2 (2026-09-25)
6
53
 
7
54
  - ADDED: Added host-keyed parsers for the current IANA-listed WHOIS hosts for
data/README.md CHANGED
@@ -38,7 +38,7 @@ of current WHOIS responses, explicit unknown states, registry-specific
38
38
  fixtures, and safe handling of malformed, rate-limited, retired, or changed
39
39
  server responses.
40
40
 
41
- The current source version is `0.3.2`. Check the [RubyGems project
41
+ The current source version is `0.4.0`. Check the [RubyGems project
42
42
  page](https://rubygems.org/gems/whois-parser-next) for its publication status
43
43
  and the latest published version.
44
44
 
@@ -54,10 +54,10 @@ Whois::Parser
54
54
  ```
55
55
 
56
56
  Existing applications can retain `require "whois-parser"` and their
57
- `Whois::Parser` integrations. To install version 0.3.2, pin it as follows:
57
+ `Whois::Parser` integrations. To install version 0.4.0, pin it as follows:
58
58
 
59
59
  ```ruby
60
- gem "whois-parser-next", "= 0.3.2", require: "whois-parser"
60
+ gem "whois-parser-next", "= 0.4.0", require: "whois-parser"
61
61
  ```
62
62
 
63
63
  ## Requirements
data/RELEASING.md CHANGED
@@ -21,8 +21,8 @@ RubyGems API key to the repository or its Actions secrets.
21
21
  Create and push the annotated release tag only after the checks above pass:
22
22
 
23
23
  ```shell
24
- git tag -a v0.3.2 -m "whois-parser-next 0.3.2"
25
- git push origin v0.3.2
24
+ git tag -a v0.4.0 -m "whois-parser-next 0.4.0"
25
+ git push origin v0.4.0
26
26
  ```
27
27
 
28
28
  The release workflow repeats the full suite and package smoke test before it
@@ -1,6 +1,6 @@
1
1
  module Whois
2
2
  class Parser
3
3
  # The current library version.
4
- VERSION = "0.3.2".freeze
4
+ VERSION = "0.4.1".freeze
5
5
  end
6
6
  end
@@ -9,19 +9,21 @@ module Whois
9
9
 
10
10
  def response_throttled?
11
11
  content_for_scanner.match?(
12
- /^(?:\s*(?:%|#)\s*)?(?:whois\s+)?query\s+(?:rate\s+)?limit\s+exceeded\b/i
12
+ /^(?:[ \t]*(?:%|#)[ \t]*)?(?:whois\s+)?query\s+(?:rate\s+)?limit\s+exceeded\b/i
13
13
  ) || content_for_scanner.match?(
14
- /^(?:\s*(?:%|#)\s*)?maximum\s+query\s+rate\s+reached\b/i
14
+ /^(?:[ \t]*(?:%|#)+[ \t]*)?maximum\s+query\s+rate\s+reached\b/i
15
15
  ) || content_for_scanner.match?(
16
- /^(?:\s*(?:%|#)\s*)?excessive\s+querying\b/i
16
+ /^(?:[ \t]*(?:%|#)[ \t]*)?excessive\s+querying\b/i
17
17
  ) || content_for_scanner.match?(
18
- /^\s*(?:%|#)?\s*whois\s+limit\s+exceeded\b/i
18
+ /^[ \t]*(?:%|#)?[ \t]*whois\s+limit\s+exceeded\b/i
19
+ ) || content_for_scanner.match?(
20
+ /^[ \t]*(?:%|#)?[ \t]*(?:too\s+many\s+requests|rate\s+limit\s+exceeded)\b/i
19
21
  )
20
22
  end
21
23
 
22
24
  def response_unavailable?
23
25
  response_incomplete? || content_for_scanner.match?(
24
- /^(?:\s*(?:%|#)\s*)?(?:requests of this client are not permitted|access to the whois service is denied|whois service is unavailable)\b/i
26
+ /^(?:[ \t]*(?:%|#)[ \t]*)?(?:requests of this client are not permitted|access to the whois service is denied|whois service is unavailable|access denied|permission denied|not authori[sz]ed|unauthori[sz]ed)\b/i
25
27
  ) || content_for_scanner.match?(
26
28
  /\ATLD is not supported\.[ \t]*(?:\n|\z)/i
27
29
  )
@@ -1,11 +1,53 @@
1
1
  require_relative 'whois.audns.net.au'
2
+ require_relative 'registry_response_safety'
2
3
 
3
4
  module Whois
4
5
  class Parsers
5
6
  # Parser for the whois.auda.org.au server.
6
7
  class WhoisAudaOrgAu < WhoisAudnsNetAu
8
+ include RegistryResponseSafety
9
+
10
+ KNOWN_STATUSES = %w[
11
+ clientDeleteProhibited clientHold clientRenewProhibited clientTransferProhibited
12
+ clientUpdateProhibited inactive ok pendingCreate pendingDelete pendingRenew
13
+ pendingRestore pendingTransfer pendingUpdate serverDeleteProhibited serverHold
14
+ serverRenewProhibited serverTransferProhibited serverUpdateProhibited
15
+ ].freeze
16
+
17
+ property_supported :status do
18
+ classify_status
19
+ end
20
+
7
21
  property_supported :available? do
8
- super() || !!(content_for_scanner =~ /^Domain not found\.\n/)
22
+ classify_status == :available
23
+ end
24
+
25
+ property_supported :registered? do
26
+ classify_status == :registered
27
+ end
28
+
29
+ private
30
+
31
+ def classify_status
32
+ cached_properties_fetch(:classified_status) do
33
+ domain_values = content_for_scanner.scan(/^Domain Name:[ \t]*([^\r\n]+?)[ \t]*$/i).flatten
34
+ domain = domain_values.length == 1 && domain_values.first.match?(/\A(?:[a-z0-9-]+\.)+au\z/i)
35
+ statuses = content_for_scanner.scan(/^Status:[ \t]*([^\r\n]*)$/i).flatten.map(&:strip).reject(&:empty?)
36
+ known_statuses = statuses.map { |value| value.split(/[ \t]/, 2).first }.all? do |value|
37
+ KNOWN_STATUSES.include?(value)
38
+ end
39
+ record_fields = content_for_scanner.scan(/^(?:Registry Domain ID|Registrar Name|Name Server):[ \t]*\S[^\r\n]*$/i)
40
+ has_record_fields = content_for_scanner.match?(/^(?:Domain Name|Registry Domain ID|Registrar Name|Status|Name Server):[ \t]*\S/i)
41
+ available_marker = content_for_scanner.match?(/^Domain not found\.[ \t]*$/i)
42
+
43
+ if available_marker && !has_record_fields
44
+ :available
45
+ elsif domain && !statuses.empty? && known_statuses && record_fields.length >= 2 && !available_marker
46
+ :registered
47
+ else
48
+ :unknown
49
+ end
50
+ end
9
51
  end
10
52
  end
11
53
  end
@@ -69,6 +69,12 @@ module Whois
69
69
  private
70
70
 
71
71
  def classify_status
72
+ cached_properties_fetch(:classified_status) do
73
+ classify_status_from_content
74
+ end
75
+ end
76
+
77
+ def classify_status_from_content
72
78
  return :unknown if absence_conflicts_with_record?
73
79
 
74
80
  evidence = []
@@ -27,17 +27,19 @@ module Whois
27
27
  include RegistryResponseSafety
28
28
 
29
29
  property_supported :status do
30
- if content_for_scanner.match?(/domaintype:\s+active\s*$/i)
30
+ if response_unavailable? || response_throttled? || (lu_record? && lu_absence?)
31
+ :unknown
32
+ elsif lu_record? && content_for_scanner.match?(/^domaintype:[ \t]+active[ \t]*$/i)
31
33
  :registered
32
- elsif available?
34
+ elsif lu_absence? && !lu_record?
33
35
  :available
34
36
  else
35
- Whois::Parser.bug!(ParserError, "Unable to parse .lu response status.")
37
+ :unknown
36
38
  end
37
39
  end
38
40
 
39
41
  property_supported :available? do
40
- !!(content_for_scanner =~ /% No such domain/)
42
+ status == :available
41
43
  end
42
44
 
43
45
  property_supported :registered? do
@@ -92,6 +94,14 @@ module Whois
92
94
 
93
95
  private
94
96
 
97
+ def lu_record?
98
+ content_for_scanner.match?(/^domainname:[ \t]+\S+[ \t]*$/i)
99
+ end
100
+
101
+ def lu_absence?
102
+ content_for_scanner.match?(/^%[ \t]+No such domain(?:[ \t]*[.:].*)?[ \t]*$/i)
103
+ end
104
+
95
105
  def build_contact(element, type)
96
106
  return unless (name = value_for_key(format("%s-name", element)))
97
107
 
@@ -27,19 +27,15 @@ module Whois
27
27
 
28
28
 
29
29
  property_supported :status do
30
- if available?
31
- :available
32
- else
33
- :registered
34
- end
30
+ classify_status
35
31
  end
36
32
 
37
33
  property_supported :available? do
38
- !!(content_for_scanner =~ /Status:\s+AVAILABLE/)
34
+ classify_status == :available
39
35
  end
40
36
 
41
37
  property_supported :registered? do
42
- !available?
38
+ classify_status == :registered
43
39
  end
44
40
 
45
41
 
@@ -124,6 +120,36 @@ module Whois
124
120
  !!(content_for_scanner =~ /Still in grace period/)
125
121
  end
126
122
 
123
+ private
124
+
125
+ def classify_status
126
+ cached_properties_fetch(:classified_status) do
127
+ status_values = content_for_scanner.scan(/^Status:[ \t]*([^\r\n]*)$/i).flatten.map(&:strip)
128
+ available = status_values == ['AVAILABLE']
129
+ domain_values = content_for_scanner.scan(/^Domain:[ \t]*([^\r\n]+?)[ \t]*$/i).flatten.map(&:downcase)
130
+ domain = domain_values.length == 1 && domain_values.first.match?(/\A(?:[a-z0-9-]+\.)+eu\z/i)
131
+ echoed_queries = content_for_scanner.scan(/^%?[ \t]*WHOIS[ \t]+((?:[a-z0-9-]+\.)+eu)[ \t]*$/i).flatten.map(&:downcase)
132
+ identity_matches = echoed_queries.empty? || (echoed_queries.length == 1 && echoed_queries.first == domain_values.first)
133
+ registrar = content_for_scanner.match?(/^Registrar:[ \t]*\r?\n[ \t]+Name:[ \t]*\S[^\r\n]*$/i)
134
+ nameservers = content_for_scanner.match?(
135
+ /^Name servers:[ \t]*\r?\n(?:[ \t]+(?:[a-z0-9-]+\.)+[a-z0-9-]+(?:[ \t]+\([^\r\n()]+\))?[ \t]*\r?\n?)+/i
136
+ )
137
+ record = registrar || nameservers
138
+ record_fields = content_for_scanner.match?(/^(?:Registrar|Name servers|Registrant|Onsite(?:\(s\))?|Technical|Created|Creation date|Updated|Last updated|Expires|Expiration date):/i)
139
+ denied_or_throttled = content_for_scanner.match?(
140
+ /\b(?:access\s+denied|permission\s+denied|not\s+authori[sz]ed|unauthori[sz]ed|not\s+allowed|too\s+many\s+requests|rate[- ]?limit(?:ed)?|temporarily\s+unavailable|service\s+unavailable|try\s+again\s+later|still\s+in\s+grace\s+period)\b/i
141
+ )
142
+
143
+ if !denied_or_throttled && available && domain && identity_matches && !record_fields
144
+ :available
145
+ elsif !denied_or_throttled && domain && identity_matches && record && (status_values.empty? || status_values == ['REGISTERED'])
146
+ :registered
147
+ else
148
+ :unknown
149
+ end
150
+ end
151
+ end
152
+
127
153
  end
128
154
 
129
155
  end
@@ -9,17 +9,37 @@ module Whois
9
9
 
10
10
  # Parser for the current whois.id registry service.
11
11
  class WhoisId < BaseIcannCompliant
12
+ NO_RECORD_MARKER = /^The queried object does not exist: (?:DOMAIN NOT FOUND|(?:[a-z0-9-]+\.)+id)\s*$/i
13
+ RECORD_EVIDENCE = /^(?:Domain Name|Registry Domain ID|Creation Date|Registrar|Domain Status|Name Server):[ \t]*\S/i
12
14
 
13
- self.scanner = Scanners::BaseIcannCompliant
15
+ self.scanner = Scanners::BaseIcannCompliant, {
16
+ pattern_available: NO_RECORD_MARKER,
17
+ }
14
18
 
15
19
  property_supported :available? do
16
- !!(content_for_scanner =~ /DOMAIN NOT FOUND/i)
20
+ raise ResponseIsThrottled if response_throttled?
21
+ raise ResponseIsUnavailable if response_unavailable?
22
+
23
+ no_record_marker_count == 1
17
24
  end
18
25
 
19
26
  property_supported :expires_on do
20
27
  node("Registry Expiry Date") { |value| parse_time(value) }
21
28
  end
22
29
 
30
+ def response_unavailable?
31
+ body = content_for_scanner
32
+ marker_count = no_record_marker_count
33
+
34
+ super || (marker_count.positive? && (marker_count != 1 || body.match?(RECORD_EVIDENCE)))
35
+ end
36
+
37
+ private
38
+
39
+ def no_record_marker_count
40
+ content_for_scanner.lines.count { |line| line.match?(NO_RECORD_MARKER) }
41
+ end
42
+
23
43
  end
24
44
 
25
45
  end
@@ -0,0 +1,38 @@
1
+ require_relative 'whois.biz'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # The current IANA delegation points .biz to whois.nic.biz. Its record and
6
+ # absence formats match the existing .biz registry parser.
7
+ class WhoisNicBiz < WhoisBiz
8
+ DOMAIN_LINE = /^Domain Name:[ \t]*(\S+)[ \t]*$/i
9
+ REGISTERED_EVIDENCE = /^(?:Registry Domain ID|Creation Date|Updated Date|Registry Expiry Date|Registrar(?: IANA ID)?):[ \t]*\S/im
10
+ ABSENCE_LINE = /^No Data Found[ \t]*\r?$/i
11
+ RECORD_EVIDENCE = /^(?:Domain Name|Registry Domain ID|Creation Date|Updated Date|Registry Expiry Date|Registrar(?: IANA ID)?|Domain Status|Name Server):[ \t]*\S/im
12
+
13
+ property_supported :status do
14
+ body = content_for_scanner
15
+ absence_count = body.lines.count { |line| line.match?(ABSENCE_LINE) }
16
+
17
+ if absence_count.positive?
18
+ absence_count == 1 && !body.match?(RECORD_EVIDENCE) ? :available : :unknown
19
+ else
20
+ domains = body.scan(DOMAIN_LINE).flatten
21
+ if domains.length == 1 && domains.first.downcase.end_with?('.biz') && body.match?(REGISTERED_EVIDENCE)
22
+ :registered
23
+ else
24
+ :unknown
25
+ end
26
+ end
27
+ end
28
+
29
+ property_supported :available? do
30
+ status == :available
31
+ end
32
+
33
+ property_supported :registered? do
34
+ status == :registered
35
+ end
36
+ end
37
+ end
38
+ end
@@ -27,20 +27,15 @@ module Whois
27
27
  class WhoisNicCl < Base
28
28
 
29
29
  property_supported :status do
30
- if available?
31
- :available
32
- else
33
- :registered
34
- end
30
+ classify_status
35
31
  end
36
32
 
37
33
  property_supported :available? do
38
- !!(content_for_scanner =~ /^(.+?): no existe$/i ||
39
- content_for_scanner =~ /^.+:\s+no entries found\.$/i)
34
+ classify_status == :available
40
35
  end
41
36
 
42
37
  property_supported :registered? do
43
- !available?
38
+ classify_status == :registered
44
39
  end
45
40
 
46
41
 
@@ -70,6 +65,35 @@ module Whois
70
65
  end
71
66
  end
72
67
 
68
+ private
69
+
70
+ # The current response layout in the DomScan corpus does not echo the
71
+ # queried domain. This parser can require a record-shaped response, but
72
+ # cannot independently verify that the returned record matches the query.
73
+ def classify_status
74
+ cached_properties_fetch(:classified_status) do
75
+ available = content_for_scanner.match?(/^(?:[a-z0-9-]+\.)+cl:[ \t]*(?:no existe|no entries found\.)[ \t]*$/i)
76
+ current_record = content_for_scanner.match?(/^Registrar name:[ \t]*\S[^\r\n]*$/i) &&
77
+ content_for_scanner.match?(/^Creation date:[ \t]*\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])(?:[ \t]+\d{2}:\d{2}:\d{2}[ \t]+[A-Z]{2,5})?[ \t]*$/i) &&
78
+ content_for_scanner.match?(/^Expiration date:[ \t]*\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])(?:[ \t]+\d{2}:\d{2}:\d{2}[ \t]+[A-Z]{2,5})?[ \t]*$/i) &&
79
+ content_for_scanner.match?(/^Name server:[ \t]*(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?[ \t]*$/i)
80
+ legacy_record = content_for_scanner.match?(/^ACE:[ \t]*(?:[a-z0-9-]+\.)+cl[ \t]+\(RFC-3490, RFC-3491, RFC-3492\)/i) &&
81
+ content_for_scanner.match?(/^Servidores de nombre \(Domain servers\):/i)
82
+ record = current_record || legacy_record
83
+ record_marker = content_for_scanner.match?(/^(?:Registrant(?: name| organisation| organization| email| address)?|Registrar(?: name| URL)?|Creation date|Expiration date|Name server|ACE):/i) ||
84
+ content_for_scanner.match?(/^Servidores de nombre \(Domain servers\):/i)
85
+ denied = content_for_scanner.match?(/\b(?:not\s+authori[sz]ed|unauthori[sz]ed|access\s+denied|permission\s+denied|too\s+many\s+requests|rate[- ]?limited?)\b|\bno\s+autorizad[oa]\b|\bacceso\s+denegado\b/i)
86
+
87
+ if !denied && available && !record_marker
88
+ :available
89
+ elsif !denied && record && !available
90
+ :registered
91
+ else
92
+ :unknown
93
+ end
94
+ end
95
+ end
96
+
73
97
  end
74
98
 
75
99
  end
@@ -3,6 +3,10 @@ require_relative 'base_icann_compliant'
3
3
  module Whois
4
4
  class Parsers
5
5
  class WhoisNicFoundation < BaseIcannCompliant
6
+ self.scanner = Scanners::BaseIcannCompliant, {
7
+ pattern_available: /^Domain not found\.[ \t]*$/i,
8
+ }
9
+
6
10
  end
7
11
  end
8
12
  end
@@ -34,6 +34,12 @@ module Whois
34
34
  private
35
35
 
36
36
  def classify_status
37
+ cached_properties_fetch(:classified_status) do
38
+ classify_status_from_content
39
+ end
40
+ end
41
+
42
+ def classify_status_from_content
37
43
  if registered_evidence?
38
44
  :registered
39
45
  elsif available_evidence?
@@ -0,0 +1,13 @@
1
+ require_relative 'base_unsupported_registry'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # Identity Digital's shared endpoint explicitly reports that .GIFTS is
6
+ # unsupported over WHOIS. The TLD's authoritative registry data is RDAP.
7
+ class WhoisNicGifts < BaseUnsupportedRegistry
8
+ def response_unavailable?
9
+ content_for_scanner.match?(/\A[ \t]*TLD is not supported\.[ \t]*(?:\r?\n|\z)/i)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'base_icann_compliant'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # Parser for the PIR WHOIS service serving .GIVES.
6
+ class WhoisNicGives < BaseIcannCompliant
7
+ self.scanner = Scanners::BaseIcannCompliant, {
8
+ pattern_available: /^[ \t]*Domain not found\.[ \t\r]*$/i,
9
+ }
10
+
11
+ property_supported :status do
12
+ classify_status
13
+ end
14
+
15
+ property_supported :available? do
16
+ classify_status == :available
17
+ end
18
+
19
+ property_supported :registered? do
20
+ classify_status == :registered
21
+ end
22
+
23
+ private
24
+
25
+ def classify_status
26
+ cached_properties_fetch(:classified_status) do
27
+ response = content_for_scanner
28
+ missing = response.match?(/\A[ \t]*Domain not found\.[ \t]*(?:\r?\n|\z)/i)
29
+ denial = response.match?(/^[ \t]*(?:[%#][ \t]*)?(?:access denied|permission denied|request denied|forbidden)\b/i)
30
+ domains = response.scan(/^[ \t]*Domain Name:[ \t]*((?:[a-z0-9-]+\.)+gives)[ \t\r]*$/i).flatten.uniq
31
+ has_registrar = response.match?(/^[ \t]*Registrar:[ \t]*\S/i)
32
+ has_record_detail = response.match?(/^[ \t]*(?:Creation Date|Updated Date|Registry Expiry Date|Name Server):[ \t]*\S/i)
33
+ has_record = domains.one? && has_registrar && has_record_detail
34
+
35
+ if missing && domains.empty? && !denial && !response_throttled?
36
+ :available
37
+ elsif has_record && !missing && !denial && !response_throttled?
38
+ :registered
39
+ else
40
+ :unknown
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,8 +1,52 @@
1
1
  require_relative 'base_icann_compliant'
2
+ require_relative 'registry_response_safety'
2
3
 
3
4
  module Whois
4
5
  class Parsers
5
6
  class WhoisNicGiving < BaseIcannCompliant
7
+ include RegistryResponseSafety
8
+
9
+ DOMAIN_PATTERN = /\A(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+giving\z/i
10
+ NO_RECORD_MARKER = /^[ \t]*Domain not found\.[ \t]*$/i
11
+
12
+ self.scanner = Scanners::BaseIcannCompliant, {
13
+ pattern_available: NO_RECORD_MARKER,
14
+ }
15
+
16
+ property_supported :status do
17
+ classify_status
18
+ end
19
+
20
+ property_supported :available? do
21
+ classify_status == :available
22
+ end
23
+
24
+ property_supported :registered? do
25
+ classify_status == :registered
26
+ end
27
+
28
+ private
29
+
30
+ def classify_status
31
+ cached_properties_fetch(:classified_status) do
32
+ response = content_for_scanner
33
+ missing = response.lines.one? { |line| line.match?(NO_RECORD_MARKER) }
34
+ denial = response.match?(/^[ \t]*(?:[%#][ \t]*)?(?:access denied|permission denied|request denied|forbidden)\b/i)
35
+ domain_lines = response.scan(/^[ \t]*Domain Name:[ \t]*(.*?)[ \t\r]*$/i).flatten
36
+ domains = domain_lines.grep(DOMAIN_PATTERN)
37
+ has_registrar = response.match?(/^[ \t]*Registrar:[ \t]*\S/i)
38
+ has_record_detail = response.match?(/^[ \t]*(?:Creation Date|Updated Date|Registry Expiry Date|Name Server):[ \t]*\S/i)
39
+ has_record = domain_lines.one? && domains.one? && has_registrar && has_record_detail
40
+
41
+ if missing && domain_lines.empty? && !denial && !response_unavailable? && !response_throttled?
42
+ :available
43
+ elsif has_record && !missing && !denial && !response_unavailable? && !response_throttled?
44
+ :registered
45
+ else
46
+ :unknown
47
+ end
48
+ end
49
+ end
6
50
  end
7
51
  end
8
52
  end
@@ -0,0 +1,12 @@
1
+ require_relative 'base_identity_digital'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # CentralNic's .HELP registry endpoint uses its exact no-object marker.
6
+ class WhoisNicHelp < BaseIdentityDigital
7
+ self.scanner = Scanners::BaseIcannCompliant, {
8
+ pattern_available: /^The queried object does not exist: DOMAIN NOT FOUND\s*$/i,
9
+ }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'base_identity_digital'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # CentralNic's .HOMES registry endpoint uses its exact no-object marker.
6
+ class WhoisNicHomes < BaseIdentityDigital
7
+ self.scanner = Scanners::BaseIcannCompliant, {
8
+ pattern_available: /^The queried object does not exist: DOMAIN NOT FOUND\s*$/i,
9
+ }
10
+ end
11
+ end
12
+ end
@@ -4,9 +4,19 @@ module Whois
4
4
  class Parsers
5
5
  # Parser for the Identity Digital endpoint serving .ICU.
6
6
  class WhoisNicIcu < BaseIcannCompliant
7
+ NO_RECORD_MARKER = /^The queried object does not exist: DOMAIN NOT FOUND\s*$/i
8
+ RECORD_EVIDENCE = /^(?:Domain Name|Registry Domain ID|Creation Date|Registrar|Domain Status|Name Server):[ \t]*\S/i
9
+
7
10
  self.scanner = Scanners::BaseIcannCompliant, {
8
- pattern_available: /^The queried object does not exist: DOMAIN NOT FOUND$/i,
11
+ pattern_available: NO_RECORD_MARKER,
9
12
  }
13
+
14
+ def response_unavailable?
15
+ body = content_for_scanner
16
+ marker_count = body.lines.count { |line| line.match?(NO_RECORD_MARKER) }
17
+
18
+ super || (marker_count.positive? && (marker_count != 1 || body.match?(RECORD_EVIDENCE)))
19
+ end
10
20
  end
11
21
  end
12
22
  end
@@ -2,7 +2,21 @@ require_relative 'base_identity_digital'
2
2
  module Whois
3
3
  class Parsers
4
4
  class WhoisNicLat < BaseIdentityDigital
5
- self.scanner = Scanners::BaseIcannCompliant, { pattern_available: /^The queried object does not exist: DOMAIN NOT FOUND\s*$/i }
5
+ NO_RECORD_MARKER = /^The queried object does not exist: DOMAIN NOT FOUND\s*$/i
6
+ RECORD_EVIDENCE = /^[ \t]*(?:Domain Name|Registry Domain ID|Creation Date|Registrar|Domain Status|Name Server):[ \t]*\S/i
7
+
8
+ self.scanner = Scanners::BaseIcannCompliant, { pattern_available: NO_RECORD_MARKER }
9
+
10
+ property_supported :available? do
11
+ content_for_scanner.lines.one? { |line| line.match?(NO_RECORD_MARKER) }
12
+ end
13
+
14
+ def response_unavailable?
15
+ body = content_for_scanner
16
+ marker_count = body.lines.count { |line| line.match?(NO_RECORD_MARKER) }
17
+
18
+ super || (marker_count.positive? && (marker_count != 1 || body.match?(RECORD_EVIDENCE)))
19
+ end
6
20
  end
7
21
  end
8
22
  end