#!/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<Webinject> 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' => "<td>" }; # 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' => "<b>$case->{$_}</b><br />" };
    }
    my $method;
    if (defined $case->{method}) {
        $method = uc($case->{method});
    } else {
        $method = "GET";
    }
    push @{$case->{'messages'}}, { 'html' => qq|<small>$method <a href="$case->{url}">$case->{url}</a> </small><br />\n| };

    push @{$case->{'messages'}}, { 'html' => "</td><td>" }; # 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}."<br />" };
        }

        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}."<br />" };
        }

        # 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' => "</td><td>\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'  => "<b><span class=\"fail\">FAILED :</span> ".$message."</b>"
            };
            $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'  => "<b><span class=\"fail\">FAILED :</span> ".$case->{errormessage}."</b>"
            };
            $self->_out(qq|TEST CASE FAILED : $case->{errormessage}\n|);
        }
        else {
            push @{$case->{'messages'}}, {
                'key'   => 'result-message',
                'value' => 'TEST CASE FAILED',
                'html'  => "<b><span class=\"fail\">FAILED</span></b>"
            };
            $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' => "<b><span class=\"fail\">WARNED :</span> ".$case->{errormessage}."</b>" };
            $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' => "<b><span class=\"fail\">WARNED</span></b>" };
            $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' => "<b><span class=\"pass\">PASSED</span></b>"
        };
        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' => "<br />".$latency." sec </td>\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|<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>WebInject Test Results</title>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
    <style type="text/css">
        body {
            background-color: #F5F5F5;
            color: #000000;
            font-family: Verdana, Arial, Helvetica, sans-serif;
            font-size: 10px;
        }
        table, td {
            border:  solid #ddd 1px;
        }
        .pass {
            color: green;
        }
        .fail {
            color: red;
        }
    </style>
</head>
<body>
<table>
<tr>
<th>Test</th>
<th>Description<br />Request URL</th>
<th>Results</th>
<th>Summary<br />Response Time</th>
</tr>
|;
    for my $file (@{$self->{'result'}->{'files'}}) {
        for my $case (@{$file->{'cases'}}) {
            print $resultshtml qq|<tr><td>$file->{'name'}<br /><b>$case->{'id'} </b></td>\n|;
            for my $message (@{$case->{'messages'}}) {
                next unless defined $message->{'html'};
                print $resultshtml $message->{'html'} . "\n";
            }
            print $resultshtml "</tr>\n";
        }
    }

    print $resultshtml qq|
</table>
<b>
Start Time: $self->{'config'}->{'currentdatetime'} <br />
Total Run Time: $self->{'result'}->{'totalruntime'} seconds <br />
<br />
Test Cases Run: $self->{'result'}->{'totalruncount'} <br />
Test Cases Passed: $self->{'result'}->{'totalcasespassedcount'} <br />
Test Cases Failed: $self->{'result'}->{'totalcasesfailedcount'} <br />
Verifications Passed: $self->{'result'}->{'totalpassedcount'} <br />
Verifications Failed: $self->{'result'}->{'totalfailedcount'} <br />
<br />
Average Response Time: $self->{'result'}->{'avgresponse'} seconds <br />
Max Response Time: $self->{'result'}->{'maxresponse'} seconds <br />
Min Response Time: $self->{'result'}->{'minresponse'} seconds <br />
</b>
<br />

</body>
</html>
|;
    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 "<results>\n\n";

    for my $file (@{$self->{'result'}->{'files'}}) {
        print $resultsxml "    <testcases file=\"".$file->{'name'}."\">\n\n";
        for my $case (@{$file->{'cases'}}) {
            print $resultsxml "        <testcase id=\"".$case->{'id'}."\">\n";
            for my $message (@{$case->{'messages'}}) {
                next unless defined $message->{'key'};
                print $resultsxml "            <".$message->{'key'}.">".$message->{'value'}."</".$message->{'key'}.">\n";
            }
            print $resultsxml "        </testcase>\n\n";
        }
        print $resultsxml "    </testcases>\n";
    }

    print $resultsxml qq|
    <test-summary>
        <start-time>$self->{'config'}->{'currentdatetime'}</start-time>
        <total-run-time>$self->{'result'}->{'totalruntime'}</total-run-time>
        <test-cases-run>$self->{'result'}->{'totalruncount'}</test-cases-run>
        <test-cases-passed>$self->{'result'}->{'totalcasespassedcount'}</test-cases-passed>
        <test-cases-failed>$self->{'result'}->{'totalcasesfailedcount'}</test-cases-failed>
        <verifications-passed>$self->{'result'}->{'totalpassedcount'}</verifications-passed>
        <verifications-failed>$self->{'result'}->{'totalfailedcount'}</verifications-failed>
        <average-response-time>$self->{'result'}->{'avgresponse'}</average-response-time>
        <max-response-time>$self->{'result'}->{'maxresponse'}</max-response-time>
        <min-response-time>$self->{'result'}->{'minresponse'}</min-response-time>
    </test-summary>

</results>
|;
    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' => '<span class="pass">Passed XML Parser (content is well-formed)</span>' };
        $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' => "<span class=\"fail\">Failed XML parser on response:</span> ".$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' => '<span class="pass">Passed HTTP Response Code:</span> '.$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' => '<span class="fail">Failed HTTP Response Code:</span> 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' => '<span class="pass">Passed HTTP Response Code Verification (not in error range)</span>' };
            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' => '<span class="fail">Failed HTTP Response Code Verification ('.$1.$2.')</span>' };
                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' => '<span class="fail">Failed - No Response</span>' };
                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' => '<br />' };

    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' => "<span class=\"pass\">Passed:</span> ".$case->{$key} };
                $self->_out("Passed Positive Verification \n");
                $case->{'passedcount'}++;
            }
            else {
                push @{$case->{'messages'}}, {'key' => $key.'-success', 'value' => 'false', 'html' => "<span class=\"fail\">Failed:</span> ".$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' => '<br />' };
        }
        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' => '<span class="fail">Failed Negative:</span> '.$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' => '<span class="pass">Passed Negative:</span> '.$case->{$key} };
                $self->_out("Passed Negative Verification \n");
                $case->{'passedcount'}++;
            }
            push @{$case->{'messages'}}, { 'html' => '<br />' };
        }
        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' => '<span class="pass">Passed Positive Verification (verification set in previous test case)</span>' };
            $self->_out("Passed Positive Verification (verification set in previous test case) \n");
            $case->{'passedcount'}++;
        }
        else {
            push @{$case->{'messages'}}, {'key' => 'verifypositivenext-success', 'value' => 'false', 'html' => '<span class="fail">Failed Positive Verification (verification set in previous test case)</span>' };
            $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' => '<br />' };
        # 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' => '<span class="fail">Failed Negative Verification (negative verification set in previous test case)</span>' };
            $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' => '<span class="pass">Passed Negative Verification (negative verification set in previous test case)</span>' };
            $self->_out("Passed Negative Verification (negative verification set in previous test case) \n");
            $case->{'passedcount'}++;
        }
        push @{$case->{'messages'}}, { 'html' => '<br />' };
        # 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' => "<span class=\"fail\">Failed Warning Threshold:</span> ".$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' => "<span class=\"pass\">Passed Warning Threshold:</span> ".$case->{'warning'} };
            $case->{'passedcount'}++;
        }
        push @{$case->{'messages'}}, { 'html' => '<br />' };
    }

    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' => "<span class=\"fail\">Failed Critical Threshold:</span> ".$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' => "<span class=\"pass\">Passed Critical Threshold:</span> ".$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' => "<span class=\"fail\">Failed Parseresult, cannot find</span> $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~<comment>.*</comment>~mx) {    # single line comment
                                                     # multi-line comments
                if (/<comment>/mx) {
                    $comment_mode = 1;
                }
                elsif (m~</comment>~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>(.*)</$key>~mx;
                $self->{'config'}->{$key} = $1;

                #print "\n$_ : $self->{'config'}->{$_} \n\n";
            }
        }

        if (/<reporttype>/mx) {
            $_ =~ m~<reporttype>(.*)</reporttype>~mx;
            if ( $1 ne "standard" ) {
                $self->{'config'}->{'reporttype'} = $1;
                $self->{'config'}->{'nooutput'}   = "set";
            }

            #print "\nreporttype : $self->{'config'}->{'reporttype'} \n\n";
        }

        if (/<httpauth>/mx) {

            $_ =~ m~<httpauth>(.*)</httpauth>~mx;
            $self->_set_http_auth($1);

            #print "\nhttpauth : @{$self->{'config'}->{'httpauth'}} \n\n";
        }

        if(/<testcasefile>/mx) {
            my $firstparse = $';    #print "$' \n\n";
            $firstparse =~ m~</testcasefile>~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 <httpauth>, 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/\\</{LESSTHAN}/gmx;

        # convert variables to lowercase
        $line =~ s/(\$\{[\w\.]+\})/\L$1\E/gmx;
        $line =~ s/(varname=('|").*?('|"))/\L$1\E/gmx;

        # count cases while we are here
        if ( $line =~ /<case/mx ) {
            $self->{'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/<br>/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:
        # <reporttype>external:/home/webinject/Plugin.pm</reporttype>
        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 <<EOB;
    Usage:
      $0
                [-c|--config config_file]
                [-o|--output output_location]
                [-n|--no-output]
                [-t|--timeout]
                [-r|--report-type]
                [-s key=value]
                [testcase_file [XPath]]
      $0 --version|-v
EOB
    exit 3;
}

################################################################################
# make sure we don't keep the cookie temp file
END {
    our $cookietempfilename;
    unlink($cookietempfilename) if $cookietempfilename;
}

=head1 EXAMPLES

=head2 example test case

  <testcases>
    <case
      id             = "1"
      description1   = "Sample Test Case"
      method         = "get"
      url            = "{BASEURL}/test.jsp"
      verifypositive = "All tests succeded"
      warning        = "5"
      critical       = "15"
      label          = "testpage"
      errormessage   = "got error: {PARSERESPONSE}"
    />
  </testcases>

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, E<lt>corey@goldb.orgE<gt>

Sven Nierlein, E<lt>nierlein@cpan.orgE<gt>

=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;
