=begin Copyright 2010-2017 Sarosys LLC This file is part of the Arachni Framework project and is subject to redistribution and commercial restrictions. Please see the Arachni Framework web site for more information on licensing and terms of use. =end module Arachni module HTTP # HTTP Response representation. # # @author Tasos "Zapotek" Laskos class Response < Message require_relative 'response/scope' HTML_CONTENT_TYPES = Set.new(%w(text/html application/xhtml+xml)) HTML_IDENTIFIERS = [ '] # Automatically followed redirections that eventually led to this response. attr_accessor :redirections # @return [Symbol] # `libcurl` return code. attr_accessor :return_code # @return [String] # `libcurl` return code. attr_accessor :return_message # @return [String] # Raw headers. attr_accessor :headers_string # @return [Float] # Total time in seconds for the transfer, including name resolving, TCP # connect etc. attr_accessor :total_time # @return [Float] # Time, in seconds, it took from the start until the full response was # received. attr_accessor :time # @return [Float] # Approximate time the web application took to process the {#request}. attr_accessor :app_time def initialize( options = {} ) super( options ) @body ||= '' @code ||= 0 # Holds the redirection responses that eventually led to this one. @redirections ||= [] @time ||= 0.0 end def time=( t ) @time = t.to_f end # @return [Boolean] # `true` if the client could not read the entire response, `false` otherwise. def partial? # Streamed response which was aborted before completing. return_code == :partial_file || return_code == :recv_error || # Normal response with some data written, but without reaching # content-length. (code != 0 && timed_out?) end # @return [Platform] # Applicable platforms for the page. def platforms Platform::Manager[url] end # @return [String] # First line of the response. def status_line return if !headers_string @status_line ||= headers_string.lines.first.to_s.chomp.freeze end # @return [String] # HTTP response string. def to_s "#{headers_string}#{body}" end # @return [Boolean] # `true` if the response is a `3xx` redirect **and** there is a `Location` # header field. def redirect? code >= 300 && code <= 399 && !!headers.location end alias :redirection? :redirect? def headers_string=( string ) @headers_string = string.to_s.recode.freeze end # @note Depends on the response code. # # @return [Boolean] # `true` if the remote resource has been modified since the date given in # the `If-Modified-Since` request header field, `false` otherwise. def modified? code != 304 end # @return [Boolean] # `true` if the request was performed successfully and the response was # received in full, `false` otherwise. def ok? !return_code || return_code == :ok end # @return [Bool] # `true` if the response body is textual in nature, `false` if binary, # `nil` if could not be determined. def text? return nil if !@body return nil if @is_text == :inconclusive return @is_text if !@is_text.nil? if (type = headers.content_type) return @is_text = true if type.start_with?( 'text/' ) # Non "text/" nor "application/" content types will surely not be # text-based so bail out early. return @is_text = false if !type.start_with?( 'application/' ) end # Last resort, more resource intensive binary detection. begin @is_text = !@body.binary? rescue ArgumentError @is_text = :inconclusive nil end end # @return [Boolean] # `true` if timed out, `false` otherwise. def timed_out? return_code == :operation_timedout end def html? # IF we've got a Content-Type that's all we need to know. if (ct = headers.content_type) ct = ct.split( ';' ).first ct.strip! return HTML_CONTENT_TYPES.include?( ct.downcase ) end # Server insists we should only only use the content-type. respect it. return false if headers['X-Content-Type-Options'].to_s.downcase.include?( 'nosniff' ) # If there's a doctype then we're good to go. return true if body.start_with?( '