require 'custodian/settings' require 'timeout' require 'uri' # # The HTTP-protocol test. # # This object is instantiated if the parser sees a line such as: # ### ### http://foo.vm.bytemark.co.uk/ must run http with content 'page text' otherwise 'http fail'. ### # # module Custodian module ProtocolTest class HTTPTest < TestFactory # # The line from which we were constructed. # attr_reader :line # # The URL to poll # attr_reader :url # # The expected status + content # attr_reader :expected_status, :expected_content # # Constructor # def initialize( line ) # # Save the line # @line = line # # Save the URL # @url = line.split( /\s+/)[0] @host = @url # # Set the resolve modes # @resolve_modes = [] # # Will we follow redirects? # @redirect = true # # Ensure we've got a HTTP/HTTPS url. # if ( @url !~ /^https?:/ ) raise ArgumentError, "The target wasn't a HTTP/HTTPS URL: #{line}" end # # Determine that the protocol of the URL matches the # protocol-test we're running # test_type = nil case line when /\s+must\s(not\s+)?run\s+http(\s+|\.|$)/i then test_type = "http" when /\s+must\s+(not\s+)?run\s+https(\s+|\.|$)/i then test_type = "https" else raise ArgumentError, "URL has invalid scheme: #{@line}" end # # Get the schema of the URL # u = URI.parse( @url ) if ( u.scheme != test_type ) raise ArgumentError, "The test case has a different protocol in the URI than that which we're testing: #{@line} - \"#{test_type} != #{u.scheme}\"" end # # Is this test inverted? # if ( line =~ /must\s+not\s+run\s+/ ) @inverted = true else @inverted = false end # # Expected status # if ( line =~ /with status ([0-9]+)/ ) @expected_status = $1.dup else @expected_status = "200" end if ( line =~ /with (IPv[46])/i ) @resolve_modes << $1.downcase.to_sym end # # The content we expect to find # if ( line =~ /with content '([^']+)'/ ) @expected_content = $1.dup else @expected_content = nil end # # Do we follow redirects? # if ( line =~ /not following redirects?/i ) @redirect = false end # # Do we use cache-busting? # @cache_busting = true if ( line =~ /with\s+cache\s+busting/ ) @cache_busting = true end if ( line =~ /without\s+cache\s+busting/ ) @cache_busting = false end # Do we need to override the HTTP Host: Header? @host_override = nil if ( line =~ /with host header '([^']+)'/) @host_override = $1.dup end end # # Get the right type of this object, based on the URL # def get_type if ( @url =~ /^https:/ ) "https" elsif ( @url =~ /^http:/ ) "http" else raise ArgumentError, "URL isn't http/https: #{@url}" end end # # Do we follow redirects? # def follow_redirects? @redirect end # # Do we have cache-busting? # def cache_busting? @cache_busting end # # Allow this test to be serialized. # def to_s @line end # # Run the test. # def run_test # Reset state, in case we've previously run. @error = nil begin require 'rubygems' require 'curb' rescue LoadError @error = "The required rubygem 'curb' was not found." return false end # # Get the timeout period for this test. # settings = Custodian::Settings.instance() period = settings.timeout() # # The URL we'll fetch/poll. # test_url = @url # # Parse and append a query-string if not present, if we're # running with cache-busting. # if ( @cache_busting ) u = URI.parse( test_url ) if ( ! u.query ) u.query = "ctime=#{Time.now.to_i}" test_url = u.to_s end end errors = [] resolution_errors = [] if @resolve_modes.empty? resolve_modes = [:ipv4, :ipv6] else resolve_modes = @resolve_modes end resolve_modes.each do |resolve_mode| status = nil content = nil c = Curl::Easy.new(test_url) c.resolve_mode = resolve_mode # # Should we follow redirections? # if ( follow_redirects? ) c.follow_location = true c.max_redirects = 10 end unless @host_override.nil? c.headers["Host"] = @host_override end c.ssl_verify_host = false c.ssl_verify_peer = false c.timeout = period # # Set a basic protocol message, for use later. # protocol_msg = (resolve_mode == :ipv4 ? "IPv4" : "IPv6") begin timeout( period ) do c.perform status = c.response_code content = c.body_str end # # Overwrite protocol_msg with the IP we connect to. # if c.primary_ip if :ipv4 == resolve_mode protocol_msg = "#{c.primary_ip}" else protocol_msg = "[#{c.primary_ip}]" end end rescue Curl::Err::SSLCACertificateError => x errors << "#{protocol_msg}: SSL validation error: #{x.message}." rescue Curl::Err::TimeoutError, Timeout::Error errors << "#{protocol_msg}: Timed out fetching page." rescue Curl::Err::ConnectionFailedError errors << "#{protocol_msg}: Connection failed." rescue Curl::Err::TooManyRedirectsError errors << "#{protocol_msg}: More than 10 redirections." rescue Curl::Err::HostResolutionError # Nothing to see here..! resolution_errors << resolve_mode rescue => x errors << "#{protocol_msg}: #{x.class.to_s}: #{x.message}\n #{x.backtrace.join("\n ")}." end # # A this point we've either had an exception, or we've # got a result # if ( status and expected_status.to_i != status.to_i ) errors << "#{protocol_msg}: Status code was #{status} not the expected #{expected_status}." end if ( content.is_a?(String) and expected_content.is_a?(String) and content !~ /#{expected_content}/i ) errors << "#{protocol_msg}: The response did not contain our expected text '#{expected_content}'." end end # uh-oh! Resolution failed on both protocols! if resolution_errors.length > 1 errors << "Hostname did not resolve for #{resolution_errors.join(", ")}" end if errors.length > 0 if @host_override errors << "Host header was overridden as Host: #{@host_override}" end @error = errors.join("\n") return false end # # All done. # return true end # # If the test fails then report the error. # def error @error end register_test_type "http" register_test_type "https" end end end