site-inspector 3.1.0 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +34 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile +1 -1
  5. data/Guardfile +1 -1
  6. data/README.md +6 -1
  7. data/Rakefile +2 -2
  8. data/bin/site-inspector +15 -15
  9. data/lib/cliver/dependency_ext.rb +21 -0
  10. data/lib/site-inspector.rb +13 -11
  11. data/lib/site-inspector/checks/accessibility.rb +27 -17
  12. data/lib/site-inspector/checks/check.rb +1 -3
  13. data/lib/site-inspector/checks/content.rb +6 -6
  14. data/lib/site-inspector/checks/cookies.rb +6 -8
  15. data/lib/site-inspector/checks/dns.rb +21 -20
  16. data/lib/site-inspector/checks/headers.rb +12 -13
  17. data/lib/site-inspector/checks/hsts.rb +8 -9
  18. data/lib/site-inspector/checks/https.rb +3 -5
  19. data/lib/site-inspector/checks/sniffer.rb +8 -9
  20. data/lib/site-inspector/domain.rb +28 -32
  21. data/lib/site-inspector/endpoint.rb +31 -32
  22. data/lib/site-inspector/version.rb +1 -1
  23. data/script/cibuild +3 -1
  24. data/script/pa11y-version +9 -0
  25. data/site-inspector.gemspec +25 -25
  26. data/spec/checks/site_inspector_endpoint_accessibility_spec.rb +31 -30
  27. data/spec/checks/site_inspector_endpoint_check_spec.rb +10 -11
  28. data/spec/checks/site_inspector_endpoint_content_spec.rb +43 -44
  29. data/spec/checks/site_inspector_endpoint_cookies_spec.rb +30 -31
  30. data/spec/checks/site_inspector_endpoint_dns_spec.rb +72 -77
  31. data/spec/checks/site_inspector_endpoint_headers_spec.rb +26 -27
  32. data/spec/checks/site_inspector_endpoint_hsts_spec.rb +26 -27
  33. data/spec/checks/site_inspector_endpoint_https_spec.rb +11 -12
  34. data/spec/checks/site_inspector_endpoint_sniffer_spec.rb +56 -57
  35. data/spec/site_inspector_cache_spec.rb +6 -6
  36. data/spec/site_inspector_disk_cache_spec.rb +9 -9
  37. data/spec/site_inspector_domain_spec.rb +132 -136
  38. data/spec/site_inspector_endpoint_spec.rb +108 -108
  39. data/spec/site_inspector_spec.rb +17 -18
  40. data/spec/spec_helper.rb +3 -3
  41. metadata +21 -3
@@ -4,15 +4,15 @@ class SiteInspector
4
4
  class LocalhostError < StandardError; end
5
5
 
6
6
  def self.resolver
7
- require "dnsruby"
7
+ require 'dnsruby'
8
8
  @resolver ||= begin
9
9
  resolver = Dnsruby::Resolver.new
10
- resolver.config.nameserver = ["8.8.8.8", "8.8.4.4"]
10
+ resolver.config.nameserver = ['8.8.8.8', '8.8.4.4']
11
11
  resolver
12
12
  end
13
13
  end
14
14
 
15
- def query(type="ANY")
15
+ def query(type = 'ANY')
16
16
  SiteInspector::Endpoint::Dns.resolver.query(host.to_s, type).answer
17
17
  rescue Dnsruby::ResolvTimeout, Dnsruby::ServFail, Dnsruby::NXDomain
18
18
  []
@@ -22,20 +22,21 @@ class SiteInspector
22
22
  @records ||= query
23
23
  end
24
24
 
25
- def has_record?(type)
25
+ def record?(type)
26
26
  records.any? { |record| record.type == type } || query(type).count != 0
27
27
  end
28
+ alias_method :has_record?, :record?
28
29
 
29
30
  def dnssec?
30
- @dnssec ||= has_record? "DNSKEY"
31
+ @dnssec ||= has_record? 'DNSKEY'
31
32
  end
32
33
 
33
34
  def ipv6?
34
- @ipv6 ||= has_record? "AAAA"
35
+ @ipv6 ||= has_record? 'AAAA'
35
36
  end
36
37
 
37
38
  def cdn
38
- detect_by_hostname "cdn"
39
+ detect_by_hostname 'cdn'
39
40
  end
40
41
 
41
42
  def cdn?
@@ -43,7 +44,7 @@ class SiteInspector
43
44
  end
44
45
 
45
46
  def cloud_provider
46
- detect_by_hostname "cloud"
47
+ detect_by_hostname 'cloud'
47
48
  end
48
49
 
49
50
  def cloud?
@@ -52,7 +53,7 @@ class SiteInspector
52
53
 
53
54
  def google_apps?
54
55
  @google ||= records.any? do |record|
55
- record.type == "MX" && record.exchange.to_s =~ /google(mail)?\.com\.?$/
56
+ record.type == 'MX' && record.exchange.to_s =~ /google(mail)?\.com\.?\z/i
56
57
  end
57
58
  end
58
59
 
@@ -75,7 +76,7 @@ class SiteInspector
75
76
  end
76
77
 
77
78
  def cnames
78
- @cnames ||= records.select { |record| record.type == "CNAME" }.map do |record|
79
+ @cnames ||= records.select { |record| record.type == 'CNAME' }.map do |record|
79
80
  PublicSuffix.parse(record.cname.to_s)
80
81
  end
81
82
  end
@@ -85,15 +86,15 @@ class SiteInspector
85
86
  end
86
87
 
87
88
  def to_h
88
- return { :error => LocalhostError } if localhost?
89
+ return { error: LocalhostError } if localhost?
89
90
  {
90
- :dnssec => dnssec?,
91
- :ipv6 => ipv6?,
92
- :cdn => cdn,
93
- :cloud_provider => cloud_provider,
94
- :google_apps => google_apps?,
95
- :hostname => hostname,
96
- :ip => ip
91
+ dnssec: dnssec?,
92
+ ipv6: ipv6?,
93
+ cdn: cdn,
94
+ cloud_provider: cloud_provider,
95
+ google_apps: google_apps?,
96
+ hostname: hostname,
97
+ ip: ip
97
98
  }
98
99
  end
99
100
 
@@ -115,7 +116,7 @@ class SiteInspector
115
116
 
116
117
  def detect_by_hostname(type)
117
118
  haystack = load_data(type)
118
- needle = haystack.find do |name, domain|
119
+ needle = haystack.find do |_name, domain|
119
120
  cnames.any? do |cname|
120
121
  domain == cname.tld || domain == "#{cname.sld}.#{cname.tld}"
121
122
  end
@@ -124,7 +125,7 @@ class SiteInspector
124
125
  return needle[0].to_sym if needle
125
126
  return nil unless hostname
126
127
 
127
- needle = haystack.find do |name, domain|
128
+ needle = haystack.find do |_name, domain|
128
129
  domain == hostname.tld || domain == "#{hostname.sld}.#{hostname.tld}"
129
130
  end
130
131
 
@@ -1,7 +1,6 @@
1
1
  class SiteInspector
2
2
  class Endpoint
3
3
  class Headers < Check
4
-
5
4
  # TODO: kill this
6
5
  def strict_transport_security?
7
6
  !!strict_transport_security
@@ -19,33 +18,33 @@ class SiteInspector
19
18
 
20
19
  # TODO: kill this
21
20
  def strict_transport_security
22
- headers["strict-transport-security"]
21
+ headers['strict-transport-security']
23
22
  end
24
23
 
25
24
  def content_security_policy
26
- headers["content-security-policy"]
25
+ headers['content-security-policy']
27
26
  end
28
27
 
29
28
  def click_jacking_protection
30
- headers["x-frame-options"]
29
+ headers['x-frame-options']
31
30
  end
32
31
 
33
32
  def server
34
- headers["server"]
33
+ headers['server']
35
34
  end
36
35
 
37
36
  def xss_protection
38
- headers["x-xss-protection"]
37
+ headers['x-xss-protection']
39
38
  end
40
39
 
41
40
  # more specific checks than presence of headers
42
41
  def xss_protection?
43
- xss_protection == "1; mode=block"
42
+ xss_protection == '1; mode=block'
44
43
  end
45
44
 
46
45
  # Returns an array of hashes of downcased key/value header pairs (or an empty hash)
47
46
  def all
48
- @all ||= (response && response.headers) ? Hash[response.headers.map{ |k,v| [k.downcase,v] }] : {}
47
+ @all ||= (response && response.headers) ? Hash[response.headers.map { |k, v| [k.downcase, v] }] : {}
49
48
  end
50
49
  alias_method :headers, :all
51
50
 
@@ -55,11 +54,11 @@ class SiteInspector
55
54
 
56
55
  def to_h
57
56
  {
58
- :strict_transport_security => strict_transport_security || false,
59
- :content_security_policy => content_security_policy || false,
60
- :click_jacking_protection => click_jacking_protection || false,
61
- :server => server,
62
- :xss_protection => xss_protection || false,
57
+ strict_transport_security: strict_transport_security || false,
58
+ content_security_policy: content_security_policy || false,
59
+ click_jacking_protection: click_jacking_protection || false,
60
+ server: server,
61
+ xss_protection: xss_protection || false
63
62
  }
64
63
  end
65
64
  end
@@ -3,7 +3,6 @@ class SiteInspector
3
3
  # Utility parser for HSTS headers.
4
4
  # RFC: http://tools.ietf.org/html/rfc6797
5
5
  class Hsts < Check
6
-
7
6
  def valid?
8
7
  return false unless header
9
8
  pairs.none? { |key, value| "#{key}#{value}" =~ /[\s\'\"]/ }
@@ -28,17 +27,17 @@ class SiteInspector
28
27
 
29
28
  # Google's minimum max-age for automatic preloading
30
29
  def preload_ready?
31
- include_subdomains? and preload? and max_age >= 10886400
30
+ include_subdomains? && preload? && max_age >= 10_886_400
32
31
  end
33
32
 
34
33
  def to_h
35
34
  {
36
- valid: valid?,
37
- max_age: max_age,
35
+ valid: valid?,
36
+ max_age: max_age,
38
37
  include_subdomains: include_subdomains?,
39
- preload: preload?,
40
- enabled: enabled?,
41
- preload_ready: preload_ready?
38
+ preload: preload?,
39
+ enabled: enabled?,
40
+ preload_ready: preload_ready?
42
41
  }
43
42
  end
44
43
 
@@ -49,7 +48,7 @@ class SiteInspector
49
48
  end
50
49
 
51
50
  def header
52
- @header ||= headers["strict-transport-security"]
51
+ @header ||= headers['strict-transport-security']
53
52
  end
54
53
 
55
54
  def directives
@@ -60,7 +59,7 @@ class SiteInspector
60
59
  @pairs ||= begin
61
60
  pairs = {}
62
61
  directives.each do |directive|
63
- key, value = directive.downcase.split("=")
62
+ key, value = directive.downcase.split('=')
64
63
 
65
64
  if value =~ /\".*\"/
66
65
  value = value.sub(/^\"/, '')
@@ -1,9 +1,8 @@
1
1
  class SiteInspector
2
2
  class Endpoint
3
3
  class Https < Check
4
-
5
4
  def scheme?
6
- scheme == "https"
5
+ scheme == 'https'
7
6
  end
8
7
 
9
8
  def valid?
@@ -24,8 +23,8 @@ class SiteInspector
24
23
 
25
24
  def to_h
26
25
  {
27
- valid: valid?,
28
- return_code: response.return_code,
26
+ valid: valid?,
27
+ return_code: response.return_code
29
28
  }
30
29
  end
31
30
 
@@ -34,7 +33,6 @@ class SiteInspector
34
33
  def scheme
35
34
  @scheme ||= request.base_url.scheme
36
35
  end
37
-
38
36
  end
39
37
  end
40
38
  end
@@ -1,7 +1,6 @@
1
1
  class SiteInspector
2
2
  class Endpoint
3
3
  class Sniffer < Check
4
-
5
4
  OPEN_SOURCE_FRAMEWORKS = [
6
5
  # Sniffles
7
6
  :drupal,
@@ -20,9 +19,9 @@ class SiteInspector
20
19
  cms = sniff :cms
21
20
  return cms unless cms.nil?
22
21
  return :expression_engine if endpoint.cookies.any? { |c| c.keys.first =~ /^exp_/ }
23
- return :php if endpoint.cookies["PHPSESSID"]
24
- return :coldfusion if endpoint.cookies["CFID"] && endpoint.cookies["CFTOKEN"]
25
- return :cowboy if endpoint.headers.server.to_s.downcase == "cowboy"
22
+ return :php if endpoint.cookies['PHPSESSID']
23
+ return :coldfusion if endpoint.cookies['CFID'] && endpoint.cookies['CFTOKEN']
24
+ return :cowboy if endpoint.headers.server.to_s.downcase == 'cowboy'
26
25
  nil
27
26
  end
28
27
 
@@ -44,10 +43,10 @@ class SiteInspector
44
43
 
45
44
  def to_h
46
45
  {
47
- :framework => framework,
48
- :analytics => analytics,
49
- :javascript => javascript,
50
- :advertising => advertising
46
+ framework: framework,
47
+ analytics: analytics,
48
+ javascript: javascript,
49
+ advertising: advertising
51
50
  }
52
51
  end
53
52
 
@@ -55,7 +54,7 @@ class SiteInspector
55
54
 
56
55
  def sniff(type)
57
56
  require 'sniffles'
58
- results = Sniffles.sniff(endpoint.content.body, type).select { |name, meta| meta[:found] }
57
+ results = Sniffles.sniff(endpoint.content.body, type).select { |_name, meta| meta[:found] }
59
58
  results.keys.first if results
60
59
  rescue
61
60
  nil
@@ -1,23 +1,22 @@
1
1
  class SiteInspector
2
2
  class Domain
3
-
4
3
  attr_reader :host
5
4
 
6
5
  def initialize(host)
7
6
  host = host.downcase
8
- host = host.sub /^https?\:/, ""
9
- host = host.sub /^\/+/, ""
10
- host = host.sub /^www\./, ""
7
+ host = host.sub(/^https?\:/, '')
8
+ host = host.sub(%r{^/+}, '')
9
+ host = host.sub(/^www\./, '')
11
10
  uri = Addressable::URI.parse "//#{host}"
12
11
  @host = uri.host
13
12
  end
14
13
 
15
14
  def endpoints
16
15
  @endpoints ||= [
17
- Endpoint.new("https://#{host}", :domain => self),
18
- Endpoint.new("https://www.#{host}", :domain => self),
19
- Endpoint.new("http://#{host}", :domain => self),
20
- Endpoint.new("http://www.#{host}", :domain => self)
16
+ Endpoint.new("https://#{host}", domain: self),
17
+ Endpoint.new("https://www.#{host}", domain: self),
18
+ Endpoint.new("http://#{host}", domain: self),
19
+ Endpoint.new("http://www.#{host}", domain: self)
21
20
  ]
22
21
  end
23
22
 
@@ -37,16 +36,15 @@ class SiteInspector
37
36
 
38
37
  # Does *any* endpoint return a 200 or 300 response code?
39
38
  def up?
40
- endpoints.any? { |e| e.up? }
39
+ endpoints.any?(&:up?)
41
40
  end
42
41
 
43
42
  # Does *any* endpoint respond to HTTP?
44
43
  # TODO: needs to allow an invalid chain.
45
44
  def responds?
46
- endpoints.any? { |e| e.responds? }
45
+ endpoints.any?(&:responds?)
47
46
  end
48
47
 
49
-
50
48
  # TODO: These weren't present before, and may not be useful.
51
49
  # Can you connect to www?
52
50
  def www?
@@ -84,7 +82,7 @@ class SiteInspector
84
82
  # TODO: don't need to require that the HTTPS cert is valid for this purpose.
85
83
  def enforces_https?
86
84
  return false unless https?
87
- endpoints.select { |e| e.http? }.all? { |e| !e.up? || (e.redirect && e.redirect.https?) }
85
+ endpoints.select(&:http?).all? { |e| !e.up? || (e.redirect && e.redirect.https?) }
88
86
  end
89
87
 
90
88
  # we can say that a canonical HTTPS site "defaults" to HTTPS,
@@ -93,7 +91,7 @@ class SiteInspector
93
91
  #
94
92
  # TODO: not implemented.
95
93
  def defaults_https?
96
- raise "Not implemented. Halp?"
94
+ fail 'Not implemented. Halp?'
97
95
  end
98
96
 
99
97
  # HTTPS is "downgraded" if both:
@@ -128,10 +126,10 @@ class SiteInspector
128
126
  return false unless www?
129
127
 
130
128
  # Are both root endpoints down?
131
- return true if endpoints.select { |e| e.root? }.all? { |e| !e.up? }
129
+ return true if endpoints.select(&:root?).all? { |e| !e.up? }
132
130
 
133
131
  # Does either root endpoint redirect to a www endpoint?
134
- endpoints.select { |e| e.root? }.any? { |e| e.redirect && e.redirect.www? }
132
+ endpoints.select(&:root?).any? { |e| e.redirect && e.redirect.www? }
135
133
  end
136
134
 
137
135
  # A domain is "canonically" at https if:
@@ -159,10 +157,10 @@ class SiteInspector
159
157
  return false unless https?
160
158
 
161
159
  # Both http endpoints are down
162
- return true if endpoints.select { |e| e.http? }.all? { |e| !e.up? }
160
+ return true if endpoints.select(&:http?).all? { |e| !e.up? }
163
161
 
164
162
  # at least one http endpoint redirects immediately to https
165
- endpoints.select { |e| e.http? }.any? { |e| e.redirect && e.redirect.https? }
163
+ endpoints.select(&:http?).any? { |e| e.redirect && e.redirect.https? }
166
164
  end
167
165
 
168
166
  # A domain redirects if
@@ -175,7 +173,7 @@ class SiteInspector
175
173
 
176
174
  # The first endpoint to respond with a redirect
177
175
  def redirect
178
- endpoints.find { |e| e.external_redirect? }
176
+ endpoints.find(&:external_redirect?)
179
177
  end
180
178
 
181
179
  # HSTS on the canonical domain?
@@ -223,7 +221,7 @@ class SiteInspector
223
221
  # :all - return information about all endpoints
224
222
  #
225
223
  # Returns a complete hash of the domain's information
226
- def to_h(options={})
224
+ def to_h(options = {})
227
225
  prefetch
228
226
 
229
227
  hash = {
@@ -244,19 +242,17 @@ class SiteInspector
244
242
  canonical_endpoint: canonical_endpoint.to_h(options)
245
243
  }
246
244
 
247
- if options["all"]
248
- hash.merge!({
249
- endpoints: {
250
- https: {
251
- root: endpoints[0].to_h(options),
252
- www: endpoints[1].to_h(options)
253
- },
254
- http: {
255
- root: endpoints[2].to_h(options),
256
- www: endpoints[3].to_h(options)
257
- }
258
- }
259
- })
245
+ if options['all']
246
+ hash.merge!(endpoints: {
247
+ https: {
248
+ root: endpoints[0].to_h(options),
249
+ www: endpoints[1].to_h(options)
250
+ },
251
+ http: {
252
+ root: endpoints[2].to_h(options),
253
+ www: endpoints[3].to_h(options)
254
+ }
255
+ })
260
256
  end
261
257
 
262
258
  hash
@@ -17,12 +17,12 @@ class SiteInspector
17
17
  # endpoint - (string) the endpoint to query (e.g., `https://example.com`)
18
18
  # options - A hash of options
19
19
  # domain - the parent domain object, if passed, facilitates caching of redirects
20
- def initialize(host, options={})
20
+ def initialize(host, options = {})
21
21
  @uri = Addressable::URI.parse(host.downcase)
22
22
  # The root URL always has an implict path of "/", even if not requested
23
23
  # Make it explicit to facilitate caching and prevent a potential redirect
24
- @uri.path = "/"
25
- @host = uri.host.sub(/^www\./, "")
24
+ @uri.path = '/'
25
+ @host = uri.host.sub(/^www\./, '')
26
26
  @checks = {}
27
27
  @domain = options[:domain]
28
28
  end
@@ -72,24 +72,24 @@ class SiteInspector
72
72
 
73
73
  # Does the endpoint return a 2xx or 3xx response code?
74
74
  def up?
75
- response && response_code.start_with?("2") || response_code.start_with?("3")
75
+ response && response_code.start_with?('2') || response_code.start_with?('3')
76
76
  end
77
77
 
78
78
  # Does the server respond at all?
79
79
  def responds?
80
- response.code != 0 && !timed_out?
80
+ response.code != 0 && !timed_out?
81
81
  end
82
82
 
83
83
  # If the domain is a redirect, what's the first endpoint we're redirected to?
84
84
  def redirect
85
- return unless response && response_code.start_with?("3")
85
+ return unless response && response_code.start_with?('3')
86
86
 
87
87
  @redirect ||= begin
88
- redirect = Addressable::URI.parse(headers["location"])
88
+ redirect = Addressable::URI.parse(headers['location'])
89
89
 
90
90
  # This is a relative redirect, but we still need the absolute URI
91
91
  if redirect.relative?
92
- redirect.path = "/#{redirect.path}" unless redirect.path[0] == "/"
92
+ redirect.path = "/#{redirect.path}" unless redirect.path[0] == '/'
93
93
  redirect.host = host
94
94
  redirect.scheme = scheme
95
95
  end
@@ -116,12 +116,12 @@ class SiteInspector
116
116
  return redirect unless redirect.redirect?
117
117
 
118
118
  @resolves_to ||= begin
119
- response = request(:followlocation => true)
119
+ response = request(followlocation: true)
120
120
 
121
121
  # Workaround for Webmock not playing nicely with Typhoeus redirects
122
122
  if response.mock?
123
- if response.headers["Location"]
124
- url = response.headers["Location"]
123
+ if response.headers['Location']
124
+ url = response.headers['Location']
125
125
  else
126
126
  url = response.request.url
127
127
  end
@@ -142,7 +142,7 @@ class SiteInspector
142
142
  end
143
143
 
144
144
  def inspect
145
- "#<SiteInspector::Endpoint uri=\"#{uri.to_s}\">"
145
+ "#<SiteInspector::Endpoint uri=\"#{uri}\">"
146
146
  end
147
147
 
148
148
  # Returns information about the endpoint
@@ -154,37 +154,39 @@ class SiteInspector
154
154
  # a hash of check symbols and bools representing which checks should be run
155
155
  #
156
156
  # Returns the hash representing the endpoint and its checks
157
- def to_h(options={})
157
+ def to_h(options = {})
158
158
  hash = {
159
- uri: uri.to_s,
160
- host: host,
161
- www: www?,
162
- https: https?,
163
- scheme: scheme,
164
- up: up?,
165
- responds: responds?,
166
- timed_out: timed_out?,
167
- redirect: redirect?,
168
- external_redirect: external_redirect?,
159
+ uri: uri.to_s,
160
+ host: host,
161
+ www: www?,
162
+ https: https?,
163
+ scheme: scheme,
164
+ up: up?,
165
+ responds: responds?,
166
+ timed_out: timed_out?,
167
+ redirect: redirect?,
168
+ external_redirect: external_redirect?
169
169
  }
170
170
 
171
171
  # Either they've specifically asked for a check, or we throw everything at them
172
172
  checks = SiteInspector::Endpoint.checks.select { |c| options.keys.include?(c.name) }
173
173
  checks = SiteInspector::Endpoint.checks if checks.empty?
174
174
 
175
- Parallel.each(checks, :in_threads => 4) do |check|
176
- hash[check.name] = self.send(check.name).to_h
175
+ Parallel.each(checks, in_threads: 4) do |check|
176
+ hash[check.name] = send(check.name).to_h
177
177
  end
178
178
 
179
179
  hash
180
180
  end
181
181
 
182
182
  def self.checks
183
- ObjectSpace.each_object(Class).select { |klass| klass < Check }.select { |check| check.enabled? }
183
+ return @checks if defined? @checks
184
+ @checks = ObjectSpace.each_object(Class).select { |klass| klass < Check }.select(&:enabled?)
184
185
  end
185
186
 
186
187
  def method_missing(method_sym, *arguments, &block)
187
- if check = SiteInspector::Endpoint.checks.find { |c| c.name == method_sym }
188
+ check = SiteInspector::Endpoint.checks.find { |c| c.name == method_sym }
189
+ if check
188
190
  @checks[method_sym] ||= check.new(self)
189
191
  else
190
192
  super
@@ -209,11 +211,8 @@ class SiteInspector
209
211
  # Try to return the existing endpoint, rather than create a new one
210
212
  def find_or_create_by_uri(uri)
211
213
  uri = Addressable::URI.parse(uri.downcase)
212
- if domain && cached_endpoint = domain.endpoints.find { |e| e.uri.to_s == uri.to_s }
213
- cached_endpoint
214
- else
215
- Endpoint.new(uri.to_s)
216
- end
214
+ cached_endpoint = domain.endpoints.find { |e| e.uri.to_s == uri.to_s } if domain
215
+ cached_endpoint || Endpoint.new(uri.to_s)
217
216
  end
218
217
  end
219
218
  end