]> git.ipfire.org Git - ipfire-2.x.git/blob - html/cgi-bin/imspector.cgi
First integration of imspector, some more work to do.
[ipfire-2.x.git] / html / cgi-bin / imspector.cgi
1 #!/usr/bin/perl
2 #
3 # IMSpector real-time log viewer
4 # (c) SmoothWall Ltd 2008
5 #
6 # Released under the GPL v2.
7
8 use POSIX qw(strftime);
9
10 # Common configuration parameters.
11
12 my $logbase = "/var/log/imspector/";
13 my $oururl = '/cgi-bin/imspector.cgi';
14
15 # Colours
16
17 my $protocol_colour = '#06264d';
18 my $local_colour = '#1d398b';
19 my $remote_colour = '#2149c1';
20 my $conversation_colour = '#335ebe';
21
22 my $local_user_colour = 'blue';
23 my $remote_user_colour = 'green';
24
25 # No need to change anything from this point
26
27 # Page declaration, The following code should parse the CGI headers, and render the page
28 # accordingly... How you do this depends what environment you're in.
29
30 my %cgiparams;
31
32 print "Content-type: text/html\n";
33 print "\n";
34
35 if ($ENV{'QUERY_STRING'})
36 {
37 my @vars = split('\&', $ENV{'QUERY_STRING'});
38 foreach $_ (@vars)
39 {
40 my ($var, $val) = split(/\=/);
41 $cgiparams{$var} = $val;
42 }
43 }
44
45 # Act in Tail mode (as in just generate the raw logs and pass back to the other CGI
46
47 if ( defined $cgiparams{'mode'} and $cgiparams{'mode'} eq "render" ){
48 &parser( $cgiparams{'section'}, $cgiparams{'offset'}, $cgiparams{'conversation'}, $cgiparams{'skimhtml'} );
49 exit;
50 }
51
52 # Start rendering the Page using Express' rendering functions
53
54 my $script = &scriptheader();
55
56 # Print Some header information
57
58 print qq|
59 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
60 <html>
61 <head>
62 <title>IMSpector real-time log viewer</title>
63 $script
64 </head>
65 <body>
66 |;
67
68 print &pagebody();
69
70 # and now finish off the HTML page.
71
72 print qq|
73 </body>
74 </html>
75 |;
76
77 exit;
78
79 # -----------------------------------------------------------------------------
80 # ---------------------- IMSPector Log Viewer Code ----------------------------
81 # -----------------------------------------------------------------------------
82 # ^"^ ^"^
83
84 # Scriptheader
85 # ------------
86 # Return the bulk of the page, which should reside in the pages <head> field
87
88 sub scriptheader
89 {
90 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = localtime( time() );
91 $year += 1900; $mon++;
92 my $conversation = sprintf( "%.4d-%.2d-%.2d", $year, $mon, $mday );
93
94 my $script = qq {
95 <script language="Javascript">
96 var section ='none';
97 var moveit = 1;
98 var skimhtml = 1;
99 var the_timeout;
100 var offset = 0;
101 var fragment = "";
102 var conversationdate = "$conversation";
103
104 function xmlhttpPost()
105 {
106 var self = this;
107
108 if (window.XMLHttpRequest) {
109 // Mozilla/Safari
110 self.xmlHttpReq = new XMLHttpRequest();
111 } else if (window.ActiveXObject) {
112 // IE
113 self.xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
114 }
115
116 var url = "$url" + "?mode=render&section=" + section + "&skimhtml=" + skimhtml + "&offset=" + offset + "&conversation=" + conversationdate;
117 self.xmlHttpReq.open('POST', url, true);
118 self.xmlHttpReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
119
120 self.xmlHttpReq.onreadystatechange = function() {
121 if ( self.xmlHttpReq && self.xmlHttpReq.readyState == 4) {
122 updatepage(self.xmlHttpReq.responseText);
123 }
124 }
125
126 document.getElementById('status').style.display = "inline";
127
128 self.xmlHttpReq.send( url );
129 delete self;
130 }
131
132 function updatepage(str){
133 /* update the list of conversations ( if we need to ) */
134
135 var parts = str.split( "--END--\\n" );
136
137 var lines = parts[0].split( "\\n" );
138
139 for ( var line = 0 ; line < lines.length ; line ++ ){
140 var a = lines[line].split("|");
141
142 if ( !a[1] || !a[2] || !a[3] ){
143 continue;
144 }
145
146 /* convert the modification stamp into something sensible */
147 a[5] = parseInt( a[5] * 24 * 60 * 60 );
148
149 /* create titling information if needed */
150 if ( !document.getElementById( a[1] ) ){
151 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>";
152 }
153
154 if ( !document.getElementById( a[1] + "_" + a[2] ) ){
155 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>";
156 }
157
158 if ( !document.getElementById( a[1] + "_" + a[2] + "_" + a[3] ) ){
159 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] + "');" + '"' + "' + >&raquo;&nbsp;" + a[3] + "</div><div id='" + a[1] + "_" + a[2] + "_" + a[3] + "' style='width: 1%; display: none;'></div>";
160 }
161
162 if ( document.getElementById( a[1] + "_" + a[2] + "_" + a[3] ) && a[5] <= 60 ){
163 /* modified within the last minute! */
164 document.getElementById( a[1] + "_" + a[2] + "_" + a[3] + "_t" ).style.fontWeight = "bold";
165 } else {
166 document.getElementById( a[1] + "_" + a[2] + "_" + a[3] + "_t" ).style.fontWeight = "normal";
167 }
168 delete a;
169 }
170
171 delete lines;
172
173 /* rework the list of active conversation dates ... */
174
175 var lines = parts[1].split( "\\n" );
176
177 var the_select = document.getElementById('conversationdates');
178 the_select.options.length = 0;
179
180 for ( var line = 0 ; line < lines.length ; line ++ ){
181 if ( lines[ line ] != "" ){
182 the_select.options.length ++;
183 the_select.options[ line ].text = lines[line];
184 the_select.options[ line ].value = lines[line];
185 if ( lines[line] == conversationdate ){
186 the_select.selectedIndex = line;
187 }
188 }
189 }
190
191 delete the_select;
192 delete lines;
193
194 /* determine the title of this conversation */
195 if ( parts[2] ){
196 var details = parts[2].split(",");
197 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>";
198 if ( !details[1] ){
199 title = "&nbsp;";
200 }
201
202 document.getElementById('status').style.display = "none";
203
204 var bottom = parseInt( document.getElementById('content').scrollTop );
205 var bottom2 = parseInt( document.getElementById('content').style.height );
206 var absheight = parseInt( bottom + bottom2 );
207
208 if ( absheight == document.getElementById('content').scrollHeight ){
209 moveit = 1;
210 }
211
212 fragment += parts[4];
213 document.getElementById('content').innerHTML = "<table style='width: 100%'>" + fragment + "</table>";
214 if (moveit == 1 ){
215 document.getElementById('content').scrollTop = 0;
216 document.getElementById('content').scrollTop = document.getElementById('content').scrollHeight;
217 }
218
219 document.getElementById('content_title').innerHTML = title;
220 delete details;
221 delete title;
222 delete bottom;
223 delete bottom2;
224 delete absheight;
225 }
226
227 /* set the file offset */
228 offset = parts[3];
229
230 if ( moveit == 1 ){
231 document.getElementById('scrlck').style.color = 'green';
232 } else {
233 document.getElementById('scrlck').style.color = '#202020';
234 }
235
236 if ( skimhtml == 1 ){
237 document.getElementById('skimhtml').style.color = 'green';
238 } else {
239 document.getElementById('skimhtml').style.color = '#202020';
240 }
241
242 delete parts;
243
244 the_timeout = setTimeout( "xmlhttpPost();", 5000 );
245 }
246
247 function setsection( value )
248 {
249 section = value;
250 offset = 0;
251 fragment = "";
252 moveit = 1;
253 clearTimeout(the_timeout);
254 xmlhttpPost();
255 document.getElementById('content').scrollTop = 0;
256 document.getElementById('content').scrollTop = document.getElementById('content').scrollHeight;
257 }
258
259 function togglescrlck()
260 {
261 if ( moveit == 1 ){
262 moveit = 0;
263 document.getElementById('scrlck').style.color = '#202020';
264 } else {
265 moveit = 1;
266 document.getElementById('scrlck').style.color = 'green';
267 }
268 }
269
270 function toggleskimhtml()
271 {
272 if ( skimhtml == 1 ){
273 skimhtml = 0;
274 document.getElementById('skimhtml').style.color = '#202020';
275 } else {
276 skimhtml = 1;
277 document.getElementById('skimhtml').style.color = 'green';
278 }
279 clearTimeout(the_timeout);
280 xmlhttpPost();
281 }
282
283 function setDate()
284 {
285 var the_select = document.getElementById('conversationdates');
286 conversationdate = the_select.options[ the_select.selectedIndex ].value;
287 document.getElementById('conversations').innerHTML = "";
288 fragment = "";
289 offset = 0;
290 section = "";
291 clearTimeout(the_timeout);
292 xmlhttpPost();
293 }
294
295 </script>
296 };
297
298 return $script;
299 }
300
301 # pagebody function
302 # -----------------
303 # Return the HTML fragment which includes the page body.
304
305 sub pagebody
306 {
307 my $body = qq {
308 <div style='width: 100%; text-align: right;'><span id='status' style='background-color: #fef1b5; display: none;'>Updating</span>&nbsp;</div>
309 <style>
310
311 .powerbutton {
312 color: #202020;
313 font-size: 9pt;
314 cursor: pointer;
315 }
316
317 .remoteuser {
318 color: $remote_user_colour;
319 font-size: 9pt;
320 }
321
322 .localuser {
323 color: $local_user_colour;
324 font-size: 9pt;
325 }
326
327 </style>
328 <table style='width: 100%;'>
329 <tr>
330 <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>
331 <td style='border: solid 1px #c0c0c0;'>
332 <div id='content_title' style='height: 20px; overflow: auto; vertical-align: top; background-color: #E6E8FA; border-bottom: solid 1px #c0c0c0;'></div>
333 <div id='content' style='height: 376px; overflow: auto; vertical-align: bottom; border-bottom: solid 1px #c0c0c0; overflow-x: hidden;'></div>
334 <div id='content_subtitle' style='height: 24px; overflow: auto; vertical-align: top; background-color: #E6E8FA; width: 100%; padding: 2px;'>
335 <div style='width: 60%; float: left;' id='statuswindow'>
336 For conversations on:&nbsp;
337 <select id='conversationdates' onChange='setDate()';>
338 </select>
339 </div>
340 <div style='width: 40%; text-align: right; float: right;'>
341 <span class='powerbutton' id='skimhtml' onClick='toggleskimhtml();'>[HTML]</span>
342 <span class='powerbutton' id='scrlck' onClick='togglescrlck();'>[SCROLL LOCK]</span>
343 </div>
344 </div>
345 </td>
346 </tr>
347 </table>
348 <script>xmlhttpPost();</script>
349 };
350 return $body;
351 }
352
353 # Parser function ...
354 # ---------------
355 # Retrieves the IMspector logs from their nestling place and displays them accordingly.
356
357 sub parser
358 {
359 my ( $section, $offset, $conversationdate, $skimhtml ) = @_;
360 # render the user list ...
361
362 chomp $offset;
363
364 unless ( $offset =~ /^([\d]*)$/ ){
365 print STDERR "Illegal offset ($offset $1) resetting...\n";
366 $offset = 0;
367 }
368
369 # browse for the available protocols
370 unless ( opendir DIR, $logbase ){
371 exit;
372 }
373
374 my %conversationaldates;
375 my @protocols = grep {!/^\./} readdir(DIR);
376
377 foreach my $protocol ( @protocols ){
378 unless ( opendir LUSER, "$logbase$protocol" ){
379 next;
380 }
381
382 my @localusers = grep {!/^\./} readdir(LUSER);
383 foreach my $localuser ( @localusers ){
384 unless ( opendir RUSER, "$logbase$protocol/$localuser/" ){
385 next;
386 }
387 my @remoteusers = grep {!/^\./} readdir( RUSER );
388 foreach my $remoteuser ( @remoteusers ){
389 unless ( opendir CONVERSATIONS, "$logbase$protocol/$localuser/$remoteuser/" ){
390 next;
391 }
392 my @conversations = grep {!/^\./} readdir( CONVERSATIONS );
393 foreach my $conversation ( @conversations ){
394 $conversationaldates{ $conversation } = $localuser;
395 }
396
397 closedir CONVERSATIONS;
398
399 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = localtime( time() );
400 $year += 1900; $mon++;
401 my $conversation = sprintf( "%.4d-%.2d-%.2d", $year, $mon, $mday );
402
403 $conversation = $conversationdate if ( defined $conversationdate and $conversationdate ne "" );
404
405 if ( -e "$logbase$protocol/$localuser/$remoteuser/$conversation" ){
406 my $modi = -M "$logbase$protocol/$localuser/$remoteuser/$conversation";
407 print "|$protocol|$localuser|$remoteuser|$conversation|$modi\n";
408 }
409 }
410 closedir RUSER;
411 }
412 closedir LUSER;
413 }
414 closedir DIR;
415
416 print "--END--\n";
417
418 # display a list of conversational dates .. i.e. the dates which we have conversations on.
419 foreach my $key ( sort keys %conversationaldates ){
420 print "$key\n";
421 }
422
423 print "--END--\n";
424
425
426 # now check the log file ...
427
428 if ( $section ne "none" ){
429 my ( $protocol, $localuser, $remoteuser, $conversation ) = split /\|/, $section;
430
431 print "$protocol, $localuser, $remoteuser, $conversation\n";
432 print "--END--\n";
433
434 my $filename = "$logbase$protocol/$localuser/$remoteuser/$conversation";
435
436 unless ( open(FD, "$filename" ) ){
437 exit;
438 };
439
440 # perform some *reasonably* complicated file hopping and stuff of that ilk.
441 # it's not beyond reason that logs *could* be extremely large, so what we
442 # should do to speed up their processing is to jump to the end of the file,
443 # then backtrack a little (say a meg, which is a reasonably amount of logs)
444 # and parse from that point onwards. This, *post* filtering might of course
445 # not leave us with the desired resolution for the tail. If this is the case,
446 # we keep that array and jump back another meg and have another go, concatinating
447 # the logs as we go.... <wheh>
448
449 my $jumpback = 100000; # not quite a meg, but hey ho
450 my $goneback = 0;
451 my $gonebacklimit = 1000000000; # don't go back more than 100MB
452
453 # firstly jump to the end of the file.
454 seek( FD, 0, 2 );
455
456 my $log_position = tell( FD );
457 my $end = $log_position;
458 my $end_position = $log_position;
459
460 my $lines;
461 my @content;
462
463 my $TAILSIZE = 100;
464
465 do {
466 $end_position = $log_position;
467
468 if ( $offset != 0 ){
469 # we were given a hint as to where we should have been anyhow ..
470 # so we might as well use that to go back to.
471 $log_position = $offset;
472 $goneback = $end_position - $log_position;
473 } else {
474 $log_position -= $jumpback;
475 $goneback += $jumpback;
476 }
477
478 last if ( $goneback > $gonebacklimit );
479
480 if ( $log_position > 0 ){
481 seek( FD, $log_position, 0 );
482 } else {
483 seek( FD, 0, 0 );
484 }
485
486 my @newcontent;
487
488 while ( my $line = <FD> and ( tell( FD ) <= $end_position ) ){
489 chomp $line;
490 push @content, $line;
491 }
492 shift @content if $#content >= $TAILSIZE;
493 } while ( $#content < $TAILSIZE and $log_position > 0 and $offset == 0 );
494
495 # trim the content down as we may have more entries than we should.
496
497 while ( $#content > $TAILSIZE ){ shift @content; };
498 close FD;
499
500 print "$end_position\n--END--\n";
501
502 foreach my $line ( @content ){
503 my ( $address, $timestamp, $direction, $type, $filtered, $cat, $data );
504
505 ( $address, $timestamp, $direction, $type, $filtered, $cat, $data ) = ( $line =~ /([^,]*),(\d+),(\d+),(\d+),(\d+),([^,]*),(.*)/ );
506
507 # are we using the oldstyle or new style logs ?
508 if ( not defined $address and not defined $timestamp ){
509 ( $address, $timestamp, $type, $data ) = ( $line =~ /([^,]*),([^,]*),([^,]*),(.*)/ );
510 if ( $type eq "1" ){
511 $direction = 0;
512 $type = 1;
513 } elsif ( $type eq "2" ){
514 $direction = 1;
515 $type = 1;
516 } elsif ( $type eq "3" ){
517 $direction = 0;
518 $type = 2;
519 } elsif ( $type eq "4" ){
520 $direction = 1;
521 $type = 4;
522 }
523 }
524
525 my ( $severity, $classification ) = '0', 'None';
526 if ($cat) {
527 ( $severity, $classification) = split(/ /, $cat, 2); }
528 else {
529 $cat = 'N/A'; }
530
531 my $red = 255;
532 my $green = 255;
533 my $blue = 255;
534
535 if ($severity < 0 && $severity >= -5) {
536 $red = 0; $green = abs($severity) * (255 / 5); $blue = 0; }
537 elsif ($severity > 0 && $severity <= 5) {
538 $red = $severity * (255 / 5); $green = 0; $blue = 0; }
539 else {
540 $red = 0; $green = 0; $blue = 0; }
541
542 my $severitycolour = '';
543 if ($cat ne 'N/A') {
544 $severitycolour = sprintf("background-color: #%02x%02x%02x;", $red, $green, $blue); }
545
546 # some protocols (ICQ, I'm looking in your direction) have a habit of starting
547 # and ending each sentence with HTML (evil program)
548
549 if ( defined $skimhtml and $skimhtml eq "1" ){
550 $data =~ s/^<HTML><BODY[^>]*><FONT[^>]*>//ig;
551 $data =~ s/<\/FONT><\/BODY><\/HTML>//ig;
552 }
553
554 $data = &htmlescape($data);
555 $data =~ s/\r\\n/<br>\n/g;
556 my $user = "";
557
558 my $bstyle = "";
559 $bstyle = "style='background-color: #FFE4E1;'" if ( $filtered eq "1" );
560
561 if ( $type eq "1" ){
562 # a message message (from remote user)
563 if ( $direction eq "0" ){
564 # incoming
565 my $u = $remoteuser;
566 $u =~ s/\@.*//g;
567 $user = "&lt;<span class='remoteuser'>$u</span>&gt;";
568 } else {
569 # outgoing message
570 my $u = $localuser;
571 $u =~ s/\@.*//g;
572 $user = "&lt;<span class='localuser'>$u</span>&gt;";
573 }
574 } elsif ($type eq "2") {
575 if ( $direction eq "0" ){
576 # incoming file
577 my $u = $remoteuser;
578 $u =~ s/\@.*//g;
579 $user = "&lt;<span class='remoteuser'><b><i>$u</i></b></span>&gt;";
580 } else {
581 # outgoing file
582 my $u = $localuser;
583 $u =~ s/\@.*//g;
584 $user = "&lt;<span class='localuser'><b><i>$u</i></b></span>&gt;";
585 }
586 }
587
588 my $t = strftime "%H:%M:%S", localtime($timestamp);
589 if ($type eq "3" or $type eq "4") {
590 $data = "<b><i>$data</i></b>";
591 }
592 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>";
593 }
594 }
595 return;
596 }
597
598 sub htmlescape
599 {
600 my ($value) = @_;
601 $value =~ s/&/\&amp;/g;
602 $value =~ s/</\&lt;/g;
603 $value =~ s/>/\&gt;/g;
604 $value =~ s/"/\&quot;/g;
605 $value =~ s/'/\&#39;/g;
606 return $value;
607 }