new_cms_scanner 0.13.7

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 (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