From 1e58c77e53505ce3ed7b8e3719ccb6653f8d228a Mon Sep 17 00:00:00 2001
From: Olav Morken <olav.morken@uninett.no>
Date: Fri, 24 Apr 2009 07:32:10 +0000
Subject: [PATCH] Impemented logpeek to do blockwise reading of logfile for
 faster execution.

Patch by Thomas Graff <thomas.graff@uninett.no>

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@1474 44740490-163a-0410-bde0-09ae8108e29a
---
 .../config-templates/module_logpeek.php       |   2 +
 modules/logpeek/lib/File/reverseRead.php      | 224 ++++++++++++++++++
 modules/logpeek/lib/Syslog/parseLine.php      |  25 ++
 modules/logpeek/templates/logpeek.php         |  16 +-
 modules/logpeek/www/index.php                 |  66 +++---
 5 files changed, 290 insertions(+), 43 deletions(-)
 create mode 100644 modules/logpeek/lib/File/reverseRead.php
 create mode 100644 modules/logpeek/lib/Syslog/parseLine.php

diff --git a/modules/logpeek/config-templates/module_logpeek.php b/modules/logpeek/config-templates/module_logpeek.php
index 89255736d..5dd163a7a 100644
--- a/modules/logpeek/config-templates/module_logpeek.php
+++ b/modules/logpeek/config-templates/module_logpeek.php
@@ -8,6 +8,8 @@
 $config = array (
 	'logfile'	=> '/var/log/simplesamlphp.log',
 	'lines'		=> 1500,
+	// Read block size. 8192 is max, limited by fread.
+	'blocksz'	=> 8192,
 );
 
 ?>
diff --git a/modules/logpeek/lib/File/reverseRead.php b/modules/logpeek/lib/File/reverseRead.php
new file mode 100644
index 000000000..99ceac57a
--- /dev/null
+++ b/modules/logpeek/lib/File/reverseRead.php
@@ -0,0 +1,224 @@
+<?php
+/**
+ * Functionatility for line by line reverse reading of a file. It is done by blockwise
+ * fetching the file from the end and putting the lines into an array.
+ * 
+ * @author Thomas Graff<thomas.graff@uninett.no>
+ *
+ */
+class sspmod_logpeek_File_reverseRead{
+	// 8192 is max number of octets limited by fread.
+	private $blockSize;
+	private $blockStart;
+	private $fileHandle;
+	// fileSize may be changed after initial file size check
+	private $fileSize;
+	private $fileMtime;
+	// Array containing file lines
+	private $content;
+	// Leftover before first complete line
+	private $remainder;
+	// Count read lines from the end
+	private $readPointer;
+	
+	/**
+	 * File is checked and file handle to file is opend. But no data is read
+	 * from the file.
+	 * 
+	 * @param string $fileUrl Path and filename to file to be read
+	 * @param int $blockSize File read block size in byte
+	 * @return bool Success
+	 */
+	public function __construct($fileUrl, $blockSize = 8192){
+		if(!is_readable($fileUrl)){
+			return FALSE;
+		}
+		
+		$this->blockSize = $blockSize;
+		$this->content = array();
+		$this->remainder = '';
+		$this->readPointer = 0;
+		
+		$fileInfo = stat($fileUrl);
+		$this->fileSize = $this->blockStart = $fileInfo['size'];
+		$this->fileMtime = $fileInfo['mtime'];
+		
+		if($this->fileSize > 0){
+			$this->fileHandle = fopen($fileUrl, 'rb');
+			return TRUE;
+		}else{
+			return FALSE;
+		}
+	}
+	
+	
+	public function __destruct(){
+		if(is_resource($this->fileHandle)){
+			fclose($this->fileHandle);
+		}
+	}
+	
+	/**
+	 * Fetch chunk of data from file.
+	 * Each time this function is called, will it fetch a chunk
+	 * of data from the file. It starts from the end of the file
+	 * and work towards the beginning of the file.
+	 * 
+	 * @return string buffer with datablock.
+	 * Will return bool FALSE when there is no more data to get.
+	 */
+	private function readChunk(){
+		$splits = $this->blockSize;
+		
+		$this->blockStart -= $splits;
+		if($this->blockStart < 0){
+			$splits += $this->blockStart;
+			$this->blockStart = 0;
+		}
+		
+		// Return false if nothing more to read
+		if($splits === 0){
+			return FALSE;
+		}
+		
+		fseek($this->fileHandle, $this->blockStart, SEEK_SET);
+		$buff = fread($this->fileHandle, $splits);
+		
+		// $buff = stream_get_contents($this->fileHandle, $splits, $this->blockStart);
+		
+		return $buff;
+	}
+	
+	/**
+	 * Get one line of data from the file, starting from the end of the file.
+	 * 
+	 * @return string One line of data from the file.
+	 * Bool FALSE when there is no more data to get.
+	 */
+	public function getPreviousLine(){
+		if(count($this->content) === 0 || $this->readPointer < 1){
+			
+			do {
+				$buff = $this->readChunk();
+				
+				if($buff !== FALSE){
+					$eolPos = strpos($buff, "\n");
+				}else{
+					// Empty buffer, no more to read.
+					if(strlen($this->remainder) > 0){
+						$buff = $this->remainder;
+						$this->remainder = '';
+						// Exit from while-loop
+						break;
+					}else{
+						// Remainder also empty.
+						return FALSE;
+					}
+				}
+				
+				if($eolPos === FALSE){
+					// No eol found. Make buffer head of remainder and empty buffer.
+					$this->remainder = $buff . $this->remainder;
+					$buff = '';
+				}elseif($eolPos !== 0){
+					// eol found.
+					$buff .= $this->remainder;
+					$this->remainder = substr($buff, 0, $eolPos);
+					$buff = substr($buff, $eolPos+1);
+				}elseif($eolPos === 0){
+					$buff .= $this->remainder;
+					$buff = substr($buff, 1);
+					$this->remainder = '';
+				}
+				
+			}while(($buff !== FALSE) && ($eolPos === FALSE));
+			
+			$this->content = explode("\n", $buff);
+			$this->readPointer = count($this->content);
+		}
+		
+		if(count($this->content) > 0){
+			return $this->content[--$this->readPointer];
+		}else{
+			return FALSE;
+		}
+	}
+	
+	
+	private function cutHead(&$haystack, $needle, $exit){
+		$pos = 0;
+		$cnt = 0;
+		// Holder på inntill antall ønskede linjer eller vi ikke finner flere linjer
+		while($cnt < $exit && ($pos = strpos($haystack, $needle, $pos)) !==false ){
+			$pos++;
+			$cnt++;
+		}   
+		return $pos == false? false: substr($haystack, $pos, strlen($haystack));
+	}
+	
+	
+	// FIXME: This function hawe som error, do not use before auditing and testing
+	public function getTail($lines = 10){
+		$this->blockStart = $this->fileSize;
+		$buff1 = Array();
+		$lastLines = array();
+		
+		while($this->blockStart){
+			$buff = $this->readChunk();
+			if(!$buff)break;
+			
+			$lines -= substr_count($buff, "\n");
+			
+			if($lines <= 0)
+			{
+				$buff1[] = $this->cutHead($buff, "\n", abs($lines)+1);
+				break;
+			}
+			$buff1[] = $buff;
+		}
+		
+		for($i = count($buff1); $i >= 0; $i--){
+			$lastLines = array_merge($lastLines, explode("\n", $buff1[$i]));
+		}
+		
+		return $lastLines;
+		// return str_replace("\r", '', implode('', array_reverse($buff1)));
+	}
+	
+	
+	private function getLineAtPost($pos){
+		if($pos < 0 || $pos > $this->fileSize){
+			return FALSE;
+		}
+		
+		$seeker = $pos;
+		fseek($this->fileHandle, $seeker, SEEK_SET);
+		while($seeker > 0 && fgetc($this->fileHandle) !== "\n"){
+			fseek($this->fileHandle, --$seeker, SEEK_SET);
+		}
+		
+		return rtrim(fgets($this->fileHandle));
+	}
+	
+	
+	public function getFirstLine(){
+		return $this->getLineAtPost(0);
+	}
+	
+	
+	public function getLastLine(){
+		return $this->getLineAtPost($this->fileSize-2);
+	}
+	
+	
+	public function getFileSize(){
+		return $this->fileSize;
+	}
+	
+	
+	public function getFileMtime(){
+		return $this->fileMtime;
+	}
+	
+}
+?>
\ No newline at end of file
diff --git a/modules/logpeek/lib/Syslog/parseLine.php b/modules/logpeek/lib/Syslog/parseLine.php
new file mode 100644
index 000000000..be354b3f2
--- /dev/null
+++ b/modules/logpeek/lib/Syslog/parseLine.php
@@ -0,0 +1,25 @@
+<?php
+class sspmod_logpeek_Syslog_parseLine{
+	
+	
+	public static function isOlderThan($time, $logLine){
+		return true;
+	}
+	
+	public static function getUnixTime($logLine, $year = NULL){
+		// I can read month and day and time from the file.
+		// but I will assum year is current year retured by time().
+		// Unless month and day in the file is bigger than current month and day,
+		// I will then asume prevous year.
+		// A better approach would be to get the year from last modification time (mtime) of the
+		// file this record is taken from. But that require knowledge about the file.
+		if(!$year){
+			$now = getdate();
+			$year = (int)$now['year'];
+		}
+		list($month, $day, $hour, $minute, $second) = sscanf($logLine, "%s %d %d:%d:%d ");
+		$time = sprintf("%d %s %d %d:%d:%d", $day, $month, $year, $hour, $minute, $second);
+		return strtotime($time);
+	}
+}
+?>
\ No newline at end of file
diff --git a/modules/logpeek/templates/logpeek.php b/modules/logpeek/templates/logpeek.php
index b85e67838..848ce87cb 100644
--- a/modules/logpeek/templates/logpeek.php
+++ b/modules/logpeek/templates/logpeek.php
@@ -1,27 +1,27 @@
 <?php
 $this->data['header'] = 'Log peek';
 $this->includeAtTemplateBase('includes/header.php');
-
-
 ?>
 
 <h2>SimpleSAMLphp logs (admin utility)</h2>
 
 <form method="get" action="?">
-	<input type="text" name="tag" value="<?php echo $this->data['trackid']; ?>" />
-	<input type="submit" value="Show logs" />
+	<table>
+		<tr><th><label for="start">First entry in logfile</label></th><td id="star"><?php echo $this->data['timestart']; ?></td></tr>
+		<tr><th><label for="end">Last entry in logfile</label></th><td id="end"><?php echo $this->data['endtime']; ?></td></tr>
+		<tr><th><label for="size">Logfile size</label></th><td id="size"><?php echo $this->data['filesize']; ?></td></tr>
+		<tr><th><label for="tag">Tag id for search</label></th><td><input type="text" name="tag" id="tag" value="<?php echo $this->data['trackid']; ?>" /></td></tr>
+		<tr><th><input type="submit" value="Search log" /></th><td></td></tr>
+	</table>
 </form>
 
-
 <pre style="background: #eee; border: 1px solid #666; padding: 1em; margin: .4em; overflow: scroll">
 <?php
-
 if (!empty($this->data['results'])) {
 	foreach($this->data['results'] AS $line) {
-		echo $line;
+		echo htmlspecialchars($line) . "\n";
 	}
 }
-
 ?>
 </pre>
 <?php $this->includeAtTemplateBase('includes/footer.php'); ?>
\ No newline at end of file
diff --git a/modules/logpeek/www/index.php b/modules/logpeek/www/index.php
index dc9a5728e..fbc5688a7 100644
--- a/modules/logpeek/www/index.php
+++ b/modules/logpeek/www/index.php
@@ -1,58 +1,54 @@
 <?php
 
+function logFilter($objFile, $tag, $cut){
+	if (!preg_match('/^[a-f0-9]{10}$/', $tag)) throw new Exception('Invalid search tag');
+	
+	$i = 0;
+	$results = array();
+	$line = $objFile->getPreviousLine();
+	while($line !== FALSE && ($i++ < $cut)){
+		if(strstr($line, '[' . $tag . ']')){
+			$results[] = $line;
+		}
+		$line = $objFile->getPreviousLine();
+	}
+	$results[] = 'Searched ' . $i . ' lines backward. ' . count($results) . ' lines found.';
+	$results = array_reverse($results);
+	return $results;
+}
+
 
 $config = SimpleSAML_Configuration::getInstance();
 $session = SimpleSAML_Session::getInstance();
 
 SimpleSAML_Utilities::requireAdmin();
 
-
 $logpeekconfig = SimpleSAML_Configuration::getConfig('module_logpeek.php');
-
 $logfile = $logpeekconfig->getValue('logfile', '/var/simplesamlphp.log');
+$blockSize = $logpeekconfig->getValue('blocksz', 8192);
 
-function grepLog($logfile, $tag, $lines) {
+$myLog = new sspmod_logpeek_File_reverseRead($logfile, $blockSize);
 
-	if (!is_readable($logfile)) throw new Exception('Log file [' . $logfile . '] is not readable. Consider checking the file permissions');
-	if (!preg_match('/^[a-f0-9]{10}$/', $tag)) throw new Exception('Invalid search tag');
-	
-	$results = array();
-	$i=0 ;
-	$line = '';
-	$fp = fopen($logfile,"r") ;
-	if(is_resource($fp)){
-		fseek($fp,0,SEEK_END) ;
-		$a = ftell($fp) ;
-		while($i <= $lines){
-			if(fgetc($fp) == "\n"){
-				$line = fgets($fp);
-				$i++ ;
-				if (strstr($line, '[' . $tag . ']')) 
-					$results[] = $line;
-			}
-			fseek($fp,$a);
-			$a-- ;
-		}
-	}
-
-	$results[] = 'Start search line (' . $lines . ' lines back): ' . substr($line,0,40) . '...';
-	$results = array_reverse($results);
-	return $results;
-}
 
 $results = NULL;
 if (isset($_REQUEST['tag'])) {
-	$results = grepLog($logfile, $_REQUEST['tag'], $logpeekconfig->getValue('lines', 500));
-// 	echo('<pre>log:');
-// 	print_r($results);
+	$results = logFilter($myLog, $_REQUEST['tag'], $logpeekconfig->getValue('lines', 500));
 }
 
 
+$fileModYear = date("Y", $myLog->getFileMtime());
+$firstLine = $myLog->getFirstLine();
+$firstTimeEpoch = sspmod_logpeek_Syslog_parseLine::getUnixTime($firstLine, $fileModYear);
+$lastLine = $myLog->getLastLine();
+$lastTimeEpoch = sspmod_logpeek_Syslog_parseLine::getUnixTime($lastLine, $fileModYear);
+$fileSize = $myLog->getFileSize();
 
 $t = new SimpleSAML_XHTML_Template($config, 'logpeek:logpeek.php');
 $t->data['results'] = $results;
 $t->data['trackid'] = $session->getTrackID();
-$t->show();
-exit;
+$t->data['timestart'] = date(DATE_RFC822, $firstTimeEpoch);
+$t->data['endtime'] = date(DATE_RFC822, $lastTimeEpoch);
+$t->data['filesize'] = $fileSize;
 
-?>
+$t->show();
+?>
\ No newline at end of file
-- 
GitLab