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