From 5cb864a7d913ac6c7842f6b4e61be49bcaf0a02a Mon Sep 17 00:00:00 2001 From: lostcontrol Date: Mon, 14 Jan 2008 23:12:21 +0000 Subject: [PATCH] - New log rotation detection algorithm. - Print monitored files in status. git-svn-id: https://fail2ban.svn.sourceforge.net/svnroot/fail2ban/branches/FAIL2BAN-0_8@644 a942ae1a-1317-0410-a47c-b1dcaea8d605 --- CHANGELOG | 6 +- client/beautifier.py | 5 ++ server/filter.py | 180 +++++++++++++++++++++++------------------- server/filtergamin.py | 4 +- 4 files changed, 110 insertions(+), 85 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8683b41c..2833ec79 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,10 +4,10 @@ |_| \__,_|_|_/___|_.__/\__,_|_||_| ============================================================= -Fail2Ban (version 0.8.2) 2007/??/?? +Fail2Ban (version 0.8.2) 2008/??/?? ============================================================= -ver. 0.8.2 (2007/??/??) - stable +ver. 0.8.2 (2008/??/??) - stable ---------- - Fixed named filter. Thanks to Yaroslav Halchenko - Fixed wrong path for apache-auth in jail.conf. Thanks to @@ -27,6 +27,8 @@ ver. 0.8.2 (2007/??/??) - stable - Refactoring. Reduced number of files. - Removed Python 2.4. Minimum required version is now Python 2.3. +- New log rotation detection algorithm. +- Print monitored files in status. ver. 0.8.1 (2007/08/14) - stable ---------- diff --git a/client/beautifier.py b/client/beautifier.py index 132d1626..ab98b294 100644 --- a/client/beautifier.py +++ b/client/beautifier.py @@ -72,9 +72,14 @@ class Beautifier: ipList = "" for ip in response[1][1][2][1]: ipList += ip + " " + # Creates file list. + fileList = "" + for f in response[0][1][2][1]: + fileList += f + " " # Display information msg = "Status for the jail: " + inC[1] + "\n" msg = msg + "|- " + response[0][0] + "\n" + msg = msg + "| |- " + response[0][1][2][0] + ":\t" + fileList + "\n" msg = msg + "| |- " + response[0][1][0][0] + ":\t" + `response[0][1][0][1]` + "\n" msg = msg + "| `- " + response[0][1][1][0] + ":\t" + `response[0][1][1][1]` + "\n" msg = msg + "`- " + response[1][0] + "\n" diff --git a/server/filter.py b/server/filter.py index d6af045d..c869695e 100644 --- a/server/filter.py +++ b/server/filter.py @@ -242,8 +242,8 @@ class Filter(JailThread): # Decode line to UTF-8 l = line.decode('utf-8') except UnicodeDecodeError: - pass - timeMatch = self.dateDetector.matchTime(line) + l = line + timeMatch = self.dateDetector.matchTime(l) if not timeMatch: # There is no valid time in this line return @@ -335,26 +335,17 @@ class FileFilter(Filter): def __init__(self, jail): Filter.__init__(self, jail) - ## The log file handler. - self.__crtHandler = None - self.__crtFilename = None ## The log file path. self.__logPath = [] - ## The last position of the file. - self.__lastPos = dict() - ## The last date in tht log file. - self.__lastDate = dict() ## # Add a log file path # # @param path log file path - def addLogPath(self, path): - self.getLogPath().append(path) - # Initialize default values - self.__lastDate[path] = 0 - self.__lastPos[path] = 0 + def addLogPath(self, path, tail = False): + container = FileContainer(path, tail) + self.__logPath.append(container) ## # Delete a log path @@ -362,9 +353,10 @@ class FileFilter(Filter): # @param path the log file to delete def delLogPath(self, path): - self.getLogPath().remove(path) - del self.__lastDate[path] - del self.__lastPos[path] + for log in self.__logPath: + if log.getFileName() == path: + self.__logPath.remove(log) + return ## # Get the log file path @@ -381,62 +373,16 @@ class FileFilter(Filter): # @return True if the path is already monitored else False def containsLogPath(self, path): - try: - self.getLogPath().index(path) - return True - except ValueError: - return False - - ## - # Open the log file. - - def __openLogFile(self, filename): - """ Opens the log file specified on init. - """ - try: - self.__crtFilename = filename - self.__crtHandler = open(filename) - logSys.debug("Opened " + filename) - return True - except OSError: - logSys.error("Unable to open " + filename) - except IOError: - logSys.error("Unable to read " + filename + - ". Please check permissions") + for log in self.__logPath: + if log.getFileName() == path: + return True return False - ## - # Close the log file. - - def __closeLogFile(self): - self.__crtFilename = None - self.__crtHandler.close() - - ## - # Set the file position. - # - # Sets the file position. We must take care of log file rotation - # and reset the position to 0 in that case. Use the log message - # timestamp in order to detect this. - - def __setFilePos(self): - line = self.__crtHandler.readline() - lastDate = self.__lastDate[self.__crtFilename] - lineDate = self.dateDetector.getUnixTime(line) - if lastDate < lineDate: - logSys.debug("Date " + `lastDate` + " is smaller than " + `lineDate`) - logSys.debug("Log rotation detected for " + self.__crtFilename) - self.__lastPos[self.__crtFilename] = 0 - lastPos = self.__lastPos[self.__crtFilename] - logSys.debug("Setting file position to " + `lastPos` + " for " + - self.__crtFilename) - self.__crtHandler.seek(lastPos) - - ## - # Get the file position. - - def __getFilePos(self): - return self.__crtHandler.tell() + def getFileContainer(self, path): + for log in self.__logPath: + if log.getFileName() == path: + return log + return None ## # Gets all the failure in the log file. @@ -446,13 +392,20 @@ class FileFilter(Filter): # is created and is added to the FailManager. def getFailures(self, filename): - # Try to open log file. - if not self.__openLogFile(filename): + container = self.getFileContainer(filename) + if container == None: logSys.error("Unable to get failures in " + filename) return False - self.__setFilePos() - lastTimeLine = None - for line in self.__crtHandler: + # Try to open log file. + try: + container.open() + except Exception, e: + logSys.error("Unable to open %s" % filename) + logSys.exception(e) + return False + + line = container.readline() + while not line == "": if not self._isActive(): # The jail has been stopped break @@ -464,6 +417,7 @@ class FileFilter(Filter): timeMatch = self.dateDetector.matchTime(line) if not timeMatch: # There is no valid time in this line + line = container.readline() continue # Lets split into time part and log part of the line timeLine = timeMatch.group() @@ -471,7 +425,6 @@ class FileFilter(Filter): # anchore at the beginning of the time regexp, we don't # at least allow injection. Should be harmless otherwise logLine = line[:timeMatch.start()] + line[timeMatch.end():] - lastTimeLine = timeLine for element in self.findFailure(timeLine, logLine): ip = element[0] unixTime = element[1] @@ -482,11 +435,76 @@ class FileFilter(Filter): continue logSys.debug("Found "+ip) self.failManager.addFailure(FailTicket(ip, unixTime)) - self.__lastPos[filename] = self.__getFilePos() - if lastTimeLine: - self.__lastDate[filename] = self.dateDetector.getUnixTime(lastTimeLine) - self.__closeLogFile() + # Read a new line. + line = container.readline() + container.close() return True + + def status(self): + ret = Filter.status(self) + path = [m.getFileName() for m in self.getLogPath()] + ret.append(("File list", path)) + return ret + +## +# FileContainer class. +# +# This class manages a file handler and takes care of log rotation detection. +# In order to detect log rotation, the hash (MD5) of the first line of the file +# is computed and compared to the previous hash of this line. + +import md5 + +class FileContainer: + + def __init__(self, filename, tail = False): + self.__filename = filename + self.__tail = tail + self.__handler = None + # Try to open the file. Raises an exception if an error occured. + handler = open(filename) + try: + firstLine = handler.readline() + # Computes the MD5 of the first line. + self.__hash = md5.new(firstLine).digest() + # Start at the beginning of file if tail mode is off. + if tail: + handler.seek(0, 2) + self.__pos = handler.tell() + else: + self.__pos = 0 + finally: + handler.close() + + def getFileName(self): + return self.__filename + + def open(self): + self.__handler = open(self.__filename) + firstLine = self.__handler.readline() + # Computes the MD5 of the first line. + myHash = md5.new(firstLine).digest() + # Compare hash. + if not self.__hash == myHash: + logSys.info("Log rotation detected for %s" % self.__filename) + self.__hash = myHash + self.__pos = 0 + # Sets the file pointer to the last position. + self.__handler.seek(self.__pos) + + def readline(self): + if self.__handler == None: + return "" + return self.__handler.readline() + + def close(self): + if not self.__handler == None: + # Saves the last position. + self.__pos = self.__handler.tell() + # Closes the file. + self.__handler.close() + self.__handler = None + ## diff --git a/server/filtergamin.py b/server/filtergamin.py index 75a78844..6c4a20ca 100644 --- a/server/filtergamin.py +++ b/server/filtergamin.py @@ -126,6 +126,6 @@ class FilterGamin(FileFilter): # Desallocates the resources used by Gamin. def __cleanup(self): - for path in self.getLogPath(self): - self.monitor.stop_watch(path) + for path in self.getLogPath(): + self.monitor.stop_watch(path.getFileName()) del self.monitor