=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 require 'securerandom' require 'digest/sha2' require 'cgi' module Arachni # Includes some useful methods for the system. # # @author Tasos "Zapotek" Laskos module Utilities # @return [String] # Filename (without extension) of the caller. def caller_name File.basename( caller_path( 3 ), '.rb' ) end # @return [String] # Filepath of the caller. def caller_path( offset = 2 ) ::Kernel.caller[offset].split( /:(\d+):in/ ).first end # @return [String] random HEX (SHA2) string def random_seed @@random_seed ||= generate_token end # @see Arachni::Element::Form.from_response def forms_from_response( *args ) Form.from_response( *args ) end # @see Arachni::Element::Form.from_parser def forms_from_parser( *args ) Form.from_parser(*args ) end # @see Arachni::Element::Form.encode def form_encode( *args ) Form.encode( *args ) end # @see Arachni::Element::Form.decode def form_decode( *args ) Form.decode( *args ) end # @see Arachni::HTTP::Request.parse_body def request_parse_body( *args ) HTTP::Request.parse_body( *args ) end # @see Arachni::Element::Link.from_response def links_from_response( *args ) Link.from_response( *args ) end # @see Arachni::Element::Link.from_parser def links_from_parser( *args ) Link.from_parser(*args ) end # @see Arachni::Element::Cookie.from_response def cookies_from_response( *args ) Cookie.from_response( *args ) end # @see Arachni::Element::Cookie.from_parser def cookies_from_parser( *args ) Cookie.from_parser(*args ) end # @see Arachni::Element::Cookie.parse_set_cookie def parse_set_cookie( *args ) Cookie.parse_set_cookie( *args ) end # @see Arachni::Element::Cookie.from_file def cookies_from_file( *args ) Cookie.from_file( *args ) end # @see Arachni::Element::Cookie.encode def cookie_encode( *args ) Cookie.encode( *args ) end # @see Arachni::Element::Cookie.decode def cookie_decode( *args ) Cookie.decode( *args ) end # @see Arachni::Page.from_response def page_from_response( *args ) Page.from_response( *args ) end # @see Arachni::Page.from_url def page_from_url( *args, &block ) Page.from_url( *args, &block ) end def html_decode( str ) ::CGI.unescapeHTML( str.to_s ) end alias :html_unescape :html_decode def html_encode( str ) ::CGI.escapeHTML( str.to_s ) end alias :html_escape :html_encode # @return [URI::Parser] cached URI parser def uri_parser URI.parser end # @see URI.parse def uri_parse( url ) URI.parse( url ) end # @see URI.encode def uri_encode( *args ) URI.encode( *args ) end # @see URI.encode def uri_decode( url ) URI.decode( url ) end def uri_rewrite( *args ) URI.rewrite( *args ) end # @see Arachni::URI.parse_query def uri_parse_query( url ) URI.parse_query( url ) end # @see URI.to_absolute def to_absolute( relative_url, reference_url = Options.instance.url.to_s ) URI.to_absolute( relative_url, reference_url ) end # @see URI.normalize def normalize_url( url ) URI.normalize( url ) end # @see URI.full_and_absolute? def full_and_absolute_url?( url ) Arachni::URI.full_and_absolute?( url.to_s ) end # @param [String] url # # @return [String] path # Full URL up to the path component (no resource, query etc.). # # @see URI.up_to_path def get_path( url ) uri_parse( url ).up_to_path end # @param [String] url # # @return [Bool] # `true` is the path exceeds the framework limit, `false` otherwise. # # @see URI::Scope.too_deep? def path_too_deep?( url ) uri_parse( url ).scope.too_deep? end # Compares 2 urls in order to decide whether or not they belong to the same domain. # # @param [String] url # @param [String] reference # # @return [Bool] # `true` if self is in the same domain as the `reference` URL, false otherwise. # # @see URI.in_domain? # @see OptionGroups::Scope#include_subdomains def path_in_domain?( url, reference = Options.url ) uri_parse( url ).scope.in_domain?( reference ) end # Decides whether the given `url` matches any framework exclusion rules. # # @param [String] url # # @return [Bool] # # @see URI.exclude? # @see OptionGroups::Scope#exclude_path_patterns def exclude_path?( url ) uri_parse( url ).scope.exclude? end # Decides whether the given `url` matches any framework inclusion rules. # # @param [String] url # # @return [Bool] # # @see URI.include? # @see Options#include def include_path?( url ) uri_parse( url ).scope.include? end # Checks if the provided URL matches a redundant filter and decreases its # counter if so. # # If a filter's counter has reached 0 the method returns true. # # @param [String] url # # @return [Bool] # `true` if the `url` is redundant, `false` otherwise. # # @see OptionGroups::Scope#redundant_path_patterns? def redundant_path?( url, update_counters = false ) uri_parse( url ).scope.redundant?( update_counters ) end # # Decides whether the given `url` has an acceptable protocol. # # @param [String] url # @param [String] reference Reference URL. # # @return [Bool] # # @see OptionGroups::Scope#https_only # @see OptionGroups::Scope#https_only? # def follow_protocol?( url, reference = Options.url ) uri_parse( url ).scope.follow_protocol?( reference ) end # @note Does **not** call {#redundant_path?}. # # Decides whether or not the provided `path` should be skipped based on: # # * {#include_path?} # * {#exclude_path?} # * {#path_too_deep?} # * {#path_in_domain?} # # @param [Arachni::URI, ::URI, Hash, String] path # # @return [Bool] def skip_path?( path ) return true if !path parsed = uri_parse( path.to_s ) return true if !parsed parsed.scope.out? end # Determines whether or not the given {Arachni::HTTP::Response} should be # ignored. # # @param [Arachni::HTTP::Response] response # # @return [Bool] # `true` if the `#body` of the given object matches any of the exclusion # patterns, `false` otherwise. # # @see #skip_path? # @see OptionGroups::Scope#exclude_binaries? # @see OptionGroups::Scope#exclude_page? def skip_response?( response ) response.scope.out? end # Determines whether or not the given {Arachni::Page}. # # @param [Page] page # # @return [Bool] # `true` if the `#body` of the given object matches any of the exclusion # patterns or the {OptionGroups::Scope#dom_depth_limit} has been reached, # `false` otherwise. # # @see #skip_path? # @see OptionGroups::Audit#exclude_binaries? # @see OptionGroups::Scope#exclude_page? # @see OptionGroups::Scope#dom_depth_limit def skip_page?( page ) page.scope.out? end # # Determines whether or not the given `resource` should be ignored # depending on its type and content. # # @param [Page,Arachni::HTTP::Response,String] resource # If given a: # # * {Page}: both its URL and body will be examined. # * {Arachni::HTTP::Response}: both its effective URL and body will be examined. # * {String}: if multi-line it will be treated as a response body, # otherwise as a path. # # @return [Bool] # `true` if the resource should be ignore,`false` otherwise. # # @see skip_path? # @see ignore_page? # @see ignore_response? # @see Options#ignore? # def skip_resource?( resource ) case resource when Page skip_page?( resource ) when Arachni::HTTP::Response skip_response?( resource ) else skip_path? resource.to_s end end # @return [Fixnum] # Random available port number. def available_port available_port_mutex.synchronize do @used_ports ||= Set.new loop do port = self.rand_port if port_available?( port ) && !@used_ports.include?( port ) @used_ports << port return port end end end end def self.available_port_mutex @available_port_mutex ||= Mutex.new end available_port_mutex # @return [Integer] # Random port within the user specified range. # # @see OptionGroups::Dispatcher#instance_port_range def rand_port first, last = Options.dispatcher.instance_port_range range = (first..last).to_a range[ rand( range.last - range.first ) ] end def generate_token SecureRandom.hex end # Checks whether the port number is available. # # @param [Fixnum] port # # @return [Bool] def port_available?( port ) begin socket = ::Socket.new( :INET, :STREAM, 0 ) socket.bind( ::Socket.sockaddr_in( port, '127.0.0.1' ) ) socket.close true rescue Errno::EADDRINUSE, Errno::EACCES false end end # @param [String, Float, Integer] seconds # # @return [String] # Time in `00:00:00` (`hours:minutes:seconds`) format. def seconds_to_hms( seconds ) seconds = seconds.to_i [seconds / 3600, seconds / 60 % 60, seconds % 60]. map { |t| t.to_s.rjust( 2, '0' ) }.join( ':' ) end def hms_to_seconds( time ) a = [1, 60, 3600] * 2 time.split( /[:\.]/ ).map { |t| t.to_i * a.pop }.inject(&:+) rescue 0 end def bytes_to_megabytes( bytes ) (bytes / 1024.0 / 1024.0).round( 3 ) end def bytes_to_kilobytes( bytes ) (bytes / 1024.0 ).round( 3 ) end # Wraps the `block` in exception handling code and runs it. # # @param [Bool] raise_exception # Re-raise exception? # @param [Block] block def exception_jail( raise_exception = true, &block ) block.call rescue => e if respond_to?( :print_error ) && respond_to?( :print_exception ) print_exception e print_error print_error 'Parent:' print_error self.class.to_s print_error print_error 'Block:' print_error block.to_s print_error print_error 'Caller:' ::Kernel.caller.each { |l| print_error l } print_error '-' * 80 end raise e if raise_exception nil end def regexp_array_match( regexps, str ) regexps = [regexps].flatten.compact. map { |s| s.is_a?( Regexp ) ? s : Regexp.new( s.to_s ) } return true if regexps.empty? cnt = 0 regexps.each { |filter| cnt += 1 if str =~ filter } cnt == regexps.size end def remove_constants( mod, skip = [] ) return if skip.include?( mod ) return if !(mod.is_a?( Class ) || mod.is_a?( Module )) || !mod.to_s.start_with?( 'Arachni' ) parent = Object mod.to_s.split( '::' )[0..-2].each do |ancestor| parent = parent.const_get( ancestor.to_sym ) end mod.constants.each { |m| mod.send( :remove_const, m ) } nil end extend self end end