new_cms_scanner 0.13.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +26 -0
  4. data/app/app.rb +24 -0
  5. data/app/controllers/core/cli_options.rb +117 -0
  6. data/app/controllers/core.rb +82 -0
  7. data/app/controllers/interesting_findings.rb +25 -0
  8. data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
  9. data/app/finders/interesting_findings/headers.rb +17 -0
  10. data/app/finders/interesting_findings/robots_txt.rb +20 -0
  11. data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
  12. data/app/finders/interesting_findings/xml_rpc.rb +61 -0
  13. data/app/finders/interesting_findings.rb +25 -0
  14. data/app/formatters/cli.rb +65 -0
  15. data/app/formatters/cli_no_color.rb +9 -0
  16. data/app/formatters/cli_no_colour.rb +17 -0
  17. data/app/formatters/json.rb +14 -0
  18. data/app/models/fantastico_fileslist.rb +34 -0
  19. data/app/models/headers.rb +44 -0
  20. data/app/models/interesting_finding.rb +48 -0
  21. data/app/models/robots_txt.rb +31 -0
  22. data/app/models/search_replace_db_2.rb +17 -0
  23. data/app/models/user.rb +35 -0
  24. data/app/models/version.rb +49 -0
  25. data/app/models/xml_rpc.rb +78 -0
  26. data/app/user_agents.txt +46 -0
  27. data/app/views/cli/core/banner.erb +1 -0
  28. data/app/views/cli/core/finished.erb +8 -0
  29. data/app/views/cli/core/help.erb +4 -0
  30. data/app/views/cli/core/started.erb +6 -0
  31. data/app/views/cli/core/version.erb +1 -0
  32. data/app/views/cli/interesting_findings/_array.erb +10 -0
  33. data/app/views/cli/interesting_findings/findings.erb +23 -0
  34. data/app/views/cli/scan_aborted.erb +5 -0
  35. data/app/views/cli/usage.erb +3 -0
  36. data/app/views/json/core/banner.erb +1 -0
  37. data/app/views/json/core/finished.erb +10 -0
  38. data/app/views/json/core/help.erb +4 -0
  39. data/app/views/json/core/started.erb +5 -0
  40. data/app/views/json/core/version.erb +1 -0
  41. data/app/views/json/interesting_findings/findings.erb +24 -0
  42. data/app/views/json/scan_aborted.erb +5 -0
  43. data/lib/cms_scanner/browser/actions.rb +48 -0
  44. data/lib/cms_scanner/browser/options.rb +90 -0
  45. data/lib/cms_scanner/browser.rb +96 -0
  46. data/lib/cms_scanner/cache/file_store.rb +77 -0
  47. data/lib/cms_scanner/cache/typhoeus.rb +25 -0
  48. data/lib/cms_scanner/controller.rb +105 -0
  49. data/lib/cms_scanner/controllers.rb +67 -0
  50. data/lib/cms_scanner/errors/http.rb +72 -0
  51. data/lib/cms_scanner/errors/scan.rb +14 -0
  52. data/lib/cms_scanner/errors.rb +11 -0
  53. data/lib/cms_scanner/exit_code.rb +25 -0
  54. data/lib/cms_scanner/finders/base_finders.rb +45 -0
  55. data/lib/cms_scanner/finders/finder/breadth_first_dictionary_attack.rb +121 -0
  56. data/lib/cms_scanner/finders/finder/enumerator.rb +77 -0
  57. data/lib/cms_scanner/finders/finder/fingerprinter.rb +48 -0
  58. data/lib/cms_scanner/finders/finder/smart_url_checker/findings.rb +33 -0
  59. data/lib/cms_scanner/finders/finder/smart_url_checker.rb +60 -0
  60. data/lib/cms_scanner/finders/finder.rb +75 -0
  61. data/lib/cms_scanner/finders/finding.rb +54 -0
  62. data/lib/cms_scanner/finders/findings.rb +26 -0
  63. data/lib/cms_scanner/finders/independent_finder.rb +30 -0
  64. data/lib/cms_scanner/finders/independent_finders.rb +26 -0
  65. data/lib/cms_scanner/finders/same_type_finder.rb +19 -0
  66. data/lib/cms_scanner/finders/same_type_finders.rb +26 -0
  67. data/lib/cms_scanner/finders/unique_finder.rb +19 -0
  68. data/lib/cms_scanner/finders/unique_finders.rb +47 -0
  69. data/lib/cms_scanner/finders.rb +12 -0
  70. data/lib/cms_scanner/formatter/buffer.rb +17 -0
  71. data/lib/cms_scanner/formatter.rb +149 -0
  72. data/lib/cms_scanner/helper.rb +7 -0
  73. data/lib/cms_scanner/numeric.rb +13 -0
  74. data/lib/cms_scanner/parsed_cli.rb +37 -0
  75. data/lib/cms_scanner/progressbar_null_output.rb +23 -0
  76. data/lib/cms_scanner/public_suffix/domain.rb +42 -0
  77. data/lib/cms_scanner/references.rb +132 -0
  78. data/lib/cms_scanner/scan.rb +88 -0
  79. data/lib/cms_scanner/target/hashes.rb +45 -0
  80. data/lib/cms_scanner/target/platform/php.rb +62 -0
  81. data/lib/cms_scanner/target/platform.rb +3 -0
  82. data/lib/cms_scanner/target/scope.rb +103 -0
  83. data/lib/cms_scanner/target/server/apache.rb +27 -0
  84. data/lib/cms_scanner/target/server/generic.rb +72 -0
  85. data/lib/cms_scanner/target/server/iis.rb +29 -0
  86. data/lib/cms_scanner/target/server/nginx.rb +27 -0
  87. data/lib/cms_scanner/target/server.rb +6 -0
  88. data/lib/cms_scanner/target.rb +124 -0
  89. data/lib/cms_scanner/typhoeus/hydra.rb +12 -0
  90. data/lib/cms_scanner/typhoeus/response.rb +27 -0
  91. data/lib/cms_scanner/version.rb +6 -0
  92. data/lib/cms_scanner/vulnerability.rb +46 -0
  93. data/lib/cms_scanner/web_site.rb +145 -0
  94. data/lib/cms_scanner.rb +141 -0
  95. metadata +426 -0
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ # Scope system logic
5
+ class Target < WebSite
6
+ # @return [ Array<PublicSuffix::Domain, String> ]
7
+ def scope
8
+ @scope ||= Scope.new
9
+ end
10
+
11
+ # @param [ String, Addressable::URI ] url An absolute URL or URI
12
+ #
13
+ # @return [ Boolean ] true if the url given is in scope
14
+ def in_scope?(url_or_uri)
15
+ url_or_uri = Addressable::URI.parse(url_or_uri.strip) unless url_or_uri.is_a?(Addressable::URI)
16
+
17
+ scope.include?(url_or_uri.host)
18
+ rescue StandardError
19
+ false
20
+ end
21
+
22
+ # @param [ Typhoeus::Response ] res
23
+ # @param [ String ] xpath
24
+ #
25
+ # @yield [ Addressable::URI, Nokogiri::XML::Element ] The in scope url and its associated tag
26
+ #
27
+ # @return [ Array<Addressable::URI> ] The in scope absolute URIs detected in the response's body
28
+ #
29
+ # @note It is highly recommended to use the xpath parameter to focus on the uris needed, as this method can be quite
30
+ # time consuming when there are a lof of uris to check
31
+ def in_scope_uris(res, xpath = '//@href|//@src|//@data-src')
32
+ found = []
33
+
34
+ uris_from_page(res, xpath) do |uri, tag|
35
+ next unless in_scope?(uri)
36
+
37
+ yield uri, tag if block_given?
38
+
39
+ found << uri
40
+ end
41
+
42
+ found
43
+ end
44
+
45
+ # Similar to Target#url_pattern but considering the in scope domains as well
46
+ #
47
+ # @return [ Regexp ] The pattern related to the target url and in scope domains,
48
+ # it also matches escaped /, such as in JSON JS data: http:\/\/t.com\/
49
+ # rubocop:disable Metrics/AbcSize
50
+ def scope_url_pattern
51
+ return @scope_url_pattern if @scope_url_pattern
52
+
53
+ domains = [uri.host + uri.path]
54
+
55
+ domains += if scope.domains.empty?
56
+ Array(scope.invalid_domains[1..-1])
57
+ else
58
+ Array(scope.domains[1..-1]).map(&:to_s) + scope.invalid_domains
59
+ end
60
+
61
+ domains.map! { |d| Regexp.escape(d.delete_suffix('/')).gsub('\*', '.*').gsub('/', '\\\\\?/') }
62
+
63
+ domains[0].gsub!(Regexp.escape(uri.host), "#{Regexp.escape(uri.host)}(?::\\d+)?") if uri.port
64
+
65
+ @scope_url_pattern = %r{https?:\\?/\\?/(?:#{domains.join('|')})\\?/?}i
66
+ end
67
+ # rubocop:enable Metrics/AbcSize
68
+
69
+ # Scope Implementation
70
+ class Scope
71
+ # @return [ Array<PublicSuffix::Domain> ] The valid domains in scope
72
+ def domains
73
+ @domains ||= []
74
+ end
75
+
76
+ # @return [ Array<String> ] The invalid domains in scope (such as IP addresses etc)
77
+ def invalid_domains
78
+ @invalid_domains ||= []
79
+ end
80
+
81
+ def <<(element)
82
+ if PublicSuffix.valid?(element, ignore_private: true)
83
+ domains << PublicSuffix.parse(element, ignore_private: true)
84
+ else
85
+ invalid_domains << element
86
+ end
87
+ end
88
+
89
+ # @return [ Boolean ] Wether or not the host is in the scope
90
+ def include?(host)
91
+ if PublicSuffix.valid?(host, ignore_private: true)
92
+ domain = PublicSuffix.parse(host, ignore_private: true)
93
+
94
+ domains.each { |d| return true if domain.match(d) }
95
+ else
96
+ invalid_domains.each { |d| return true if host == d }
97
+ end
98
+
99
+ false
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ class Target < WebSite
5
+ module Server
6
+ # Some Apche specific implementation
7
+ module Apache
8
+ # @param [ String ] path
9
+ # @param [ Hash ] params The request params
10
+ #
11
+ # @return [ Symbol ] :Apache
12
+ def server(_path = nil, _params = {})
13
+ :Apache
14
+ end
15
+
16
+ # @param [ String ] path
17
+ # @param [ Hash ] params The request params
18
+ #
19
+ # @return [ Array<String> ] The first level of directories/files listed,
20
+ # or an empty array if none
21
+ def directory_listing_entries(path = nil, params = {})
22
+ super(path, params, 'td a')
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ class Target < WebSite
5
+ module Server
6
+ # Generic Server methods
7
+ module Generic
8
+ # @param [ String ] path
9
+ # @param [ Hash ] params The request params
10
+ #
11
+ # @return [ Symbol ] The detected remote server (:Apache, :IIS, :Nginx)
12
+ def server(path = nil, params = {})
13
+ headers = headers(path, params)
14
+
15
+ return unless headers
16
+
17
+ case headers[:server]
18
+ when /\Aapache/i
19
+ :Apache
20
+ when /\AMicrosoft-IIS/i
21
+ :IIS
22
+ when /\Anginx/
23
+ :Nginx
24
+ end
25
+ end
26
+
27
+ # @param [ String ] path
28
+ # @param [ Hash ] params The request params
29
+ #
30
+ # @return [ Hash ] The headers
31
+ def headers(path = nil, params = {})
32
+ # The HEAD method might be rejected by some servers ... maybe switch to GET ?
33
+ NS::Browser.head(url(path), params).headers
34
+ end
35
+
36
+ # @param [ String ] path
37
+ # @param [ Hash ] params The request params
38
+ #
39
+ # @return [ Boolean ] true if url(path) has the directory
40
+ # listing enabled, false otherwise
41
+ def directory_listing?(path = nil, params = {})
42
+ res = NS::Browser.get(url(path), params)
43
+
44
+ res.code == 200 && res.body.include?('<h1>Index of')
45
+ end
46
+
47
+ # @param [ String ] path
48
+ # @param [ Hash ] params The request params
49
+ # @param [ String ] selector
50
+ # @param [ Regexp ] ignore
51
+ #
52
+ # @return [ Array<String> ] The first level of directories/files listed,
53
+ # or an empty array if none
54
+ def directory_listing_entries(path = nil, params = {}, selector = 'pre a', ignore = /parent directory/i)
55
+ return [] unless directory_listing?(path, params)
56
+
57
+ found = []
58
+
59
+ NS::Browser.get(url(path), params).html.css(selector).each do |node|
60
+ entry = node.text.to_s
61
+
62
+ next if entry&.match?(ignore)
63
+
64
+ found << entry
65
+ end
66
+
67
+ found
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ class Target < WebSite
5
+ module Server
6
+ # Some IIS specific implementation
7
+ module IIS
8
+ # @param [ String ] path
9
+ # @param [ Hash ] params The request params
10
+ #
11
+ # @return [ Symbol ] :IIS
12
+ def server(_path = nil, _params = {})
13
+ :IIS
14
+ end
15
+
16
+ # @param [ String ] path
17
+ # @param [ Hash ] params The request params
18
+ #
19
+ # @return [ Boolean ] true if url(path) has the directory
20
+ # listing enabled, false otherwise
21
+ def directory_listing?(path = nil, params = {})
22
+ res = NS::Browser.get(url(path), params)
23
+
24
+ res.code == 200 && res.body =~ %r{<H1>#{uri.host} - /} ? true : false
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ class Target < WebSite
5
+ module Server
6
+ # Some Nginx specific implementation
7
+ module Nginx
8
+ # @param [ String ] path
9
+ # @param [ Hash ] params The request params
10
+ #
11
+ # @return [ Symbol ] :Nginx
12
+ def server(_path = nil, _params = {})
13
+ :Nginx
14
+ end
15
+
16
+ # @param [ String ] path
17
+ # @param [ Hash ] params The request params
18
+ #
19
+ # @return [ Array<String> ] The first level of directories/files listed,
20
+ # or an empty array if none
21
+ def directory_listing_entries(path = nil, params = {})
22
+ super(path, params, 'pre a', /\A\.\./i)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cms_scanner/target/server/generic'
4
+ require 'cms_scanner/target/server/apache'
5
+ require 'cms_scanner/target/server/iis'
6
+ require 'cms_scanner/target/server/nginx'
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cms_scanner/web_site'
4
+ require 'cms_scanner/target/platform'
5
+ require 'cms_scanner/target/server'
6
+ require 'cms_scanner/target/scope'
7
+ require 'cms_scanner/target/hashes'
8
+
9
+ module CMSScanner
10
+ # Target to Scan
11
+ class Target < WebSite
12
+ include Server::Generic
13
+
14
+ # @param [ String ] url
15
+ # @param [ Hash ] opts
16
+ # @option opts [ Array<PublicSuffix::Domain, String> ] :scope
17
+ def initialize(url, opts = {})
18
+ super(url, opts)
19
+
20
+ scope << uri.host
21
+ Array(opts[:scope]).each { |s| scope << s }
22
+ end
23
+
24
+ # @param [ Hash ] opts
25
+ #
26
+ # @return [ Findings ]
27
+ def interesting_findings(opts = {})
28
+ @interesting_findings ||= NS::Finders::InterestingFindings::Base.find(self, opts)
29
+ end
30
+
31
+ # Weteher or not vulnerabilities have been found.
32
+ # Used to set the exit code of the scanner
33
+ # and it should be overriden in the implementation
34
+ #
35
+ # @return [ Boolean ]
36
+ def vulnerable?
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # @return [ Regexp ] The pattern related to the target url, also matches escaped /, such as
41
+ # in JSON JS data: http:\/\/t.com\/
42
+ def url_pattern
43
+ @url_pattern ||= Regexp.new(Regexp.escape(url).gsub(/https?/i, 'https?').gsub('/', '\\\\\?/'), Regexp::IGNORECASE)
44
+ end
45
+
46
+ # @param [ String ] xpath
47
+ # @param [ Regexp ] pattern
48
+ # @param [ Typhoeus::Response, String ] page
49
+ #
50
+ # @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ]
51
+ # @yield [ MatchData, Nokogiri::XML::Element ]
52
+ def xpath_pattern_from_page(xpath, pattern, page = nil)
53
+ page = NS::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response)
54
+ matches = []
55
+
56
+ page.html.xpath(xpath).each do |node|
57
+ next unless node.text.strip =~ pattern
58
+
59
+ yield Regexp.last_match, node if block_given?
60
+
61
+ matches << [Regexp.last_match, node]
62
+ end
63
+
64
+ matches
65
+ end
66
+
67
+ # @param [ Regexp ] pattern
68
+ # @param [ Typhoeus::Response, String ] page
69
+ #
70
+ # @return [ Array<Array<MatchData, Nokogiri::XML::Comment>> ]
71
+ # @yield [ MatchData, Nokogiri::XML::Comment ]
72
+ def comments_from_page(pattern, page = nil)
73
+ xpath_pattern_from_page('//comment()', pattern, page) do |match, node|
74
+ yield match, node if block_given?
75
+ end
76
+ end
77
+
78
+ # @param [ Regexp ] pattern
79
+ # @param [ Typhoeus::Response, String ] page
80
+ #
81
+ # @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ]
82
+ # @yield [ MatchData, Nokogiri::XML::Element ]
83
+ def javascripts_from_page(pattern, page = nil)
84
+ xpath_pattern_from_page('//script', pattern, page) do |match, node|
85
+ yield match, node if block_given?
86
+ end
87
+ end
88
+
89
+ # @param [ Typhoeus::Response, String ] page
90
+ # @param [ String ] xpath
91
+ #
92
+ # @yield [ Addressable::URI, Nokogiri::XML::Element ] The url and its associated tag
93
+ #
94
+ # @return [ Array<Addressable::URI> ] The absolute URIs detected in the response's body from the HTML tags
95
+ #
96
+ # @note It is highly recommended to use the xpath parameter to focus on the uris needed, as this method can be quite
97
+ # time consuming when there are a lof of uris to check
98
+ def uris_from_page(page = nil, xpath = '//@href|//@src|//@data-src')
99
+ page = NS::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response)
100
+ found = []
101
+
102
+ page.html.xpath(xpath).each do |node|
103
+ attr_value = node.text.to_s
104
+
105
+ next unless attr_value && !attr_value.empty?
106
+
107
+ node_uri = begin
108
+ uri.join(attr_value.strip)
109
+ rescue StandardError
110
+ # Skip potential malformed URLs etc.
111
+ next
112
+ end
113
+
114
+ next unless node_uri.host
115
+
116
+ yield node_uri, node.parent if block_given? && !found.include?(node_uri)
117
+
118
+ found << node_uri
119
+ end
120
+
121
+ found.uniq
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typhoeus
4
+ # Ensure a clean abort of hydra
5
+ # See https://github.com/typhoeus/typhoeus/issues/439
6
+ class Hydra
7
+ def abort
8
+ super
9
+ run
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typhoeus
4
+ # Custom Response class
5
+ class Response
6
+ # @return [ Nokogiri::XML ] The response's body parsed by Nokogiri::HTML
7
+ def html
8
+ @html ||= Nokogiri::HTML(body.encode('UTF-8', invalid: :replace, undef: :replace))
9
+ end
10
+
11
+ # @return [ Nokogiri::XML ] The response's body parsed by Nokogiri::XML
12
+ def xml
13
+ @xml ||= Nokogiri::XML(body.encode('UTF-8', invalid: :replace, undef: :replace))
14
+ end
15
+
16
+ # Override of the original to ensure an integer is returned
17
+ # @return [ Integer ]
18
+ def request_size
19
+ super || 0
20
+ end
21
+
22
+ # @return [ Integer ]
23
+ def size
24
+ (body.nil? ? 0 : body.size) + (response_headers.nil? ? 0 : response_headers.size)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Version
4
+ module CMSScanner
5
+ VERSION = '0.13.7'
6
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ # Generic Vulnerability
5
+ class Vulnerability
6
+ include References
7
+
8
+ attr_reader :title, :type, :fixed_in, :introduced_in, :cvss
9
+
10
+ # @param [ String ] title
11
+ # @param [ Hash ] references
12
+ # @option references [ Array<String>, String ] :cve
13
+ # @option references [ Array<String>, String ] :secunia
14
+ # @option references [ Array<String>, String ] :osvdb
15
+ # @option references [ Array<String>, String ] :exploitdb
16
+ # @option references [ Array<String> ] :url URL(s) to related advisories etc
17
+ # @option references [ Array<String>, String ] :metasploit The related metasploit module(s)
18
+ # @option references [ Array<String> ] :youtube
19
+ # @param [ String ] type
20
+ # @param [ String ] fixed_in
21
+ # @param [ String ] introduced_in
22
+ # @param [ HashSymbol ] cvss
23
+ # @option cvss [ String ] :score
24
+ # @option cvss [ String ] :vector
25
+ def initialize(title, references: {}, type: nil, fixed_in: nil, introduced_in: nil, cvss: nil)
26
+ @title = title
27
+ @type = type
28
+ @fixed_in = fixed_in
29
+ @introduced_in = introduced_in
30
+ @cvss = { score: cvss[:score], vector: cvss[:vector] } if cvss
31
+
32
+ self.references = references
33
+ end
34
+
35
+ # param [ Vulnerability ] other
36
+ #
37
+ # @return [ Boolean ]
38
+ def ==(other)
39
+ title == other.title &&
40
+ type == other.type &&
41
+ references == other.references &&
42
+ fixed_in == other.fixed_in &&
43
+ cvss == other.cvss
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ # WebSite Implementation
5
+ class WebSite
6
+ attr_reader :uri, :opts
7
+
8
+ # @param [ String ] site_url
9
+ # @param [ Hash ] opts
10
+ def initialize(site_url, opts = {})
11
+ self.url = site_url
12
+ @opts = opts
13
+ end
14
+
15
+ def url=(site_url)
16
+ new_url = site_url.dup
17
+
18
+ # Add a trailing slash to the URL
19
+ new_url << '/' if new_url[-1, 1] != '/'
20
+
21
+ # Use the validator to ensure the URL has a correct format
22
+ OptParseValidator::OptURL.new([]).validate(new_url)
23
+
24
+ @uri = Addressable::URI.parse(new_url).normalize
25
+ end
26
+
27
+ # @param [ String ] path Optional path to merge with the uri
28
+ #
29
+ # @return [ String ]
30
+ def url(path = nil)
31
+ return @uri.to_s unless path
32
+
33
+ @uri.join(Addressable::URI.encode(path).gsub('#', '%23')).to_s
34
+ end
35
+
36
+ # @return [ String ] The IP address of the target
37
+ def ip
38
+ @ip ||= IPSocket.getaddress(uri.host)
39
+ rescue SocketError
40
+ 'Unknown'
41
+ end
42
+
43
+ attr_writer :homepage_res
44
+
45
+ # @return [ Typhoeus::Response ]
46
+ #
47
+ # As webmock does not support redirects mocking, coverage is ignored
48
+ # :nocov:
49
+ def homepage_res
50
+ @homepage_res ||= NS::Browser.get_and_follow_location(url)
51
+ end
52
+ # :nocov:
53
+
54
+ # @return [ String ]
55
+ def homepage_url
56
+ @homepage_url ||= homepage_res.effective_url
57
+ end
58
+
59
+ # @return [ Typhoeus::Response ]
60
+ def error_404_res
61
+ @error_404_res ||= NS::Browser.get_and_follow_location(error_404_url)
62
+ end
63
+
64
+ # @return [ String ] The URL of an unlikely existant page
65
+ def error_404_url
66
+ @error_404_url ||= uri.join("#{Digest::MD5.hexdigest(rand(999_999).to_s)[0..6]}.html").to_s
67
+ end
68
+
69
+ # Checks if the remote website is up.
70
+ #
71
+ # @param [ String ] path
72
+ #
73
+ # @return [ Boolean ]
74
+ def online?(path = nil)
75
+ NS::Browser.get(url(path)).code.nonzero? ? true : false
76
+ end
77
+
78
+ # @param [ String ] path
79
+ #
80
+ # @return [ Boolean ]
81
+ def http_auth?(path = nil)
82
+ NS::Browser.get(url(path)).code == 401
83
+ end
84
+
85
+ # @param [ String ] path
86
+ #
87
+ # @return [ Boolean ]
88
+ def access_forbidden?(path = nil)
89
+ NS::Browser.get(url(path)).code == 403
90
+ end
91
+
92
+ # @param [ String ] path
93
+ #
94
+ # @return [ Boolean ]
95
+ def proxy_auth?(path = nil)
96
+ NS::Browser.get(url(path)).code == 407
97
+ end
98
+
99
+ # @param [ String ] url
100
+ #
101
+ # @return [ String ] The redirection url or nil
102
+ #
103
+ # As webmock does not support redirects mocking, coverage is ignored
104
+ # :nocov:
105
+ def redirection(url = nil)
106
+ url ||= @uri.to_s
107
+
108
+ return unless [301, 302].include?(NS::Browser.get(url).code)
109
+
110
+ res = NS::Browser.get(url, followlocation: true, maxredirs: 10)
111
+
112
+ res.effective_url == url ? nil : res.effective_url
113
+ end
114
+ # :nocov:
115
+
116
+ # @return [ Hash ] The Typhoeus params to use to perform head requests
117
+ def head_or_get_params
118
+ @head_or_get_params ||= if NS::Browser.head(homepage_url).code == 405
119
+ { method: :get, maxfilesize: 1 }
120
+ else
121
+ { method: :head }
122
+ end
123
+ end
124
+
125
+ # Perform a HEAD request to the path provided, then if its response code
126
+ # is in the array of codes given, a GET is done and the response returned. Otherwise the
127
+ # HEAD response is returned.
128
+ #
129
+ # @param [ String ] path
130
+ # @param [ Array<String> ] codes
131
+ # @param [ Hash ] params The requests params
132
+ # @option params [ Hash ] :head Request params for the HEAD
133
+ # @option params [ hash ] :get Request params for the GET
134
+ #
135
+ # @return [ Typhoeus::Response ]
136
+ def head_and_get(path, codes = [200], params = {})
137
+ url_to_get = url(path)
138
+ head_params = (params[:head] || {}).merge(head_or_get_params)
139
+
140
+ head_res = NS::Browser.forge_request(url_to_get, head_params).run
141
+
142
+ codes.include?(head_res.code) ? NS::Browser.get(url_to_get, params[:get] || {}) : head_res
143
+ end
144
+ end
145
+ end