#!/usr/bin/perl # nagios: +epn package Webinject; # Copyright 2010-2012 Sven Nierlein (nierlein@cpan.org) # Copyright 2004-2006 Corey Goldberg (corey@goldb.org) # # This file is part of WebInject. # # WebInject is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # WebInject is distributed in the hope that it will be useful, # but without any warranty; without even the implied warranty of # merchantability or fitness for a particular purpose. See the # GNU General Public License for more details. use 5.006; use strict; use warnings; use Carp; use LWP; use HTTP::Request::Common; use HTTP::Cookies; use XML::Simple; use Time::HiRes 'time', 'sleep'; use Getopt::Long; use Crypt::SSLeay; # for SSL/HTTPS (you may comment this out if you don't need it) use XML::Parser; # for web services verification (you may comment this out if aren't doing XML verifications for web services) use Error qw(:try); # for web services verification (you may comment this out if aren't doing XML verifications for web services) use Data::Dumper; # dump hashes for debugging use File::Temp qw/ tempfile /; # create temp files our $VERSION = '1.80'; =head1 NAME Webinject - Perl Module for testing web services =head1 SYNOPSIS use Webinject; my $webinject = Webinject->new(reporttype => "nagios", timeout => 30, break_on_errors => 1); $webinject->engine(); =head1 DESCRIPTION WebInject is a free tool for automated testing of web applications and web services. It can be used to test individual system components that have HTTP interfaces (JSP, ASP, CGI, PHP, AJAX, Servlets, HTML Forms, XML/SOAP Web Services, REST, etc), and can be used as a test harness to create a suite of [HTTP level] automated functional, acceptance, and regression tests. A test harness allows you to run many test cases and collect/report your results. WebInject offers real-time results display and may also be used for monitoring system response times. =head1 CONSTRUCTOR =head2 new ( [ARGS] ) Creates an C object. =over 4 =item reporttype possible values are 'standard', 'nagios', 'nagios2', 'mrtg' or 'external:' =item nooutput suppress all output to STDOUT, create only logilfes =item break_on_errors stop after the first testcase fails, otherwise Webinject would go on and execute all tests regardless of the previous case. =item timeout Default timeout is 180seconds. Timeout starts again for every testcase. =item useragent Set the useragent used in HTTP requests. Default is 'Webinject'. =item max_redirect Set maximum number of HTTP redirects. Default is 0. =item proxy Sets a proxy which is then used for http and https requests. =item output_dir Output directory where all logfiles will go to. Defaults to current directory. =item globalhttplog Can be 'yes' or 'onfail'. Will log the http request and response to a http.log file. =item httpauth Provides credentials for webserver authentications. The format is: ['servername', 'portnumber', 'realm-name', 'username', 'password'] =item baseurl the value can be used as {BASEURL} in the test cases =item baseurl1 the value can be used as {BASEURL1} in the test cases =item baseurl2 the value can be used as {BASEURL2} in the test cases =item standaloneplot can be "on" or "off". Default is off. Create gnuplot graphs when enabled. =item graphtype Defaults to 'lines' =item gnuplot Defines the path to your gnuplot binary. =back =cut sub new { my $class = shift; my (%options) = @_; $| = 1; # don't buffer output to STDOUT my $self = {}; bless $self, $class; # set default config options $self->_set_defaults(); for my $opt_key ( keys %options ) { if( exists $self->{'config'}->{$opt_key} ) { if($opt_key eq 'httpauth') { $self->_set_http_auth($options{$opt_key}); } else { $self->{'config'}->{$opt_key} = $options{$opt_key}; } } else { $self->_usage("ERROR: unknown option: ".$opt_key); } } # get command line options $self->_getoptions(); return $self; } ######################################## =head1 METHODS =head2 engine start the engine of webinject =cut sub engine { #wrap the whole engine in a subroutine so it can be integrated with the gui my $self = shift; if($self->{'gui'}) { $self->_gui_initial(); } else { # delete files leftover from previous run (do this here so they are whacked each run) $self->_whackoldfiles(); } $self->_processcasefile(); my $useragent = $self->_get_useragent(); # write opening tags for STDOUT. $self->_writeinitialstdout(); # create the gnuplot config file $self->_plotcfg(); # timer for entire test run my $startruntimer = time(); # process test case files named in config for my $currentcasefile ( @{ $self->{'casefilelist'} } ) { #print "\n$currentcasefile\n\n"; my $resultfile = { 'name' => $currentcasefile, 'cases' => [], }; if($self->{'gui'}) { $self->_gui_processing_msg($currentcasefile); } my $tempfile = $self->_convtestcases($currentcasefile); my $xmltestcases; eval { $xmltestcases = XMLin( $tempfile, varattr => 'varname', variables => $self->{'config'} ); # slurp test case file to parse (and specify variables tag) }; if($@) { my $error = $@; $error =~ s/^\s*//mx; $self->_usage("ERROR: reading xml test case ".$currentcasefile." failed: ".$error); } unless( defined $xmltestcases->{case} ) { $self->_usage("ERROR: no test cases defined!"); } # fix case if there is only one case if( defined $xmltestcases->{'case'}->{'id'} ) { my $tmpcase = $xmltestcases->{'case'}; $xmltestcases->{'case'} = { $tmpcase->{'id'} => $tmpcase }; } #delete the temp file as soon as we are done reading it if ( -e $tempfile ) { unlink $tempfile; } my $repeat = 1; if(defined $xmltestcases->{repeat} and $xmltestcases->{repeat} > 0) { $repeat = $xmltestcases->{repeat}; } my $useragent = $self->_get_useragent(); for my $run_nr (1 .. $repeat) { # process cases in sorted order for my $testnum ( sort { $a <=> $b } keys %{ $xmltestcases->{case} } ) { # if an XPath Node is defined, only process the single Node if( $self->{'xnode'} ) { $testnum = $self->{'xnode'}; } # create testcase my $case = { 'id' => $testnum }; # populate variables with values from testcase file, do substitutions, and revert converted values back for my $key (keys %{$xmltestcases->{'case'}->{$testnum}}) { $case->{$key} = $xmltestcases->{'case'}->{$testnum}->{$key}; } my $label = ''; if(defined $case->{'label'}) { $label = $case->{'label'}." - "; } $self->_out(qq|Test: $label$currentcasefile - $testnum \n|); $case = $self->_run_test_case($case, $useragent); push @{$resultfile->{'cases'}}, $case; # break from sub if user presses stop button in gui if( $self->{'switches'}->{'stop'} eq 'yes' ) { my $rc = $self->_finaltasks(); $self->{'switches'}->{'stop'} = 'no'; return $rc; # break from sub } # break here if the last result was an error if($self->{'config'}->{'break_on_errors'} and $self->{'result'}->{'iscritical'}) { last; } # if an XPath Node is defined, only process the single Node if( $self->{'xnode'} ) { last; } } } push @{$self->{'result'}->{'files'}}, $resultfile; } my $endruntimer = time(); $self->{'result'}->{'totalruntime'} = ( int( 1000 * ( $endruntimer - $startruntimer ) ) / 1000 ); #elapsed time rounded to thousandths # do return/cleanup tasks return $self->_finaltasks(); } ################################################################################ # runs a single test case sub _run_test_case { my($self,$case,$useragent) =@_; confess("no testcase!") unless defined $case; # set some defaults $case->{'id'} = 1 unless defined $case->{'id'}; $case->{'passedcount'} = 0; $case->{'failedcount'} = 0; $case->{'iswarning'} = 0; $case->{'iscritical'} = 0; $case->{'messages'} = []; $useragent = $self->_get_useragent() unless defined $useragent; # don't do this if monitor is disabled in gui if($self->{'gui'} and $self->{'monitorenabledchkbx'} ne 'monitor_off') { my $curgraphtype = $self->{'config'}->{'graphtype'}; } # used to replace parsed {timestamp} with real timestamp value my $timestamp = time(); for my $key (keys %{$case}) { $case->{$key} = $self->_convertbackxml($case->{$key}, $timestamp); next if $key eq 'errormessage'; $case->{$key} = $self->_convertbackxmlresult($case->{$key}); } if( $self->{'gui'} ) { $self->_gui_tc_descript($case); } push @{$case->{'messages'}}, { 'html' => "" }; # HTML: open table column for(qw/description1 description2/) { next unless defined $case->{$_}; $self->_out(qq|Desc: $case->{$_}\n|); push @{$case->{'messages'}}, {'key' => $_, 'value' => $case->{$_}, 'html' => "$case->{$_}
" }; } my $method; if (defined $case->{method}) { $method = uc($case->{method}); } else { $method = "GET"; } push @{$case->{'messages'}}, { 'html' => qq|$method $case->{url}
\n| }; push @{$case->{'messages'}}, { 'html' => "" }; # HTML: next column my($latency,$request,$response); alarm($self->{'config'}->{'timeout'}+1); # timeout should be handled by LWP, but just in case... eval { local $SIG{ALRM} = sub { die("alarm") }; if($case->{method}){ if(lc $case->{method} eq "get") { ($latency,$request,$response) = $self->_httpget($useragent, $case); } elsif(lc $case->{method} eq "post") { ($latency,$request,$response) = $self->_httppost($useragent, $case); } else { $self->_usage('ERROR: bad HTTP Request Method Type, you must use "get" or "post"'); } } else { ($latency,$request,$response) = $self->_httpget($useragent, $case); # use "get" if no method is specified } }; alarm(0); if($@) { $case->{'iscritical'} = 1; } else { $case->{'latency'} = $latency; $case->{'request'} = $request->as_string(); $case->{'response'} = $response->as_string(); # verify result from http response $self->_verify($response, $case); if($case->{verifypositivenext}) { $self->{'verifylater'} = $case->{'verifypositivenext'}; $self->_out("Verify On Next Case: '".$case->{verifypositivenext}."' \n"); push @{$case->{'messages'}}, {'key' => 'verifypositivenext', 'value' => $case->{verifypositivenext}, 'html' => "Verify On Next Case: ".$case->{verifypositivenext}."
" }; } if($case->{verifynegativenext}) { $self->{'verifylaterneg'} = $case->{'verifynegativenext'}; $self->_out("Verify Negative On Next Case: '".$case->{verifynegativenext}."' \n"); push @{$case->{'messages'}}, {'key' => 'verifynegativenext', 'value' => $case->{verifynegativenext}, 'html' => "Verify Negative On Next Case: ".$case->{verifynegativenext}."
" }; } # write to http.log file $self->_httplog($request, $response, $case); # send perf data to log file for plotting $self->_plotlog($latency); # call the external plotter to create a graph $self->_plotit(); if( $self->{'gui'} ) { $self->_gui_updatemontab(); # update monitor with the newly rendered plot graph } $self->_parseresponse($response, $case); # grab string from response to send later # make parsed results available in the errormessage for my $key (keys %{$case}) { next unless $key eq 'errormessage'; $case->{$key} = $self->_convertbackxmlresult($case->{$key}); } } push @{$case->{'messages'}}, { 'html' => "\n" }; # HTML: next column # if any verification fails, test case is considered a failure if($case->{'iscritical'}) { # end result will be also critical $self->{'result'}->{'iscritical'} = 1; push @{$case->{'messages'}}, {'key' => 'success', 'value' => 'false' }; if( $self->{'result'}->{'returnmessage'} ) { # Add returnmessage to the output my $prefix = "case #".$case->{'id'}.": "; if(defined $case->{'label'}) { $prefix = $case->{'label'}." (case #".$case->{'id'}."): "; } $self->{'result'}->{'returnmessage'} = $prefix.$self->{'result'}->{'returnmessage'}; my $message = $self->{'result'}->{'returnmessage'}; $message = $message.' - '.$case->{errormessage} if defined $case->{errormessage}; push @{$case->{'messages'}}, { 'key' => 'result-message', 'value' => $message, 'html' => "FAILED : ".$message."" }; $self->_out("TEST CASE FAILED : ".$message."\n"); } # print regular error output elsif ( $case->{errormessage} ) { # Add defined error message to the output push @{$case->{'messages'}}, { 'key' => 'result-message', 'value' => $case->{errormessage}, 'html' => "FAILED : ".$case->{errormessage}."" }; $self->_out(qq|TEST CASE FAILED : $case->{errormessage}\n|); } else { push @{$case->{'messages'}}, { 'key' => 'result-message', 'value' => 'TEST CASE FAILED', 'html' => "FAILED" }; $self->_out(qq|TEST CASE FAILED\n|); } unless( $self->{'result'}->{'returnmessage'} ) { #(used for plugin compatibility) if it's the first error message, set it to variable if( $case->{errormessage} ) { $self->{'result'}->{'returnmessage'} = $case->{errormessage}; } else { $self->{'result'}->{'returnmessage'} = "Test case number ".$case->{'id'}." failed"; if(defined $case->{'label'}) { $self->{'result'}->{'returnmessage'} = "Test case ".$case->{'label'}." (#".$case->{'id'}.") failed"; } } } if( $self->{'gui'} ) { $self->_gui_status_failed(); } } elsif($case->{'iswarning'}) { # end result will be also warning $self->{'result'}->{'iswarning'} = 1; push @{$case->{'messages'}}, {'key' => 'success', 'value' => 'false' }; if( $case->{errormessage} ) { # Add defined error message to the output push @{$case->{'messages'}}, {'key' => 'result-message', 'value' => $case->{errormessage}, 'html' => "WARNED : ".$case->{errormessage}."" }; $self->_out(qq|TEST CASE WARNED : $case->{errormessage}\n|); } # print regular error output else { # we suppress most logging when running in a plugin mode push @{$case->{'messages'}}, {'key' => 'result-message', 'value' => 'TEST CASE WARNED', 'html' => "WARNED" }; $self->_out(qq|TEST CASE WARNED\n|); } unless( $self->{'result'}->{'returnmessage'} ) { #(used for plugin compatibility) if it's the first error message, set it to variable if( $case->{errormessage} ) { $self->{'result'}->{'returnmessage'} = $case->{errormessage}; } else { $self->{'result'}->{'returnmessage'} = "Test case number ".$case->{'id'}." warned"; if(defined $case->{'label'}) { $self->{'result'}->{'returnmessage'} = "Test case ".$case->{'label'}." (#".$case->{'id'}.") warned"; } } } if( $self->{'gui'} ) { $self->_gui_status_failed(); } } else { $self->_out(qq|TEST CASE PASSED\n|); push @{$case->{'messages'}}, {'key' => 'success', 'value' => 'true' }; push @{$case->{'messages'}}, { 'key' => 'result-message', 'value' => 'TEST CASE PASSED', 'html' => "PASSED" }; if( $self->{'gui'} ) { $self->_gui_status_passed(); } } if( $self->{'gui'} ) { $self->_gui_timer_output($latency); } $self->_out(qq|Response Time = $latency sec \n|); $self->_out(qq|------------------------------------------------------- \n|); push @{$case->{'messages'}}, { 'key' => 'responsetime', 'value' => $latency, 'html' => "
".$latency." sec \n" }; $self->{'result'}->{'runcount'}++; $self->{'result'}->{'totalruncount'}++; if( $self->{'gui'} ) { # update the statusbar $self->_gui_statusbar(); } if( $latency > $self->{'result'}->{'maxresponse'} ) { # set max response time $self->{'result'}->{'maxresponse'} = $latency; } if(!defined $self->{'result'}->{'minresponse'} or $latency < $self->{'result'}->{'minresponse'} ) { # set min response time $self->{'result'}->{'minresponse'} = $latency; } # keep total of response times for calculating avg $self->{'result'}->{'totalresponse'} = ( $self->{'result'}->{'totalresponse'} + $latency ); # avg response rounded to thousands $self->{'result'}->{'avgresponse'} = ( int( 1000 * ( $self->{'result'}->{'totalresponse'} / $self->{'result'}->{'totalruncount'} ) ) / 1000 ); if( $self->{'gui'} ) { # update timers and counts in monitor tab $self->_gui_updatemonstats(); } # if a sleep value is set in the test case, sleep that amount if( $case->{sleep} ) { sleep( $case->{sleep} ); } $self->{'result'}->{'totalpassedcount'} += $case->{'passedcount'}; $self->{'result'}->{'totalfailedcount'} += $case->{'failedcount'}; if($case->{'iscritical'} or $case->{'iswarning'}) { $self->{'result'}->{'totalcasesfailedcount'}++; } else { $self->{'result'}->{'totalcasespassedcount'}++; } return $case; } ################################################################################ sub _get_useragent { my $self = shift; # construct LWP object my $useragent = LWP::UserAgent->new(keep_alive=>1); # store cookies in our LWP object my $fh; our $cookietempfilename; ($fh, $cookietempfilename) = tempfile(undef, UNLINK => 1); unlink ($cookietempfilename); $useragent->cookie_jar(HTTP::Cookies->new( file => $cookietempfilename, autosave => 1, )); # http useragent that will show up in webserver logs unless(defined $self->{'config'}->{'useragent'}) { $useragent->agent('WebInject'); } else { $useragent->agent($self->{'config'}->{'useragent'}); } # add proxy support if it is set in config.xml if( $self->{'config'}->{'proxy'} ) { my $proxy = $self->{'config'}->{'proxy'}; $proxy =~ s/^http:\/\///mx; $useragent->proxy([qw( http )], "http://".$proxy); $ENV{'HTTPS_PROXY'} = "http://".$proxy; } # don't follow redirects unless set by config push @{$useragent->requests_redirectable}, 'POST'; $useragent->max_redirect($self->{'config'}->{'max_redirect'}); # add http basic authentication support # corresponds to: # $useragent->credentials('servername:portnumber', 'realm-name', 'username' => 'password'); if(scalar @{$self->{'config'}->{'httpauth'}}) { # add the credentials to the user agent here. The foreach gives the reference to the tuple ($elem), and we # deref $elem to get the array elements. for my $elem ( @{ $self->{'config'}->{'httpauth'} } ) { #print "adding credential: $elem->[0]:$elem->[1], $elem->[2], $elem->[3] => $elem->[4]\n"; $useragent->credentials( $elem->[0].":".$elem->[1], $elem->[2], $elem->[3] => $elem->[4] ); } } # change response delay timeout in seconds if it is set in config.xml if($self->{'config'}->{'timeout'}) { $useragent->timeout($self->{'config'}->{'timeout'}); # default LWP timeout is 180 secs. } return $useragent; } ################################################################################ # set defaults sub _set_defaults { my $self = shift; $self->{'config'} = { 'currentdatetime' => scalar localtime time, #get current date and time for results report 'standaloneplot' => 'off', 'graphtype' => 'lines', 'httpauth' => [], 'reporttype' => 'standard', 'output_dir' => './', 'nooutput' => undef, 'baseurl' => '', 'baseurl1' => '', 'baseurl2' => '', 'break_on_errors' => 0, 'max_redirect' => 0, 'globalhttplog' => 'no', 'proxy' => '', 'timeout' => 180, }; $self->{'exit_codes'} = { 'UNKNOWN' => 3, 'OK' => 0, 'WARNING' => 1, 'CRITICAL' => 2, }; $self->{'switches'} = { 'stop' => 'no', 'plotclear' => 'no', }; $self->{'out'} = ''; $self->_reset_result(); return; } ################################################################################ # reset result sub _reset_result { my $self = shift; $self->{'result'} = { 'cases' => [], 'returnmessage' => undef, 'totalcasesfailedcount' => 0, 'totalcasespassedcount' => 0, 'totalfailedcount' => 0, 'totalpassedcount' => 0, 'totalresponse' => 0, 'totalruncount' => 0, 'totalruntime' => 0, 'casecount' => 0, 'avgresponse' => 0, 'iscritical' => 0, 'iswarning' => 0, 'maxresponse' => 0, 'minresponse' => undef, 'runcount' => 0, }; return; } ################################################################################ # write initial text for STDOUT sub _writeinitialstdout { my $self = shift; if($self->{'config'}->{'reporttype'} !~ /^nagios/mx) { $self->_out(qq| Starting WebInject Engine (v$Webinject::VERSION)... |); } $self->_out("-------------------------------------------------------\n"); return; } ################################################################################ # write summary and closing tags for results file sub _write_result_html { my $self = shift; my $file = $self->{'config'}->{'output_dir'}."results.html"; open( my $resultshtml, ">", $file ) or $self->_usage("ERROR: Failed to write ".$file.": ".$!); print $resultshtml qq| WebInject Test Results |; for my $file (@{$self->{'result'}->{'files'}}) { for my $case (@{$file->{'cases'}}) { print $resultshtml qq|\n|; for my $message (@{$case->{'messages'}}) { next unless defined $message->{'html'}; print $resultshtml $message->{'html'} . "\n"; } print $resultshtml "\n"; } } print $resultshtml qq|
Test Description
Request URL
Results Summary
Response Time
$file->{'name'}
$case->{'id'}
Start Time: $self->{'config'}->{'currentdatetime'}
Total Run Time: $self->{'result'}->{'totalruntime'} seconds

Test Cases Run: $self->{'result'}->{'totalruncount'}
Test Cases Passed: $self->{'result'}->{'totalcasespassedcount'}
Test Cases Failed: $self->{'result'}->{'totalcasesfailedcount'}
Verifications Passed: $self->{'result'}->{'totalpassedcount'}
Verifications Failed: $self->{'result'}->{'totalfailedcount'}

Average Response Time: $self->{'result'}->{'avgresponse'} seconds
Max Response Time: $self->{'result'}->{'maxresponse'} seconds
Min Response Time: $self->{'result'}->{'minresponse'} seconds

|; close($resultshtml); return; } ################################################################################ # write summary and closing tags for XML results file sub _write_result_xml { my $self = shift; my $file = $self->{'config'}->{'output_dir'}."results.xml"; open( my $resultsxml, ">", $file ) or $self->_usage("ERROR: Failed to write ".$file.": ".$!); print $resultsxml "\n\n"; for my $file (@{$self->{'result'}->{'files'}}) { print $resultsxml " {'name'}."\">\n\n"; for my $case (@{$file->{'cases'}}) { print $resultsxml " {'id'}."\">\n"; for my $message (@{$case->{'messages'}}) { next unless defined $message->{'key'}; print $resultsxml " <".$message->{'key'}.">".$message->{'value'}."{'key'}.">\n"; } print $resultsxml " \n\n"; } print $resultsxml " \n"; } print $resultsxml qq| $self->{'config'}->{'currentdatetime'} $self->{'result'}->{'totalruntime'} $self->{'result'}->{'totalruncount'} $self->{'result'}->{'totalcasespassedcount'} $self->{'result'}->{'totalcasesfailedcount'} $self->{'result'}->{'totalpassedcount'} $self->{'result'}->{'totalfailedcount'} $self->{'result'}->{'avgresponse'} $self->{'result'}->{'maxresponse'} $self->{'result'}->{'minresponse'} |; close($resultsxml); return; } ################################################################################ # write summary and closing text for STDOUT sub _writefinalstdout { my $self = shift; if($self->{'config'}->{'reporttype'} !~ /^nagios/mx) { $self->_out(qq| Start Time: $self->{'config'}->{'currentdatetime'} Total Run Time: $self->{'result'}->{'totalruntime'} seconds |); } $self->_out(qq| Test Cases Run: $self->{'result'}->{'totalruncount'} Test Cases Passed: $self->{'result'}->{'totalcasespassedcount'} Test Cases Failed: $self->{'result'}->{'totalcasesfailedcount'} Verifications Passed: $self->{'result'}->{'totalpassedcount'} Verifications Failed: $self->{'result'}->{'totalfailedcount'} |); return; } ################################################################################ sub _http_defaults { my $self = shift; my $request = shift; my $useragent = shift; my $case = shift; # add an additional HTTP Header if specified if($case->{'addheader'}) { # can add multiple headers with a pipe delimiter for my $addheader (split /\|/mx, $case->{'addheader'}) { $addheader =~ m~(.*):\ (.*)~mx; $request->header( $1 => $2 ); # using HTTP::Headers Class } } # print $self->{'request'}->as_string; print "\n\n"; my $starttimer = time(); my $response = $useragent->request($request); my $endtimer = time(); my $latency = ( int( 1000 * ( $endtimer - $starttimer ) ) / 1000 ); # elapsed time rounded to thousandths # print $response->as_string; print "\n\n"; return($latency,$request,$response); } ################################################################################ # send http request and read response sub _httpget { my $self = shift; my $useragent = shift; my $case = shift; $self->_out("GET Request: ".$case->{url}."\n"); my $request = new HTTP::Request( 'GET', $case->{url} ); return $self->_http_defaults($request, $useragent, $case); } ################################################################################ # post request based on specified encoding sub _httppost { my $self = shift; my $useragent = shift; my $case = shift; if($case->{posttype} ) { if($case->{posttype} =~ m~application/x\-www\-form\-urlencoded~mx) { return $self->_httppost_form_urlencoded($useragent, $case); } elsif($case->{posttype} =~ m~multipart/form\-data~mx) { return $self->_httppost_form_data($useragent, $case); } elsif( ($case->{posttype} =~ m~text/xml~mx) or ($case->{posttype} =~ m~application/soap\+xml~mx) ) { return $self->_httppost_xml($useragent, $case); } else { $self->_usage('ERROR: Bad Form Encoding Type, I only accept "application/x-www-form-urlencoded", "multipart/form-data", "text/xml", "application/soap+xml"'); } } else { # use "x-www-form-urlencoded" if no encoding is specified $case->{posttype} = 'application/x-www-form-urlencoded'; return $self->_httppost_form_urlencoded($useragent, $case); } return; } ################################################################################ # send application/x-www-form-urlencoded HTTP request and read response sub _httppost_form_urlencoded { my $self = shift; my $useragent = shift; my $case = shift; $self->_out("POST Request: ".$case->{url}."\n"); my $request = new HTTP::Request('POST', $case->{url} ); $request->content_type($case->{posttype}); $request->content($case->{postbody}); return $self->_http_defaults($request,$useragent, $case); } ################################################################################ # send text/xml HTTP request and read response sub _httppost_xml { my $self = shift; my $useragent = shift; my $case = shift; my($latency,$request,$response); # read the xml file specified in the testcase $case->{postbody} =~ m~file=>(.*)~imx; open( my $xmlbody, "<", $1 ) or $self->_usage("ERROR: Failed to open text/xml file ".$1.": ".$!); # open file handle my @xmlbody = <$xmlbody>; # read the file into an array close($xmlbody); # Get the XML input file to use PARSEDRESULT and substitute the contents my $content = $self->_convertbackxmlresult(join( " ", @xmlbody )); $self->_out("POST Request: ".$case->{url}."\n"); $request = new HTTP::Request( 'POST', $case->{url} ); $request->content_type($case->{posttype}); $request->content( $content ); # load the contents of the file into the request body ($latency,$request,$response) = $self->_http_defaults($request, $useragent, $case); my $xmlparser = new XML::Parser; # see if the XML parses properly try { $xmlparser->parse($response->decoded_content); # print "good xml\n"; push @{$case->{'messages'}}, {'key' => 'verifyxml-success', 'value' => 'true', 'html' => 'Passed XML Parser (content is well-formed)' }; $self->_out("Passed XML Parser (content is well-formed) \n"); $case->{'passedcount'}++; # exit try block return; } catch Error with { # get the exception object my $ex = shift; # print "bad xml\n"; # we suppress most logging when running in a plugin mode if($self->{'config'}->{'reporttype'} eq 'standard') { push @{$case->{'messages'}}, {'key' => 'verifyxml-success', 'value' => 'false', 'html' => "Failed XML parser on response: ".$ex }; } $self->_out("Failed XML parser on response: $ex \n"); $case->{'failedcount'}++; $case->{'iscritical'} = 1; }; # <-- remember the semicolon return($latency,$request,$response); } ################################################################################ # send multipart/form-data HTTP request and read response sub _httppost_form_data { my $self = shift; my $useragent = shift; my $case = shift; my %myContent_; ## no critic eval "\%myContent_ = $case->{postbody}"; ## use critic $self->_out("POST Request: ".$case->{url}."\n"); my $request = POST($case->{url}, Content_Type => $case->{posttype}, Content => \%myContent_); return $self->_http_defaults($request, $useragent, $case); } ################################################################################ # do verification of http response and print status to HTML/XML/STDOUT/UI sub _verify { my $self = shift; my $response = shift; my $case = shift; confess("no response") unless defined $response; confess("no case") unless defined $case; if( $case->{verifyresponsecode} ) { $self->_out(qq|Verify Response Code: "$case->{verifyresponsecode}" \n|); push @{$case->{'messages'}}, {'key' => 'verifyresponsecode', 'value' => $case->{verifyresponsecode} }; # verify returned HTTP response code matches verifyresponsecode set in test case if ( $case->{verifyresponsecode} == $response->code() ) { push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-success', 'value' => 'true', 'html' => 'Passed HTTP Response Code: '.$case->{verifyresponsecode} }; push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-messages', 'value' => 'Passed HTTP Response Code Verification' }; $self->_out(qq|Passed HTTP Response Code Verification \n|); $case->{'passedcount'}++; } else { push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-success', 'value' => 'false', 'html' => 'Failed HTTP Response Code: received '.$response->code().', expecting '.$case->{verifyresponsecode} }; push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-messages', 'value' => 'Failed HTTP Response Code Verification (received '.$response->code().', expecting '.$case->{verifyresponsecode}.')' }; $self->_out(qq|Failed HTTP Response Code Verification (received |.$response->code().qq|, expecting $case->{verifyresponsecode}) \n|); $case->{'failedcount'}++; $case->{'iscritical'} = 1; if($self->{'config'}->{'break_on_errors'}) { $self->{'result'}->{'returnmessage'} = 'Failed HTTP Response Code Verification (received '.$response->code().', expecting '.$case->{verifyresponsecode}.')'; return; } } } else { # verify http response code is in the 100-399 range if($response->as_string() =~ /HTTP\/1.(0|1)\ (1|2|3)/imx ) { # verify existance of string in response push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-success', 'value' => 'true', 'html' => 'Passed HTTP Response Code Verification (not in error range)' }; push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-messages', 'value' => 'Passed HTTP Response Code Verification (not in error range)' }; $self->_out(qq|Passed HTTP Response Code Verification (not in error range) \n|); # succesful response codes: 100-399 $case->{'passedcount'}++; } else { $response->as_string() =~ /(HTTP\/1.)(.*)/mxi; if($1) { #this is true if an HTTP response returned push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-success', 'value' => 'false', 'html' => 'Failed HTTP Response Code Verification ('.$1.$2.')' }; push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-messages', 'value' => 'Failed HTTP Response Code Verification ('.$1.$2.')' }; $self->_out("Failed HTTP Response Code Verification ($1$2) \n"); #($1$2) is HTTP response code $case->{'failedcount'}++; $case->{'iscritical'} = 1; if($self->{'config'}->{'break_on_errors'}) { $self->{'result'}->{'returnmessage'} = 'Failed HTTP Response Code Verification ('.$1.$2.')'; return; } } #no HTTP response returned.. could be error in connection, bad hostname/address, or can not connect to web server else { push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-success', 'value' => 'false', 'html' => 'Failed - No Response' }; push @{$case->{'messages'}}, {'key' => 'verifyresponsecode-messages', 'value' => 'Failed - No Response' }; $self->_out("Failed - No valid HTTP response:\n".$response->as_string()); $case->{'failedcount'}++; $case->{'iscritical'} = 1; if($self->{'config'}->{'break_on_errors'}) { $self->{'result'}->{'returnmessage'} = 'Failed - No valid HTTP response: '.$response->as_string(); return; } } } } push @{$case->{'messages'}}, { 'html' => '
' }; for my $nr ('', 1..1000) { my $key = "verifypositive".$nr; if( $case->{$key} ) { $self->_out("Verify: '".$case->{$key}."' \n"); push @{$case->{'messages'}}, {'key' => $key, 'value' => $case->{$key} }; my $regex = $self->_fix_regex($case->{$key}); # verify existence of string in response if( $response->as_string() =~ m~$regex~simx ) { push @{$case->{'messages'}}, {'key' => $key.'-success', 'value' => 'true', 'html' => "Passed: ".$case->{$key} }; $self->_out("Passed Positive Verification \n"); $case->{'passedcount'}++; } else { push @{$case->{'messages'}}, {'key' => $key.'-success', 'value' => 'false', 'html' => "Failed: ".$case->{$key} }; $self->_out("Failed Positive Verification \n"); $case->{'failedcount'}++; $case->{'iscritical'} = 1; if($self->{'config'}->{'break_on_errors'}) { $self->{'result'}->{'returnmessage'} = 'Failed Positive Verification, can not find a string matching regex: '.$regex; return; } } push @{$case->{'messages'}}, { 'html' => '
' }; } elsif($nr ne '' and $nr > 5) { last; } } for my $nr ('', 1..1000) { my $key = "verifynegative".$nr; if( $case->{$key} ) { $self->_out("Verify Negative: '".$case->{$key}."' \n"); push @{$case->{'messages'}}, {'key' => $key, 'value' => $case->{$key} }; my $regex = $self->_fix_regex($case->{$key}); # verify existence of string in response if( $response->as_string() =~ m~$regex~simx ) { push @{$case->{'messages'}}, {'key' => $key.'-success', 'value' => 'false', 'html' => 'Failed Negative: '.$case->{$key} }; $self->_out("Failed Negative Verification \n"); $case->{'failedcount'}++; $case->{'iscritical'} = 1; if($self->{'config'}->{'break_on_errors'}) { $self->{'result'}->{'returnmessage'} = 'Failed Negative Verification, found regex matched string: '.$regex; return; } } else { push @{$case->{'messages'}}, {'key' => $key.'-success', 'value' => 'true', 'html' => 'Passed Negative: '.$case->{$key} }; $self->_out("Passed Negative Verification \n"); $case->{'passedcount'}++; } push @{$case->{'messages'}}, { 'html' => '
' }; } elsif($nr ne '' and $nr > 5) { last; } } if($self->{'verifylater'}) { my $regex = $self->_fix_regex($self->{'verifylater'}); # verify existence of string in response if($response->as_string() =~ m~$regex~simx ) { push @{$case->{'messages'}}, {'key' => 'verifypositivenext-success', 'value' => 'true', 'html' => 'Passed Positive Verification (verification set in previous test case)' }; $self->_out("Passed Positive Verification (verification set in previous test case) \n"); $case->{'passedcount'}++; } else { push @{$case->{'messages'}}, {'key' => 'verifypositivenext-success', 'value' => 'false', 'html' => 'Failed Positive Verification (verification set in previous test case)' }; $self->_out("Failed Positive Verification (verification set in previous test case) \n"); $case->{'failedcount'}++; $case->{'iscritical'} = 1; if($self->{'config'}->{'break_on_errors'}) { $self->{'result'}->{'returnmessage'} = 'Failed Positive Verification (verification set in previous test case), can not find a string matching regex: '.$regex; return; } } push @{$case->{'messages'}}, { 'html' => '
' }; # set to null after verification delete $self->{'verifylater'}; } if($self->{'verifylaterneg'}) { my $regex = $self->_fix_regex($self->{'verifylaterneg'}); # verify existence of string in response if($response->as_string() =~ m~$regex~simx) { push @{$case->{'messages'}}, {'key' => 'verifynegativenext-success', 'value' => 'false', 'html' => 'Failed Negative Verification (negative verification set in previous test case)' }; $self->_out("Failed Negative Verification (negative verification set in previous test case) \n"); $case->{'failedcount'}++; $case->{'iscritical'} = 1; if($self->{'config'}->{'break_on_errors'}) { $self->{'result'}->{'returnmessage'} = 'Failed Negative Verification (negative verification set in previous test case), found regex matched string: '.$regex; return; } } else { push @{$case->{'messages'}}, {'key' => 'verifynegativenext-success', 'value' => 'true', 'html' => 'Passed Negative Verification (negative verification set in previous test case)' }; $self->_out("Passed Negative Verification (negative verification set in previous test case) \n"); $case->{'passedcount'}++; } push @{$case->{'messages'}}, { 'html' => '
' }; # set to null after verification delete $self->{'verifylaterneg'}; } if($case->{'warning'}) { $self->_out("Verify Warning Threshold: ".$case->{'warning'}."\n"); push @{$case->{'messages'}}, {'key' => "Warning Threshold", 'value' => $case->{''} }; if($case->{'latency'} > $case->{'warning'}) { push @{$case->{'messages'}}, {'key' => 'warning-success', 'value' => 'false', 'html' => "Failed Warning Threshold: ".$case->{'warning'} }; $self->_out("Failed Warning Threshold \n"); $case->{'failedcount'}++; $case->{'iswarning'} = 1; } else { $self->_out("Passed Warning Threshold \n"); push @{$case->{'messages'}}, {'key' => 'warning-success', 'value' => 'true', 'html' => "Passed Warning Threshold: ".$case->{'warning'} }; $case->{'passedcount'}++; } push @{$case->{'messages'}}, { 'html' => '
' }; } if($case->{'critical'}) { $self->_out("Verify Critical Threshold: ".$case->{'critical'}."\n"); push @{$case->{'messages'}}, {'key' => "Critical Threshold", 'value' => $case->{''} }; if($case->{'latency'} > $case->{'critical'}) { push @{$case->{'messages'}}, {'key' => 'critical-success', 'value' => 'false', 'html' => "Failed Critical Threshold: ".$case->{'critical'} }; $self->_out("Failed Critical Threshold \n"); $case->{'failedcount'}++; $case->{'iscritical'} = 1; } else { $self->_out("Passed Critical Threshold \n"); push @{$case->{'messages'}}, {'key' => 'critical-success', 'value' => 'true', 'html' => "Passed Critical Threshold: ".$case->{'critical'} }; $case->{'passedcount'}++; } } return; } ################################################################################ # parse values from responses for use in future request (for session id's, dynamic URL rewriting, etc) sub _parseresponse { my $self = shift; my $response = shift; my $case = shift; my ( $resptoparse, @parseargs ); my ( $leftboundary, $rightboundary, $escape ); for my $type ( qw/parseresponse parseresponse1 parseresponse2 parseresponse3 parseresponse4 parseresponse5/ ) { next unless $case->{$type}; @parseargs = split( /\|/mx, $case->{$type} ); $leftboundary = $parseargs[0]; $rightboundary = $parseargs[1]; $escape = $parseargs[2]; $resptoparse = $response->as_string; ## no critic if ( $resptoparse =~ m~$leftboundary(.*?)$rightboundary~s ) { $self->{'parsedresult'}->{$type} = $1; } ## use critic elsif(!defined $case->{'parsewarning'} or $case->{'parsewarning'}) { push @{$case->{'messages'}}, {'key' => $type.'-success', 'value' => 'false', 'html' => "Failed Parseresult, cannot find $leftboundary(.*?)$rightboundary" }; $self->_out("Failed Parseresult, cannot find $leftboundary(*)$rightboundary\n"); $case->{'iswarning'} = 1; } if ($escape) { if ( $escape eq 'escape' ) { $self->{'parsedresult'}->{$type} = $self->_url_escape( $self->{'parsedresult'}->{$type} ); } } #print "\n\nParsed String: $self->{'parsedresult'}->{$type}\n\n"; } return; } ################################################################################ # read config.xml sub _read_config_xml { my $self = shift; my $config_file = shift; my($config, $comment_mode,@configlines); # process the config file # if -c option was set on command line, use specified config file if(defined $config_file) { open( $config, '<', $config_file ) or $self->_usage("ERROR: Failed to open ".$config_file." file: ".$!); $self->{'config'}->{'exists'} = 1; # flag we are going to use a config file } # if config.xml exists, read it elsif( -e "config.xml" ) { open( $config, '<', "config.xml" ) or $self->_usage("ERROR: Failed to open config.xml file: ".$!); $self->{'config'}->{'exists'} = 1; # flag we are going to use a config file } if( $self->{'config'}->{'exists'} ) { #if we have a config file, use it my @precomment = <$config>; #read the config file into an array #remove any commented blocks from config file foreach (@precomment) { unless (m~.*~mx) { # single line comment # multi-line comments if (//mx) { $comment_mode = 1; } elsif (m~~mx) { $comment_mode = 0; } elsif ( !$comment_mode ) { push( @configlines, $_ ); } } } close($config); } #grab values for constants in config file: foreach (@configlines) { for my $key ( qw/baseurl baseurl1 baseurl2 gnuplot proxy timeout output_dir globaltimeout globalhttplog standaloneplot max_redirect break_on_errors useragent/ ) { if (/<$key>/mx) { $_ =~ m~<$key>(.*)~mx; $self->{'config'}->{$key} = $1; #print "\n$_ : $self->{'config'}->{$_} \n\n"; } } if (//mx) { $_ =~ m~(.*)~mx; if ( $1 ne "standard" ) { $self->{'config'}->{'reporttype'} = $1; $self->{'config'}->{'nooutput'} = "set"; } #print "\nreporttype : $self->{'config'}->{'reporttype'} \n\n"; } if (//mx) { $_ =~ m~(.*)~mx; $self->_set_http_auth($1); #print "\nhttpauth : @{$self->{'config'}->{'httpauth'}} \n\n"; } if(//mx) { my $firstparse = $'; #print "$' \n\n"; $firstparse =~ m~~mx; my $filename = $`; #string between tags will be in $filename #print "\n$filename \n\n"; push @{ $self->{'casefilelist'} }, $filename; #add next filename we grab to end of array } } return; } ################################################################################ # parse and set http auth config sub _set_http_auth { my $self = shift; my $confstring = shift; #each time we see an , we set @authentry to be the #array of values, then we use [] to get a reference to that array #and push that reference onto @httpauth. my @authentry = split( /:/mx, $confstring ); if( scalar @authentry != 5 ) { $self->_usage("ERROR: httpauth should have 5 fields delimited by colons, got: ".$confstring); } else { push( @{ $self->{'config'}->{'httpauth'} }, [@authentry] ); } # basic authentication only works with redirects enabled if($self->{'config'}->{'max_redirect'} == 0) { $self->{'config'}->{'max_redirect'}++; } return; } ################################################################################ # get test case files to run (from command line or config file) and evaluate constants sub _processcasefile { # parse config file and grab values it sets my $self = shift; if( ( $#ARGV + 1 ) < 1 ) { #no command line args were passed unless( $self->{'casefilelist'}->[0] ) { if ( -e "testcases.xml" ) { # if no files are specified in config.xml, default to testcases.xml push @{ $self->{'casefilelist'} }, "testcases.xml"; } else { $self->_usage("ERROR: I can't find any test case files to run.\nYou must either use a config file or pass a filename " . "on the command line if you are not using the default testcase file (testcases.xml)."); } } } elsif( ( $#ARGV + 1 ) == 1 ) { # one command line arg was passed # use testcase filename passed on command line (config.xml is only used for other options) push @{ $self->{'casefilelist'} }, $ARGV[0]; # first commandline argument is the test case file, put this on the array for processing } elsif( ( $#ARGV + 1 ) == 2 ) { # two command line args were passed my $xpath = $ARGV[1]; if ( $xpath =~ /\/(.*)\[/mx ) { # if the argument contains a "/" and "[", it is really an XPath $xpath =~ /(.*)\/(.*)\[(.*?)\]/mx; #if it contains XPath info, just grab the file name $self->{'xnode'} = $3; # grab the XPath Node value.. (from inside the "[]") # print "\nXPath Node is: $self->{'xnode'} \n"; } else { $self->_usage("ERROR: Sorry, $xpath is not in the XPath format I was expecting, I'm ignoring it..."); } # use testcase filename passed on command line (config.xml is only used for other options) push @{ $self->{'casefilelist'} }, $ARGV[0]; # first command line argument is the test case file, put this on the array for processing } elsif ( ( $#ARGV + 1 ) > 2 ) { #too many command line args were passed $self->_usage("ERROR: Too many arguments."); } #print "\ntestcase file list: @{$self->{'casefilelist'}}\n\n"; return; } ################################################################################ # here we do some pre-processing of the test case file and write it out to a temp file. # we convert certain chars so xml parser doesn't puke. sub _convtestcases { my $self = shift; my $currentcasefile = shift; my @xmltoconvert; my ( $fh, $tempfilename ) = tempfile(); my $filename = $currentcasefile; open( my $xmltoconvert, '<', $filename ) or $self->_usage("ERROR: Failed to read test case file: ".$filename.": ".$!); # read the file into an array @xmltoconvert = <$xmltoconvert>; my $ids = {}; for my $line (@xmltoconvert) { # convert escaped chars and certain reserved chars to temporary values that the parser can handle # these are converted back later in processing $line =~ s/&/{AMPERSAND}/gmx; $line =~ s/\\{'result'}->{'casecount'}++; } # verify id is only use once per file if ( $line =~ /^\s*id\s*=\s*\"*(\d+)\"*/mx ) { if(defined $ids->{$1}) { $self->{'result'}->{'iswarning'} = 1; $self->_out("Warning: case id $1 is used more than once!\n"); } $ids->{$1} = 1; } } close($xmltoconvert); # open file handle to temp file open( $xmltoconvert, '>', $tempfilename ) or $self->_usage("ERROR: Failed to write ".$tempfilename.": ".$!); print $xmltoconvert @xmltoconvert; # overwrite file with converted array close($xmltoconvert); return $tempfilename; } ################################################################################ # converts replaced xml with substitutions sub _convertbackxml { my ( $self, $string, $timestamp ) = @_; return unless defined $string; $string =~ s~{AMPERSAND}~&~gmx; $string =~ s~{LESSTHAN}~<~gmx; $string =~ s~{TIMESTAMP}~$timestamp~gmx; $string =~ s~{BASEURL}~$self->{'config'}->{baseurl}~gmx; $string =~ s~{BASEURL1}~$self->{'config'}->{baseurl1}~gmx; $string =~ s~{BASEURL2}~$self->{'config'}->{baseurl2}~gmx; return $string; } ################################################################################ # converts replaced xml with parsed result sub _convertbackxmlresult { my ( $self, $string) = @_; return unless defined $string; $string =~ s~\{PARSEDRESULT\}~$self->{'parsedresult'}->{'parseresponse'}~gmx if defined $self->{'parsedresult'}->{'parseresponse'}; for my $x (1..5) { $string =~ s~\{PARSEDRESULT$x\}~$self->{'parsedresult'}->{"parseresponse$x"}~gmx if defined $self->{'parsedresult'}->{"parseresponse$x"}; } return $string; } ################################################################################ # escapes difficult characters with %hexvalue sub _url_escape { my ( $self, @values ) = @_; # LWP handles url encoding already, but use this to escape valid chars that LWP won't convert (like +) my @return; for my $val (@values) { $val =~ s/[^-\w.,!~'()\/\ ]/uc sprintf "%%%02x", ord $&/egmx; push @return, $val; } return wantarray ? @return : $return[0]; } ################################################################################ # write requests and responses to http.log file sub _httplog { my $self = shift; my $request = shift; my $response = shift; my $case = shift; my $output = ''; # http request - log setting per test case if($case->{'logrequest'} && $case->{'logrequest'} =~ /yes/mxi ) { $output .= $request->as_string."\n\n"; } # http response - log setting per test case if($case->{'logresponse'} && $case->{'logresponse'} =~ /yes/mxi ) { $output .= $response->as_string."\n\n"; } # global http log setting if($self->{'config'}->{'globalhttplog'} && $self->{'config'}->{'globalhttplog'} =~ /yes/mxi ) { $output .= $request->as_string."\n\n"; $output .= $response->as_string."\n\n"; } # global http log setting - onfail mode if($self->{'config'}->{'globalhttplog'} && $self->{'config'}->{'globalhttplog'} =~ /onfail/mxi && $case->{'iscritical'}) { $output .= $request->as_string."\n\n"; $output .= $response->as_string."\n\n"; } if($output ne '') { my $file = $self->{'config'}->{'output_dir'}."http.log"; open( my $httplogfile, ">>", $file ) or $self->_usage("ERROR: Failed to write ".$file.": ".$!); print $httplogfile $output; print $httplogfile "\n************************* LOG SEPARATOR *************************\n\n\n"; close($httplogfile); } return; } ################################################################################ # write performance results to plot.log in the format gnuplot can use sub _plotlog { my ( $self, $value ) = @_; my ( %months, $date, $time, $mon, $mday, $hours, $min, $sec, $year ); # do this unless: monitor is disabled in gui, or running standalone mode without config setting to turn on plotting if( ( $self->{'gui'} and $self->{'monitorenabledchkbx'} ne 'monitor_off') or (!$self->{'gui'} and $self->{'config'}->{'standaloneplot'} eq 'on') ) { %months = ( "Jan" => 1, "Feb" => 2, "Mar" => 3, "Apr" => 4, "May" => 5, "Jun" => 6, "Jul" => 7, "Aug" => 8, "Sep" => 9, "Oct" => 10, "Nov" => 11, "Dec" => 12 ); $date = scalar localtime; ($mon, $mday, $hours, $min, $sec, $year) = $date =~ /\w+\ (\w+)\ +(\d+)\ (\d\d):(\d\d):(\d\d)\ (\d\d\d\d)/mx; $time = "$months{$mon} $mday $hours $min $sec $year"; my $plotlog; # used to clear the graph when requested if( $self->{'switches'}->{'plotclear'} eq 'yes' ) { # open in clobber mode so log gets truncated my $file = $self->{'config'}->{'output_dir'}."plot.log"; open( $plotlog, '>', $file ) or $self->_usage("ERROR: Failed to write ".$file.": ".$!); $self->{'switches'}->{'plotclear'} = 'no'; # reset the value } else { my $file = $self->{'config'}->{'output_dir'}."plot.log"; open( $plotlog, '>>', $file ) or $self->_usage("ERROR: Failed to write ".$file.": ".$!); #open in append mode } printf $plotlog "%s %2.4f\n", $time, $value; close($plotlog); } return; } ################################################################################ # create gnuplot config file sub _plotcfg { my $self = shift; # do this unless: monitor is disabled in gui, or running standalone mode without config setting to turn on plotting if( ( $self->{'gui'} and $self->{'monitorenabledchkbx'} ne 'monitor_off') or (!$self->{'gui'} and $self->{'config'}->{'standaloneplot'} eq 'on') ) { my $file = $self->{'config'}->{'output_dir'}."plot.plt"; open( my $gnuplotplt, ">", $file ) or _usage("ERROR: Could not open ".$file.": ".$!); print $gnuplotplt qq| set term png set output \"$self->{'config'}->{'output_dir'}plot.png\" set size 1.1,0.5 set pointsize .5 set xdata time set ylabel \"Response Time (seconds)\" set yrange [0:] set bmargin 2 set tmargin 2 set timefmt \"%m %d %H %M %S %Y\" plot \"$self->{'config'}->{'output_dir'}plot.log\" using 1:7 title \"Response Times" w $self->{'config'}->{'graphtype'} |; close($gnuplotplt); } return; } ################################################################################ # do ending tasks sub _finaltasks { my $self = shift; if ( $self->{'gui'} ) { $self->_gui_stop(); } # we suppress most logging when running in a plugin mode if($self->{'config'}->{'reporttype'} eq 'standard') { # write summary and closing tags for results file $self->_write_result_html(); #write summary and closing tags for XML results file $self->_write_result_xml(); } # write summary and closing tags for STDOUT $self->_writefinalstdout(); #plugin modes if($self->{'config'}->{'reporttype'} ne 'standard') { # return value is set which corresponds to a monitoring program # Nagios plugin compatibility if($self->{'config'}->{'reporttype'} =~ /^nagios/mx) { # nagios perf data has following format # 'label'=value[UOM];[warn];[crit];[min];[max] my $crit = 0; if(defined $self->{'config'}->{globaltimeout}) { $crit = $self->{'config'}->{globaltimeout}; } my $lastid = 0; my $perfdata = '|time='.$self->{'result'}->{'totalruntime'}.'s;0;'.$crit.';0;0'; for my $file (@{$self->{'result'}->{'files'}}) { for my $case (@{$file->{'cases'}}) { my $warn = $case->{'warning'} || 0; my $crit = $case->{'critical'} || 0; my $label = $case->{'label'} || 'case'.$case->{'id'}; $perfdata .= ' '.$label.'='.$case->{'latency'}.'s;'.$warn.';'.$crit.';0;0'; $lastid = $case->{'id'}; } } # report performance data for missed cases too for my $nr (1..($self->{'result'}->{'casecount'} - $self->{'result'}->{'totalruncount'})) { $lastid++; my $label = 'case'.$lastid; $perfdata .= ' '.$label.'=0s;0;0;0;0'; } my($rc,$message); if($self->{'result'}->{'iscritical'}) { $message = "WebInject CRITICAL - ".$self->{'result'}->{'returnmessage'}; $rc = $self->{'exit_codes'}->{'CRITICAL'}; } elsif($self->{'result'}->{'iswarning'}) { $message = "WebInject WARNING - ".$self->{'result'}->{'returnmessage'}; $rc = $self->{'exit_codes'}->{'WARNING'}; } elsif( $self->{'config'}->{globaltimeout} && $self->{'result'}->{'totalruntime'} > $self->{'config'}->{globaltimeout} ) { $message = "WebInject WARNING - All tests passed successfully but global timeout (".$self->{'config'}->{globaltimeout}." seconds) has been reached"; $rc = $self->{'exit_codes'}->{'WARNING'}; } else { $message = "WebInject OK - All tests passed successfully in ".$self->{'result'}->{'totalruntime'}." seconds"; $rc = $self->{'exit_codes'}->{'OK'}; } if($self->{'result'}->{'iscritical'} or $self->{'result'}->{'iswarning'}) { $message .= "\n".$self->{'out'}; $message =~ s/^\-+$//mx; } if($self->{'config'}->{'reporttype'} eq 'nagios2') { $message =~ s/\n/
/mxg; } print $message.$perfdata."\n"; $self->{'result'}->{'perfdata'} = $perfdata; return $rc; } #MRTG plugin compatibility elsif( $self->{'config'}->{'reporttype'} eq 'mrtg' ) { #report results in MRTG format if( $self->{'result'}->{'totalcasesfailedcount'} > 0 ) { print "$self->{'result'}->{'totalruntime'}\n$self->{'result'}->{'totalruntime'}\n\nWebInject CRITICAL - $self->{'result'}->{'returnmessage'} \n"; } else { print "$self->{'result'}->{'totalruntime'}\n$self->{'result'}->{'totalruntime'}\n\nWebInject OK - All tests passed successfully in $self->{'result'}->{'totalruntime'} seconds \n"; } } #External plugin. To use it, add something like that in the config file: # external:/home/webinject/Plugin.pm elsif ( $self->{'config'}->{'reporttype'} =~ /^external:(.*)/mx ) { our $webinject = $self; # set scope of $self to global, so it can be access in the external module unless( my $return = do $1 ) { croak "couldn't parse $1: $@\n" if $@; croak "couldn't do $1: $!\n" unless defined $return; croak "couldn't run $1\n" unless $return; } } else { $self->_usage("ERROR: only 'nagios', 'nagios2', 'mrtg', 'external', or 'standard' are supported reporttype values"); } } return 1 if $self->{'result'}->{'totalcasesfailedcount'} > 0; return 0; } ################################################################################ # delete any files leftover from previous run if they exist sub _whackoldfiles { my $self = shift; for my $file (qw/plot.log plot.plt plot.png/) { unlink $self->{'config'}->{'output_dir'}.$file if -e $self->{'config'}->{'output_dir'}.$file; } # verify files are deleted, if not give the filesystem time to delete them before continuing while (-e $self->{'config'}->{'output_dir'}."plot.log" or -e $self->{'config'}->{'output_dir'}."plot.plt" or -e $self->{'config'}->{'output_dir'}."plot.png" ) { sleep .5; } return; } ################################################################################ # call the external plotter to create a graph (if we are in the appropriate mode) sub _plotit { my $self = shift; # do this unless: monitor is disabled in gui, or running standalone mode without config setting to turn on plotting if( ( $self->{'gui'} and $self->{'monitorenabledchkbx'} ne 'monitor_off') or (!$self->{'gui'} and $self->{'config'}->{'standaloneplot'} eq 'on') ) { # do this unless its being called from the gui with No Graph set unless ( $self->{'config'}->{'graphtype'} eq 'nograph' ) { my $gnuplot; if(defined $self->{'config'}->{gnuplot}) { $gnuplot = $self->{'config'}->{gnuplot} } elsif($^O eq 'MSWin32') { $gnuplot = "./wgnupl32.exe"; } else { $gnuplot = "/usr/bin/gnuplot"; } # if gnuplot exists if( -e $gnuplot ) { system $gnuplot, $self->{'config'}->{output_dir}."plot.plt"; # plot it } elsif( $self->{'gui'} ) { # if gnuplot not specified, notify on gui $self->_gui_no_plotter_found(); } } } return; } ################################################################################ # fix a user supplied regex to make it compliant with mx options sub _fix_regex { my $self = shift; my $regex = shift; $regex =~ s/\\\ / /mx; $regex =~ s/\ /\\ /gmx; return $regex; } ################################################################################ # command line options sub _getoptions { my $self = shift; my( @sets, $opt_version, $opt_help, $opt_configfile ); Getopt::Long::Configure('bundling'); my $opt_rc = GetOptions( 'h|help' => \$opt_help, 'v|V|version' => \$opt_version, 'c|config=s' => \$opt_configfile, 'o|output=s' => \$self->{'config'}->{'output_dir'}, 'n|no-output' => \$self->{'config'}->{'nooutput'}, 'r|report-type=s' => \$self->{'config'}->{'reporttype'}, 't|timeout=i' => \$self->{'config'}->{'timeout'}, 's=s' => \@sets, ); if(!$opt_rc or $opt_help) { $self->_usage(); } if($opt_version) { print "WebInject version $Webinject::VERSION\nFor more info: http://www.webinject.org\n"; exit 3; } $self->_read_config_xml($opt_configfile); for my $set (@sets) { my ( $key, $val ) = split /=/mx, $set, 2; if($key eq 'httpauth') { $self->_set_http_auth($val); } else { $self->{'config'}->{ lc $key } = $val; } } return; } ################################################################################ # _out - print text to STDOUT and save it for later retrieval sub _out { my $self = shift; my $text = shift; if($self->{'config'}->{'reporttype'} !~ /^nagios/mx and !$self->{'config'}->{'nooutput'}) { print $text; } $self->{'out'} .= $text; return; } ################################################################################ # print usage sub _usage { my $self = shift; my $text = shift; print $text."\n\n" if defined $text; print < detailed description about the syntax of testcases can be found on the Webinject homepage. =head1 SEE ALSO For more information about webinject visit http://www.webinject.org =head1 AUTHOR Corey Goldberg, Ecorey@goldb.orgE Sven Nierlein, Enierlein@cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright (C) 2010 by Sven Nierlein Copyright (C) 2004-2006 by Corey Goldberg This library is free software; you can redistribute it under the GPL2 license. =cut 1; #!/usr/bin/env perl # Copyright 2010 Sven Nierlein (nierlein@cpan.org) # Copyright 2004-2006 Corey Goldberg (corey@goldb.org) # # This file is part of WebInject. # # WebInject is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # WebInject is distributed in the hope that it will be useful, # but without any warranty; without even the implied warranty of # merchantability or fitness for a particular purpose. See the # GNU General Public License for more details. use warnings; use strict; my $webinject = Webinject->new(reporttype => "nagios", timeout => 30, break_on_errors => 1); my $rc = $webinject->engine(); exit $rc;