lib/arachni/session.rb in arachni-0.4.7 vs lib/arachni/session.rb in arachni-1.0

- old
+ new

@@ -1,121 +1,76 @@ =begin - Copyright 2010-2014 Tasos Laskos <tasos.laskos@gmail.com> + Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com> - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + 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 -# # Session management class. # # Handles logins, provided log-out detection, stores and executes login sequences # and provided general webapp session related helpers. # -# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com> -# +# @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com> class Session include UI::Output include Utilities - # + personalize_output + # {Session} error namespace. # # All {Session} errors inherit from and live under it. # - # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com> - # + # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com> class Error < Arachni::Error + # Raised when trying to {#login} without proper {#configure configuration}. # + # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com> + class NotConfigured < Error + end + # Raised when a login check is required to perform an action but none # has been configured. # - # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com> - # + # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com> class NoLoginCheck < Error end + + # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com> + class FormNotFound < Error + end end LOGIN_TRIES = 5 LOGIN_RETRY_WAIT = 5 - # @return [Options] options - attr_reader :opts + # @return [Browser] + attr_reader :browser - # - # A block used to login to the webapp. - # - # The block should log the framework into the webapp and return `true` on - # success, `false` on failure. - # - # @return [Block] - # - attr_accessor :login_sequence - - # - # A block used to check whether or not we're logged in to the webapp. - # - # It should: - # - # * return `true` on success, `false` on failure. - # * expect 2 parameters, the first one being a hash of HTTP options and - # the second one an optional block. - # - # If a block has been set, the check should work async and pass the result - # to the block, otherwise it should simply return the result. - # - # The result of the check should be `true` or `false`. - # - # A good example of this can be found in {#set_login_check}. - # - # @return [Block] - # - # @see #set_login_check - # - attr_accessor :login_check - - # - # Sets a login form and generates a login sequence from it. - # - # The form must be kosher, best be generated by one of the {Arachni::Element::Form} - # helpers, {Parser} or {#find_login_form}. - # - # Once you get the right form you need to update it with the appropriate values - # before passing it to this accessor. - # - # @return [Element::Form] - # - attr_accessor :login_form - - def initialize( opts = Arachni::Options.instance ) - @opts = opts + def clean_up + configuration.clear + shutdown_browser end - # @return [Array<Element::Cookie>] session cookies + # @return [Array<Element::Cookie>] + # Session cookies. def cookies - http.cookies.select{ |c| c.session? } + http.cookies.select(&:session?) end - # # Tries to find the main session (login/ID) cookie. # - # @param [Block] block block to be passed the cookie + # @param [Block] block + # Block to be passed the cookie. # - # @raise [Error::NoLoginCheck] If no login-check has been configured. - # + # @raise [Error::NoLoginCheck] + # If no login-check has been configured. def cookie( &block ) return block.call( @session_cookie ) if @session_cookie fail Error::NoLoginCheck, 'No login-check has been configured.' if !has_login_check? cookies.each do |cookie| @@ -124,35 +79,61 @@ block.call( @session_cookie = cookie ) end end end - # + # @param [Hash] options + # @option options [String] :url + # URL containing the login form. + # @option options [Hash{String=>String}] :inputs + # Hash containing inputs with which to locate and fill-in the form. + def configure( options ) + configuration.clear + configuration.merge! options + end + + def configuration + Data.session.configuration + end + + # @return [Bool] + # `true` if {#configure configured}, `false` otherwise. + def configured? + configuration.any? + end + # Finds a login forms based on supplied location, collection and criteria. # # @param [Hash] opts - # @option opts [Bool] :requires_password Does the login form include a password field? (Defaults to `true`) - # @option opts [Array, Regexp] :action Regexp to match or String to compare against the form action. - # @option opts [String, Array, Hash, Symbol] :inputs Inputs that the form must contain. - # @option opts [Array<Element::Form>] :forms Collection of forms to look through. - # @option opts [Page, Array<Page>] :pages Pages to look through. - # @option opts [String] :url URL to fetch and look for forms. + # @option opts [Bool] :requires_password + # Does the login form include a password field? (Defaults to `true`) + # @option opts [Array, Regexp] :action + # Regexp to match or String to compare against the form action. + # @option opts [String, Array, Hash, Symbol] :inputs + # Inputs that the form must contain. + # @option opts [Array<Element::Form>] :forms + # Collection of forms to look through. + # @option opts [Page, Array<Page>] :pages + # Pages to look through. + # @option opts [String] :url + # URL to fetch and look for forms. + # @option opts [Bool] :with_browser + # Does the login form require a {Browser} environment? # - # @param [Block] block if a block and a :url are given, the request - # will run async and the block will be called - # with the result of this method. - # + # @param [Block] block + # If a block and a :url are given, the request will run async and the + # block will be called with the result of this method. def find_login_form( opts = {}, &block ) async = block_given? requires_password = (opts[:requires_password].nil? ? true : opts[:requires_password]) find = proc do |cforms| cforms.select do |f| next if requires_password && !f.requires_password? - oks = [] + oks = [] if action = opts[:action] oks << !!(action.is_a?( Regexp ) ? f.action =~ action : f.action == action) end @@ -166,12 +147,18 @@ forms = if opts[:pages] [opts[:pages]].flatten.map { |p| p.forms }.flatten elsif opts[:forms] opts[:forms] - elsif url = opts[:url] - http_opts = { http: { update_cookies: true } } + elsif (url = opts[:url]) + http_opts = { + precision: false, + http: { + update_cookies: true, + follow_location: true + } + } if async page_from_url( url, http_opts ) { |p| block.call find.call( p.forms ) } else page_from_url( url, http_opts ).forms @@ -179,13 +166,14 @@ end find.call( forms || [] ) if !async end - # @return [Bool] `true` if there is log-in capability, `false` otherwise. + # @return [Bool] + # `true` if there is log-in capability, `false` otherwise. def can_login? - has_login_sequence? && @login_check + configured? && has_login_check? end # @return [Bool, nil] # `true` if logged-in, `false` otherwise, `nil` if there's no log-in # capability. @@ -195,125 +183,114 @@ print_bad 'The scanner has been logged out.' print_info 'Trying to re-login...' LOGIN_TRIES.times do |i| - break if login + break if !login.response.timed_out? rescue Error + print_bad "Login attempt #{i+1} failed, retrying after " << "#{LOGIN_RETRY_WAIT} seconds..." sleep LOGIN_RETRY_WAIT end - if !logged_in? - print_bad 'Could not re-login.' - false - else + if logged_in? print_ok 'Logged-in successfully.' true + else + print_bad 'Could not re-login.' + false end end + # Uses the information provided by {#configure} to login. # - # Uses the block in {#login_sequence} to login to the webapp. + # @return [Page, nil] + # {HTTP::Response} if the login form was submitted successfully, + # `nil` if not {#configured?}. # - # @return [Bool, nil] - # `true` if login was successful, `false` if not, `nil` if no - # {#login_sequence} has been set. - # + # @raise [Error::FormNotFound] + # If the form could not be found. def login - login_sequence.call if has_login_sequence? + fail Error::NotConfigured, 'Please #configure the session first.' if !configured? + + refresh_browser + + form = find_login_form( + pages: browser.load( configuration[:url] ).to_page, + inputs: configuration[:inputs].keys + ) + + if !form + fail Error::FormNotFound, + "Login form could not be found with: #{configuration}" + end + + form.dom.update configuration[:inputs] + form.dom.auditor = self + + page = nil + form.dom.submit { |p| page = p } + + http.update_cookies browser.cookies + + page end - # @return [Bool] `true` if a login sequence exists, `false` otherwise. - def has_login_sequence? - !!login_sequence + # @param [Block] block + # Block to be passed the {#browser}. + def with_browser( &block ) + block.call browser end - # - # Uses the block in {#login_check} to check in we're logged in to the webapp. - # - # @param [Hash] http_opts Extra HTTP options to use for the check. + # @param [Hash] http_options + # HTTP options to use for the check. # @param [Block] block # If a block has been provided the check will be async and the result will # be passed to it, otherwise the method will return the result. # - # # @return [Bool, nil] - # `true` if we're logged-in, `false` if not, `nil` if no - # {#login_sequence} has been set. + # `true` if we're logged-in, `false` otherwise. # - def logged_in?( http_opts = {}, &block ) - login_check.call( http_opts, block ) if has_login_check? + # @raise [Error::NoLoginCheck] + # If no login-check has been configured. + def logged_in?( http_options = {}, &block ) + fail Error::NoLoginCheck if !has_login_check? + + http_options = http_options.merge( + mode: block_given? ? :async : :sync + ) + + bool = nil + http.get( Options.session.check_url, http_options ) do |response| + bool = !!response.body.match( Options.session.check_pattern ) + block.call( bool ) if block + end + bool end - # @return [Bool] `true` if a login check exists, `false` otherwise. + # @return [Bool] + # `true` if a login check exists, `false` otherwise. def has_login_check? - !!login_check + !!(Options.session.check_url && Options.session.check_pattern) end - def login_check( &block ) - return @login_check = block if block_given? - - if @opts.login_check_url && @opts.login_check_pattern - set_login_check( @opts.login_check_url, @opts.login_check_pattern ) - end - - @login_check + # @return [HTTP::Client] + def http + HTTP::Client end - # - # A block used to login to the webapp. - # - # The block should log the framework into the webapp and return `true` on - # success, `false` on failure. - # - # @param [Block] block - # if a block has been given it will be set as the login sequence. - # - # @return [Block] - # - def login_sequence( &block ) - if @login_form && !block_given? - @login_sequence = proc do - if !(refreshed = @login_form.refresh( update_cookies: true )) - print_bad 'Login form has disappeared, cannot login.' - next - end + private - refreshed.submit( - async: false, - update_cookies: true, - follow_location: false - ).response - end - end + def shutdown_browser + return if !@browser - return @login_sequence if !block_given? - @login_sequence = block + @browser.shutdown + @browser = nil end - # - # Sets a login check using the provided `url` and `regexp`. - # - # @param [String, #to_s] url URL to request. - # @param [String, Regexp] pattern - # Pattern to match against the body of the response. - # - def set_login_check( url, pattern ) - login_check do |opts, block| - bool = nil - http.get( url.to_s, opts.merge( async: !!block ) ) do |res| - bool = !!res.body.match( pattern ) - block.call( bool ) if block - end - - bool - end - end - - # @return [HTTP] http interface - def http - HTTP + def refresh_browser + shutdown_browser + @browser = Browser.new end end end