whois-parser-next 0.3.2 → 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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -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.eu.rb +33 -7
  10. data/lib/whois/parsers/whois.id.rb +22 -2
  11. data/lib/whois/parsers/whois.nic.biz.rb +38 -0
  12. data/lib/whois/parsers/whois.nic.cl.rb +32 -8
  13. data/lib/whois/parsers/whois.nic.foundation.rb +4 -0
  14. data/lib/whois/parsers/whois.nic.ge.rb +6 -0
  15. data/lib/whois/parsers/whois.nic.gifts.rb +13 -0
  16. data/lib/whois/parsers/whois.nic.gives.rb +46 -0
  17. data/lib/whois/parsers/whois.nic.giving.rb +44 -0
  18. data/lib/whois/parsers/whois.nic.help.rb +12 -0
  19. data/lib/whois/parsers/whois.nic.homes.rb +12 -0
  20. data/lib/whois/parsers/whois.nic.icu.rb +11 -1
  21. data/lib/whois/parsers/whois.nic.lat.rb +15 -1
  22. data/lib/whois/parsers/whois.nic.love.rb +19 -0
  23. data/lib/whois/parsers/whois.nic.mn.rb +51 -0
  24. data/lib/whois/parsers/whois.nic.mx.rb +47 -7
  25. data/lib/whois/parsers/whois.nic.net.sa.rb +6 -0
  26. data/lib/whois/parsers/whois.nic.news.rb +2 -0
  27. data/lib/whois/parsers/whois.nic.one.rb +7 -1
  28. data/lib/whois/parsers/whois.nic.online.rb +25 -1
  29. data/lib/whois/parsers/whois.nic.open.rb +95 -0
  30. data/lib/whois/parsers/whois.nic.quest.rb +17 -0
  31. data/lib/whois/parsers/whois.nic.sbs.rb +28 -0
  32. data/lib/whois/parsers/whois.nic.store.rb +28 -0
  33. data/lib/whois/parsers/whois.nic.top.rb +25 -3
  34. data/lib/whois/parsers/whois.nic.tv.rb +21 -0
  35. data/lib/whois/parsers/whois.nic.uk.rb +84 -23
  36. data/lib/whois/parsers/whois.nic.vip.rb +21 -0
  37. data/lib/whois/parsers/whois.nic.wiki.rb +21 -0
  38. data/lib/whois/parsers/whois.nic.win.rb +27 -0
  39. data/lib/whois/parsers/whois.publicinterestregistry.org.rb +52 -0
  40. data/lib/whois/parsers/whois.punktum.dk.rb +53 -0
  41. data/lib/whois/parsers/whois.registry.click.rb +9 -0
  42. data/lib/whois/parsers/whois.registry.gift.rb +10 -0
  43. data/lib/whois/parsers/whois.tcinet.ru.rb +83 -8
  44. data/lib/whois/parsers/whois.tonicregistry.to.rb +55 -1
  45. data/lib/whois/parsers/whois.uniregistry.net.rb +15 -2
  46. data/lib/whois/parsers/whois.weare.ie.rb +12 -0
  47. 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: 5351ca8b47d24f7a194be90595c52c18210e24682bf223134f075cc66c223b04
4
+ data.tar.gz: 4befccbf200b2c77f9338c91d2cf122eef11e25883916e070f887db1bdbe1bb3
5
5
  SHA512:
6
- metadata.gz: 0f041533d14ae1f57dfe6ea92e1967bb1ef5fdfb40379f4eea079c2bf37c58adadf53e8ed0685e0e55076ba9df44341b5a91f9115e59478a67f1c178d66fae76
7
- data.tar.gz: 762d431b3ec1aa9b967a68b74f346ed41fbb37adbbb4f5304fb8e615dec298308f5f276f54a06e57da5b8a1bc217f6dd4b5f57c0df5e62c6142f1202d1069c41
6
+ metadata.gz: f9b65eaac1e8ebcaac940563ca1c9748fa7607f900dc30e5977844323b1799dcd4f365166dc54920a2b2eb8eda094cd68eb42c62920b8a76495ffaab0eb9e45d
7
+ data.tar.gz: f2a5197d3b4e7a78f486a43d76b34a7ebd91da46fe3485f4bc3fcaa117c67b78b0c202a645ae11f4ebd1a7c755d24305d180ddd442b0b67c0acf75cda4431fd7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  This project uses [Semantic Versioning 2.0.0](http://semver.org/).
4
4
 
5
+ #### Release 0.4.0 (2026-09-29)
6
+
7
+ - ADDED: Expanded and tightened first-class parsing for current official WHOIS
8
+ hosts used by the pinned DomScan high-demand cohort. New parser examples
9
+ include `.biz`, `.click`, `.gift`, `.help`, `.homes`, `.ie`, `.org`, and
10
+ `.dk`; updated parser examples include `.mn`, `.cl`, `.eu`, `.giving`, `.id`,
11
+ `.mx`, `.love`, `.link`, `.icu`, `.lat`, `.to`, and `.open`. The audit tracks
12
+ all 153 suffixes with more than five observed endpoint requests (44,402
13
+ request items), with 94 registered/absence regression pairs and no
14
+ parser-pending rows. Restricted registry cases use minimal synthetic
15
+ fixtures. The audit keeps RDAP-only, retired, unsupported, and restricted
16
+ cases as separate outcomes; see the [full audit](docs/audits/domscan-whois-usage-2026-09-28.md)
17
+ for per-suffix evidence and route limitations.
18
+ - SAFETY: Registration and absence require exact registry evidence. Denied,
19
+ throttled, malformed, incomplete, or contradictory responses do not become
20
+ positive registration or availability results. Unknown and unsupported
21
+ responses remain distinct from authoritative absence.
22
+ - CLARIFIED: `.uk` remains a temporary WHOIS exception based on Nominet's
23
+ guidance that `whois.nic.uk` continues through 2027-02-09, despite IANA's
24
+ RDAP-only listing. The release tests current registered and absence response
25
+ shapes; DomScan's audited 30-day snapshot contained no traditional `.uk`
26
+ WHOIS calls.
27
+ - SAFETY: `.open` registration is restricted by the registry's
28
+ [registration policy](https://web.aexp-static.com/content/dam/amex/us/staticassets/pdf/nic/Registry-Policies-OPEN.pdf).
29
+ A synthetic complete-record test checks the exact `nic.open` registered
30
+ shape. `No Data Found` remains unknown, and this release makes no
31
+ public-availability claim for `.open`.
32
+ - TESTED: The integrated parser suite passes 7,001 examples with 0 failures;
33
+ the pinned-cohort coverage audit passes 3 examples with 0 failures. The
34
+ linked audit records the separate DomScan route checks and their local,
35
+ unreleased status.
36
+
5
37
  #### Release 0.3.2 (2026-09-25)
6
38
 
7
39
  - 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.0".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,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
@@ -3,6 +3,25 @@ require_relative 'base_top1000_icann'
3
3
  module Whois
4
4
  class Parsers
5
5
  class WhoisNicLove < BaseTop1000Icann
6
+ AVAILABLE_DOMAIN_LINE = /^>>> Domain (?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+love is available for registration[ \t]*$/i
7
+ RECORD_EVIDENCE = /^[ \t]*(?:Domain Name|Registry Domain ID|Creation Date|Registrar|Sponsoring Registrar|Domain Status|Name Server):[ \t]*\S/i
8
+
9
+ property_supported :available? do
10
+ available_marker_count == 1
11
+ end
12
+
13
+ def response_unavailable?
14
+ body = content_for_scanner
15
+ marker_count = available_marker_count
16
+
17
+ super || (marker_count.positive? && (marker_count != 1 || body.match?(RECORD_EVIDENCE)))
18
+ end
19
+
20
+ private
21
+
22
+ def available_marker_count
23
+ content_for_scanner.lines.count { |line| line.match?(AVAILABLE_DOMAIN_LINE) }
24
+ end
6
25
  end
7
26
  end
8
27
  end