From a3e0cc9830969f09442fc79d8c8be6353791ec49 Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 14 May 2014 16:01:35 +0200 Subject: [PATCH 001/623] introduced string to seconds (str2seconds) for configuration entries with time; todo: expands it for all time config entries; --- fail2ban/server/mytime.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/fail2ban/server/mytime.py b/fail2ban/server/mytime.py index 166f4796..0a7e6a79 100644 --- a/fail2ban/server/mytime.py +++ b/fail2ban/server/mytime.py @@ -22,6 +22,7 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import datetime +import re import time @@ -96,3 +97,32 @@ class MyTime: return time.localtime(x) else: return time.localtime(MyTime.myTime) + + ## + # Wraps string expression like "1h 2m 3s" into number contains seconds (3723). + # The string expression will be evaluated as mathematical expression, spaces between each groups + # will be wrapped to "+" operand (only if any operand does not specified between). + # Because of case insensitivity and overwriting with minutes ("m" or "mm"), the short replacement for month + # are "mo" or "mon" (like %b by date formating). + # Ex: 1hour+30min = 5400 + # 0d 1h 30m = 5400 + # 1year-6mo = 15778800 + # 6 months = 15778800 + # warn: month is not 30 days, it is a year in seconds / 12, the leap years will be respected also: + # >>>> float(Test.str2seconds("1month")) / 60 / 60 / 24 + # 30.4375 + # >>>> float(Test.str2seconds("1year")) / 60 / 60 / 24 + # 365.25 + # + # @returns number (calculated seconds from expression "val") + + #@staticmethod + def str2seconds(val): + for rexp, rpl in ( + (r"days?|da|dd?", 24*60*60), (r"week?|wee?|ww?", 7*24*60*60), (r"months?|mon?", (365*3+366)*24*60*60/4/12), (r"years?|yea?|yy?", (365*3+366)*24*60*60/4), + (r"seconds?|sec?|ss?", 1), (r"minutes?|min?|mm?", 60), (r"hours?|ho|hh?", 60*60), + ): + val = re.sub(r"(?i)(?<=[\d\s])(%s)\b" % rexp, "*"+str(rpl), val) + val = re.sub(r"(\d)\s+(\d)", r"\1+\2", val); + return eval(val) + str2seconds = staticmethod(str2seconds) From 3cfdc5ecb36796ca2c5733ed8e20668821c442ee Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 10 Jun 2014 12:31:55 +0200 Subject: [PATCH 002/623] test cases extended; code review --- fail2ban/tests/dummyjail.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/fail2ban/tests/dummyjail.py b/fail2ban/tests/dummyjail.py index 9b784f77..6be571d3 100644 --- a/fail2ban/tests/dummyjail.py +++ b/fail2ban/tests/dummyjail.py @@ -24,18 +24,19 @@ __license__ = "GPL" from threading import Lock +from ..server.jail import Jail from ..server.actions import Actions -class DummyJail(object): +class DummyJail(Jail, object): """A simple 'jail' to suck in all the tickets generated by Filter's """ - def __init__(self): + def __init__(self, backend=None): self.lock = Lock() self.queue = [] - self.idle = False - self.database = None - self.actions = Actions(self) + super(DummyJail, self).__init__(name='DummyJail', backend=backend) + self.__db = None + self.__actions = Actions(self) def __len__(self): try: @@ -64,3 +65,26 @@ class DummyJail(object): @property def name(self): return "DummyJail #%s with %d tickets" % (id(self), len(self)) + + @property + def idle(self): + return False; + + @idle.setter + def idle(self, value): + pass + + @property + def database(self): + return self.__db; + + @database.setter + def database(self, value): + self.__db = value; + + @property + def actions(self): + return self.__actions; + + def is_alive(self): + return True; From 42199957d02de7dd2abd5c651638b635a0f7860b Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 16 Sep 2014 13:50:32 +0200 Subject: [PATCH 003/623] prevent completely read of big files first time (after start of service), initial seek to start time using half-interval search algorithm (see issue #795): now polling backend only (currently not implemented for gamin and pyinotify backends); python3/pypy compatibility fix + removing obsolete code --- fail2ban/server/filter.py | 90 +++++++++++++++++++++++++++++++- fail2ban/server/filterpoll.py | 7 ++- fail2ban/tests/filtertestcase.py | 9 ++++ 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index dae5c527..2dfeb174 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -24,6 +24,7 @@ __license__ = "GPL" import codecs import fcntl import locale +import logging import os import re import sys @@ -190,6 +191,7 @@ class Filter(JailThread): # @param value the time def setFindTime(self, value): + value = MyTime.str2seconds(value) self.__findTime = value self.failManager.setMaxTime(value) logSys.info("Set findtime = %s" % value) @@ -651,7 +653,7 @@ class FileFilter(Filter): # MyTime.time()-self.findTime. When a failure is detected, a FailTicket # is created and is added to the FailManager. - def getFailures(self, filename): + def getFailures(self, filename, startTime=None): log = self.getLog(filename) if log is None: logSys.error("Unable to get failures in " + filename) @@ -673,6 +675,11 @@ class FileFilter(Filter): logSys.exception(e) return False + # prevent completely read of big files first time (after start of service), initial seek to start time using half-interval search algorithm: + if log.getPos() == 0 and startTime is not None: + # startTime = MyTime.time() - self.getFindTime() + self.seekToTime(log, startTime) + # yoh: has_content is just a bool, so do not expect it to # change -- loop is exited upon break, and is not entered at # all if upon container opening that one was empty. If we @@ -690,6 +697,74 @@ class FileFilter(Filter): db.updateLog(self.jail, log) return True + ## + # Seeks to line with date (search using half-interval search algorithm), to start polling from it + # + + def seekToTime(self, container, date): + fs = container.getFileSize() + if logSys.getEffectiveLevel() <= logging.DEBUG: + logSys.debug("Seek to find time %s (%s), file size %s", date, + datetime.datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M:%S"), fs) + date -= 0.009 + minp = 0 + maxp = fs + lastpos = 0 + lastFew = 0 + lastTime = None + cntr = 0 + unixTime = None + lasti = 0 + movecntr = 3 + while maxp > minp: + i = int(minp + (maxp - minp) / 2) + pos = container.seek(i) + cntr += 1 + # within next 5 lines try to find any legal datetime: + lncntr = 5; + dateTimeMatch = None + llen = 0 + i = pos + while True: + line = container.readline() + if not line: + break + llen += len(line) + l = line.rstrip('\r\n') + timeMatch = self.dateDetector.matchTime(l) + if timeMatch: + dateTimeMatch = self.dateDetector.getTime(l[timeMatch.start():timeMatch.end()]) + if not dateTimeMatch and lncntr: + lncntr -= 1 + continue + break + # if we can't move (position not changed) + if i + llen == lasti: + movecntr -= 1 + if movecntr <= 0: + break + lasti = i + llen; + # not found at this step - stop searching + if not dateTimeMatch: + break + unixTime = dateTimeMatch[0] + if unixTime >= date: + maxp = i + else: + minp = i + llen + lastFew = pos; + lastTime = unixTime + lastpos = pos + # if found position have a time greater as given - use smallest time we have found + if unixTime is None or unixTime > date: + unixTime = lastTime + lastpos = container.seek(lastFew, False) + else: + lastpos = container.seek(lastpos, False) + if logSys.getEffectiveLevel() <= logging.DEBUG: + logSys.debug("Position %s from %s, found time %s (%s) within %s seeks", lastpos, fs, unixTime, + (datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S") if unixTime is not None else ''), cntr) + def status(self, flavor="basic"): """Status of Filter plus files being monitored. """ @@ -742,6 +817,9 @@ class FileContainer: def getFileName(self): return self.__filename + def getFileSize(self): + return os.path.getsize(self.__filename); + def setEncoding(self, encoding): codecs.lookup(encoding) # Raises LookupError if invalid self.__encoding = encoding @@ -788,6 +866,16 @@ class FileContainer: self.__handler.seek(self.__pos) return True + def seek(self, offs, endLine=True): + h = self.__handler + # seek to given position + h.seek(offs, 0) + # goto end of next line + if endLine: + h.readline() + # get current real position + return h.tell() + @staticmethod def decode_line(filename, enc, line): try: diff --git a/fail2ban/server/filterpoll.py b/fail2ban/server/filterpoll.py index d0b37775..0e99455e 100644 --- a/fail2ban/server/filterpoll.py +++ b/fail2ban/server/filterpoll.py @@ -57,6 +57,7 @@ class FilterPoll(FileFilter): ## The time of the last modification of the file. self.__prevStats = dict() self.__file404Cnt = dict() + self.__initial = dict() logSys.debug("Created FilterPoll") ## @@ -94,7 +95,11 @@ class FilterPoll(FileFilter): for container in self.getLogs(): filename = container.getFileName() if self.isModified(filename): - self.getFailures(filename) + # set start time as now - find time for first usage only (prevent performance bug with polling of big files) + self.getFailures(filename, + (MyTime.time() - self.getFindTime()) if not self.__initial.get(filename) else None + ) + self.__initial[filename] = True self.__modified = True if self.__modified: diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 40879b66..c7dba5ce 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -912,6 +912,15 @@ class GetFailures(LogCaptureTestCase): self.filter.getFailures(GetFailures.FILENAME_03) _assert_correct_last_attempt(self, self.filter, output) + def testGetFailures03_seek(self): + # same test as above but with seek to 'Aug 14 11:55:04' - so other output ... + output = ('203.162.223.135', 5, 1124013544.0) + + self.filter.addLogPath(GetFailures.FILENAME_03) + self.filter.addFailRegex("error,relay=,.*550 User unknown") + self.filter.getFailures(GetFailures.FILENAME_03, output[2] - 4*60 + 1) + _assert_correct_last_attempt(self, self.filter, output) + def testGetFailures04(self): output = [('212.41.96.186', 4, 1124013600.0), ('212.41.96.185', 4, 1124017198.0)] From 410f9d7c101b5987071fcd829d6eeac3bba0b505 Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 24 Sep 2014 13:21:37 +0200 Subject: [PATCH 004/623] filter, datedetector, datetemplate: performance optimizing of combination datedetector.matchTime/getTime2, because early getTime search a template and call template.matchTime again (so the date parsing was really executed twice, now just once); debug logging optimized; added info line log "Start Fail2ban ..." after changed logging target; --- fail2ban/server/action.py | 10 +++-- fail2ban/server/datedetector.py | 56 +++++++++++++++++++++----- fail2ban/server/datetemplate.py | 17 ++++---- fail2ban/server/filter.py | 16 ++++---- fail2ban/server/server.py | 3 +- fail2ban/tests/datedetectortestcase.py | 15 +++++++ 6 files changed, 89 insertions(+), 28 deletions(-) diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index de0c8efc..85bdd166 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -592,10 +592,12 @@ class CommandAction(ActionBase): std_level = retcode == 0 and logging.DEBUG or logging.ERROR if std_level >= logSys.getEffectiveLevel(): - stdout.seek(0) - logSys.log(std_level, "%s -- stdout: %r" % (realCmd, stdout.read())) - stderr.seek(0) - logSys.log(std_level, "%s -- stderr: %r" % (realCmd, stderr.read())) + stdout.seek(0); msg = stdout.read() + if msg != '': + logSys.log(std_level, "%s -- stdout: %r", realCmd, msg) + stderr.seek(0); msg = stderr.read() + if msg != '': + logSys.log(std_level, "%s -- stderr: %r", realCmd, msg) stdout.close() stderr.close() diff --git a/fail2ban/server/datedetector.py b/fail2ban/server/datedetector.py index a4839f5c..dbfb9e5d 100644 --- a/fail2ban/server/datedetector.py +++ b/fail2ban/server/datedetector.py @@ -29,6 +29,8 @@ from ..helpers import getLogger # Gets the instance of the logger. logSys = getLogger(__name__) +logLevel = 6 + class DateDetector(object): """Manages one or more date templates to find a date within a log line. @@ -147,7 +149,7 @@ class DateDetector(object): Returns ------- - re.MatchObject + re.MatchObject, DateTemplate The regex match returned from the first successfully matched template. """ @@ -156,10 +158,11 @@ class DateDetector(object): for template in self.__templates: match = template.matchDate(line) if not match is None: - logSys.debug("Matched time template %s" % template.name) + if logSys.getEffectiveLevel() <= logLevel: + logSys.log(logLevel, "Matched time template %s", template.name) template.hits += 1 - return match - return None + return (match, template) + return (None, None) finally: self.__lock.release() @@ -178,7 +181,7 @@ class DateDetector(object): ------- float The Unix timestamp returned from the first successfully matched - template. + template or None if not found. """ self.__lock.acquire() try: @@ -187,8 +190,9 @@ class DateDetector(object): date = template.getDate(line) if date is None: continue - logSys.debug("Got time %f for \"%r\" using template %s" % - (date[0], date[1].group(), template.name)) + if logSys.getEffectiveLevel() <= logLevel: + logSys.log(logLevel, "Got time %f for \"%r\" using template %s", + date[0], date[1].group(), template.name) return date except ValueError: pass @@ -196,6 +200,38 @@ class DateDetector(object): finally: self.__lock.release() + def getTime2(self, line, timeMatch = None): + """Attempts to return the date on a log line using given template. + + This uses the templates' `getDate` method in an attempt to find + a date. + Method 'getTime2' is a little bit faster as 'getTime' if template was specified (cause works without locking and without cycle) + + Parameters + ---------- + line : str + Line which is searched by the date templates. + timeMatch (timeMatch, template) : (Match, DateTemplate) + Time match and template previously returned from matchTime + + Returns + ------- + float + The Unix timestamp returned from the first successfully matched + template or None if not found. + """ + date = None + if timeMatch: + template = timeMatch[1] + if template is not None: + date = template.getDate(line, timeMatch[0]) + if date is not None: + if logSys.getEffectiveLevel() <= logLevel: + logSys.log(logLevel, "Got time(2) %f for \"%r\" using template %s", + date[0], date[1].group(), template.name) + return date + return self.getTime(line) + def sortTemplate(self): """Sort the date templates by number of hits @@ -206,9 +242,11 @@ class DateDetector(object): """ self.__lock.acquire() try: - logSys.debug("Sorting the template list") + if logSys.getEffectiveLevel() <= logLevel: + logSys.log(logLevel, "Sorting the template list") self.__templates.sort(key=lambda x: x.hits, reverse=True) t = self.__templates[0] - logSys.debug("Winning template: %s with %d hits" % (t.name, t.hits)) + if logSys.getEffectiveLevel() <= logLevel: + logSys.log(logLevel, "Winning template: %s with %d hits", t.name, t.hits) finally: self.__lock.release() diff --git a/fail2ban/server/datetemplate.py b/fail2ban/server/datetemplate.py index bcd17ec1..e1f6b3de 100644 --- a/fail2ban/server/datetemplate.py +++ b/fail2ban/server/datetemplate.py @@ -98,7 +98,7 @@ class DateTemplate(object): return dateMatch @abstractmethod - def getDate(self, line): + def getDate(self, line, dateMatch=None): """Abstract method, which should return the date for a log line This should return the date for a log line, typically taking the @@ -134,7 +134,7 @@ class DateEpoch(DateTemplate): DateTemplate.__init__(self) self.regex = "(?:^|(?P(?<=^\[))|(?P(?<=audit\()))\d{10}(?:\.\d{3,6})?(?(selinux)(?=:\d+\))(?(square)(?=\])))" - def getDate(self, line): + def getDate(self, line, dateMatch=None): """Method to return the date for a log line. Parameters @@ -148,7 +148,8 @@ class DateEpoch(DateTemplate): Tuple containing a Unix timestamp, and the string of the date which was matched and in turned used to calculated the timestamp. """ - dateMatch = self.matchDate(line) + if not dateMatch: + dateMatch = self.matchDate(line) if dateMatch: # extract part of format which represents seconds since epoch return (float(dateMatch.group()), dateMatch) @@ -212,7 +213,7 @@ class DatePatternRegex(DateTemplate): def name(self, value): raise NotImplementedError("Name derived from pattern") - def getDate(self, line): + def getDate(self, line, dateMatch=None): """Method to return the date for a log line. This uses a custom version of strptime, using the named groups @@ -229,7 +230,8 @@ class DatePatternRegex(DateTemplate): Tuple containing a Unix timestamp, and the string of the date which was matched and in turned used to calculated the timestamp. """ - dateMatch = self.matchDate(line) + if not dateMatch: + dateMatch = self.matchDate(line) if dateMatch: groupdict = dict( (key, value) @@ -253,7 +255,7 @@ class DateTai64n(DateTemplate): # yoh: we should not add an additional front anchor self.setRegex("@[0-9a-f]{24}", wordBegin=False) - def getDate(self, line): + def getDate(self, line, dateMatch=None): """Method to return the date for a log line. Parameters @@ -267,7 +269,8 @@ class DateTai64n(DateTemplate): Tuple containing a Unix timestamp, and the string of the date which was matched and in turned used to calculated the timestamp. """ - dateMatch = self.matchDate(line) + if not dateMatch: + dateMatch = self.matchDate(line) if dateMatch: # extract part of format which represents seconds since epoch value = dateMatch.group() diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 2dfeb174..296ff101 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -406,14 +406,16 @@ class Filter(JailThread): l = line.rstrip('\r\n') logSys.log(7, "Working on line %r", line) - timeMatch = self.dateDetector.matchTime(l) + (timeMatch, template) = self.dateDetector.matchTime(l) if timeMatch: tupleLine = ( l[:timeMatch.start()], l[timeMatch.start():timeMatch.end()], - l[timeMatch.end():]) + l[timeMatch.end():], + (timeMatch, template) + ) else: - tupleLine = (l, "", "") + tupleLine = (l, "", "", None) return "".join(tupleLine[::2]), self.findFailure( tupleLine, date, returnRawHost, checkAllRegex) @@ -475,7 +477,7 @@ class Filter(JailThread): self.__lastDate = date elif timeText: - dateTimeMatch = self.dateDetector.getTime(timeText) + dateTimeMatch = self.dateDetector.getTime2(timeText, tupleLine[3]) if dateTimeMatch is None: logSys.error("findFailure failed to parse timeText: " + timeText) @@ -492,7 +494,7 @@ class Filter(JailThread): date = self.__lastDate self.__lineBuffer = ( - self.__lineBuffer + [tupleLine])[-self.__lineBufferSize:] + self.__lineBuffer + [tupleLine[:3]])[-self.__lineBufferSize:] logSys.log(5, "Looking for failregex match of %r" % self.__lineBuffer) # Iterates over all the regular expressions. @@ -731,9 +733,9 @@ class FileFilter(Filter): break llen += len(line) l = line.rstrip('\r\n') - timeMatch = self.dateDetector.matchTime(l) + (timeMatch, template) = self.dateDetector.matchTime(l) if timeMatch: - dateTimeMatch = self.dateDetector.getTime(l[timeMatch.start():timeMatch.end()]) + dateTimeMatch = self.dateDetector.getTime2(l[timeMatch.start():timeMatch.end()], (timeMatch, template)) if not dateTimeMatch and lncntr: lncntr -= 1 continue diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 3e371945..9836b61d 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -81,7 +81,7 @@ class Server: self.flushLogs() def start(self, sock, pidfile, force = False): - logSys.info("Starting Fail2ban v" + version.version) + logSys.info("Starting Fail2ban v%s", version.version) # Install signal handlers signal.signal(signal.SIGTERM, self.__sigTERMhandler) @@ -443,6 +443,7 @@ class Server: logger.addHandler(hdlr) # Does not display this message at startup. if not self.__logTarget is None: + logSys.info("Start Fail2ban v%s", version.version) logSys.info( "Changed logging target to %s for Fail2ban v%s" % ((target diff --git a/fail2ban/tests/datedetectortestcase.py b/fail2ban/tests/datedetectortestcase.py index facfda6d..095b706a 100644 --- a/fail2ban/tests/datedetectortestcase.py +++ b/fail2ban/tests/datedetectortestcase.py @@ -106,6 +106,7 @@ class DateDetectorTest(unittest.TestCase): (not anchored, "bogus-prefix ")): log = prefix + sdate + "[sshd] error: PAM: Authentication failure" + # with getTime: logtime = self.__datedetector.getTime(log) if should_match: self.assertNotEqual(logtime, None, "getTime retrieved nothing: failure for %s, anchored: %r, log: %s" % ( sdate, anchored, log)) @@ -118,6 +119,20 @@ class DateDetectorTest(unittest.TestCase): self.assertEqual(logMatch.group(), sdate) else: self.assertEqual(logtime, None, "getTime should have not matched for %r Got: %s" % (sdate, logtime)) + # with matchTime and getTime2 (this combination used in filter) : + matchTime = self.__datedetector.matchTime(log) + logtime = self.__datedetector.getTime2(log, matchTime) + if should_match: + self.assertNotEqual(logtime, None, "getTime retrieved nothing: failure for %s, anchored: %r, log: %s" % ( sdate, anchored, log)) + ( logUnix, logMatch ) = logtime + self.assertEqual(logUnix, dateUnix, "getTime comparison failure for %s: \"%s\" is not \"%s\"" % (sdate, logUnix, dateUnix)) + if sdate.startswith('audit('): + # yes, special case, the group only matches the number + self.assertEqual(logMatch.group(), '1106513999.000') + else: + self.assertEqual(logMatch.group(), sdate) + else: + self.assertEqual(logtime, None, "getTime should have not matched for %r Got: %s" % (sdate, logtime)) def testStableSortTemplate(self): old_names = [x.name for x in self.__datedetector.templates] From dad4234beb325a1d9836c525575956d768938208 Mon Sep 17 00:00:00 2001 From: sebres Date: Thu, 25 Sep 2014 18:29:10 +0200 Subject: [PATCH 005/623] The tricky bug fixed - last position of log file will be never retrieved (#795): addJail (executed before addLog) early uses a "INSERT OR REPLACE" statement to update "enabled" to 1 (and add jail the first time used at once), but this syntax in sqlite always deletes an entry (cause of constraint) and inserts it again, so because of CASCADE all log entries with this jail will be also deleted from logs table. --- fail2ban/server/database.py | 6 +++++- fail2ban/server/filter.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index 7de87554..fcc56f73 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -274,8 +274,12 @@ class Fail2BanDb(object): Jail to be added to the database. """ cur.execute( - "INSERT OR REPLACE INTO jails(name, enabled) VALUES(?, 1)", + "INSERT OR IGNORE INTO jails(name, enabled) VALUES(?, 1)", (jail.name,)) + if cur.rowcount <= 0: + cur.execute( + "UPDATE jails SET enabled = 1 WHERE name = ? AND enabled != 1", + (jail.name,)) @commitandrollback def delJail(self, cur, jail): diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 296ff101..ee0e2a1d 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -576,7 +576,7 @@ class FileFilter(Filter): if lastpos and not tail: log.setPos(lastpos) self.__logs[path] = log - logSys.info("Added logfile = %s" % path) + logSys.info("Added logfile = %s (pos = %s, hash = %s)" , path, log.getPos(), log.getHash()) self._addLogPath(path) # backend specific def _addLogPath(self, path): From d22b2498d4dd2ba3f4ea45260335783648eb62cf Mon Sep 17 00:00:00 2001 From: sebres Date: Fri, 24 Oct 2014 01:32:04 +0200 Subject: [PATCH 006/623] normalizing time config entries: use time abbreviation (str2seconds) for all time options such 'dbpurgeage', 'bantime', 'findtime', ex.: default '1d' instead '86400'; code review and test case extended; --- config/fail2ban.conf | 2 +- config/jail.conf | 14 +++++++------- fail2ban/client/fail2banreader.py | 2 +- fail2ban/client/jailreader.py | 6 +++--- fail2ban/server/database.py | 2 +- fail2ban/server/mytime.py | 6 ++++-- fail2ban/server/transmitter.py | 4 ++-- fail2ban/tests/clientreadertestcase.py | 2 +- fail2ban/tests/databasetestcase.py | 9 +++++++++ fail2ban/tests/servertestcase.py | 2 ++ 10 files changed, 31 insertions(+), 18 deletions(-) diff --git a/config/fail2ban.conf b/config/fail2ban.conf index 89e2538e..7c001155 100644 --- a/config/fail2ban.conf +++ b/config/fail2ban.conf @@ -66,4 +66,4 @@ dbfile = /var/lib/fail2ban/fail2ban.sqlite3 # Options: dbpurgeage # Notes.: Sets age at which bans should be purged from the database # Values: [ SECONDS ] Default: 86400 (24hours) -dbpurgeage = 86400 +dbpurgeage = 1d diff --git a/config/jail.conf b/config/jail.conf index c8dc6d9c..21b97798 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -18,7 +18,7 @@ # See man 5 jail.conf for details. # # [DEFAULT] -# bantime = 3600 +# bantime = 1h # # [sshd] # enabled = true @@ -56,11 +56,11 @@ ignoreip = 127.0.0.1/8 ignorecommand = # "bantime" is the number of seconds that a host is banned. -bantime = 600 +bantime = 10m # A host is banned if it has generated "maxretry" during the last "findtime" # seconds. -findtime = 600 +findtime = 10m # "maxretry" is the number of failures before a host get banned. maxretry = 5 @@ -258,7 +258,7 @@ logpath = %(apache_error_log)s # for email addresses. The mail outputs are buffered. port = http,https logpath = %(apache_access_log)s -bantime = 172800 +bantime = 48h maxretry = 1 @@ -738,8 +738,8 @@ maxretry = 5 logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s -bantime = 604800 ; 1 week -findtime = 86400 ; 1 day +bantime = 1w +findtime = 1d maxretry = 5 @@ -818,7 +818,7 @@ filter = apache-pass logpath = %(apache_access_log)s blocktype = RETURN returntype = DROP -bantime = 3600 +bantime = 1h maxretry = 1 findtime = 1 diff --git a/fail2ban/client/fail2banreader.py b/fail2ban/client/fail2banreader.py index 709f4b5d..c55f65ea 100644 --- a/fail2ban/client/fail2banreader.py +++ b/fail2ban/client/fail2banreader.py @@ -49,7 +49,7 @@ class Fail2banReader(ConfigReader): ["string", "logtarget", "STDERR"], ["string", "syslogsocket", "auto"], ["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"], - ["int", "dbpurgeage", 86400]] + ["string", "dbpurgeage", "1d"]] self.__opts = ConfigReader.getOptions(self, "Definition", opts) def convert(self): diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index 54ac59fa..46f910e3 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -97,9 +97,9 @@ class JailReader(ConfigReader): ["string", "logpath", None], ["string", "logencoding", None], ["string", "backend", "auto"], - ["int", "maxretry", None], - ["int", "findtime", None], - ["int", "bantime", None], + ["int", "maxretry", None], + ["string", "findtime", None], + ["string", "bantime", None], ["string", "usedns", None], ["string", "failregex", None], ["string", "ignoreregex", None], diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index fcc56f73..3cfaa5a6 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -218,7 +218,7 @@ class Fail2BanDb(object): @purgeage.setter def purgeage(self, value): - self._purgeAge = int(value) + self._purgeAge = MyTime.str2seconds(value) @commitandrollback def createDb(self, cur): diff --git a/fail2ban/server/mytime.py b/fail2ban/server/mytime.py index 0a7e6a79..47509f19 100644 --- a/fail2ban/server/mytime.py +++ b/fail2ban/server/mytime.py @@ -116,8 +116,11 @@ class MyTime: # # @returns number (calculated seconds from expression "val") - #@staticmethod + @staticmethod def str2seconds(val): + # replace together standing abbreviations, example '1d12h' -> '1d 12h': + val = re.sub(r"(?i)(?<=[a-z])(\d)", r" \1", val) + # replace abbreviation with expression: for rexp, rpl in ( (r"days?|da|dd?", 24*60*60), (r"week?|wee?|ww?", 7*24*60*60), (r"months?|mon?", (365*3+366)*24*60*60/4/12), (r"years?|yea?|yy?", (365*3+366)*24*60*60/4), (r"seconds?|sec?|ss?", 1), (r"minutes?|min?|mm?", 60), (r"hours?|ho|hh?", 60*60), @@ -125,4 +128,3 @@ class MyTime: val = re.sub(r"(?i)(?<=[\d\s])(%s)\b" % rexp, "*"+str(rpl), val) val = re.sub(r"(\d)\s+(\d)", r"\1+\2", val); return eval(val) - str2seconds = staticmethod(str2seconds) diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 0d9f0fe4..eb5b7713 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -216,7 +216,7 @@ class Transmitter: return self.__server.getUseDns(name) elif command[1] == "findtime": value = command[2] - self.__server.setFindTime(name, int(value)) + self.__server.setFindTime(name, value) return self.__server.getFindTime(name) elif command[1] == "datepattern": value = command[2] @@ -233,7 +233,7 @@ class Transmitter: # command elif command[1] == "bantime": value = command[2] - self.__server.setBanTime(name, int(value)) + self.__server.setBanTime(name, value) return self.__server.getBanTime(name) elif command[1] == "banip": value = command[2] diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index d19090be..d0c37a3e 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -668,7 +668,7 @@ class JailsReaderTest(LogCaptureTestCase): self.assertEqual(sorted(commands), [['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3'], - ['set', 'dbpurgeage', 86400], + ['set', 'dbpurgeage', '1d'], ['set', 'loglevel', "INFO"], ['set', 'logtarget', '/var/log/fail2ban.log'], ['set', 'syslogsocket', 'auto']]) diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index 3d156eda..083be2b2 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -70,6 +70,15 @@ class DatabaseTest(LogCaptureTestCase): return self.assertEqual(self.dbFilename, self.db.filename) + def testPurgeAge(self): + if Fail2BanDb is None: # pragma: no cover + return + self.assertEqual(self.db.purgeage, 86400) + self.db.purgeage = '1y6mon15d5h30m' + self.assertEqual(self.db.purgeage, 48652200) + self.db.purgeage = '2y 12mon 30d 10h 60m' + self.assertEqual(self.db.purgeage, 48652200*2) + def testCreateInvalidPath(self): if Fail2BanDb is None: # pragma: no cover return diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 07e10c7d..a7d7adef 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -259,6 +259,7 @@ class Transmitter(TransmitterBase): def testJailFindTime(self): self.setGetTest("findtime", "120", 120, jail=self.jailName) self.setGetTest("findtime", "60", 60, jail=self.jailName) + self.setGetTest("findtime", "30m", 30*60, jail=self.jailName) self.setGetTest("findtime", "-60", -60, jail=self.jailName) self.setGetTestNOK("findtime", "Dog", jail=self.jailName) @@ -266,6 +267,7 @@ class Transmitter(TransmitterBase): self.setGetTest("bantime", "600", 600, jail=self.jailName) self.setGetTest("bantime", "50", 50, jail=self.jailName) self.setGetTest("bantime", "-50", -50, jail=self.jailName) + self.setGetTest("bantime", "15d 5h 30m", 1315800, jail=self.jailName) self.setGetTestNOK("bantime", "Cat", jail=self.jailName) def testDatePattern(self): From db5ee0842a56a5db0e933e0125b0d0ab4773c86a Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 28 Oct 2014 21:57:16 +0100 Subject: [PATCH 007/623] increase code coverage --- fail2ban/server/datedetector.py | 2 +- fail2ban/server/jail.py | 6 ++++-- fail2ban/tests/datedetectortestcase.py | 13 +++++++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/fail2ban/server/datedetector.py b/fail2ban/server/datedetector.py index dbfb9e5d..4bdcefd2 100644 --- a/fail2ban/server/datedetector.py +++ b/fail2ban/server/datedetector.py @@ -194,7 +194,7 @@ class DateDetector(object): logSys.log(logLevel, "Got time %f for \"%r\" using template %s", date[0], date[1].group(), template.name) return date - except ValueError: + except ValueError: # pragma: no cover pass return None finally: diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index a866cb51..c5f2e9cd 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -76,7 +76,8 @@ class Jail: self.__queue = Queue.Queue() self.__filter = None logSys.info("Creating new jail '%s'" % self.name) - self._setBackend(backend) + if backend is not None: + self._setBackend(backend) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self.name) @@ -106,11 +107,12 @@ class Jail: logSys.info("Initiated %r backend" % b) self.__actions = Actions(self) return # we are done - except ImportError, e: + except ImportError, e: # pragma: no cover # Log debug if auto, but error if specific logSys.log( logging.DEBUG if backend == "auto" else logging.ERROR, "Backend %r failed to initialize due to %s" % (b, e)) + # pragma: no cover # log error since runtime error message isn't printed, INVALID COMMAND logSys.error( "Failed to initialize any backend for Jail %r" % self.name) diff --git a/fail2ban/tests/datedetectortestcase.py b/fail2ban/tests/datedetectortestcase.py index 095b706a..9a7d3e78 100644 --- a/fail2ban/tests/datedetectortestcase.py +++ b/fail2ban/tests/datedetectortestcase.py @@ -29,20 +29,29 @@ import time import datetime from ..server.datedetector import DateDetector +from ..server import datedetector from ..server.datetemplate import DateTemplate -from .utils import setUpMyTime, tearDownMyTime +from .utils import setUpMyTime, tearDownMyTime, LogCaptureTestCase +from ..helpers import getLogger + +logSys = getLogger("fail2ban") -class DateDetectorTest(unittest.TestCase): +class DateDetectorTest(LogCaptureTestCase): def setUp(self): """Call before every test case.""" + LogCaptureTestCase.setUp(self) + self.__old_eff_level = datedetector.logLevel + datedetector.logLevel = logSys.getEffectiveLevel() setUpMyTime() self.__datedetector = DateDetector() self.__datedetector.addDefaultTemplate() def tearDown(self): """Call after every test case.""" + LogCaptureTestCase.tearDown(self) + datedetector.logLevel = self.__old_eff_level tearDownMyTime() def testGetEpochTime(self): From d23d103f967440942ff598e7543153a54be3f738 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 2 Dec 2014 00:56:20 +0100 Subject: [PATCH 008/623] code review and few new test cases --- fail2ban/server/actions.py | 1 + fail2ban/server/mytime.py | 42 ++++++++++++++++++---------------- fail2ban/tests/misctestcase.py | 18 +++++++++++++++ fail2ban/tests/utils.py | 3 ++- 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index b4612f8c..c00dc7fc 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -167,6 +167,7 @@ class Actions(JailThread, Mapping): # @param value the time def setBanTime(self, value): + value = MyTime.str2seconds(value) self.__banManager.setBanTime(value) logSys.info("Set banTime = %s" % value) diff --git a/fail2ban/server/mytime.py b/fail2ban/server/mytime.py index 47509f19..329e2465 100644 --- a/fail2ban/server/mytime.py +++ b/fail2ban/server/mytime.py @@ -98,32 +98,34 @@ class MyTime: else: return time.localtime(MyTime.myTime) - ## - # Wraps string expression like "1h 2m 3s" into number contains seconds (3723). - # The string expression will be evaluated as mathematical expression, spaces between each groups - # will be wrapped to "+" operand (only if any operand does not specified between). - # Because of case insensitivity and overwriting with minutes ("m" or "mm"), the short replacement for month - # are "mo" or "mon" (like %b by date formating). - # Ex: 1hour+30min = 5400 - # 0d 1h 30m = 5400 - # 1year-6mo = 15778800 - # 6 months = 15778800 - # warn: month is not 30 days, it is a year in seconds / 12, the leap years will be respected also: - # >>>> float(Test.str2seconds("1month")) / 60 / 60 / 24 - # 30.4375 - # >>>> float(Test.str2seconds("1year")) / 60 / 60 / 24 - # 365.25 - # - # @returns number (calculated seconds from expression "val") - @staticmethod def str2seconds(val): + """Wraps string expression like "1h 2m 3s" into number contains seconds (3723). + The string expression will be evaluated as mathematical expression, spaces between each groups + will be wrapped to "+" operand (only if any operand does not specified between). + Because of case insensitivity and overwriting with minutes ("m" or "mm"), the short replacement for month + are "mo" or "mon". + Ex: 1hour+30min = 5400 + 0d 1h 30m = 5400 + 1year-6mo = 15778800 + 6 months = 15778800 + warn: month is not 30 days, it is a year in seconds / 12, the leap years will be respected also: + >>>> float(str2seconds("1month")) / 60 / 60 / 24 + 30.4375 + >>>> float(str2seconds("1year")) / 60 / 60 / 24 + 365.25 + + @returns number (calculated seconds from expression "val") + """ + if isinstance(val, (int, long, float, complex)): + return val # replace together standing abbreviations, example '1d12h' -> '1d 12h': val = re.sub(r"(?i)(?<=[a-z])(\d)", r" \1", val) # replace abbreviation with expression: for rexp, rpl in ( - (r"days?|da|dd?", 24*60*60), (r"week?|wee?|ww?", 7*24*60*60), (r"months?|mon?", (365*3+366)*24*60*60/4/12), (r"years?|yea?|yy?", (365*3+366)*24*60*60/4), - (r"seconds?|sec?|ss?", 1), (r"minutes?|min?|mm?", 60), (r"hours?|ho|hh?", 60*60), + (r"days?|da|dd?", 24*60*60), (r"weeks?|wee?|ww?", 7*24*60*60), (r"months?|mon?", (365*3+366)*24*60*60/4/12), + (r"years?|yea?|yy?", (365*3+366)*24*60*60/4), + (r"seconds?|sec?|ss?", 1), (r"minutes?|min?|mm?", 60), (r"hours?|hou?|hh?", 60*60), ): val = re.sub(r"(?i)(?<=[\d\s])(%s)\b" % rexp, "*"+str(rpl), val) val = re.sub(r"(\d)\s+(\d)", r"\1+\2", val); diff --git a/fail2ban/tests/misctestcase.py b/fail2ban/tests/misctestcase.py index e28ce422..a2310ec5 100644 --- a/fail2ban/tests/misctestcase.py +++ b/fail2ban/tests/misctestcase.py @@ -35,6 +35,7 @@ from StringIO import StringIO from ..helpers import formatExceptionInfo, mbasename, TraceBack, FormatterWithTraceBack, getLogger from ..helpers import splitcommaspace from ..server.datetemplate import DatePatternRegex +from ..server.mytime import MyTime class HelpersTest(unittest.TestCase): @@ -229,3 +230,20 @@ class CustomDateFormatsTest(unittest.TestCase): self.assertEqual( date, datetime.datetime(2007, 1, 25, 16, 0)) + +class MyTimeTest(unittest.TestCase): + + def testStr2Seconds(self): + # several formats / write styles: + str2sec = MyTime.str2seconds + self.assertEqual(str2sec('1y6mo30w15d12h35m25s'), 66821725) + self.assertEqual(str2sec('2yy 3mo 4ww 10dd 5hh 30mm 20ss'), 74307620) + self.assertEqual(str2sec('2 years 3 months 4 weeks 10 days 5 hours 30 minutes 20 seconds'), 74307620) + self.assertEqual(str2sec('1 year + 1 month - 1 week + 1 day'), 33669000) + self.assertEqual(str2sec('2 * 0.5 yea + 1*1 mon - 3*1/3 wee + 2/2 day - (2*12 hou 3*20 min 80 sec) '), 33578920.0) + self.assertEqual(str2sec('2*.5y+1*1mo-3*1/3w+2/2d-(2*12h3*20m80s) '), 33578920.0) + self.assertEqual(str2sec('1ye -2mo -3we -4da -5ho -6mi -7se'), 24119633) + # month and year in days : + self.assertEqual(float(str2sec("1 month")) / 60 / 60 / 24, 30.4375) + self.assertEqual(float(str2sec("1 year")) / 60 / 60 / 24, 365.25) + diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 8172e7ec..dff3faae 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -133,6 +133,7 @@ def gatherTests(regexps=None, no_network=False): tests.addTest(unittest.makeSuite(misctestcase.SetupTest)) tests.addTest(unittest.makeSuite(misctestcase.TestsUtilsTest)) tests.addTest(unittest.makeSuite(misctestcase.CustomDateFormatsTest)) + tests.addTest(unittest.makeSuite(misctestcase.MyTimeTest)) # Database tests.addTest(unittest.makeSuite(databasetestcase.DatabaseTest)) @@ -300,4 +301,4 @@ else: kernel32.CloseHandle(process) return True else: - return False \ No newline at end of file + return False From 3540619a736644180c603dd324c8242d0f4e9df4 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 2 Dec 2014 11:57:43 +0100 Subject: [PATCH 009/623] code review, test case extended; --- fail2ban/server/filter.py | 23 +++++++------ fail2ban/tests/filtertestcase.py | 58 ++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index ee0e2a1d..621f826b 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -673,14 +673,19 @@ class FileFilter(Filter): logSys.exception(e) return False except OSError, e: # pragma: no cover - Requires implemention error in FileContainer to generate - logSys.error("Internal errror in FileContainer open method - please report as a bug to https://github.com/fail2ban/fail2ban/issues") + logSys.error("Internal error in FileContainer open method - please report as a bug to https://github.com/fail2ban/fail2ban/issues") logSys.exception(e) return False # prevent completely read of big files first time (after start of service), initial seek to start time using half-interval search algorithm: if log.getPos() == 0 and startTime is not None: - # startTime = MyTime.time() - self.getFindTime() - self.seekToTime(log, startTime) + try: + # startTime = MyTime.time() - self.getFindTime() + self.seekToTime(log, startTime) + except Exception, e: # pragma: no cover + logSys.error("Error during seek to start time in \"%s\"", filename) + logSys.exception(e) + return False # yoh: has_content is just a bool, so do not expect it to # change -- loop is exited upon break, and is not entered at @@ -717,7 +722,7 @@ class FileFilter(Filter): cntr = 0 unixTime = None lasti = 0 - movecntr = 3 + movecntr = 1 while maxp > minp: i = int(minp + (maxp - minp) / 2) pos = container.seek(i) @@ -726,7 +731,8 @@ class FileFilter(Filter): lncntr = 5; dateTimeMatch = None llen = 0 - i = pos + if lastpos == pos: + i = pos while True: line = container.readline() if not line: @@ -763,6 +769,7 @@ class FileFilter(Filter): lastpos = container.seek(lastFew, False) else: lastpos = container.seek(lastpos, False) + container.setPos(lastpos) if logSys.getEffectiveLevel() <= logging.DEBUG: logSys.debug("Position %s from %s, found time %s (%s) within %s seeks", lastpos, fs, unixTime, (datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S") if unixTime is not None else ''), cntr) @@ -952,10 +959,6 @@ class DNSUtils: logSys.warning("Unable to find a corresponding IP address for %s: %s" % (dns, e)) return list() - except socket.error, e: - logSys.warning("Socket error raised trying to resolve hostname %s: %s" - % (dns, e)) - return list() @staticmethod def ipToName(ip): @@ -984,7 +987,7 @@ class DNSUtils: try: socket.inet_aton(s[0]) return True - except socket.error: + except socket.error: # pragma: no cover return False @staticmethod diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index c7dba5ce..7a35bdd9 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -27,7 +27,7 @@ import unittest import getpass import os import sys -import time +import time, datetime import tempfile import uuid @@ -38,7 +38,7 @@ except ImportError: from ..server.jail import Jail from ..server.filterpoll import FilterPoll -from ..server.filter import Filter, FileFilter, DNSUtils +from ..server.filter import Filter, FileFilter, FileContainer, DNSUtils from ..server.failmanager import FailManagerEmpty from ..server.mytime import MyTime from .utils import setUpMyTime, tearDownMyTime, mtimesleep, LogCaptureTestCase @@ -346,6 +346,60 @@ class LogFileFilterPoll(unittest.TestCase): self.assertTrue(self.filter.isModified(LogFileFilterPoll.FILENAME)) self.assertFalse(self.filter.isModified(LogFileFilterPoll.FILENAME)) + def testSeekToTime(self): + fname = tempfile.mktemp(prefix='tmp_fail2ban', suffix='.log') + tm = lambda time: datetime.datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M:%S") + time = 1417512352 + f = open(fname, 'w') + fc = FileContainer(fname, self.filter.getLogEncoding()) + fc.open() + fc.setPos(0); self.filter.seekToTime(fc, time) + try: + f.flush() + # empty : + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 0) + # one entry with exact time: + f.write("%s [sshd] error: PAM: failure len 1\n" % tm(time)) + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + # one entry with smaller time: + f.seek(0) + f.write("%s [sshd] error: PAM: failure len 1\n" % tm(time - 10)) + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 0) + f.write("%s [sshd] error: PAM: failure len 3 2 1\n" % tm(time - 9)) + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 0) + # add exact time between: + f.write("%s [sshd] error: PAM: failure\n" % tm(time - 1)) + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 110) + # stil one exact line: + f.write("%s [sshd] error: PAM: Authentication failure\n" % tm(time)) + f.write("%s [sshd] error: PAM: failure len 1\n" % tm(time)) + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 110) + # add something hereafter: + f.write("%s [sshd] error: PAM: failure len 3 2 1\n" % tm(time + 2)) + f.write("%s [sshd] error: PAM: Authentication failure\n" % tm(time + 3)) + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 110) + # add something hereafter: + f.write("%s [sshd] error: PAM: failure\n" % tm(time + 9)) + f.write("%s [sshd] error: PAM: failure len 3 2 1\n" % tm(time + 9)) + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 110) + + finally: + fc.close() + _killfile(f, fname) class LogFileMonitor(LogCaptureTestCase): """Few more tests for FilterPoll API From 59bf5013c0f7ddcfa88ee638031ec6f79c97393e Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 15 Jul 2015 14:58:00 +0200 Subject: [PATCH 010/623] - performance of fail2ban optimized -- cache dnsToIp, ipToName to prevent long wait during retrieving of ip/name for wrong dns or lazy dns-system; -- instead of simple "sleep" used conditional wait "wait_for", that internal increases sleep interval up to sleeptime; -- ticket / banmanager / failmanager modules are performance optimized; -- api of filter (log files), jail, etc. rewritten and extended for performance purposes; - performance of test cases optimized: -- added option "--fast" to decrease wait intervals, avoid passive waiting, and skip few very slow test cases; - code review after partially cherry pick of branch 'ban-time-incr' (see gh-716) -- ticket module prepared to easy merge with newest version of 'ban-time-incr', now additionally holds banTime, banCount and json-data; -- executeCmd partially moved from action to new module utils, etc. -- python 2.6 compatibility; - testExecuteTimeoutWithNastyChildren: test case repaired - wait for pid file inside bash, kill tree in any case (gh-1155); - testSocket: test case repaired - wait for server thread starts a socket (listener) --- MANIFEST | 2 +- bin/fail2ban-testcases | 5 +- doc/fail2ban.server.rst | 2 +- ...faildata.rst => fail2ban.server.utils.rst} | 4 +- fail2ban/server/action.py | 73 +----- fail2ban/server/actions.py | 12 +- fail2ban/server/banmanager.py | 37 ++- fail2ban/server/database.py | 13 +- fail2ban/server/faildata.py | 71 ----- fail2ban/server/failmanager.py | 38 +-- fail2ban/server/filter.py | 70 +++-- fail2ban/server/filtergamin.py | 26 +- fail2ban/server/filterpoll.py | 68 ++--- fail2ban/server/jailthread.py | 7 +- fail2ban/server/server.py | 12 +- fail2ban/server/ticket.py | 145 +++++++++-- fail2ban/server/transmitter.py | 2 +- fail2ban/server/utils.py | 242 ++++++++++++++++++ fail2ban/tests/action_d/test_badips.py | 2 + fail2ban/tests/action_d/test_smtp.py | 2 + fail2ban/tests/actionstestcase.py | 10 +- fail2ban/tests/actiontestcase.py | 59 +++-- fail2ban/tests/banmanagertestcase.py | 1 + fail2ban/tests/databasetestcase.py | 13 +- fail2ban/tests/dummyjail.py | 23 +- fail2ban/tests/filtertestcase.py | 70 ++--- fail2ban/tests/servertestcase.py | 47 ++-- fail2ban/tests/sockettestcase.py | 17 +- fail2ban/tests/utils.py | 85 +++--- 29 files changed, 768 insertions(+), 390 deletions(-) rename doc/{fail2ban.server.faildata.rst => fail2ban.server.utils.rst} (55%) delete mode 100644 fail2ban/server/faildata.py create mode 100644 fail2ban/server/utils.py diff --git a/MANIFEST b/MANIFEST index d6bdba10..7306cc41 100644 --- a/MANIFEST +++ b/MANIFEST @@ -180,7 +180,6 @@ fail2ban/server/banmanager.py fail2ban/server/database.py fail2ban/server/datedetector.py fail2ban/server/datetemplate.py -fail2ban/server/faildata.py fail2ban/server/failmanager.py fail2ban/server/failregex.py fail2ban/server/filter.py @@ -197,6 +196,7 @@ fail2ban/server/server.py fail2ban/server/strptime.py fail2ban/server/ticket.py fail2ban/server/transmitter.py +fail2ban/server/utils.py fail2ban/tests/__init__.py fail2ban/tests/action_d/__init__.py fail2ban/tests/action_d/test_badips.py diff --git a/bin/fail2ban-testcases b/bin/fail2ban-testcases index dd6547a5..28af8265 100755 --- a/bin/fail2ban-testcases +++ b/bin/fail2ban-testcases @@ -58,6 +58,9 @@ def get_opt_parser(): Option('-n', "--no-network", action="store_true", dest="no_network", help="Do not run tests that require the network"), + Option('-f', "--fast", action="store_true", + dest="fast", + help="Try to increase speed of the tests, decreasing of wait intervals, memory database"), Option("-t", "--log-traceback", action='store_true', help="Enrich log-messages with compressed tracebacks"), Option("--full-traceback", action='store_true', @@ -120,7 +123,7 @@ if not opts.log_level or opts.log_level != 'critical': # pragma: no cover print("Fail2ban %s test suite. Python %s. Please wait..." \ % (version, str(sys.version).replace('\n', ''))) -tests = gatherTests(regexps, opts.no_network) +tests = gatherTests(regexps, opts) # # Run the tests # diff --git a/doc/fail2ban.server.rst b/doc/fail2ban.server.rst index fdc3bd20..dc5d1dee 100644 --- a/doc/fail2ban.server.rst +++ b/doc/fail2ban.server.rst @@ -10,7 +10,6 @@ fail2ban.server package fail2ban.server.database fail2ban.server.datedetector fail2ban.server.datetemplate - fail2ban.server.faildata fail2ban.server.failmanager fail2ban.server.failregex fail2ban.server.filter @@ -26,3 +25,4 @@ fail2ban.server package fail2ban.server.strptime fail2ban.server.ticket fail2ban.server.transmitter + fail2ban.server.utils diff --git a/doc/fail2ban.server.faildata.rst b/doc/fail2ban.server.utils.rst similarity index 55% rename from doc/fail2ban.server.faildata.rst rename to doc/fail2ban.server.utils.rst index c11d8208..3061e632 100644 --- a/doc/fail2ban.server.faildata.rst +++ b/doc/fail2ban.server.utils.rst @@ -1,7 +1,7 @@ -fail2ban.server.faildata module +fail2ban.server.utils module =============================== -.. automodule:: fail2ban.server.faildata +.. automodule:: fail2ban.server.utils :members: :undoc-members: :show-inheritance: diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 85bdd166..446ae853 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -32,6 +32,7 @@ import time from abc import ABCMeta from collections import MutableMapping +from .utils import Utils from ..helpers import getLogger # Gets the instance of the logger. @@ -40,21 +41,6 @@ logSys = getLogger(__name__) # Create a lock for running system commands _cmd_lock = threading.Lock() -# Some hints on common abnormal exit codes -_RETCODE_HINTS = { - 127: '"Command not found". Make sure that all commands in %(realCmd)r ' - 'are in the PATH of fail2ban-server process ' - '(grep -a PATH= /proc/`pidof -x fail2ban-server`/environ). ' - 'You may want to start ' - '"fail2ban-server -f" separately, initiate it with ' - '"fail2ban-client reload" in another shell session and observe if ' - 'additional informative error messages appear in the terminals.' - } - -# Dictionary to lookup signal name from number -signame = dict((num, name) - for name, num in signal.__dict__.iteritems() if name.startswith("SIG")) - class CallingMap(MutableMapping): """A Mapping type which returns the result of callable values. @@ -561,61 +547,6 @@ class CommandAction(ActionBase): _cmd_lock.acquire() try: - retcode = None # to guarantee being defined upon early except - stdout = tempfile.TemporaryFile(suffix=".stdout", prefix="fai2ban_") - stderr = tempfile.TemporaryFile(suffix=".stderr", prefix="fai2ban_") - - popen = subprocess.Popen( - realCmd, stdout=stdout, stderr=stderr, shell=True, - preexec_fn=os.setsid # so that killpg does not kill our process - ) - stime = time.time() - retcode = popen.poll() - while time.time() - stime <= timeout and retcode is None: - time.sleep(0.1) - retcode = popen.poll() - if retcode is None: - logSys.error("%s -- timed out after %i seconds." % - (realCmd, timeout)) - pgid = os.getpgid(popen.pid) - os.killpg(pgid, signal.SIGTERM) # Terminate the process - time.sleep(0.1) - retcode = popen.poll() - if retcode is None: # Still going... - os.killpg(pgid, signal.SIGKILL) # Kill the process - time.sleep(0.1) - retcode = popen.poll() - except OSError as e: - logSys.error("%s -- failed with %s" % (realCmd, e)) + return Utils.executeCmd(realCmd, timeout, shell=True, output=False) finally: _cmd_lock.release() - - std_level = retcode == 0 and logging.DEBUG or logging.ERROR - if std_level >= logSys.getEffectiveLevel(): - stdout.seek(0); msg = stdout.read() - if msg != '': - logSys.log(std_level, "%s -- stdout: %r", realCmd, msg) - stderr.seek(0); msg = stderr.read() - if msg != '': - logSys.log(std_level, "%s -- stderr: %r", realCmd, msg) - stdout.close() - stderr.close() - - if retcode == 0: - logSys.debug("%s -- returned successfully" % realCmd) - return True - elif retcode is None: - logSys.error("%s -- unable to kill PID %i" % (realCmd, popen.pid)) - elif retcode < 0 or retcode > 128: - # dash would return negative while bash 128 + n - sigcode = -retcode if retcode < 0 else retcode - 128 - logSys.error("%s -- killed with %s (return code: %s)" % - (realCmd, signame.get(sigcode, "signal %i" % sigcode), retcode)) - else: - msg = _RETCODE_HINTS.get(retcode, None) - logSys.error("%s -- returned %i" % (realCmd, retcode)) - if msg: - logSys.info("HINT on %i: %s" - % (retcode, msg % locals())) - return False - diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index c00dc7fc..5469722f 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -42,6 +42,7 @@ from .banmanager import BanManager from .jailthread import JailThread from .action import ActionBase, CommandAction, CallingMap from .mytime import MyTime +from .utils import Utils from ..helpers import getLogger # Gets the instance of the logger. @@ -225,14 +226,11 @@ class Actions(JailThread, Mapping): self._jail.name, name, e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) while self.active: - if not self.idle: - #logSys.debug(self._jail.name + ": action") - ret = self.__checkBan() - if not ret: - self.__checkUnBan() - time.sleep(self.sleeptime) - else: + if self.idle: time.sleep(self.sleeptime) + continue + if not Utils.wait_for(self.__checkBan, self.sleeptime): + self.__checkUnBan() self.__flushBan() actions = self._actions.items() diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index 662666b0..a958a5c8 100644 --- a/fail2ban/server/banmanager.py +++ b/fail2ban/server/banmanager.py @@ -247,12 +247,10 @@ class BanManager: @staticmethod def createBanTicket(ticket): - ip = ticket.getIP() - #lastTime = ticket.getTime() - lastTime = MyTime.time() - banTicket = BanTicket(ip, lastTime, ticket.getMatches()) - banTicket.setAttempt(ticket.getAttempt()) - return banTicket + # we should always use correct time to calculate correct end time (ban time is variable now, + # + possible double banning by restore from database and from log file) + # so use as lastTime always time from ticket. + return BanTicket(ticket=ticket) ## # Add a ban ticket. @@ -264,11 +262,25 @@ class BanManager: def addBanTicket(self, ticket): try: self.__lock.acquire() - if not self._inBanList(ticket): - self.__banList.append(ticket) - self.__banTotal += 1 - return True - return False + # check already banned + for i in self.__banList: + if ticket.getIP() == i.getIP(): + # if already permanent + btorg, torg = i.getBanTime(self.__banTime), i.getTime() + if btorg == -1: + return False + # if given time is less than already banned time + btnew, tnew = ticket.getBanTime(self.__banTime), ticket.getTime() + if btnew != -1 and tnew + btnew <= torg + btorg: + return False + # we have longest ban - set new (increment) ban time + i.setTime(tnew) + i.setBanTime(btnew) + return False + # not yet banned - add new + self.__banList.append(ticket) + self.__banTotal += 1 + return True finally: self.__lock.release() @@ -313,8 +325,7 @@ class BanManager: return list() # Gets the list of ticket to remove. - unBanList = [ticket for ticket in self.__banList - if ticket.getTime() < time - self.__banTime] + unBanList = [ticket for ticket in self.__banList if ticket.isTimedOut(time, self.__banTime)] # Removes tickets. self.__banList = [ticket for ticket in self.__banList diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index 3cfaa5a6..c6e8c95c 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -418,8 +418,7 @@ class Fail2BanDb(object): cur.execute( "INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)", (jail.name, ticket.getIP(), int(round(ticket.getTime())), - {"matches": ticket.getMatches(), - "failures": ticket.getAttempt()})) + ticket.getData())) @commitandrollback def delBan(self, cur, jail, ip): @@ -477,8 +476,8 @@ class Fail2BanDb(object): tickets = [] for ip, timeofban, data in self._getBans(**kwargs): #TODO: Implement data parts once arbitrary match keys completed - tickets.append(FailTicket(ip, timeofban, data.get('matches'))) - tickets[-1].setAttempt(data.get('failures', 1)) + tickets.append(FailTicket(ip, timeofban)) + tickets[-1].setData(data) return tickets def getBansMerged(self, ip=None, jail=None, bantime=None): @@ -520,6 +519,7 @@ class Fail2BanDb(object): prev_banip = results[0][0] matches = [] failures = 0 + tickdata = {} for banip, timeofban, data in results: #TODO: Implement data parts once arbitrary match keys completed if banip != prev_banip: @@ -530,11 +530,14 @@ class Fail2BanDb(object): prev_banip = banip matches = [] failures = 0 - matches.extend(data.get('matches', [])) + tickdata = {} + matches.extend(data.get('matches', ())) failures += data.get('failures', 1) + tickdata.update(data.get('data', {})) prev_timeofban = timeofban ticket = FailTicket(banip, prev_timeofban, matches) ticket.setAttempt(failures) + ticket.setData(**tickdata) tickets.append(ticket) if cacheKey: diff --git a/fail2ban/server/faildata.py b/fail2ban/server/faildata.py deleted file mode 100644 index 2dd8d4d8..00000000 --- a/fail2ban/server/faildata.py +++ /dev/null @@ -1,71 +0,0 @@ -# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- -# vi: set ft=python sts=4 ts=4 sw=4 noet : - -# This file is part of Fail2Ban. -# -# Fail2Ban is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# Fail2Ban is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Fail2Ban; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -# Author: Cyril Jaquier -# - -__author__ = "Cyril Jaquier" -__copyright__ = "Copyright (c) 2004 Cyril Jaquier" -__license__ = "GPL" - -from ..helpers import getLogger - -# Gets the instance of the logger. -logSys = getLogger(__name__) - - -class FailData: - - def __init__(self): - self.__retry = 0 - self.__lastTime = 0 - self.__lastReset = 0 - self.__matches = [] - - def setRetry(self, value): - self.__retry = value - # keep only the last matches or reset entirely - # Explicit if/else for compatibility with Python 2.4 - if value: - self.__matches = self.__matches[-min(len(self.__matches, value)):] - else: - self.__matches = [] - - def getRetry(self): - return self.__retry - - def getMatches(self): - return self.__matches - - def inc(self, matches=None): - self.__retry += 1 - self.__matches += matches or [] - - def setLastTime(self, value): - if value > self.__lastTime: - self.__lastTime = value - - def getLastTime(self): - return self.__lastTime - - def getLastReset(self): - return self.__lastReset - - def setLastReset(self, value): - self.__lastReset = value diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index 37a5fe55..4d006098 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -27,7 +27,6 @@ __license__ = "GPL" from threading import Lock import logging -from .faildata import FailData from .ticket import FailTicket from ..helpers import getLogger @@ -86,26 +85,35 @@ class FailManager: finally: self.__lock.release() - def addFailure(self, ticket): + def addFailure(self, ticket, count=1): + attempts = 1 try: self.__lock.acquire() ip = ticket.getIP() - unixTime = ticket.getTime() - matches = ticket.getMatches() if ip in self.__failList: fData = self.__failList[ip] + # if the same object: + if fData is ticket: + matches = None + else: + matches = ticket.getMatches() + unixTime = ticket.getTime() if fData.getLastReset() < unixTime - self.__maxTime: fData.setLastReset(unixTime) fData.setRetry(0) - fData.inc(matches) + fData.inc(matches, 1, count) fData.setLastTime(unixTime) else: - fData = FailData() - fData.inc(matches) - fData.setLastReset(unixTime) - fData.setLastTime(unixTime) + # if already FailTicket - add it direct, otherwise create (using copy all ticket data): + if isinstance(ticket, FailTicket): + fData = ticket; + else: + fData = FailTicket(ticket=ticket) + if count > ticket.getAttempt(): + fData.setRetry(count) self.__failList[ip] = fData + attempts = fData.getRetry() self.__failTotal += 1 if logSys.getEffectiveLevel() <= logging.DEBUG: @@ -118,6 +126,7 @@ class FailManager: % (self.__failTotal, len(self.__failList), failures_summary)) finally: self.__lock.release() + return attempts def size(self): try: @@ -140,17 +149,14 @@ class FailManager: if ip in self.__failList: del self.__failList[ip] - def toBan(self): + def toBan(self, ip=None): try: self.__lock.acquire() - for ip in self.__failList: + for ip in ([ip] if ip != None and ip in self.__failList else self.__failList): data = self.__failList[ip] if data.getRetry() >= self.__maxRetry: - self.__delFailure(ip) - # Create a FailTicket from BanData - failTicket = FailTicket(ip, data.getLastTime(), data.getMatches()) - failTicket.setAttempt(data.getRetry()) - return failTicket + del self.__failList[ip] + return data raise FailManagerEmpty finally: self.__lock.release() diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 621f826b..0a2a9949 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -22,6 +22,7 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __license__ = "GPL" import codecs +import datetime import fcntl import locale import logging @@ -316,13 +317,12 @@ class Filter(JailThread): logSys.warning('Requested to manually ban an ignored IP %s. User knows best. Proceeding to ban it.' % ip) unixTime = MyTime.time() - for i in xrange(self.failManager.getMaxRetry()): - self.failManager.addFailure(FailTicket(ip, unixTime)) + self.failManager.addFailure(FailTicket(ip, unixTime), self.failManager.getMaxRetry()) # Perform the banning of the IP now. try: # pragma: no branch - exception is the only way out while True: - ticket = self.failManager.toBan() + ticket = self.failManager.toBan(ip) self.jail.putFailTicket(ticket) except FailManagerEmpty: self.failManager.cleanup(MyTime.time()) @@ -427,17 +427,19 @@ class Filter(JailThread): ip = element[1] unixTime = element[2] lines = element[3] - logSys.debug("Processing line with time:%s and ip:%s" - % (unixTime, ip)) + logSys.debug("Processing line with time:%s and ip:%s", + unixTime, ip) if unixTime < MyTime.time() - self.getFindTime(): - logSys.debug("Ignore line since time %s < %s - %s" - % (unixTime, MyTime.time(), self.getFindTime())) + logSys.debug("Ignore line since time %s < %s - %s", + unixTime, MyTime.time(), self.getFindTime()) break if self.inIgnoreIPList(ip, log_ignore=True): continue - logSys.info("[%s] Found %s" % (self.jail.name, ip)) - ## print "D: Adding a ticket for %s" % ((ip, unixTime, [line]),) - self.failManager.addFailure(FailTicket(ip, unixTime, lines)) + logSys.info( + "[%s] Found %s - %s", self.jail.name, ip, datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S") + ) + tick = FailTicket(ip, unixTime, lines) + self.failManager.addFailure(tick) ## # Returns true if the line should be ignored. @@ -606,6 +608,14 @@ class FileFilter(Filter): # to be overridden by backends pass + ## + # Get the log file names + # + # @return log paths + + def getLogPaths(self): + return self.__logs.keys() + ## # Get the log containers # @@ -614,6 +624,14 @@ class FileFilter(Filter): def getLogs(self): return self.__logs.values() + ## + # Get the count of log containers + # + # @return count of log containers + + def getLogCount(self): + return len(self.__logs) + ## # Check whether path is already monitored. # @@ -941,32 +959,50 @@ class JournalFilter(Filter): # pragma: systemd no cover import socket import struct +from .utils import Utils class DNSUtils: IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") + # todo: make configurable the expired time and max count of cache entries: + CACHE_dnsToIp = Utils.Cache(maxCount=1000, maxTime=60*60) + CACHE_ipToName = Utils.Cache(maxCount=1000, maxTime=60*60) + @staticmethod def dnsToIp(dns): """ Convert a DNS into an IP address using the Python socket module. Thanks to Kevin Drapel. """ + # cache, also prevent long wait during retrieving of ip for wrong dns or lazy dns-system: + v = DNSUtils.CACHE_dnsToIp.get(dns) + if v is not None: + return v # retrieve ip (todo: use AF_INET6 for IPv6) try: - return set([i[4][0] for i in socket.getaddrinfo(dns, None, socket.AF_INET, 0, socket.IPPROTO_TCP)]) + v = set([i[4][0] for i in socket.getaddrinfo(dns, None, socket.AF_INET, 0, socket.IPPROTO_TCP)]) except socket.error, e: - logSys.warning("Unable to find a corresponding IP address for %s: %s" - % (dns, e)) - return list() + # todo: make configurable the expired time of cache entry: + logSys.warning("Unable to find a corresponding IP address for %s: %s", dns, e) + v = list() + DNSUtils.CACHE_dnsToIp.set(dns, v) + return v @staticmethod def ipToName(ip): + # cache, also prevent long wait during retrieving of name for wrong addresses, lazy dns: + v = DNSUtils.CACHE_ipToName.get(ip) + if v is not None: + return v + # retrieve name try: - return socket.gethostbyaddr(ip)[0] + v = socket.gethostbyaddr(ip)[0] except socket.error, e: - logSys.debug("Unable to find a name for the IP %s: %s" % (ip, e)) - return None + logSys.debug("Unable to find a name for the IP %s: %s", ip, e) + v = None + DNSUtils.CACHE_ipToName.set(ip, v) + return v @staticmethod def searchIP(text): diff --git a/fail2ban/server/filtergamin.py b/fail2ban/server/filtergamin.py index e731a8e9..b094cfeb 100644 --- a/fail2ban/server/filtergamin.py +++ b/fail2ban/server/filtergamin.py @@ -31,6 +31,7 @@ import gamin from .failmanager import FailManagerEmpty from .filter import FileFilter from .mytime import MyTime +from .utils import Utils from ..helpers import getLogger # Gets the instance of the logger. @@ -102,6 +103,15 @@ class FilterGamin(FileFilter): def _delLogPath(self, path): self.monitor.stop_watch(path) + def _handleEvents(self): + ret = False + mon = self.monitor + while mon and mon.event_pending(): + mon.handle_events() + mon = self.monitor + ret = True + return ret + ## # Main loop. # @@ -112,12 +122,10 @@ class FilterGamin(FileFilter): def run(self): # Gamin needs a loop to collect and dispatch events while self.active: - if not self.idle: - # We cannot block here because we want to be able to - # exit. - if self.monitor.event_pending(): - self.monitor.handle_events() - time.sleep(self.sleeptime) + if self.idle: + time.sleep(self.sleeptime) + continue + Utils.wait_for(self._handleEvents, self.sleeptime) logSys.debug(self.jail.name + ": filter terminated") return True @@ -129,6 +137,6 @@ class FilterGamin(FileFilter): # Desallocates the resources used by Gamin. def __cleanup(self): - for log in self.getLogs(): - self.monitor.stop_watch(log.getFileName()) - del self.monitor + for filename in self.getLogPaths(): + self.monitor.stop_watch(filename) + self.monitor = None diff --git a/fail2ban/server/filterpoll.py b/fail2ban/server/filterpoll.py index 0e99455e..1afda7f2 100644 --- a/fail2ban/server/filterpoll.py +++ b/fail2ban/server/filterpoll.py @@ -31,6 +31,7 @@ from .failmanager import FailManagerEmpty from .filter import FileFilter from .mytime import MyTime from ..helpers import getLogger +from ..server.utils import Utils # Gets the instance of the logger. logSys = getLogger(__name__) @@ -78,6 +79,15 @@ class FilterPoll(FileFilter): del self.__prevStats[path] del self.__file404Cnt[path] + ## + # Get a modified log path at once + # + def getModified(self, modlst): + for filename in self.getLogPaths(): + if self.isModified(filename): + modlst.append(filename) + return modlst + ## # Main loop. # @@ -89,31 +99,32 @@ class FilterPoll(FileFilter): while self.active: if logSys.getEffectiveLevel() <= 6: logSys.log(6, "Woke up idle=%s with %d files monitored", - self.idle, len(self.getLogs())) - if not self.idle: - # Get file modification - for container in self.getLogs(): - filename = container.getFileName() - if self.isModified(filename): - # set start time as now - find time for first usage only (prevent performance bug with polling of big files) - self.getFailures(filename, - (MyTime.time() - self.getFindTime()) if not self.__initial.get(filename) else None - ) - self.__initial[filename] = True - self.__modified = True + self.idle, self.getLogCount()) + if self.idle: + if not Utils.wait_for(lambda: not self.idle, + self.sleeptime * 100, self.sleeptime + ): + continue + # Get file modification + modlst = [] + Utils.wait_for(lambda: self.getModified(modlst), self.sleeptime) + for filename in modlst: + # set start time as now - find time for first usage only (prevent performance bug with polling of big files) + self.getFailures(filename, + (MyTime.time() - self.getFindTime()) if not self.__initial.get(filename) else None + ) + self.__initial[filename] = True + self.__modified = True - if self.__modified: - try: - while True: - ticket = self.failManager.toBan() - self.jail.putFailTicket(ticket) - except FailManagerEmpty: - self.failManager.cleanup(MyTime.time()) - self.dateDetector.sortTemplate() - self.__modified = False - time.sleep(self.sleeptime) - else: - time.sleep(self.sleeptime) + if self.__modified: + try: + while True: + ticket = self.failManager.toBan() + self.jail.putFailTicket(ticket) + except FailManagerEmpty: + self.failManager.cleanup(MyTime.time()) + self.dateDetector.sortTemplate() + self.__modified = False logSys.debug( (self.jail is not None and self.jail.name or "jailless") + " filter terminated") @@ -129,7 +140,7 @@ class FilterPoll(FileFilter): try: logStats = os.stat(filename) stats = logStats.st_mtime, logStats.st_ino, logStats.st_size - pstats = self.__prevStats[filename] + pstats = self.__prevStats.get(filename, ()) self.__file404Cnt[filename] = 0 if logSys.getEffectiveLevel() <= 7: # we do not want to waste time on strftime etc if not necessary @@ -139,10 +150,9 @@ class FilterPoll(FileFilter): # os.system("stat %s | grep Modify" % filename) if pstats == stats: return False - else: - logSys.debug("%s has been modified", filename) - self.__prevStats[filename] = stats - return True + logSys.debug("%s has been modified", filename) + self.__prevStats[filename] = stats + return True except OSError, e: logSys.error("Unable to get stat on %s because of: %s" % (filename, e)) diff --git a/fail2ban/server/jailthread.py b/fail2ban/server/jailthread.py index 3897801a..eb43e453 100644 --- a/fail2ban/server/jailthread.py +++ b/fail2ban/server/jailthread.py @@ -28,6 +28,7 @@ import sys from threading import Thread from abc import abstractmethod +from .utils import Utils from ..helpers import excepthook @@ -48,14 +49,14 @@ class JailThread(Thread): The time the thread sleeps for in the loop. """ - def __init__(self): - super(JailThread, self).__init__() + def __init__(self, name=None): + super(JailThread, self).__init__(name=name) ## Control the state of the thread. self.active = False ## Control the idle state of the thread. self.idle = False ## The time the thread sleeps in the loop. - self.sleeptime = 1 + self.sleeptime = Utils.DEFAULT_SLEEP_TIME # excepthook workaround for threads, derived from: # http://bugs.python.org/issue1230540#msg91244 diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 9836b61d..1b335d02 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -211,8 +211,7 @@ class Server: def getLogPath(self, name): filter_ = self.__jails[name].filter if isinstance(filter_, FileFilter): - return [m.getFileName() - for m in filter_.getLogs()] + return filter_.getLogPaths() else: # pragma: systemd no cover logSys.info("Jail %s is not a FileFilter instance" % name) return [] @@ -324,6 +323,15 @@ class Server: def getBanTime(self, name): return self.__jails[name].actions.getBanTime() + def is_alive(self, jailnum=None): + if jailnum is not None and len(self.__jails) != jailnum: + return 0 + for j in self.__jails: + j = self.__jails[j] + if not j.is_alive(): + return 0 + return 1 + # Status def status(self): try: diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index 70be06fe..49ebf9ea 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -24,7 +24,10 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" +import sys + from ..helpers import getLogger +from .mytime import MyTime # Gets the instance of the logger. logSys = getLogger(__name__) @@ -32,7 +35,7 @@ logSys = getLogger(__name__) class Ticket: - def __init__(self, ip, time, matches=None): + def __init__(self, ip=None, time=None, matches=None, ticket=None): """Ticket constructor @param ip the IP address @@ -41,14 +44,21 @@ class Ticket: """ self.setIP(ip) - self.__time = time - self.__attempt = 0 - self.__file = None - self.__matches = matches or [] + self._flags = 0; + self._banCount = 0; + self._banTime = None; + self._time = time if time is not None else MyTime.time() + self._data = {'matches': [], 'failures': 0} + if ticket: + # ticket available - copy whole information from ticket: + self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__) + else: + self._data['matches'] = matches or [] def __str__(self): return "%s: ip=%s time=%s #attempts=%d matches=%r" % \ - (self.__class__.__name__.split('.')[-1], self.__ip, self.__time, self.__attempt, self.__matches) + (self.__class__.__name__.split('.')[-1], self.__ip, self._time, + self._data['failures'], self._data.get('matches', [])) def __repr__(self): return str(self) @@ -56,9 +66,8 @@ class Ticket: def __eq__(self, other): try: return self.__ip == other.__ip and \ - round(self.__time, 2) == round(other.__time, 2) and \ - self.__attempt == other.__attempt and \ - self.__matches == other.__matches + round(self._time, 2) == round(other._time, 2) and \ + self._data == other._data except AttributeError: return False @@ -72,24 +81,128 @@ class Ticket: return self.__ip def setTime(self, value): - self.__time = value + self._time = value def getTime(self): - return self.__time - + return self._time + + def setBanTime(self, value): + self._banTime = value; + + def getBanTime(self, defaultBT = None): + return (self._banTime if not self._banTime is None else defaultBT); + + def setBanCount(self, value): + self._banCount = value; + + def incrBanCount(self, value = 1): + self._banCount += value; + + def getBanCount(self): + return self._banCount; + + def isTimedOut(self, time, defaultBT = None): + bantime = (self._banTime if not self._banTime is None else defaultBT); + # permanent + if bantime == -1: + return False + # timed out + return (time > self._time + bantime) + def setAttempt(self, value): - self.__attempt = value + self._data['failures'] = value def getAttempt(self): - return self.__attempt + return self._data['failures'] + + def setMatches(self, matches): + self._data['matches'] = matches or [] def getMatches(self): - return self.__matches + return self._data.get('matches', []) + + def setData(self, *args, **argv): + # if overwrite - set data and filter None values: + if len(args) == 1: + # todo: if support >= 2.7 only: + # self._data = {k:v for k,v in args[0].iteritems() if v is not None} + self._data = dict([(k,v) for k,v in args[0].iteritems() if v is not None]) + # add k,v list or dict (merge): + elif len(args) == 2: + self._data.update((args,)) + elif len(args) > 2: + self._data.update((k,v) for k,v in zip(*[iter(args)]*2)) + if len(argv): + self._data.update(argv) + # filter (delete) None values: + # todo: if support >= 2.7 only: + # self._data = {k:v for k,v in self._data.iteritems() if v is not None} + self._data = dict([(k,v) for k,v in self._data.iteritems() if v is not None]) + + def getData(self, key=None, default=None): + # return whole data dict: + if key is None: + return self._data + # return default if not exists: + if not self._data: + return default + # return filtered by lambda/function: + if callable(key): + # todo: if support >= 2.7 only: + # return {k:v for k,v in self._data.iteritems() if key(k)} + return dict([(k,v) for k,v in self._data.iteritems() if key(k)]) + # return filtered by keys: + if hasattr(key, '__iter__'): + # todo: if support >= 2.7 only: + # return {k:v for k,v in self._data.iteritems() if k in key} + return dict([(k,v) for k,v in self._data.iteritems() if k in key]) + # return single value of data: + return self._data.get(key, default) class FailTicket(Ticket): - pass + def __init__(self, ip=None, time=None, matches=None, ticket=None): + # this class variables: + self.__retry = 0 + self.__lastReset = None + # create/copy using default ticket constructor: + Ticket.__init__(self, ip, time, matches, ticket) + # init: + if ticket is None: + self.__lastReset = time if time is not None else self.getTime() + if not self.__retry: + self.__retry = self._data['failures']; + + def setRetry(self, value): + self.__retry = value + if not self._data['failures']: + self._data['failures'] = 1 + if not value: + self._data['failures'] = 0 + self._data['matches'] = [] + + def getRetry(self): + return max(self.__retry, self._data['failures']) + + def inc(self, matches=None, attempt=1, count=1): + self.__retry += count + self._data['failures'] += attempt + if matches: + self._data['matches'] += matches + + def setLastTime(self, value): + if value > self._time: + self._time = value + + def getLastTime(self): + return self._time + + def getLastReset(self): + return self.__lastReset + + def setLastReset(self, value): + self.__lastReset = value ## # Ban Ticket. diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index eb5b7713..4c4c32f7 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -95,7 +95,7 @@ class Transmitter: return None elif command[0] == "sleep": value = command[1] - time.sleep(int(value)) + time.sleep(float(value)) return None elif command[0] == "flushlogs": return self.__server.flushLogs() diff --git a/fail2ban/server/utils.py b/fail2ban/server/utils.py new file mode 100644 index 00000000..7e69ddca --- /dev/null +++ b/fail2ban/server/utils.py @@ -0,0 +1,242 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : + +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +__author__ = "Serg G. Brester (sebres) and Fail2Ban Contributors" +__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Yaroslav Halchenko, 2012-2015 Serg G. Brester" +__license__ = "GPL" + +import logging, os, fcntl, subprocess, time, signal +from ..helpers import getLogger + +# Gets the instance of the logger. +logSys = getLogger(__name__) + +# Some hints on common abnormal exit codes +_RETCODE_HINTS = { + 127: '"Command not found". Make sure that all commands in %(realCmd)r ' + 'are in the PATH of fail2ban-server process ' + '(grep -a PATH= /proc/`pidof -x fail2ban-server`/environ). ' + 'You may want to start ' + '"fail2ban-server -f" separately, initiate it with ' + '"fail2ban-client reload" in another shell session and observe if ' + 'additional informative error messages appear in the terminals.' + } + +# Dictionary to lookup signal name from number +signame = dict((num, name) + for name, num in signal.__dict__.iteritems() if name.startswith("SIG")) + +class Utils(): + """Utilities provide diverse static methods like executes OS shell commands, etc. + """ + + DEFAULT_SLEEP_TIME = 0.1 + DEFAULT_SLEEP_INTERVAL = 0.01 + + + class Cache(dict): + + def __init__(self, maxCount=1000, maxTime=60*60): + self.maxCount = maxCount + self.maxTime = maxTime + + def get(self, k, defv=None): + v = dict.get(self, k) + if v: + if v[1] > time.time(): + return v[0] + del self[k] + return defv + + def set(self, k, v): + t = time.time() + # clean cache if max count reached: + if len(self) >= self.maxCount: + for (ck,cv) in self.items(): + if cv[1] < t: + del self[ck] + # if still max count - remove any one: + if len(self) >= self.maxCount: + self.popitem() + self[k] = (v, t + self.maxTime) + + + @staticmethod + def setFBlockMode(fhandle, value): + flags = fcntl.fcntl(fhandle, fcntl.F_GETFL) + if not value: + flags |= os.O_NONBLOCK + else: + flags &= ~os.O_NONBLOCK + fcntl.fcntl(fhandle, fcntl.F_SETFL, flags) + return flags + + @staticmethod + def executeCmd(realCmd, timeout=60, shell=True, output=False, tout_kill_tree=True): + """Executes a command. + + Parameters + ---------- + realCmd : str + The command to execute. + timeout : int + The time out in seconds for the command. + shell : bool + If shell is True (default), the specified command (may be a string) will be + executed through the shell. + output : bool + If output is True, the function returns tuple (success, stdoutdata, stderrdata, returncode) + + Returns + ------- + bool + True if the command succeeded. + + Raises + ------ + OSError + If command fails to be executed. + RuntimeError + If command execution times out. + """ + stdout = stderr = None + retcode = None + if not callable(timeout): + stime = time.time() + timeout_expr = lambda: time.time() - stime <= timeout + else: + timeout_expr = timeout + try: + popen = subprocess.Popen( + realCmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, + preexec_fn=os.setsid # so that killpg does not kill our process + ) + retcode = popen.poll() + while retcode is None and timeout_expr(): + time.sleep(Utils.DEFAULT_SLEEP_INTERVAL) + retcode = popen.poll() + if retcode is None: + logSys.error("%s -- timed out after %s seconds." % + (realCmd, timeout)) + pgid = os.getpgid(popen.pid) + # if not tree - first try to terminate and then kill, otherwise - kill (-9) only: + os.killpg(pgid, signal.SIGTERM) # Terminate the process + time.sleep(Utils.DEFAULT_SLEEP_INTERVAL) + retcode = popen.poll() + #logSys.debug("%s -- terminated %s ", realCmd, retcode) + if retcode is None or tout_kill_tree: # Still going... + os.killpg(pgid, signal.SIGKILL) # Kill the process + time.sleep(Utils.DEFAULT_SLEEP_INTERVAL) + retcode = popen.poll() + #logSys.debug("%s -- killed %s ", realCmd, retcode) + if retcode is None and not Utils.pid_exists(pgid): + retcode = signal.SIGKILL + except OSError as e: + logSys.error("%s -- failed with %s" % (realCmd, e)) + + std_level = retcode == 0 and logging.DEBUG or logging.ERROR + # if we need output (to return or to log it): + if output or std_level >= logSys.getEffectiveLevel(): + # if was timeouted (killed/terminated) - to prevent waiting, set std handles to non-blocking mode. + if popen.stdout: + try: + if retcode < 0: + Utils.setFBlockMode(popen.stdout, False) + stdout = popen.stdout.read() + except IOError as e: + logSys.error(" ... -- failed to read stdout %s", e) + if stdout is not None and stdout != '': + logSys.log(std_level, "%s -- stdout: %r", realCmd, stdout) + popen.stdout.close() + if popen.stderr: + try: + if retcode < 0: + Utils.setFBlockMode(popen.stderr, False) + stderr = popen.stderr.read() + except IOError as e: + logSys.error(" ... -- failed to read stderr %s", e) + if stderr is not None and stderr != '': + logSys.log(std_level, "%s -- stderr: %r", realCmd, stderr) + popen.stderr.close() + + if retcode == 0: + logSys.debug("%s -- returned successfully", realCmd) + return True if not output else (True, stdout, stderr, retcode) + elif retcode is None: + logSys.error("%s -- unable to kill PID %i" % (realCmd, popen.pid)) + elif retcode < 0 or retcode > 128: + # dash would return negative while bash 128 + n + sigcode = -retcode if retcode < 0 else retcode - 128 + logSys.error("%s -- killed with %s (return code: %s)" % + (realCmd, signame.get(sigcode, "signal %i" % sigcode), retcode)) + else: + msg = _RETCODE_HINTS.get(retcode, None) + logSys.error("%s -- returned %i" % (realCmd, retcode)) + if msg: + logSys.info("HINT on %i: %s", retcode, msg % locals()) + return False if not output else (False, stdout, stderr, retcode) + + @staticmethod + def wait_for(cond, timeout, interval=None): + """Wait until condition expression `cond` is True, up to `timeout` sec + """ + ini = 1 + while True: + ret = cond() + if ret: + return ret + if ini: + ini = stm = 0 + time0 = time.time() + timeout + if not interval: + interval = Utils.DEFAULT_SLEEP_INTERVAL + if time.time() > time0: + break + stm = min(stm + interval, Utils.DEFAULT_SLEEP_TIME) + time.sleep(stm) + return ret + + # Solution from http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid + # under cc by-sa 3.0 + if os.name == 'posix': + @staticmethod + def pid_exists(pid): + """Check whether pid exists in the current process table.""" + import errno + if pid < 0: + return False + try: + os.kill(pid, 0) + except OSError as e: + return e.errno == errno.EPERM + else: + return True + else: + @staticmethod + def pid_exists(pid): + import ctypes + kernel32 = ctypes.windll.kernel32 + SYNCHRONIZE = 0x100000 + + process = kernel32.OpenProcess(SYNCHRONIZE, 0, pid) + if process != 0: + kernel32.CloseHandle(process) + return True + else: + return False diff --git a/fail2ban/tests/action_d/test_badips.py b/fail2ban/tests/action_d/test_badips.py index a7f148b1..74594420 100644 --- a/fail2ban/tests/action_d/test_badips.py +++ b/fail2ban/tests/action_d/test_badips.py @@ -29,6 +29,8 @@ if sys.version_info >= (2,7): def setUp(self): """Call before every test case.""" + unittest.F2B.SkipIfNoNetwork() + self.jail = DummyJail() self.jail.actions.add("test") diff --git a/fail2ban/tests/action_d/test_smtp.py b/fail2ban/tests/action_d/test_smtp.py index 35ac2393..27442832 100644 --- a/fail2ban/tests/action_d/test_smtp.py +++ b/fail2ban/tests/action_d/test_smtp.py @@ -46,6 +46,8 @@ class SMTPActionTest(unittest.TestCase): def setUp(self): """Call before every test case.""" + unittest.F2B.SkipIfNoNetwork() + self.jail = DummyJail() pythonModule = os.path.join(CONFIG_DIR, "action.d", "smtp.py") pythonModuleName = os.path.basename(pythonModule.rstrip(".py")) diff --git a/fail2ban/tests/actionstestcase.py b/fail2ban/tests/actionstestcase.py index 0ceb35d5..dff0d4c6 100644 --- a/fail2ban/tests/actionstestcase.py +++ b/fail2ban/tests/actionstestcase.py @@ -30,6 +30,7 @@ import tempfile from ..server.actions import Actions from ..server.ticket import FailTicket +from ..server.utils import Utils from .dummyjail import DummyJail from .utils import LogCaptureTestCase @@ -81,8 +82,7 @@ class ExecuteActions(LogCaptureTestCase): self.defaultActions() self.__actions.start() with open(self.__tmpfilename) as f: - time.sleep(3) - self.assertEqual(f.read(),"ip start 64\n") + self.assertTrue( Utils.wait_for(lambda: (f.read() == "ip start 64\n"), 3) ) self.__actions.stop() self.__actions.join() @@ -97,8 +97,7 @@ class ExecuteActions(LogCaptureTestCase): self.assertLogged("TestAction initialised") self.__actions.start() - time.sleep(3) - self.assertLogged("TestAction action start") + self.assertTrue( Utils.wait_for(lambda: self._is_logged("TestAction action start"), 3) ) self.__actions.stop() self.__actions.join() @@ -135,8 +134,7 @@ class ExecuteActions(LogCaptureTestCase): "action.d/action_errors.py"), {}) self.__actions.start() - time.sleep(3) - self.assertLogged("Failed to start") + self.assertTrue( Utils.wait_for(lambda: self._is_logged("Failed to start"), 3) ) self.__actions.stop() self.__actions.join() self.assertLogged("Failed to stop") diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 289d8896..8c9b5ef1 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -25,10 +25,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import os -import time import tempfile +import time +import unittest from ..server.action import CommandAction, CallingMap +from ..server.utils import Utils from .utils import LogCaptureTestCase from .utils import pid_exists @@ -194,16 +196,17 @@ class CommandActionTest(LogCaptureTestCase): self.assertLogged('HINT on 127: "Command not found"') def testExecuteTimeout(self): + unittest.F2B.SkipIfFast() stime = time.time() # Should take a minute - self.assertFalse(CommandAction.executeCmd('sleep 60', timeout=2)) + self.assertFalse(CommandAction.executeCmd('sleep 30', timeout=1)) # give a test still 1 second, because system could be too busy - self.assertTrue(time.time() >= stime + 2 and time.time() <= stime + 3) + self.assertTrue(time.time() >= stime + 1 and time.time() <= stime + 2) self.assertLogged( - 'sleep 60 -- timed out after 2 seconds', - 'sleep 60 -- timed out after 3 seconds' + 'sleep 30 -- timed out after 1 seconds', + 'sleep 30 -- timed out after 2 seconds' ) - self.assertLogged('sleep 60 -- killed with SIGTERM') + self.assertLogged('sleep 30 -- killed with SIGTERM') def testExecuteTimeoutWithNastyChildren(self): # temporary file for a nasty kid shell script @@ -215,29 +218,53 @@ class CommandActionTest(LogCaptureTestCase): echo "$$" > %s.pid echo "my pid $$ . sleeping lo-o-o-ong" - sleep 10000 + sleep 30 """ % tmpFilename) + stime = 0 + + # timeout as long as pid-file was not created, but max 5 seconds + def getnasty_tout(): + return ( + getnastypid() is None + and time.time() - stime <= 5 + ) def getnastypid(): - with open(tmpFilename + '.pid') as f: - return int(f.read()) + cpid = None + if os.path.isfile(tmpFilename + '.pid'): + with open(tmpFilename + '.pid') as f: + try: + cpid = int(f.read()) + except ValueError: + pass + return cpid # First test if can kill the bastard + stime = time.time() self.assertFalse(CommandAction.executeCmd( - 'bash %s' % tmpFilename, timeout=.1)) + 'bash %s' % tmpFilename, timeout=getnasty_tout)) + # Wait up to 3 seconds, the child got killed + cpid = getnastypid() # Verify that the process itself got killed - self.assertFalse(pid_exists(getnastypid())) # process should have been killed + self.assertTrue(Utils.wait_for(lambda: not pid_exists(cpid), 3)) # process should have been killed + self.assertLogged('my pid ') self.assertLogged('timed out') - self.assertLogged('killed with SIGTERM') + self.assertLogged('killed with SIGTERM', + 'killed with SIGKILL') + os.unlink(tmpFilename + '.pid') # A bit evolved case even though, previous test already tests killing children processes + stime = time.time() self.assertFalse(CommandAction.executeCmd( - 'out=`bash %s`; echo ALRIGHT' % tmpFilename, timeout=.2)) + 'out=`bash %s`; echo ALRIGHT' % tmpFilename, timeout=getnasty_tout)) + # Wait up to 3 seconds, the child got killed + cpid = getnastypid() # Verify that the process itself got killed - self.assertFalse(pid_exists(getnastypid())) + self.assertTrue(Utils.wait_for(lambda: not pid_exists(cpid), 3)) + self.assertLogged('my pid ') self.assertLogged('timed out') - self.assertLogged('killed with SIGTERM') - + self.assertLogged('killed with SIGTERM', + 'killed with SIGKILL') os.unlink(tmpFilename) os.unlink(tmpFilename + '.pid') diff --git a/fail2ban/tests/banmanagertestcase.py b/fail2ban/tests/banmanagertestcase.py index 09d2411e..a2d399b3 100644 --- a/fail2ban/tests/banmanagertestcase.py +++ b/fail2ban/tests/banmanagertestcase.py @@ -60,6 +60,7 @@ class AddFailure(unittest.TestCase): class StatusExtendedCymruInfo(unittest.TestCase): def setUp(self): """Call before every test case.""" + unittest.F2B.SkipIfNoNetwork() self.__ban_ip = "93.184.216.34" self.__asn = "15133" self.__country = "EU" diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index 083be2b2..20baa847 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -35,7 +35,12 @@ from ..server.ticket import FailTicket from ..server.actions import Actions from .dummyjail import DummyJail try: - from ..server.database import Fail2BanDb + from ..server.database import Fail2BanDb as Fail2BanDb + # because of tests performance use memory instead of file: + def TestFail2BanDb(filename): + if unittest.F2B.fast: + return Fail2BanDb(':memory:') + return Fail2BanDb(filename) except ImportError: Fail2BanDb = None from .utils import LogCaptureTestCase @@ -55,7 +60,7 @@ class DatabaseTest(LogCaptureTestCase): elif Fail2BanDb is None: return _, self.dbFilename = tempfile.mkstemp(".db", "fail2ban_") - self.db = Fail2BanDb(self.dbFilename) + self.db = TestFail2BanDb(self.dbFilename) def tearDown(self): """Call after every test case.""" @@ -66,7 +71,7 @@ class DatabaseTest(LogCaptureTestCase): os.remove(self.dbFilename) def testGetFilename(self): - if Fail2BanDb is None: # pragma: no cover + if Fail2BanDb is None or self.db.filename == ':memory:': # pragma: no cover return self.assertEqual(self.dbFilename, self.db.filename) @@ -88,7 +93,7 @@ class DatabaseTest(LogCaptureTestCase): "/this/path/should/not/exist") def testCreateAndReconnect(self): - if Fail2BanDb is None: # pragma: no cover + if Fail2BanDb is None or self.db.filename == ':memory:': # pragma: no cover return self.testAddJail() # Reconnect... diff --git a/fail2ban/tests/dummyjail.py b/fail2ban/tests/dummyjail.py index 6be571d3..bade2f12 100644 --- a/fail2ban/tests/dummyjail.py +++ b/fail2ban/tests/dummyjail.py @@ -39,28 +39,27 @@ class DummyJail(Jail, object): self.__actions = Actions(self) def __len__(self): - try: - self.lock.acquire() + with self.lock: return len(self.queue) - finally: - self.lock.release() + + def isEmpty(self): + with self.lock: + return not self.queue + + def isFilled(self): + with self.lock: + return bool(self.queue) def putFailTicket(self, ticket): - try: - self.lock.acquire() + with self.lock: self.queue.append(ticket) - finally: - self.lock.release() def getFailTicket(self): - try: - self.lock.acquire() + with self.lock: try: return self.queue.pop() except IndexError: return False - finally: - self.lock.release() @property def name(self): diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 7a35bdd9..59512530 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -41,6 +41,7 @@ from ..server.filterpoll import FilterPoll from ..server.filter import Filter, FileFilter, FileContainer, DNSUtils from ..server.failmanager import FailManagerEmpty from ..server.mytime import MyTime +from ..server.utils import Utils from .utils import setUpMyTime, tearDownMyTime, mtimesleep, LogCaptureTestCase from .dummyjail import DummyJail @@ -162,7 +163,7 @@ def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line # Opened earlier, therefore must close it fin.close() # to give other threads possibly some time to crunch - time.sleep(0.1) + time.sleep(Utils.DEFAULT_SLEEP_INTERVAL) return fout @@ -299,6 +300,11 @@ class IgnoreIP(LogCaptureTestCase): class IgnoreIPDNS(IgnoreIP): + def setUp(self): + """Call before every test case.""" + unittest.F2B.SkipIfNoNetwork() + IgnoreIP.setUp(self) + def testIgnoreIPDNSOK(self): self.filter.addIgnoreIP("www.epfl.ch") self.assertTrue(self.filter.inIgnoreIPList("128.178.50.12")) @@ -425,16 +431,11 @@ class LogFileMonitor(LogCaptureTestCase): def isModified(self, delay=2.): """Wait up to `delay` sec to assure that it was modified or not """ - time0 = time.time() - while time.time() < time0 + delay: - if self.filter.isModified(self.name): - return True - time.sleep(0.1) - return False + return Utils.wait_for(lambda: self.filter.isModified(self.name), delay) def notModified(self): # shorter wait time for not modified status - return not self.isModified(0.4) + return not self.isModified(4*Utils.DEFAULT_SLEEP_TIME) def testUnaccessibleLogFile(self): os.chmod(self.name, 0) @@ -583,26 +584,21 @@ def get_monitor_failures_testcase(Filter_): #time.sleep(0.2) # Give FS time to ack the removal pass - def isFilled(self, delay=2.): + def isFilled(self, delay=1.): """Wait up to `delay` sec to assure that it was modified or not """ - time0 = time.time() - while time.time() < time0 + delay: - if len(self.jail): - return True - time.sleep(0.1) - return False + return Utils.wait_for(lambda: self.jail.isFilled(), delay) 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): - mtimesleep() + Utils.wait_for(lambda: self.filter.is_alive(), 4*Utils.DEFAULT_SLEEP_TIME) - def isEmpty(self, delay=0.4): + def isEmpty(self, delay=4*Utils.DEFAULT_SLEEP_TIME): # shorter wait time for not modified status - return not self.isFilled(delay) + return Utils.wait_for(lambda: self.jail.isEmpty(), delay) def assert_correct_last_attempt(self, failures, count=None): self.assertTrue(self.isFilled(20)) # give Filter a chance to react @@ -657,10 +653,11 @@ def get_monitor_failures_testcase(Filter_): self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=14, mode='w') # Poll might need more time - self.assertTrue(self.isEmpty(4 + int(isinstance(self.filter, FilterPoll))*2), + self.assertTrue(self.isEmpty(min(4, 100 * Utils.DEFAULT_SLEEP_TIME)), "Queue must be empty but it is not: %s." % (', '.join([str(x) for x in self.jail.queue]))) self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) + Utils.wait_for(lambda: self.filter.failManager.getFailTotal() == 2, 50 * Utils.DEFAULT_SLEEP_TIME) self.assertEqual(self.filter.failManager.getFailTotal(), 2) # move aside, but leaving the handle still open... @@ -685,7 +682,7 @@ def get_monitor_failures_testcase(Filter_): if interim_kill: _killfile(None, self.name) - time.sleep(0.2) # let them know + time.sleep(Utils.DEFAULT_SLEEP_TIME) # let them know # now create a new one to override old one _copy_lines_between_files(GetFailures.FILENAME_01, self.name + '.new', @@ -732,7 +729,7 @@ def get_monitor_failures_testcase(Filter_): _copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=100) # so we should get no more failures detected - self.assertTrue(self.isEmpty(2)) + self.assertTrue(self.isEmpty(200 * Utils.DEFAULT_SLEEP_TIME)) # but then if we add it back again self.filter.addLogPath(self.name) @@ -789,19 +786,14 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover return "MonitorJournalFailures%s(%s)" \ % (Filter_, hasattr(self, 'name') and self.name or 'tempfile') - def isFilled(self, delay=2.): + def isFilled(self, delay=1.): """Wait up to `delay` sec to assure that it was modified or not """ - time0 = time.time() - while time.time() < time0 + delay: - if len(self.jail): - return True - time.sleep(0.1) - return False + return Utils.wait_for(lambda: self.jail.isFilled(), delay) - def isEmpty(self, delay=0.4): + def isEmpty(self, delay=4*Utils.DEFAULT_SLEEP_TIME): # shorter wait time for not modified status - return not self.isFilled(delay) + return Utils.wait_for(lambda: self.jail.isEmpty(), delay) def assert_correct_ban(self, test_ip, test_attempts): self.assertTrue(self.isFilled(10)) # give Filter a chance to react @@ -860,7 +852,7 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover _copy_lines_to_journal( self.test_file, self.journal_fields, n=5, skip=5) # so we should get no more failures detected - self.assertTrue(self.isEmpty(2)) + self.assertTrue(self.isEmpty(200 * Utils.DEFAULT_SLEEP_TIME)) # but then if we add it back again self.filter.addJournalMatch([ @@ -905,6 +897,16 @@ class GetFailures(LogCaptureTestCase): tearDownMyTime() LogCaptureTestCase.tearDown(self) + def testFilterAPI(self): + self.assertEqual(self.filter.getLogs(), []) + self.assertEqual(self.filter.getLogCount(), 0) + self.filter.addLogPath(GetFailures.FILENAME_01, tail=True) + self.assertEqual(self.filter.getLogCount(), 1) + self.assertEqual(self.filter.getLogPaths(), [GetFailures.FILENAME_01]) + self.filter.addLogPath(GetFailures.FILENAME_02, tail=True) + self.assertEqual(self.filter.getLogCount(), 2) + self.assertEqual(sorted(self.filter.getLogPaths()), sorted([GetFailures.FILENAME_01, GetFailures.FILENAME_02])) + def testTail(self): # There must be no containters registered, otherwise [-1] indexing would be wrong self.assertEqual(self.filter.getLogs(), []) @@ -1025,6 +1027,7 @@ class GetFailures(LogCaptureTestCase): _killfile(fout, fname) def testGetFailuresUseDNS(self): + unittest.F2B.SkipIfNoNetwork() # We should still catch failures with usedns = no ;-) output_yes = ('93.184.216.34', 2, 1124013539.0, [u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2', @@ -1126,6 +1129,10 @@ class GetFailures(LogCaptureTestCase): class DNSUtilsTests(unittest.TestCase): + def setUp(self): + """Call before every test case.""" + unittest.F2B.SkipIfNoNetwork() + def testUseDns(self): res = DNSUtils.textToIp('www.example.com', 'no') self.assertEqual(res, []) @@ -1151,6 +1158,7 @@ class DNSUtilsTests(unittest.TestCase): def testIpToName(self): res = DNSUtils.ipToName('8.8.4.4') self.assertEqual(res, 'google-public-dns-b.google.com') + unittest.F2B.SkipIfNoNetwork() # invalid ip (TEST-NET-1 according to RFC 5737) res = DNSUtils.ipToName('192.0.2.0') self.assertEqual(res, None) diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index a7d7adef..d547b79a 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -36,6 +36,7 @@ from ..server.failregex import Regex, FailRegex, RegexException from ..server.server import Server from ..server.jail import Jail from ..server.jailthread import JailThread +from ..server.utils import Utils from .utils import LogCaptureTestCase from ..helpers import getLogger from .. import version @@ -74,14 +75,14 @@ class TransmitterBase(unittest.TestCase): """Call after every test case.""" self.server.quit() - def setGetTest(self, cmd, inValue, outValue=None, outCode=0, jail=None, repr_=False): + def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False): setCmd = ["set", cmd, inValue] getCmd = ["get", cmd] if jail is not None: setCmd.insert(1, jail) getCmd.insert(1, jail) - if outValue is None: + if outValue == (None,): outValue = inValue def v(x): @@ -161,15 +162,21 @@ class Transmitter(TransmitterBase): self.assertEqual(self.transm.proceed(["version"]), (0, version.version)) def testSleep(self): - t0 = time.time() - self.assertEqual(self.transm.proceed(["sleep", "1"]), (0, None)) - t1 = time.time() - # Approx 1 second delay but not faster - dt = t1 - t0 - self.assertTrue(0.99 < dt < 1.1, msg="Sleep was %g sec" % dt) + if not unittest.F2B.fast: + t0 = time.time() + self.assertEqual(self.transm.proceed(["sleep", "0.1"]), (0, None)) + t1 = time.time() + # Approx 0.1 second delay but not faster + dt = t1 - t0 + self.assertTrue(0.09 < dt < 0.2, msg="Sleep was %g sec" % dt) + else: # pragma: no cover + self.assertEqual(self.transm.proceed(["sleep", "0.0001"]), (0, None)) def testDatabase(self): - tmp, tmpFilename = tempfile.mkstemp(".db", "fail2ban_") + if not unittest.F2B.fast: + tmp, tmpFilename = tempfile.mkstemp(".db", "fail2ban_") + else: # pragma: no cover + tmpFilename = ':memory:' # Jails present, can't change database self.setGetTestNOK("dbfile", tmpFilename) self.server.delJail(self.jailName) @@ -201,8 +208,9 @@ class Transmitter(TransmitterBase): self.assertEqual(self.transm.proceed( ["set", "dbfile", "None"]), (0, None)) - os.close(tmp) - os.unlink(tmpFilename) + if not unittest.F2B.fast: + os.close(tmp) + os.unlink(tmpFilename) def testAddJail(self): jail2 = "TestJail2" @@ -225,7 +233,11 @@ class Transmitter(TransmitterBase): def testStartStopJail(self): self.assertEqual( self.transm.proceed(["start", self.jailName]), (0, None)) - time.sleep(1) + time.sleep(Utils.DEFAULT_SLEEP_TIME) + # wait until not started (3 seconds as long as any RuntimeError, ex.: RuntimeError('cannot join thread before it is started',)): + self.assertTrue( Utils.wait_for( + lambda: self.server.is_alive(1) and not isinstance(self.transm.proceed(["status", self.jailName]), RuntimeError), + 3) ) self.assertEqual( self.transm.proceed(["stop", self.jailName]), (0, None)) self.assertTrue(self.jailName not in self.server._Server__jails) @@ -239,9 +251,12 @@ class Transmitter(TransmitterBase): # yoh: workaround for gh-146. I still think that there is some # race condition and missing locking somewhere, but for now # giving it a small delay reliably helps to proceed with tests - time.sleep(0.1) + time.sleep(Utils.DEFAULT_SLEEP_TIME) + self.assertTrue( Utils.wait_for( + lambda: self.server.is_alive(2) and not isinstance(self.transm.proceed(["status", self.jailName]), RuntimeError), + 3) ) self.assertEqual(self.transm.proceed(["stop", "all"]), (0, None)) - time.sleep(1) + self.assertTrue( Utils.wait_for( lambda: not len(self.server._Server__jails), 3) ) self.assertTrue(self.jailName not in self.server._Server__jails) self.assertTrue("TestJail2" not in self.server._Server__jails) @@ -297,11 +312,11 @@ class Transmitter(TransmitterBase): self.assertEqual( self.transm.proceed(["set", self.jailName, "banip", "127.0.0.1"]), (0, "127.0.0.1")) - time.sleep(1) # Give chance to ban + time.sleep(Utils.DEFAULT_SLEEP_TIME) # Give chance to ban self.assertEqual( self.transm.proceed(["set", self.jailName, "banip", "Badger"]), (0, "Badger")) #NOTE: Is IP address validated? Is DNS Lookup done? - time.sleep(1) # Give chance to ban + time.sleep(Utils.DEFAULT_SLEEP_TIME) # Give chance to ban # Unban IP self.assertEqual( self.transm.proceed( diff --git a/fail2ban/tests/sockettestcase.py b/fail2ban/tests/sockettestcase.py index 8eeb7b51..a9408fde 100644 --- a/fail2ban/tests/sockettestcase.py +++ b/fail2ban/tests/sockettestcase.py @@ -33,6 +33,7 @@ import unittest from .. import protocol from ..server.asyncserver import AsyncServer, AsyncServerException +from ..server.utils import Utils from ..client.csocket import CSocket @@ -54,14 +55,20 @@ class Socket(unittest.TestCase): """Test transmitter proceed method which just returns first arg""" return message + def _serverSocket(self): + try: + return CSocket(self.sock_name) + except Exception as e: + return None + def testSocket(self): serverThread = threading.Thread( target=self.server.start, args=(self.sock_name, False)) serverThread.daemon = True serverThread.start() - time.sleep(1) + time.sleep(Utils.DEFAULT_SLEEP_TIME) - client = CSocket(self.sock_name) + client = Utils.wait_for(self._serverSocket, 2) testMessage = ["A", "test", "message"] self.assertEqual(client.send(testMessage), testMessage) @@ -71,7 +78,7 @@ class Socket(unittest.TestCase): client.close() self.server.stop() - serverThread.join(1) + serverThread.join(Utils.DEFAULT_SLEEP_TIME) self.assertFalse(os.path.exists(self.sock_name)) def testSocketForce(self): @@ -85,10 +92,10 @@ class Socket(unittest.TestCase): target=self.server.start, args=(self.sock_name, True)) serverThread.daemon = True serverThread.start() - time.sleep(1) + time.sleep(Utils.DEFAULT_SLEEP_TIME) self.server.stop() - serverThread.join(1) + serverThread.join(Utils.DEFAULT_SLEEP_TIME) self.assertFalse(os.path.exists(self.sock_name)) diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index dff3faae..a66325d8 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -30,8 +30,10 @@ import time import unittest from StringIO import StringIO -from ..server.mytime import MyTime from ..helpers import getLogger +from ..server.filter import DNSUtils +from ..server.mytime import MyTime +from ..server.utils import Utils logSys = getLogger(__name__) @@ -45,6 +47,43 @@ if not CONFIG_DIR: CONFIG_DIR = '/etc/fail2ban' +class F2B(): + def __init__(self, fast=False, no_network=False): + self.fast=fast + self.no_network=no_network + def SkipIfFast(self): + pass + def SkipIfNoNetwork(self): + pass + + +def initTests(opts): + if opts: # pragma: no cover + unittest.F2B = F2B(opts.fast, opts.no_network) + else: + unittest.F2B = F2B() + # --fast : + if unittest.F2B.fast: # pragma: no cover + # prevent long sleeping during test cases... + Utils.DEFAULT_SLEEP_TIME = 0.0025 + Utils.DEFAULT_SLEEP_INTERVAL = 0.0005 + def F2B_SkipIfFast(): + raise unittest.SkipTest('Skip test because of "--fast"') + unittest.F2B.SkipIfFast = F2B_SkipIfFast + else: + # sleep intervals are large - use replacement for sleep to check time to sleep: + _org_sleep = time.sleep + def _new_sleep(v): + if (v > Utils.DEFAULT_SLEEP_TIME): + raise ValueError('[BAD-CODE] To long sleep interval: %s, try to use conditional Utils.wait_for instead' % v) + _org_sleep(min(v, Utils.DEFAULT_SLEEP_TIME)) + time.sleep = _new_sleep + # --no-network : + if unittest.F2B.no_network: # pragma: no cover + def F2B_SkipIfNoNetwork(): + raise unittest.SkipTest('Skip test because of "--no-network"') + unittest.F2B.SkipIfNoNetwork = F2B_SkipIfNoNetwork + def mtimesleep(): # no sleep now should be necessary since polling tracks now not only # mtime but also ino and size @@ -70,7 +109,8 @@ def tearDownMyTime(): MyTime.myTime = None -def gatherTests(regexps=None, no_network=False): +def gatherTests(regexps=None, opts=None): + initTests(opts) # Import all the test cases here instead of a module level to # avoid circular imports from . import banmanagertestcase @@ -143,10 +183,10 @@ def gatherTests(regexps=None, no_network=False): tests.addTest(unittest.makeSuite(filtertestcase.LogFile)) tests.addTest(unittest.makeSuite(filtertestcase.LogFileMonitor)) tests.addTest(unittest.makeSuite(filtertestcase.LogFileFilterPoll)) - if not no_network: - tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIPDNS)) - tests.addTest(unittest.makeSuite(filtertestcase.GetFailures)) - tests.addTest(unittest.makeSuite(filtertestcase.DNSUtilsTests)) + # each test case class self will check no network, and skip it (we see it in log) + tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIPDNS)) + tests.addTest(unittest.makeSuite(filtertestcase.GetFailures)) + tests.addTest(unittest.makeSuite(filtertestcase.DNSUtilsTests)) tests.addTest(unittest.makeSuite(filtertestcase.JailTests)) # DateDetector @@ -165,9 +205,6 @@ def gatherTests(regexps=None, no_network=False): for file_ in os.listdir( os.path.abspath(os.path.dirname(action_d.__file__))): if file_.startswith("test_") and file_.endswith(".py"): - if no_network and file_ in ['test_badips.py','test_smtp.py']: #pragma: no cover - # Test required network - continue tests.addTest(testloader.loadTestsFromName( "%s.%s" % (action_d.__name__, os.path.splitext(file_)[0]))) @@ -182,6 +219,9 @@ def gatherTests(regexps=None, no_network=False): # yoh: Since I do not know better way for parametric tests # with good old unittest try: + # because gamin can be very slow on some platforms (and can produce many failures + # with fast sleep interval) - skip it by fast run: + unittest.F2B.SkipIfFast() from ..server.filtergamin import FilterGamin filters.append(FilterGamin) except Exception, e: # pragma: no cover @@ -276,29 +316,4 @@ class LogCaptureTestCase(unittest.TestCase): def printLog(self): print(self._log.getvalue()) -# Solution from http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid -# under cc by-sa 3.0 -if os.name == 'posix': - def pid_exists(pid): - """Check whether pid exists in the current process table.""" - import errno - if pid < 0: - return False - try: - os.kill(pid, 0) - except OSError as e: - return e.errno == errno.EPERM - else: - return True -else: - def pid_exists(pid): - import ctypes - kernel32 = ctypes.windll.kernel32 - SYNCHRONIZE = 0x100000 - - process = kernel32.OpenProcess(SYNCHRONIZE, 0, pid) - if process != 0: - kernel32.CloseHandle(process) - return True - else: - return False +pid_exists = Utils.pid_exists From 78eb4d95eb04c6aeb8b7afa1382a6a48f9ad584a Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 16 Jul 2015 17:14:10 -0400 Subject: [PATCH 011/623] RF: is_alive -> isAlive to stay consistent in camelCasing --- fail2ban/server/jail.py | 6 +++--- fail2ban/server/server.py | 8 ++++---- fail2ban/tests/dummyjail.py | 2 +- fail2ban/tests/filtertestcase.py | 2 +- fail2ban/tests/servertestcase.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index c5f2e9cd..cf16737a 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -229,7 +229,7 @@ class Jail: self.actions.join() logSys.info("Jail '%s' stopped" % self.name) - def is_alive(self): - """Check jail "is_alive" by checking filter and actions threads. + def isAlive(self): + """Check jail "isAlive" by checking filter and actions threads. """ - return self.filter.is_alive() or self.actions.is_alive() + return self.filter.isAlive() or self.actions.isAlive() diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 1b335d02..cdfb4465 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -157,7 +157,7 @@ class Server: def startJail(self, name): try: self.__lock.acquire() - if not self.__jails[name].is_alive(): + if not self.__jails[name].isAlive(): self.__jails[name].start() finally: self.__lock.release() @@ -166,7 +166,7 @@ class Server: logSys.debug("Stopping jail %s" % name) try: self.__lock.acquire() - if self.__jails[name].is_alive(): + if self.__jails[name].isAlive(): self.__jails[name].stop() self.delJail(name) finally: @@ -323,12 +323,12 @@ class Server: def getBanTime(self, name): return self.__jails[name].actions.getBanTime() - def is_alive(self, jailnum=None): + def isAlive(self, jailnum=None): if jailnum is not None and len(self.__jails) != jailnum: return 0 for j in self.__jails: j = self.__jails[j] - if not j.is_alive(): + if not j.isAlive(): return 0 return 1 diff --git a/fail2ban/tests/dummyjail.py b/fail2ban/tests/dummyjail.py index bade2f12..33a102f2 100644 --- a/fail2ban/tests/dummyjail.py +++ b/fail2ban/tests/dummyjail.py @@ -85,5 +85,5 @@ class DummyJail(Jail, object): def actions(self): return self.__actions; - def is_alive(self): + def isAlive(self): return True; diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 59512530..044e0346 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -594,7 +594,7 @@ def get_monitor_failures_testcase(Filter_): # actions might be happening too fast in the tests, # sleep a bit to guarantee reliable time stamps if isinstance(self.filter, FilterPoll): - Utils.wait_for(lambda: self.filter.is_alive(), 4*Utils.DEFAULT_SLEEP_TIME) + Utils.wait_for(lambda: self.filter.isAlive(), 4*Utils.DEFAULT_SLEEP_TIME) def isEmpty(self, delay=4*Utils.DEFAULT_SLEEP_TIME): # shorter wait time for not modified status diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index d547b79a..eedc8c94 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -236,7 +236,7 @@ class Transmitter(TransmitterBase): time.sleep(Utils.DEFAULT_SLEEP_TIME) # wait until not started (3 seconds as long as any RuntimeError, ex.: RuntimeError('cannot join thread before it is started',)): self.assertTrue( Utils.wait_for( - lambda: self.server.is_alive(1) and not isinstance(self.transm.proceed(["status", self.jailName]), RuntimeError), + lambda: self.server.isAlive(1) and not isinstance(self.transm.proceed(["status", self.jailName]), RuntimeError), 3) ) self.assertEqual( self.transm.proceed(["stop", self.jailName]), (0, None)) @@ -253,7 +253,7 @@ class Transmitter(TransmitterBase): # giving it a small delay reliably helps to proceed with tests time.sleep(Utils.DEFAULT_SLEEP_TIME) self.assertTrue( Utils.wait_for( - lambda: self.server.is_alive(2) and not isinstance(self.transm.proceed(["status", self.jailName]), RuntimeError), + lambda: self.server.isAlive(2) and not isinstance(self.transm.proceed(["status", self.jailName]), RuntimeError), 3) ) self.assertEqual(self.transm.proceed(["stop", "all"]), (0, None)) self.assertTrue( Utils.wait_for( lambda: not len(self.server._Server__jails), 3) ) From e2a68bb02de740fb3a4d095d0af323f289e112fd Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 16 Jul 2015 17:15:11 -0400 Subject: [PATCH 012/623] RF: make Jail new style class, avoiding multiple inheritance with object --- fail2ban/server/jail.py | 2 +- fail2ban/tests/dummyjail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index cf16737a..6345d300 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -33,7 +33,7 @@ from ..helpers import getLogger logSys = getLogger(__name__) -class Jail: +class Jail(object): """Fail2Ban jail, which manages a filter and associated actions. The class handles the initialisation of a filter, and actions. It's diff --git a/fail2ban/tests/dummyjail.py b/fail2ban/tests/dummyjail.py index 33a102f2..19f97f4e 100644 --- a/fail2ban/tests/dummyjail.py +++ b/fail2ban/tests/dummyjail.py @@ -28,7 +28,7 @@ from ..server.jail import Jail from ..server.actions import Actions -class DummyJail(Jail, object): +class DummyJail(Jail): """A simple 'jail' to suck in all the tickets generated by Filter's """ def __init__(self, backend=None): From 0e61b7586a5e8ca4bb769e14066af403d55d5bd0 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 16 Jul 2015 17:22:36 -0400 Subject: [PATCH 013/623] RF: use descriptive loop variable (oldticket) and matching associated variable suffixes (old) --- fail2ban/server/banmanager.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index a958a5c8..0ee028ef 100644 --- a/fail2ban/server/banmanager.py +++ b/fail2ban/server/banmanager.py @@ -263,19 +263,19 @@ class BanManager: try: self.__lock.acquire() # check already banned - for i in self.__banList: - if ticket.getIP() == i.getIP(): + for oldticket in self.__banList: + if ticket.getIP() == oldticket.getIP(): # if already permanent - btorg, torg = i.getBanTime(self.__banTime), i.getTime() - if btorg == -1: + btold, told = oldticket.getBanTime(self.__banTime), oldticket.getTime() + if btold == -1: return False # if given time is less than already banned time btnew, tnew = ticket.getBanTime(self.__banTime), ticket.getTime() - if btnew != -1 and tnew + btnew <= torg + btorg: + if btnew != -1 and tnew + btnew <= told + btold: return False # we have longest ban - set new (increment) ban time - i.setTime(tnew) - i.setBanTime(btnew) + oldticket.setTime(tnew) + oldticket.setBanTime(btnew) return False # not yet banned - add new self.__banList.append(ticket) From 6faffe3201ee34d98c36c985a35f2c3810a40aec Mon Sep 17 00:00:00 2001 From: sebres Date: Fri, 17 Jul 2015 18:34:13 +0200 Subject: [PATCH 014/623] test cases extended, code review (+ python 3.x compatibility); database test cases extended - enable deleted (disabled) jail in addJail; --- MANIFEST | 1 + fail2ban/server/database.py | 8 +- fail2ban/server/filter.py | 4 +- fail2ban/server/server.py | 2 +- fail2ban/server/ticket.py | 21 ++-- fail2ban/server/utils.py | 5 +- fail2ban/tests/banmanagertestcase.py | 51 +++++++- fail2ban/tests/databasetestcase.py | 21 +++- fail2ban/tests/filtertestcase.py | 56 ++++++++- fail2ban/tests/tickettestcase.py | 176 +++++++++++++++++++++++++++ fail2ban/tests/utils.py | 4 + 11 files changed, 325 insertions(+), 24 deletions(-) create mode 100644 fail2ban/tests/tickettestcase.py diff --git a/MANIFEST b/MANIFEST index 7306cc41..fb70bb4b 100644 --- a/MANIFEST +++ b/MANIFEST @@ -331,6 +331,7 @@ fail2ban/tests/misctestcase.py fail2ban/tests/samplestestcase.py fail2ban/tests/servertestcase.py fail2ban/tests/sockettestcase.py +fail2ban/tests/tickettestcase.py fail2ban/tests/utils.py fail2ban/version.py files/bash-completion diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index c6e8c95c..3b419ed3 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -302,7 +302,7 @@ class Fail2BanDb(object): cur.execute("UPDATE jails SET enabled=0") @commitandrollback - def getJailNames(self, cur): + def getJailNames(self, cur, enabled=None): """Get name of jails in database. Currently only used for testing purposes. @@ -312,7 +312,11 @@ class Fail2BanDb(object): set Set of jail names. """ - cur.execute("SELECT name FROM jails") + if enabled is None: + cur.execute("SELECT name FROM jails") + else: + cur.execute("SELECT name FROM jails WHERE enabled=%s" % + (int(enabled),)) return set(row[0] for row in cur.fetchmany()) @commitandrollback diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 0a2a9949..e5cf648c 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -967,8 +967,8 @@ class DNSUtils: IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") # todo: make configurable the expired time and max count of cache entries: - CACHE_dnsToIp = Utils.Cache(maxCount=1000, maxTime=60*60) - CACHE_ipToName = Utils.Cache(maxCount=1000, maxTime=60*60) + CACHE_dnsToIp = Utils.Cache(maxCount=1000, maxTime=5*60) + CACHE_ipToName = Utils.Cache(maxCount=1000, maxTime=5*60) @staticmethod def dnsToIp(dns): diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index cdfb4465..86197bc5 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -44,7 +44,7 @@ logSys = getLogger(__name__) try: from .database import Fail2BanDb -except ImportError: +except ImportError: # pragma: no cover # Dont print error here, as database may not even be used Fail2BanDb = None diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index 49ebf9ea..e856b66d 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -146,16 +146,17 @@ class Ticket: # return default if not exists: if not self._data: return default - # return filtered by lambda/function: - if callable(key): - # todo: if support >= 2.7 only: - # return {k:v for k,v in self._data.iteritems() if key(k)} - return dict([(k,v) for k,v in self._data.iteritems() if key(k)]) - # return filtered by keys: - if hasattr(key, '__iter__'): - # todo: if support >= 2.7 only: - # return {k:v for k,v in self._data.iteritems() if k in key} - return dict([(k,v) for k,v in self._data.iteritems() if k in key]) + if not isinstance(key,(str,unicode,type(None),int,float,bool,complex)): + # return filtered by lambda/function: + if callable(key): + # todo: if support >= 2.7 only: + # return {k:v for k,v in self._data.iteritems() if key(k)} + return dict([(k,v) for k,v in self._data.iteritems() if key(k)]) + # return filtered by keys: + if hasattr(key, '__iter__'): + # todo: if support >= 2.7 only: + # return {k:v for k,v in self._data.iteritems() if k in key} + return dict([(k,v) for k,v in self._data.iteritems() if k in key]) # return single value of data: return self._data.get(key, default) diff --git a/fail2ban/server/utils.py b/fail2ban/server/utils.py index 7e69ddca..262b303d 100644 --- a/fail2ban/server/utils.py +++ b/fail2ban/server/utils.py @@ -52,7 +52,10 @@ class Utils(): class Cache(dict): - def __init__(self, maxCount=1000, maxTime=60*60): + def __init__(self, *args, **kwargs): + self.setOptions(*args, **kwargs) + + def setOptions(self, maxCount=1000, maxTime=60): self.maxCount = maxCount self.maxTime = maxTime diff --git a/fail2ban/tests/banmanagertestcase.py b/fail2ban/tests/banmanagertestcase.py index a2d399b3..9e865a1b 100644 --- a/fail2ban/tests/banmanagertestcase.py +++ b/fail2ban/tests/banmanagertestcase.py @@ -35,27 +35,74 @@ class AddFailure(unittest.TestCase): """Call before every test case.""" self.__ticket = BanTicket('193.168.0.128', 1167605999.0) self.__banManager = BanManager() - self.assertTrue(self.__banManager.addBanTicket(self.__ticket)) def tearDown(self): """Call after every test case.""" pass def testAdd(self): + self.assertTrue(self.__banManager.addBanTicket(self.__ticket)) self.assertEqual(self.__banManager.size(), 1) - + self.assertEqual(self.__banManager.getBanTotal(), 1) + self.__banManager.setBanTotal(0) + self.assertEqual(self.__banManager.getBanTotal(), 0) + def testAddDuplicate(self): + self.assertTrue(self.__banManager.addBanTicket(self.__ticket)) self.assertFalse(self.__banManager.addBanTicket(self.__ticket)) self.assertEqual(self.__banManager.size(), 1) + def testAddDuplicateWithTime(self): + # add again a duplicate : + # 1) with newer start time and the same ban time + # 2) with same start time and longer ban time + # 3) with permanent ban time (-1) + for tnew, btnew in ( + (1167605999.0 + 100, None), + (1167605999.0, 24*60*60), + (1167605999.0, -1), + ): + ticket1 = BanTicket('193.168.0.128', 1167605999.0) + ticket2 = BanTicket('193.168.0.128', tnew) + if btnew is not None: + ticket2.setBanTime(btnew) + self.assertTrue(self.__banManager.addBanTicket(ticket1)) + self.assertFalse(self.__banManager.addBanTicket(ticket2)) + self.assertEqual(self.__banManager.size(), 1) + # pop ticket and check it was prolonged : + banticket = self.__banManager.getTicketByIP(ticket2.getIP()) + self.assertEqual(banticket.getTime(), ticket2.getTime()) + self.assertEqual(banticket.getTime(), ticket2.getTime()) + self.assertEqual(banticket.getBanTime(), ticket2.getBanTime(self.__banManager.getBanTime())) + def testInListOK(self): + self.assertTrue(self.__banManager.addBanTicket(self.__ticket)) ticket = BanTicket('193.168.0.128', 1167605999.0) self.assertTrue(self.__banManager._inBanList(ticket)) def testInListNOK(self): + self.assertTrue(self.__banManager.addBanTicket(self.__ticket)) ticket = BanTicket('111.111.1.111', 1167605999.0) self.assertFalse(self.__banManager._inBanList(ticket)) + def testUnban(self): + btime = self.__banManager.getBanTime() + self.assertTrue(self.__banManager.addBanTicket(self.__ticket)) + self.assertTrue(self.__banManager._inBanList(self.__ticket)) + self.assertEqual(self.__banManager.unBanList(self.__ticket.getTime() + btime + 1), [self.__ticket]) + self.assertEqual(self.__banManager.size(), 0) + + def testUnbanPermanent(self): + btime = self.__banManager.getBanTime() + self.__banManager.setBanTime(-1) + try: + self.assertTrue(self.__banManager.addBanTicket(self.__ticket)) + self.assertTrue(self.__banManager._inBanList(self.__ticket)) + self.assertEqual(self.__banManager.unBanList(self.__ticket.getTime() + btime + 1), []) + self.assertEqual(self.__banManager.size(), 1) + finally: + self.__banManager.setBanTime(btime) + class StatusExtendedCymruInfo(unittest.TestCase): def setUp(self): diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index 20baa847..3f0e4c10 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -124,7 +124,7 @@ class DatabaseTest(LogCaptureTestCase): self.jail = DummyJail() self.db.addJail(self.jail) self.assertTrue( - self.jail.name in self.db.getJailNames(), + self.jail.name in self.db.getJailNames(True), "Jail not added to database") def testAddLog(self): @@ -332,6 +332,25 @@ class DatabaseTest(LogCaptureTestCase): actions._Actions__checkBan() self.assertLogged("ban ainfo %s, %s, %s, %s" % (True, True, True, True)) + def testDelAndAddJail(self): + self.testAddJail() # Add jail + # Delete jail (just disabled it): + self.db.delJail(self.jail) + jails = self.db.getJailNames() + self.assertTrue(len(jails) == 1 and self.jail.name in jails) + jails = self.db.getJailNames(enabled=False) + self.assertTrue(len(jails) == 1 and self.jail.name in jails) + jails = self.db.getJailNames(enabled=True) + self.assertTrue(len(jails) == 0) + # Add it again - should just enable it: + self.db.addJail(self.jail) + jails = self.db.getJailNames() + self.assertTrue(len(jails) == 1 and self.jail.name in jails) + jails = self.db.getJailNames(enabled=True) + self.assertTrue(len(jails) == 1 and self.jail.name in jails) + jails = self.db.getJailNames(enabled=False) + self.assertTrue(len(jails) == 0) + def testPurge(self): if Fail2BanDb is None: # pragma: no cover return diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 044e0346..df98e1ac 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -587,18 +587,18 @@ def get_monitor_failures_testcase(Filter_): def isFilled(self, delay=1.): """Wait up to `delay` sec to assure that it was modified or not """ - return Utils.wait_for(lambda: self.jail.isFilled(), delay) + return Utils.wait_for(self.jail.isFilled, delay) 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): - Utils.wait_for(lambda: self.filter.isAlive(), 4*Utils.DEFAULT_SLEEP_TIME) + Utils.wait_for(self.filter.isAlive, 4*Utils.DEFAULT_SLEEP_TIME) def isEmpty(self, delay=4*Utils.DEFAULT_SLEEP_TIME): # shorter wait time for not modified status - return Utils.wait_for(lambda: self.jail.isEmpty(), delay) + return Utils.wait_for(self.jail.isEmpty, delay) def assert_correct_last_attempt(self, failures, count=None): self.assertTrue(self.isFilled(20)) # give Filter a chance to react @@ -789,11 +789,11 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover def isFilled(self, delay=1.): """Wait up to `delay` sec to assure that it was modified or not """ - return Utils.wait_for(lambda: self.jail.isFilled(), delay) + return Utils.wait_for(self.jail.isFilled, delay) def isEmpty(self, delay=4*Utils.DEFAULT_SLEEP_TIME): # shorter wait time for not modified status - return Utils.wait_for(lambda: self.jail.isEmpty(), delay) + return Utils.wait_for(self.jail.isEmpty, delay) def assert_correct_ban(self, test_ip, test_attempts): self.assertTrue(self.isFilled(10)) # give Filter a chance to react @@ -1129,6 +1129,52 @@ class GetFailures(LogCaptureTestCase): class DNSUtilsTests(unittest.TestCase): + def testCache(self): + c = Utils.Cache(maxCount=5, maxTime=60) + # not available : + self.assertTrue(c.get('a') is None) + self.assertEqual(c.get('a', 'test'), 'test') + # exact 5 elements : + for i in xrange(5): + c.set(i, i) + for i in xrange(5): + self.assertEqual(c.get(i), i) + + def testCacheMaxSize(self): + c = Utils.Cache(maxCount=5, maxTime=60) + # exact 5 elements : + for i in xrange(5): + c.set(i, i) + self.assertEqual([c.get(i) for i in xrange(5)], [i for i in xrange(5)]) + self.assertFalse(-1 in [c.get(i, -1) for i in xrange(5)]) + # add one - too many: + c.set(10, i) + # one element should be removed : + self.assertTrue(-1 in [c.get(i, -1) for i in xrange(5)]) + # test max size (not expired): + for i in xrange(10): + c.set(i, 1) + self.assertEqual(len(c), 5) + + def testCacheMaxTime(self): + # test max time (expired, timeout reached) : + c = Utils.Cache(maxCount=5, maxTime=0.0005) + for i in xrange(10): + c.set(i, 1) + st = time.time() + self.assertTrue(Utils.wait_for(lambda: time.time() >= st + 0.0005, 1)) + # we have still 5 elements (or fewer if too slow test mashine): + self.assertTrue(len(c) <= 5) + # but all that are expiered also: + for i in xrange(10): + self.assertTrue(c.get(i) is None) + # here the whole cache should be empty: + self.assertEqual(len(c), 0) + + + +class DNSUtilsNetworkTests(unittest.TestCase): + def setUp(self): """Call before every test case.""" unittest.F2B.SkipIfNoNetwork() diff --git a/fail2ban/tests/tickettestcase.py b/fail2ban/tests/tickettestcase.py new file mode 100644 index 00000000..68a44bb5 --- /dev/null +++ b/fail2ban/tests/tickettestcase.py @@ -0,0 +1,176 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : + +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +__author__ = "Serg G. Brester (sebres)" +__copyright__ = "Copyright (c) 2015 Serg G. Brester, 2015- Fail2Ban Contributors" +__license__ = "GPL" + +from ..server.mytime import MyTime +import unittest + +from ..server.ticket import Ticket, FailTicket, BanTicket + + +class TicketTests(unittest.TestCase): + + def testTicket(self): + + tm = MyTime.time() + matches = ['first', 'second'] + matches2 = ['first', 'second'] + matches3 = ['first', 'second', 'third'] + + # Ticket + t = Ticket('193.168.0.128', tm, matches) + self.assertEqual(t.getIP(), '193.168.0.128') + self.assertEqual(t.getTime(), tm) + self.assertEqual(t.getMatches(), matches2) + t.setAttempt(2) + self.assertEqual(t.getAttempt(), 2) + t.setBanCount(10) + self.assertEqual(t.getBanCount(), 10) + # default ban time (from manager): + self.assertEqual(t.getBanTime(60*60), 60*60) + self.assertFalse(t.isTimedOut(tm + 60 + 1, 60*60)) + self.assertTrue(t.isTimedOut(tm + 60*60 + 1, 60*60)) + t.setBanTime(60) + self.assertEqual(t.getBanTime(60*60), 60) + self.assertEqual(t.getBanTime(), 60) + self.assertFalse(t.isTimedOut(tm)) + self.assertTrue(t.isTimedOut(tm + 60 + 1)) + # permanent : + t.setBanTime(-1) + self.assertFalse(t.isTimedOut(tm + 60 + 1)) + t.setBanTime(60) + + # BanTicket + tm = MyTime.time() + matches = ['first', 'second'] + ft = FailTicket('193.168.0.128', tm, matches) + ft.setBanTime(60*60) + self.assertEqual(ft.getIP(), '193.168.0.128') + self.assertEqual(ft.getTime(), tm) + self.assertEqual(ft.getMatches(), matches2) + ft.setAttempt(2) + self.assertEqual(ft.getAttempt(), 2) + # retry is max of set retry and failures: + self.assertEqual(ft.getRetry(), 2) + ft.setRetry(1) + self.assertEqual(ft.getRetry(), 2) + ft.setRetry(3) + self.assertEqual(ft.getRetry(), 3) + ft.inc() + self.assertEqual(ft.getAttempt(), 3) + self.assertEqual(ft.getRetry(), 4) + self.assertEqual(ft.getMatches(), matches2) + # with 1 match, 1 failure and factor 10 (retry count) : + ft.inc(['third'], 1, 10) + self.assertEqual(ft.getAttempt(), 4) + self.assertEqual(ft.getRetry(), 14) + self.assertEqual(ft.getMatches(), matches3) + # last time (ignore if smaller as time): + self.assertEqual(ft.getLastTime(), tm) + ft.setLastTime(tm-60) + self.assertEqual(ft.getTime(), tm) + self.assertEqual(ft.getLastTime(), tm) + ft.setLastTime(tm+60) + self.assertEqual(ft.getTime(), tm+60) + self.assertEqual(ft.getLastTime(), tm+60) + ft.setData('country', 'DE') + self.assertEqual(ft.getData(), + {'matches': ['first', 'second', 'third'], 'failures': 4, 'country': 'DE'}) + + # copy all from another ticket: + ft2 = FailTicket(ticket=ft) + self.assertEqual(ft, ft2) + self.assertEqual(ft.getData(), ft2.getData()) + self.assertEqual(ft2.getAttempt(), 4) + self.assertEqual(ft2.getRetry(), 14) + self.assertEqual(ft2.getMatches(), matches3) + self.assertEqual(ft2.getTime(), ft.getTime()) + self.assertEqual(ft2.getLastTime(), ft.getLastTime()) + self.assertEqual(ft2.getBanTime(), ft.getBanTime()) + + def testTicketData(self): + t = BanTicket('193.168.0.128', None, ['first', 'second']) + # expand data (no overwrites, matches are available) : + t.setData('region', 'Hamburg', 'country', 'DE', 'city', 'Hamburg') + self.assertEqual( + t.getData(), + {'matches': ['first', 'second'], 'failures':0, 'region': 'Hamburg', 'country': 'DE', 'city': 'Hamburg'}) + # at once as dict (single argument, overwrites it completelly, no more matches/failures) : + t.setData({'region': None, 'country': 'FR', 'city': 'Paris'},) + self.assertEqual( + t.getData(), + {'city': 'Paris', 'country': 'FR'}) + # at once as dict (overwrites it completelly, no more matches/failures) : + t.setData({'region': 'Hamburg', 'country': 'DE', 'city': None}) + self.assertEqual( + t.getData(), + {'region': 'Hamburg', 'country': 'DE'}) + self.assertEqual( + t.getData('region'), + 'Hamburg') + self.assertEqual( + t.getData('country'), + 'DE') + # again, named arguments: + t.setData(region='Bremen', city='Bremen') + self.assertEqual(t.getData(), + {'region': 'Bremen', 'country': 'DE', 'city': 'Bremen'}) + # again, but as args (key value pair): + t.setData('region', 'Brandenburg', 'city', 'Berlin') + self.assertEqual( + t.getData('region'), + 'Brandenburg') + self.assertEqual( + t.getData('city'), + 'Berlin') + self.assertEqual( + t.getData(), + {'city':'Berlin', 'region': 'Brandenburg', 'country': 'DE'}) + # interator filter : + self.assertEqual( + t.getData(('city', 'country')), + {'city':'Berlin', 'country': 'DE'}) + # callable filter : + self.assertEqual( + t.getData(lambda k: k.upper() == 'COUNTRY'), + {'country': 'DE'}) + # remove one data entry: + t.setData('city', None) + self.assertEqual( + t.getData(), + {'region': 'Brandenburg', 'country': 'DE'}) + # default if not available: + self.assertEqual( + t.getData('city', 'Unknown'), + 'Unknown') + # add continent : + t.setData('continent', 'Europe') + # again, but as argument list (overwrite new only, leave continent unchanged) : + t.setData(*['country', 'RU', 'region', 'Moscow']) + self.assertEqual( + t.getData(), + {'continent': 'Europe', 'country': 'RU', 'region': 'Moscow'}) + # clear: + t.setData({}) + self.assertEqual(t.getData(), {}) + self.assertEqual(t.getData('anything', 'default'), 'default') diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index a66325d8..017ad3f6 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -115,6 +115,7 @@ def gatherTests(regexps=None, opts=None): # avoid circular imports from . import banmanagertestcase from . import clientreadertestcase + from . import tickettestcase from . import failmanagertestcase from . import filtertestcase from . import servertestcase @@ -150,6 +151,8 @@ def gatherTests(regexps=None, opts=None): tests.addTest(unittest.makeSuite(servertestcase.LoggingTests)) tests.addTest(unittest.makeSuite(actiontestcase.CommandActionTest)) tests.addTest(unittest.makeSuite(actionstestcase.ExecuteActions)) + # Ticket, BanTicket, FailTicket + tests.addTest(unittest.makeSuite(tickettestcase.TicketTests)) # FailManager tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure)) # BanManager @@ -187,6 +190,7 @@ def gatherTests(regexps=None, opts=None): tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIPDNS)) tests.addTest(unittest.makeSuite(filtertestcase.GetFailures)) tests.addTest(unittest.makeSuite(filtertestcase.DNSUtilsTests)) + tests.addTest(unittest.makeSuite(filtertestcase.DNSUtilsNetworkTests)) tests.addTest(unittest.makeSuite(filtertestcase.JailTests)) # DateDetector From cf3cf27fa3c57c6a43448e23faa2b37c553395ee Mon Sep 17 00:00:00 2001 From: sebres Date: Fri, 17 Jul 2015 21:05:50 +0200 Subject: [PATCH 015/623] - filtering of test cases extended for method additionally to class (matches method only not whole class for regexp matched method name); - new options for "fail2ban-testcases" introduced: "-g" or "--no-gamin" and "-m" or "--memory-db", both are true also if "-f" or "--fast" specified, for example: `fail2ban-testcases -ngm` will runs faster (because no network, no gamin, memory database) but will use the same default sleep intervals as in production (in comparison to -nf); - seekToTime rewritten, accuracy increased by seekToTime, extended for all FileFilter (PyInotify, Gamin also), test cases extended etc. - common performance optimized and code reviewed; --- bin/fail2ban-testcases | 6 + fail2ban/server/filter.py | 119 +++++++++------ fail2ban/server/filterpoll.py | 7 +- fail2ban/server/ticket.py | 6 + fail2ban/tests/databasetestcase.py | 26 ++-- fail2ban/tests/filtertestcase.py | 228 +++++++++++++++++++++-------- fail2ban/tests/servertestcase.py | 7 +- fail2ban/tests/utils.py | 45 +++--- 8 files changed, 301 insertions(+), 143 deletions(-) diff --git a/bin/fail2ban-testcases b/bin/fail2ban-testcases index 28af8265..22a44a8b 100755 --- a/bin/fail2ban-testcases +++ b/bin/fail2ban-testcases @@ -58,6 +58,12 @@ def get_opt_parser(): Option('-n', "--no-network", action="store_true", dest="no_network", help="Do not run tests that require the network"), + Option('-g', "--no-gamin", action="store_true", + dest="no_gamin", + help="Do not run tests that require the gamin"), + Option('-m', "--memory-db", action="store_true", + dest="memory_db", + help="Run database tests using memory instead of file"), Option('-f', "--fast", action="store_true", dest="fast", help="Try to increase speed of the tests, decreasing of wait intervals, memory database"), diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index e5cf648c..d1ac3680 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -560,6 +560,7 @@ class FileFilter(Filter): Filter.__init__(self, jail, **kwargs) ## The log file path. self.__logs = dict() + self.__autoSeek = dict() self.setLogEncoding("auto") ## @@ -567,7 +568,7 @@ class FileFilter(Filter): # # @param path log file path - def addLogPath(self, path, tail=False): + def addLogPath(self, path, tail=False, autoSeek=True): if path in self.__logs: logSys.error(path + " already exists") else: @@ -579,6 +580,11 @@ class FileFilter(Filter): log.setPos(lastpos) self.__logs[path] = log logSys.info("Added logfile = %s (pos = %s, hash = %s)" , path, log.getPos(), log.getHash()) + if autoSeek: + # if default, seek to "current time" - "find time": + if isinstance(autoSeek, bool): + autoSeek = MyTime.time() - self.getFindTime() + self.__autoSeek[path] = autoSeek self._addLogPath(path) # backend specific def _addLogPath(self, path): @@ -673,7 +679,7 @@ class FileFilter(Filter): # MyTime.time()-self.findTime. When a failure is detected, a FailTicket # is created and is added to the FailManager. - def getFailures(self, filename, startTime=None): + def getFailures(self, filename): log = self.getLog(filename) if log is None: logSys.error("Unable to get failures in " + filename) @@ -695,13 +701,17 @@ class FileFilter(Filter): logSys.exception(e) return False - # prevent completely read of big files first time (after start of service), initial seek to start time using half-interval search algorithm: - if log.getPos() == 0 and startTime is not None: + # seek to find time for first usage only (prevent performance decline with polling of big files) + if self.__autoSeek.get(filename): + startTime = self.__autoSeek[filename] + del self.__autoSeek[filename] + # prevent completely read of big files first time (after start of service), + # initial seek to start time using half-interval search algorithm: try: - # startTime = MyTime.time() - self.getFindTime() self.seekToTime(log, startTime) except Exception, e: # pragma: no cover logSys.error("Error during seek to start time in \"%s\"", filename) + raise logSys.exception(e) return False @@ -726,71 +736,88 @@ class FileFilter(Filter): # Seeks to line with date (search using half-interval search algorithm), to start polling from it # - def seekToTime(self, container, date): + def seekToTime(self, container, date, accuracy=3): fs = container.getFileSize() if logSys.getEffectiveLevel() <= logging.DEBUG: logSys.debug("Seek to find time %s (%s), file size %s", date, datetime.datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M:%S"), fs) - date -= 0.009 - minp = 0 + minp = container.getPos() maxp = fs - lastpos = 0 - lastFew = 0 - lastTime = None + tryPos = minp + lastPos = -1 + foundPos = 0 + foundTime = None cntr = 0 unixTime = None - lasti = 0 - movecntr = 1 + movecntr = accuracy while maxp > minp: - i = int(minp + (maxp - minp) / 2) - pos = container.seek(i) + if tryPos is None: + pos = int(minp + (maxp - minp) / 2) + else: + pos, tryPos = tryPos, None + # because container seek will go to start of next line (minus CRLF): + pos = max(0, pos-2) + seekpos = pos = container.seek(pos) cntr += 1 # within next 5 lines try to find any legal datetime: lncntr = 5; dateTimeMatch = None - llen = 0 - if lastpos == pos: - i = pos + nextp = None while True: line = container.readline() if not line: break - llen += len(line) - l = line.rstrip('\r\n') - (timeMatch, template) = self.dateDetector.matchTime(l) + (timeMatch, template) = self.dateDetector.matchTime(line) if timeMatch: - dateTimeMatch = self.dateDetector.getTime2(l[timeMatch.start():timeMatch.end()], (timeMatch, template)) + dateTimeMatch = self.dateDetector.getTime2(line[timeMatch.start():timeMatch.end()], (timeMatch, template)) + else: + nextp = container.tell() + if nextp > maxp: + pos = seekpos + break + pos = nextp if not dateTimeMatch and lncntr: lncntr -= 1 continue break + # not found at this step - stop searching + if dateTimeMatch: + unixTime = dateTimeMatch[0] + if unixTime >= date: + if foundTime is None or unixTime <= foundTime: + foundPos = pos + foundTime = unixTime + if pos == maxp: + pos = seekpos + if pos < maxp: + maxp = pos + else: + if foundTime is None or unixTime >= foundTime: + foundPos = pos + foundTime = unixTime + if nextp is None: + nextp = container.tell() + pos = nextp + if pos > minp: + minp = pos # if we can't move (position not changed) - if i + llen == lasti: + if pos == lastPos: movecntr -= 1 if movecntr <= 0: break - lasti = i + llen; - # not found at this step - stop searching - if not dateTimeMatch: + # we have found large area without any date mached + # or end of search - try min position (because can be end of previous line): + if minp != lastPos: + lastPos = tryPos = minp + continue break - unixTime = dateTimeMatch[0] - if unixTime >= date: - maxp = i - else: - minp = i + llen - lastFew = pos; - lastTime = unixTime - lastpos = pos - # if found position have a time greater as given - use smallest time we have found - if unixTime is None or unixTime > date: - unixTime = lastTime - lastpos = container.seek(lastFew, False) - else: - lastpos = container.seek(lastpos, False) - container.setPos(lastpos) + lastPos = pos + # always use smallest pos, that could be found: + foundPos = container.seek(minp, False) + container.setPos(foundPos) if logSys.getEffectiveLevel() <= logging.DEBUG: - logSys.debug("Position %s from %s, found time %s (%s) within %s seeks", lastpos, fs, unixTime, - (datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S") if unixTime is not None else ''), cntr) + logSys.debug("Position %s from %s, found time %s (%s) within %s seeks", lastPos, fs, foundTime, + (datetime.datetime.fromtimestamp(foundTime).strftime("%Y-%m-%d %H:%M:%S") if foundTime is not None else ''), cntr) def status(self, flavor="basic"): """Status of Filter plus files being monitored. @@ -898,11 +925,15 @@ class FileContainer: # seek to given position h.seek(offs, 0) # goto end of next line - if endLine: + if offs and endLine: h.readline() # get current real position return h.tell() + def tell(self): + # get current real position + return self.__handler.tell() + @staticmethod def decode_line(filename, enc, line): try: diff --git a/fail2ban/server/filterpoll.py b/fail2ban/server/filterpoll.py index 1afda7f2..d7ff9075 100644 --- a/fail2ban/server/filterpoll.py +++ b/fail2ban/server/filterpoll.py @@ -58,7 +58,6 @@ class FilterPoll(FileFilter): ## The time of the last modification of the file. self.__prevStats = dict() self.__file404Cnt = dict() - self.__initial = dict() logSys.debug("Created FilterPoll") ## @@ -109,11 +108,7 @@ class FilterPoll(FileFilter): modlst = [] Utils.wait_for(lambda: self.getModified(modlst), self.sleeptime) for filename in modlst: - # set start time as now - find time for first usage only (prevent performance bug with polling of big files) - self.getFailures(filename, - (MyTime.time() - self.getFindTime()) if not self.__initial.get(filename) else None - ) - self.__initial[filename] = True + self.getFailures(filename) self.__modified = True if self.__modified: diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index e856b66d..b3eea052 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -176,6 +176,9 @@ class FailTicket(Ticket): self.__retry = self._data['failures']; def setRetry(self, value): + """ Set artificial retry count, normally equal failures / attempt, + used in incremental features (BanTimeIncr) to increase retry count for bad IPs + """ self.__retry = value if not self._data['failures']: self._data['failures'] = 1 @@ -184,6 +187,9 @@ class FailTicket(Ticket): self._data['matches'] = [] def getRetry(self): + """ Returns failures / attempt count or + artificial retry count increased for bad IPs + """ return max(self.__retry, self._data['failures']) def inc(self, matches=None, attempt=1, count=1): diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index 3f0e4c10..6b85af56 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -36,18 +36,20 @@ from ..server.actions import Actions from .dummyjail import DummyJail try: from ..server.database import Fail2BanDb as Fail2BanDb - # because of tests performance use memory instead of file: - def TestFail2BanDb(filename): - if unittest.F2B.fast: - return Fail2BanDb(':memory:') - return Fail2BanDb(filename) -except ImportError: +except ImportError: # pragma: no cover Fail2BanDb = None from .utils import LogCaptureTestCase TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") +# because of tests performance use memory instead of file: +def getFail2BanDb(filename): + if unittest.F2B.memory_db: # pragma: no cover + return Fail2BanDb(':memory:') + return Fail2BanDb(filename) + + class DatabaseTest(LogCaptureTestCase): def setUp(self): @@ -59,8 +61,10 @@ class DatabaseTest(LogCaptureTestCase): "available.") elif Fail2BanDb is None: return - _, self.dbFilename = tempfile.mkstemp(".db", "fail2ban_") - self.db = TestFail2BanDb(self.dbFilename) + self.dbFilename = None + if not unittest.F2B.memory_db: + _, self.dbFilename = tempfile.mkstemp(".db", "fail2ban_") + self.db = getFail2BanDb(self.dbFilename) def tearDown(self): """Call after every test case.""" @@ -68,7 +72,8 @@ class DatabaseTest(LogCaptureTestCase): if Fail2BanDb is None: # pragma: no cover return # Cleanup - os.remove(self.dbFilename) + if self.dbFilename is not None: + os.remove(self.dbFilename) def testGetFilename(self): if Fail2BanDb is None or self.db.filename == ':memory:': # pragma: no cover @@ -106,6 +111,9 @@ class DatabaseTest(LogCaptureTestCase): def testUpdateDb(self): if Fail2BanDb is None: # pragma: no cover return + self.db = None + if self.dbFilename is None: # pragma: no cover + _, self.dbFilename = tempfile.mkstemp(".db", "fail2ban_") shutil.copyfile( os.path.join(TEST_FILES_DIR, 'database_v1.db'), self.dbFilename) self.db = Fail2BanDb(self.dbFilename) diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index df98e1ac..11642d2c 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -81,6 +81,16 @@ def _killfile(f, name): _killfile(None, name + '.bak') +def _maxWaitTime(wtime): + if unittest.F2B.fast: + wtime /= 10 + return wtime + + +def _tm(time): + return datetime.datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M:%S") + + def _assert_equal_entries(utest, found, output, count=None): """Little helper to unify comparisons with the target entries @@ -122,9 +132,15 @@ def _assert_correct_last_attempt(utest, filter_, output, count=None): Test filter to contain target ticket """ if isinstance(filter_, DummyJail): + # get fail ticket from jail found = _ticket_tuple(filter_.getFailTicket()) else: # when we are testing without jails + # wait for failures (up to max time) + Utils.wait_for( + lambda: filter_.failManager.getFailTotal() >= (count if count else output[1]), + _maxWaitTime(10)) + # get fail ticket from filter found = _ticket_tuple(filter_.failManager.toBan()) _assert_equal_entries(utest, found, output, count) @@ -352,59 +368,132 @@ class LogFileFilterPoll(unittest.TestCase): self.assertTrue(self.filter.isModified(LogFileFilterPoll.FILENAME)) self.assertFalse(self.filter.isModified(LogFileFilterPoll.FILENAME)) - def testSeekToTime(self): + def testSeekToTimeSmallFile(self): fname = tempfile.mktemp(prefix='tmp_fail2ban', suffix='.log') - tm = lambda time: datetime.datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M:%S") time = 1417512352 f = open(fname, 'w') - fc = FileContainer(fname, self.filter.getLogEncoding()) - fc.open() - fc.setPos(0); self.filter.seekToTime(fc, time) + fc = None try: + fc = FileContainer(fname, self.filter.getLogEncoding()) + fc.open() + fc.setPos(0); self.filter.seekToTime(fc, time) f.flush() # empty : fc.setPos(0); self.filter.seekToTime(fc, time) self.assertEqual(fc.getPos(), 0) # one entry with exact time: - f.write("%s [sshd] error: PAM: failure len 1\n" % tm(time)) + f.write("%s [sshd] error: PAM: failure len 1\n" % _tm(time)) f.flush() fc.setPos(0); self.filter.seekToTime(fc, time) - # one entry with smaller time: + + # rewrite : f.seek(0) - f.write("%s [sshd] error: PAM: failure len 1\n" % tm(time - 10)) + f.truncate() + fc.close() + fc = FileContainer(fname, self.filter.getLogEncoding()) + fc.open() + # no time - nothing should be found : + for i in xrange(10): + f.write("[sshd] error: PAM: failure len 1\n") + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + + # rewrite + f.seek(0) + f.truncate() + fc.close() + fc = FileContainer(fname, self.filter.getLogEncoding()) + fc.open() + # one entry with smaller time: + f.write("%s [sshd] error: PAM: failure len 2\n" % _tm(time - 10)) f.flush() fc.setPos(0); self.filter.seekToTime(fc, time) - self.assertEqual(fc.getPos(), 0) - f.write("%s [sshd] error: PAM: failure len 3 2 1\n" % tm(time - 9)) - f.flush() - fc.setPos(0); self.filter.seekToTime(fc, time) - self.assertEqual(fc.getPos(), 0) - # add exact time between: - f.write("%s [sshd] error: PAM: failure\n" % tm(time - 1)) + self.assertEqual(fc.getPos(), 53) + # two entries with smaller time: + f.write("%s [sshd] error: PAM: failure len 3 2 1\n" % _tm(time - 9)) f.flush() fc.setPos(0); self.filter.seekToTime(fc, time) self.assertEqual(fc.getPos(), 110) + # check move after end (all of time smaller): + f.write("%s [sshd] error: PAM: failure\n" % _tm(time - 1)) + f.flush() + self.assertEqual(fc.getFileSize(), 157) + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 157) + # stil one exact line: - f.write("%s [sshd] error: PAM: Authentication failure\n" % tm(time)) - f.write("%s [sshd] error: PAM: failure len 1\n" % tm(time)) + f.write("%s [sshd] error: PAM: Authentication failure\n" % _tm(time)) + f.write("%s [sshd] error: PAM: failure len 1\n" % _tm(time)) f.flush() fc.setPos(0); self.filter.seekToTime(fc, time) - self.assertEqual(fc.getPos(), 110) + self.assertEqual(fc.getPos(), 157) + # add something hereafter: - f.write("%s [sshd] error: PAM: failure len 3 2 1\n" % tm(time + 2)) - f.write("%s [sshd] error: PAM: Authentication failure\n" % tm(time + 3)) + f.write("%s [sshd] error: PAM: failure len 3 2 1\n" % _tm(time + 2)) + f.write("%s [sshd] error: PAM: Authentication failure\n" % _tm(time + 3)) f.flush() fc.setPos(0); self.filter.seekToTime(fc, time) - self.assertEqual(fc.getPos(), 110) + self.assertEqual(fc.getPos(), 157) # add something hereafter: - f.write("%s [sshd] error: PAM: failure\n" % tm(time + 9)) - f.write("%s [sshd] error: PAM: failure len 3 2 1\n" % tm(time + 9)) + f.write("%s [sshd] error: PAM: failure\n" % _tm(time + 9)) + f.write("%s [sshd] error: PAM: failure len 4 3 2\n" % _tm(time + 9)) f.flush() fc.setPos(0); self.filter.seekToTime(fc, time) - self.assertEqual(fc.getPos(), 110) + self.assertEqual(fc.getPos(), 157) + # start search from current pos : + fc.setPos(157); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 157) + # start search from current pos : + fc.setPos(110); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 157) finally: - fc.close() + if fc: + fc.close() + _killfile(f, fname) + + def testSeekToTimeLargeFile(self): + fname = tempfile.mktemp(prefix='tmp_fail2ban', suffix='.log') + time = 1417512352 + f = open(fname, 'w') + fc = None + count = 1000 if unittest.F2B.fast else 10000 + try: + fc = FileContainer(fname, self.filter.getLogEncoding()) + fc.open() + f.seek(0) + # variable length of file (ca 45K or 450K before and hereafter): + # write lines with smaller as search time: + t = time - count - 1 + for i in xrange(count): + f.write("%s [sshd] error: PAM: failure\n" % _tm(t)) + t += 1 + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 47*count) + # write lines with exact search time: + for i in xrange(10): + f.write("%s [sshd] error: PAM: failure\n" % _tm(time)) + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 47*count) + fc.setPos(4*count); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 47*count) + # write lines with greater as search time: + t = time+1 + for i in xrange(count//500): + for j in xrange(500): + f.write("%s [sshd] error: PAM: failure\n" % _tm(t)) + t += 1 + f.flush() + fc.setPos(0); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 47*count) + fc.setPos(53); self.filter.seekToTime(fc, time) + self.assertEqual(fc.getPos(), 47*count) + + finally: + if fc: + fc.close() _killfile(f, fname) class LogFileMonitor(LogCaptureTestCase): @@ -418,7 +507,7 @@ class LogFileMonitor(LogCaptureTestCase): _, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures') self.file = open(self.name, 'a') self.filter = FilterPoll(DummyJail()) - self.filter.addLogPath(self.name) + self.filter.addLogPath(self.name, autoSeek=False) self.filter.active = True self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) ") @@ -431,11 +520,12 @@ class LogFileMonitor(LogCaptureTestCase): def isModified(self, delay=2.): """Wait up to `delay` sec to assure that it was modified or not """ - return Utils.wait_for(lambda: self.filter.isModified(self.name), delay) + return Utils.wait_for(lambda: self.filter.isModified(self.name), _maxWaitTime(delay)) - def notModified(self): - # shorter wait time for not modified status - return not self.isModified(4*Utils.DEFAULT_SLEEP_TIME) + def notModified(self, delay=2.): + """Wait up to `delay` sec as long as it was not modified + """ + return Utils.wait_for(lambda: not self.filter.isModified(self.name), _maxWaitTime(delay)) def testUnaccessibleLogFile(self): os.chmod(self.name, 0) @@ -478,7 +568,7 @@ class LogFileMonitor(LogCaptureTestCase): os.rename(self.name, self.name + '.old') # we are not signaling as modified whenever # it gets away - self.assertTrue(self.notModified()) + self.assertTrue(self.notModified(1)) f = open(self.name, 'a') self.assertTrue(self.isModified()) self.assertTrue(self.notModified()) @@ -562,7 +652,7 @@ def get_monitor_failures_testcase(Filter_): self.file = open(self.name, 'a') self.jail = DummyJail() self.filter = Filter_(self.jail) - self.filter.addLogPath(self.name) + self.filter.addLogPath(self.name, autoSeek=False) self.filter.active = 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() @@ -587,21 +677,21 @@ def get_monitor_failures_testcase(Filter_): def isFilled(self, delay=1.): """Wait up to `delay` sec to assure that it was modified or not """ - return Utils.wait_for(self.jail.isFilled, delay) + return Utils.wait_for(self.jail.isFilled, _maxWaitTime(delay)) 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): - Utils.wait_for(self.filter.isAlive, 4*Utils.DEFAULT_SLEEP_TIME) + Utils.wait_for(self.filter.isAlive, _maxWaitTime(5)) - def isEmpty(self, delay=4*Utils.DEFAULT_SLEEP_TIME): + def isEmpty(self, delay=_maxWaitTime(5)): # shorter wait time for not modified status - return Utils.wait_for(self.jail.isEmpty, delay) + return Utils.wait_for(self.jail.isEmpty, _maxWaitTime(delay)) def assert_correct_last_attempt(self, failures, count=None): - self.assertTrue(self.isFilled(20)) # give Filter a chance to react + self.assertTrue(self.isFilled(10)) # give Filter a chance to react _assert_correct_last_attempt(self, self.jail, failures, count=count) def test_grow_file(self): @@ -616,7 +706,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.assertTrue(self.isFilled(6)) + self.assertTrue(self.isFilled(10)) # 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 @@ -653,11 +743,11 @@ def get_monitor_failures_testcase(Filter_): self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=14, mode='w') # Poll might need more time - self.assertTrue(self.isEmpty(min(4, 100 * Utils.DEFAULT_SLEEP_TIME)), + self.assertTrue(self.isEmpty(_maxWaitTime(5)), "Queue must be empty but it is not: %s." % (', '.join([str(x) for x in self.jail.queue]))) self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) - Utils.wait_for(lambda: self.filter.failManager.getFailTotal() == 2, 50 * Utils.DEFAULT_SLEEP_TIME) + Utils.wait_for(lambda: self.filter.failManager.getFailTotal() == 2, _maxWaitTime(10)) self.assertEqual(self.filter.failManager.getFailTotal(), 2) # move aside, but leaving the handle still open... @@ -682,7 +772,7 @@ def get_monitor_failures_testcase(Filter_): if interim_kill: _killfile(None, self.name) - time.sleep(Utils.DEFAULT_SLEEP_TIME) # let them know + time.sleep(Utils.DEFAULT_SLEEP_INTERVAL) # let them know # now create a new one to override old one _copy_lines_between_files(GetFailures.FILENAME_01, self.name + '.new', @@ -729,10 +819,10 @@ def get_monitor_failures_testcase(Filter_): _copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=100) # so we should get no more failures detected - self.assertTrue(self.isEmpty(200 * Utils.DEFAULT_SLEEP_TIME)) + self.assertTrue(self.isEmpty(_maxWaitTime(10))) - # but then if we add it back again - self.filter.addLogPath(self.name) + # but then if we add it back again (no seek to time in FileFilter's, because in file used the same time) + self.filter.addLogPath(self.name, autoSeek=False) # Tricky catch here is that it should get them from the # tail written before, so let's not copy anything yet #_copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100) @@ -789,14 +879,14 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover def isFilled(self, delay=1.): """Wait up to `delay` sec to assure that it was modified or not """ - return Utils.wait_for(self.jail.isFilled, delay) + return Utils.wait_for(self.jail.isFilled, _maxWaitTime(delay)) - def isEmpty(self, delay=4*Utils.DEFAULT_SLEEP_TIME): + def isEmpty(self, delay=_maxWaitTime(5)): # shorter wait time for not modified status - return Utils.wait_for(self.jail.isEmpty, delay) + return Utils.wait_for(self.jail.isEmpty, _maxWaitTime(delay)) def assert_correct_ban(self, test_ip, test_attempts): - self.assertTrue(self.isFilled(10)) # give Filter a chance to react + self.assertTrue(self.isFilled(_maxWaitTime(10))) # give Filter a chance to react ticket = self.jail.getFailTicket() attempts = ticket.getAttempt() @@ -819,7 +909,7 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover _copy_lines_to_journal( self.test_file, self.journal_fields, skip=2, n=3) - self.assertTrue(self.isFilled(6)) + self.assertTrue(self.isFilled(10)) # so we sleep for up to 6 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 @@ -852,7 +942,7 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover _copy_lines_to_journal( self.test_file, self.journal_fields, n=5, skip=5) # so we should get no more failures detected - self.assertTrue(self.isEmpty(200 * Utils.DEFAULT_SLEEP_TIME)) + self.assertTrue(self.isEmpty(_maxWaitTime(10))) # but then if we add it back again self.filter.addJournalMatch([ @@ -863,7 +953,7 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover _copy_lines_to_journal( self.test_file, self.journal_fields, n=6, skip=10) # we should detect the failures - self.assertTrue(self.isFilled(6)) + self.assertTrue(self.isFilled(10)) return MonitorJournalFailures @@ -931,7 +1021,7 @@ class GetFailures(LogCaptureTestCase): filename = filename or GetFailures.FILENAME_01 failures = failures or GetFailures.FAILURES_01 - self.filter.addLogPath(filename) + self.filter.addLogPath(filename, autoSeek=0) self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) $") self.filter.getFailures(filename) _assert_correct_last_attempt(self, self.filter, failures) @@ -955,7 +1045,7 @@ class GetFailures(LogCaptureTestCase): [u'Aug 14 11:%d:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:141.3.81.106 port 51332 ssh2' % m for m in 53, 54, 57, 58]) - self.filter.addLogPath(GetFailures.FILENAME_02) + self.filter.addLogPath(GetFailures.FILENAME_02, autoSeek=0) self.filter.addFailRegex("Failed .* from ") self.filter.getFailures(GetFailures.FILENAME_02) _assert_correct_last_attempt(self, self.filter, output) @@ -963,25 +1053,35 @@ class GetFailures(LogCaptureTestCase): def testGetFailures03(self): output = ('203.162.223.135', 7, 1124013544.0) - self.filter.addLogPath(GetFailures.FILENAME_03) + self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=0) self.filter.addFailRegex("error,relay=,.*550 User unknown") self.filter.getFailures(GetFailures.FILENAME_03) _assert_correct_last_attempt(self, self.filter, output) - def testGetFailures03_seek(self): + def testGetFailures03_Seek1(self): # same test as above but with seek to 'Aug 14 11:55:04' - so other output ... output = ('203.162.223.135', 5, 1124013544.0) - self.filter.addLogPath(GetFailures.FILENAME_03) + self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=output[2] - 4*60) self.filter.addFailRegex("error,relay=,.*550 User unknown") - self.filter.getFailures(GetFailures.FILENAME_03, output[2] - 4*60 + 1) + self.filter.getFailures(GetFailures.FILENAME_03) + _assert_correct_last_attempt(self, self.filter, output) + + def testGetFailures03_Seek2(self): + # same test as above but with seek to 'Aug 14 11:59:04' - so other output ... + output = ('203.162.223.135', 1, 1124013544.0) + self.filter.setMaxRetry(1) + + self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=output[2]) + self.filter.addFailRegex("error,relay=,.*550 User unknown") + self.filter.getFailures(GetFailures.FILENAME_03) _assert_correct_last_attempt(self, self.filter, output) def testGetFailures04(self): output = [('212.41.96.186', 4, 1124013600.0), ('212.41.96.185', 4, 1124017198.0)] - self.filter.addLogPath(GetFailures.FILENAME_04) + self.filter.addLogPath(GetFailures.FILENAME_04, autoSeek=0) self.filter.addFailRegex("Invalid user .* ") self.filter.getFailures(GetFailures.FILENAME_04) @@ -1048,7 +1148,7 @@ class GetFailures(LogCaptureTestCase): filter_.active = True filter_.failManager.setMaxRetry(1) # we might have just few failures - filter_.addLogPath(GetFailures.FILENAME_USEDNS) + filter_.addLogPath(GetFailures.FILENAME_USEDNS, autoSeek=False) filter_.addFailRegex("Failed .* from ") filter_.getFailures(GetFailures.FILENAME_USEDNS) _assert_correct_last_attempt(self, filter_, output) @@ -1056,14 +1156,14 @@ class GetFailures(LogCaptureTestCase): def testGetFailuresMultiRegex(self): output = ('141.3.81.106', 8, 1124013541.0) - self.filter.addLogPath(GetFailures.FILENAME_02) + self.filter.addLogPath(GetFailures.FILENAME_02, autoSeek=False) self.filter.addFailRegex("Failed .* from ") self.filter.addFailRegex("Accepted .* from ") self.filter.getFailures(GetFailures.FILENAME_02) _assert_correct_last_attempt(self, self.filter, output) def testGetFailuresIgnoreRegex(self): - self.filter.addLogPath(GetFailures.FILENAME_02) + self.filter.addLogPath(GetFailures.FILENAME_02, autoSeek=False) self.filter.addFailRegex("Failed .* from ") self.filter.addFailRegex("Accepted .* from ") self.filter.addIgnoreRegex("for roehl") @@ -1075,7 +1175,7 @@ class GetFailures(LogCaptureTestCase): def testGetFailuresMultiLine(self): output = [("192.0.43.10", 2, 1124013599.0), ("192.0.43.11", 1, 1124013598.0)] - self.filter.addLogPath(GetFailures.FILENAME_MULTILINE) + self.filter.addLogPath(GetFailures.FILENAME_MULTILINE, autoSeek=False) self.filter.addFailRegex("^.*rsyncd\[(?P\d+)\]: connect from .+ \(\)$^.+ rsyncd\[(?P=pid)\]: rsync error: .*$") self.filter.setMaxLines(100) self.filter.setMaxRetry(1) @@ -1093,7 +1193,7 @@ class GetFailures(LogCaptureTestCase): def testGetFailuresMultiLineIgnoreRegex(self): output = [("192.0.43.10", 2, 1124013599.0)] - self.filter.addLogPath(GetFailures.FILENAME_MULTILINE) + self.filter.addLogPath(GetFailures.FILENAME_MULTILINE, autoSeek=False) self.filter.addFailRegex("^.*rsyncd\[(?P\d+)\]: connect from .+ \(\)$^.+ rsyncd\[(?P=pid)\]: rsync error: .*$") self.filter.addIgnoreRegex("rsync error: Received SIGINT") self.filter.setMaxLines(100) @@ -1109,7 +1209,7 @@ class GetFailures(LogCaptureTestCase): output = [("192.0.43.10", 2, 1124013599.0), ("192.0.43.11", 1, 1124013598.0), ("192.0.43.15", 1, 1124013598.0)] - self.filter.addLogPath(GetFailures.FILENAME_MULTILINE) + self.filter.addLogPath(GetFailures.FILENAME_MULTILINE, autoSeek=False) self.filter.addFailRegex("^.*rsyncd\[(?P\d+)\]: connect from .+ \(\)$^.+ rsyncd\[(?P=pid)\]: rsync error: .*$") self.filter.addFailRegex("^.* sendmail\[.*, msgid=<(?P[^>]+).*relay=\[\].*$^.+ spamd: result: Y \d+ .*,mid=<(?P=msgid)>(,bayes=[.\d]+)?(,autolearn=\S+)?\s*$") self.filter.setMaxLines(100) diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index eedc8c94..d26d7562 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -76,12 +76,15 @@ class TransmitterBase(unittest.TestCase): self.server.quit() def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False): + """Process set/get commands and compare both return values + with outValue if it was given otherwise with inValue""" setCmd = ["set", cmd, inValue] getCmd = ["get", cmd] if jail is not None: setCmd.insert(1, jail) getCmd.insert(1, jail) + # if outValue was not given (now None is allowed return/compare value also) if outValue == (None,): outValue = inValue @@ -173,7 +176,7 @@ class Transmitter(TransmitterBase): self.assertEqual(self.transm.proceed(["sleep", "0.0001"]), (0, None)) def testDatabase(self): - if not unittest.F2B.fast: + if not unittest.F2B.memory_db: tmp, tmpFilename = tempfile.mkstemp(".db", "fail2ban_") else: # pragma: no cover tmpFilename = ':memory:' @@ -208,7 +211,7 @@ class Transmitter(TransmitterBase): self.assertEqual(self.transm.proceed( ["set", "dbfile", "None"]), (0, None)) - if not unittest.F2B.fast: + if not unittest.F2B.memory_db: os.close(tmp) os.unlink(tmpFilename) diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 017ad3f6..338ccec1 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -23,6 +23,7 @@ __copyright__ = "Copyright (c) 2013 Yaroslav Halchenko" __license__ = "GPL" import logging +import optparse import os import re import sys @@ -35,6 +36,7 @@ from ..server.filter import DNSUtils from ..server.mytime import MyTime from ..server.utils import Utils + logSys = getLogger(__name__) CONFIG_DIR = os.environ.get('FAIL2BAN_CONFIG_DIR', None) @@ -47,10 +49,14 @@ if not CONFIG_DIR: CONFIG_DIR = '/etc/fail2ban' -class F2B(): - def __init__(self, fast=False, no_network=False): - self.fast=fast - self.no_network=no_network +class F2B(optparse.Values): + def __init__(self, opts={}): + self.__dict__ = opts.__dict__ if opts else { + 'fast': False, 'memory_db':False, 'no_gamin': False, 'no_network': False, + } + if self.fast: + self.memory_db = True + self.no_gamin = True def SkipIfFast(self): pass def SkipIfNoNetwork(self): @@ -58,13 +64,11 @@ class F2B(): def initTests(opts): - if opts: # pragma: no cover - unittest.F2B = F2B(opts.fast, opts.no_network) - else: - unittest.F2B = F2B() + unittest.F2B = F2B(opts) # --fast : if unittest.F2B.fast: # pragma: no cover - # prevent long sleeping during test cases... + # racy decrease default sleep intervals to test it faster + # (prevent long sleeping during test cases ... less time goes to sleep): Utils.DEFAULT_SLEEP_TIME = 0.0025 Utils.DEFAULT_SLEEP_INTERVAL = 0.0005 def F2B_SkipIfFast(): @@ -74,7 +78,7 @@ def initTests(opts): # sleep intervals are large - use replacement for sleep to check time to sleep: _org_sleep = time.sleep def _new_sleep(v): - if (v > Utils.DEFAULT_SLEEP_TIME): + if (v > Utils.DEFAULT_SLEEP_TIME): # pragma: no cover raise ValueError('[BAD-CODE] To long sleep interval: %s, try to use conditional Utils.wait_for instead' % v) _org_sleep(min(v, Utils.DEFAULT_SLEEP_TIME)) time.sleep = _new_sleep @@ -103,7 +107,7 @@ def setUpMyTime(): def tearDownMyTime(): os.environ.pop('TZ') - if old_TZ: + if old_TZ: # pragma: no cover os.environ['TZ'] = old_TZ time.tzset() MyTime.myTime = None @@ -135,11 +139,15 @@ def gatherTests(regexps=None, opts=None): _regexps = [re.compile(r) for r in regexps] def addTest(self, suite): - suite_str = str(suite) - for r in self._regexps: - if r.search(suite_str): - super(FilteredTestSuite, self).addTest(suite) - return + matched = [] + for test in suite: + s = str(test) + for r in self._regexps: + if r.search(s): + matched.append(test) + break + for test in matched: + super(FilteredTestSuite, self).addTest(test) tests = FilteredTestSuite() @@ -160,7 +168,7 @@ def gatherTests(regexps=None, opts=None): try: import dns tests.addTest(unittest.makeSuite(banmanagertestcase.StatusExtendedCymruInfo)) - except ImportError: + except ImportError: # pragma: no cover pass # ClientReaders tests.addTest(unittest.makeSuite(clientreadertestcase.ConfigReaderTest)) @@ -225,7 +233,8 @@ def gatherTests(regexps=None, opts=None): try: # because gamin can be very slow on some platforms (and can produce many failures # with fast sleep interval) - skip it by fast run: - unittest.F2B.SkipIfFast() + if unittest.F2B.fast or unittest.F2B.no_gamin: # pragma: no cover + raise Exception('Skip, fast: %s, no_gamin: %s' % (unittest.F2B.fast, unittest.F2B.no_gamin)) from ..server.filtergamin import FilterGamin filters.append(FilterGamin) except Exception, e: # pragma: no cover From 770c219ab69411029910b4ee2d9cb8bc7b25bccc Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 22 Jul 2015 17:32:37 +0200 Subject: [PATCH 016/623] SetupTest should run at version of python currently installed (resp. active one), skip if started with another version. --- fail2ban/tests/misctestcase.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/fail2ban/tests/misctestcase.py b/fail2ban/tests/misctestcase.py index a2310ec5..c4dd716d 100644 --- a/fail2ban/tests/misctestcase.py +++ b/fail2ban/tests/misctestcase.py @@ -66,6 +66,17 @@ class HelpersTest(unittest.TestCase): self.assertEqual(splitcommaspace(' 1, 2 , '), ['1', '2']) +def _getSysPythonVersion(): + import subprocess, locale + sysVerCmd = "python -c 'import sys; print(tuple(sys.version_info))'" + if sys.version_info >= (2,7): + sysVer = subprocess.check_output(sysVerCmd, shell=True) + else: + sysVer = subprocess.Popen(sysVerCmd, shell=True, stdout=subprocess.PIPE).stdout.read() + if sys.version_info >= (3,): + sysVer = sysVer.decode(locale.getpreferredencoding(), 'replace') + return str(sysVer).rstrip() + class SetupTest(unittest.TestCase): def setUp(self): @@ -75,6 +86,12 @@ class SetupTest(unittest.TestCase): raise unittest.SkipTest( "Seems to be running not out of source distribution" " -- cannot locate setup.py") + # compare current version of python installed resp. active one: + sysVer = _getSysPythonVersion() + if sysVer != str(tuple(sys.version_info)): + raise unittest.SkipTest( + "Seems to be running with python distribution %s" + " -- install can be tested only with system distribution %s" % (str(tuple(sys.version_info)), sysVer)) def testSetupInstallRoot(self): if not self.setup: From 72f29e90619dbc02bc3192c9f44ddf38195a2393 Mon Sep 17 00:00:00 2001 From: sebres Date: Thu, 23 Jul 2015 20:23:07 +0200 Subject: [PATCH 017/623] asyncserver (asyncore) code fixed and test cases repaired (always delete temp files, wait for end of thread/server, etc) definitely closes gh-161, also other usage of asyncore event loop (in test_smtp.py) repair cache in ipToName (can returns None), precaching of invalid IPs (according to RFC 5737) to stop endless wait for resolving it in test cases. --- fail2ban/server/asyncserver.py | 67 ++++++++++++++++++++-------- fail2ban/server/filter.py | 4 +- fail2ban/tests/action_d/test_smtp.py | 10 ++--- fail2ban/tests/servertestcase.py | 6 +++ fail2ban/tests/sockettestcase.py | 32 +++++++++++-- fail2ban/tests/utils.py | 13 ++++++ 6 files changed, 103 insertions(+), 29 deletions(-) diff --git a/fail2ban/server/asyncserver.py b/fail2ban/server/asyncserver.py index a9be0ae2..ad37544a 100644 --- a/fail2ban/server/asyncserver.py +++ b/fail2ban/server/asyncserver.py @@ -27,12 +27,14 @@ __license__ = "GPL" from pickle import dumps, loads, HIGHEST_PROTOCOL import asynchat import asyncore +import errno import fcntl import os import socket import sys import traceback +from .utils import Utils from ..protocol import CSPROTO from ..helpers import getLogger,formatExceptionInfo @@ -89,6 +91,29 @@ class RequestHandler(asynchat.async_chat): self.close() +def loop(active, timeout=None, use_poll=False): + # Use poll instead of loop, because of recognition of active flag, + # because of loop timeout mistake: different in poll and poll2 (sec vs ms), + # and to prevent sporadical errors like EBADF 'Bad file descriptor' etc. (see gh-161) + if timeout is None: + timeout = Utils.DEFAULT_SLEEP_TIME + poll = asyncore.poll + if use_poll and asyncore.poll2 and hasattr(asyncore.select, 'poll'): # pragma: no cover + logSys.debug('Server listener (select) uses poll') + # poll2 expected a timeout in milliseconds (but poll and loop in seconds): + timeout = float(timeout) / 1000 + poll = asyncore.poll2 + # Poll as long as active: + while active(): + try: + poll(timeout) + except Exception as e: # pragma: no cover + if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') + logSys.info('Server connection was closed: %s', str(e)) + else: + logSys.error('Server connection was closed: %s', str(e)) + + ## # Asynchronous server class. # @@ -102,6 +127,7 @@ class AsyncServer(asyncore.dispatcher): self.__transmitter = transmitter self.__sock = "/var/run/fail2ban/fail2ban.sock" self.__init = False + self.__active = False ## # Returns False as we only read the socket first. @@ -129,7 +155,7 @@ class AsyncServer(asyncore.dispatcher): # @param sock: socket file. # @param force: remove the socket file if exists. - def start(self, sock, force): + def start(self, sock, force, use_poll=False): self.__sock = sock # Remove socket if os.path.exists(sock): @@ -149,28 +175,31 @@ class AsyncServer(asyncore.dispatcher): AsyncServer.__markCloseOnExec(self.socket) self.listen(1) # Sets the init flag. - self.__init = True - # TODO Add try..catch - # There's a bug report for Python 2.6/3.0 that use_poll=True yields some 2.5 incompatibilities: - if (sys.version_info >= (2, 7) and sys.version_info < (2, 8)) \ - or (sys.version_info >= (3, 4)): # if python 2.7 ... - logSys.debug("Detected Python 2.7. asyncore.loop() using poll") - asyncore.loop(use_poll=True) # workaround for the "Bad file descriptor" issue on Python 2.7, gh-161 - else: - asyncore.loop(use_poll=False) # fixes the "Unexpected communication problem" issue on Python 2.6 and 3.0 - + self.__init = self.__active = True + # Event loop as long as active: + loop(lambda: self.__active) + # Cleanup all + self.stop() + + + def close(self): + if self.__active: + asyncore.dispatcher.close(self) + # Remove socket (file) only if it was created: + if self.__init and os.path.exists(self.__sock): + logSys.debug("Removed socket file " + self.__sock) + os.remove(self.__sock) + logSys.debug("Socket shutdown") + self.__active = False + ## # Stops the communication server. def stop(self): - if self.__init: - # Only closes the socket if it was initialized first. - self.close() - # Remove socket - if os.path.exists(self.__sock): - logSys.debug("Removed socket file " + self.__sock) - os.remove(self.__sock) - logSys.debug("Socket shutdown") + self.close() + + def isActive(self): + return self.__active ## # Marks socket as close-on-exec to avoid leaking file descriptors when diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index d1ac3680..8dbb6a7a 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -1023,8 +1023,8 @@ class DNSUtils: @staticmethod def ipToName(ip): # cache, also prevent long wait during retrieving of name for wrong addresses, lazy dns: - v = DNSUtils.CACHE_ipToName.get(ip) - if v is not None: + v = DNSUtils.CACHE_ipToName.get(ip, ()) + if v != (): return v # retrieve name try: diff --git a/fail2ban/tests/action_d/test_smtp.py b/fail2ban/tests/action_d/test_smtp.py index 27442832..37fe0138 100644 --- a/fail2ban/tests/action_d/test_smtp.py +++ b/fail2ban/tests/action_d/test_smtp.py @@ -19,7 +19,6 @@ import os import smtpd -import asyncore import threading import unittest import sys @@ -30,7 +29,7 @@ else: from ..dummyjail import DummyJail -from ..utils import CONFIG_DIR +from ..utils import CONFIG_DIR, asyncserver class TestSMTPServer(smtpd.SMTPServer): @@ -46,8 +45,6 @@ class SMTPActionTest(unittest.TestCase): def setUp(self): """Call before every test case.""" - unittest.F2B.SkipIfNoNetwork() - self.jail = DummyJail() pythonModule = os.path.join(CONFIG_DIR, "action.d", "smtp.py") pythonModuleName = os.path.basename(pythonModule.rstrip(".py")) @@ -64,13 +61,16 @@ class SMTPActionTest(unittest.TestCase): self.action = customActionModule.Action( self.jail, "test", host="127.0.0.1:%i" % port) + ## because of bug in loop (see loop in asyncserver.py) use it's loop instead of asyncore.loop: + self._active = True self._loop_thread = threading.Thread( - target=asyncore.loop, kwargs={'timeout': 1}) + target=asyncserver.loop, kwargs={'active': lambda: self._active}) self._loop_thread.start() def tearDown(self): """Call after every test case.""" self.smtpd.close() + self._active = False self._loop_thread.join() def testStart(self): diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index d26d7562..87ca4289 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -62,11 +62,14 @@ class TransmitterBase(unittest.TestCase): def setUp(self): """Call before every test case.""" self.transm = self.server._Server__transm + self.tmp_files = [] sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'transmitter') os.close(sock_fd) + self.tmp_files.append(sock_name) pidfile_fd, pidfile_name = tempfile.mkstemp( 'fail2ban.pid', 'transmitter') os.close(pidfile_fd) + self.tmp_files.append(pidfile_name) self.server.start(sock_name, pidfile_name, force=False) self.jailName = "TestJail1" self.server.addJail(self.jailName, "auto") @@ -74,6 +77,9 @@ class TransmitterBase(unittest.TestCase): def tearDown(self): """Call after every test case.""" self.server.quit() + for f in self.tmp_files: + if os.path.exists(f): + os.remove(f) def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False): """Process set/get commands and compare both return values diff --git a/fail2ban/tests/sockettestcase.py b/fail2ban/tests/sockettestcase.py index a9408fde..5bf0be57 100644 --- a/fail2ban/tests/sockettestcase.py +++ b/fail2ban/tests/sockettestcase.py @@ -55,6 +55,24 @@ class Socket(unittest.TestCase): """Test transmitter proceed method which just returns first arg""" return message + def testStopPerCloseUnexpected(self): + # start in separate thread : + serverThread = threading.Thread( + target=self.server.start, args=(self.sock_name, False)) + serverThread.daemon = True + serverThread.start() + self.assertTrue(Utils.wait_for(self.server.isActive, unittest.F2B.maxWaitTime(10))) + # unexpected stop directly after start: + self.server.close() + # wait for end of thread : + Utils.wait_for(lambda: not serverThread.isAlive() + or serverThread.join(Utils.DEFAULT_SLEEP_INTERVAL), unittest.F2B.maxWaitTime(10)) + self.assertFalse(serverThread.isAlive()) + # clean : + self.server.stop() + self.assertFalse(self.server.isActive()) + self.assertFalse(os.path.exists(self.sock_name)) + def _serverSocket(self): try: return CSocket(self.sock_name) @@ -66,6 +84,7 @@ class Socket(unittest.TestCase): target=self.server.start, args=(self.sock_name, False)) serverThread.daemon = True serverThread.start() + self.assertTrue(Utils.wait_for(self.server.isActive, unittest.F2B.maxWaitTime(10))) time.sleep(Utils.DEFAULT_SLEEP_TIME) client = Utils.wait_for(self._serverSocket, 2) @@ -78,7 +97,11 @@ class Socket(unittest.TestCase): client.close() self.server.stop() - serverThread.join(Utils.DEFAULT_SLEEP_TIME) + # wait for end of thread : + Utils.wait_for(lambda: not serverThread.isAlive() + or serverThread.join(Utils.DEFAULT_SLEEP_INTERVAL), unittest.F2B.maxWaitTime(10)) + self.assertFalse(serverThread.isAlive()) + self.assertFalse(self.server.isActive()) self.assertFalse(os.path.exists(self.sock_name)) def testSocketForce(self): @@ -92,10 +115,13 @@ class Socket(unittest.TestCase): target=self.server.start, args=(self.sock_name, True)) serverThread.daemon = True serverThread.start() - time.sleep(Utils.DEFAULT_SLEEP_TIME) + self.assertTrue(Utils.wait_for(self.server.isActive, unittest.F2B.maxWaitTime(10))) self.server.stop() - serverThread.join(Utils.DEFAULT_SLEEP_TIME) + # wait for end of thread : + Utils.wait_for(lambda: not serverThread.isAlive() + or serverThread.join(Utils.DEFAULT_SLEEP_INTERVAL), unittest.F2B.maxWaitTime(10)) + self.assertFalse(self.server.isActive()) self.assertFalse(os.path.exists(self.sock_name)) diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 338ccec1..eb4e6e2e 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -35,6 +35,8 @@ from ..helpers import getLogger from ..server.filter import DNSUtils from ..server.mytime import MyTime from ..server.utils import Utils +# for action_d.test_smtp : +from ..server import asyncserver logSys = getLogger(__name__) @@ -61,6 +63,10 @@ class F2B(optparse.Values): pass def SkipIfNoNetwork(self): pass + def maxWaitTime(self,wtime): + if self.fast: + wtime = float(wtime) / 10 + return wtime def initTests(opts): @@ -87,6 +93,13 @@ def initTests(opts): def F2B_SkipIfNoNetwork(): raise unittest.SkipTest('Skip test because of "--no-network"') unittest.F2B.SkipIfNoNetwork = F2B_SkipIfNoNetwork + # precache all invalid ip's (TEST-NET-1, ..., TEST-NET-3 according to RFC 5737): + c = DNSUtils.CACHE_ipToName + for i in xrange(255): + c.set('192.0.2.%s' % i, None) + c.set('198.51.100.%s' % i, None) + c.set('203.0.113.%s' % i, None) + def mtimesleep(): # no sleep now should be necessary since polling tracks now not only From f143ae479cee8510ddc7385cb84228a4823b58aa Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 1 Sep 2015 20:10:47 +0200 Subject: [PATCH 018/623] code and config review after rebase to current master --- fail2ban/server/server.py | 5 ++--- fail2ban/tests/filtertestcase.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 86197bc5..3bdfd71b 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -326,9 +326,8 @@ class Server: def isAlive(self, jailnum=None): if jailnum is not None and len(self.__jails) != jailnum: return 0 - for j in self.__jails: - j = self.__jails[j] - if not j.isAlive(): + for jail in self.__jails.values(): + if not jail.isAlive(): return 0 return 1 diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 11642d2c..5edfd267 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -1302,9 +1302,9 @@ class DNSUtilsNetworkTests(unittest.TestCase): self.assertEqual(res, []) def testIpToName(self): + unittest.F2B.SkipIfNoNetwork() res = DNSUtils.ipToName('8.8.4.4') self.assertEqual(res, 'google-public-dns-b.google.com') - unittest.F2B.SkipIfNoNetwork() # invalid ip (TEST-NET-1 according to RFC 5737) res = DNSUtils.ipToName('192.0.2.0') self.assertEqual(res, None) From f7cc55103c77cd4dc0502268ea05215c32a502c6 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 17 Nov 2015 08:56:41 +0100 Subject: [PATCH 019/623] optimized FailManager: increase performance, try to prevent memory leakage (don't copy failures resp. it list on some operations) --- fail2ban/server/failmanager.py | 87 +++++++++++---------------- fail2ban/tests/failmanagertestcase.py | 4 +- 2 files changed, 36 insertions(+), 55 deletions(-) diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index 4d006098..905553d4 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -44,53 +44,30 @@ class FailManager: self.__failTotal = 0 def setFailTotal(self, value): - try: - self.__lock.acquire() + with self.__lock: self.__failTotal = value - finally: - self.__lock.release() def getFailTotal(self): - try: - self.__lock.acquire() + with self.__lock: return self.__failTotal - finally: - self.__lock.release() def setMaxRetry(self, value): - try: - self.__lock.acquire() - self.__maxRetry = value - finally: - self.__lock.release() + self.__maxRetry = value def getMaxRetry(self): - try: - self.__lock.acquire() - return self.__maxRetry - finally: - self.__lock.release() + return self.__maxRetry def setMaxTime(self, value): - try: - self.__lock.acquire() - self.__maxTime = value - finally: - self.__lock.release() + self.__maxTime = value def getMaxTime(self): - try: - self.__lock.acquire() - return self.__maxTime - finally: - self.__lock.release() + return self.__maxTime def addFailure(self, ticket, count=1): attempts = 1 - try: - self.__lock.acquire() + with self.__lock: ip = ticket.getIP() - if ip in self.__failList: + try: fData = self.__failList[ip] # if the same object: if fData is ticket: @@ -103,7 +80,7 @@ class FailManager: fData.setRetry(0) fData.inc(matches, 1, count) fData.setLastTime(unixTime) - else: + except KeyError: # if already FailTicket - add it direct, otherwise create (using copy all ticket data): if isinstance(ticket, FailTicket): fData = ticket; @@ -124,42 +101,46 @@ class FailManager: for k,v in self.__failList.iteritems()]) logSys.debug("Total # of detected failures: %d. Current failures from %d IPs (IP:count): %s" % (self.__failTotal, len(self.__failList), failures_summary)) - finally: - self.__lock.release() return attempts def size(self): - try: - self.__lock.acquire() + with self.__lock: return len(self.__failList) - finally: - self.__lock.release() def cleanup(self, time): - try: - self.__lock.acquire() - tmp = self.__failList.copy() - for item in tmp: - if tmp[item].getLastTime() < time - self.__maxTime: - self.__delFailure(item) - finally: - self.__lock.release() + with self.__lock: + todelete = [ip for ip,item in self.__failList.iteritems() \ + if item.getLastTime() + self.__maxTime <= time] + if len(todelete) == len(self.__failList): + # remove all: + self.__failList = dict() + elif not len(todelete): + # nothing: + return + if len(todelete) / 2.0 <= len(self.__failList) / 3.0: + # few as 2/3 should be removed - remove particular items: + for ip in todelete: + del self.__failList[ip] + else: + # create new dictionary without items to be deleted: + self.__failList = dict((ip,item) for ip,item in self.__failList.iteritems() \ + if item.getLastTime() + self.__maxTime > time) - def __delFailure(self, ip): - if ip in self.__failList: - del self.__failList[ip] + def delFailure(self, ip): + with self.__lock: + try: + del self.__failList[ip] + except KeyError: + pass def toBan(self, ip=None): - try: - self.__lock.acquire() + with self.__lock: for ip in ([ip] if ip != None and ip in self.__failList else self.__failList): data = self.__failList[ip] if data.getRetry() >= self.__maxRetry: del self.__failList[ip] return data raise FailManagerEmpty - finally: - self.__lock.release() class FailManagerEmpty(Exception): diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index a8c0f6fc..5a6e95e7 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -68,11 +68,11 @@ class AddFailure(unittest.TestCase): self.assertEqual(self.__failManager.getMaxTime(), 13) self.__failManager.setMaxTime(600) - def _testDel(self): + def testDel(self): self.__failManager.delFailure('193.168.0.128') self.__failManager.delFailure('111.111.1.111') - self.assertEqual(self.__failManager.size(), 1) + self.assertEqual(self.__failManager.size(), 2) def testCleanupOK(self): timestamp = 1167606999.0 From a10eb39bbef646c5d1790db16882973028f0f695 Mon Sep 17 00:00:00 2001 From: sebres Date: Thu, 19 Nov 2015 04:26:35 +0100 Subject: [PATCH 020/623] test cases extended with memory leakage check --- fail2ban/server/failmanager.py | 5 +- fail2ban/tests/failmanagertestcase.py | 105 ++++++++++++++++++++++++++ fail2ban/tests/utils.py | 1 + 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index 905553d4..07be0a4c 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -32,6 +32,7 @@ from ..helpers import getLogger # Gets the instance of the logger. logSys = getLogger(__name__) +logLevel = logging.DEBUG class FailManager: @@ -93,13 +94,13 @@ class FailManager: attempts = fData.getRetry() self.__failTotal += 1 - if logSys.getEffectiveLevel() <= logging.DEBUG: + if logSys.getEffectiveLevel() <= logLevel: # yoh: Since composing this list might be somewhat time consuming # in case of having many active failures, it should be ran only # if debug level is "low" enough failures_summary = ', '.join(['%s:%d' % (k, v.getRetry()) for k,v in self.__failList.iteritems()]) - logSys.debug("Total # of detected failures: %d. Current failures from %d IPs (IP:count): %s" + logSys.log(logLevel, "Total # of detected failures: %d. Current failures from %d IPs (IP:count): %s" % (self.__failTotal, len(self.__failList), failures_summary)) return attempts diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index 5a6e95e7..2d4ce430 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -26,6 +26,7 @@ __license__ = "GPL" import unittest +from ..server import failmanager from ..server.failmanager import FailManager, FailManagerEmpty from ..server.ticket import FailTicket @@ -120,3 +121,107 @@ class AddFailure(unittest.TestCase): ticket = self.__failManager.toBan() self.assertNotEqual(ticket.getIP(), "100.100.10.10") self.assertRaises(FailManagerEmpty, self.__failManager.toBan) + + +class FailmanagerComplex(unittest.TestCase): + + def setUp(self): + """Call before every test case.""" + super(FailmanagerComplex, self).setUp() + self.__failManager = FailManager() + # down logging level for all this tests, because of extremely large failure count (several GB on heavydebug) + self.__saved_ll = failmanager.logLevel + failmanager.logLevel = 3 + + def tearDown(self): + super(FailmanagerComplex, self).tearDown() + # restore level + failmanager.logLevel = self.__saved_ll + + @staticmethod + def _ip_range(maxips): + class _ip(list): + def __str__(self): + return '.'.join(map(str, self)) + def __repr__(self): + return str(self) + def __key__(self): + return str(self) + def __hash__(self): + #return (int)(struct.unpack('I', struct.pack("BBBB",*self))[0]) + return (int)(self[0] << 24 | self[1] << 16 | self[2] << 8 | self[3]) + i = 0 + c = [127,0,0,0] + while i < maxips: + for n in range(3,0,-1): + if c[n] < 255: + c[n] += 1 + break + c[n] = 0 + yield (i, _ip(c)) + i += 1 + + def testCheckIPGenerator(self): + for i, ip in self._ip_range(65536 if not unittest.F2B.fast else 1000): + if i == 254: + self.assertEqual(str(ip), '127.0.0.255') + elif i == 255: + self.assertEqual(str(ip), '127.0.1.0') + elif i == 1000: + self.assertEqual(str(ip), '127.0.3.233') + elif i == 65534: + self.assertEqual(str(ip), '127.0.255.255') + elif i == 65535: + self.assertEqual(str(ip), '127.1.0.0') + + def testFailuresMemLeak1(self): + # use factor (divisor) instead unittest.F2B.SkipIfFast() : + modeDiv = 1 + if unittest.F2B.fast: # pragma: no cover + modeDiv = 10 + self.__failManager.setMaxTime(self.__failManager.getMaxTime() // modeDiv) + import gc + gc.collect() + timestamp = 1167606999.0 + ticktime = timestamp-(1000 // modeDiv) + for i, ip in self._ip_range(30000 // modeDiv): + t = FailTicket(ip, ticktime, list({'match': i})) + ticktime += 1 + self.__failManager.addFailure(t) + if i % (500 // modeDiv) == 0: + self.__failManager.cleanup(timestamp) + if i % (888 // modeDiv) == 0: + timestamp += (1000 // modeDiv) + ticktime = timestamp-(1000 // modeDiv) + self.assertFalse(gc.collect()) + self.__failManager.cleanup(timestamp) + ticktime -= timestamp - self.__failManager.getMaxTime() + 1 + self.assertEqual(self.__failManager.size(), ticktime if ticktime > 0 else 0) + self.assertFalse(gc.collect()) + + def testFailuresMemLeak2(self): + # use factor (divisor) instead unittest.F2B.SkipIfFast() : + modeDiv = 1 + if unittest.F2B.fast: # pragma: no cover + modeDiv = 10 + self.__failManager.setMaxTime(self.__failManager.getMaxTime() // modeDiv) + import gc + gc.collect() + timestamp = 1167606999.0 + ticktime = timestamp-(1000 // modeDiv) + for i, ip in self._ip_range(10000 // modeDiv): + for j in range(0, 5): + t = FailTicket(ip, ticktime, list({'match': i})) + ticktime += 1 + self.__failManager.addFailure(t) + if i % (500 // modeDiv) == 0: + self.__failManager.cleanup(timestamp) + if i % (888 // modeDiv) == 0 or i % (1500 // modeDiv) == 0: + timestamp += (1000 // modeDiv) + ticktime = timestamp-(1000 // modeDiv) + self.assertFalse(gc.collect()) + timestamp += 20000 + self.__failManager.cleanup(timestamp) + self.assertEqual(self.__failManager.size(), 0) + self.assertFalse(gc.collect()) + diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index eb4e6e2e..c33c2b19 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -176,6 +176,7 @@ def gatherTests(regexps=None, opts=None): tests.addTest(unittest.makeSuite(tickettestcase.TicketTests)) # FailManager tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure)) + tests.addTest(unittest.makeSuite(failmanagertestcase.FailmanagerComplex)) # BanManager tests.addTest(unittest.makeSuite(banmanagertestcase.AddFailure)) try: From 6406f6f5600deaf1d19f793cef8e30a1f55bf43f Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 23 Nov 2015 12:48:07 +0100 Subject: [PATCH 021/623] background servicing (temporally executed from failmanager): prevents memory leak on some platforms/python versions, using forced GC in periodic intervals (latency and threshold); Side effect: GC is disabled now inside fail2ban-server (to avoid multiple garbage collect) --- fail2ban/helpers.py | 58 +++++++++++++++++++++++++-- fail2ban/server/failmanager.py | 9 ++++- fail2ban/tests/failmanagertestcase.py | 23 +++++++++++ 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/fail2ban/helpers.py b/fail2ban/helpers.py index 8e1b0e32..b5ffe601 100644 --- a/fail2ban/helpers.py +++ b/fail2ban/helpers.py @@ -20,11 +20,16 @@ __author__ = "Cyril Jaquier, Arturo 'Buanzo' Busleiman, Yaroslav Halchenko" __license__ = "GPL" -import sys -import os -import traceback -import re +import gc import logging +import os +import re +import sys +import traceback + +from threading import Lock + +from .server.mytime import MyTime def formatExceptionInfo(): @@ -137,3 +142,48 @@ def splitcommaspace(s): if not s: return [] return filter(bool, re.split('[ ,]', s)) + + +class BgService(object): + """Background servicing + + Prevents memory leak on some platforms/python versions, + using forced GC in periodical intervals. + """ + + _mutex = Lock() + _instance = None + def __new__(cls): + if not cls._instance: + cls._instance = \ + super(BgService, cls).__new__(cls) + return cls._instance + + def __init__(self): + self.__serviceTime = -0x7fffffff + self.__periodTime = 30 + self.__threshold = 100; + self.__count = self.__threshold; + if hasattr(gc, 'set_threshold'): + gc.set_threshold(0) + gc.disable() + + def service(self, force=False, wait=False): + self.__count -= 1 + # avoid locking if next service time don't reached + if not force and (self.__count > 0 or MyTime.time() < self.__serviceTime): + return False + # return immediately if mutex already locked (other thread in servicing): + if not BgService._mutex.acquire(wait): + return False + try: + # check again in lock: + if MyTime.time() < self.__serviceTime: + return False + gc.collect() + self.__serviceTime = MyTime.time() + self.__periodTime + self.__count = self.__threshold + return True + finally: + BgService._mutex.release() + return False diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index 07be0a4c..45f3a393 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -28,7 +28,7 @@ from threading import Lock import logging from .ticket import FailTicket -from ..helpers import getLogger +from ..helpers import getLogger, BgService # Gets the instance of the logger. logSys = getLogger(__name__) @@ -43,6 +43,7 @@ class FailManager: self.__maxRetry = 3 self.__maxTime = 600 self.__failTotal = 0 + self.__bgSvc = BgService() def setFailTotal(self, value): with self.__lock: @@ -102,6 +103,8 @@ class FailManager: for k,v in self.__failList.iteritems()]) logSys.log(logLevel, "Total # of detected failures: %d. Current failures from %d IPs (IP:count): %s" % (self.__failTotal, len(self.__failList), failures_summary)) + + self.__bgSvc.service() return attempts def size(self): @@ -126,6 +129,7 @@ class FailManager: # create new dictionary without items to be deleted: self.__failList = dict((ip,item) for ip,item in self.__failList.iteritems() \ if item.getLastTime() + self.__maxTime > time) + self.__bgSvc.service() def delFailure(self, ip): with self.__lock: @@ -141,7 +145,8 @@ class FailManager: if data.getRetry() >= self.__maxRetry: del self.__failList[ip] return data - raise FailManagerEmpty + self.__bgSvc.service() + raise FailManagerEmpty class FailManagerEmpty(Exception): diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index 2d4ce430..78f7b509 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -122,6 +122,29 @@ class AddFailure(unittest.TestCase): self.assertNotEqual(ticket.getIP(), "100.100.10.10") self.assertRaises(FailManagerEmpty, self.__failManager.toBan) + def testBgService(self): + bgSvc = self.__failManager._FailManager__bgSvc + failManager2nd = FailManager() + # test singleton (same object): + bgSvc2 = failManager2nd._FailManager__bgSvc + self.assertTrue(id(bgSvc) == id(bgSvc2)) + bgSvc2 = None + # test service : + self.assertTrue(bgSvc.service(True, True)) + self.assertFalse(bgSvc.service()) + # bypass threshold and time: + for i in range(1, bgSvc._BgService__threshold): + self.assertFalse(bgSvc.service()) + # bypass time check: + bgSvc._BgService__serviceTime = -0x7fffffff + self.assertTrue(bgSvc.service()) + # bypass threshold and time: + bgSvc._BgService__serviceTime = -0x7fffffff + for i in range(1, bgSvc._BgService__threshold): + self.assertFalse(bgSvc.service()) + self.assertTrue(bgSvc.service(False, True)) + self.assertFalse(bgSvc.service(False, True)) + class FailmanagerComplex(unittest.TestCase): From 935d79eaaedef532230ab72a8256be713b27de1f Mon Sep 17 00:00:00 2001 From: sebres Date: Sun, 29 Nov 2015 01:04:55 +0100 Subject: [PATCH 022/623] 1) prevents a bug by logging stdout/stderr if retcode still None: ``` in executeCmd if retcode < 0: TypeError: unorderable types: NoneType() < int() ``` 2) prevents a rarely test case bug of testExecuteTimeoutWithNastyChildren, because no stdout (Resource temporarily unavailable), possible no flush by IO of the killing process; --- fail2ban/server/utils.py | 4 ++-- fail2ban/tests/actiontestcase.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fail2ban/server/utils.py b/fail2ban/server/utils.py index 262b303d..45d1c09d 100644 --- a/fail2ban/server/utils.py +++ b/fail2ban/server/utils.py @@ -159,7 +159,7 @@ class Utils(): # if was timeouted (killed/terminated) - to prevent waiting, set std handles to non-blocking mode. if popen.stdout: try: - if retcode < 0: + if retcode is None or retcode < 0: Utils.setFBlockMode(popen.stdout, False) stdout = popen.stdout.read() except IOError as e: @@ -169,7 +169,7 @@ class Utils(): popen.stdout.close() if popen.stderr: try: - if retcode < 0: + if retcode is None or retcode < 0: Utils.setFBlockMode(popen.stderr, False) stderr = popen.stderr.read() except IOError as e: diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 8c9b5ef1..6d8fcc82 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -247,7 +247,7 @@ class CommandActionTest(LogCaptureTestCase): cpid = getnastypid() # Verify that the process itself got killed self.assertTrue(Utils.wait_for(lambda: not pid_exists(cpid), 3)) # process should have been killed - self.assertLogged('my pid ') + self.assertLogged('my pid ', 'Resource temporarily unavailable') self.assertLogged('timed out') self.assertLogged('killed with SIGTERM', 'killed with SIGKILL') @@ -261,7 +261,7 @@ class CommandActionTest(LogCaptureTestCase): cpid = getnastypid() # Verify that the process itself got killed self.assertTrue(Utils.wait_for(lambda: not pid_exists(cpid), 3)) - self.assertLogged('my pid ') + self.assertLogged('my pid ', 'Resource temporarily unavailable') self.assertLogged('timed out') self.assertLogged('killed with SIGTERM', 'killed with SIGKILL') From e065941ac52c2566595d9dfa29f07e5fa419cf1c Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 22 Dec 2015 21:35:37 +0100 Subject: [PATCH 023/623] use "maxEntries" (currently 50 as default) as range for max number of the last matches/failures, fail2ban will hold per IP in the list of failures in failmanager resp. in the database; prevents out of memory situation if many IP's makes extremely many failures (or very large files since last fail2ban run); closes gh-1277 todo: parameter `maxentries` should be configurable (jail.conf resp. fail2ban.conf); todo: adjust ban-time-incr branch by merge (table "bips"). --- fail2ban/server/database.py | 12 +++++- fail2ban/server/failmanager.py | 16 +++++-- fail2ban/tests/databasetestcase.py | 31 ++++++++++++++ fail2ban/tests/failmanagertestcase.py | 62 ++++++++++++++++++++++++--- 4 files changed, 111 insertions(+), 10 deletions(-) diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index 3b419ed3..b7fd4d47 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -163,6 +163,7 @@ class Fail2BanDb(object): def __init__(self, filename, purgeAge=24*60*60): + self.maxEntries = 50 try: self._lock = RLock() self._db = sqlite3.connect( @@ -454,7 +455,7 @@ class Fail2BanDb(object): if ip is not None: query += " AND ip=?" queryArgs.append(ip) - query += " ORDER BY ip, timeofban" + query += " ORDER BY ip, timeofban desc" return cur.execute(query, queryArgs) @@ -535,7 +536,14 @@ class Fail2BanDb(object): matches = [] failures = 0 tickdata = {} - matches.extend(data.get('matches', ())) + m = data.get('matches', []) + # pre-insert "maxadd" enries (because tickets are ordered desc by time) + maxadd = self.maxEntries - len(matches) + if maxadd > 0: + if len(m) <= maxadd: + matches = m + matches + else: + matches = m[-maxadd:] + matches failures += data.get('failures', 1) tickdata.update(data.get('data', {})) prev_timeofban = timeofban diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index 45f3a393..ae97b36a 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -43,6 +43,7 @@ class FailManager: self.__maxRetry = 3 self.__maxTime = 600 self.__failTotal = 0 + self.maxEntries = 50 self.__bgSvc = BgService() def setFailTotal(self, value): @@ -71,17 +72,26 @@ class FailManager: ip = ticket.getIP() try: fData = self.__failList[ip] - # if the same object: + # if the same object - the same matches but +1 attempt: if fData is ticket: matches = None + attempt = 1 else: + # will be incremented / extended (be sure we have at least +1 attempt): matches = ticket.getMatches() + attempt = ticket.getAttempt() + if attempt <= 0: + attempt += 1 unixTime = ticket.getTime() + fData.setLastTime(unixTime) if fData.getLastReset() < unixTime - self.__maxTime: fData.setLastReset(unixTime) fData.setRetry(0) - fData.inc(matches, 1, count) - fData.setLastTime(unixTime) + fData.inc(matches, attempt, count) + # truncate to maxEntries: + matches = fData.getMatches() + if len(matches) > self.maxEntries: + fData.setMatches(matches[-self.maxEntries:]) except KeyError: # if already FailTicket - add it direct, otherwise create (using copy all ticket data): if isinstance(ticket, FailTicket): diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index 6b85af56..5d83710f 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -251,6 +251,37 @@ class DatabaseTest(LogCaptureTestCase): # be returned self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=-1)), 2) + def testGetBansMerged_MaxEntries(self): + if Fail2BanDb is None: # pragma: no cover + return + self.testAddJail() + maxEntries = 2 + failures = ["abc\n", "123\n", "ABC\n", "1234\n"] + # add failures sequential: + i = 80 + for f in failures: + i -= 10 + ticket = FailTicket("127.0.0.1", MyTime.time() - i, [f]) + ticket.setAttempt(1) + self.db.addBan(self.jail, ticket) + # should retrieve 2 matches only, but count of all attempts: + self.db.maxEntries = maxEntries; + ticket = self.db.getBansMerged("127.0.0.1") + self.assertEqual(ticket.getIP(), "127.0.0.1") + self.assertEqual(ticket.getAttempt(), len(failures)) + self.assertEqual(len(ticket.getMatches()), maxEntries) + self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:]) + # add more failures at once: + ticket = FailTicket("127.0.0.1", MyTime.time() - 10, failures) + ticket.setAttempt(len(failures)) + self.db.addBan(self.jail, ticket) + # should retrieve 2 matches only, but count of all attempts: + self.db.maxEntries = maxEntries; + ticket = self.db.getBansMerged("127.0.0.1") + self.assertEqual(ticket.getAttempt(), 2 * len(failures)) + self.assertEqual(len(ticket.getMatches()), maxEntries) + self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:]) + def testGetBansMerged(self): if Fail2BanDb is None: # pragma: no cover return diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index 78f7b509..3cac0540 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -35,6 +35,13 @@ class AddFailure(unittest.TestCase): def setUp(self): """Call before every test case.""" + self.__items = None + self.__failManager = FailManager() + + def tearDown(self): + """Call after every test case.""" + + def _addDefItems(self): self.__items = [[u'193.168.0.128', 1167605999.0], [u'193.168.0.128', 1167605999.0], [u'193.168.0.128', 1167605999.0], @@ -48,44 +55,87 @@ class AddFailure(unittest.TestCase): ['100.100.10.10', 1000001000.0], ['100.100.10.10', 1000001500.0], ['100.100.10.10', 1000002000.0]] - - self.__failManager = FailManager() for i in self.__items: self.__failManager.addFailure(FailTicket(i[0], i[1])) - def tearDown(self): - """Call after every test case.""" - def testFailManagerAdd(self): + self._addDefItems() self.assertEqual(self.__failManager.size(), 3) self.assertEqual(self.__failManager.getFailTotal(), 13) self.__failManager.setFailTotal(0) self.assertEqual(self.__failManager.getFailTotal(), 0) self.__failManager.setFailTotal(13) + def testFailManagerAdd_MaxEntries(self): + maxEntries = 2 + self.__failManager.maxEntries = maxEntries + failures = ["abc\n", "123\n", "ABC\n", "1234\n"] + # add failures sequential: + i = 80 + for f in failures: + i -= 10 + ticket = FailTicket("127.0.0.1", 1000002000 - i, [f]) + ticket.setAttempt(1) + self.__failManager.addFailure(ticket) + # + manFailList = self.__failManager._FailManager__failList + self.assertEqual(len(manFailList), 1) + ticket = manFailList["127.0.0.1"] + # should retrieve 2 matches only, but count of all attempts (4): + self.assertEqual(ticket.getAttempt(), len(failures)) + self.assertEqual(len(ticket.getMatches()), maxEntries) + self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:]) + # add more failures at once: + ticket = FailTicket("127.0.0.1", 1000002000 - 10, failures) + ticket.setAttempt(len(failures)) + self.__failManager.addFailure(ticket) + # + manFailList = self.__failManager._FailManager__failList + self.assertEqual(len(manFailList), 1) + ticket = manFailList["127.0.0.1"] + # should retrieve 2 matches only, but count of all attempts (8): + self.assertEqual(ticket.getAttempt(), 2 * len(failures)) + self.assertEqual(len(ticket.getMatches()), maxEntries) + self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:]) + # add self ticket again: + self.__failManager.addFailure(ticket) + # + manFailList = self.__failManager._FailManager__failList + self.assertEqual(len(manFailList), 1) + ticket = manFailList["127.0.0.1"] + # same matches, but +1 attempt (9) + self.assertEqual(ticket.getAttempt(), 2 * len(failures) + 1) + self.assertEqual(len(ticket.getMatches()), maxEntries) + self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:]) + def testFailManagerMaxTime(self): + self._addDefItems() self.assertEqual(self.__failManager.getMaxTime(), 600) self.__failManager.setMaxTime(13) self.assertEqual(self.__failManager.getMaxTime(), 13) self.__failManager.setMaxTime(600) def testDel(self): + self._addDefItems() self.__failManager.delFailure('193.168.0.128') self.__failManager.delFailure('111.111.1.111') self.assertEqual(self.__failManager.size(), 2) def testCleanupOK(self): + self._addDefItems() timestamp = 1167606999.0 self.__failManager.cleanup(timestamp) self.assertEqual(self.__failManager.size(), 0) def testCleanupNOK(self): + self._addDefItems() timestamp = 1167605990.0 self.__failManager.cleanup(timestamp) self.assertEqual(self.__failManager.size(), 2) def testbanOK(self): + self._addDefItems() self.__failManager.setMaxRetry(5) #ticket = FailTicket('193.168.0.128', None) ticket = self.__failManager.toBan() @@ -112,10 +162,12 @@ class AddFailure(unittest.TestCase): 'FailTicket: ip=193.168.0.128 time=1000002000.0 #attempts=5 matches=[]') def testbanNOK(self): + self._addDefItems() self.__failManager.setMaxRetry(10) self.assertRaises(FailManagerEmpty, self.__failManager.toBan) def testWindow(self): + self._addDefItems() ticket = self.__failManager.toBan() self.assertNotEqual(ticket.getIP(), "100.100.10.10") ticket = self.__failManager.toBan() From 3f2b58e9730e0c743fd0db2eeacd34585b7fa36b Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 23 Dec 2015 18:49:30 +0100 Subject: [PATCH 024/623] datedetector: matchTime, template list etc. rewritten because of performance degradation (without sorting templates now) - in-place reordering using hits and last used time; todo: rewrite "reGroupDictStrptime" because may be too slow; --- fail2ban/client/fail2banregex.py | 4 +- fail2ban/server/datedetector.py | 65 ++++++++++++++++---------- fail2ban/server/datetemplate.py | 1 + fail2ban/server/filtergamin.py | 1 - fail2ban/server/filterpoll.py | 1 - fail2ban/server/filterpyinotify.py | 1 - fail2ban/tests/datedetectortestcase.py | 9 ---- fail2ban/tests/filtertestcase.py | 35 +++++++++++++- 8 files changed, 75 insertions(+), 42 deletions(-) diff --git a/fail2ban/client/fail2banregex.py b/fail2ban/client/fail2banregex.py index d0cdab84..1fc5316e 100755 --- a/fail2ban/client/fail2banregex.py +++ b/fail2ban/client/fail2banregex.py @@ -366,7 +366,7 @@ class Fail2banRegex(object): def process(self, test_lines): t0 = time.time() - for line_no, line in enumerate(test_lines): + for line in test_lines: if isinstance(line, tuple): line_datetimestripped, ret = self.testRegex( line[0], line[1]) @@ -398,8 +398,6 @@ class Fail2banRegex(object): self._line_stats.missed_lines_timeextracted.append(line_datetimestripped) self._line_stats.tested += 1 - if line_no % 10 == 0 and self._filter.dateDetector is not None: - self._filter.dateDetector.sortTemplate() self._time_elapsed = time.time() - t0 def printLines(self, ltype): diff --git a/fail2ban/server/datedetector.py b/fail2ban/server/datedetector.py index 4bdcefd2..4004d5c5 100644 --- a/fail2ban/server/datedetector.py +++ b/fail2ban/server/datedetector.py @@ -21,6 +21,8 @@ __author__ = "Cyril Jaquier and Fail2Ban Contributors" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" +import time + from threading import Lock from .datetemplate import DatePatternRegex, DateTai64n, DateEpoch @@ -44,6 +46,8 @@ class DateDetector(object): self.__lock = Lock() self.__templates = list() self.__known_names = set() + # time the template was long unused (currently 300 == 5m): + self.__unusedTime = 300 def _appendTemplate(self, template): name = template.name @@ -153,22 +157,29 @@ class DateDetector(object): The regex match returned from the first successfully matched template. """ - self.__lock.acquire() - try: + i = 0 + with self.__lock: for template in self.__templates: match = template.matchDate(line) if not match is None: if logSys.getEffectiveLevel() <= logLevel: logSys.log(logLevel, "Matched time template %s", template.name) template.hits += 1 + template.lastUsed = time.time() + # if not first - try to reorder current template (bubble up), they will be not sorted anymore: + if i: + self._reorderTemplate(i) + # return tuple with match and template reference used for parsing: return (match, template) - return (None, None) - finally: - self.__lock.release() + i += 1 + # not found: + return (None, None) def getTime(self, line): """Attempts to return the date on a log line using templates. + Obsolete: Use "getTime2" instead. + This uses the templates' `getDate` method in an attempt to find a date. @@ -183,8 +194,7 @@ class DateDetector(object): The Unix timestamp returned from the first successfully matched template or None if not found. """ - self.__lock.acquire() - try: + with self.__lock: for template in self.__templates: try: date = template.getDate(line) @@ -197,8 +207,6 @@ class DateDetector(object): except ValueError: # pragma: no cover pass return None - finally: - self.__lock.release() def getTime2(self, line, timeMatch = None): """Attempts to return the date on a log line using given template. @@ -232,21 +240,28 @@ class DateDetector(object): return date return self.getTime(line) - def sortTemplate(self): - """Sort the date templates by number of hits + def _reorderTemplate(self, num): + """Reorder template (bubble up) in template list if hits grows enough. - Sort the template lists using the hits score. This method is not - called in this object and thus should be called from time to time. - This ensures the most commonly matched templates are checked first, - improving performance of matchTime and getTime. + Parameters + ---------- + num : int + Index of template should be moved. """ - self.__lock.acquire() - try: - if logSys.getEffectiveLevel() <= logLevel: - logSys.log(logLevel, "Sorting the template list") - self.__templates.sort(key=lambda x: x.hits, reverse=True) - t = self.__templates[0] - if logSys.getEffectiveLevel() <= logLevel: - logSys.log(logLevel, "Winning template: %s with %d hits", t.name, t.hits) - finally: - self.__lock.release() + if num: + templates = self.__templates + template = templates[num] + ## current hits and time the template was long unused: + untime = template.lastUsed - self.__unusedTime + hits = template.hits + ## don't move too often (multiline logs resp. log's with different date patterns), + ## if template not used too long, replace it also : + if hits > templates[num-1].hits + 5 or templates[num-1].lastUsed < untime: + ## try to move faster (half of part to current template): + pos = num // 2 + ## if not larger - move slow (exact 1 position): + if hits <= templates[pos].hits or templates[pos].lastUsed < untime: + pos = num-1 + templates[pos], templates[num] = template, templates[pos] + + diff --git a/fail2ban/server/datetemplate.py b/fail2ban/server/datetemplate.py index e1f6b3de..88e2ae3f 100644 --- a/fail2ban/server/datetemplate.py +++ b/fail2ban/server/datetemplate.py @@ -50,6 +50,7 @@ class DateTemplate(object): self._regex = "" self._cRegex = None self.hits = 0 + self.lastUsed = 0 @property def name(self): diff --git a/fail2ban/server/filtergamin.py b/fail2ban/server/filtergamin.py index b094cfeb..6b562a5c 100644 --- a/fail2ban/server/filtergamin.py +++ b/fail2ban/server/filtergamin.py @@ -84,7 +84,6 @@ class FilterGamin(FileFilter): self.jail.putFailTicket(ticket) except FailManagerEmpty: self.failManager.cleanup(MyTime.time()) - self.dateDetector.sortTemplate() self.__modified = False ## diff --git a/fail2ban/server/filterpoll.py b/fail2ban/server/filterpoll.py index d7ff9075..c7b04970 100644 --- a/fail2ban/server/filterpoll.py +++ b/fail2ban/server/filterpoll.py @@ -118,7 +118,6 @@ class FilterPoll(FileFilter): self.jail.putFailTicket(ticket) except FailManagerEmpty: self.failManager.cleanup(MyTime.time()) - self.dateDetector.sortTemplate() self.__modified = False logSys.debug( (self.jail is not None and self.jail.name or "jailless") + diff --git a/fail2ban/server/filterpyinotify.py b/fail2ban/server/filterpyinotify.py index 100ad233..7bd638d9 100644 --- a/fail2ban/server/filterpyinotify.py +++ b/fail2ban/server/filterpyinotify.py @@ -108,7 +108,6 @@ class FilterPyinotify(FileFilter): self.jail.putFailTicket(ticket) except FailManagerEmpty: self.failManager.cleanup(MyTime.time()) - self.dateDetector.sortTemplate() self.__modified = False def _addFileWatcher(self, path): diff --git a/fail2ban/tests/datedetectortestcase.py b/fail2ban/tests/datedetectortestcase.py index 9a7d3e78..ab7310ba 100644 --- a/fail2ban/tests/datedetectortestcase.py +++ b/fail2ban/tests/datedetectortestcase.py @@ -143,13 +143,6 @@ class DateDetectorTest(LogCaptureTestCase): else: self.assertEqual(logtime, None, "getTime should have not matched for %r Got: %s" % (sdate, logtime)) - def testStableSortTemplate(self): - old_names = [x.name for x in self.__datedetector.templates] - self.__datedetector.sortTemplate() - # If there were no hits -- sorting should not change the order - for old_name, n in zip(old_names, self.__datedetector.templates): - self.assertEqual(old_name, n.name) # "Sort must be stable" - def testAllUniqueTemplateNames(self): self.assertRaises(ValueError, self.__datedetector.appendTemplate, self.__datedetector.templates[0]) @@ -164,13 +157,11 @@ class DateDetectorTest(LogCaptureTestCase): ( logTime, logMatch ) = logdate self.assertEqual(logTime, mu) self.assertEqual(logMatch.group(), '2012/10/11 02:37:17') - self.__datedetector.sortTemplate() # confuse it with year being at the end for i in xrange(10): ( logTime, logMatch ) = self.__datedetector.getTime('11/10/2012 02:37:17 [error] 18434#0') self.assertEqual(logTime, mu) self.assertEqual(logMatch.group(), '11/10/2012 02:37:17') - self.__datedetector.sortTemplate() # and now back to the original ( logTime, logMatch ) = self.__datedetector.getTime('2012/10/11 02:37:17 [error] 18434#0') self.assertEqual(logTime, mu) diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 5edfd267..ae22b88f 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -87,8 +87,31 @@ def _maxWaitTime(wtime): return wtime -def _tm(time): - return datetime.datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M:%S") +class _tmSerial(): + _last_s = -0x7fffffff + _last_m = -0x7fffffff + _str_s = "" + _str_m = "" + @staticmethod + def _tm(time): + # ## strftime it too slow for large time serializer : + # return datetime.datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M:%S") + c = _tmSerial + sec = (time % 60) + if c._last_s == time - sec: + return "%s%02u" % (c._str_s, sec) + mt = (time % 3600) + if c._last_m == time - mt: + c._last_s = time - sec + c._str_s = "%s%02u:" % (c._str_m, mt // 60) + return "%s%02u" % (c._str_s, sec) + c._last_m = time - mt + c._str_m = datetime.datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:") + c._last_s = time - sec + c._str_s = "%s%02u:" % (c._str_m, mt // 60) + return "%s%02u" % (c._str_s, sec) + +_tm = _tmSerial._tm def _assert_equal_entries(utest, found, output, count=None): @@ -245,6 +268,14 @@ class BasicFilter(unittest.TestCase): 1) ) + def testTest_tm(self): + unittest.F2B.SkipIfFast() + ## test function "_tm" works correct (returns the same as slow strftime): + for i in xrange(1417512352, (1417512352 // 3600 + 3) * 3600): + tm = datetime.datetime.fromtimestamp(i).strftime("%Y-%m-%d %H:%M:%S") + if _tm(i) != tm: + self.assertEqual((_tm(i), i), (tm, i)) + class IgnoreIP(LogCaptureTestCase): From da51fbf9c626a206c67e6b9e7dec61eadec2f8ba Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 29 Dec 2015 12:40:26 +0100 Subject: [PATCH 025/623] code review and performance optimization: - default date templates cache + regexp compiling on the fly (if required); - better usage of shared config over all test cases; - obsolete test cases removed (simple test of memleak/gc); - skip some slow test cases in fast mode (setup test, etc.); --- fail2ban/server/datedetector.py | 133 +++++++++++++++---------- fail2ban/server/datetemplate.py | 12 +-- fail2ban/tests/clientreadertestcase.py | 44 ++++---- fail2ban/tests/failmanagertestcase.py | 51 ---------- fail2ban/tests/misctestcase.py | 1 + fail2ban/tests/samplestestcase.py | 3 +- fail2ban/tests/servertestcase.py | 11 +- fail2ban/tests/utils.py | 1 + 8 files changed, 119 insertions(+), 137 deletions(-) diff --git a/fail2ban/server/datedetector.py b/fail2ban/server/datedetector.py index 4004d5c5..ae6d71d9 100644 --- a/fail2ban/server/datedetector.py +++ b/fail2ban/server/datedetector.py @@ -34,6 +34,82 @@ logSys = getLogger(__name__) logLevel = 6 +class DateDetectorCache(object): + def __init__(self): + self.__lock = Lock() + self.__templates = list() + + @property + def templates(self): + """List of template instances managed by the detector. + """ + with self.__lock: + if self.__templates: + return self.__templates + self._addDefaultTemplate() + return self.__templates + + def _cacheTemplate(self, template): + """Cache Fail2Ban's default template. + """ + if isinstance(template, str): + template = DatePatternRegex(template) + self.__templates.append(template) + + def _addDefaultTemplate(self): + """Add resp. cache Fail2Ban's default set of date templates. + """ + # asctime with optional day, subsecond and/or year: + # Sun Jan 23 21:59:59.011 2005 + self._cacheTemplate("(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %Y)?") + # asctime with optional day, subsecond and/or year coming after day + # http://bugs.debian.org/798923 + # Sun Jan 23 2005 21:59:59.011 + self._cacheTemplate("(?:%a )?%b %d %Y %H:%M:%S(?:\.%f)?") + # simple date, optional subsecond (proftpd): + # 2005-01-23 21:59:59 + # simple date: 2005/01/23 21:59:59 + # custom for syslog-ng 2006.12.21 06:43:20 + self._cacheTemplate("%Y(?P<_sep>[-/.])%m(?P=_sep)%d %H:%M:%S(?:,%f)?") + # simple date too (from x11vnc): 23/01/2005 21:59:59 + # and with optional year given by 2 digits: 23/01/05 21:59:59 + # (See http://bugs.debian.org/537610) + # 17-07-2008 17:23:25 + self._cacheTemplate("%d(?P<_sep>[-/])%m(?P=_sep)(?:%Y|%y) %H:%M:%S") + # Apache format optional time zone: + # [31/Oct/2006:09:22:55 -0000] + # 26-Jul-2007 15:20:52 + self._cacheTemplate("%d(?P<_sep>[-/])%b(?P=_sep)%Y[ :]?%H:%M:%S(?:\.%f)?(?: %z)?") + # CPanel 05/20/2008:01:57:39 + self._cacheTemplate("%m/%d/%Y:%H:%M:%S") + # named 26-Jul-2007 15:20:52.252 + # roundcube 26-Jul-2007 15:20:52 +0200 + # 01-27-2012 16:22:44.252 + # subseconds explicit to avoid possible %m<->%d confusion + # with previous + self._cacheTemplate("%m-%d-%Y %H:%M:%S\.%f") + # TAI64N + template = DateTai64n() + template.name = "TAI64N" + self._cacheTemplate(template) + # Epoch + template = DateEpoch() + template.name = "Epoch" + self._cacheTemplate(template) + # ISO 8601 + self._cacheTemplate("%Y-%m-%d[T ]%H:%M:%S(?:\.%f)?(?:%z)?") + # Only time information in the log + self._cacheTemplate("^%H:%M:%S") + # <09/16/08@05:03:30> + self._cacheTemplate("^<%m/%d/%y@%H:%M:%S>") + # MySQL: 130322 11:46:11 + self._cacheTemplate("^%y%m%d ?%H:%M:%S") + # Apache Tomcat + self._cacheTemplate("%b %d, %Y %I:%M:%S %p") + # ASSP: Apr-27-13 02:33:06 + self._cacheTemplate("^%b-%d-%y %H:%M:%S") + + class DateDetector(object): """Manages one or more date templates to find a date within a log line. @@ -41,6 +117,7 @@ class DateDetector(object): ---------- templates """ + _defCache = DateDetectorCache() def __init__(self): self.__lock = Lock() @@ -79,59 +156,9 @@ class DateDetector(object): def addDefaultTemplate(self): """Add Fail2Ban's default set of date templates. """ - self.__lock.acquire() - try: - # asctime with optional day, subsecond and/or year: - # Sun Jan 23 21:59:59.011 2005 - self.appendTemplate("(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %Y)?") - # asctime with optional day, subsecond and/or year coming after day - # http://bugs.debian.org/798923 - # Sun Jan 23 2005 21:59:59.011 - self.appendTemplate("(?:%a )?%b %d %Y %H:%M:%S(?:\.%f)?") - # simple date, optional subsecond (proftpd): - # 2005-01-23 21:59:59 - # simple date: 2005/01/23 21:59:59 - # custom for syslog-ng 2006.12.21 06:43:20 - self.appendTemplate("%Y(?P<_sep>[-/.])%m(?P=_sep)%d %H:%M:%S(?:,%f)?") - # simple date too (from x11vnc): 23/01/2005 21:59:59 - # and with optional year given by 2 digits: 23/01/05 21:59:59 - # (See http://bugs.debian.org/537610) - # 17-07-2008 17:23:25 - self.appendTemplate("%d(?P<_sep>[-/])%m(?P=_sep)(?:%Y|%y) %H:%M:%S") - # Apache format optional time zone: - # [31/Oct/2006:09:22:55 -0000] - # 26-Jul-2007 15:20:52 - self.appendTemplate("%d(?P<_sep>[-/])%b(?P=_sep)%Y[ :]?%H:%M:%S(?:\.%f)?(?: %z)?") - # CPanel 05/20/2008:01:57:39 - self.appendTemplate("%m/%d/%Y:%H:%M:%S") - # named 26-Jul-2007 15:20:52.252 - # roundcube 26-Jul-2007 15:20:52 +0200 - # 01-27-2012 16:22:44.252 - # subseconds explicit to avoid possible %m<->%d confusion - # with previous - self.appendTemplate("%m-%d-%Y %H:%M:%S\.%f") - # TAI64N - template = DateTai64n() - template.name = "TAI64N" - self.appendTemplate(template) - # Epoch - template = DateEpoch() - template.name = "Epoch" - self.appendTemplate(template) - # ISO 8601 - self.appendTemplate("%Y-%m-%d[T ]%H:%M:%S(?:\.%f)?(?:%z)?") - # Only time information in the log - self.appendTemplate("^%H:%M:%S") - # <09/16/08@05:03:30> - self.appendTemplate("^<%m/%d/%y@%H:%M:%S>") - # MySQL: 130322 11:46:11 - self.appendTemplate("^%y%m%d ?%H:%M:%S") - # Apache Tomcat - self.appendTemplate("%b %d, %Y %I:%M:%S %p") - # ASSP: Apr-27-13 02:33:06 - self.appendTemplate("^%b-%d-%y %H:%M:%S") - finally: - self.__lock.release() + with self.__lock: + for template in DateDetector._defCache.templates: + self._appendTemplate(template) @property def templates(self): diff --git a/fail2ban/server/datetemplate.py b/fail2ban/server/datetemplate.py index 88e2ae3f..746380ed 100644 --- a/fail2ban/server/datetemplate.py +++ b/fail2ban/server/datetemplate.py @@ -86,7 +86,6 @@ class DateTemplate(object): if (wordBegin and not re.search(r'^\^', regex)): regex = r'\b' + regex self._regex = regex - self._cRegex = re.compile(regex, re.UNICODE | re.IGNORECASE) regex = property(getRegex, setRegex, doc= """Regex used to search for date. @@ -95,6 +94,8 @@ class DateTemplate(object): def matchDate(self, line): """Check if regex for date matches on a log line. """ + if not self._cRegex: + self._cRegex = re.compile(self.regex, re.UNICODE | re.IGNORECASE) dateMatch = self._cRegex.search(line) return dateMatch @@ -171,7 +172,7 @@ class DatePatternRegex(DateTemplate): regex pattern """ - _patternRE = r"%%(%%|[%s])" % "".join(timeRE.keys()) + _patternRE = re.compile(r"%%(%%|[%s])" % "".join(timeRE.keys())) _patternName = { 'a': "DAY", 'A': "DAYNAME", 'b': "MON", 'B': "MONTH", 'd': "Day", 'H': "24hour", 'I': "12hour", 'j': "Yearday", 'm': "Month", @@ -202,10 +203,9 @@ class DatePatternRegex(DateTemplate): @pattern.setter def pattern(self, pattern): self._pattern = pattern - self._name = re.sub( - self._patternRE, r'%(\1)s', pattern) % self._patternName - super(DatePatternRegex, self).setRegex( - re.sub(self._patternRE, r'%(\1)s', pattern) % timeRE) + fmt = self._patternRE.sub(r'%(\1)s', pattern) + self._name = fmt % self._patternName + super(DatePatternRegex, self).setRegex(fmt % timeRE) def setRegex(self, value): raise NotImplementedError("Regex derived from pattern") diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index d0c37a3e..78c9a582 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -38,12 +38,15 @@ from ..client.configurator import Configurator from .utils import LogCaptureTestCase TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") +TEST_FILES_DIR_SHARE_CFG = {} from .utils import CONFIG_DIR +CONFIG_DIR_SHARE_CFG = unittest.F2B.share_config STOCK = os.path.exists(os.path.join('config','fail2ban.conf')) IMPERFECT_CONFIG = os.path.join(os.path.dirname(__file__), 'config') +IMPERFECT_CONFIG_SHARE_CFG = {} class ConfigReaderTest(unittest.TestCase): @@ -162,14 +165,13 @@ class JailReaderTest(LogCaptureTestCase): def __init__(self, *args, **kwargs): super(JailReaderTest, self).__init__(*args, **kwargs) - self.__share_cfg = {} def testIncorrectJail(self): - jail = JailReader('XXXABSENTXXX', basedir=CONFIG_DIR, share_config=self.__share_cfg) + jail = JailReader('XXXABSENTXXX', basedir=CONFIG_DIR, share_config=CONFIG_DIR_SHARE_CFG) self.assertRaises(ValueError, jail.read) def testJailActionEmpty(self): - jail = JailReader('emptyaction', basedir=IMPERFECT_CONFIG, share_config=self.__share_cfg) + jail = JailReader('emptyaction', basedir=IMPERFECT_CONFIG, share_config=IMPERFECT_CONFIG_SHARE_CFG) self.assertTrue(jail.read()) self.assertTrue(jail.getOptions()) self.assertTrue(jail.isEnabled()) @@ -177,7 +179,7 @@ class JailReaderTest(LogCaptureTestCase): self.assertLogged('No actions were defined for emptyaction') def testJailActionFilterMissing(self): - jail = JailReader('missingbitsjail', basedir=IMPERFECT_CONFIG, share_config=self.__share_cfg) + jail = JailReader('missingbitsjail', basedir=IMPERFECT_CONFIG, share_config=IMPERFECT_CONFIG_SHARE_CFG) self.assertTrue(jail.read()) self.assertFalse(jail.getOptions()) self.assertTrue(jail.isEnabled()) @@ -186,7 +188,7 @@ class JailReaderTest(LogCaptureTestCase): def testJailActionBrokenDef(self): jail = JailReader('brokenactiondef', basedir=IMPERFECT_CONFIG, - share_config=self.__share_cfg) + share_config=IMPERFECT_CONFIG_SHARE_CFG) self.assertTrue(jail.read()) self.assertFalse(jail.getOptions()) self.assertTrue(jail.isEnabled()) @@ -200,7 +202,7 @@ class JailReaderTest(LogCaptureTestCase): if STOCK: def testStockSSHJail(self): - jail = JailReader('sshd', basedir=CONFIG_DIR, share_config=self.__share_cfg) # we are running tests from root project dir atm + jail = JailReader('sshd', basedir=CONFIG_DIR, share_config=CONFIG_DIR_SHARE_CFG) # we are running tests from root project dir atm self.assertTrue(jail.read()) self.assertTrue(jail.getOptions()) self.assertFalse(jail.isEnabled()) @@ -316,7 +318,7 @@ class FilterReaderTest(unittest.TestCase): self.assertEqual(sorted(filterReader.convert()), sorted(output)) filterReader = FilterReader("testcase01", "testcase01", {'maxlines': "5"}, - share_config=self.__share_cfg, basedir=TEST_FILES_DIR) + share_config=TEST_FILES_DIR_SHARE_CFG, basedir=TEST_FILES_DIR) filterReader.read() #filterReader.getOptions(["failregex", "ignoreregex"]) filterReader.getOptions(None) @@ -326,7 +328,7 @@ class FilterReaderTest(unittest.TestCase): def testFilterReaderSubstitionDefault(self): output = [['set', 'jailname', 'addfailregex', 'to=sweet@example.com fromip=']] filterReader = FilterReader('substition', "jailname", {}, - share_config=self.__share_cfg, basedir=TEST_FILES_DIR) + share_config=TEST_FILES_DIR_SHARE_CFG, basedir=TEST_FILES_DIR) filterReader.read() filterReader.getOptions(None) c = filterReader.convert() @@ -335,7 +337,7 @@ class FilterReaderTest(unittest.TestCase): def testFilterReaderSubstitionSet(self): output = [['set', 'jailname', 'addfailregex', 'to=sour@example.com fromip=']] filterReader = FilterReader('substition', "jailname", {'honeypot': 'sour@example.com'}, - share_config=self.__share_cfg, basedir=TEST_FILES_DIR) + share_config=TEST_FILES_DIR_SHARE_CFG, basedir=TEST_FILES_DIR) filterReader.read() filterReader.getOptions(None) c = filterReader.convert() @@ -346,7 +348,7 @@ class FilterReaderTest(unittest.TestCase): filterName, filterOpt = JailReader.extractOptions( 'substition[honeypot=",", sweet="test,,test2"]') filterReader = FilterReader('substition', "jailname", filterOpt, - share_config=self.__share_cfg, basedir=TEST_FILES_DIR) + share_config=TEST_FILES_DIR_SHARE_CFG, basedir=TEST_FILES_DIR) filterReader.read() filterReader.getOptions(None) c = filterReader.convert() @@ -355,13 +357,13 @@ class FilterReaderTest(unittest.TestCase): def testFilterReaderSubstitionFail(self): # directly subst the same var : filterReader = FilterReader('substition', "jailname", {'honeypot': ''}, - share_config=self.__share_cfg, basedir=TEST_FILES_DIR) + share_config=TEST_FILES_DIR_SHARE_CFG, basedir=TEST_FILES_DIR) filterReader.read() filterReader.getOptions(None) self.assertRaises(ValueError, FilterReader.convert, filterReader) # cross subst the same var : filterReader = FilterReader('substition', "jailname", {'honeypot': '', 'sweet': ''}, - share_config=self.__share_cfg, basedir=TEST_FILES_DIR) + share_config=TEST_FILES_DIR_SHARE_CFG, basedir=TEST_FILES_DIR) filterReader.read() filterReader.getOptions(None) self.assertRaises(ValueError, FilterReader.convert, filterReader) @@ -404,6 +406,7 @@ class JailsReaderTestCache(LogCaptureTestCase): return cnt def testTestJailConfCache(self): + unittest.F2B.SkipIfFast() saved_ll = configparserinc.logLevel configparserinc.logLevel = logging.DEBUG basedir = tempfile.mkdtemp("fail2ban_conf") @@ -446,7 +449,6 @@ class JailsReaderTest(LogCaptureTestCase): def __init__(self, *args, **kwargs): super(JailsReaderTest, self).__init__(*args, **kwargs) - self.__share_cfg = {} def testProvidingBadBasedir(self): if not os.path.exists('/XXX'): @@ -454,7 +456,7 @@ class JailsReaderTest(LogCaptureTestCase): self.assertRaises(ValueError, reader.read) def testReadTestJailConf(self): - jails = JailsReader(basedir=IMPERFECT_CONFIG, share_config=self.__share_cfg) + jails = JailsReader(basedir=IMPERFECT_CONFIG, share_config=IMPERFECT_CONFIG_SHARE_CFG) self.assertTrue(jails.read()) self.assertFalse(jails.getOptions()) self.assertRaises(ValueError, jails.convert) @@ -504,7 +506,7 @@ class JailsReaderTest(LogCaptureTestCase): msg="Action file %r is lacking [Init] section" % actionConfig) def testReadStockJailConf(self): - jails = JailsReader(basedir=CONFIG_DIR, share_config=self.__share_cfg) # we are running tests from root project dir atm + jails = JailsReader(basedir=CONFIG_DIR, share_config=CONFIG_DIR_SHARE_CFG) # we are running tests from root project dir atm self.assertTrue(jails.read()) # opens fine self.assertTrue(jails.getOptions()) # reads fine comm_commands = jails.convert() @@ -535,7 +537,7 @@ class JailsReaderTest(LogCaptureTestCase): # moreover we must have a file for it # and it must be readable as a Filter filterReader = FilterReader(filterName, jail, filterOpt, - share_config=self.__share_cfg, basedir=CONFIG_DIR) + share_config=CONFIG_DIR_SHARE_CFG, basedir=CONFIG_DIR) self.assertTrue(filterReader.read(),"Failed to read filter:" + filterName) # opens fine filterReader.getOptions({}) # reads fine @@ -554,8 +556,8 @@ class JailsReaderTest(LogCaptureTestCase): if actName == 'iptables-multiport': self.assertTrue('port' in actOpt) - actionReader = ActionReader( - actName, jail, {}, basedir=CONFIG_DIR) + actionReader = ActionReader(actName, jail, {}, + share_config=CONFIG_DIR_SHARE_CFG, basedir=CONFIG_DIR) self.assertTrue(actionReader.read()) actionReader.getOptions({}) # populate _opts cmds = actionReader.convert() @@ -566,7 +568,7 @@ class JailsReaderTest(LogCaptureTestCase): # Verify that all filters found under config/ have a jail def testReadStockJailFilterComplete(self): - jails = JailsReader(basedir=CONFIG_DIR, force_enable=True, share_config=self.__share_cfg) + jails = JailsReader(basedir=CONFIG_DIR, force_enable=True, share_config=CONFIG_DIR_SHARE_CFG) self.assertTrue(jails.read()) # opens fine self.assertTrue(jails.getOptions()) # reads fine # grab all filter names @@ -586,7 +588,7 @@ class JailsReaderTest(LogCaptureTestCase): def testReadStockJailConfForceEnabled(self): # more of a smoke test to make sure that no obvious surprises # on users' systems when enabling shipped jails - jails = JailsReader(basedir=CONFIG_DIR, force_enable=True, share_config=self.__share_cfg) # we are running tests from root project dir atm + jails = JailsReader(basedir=CONFIG_DIR, force_enable=True, share_config=CONFIG_DIR_SHARE_CFG) # we are running tests from root project dir atm self.assertTrue(jails.read()) # opens fine self.assertTrue(jails.getOptions()) # reads fine comm_commands = jails.convert(allow_no_files=True) @@ -697,7 +699,7 @@ action = testaction1[actname=test1] filter = testfilter1 """) jailfd.close() - jails = JailsReader(basedir=basedir, share_config=self.__share_cfg) + jails = JailsReader(basedir=basedir, share_config={}) self.assertTrue(jails.read()) self.assertTrue(jails.getOptions()) comm_commands = jails.convert(allow_no_files=True) diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index 3cac0540..a8a71723 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -248,55 +248,4 @@ class FailmanagerComplex(unittest.TestCase): self.assertEqual(str(ip), '127.0.255.255') elif i == 65535: self.assertEqual(str(ip), '127.1.0.0') - - def testFailuresMemLeak1(self): - # use factor (divisor) instead unittest.F2B.SkipIfFast() : - modeDiv = 1 - if unittest.F2B.fast: # pragma: no cover - modeDiv = 10 - self.__failManager.setMaxTime(self.__failManager.getMaxTime() // modeDiv) - import gc - gc.collect() - timestamp = 1167606999.0 - ticktime = timestamp-(1000 // modeDiv) - for i, ip in self._ip_range(30000 // modeDiv): - t = FailTicket(ip, ticktime, list({'match': i})) - ticktime += 1 - self.__failManager.addFailure(t) - if i % (500 // modeDiv) == 0: - self.__failManager.cleanup(timestamp) - if i % (888 // modeDiv) == 0: - timestamp += (1000 // modeDiv) - ticktime = timestamp-(1000 // modeDiv) - self.assertFalse(gc.collect()) - self.__failManager.cleanup(timestamp) - ticktime -= timestamp - self.__failManager.getMaxTime() + 1 - self.assertEqual(self.__failManager.size(), ticktime if ticktime > 0 else 0) - self.assertFalse(gc.collect()) - - def testFailuresMemLeak2(self): - # use factor (divisor) instead unittest.F2B.SkipIfFast() : - modeDiv = 1 - if unittest.F2B.fast: # pragma: no cover - modeDiv = 10 - self.__failManager.setMaxTime(self.__failManager.getMaxTime() // modeDiv) - import gc - gc.collect() - timestamp = 1167606999.0 - ticktime = timestamp-(1000 // modeDiv) - for i, ip in self._ip_range(10000 // modeDiv): - for j in range(0, 5): - t = FailTicket(ip, ticktime, list({'match': i})) - ticktime += 1 - self.__failManager.addFailure(t) - if i % (500 // modeDiv) == 0: - self.__failManager.cleanup(timestamp) - if i % (888 // modeDiv) == 0 or i % (1500 // modeDiv) == 0: - timestamp += (1000 // modeDiv) - ticktime = timestamp-(1000 // modeDiv) - self.assertFalse(gc.collect()) - timestamp += 20000 - self.__failManager.cleanup(timestamp) - self.assertEqual(self.__failManager.size(), 0) - self.assertFalse(gc.collect()) diff --git a/fail2ban/tests/misctestcase.py b/fail2ban/tests/misctestcase.py index c4dd716d..c9eddca1 100644 --- a/fail2ban/tests/misctestcase.py +++ b/fail2ban/tests/misctestcase.py @@ -80,6 +80,7 @@ def _getSysPythonVersion(): class SetupTest(unittest.TestCase): def setUp(self): + unittest.F2B.SkipIfFast() setup = os.path.join(os.path.dirname(__file__), '..', '..', 'setup.py') self.setup = os.path.exists(setup) and setup or None if not self.setup and sys.version_info >= (2,7): # pragma: no cover - running not out of the source diff --git a/fail2ban/tests/samplestestcase.py b/fail2ban/tests/samplestestcase.py index 9e6c0ee7..2ed77554 100644 --- a/fail2ban/tests/samplestestcase.py +++ b/fail2ban/tests/samplestestcase.py @@ -64,7 +64,8 @@ def testSampleRegexsFactory(name): def testFilter(self): # Check filter exists - filterConf = FilterReader(name, "jail", {}, basedir=CONFIG_DIR) + filterConf = FilterReader(name, "jail", {}, + basedir=CONFIG_DIR, share_config=unittest.F2B.share_config) self.assertEqual(filterConf.getFile(), name) self.assertEqual(filterConf.getJailName(), "jail") filterConf.read() diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 87ca4289..96734262 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -47,6 +47,7 @@ except ImportError: # pragma: no cover filtersystemd = None TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") +FAST_BACKEND = "polling" class TestServer(Server): @@ -72,7 +73,7 @@ class TransmitterBase(unittest.TestCase): self.tmp_files.append(pidfile_name) self.server.start(sock_name, pidfile_name, force=False) self.jailName = "TestJail1" - self.server.addJail(self.jailName, "auto") + self.server.addJail(self.jailName, FAST_BACKEND) def tearDown(self): """Call after every test case.""" @@ -195,7 +196,7 @@ class Transmitter(TransmitterBase): self.setGetTest("dbpurgeage", "600", 600) self.setGetTestNOK("dbpurgeage", "LIZARD") # the same file name (again with jails / not changed): - self.server.addJail(self.jailName, "auto") + self.server.addJail(self.jailName, FAST_BACKEND) self.setGetTest("dbfile", tmpFilename) self.server.delJail(self.jailName) @@ -213,7 +214,7 @@ class Transmitter(TransmitterBase): ["get", "dbpurgeage"]), (0, None)) # the same (again with jails / not changed): - self.server.addJail(self.jailName, "auto") + self.server.addJail(self.jailName, FAST_BACKEND) self.assertEqual(self.transm.proceed( ["set", "dbfile", "None"]), (0, None)) @@ -252,7 +253,7 @@ class Transmitter(TransmitterBase): self.assertTrue(self.jailName not in self.server._Server__jails) def testStartStopAllJail(self): - self.server.addJail("TestJail2", "auto") + self.server.addJail("TestJail2", FAST_BACKEND) self.assertEqual( self.transm.proceed(["start", self.jailName]), (0, None)) self.assertEqual( @@ -497,7 +498,7 @@ class Transmitter(TransmitterBase): jails = [self.jailName] self.assertEqual(self.transm.proceed(["status"]), (0, [('Number of jail', len(jails)), ('Jail list', ", ".join(jails))])) - self.server.addJail("TestJail2", "auto") + self.server.addJail("TestJail2", FAST_BACKEND) jails.append("TestJail2") self.assertEqual(self.transm.proceed(["status"]), (0, [('Number of jail', len(jails)), ('Jail list', ", ".join(jails))])) diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index c33c2b19..0cac9df5 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -59,6 +59,7 @@ class F2B(optparse.Values): if self.fast: self.memory_db = True self.no_gamin = True + self.__dict__['share_config'] = {} def SkipIfFast(self): pass def SkipIfNoNetwork(self): From 8eca2ecd84182242973aed9fba5760bd32947dc9 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 29 Dec 2015 17:38:47 +0100 Subject: [PATCH 026/623] optimized version of "str2seconds" with pre-compiled regexp; --- fail2ban/server/mytime.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/fail2ban/server/mytime.py b/fail2ban/server/mytime.py index 329e2465..816b04c4 100644 --- a/fail2ban/server/mytime.py +++ b/fail2ban/server/mytime.py @@ -98,6 +98,25 @@ class MyTime: else: return time.localtime(MyTime.myTime) + ## precreate/precompile primitives used in str2seconds: + + ## preparing expression: + _str2sec_prep = re.compile(r"(?i)(?<=[a-z])(\d)") + ## finally expression: + _str2sec_fini = re.compile(r"(\d)\s+(\d)") + ## wrapper for each sub part: + _str2sec_subpart = r"(?i)(?<=[\d\s])(%s)\b" + ## parts to be replaced - pair of (regexp x replacement): + _str2sec_parts = ( + (re.compile(_str2sec_subpart % r"days?|da|dd?"), "*"+str(24*60*60)), + (re.compile(_str2sec_subpart % r"weeks?|wee?|ww?"), "*"+str(7*24*60*60)), + (re.compile(_str2sec_subpart % r"months?|mon?"), "*"+str((365*3+366)*24*60*60/4/12)), + (re.compile(_str2sec_subpart % r"years?|yea?|yy?"), "*"+str((365*3+366)*24*60*60/4)), + (re.compile(_str2sec_subpart % r"seconds?|sec?|ss?"), "*"+str(1)), + (re.compile(_str2sec_subpart % r"minutes?|min?|mm?"), "*"+str(60)), + (re.compile(_str2sec_subpart % r"hours?|hou?|hh?"), "*"+str(60*60)), + ) + @staticmethod def str2seconds(val): """Wraps string expression like "1h 2m 3s" into number contains seconds (3723). @@ -120,13 +139,9 @@ class MyTime: if isinstance(val, (int, long, float, complex)): return val # replace together standing abbreviations, example '1d12h' -> '1d 12h': - val = re.sub(r"(?i)(?<=[a-z])(\d)", r" \1", val) + val = MyTime._str2sec_prep.sub(r" \1", val) # replace abbreviation with expression: - for rexp, rpl in ( - (r"days?|da|dd?", 24*60*60), (r"weeks?|wee?|ww?", 7*24*60*60), (r"months?|mon?", (365*3+366)*24*60*60/4/12), - (r"years?|yea?|yy?", (365*3+366)*24*60*60/4), - (r"seconds?|sec?|ss?", 1), (r"minutes?|min?|mm?", 60), (r"hours?|hou?|hh?", 60*60), - ): - val = re.sub(r"(?i)(?<=[\d\s])(%s)\b" % rexp, "*"+str(rpl), val) - val = re.sub(r"(\d)\s+(\d)", r"\1+\2", val); + for rexp, rpl in MyTime._str2sec_parts: + val = rexp.sub(rpl, val) + val = MyTime._str2sec_fini.sub(r"\1+\2", val) return eval(val) From 134c33cc6d11249c5f1532f098345681e1875eec Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 29 Dec 2015 17:40:37 +0100 Subject: [PATCH 027/623] fail2ban-testcases: added option "--ignore": negate [regexps] filter, to ignore tests matched specified regexps --- bin/fail2ban-testcases | 3 +++ fail2ban/tests/utils.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/bin/fail2ban-testcases b/bin/fail2ban-testcases index 22a44a8b..606b0b06 100755 --- a/bin/fail2ban-testcases +++ b/bin/fail2ban-testcases @@ -67,6 +67,9 @@ def get_opt_parser(): Option('-f', "--fast", action="store_true", dest="fast", help="Try to increase speed of the tests, decreasing of wait intervals, memory database"), + Option('-i', "--ignore", action="store_true", + dest="negate_re", + help="negate [regexps] filter to ignore tests matched specified regexps"), Option("-t", "--log-traceback", action='store_true', help="Enrich log-messages with compressed tracebacks"), Option("--full-traceback", action='store_true', diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 0cac9df5..ce6d638a 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -55,6 +55,7 @@ class F2B(optparse.Values): def __init__(self, opts={}): self.__dict__ = opts.__dict__ if opts else { 'fast': False, 'memory_db':False, 'no_gamin': False, 'no_network': False, + "negate_re": False, } if self.fast: self.memory_db = True @@ -157,7 +158,8 @@ def gatherTests(regexps=None, opts=None): for test in suite: s = str(test) for r in self._regexps: - if r.search(s): + m = r.search(s) + if (m if not opts.negate_re else not m): matched.append(test) break for test in matched: From dca3db941e049d79c6ffcbd26a235a512d29eea1 Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 7 Mar 2016 17:28:37 +0100 Subject: [PATCH 028/623] merge getTime2 functionality to getTime --- fail2ban/server/datedetector.py | 49 +++++++------------------- fail2ban/server/filter.py | 4 +-- fail2ban/tests/datedetectortestcase.py | 4 +-- 3 files changed, 17 insertions(+), 40 deletions(-) diff --git a/fail2ban/server/datedetector.py b/fail2ban/server/datedetector.py index ae6d71d9..0c5da94b 100644 --- a/fail2ban/server/datedetector.py +++ b/fail2ban/server/datedetector.py @@ -202,13 +202,13 @@ class DateDetector(object): # not found: return (None, None) - def getTime(self, line): + def getTime(self, line, timeMatch=None): """Attempts to return the date on a log line using templates. - Obsolete: Use "getTime2" instead. - This uses the templates' `getDate` method in an attempt to find - a date. + a date. + For the faster usage, always specify a parameter timeMatch (the previous tuple result + of the matchTime), then this will work without locking and without cycle over templates. Parameters ---------- @@ -221,6 +221,15 @@ class DateDetector(object): The Unix timestamp returned from the first successfully matched template or None if not found. """ + if timeMatch: + template = timeMatch[1] + if template is not None: + date = template.getDate(line, timeMatch[0]) + if date is not None: + if logSys.getEffectiveLevel() <= logLevel: + logSys.log(logLevel, "Got time %f for \"%r\" using template %s", + date[0], date[1].group(), template.name) + return date with self.__lock: for template in self.__templates: try: @@ -235,38 +244,6 @@ class DateDetector(object): pass return None - def getTime2(self, line, timeMatch = None): - """Attempts to return the date on a log line using given template. - - This uses the templates' `getDate` method in an attempt to find - a date. - Method 'getTime2' is a little bit faster as 'getTime' if template was specified (cause works without locking and without cycle) - - Parameters - ---------- - line : str - Line which is searched by the date templates. - timeMatch (timeMatch, template) : (Match, DateTemplate) - Time match and template previously returned from matchTime - - Returns - ------- - float - The Unix timestamp returned from the first successfully matched - template or None if not found. - """ - date = None - if timeMatch: - template = timeMatch[1] - if template is not None: - date = template.getDate(line, timeMatch[0]) - if date is not None: - if logSys.getEffectiveLevel() <= logLevel: - logSys.log(logLevel, "Got time(2) %f for \"%r\" using template %s", - date[0], date[1].group(), template.name) - return date - return self.getTime(line) - def _reorderTemplate(self, num): """Reorder template (bubble up) in template list if hits grows enough. diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 8dbb6a7a..b7de21c8 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -479,7 +479,7 @@ class Filter(JailThread): self.__lastDate = date elif timeText: - dateTimeMatch = self.dateDetector.getTime2(timeText, tupleLine[3]) + dateTimeMatch = self.dateDetector.getTime(timeText, tupleLine[3]) if dateTimeMatch is None: logSys.error("findFailure failed to parse timeText: " + timeText) @@ -769,7 +769,7 @@ class FileFilter(Filter): break (timeMatch, template) = self.dateDetector.matchTime(line) if timeMatch: - dateTimeMatch = self.dateDetector.getTime2(line[timeMatch.start():timeMatch.end()], (timeMatch, template)) + dateTimeMatch = self.dateDetector.getTime(line[timeMatch.start():timeMatch.end()], (timeMatch, template)) else: nextp = container.tell() if nextp > maxp: diff --git a/fail2ban/tests/datedetectortestcase.py b/fail2ban/tests/datedetectortestcase.py index ab7310ba..dc01fd49 100644 --- a/fail2ban/tests/datedetectortestcase.py +++ b/fail2ban/tests/datedetectortestcase.py @@ -128,9 +128,9 @@ class DateDetectorTest(LogCaptureTestCase): self.assertEqual(logMatch.group(), sdate) else: self.assertEqual(logtime, None, "getTime should have not matched for %r Got: %s" % (sdate, logtime)) - # with matchTime and getTime2 (this combination used in filter) : + # with getTime(matchTime) - this combination used in filter: matchTime = self.__datedetector.matchTime(log) - logtime = self.__datedetector.getTime2(log, matchTime) + logtime = self.__datedetector.getTime(log, matchTime) if should_match: self.assertNotEqual(logtime, None, "getTime retrieved nothing: failure for %s, anchored: %r, log: %s" % ( sdate, anchored, log)) ( logUnix, logMatch ) = logtime From 7eafb0c2066dd147c054bee7dfa34eac4075a3ea Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Wed, 9 Mar 2016 22:02:07 -0500 Subject: [PATCH 029/623] ENH: avoid subclassing dict (in favor of delegating) if not entire interface is used/adjusted since otherwise use of dict"s __getitem__ could lead to undesired effects etc --- fail2ban/server/utils.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/fail2ban/server/utils.py b/fail2ban/server/utils.py index 45d1c09d..ad2607c5 100644 --- a/fail2ban/server/utils.py +++ b/fail2ban/server/utils.py @@ -50,34 +50,41 @@ class Utils(): DEFAULT_SLEEP_INTERVAL = 0.01 - class Cache(dict): + class Cache(object): + """A simple cache with a TTL and limit on size + """ def __init__(self, *args, **kwargs): self.setOptions(*args, **kwargs) + self._cache = {} def setOptions(self, maxCount=1000, maxTime=60): self.maxCount = maxCount self.maxTime = maxTime + def __len__(self): + return len(self._cache) + def get(self, k, defv=None): - v = dict.get(self, k) + v = self._cache.get(k) if v: if v[1] > time.time(): return v[0] - del self[k] + del self._cache[k] return defv def set(self, k, v): t = time.time() + cache = self._cache # for shorter local access # clean cache if max count reached: - if len(self) >= self.maxCount: - for (ck,cv) in self.items(): + if len(cache) >= self.maxCount: + for (ck, cv) in cache.items(): if cv[1] < t: - del self[ck] + del cache[ck] # if still max count - remove any one: - if len(self) >= self.maxCount: - self.popitem() - self[k] = (v, t + self.maxTime) + if len(cache) >= self.maxCount: + cache.popitem() + cache[k] = (v, t + self.maxTime) @staticmethod From c84b6370c548884fd17028d4a5f3f86efd89171c Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Wed, 9 Mar 2016 22:02:51 -0500 Subject: [PATCH 030/623] ENH: remove some code duplication, enhance docstrings, uniformize naming --- fail2ban/server/filter.py | 6 +++--- fail2ban/server/utils.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index b0ccda84..f5cb8546 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -998,7 +998,7 @@ class DNSUtils: IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") # todo: make configurable the expired time and max count of cache entries: - CACHE_dnsToIp = Utils.Cache(maxCount=1000, maxTime=5*60) + CACHE_nameToIp = Utils.Cache(maxCount=1000, maxTime=5*60) CACHE_ipToName = Utils.Cache(maxCount=1000, maxTime=5*60) @staticmethod @@ -1007,7 +1007,7 @@ class DNSUtils: Thanks to Kevin Drapel. """ # cache, also prevent long wait during retrieving of ip for wrong dns or lazy dns-system: - v = DNSUtils.CACHE_dnsToIp.get(dns) + v = DNSUtils.CACHE_nameToIp.get(dns) if v is not None: return v # retrieve ip (todo: use AF_INET6 for IPv6) @@ -1017,7 +1017,7 @@ class DNSUtils: # todo: make configurable the expired time of cache entry: logSys.warning("Unable to find a corresponding IP address for %s: %s", dns, e) v = list() - DNSUtils.CACHE_dnsToIp.set(dns, v) + DNSUtils.CACHE_nameToIp.set(dns, v) return v @staticmethod diff --git a/fail2ban/server/utils.py b/fail2ban/server/utils.py index ad2607c5..cd05bf93 100644 --- a/fail2ban/server/utils.py +++ b/fail2ban/server/utils.py @@ -111,12 +111,13 @@ class Utils(): If shell is True (default), the specified command (may be a string) will be executed through the shell. output : bool - If output is True, the function returns tuple (success, stdoutdata, stderrdata, returncode) + If output is True, the function returns tuple (success, stdoutdata, stderrdata, returncode). + If False, just indication of success is returned Returns ------- - bool - True if the command succeeded. + bool or (bool, str, str, int) + True if the command succeeded and with stdout, stderr, returncode if output was set to True Raises ------ @@ -185,9 +186,10 @@ class Utils(): logSys.log(std_level, "%s -- stderr: %r", realCmd, stderr) popen.stderr.close() + success = False if retcode == 0: logSys.debug("%s -- returned successfully", realCmd) - return True if not output else (True, stdout, stderr, retcode) + success = True elif retcode is None: logSys.error("%s -- unable to kill PID %i" % (realCmd, popen.pid)) elif retcode < 0 or retcode > 128: @@ -200,13 +202,13 @@ class Utils(): logSys.error("%s -- returned %i" % (realCmd, retcode)) if msg: logSys.info("HINT on %i: %s", retcode, msg % locals()) - return False if not output else (False, stdout, stderr, retcode) + return success if not output else (success, stdout, stderr, retcode) @staticmethod def wait_for(cond, timeout, interval=None): """Wait until condition expression `cond` is True, up to `timeout` sec """ - ini = 1 + ini = 1 # to delay initializations until/when necessary while True: ret = cond() if ret: From deca0b80abcbdd6129d7448d8499fe31cb4ec49e Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Wed, 9 Mar 2016 22:17:14 -0500 Subject: [PATCH 031/623] ENH: removed double tab indentation, pass use_poll into loop, reorderd log msg to come after action to be factually correct --- fail2ban/server/asyncserver.py | 48 ++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/fail2ban/server/asyncserver.py b/fail2ban/server/asyncserver.py index ad37544a..d43874c7 100644 --- a/fail2ban/server/asyncserver.py +++ b/fail2ban/server/asyncserver.py @@ -92,26 +92,29 @@ class RequestHandler(asynchat.async_chat): def loop(active, timeout=None, use_poll=False): - # Use poll instead of loop, because of recognition of active flag, - # because of loop timeout mistake: different in poll and poll2 (sec vs ms), - # and to prevent sporadical errors like EBADF 'Bad file descriptor' etc. (see gh-161) - if timeout is None: - timeout = Utils.DEFAULT_SLEEP_TIME - poll = asyncore.poll - if use_poll and asyncore.poll2 and hasattr(asyncore.select, 'poll'): # pragma: no cover - logSys.debug('Server listener (select) uses poll') - # poll2 expected a timeout in milliseconds (but poll and loop in seconds): - timeout = float(timeout) / 1000 - poll = asyncore.poll2 - # Poll as long as active: - while active(): - try: - poll(timeout) - except Exception as e: # pragma: no cover - if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') - logSys.info('Server connection was closed: %s', str(e)) - else: - logSys.error('Server connection was closed: %s', str(e)) + """Custom event loop implementation + + Uses poll instead of loop to respect `active` flag, + to avoid loop timeout mistake: different in poll and poll2 (sec vs ms), + and to prevent sporadic errors like EBADF 'Bad file descriptor' etc. (see gh-161) + """ + if timeout is None: + timeout = Utils.DEFAULT_SLEEP_TIME + poll = asyncore.poll + if use_poll and asyncore.poll2 and hasattr(asyncore.select, 'poll'): # pragma: no cover + logSys.debug('Server listener (select) uses poll') + # poll2 expected a timeout in milliseconds (but poll and loop in seconds): + timeout = float(timeout) / 1000 + poll = asyncore.poll2 + # Poll as long as active: + while active(): + try: + poll(timeout) + except Exception as e: # pragma: no cover + if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') + logSys.info('Server connection was closed: %s', str(e)) + else: + logSys.error('Server connection was closed: %s', str(e)) ## @@ -177,7 +180,7 @@ class AsyncServer(asyncore.dispatcher): # Sets the init flag. self.__init = self.__active = True # Event loop as long as active: - loop(lambda: self.__active) + loop(lambda: self.__active, use_poll=use_poll) # Cleanup all self.stop() @@ -187,8 +190,8 @@ class AsyncServer(asyncore.dispatcher): asyncore.dispatcher.close(self) # Remove socket (file) only if it was created: if self.__init and os.path.exists(self.__sock): - logSys.debug("Removed socket file " + self.__sock) os.remove(self.__sock) + logSys.debug("Removed socket file " + self.__sock) logSys.debug("Socket shutdown") self.__active = False @@ -198,6 +201,7 @@ class AsyncServer(asyncore.dispatcher): def stop(self): self.close() + # better remains a method (not a property) since used as a callable for wait_for def isActive(self): return self.__active From 01b379ab2ebb4b3b9d40e4e1cb0fc8b0cca49f18 Mon Sep 17 00:00:00 2001 From: sebres Date: Thu, 10 Mar 2016 11:47:26 +0100 Subject: [PATCH 032/623] fix sporadic bugs within asyncserver, cherry picked from "f2b-perfom-prepare-716-cs": - differentiate between in loop and active (reset active only after really leaves a loop); - differentiate between foreign and self loop-thread by stop/close connection; - stops logging connection errors if too many errors; - safe remove socket (no sporadic errors if too fast start/stop occurred); --- fail2ban/server/asyncserver.py | 48 +++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/fail2ban/server/asyncserver.py b/fail2ban/server/asyncserver.py index d43874c7..bb237cd7 100644 --- a/fail2ban/server/asyncserver.py +++ b/fail2ban/server/asyncserver.py @@ -32,6 +32,7 @@ import fcntl import os import socket import sys +import threading import traceback from .utils import Utils @@ -98,10 +99,11 @@ def loop(active, timeout=None, use_poll=False): to avoid loop timeout mistake: different in poll and poll2 (sec vs ms), and to prevent sporadic errors like EBADF 'Bad file descriptor' etc. (see gh-161) """ + errCount = 0 if timeout is None: timeout = Utils.DEFAULT_SLEEP_TIME poll = asyncore.poll - if use_poll and asyncore.poll2 and hasattr(asyncore.select, 'poll'): # pragma: no cover + if use_poll and asyncore.poll2 and hasattr(asyncore.select, 'poll'): # pragma: no cover logSys.debug('Server listener (select) uses poll') # poll2 expected a timeout in milliseconds (but poll and loop in seconds): timeout = float(timeout) / 1000 @@ -110,11 +112,20 @@ def loop(active, timeout=None, use_poll=False): while active(): try: poll(timeout) - except Exception as e: # pragma: no cover - if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') - logSys.info('Server connection was closed: %s', str(e)) - else: - logSys.error('Server connection was closed: %s', str(e)) + if errCount: + errCount -= 1 + except Exception as e: # pragma: no cover + if not active(): + break + errCount += 1 + if errCount < 20: + if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') + logSys.info('Server connection was closed: %s', str(e)) + else: + logSys.error('Server connection was closed: %s', str(e)) + elif errCount == 20: + logSys.info('Too many errors - stop logging connection errors') + logSys.exception(e) ## @@ -159,13 +170,14 @@ class AsyncServer(asyncore.dispatcher): # @param force: remove the socket file if exists. def start(self, sock, force, use_poll=False): + self.__worker = threading.current_thread() self.__sock = sock # Remove socket if os.path.exists(sock): logSys.error("Fail2ban seems to be already running") if force: logSys.warning("Forcing execution of the server") - os.remove(sock) + self._remove_sock() else: raise AsyncServerException("Server already running") # Creates the socket. @@ -178,19 +190,25 @@ class AsyncServer(asyncore.dispatcher): AsyncServer.__markCloseOnExec(self.socket) self.listen(1) # Sets the init flag. - self.__init = self.__active = True + self.__init = self.__loop = self.__active = True # Event loop as long as active: - loop(lambda: self.__active, use_poll=use_poll) + loop(lambda: self.__loop, use_poll=use_poll) + self.__active = False # Cleanup all self.stop() def close(self): if self.__active: + self.__loop = False asyncore.dispatcher.close(self) + # If not the loop thread (stops self in handler), wait (a little bit) + # for the server leaves loop, before remove socket + if threading.current_thread() != self.__worker: + Utils.wait_for(lambda: not self.__active, 1) # Remove socket (file) only if it was created: if self.__init and os.path.exists(self.__sock): - os.remove(self.__sock) + self._remove_sock() logSys.debug("Removed socket file " + self.__sock) logSys.debug("Socket shutdown") self.__active = False @@ -205,6 +223,16 @@ class AsyncServer(asyncore.dispatcher): def isActive(self): return self.__active + ## + # Safe remove (in multithreaded mode): + + def _remove_sock(self): + try: + os.remove(self.__sock) + except OSError as e: + if e.errno != errno.ENOENT: + raise + ## # Marks socket as close-on-exec to avoid leaking file descriptors when # running actions involving command execution. From 74b88e1706ba40e735314defb9f71beb8cc700b7 Mon Sep 17 00:00:00 2001 From: sebres Date: Thu, 10 Mar 2016 12:05:59 +0100 Subject: [PATCH 033/623] fix sporadic bugs by starting of fast actions (execute fast sub processes), cherry picked from "f2b-perfom-prepare-716-cs": - missing `popen` variable if not started; - better readability for stdout and stderr in log (+ check log level before do logging); --- fail2ban/server/utils.py | 18 +++++++++++++----- fail2ban/tests/actiontestcase.py | 4 ++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/fail2ban/server/utils.py b/fail2ban/server/utils.py index cd05bf93..738394c3 100644 --- a/fail2ban/server/utils.py +++ b/fail2ban/server/utils.py @@ -133,6 +133,7 @@ class Utils(): timeout_expr = lambda: time.time() - stime <= timeout else: timeout_expr = timeout + popen = None try: popen = subprocess.Popen( realCmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, @@ -159,7 +160,10 @@ class Utils(): if retcode is None and not Utils.pid_exists(pgid): retcode = signal.SIGKILL except OSError as e: - logSys.error("%s -- failed with %s" % (realCmd, e)) + stderr = "%s -- failed with %s" % (realCmd, e) + logSys.error(stderr) + if not popen: + return False if not output else (False, stdout, stderr, retcode) std_level = retcode == 0 and logging.DEBUG or logging.ERROR # if we need output (to return or to log it): @@ -172,8 +176,10 @@ class Utils(): stdout = popen.stdout.read() except IOError as e: logSys.error(" ... -- failed to read stdout %s", e) - if stdout is not None and stdout != '': - logSys.log(std_level, "%s -- stdout: %r", realCmd, stdout) + if stdout is not None and stdout != '' and std_level >= logSys.getEffectiveLevel(): + logSys.log(std_level, "%s -- stdout:", realCmd) + for l in stdout.splitlines(): + logSys.log(std_level, " -- stdout: %r", l) popen.stdout.close() if popen.stderr: try: @@ -182,8 +188,10 @@ class Utils(): stderr = popen.stderr.read() except IOError as e: logSys.error(" ... -- failed to read stderr %s", e) - if stderr is not None and stderr != '': - logSys.log(std_level, "%s -- stderr: %r", realCmd, stderr) + if stderr is not None and stderr != '' and std_level >= logSys.getEffectiveLevel(): + logSys.log(std_level, "%s -- stderr:", realCmd) + for l in stderr.splitlines(): + logSys.log(std_level, " -- stderr: %r", l) popen.stderr.close() success = False diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 6d8fcc82..1872eb1f 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -271,11 +271,11 @@ class CommandActionTest(LogCaptureTestCase): def testCaptureStdOutErr(self): CommandAction.executeCmd('echo "How now brown cow"') - self.assertLogged("'How now brown cow\\n'") + self.assertLogged("stdout: 'How now brown cow'\n", "stdout: b'How now brown cow'\n") CommandAction.executeCmd( 'echo "The rain in Spain stays mainly in the plain" 1>&2') self.assertLogged( - "'The rain in Spain stays mainly in the plain\\n'") + "stderr: 'The rain in Spain stays mainly in the plain'\n", "stderr: b'The rain in Spain stays mainly in the plain'\n") def testCallingMap(self): mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'), From 5773eb71b8cc738d1e2a024a93fce245818d0fcd Mon Sep 17 00:00:00 2001 From: Alexey Korepanov Date: Tue, 12 Apr 2016 21:20:19 +0100 Subject: [PATCH 034/623] ensure /var/run/fail2ban is created in systemd service file --- files/fail2ban.service | 1 + 1 file changed, 1 insertion(+) diff --git a/files/fail2ban.service b/files/fail2ban.service index e79faed1..985f5c1f 100644 --- a/files/fail2ban.service +++ b/files/fail2ban.service @@ -6,6 +6,7 @@ PartOf=iptables.service firewalld.service [Service] Type=forking +ExecStartPre=/bin/mkdir -p /var/run/fail2ban ExecStart=/usr/bin/fail2ban-client -x start ExecStop=/usr/bin/fail2ban-client stop ExecReload=/usr/bin/fail2ban-client reload From 01e1383c9bb3b6f75f087d2d2303b37811fa5e32 Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Sun, 28 Feb 2016 11:11:24 +0100 Subject: [PATCH 035/623] New class IPAddr for handling IPv4 and IPv6 addresses --- fail2ban/server/filter.py | 243 +++++++++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 4 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index f5cb8546..da5fc1ea 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -433,7 +433,7 @@ class Filter(JailThread): logSys.debug("Ignore line since time %s < %s - %s", unixTime, MyTime.time(), self.getFindTime()) break - if self.inIgnoreIPList(ip, log_ignore=True): + if self.inIgnoreIPList(ip.ntoa(), log_ignore=True): continue logSys.info( "[%s] Found %s - %s", self.jail.name, ip, datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S") @@ -530,7 +530,8 @@ class Filter(JailThread): try: host = failRegex.getHost() if returnRawHost: - failList.append([failRegexIndex, host, date, + ipaddr = IPAddr(host) + failList.append([failRegexIndex, ipaddr, date, failRegex.getMatchedLines()]) if not checkAllRegex: break @@ -538,8 +539,9 @@ class Filter(JailThread): ipMatch = DNSUtils.textToIp(host, self.__useDns) if ipMatch: for ip in ipMatch: - failList.append([failRegexIndex, ip, date, - failRegex.getMatchedLines()]) + ipaddr = IPAddr(ip) + failList.append([failRegexIndex, ipaddr, + date, failRegex.getMatchedLines()]) if not checkAllRegex: break except RegexException, e: # pragma: no cover - unsure if reachable @@ -1096,3 +1098,236 @@ class DNSUtils: """ Convert a binary IPv4 address into string n.n.n.n form. """ return socket.inet_ntoa(struct.pack("!L", ipbin)) + + + +## +# Class for IP address handling. +# +# This class contains methods for handling IPv4 and IPv6 addresses. + +class IPAddr: + """ provide functions to handle IPv4 and IPv6 addresses + """ + + IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") + IP6_CRE = re.compile("^[0-9a-fA-F]{4}[0-9a-fA-F:]+:[0-9a-fA-F]{1,4}|::1$") + + # object attributes + addr = 0 + family = socket.AF_UNSPEC + plen = 0 + valid = False + raw = "" + + # object methods + def __init__(self, ipstring, cidr=-1): + """ initialize IP object by converting IP address string + to binary to integer + """ + for family in [socket.AF_INET, socket.AF_INET6]: + try: + binary = socket.inet_pton(family, ipstring) + except socket.error: + continue + else: + self.valid = True + break + + if self.valid and family == socket.AF_INET: + # convert host to network byte order + self.addr, = struct.unpack("!L", binary) + self.family = family + self.plen = 32 + + # mask out host portion if prefix length is supplied + if cidr != None and cidr >= 0: + mask = ~(0xFFFFFFFFL >> cidr) + self.addr = self.addr & mask + self.plen = cidr + + elif self.valid and family == socket.AF_INET6: + # convert host to network byte order + hi, lo = struct.unpack("!QQ", binary) + self.addr = (hi << 64) | lo + self.family = family + self.plen = 128 + + # mask out host portion if prefix length is supplied + if cidr != None and cidr >= 0: + mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> cidr) + self.addr = self.addr & mask + self.plen = cidr + + # if IPv6 address is a IPv4-compatible, make instance a IPv4 + elif self.isInNet(IPAddr("::ffff:0:0", 96)): + self.addr = lo & 0xFFFFFFFFL + self.family = socket.AF_INET + self.plen = 32 + else: + # string couldn't be converted neither to a IPv4 nor + # to a IPv6 address - retain raw input for later use + # (e.g. DNS resolution) + self.raw = ipstring + + def __repr__(self): + return self.ntoa() + + def __str__(self): + return self.ntoa() + + def __eq__(self, other): + other = other if isinstance(other, IPAddr) else IPAddr(other) + if not self.valid and not other.valid: return self.raw == other.raw + if not self.valid or not other.valid: return False + if self.addr != other.addr: return False + if self.family != other.family: return False + if self.plen != other.plen: return False + return True + + def __ne__(self, other): + other = other if isinstance(other, IPAddr) else IPAddr(other) + if not self.valid and not other.valid: return self.raw != other.raw + if self.addr != other.addr: return True + if self.family != other.family: return True + if self.plen != other.plen: return True + return False + + def __lt__(self, other): + other = other if isinstance(other, IPAddr) else IPAddr(other) + return self.family < other.family or self.addr < other.addr + + def __add__(self, other): + return "%s%s" % (self, other) + + def __radd__(self, other): + return "%s%s" % (other, self) + + def __hash__(self): + return hash(self.addr)^hash((self.plen<<16)|self.family) + + def hexdump(self): + """ dump the ip address in as a hex sequence in + network byte order - for debug purpose + """ + if self.family == socket.AF_INET: + return "%08x" % self.addr + elif self.family == socket.AF_INET6: + return "%032x" % self.addr + else: + return "" + + def ntoa(self): + """ represent IP object as text like the depricated + C pendant inet_ntoa() but address family independent + """ + if self.family == socket.AF_INET: + # convert network to host byte order + binary = struct.pack("!L", self.addr) + elif self.family == socket.AF_INET6: + # convert network to host byte order + hi = self.addr >> 64 + lo = self.addr & 0xFFFFFFFFFFFFFFFFL + binary = struct.pack("!QQ", hi, lo) + else: + return self.getRaw() + + return socket.inet_ntop(self.family, binary) + + def getPTR(self, suffix=""): + """ generates the DNS PTR string of the provided IP address object + if "suffix" is provided it will be appended as the second and top + level reverse domain. + if omitted it is implicitely set to the second and top level reverse + domain of the according IP address family + """ + if self.family == socket.AF_INET: + reversed_ip = ".".join(reversed(self.ntoa().split("."))) + if not suffix: + suffix = "in-addr.arpa." + + return "%s.%s" % (reversed_ip, suffix) + + elif self.family == socket.AF_INET6: + reversed_ip = ".".join(reversed(self.hexdump())) + if not suffix: + suffix = "ip6.arpa." + + return "%s.%s" % (reversed_ip, suffix) + + else: + return "" + + def isIPv4(self): + """ return true if the IP object is of address family AF_INET + """ + return True if self.family == socket.AF_INET else False + + def isIPv6(self): + """ return true if the IP object is of address family AF_INET6 + """ + return True if self.family == socket.AF_INET6 else False + + def getRaw(self): + """ returns the raw attribute - should only be set + to a non-empty string if prior address conversion + wasn't possible + """ + return self.raw + + def isValidIP(self): + """ returns true if the IP object has been created + from a valid IP address or false if not + """ + return self.valid + + + def isInNet(self, net): + """ returns true if the IP object is in the provided + network (object) + """ + if self.family != net.family: + return False + + if self.family == socket.AF_INET: + mask = ~(0xFFFFFFFFL >> net.plen) + + elif self.family == socket.AF_INET6: + mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> net.plen) + else: + return False + + if self.addr & mask == net.addr: + return True + + return False + + + @staticmethod + def masktoplen(maskstr): + """ converts mask string to prefix length + only used for IPv4 masks + """ + mask = IPAddr(maskstr) + plen = 0 + while mask.addr: + mask.addr = (mask.addr << 1) & 0xFFFFFFFFL + plen += 1 + return plen + + + @staticmethod + def searchIP(text): + """ Search if an IP address if directly available and return + it. + """ + match = IPAddr.IP_CRE.match(text) + if match: + return match + else: + match = IPAddr.IP6_CRE.match(text) + if match: + return match + else: + return None + From 130874434879d2d59b4b81cd20c67be036083f77 Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 4 May 2016 12:53:34 +0200 Subject: [PATCH 036/623] meantime commit: code review, simplification, pythonization, etc. --- fail2ban/server/filter.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index da5fc1ea..1b46aa2a 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -433,7 +433,7 @@ class Filter(JailThread): logSys.debug("Ignore line since time %s < %s - %s", unixTime, MyTime.time(), self.getFindTime()) break - if self.inIgnoreIPList(ip.ntoa(), log_ignore=True): + if self.inIgnoreIPList(ip, log_ignore=True): continue logSys.info( "[%s] Found %s - %s", self.jail.name, ip, datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S") @@ -530,8 +530,7 @@ class Filter(JailThread): try: host = failRegex.getHost() if returnRawHost: - ipaddr = IPAddr(host) - failList.append([failRegexIndex, ipaddr, date, + failList.append([failRegexIndex, IPAddr(host), date, failRegex.getMatchedLines()]) if not checkAllRegex: break @@ -539,9 +538,8 @@ class Filter(JailThread): ipMatch = DNSUtils.textToIp(host, self.__useDns) if ipMatch: for ip in ipMatch: - ipaddr = IPAddr(ip) - failList.append([failRegexIndex, ipaddr, - date, failRegex.getMatchedLines()]) + failList.append([failRegexIndex, ip, date, + failRegex.getMatchedLines()]) if not checkAllRegex: break except RegexException, e: # pragma: no cover - unsure if reachable From a0938286023ea09f1c668ecb526f3f227e4a3d5e Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Mon, 29 Feb 2016 20:38:40 +0100 Subject: [PATCH 037/623] Make ignoreip checking address family idependent --- fail2ban/server/filter.py | 44 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 1b46aa2a..af23a1d2 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -336,7 +336,22 @@ class Filter(JailThread): # when finding failures. CIDR mask and DNS are also accepted. # @param ip IP address to ignore - def addIgnoreIP(self, ip): + def addIgnoreIP(self, ipstr): + # An empty string is always false + if ipstr == "": + return + s = ipstr.split('/', 1) + # IP address without CIDR mask + if len(s) == 1: + s.insert(1, -1) # <0 means no CIDR + elif "." in s[1]: # 255.255.255.0 style mask + s[1] = IPAddr.masktoplen(s[1]) + s[1] = long(s[1]) + + # Create IP address object + ip = IPAddr(s[0], s[1]) + + # log and append to ignore list logSys.debug("Add " + ip + " to ignore list") self.__ignoreIpList.append(ip) @@ -356,34 +371,22 @@ class Filter(JailThread): # # Check if the given IP address matches an IP address/DNS or a CIDR # mask in the ignore list. - # @param ip IP address + # @param ip IP address object # @return True if IP address is in ignore list def inIgnoreIPList(self, ip, log_ignore=False): - for i in self.__ignoreIpList: - # An empty string is always false - if i == "": - continue - s = i.split('/', 1) - # IP address without CIDR mask - if len(s) == 1: - s.insert(1, '32') - elif "." in s[1]: # 255.255.255.0 style mask - s[1] = len(re.search( - "(?<=b)1+", bin(DNSUtils.addr2bin(s[1]))).group()) - s[1] = long(s[1]) - try: - a = DNSUtils.addr2bin(s[0], cidr=s[1]) - b = DNSUtils.addr2bin(ip, cidr=s[1]) - except Exception: + for net in self.__ignoreIpList: + # if it isn't a valid IP address, try DNS resolution + if not net.isValidIP() and net.getRaw() != "": # Check if IP in DNS - ips = DNSUtils.dnsToIp(i) + ips = DNSUtils.dnsToIp(net.getRaw()) if ip in ips: self.logIgnoreIp(ip, log_ignore, ignore_source="dns") return True else: continue - if a == b: + # check if the IP is covered by ignore IP + if ip.isInNet(net): self.logIgnoreIp(ip, log_ignore, ignore_source="ip") return True @@ -391,6 +394,7 @@ class Filter(JailThread): command = CommandAction.replaceTag(self.__ignoreCommand, { 'ip': ip } ) logSys.debug('ignore command: ' + command) ret_ignore = CommandAction.executeCmd(command) + self.logIgnoreIp(ip, log_ignore and ret_ignore, ignore_source="command") return ret_ignore From 3893a6b780ad8271f5a10e452ccd6a6d652d51ed Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 4 May 2016 13:50:37 +0200 Subject: [PATCH 038/623] meantime commit: code review, simplification, pythonization, etc. --- fail2ban/server/filter.py | 3 ++- fail2ban/tests/servertestcase.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index af23a1d2..e19b322e 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -375,6 +375,8 @@ class Filter(JailThread): # @return True if IP address is in ignore list def inIgnoreIPList(self, ip, log_ignore=False): + if isinstance(ip, basestring): + ip = IPAddr(ip) for net in self.__ignoreIpList: # if it isn't a valid IP address, try DNS resolution if not net.isValidIP() and net.getRaw() != "": @@ -394,7 +396,6 @@ class Filter(JailThread): command = CommandAction.replaceTag(self.__ignoreCommand, { 'ip': ip } ) logSys.debug('ignore command: ' + command) ret_ignore = CommandAction.executeCmd(command) - self.logIgnoreIp(ip, log_ignore and ret_ignore, ignore_source="command") return ret_ignore diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 96734262..d9db6c47 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -125,14 +125,14 @@ class TransmitterBase(unittest.TestCase): self.transm.proceed(["get", jail, cmd]), (0, [])) for n, value in enumerate(values): ret = self.transm.proceed(["set", jail, cmdAdd, value]) - self.assertEqual((ret[0], sorted(ret[1])), (0, sorted(values[:n+1]))) + self.assertEqual((ret[0], sorted(map(str, ret[1]))), (0, sorted(map(str, values[:n+1])))) ret = self.transm.proceed(["get", jail, cmd]) - self.assertEqual((ret[0], sorted(ret[1])), (0, sorted(values[:n+1]))) + self.assertEqual((ret[0], sorted(map(str, ret[1]))), (0, sorted(map(str, values[:n+1])))) for n, value in enumerate(values): ret = self.transm.proceed(["set", jail, cmdDel, value]) - self.assertEqual((ret[0], sorted(ret[1])), (0, sorted(values[n+1:]))) + self.assertEqual((ret[0], sorted(map(str, ret[1]))), (0, sorted(map(str, values[n+1:])))) ret = self.transm.proceed(["get", jail, cmd]) - self.assertEqual((ret[0], sorted(ret[1])), (0, sorted(values[n+1:]))) + self.assertEqual((ret[0], sorted(map(str, ret[1]))), (0, sorted(map(str, values[n+1:])))) def jailAddDelRegexTest(self, cmd, inValues, outValues, jail): cmdAdd = "add" + cmd From a7570376717c07e783bb97ee75893f9b0713b0eb Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Mon, 29 Feb 2016 20:54:58 +0100 Subject: [PATCH 039/623] Make DNS resolution IP address family idependent --- fail2ban/server/filter.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index e19b322e..deb47a4b 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -1012,18 +1012,22 @@ class DNSUtils: Thanks to Kevin Drapel. """ # cache, also prevent long wait during retrieving of ip for wrong dns or lazy dns-system: - v = DNSUtils.CACHE_nameToIp.get(dns) - if v is not None: - return v - # retrieve ip (todo: use AF_INET6 for IPv6) + ips = DNSUtils.CACHE_nameToIp.get(dns) + if ips is not None: + return ips + # retrieve ips try: - v = set([i[4][0] for i in socket.getaddrinfo(dns, None, socket.AF_INET, 0, socket.IPPROTO_TCP)]) + ips = list() + for result in socket.getaddrinfo(dns, None, 0, 0, socket.IPPROTO_TCP): + ip = IPAddr(result[4][0]) + if ip.isValidIP(): + ips.append(ip) except socket.error, e: # todo: make configurable the expired time of cache entry: logSys.warning("Unable to find a corresponding IP address for %s: %s", dns, e) - v = list() - DNSUtils.CACHE_nameToIp.set(dns, v) - return v + ips = list() + DNSUtils.CACHE_nameToIp.set(dns, ips) + return ips @staticmethod def ipToName(ip): @@ -1033,7 +1037,7 @@ class DNSUtils: return v # retrieve name try: - v = socket.gethostbyaddr(ip)[0] + v = socket.gethostbyaddr(ip.ntoa())[0] except socket.error, e: logSys.debug("Unable to find a name for the IP %s: %s", ip, e) v = None @@ -1068,11 +1072,11 @@ class DNSUtils: """ ipList = list() # Search for plain IP - plainIP = DNSUtils.searchIP(text) + plainIP = IPAddr.searchIP(text) if not plainIP is None: - plainIPStr = plainIP.group(0) - if DNSUtils.isValidIP(plainIPStr): - ipList.append(plainIPStr) + ip = IPAddr(plainIP.group(0)) + if ip.isValidIP(): + ipList.append(ip) # If we are allowed to resolve -- give it a try if nothing was found if useDns in ("yes", "warn") and not ipList: From 07c9f38e4529cc45e3336d30e66d3c4c195fefff Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 4 May 2016 14:02:03 +0200 Subject: [PATCH 040/623] meantime commit: code review, simplification, pythonization, etc. (test cases passed) --- fail2ban/server/filter.py | 7 +++++-- fail2ban/tests/filtertestcase.py | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index deb47a4b..3682125c 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -375,7 +375,7 @@ class Filter(JailThread): # @return True if IP address is in ignore list def inIgnoreIPList(self, ip, log_ignore=False): - if isinstance(ip, basestring): + if not isinstance(ip, IPAddr): ip = IPAddr(ip) for net in self.__ignoreIpList: # if it isn't a valid IP address, try DNS resolution @@ -1037,7 +1037,10 @@ class DNSUtils: return v # retrieve name try: - v = socket.gethostbyaddr(ip.ntoa())[0] + if not isinstance(ip, IPAddr): + v = socket.gethostbyaddr(ip)[0] + else: + v = socket.gethostbyaddr(ip.ntoa())[0] except socket.error, e: logSys.debug("Unable to find a name for the IP %s: %s", ip, e) v = None diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index ae22b88f..b5a772d9 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -1314,9 +1314,9 @@ class DNSUtilsNetworkTests(unittest.TestCase): res = DNSUtils.textToIp('www.example.com', 'no') self.assertEqual(res, []) res = DNSUtils.textToIp('www.example.com', 'warn') - self.assertEqual(res, ['93.184.216.34']) + self.assertEqual(res, ['93.184.216.34', '2606:2800:220:1:248:1893:25c8:1946']) res = DNSUtils.textToIp('www.example.com', 'yes') - self.assertEqual(res, ['93.184.216.34']) + self.assertEqual(res, ['93.184.216.34', '2606:2800:220:1:248:1893:25c8:1946']) def testTextToIp(self): # Test hostnames @@ -1328,7 +1328,7 @@ class DNSUtilsNetworkTests(unittest.TestCase): for s in hostnames: res = DNSUtils.textToIp(s, 'yes') if s == 'www.example.com': - self.assertEqual(res, ['93.184.216.34']) + self.assertEqual(res, ['93.184.216.34', '2606:2800:220:1:248:1893:25c8:1946']) else: self.assertEqual(res, []) From 85b895178b8a6dd9ce7c9086c8c451b72efdb4c1 Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Wed, 2 Mar 2016 06:56:57 +0100 Subject: [PATCH 041/623] change IP address string to object handling part 1 # Conflicts: # fail2ban/server/filter.py --- fail2ban/server/actions.py | 9 ++++++--- fail2ban/server/banmanager.py | 6 +++--- fail2ban/server/database.py | 16 ++++++++++------ fail2ban/server/filter.py | 18 ++++++++++++------ fail2ban/server/ticket.py | 3 --- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 5469722f..428d1ccf 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -42,6 +42,7 @@ from .banmanager import BanManager from .jailthread import JailThread from .action import ActionBase, CommandAction, CallingMap from .mytime import MyTime +from .filter import IPAddr from .utils import Utils from ..helpers import getLogger @@ -180,7 +181,7 @@ class Actions(JailThread, Mapping): def getBanTime(self): return self.__banManager.getBanTime() - def removeBannedIP(self, ip): + def removeBannedIP(self, ipstr): """Removes banned IP calling actions' unban method Remove a banned IP now, rather than waiting for it to expire, @@ -188,14 +189,16 @@ class Actions(JailThread, Mapping): Parameters ---------- - ip : str - The IP address to unban + ipstr : str + The IP address string to unban Raises ------ ValueError If `ip` is not banned """ + # Create new IPAddr object from IP string + ip = IPAddr(ipstr) # Always delete ip from database (also if currently not banned) if self._jail.database is not None: self._jail.database.delBan(self._jail, ip) diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index 0ee028ef..67ae1b71 100644 --- a/fail2ban/server/banmanager.py +++ b/fail2ban/server/banmanager.py @@ -152,9 +152,9 @@ class BanManager: for banData in self.__banList: ip = banData.getIP() # Reference: http://www.team-cymru.org/Services/ip-to-asn.html#dns - # TODO: IPv6 compatibility - reversed_ip = ".".join(reversed(ip.split("."))) - question = "%s.origin.asn.cymru.com" % reversed_ip + question = ip.getPTR("origin.asn.cymru.com" if ip.isIPv4() + else "origin6.asn.cymru.com" + ) try: answers = dns.resolver.query(question, "TXT") for rdata in answers: diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index b7fd4d47..e5e51992 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -32,6 +32,7 @@ from threading import RLock from .mytime import MyTime from .ticket import FailTicket +from .filter import IPAddr from ..helpers import getLogger # Gets the instance of the logger. @@ -422,7 +423,7 @@ class Fail2BanDb(object): #TODO: Implement data parts once arbitrary match keys completed cur.execute( "INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)", - (jail.name, ticket.getIP(), int(round(ticket.getTime())), + (jail.name, ticket.getIP().ntoa(), int(round(ticket.getTime())), ticket.getData())) @commitandrollback @@ -436,7 +437,7 @@ class Fail2BanDb(object): ip : str IP to be removed. """ - queryArgs = (jail.name, ip); + queryArgs = (jail.name, ip.ntoa()); cur.execute( "DELETE FROM bans WHERE jail = ? AND ip = ?", queryArgs); @@ -454,7 +455,7 @@ class Fail2BanDb(object): queryArgs.append(MyTime.time() - bantime) if ip is not None: query += " AND ip=?" - queryArgs.append(ip) + queryArgs.append(ip.ntoa()) query += " ORDER BY ip, timeofban desc" return cur.execute(query, queryArgs) @@ -470,7 +471,7 @@ class Fail2BanDb(object): Ban time in seconds, such that bans returned would still be valid now. Negative values are equivalent to `None`. Default `None`; no limit. - ip : str + ip : IPAddr object IP Address to filter bans by. Default `None`; all IPs. Returns @@ -479,7 +480,8 @@ class Fail2BanDb(object): List of `Ticket`s for bans stored in database. """ tickets = [] - for ip, timeofban, data in self._getBans(**kwargs): + for ipstr, timeofban, data in self._getBans(**kwargs): + ip = IPAddr(ipstr) #TODO: Implement data parts once arbitrary match keys completed tickets.append(FailTicket(ip, timeofban)) tickets[-1].setData(data) @@ -499,7 +501,7 @@ class Fail2BanDb(object): Ban time in seconds, such that bans returned would still be valid now. Negative values are equivalent to `None`. Default `None`; no limit. - ip : str + ip : IPAddr object IP Address to filter bans by. Default `None`; all IPs. Returns @@ -520,6 +522,8 @@ class Fail2BanDb(object): ticket = None results = list(self._getBans(ip=ip, jail=jail, bantime=bantime)) + # Convert IP strings to IPAddr objects + results = map(lambda i:(IPAddr(i[0]),)+i[1:], results) if results: prev_banip = results[0][0] matches = [] diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 3682125c..371a13d7 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -306,13 +306,19 @@ class Filter(JailThread): def getIgnoreCommand(self): return self.__ignoreCommand + ## + # create new IPAddr object from IP address string + def newIP(self, ipstr): + return IPAddr(ipstr) + ## # Ban an IP - http://blogs.buanzo.com.ar/2009/04/fail2ban-patch-ban-ip-address-manually.html # Arturo 'Buanzo' Busleiman # # to enable banip fail2ban-client BAN command - def addBannedIP(self, ip): + def addBannedIP(self, ipstr): + ip = IPAddr(ipstr) if self.inIgnoreIPList(ip): logSys.warning('Requested to manually ban an ignored IP %s. User knows best. Proceeding to ban it.' % ip) @@ -540,11 +546,11 @@ class Filter(JailThread): if not checkAllRegex: break else: - ipMatch = DNSUtils.textToIp(host, self.__useDns) - if ipMatch: - for ip in ipMatch: - failList.append([failRegexIndex, ip, date, - failRegex.getMatchedLines()]) + ips = DNSUtils.textToIp(host, self.__useDns) + if ips: + for ip in ips: + failList.append([failRegexIndex, ip, + date, failRegex.getMatchedLines()]) if not checkAllRegex: break except RegexException, e: # pragma: no cover - unsure if reachable diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index b3eea052..7b7eb908 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -72,9 +72,6 @@ class Ticket: return False def setIP(self, value): - if isinstance(value, basestring): - # guarantee using regular str instead of unicode for the IP - value = str(value) self.__ip = value def getIP(self): From afe1f73af27503972a4843a770adf3e46f207cde Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 4 May 2016 19:44:52 +0200 Subject: [PATCH 042/623] meantime commit: code review, simplification, pythonization, etc. (test cases passed) unnecessarily code aggravation with explicit converting reverted - implicit converting inside internal functions if not IPAddr object; --- fail2ban/server/actions.py | 8 +- fail2ban/server/database.py | 21 ++-- fail2ban/server/failmanager.py | 9 ++ fail2ban/server/filter.py | 100 +++++++++-------- fail2ban/server/ticket.py | 4 + fail2ban/tests/failmanagertestcase.py | 3 +- fail2ban/tests/filtertestcase.py | 153 ++++++++++++++++++++++---- 7 files changed, 212 insertions(+), 86 deletions(-) diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 428d1ccf..1b72af91 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -181,7 +181,7 @@ class Actions(JailThread, Mapping): def getBanTime(self): return self.__banManager.getBanTime() - def removeBannedIP(self, ipstr): + def removeBannedIP(self, ip): """Removes banned IP calling actions' unban method Remove a banned IP now, rather than waiting for it to expire, @@ -189,16 +189,14 @@ class Actions(JailThread, Mapping): Parameters ---------- - ipstr : str - The IP address string to unban + ip : str or IPAddr + The IP address to unban Raises ------ ValueError If `ip` is not banned """ - # Create new IPAddr object from IP string - ip = IPAddr(ipstr) # Always delete ip from database (also if currently not banned) if self._jail.database is not None: self._jail.database.delBan(self._jail, ip) diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index e5e51992..6a3d87c3 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -32,7 +32,6 @@ from threading import RLock from .mytime import MyTime from .ticket import FailTicket -from .filter import IPAddr from ..helpers import getLogger # Gets the instance of the logger. @@ -412,18 +411,19 @@ class Fail2BanDb(object): ticket : BanTicket Ticket of the ban to be added. """ + ip = str(ticket.getIP()) try: - del self._bansMergedCache[(ticket.getIP(), jail)] + del self._bansMergedCache[(ip, jail)] except KeyError: pass try: - del self._bansMergedCache[(ticket.getIP(), None)] + del self._bansMergedCache[(ip, None)] except KeyError: pass #TODO: Implement data parts once arbitrary match keys completed cur.execute( "INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)", - (jail.name, ticket.getIP().ntoa(), int(round(ticket.getTime())), + (jail.name, ip, int(round(ticket.getTime())), ticket.getData())) @commitandrollback @@ -437,7 +437,7 @@ class Fail2BanDb(object): ip : str IP to be removed. """ - queryArgs = (jail.name, ip.ntoa()); + queryArgs = (jail.name, str(ip)); cur.execute( "DELETE FROM bans WHERE jail = ? AND ip = ?", queryArgs); @@ -455,7 +455,7 @@ class Fail2BanDb(object): queryArgs.append(MyTime.time() - bantime) if ip is not None: query += " AND ip=?" - queryArgs.append(ip.ntoa()) + queryArgs.append(ip) query += " ORDER BY ip, timeofban desc" return cur.execute(query, queryArgs) @@ -471,7 +471,7 @@ class Fail2BanDb(object): Ban time in seconds, such that bans returned would still be valid now. Negative values are equivalent to `None`. Default `None`; no limit. - ip : IPAddr object + ip : str IP Address to filter bans by. Default `None`; all IPs. Returns @@ -480,8 +480,7 @@ class Fail2BanDb(object): List of `Ticket`s for bans stored in database. """ tickets = [] - for ipstr, timeofban, data in self._getBans(**kwargs): - ip = IPAddr(ipstr) + for ip, timeofban, data in self._getBans(**kwargs): #TODO: Implement data parts once arbitrary match keys completed tickets.append(FailTicket(ip, timeofban)) tickets[-1].setData(data) @@ -501,7 +500,7 @@ class Fail2BanDb(object): Ban time in seconds, such that bans returned would still be valid now. Negative values are equivalent to `None`. Default `None`; no limit. - ip : IPAddr object + ip : str IP Address to filter bans by. Default `None`; all IPs. Returns @@ -522,8 +521,6 @@ class Fail2BanDb(object): ticket = None results = list(self._getBans(ip=ip, jail=jail, bantime=bantime)) - # Convert IP strings to IPAddr objects - results = map(lambda i:(IPAddr(i[0]),)+i[1:], results) if results: prev_banip = results[0][0] matches = [] diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index ae97b36a..b342b280 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -54,6 +54,15 @@ class FailManager: with self.__lock: return self.__failTotal + def getFailCount(self): + # may be slow on large list of failures, should be used for test purposes only... + with self.__lock: + return len(self.__failList), sum([f.getRetry() for f in self.__failList.values()]) + + def getFailTotal(self): + with self.__lock: + return self.__failTotal + def setMaxRetry(self, value): self.__maxRetry = value diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 371a13d7..04f34821 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -306,19 +306,15 @@ class Filter(JailThread): def getIgnoreCommand(self): return self.__ignoreCommand - ## - # create new IPAddr object from IP address string - def newIP(self, ipstr): - return IPAddr(ipstr) - ## # Ban an IP - http://blogs.buanzo.com.ar/2009/04/fail2ban-patch-ban-ip-address-manually.html # Arturo 'Buanzo' Busleiman # # to enable banip fail2ban-client BAN command - def addBannedIP(self, ipstr): - ip = IPAddr(ipstr) + def addBannedIP(self, ip): + if not isinstance(ip, IPAddr): + ip = IPAddr(ip) if self.inIgnoreIPList(ip): logSys.warning('Requested to manually ban an ignored IP %s. User knows best. Proceeding to ban it.' % ip) @@ -358,11 +354,11 @@ class Filter(JailThread): ip = IPAddr(s[0], s[1]) # log and append to ignore list - logSys.debug("Add " + ip + " to ignore list") + logSys.debug("Add %r to ignore list (%r, %r)", ip, s[0], s[1]) self.__ignoreIpList.append(ip) def delIgnoreIP(self, ip): - logSys.debug("Remove " + ip + " from ignore list") + logSys.debug("Remove %r from ignore list", ip) self.__ignoreIpList.remove(ip) def logIgnoreIp(self, ip, log_ignore, ignore_source="unknown source"): @@ -384,18 +380,9 @@ class Filter(JailThread): if not isinstance(ip, IPAddr): ip = IPAddr(ip) for net in self.__ignoreIpList: - # if it isn't a valid IP address, try DNS resolution - if not net.isValidIP() and net.getRaw() != "": - # Check if IP in DNS - ips = DNSUtils.dnsToIp(net.getRaw()) - if ip in ips: - self.logIgnoreIp(ip, log_ignore, ignore_source="dns") - return True - else: - continue # check if the IP is covered by ignore IP if ip.isInNet(net): - self.logIgnoreIp(ip, log_ignore, ignore_source="ip") + self.logIgnoreIp(ip, log_ignore, ignore_source=("ip" if net.isValidIP() else "dns")) return True if self.__ignoreCommand: @@ -1006,8 +993,6 @@ from .utils import Utils class DNSUtils: - IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") - # todo: make configurable the expired time and max count of cache entries: CACHE_nameToIp = Utils.Cache(maxCount=1000, maxTime=5*60) CACHE_ipToName = Utils.Cache(maxCount=1000, maxTime=5*60) @@ -1053,17 +1038,6 @@ class DNSUtils: DNSUtils.CACHE_ipToName.set(ip, v) return v - @staticmethod - def searchIP(text): - """ Search if an IP address if directly available and return - it. - """ - match = DNSUtils.IP_CRE.match(text) - if match: - return match - else: - return None - @staticmethod def isValidIP(string): """ Return true if str is a valid IP @@ -1082,7 +1056,7 @@ class DNSUtils: ipList = list() # Search for plain IP plainIP = IPAddr.searchIP(text) - if not plainIP is None: + if plainIP is not None: ip = IPAddr(plainIP.group(0)) if ip.isValidIP(): ipList.append(ip) @@ -1122,7 +1096,7 @@ class DNSUtils: # # This class contains methods for handling IPv4 and IPv6 addresses. -class IPAddr: +class IPAddr(object): """ provide functions to handle IPv4 and IPv6 addresses """ @@ -1136,8 +1110,22 @@ class IPAddr: valid = False raw = "" + # todo: make configurable the expired time and max count of cache entries: + CACHE_OBJ = Utils.Cache(maxCount=1000, maxTime=5*60) + + def __new__(cls, ipstring, cidr=-1): + # already correct IPAddr + args = (ipstring, cidr) + ip = IPAddr.CACHE_OBJ.get(args) + if ip is not None: + return ip + ip = super(IPAddr, cls).__new__(cls) + ip.__init(ipstring, cidr) + IPAddr.CACHE_OBJ.set(args, ip) + return ip + # object methods - def __init__(self, ipstring, cidr=-1): + def __init(self, ipstring, cidr=-1): """ initialize IP object by converting IP address string to binary to integer """ @@ -1193,7 +1181,9 @@ class IPAddr: return self.ntoa() def __eq__(self, other): - other = other if isinstance(other, IPAddr) else IPAddr(other) + if not isinstance(other, IPAddr): + if other is None: return False + other = IPAddr(other) if not self.valid and not other.valid: return self.raw == other.raw if not self.valid or not other.valid: return False if self.addr != other.addr: return False @@ -1202,7 +1192,9 @@ class IPAddr: return True def __ne__(self, other): - other = other if isinstance(other, IPAddr) else IPAddr(other) + if not isinstance(other, IPAddr): + if other is None: return True + other = IPAddr(other) if not self.valid and not other.valid: return self.raw != other.raw if self.addr != other.addr: return True if self.family != other.family: return True @@ -1210,7 +1202,9 @@ class IPAddr: return False def __lt__(self, other): - other = other if isinstance(other, IPAddr) else IPAddr(other) + if not isinstance(other, IPAddr): + if other is None: return False + other = IPAddr(other) return self.family < other.family or self.addr < other.addr def __add__(self, other): @@ -1220,7 +1214,9 @@ class IPAddr: return "%s%s" % (other, self) def __hash__(self): - return hash(self.addr)^hash((self.plen<<16)|self.family) + # should be the same as by string (because of possible compare with string): + return hash(self.ntoa()) + #return hash(self.addr)^hash((self.plen<<16)|self.family) def hexdump(self): """ dump the ip address in as a hex sequence in @@ -1302,6 +1298,11 @@ class IPAddr: """ returns true if the IP object is in the provided network (object) """ + # if it isn't a valid IP address, try DNS resolution + if not net.isValidIP() and net.getRaw() != "": + # Check if IP in DNS + return self in DNSUtils.dnsToIp(net.getRaw()) + if self.family != net.family: return False @@ -1318,18 +1319,27 @@ class IPAddr: return False + @property + def maskplen(self): + plen = 0 + if (hasattr(self, '_maskplen')): + return self._plen + maddr = self.addr + while maddr: + if not (maddr & 0x80000000): + raise ValueError("invalid mask %r, no plen representation" % (self.ntoa(),)) + maddr = (maddr << 1) & 0xFFFFFFFFL + plen += 1 + self._maskplen = plen + return plen + @staticmethod def masktoplen(maskstr): """ converts mask string to prefix length only used for IPv4 masks """ - mask = IPAddr(maskstr) - plen = 0 - while mask.addr: - mask.addr = (mask.addr << 1) & 0xFFFFFFFFL - plen += 1 - return plen + return IPAddr(maskstr).maskplen @staticmethod diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index 7b7eb908..3307a6c9 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -72,6 +72,10 @@ class Ticket: return False def setIP(self, value): + # guarantee using IPAddr instead of unicode, str for the IP + if isinstance(value, basestring): + from .filter import IPAddr + value = IPAddr(value) self.__ip = value def getIP(self): diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index a8a71723..36bc87a3 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -28,6 +28,7 @@ import unittest from ..server import failmanager from ..server.failmanager import FailManager, FailManagerEmpty +from ..server.filter import IPAddr from ..server.ticket import FailTicket @@ -140,7 +141,7 @@ class AddFailure(unittest.TestCase): #ticket = FailTicket('193.168.0.128', None) ticket = self.__failManager.toBan() self.assertEqual(ticket.getIP(), "193.168.0.128") - self.assertTrue(isinstance(ticket.getIP(), str)) + self.assertTrue(isinstance(ticket.getIP(), (str, IPAddr))) # finish with rudimentary tests of the ticket # verify consistent str diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index b5a772d9..7198185a 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -38,7 +38,7 @@ except ImportError: from ..server.jail import Jail from ..server.filterpoll import FilterPoll -from ..server.filter import Filter, FileFilter, FileContainer, DNSUtils +from ..server.filter import Filter, FileFilter, FileContainer, DNSUtils, IPAddr from ..server.failmanager import FailManagerEmpty from ..server.mytime import MyTime from ..server.utils import Utils @@ -154,19 +154,41 @@ def _assert_correct_last_attempt(utest, filter_, output, count=None): Test filter to contain target ticket """ + # one or multiple tickets: + if not isinstance(output[0], (tuple,list)): + tickcount = 1 + failcount = (count if count else output[1]) + else: + tickcount = len(output) + failcount = (count if count else sum((o[1] for o in output))) + + found = [] if isinstance(filter_, DummyJail): # get fail ticket from jail - found = _ticket_tuple(filter_.getFailTicket()) + found.append(_ticket_tuple(filter_.getFailTicket())) else: # when we are testing without jails # wait for failures (up to max time) Utils.wait_for( - lambda: filter_.failManager.getFailTotal() >= (count if count else output[1]), + lambda: filter_.failManager.getFailCount() >= (tickcount, failcount), _maxWaitTime(10)) - # get fail ticket from filter - found = _ticket_tuple(filter_.failManager.toBan()) + # get fail ticket(s) from filter + while tickcount: + try: + found.append(_ticket_tuple(filter_.failManager.toBan())) + except FailManagerEmpty: + break + tickcount -= 1 - _assert_equal_entries(utest, found, output, count) + if not isinstance(output[0], (tuple,list)): + utest.assertEqual(len(found), 1) + _assert_equal_entries(utest, found[0], output, count) + else: + # sort by string representation of ip (multiple failures with different ips): + found = sorted(found, key=lambda x: str(x)) + output = sorted(output, key=lambda x: str(x)) + for f, o in zip(found, output): + _assert_equal_entries(utest, f, o) def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line=""): @@ -315,6 +337,10 @@ class IgnoreIP(LogCaptureTestCase): self.assertFalse(self.filter.inIgnoreIPList('192.168.1.255')) self.assertFalse(self.filter.inIgnoreIPList('192.168.0.255')) + def testWrongIPMask(self): + self.filter.addIgnoreIP('192.168.1.0/255.255.0.0') + self.assertRaises(ValueError, self.filter.addIgnoreIP, '192.168.1.0/255.255.0.128') + def testIgnoreInProcessLine(self): setUpMyTime() self.filter.addIgnoreIP('192.168.1.0/25') @@ -345,16 +371,21 @@ class IgnoreIP(LogCaptureTestCase): self.assertNotLogged("[%s] Ignore %s by %s" % (self.jail.name, "example.com", "NOT_LOGGED")) -class IgnoreIPDNS(IgnoreIP): +class IgnoreIPDNS(LogCaptureTestCase): def setUp(self): """Call before every test case.""" unittest.F2B.SkipIfNoNetwork() - IgnoreIP.setUp(self) + LogCaptureTestCase.setUp(self) + self.jail = DummyJail() + self.filter = FileFilter(self.jail) def testIgnoreIPDNSOK(self): self.filter.addIgnoreIP("www.epfl.ch") self.assertTrue(self.filter.inIgnoreIPList("128.178.50.12")) + self.filter.addIgnoreIP("example.com") + self.assertTrue(self.filter.inIgnoreIPList("93.184.216.34")) + self.assertTrue(self.filter.inIgnoreIPList("2606:2800:220:1:248:1893:25c8:1946")) def testIgnoreIPDNSNOK(self): # Test DNS @@ -1109,18 +1140,18 @@ class GetFailures(LogCaptureTestCase): _assert_correct_last_attempt(self, self.filter, output) def testGetFailures04(self): - output = [('212.41.96.186', 4, 1124013600.0), - ('212.41.96.185', 4, 1124017198.0)] + # because of not exact time in testcase04.log (no year), we should always use our test time: + self.assertEqual(MyTime.time(), 1124013600) + # should find exact 4 failures for *.186 and 2 failures for *.185 + output = (('212.41.96.186', 4, 1124013600.0), + ('212.41.96.185', 2, 1124013598.0)) + self.filter.setMaxRetry(2) self.filter.addLogPath(GetFailures.FILENAME_04, autoSeek=0) self.filter.addFailRegex("Invalid user .* ") self.filter.getFailures(GetFailures.FILENAME_04) - try: - for i, out in enumerate(output): - _assert_correct_last_attempt(self, self.filter, out) - except FailManagerEmpty: - pass + _assert_correct_last_attempt(self, self.filter, output) def testGetFailuresWrongChar(self): # write wrong utf-8 char: @@ -1160,20 +1191,31 @@ class GetFailures(LogCaptureTestCase): def testGetFailuresUseDNS(self): unittest.F2B.SkipIfNoNetwork() # We should still catch failures with usedns = no ;-) - output_yes = ('93.184.216.34', 2, 1124013539.0, - [u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2', - u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.34 port 51332 ssh2']) + output_yes = ( + ('93.184.216.34', 2, 1124013539.0, + [u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2', + u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.34 port 51332 ssh2'] + ), + ('2606:2800:220:1:248:1893:25c8:1946', 1, 1124013299.0, + [u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2'] + ), + ) - output_no = ('93.184.216.34', 1, 1124013539.0, - [u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.34 port 51332 ssh2']) + output_no = ( + ('93.184.216.34', 1, 1124013539.0, + [u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.34 port 51332 ssh2'] + ) + ) # Actually no exception would be raised -- it will be just set to 'no' #self.assertRaises(ValueError, # FileFilter, None, useDns='wrong_value_for_useDns') - for useDns, output in (('yes', output_yes), - ('no', output_no), - ('warn', output_yes)): + for useDns, output in ( + ('yes', output_yes), + ('no', output_no), + ('warn', output_yes) + ): jail = DummyJail() filter_ = FileFilter(jail, useDns=useDns) filter_.active = True @@ -1356,6 +1398,71 @@ class DNSUtilsNetworkTests(unittest.TestCase): res = DNSUtils.bin2addr(167772160L) self.assertEqual(res, '10.0.0.0') + def testIPAddr_Equal6(self): + self.assertEqual( + IPAddr('2606:2800:220:1:248:1893::'), + IPAddr('2606:2800:220:1:248:1893:0:0') + ) + + def testIPAddr_Compare(self): + ip4 = [ + IPAddr('93.184.0.1'), + IPAddr('93.184.216.1'), + IPAddr('93.184.216.34') + ] + ip6 = [ + IPAddr('2606:2800:220:1:248:1893::'), + IPAddr('2606:2800:220:1:248:1893:25c8:0'), + IPAddr('2606:2800:220:1:248:1893:25c8:1946') + ] + # ip4 + self.assertNotEqual(ip4[0], None) + self.assertTrue(ip4[0] is not None) + self.assertFalse(ip4[0] is None) + self.assertLess(None, ip4[0]) + self.assertLess(ip4[0], ip4[1]) + self.assertLess(ip4[1], ip4[2]) + self.assertEqual(sorted(reversed(ip4)), ip4) + # ip6 + self.assertNotEqual(ip6[0], None) + self.assertTrue(ip6[0] is not None) + self.assertFalse(ip6[0] is None) + self.assertLess(None, ip6[0]) + self.assertLess(ip6[0], ip6[1]) + self.assertLess(ip6[1], ip6[2]) + self.assertEqual(sorted(reversed(ip6)), ip6) + # ip4 vs ip6 + self.assertNotEqual(ip4[0], ip6[0]) + self.assertLess(ip4[0], ip6[0]) + self.assertLess(ip4[2], ip6[2]) + self.assertEqual(sorted(reversed(ip4+ip6)), ip4+ip6) + # hashing (with string as key): + d={ + '93.184.216.34': 'ip4-test', + '2606:2800:220:1:248:1893:25c8:1946': 'ip6-test' + } + d2 = dict([(IPAddr(k), v) for k, v in d.iteritems()]) + self.assertTrue(isinstance(d.keys()[0], basestring)) + self.assertTrue(isinstance(d2.keys()[0], IPAddr)) + self.assertEqual(d.get(ip4[2], ''), 'ip4-test') + self.assertEqual(d.get(ip6[2], ''), 'ip6-test') + self.assertEqual(d2.get(str(ip4[2]), ''), 'ip4-test') + self.assertEqual(d2.get(str(ip6[2]), ''), 'ip6-test') + # compare with string direct: + self.assertEqual(d, d2) + + def testIPAddr_CompareDNS(self): + ips = IPAddr('example.com') + self.assertTrue(IPAddr("93.184.216.34").isInNet(ips)) + self.assertTrue(IPAddr("2606:2800:220:1:248:1893:25c8:1946").isInNet(ips)) + + def testIPAddr_Cached(self): + ips = [DNSUtils.dnsToIp('example.com'), DNSUtils.dnsToIp('example.com')] + for ip1, ip2 in zip(ips, ips): + self.assertEqual(id(ip1), id(ip2)) + ip1 = IPAddr('93.184.216.34'); ip2 = IPAddr('93.184.216.34'); self.assertEqual(id(ip1), id(ip2)) + ip1 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); ip2 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); self.assertEqual(id(ip1), id(ip2)) + class JailTests(unittest.TestCase): From d125f882d4fd1d1d9e096aa328cdb3eec6abac5e Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Sat, 5 Mar 2016 12:02:36 +0100 Subject: [PATCH 043/623] explicitely treat join arguments as string in beautifier --- fail2ban/client/beautifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 812fbe65..308cb035 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -78,13 +78,13 @@ class Beautifier: prefix1 = " " if n == len(response) - 1 else "| " for m, res2 in enumerate(res1[1]): prefix2 = prefix1 + ("`-" if m == len(res1[1]) - 1 else "|-") - val = " ".join(res2[1]) if isinstance(res2[1], list) else res2[1] + val = " ".join(map(str, res2[1])) if isinstance(res2[1], list) else res2[1] msg.append("%s %s:\t%s" % (prefix2, res2[0], val)) else: msg = ["Status"] for n, res1 in enumerate(response): prefix1 = "`-" if n == len(response) - 1 else "|-" - val = " ".join(res1[1]) if isinstance(res1[1], list) else res1[1] + val = " ".join(map(str, res1[1])) if isinstance(res1[1], list) else res1[1] msg.append("%s %s:\t%s" % (prefix1, res1[0], val)) msg = "\n".join(msg) elif inC[1] == "syslogsocket": From db9f3f738f88ef0f7ae535a578055666f1843292 Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Mon, 14 Mar 2016 23:42:06 +0100 Subject: [PATCH 044/623] add ip6-loopback to default ignoreip statement --- config/jail.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/jail.conf b/config/jail.conf index b41ce24c..36c4eecb 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -47,7 +47,7 @@ before = paths-debian.conf # "ignoreip" can be an IP address, a CIDR mask or a DNS host. Fail2ban will not # ban a host which matches an address in this list. Several addresses can be # defined using space (and/or comma) separator. -ignoreip = 127.0.0.1/8 +ignoreip = 127.0.0.1/8 ::1 # External command that will take an tagged arguments to ignore, e.g. , # and return true if the IP is to be ignored. False otherwise. From ce196744d1d509c895995c759ee2f4d014195ac1 Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Mon, 14 Mar 2016 23:29:12 +0100 Subject: [PATCH 045/623] Update ChangeLog / THANKS entries --- ChangeLog | 8 ++++++++ THANKS | 1 + 2 files changed, 9 insertions(+) diff --git a/ChangeLog b/ChangeLog index 033cd9ec..0bebb471 100644 --- a/ChangeLog +++ b/ChangeLog @@ -78,6 +78,14 @@ ver. 0.9.4 (2016/03/08) - for-you-ladies * sshd filter got new failregex to match "maximum authentication attempts exceeded" (introduced in openssh 6.8) * Added filter for Mac OS screen sharing (VNC) daemon + * IPv6 support: + - IP addresses are now handled as objects rather than strings capable for + handling both address types IPv4 and IPv6 + - iptables related actions have been amended to support IPv6 specific actions + additionally + - hostsdeny and route actions have been tested to be aware of v4 and v6 already + - pf action for *BSD systems has been improved and supports now also v4 and v6 + - Name resolution is now working for either address type - Enhancements: * Do not rotate empty log files diff --git a/THANKS b/THANKS index 64de43a4..c8a019a6 100644 --- a/THANKS +++ b/THANKS @@ -12,6 +12,7 @@ Adrien Clerc ache ag4ve (Shawn) Alasdair D. Campbell +Alexander Koeppe (IPv6 support) Alexandre Perrin (kAworu) Amir Caspi Amy From 9ede535a616802434ecb9826ed5c40a6ac81317f Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Tue, 15 Mar 2016 00:36:08 +0100 Subject: [PATCH 046/623] remove obsolete IP related code from DNSUtils class # Conflicts: # fail2ban/server/filter.py --- fail2ban/server/filter.py | 36 +++--------------------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 04f34821..baf4bc9b 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -981,10 +981,10 @@ class JournalFilter(Filter): # pragma: systemd no cover return [] ## -# Utils class for DNS and IP handling. +# Utils class for DNS handling. +# +# This class contains only static methods used to handle DNS # -# This class contains only static methods used to handle DNS and IP -# addresses. import socket import struct @@ -1038,17 +1038,6 @@ class DNSUtils: DNSUtils.CACHE_ipToName.set(ip, v) return v - @staticmethod - def isValidIP(string): - """ Return true if str is a valid IP - """ - s = string.split('/', 1) - try: - socket.inet_aton(s[0]) - return True - except socket.error: # pragma: no cover - return False - @staticmethod def textToIp(text, useDns): """ Return the IP of DNS found in a given text. @@ -1072,25 +1061,6 @@ class DNSUtils: return ipList - @staticmethod - def addr2bin(ipstring, cidr=None): - """ Convert a string IPv4 address into binary form. - If cidr is supplied, return the network address for the given block - """ - if cidr is None: - return struct.unpack("!L", socket.inet_aton(ipstring))[0] - else: - MASK = 0xFFFFFFFFL - return ~(MASK >> cidr) & MASK & DNSUtils.addr2bin(ipstring) - - @staticmethod - def bin2addr(ipbin): - """ Convert a binary IPv4 address into string n.n.n.n form. - """ - return socket.inet_ntoa(struct.pack("!L", ipbin)) - - - ## # Class for IP address handling. # From 6985531e91085c0daa7aa746c909a1293bc91bc2 Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 9 May 2016 16:23:59 +0200 Subject: [PATCH 047/623] meantime commit: code review, simplification, pythonization, etc. (test cases passed) --- fail2ban/server/filter.py | 9 ++++++--- fail2ban/tests/filtertestcase.py | 24 ++++++++++-------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index baf4bc9b..b808a6e3 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -1102,11 +1102,10 @@ class IPAddr(object): for family in [socket.AF_INET, socket.AF_INET6]: try: binary = socket.inet_pton(family, ipstring) - except socket.error: - continue - else: self.valid = True break + except socket.error: + continue if self.valid and family == socket.AF_INET: # convert host to network byte order @@ -1178,9 +1177,13 @@ class IPAddr(object): return self.family < other.family or self.addr < other.addr def __add__(self, other): + if not isinstance(other, IPAddr): + other = IPAddr(other) return "%s%s" % (self, other) def __radd__(self, other): + if not isinstance(other, IPAddr): + other = IPAddr(other) return "%s%s" % (other, self) def __hash__(self): diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 7198185a..371db3bc 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -1383,20 +1383,16 @@ class DNSUtilsNetworkTests(unittest.TestCase): self.assertEqual(res, None) def testAddr2bin(self): - res = DNSUtils.addr2bin('10.0.0.0') - self.assertEqual(res, 167772160L) - res = DNSUtils.addr2bin('10.0.0.0', cidr=None) - self.assertEqual(res, 167772160L) - res = DNSUtils.addr2bin('10.0.0.0', cidr=32L) - self.assertEqual(res, 167772160L) - res = DNSUtils.addr2bin('10.0.0.1', cidr=32L) - self.assertEqual(res, 167772161L) - res = DNSUtils.addr2bin('10.0.0.1', cidr=31L) - self.assertEqual(res, 167772160L) - - def testBin2addr(self): - res = DNSUtils.bin2addr(167772160L) - self.assertEqual(res, '10.0.0.0') + res = IPAddr('10.0.0.0') + self.assertEqual(res.addr, 167772160L) + res = IPAddr('10.0.0.0', cidr=None) + self.assertEqual(res.addr, 167772160L) + res = IPAddr('10.0.0.0', cidr=32L) + self.assertEqual(res.addr, 167772160L) + res = IPAddr('10.0.0.1', cidr=32L) + self.assertEqual(res.addr, 167772161L) + res = IPAddr('10.0.0.1', cidr=31L) + self.assertEqual(res.addr, 167772160L) def testIPAddr_Equal6(self): self.assertEqual( From 8cb4a3f59ef1a8078ef6919ce587bb6ebed66711 Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 9 May 2016 17:00:35 +0200 Subject: [PATCH 048/623] move DNTUtils, IPAddr related code to dedicated source file ipdns.py (also resolves some cyclic import references) --- MANIFEST | 1 + .../ignorecommands/apache-fakegooglebot | 4 +- fail2ban/server/filter.py | 351 +--------------- fail2ban/server/ipdns.py | 389 ++++++++++++++++++ fail2ban/server/ticket.py | 2 +- fail2ban/tests/failmanagertestcase.py | 2 +- fail2ban/tests/filtertestcase.py | 3 +- fail2ban/tests/utils.py | 2 +- 8 files changed, 398 insertions(+), 356 deletions(-) create mode 100644 fail2ban/server/ipdns.py diff --git a/MANIFEST b/MANIFEST index 4ebf3fad..3a5b4477 100644 --- a/MANIFEST +++ b/MANIFEST @@ -164,6 +164,7 @@ fail2ban/client/jailreader.py fail2ban/client/jailsreader.py fail2ban/exceptions.py fail2ban/helpers.py +fail2ban/ipdns.py fail2ban/__init__.py fail2ban/protocol.py fail2ban/server/action.py diff --git a/config/filter.d/ignorecommands/apache-fakegooglebot b/config/filter.d/ignorecommands/apache-fakegooglebot index 19fb5107..86a28eaa 100755 --- a/config/filter.d/ignorecommands/apache-fakegooglebot +++ b/config/filter.d/ignorecommands/apache-fakegooglebot @@ -14,7 +14,7 @@ def process_args(argv): ip = argv[1] - from fail2ban.server.filter import DNSUtils + from fail2ban.server.ipdns import DNSUtils if not DNSUtils.isValidIP(ip): sys.stderr.write("Argument must be a single valid IP. Got: %s\n" % ip) @@ -23,7 +23,7 @@ def process_args(argv): def is_googlebot(ip): import re - from fail2ban.server.filter import DNSUtils + from fail2ban.server.ipdns import DNSUtils host = DNSUtils.ipToName(ip) if not host or not re.match('.*\.google(bot)?\.com$', host): diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index b808a6e3..49aba907 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -31,6 +31,7 @@ import re import sys from .failmanager import FailManagerEmpty, FailManager +from .ipdns import DNSUtils, IPAddr from .ticket import FailTicket from .jailthread import JailThread from .datedetector import DateDetector @@ -980,353 +981,3 @@ class JournalFilter(Filter): # pragma: systemd no cover def getJournalMatch(self, match): # pragma: no cover - Base class, not used return [] -## -# Utils class for DNS handling. -# -# This class contains only static methods used to handle DNS -# - -import socket -import struct -from .utils import Utils - - -class DNSUtils: - - # todo: make configurable the expired time and max count of cache entries: - CACHE_nameToIp = Utils.Cache(maxCount=1000, maxTime=5*60) - CACHE_ipToName = Utils.Cache(maxCount=1000, maxTime=5*60) - - @staticmethod - def dnsToIp(dns): - """ Convert a DNS into an IP address using the Python socket module. - Thanks to Kevin Drapel. - """ - # cache, also prevent long wait during retrieving of ip for wrong dns or lazy dns-system: - ips = DNSUtils.CACHE_nameToIp.get(dns) - if ips is not None: - return ips - # retrieve ips - try: - ips = list() - for result in socket.getaddrinfo(dns, None, 0, 0, socket.IPPROTO_TCP): - ip = IPAddr(result[4][0]) - if ip.isValidIP(): - ips.append(ip) - except socket.error, e: - # todo: make configurable the expired time of cache entry: - logSys.warning("Unable to find a corresponding IP address for %s: %s", dns, e) - ips = list() - DNSUtils.CACHE_nameToIp.set(dns, ips) - return ips - - @staticmethod - def ipToName(ip): - # cache, also prevent long wait during retrieving of name for wrong addresses, lazy dns: - v = DNSUtils.CACHE_ipToName.get(ip, ()) - if v != (): - return v - # retrieve name - try: - if not isinstance(ip, IPAddr): - v = socket.gethostbyaddr(ip)[0] - else: - v = socket.gethostbyaddr(ip.ntoa())[0] - except socket.error, e: - logSys.debug("Unable to find a name for the IP %s: %s", ip, e) - v = None - DNSUtils.CACHE_ipToName.set(ip, v) - return v - - @staticmethod - def textToIp(text, useDns): - """ Return the IP of DNS found in a given text. - """ - ipList = list() - # Search for plain IP - plainIP = IPAddr.searchIP(text) - if plainIP is not None: - ip = IPAddr(plainIP.group(0)) - if ip.isValidIP(): - ipList.append(ip) - - # If we are allowed to resolve -- give it a try if nothing was found - if useDns in ("yes", "warn") and not ipList: - # Try to get IP from possible DNS - ip = DNSUtils.dnsToIp(text) - ipList.extend(ip) - if ip and useDns == "warn": - logSys.warning("Determined IP using DNS Lookup: %s = %s", - text, ipList) - - return ipList - -## -# Class for IP address handling. -# -# This class contains methods for handling IPv4 and IPv6 addresses. - -class IPAddr(object): - """ provide functions to handle IPv4 and IPv6 addresses - """ - - IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") - IP6_CRE = re.compile("^[0-9a-fA-F]{4}[0-9a-fA-F:]+:[0-9a-fA-F]{1,4}|::1$") - - # object attributes - addr = 0 - family = socket.AF_UNSPEC - plen = 0 - valid = False - raw = "" - - # todo: make configurable the expired time and max count of cache entries: - CACHE_OBJ = Utils.Cache(maxCount=1000, maxTime=5*60) - - def __new__(cls, ipstring, cidr=-1): - # already correct IPAddr - args = (ipstring, cidr) - ip = IPAddr.CACHE_OBJ.get(args) - if ip is not None: - return ip - ip = super(IPAddr, cls).__new__(cls) - ip.__init(ipstring, cidr) - IPAddr.CACHE_OBJ.set(args, ip) - return ip - - # object methods - def __init(self, ipstring, cidr=-1): - """ initialize IP object by converting IP address string - to binary to integer - """ - for family in [socket.AF_INET, socket.AF_INET6]: - try: - binary = socket.inet_pton(family, ipstring) - self.valid = True - break - except socket.error: - continue - - if self.valid and family == socket.AF_INET: - # convert host to network byte order - self.addr, = struct.unpack("!L", binary) - self.family = family - self.plen = 32 - - # mask out host portion if prefix length is supplied - if cidr != None and cidr >= 0: - mask = ~(0xFFFFFFFFL >> cidr) - self.addr = self.addr & mask - self.plen = cidr - - elif self.valid and family == socket.AF_INET6: - # convert host to network byte order - hi, lo = struct.unpack("!QQ", binary) - self.addr = (hi << 64) | lo - self.family = family - self.plen = 128 - - # mask out host portion if prefix length is supplied - if cidr != None and cidr >= 0: - mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> cidr) - self.addr = self.addr & mask - self.plen = cidr - - # if IPv6 address is a IPv4-compatible, make instance a IPv4 - elif self.isInNet(IPAddr("::ffff:0:0", 96)): - self.addr = lo & 0xFFFFFFFFL - self.family = socket.AF_INET - self.plen = 32 - else: - # string couldn't be converted neither to a IPv4 nor - # to a IPv6 address - retain raw input for later use - # (e.g. DNS resolution) - self.raw = ipstring - - def __repr__(self): - return self.ntoa() - - def __str__(self): - return self.ntoa() - - def __eq__(self, other): - if not isinstance(other, IPAddr): - if other is None: return False - other = IPAddr(other) - if not self.valid and not other.valid: return self.raw == other.raw - if not self.valid or not other.valid: return False - if self.addr != other.addr: return False - if self.family != other.family: return False - if self.plen != other.plen: return False - return True - - def __ne__(self, other): - if not isinstance(other, IPAddr): - if other is None: return True - other = IPAddr(other) - if not self.valid and not other.valid: return self.raw != other.raw - if self.addr != other.addr: return True - if self.family != other.family: return True - if self.plen != other.plen: return True - return False - - def __lt__(self, other): - if not isinstance(other, IPAddr): - if other is None: return False - other = IPAddr(other) - return self.family < other.family or self.addr < other.addr - - def __add__(self, other): - if not isinstance(other, IPAddr): - other = IPAddr(other) - return "%s%s" % (self, other) - - def __radd__(self, other): - if not isinstance(other, IPAddr): - other = IPAddr(other) - return "%s%s" % (other, self) - - def __hash__(self): - # should be the same as by string (because of possible compare with string): - return hash(self.ntoa()) - #return hash(self.addr)^hash((self.plen<<16)|self.family) - - def hexdump(self): - """ dump the ip address in as a hex sequence in - network byte order - for debug purpose - """ - if self.family == socket.AF_INET: - return "%08x" % self.addr - elif self.family == socket.AF_INET6: - return "%032x" % self.addr - else: - return "" - - def ntoa(self): - """ represent IP object as text like the depricated - C pendant inet_ntoa() but address family independent - """ - if self.family == socket.AF_INET: - # convert network to host byte order - binary = struct.pack("!L", self.addr) - elif self.family == socket.AF_INET6: - # convert network to host byte order - hi = self.addr >> 64 - lo = self.addr & 0xFFFFFFFFFFFFFFFFL - binary = struct.pack("!QQ", hi, lo) - else: - return self.getRaw() - - return socket.inet_ntop(self.family, binary) - - def getPTR(self, suffix=""): - """ generates the DNS PTR string of the provided IP address object - if "suffix" is provided it will be appended as the second and top - level reverse domain. - if omitted it is implicitely set to the second and top level reverse - domain of the according IP address family - """ - if self.family == socket.AF_INET: - reversed_ip = ".".join(reversed(self.ntoa().split("."))) - if not suffix: - suffix = "in-addr.arpa." - - return "%s.%s" % (reversed_ip, suffix) - - elif self.family == socket.AF_INET6: - reversed_ip = ".".join(reversed(self.hexdump())) - if not suffix: - suffix = "ip6.arpa." - - return "%s.%s" % (reversed_ip, suffix) - - else: - return "" - - def isIPv4(self): - """ return true if the IP object is of address family AF_INET - """ - return True if self.family == socket.AF_INET else False - - def isIPv6(self): - """ return true if the IP object is of address family AF_INET6 - """ - return True if self.family == socket.AF_INET6 else False - - def getRaw(self): - """ returns the raw attribute - should only be set - to a non-empty string if prior address conversion - wasn't possible - """ - return self.raw - - def isValidIP(self): - """ returns true if the IP object has been created - from a valid IP address or false if not - """ - return self.valid - - - def isInNet(self, net): - """ returns true if the IP object is in the provided - network (object) - """ - # if it isn't a valid IP address, try DNS resolution - if not net.isValidIP() and net.getRaw() != "": - # Check if IP in DNS - return self in DNSUtils.dnsToIp(net.getRaw()) - - if self.family != net.family: - return False - - if self.family == socket.AF_INET: - mask = ~(0xFFFFFFFFL >> net.plen) - - elif self.family == socket.AF_INET6: - mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> net.plen) - else: - return False - - if self.addr & mask == net.addr: - return True - - return False - - @property - def maskplen(self): - plen = 0 - if (hasattr(self, '_maskplen')): - return self._plen - maddr = self.addr - while maddr: - if not (maddr & 0x80000000): - raise ValueError("invalid mask %r, no plen representation" % (self.ntoa(),)) - maddr = (maddr << 1) & 0xFFFFFFFFL - plen += 1 - self._maskplen = plen - return plen - - - @staticmethod - def masktoplen(maskstr): - """ converts mask string to prefix length - only used for IPv4 masks - """ - return IPAddr(maskstr).maskplen - - - @staticmethod - def searchIP(text): - """ Search if an IP address if directly available and return - it. - """ - match = IPAddr.IP_CRE.match(text) - if match: - return match - else: - match = IPAddr.IP6_CRE.match(text) - if match: - return match - else: - return None - diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py new file mode 100644 index 00000000..e75cd9ed --- /dev/null +++ b/fail2ban/server/ipdns.py @@ -0,0 +1,389 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : + +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +__author__ = "Fail2Ban Developers, Alexander Koeppe, Serg G. Brester" +__copyright__ = "Copyright (c) 2004-2016 Fail2ban Developers" +__license__ = "GPL" + +import re +import socket +import struct + +from .utils import Utils +from ..helpers import getLogger + +# Gets the instance of the logger. +logSys = getLogger(__name__) + + +## +# Helper functions +# +# +def asip(ip): + """A little helper to guarantee ip being an IPAddr instance""" + if isinstance(ip, IPAddr): + return ip + return IPAddr(ip) + + +## +# Utils class for DNS handling. +# +# This class contains only static methods used to handle DNS +# +class DNSUtils: + + # todo: make configurable the expired time and max count of cache entries: + CACHE_nameToIp = Utils.Cache(maxCount=1000, maxTime=5*60) + CACHE_ipToName = Utils.Cache(maxCount=1000, maxTime=5*60) + + @staticmethod + def dnsToIp(dns): + """ Convert a DNS into an IP address using the Python socket module. + Thanks to Kevin Drapel. + """ + # cache, also prevent long wait during retrieving of ip for wrong dns or lazy dns-system: + ips = DNSUtils.CACHE_nameToIp.get(dns) + if ips is not None: + return ips + # retrieve ips + try: + ips = list() + for result in socket.getaddrinfo(dns, None, 0, 0, socket.IPPROTO_TCP): + ip = IPAddr(result[4][0]) + if ip.isValidIP(): + ips.append(ip) + except socket.error, e: + # todo: make configurable the expired time of cache entry: + logSys.warning("Unable to find a corresponding IP address for %s: %s", dns, e) + ips = list() + DNSUtils.CACHE_nameToIp.set(dns, ips) + return ips + + @staticmethod + def ipToName(ip): + # cache, also prevent long wait during retrieving of name for wrong addresses, lazy dns: + v = DNSUtils.CACHE_ipToName.get(ip, ()) + if v != (): + return v + # retrieve name + try: + if not isinstance(ip, IPAddr): + v = socket.gethostbyaddr(ip)[0] + else: + v = socket.gethostbyaddr(ip.ntoa())[0] + except socket.error, e: + logSys.debug("Unable to find a name for the IP %s: %s", ip, e) + v = None + DNSUtils.CACHE_ipToName.set(ip, v) + return v + + @staticmethod + def textToIp(text, useDns): + """ Return the IP of DNS found in a given text. + """ + ipList = list() + # Search for plain IP + plainIP = IPAddr.searchIP(text) + if plainIP is not None: + ip = IPAddr(plainIP.group(0)) + if ip.isValidIP(): + ipList.append(ip) + + # If we are allowed to resolve -- give it a try if nothing was found + if useDns in ("yes", "warn") and not ipList: + # Try to get IP from possible DNS + ip = DNSUtils.dnsToIp(text) + ipList.extend(ip) + if ip and useDns == "warn": + logSys.warning("Determined IP using DNS Lookup: %s = %s", + text, ipList) + + return ipList + + +## +# Class for IP address handling. +# +# This class contains methods for handling IPv4 and IPv6 addresses. +class IPAddr(object): + """ provide functions to handle IPv4 and IPv6 addresses + """ + + IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") + IP6_CRE = re.compile("^[0-9a-fA-F]{4}[0-9a-fA-F:]+:[0-9a-fA-F]{1,4}|::1$") + + # object attributes + addr = 0 + family = socket.AF_UNSPEC + plen = 0 + valid = False + raw = "" + + # todo: make configurable the expired time and max count of cache entries: + CACHE_OBJ = Utils.Cache(maxCount=1000, maxTime=5*60) + + def __new__(cls, ipstring, cidr=-1): + # already correct IPAddr + args = (ipstring, cidr) + ip = IPAddr.CACHE_OBJ.get(args) + if ip is not None: + return ip + ip = super(IPAddr, cls).__new__(cls) + ip.__init(ipstring, cidr) + IPAddr.CACHE_OBJ.set(args, ip) + return ip + + # object methods + def __init(self, ipstring, cidr=-1): + """ initialize IP object by converting IP address string + to binary to integer + """ + for family in [socket.AF_INET, socket.AF_INET6]: + try: + binary = socket.inet_pton(family, ipstring) + self.valid = True + break + except socket.error: + continue + + if self.valid and family == socket.AF_INET: + # convert host to network byte order + self.addr, = struct.unpack("!L", binary) + self.family = family + self.plen = 32 + + # mask out host portion if prefix length is supplied + if cidr != None and cidr >= 0: + mask = ~(0xFFFFFFFFL >> cidr) + self.addr = self.addr & mask + self.plen = cidr + + elif self.valid and family == socket.AF_INET6: + # convert host to network byte order + hi, lo = struct.unpack("!QQ", binary) + self.addr = (hi << 64) | lo + self.family = family + self.plen = 128 + + # mask out host portion if prefix length is supplied + if cidr != None and cidr >= 0: + mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> cidr) + self.addr = self.addr & mask + self.plen = cidr + + # if IPv6 address is a IPv4-compatible, make instance a IPv4 + elif self.isInNet(IPAddr("::ffff:0:0", 96)): + self.addr = lo & 0xFFFFFFFFL + self.family = socket.AF_INET + self.plen = 32 + else: + # string couldn't be converted neither to a IPv4 nor + # to a IPv6 address - retain raw input for later use + # (e.g. DNS resolution) + self.raw = ipstring + + def __repr__(self): + return self.ntoa() + + def __str__(self): + return self.ntoa() + + def __eq__(self, other): + if not isinstance(other, IPAddr): + if other is None: return False + other = IPAddr(other) + if not self.valid and not other.valid: return self.raw == other.raw + if not self.valid or not other.valid: return False + if self.addr != other.addr: return False + if self.family != other.family: return False + if self.plen != other.plen: return False + return True + + def __ne__(self, other): + if not isinstance(other, IPAddr): + if other is None: return True + other = IPAddr(other) + if not self.valid and not other.valid: return self.raw != other.raw + if self.addr != other.addr: return True + if self.family != other.family: return True + if self.plen != other.plen: return True + return False + + def __lt__(self, other): + if not isinstance(other, IPAddr): + if other is None: return False + other = IPAddr(other) + return self.family < other.family or self.addr < other.addr + + def __add__(self, other): + if not isinstance(other, IPAddr): + other = IPAddr(other) + return "%s%s" % (self, other) + + def __radd__(self, other): + if not isinstance(other, IPAddr): + other = IPAddr(other) + return "%s%s" % (other, self) + + def __hash__(self): + # should be the same as by string (because of possible compare with string): + return hash(self.ntoa()) + #return hash(self.addr)^hash((self.plen<<16)|self.family) + + def hexdump(self): + """ dump the ip address in as a hex sequence in + network byte order - for debug purpose + """ + if self.family == socket.AF_INET: + return "%08x" % self.addr + elif self.family == socket.AF_INET6: + return "%032x" % self.addr + else: + return "" + + def ntoa(self): + """ represent IP object as text like the depricated + C pendant inet_ntoa() but address family independent + """ + if self.family == socket.AF_INET: + # convert network to host byte order + binary = struct.pack("!L", self.addr) + elif self.family == socket.AF_INET6: + # convert network to host byte order + hi = self.addr >> 64 + lo = self.addr & 0xFFFFFFFFFFFFFFFFL + binary = struct.pack("!QQ", hi, lo) + else: + return self.getRaw() + + return socket.inet_ntop(self.family, binary) + + def getPTR(self, suffix=""): + """ generates the DNS PTR string of the provided IP address object + if "suffix" is provided it will be appended as the second and top + level reverse domain. + if omitted it is implicitely set to the second and top level reverse + domain of the according IP address family + """ + if self.family == socket.AF_INET: + reversed_ip = ".".join(reversed(self.ntoa().split("."))) + if not suffix: + suffix = "in-addr.arpa." + + return "%s.%s" % (reversed_ip, suffix) + + elif self.family == socket.AF_INET6: + reversed_ip = ".".join(reversed(self.hexdump())) + if not suffix: + suffix = "ip6.arpa." + + return "%s.%s" % (reversed_ip, suffix) + + else: + return "" + + def isIPv4(self): + """ return true if the IP object is of address family AF_INET + """ + return True if self.family == socket.AF_INET else False + + def isIPv6(self): + """ return true if the IP object is of address family AF_INET6 + """ + return True if self.family == socket.AF_INET6 else False + + def getRaw(self): + """ returns the raw attribute - should only be set + to a non-empty string if prior address conversion + wasn't possible + """ + return self.raw + + def isValidIP(self): + """ returns true if the IP object has been created + from a valid IP address or false if not + """ + return self.valid + + + def isInNet(self, net): + """ returns true if the IP object is in the provided + network (object) + """ + # if it isn't a valid IP address, try DNS resolution + if not net.isValidIP() and net.getRaw() != "": + # Check if IP in DNS + return self in DNSUtils.dnsToIp(net.getRaw()) + + if self.family != net.family: + return False + + if self.family == socket.AF_INET: + mask = ~(0xFFFFFFFFL >> net.plen) + + elif self.family == socket.AF_INET6: + mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> net.plen) + else: + return False + + if self.addr & mask == net.addr: + return True + + return False + + @property + def maskplen(self): + plen = 0 + if (hasattr(self, '_maskplen')): + return self._plen + maddr = self.addr + while maddr: + if not (maddr & 0x80000000): + raise ValueError("invalid mask %r, no plen representation" % (self.ntoa(),)) + maddr = (maddr << 1) & 0xFFFFFFFFL + plen += 1 + self._maskplen = plen + return plen + + + @staticmethod + def masktoplen(maskstr): + """ converts mask string to prefix length + only used for IPv4 masks + """ + return IPAddr(maskstr).maskplen + + + @staticmethod + def searchIP(text): + """ Search if an IP address if directly available and return + it. + """ + match = IPAddr.IP_CRE.match(text) + if match: + return match + else: + match = IPAddr.IP6_CRE.match(text) + if match: + return match + else: + return None + diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index 3307a6c9..130de4f2 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -27,6 +27,7 @@ __license__ = "GPL" import sys from ..helpers import getLogger +from .ipdns import IPAddr from .mytime import MyTime # Gets the instance of the logger. @@ -74,7 +75,6 @@ class Ticket: def setIP(self, value): # guarantee using IPAddr instead of unicode, str for the IP if isinstance(value, basestring): - from .filter import IPAddr value = IPAddr(value) self.__ip = value diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index 36bc87a3..6e7bf367 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -28,7 +28,7 @@ import unittest from ..server import failmanager from ..server.failmanager import FailManager, FailManagerEmpty -from ..server.filter import IPAddr +from ..server.ipdns import IPAddr from ..server.ticket import FailTicket diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 371db3bc..b5a9b8d5 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -38,8 +38,9 @@ except ImportError: from ..server.jail import Jail from ..server.filterpoll import FilterPoll -from ..server.filter import Filter, FileFilter, FileContainer, DNSUtils, IPAddr +from ..server.filter import Filter, FileFilter, FileContainer from ..server.failmanager import FailManagerEmpty +from ..server.ipdns import DNSUtils, IPAddr from ..server.mytime import MyTime from ..server.utils import Utils from .utils import setUpMyTime, tearDownMyTime, mtimesleep, LogCaptureTestCase diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 0f328564..756dcc4f 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -32,7 +32,7 @@ import unittest from StringIO import StringIO from ..helpers import getLogger -from ..server.filter import DNSUtils +from ..server.ipdns import DNSUtils from ..server.mytime import MyTime from ..server.utils import Utils # for action_d.test_smtp : From 4274ae89c630e478dbf5e9c6dd55ed8dc9fe5d68 Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Sat, 9 Apr 2016 00:46:17 +0200 Subject: [PATCH 049/623] Quite little adjustments on tests and return value # Conflicts: # fail2ban/server/ipdns.py --- fail2ban/server/ipdns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py index e75cd9ed..b36b5cb1 100644 --- a/fail2ban/server/ipdns.py +++ b/fail2ban/server/ipdns.py @@ -303,12 +303,12 @@ class IPAddr(object): def isIPv4(self): """ return true if the IP object is of address family AF_INET """ - return True if self.family == socket.AF_INET else False + return self.family == socket.AF_INET def isIPv6(self): """ return true if the IP object is of address family AF_INET6 """ - return True if self.family == socket.AF_INET6 else False + return self.family == socket.AF_INET6 def getRaw(self): """ returns the raw attribute - should only be set From dbd7e347b19d5ca0506ddc8ae2f6c25d4734e711 Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Sat, 23 Apr 2016 10:08:44 +0200 Subject: [PATCH 050/623] new testcase to test beautifier code --- fail2ban/tests/clientbeautifiertestcase.py | 111 +++++++++++++++++++++ fail2ban/tests/utils.py | 5 + 2 files changed, 116 insertions(+) create mode 100644 fail2ban/tests/clientbeautifiertestcase.py diff --git a/fail2ban/tests/clientbeautifiertestcase.py b/fail2ban/tests/clientbeautifiertestcase.py new file mode 100644 index 00000000..d14030ea --- /dev/null +++ b/fail2ban/tests/clientbeautifiertestcase.py @@ -0,0 +1,111 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : + +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +__author__ = "Alexander Koeppe" +__copyright__ = "Copyright (c) 2016 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" +__license__ = "GPL" + +import unittest + +from ..client.beautifier import Beautifier +from ..version import version +from ..ipaddr import IPAddr + +class BeautifierTest(unittest.TestCase): + + def setUp(self): + """ Call before every test case """ + self.b = Beautifier() + + def tearDown(self): + """ Call after every test case """ + + def testGetInputCmd(self): + cmd = ["test"] + self.b.setInputCmd(cmd) + self.assertEqual(self.b.getInputCmd(), cmd) + + def testPing(self): + self.b.setInputCmd(["ping"]) + self.assertEqual(self.b.beautify("pong"), "Server replied: pong") + + def testVersion(self): + self.b.setInputCmd(["version"]) + self.assertEqual(self.b.beautify(version), version) + + def testAddJail(self): + self.b.setInputCmd(["add"]) + self.assertEqual(self.b.beautify("ssh"), "Added jail ssh") + + def testStartJail(self): + self.b.setInputCmd(["start"]) + self.assertEqual(self.b.beautify(None), "Jail started") + + def testFlushLogs(self): + self.b.setInputCmd(["flushlogs"]) + self.assertEqual(self.b.beautify("rolled over"), "logs: rolled over") + + def testStopJail(self): + self.b.setInputCmd(["stop", "ssh"]) + self.assertEqual(self.b.beautify(None), "Jail stopped") + + def testShutdown(self): + self.b.setInputCmd(["stop"]) + self.assertEqual(self.b.beautify(None), "Shutdown successful") + + def testStatus(self): + self.b.setInputCmd(["status"]) + response = (("Number of jails", 0), ("Jail list", ["ssh", "exim4"])) + output = "Status\n|- Number of jails:\t0\n`- Jail list:\tssh exim4" + self.assertEqual(self.b.beautify(response), output) + + self.b.setInputCmd(["status", "ssh"]) + response = ( + ("Filter", [ + ("Currently failed", 0), + ("Total failed", 0), + ("File list", "/var/log/auth.log") + ] + ), + ("Actions", [ + ("Currently banned", 3), + ("Total banned", 3), + ("Banned IP list", [ + IPAddr("192.168.0.1"), + IPAddr("::ffff:10.2.2.1"), + IPAddr("2001:db8::1") + ] + ) + ] + ) + ) + output = """Status for the jail: ssh +|- Filter +| |- Currently failed: 0 +| |- Total failed: 0 +| `- File list: /var/log/auth.log +`- Actions + |- Currently banned: 3 + |- Total banned: 3 + `- Banned IP list: 192.168.0.1 10.2.2.1 2001:db8::1""" + + self.assertEqual(self.b.beautify(response), output) + + + diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 756dcc4f..4c429105 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -133,6 +133,7 @@ def gatherTests(regexps=None, opts=None): # Import all the test cases here instead of a module level to # avoid circular imports from . import banmanagertestcase + from . import clientbeautifiertestcase from . import clientreadertestcase from . import tickettestcase from . import failmanagertestcase @@ -187,6 +188,10 @@ def gatherTests(regexps=None, opts=None): tests.addTest(unittest.makeSuite(banmanagertestcase.StatusExtendedCymruInfo)) except ImportError: # pragma: no cover pass + + # ClientBeautifier + tests.addTest(unittest.makeSuite(clientbeautifiertestcase.BeautifierTest)) + # ClientReaders tests.addTest(unittest.makeSuite(clientreadertestcase.ConfigReaderTest)) tests.addTest(unittest.makeSuite(clientreadertestcase.JailReaderTest)) From 5040c95bdb51fb230cf133611fca1a651d4b89a0 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Tue, 10 May 2016 21:17:09 -0400 Subject: [PATCH 051/623] DOC: stub for 0.10.0 release changelog --- ChangeLog | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ChangeLog b/ChangeLog index 033cd9ec..bf9df1b5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,20 @@ Fail2Ban: Changelog =================== +ver. 0.10.0 (2016/XX/XXX) - gonna-be-released-some-time-shining +----------- + +TODO: list all the fixes/enhancements/etc which came with the PR #1346 + +- Fixes: + + +- New Features: + + +- Enhancements: + + ver. 0.9.5 (2016/XX/XXX) - wanna-be-released ----------- From c1a54974e921206708ce033f684188c2d9b5d12e Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Sat, 23 Apr 2016 20:05:27 -0400 Subject: [PATCH 052/623] RF/ENH: 1st wave of IPAddr pythonization - properties, logical statements, etc # Conflicts: # fail2ban/server/ipdns.py --- MANIFEST | 1 + fail2ban/server/banmanager.py | 5 +- fail2ban/server/filter.py | 2 +- fail2ban/server/ipdns.py | 284 +++++++++------------ fail2ban/tests/clientbeautifiertestcase.py | 2 - 5 files changed, 126 insertions(+), 168 deletions(-) diff --git a/MANIFEST b/MANIFEST index 3a5b4477..11f635c3 100644 --- a/MANIFEST +++ b/MANIFEST @@ -201,6 +201,7 @@ fail2ban/tests/actionstestcase.py fail2ban/tests/actiontestcase.py fail2ban/tests/banmanagertestcase.py fail2ban/tests/clientreadertestcase.py +fail2ban/tests/clientbeautifiertestcase.py fail2ban/tests/config/action.d/brokenaction.conf fail2ban/tests/config/fail2ban.conf fail2ban/tests/config/filter.d/simple.conf diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index 67ae1b71..afa70685 100644 --- a/fail2ban/server/banmanager.py +++ b/fail2ban/server/banmanager.py @@ -152,8 +152,9 @@ class BanManager: for banData in self.__banList: ip = banData.getIP() # Reference: http://www.team-cymru.org/Services/ip-to-asn.html#dns - question = ip.getPTR("origin.asn.cymru.com" if ip.isIPv4() - else "origin6.asn.cymru.com" + question = ip.getPTR( + "origin.asn.cymru.com" if ip.isIPv4 + else "origin6.asn.cymru.com" ) try: answers = dns.resolver.query(question, "TXT") diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 49aba907..4bbf7996 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -383,7 +383,7 @@ class Filter(JailThread): for net in self.__ignoreIpList: # check if the IP is covered by ignore IP if ip.isInNet(net): - self.logIgnoreIp(ip, log_ignore, ignore_source=("ip" if net.isValidIP() else "dns")) + self.logIgnoreIp(ip, log_ignore, ignore_source=("ip" if net.isValid else "dns")) return True if self.__ignoreCommand: diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py index b36b5cb1..744a8a73 100644 --- a/fail2ban/server/ipdns.py +++ b/fail2ban/server/ipdns.py @@ -17,13 +17,13 @@ # along with Fail2Ban; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -__author__ = "Fail2Ban Developers, Alexander Koeppe, Serg G. Brester" +__author__ = "Fail2Ban Developers, Alexander Koeppe, Serg G. Brester, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004-2016 Fail2ban Developers" __license__ = "GPL" -import re import socket import struct +import re from .utils import Utils from ..helpers import getLogger @@ -38,9 +38,7 @@ logSys = getLogger(__name__) # def asip(ip): """A little helper to guarantee ip being an IPAddr instance""" - if isinstance(ip, IPAddr): - return ip - return IPAddr(ip) + return ip if isinstance(ip, IPAddr) or ip is None else IPAddr(ip) ## @@ -68,7 +66,7 @@ class DNSUtils: ips = list() for result in socket.getaddrinfo(dns, None, 0, 0, socket.IPPROTO_TCP): ip = IPAddr(result[4][0]) - if ip.isValidIP(): + if ip.isValid: ips.append(ip) except socket.error, e: # todo: make configurable the expired time of cache entry: @@ -88,7 +86,7 @@ class DNSUtils: if not isinstance(ip, IPAddr): v = socket.gethostbyaddr(ip)[0] else: - v = socket.gethostbyaddr(ip.ntoa())[0] + v = socket.gethostbyaddr(ip.ntoa)[0] except socket.error, e: logSys.debug("Unable to find a name for the IP %s: %s", ip, e) v = None @@ -104,7 +102,7 @@ class DNSUtils: plainIP = IPAddr.searchIP(text) if plainIP is not None: ip = IPAddr(plainIP.group(0)) - if ip.isValidIP(): + if ip.isValid: ipList.append(ip) # If we are allowed to resolve -- give it a try if nothing was found @@ -123,134 +121,132 @@ class DNSUtils: # Class for IP address handling. # # This class contains methods for handling IPv4 and IPv6 addresses. -class IPAddr(object): - """ provide functions to handle IPv4 and IPv6 addresses +# +class IPAddr: + """Encapsulate functionality for IPv4 and IPv6 addresses """ IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") IP6_CRE = re.compile("^[0-9a-fA-F]{4}[0-9a-fA-F:]+:[0-9a-fA-F]{1,4}|::1$") # object attributes - addr = 0 - family = socket.AF_UNSPEC - plen = 0 - valid = False - raw = "" - - # todo: make configurable the expired time and max count of cache entries: - CACHE_OBJ = Utils.Cache(maxCount=1000, maxTime=5*60) - - def __new__(cls, ipstring, cidr=-1): - # already correct IPAddr - args = (ipstring, cidr) - ip = IPAddr.CACHE_OBJ.get(args) - if ip is not None: - return ip - ip = super(IPAddr, cls).__new__(cls) - ip.__init(ipstring, cidr) - IPAddr.CACHE_OBJ.set(args, ip) - return ip + _addr = 0 + _family = socket.AF_UNSPEC + _plen = 0 + _isValid = False + _raw = "" # object methods - def __init(self, ipstring, cidr=-1): + def __init__(self, ipstring, cidr=-1): """ initialize IP object by converting IP address string to binary to integer """ for family in [socket.AF_INET, socket.AF_INET6]: try: binary = socket.inet_pton(family, ipstring) - self.valid = True - break except socket.error: continue + else: + self._isValid = True + break - if self.valid and family == socket.AF_INET: + if self.isValid and family == socket.AF_INET: # convert host to network byte order - self.addr, = struct.unpack("!L", binary) - self.family = family - self.plen = 32 + self._addr, = struct.unpack("!L", binary) + self._family = family + self._plen = 32 # mask out host portion if prefix length is supplied - if cidr != None and cidr >= 0: + if cidr is not None and cidr >= 0: mask = ~(0xFFFFFFFFL >> cidr) - self.addr = self.addr & mask - self.plen = cidr + self._addr &= mask + self._plen = cidr - elif self.valid and family == socket.AF_INET6: + elif self.isValid and family == socket.AF_INET6: # convert host to network byte order hi, lo = struct.unpack("!QQ", binary) - self.addr = (hi << 64) | lo - self.family = family - self.plen = 128 + self._addr = (hi << 64) | lo + self._family = family + self._plen = 128 # mask out host portion if prefix length is supplied - if cidr != None and cidr >= 0: + if cidr is not None and cidr >= 0: mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> cidr) - self.addr = self.addr & mask - self.plen = cidr + self._addr &= mask + self._plen = cidr # if IPv6 address is a IPv4-compatible, make instance a IPv4 - elif self.isInNet(IPAddr("::ffff:0:0", 96)): - self.addr = lo & 0xFFFFFFFFL - self.family = socket.AF_INET - self.plen = 32 + elif self.isInNet(_IPv6_v4COMPAT): + self._addr = lo & 0xFFFFFFFFL + self._family = socket.AF_INET + self._plen = 32 else: # string couldn't be converted neither to a IPv4 nor # to a IPv6 address - retain raw input for later use # (e.g. DNS resolution) - self.raw = ipstring + self._raw = ipstring def __repr__(self): - return self.ntoa() + return self.ntoa def __str__(self): - return self.ntoa() + return self.ntoa + + @property + def addr(self): + return self._addr + + @property + def family(self): + return self._family + + @property + def plen(self): + return self._plen + + @property + def raw(self): + """The raw address + + Should only be set to a non-empty string if prior address + conversion wasn't possible + """ + return self._raw + + @property + def isValid(self): + """Either the object corresponds to a valid IP address + """ + return self._isValid def __eq__(self, other): - if not isinstance(other, IPAddr): - if other is None: return False - other = IPAddr(other) - if not self.valid and not other.valid: return self.raw == other.raw - if not self.valid or not other.valid: return False - if self.addr != other.addr: return False - if self.family != other.family: return False - if self.plen != other.plen: return False - return True + if not (self.isValid or other.isValid): + return self.raw == other.raw + return ( + (self.isValid and other.isValid) and + (self.addr == other.addr) and + (self.family == other.family) and + (self.plen == other.plen) + ) def __ne__(self, other): - if not isinstance(other, IPAddr): - if other is None: return True - other = IPAddr(other) - if not self.valid and not other.valid: return self.raw != other.raw - if self.addr != other.addr: return True - if self.family != other.family: return True - if self.plen != other.plen: return True - return False + return not (self == other) def __lt__(self, other): - if not isinstance(other, IPAddr): - if other is None: return False - other = IPAddr(other) return self.family < other.family or self.addr < other.addr def __add__(self, other): - if not isinstance(other, IPAddr): - other = IPAddr(other) return "%s%s" % (self, other) def __radd__(self, other): - if not isinstance(other, IPAddr): - other = IPAddr(other) return "%s%s" % (other, self) def __hash__(self): - # should be the same as by string (because of possible compare with string): - return hash(self.ntoa()) - #return hash(self.addr)^hash((self.plen<<16)|self.family) + return hash(self.addr) ^ hash((self.plen << 16) | self.family) + @property def hexdump(self): - """ dump the ip address in as a hex sequence in - network byte order - for debug purpose + """Hex representation of the IP address (for debug purposes) """ if self.family == socket.AF_INET: return "%08x" % self.addr @@ -258,132 +254,94 @@ class IPAddr(object): return "%032x" % self.addr else: return "" - + + # TODO: could be lazily evaluated + @property def ntoa(self): - """ represent IP object as text like the depricated - C pendant inet_ntoa() but address family independent + """ represent IP object as text like the deprecated + C pendant inet.ntoa but address family independent """ - if self.family == socket.AF_INET: + if self.isIPv4: # convert network to host byte order - binary = struct.pack("!L", self.addr) - elif self.family == socket.AF_INET6: + binary = struct.pack("!L", self._addr) + elif self.isIPv6: # convert network to host byte order hi = self.addr >> 64 lo = self.addr & 0xFFFFFFFFFFFFFFFFL binary = struct.pack("!QQ", hi, lo) else: - return self.getRaw() + return self.raw return socket.inet_ntop(self.family, binary) def getPTR(self, suffix=""): - """ generates the DNS PTR string of the provided IP address object - if "suffix" is provided it will be appended as the second and top + """ return the DNS PTR string of the provided IP address object + + If "suffix" is provided it will be appended as the second and top level reverse domain. - if omitted it is implicitely set to the second and top level reverse + If omitted it is implicitly set to the second and top level reverse domain of the according IP address family """ - if self.family == socket.AF_INET: - reversed_ip = ".".join(reversed(self.ntoa().split("."))) + if self.isIPv4: + exploded_ip = self.ntoa.split(".") if not suffix: suffix = "in-addr.arpa." - - return "%s.%s" % (reversed_ip, suffix) - - elif self.family == socket.AF_INET6: - reversed_ip = ".".join(reversed(self.hexdump())) + elif self.isIPv6: + exploded_ip = self.hexdump() if not suffix: - suffix = "ip6.arpa." - - return "%s.%s" % (reversed_ip, suffix) - + suffix = "ip6.arpa." else: return "" + return "%s.%s" % (".".join(reversed(exploded_ip)), suffix) + + @property def isIPv4(self): - """ return true if the IP object is of address family AF_INET + """Either the IP object is of address family AF_INET """ return self.family == socket.AF_INET + @property def isIPv6(self): - """ return true if the IP object is of address family AF_INET6 + """Either the IP object is of address family AF_INET6 """ return self.family == socket.AF_INET6 - def getRaw(self): - """ returns the raw attribute - should only be set - to a non-empty string if prior address conversion - wasn't possible - """ - return self.raw - - def isValidIP(self): - """ returns true if the IP object has been created - from a valid IP address or false if not - """ - return self.valid - - def isInNet(self, net): - """ returns true if the IP object is in the provided - network (object) + """Return either the IP object is in the provided network """ - # if it isn't a valid IP address, try DNS resolution - if not net.isValidIP() and net.getRaw() != "": - # Check if IP in DNS - return self in DNSUtils.dnsToIp(net.getRaw()) - if self.family != net.family: return False - - if self.family == socket.AF_INET: + if self.isIPv4: mask = ~(0xFFFFFFFFL >> net.plen) - - elif self.family == socket.AF_INET6: + elif self.isIPv6: mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> net.plen) else: return False - if self.addr & mask == net.addr: - return True + return self.addr & mask == net.addr - return False - - @property - def maskplen(self): - plen = 0 - if (hasattr(self, '_maskplen')): - return self._plen - maddr = self.addr - while maddr: - if not (maddr & 0x80000000): - raise ValueError("invalid mask %r, no plen representation" % (self.ntoa(),)) - maddr = (maddr << 1) & 0xFFFFFFFFL - plen += 1 - self._maskplen = plen - return plen - - @staticmethod - def masktoplen(maskstr): - """ converts mask string to prefix length - only used for IPv4 masks - """ - return IPAddr(maskstr).maskplen + def masktoplen(mask): + """Convert mask string to prefix length + To be used only for IPv4 masks + """ + mask = mask.addr # to avoid side-effect within original mask + plen = 0 + while mask: + mask = (mask << 1) & 0xFFFFFFFFL + plen += 1 + return plen @staticmethod def searchIP(text): - """ Search if an IP address if directly available and return - it. + """Search if text is an IP address, and return it if so, else None """ match = IPAddr.IP_CRE.match(text) - if match: - return match - else: + if not match: match = IPAddr.IP6_CRE.match(text) - if match: - return match - else: - return None + return match if match else None +# An IPv4 compatible IPv6 to be reused +_IPv6_v4COMPAT = IPAddr("::ffff:0:0", 96) diff --git a/fail2ban/tests/clientbeautifiertestcase.py b/fail2ban/tests/clientbeautifiertestcase.py index d14030ea..704aedd0 100644 --- a/fail2ban/tests/clientbeautifiertestcase.py +++ b/fail2ban/tests/clientbeautifiertestcase.py @@ -107,5 +107,3 @@ class BeautifierTest(unittest.TestCase): self.assertEqual(self.b.beautify(response), output) - - From 9b06c325e12c49de1ef54417c428471820399341 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 10 May 2016 10:08:20 +0200 Subject: [PATCH 053/623] 2nd wave: code review, simplification, pythonization, etc. (test cases passed) --- fail2ban/server/ipdns.py | 110 ++++++++++++++------- fail2ban/tests/clientbeautifiertestcase.py | 2 +- 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py index 744a8a73..da820366 100644 --- a/fail2ban/server/ipdns.py +++ b/fail2ban/server/ipdns.py @@ -38,7 +38,9 @@ logSys = getLogger(__name__) # def asip(ip): """A little helper to guarantee ip being an IPAddr instance""" - return ip if isinstance(ip, IPAddr) or ip is None else IPAddr(ip) + if isinstance(ip, IPAddr): + return ip + return IPAddr(ip) ## @@ -122,38 +124,53 @@ class DNSUtils: # # This class contains methods for handling IPv4 and IPv6 addresses. # -class IPAddr: +class IPAddr(object): """Encapsulate functionality for IPv4 and IPv6 addresses """ IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") IP6_CRE = re.compile("^[0-9a-fA-F]{4}[0-9a-fA-F:]+:[0-9a-fA-F]{1,4}|::1$") + # An IPv4 compatible IPv6 to be reused (see below) + IP6_4COMPAT = None # object attributes - _addr = 0 - _family = socket.AF_UNSPEC - _plen = 0 - _isValid = False - _raw = "" + __slots__ = '_family','_addr','_plen','_maskplen','_raw' + + # todo: make configurable the expired time and max count of cache entries: + CACHE_OBJ = Utils.Cache(maxCount=1000, maxTime=5*60) + + def __new__(cls, ipstring, cidr=-1): + # already correct IPAddr + args = (ipstring, cidr) + ip = IPAddr.CACHE_OBJ.get(args) + if ip is not None: + return ip + ip = super(IPAddr, cls).__new__(cls) + ip.__init(ipstring, cidr) + IPAddr.CACHE_OBJ.set(args, ip) + return ip # object methods - def __init__(self, ipstring, cidr=-1): + def __init(self, ipstring, cidr=-1): """ initialize IP object by converting IP address string to binary to integer """ + self._family = socket.AF_UNSPEC + self._addr = 0 + self._plen = 0 + self._maskplen = None + self._raw = "" for family in [socket.AF_INET, socket.AF_INET6]: try: binary = socket.inet_pton(family, ipstring) + self._family = family + break except socket.error: continue - else: - self._isValid = True - break - if self.isValid and family == socket.AF_INET: + if self._family == socket.AF_INET: # convert host to network byte order self._addr, = struct.unpack("!L", binary) - self._family = family self._plen = 32 # mask out host portion if prefix length is supplied @@ -162,11 +179,10 @@ class IPAddr: self._addr &= mask self._plen = cidr - elif self.isValid and family == socket.AF_INET6: + elif self._family == socket.AF_INET6: # convert host to network byte order hi, lo = struct.unpack("!QQ", binary) self._addr = (hi << 64) | lo - self._family = family self._plen = 128 # mask out host portion if prefix length is supplied @@ -176,7 +192,7 @@ class IPAddr: self._plen = cidr # if IPv6 address is a IPv4-compatible, make instance a IPv4 - elif self.isInNet(_IPv6_v4COMPAT): + elif self.isInNet(IPAddr.IP6_4COMPAT): self._addr = lo & 0xFFFFFFFFL self._family = socket.AF_INET self._plen = 32 @@ -217,32 +233,43 @@ class IPAddr: def isValid(self): """Either the object corresponds to a valid IP address """ - return self._isValid + return self._family != socket.AF_UNSPEC def __eq__(self, other): - if not (self.isValid or other.isValid): - return self.raw == other.raw + if not isinstance(other, IPAddr): + if other is None: return False + other = IPAddr(other) + if self._family != other._family: return False + if self._family == socket.AF_UNSPEC: + return self._raw == other._raw return ( - (self.isValid and other.isValid) and - (self.addr == other.addr) and - (self.family == other.family) and - (self.plen == other.plen) + (self._addr == other._addr) and + (self._plen == other._plen) ) def __ne__(self, other): return not (self == other) def __lt__(self, other): - return self.family < other.family or self.addr < other.addr + if not isinstance(other, IPAddr): + if other is None: return False + other = IPAddr(other) + return self._family < other._family or self._addr < other._addr def __add__(self, other): + if not isinstance(other, IPAddr): + other = IPAddr(other) return "%s%s" % (self, other) def __radd__(self, other): + if not isinstance(other, IPAddr): + other = IPAddr(other) return "%s%s" % (other, self) def __hash__(self): - return hash(self.addr) ^ hash((self.plen << 16) | self.family) + # should be the same as by string (because of possible compare with string): + return hash(self.ntoa) + #return hash(self._addr)^hash((self._plen<<16)|self._family) @property def hexdump(self): @@ -270,7 +297,7 @@ class IPAddr: lo = self.addr & 0xFFFFFFFFFFFFFFFFL binary = struct.pack("!QQ", hi, lo) else: - return self.raw + return self._raw return socket.inet_ntop(self.family, binary) @@ -310,6 +337,11 @@ class IPAddr: def isInNet(self, net): """Return either the IP object is in the provided network """ + # if it isn't a valid IP address, try DNS resolution + if not net.isValid and net.raw != "": + # Check if IP in DNS + return self in DNSUtils.dnsToIp(net.raw) + if self.family != net.family: return False if self.isIPv4: @@ -319,20 +351,29 @@ class IPAddr: else: return False - return self.addr & mask == net.addr + return (self.addr & mask) == net.addr + @property + def maskplen(self): + plen = 0 + if self._maskplen is not None: + return self._plen + maddr = self.addr + while maddr: + if not (maddr & 0x80000000): + raise ValueError("invalid mask %r, no plen representation" % (str(self),)) + maddr = (maddr << 1) & 0xFFFFFFFFL + plen += 1 + self._maskplen = plen + return plen + @staticmethod def masktoplen(mask): """Convert mask string to prefix length To be used only for IPv4 masks """ - mask = mask.addr # to avoid side-effect within original mask - plen = 0 - while mask: - mask = (mask << 1) & 0xFFFFFFFFL - plen += 1 - return plen + return IPAddr(mask).maskplen @staticmethod def searchIP(text): @@ -343,5 +384,6 @@ class IPAddr: match = IPAddr.IP6_CRE.match(text) return match if match else None + # An IPv4 compatible IPv6 to be reused -_IPv6_v4COMPAT = IPAddr("::ffff:0:0", 96) +IPAddr.IP6_4COMPAT = IPAddr("::ffff:0:0", 96) diff --git a/fail2ban/tests/clientbeautifiertestcase.py b/fail2ban/tests/clientbeautifiertestcase.py index 704aedd0..397bce1d 100644 --- a/fail2ban/tests/clientbeautifiertestcase.py +++ b/fail2ban/tests/clientbeautifiertestcase.py @@ -25,7 +25,7 @@ import unittest from ..client.beautifier import Beautifier from ..version import version -from ..ipaddr import IPAddr +from ..server.ipdns import IPAddr class BeautifierTest(unittest.TestCase): From 25d6cf8dd23e150055baa67d49a122712fa83379 Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 2 May 2016 19:00:06 +0200 Subject: [PATCH 054/623] fix suhosin_log in common paths - log files should be separated using "\n": prevents to throw an error "File option must be 'head' or 'tail'", if jail suhosin will be enabled. --- config/paths-common.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/paths-common.conf b/config/paths-common.conf index e2f08325..9072136c 100644 --- a/config/paths-common.conf +++ b/config/paths-common.conf @@ -40,7 +40,8 @@ lighttpd_error_log = /var/log/lighttpd/error.log # http://www.hardened-php.net/suhosin/configuration.html#suhosin.log.syslog.facility # syslog_user is the default. Lighttpd also hooks errors into its log. -suhosin_log = %(syslog_user)s %(lighttpd_error_log)s +suhosin_log = %(syslog_user)s + %(lighttpd_error_log)s # defaults to ftp or local2 if ftp doesn't exist proftpd_log = %(syslog_ftp)s From 2497b05abc30a2232c6cd846b11bc04332b9ab03 Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 2 May 2016 22:42:59 +0200 Subject: [PATCH 055/623] test cases preliminary extended; --- fail2ban/tests/servertestcase.py | 228 +++++++++++++++++++++++++++++++ fail2ban/tests/utils.py | 3 + 2 files changed, 231 insertions(+) diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index d9db6c47..99afa012 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -33,7 +33,9 @@ import sys import platform from ..server.failregex import Regex, FailRegex, RegexException +from ..server import actions as _actions from ..server.server import Server +from ..server.ipdns import IPAddr from ..server.jail import Jail from ..server.jailthread import JailThread from ..server.utils import Utils @@ -49,6 +51,8 @@ except ImportError: # pragma: no cover TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") FAST_BACKEND = "polling" +logSys = getLogger("fail2ban") + class TestServer(Server): def setLogLevel(self, *args, **kwargs): @@ -963,3 +967,227 @@ class LoggingTests(LogCaptureTestCase): sys.__excepthook__ = prev_exchook self.assertEqual(len(x), 1) self.assertEqual(x[0][0], RuntimeError) + + +from clientreadertestcase import ActionReader, JailReader, JailsReader, CONFIG_DIR, STOCK + +class ServerConfigReaderTests(LogCaptureTestCase): + + def __init__(self, *args, **kwargs): + super(ServerConfigReaderTests, self).__init__(*args, **kwargs) + self.__share_cfg = {} + + def setUp(self): + """Call before every test case.""" + super(ServerConfigReaderTests, self).setUp() + self._execCmdLst = [] + + def tearDown(self): + """Call after every test case.""" + super(ServerConfigReaderTests, self).tearDown() + + def _executeCmd(self, realCmd, timeout=60): + for l in realCmd.split('\n'): + if not l.startswith('#'): + logSys.debug('exec-cmd: `%s`', l) + else: + logSys.debug(l) + return True + + def test_IPAddr(self): + self.assertTrue(IPAddr('192.0.2.1').isIPv4) + self.assertTrue(IPAddr('2001:DB8::').isIPv6) + + if STOCK: + + def testCheckStockJailActions(self): + return + jails = JailsReader(basedir=CONFIG_DIR, force_enable=True, share_config=self.__share_cfg) # we are running tests from root project dir atm + self.assertTrue(jails.read()) # opens fine + self.assertTrue(jails.getOptions()) # reads fine + stream = jails.convert(allow_no_files=True) + + server = TestServer() + transm = server._Server__transm + cmdHandler = transm._Transmitter__commandHandler + + # for cmd in stream: + # print(cmd) + + # filter all start commands (we want not start all jails): + for cmd in stream: + if cmd[0] != 'start': + # change to the fast init backend: + if cmd[0] == 'add': + cmd[2] = 'polling' + # add dummy regex to prevent too long compile of all regexp (we don't use it in this test at all): + # [todo sebres] remove `not hasattr(unittest, 'F2B') or `, after merge with "f2b-perfom-prepare-716" ... + elif (not hasattr(unittest, 'F2B') or unittest.F2B.fast) and len(cmd) > 3 and cmd[0] == 'set' and cmd[2] == 'addfailregex': + cmd[3] = "DUMMY-REGEX " + # command to server, use cmdHandler direct instead of `transm.proceed(cmd)`: + try: + cmdHandler(cmd) + except Exception, e: # pragma: no cover + self.fail("Command %r has failed. Received %r" % (cmd, e)) + + # jails = server._Server__jails + # for j in jails: + # print(j, jails[j]) + + def getDefaultJailStream(self, jail, act): + act = act.replace('%(__name__)s', jail) + actName, actOpt = JailReader.extractOptions(act) + stream = [ + ['add', jail, 'polling'], + # ['set', jail, 'addfailregex', 'DUMMY-REGEX '], + ] + action = ActionReader( + actName, jail, actOpt, + share_config=self.__share_cfg, basedir=CONFIG_DIR) + self.assertTrue(action.read()) + action.getOptions({}) + stream.extend(action.convert()) + return stream + + def _assertLoggedAllTests(self, tests): + for t in tests: + self.assertLogged(t) + + + def testCheckStockCommandActions(self): + server = TestServer() + transm = server._Server__transm + cmdHandler = transm._Transmitter__commandHandler + + testJailsActions = ( + ('j-w-iptables-mp', 'iptables-multiport[name=%(__name__)s, bantime="600", port="http,https", protocol="tcp", chain="INPUT"]', { + 'ip4': '`iptables ', 'ip6': '`ip6tables ', + 'start': ( + "`iptables -w -N f2b-j-w-iptables-mp`", + "`iptables -w -A f2b-j-w-iptables-mp -j RETURN`", + "`iptables -w -I INPUT -p tcp -m multiport --dports http,https -j f2b-j-w-iptables-mp`", + "`ip6tables -w -N f2b-j-w-iptables-mp`", + "`ip6tables -w -A f2b-j-w-iptables-mp -j RETURN`", + "`ip6tables -w -I INPUT -p tcp -m multiport --dports http,https -j f2b-j-w-iptables-mp`", + ), + 'stop': ( + "`iptables -w -D INPUT -p tcp -m multiport --dports http,https -j f2b-j-w-iptables-mp`", + "`iptables -w -F f2b-j-w-iptables-mp`", + "`iptables -w -X f2b-j-w-iptables-mp`", + "`ip6tables -w -D INPUT -p tcp -m multiport --dports http,https -j f2b-j-w-iptables-mp`", + "`ip6tables -w -F f2b-j-w-iptables-mp`", + "`ip6tables -w -X f2b-j-w-iptables-mp`", + ), + 'ip4-check': ( + r"""`iptables -w -n -L INPUT | grep -q 'f2b-j-w-iptables-mp[ \t]'`""", + ), + 'ip6-check': ( + r"""`ip6tables -w -n -L INPUT | grep -q 'f2b-j-w-iptables-mp[ \t]'`""", + ), + 'ip4-ban': ( + r"`iptables -w -I f2b-j-w-iptables-mp 1 -s 192.0.2.1 -j REJECT --reject-with icmp-port-unreachable`", + ), + 'ip4-unban': ( + r"`iptables -w -D f2b-j-w-iptables-mp -s 192.0.2.1 -j REJECT --reject-with icmp-port-unreachable`", + ), + 'ip6-ban': ( + r"`ip6tables -w -I f2b-j-w-iptables-mp 1 -s 2001:db8:: -j REJECT --reject-with icmp6-port-unreachable`", + ), + 'ip6-unban': ( + r"`ip6tables -w -D f2b-j-w-iptables-mp -s 2001:db8:: -j REJECT --reject-with icmp6-port-unreachable`", + ), + }), + ('j-w-iptables-ap', 'iptables-allports[name=%(__name__)s, bantime="600", protocol="tcp", chain="INPUT"]', { + 'ip4': '`iptables ', 'ip6': '`ip6tables ', + 'start': ( + "`iptables -w -N f2b-j-w-iptables-ap`", + "`iptables -w -A f2b-j-w-iptables-ap -j RETURN`", + "`iptables -w -I INPUT -p tcp -j f2b-j-w-iptables-ap`", + "`ip6tables -w -N f2b-j-w-iptables-ap`", + "`ip6tables -w -A f2b-j-w-iptables-ap -j RETURN`", + "`ip6tables -w -I INPUT -p tcp -j f2b-j-w-iptables-ap`", + ), + 'stop': ( + "`iptables -w -D INPUT -p tcp -j f2b-j-w-iptables-ap`", + "`iptables -w -F f2b-j-w-iptables-ap`", + "`iptables -w -X f2b-j-w-iptables-ap`", + "`ip6tables -w -D INPUT -p tcp -j f2b-j-w-iptables-ap`", + "`ip6tables -w -F f2b-j-w-iptables-ap`", + "`ip6tables -w -X f2b-j-w-iptables-ap`", + ), + 'ip4-check': ( + r"""`iptables -w -n -L INPUT | grep -q 'f2b-j-w-iptables-ap[ \t]'`""", + ), + 'ip6-check': ( + r"""`ip6tables -w -n -L INPUT | grep -q 'f2b-j-w-iptables-ap[ \t]'`""", + ), + 'ip4-ban': ( + r"`iptables -w -I f2b-j-w-iptables-ap 1 -s 192.0.2.1 -j REJECT --reject-with icmp-port-unreachable`", + ), + 'ip4-unban': ( + r"`iptables -w -D f2b-j-w-iptables-ap -s 192.0.2.1 -j REJECT --reject-with icmp-port-unreachable`", + ), + 'ip6-ban': ( + r"`ip6tables -w -I f2b-j-w-iptables-ap 1 -s 2001:db8:: -j REJECT --reject-with icmp6-port-unreachable`", + ), + 'ip6-unban': ( + r"`ip6tables -w -D f2b-j-w-iptables-ap -s 2001:db8:: -j REJECT --reject-with icmp6-port-unreachable`", + ), + }), + ) + + for jail, act, tests in testJailsActions: + stream = self.getDefaultJailStream(jail, act) + + # for cmd in stream: + # print(cmd) + + # filter all start commands (we want not start all jails): + for cmd in stream: + # command to server, use cmdHandler direct instead of `transm.proceed(cmd)`: + try: + cmdHandler(cmd) + except Exception, e: # pragma: no cover + self.fail("Command %r has failed. Received %r" % (cmd, e)) + + jails = server._Server__jails + + for jail, act, tests in testJailsActions: + # print(jail, jails[jail]) + for a in jails[jail].actions: + action = jails[jail].actions[a] + logSys.debug('# ' + ('=' * 50)) + logSys.debug('# == %-44s ==', jail + ' - ' + action._name) + logSys.debug('# ' + ('=' * 50)) + self.assertTrue(isinstance(action, _actions.CommandAction)) + # wrap default command processor: + action.executeCmd = self._executeCmd + # test start : + logSys.debug('# === start ==='); self.pruneLog() + action.start() + self._assertLoggedAllTests(tests['start']) + # test ban ip4 : + logSys.debug('# === ban-ipv4 ==='); self.pruneLog() + action.ban({'ip': IPAddr('192.0.2.1')}) + self._assertLoggedAllTests(tests['ip4-check']+tests['ip4-ban']) + self.assertNotLogged(tests['ip6']) + # test unban ip4 : + logSys.debug('# === unban ipv4 ==='); self.pruneLog() + action.unban({'ip': IPAddr('192.0.2.1')}) + self._assertLoggedAllTests(tests['ip4-check']+tests['ip4-unban']) + self.assertNotLogged(tests['ip6']) + # test ban ip6 : + logSys.debug('# === ban ipv6 ==='); self.pruneLog() + action.ban({'ip': IPAddr('2001:DB8::')}) + self._assertLoggedAllTests(tests['ip6-check']+tests['ip6-ban']) + self.assertNotLogged(tests['ip4']) + # test unban ip6 : + logSys.debug('# === unban ipv6 ==='); self.pruneLog() + action.unban({'ip': IPAddr('2001:DB8::')}) + self._assertLoggedAllTests(tests['ip6-check']+tests['ip6-unban']) + self.assertNotLogged(tests['ip4']) + # test stop : + logSys.debug('# === stop ==='); self.pruneLog() + action.stop() + self._assertLoggedAllTests(tests['stop']) + diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 4c429105..fe3ab783 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -174,6 +174,7 @@ def gatherTests(regexps=None, opts=None): tests.addTest(unittest.makeSuite(servertestcase.JailTests)) tests.addTest(unittest.makeSuite(servertestcase.RegexTests)) tests.addTest(unittest.makeSuite(servertestcase.LoggingTests)) + tests.addTest(unittest.makeSuite(servertestcase.ServerConfigReaderTests)) tests.addTest(unittest.makeSuite(actiontestcase.CommandActionTest)) tests.addTest(unittest.makeSuite(actionstestcase.ExecuteActions)) # Ticket, BanTicket, FailTicket @@ -356,6 +357,8 @@ class LogCaptureTestCase(unittest.TestCase): return raise AssertionError("All of the %r were found present in the log: %r" % (s, logged)) + def pruneLog(self): + self._log.truncate(0) def getLog(self): return self._log.getvalue() From 43c0f3cdc43a31cfdf42b12d148085f3bce0dea0 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 3 May 2016 10:38:33 +0200 Subject: [PATCH 056/623] test cases extended --- fail2ban/tests/servertestcase.py | 60 +++++++++++++++++++++++--------- fail2ban/tests/utils.py | 23 ++++++++---- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 99afa012..bac64dd1 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -1001,7 +1001,6 @@ class ServerConfigReaderTests(LogCaptureTestCase): if STOCK: def testCheckStockJailActions(self): - return jails = JailsReader(basedir=CONFIG_DIR, force_enable=True, share_config=self.__share_cfg) # we are running tests from root project dir atm self.assertTrue(jails.read()) # opens fine self.assertTrue(jails.getOptions()) # reads fine @@ -1020,6 +1019,9 @@ class ServerConfigReaderTests(LogCaptureTestCase): # change to the fast init backend: if cmd[0] == 'add': cmd[2] = 'polling' + # change log path to test log of jail (to prevent "Permission denied" on /var/logs/ for test-user): + elif len(cmd) > 3 and cmd[0] == 'set' and cmd[2] == 'addlogpath': + cmd[3] = os.path.join(TEST_FILES_DIR, 'logs', cmd[1]) # add dummy regex to prevent too long compile of all regexp (we don't use it in this test at all): # [todo sebres] remove `not hasattr(unittest, 'F2B') or `, after merge with "f2b-perfom-prepare-716" ... elif (not hasattr(unittest, 'F2B') or unittest.F2B.fast) and len(cmd) > 3 and cmd[0] == 'set' and cmd[2] == 'addfailregex': @@ -1049,11 +1051,6 @@ class ServerConfigReaderTests(LogCaptureTestCase): stream.extend(action.convert()) return stream - def _assertLoggedAllTests(self, tests): - for t in tests: - self.assertLogged(t) - - def testCheckStockCommandActions(self): server = TestServer() transm = server._Server__transm @@ -1134,6 +1131,37 @@ class ServerConfigReaderTests(LogCaptureTestCase): r"`ip6tables -w -D f2b-j-w-iptables-ap -s 2001:db8:: -j REJECT --reject-with icmp6-port-unreachable`", ), }), + ('j-w-iptables-ipset', 'iptables-ipset-proto6[name=%(__name__)s, bantime="600", port="http", protocol="tcp", chain="INPUT"]', { + 'ip4': ' f2b-j-w-iptables-ipset ', 'ip6': ' f2b-j-w-iptables-ipset6 ', + 'start': ( + "`ipset create f2b-j-w-iptables-ipset hash:ip timeout 600`", + "`iptables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset src -j REJECT --reject-with icmp-port-unreachable`", + "`ipset create f2b-j-w-iptables-ipset6 hash:ip timeout 600 family inet6`", + "`ip6tables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`", + ), + 'stop': ( + "`iptables -w -D INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset src -j REJECT --reject-with icmp-port-unreachable`", + "`ipset flush f2b-j-w-iptables-ipset`", + "`ipset destroy f2b-j-w-iptables-ipset`", + "`ip6tables -w -D INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`", + "`ipset flush f2b-j-w-iptables-ipset6`", + "`ipset destroy f2b-j-w-iptables-ipset6`", + ), + 'ip4-check': (), + 'ip6-check': (), + 'ip4-ban': ( + r"`ipset add f2b-j-w-iptables-ipset 192.0.2.1 timeout 600 -exist`", + ), + 'ip4-unban': ( + r"`ipset del f2b-j-w-iptables-ipset 192.0.2.1 -exist`", + ), + 'ip6-ban': ( + r"`ipset add f2b-j-w-iptables-ipset6 2001:db8:: timeout 600 -exist`", + ), + 'ip6-unban': ( + r"`ipset del f2b-j-w-iptables-ipset6 2001:db8:: -exist`", + ), + }), ) for jail, act, tests in testJailsActions: @@ -1144,11 +1172,9 @@ class ServerConfigReaderTests(LogCaptureTestCase): # filter all start commands (we want not start all jails): for cmd in stream: - # command to server, use cmdHandler direct instead of `transm.proceed(cmd)`: - try: - cmdHandler(cmd) - except Exception, e: # pragma: no cover - self.fail("Command %r has failed. Received %r" % (cmd, e)) + # command to server: + ret, res = transm.proceed(cmd) + self.assertEqual(ret, 0) jails = server._Server__jails @@ -1165,29 +1191,29 @@ class ServerConfigReaderTests(LogCaptureTestCase): # test start : logSys.debug('# === start ==='); self.pruneLog() action.start() - self._assertLoggedAllTests(tests['start']) + self.assertLogged(*tests['start'], all=True) # test ban ip4 : logSys.debug('# === ban-ipv4 ==='); self.pruneLog() action.ban({'ip': IPAddr('192.0.2.1')}) - self._assertLoggedAllTests(tests['ip4-check']+tests['ip4-ban']) + self.assertLogged(*tests['ip4-check']+tests['ip4-ban'], all=True) self.assertNotLogged(tests['ip6']) # test unban ip4 : logSys.debug('# === unban ipv4 ==='); self.pruneLog() action.unban({'ip': IPAddr('192.0.2.1')}) - self._assertLoggedAllTests(tests['ip4-check']+tests['ip4-unban']) + self.assertLogged(*tests['ip4-check']+tests['ip4-unban'], all=True) self.assertNotLogged(tests['ip6']) # test ban ip6 : logSys.debug('# === ban ipv6 ==='); self.pruneLog() action.ban({'ip': IPAddr('2001:DB8::')}) - self._assertLoggedAllTests(tests['ip6-check']+tests['ip6-ban']) + self.assertLogged(*tests['ip6-check']+tests['ip6-ban'], all=True) self.assertNotLogged(tests['ip4']) # test unban ip6 : logSys.debug('# === unban ipv6 ==='); self.pruneLog() action.unban({'ip': IPAddr('2001:DB8::')}) - self._assertLoggedAllTests(tests['ip6-check']+tests['ip6-unban']) + self.assertLogged(*tests['ip6-check']+tests['ip6-unban'], all=True) self.assertNotLogged(tests['ip4']) # test stop : logSys.debug('# === stop ==='); self.pruneLog() action.stop() - self._assertLoggedAllTests(tests['stop']) + self.assertLogged(*tests['stop'], all=True) diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index fe3ab783..52097fa5 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -325,7 +325,7 @@ class LogCaptureTestCase(unittest.TestCase): def _is_logged(self, s): return s in self._log.getvalue() - def assertLogged(self, *s): + def assertLogged(self, *s, **kwargs): """Assert that one of the strings was logged Preferable to assertTrue(self._is_logged(..))) @@ -335,12 +335,22 @@ class LogCaptureTestCase(unittest.TestCase): ---------- s : string or list/set/tuple of strings Test should succeed if string (or any of the listed) is present in the log + all : boolean, should find all in s """ logged = self._log.getvalue() - for s_ in s: - if s_ in logged: - return - raise AssertionError("None among %r was found in the log: %r" % (s, logged)) + if not kwargs.get('all', False): + # at least one entry should be found: + for s_ in s: + if s_ in logged: + return + # pragma: no cover + self.fail("None among %r was found in the log: ===\n%s===" % (s, logged)) + else: + # each entry should be found: + for s_ in s: + if s_ not in logged: + # pragma: no cover + self.fail("%r was not found in the log: ===\n%s===" % (s_, logged)) def assertNotLogged(self, *s): """Assert that strings were not logged @@ -355,7 +365,8 @@ class LogCaptureTestCase(unittest.TestCase): for s_ in s: if s_ not in logged: return - raise AssertionError("All of the %r were found present in the log: %r" % (s, logged)) + # pragma: no cover + self.fail("All of the %r were found present in the log: ===\n%s===" % (s, logged)) def pruneLog(self): self._log.truncate(0) From 1a6450643d2d96738500d45a1f8ebe388f9f1b9f Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 3 May 2016 17:52:46 +0200 Subject: [PATCH 057/623] partially cherry pick from branch 'multi-set', prepare for conditional config parameters logic: - new readers logic (group some by multiple parameters 'set' -> 'multi-set'; - prevent to add 'known/' parameters twice (by merge section etc); - test cases fixed; # Conflicts: # fail2ban/client/actionreader.py --- fail2ban/client/actionreader.py | 38 +++++++++---------- fail2ban/client/configparserinc.py | 4 +- fail2ban/client/configreader.py | 52 +++++++++++++++----------- fail2ban/client/fail2banregex.py | 9 ++++- fail2ban/client/filterreader.py | 22 +++++------ fail2ban/client/jailreader.py | 36 +++++++++--------- fail2ban/server/server.py | 18 +++++++-- fail2ban/server/transmitter.py | 37 +++++++++++++----- fail2ban/tests/clientreadertestcase.py | 42 +++++++++++++-------- fail2ban/tests/samplestestcase.py | 23 ++++++++---- fail2ban/tests/servertestcase.py | 19 ++++++---- fail2ban/tests/utils.py | 27 +++++++------ 12 files changed, 196 insertions(+), 131 deletions(-) diff --git a/fail2ban/client/actionreader.py b/fail2ban/client/actionreader.py index c80b230e..698360ac 100644 --- a/fail2ban/client/actionreader.py +++ b/fail2ban/client/actionreader.py @@ -35,13 +35,13 @@ logSys = getLogger(__name__) class ActionReader(DefinitionInitConfigReader): - _configOpts = [ - ["string", "actionstart", None], - ["string", "actionstop", None], - ["string", "actioncheck", None], - ["string", "actionban", None], - ["string", "actionunban", None], - ] + _configOpts = { + "actionstart": ["string", None], + "actionstop": ["string", None], + "actioncheck": ["string", None], + "actionban": ["string", None], + "actionunban": ["string", None], + } def __init__(self, file_, jailName, initOpts, **kwargs): self._name = initOpts.get("actname", file_) @@ -65,20 +65,16 @@ class ActionReader(DefinitionInitConfigReader): head = ["set", self._jailName] stream = list() stream.append(head + ["addaction", self._name]) - head.extend(["action", self._name]) - for opt in self._opts: - if opt == "actionstart": - stream.append(head + ["actionstart", self._opts[opt]]) - elif opt == "actionstop": - stream.append(head + ["actionstop", self._opts[opt]]) - elif opt == "actioncheck": - stream.append(head + ["actioncheck", self._opts[opt]]) - elif opt == "actionban": - stream.append(head + ["actionban", self._opts[opt]]) - elif opt == "actionunban": - stream.append(head + ["actionunban", self._opts[opt]]) + multi = [] + for opt, optval in self._opts.iteritems(): + if opt in self._configOpts: + multi.append([opt, optval]) if self._initOpts: - for p in self._initOpts: - stream.append(head + [p, self._initOpts[p]]) + for opt, optval in self._initOpts.iteritems(): + multi.append([opt, optval]) + if len(multi) > 1: + stream.append(["multi-set", self._jailName, "action", self._name, multi]) + elif len(multi): + stream.append(["set", self._jailName, "action", self._name] + multi[0]) return stream diff --git a/fail2ban/client/configparserinc.py b/fail2ban/client/configparserinc.py index 7bbc7886..f4975857 100644 --- a/fail2ban/client/configparserinc.py +++ b/fail2ban/client/configparserinc.py @@ -231,7 +231,7 @@ after = 1.conf # save previous known values, for possible using in local interpolations later: sk = {} for k, v in s2.iteritems(): - if not k.startswith('known/'): + if not k.startswith('known/') and k != '__name__': sk['known/'+k] = v s2.update(sk) # merge section @@ -256,7 +256,7 @@ after = 1.conf alls = self.get_sections() sk = {} for k, v in options.iteritems(): - if pref == '' or not k.startswith(pref): + if pref == '' or (not k.startswith(pref) and k != '__name__'): sk[pref+k] = v alls[section].update(sk) diff --git a/fail2ban/client/configreader.py b/fail2ban/client/configreader.py index c6dd1b60..37e66249 100644 --- a/fail2ban/client/configreader.py +++ b/fail2ban/client/configreader.py @@ -203,40 +203,47 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes): # # Read the given option in the configuration file. Default values # are used... - # Each optionValues entry is composed of an array with: - # 0 -> the type of the option - # 1 -> the name of the option - # 2 -> the default value for the option + # Each options entry is composed of an array with: + # [[type, name, default], ...] + # Or it is a dict: + # {name: [type, default], ...} def getOptions(self, sec, options, pOptions=None): values = dict() - for option in options: - try: - if option[0] == "bool": - v = self.getboolean(sec, option[1]) - elif option[0] == "int": - v = self.getint(sec, option[1]) + for optname in options: + if isinstance(options, (list,tuple)): + if len(optname) > 2: + opttype, optname, optvalue = optname else: - v = self.get(sec, option[1]) - if not pOptions is None and option[1] in pOptions: + (opttype, optname), optvalue = optname, None + else: + opttype, optvalue = options[optname] + try: + if opttype == "bool": + v = self.getboolean(sec, optname) + elif opttype == "int": + v = self.getint(sec, optname) + else: + v = self.get(sec, optname) + if not pOptions is None and optname in pOptions: continue - values[option[1]] = v + values[optname] = v except NoSectionError, e: # No "Definition" section or wrong basedir logSys.error(e) - values[option[1]] = option[2] + values[optname] = optvalue # TODO: validate error handling here. except NoOptionError: - if not option[2] is None: + if not optvalue is None: logSys.warning("'%s' not defined in '%s'. Using default one: %r" - % (option[1], sec, option[2])) - values[option[1]] = option[2] + % (optname, sec, optvalue)) + values[optname] = optvalue elif logSys.getEffectiveLevel() <= logLevel: - logSys.log(logLevel, "Non essential option '%s' not defined in '%s'.", option[1], sec) + logSys.log(logLevel, "Non essential option '%s' not defined in '%s'.", optname, sec) except ValueError: - logSys.warning("Wrong value for '" + option[1] + "' in '" + sec + - "'. Using default one: '" + repr(option[2]) + "'") - values[option[1]] = option[2] + logSys.warning("Wrong value for '" + optname + "' in '" + sec + + "'. Using default one: '" + repr(optvalue) + "'") + values[optname] = optvalue return values @@ -286,7 +293,8 @@ class DefinitionInitConfigReader(ConfigReader): if self.has_section("Init"): for opt in self.options("Init"): v = self.get("Init", opt) - self._initOpts['known/'+opt] = v + if not opt.startswith('known/') and opt != '__name__': + self._initOpts['known/'+opt] = v if not opt in self._initOpts: self._initOpts[opt] = v diff --git a/fail2ban/client/fail2banregex.py b/fail2ban/client/fail2banregex.py index f1a9c887..9ecb7229 100755 --- a/fail2ban/client/fail2banregex.py +++ b/fail2ban/client/fail2banregex.py @@ -291,7 +291,14 @@ class Fail2banRegex(object): RegexStat(m[3]) for m in filter( lambda x: x[0] == 'set' and x[2] == "add%sregex" % regextype, - readercommands)] + readercommands) + ] + [ + RegexStat(m) + for mm in filter( + lambda x: x[0] == 'multi-set' and x[2] == "add%sregex" % regextype, + readercommands) + for m in mm[3] + ] # Read out and set possible value of maxlines for command in readercommands: if command[2] == "maxlines": diff --git a/fail2ban/client/filterreader.py b/fail2ban/client/filterreader.py index 318c8c9a..8b30f914 100644 --- a/fail2ban/client/filterreader.py +++ b/fail2ban/client/filterreader.py @@ -37,10 +37,10 @@ logSys = getLogger(__name__) class FilterReader(DefinitionInitConfigReader): - _configOpts = [ - ["string", "ignoreregex", None], - ["string", "failregex", ""], - ] + _configOpts = { + "ignoreregex": ["string", None], + "failregex": ["string", ""], + } def setFile(self, fileName): self.__file = fileName @@ -64,16 +64,16 @@ class FilterReader(DefinitionInitConfigReader): if not len(opts): return stream for opt, value in opts.iteritems(): - if opt == "failregex": + if opt in ("failregex", "ignoreregex"): + multi = [] for regex in value.split('\n'): # Do not send a command if the rule is empty. if regex != '': - stream.append(["set", self._jailName, "addfailregex", regex]) - elif opt == "ignoreregex": - for regex in value.split('\n'): - # Do not send a command if the rule is empty. - if regex != '': - stream.append(["set", self._jailName, "addignoreregex", regex]) + multi.append(regex) + if len(multi) > 1: + stream.append(["multi-set", self._jailName, "add" + opt, multi]) + elif len(multi): + stream.append(["set", self._jailName, "add" + opt, multi[0]]) if self._initOpts: if 'maxlines' in self._initOpts: # We warn when multiline regex is used without maxlines > 1 diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index 327ddf1b..fda5d40c 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -190,11 +190,11 @@ class JailReader(ConfigReader): """ stream = [] - for opt in self.__opts: + for opt, value in self.__opts.iteritems(): if opt == "logpath" and \ self.__opts.get('backend', None) != "systemd": found_files = 0 - for path in self.__opts[opt].split("\n"): + for path in value.split("\n"): path = path.rsplit(" ", 1) path, tail = path if len(path) > 1 else (path[0], "head") pathList = JailReader._glob(path) @@ -208,32 +208,32 @@ class JailReader(ConfigReader): raise ValueError( "Have not found any log file for %s jail" % self.__name) elif opt == "logencoding": - stream.append(["set", self.__name, "logencoding", self.__opts[opt]]) + stream.append(["set", self.__name, "logencoding", value]) elif opt == "backend": - backend = self.__opts[opt] + backend = value elif opt == "maxretry": - stream.append(["set", self.__name, "maxretry", self.__opts[opt]]) + stream.append(["set", self.__name, "maxretry", value]) elif opt == "ignoreip": - for ip in splitcommaspace(self.__opts[opt]): + for ip in splitcommaspace(value): stream.append(["set", self.__name, "addignoreip", ip]) elif opt == "findtime": - stream.append(["set", self.__name, "findtime", self.__opts[opt]]) + stream.append(["set", self.__name, "findtime", value]) elif opt == "bantime": - stream.append(["set", self.__name, "bantime", self.__opts[opt]]) + stream.append(["set", self.__name, "bantime", value]) elif opt == "usedns": - stream.append(["set", self.__name, "usedns", self.__opts[opt]]) - elif opt == "failregex": - for regex in self.__opts[opt].split('\n'): + stream.append(["set", self.__name, "usedns", value]) + elif opt in ("failregex", "ignoreregex"): + multi = [] + for regex in value.split('\n'): # Do not send a command if the rule is empty. if regex != '': - stream.append(["set", self.__name, "addfailregex", regex]) + multi.append(regex) + if len(multi) > 1: + stream.append(["multi-set", self.__name, "add" + opt, multi]) + elif len(multi): + stream.append(["set", self.__name, "add" + opt, multi[0]]) elif opt == "ignorecommand": - stream.append(["set", self.__name, "ignorecommand", self.__opts[opt]]) - elif opt == "ignoreregex": - for regex in self.__opts[opt].split('\n'): - # Do not send a command if the rule is empty. - if regex != '': - stream.append(["set", self.__name, "addignoreregex", regex]) + stream.append(["set", self.__name, "ignorecommand", value]) if self.__filter: stream.extend(self.__filter.convert()) for action in self.__actions: diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 3bdfd71b..7f75c347 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -262,8 +262,13 @@ class Server: def getIgnoreCommand(self, name): return self.__jails[name].filter.getIgnoreCommand() - def addFailRegex(self, name, value): - self.__jails[name].filter.addFailRegex(value) + def addFailRegex(self, name, value, multiple=False): + flt = self.__jails[name].filter + if multiple: + for value in value: + flt.addFailRegex(value) + else: + flt.addFailRegex(value) def delFailRegex(self, name, index): self.__jails[name].filter.delFailRegex(index) @@ -271,8 +276,13 @@ class Server: def getFailRegex(self, name): return self.__jails[name].filter.getFailRegex() - def addIgnoreRegex(self, name, value): - self.__jails[name].filter.addIgnoreRegex(value) + def addIgnoreRegex(self, name, value, multiple=False): + flt = self.__jails[name].filter + if multiple: + for value in value: + flt.addIgnoreRegex(value) + else: + flt.addIgnoreRegex(value) def delIgnoreRegex(self, name, index): self.__jails[name].filter.delIgnoreRegex(index) diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 4c4c32f7..29d6d189 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -99,6 +99,8 @@ class Transmitter: return None elif command[0] == "flushlogs": return self.__server.flushLogs() + elif command[0] == "multi-set": + return self.__commandSet(command[1:], True) elif command[0] == "set": return self.__commandSet(command[1:]) elif command[0] == "get": @@ -109,7 +111,7 @@ class Transmitter: return version.version raise Exception("Invalid command") - def __commandSet(self, command): + def __commandSet(self, command, multiple=False): name = command[0] # Logging if name == "loglevel": @@ -196,7 +198,9 @@ class Transmitter: return self.__server.getJournalMatch(name) elif command[1] == "addfailregex": value = command[2] - self.__server.addFailRegex(name, value) + self.__server.addFailRegex(name, value, multiple=multiple) + if multiple: + return True return self.__server.getFailRegex(name) elif command[1] == "delfailregex": value = int(command[2]) @@ -204,7 +208,9 @@ class Transmitter: return self.__server.getFailRegex(name) elif command[1] == "addignoreregex": value = command[2] - self.__server.addIgnoreRegex(name, value) + self.__server.addIgnoreRegex(name, value, multiple=multiple) + if multiple: + return True return self.__server.getIgnoreRegex(name) elif command[1] == "delignoreregex": value = int(command[2]) @@ -254,15 +260,26 @@ class Transmitter: return None elif command[1] == "action": actionname = command[2] - actionkey = command[3] action = self.__server.getAction(name, actionname) - if callable(getattr(action, actionkey, None)): - actionvalue = json.loads(command[4]) if len(command)>4 else {} - return getattr(action, actionkey)(**actionvalue) + if multiple: + for cmd in command[3]: + actionkey = cmd[0] + if callable(getattr(action, actionkey, None)): + actionvalue = json.loads(cmd[1]) if len(cmd)>1 else {} + getattr(action, actionkey)(**actionvalue) + else: + actionvalue = cmd[1] + setattr(action, actionkey, actionvalue) + return True else: - actionvalue = command[4] - setattr(action, actionkey, actionvalue) - return getattr(action, actionkey) + actionkey = command[3] + if callable(getattr(action, actionkey, None)): + actionvalue = json.loads(command[4]) if len(command)>4 else {} + return getattr(action, actionkey)(**actionvalue) + else: + actionvalue = command[4] + setattr(action, actionkey, actionvalue) + return getattr(action, actionkey) raise Exception("Invalid command (no set action or not yet implemented)") def __commandGet(self, command): diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index bd734c1b..0edbc69e 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -275,7 +275,15 @@ class JailReaderTest(LogCaptureTestCase): # convert and get stream stream = jail.convert() # get action and retrieve agent from it, compare with agent saved in version: - act = [o for o in stream if len(o) > 4 and (o[4] == 'agent' or o[4].endswith('badips.py'))] + act = [] + for cmd in stream: + if len(cmd) <= 4: + continue + # differentiate between set and multi-set (wrop it here to single set): + if cmd[0] == 'set' and (cmd[4] == 'agent' or cmd[4].endswith('badips.py')): + act.append(cmd) + elif cmd[0] == 'multi-set': + act.extend([['set'] + cmd[1:4] + o for o in cmd[4] if o[0] == 'agent']) useragent = 'Fail2Ban/%s' % version self.assertEqual(len(act), 4) self.assertEqual(act[0], ['set', 'blocklisttest', 'action', 'blocklist_de', 'agent', useragent]) @@ -311,23 +319,21 @@ class FilterReaderTest(unittest.TestCase): self.__share_cfg = {} def testConvert(self): - output = [['set', 'testcase01', 'addfailregex', + output = [['multi-set', 'testcase01', 'addfailregex', [ "^\\s*(?:\\S+ )?(?:kernel: \\[\\d+\\.\\d+\\] )?(?:@vserver_\\S+ )" "?(?:(?:\\[\\d+\\])?:\\s+[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?|" "[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?(?:\\[\\d+\\])?:)?\\s*(?:" - "error: PAM: )?Authentication failure for .* from \\s*$"], - ['set', 'testcase01', 'addfailregex', + "error: PAM: )?Authentication failure for .* from \\s*$", "^\\s*(?:\\S+ )?(?:kernel: \\[\\d+\\.\\d+\\] )?(?:@vserver_\\S+ )" "?(?:(?:\\[\\d+\\])?:\\s+[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?|" "[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?(?:\\[\\d+\\])?:)?\\s*(?:" "error: PAM: )?User not known to the underlying authentication mo" - "dule for .* from \\s*$"], - ['set', 'testcase01', 'addfailregex', + "dule for .* from \\s*$", "^\\s*(?:\\S+ )?(?:kernel: \\[\\d+\\.\\d+\\] )?(?:@vserver_\\S+ )" "?(?:(?:\\[\\d+\\])?:\\s+[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?|" "[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?(?:\\[\\d+\\])?:)?\\s*(?:" "error: PAM: )?User not known to the\\nunderlying authentication." - "+$^.+ module for .* from \\s*$"], + "+$^.+ module for .* from \\s*$"]], ['set', 'testcase01', 'addignoreregex', "^.+ john from host 192.168.1.1\\s*$"], ['set', 'testcase01', 'addjournalmatch', @@ -495,9 +501,11 @@ class JailsReaderTest(LogCaptureTestCase): self.assertEqual(sorted(comm_commands), sorted([['add', 'emptyaction', 'auto'], ['add', 'test-known-interp', 'auto'], - ['set', 'test-known-interp', 'addfailregex', 'failure test 1 (filter.d/test.conf) '], - ['set', 'test-known-interp', 'addfailregex', 'failure test 2 (filter.d/test.local) '], - ['set', 'test-known-interp', 'addfailregex', 'failure test 3 (jail.local) '], + ['multi-set', 'test-known-interp', 'addfailregex', [ + 'failure test 1 (filter.d/test.conf) ', + 'failure test 2 (filter.d/test.local) ', + 'failure test 3 (jail.local) ' + ]], ['start', 'test-known-interp'], ['add', 'missinglogfiles', 'auto'], ['set', 'missinglogfiles', 'addfailregex', ''], @@ -660,12 +668,16 @@ class JailsReaderTest(LogCaptureTestCase): self.assertTrue('blocktype' in action._initOpts) # Verify that we have a call to set it up blocktype_present = False - target_command = ['set', jail_name, 'action', action_name, 'blocktype'] + target_command = [jail_name, 'action', action_name] for command in commands: - if (len(command) > 5 and - command[:5] == target_command): - blocktype_present = True - continue + if (len(command) > 4 and command[0] == 'multi-set' and + command[1:4] == target_command): + blocktype_present = ('blocktype' in [cmd[0] for cmd in command[4]]) + elif (len(command) > 5 and command[0] == 'set' and + command[1:4] == target_command and command[4] == 'blocktype'): # pragma: no cover - because of multi-set + blocktype_present = True + if blocktype_present: + break self.assertTrue( blocktype_present, msg="Found no %s command among %s" diff --git a/fail2ban/tests/samplestestcase.py b/fail2ban/tests/samplestestcase.py index 2ed77554..074ba24c 100644 --- a/fail2ban/tests/samplestestcase.py +++ b/fail2ban/tests/samplestestcase.py @@ -72,14 +72,21 @@ def testSampleRegexsFactory(name): filterConf.getOptions({}) for opt in filterConf.convert(): - if opt[2] == "addfailregex": - self.filter.addFailRegex(opt[3]) - elif opt[2] == "maxlines": - self.filter.setMaxLines(opt[3]) - elif opt[2] == "addignoreregex": - self.filter.addIgnoreRegex(opt[3]) - elif opt[2] == "datepattern": - self.filter.setDatePattern(opt[3]) + if opt[0] == 'multi-set': + optval = opt[3] + elif opt[0] == 'set': + optval = [opt[3]] + else: + continue + for optval in optval: + if opt[2] == "addfailregex": + self.filter.addFailRegex(optval) + elif opt[2] == "addignoreregex": + self.filter.addIgnoreRegex(optval) + elif opt[2] == "maxlines": + self.filter.setMaxLines(optval) + elif opt[2] == "datepattern": + self.filter.setDatePattern(optval) self.assertTrue( os.path.isfile(os.path.join(TEST_FILES_DIR, "logs", name)), diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index bac64dd1..0e109a01 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -1024,7 +1024,10 @@ class ServerConfigReaderTests(LogCaptureTestCase): cmd[3] = os.path.join(TEST_FILES_DIR, 'logs', cmd[1]) # add dummy regex to prevent too long compile of all regexp (we don't use it in this test at all): # [todo sebres] remove `not hasattr(unittest, 'F2B') or `, after merge with "f2b-perfom-prepare-716" ... - elif (not hasattr(unittest, 'F2B') or unittest.F2B.fast) and len(cmd) > 3 and cmd[0] == 'set' and cmd[2] == 'addfailregex': + elif (not hasattr(unittest, 'F2B') or unittest.F2B.fast) and ( + len(cmd) > 3 and cmd[0] in ('set', 'multi-set') and cmd[2] == 'addfailregex' + ): + cmd[0] = "set" cmd[3] = "DUMMY-REGEX " # command to server, use cmdHandler direct instead of `transm.proceed(cmd)`: try: @@ -1058,7 +1061,7 @@ class ServerConfigReaderTests(LogCaptureTestCase): testJailsActions = ( ('j-w-iptables-mp', 'iptables-multiport[name=%(__name__)s, bantime="600", port="http,https", protocol="tcp", chain="INPUT"]', { - 'ip4': '`iptables ', 'ip6': '`ip6tables ', + 'ip4': ('`iptables ',), 'ip6': ('`ip6tables ',), 'start': ( "`iptables -w -N f2b-j-w-iptables-mp`", "`iptables -w -A f2b-j-w-iptables-mp -j RETURN`", @@ -1095,7 +1098,7 @@ class ServerConfigReaderTests(LogCaptureTestCase): ), }), ('j-w-iptables-ap', 'iptables-allports[name=%(__name__)s, bantime="600", protocol="tcp", chain="INPUT"]', { - 'ip4': '`iptables ', 'ip6': '`ip6tables ', + 'ip4': ('`iptables ',), 'ip6': ('`ip6tables ',), 'start': ( "`iptables -w -N f2b-j-w-iptables-ap`", "`iptables -w -A f2b-j-w-iptables-ap -j RETURN`", @@ -1132,7 +1135,7 @@ class ServerConfigReaderTests(LogCaptureTestCase): ), }), ('j-w-iptables-ipset', 'iptables-ipset-proto6[name=%(__name__)s, bantime="600", port="http", protocol="tcp", chain="INPUT"]', { - 'ip4': ' f2b-j-w-iptables-ipset ', 'ip6': ' f2b-j-w-iptables-ipset6 ', + 'ip4': (' f2b-j-w-iptables-ipset ',), 'ip6': (' f2b-j-w-iptables-ipset6 ',), 'start': ( "`ipset create f2b-j-w-iptables-ipset hash:ip timeout 600`", "`iptables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset src -j REJECT --reject-with icmp-port-unreachable`", @@ -1196,22 +1199,22 @@ class ServerConfigReaderTests(LogCaptureTestCase): logSys.debug('# === ban-ipv4 ==='); self.pruneLog() action.ban({'ip': IPAddr('192.0.2.1')}) self.assertLogged(*tests['ip4-check']+tests['ip4-ban'], all=True) - self.assertNotLogged(tests['ip6']) + self.assertNotLogged(*tests['ip6'], all=True) # test unban ip4 : logSys.debug('# === unban ipv4 ==='); self.pruneLog() action.unban({'ip': IPAddr('192.0.2.1')}) self.assertLogged(*tests['ip4-check']+tests['ip4-unban'], all=True) - self.assertNotLogged(tests['ip6']) + self.assertNotLogged(*tests['ip6'], all=True) # test ban ip6 : logSys.debug('# === ban ipv6 ==='); self.pruneLog() action.ban({'ip': IPAddr('2001:DB8::')}) self.assertLogged(*tests['ip6-check']+tests['ip6-ban'], all=True) - self.assertNotLogged(tests['ip4']) + self.assertNotLogged(*tests['ip4'], all=True) # test unban ip6 : logSys.debug('# === unban ipv6 ==='); self.pruneLog() action.unban({'ip': IPAddr('2001:DB8::')}) self.assertLogged(*tests['ip6-check']+tests['ip6-unban'], all=True) - self.assertNotLogged(tests['ip4']) + self.assertNotLogged(*tests['ip4'], all=True) # test stop : logSys.debug('# === stop ==='); self.pruneLog() action.stop() diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 52097fa5..e97daebf 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -335,7 +335,7 @@ class LogCaptureTestCase(unittest.TestCase): ---------- s : string or list/set/tuple of strings Test should succeed if string (or any of the listed) is present in the log - all : boolean, should find all in s + all : boolean (default False) if True should fail if any of s not logged """ logged = self._log.getvalue() if not kwargs.get('all', False): @@ -343,16 +343,15 @@ class LogCaptureTestCase(unittest.TestCase): for s_ in s: if s_ in logged: return - # pragma: no cover - self.fail("None among %r was found in the log: ===\n%s===" % (s, logged)) + if True: # pragma: no cover + self.fail("None among %r was found in the log: ===\n%s===" % (s, logged)) else: # each entry should be found: for s_ in s: - if s_ not in logged: - # pragma: no cover + if s_ not in logged: # pragma: no cover self.fail("%r was not found in the log: ===\n%s===" % (s_, logged)) - def assertNotLogged(self, *s): + def assertNotLogged(self, *s, **kwargs): """Assert that strings were not logged Parameters @@ -360,13 +359,19 @@ class LogCaptureTestCase(unittest.TestCase): s : string or list/set/tuple of strings Test should succeed if the string (or at least one of the listed) is not present in the log + all : boolean (default False) if True should fail if any of s logged """ logged = self._log.getvalue() - for s_ in s: - if s_ not in logged: - return - # pragma: no cover - self.fail("All of the %r were found present in the log: ===\n%s===" % (s, logged)) + if not kwargs.get('all', False): + for s_ in s: + if s_ not in logged: + return + if True: # pragma: no cover + self.fail("All of the %r were found present in the log: ===\n%s===" % (s, logged)) + else: + for s_ in s: + if s_ in logged: # pragma: no cover + self.fail("%r was found in the log: ===\n%s===" % (s_, logged)) def pruneLog(self): self._log.truncate(0) From ed2f3ef77d4c0443790bd0e99c4f54f1cc7668fc Mon Sep 17 00:00:00 2001 From: Alexander Koeppe Date: Mon, 14 Mar 2016 20:30:19 +0100 Subject: [PATCH 058/623] improve PF action and make IPv6 aware --- config/action.d/pf.conf | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/config/action.d/pf.conf b/config/action.d/pf.conf index edcaa175..62296458 100644 --- a/config/action.d/pf.conf +++ b/config/action.d/pf.conf @@ -3,6 +3,7 @@ # OpenBSD pf ban/unban # # Author: Nick Hilliard +# Modified by: Alexander Koeppe making PF work seamless and with IPv4 and IPv6 # # @@ -12,23 +13,27 @@ # Notes.: command executed once at the start of Fail2Ban. # Values: CMD # -# we don't enable PF automatically, as it will be enabled elsewhere -actionstart = +# we don't enable PF automatically; to enable run pfctl -e +# or add `pf_enable="YES"` to /etc/rc.conf (tested on FreeBSD) +actionstart = echo "table <-> persist counters" | pfctl -f- + echo "block proto from <-> to any port " | pfctl -f- # Option: actionstop # Notes.: command executed once at the end of Fail2Ban # Values: CMD # -# we don't disable PF automatically either -actionstop = +# we only disable PF rules we've installed prior +actionstop = pfctl -sr 2>/dev/null | grep -v '-' | pfctl -f- + pfctl -t - -T flush + pfctl -t - -T kill # Option: actioncheck # Notes.: command executed once before each actionban command # Values: CMD # -actioncheck = +actioncheck = pfctl -sr | grep -q '-' # Option: actionban @@ -39,7 +44,7 @@ actioncheck = #