diff --git a/fail2ban-client b/fail2ban-client index 7025d009..66b90333 100755 --- a/fail2ban-client +++ b/fail2ban-client @@ -101,7 +101,7 @@ class Fail2banClient: printFormatted() print - print "Report bugs to " + print "Report bugs to https://github.com/fail2ban/fail2ban/issues" def dispInteractive(self): print "Fail2Ban v" + version + " reads log file that contains password failure report" diff --git a/fail2ban-regex b/fail2ban-regex index b854dde2..45b01861 100755 --- a/fail2ban-regex +++ b/fail2ban-regex @@ -122,7 +122,7 @@ class Fail2banRegex: print " string a string representing an 'ignoreregex'" print " filename path to a filter file (filter.d/sshd.conf)" print - print "Report bugs to " + print "Report bugs to https://github.com/fail2ban/fail2ban/issues" dispUsage = staticmethod(dispUsage) def getCmdLineOptions(self, optList): diff --git a/fail2ban-server b/fail2ban-server index 3b3c6e69..f6f1dac4 100755 --- a/fail2ban-server +++ b/fail2ban-server @@ -88,7 +88,7 @@ class Fail2banServer: print " -h, --help display this help message" print " -V, --version print the version" print - print "Report bugs to " + print "Report bugs to https://github.com/fail2ban/fail2ban/issues" def __getCmdLineOptions(self, optList): """ Gets the command line options diff --git a/files/ipmasq-ZZZzzz|fail2ban.rul b/files/ipmasq-ZZZzzz_fail2ban.rul similarity index 100% rename from files/ipmasq-ZZZzzz|fail2ban.rul rename to files/ipmasq-ZZZzzz_fail2ban.rul diff --git a/server/filter.py b/server/filter.py index a9bc35f3..d69f02aa 100644 --- a/server/filter.py +++ b/server/filter.py @@ -395,8 +395,19 @@ class FileFilter(Filter): # @param path log file path def addLogPath(self, path, tail = False): - container = FileContainer(path, tail) - self.__logPath.append(container) + if self.containsLogPath(path): + logSys.error(path + " already exists") + else: + container = FileContainer(path, tail) + self.__logPath.append(container) + logSys.info("Added logfile = %s" % path) + self._addLogPath(path) # backend specific + + def _addLogPath(self, path): + # nothing to do by default + # to be overriden by backends + pass + ## # Delete a log path @@ -407,8 +418,15 @@ class FileFilter(Filter): for log in self.__logPath: if log.getFileName() == path: self.__logPath.remove(log) + logSys.info("Removed logfile = %s" % path) + self._delLogPath(path) return + def _delLogPath(self, path): + # nothing to do by default + # to be overriden by backends + pass + ## # Get the log file path # diff --git a/server/filtergamin.py b/server/filtergamin.py index c1a6be3c..84148b1a 100644 --- a/server/filtergamin.py +++ b/server/filtergamin.py @@ -72,27 +72,17 @@ class FilterGamin(FileFilter): # # @param path log file path - def addLogPath(self, path, tail = False): - if self.containsLogPath(path): - logSys.error(path + " already exists") - else: - self.monitor.watch_file(path, self.callback) - FileFilter.addLogPath(self, path, tail) - logSys.info("Added logfile = %s" % path) - + def _addLogPath(self, path): + self.monitor.watch_file(path, self.callback) + ## # Delete a log path # # @param path the log file to delete - + def delLogPath(self, path): - if not self.containsLogPath(path): - logSys.error(path + " is not monitored") - else: - self.monitor.stop_watch(path) - FileFilter.delLogPath(self, path) - logSys.info("Removed logfile = %s" % path) - + self.monitor.stop_watch(path) + ## # Main loop. # diff --git a/server/filterpoll.py b/server/filterpoll.py index 81edf813..0719545a 100644 --- a/server/filterpoll.py +++ b/server/filterpoll.py @@ -63,28 +63,18 @@ class FilterPoll(FileFilter): # # @param path log file path - def addLogPath(self, path, tail = False): - if self.containsLogPath(path): - logSys.error(path + " already exists") - else: - self.__lastModTime[path] = 0 - self.__file404Cnt[path] = 0 - FileFilter.addLogPath(self, path, tail) - logSys.info("Added logfile = %s" % path) + def _addLogPath(self, path): + self.__lastModTime[path] = 0 + self.__file404Cnt[path] = 0 ## # Delete a log path # # @param path the log file to delete - def delLogPath(self, path): - if not self.containsLogPath(path): - logSys.error(path + " is not monitored") - else: - del self.__lastModTime[path] - del self.__file404Cnt[path] - FileFilter.delLogPath(self, path) - logSys.info("Removed logfile = %s" % path) + def _delLogPath(self, path): + del self.__lastModTime[path] + del self.__file404Cnt[path] ## # Main loop. diff --git a/server/filterpyinotify.py b/server/filterpyinotify.py index b07dbba1..008f9303 100644 --- a/server/filterpyinotify.py +++ b/server/filterpyinotify.py @@ -57,7 +57,29 @@ class FilterPyinotify(FileFilter): logSys.debug("Created FilterPyinotify") - def callback(self, path): + def callback(self, event): + path = event.pathname + if event.mask == pyinotify.IN_CREATE: + # check if that is a file we care about + if not path in self.__watches: + logSys.debug("Ignoring creation of %s we do not monitor" % path) + return + else: + # we need to substitute the watcher with a new one, so first + # remove old one + self._delFileWatcher(path) + # place a new one + self._addFileWatcher(path) + + self._process_file(path) + + + def _process_file(self, path): + """Process a given file + + TODO -- RF: + this is a common logic and must be shared/provided by FileFilter + """ self.getFailures(path) try: while True: @@ -68,57 +90,58 @@ class FilterPyinotify(FileFilter): self.dateDetector.sortTemplate() self.__modified = False + + def _addFileWatcher(self, path): + wd = self.__monitor.add_watch(path, pyinotify.IN_MODIFY) + self.__watches.update(wd) + logSys.debug("Added file watcher for %s" % path) + # process the file since we did get even + self._process_file(path) + + + def _delFileWatcher(self, path): + wdInt = self.__watches[path] + wd = self.__monitor.rm_watch(wdInt) + if wd[wdInt]: + del self.__watches[path] + logSys.debug("Removed file watcher for %s" % path) + return True + else: + return False + ## # Add a log file path # # @param path log file path - def addLogPath(self, path, tail=False): - if self.containsLogPath(path): - logSys.error(path + " already exists") - else: - wd = self.__monitor.add_watch(path, pyinotify.IN_MODIFY) - self.__watches.update(wd) + def _addLogPath(self, path): + path_dir = dirname(path) + if not (path_dir in self.__watches): + # we need to watch also the directory for IN_CREATE + self.__watches.update( + self.__monitor.add_watch(path_dir, pyinotify.IN_CREATE)) + logSys.debug("Added monitor for the parent directory %s" % path_dir) - FileFilter.addLogPath(self, path, tail) - logSys.info("Added logfile = %s" % path) + self._addFileWatcher(path) - path_dir = dirname(path) - if not (path_dir in self.__watches): - # we need to watch also the directory for IN_CREATE - self.__watches.update( - self.__monitor.add_watch(path_dir, pyinotify.IN_CREATE)) - logSys.debug("Monitor also parent directory %s" % path_dir) - - # sniff the file - self.callback(path) ## # Delete a log path # # @param path the log file to delete - def delLogPath(self, path): - if not self.containsLogPath(path): - logSys.error(path + " is not monitored") - else: - wdInt = self.__watches[path] - wd = self.__monitor.rm_watch(wdInt) - if wd[wdInt]: - del self.__watches[path] - FileFilter.delLogPath(self, path) - logSys.info("Removed logfile = %s" % path) - else: - logSys.error("Failed to remove watch on path: %s", path) + def _delLogPath(self, path): + if not self._delFileWatcher(path): + logSys.error("Failed to remove watch on path: %s", path) - path_dir = dirname(path) - if not len([k for k in self.__watches - if k.startswith(path_dir + pathsep)]): - # Remove watches for the directory - # since there is no other monitored file under this directory - wdInt = self.__watches.pop(path_dir) - _ = self.__monitor.rm_watch(wdInt) - logSys.debug("Remove monitor for the parent directory %s" % path_dir) + path_dir = dirname(path) + if not len([k for k in self.__watches + if k.startswith(path_dir + pathsep)]): + # Remove watches for the directory + # since there is no other monitored file under this directory + wdInt = self.__watches.pop(path_dir) + _ = self.__monitor.rm_watch(wdInt) + logSys.debug("Removed monitor for the parent directory %s" % path_dir) ## @@ -169,4 +192,4 @@ class ProcessPyinotify(pyinotify.ProcessEvent): # just need default, since using mask on watch to limit events def process_default(self, event): logSys.debug("Callback for Event: %s" % event) - self.__FileFilter.callback(event.pathname) + self.__FileFilter.callback(event) diff --git a/testcases/filtertestcase.py b/testcases/filtertestcase.py index 47a32209..4ddede8b 100644 --- a/testcases/filtertestcase.py +++ b/testcases/filtertestcase.py @@ -24,6 +24,7 @@ __license__ = "GPL" import unittest import os +import sys import time import tempfile @@ -85,6 +86,10 @@ def _copy_lines_between_files(fin, fout, n=None, skip=0, mode='a', terminal_line Returns open fout """ + if sys.version_info[:2] <= (2,4): + # on old Python st_mtime is int, so we should give at least 1 sec so + # polling filter could detect the change + time.sleep(1) if isinstance(fin, str): fin = open(fin, 'r') if isinstance(fout, str): @@ -315,6 +320,9 @@ def get_monitor_failures_testcase(Filter_): self.filter.setActive(True) self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) ") self.filter.start() + # If filter is polling it would sleep a bit to guarantee that + # we have initial time-stamp difference to trigger "actions" + self._sleep_4_poll() #print "D: started filter %s" % self.filter @@ -326,7 +334,7 @@ def get_monitor_failures_testcase(Filter_): #print "D: WAITING FOR FILTER TO STOP" self.filter.join() # wait for the thread to terminate #print "D: KILLING THE FILE" - #_killfile(self.file, self.name) + _killfile(self.file, self.name) pass def __str__(self): @@ -343,6 +351,19 @@ def get_monitor_failures_testcase(Filter_): time.sleep(0.1) return False + def _sleep_4_poll(self): + # Since FilterPoll relies on time stamps and some + # actions might be happening too fast in the tests, + # sleep a bit to guarantee reliable time stamps + if isinstance(self.filter, FilterPoll): + if sys.version_info[:2] <= (2,4): + # on old Python st_mtime is int, so we should give + # at least 1 sec so polling filter could detect + # the change + time.sleep(0.5) + else: + time.sleep(0.1) + def isEmpty(self, delay=0.4): # shorter wait time for not modified status return not self.isFilled(delay) @@ -354,7 +375,6 @@ def get_monitor_failures_testcase(Filter_): def test_grow_file(self): # suck in lines from this sample log file - #self.filter.getFailures(self.name) self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) # Now let's feed it with entries from the file @@ -365,7 +385,7 @@ def get_monitor_failures_testcase(Filter_): # since it should have not been enough _copy_lines_between_files(GetFailures.FILENAME_01, self.file, skip=5) - self.isFilled(6) + self.assertTrue(self.isFilled(6)) # so we sleep for up to 2 sec for it not to become empty, # and meanwhile pass to other thread(s) and filter should # have gathered new failures and passed them into the @@ -383,7 +403,6 @@ def get_monitor_failures_testcase(Filter_): self.assert_correct_last_attempt(GetFailures.FAILURES_01) def test_rewrite_file(self): - # # if we rewrite the file at once self.file.close() _copy_lines_between_files(GetFailures.FILENAME_01, self.name) @@ -399,11 +418,10 @@ def get_monitor_failures_testcase(Filter_): def test_move_file(self): - # # if we move file into a new location while it has been open already self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=14, mode='w') - self.isFilled(6) + self.assertTrue(self.isEmpty(2)) self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) self.assertEqual(self.filter.failManager.getFailTotal(), 2) # Fails with Poll from time to time @@ -413,6 +431,25 @@ def get_monitor_failures_testcase(Filter_): self.assert_correct_last_attempt(GetFailures.FAILURES_01) self.assertEqual(self.filter.failManager.getFailTotal(), 3) + # now remove the moved file + _killfile(None, self.name + '.bak') + _copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100) + self.assert_correct_last_attempt(GetFailures.FAILURES_01) + self.assertEqual(self.filter.failManager.getFailTotal(), 6) + + + def test_new_bogus_file(self): + # to make sure that watching whole directory does not effect + _copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100) + self.assert_correct_last_attempt(GetFailures.FAILURES_01) + + # create a bogus file in the same directory and see if that doesn't affect + open(self.name + '.bak2', 'w').write('') + _copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100) + self.assert_correct_last_attempt(GetFailures.FAILURES_01) + self.assertEqual(self.filter.failManager.getFailTotal(), 6) + _killfile(None, self.name + '.bak2') + def test_delLogPath(self): # Smoke test for removing of the path from being watched