--- /dev/null
+#!/usr/bin/perl
+#
+# IMSpector real-time log viewer
+# (c) SmoothWall Ltd 2008
+#
+# Released under the GPL v2.
+
+use POSIX qw(strftime);
+
+# Common configuration parameters.
+
+my $logbase = "/var/log/imspector/";
+my $oururl = '/cgi-bin/imspector.cgi';
+
+# Colours
+
+my $protocol_colour = '#06264d';
+my $local_colour = '#1d398b';
+my $remote_colour = '#2149c1';
+my $conversation_colour = '#335ebe';
+
+my $local_user_colour = 'blue';
+my $remote_user_colour = 'green';
+
+# No need to change anything from this point
+
+# Page declaration, The following code should parse the CGI headers, and render the page
+# accordingly... How you do this depends what environment you're in.
+
+my %cgiparams;
+
+print "Content-type: text/html\n";
+print "\n";
+
+if ($ENV{'QUERY_STRING'})
+{
+ my @vars = split('\&', $ENV{'QUERY_STRING'});
+ foreach $_ (@vars)
+ {
+ my ($var, $val) = split(/\=/);
+ $cgiparams{$var} = $val;
+ }
+}
+
+# Act in Tail mode (as in just generate the raw logs and pass back to the other CGI
+
+if ( defined $cgiparams{'mode'} and $cgiparams{'mode'} eq "render" ){
+ &parser( $cgiparams{'section'}, $cgiparams{'offset'}, $cgiparams{'conversation'}, $cgiparams{'skimhtml'} );
+ exit;
+}
+
+# Start rendering the Page using Express' rendering functions
+
+my $script = &scriptheader();
+
+# Print Some header information
+
+print qq|
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+ <title>IMSpector real-time log viewer</title>
+ $script
+</head>
+<body>
+|;
+
+print &pagebody();
+
+# and now finish off the HTML page.
+
+print qq|
+</body>
+</html>
+|;
+
+exit;
+
+# -----------------------------------------------------------------------------
+# ---------------------- IMSPector Log Viewer Code ----------------------------
+# -----------------------------------------------------------------------------
+# ^"^ ^"^
+
+# Scriptheader
+# ------------
+# Return the bulk of the page, which should reside in the pages <head> field
+
+sub scriptheader
+{
+ my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = localtime( time() );
+ $year += 1900; $mon++;
+ my $conversation = sprintf( "%.4d-%.2d-%.2d", $year, $mon, $mday );
+
+ my $script = qq {
+<script language="Javascript">
+var section ='none';
+var moveit = 1;
+var skimhtml = 1;
+var the_timeout;
+var offset = 0;
+var fragment = "";
+var conversationdate = "$conversation";
+
+function xmlhttpPost()
+{
+ var self = this;
+
+ if (window.XMLHttpRequest) {
+ // Mozilla/Safari
+ self.xmlHttpReq = new XMLHttpRequest();
+ } else if (window.ActiveXObject) {
+ // IE
+ self.xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
+ }
+
+ var url = "$url" + "?mode=render§ion=" + section + "&skimhtml=" + skimhtml + "&offset=" + offset + "&conversation=" + conversationdate;
+ self.xmlHttpReq.open('POST', url, true);
+ self.xmlHttpReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+
+ self.xmlHttpReq.onreadystatechange = function() {
+ if ( self.xmlHttpReq && self.xmlHttpReq.readyState == 4) {
+ updatepage(self.xmlHttpReq.responseText);
+ }
+ }
+
+ document.getElementById('status').style.display = "inline";
+
+ self.xmlHttpReq.send( url );
+ delete self;
+}
+
+function updatepage(str){
+ /* update the list of conversations ( if we need to ) */
+
+ var parts = str.split( "--END--\\n" );
+
+ var lines = parts[0].split( "\\n" );
+
+ for ( var line = 0 ; line < lines.length ; line ++ ){
+ var a = lines[line].split("|");
+
+ if ( !a[1] || !a[2] || !a[3] ){
+ continue;
+ }
+
+ /* convert the modification stamp into something sensible */
+ a[5] = parseInt( a[5] * 24 * 60 * 60 );
+
+ /* create titling information if needed */
+ if ( !document.getElementById( a[1] ) ){
+ document.getElementById('conversations').innerHTML += "<div id='" + a[1] + "_t' style='width: 100%; background-color: #d9d9f3; color: $protocol_colour;'>" + a[1] + "</div><div id='" + a[1] + "' style='width: 100%; background-color: #e5e5f3;'></div>";
+ }
+
+ if ( !document.getElementById( a[1] + "_" + a[2] ) ){
+ document.getElementById( a[1] ).innerHTML += "<div id='" + a[1] + "_" + a[2] + "_t' style='width: 100%; color: $local_colour; padding-left: 5px;'>" + a[2] + "</div><div id='" + a[1] + "_" + a[2] + "' style='width: 100%; background-color: #efeffa; border-bottom: solid 1px #d9d9f3;'></div>";
+ }
+
+ if ( !document.getElementById( a[1] + "_" + a[2] + "_" + a[3] ) ){
+ document.getElementById( a[1] + "_" + a[2] ).innerHTML += "<div id='" + a[1] + "_" + a[2] + "_" + a[3] + "_t' style='width: 100%; color: $remote_colour; padding-left: 10px; cursor: pointer;' onClick=" + '"' + "setsection('" + a[1] + "|" + a[2] + "|" + a[3] + "|" + a[4] + "');" + '"' + "' + >» " + a[3] + "</div><div id='" + a[1] + "_" + a[2] + "_" + a[3] + "' style='width: 1%; display: none;'></div>";
+ }
+
+ if ( document.getElementById( a[1] + "_" + a[2] + "_" + a[3] ) && a[5] <= 60 ){
+ /* modified within the last minute! */
+ document.getElementById( a[1] + "_" + a[2] + "_" + a[3] + "_t" ).style.fontWeight = "bold";
+ } else {
+ document.getElementById( a[1] + "_" + a[2] + "_" + a[3] + "_t" ).style.fontWeight = "normal";
+ }
+ delete a;
+ }
+
+ delete lines;
+
+ /* rework the list of active conversation dates ... */
+
+ var lines = parts[1].split( "\\n" );
+
+ var the_select = document.getElementById('conversationdates');
+ the_select.options.length = 0;
+
+ for ( var line = 0 ; line < lines.length ; line ++ ){
+ if ( lines[ line ] != "" ){
+ the_select.options.length ++;
+ the_select.options[ line ].text = lines[line];
+ the_select.options[ line ].value = lines[line];
+ if ( lines[line] == conversationdate ){
+ the_select.selectedIndex = line;
+ }
+ }
+ }
+
+ delete the_select;
+ delete lines;
+
+ /* determine the title of this conversation */
+ if ( parts[2] ){
+ var details = parts[2].split(",");
+ var title = details[0] + " conversation between <span style='color: $local_user_colour;'>" + details[ 1 ] + "</span> and <span style='color: $remote_user_colour;'>" + details[2] + "</span>";
+ if ( !details[1] ){
+ title = " ";
+ }
+
+ document.getElementById('status').style.display = "none";
+
+ var bottom = parseInt( document.getElementById('content').scrollTop );
+ var bottom2 = parseInt( document.getElementById('content').style.height );
+ var absheight = parseInt( bottom + bottom2 );
+
+ if ( absheight == document.getElementById('content').scrollHeight ){
+ moveit = 1;
+ }
+
+ fragment += parts[4];
+ document.getElementById('content').innerHTML = "<table style='width: 100%'>" + fragment + "</table>";
+ if (moveit == 1 ){
+ document.getElementById('content').scrollTop = 0;
+ document.getElementById('content').scrollTop = document.getElementById('content').scrollHeight;
+ }
+
+ document.getElementById('content_title').innerHTML = title;
+ delete details;
+ delete title;
+ delete bottom;
+ delete bottom2;
+ delete absheight;
+ }
+
+ /* set the file offset */
+ offset = parts[3];
+
+ if ( moveit == 1 ){
+ document.getElementById('scrlck').style.color = 'green';
+ } else {
+ document.getElementById('scrlck').style.color = '#202020';
+ }
+
+ if ( skimhtml == 1 ){
+ document.getElementById('skimhtml').style.color = 'green';
+ } else {
+ document.getElementById('skimhtml').style.color = '#202020';
+ }
+
+ delete parts;
+
+ the_timeout = setTimeout( "xmlhttpPost();", 5000 );
+}
+
+function setsection( value )
+{
+ section = value;
+ offset = 0;
+ fragment = "";
+ moveit = 1;
+ clearTimeout(the_timeout);
+ xmlhttpPost();
+ document.getElementById('content').scrollTop = 0;
+ document.getElementById('content').scrollTop = document.getElementById('content').scrollHeight;
+}
+
+function togglescrlck()
+{
+ if ( moveit == 1 ){
+ moveit = 0;
+ document.getElementById('scrlck').style.color = '#202020';
+ } else {
+ moveit = 1;
+ document.getElementById('scrlck').style.color = 'green';
+ }
+}
+
+function toggleskimhtml()
+{
+ if ( skimhtml == 1 ){
+ skimhtml = 0;
+ document.getElementById('skimhtml').style.color = '#202020';
+ } else {
+ skimhtml = 1;
+ document.getElementById('skimhtml').style.color = 'green';
+ }
+ clearTimeout(the_timeout);
+ xmlhttpPost();
+}
+
+function setDate()
+{
+ var the_select = document.getElementById('conversationdates');
+ conversationdate = the_select.options[ the_select.selectedIndex ].value;
+ document.getElementById('conversations').innerHTML = "";
+ fragment = "";
+ offset = 0;
+ section = "";
+ clearTimeout(the_timeout);
+ xmlhttpPost();
+}
+
+</script>
+ };
+
+ return $script;
+}
+
+# pagebody function
+# -----------------
+# Return the HTML fragment which includes the page body.
+
+sub pagebody
+{
+ my $body = qq {
+ <div style='width: 100%; text-align: right;'><span id='status' style='background-color: #fef1b5; display: none;'>Updating</span> </div>
+ <style>
+
+.powerbutton {
+ color: #202020;
+ font-size: 9pt;
+ cursor: pointer;
+}
+
+.remoteuser {
+ color: $remote_user_colour;
+ font-size: 9pt;
+}
+
+.localuser {
+ color: $local_user_colour;
+ font-size: 9pt;
+}
+
+ </style>
+ <table style='width: 100%;'>
+ <tr>
+ <td style='width: 170px; text-align: left; vertical-align: top; overflow: auto; font-size: 8pt; border: solid 1px #c0c0c0;'><div id='conversations' style='height: 400px; overflow: auto; font-size: 10px; overflow-x: hidden;'></div></td>
+ <td style='border: solid 1px #c0c0c0;'>
+ <div id='content_title' style='height: 20px; overflow: auto; vertical-align: top; background-color: #E6E8FA; border-bottom: solid 1px #c0c0c0;'></div>
+ <div id='content' style='height: 376px; overflow: auto; vertical-align: bottom; border-bottom: solid 1px #c0c0c0; overflow-x: hidden;'></div>
+ <div id='content_subtitle' style='height: 24px; overflow: auto; vertical-align: top; background-color: #E6E8FA; width: 100%; padding: 2px;'>
+ <div style='width: 60%; float: left;' id='statuswindow'>
+ For conversations on:
+ <select id='conversationdates' onChange='setDate()';>
+ </select>
+ </div>
+ <div style='width: 40%; text-align: right; float: right;'>
+ <span class='powerbutton' id='skimhtml' onClick='toggleskimhtml();'>[HTML]</span>
+ <span class='powerbutton' id='scrlck' onClick='togglescrlck();'>[SCROLL LOCK]</span>
+ </div>
+ </div>
+ </td>
+ </tr>
+ </table>
+ <script>xmlhttpPost();</script>
+ };
+ return $body;
+}
+
+# Parser function ...
+# ---------------
+# Retrieves the IMspector logs from their nestling place and displays them accordingly.
+
+sub parser
+{
+ my ( $section, $offset, $conversationdate, $skimhtml ) = @_;
+ # render the user list ...
+
+ chomp $offset;
+
+ unless ( $offset =~ /^([\d]*)$/ ){
+ print STDERR "Illegal offset ($offset $1) resetting...\n";
+ $offset = 0;
+ }
+
+ # browse for the available protocols
+ unless ( opendir DIR, $logbase ){
+ exit;
+ }
+
+ my %conversationaldates;
+ my @protocols = grep {!/^\./} readdir(DIR);
+
+ foreach my $protocol ( @protocols ){
+ unless ( opendir LUSER, "$logbase$protocol" ){
+ next;
+ }
+
+ my @localusers = grep {!/^\./} readdir(LUSER);
+ foreach my $localuser ( @localusers ){
+ unless ( opendir RUSER, "$logbase$protocol/$localuser/" ){
+ next;
+ }
+ my @remoteusers = grep {!/^\./} readdir( RUSER );
+ foreach my $remoteuser ( @remoteusers ){
+ unless ( opendir CONVERSATIONS, "$logbase$protocol/$localuser/$remoteuser/" ){
+ next;
+ }
+ my @conversations = grep {!/^\./} readdir( CONVERSATIONS );
+ foreach my $conversation ( @conversations ){
+ $conversationaldates{ $conversation } = $localuser;
+ }
+
+ closedir CONVERSATIONS;
+
+ my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = localtime( time() );
+ $year += 1900; $mon++;
+ my $conversation = sprintf( "%.4d-%.2d-%.2d", $year, $mon, $mday );
+
+ $conversation = $conversationdate if ( defined $conversationdate and $conversationdate ne "" );
+
+ if ( -e "$logbase$protocol/$localuser/$remoteuser/$conversation" ){
+ my $modi = -M "$logbase$protocol/$localuser/$remoteuser/$conversation";
+ print "|$protocol|$localuser|$remoteuser|$conversation|$modi\n";
+ }
+ }
+ closedir RUSER;
+ }
+ closedir LUSER;
+ }
+ closedir DIR;
+
+ print "--END--\n";
+
+ # display a list of conversational dates .. i.e. the dates which we have conversations on.
+ foreach my $key ( sort keys %conversationaldates ){
+ print "$key\n";
+ }
+
+ print "--END--\n";
+
+
+ # now check the log file ...
+
+ if ( $section ne "none" ){
+ my ( $protocol, $localuser, $remoteuser, $conversation ) = split /\|/, $section;
+
+ print "$protocol, $localuser, $remoteuser, $conversation\n";
+ print "--END--\n";
+
+ my $filename = "$logbase$protocol/$localuser/$remoteuser/$conversation";
+
+ unless ( open(FD, "$filename" ) ){
+ exit;
+ };
+
+ # perform some *reasonably* complicated file hopping and stuff of that ilk.
+ # it's not beyond reason that logs *could* be extremely large, so what we
+ # should do to speed up their processing is to jump to the end of the file,
+ # then backtrack a little (say a meg, which is a reasonably amount of logs)
+ # and parse from that point onwards. This, *post* filtering might of course
+ # not leave us with the desired resolution for the tail. If this is the case,
+ # we keep that array and jump back another meg and have another go, concatinating
+ # the logs as we go.... <wheh>
+
+ my $jumpback = 100000; # not quite a meg, but hey ho
+ my $goneback = 0;
+ my $gonebacklimit = 1000000000; # don't go back more than 100MB
+
+ # firstly jump to the end of the file.
+ seek( FD, 0, 2 );
+
+ my $log_position = tell( FD );
+ my $end = $log_position;
+ my $end_position = $log_position;
+
+ my $lines;
+ my @content;
+
+ my $TAILSIZE = 100;
+
+ do {
+ $end_position = $log_position;
+
+ if ( $offset != 0 ){
+ # we were given a hint as to where we should have been anyhow ..
+ # so we might as well use that to go back to.
+ $log_position = $offset;
+ $goneback = $end_position - $log_position;
+ } else {
+ $log_position -= $jumpback;
+ $goneback += $jumpback;
+ }
+
+ last if ( $goneback > $gonebacklimit );
+
+ if ( $log_position > 0 ){
+ seek( FD, $log_position, 0 );
+ } else {
+ seek( FD, 0, 0 );
+ }
+
+ my @newcontent;
+
+ while ( my $line = <FD> and ( tell( FD ) <= $end_position ) ){
+ chomp $line;
+ push @content, $line;
+ }
+ shift @content if $#content >= $TAILSIZE;
+ } while ( $#content < $TAILSIZE and $log_position > 0 and $offset == 0 );
+
+ # trim the content down as we may have more entries than we should.
+
+ while ( $#content > $TAILSIZE ){ shift @content; };
+ close FD;
+
+ print "$end_position\n--END--\n";
+
+ foreach my $line ( @content ){
+ my ( $address, $timestamp, $direction, $type, $filtered, $cat, $data );
+
+ ( $address, $timestamp, $direction, $type, $filtered, $cat, $data ) = ( $line =~ /([^,]*),(\d+),(\d+),(\d+),(\d+),([^,]*),(.*)/ );
+
+ # are we using the oldstyle or new style logs ?
+ if ( not defined $address and not defined $timestamp ){
+ ( $address, $timestamp, $type, $data ) = ( $line =~ /([^,]*),([^,]*),([^,]*),(.*)/ );
+ if ( $type eq "1" ){
+ $direction = 0;
+ $type = 1;
+ } elsif ( $type eq "2" ){
+ $direction = 1;
+ $type = 1;
+ } elsif ( $type eq "3" ){
+ $direction = 0;
+ $type = 2;
+ } elsif ( $type eq "4" ){
+ $direction = 1;
+ $type = 4;
+ }
+ }
+
+ my ( $severity, $classification ) = '0', 'None';
+ if ($cat) {
+ ( $severity, $classification) = split(/ /, $cat, 2); }
+ else {
+ $cat = 'N/A'; }
+
+ my $red = 255;
+ my $green = 255;
+ my $blue = 255;
+
+ if ($severity < 0 && $severity >= -5) {
+ $red = 0; $green = abs($severity) * (255 / 5); $blue = 0; }
+ elsif ($severity > 0 && $severity <= 5) {
+ $red = $severity * (255 / 5); $green = 0; $blue = 0; }
+ else {
+ $red = 0; $green = 0; $blue = 0; }
+
+ my $severitycolour = '';
+ if ($cat ne 'N/A') {
+ $severitycolour = sprintf("background-color: #%02x%02x%02x;", $red, $green, $blue); }
+
+ # some protocols (ICQ, I'm looking in your direction) have a habit of starting
+ # and ending each sentence with HTML (evil program)
+
+ if ( defined $skimhtml and $skimhtml eq "1" ){
+ $data =~ s/^<HTML><BODY[^>]*><FONT[^>]*>//ig;
+ $data =~ s/<\/FONT><\/BODY><\/HTML>//ig;
+ }
+
+ $data = &htmlescape($data);
+ $data =~ s/\r\\n/<br>\n/g;
+ my $user = "";
+
+ my $bstyle = "";
+ $bstyle = "style='background-color: #FFE4E1;'" if ( $filtered eq "1" );
+
+ if ( $type eq "1" ){
+ # a message message (from remote user)
+ if ( $direction eq "0" ){
+ # incoming
+ my $u = $remoteuser;
+ $u =~ s/\@.*//g;
+ $user = "<<span class='remoteuser'>$u</span>>";
+ } else {
+ # outgoing message
+ my $u = $localuser;
+ $u =~ s/\@.*//g;
+ $user = "<<span class='localuser'>$u</span>>";
+ }
+ } elsif ($type eq "2") {
+ if ( $direction eq "0" ){
+ # incoming file
+ my $u = $remoteuser;
+ $u =~ s/\@.*//g;
+ $user = "<<span class='remoteuser'><b><i>$u</i></b></span>>";
+ } else {
+ # outgoing file
+ my $u = $localuser;
+ $u =~ s/\@.*//g;
+ $user = "<<span class='localuser'><b><i>$u</i></b></span>>";
+ }
+ }
+
+ my $t = strftime "%H:%M:%S", localtime($timestamp);
+ if ($type eq "3" or $type eq "4") {
+ $data = "<b><i>$data</i></b>";
+ }
+ print "<tr $bstyle><td style='width: 30px; vertical-align: top;'>[$t]</td><td style='width: 10px; $severitycolour' title='$cat'><td style=' width: 60px; vertical-align: top;'>$user</td><td style='vertical-align: top;'>$data</td></tr>";
+ }
+ }
+ return;
+}
+
+sub htmlescape
+{
+ my ($value) = @_;
+ $value =~ s/&/\&/g;
+ $value =~ s/</\</g;
+ $value =~ s/>/\>/g;
+ $value =~ s/"/\"/g;
+ $value =~ s/'/\'/g;
+ return $value;
+}