=begin Copyright 2010-2015 Tasos Laskos 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_relative 'framework/rpc' module Arachni class State # State information for {Arachni::Framework}. # # @author Tasos "Zapotek" Laskos class Framework # {Framework} error namespace. # # All {Framework} errors inherit from and live under it. # # @author Tasos "Zapotek" Laskos class Error < State::Error # @author Tasos "Zapotek" Laskos class StateNotSuspendable < Error end # @author Tasos "Zapotek" Laskos class StateNotAbortable < Error end # @author Tasos "Zapotek" Laskos class InvalidStatusMessage < Error end end # @return [RPC] attr_accessor :rpc # @return [Support::LookUp::HashSet] attr_reader :page_queue_filter # @return [Support::LookUp::HashSet] attr_reader :url_queue_filter # @return [Support::LookUp::HashSet] attr_reader :element_pre_check_filter # @return [Set] attr_reader :browser_skip_states # @return [Symbol] attr_accessor :status # @return [Bool] attr_accessor :running # @return [Integer] attr_accessor :audited_page_count # @return [Array] attr_reader :pause_signals # @return [Array] attr_reader :status_messages def initialize @rpc = RPC.new @audited_page_count = 0 @browser_skip_states = Support::LookUp::HashSet.new( hasher: :persistent_hash ) @page_queue_filter = Support::LookUp::HashSet.new( hasher: :persistent_hash ) @url_queue_filter = Support::LookUp::HashSet.new( hasher: :persistent_hash ) @element_pre_check_filter = Support::LookUp::HashSet.new( hasher: :coverage_hash ) @running = false @pre_pause_status = nil @pause_signals = Set.new @paused_signal = Queue.new @suspended_signal = Queue.new @aborted_signal = Queue.new @status_messages = [] end def statistics { rpc: @rpc.statistics, audited_page_count: @audited_page_count, browser_states: @browser_skip_states.size } end # @return [Hash{Symbol=>String}] # All possible {#status_messages} by type. def available_status_messages { suspending: 'Will suspend as soon as the current page is audited.', waiting_for_browser_cluster_jobs: 'Waiting for %i browser cluster jobs to finish.', suspending_plugins: 'Suspending plugins.', saving_snapshot: 'Saving snapshot at: %s', snapshot_location: 'Snapshot location: %s', browser_cluster_startup: 'Initialising the browser cluster.', browser_cluster_shutdown: 'Shutting down the browser cluster.', clearing_queues: 'Clearing the audit queues.', waiting_for_plugins: 'Waiting for the plugins to finish.', aborting: 'Aborting the scan.' } end # Sets a message as {#status_messages}. # # @param (see #add_status_message) # @return (see #add_status_message) def set_status_message( *args ) clear_status_messages add_status_message( *args ) end # Pushes a message to {#status_messages}. # # @param [String, Symbol] message # Status message. If `Symbol`, it will be grabbed from # {#available_status_messages}. # @param [String, Numeric] sprintf # `sprintf` arguments. def add_status_message( message, *sprintf ) if message.is_a? Symbol if !available_status_messages.include?( message ) fail Error::InvalidStatusMessage, "Could not find status message for: '#{message}'" end message = available_status_messages[message] % sprintf end @status_messages << message.to_s end # Clears {#status_messages}. def clear_status_messages @status_messages.clear end # @param [Page] page # # @return [Bool] # `true` if the `page` has already been seen (based on the # {#page_queue_filter}), `false` otherwise. # # @see #page_seen def page_seen?( page ) @page_queue_filter.include? page end # @param [Page] page # Page to mark as seen. # # @see #page_seen? def page_seen( page ) @page_queue_filter << page end # @param [String] url # # @return [Bool] # `true` if the `url` has already been seen (based on the # {#url_queue_filter}), `false` otherwise. # # @see #url_seen def url_seen?( url ) @url_queue_filter.include? url end # @param [Page] url # URL to mark as seen. # # @see #url_seen? def url_seen( url ) @url_queue_filter << url end # @param [Support::LookUp::HashSet] states def update_browser_skip_states( states ) @browser_skip_states.merge states end # @param [#coverage_hash] e # # @return [Bool] # `true` if the element has already been seen (based on the # {#element_pre_check_filter}), `false` otherwise. # # @see #element_checked def element_checked?( e ) @element_pre_check_filter.include? e end # @param [Page] e # Element to mark as seen. # # @see #element_checked? def element_checked( e ) @element_pre_check_filter << e end def running? !!@running end # @param [Bool] block # `true` if the method should block until an abortion has completed, # `false` otherwise. # # @return [Bool] # `true` if the abort request was successful, `false` if the system is # already {#suspended?} or is {#suspending?}. # # @raise [StateNotAbortable] # When not {#running?}. def abort( block = true ) return false if aborting? || aborted? if !running? fail Error::StateNotAbortable, 'Cannot abort an idle state.' end set_status_message :aborting @status = :aborting @abort = true @aborted_signal.pop if block true end # @return [Bool] # `true` if a {#abort} signal is in place , `false` otherwise. def abort? !!@abort end # Signals a completed abort operation. def aborted @abort = false @status = :aborted @aborted_signal << true nil end # @return [Bool] # `true` if the system has been aborted, `false` otherwise. def aborted? @status == :aborted end # @return [Bool] # `true` if the system is being aborted, `false` otherwise. def aborting? @status == :aborting end # @return [Bool] # `true` if the system has completed successfully, `false` otherwise. def done? @status == :done end # @param [Bool] block # `true` if the method should block until a suspend has completed, # `false` otherwise. # # @return [Bool] # `true` if the suspend request was successful, `false` if the system is # already {#suspended?} or is {#suspending?}. # # @raise [StateNotSuspendable] # When {#paused?} or {#pausing?}. def suspend( block = true ) return false if suspending? || suspended? if paused? || pausing? fail Error::StateNotSuspendable, 'Cannot suspend a paused state.' end if !running? fail Error::StateNotSuspendable, 'Cannot suspend an idle state.' end set_status_message :suspending @status = :suspending @suspend = true @suspended_signal.pop if block true end # @return [Bool] # `true` if an {#abort} signal is in place , `false` otherwise. def suspend? !!@suspend end # Signals a completed suspension. def suspended @suspend = false @status = :suspended @suspended_signal << true nil end # @return [Bool] # `true` if the system has been suspended, `false` otherwise. def suspended? @status == :suspended end # @return [Bool] # `true` if the system is being suspended, `false` otherwise. def suspending? @status == :suspending end # @return [Bool] # `true` if the system is scanning, `false` otherwise. def scanning? @status == :scanning end # @param [Object] caller # Identification for the caller which issued the pause signal. # @param [Bool] block # `true` if the method should block until the pause has completed, # `false` otherwise. # # @return [TrueClass] # Pauses the framework on a best effort basis, might take a while to take # effect. def pause( caller, block = true ) @pre_pause_status ||= @status if !paused? && !pausing? if !paused? @status = :pausing end @pause_signals << caller paused if !running? @paused_signal.pop if block && !paused? true end # Signals that the system has been paused.. def paused clear_status_messages @status = :paused @paused_signal << nil end # @return [Bool] # `true` if the framework is paused. def paused? @status == :paused end # @return [Bool] # `true` if the system is being paused, `false` otherwise. def pausing? @status == :pausing end # @return [Bool] # `true` if the framework should pause, `false` otherwise. def pause? @pause_signals.any? end # Resumes a paused system # # @param [Object] caller # Identification for the caller whose {#pause} signal to remove. The # system is resumed once there are no more {#pause} signals left. # # @return [Bool] # `true` if the system is resumed, `false` if there are more {#pause} # signals pending. def resume( caller ) @pause_signals.delete( caller ) if @pause_signals.empty? @status = @pre_pause_status @pre_pause_status = nil return true end false end def force_resume @pause_signals.to_a.each do |ref| resume ref end end def dump( directory ) FileUtils.mkdir_p( directory ) rpc.dump( "#{directory}/rpc/" ) %w(element_pre_check_filter page_queue_filter url_queue_filter browser_skip_states audited_page_count).each do |attribute| IO.binwrite( "#{directory}/#{attribute}", Marshal.dump( send(attribute) ) ) end end def self.load( directory ) framework = new framework.rpc = RPC.load( "#{directory}/rpc/" ) %w(element_pre_check_filter page_queue_filter url_queue_filter browser_skip_states).each do |attribute| path = "#{directory}/#{attribute}" next if !File.exist?( path ) framework.send(attribute).merge Marshal.load( IO.binread( path ) ) end framework.audited_page_count = Marshal.load( IO.binread( "#{directory}/audited_page_count" ) ) framework end def clear rpc.clear @element_pre_check_filter.clear @page_queue_filter.clear @url_queue_filter.clear @pause_signals.clear @paused_signal.clear @suspended_signal.clear @running = false @pre_pause_status = nil @browser_skip_states.clear @audited_page_count = 0 end end end end