require "observer" require "nokogiri" require 'pp' Stella::Utils.require_vendor "httpclient", '2.1.5.2' module Stella class Client MAX_REDIRECTS = 5.freeze unless defined?(MAX_REDIRECTS) require 'stella/client/container' include Gibbler::Complex include Observable attr_reader :index attr_accessor :base_uri attr_accessor :proxy attr_accessor :created gibbler :opts, :index, :base_uri, :proxy, :nowait, :created def initialize(base_uri=nil, index=1, opts={}) @created = Time.now.to_f opts = { :'no-templates' => false }.merge! opts @opts = opts @base_uri, @index = base_uri, index #@cookie_file = File.new("cookies-#{index}", 'w') @proxy = OpenStruct.new end def execute(usecase, &stat_collector) # Gibbler.enable_debug # We need to make sure the digest cache has a value self.digest if self.digest_cache.nil? Gibbler.disable_debug http_client = create_http_client stats = {} container = Container.new(self.digest_cache, usecase) counter = 0 usecase.requests.each do |req| counter += 1 container.reset_temp_vars stats ||= Benelux::Stats.new update(:prepare_request, usecase, req, counter) begin # This is for the values that were "set" # in the part before the response body. prepare_resources(container, req.resources) params = prepare_params(container, req.params) headers = prepare_headers(container, req.headers) container.params, container.headers = params, headers uri = build_request_uri req.uri, params, container if http_auth = req.http_auth || usecase.http_auth # TODO: The first arg is domain and can include a URI path. # Are there cases where this is important? domain = http_auth.domain # When req.uri is a fully qualified URI, domain will be # set to an incorrect value like, http://domain1/http://domain2. # So we parse it and if that fails we'll set it to the value given. uri_tmp = URI.parse(req.uri).uri rescue req.uri domain ||= '%s://%s:%d%s' % [uri.scheme, uri.host, uri.port, '/'] domain = container.instance_eval &domain if Proc === domain Stella.ld "DOMAIN " << domain user, pass = http_auth.user, http_auth.pass user = container.instance_eval &user if Proc === user pass = container.instance_eval &pass if Proc === pass update(:authenticate, usecase, req, domain, user, pass) http_client.set_auth(domain, user, pass) end if tout = req.timeout || usecase.timeout http_client.receive_timeout = tout end Stella.ld "TIMEOUT " << http_client.receive_timeout.to_s raise NoHostDefined, req.uri if uri.host.nil? || uri.host.empty? stella_id = [Time.now.to_f, self.digest_cache, req.digest_cache, params, headers, counter].digest Benelux.add_thread_tags :request => req.digest_cache Benelux.add_thread_tags :retry => counter Benelux.add_thread_tags :stella_id => stella_id container.unique_id = stella_id params['__stella'] = container.unique_id.short if @opts[:'with-param'] headers['X-Stella-ID'] = container.unique_id.short if @opts[:'with-header'] meth = req.http_method.to_s.downcase Stella.ld "#{req.http_method}: " << "#{req.uri} " << params.inspect ret, asset_duration = nil, 0 rescue => ex update(:request_unhandled_exception, usecase, uri, req, params, ex) Benelux.remove_thread_tags :status, :retry, :request, :stella_id break end begin send_request http_client, usecase, meth, uri, req, params, headers, container, counter update(:receive_response, usecase, uri, req, params, headers, counter, container) Benelux.add_thread_tags :status => container.status res = container.response [ [:request_header_size, res.request.header.dump.size], [:request_content_size, res.request.body.content.size], [:response_headers_size, res.header.dump.size], [:response_content_size, res.body.content.size] ].each do |att| Benelux.thread_timeline.add_count att[0], att[1] end ret = execute_response_handler container, req asset_start = Time.now container.assets.each do |uri| Benelux.add_thread_tags :asset => uri a = http_client.get uri Stella.stdout.info3 " FETCH ASSET: #{uri} #{a.status}" Benelux.remove_thread_tags :asset end asset_duration = Time.now - asset_start rescue HTTPClient::ConnectTimeoutError, HTTPClient::SendTimeoutError, Errno::ECONNRESET, HTTPClient::ReceiveTimeoutError => ex update(:request_timeout, usecase, uri, req, params, headers, counter, container) Benelux.remove_thread_tags :status, :retry, :request, :stella_id next rescue => ex update(:request_unhandled_exception, usecase, uri, req, params, ex) Benelux.remove_thread_tags :status, :retry, :request, :stella_id next end run_sleeper(req.wait, asset_duration) if req.wait != 0 && !nowait? # TODO: consider throw/catch case ret.class.to_s when "Stella::Client::Repeat" update(:request_repeat, counter, ret.times+1, uri, container) Benelux.remove_thread_tags :status redo if counter <= ret.times when "Stella::Client::Follow" ret.uri ||= container.header['Location'].first if counter > MAX_REDIRECTS update(:max_redirects, counter-1, ret, uri, container) break else req = ret.generate_request(req) update(:follow_redirect, ret, uri, container) Benelux.remove_thread_tags :status, :request redo end when "Stella::Client::Quit" update(:usecase_quit, ret.message, uri, container) Benelux.remove_thread_tags :status break when "Stella::Client::Fail" update(:request_fail, ret.message, uri, container) when "Stella::Client::Error" update(:request_error, ret.message, uri, container) end Benelux.remove_thread_tags :status counter = 0 # reset end Benelux.remove_thread_tags :retry, :request, :stella_id stats end def enable_nowait_mode; @nowait = true; end def disable_nowait_mode; @nowait = false; end def nowait?; @nowait == true; end private # We use a method so we can time it with Benelux def send_request(http_client, usecase, meth, uri, req, params, headers, container, counter) if meth == "delete" args = [meth, uri, headers] else args = [meth, uri, params, headers] end container.response = http_client.send(*args) # booya! end def update(kind, *args) changed and notify_observers(kind, self.digest_cache, *args) end def run_sleeper(wait, already_waited=0) # The time it took to download the assets can # be removed from the specified wait time. if wait.is_a?(::Range) ms = rand(wait.last * 1000).to_f ms = wait.first if ms < wait.first else ms = wait * 1000 end sec = ms / 1000 Stella.ld "WAIT ADJUSTED FROM %.1f TO: %.1f" % [sec, (sec - already_waited)] sleep (sec - already_waited) if (sec - already_waited) > 0 end def create_http_client opts = { :proxy => @proxy.uri || nil, # a tautology for clarity :agent_name => "Stella/#{Stella::VERSION}", :from => nil } http_client = HTTPClient.new opts http_client.set_proxy_auth(@proxy.user, @proxy.pass) if @proxy.user http_client.debug_dev = STDOUT if Stella.debug? && Stella.stdout.lev >= 3 http_client.protocol_version = "HTTP/1.1" http_client.ssl_config.verify_mode = ::OpenSSL::SSL::VERIFY_NONE http_client end def prepare_resources(container, resources) h = prepare_runtime_hash container, resources # p [container.client_id.shorter, h] container.resources.merge! h end # Process resource values from the request object def prepare_runtime_hash(container, hashobj, &extra) newh = {} #Stella.ld "PREPARE HEADERS: #{headers}" hashobj.each_pair do |n,v| unless @opts[:'no-templates'] v = container.parse_template v end v = extra.call(v) unless extra.nil? newh[n] = v end newh end alias_method :prepare_headers, :prepare_runtime_hash alias_method :prepare_params, :prepare_runtime_hash # Testplan URIs can be relative or absolute. Either one can # contain variables in the form <tt>:varname</tt>, as in: # # http://example.com/product/:productid # # This method creates a new URI object using the @base_uri # if necessary and replaces all variables with literal values. # If no replacement value can be found, the variable will remain. def build_request_uri(uri, params, container) raise "Request given with no URI" if uri.nil? newuri = uri.clone # don't modify uri template # We call uri.clone b/c we modify uri. uri.scan(/([:\$])([a-z_]+)/i) do |inst| val = find_replacement_value(inst[1], params, container, base_uri) Stella.ld "FOUND VAR: #{inst[0]}#{inst[1]} (value: #{val})" re = Regexp.new "\\#{inst[0]}#{inst[1]}" newuri.gsub! re, val.to_s unless val.nil? end uri = URI.parse(newuri) if uri.host.nil? && base_uri.nil? Stella.abort! raise NoHostDefined, uri end uri.scheme = base_uri.scheme if uri.scheme.nil? uri.host = base_uri.host if uri.host.nil? uri.port = base_uri.port if uri.port.nil? # Support for specifying default path prefix: # $ stella verify -p plan.rb http://localhost/basicauth if base_uri.path if uri.path.nil? || uri.path.empty? uri.path = base_uri.path else a = base_uri.path.gsub(/\/$/, '') b = uri.path.gsub(/^\//, '') uri.path = [a,b].join('/') end end uri end # Testplan URIs can contain variables in the form <tt>:varname</tt>. # This method looks at the request parameters and then at the # usecase's resource hash for a replacement value. # If not found, returns nil. def find_replacement_value(name, params, container, base_uri) value = nil #Stella.ld "REPLACE: #{name}" #Stella.ld "PARAMS: #{params.inspect}" #Stella.ld "IVARS: #{container.instance_variables}" if name.to_sym == :HOSTNAME && !base_uri.nil? value = base_uri.host elsif params.has_key?(name.to_sym) value = params.delete name.to_sym elsif container.resource?( name) value = container.resource name elsif Stella::Testplan.global?(name) Stella::Testplan.global(name) end value end # Find the appropriate response handler by executing the # HTTP response status against the configured handlers. # If several match, the first one is returned. def find_response_handler(container, req) handler = nil req.response.each_pair do |regex,h| Stella.ld "HANDLER REGEX: #{regex.to_s} (#{container.status})" regex = /#{regex}/ unless regex.is_a? Regexp handler = h and break if container.status.to_s =~ regex end handler end def execute_response_handler(container, req) ret = nil handler = find_response_handler container, req if handler.nil? if container.status >= 400 update(:request_fail, "No handler", req.uri, container) end return end begin ret = container.instance_eval &handler update(:execute_response_handler, req, container) rescue => ex update(:error_execute_response_handler, ex, req, container) Stella.ld ex.message, ex.backtrace end ret end class ResponseError < Stella::Error def initialize(k, m=nil) @kind, @msg = k, m end def message msg = "#{@kind}" msg << ": #{@msg}" unless @msg.nil? msg end end end end class Stella::Client class ResponseModifier attr_accessor :obj def initialize(obj=nil) @obj = obj end alias_method :message, :obj alias_method :message=, :obj= end class Repeat < ResponseModifier; alias_method :times, :obj alias_method :times=, :obj= end # # Automatically follow a Location header or you # can optionally specify the URI to load. # # get '/' do # response 302 do # follow do # header :'X-SOME-HEADER' => 'somevalue' # end # end # end # # The block is optional and accepts the same syntax as regular requests. # class Follow < ResponseModifier; alias_method :uri, :obj alias_method :uri=, :obj= attr_reader :definition def initialize(obj=nil,&definition) @obj, @definition = obj, definition end def generate_request(req) n = Stella::Data::HTTP::Request.new :GET, self.uri, req.http_version, &definition n.description = "#{req.description || 'Request'} (autofollow)" n.http_auth = req.http_auth n.response_handler = req.response_handler n.autofollow! n.freeze n end end class Quit < ResponseModifier; end class Fail < Quit; end class Error < Quit; end end