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
@@ -0,0 +1,51 @@
1
+ #--
2
+ # Ruby Whois
3
+ #
4
+ # An intelligent pure Ruby WHOIS client and parser.
5
+ #++
6
+
7
+ require_relative 'base_icann_compliant'
8
+
9
+ module Whois
10
+ class Parsers
11
+ # Parser for the IANA-listed .mn WHOIS host, whois.nic.mn.
12
+ class WhoisNicMn < BaseIcannCompliant
13
+ self.scanner = Scanners::BaseIcannCompliant, {
14
+ pattern_available: /^[ \t]*Domain not found\.[ \t\r]*$/i,
15
+ }
16
+
17
+ property_supported :status do
18
+ classify_status
19
+ end
20
+
21
+ property_supported :available? do
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
+ missing = content_for_scanner.match?(/\A[ \t]*Domain not found\.[ \t\r]*(?:\n|\z)/i)
34
+ denial = content_for_scanner.match?(/^[ \t]*(?:[%#][ \t]*)?(?:access denied|permission denied|request denied|forbidden)\b/i)
35
+ domains = content_for_scanner.scan(/^[ \t]*Domain Name:[ \t]*((?:[a-z0-9-]+\.)+mn)[ \t\r]*$/i).flatten.uniq
36
+ has_registrar = content_for_scanner.match?(/^[ \t]*Registrar:[ \t]*\S/i)
37
+ has_record_detail = content_for_scanner.match?(/^[ \t]*(?:Creation Date|Updated Date|Registry Expiry Date|Name Server):[ \t]*\S/i)
38
+ has_record = domains.one? && has_registrar && has_record_detail
39
+
40
+ if missing && domains.empty? && !denial && !response_throttled?
41
+ :available
42
+ elsif has_record && !missing && !denial && !response_throttled?
43
+ :registered
44
+ else
45
+ :unknown
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ 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
@@ -25,21 +26,22 @@ module Whois
25
26
  # and examples.
26
27
  #
27
28
  class WhoisNicMx < Base
29
+ include RegistryResponseSafety
30
+
31
+ DOMAIN_PATTERN = /\A(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+mx\z/i
32
+ NO_RECORD_MARKER = %r{^[ \t]*No_Se_Encontro_El_Objeto/Object_Not_Found[ \t]*$}i
33
+ RECORD_EVIDENCE = /^[ \t]*(?:Created On|Expiration Date|Registrar|Name Servers|DNS):/i
28
34
 
29
35
  property_supported :status do
30
- if available?
31
- :available
32
- else
33
- :registered
34
- end
36
+ classify_status
35
37
  end
36
38
 
37
39
  property_supported :available? do
38
- !!(content_for_scanner =~ /Object_Not_Found/)
40
+ classify_status == :available
39
41
  end
40
42
 
41
43
  property_supported :registered? do
42
- !available?
44
+ classify_status == :registered
43
45
  end
44
46
 
45
47
 
@@ -73,6 +75,44 @@ module Whois
73
75
  end
74
76
  end
75
77
 
78
+ def response_unavailable?
79
+ super || content_for_scanner.match?(
80
+ /^\s*(?:access denied|permission denied|request denied|forbidden)\b/i
81
+ )
82
+ end
83
+
84
+ private
85
+
86
+ def classify_status
87
+ cached_properties_fetch(:classified_status) do
88
+ response = content_for_scanner
89
+ absent = response.lines.one? { |line| line.match?(NO_RECORD_MARKER) }
90
+ domain_rows = response.scan(/^[ \t]*Domain Name:[ \t]*(.*?)[ \t\r]*$/i).flatten
91
+ domains = domain_rows.grep(DOMAIN_PATTERN)
92
+ has_created = response.match?(/^[ \t]*Created On:[ \t]*\S/i)
93
+ has_expires = response.match?(/^[ \t]*Expiration Date:[ \t]*\S/i)
94
+ has_registrar = response.match?(/^[ \t]*Registrar:[ \t]*\S/i)
95
+ registered = [
96
+ domain_rows.one?,
97
+ domains.one?,
98
+ has_created,
99
+ has_expires,
100
+ has_registrar,
101
+ !response_unavailable?,
102
+ !response_throttled?,
103
+ ].all?
104
+ has_record_evidence = response.match?(RECORD_EVIDENCE)
105
+
106
+ if absent && domain_rows.empty? && !has_record_evidence && !response_unavailable? && !response_throttled?
107
+ :available
108
+ elsif registered && !absent
109
+ :registered
110
+ else
111
+ :unknown
112
+ end
113
+ end
114
+ end
115
+
76
116
  end
77
117
 
78
118
  end
@@ -65,6 +65,12 @@ module Whois
65
65
  private
66
66
 
67
67
  def classify_status
68
+ cached_properties_fetch(:classified_status) do
69
+ classify_status_from_content
70
+ end
71
+ end
72
+
73
+ def classify_status_from_content
68
74
  registered = registered_domain_names
69
75
  absent = absence_domain_names
70
76
 
@@ -2,6 +2,8 @@ require_relative 'base_unsupported_registry'
2
2
 
3
3
  module Whois
4
4
  class Parsers
5
+ # IANA currently lists RDAP only for .news. Keep this adapter only so a
6
+ # legacy upstream WHOIS route fails explicitly instead of claiming status.
5
7
  class WhoisNicNews < BaseUnsupportedRegistry
6
8
  end
7
9
  end
@@ -8,7 +8,9 @@ module Whois
8
8
  # footer. Keep that exact evidence separate from an unparseable response.
9
9
  class WhoisNicOne < BaseIcannCompliant
10
10
  property_supported :status do
11
- if no_data_found?
11
+ if contradictory_registration_and_absence?
12
+ :unknown
13
+ elsif no_data_found?
12
14
  :available
13
15
  elsif registered_response?
14
16
  :registered
@@ -31,6 +33,10 @@ module Whois
31
33
  content_for_scanner.lines.first.to_s.strip.casecmp?("No Data Found")
32
34
  end
33
35
 
36
+ def contradictory_registration_and_absence?
37
+ no_data_found? && registered_response?
38
+ end
39
+
34
40
  def registered_response?
35
41
  content_for_scanner.match?(/^Domain Name:\s+\S+/) &&
36
42
  content_for_scanner.match?(/^Domain Status:\s+\S+/)
@@ -12,8 +12,25 @@ module Whois
12
12
 
13
13
  self.scanner = Scanners::BaseIcannCompliant
14
14
 
15
+ property_supported :status do
16
+ if contradictory_registration_and_absence?
17
+ :unknown
18
+ elsif available?
19
+ :available
20
+ elsif node('Domain Name').to_s.strip.empty? ||
21
+ [node('Registry Domain ID'), node('Creation Date'), node('Registrar')].all? { |value| value.to_s.strip.empty? }
22
+ :unknown
23
+ else
24
+ :registered
25
+ end
26
+ end
27
+
15
28
  property_supported :available? do
16
- !!(content_for_scanner =~ /^>>> Domain \S+ is available for registration\s*$/i)
29
+ !contradictory_registration_and_absence? && !!(content_for_scanner =~ /^>>> Domain \S+ is available for registration\s*$/i)
30
+ end
31
+
32
+ property_supported :registered? do
33
+ status == :registered
17
34
  end
18
35
 
19
36
  # Radix can answer with a limit or client-denial notice that contains no
@@ -33,6 +50,13 @@ module Whois
33
50
  node("Registry Expiry Date") { |value| parse_time(value) }
34
51
  end
35
52
 
53
+ private
54
+
55
+ def contradictory_registration_and_absence?
56
+ !node('Domain Name').to_s.strip.empty? &&
57
+ content_for_scanner.match?(/^>>> Domain \S+ is available for registration\s*$/i)
58
+ end
59
+
36
60
  end
37
61
 
38
62
  end
@@ -0,0 +1,95 @@
1
+ require_relative 'base_icann_compliant'
2
+
3
+ module Whois
4
+ class Parsers
5
+ # Conservative parser for American Express's restricted .OPEN registry.
6
+ # The registry's exact "No Data Found" response is not public availability:
7
+ # the published policy restricts registrations to the operator, qualified
8
+ # affiliates, and trademark licensees.
9
+ class WhoisNicOpen < BaseIcannCompliant
10
+ DOMAIN_PATTERN = /\A(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+open\z/i
11
+ DOMAIN_ID_PATTERN = /\AD\d+-OPEN\z/i
12
+ NO_DATA_MARKER = /^[ \t]*No Data Found[ \t]*$/i
13
+ RECORD_FIELD = /^[ \t]*(?:Domain Name|Registry Domain ID|Updated Date|Creation Date|Registry Expiry Date|Registrar|Registrar IANA ID|Domain Status|Name Server|DNSSEC):[ \t]*\S/i
14
+ REGISTRAR_NAME = /\AAmerican Express Travel Related Services, Inc\.\z/i
15
+ NAMESERVER_PATTERN = /\A(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.?\z/i
16
+
17
+ property_supported :domain do
18
+ values = field_values('Domain Name')
19
+ values.one? && values.first.match?(DOMAIN_PATTERN) ? values.first.downcase : nil
20
+ end
21
+
22
+ property_supported :domain_id do
23
+ values = field_values('Registry Domain ID')
24
+ values.one? && values.first.match?(DOMAIN_ID_PATTERN) ? values.first : nil
25
+ end
26
+
27
+ property_supported :status do
28
+ classify_status
29
+ end
30
+
31
+ property_supported :available? do
32
+ classify_status == :available
33
+ end
34
+
35
+ property_supported :registered? do
36
+ classify_status == :registered
37
+ end
38
+
39
+ property_supported :expires_on do
40
+ values = field_values('Registry Expiry Date')
41
+ parse_time(values.first) if values.one?
42
+ end
43
+
44
+ def response_unavailable?
45
+ body = content_for_scanner
46
+ marker_count = body.lines.count { |line| line.match?(NO_DATA_MARKER) }
47
+
48
+ super || (marker_count.positive? && (marker_count != 1 || body.match?(RECORD_FIELD)))
49
+ end
50
+
51
+ private
52
+
53
+ def classify_status
54
+ cached_properties_fetch(:classified_status) do
55
+ complete_registered_record? ? :registered : :unknown
56
+ end
57
+ end
58
+
59
+ def complete_registered_record?
60
+ domains = field_values('Domain Name')
61
+ ids = field_values('Registry Domain ID')
62
+ registrars = field_values('Registrar')
63
+ registrar_ids = field_values('Registrar IANA ID')
64
+
65
+ domains.one? && domains.first.match?(DOMAIN_PATTERN) &&
66
+ ids.one? && ids.first.match?(DOMAIN_ID_PATTERN) &&
67
+ registrars.one? && registrars.first.match?(REGISTRAR_NAME) &&
68
+ registrar_ids == ['9999'] &&
69
+ valid_date_field?('Creation Date') &&
70
+ valid_date_field?('Updated Date') &&
71
+ valid_date_field?('Registry Expiry Date') &&
72
+ valid_nameservers? && field_values('Domain Status').any?
73
+ end
74
+
75
+ def valid_date_field?(name)
76
+ values = field_values(name)
77
+ return false unless values.one?
78
+
79
+ Time.iso8601(values.first)
80
+ true
81
+ rescue ArgumentError
82
+ false
83
+ end
84
+
85
+ def valid_nameservers?
86
+ values = field_values('Name Server')
87
+ values.any? && values.all? { |value| value.match?(NAMESERVER_PATTERN) }
88
+ end
89
+
90
+ def field_values(name)
91
+ content_for_scanner.scan(/^[ \t]*#{Regexp.escape(name)}:[ \t]*(.*?)[ \t\r]*$/i).flatten
92
+ end
93
+ end
94
+ end
95
+ end
@@ -3,6 +3,23 @@ require_relative 'whois.centralnic.com'
3
3
  module Whois
4
4
  class Parsers
5
5
  class WhoisNicQuest < WhoisCentralnicCom
6
+ property_supported :available? do
7
+ !contradictory_registration_and_absence? && !!node('status:available')
8
+ end
9
+
10
+ property_supported :registered? do
11
+ !contradictory_registration_and_absence? &&
12
+ !available? && !Array.wrap(node('Status') || node('Domain Status')).empty? &&
13
+ !node('Domain Name').to_s.strip.empty? &&
14
+ Array.wrap(node('Status') || node('Domain Status')).none? { |value| value.to_s.strip.casecmp('unknown').zero? }
15
+ end
16
+
17
+ private
18
+
19
+ def contradictory_registration_and_absence?
20
+ !node('Domain Name').to_s.strip.empty? &&
21
+ content_for_scanner.match?(/^(?:DOMAIN NOT FOUND|The queried object does not exist: DOMAIN NOT FOUND|>>> Domain \S+ is available for registration)\s*$/i)
22
+ end
6
23
  end
7
24
  end
8
25
  end
@@ -3,6 +3,34 @@ module Whois
3
3
  class Parsers
4
4
  class WhoisNicSbs < BaseIdentityDigital
5
5
  self.scanner = Scanners::BaseIcannCompliant, { pattern_available: /^The queried object does not exist: DOMAIN NOT FOUND\s*$/i }
6
+
7
+ property_supported :status do
8
+ if contradictory_registration_and_absence?
9
+ :unknown
10
+ elsif available?
11
+ :available
12
+ elsif node('Domain Name').to_s.strip.empty? ||
13
+ [node('Registry Domain ID'), node('Creation Date'), node('Registrar')].all? { |value| value.to_s.strip.empty? }
14
+ :unknown
15
+ else
16
+ :registered
17
+ end
18
+ end
19
+
20
+ property_supported :available? do
21
+ !contradictory_registration_and_absence? && !!node('status:available')
22
+ end
23
+
24
+ property_supported :registered? do
25
+ status == :registered
26
+ end
27
+
28
+ private
29
+
30
+ def contradictory_registration_and_absence?
31
+ !node('Domain Name').to_s.strip.empty? &&
32
+ content_for_scanner.match?(/^The queried object does not exist: DOMAIN NOT FOUND\s*$/i)
33
+ end
6
34
  end
7
35
  end
8
36
  end
@@ -10,6 +10,34 @@ module Whois
10
10
  self.scanner = Scanners::BaseIcannCompliant, {
11
11
  pattern_available: /^>>> Domain \S+ is available for registration\s*$/i,
12
12
  }
13
+
14
+ property_supported :status do
15
+ if contradictory_registration_and_absence?
16
+ :unknown
17
+ elsif available?
18
+ :available
19
+ elsif node('Domain Name').to_s.strip.empty? ||
20
+ [node('Registry Domain ID'), node('Creation Date'), node('Registrar')].all? { |value| value.to_s.strip.empty? }
21
+ :unknown
22
+ else
23
+ :registered
24
+ end
25
+ end
26
+
27
+ property_supported :available? do
28
+ !contradictory_registration_and_absence? && !!node('status:available')
29
+ end
30
+
31
+ property_supported :registered? do
32
+ status == :registered
33
+ end
34
+
35
+ private
36
+
37
+ def contradictory_registration_and_absence?
38
+ !node('Domain Name').to_s.strip.empty? &&
39
+ content_for_scanner.match?(/^>>> Domain \S+ is available for registration\s*$/i)
40
+ end
13
41
  end
14
42
  end
15
43
  end
@@ -5,10 +5,32 @@ module Whois
5
5
  # The .top registry uses the shared ICANN record shape and returns the full
6
6
  # queried .top name in its exact no-record response.
7
7
  class WhoisNicTop < BaseTop1000Icann
8
+ RECORD_EVIDENCE = /^(?:Domain Name|Registry Domain ID|Creation Date|Registrar|Domain Status):[ \t]*\S/i
9
+ NO_RECORD_MARKER = /
10
+ ^The\ queried\ object\ does\ not\ exist:\s+
11
+ (?:DOMAIN\ NOT\ FOUND|no\ matching\ objects\ found|
12
+ (?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)*
13
+ [A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.top)
14
+ \s*$
15
+ /ix
16
+
8
17
  property_supported :available? do
9
- super() || content_for_scanner.match?(
10
- /^The queried object does not exist:\s+(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.top\s*$/i
11
- )
18
+ raise ResponseIsThrottled if response_throttled?
19
+ raise ResponseIsUnavailable if response_unavailable?
20
+
21
+ super() || content_for_scanner.match?(NO_RECORD_MARKER)
22
+ end
23
+
24
+ def response_unavailable?
25
+ body = content_for_scanner
26
+ ambiguous_absence = body.match?(NO_RECORD_MARKER) && 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)
12
34
  end
13
35
  end
14
36
  end
@@ -7,9 +7,30 @@ module Whois
7
7
  class WhoisNicTv < BaseIcannCompliant
8
8
  include RegistryResponseSafety
9
9
 
10
+ RECORD_EVIDENCE = /^(?:Domain Name|Registry Domain ID|Creation Date|Registrar|Domain Status):[ \t]*\S/i
11
+
10
12
  self.scanner = Scanners::BaseIcannCompliant, {
11
13
  pattern_available: /^No Data Found\n/,
12
14
  }
15
+
16
+ property_supported :available? do
17
+ raise ResponseIsThrottled if response_throttled?
18
+ raise ResponseIsUnavailable if response_unavailable?
19
+
20
+ super()
21
+ end
22
+
23
+ def response_unavailable?
24
+ body = content_for_scanner
25
+ ambiguous_absence = body.match?(/^No Data Found\s*$/i) && body.match?(RECORD_EVIDENCE)
26
+
27
+ super || body.match?(/^\s*access denied\b/i) ||
28
+ ambiguous_absence
29
+ end
30
+
31
+ def response_throttled?
32
+ super || content_for_scanner.match?(/^\s*too many requests\b/i)
33
+ end
13
34
  end
14
35
  end
15
36
  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
@@ -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