From 4f636eb0e3295ee1e8a35f1398926e95ab8b39ba Mon Sep 17 00:00:00 2001 From: SlowRiot Date: Fri, 26 Sep 2014 16:25:07 +0100 Subject: [PATCH 1/8] adding filter to detect Shellshock attack attempts against bash scripts through apache. See http://seclists.org/oss-sec/2014/q3/650 --- config/filter.d/apache-shellshock.conf | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 config/filter.d/apache-shellshock.conf diff --git a/config/filter.d/apache-shellshock.conf b/config/filter.d/apache-shellshock.conf new file mode 100644 index 00000000..39df1704 --- /dev/null +++ b/config/filter.d/apache-shellshock.conf @@ -0,0 +1,26 @@ +# Fail2Ban filter to block web requests containing custom headers attempting to exploit the shellshock bug +# +# + +[INCLUDES] + +# overwrite with apache-common.local if _apache_error_client is incorrect. +before = apache-common.conf + +[Definition] + +failregex = ^%(_apache_error_client)s (AH01215: )?/bin/(ba)?sh: warning: HTTP_.*?: ignoring function definition attempt(, referer: \S+)?\s*$ + ^%(_apache_error_client)s (AH01215: )?/bin/(ba)?sh: error importing function definition for `HTTP_.*?'(, referer: \S+)?\s*$ + +ignoreregex = + + +# DEV Notes: +# +# https://wiki.apache.org/httpd/ListOfErrors for apache error IDs +# +# example log lines: +# [Thu Sep 25 09:27:18.813902 2014] [cgi:error] [pid 16860] [client 89.207.132.76:59635] AH01215: /bin/bash: warning: HTTP_TEST: ignoring function definition attempt +# [Thu Sep 25 09:29:56.141832 2014] [cgi:error] [pid 16864] [client 162.247.73.206:41273] AH01215: /bin/bash: error importing function definition for `HTTP_TEST' +# +# Author: Eugene Hopkinson (riot@riot.so) From fc5f729f01dfaa8aae21e7f7a9603caf2e6fa626 Mon Sep 17 00:00:00 2001 From: SlowRiot Date: Fri, 26 Sep 2014 16:37:50 +0100 Subject: [PATCH 2/8] adding jail conf for shellshock filter --- config/jail.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/jail.conf b/config/jail.conf index c48e6a7b..99729350 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -283,6 +283,11 @@ port = http,https logpath = %(apache_error_log)s maxretry = 2 +[apache-shellshock] + +port = http,https +logpath = $(apache_error_log)s +maxretry = 1 [nginx-http-auth] From 7b5dc9f24f5624ebe6d42048b3e81920a214c7fe Mon Sep 17 00:00:00 2001 From: SlowRiot Date: Fri, 26 Sep 2014 18:48:56 +0100 Subject: [PATCH 3/8] adding test case, changelog and thanks entries for apache shellshock filter --- ChangeLog | 1 + THANKS | 1 + 2 files changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index 1a98c1a0..d92aec4a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -57,6 +57,7 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger - New filters: - monit Thanks Jason H Martin - directadmin Thanks niorg + - apache-shellshock Thanks Eugene Hopkinson (SlowRiot) - New actions: - symbiosis-blacklist-allports for Bytemark symbiosis firewall - fail2ban-client can fetch the running server version diff --git a/THANKS b/THANKS index 42887a05..0433f7ed 100644 --- a/THANKS +++ b/THANKS @@ -34,6 +34,7 @@ David Nutter Derek Atkins Eric Gerbier Enrico Labedzki +Eugene Hopkinson (SlowRiot) ftoppi François Boulogne Frédéric From 5d526bbeb15d484039c86ce655e42ce31b462714 Mon Sep 17 00:00:00 2001 From: SlowRiot Date: Mon, 29 Sep 2014 00:49:22 +0100 Subject: [PATCH 4/8] forgot to add test case to last commit --- fail2ban/tests/files/logs/apache-shellshock | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 fail2ban/tests/files/logs/apache-shellshock diff --git a/fail2ban/tests/files/logs/apache-shellshock b/fail2ban/tests/files/logs/apache-shellshock new file mode 100644 index 00000000..0acf4546 --- /dev/null +++ b/fail2ban/tests/files/logs/apache-shellshock @@ -0,0 +1,4 @@ +# failJSON: { "time": "2014-09-25T09:27:18", "match": true , "host": "89.207.132.76" } +[Thu Sep 25 09:27:18.813902 2014] [cgi:error] [pid 16860] [client 89.207.132.76:59635] AH01215: /bin/bash: warning: HTTP_TEST: ignoring function definition attempt +# failJSON: { "time": "2014-09-25T09:29:56", "match": true , "host": "162.247.73.206" } +[Thu Sep 25 09:29:56.141832 2014] [cgi:error] [pid 16864] [client 162.247.73.206:41273] AH01215: /bin/bash: error importing function definition for `HTTP_TEST' From 270ea363d35dd71fc2ca378e1606aaffe1863ec3 Mon Sep 17 00:00:00 2001 From: Daniel Schaal Date: Sun, 14 Sep 2014 08:38:39 +0200 Subject: [PATCH 5/8] tests: define CONFIG_DIR in utils. --- fail2ban/tests/action_d/test_badips.py | 6 +----- fail2ban/tests/action_d/test_smtp.py | 5 +---- fail2ban/tests/clientreadertestcase.py | 4 +++- fail2ban/tests/samplestestcase.py | 6 +----- fail2ban/tests/utils.py | 9 +++++++++ 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/fail2ban/tests/action_d/test_badips.py b/fail2ban/tests/action_d/test_badips.py index 30e016ea..a7f148b1 100644 --- a/fail2ban/tests/action_d/test_badips.py +++ b/fail2ban/tests/action_d/test_badips.py @@ -22,11 +22,7 @@ import unittest import sys from ..dummyjail import DummyJail - -if os.path.exists('config/fail2ban.conf'): - CONFIG_DIR = "config" -else: - CONFIG_DIR='/etc/fail2ban' +from ..utils import CONFIG_DIR if sys.version_info >= (2,7): class BadIPsActionTest(unittest.TestCase): diff --git a/fail2ban/tests/action_d/test_smtp.py b/fail2ban/tests/action_d/test_smtp.py index a8a8a1a9..440db55c 100644 --- a/fail2ban/tests/action_d/test_smtp.py +++ b/fail2ban/tests/action_d/test_smtp.py @@ -30,10 +30,7 @@ else: from ..dummyjail import DummyJail -if os.path.exists('config/fail2ban.conf'): - CONFIG_DIR = "config" -else: - CONFIG_DIR='/etc/fail2ban' +from ..utils import CONFIG_DIR class TestSMTPServer(smtpd.SMTPServer): diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index ce19a50e..0ad3c66e 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -32,8 +32,10 @@ from ..client.configurator import Configurator from .utils import LogCaptureTestCase TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") + +from .utils import CONFIG_DIR + STOCK = os.path.exists(os.path.join('config','fail2ban.conf')) -CONFIG_DIR='config' if STOCK else '/etc/fail2ban' IMPERFECT_CONFIG = os.path.join(os.path.dirname(__file__), 'config') diff --git a/fail2ban/tests/samplestestcase.py b/fail2ban/tests/samplestestcase.py index 132ade7b..e0831184 100644 --- a/fail2ban/tests/samplestestcase.py +++ b/fail2ban/tests/samplestestcase.py @@ -32,13 +32,9 @@ else: from ..server.filter import Filter from ..client.filterreader import FilterReader -from .utils import setUpMyTime, tearDownMyTime +from .utils import setUpMyTime, tearDownMyTime, CONFIG_DIR TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") -if os.path.exists('config/fail2ban.conf'): - CONFIG_DIR = "config" -else: - CONFIG_DIR='/etc/fail2ban' class FilterSamplesRegex(unittest.TestCase): diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 0cb8d5b8..912c5a90 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -34,6 +34,15 @@ from ..helpers import getLogger logSys = getLogger(__name__) +CONFIG_DIR = os.environ.get('FAIL2BAN_CONFIG_DIR', None) + +if not CONFIG_DIR: +# Use heuristic to figure out where configuration files are + if os.path.exists(os.path.join('config','fail2ban.conf')): + CONFIG_DIR = 'config' + else: + CONFIG_DIR = '/etc/fail2ban' + def mtimesleep(): # no sleep now should be necessary since polling tracks now not only # mtime but also ino and size From d00af327c54eb57991600e5d2c61b15eb31f4c18 Mon Sep 17 00:00:00 2001 From: sebres Date: Thu, 2 Oct 2014 22:29:09 +0200 Subject: [PATCH 6/8] caching of read config files, to make start of fail2ban faster, see issue #820 --- fail2ban/client/configparserinc.py | 186 ++++++++++++++++++------- fail2ban/client/configreader.py | 21 +-- fail2ban/tests/clientreadertestcase.py | 16 ++- 3 files changed, 158 insertions(+), 65 deletions(-) diff --git a/fail2ban/client/configparserinc.py b/fail2ban/client/configparserinc.py index 80b99517..ad0a255c 100644 --- a/fail2ban/client/configparserinc.py +++ b/fail2ban/client/configparserinc.py @@ -65,7 +65,136 @@ logSys = getLogger(__name__) __all__ = ['SafeConfigParserWithIncludes'] -class SafeConfigParserWithIncludes(SafeConfigParser): +class SafeConfigParserWithIncludes(object): + + SECTION_NAME = "INCLUDES" + CFG_CACHE = {} + CFG_INC_CACHE = {} + CFG_EMPY_CFG = None + + def __init__(self): + self.__cr = None + + def __check_read(self, attr): + if self.__cr is None: + # raise RuntimeError("Access to wrapped attribute \"%s\" before read call" % attr) + if SafeConfigParserWithIncludes.CFG_EMPY_CFG is None: + SafeConfigParserWithIncludes.CFG_EMPY_CFG = _SafeConfigParserWithIncludes() + self.__cr = SafeConfigParserWithIncludes.CFG_EMPY_CFG + + def __getattr__(self,attr): + # check we access local implementation + try: + orig_attr = self.__getattribute__(attr) + except AttributeError: + self.__check_read(attr) + orig_attr = self.__cr.__getattribute__(attr) + return orig_attr + + @staticmethod + def _resource_mtime(resource): + mt = [] + dirnames = [] + for filename in resource: + if os.path.exists(filename): + s = os.stat(filename) + mt.append(s.st_mtime) + mt.append(s.st_mode) + mt.append(s.st_size) + dirname = os.path.dirname(filename) + if dirname not in dirnames: + dirnames.append(dirname) + for dirname in dirnames: + if os.path.exists(dirname): + s = os.stat(dirname) + mt.append(s.st_mtime) + mt.append(s.st_mode) + mt.append(s.st_size) + return mt + + def read(self, resource, get_includes=True, log_info=None): + SCPWI = SafeConfigParserWithIncludes + # check includes : + fileNamesFull = [] + if not isinstance(resource, list): + resource = [ resource ] + if get_includes: + for filename in resource: + fileNamesFull += SCPWI.getIncludes(filename) + else: + fileNamesFull = resource + # check cache + hashv = '///'.join(fileNamesFull) + cr, ret, mtime = SCPWI.CFG_CACHE.get(hashv, (None, False, 0)) + curmt = SCPWI._resource_mtime(fileNamesFull) + if cr is not None and mtime == curmt: + self.__cr = cr + logSys.debug("Cached config files: %s", resource) + #logSys.debug("Cached config files: %s", fileNamesFull) + return ret + # not yet in cache - create/read and add to cache: + if log_info is not None: + logSys.info(*log_info) + cr = _SafeConfigParserWithIncludes() + ret = cr.read(fileNamesFull) + SCPWI.CFG_CACHE[hashv] = (cr, ret, curmt) + self.__cr = cr + return ret + + def getOptions(self, *args, **kwargs): + self.__check_read('getOptions') + return self.__cr.getOptions(*args, **kwargs) + + @staticmethod + def getIncludes(resource, seen = []): + """ + Given 1 config resource returns list of included files + (recursively) with the original one as well + Simple loops are taken care about + """ + + # Use a short class name ;) + SCPWI = SafeConfigParserWithIncludes + + resources = seen + [resource] + # check cache + hashv = '///'.join(resources) + cinc, mtime = SCPWI.CFG_INC_CACHE.get(hashv, (None, 0)) + curmt = SCPWI._resource_mtime(resources) + if cinc is not None and mtime == curmt: + return cinc + + parser = SCPWI() + try: + # read without includes + parser.read(resource, get_includes = False) + except UnicodeDecodeError, e: + logSys.error("Error decoding config file '%s': %s" % (resource, e)) + return [] + + resourceDir = os.path.dirname(resource) + + newFiles = [ ('before', []), ('after', []) ] + if SCPWI.SECTION_NAME in parser.sections(): + for option_name, option_list in newFiles: + if option_name in parser.options(SCPWI.SECTION_NAME): + newResources = parser.get(SCPWI.SECTION_NAME, option_name) + for newResource in newResources.split('\n'): + if os.path.isabs(newResource): + r = newResource + else: + r = os.path.join(resourceDir, newResource) + if r in seen: + continue + option_list += SCPWI.getIncludes(r, resources) + # combine lists + cinc = newFiles[0][1] + [resource] + newFiles[1][1] + # cache and return : + SCPWI.CFG_INC_CACHE[hashv] = (cinc, curmt) + return cinc + #print "Includes list for " + resource + " is " + `resources` + +class _SafeConfigParserWithIncludes(SafeConfigParser, object): """ Class adds functionality to SafeConfigParser to handle included other configuration files (or may be urls, whatever in the future) @@ -94,69 +223,22 @@ after = 1.conf """ - SECTION_NAME = "INCLUDES" - if sys.version_info >= (3,2): # overload constructor only for fancy new Python3's def __init__(self, *args, **kwargs): kwargs = kwargs.copy() kwargs['interpolation'] = BasicInterpolationWithName() kwargs['inline_comment_prefixes'] = ";" - super(SafeConfigParserWithIncludes, self).__init__( + super(_SafeConfigParserWithIncludes, self).__init__( *args, **kwargs) - #@staticmethod - def getIncludes(resource, seen = []): - """ - Given 1 config resource returns list of included files - (recursively) with the original one as well - Simple loops are taken care about - """ - - # Use a short class name ;) - SCPWI = SafeConfigParserWithIncludes - - parser = SafeConfigParser() - try: - if sys.version_info >= (3,2): # pragma: no cover - parser.read(resource, encoding='utf-8') - else: - parser.read(resource) - except UnicodeDecodeError, e: - logSys.error("Error decoding config file '%s': %s" % (resource, e)) - return [] - - resourceDir = os.path.dirname(resource) - - newFiles = [ ('before', []), ('after', []) ] - if SCPWI.SECTION_NAME in parser.sections(): - for option_name, option_list in newFiles: - if option_name in parser.options(SCPWI.SECTION_NAME): - newResources = parser.get(SCPWI.SECTION_NAME, option_name) - for newResource in newResources.split('\n'): - if os.path.isabs(newResource): - r = newResource - else: - r = os.path.join(resourceDir, newResource) - if r in seen: - continue - s = seen + [resource] - option_list += SCPWI.getIncludes(r, s) - # combine lists - return newFiles[0][1] + [resource] + newFiles[1][1] - #print "Includes list for " + resource + " is " + `resources` - getIncludes = staticmethod(getIncludes) - def read(self, filenames): - fileNamesFull = [] if not isinstance(filenames, list): filenames = [ filenames ] - for filename in filenames: - fileNamesFull += SafeConfigParserWithIncludes.getIncludes(filename) - logSys.debug("Reading files: %s" % fileNamesFull) + logSys.debug("Reading files: %s", filenames) if sys.version_info >= (3,2): # pragma: no cover - return SafeConfigParser.read(self, fileNamesFull, encoding='utf-8') + return SafeConfigParser.read(self, filenames, encoding='utf-8') else: - return SafeConfigParser.read(self, fileNamesFull) + return SafeConfigParser.read(self, filenames) diff --git a/fail2ban/client/configreader.py b/fail2ban/client/configreader.py index 22115d3a..ada48803 100644 --- a/fail2ban/client/configreader.py +++ b/fail2ban/client/configreader.py @@ -55,7 +55,7 @@ class ConfigReader(SafeConfigParserWithIncludes): raise ValueError("Base configuration directory %s does not exist " % self._basedir) basename = os.path.join(self._basedir, filename) - logSys.info("Reading configs for %s under %s " % (basename, self._basedir)) + logSys.debug("Reading configs for %s under %s " , filename, self._basedir) config_files = [ basename + ".conf" ] # possible further customizations under a .conf.d directory @@ -71,14 +71,15 @@ class ConfigReader(SafeConfigParserWithIncludes): if len(config_files): # at least one config exists and accessible - logSys.debug("Reading config files: " + ', '.join(config_files)) - config_files_read = SafeConfigParserWithIncludes.read(self, config_files) + logSys.debug("Reading config files: %s", ', '.join(config_files)) + config_files_read = SafeConfigParserWithIncludes.read(self, config_files, + log_info=("Cache configs for %s under %s " , filename, self._basedir)) missed = [ cf for cf in config_files if cf not in config_files_read ] if missed: - logSys.error("Could not read config files: " + ', '.join(missed)) + logSys.error("Could not read config files: %s", ', '.join(missed)) if config_files_read: return True - logSys.error("Found no accessible config files for %r under %s" % + logSys.error("Found no accessible config files for %r under %s", ( filename, self.getBaseDir() )) return False else: @@ -133,12 +134,12 @@ class ConfigReader(SafeConfigParserWithIncludes): class DefinitionInitConfigReader(ConfigReader): """Config reader for files with options grouped in [Definition] and - [Init] sections. + [Init] sections. - Is a base class for readers of filters and actions, where definitions - in jails might provide custom values for options defined in [Init] - section. - """ + Is a base class for readers of filters and actions, where definitions + in jails might provide custom values for options defined in [Init] + section. + """ _configOpts = [] diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 0ad3c66e..b57a0562 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -21,7 +21,7 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __license__ = "GPL" -import os, glob, shutil, tempfile, unittest +import os, glob, shutil, tempfile, unittest, time from ..client.configreader import ConfigReader from ..client.jailreader import JailReader @@ -39,6 +39,8 @@ STOCK = os.path.exists(os.path.join('config','fail2ban.conf')) IMPERFECT_CONFIG = os.path.join(os.path.dirname(__file__), 'config') +LAST_WRITE_TIME = 0 + class ConfigReaderTest(unittest.TestCase): def setUp(self): @@ -57,7 +59,8 @@ class ConfigReaderTest(unittest.TestCase): d_ = os.path.join(self.d, d) if not os.path.exists(d_): os.makedirs(d_) - f = open("%s/%s" % (self.d, fname), "w") + fname = "%s/%s" % (self.d, fname) + f = open(fname, "w") if value is not None: f.write(""" [section] @@ -66,6 +69,14 @@ option = %s if content is not None: f.write(content) f.close() + # set modification time to another second to revalidate cache (if milliseconds not supported) : + global LAST_WRITE_TIME + mtime = os.path.getmtime(fname) + if LAST_WRITE_TIME == mtime: + mtime += 1 + os.utime(fname, (mtime, mtime)) + LAST_WRITE_TIME = mtime + def _remove(self, fname): os.unlink("%s/%s" % (self.d, fname)) @@ -91,7 +102,6 @@ option = %s # raise unittest.SkipTest("Skipping on %s -- access rights are not enforced" % platform) pass - def testOptionalDotDDir(self): self.assertFalse(self.c.read('c')) # nothing is there yet self._write("c.conf", "1") From af4b48e841f6f2a6754c891f391e419c998819d5 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 7 Oct 2014 14:07:50 +0200 Subject: [PATCH 7/8] test case for check the read of config files will be cached; --- fail2ban/tests/clientreadertestcase.py | 32 +++++++++++++++++++++++++- fail2ban/tests/utils.py | 4 ++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index b57a0562..68de2e8b 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -21,7 +21,7 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __license__ = "GPL" -import os, glob, shutil, tempfile, unittest, time +import os, glob, shutil, tempfile, unittest, time, re from ..client.configreader import ConfigReader from ..client.jailreader import JailReader @@ -345,6 +345,36 @@ class FilterReaderTest(unittest.TestCase): self.assertRaises(ValueError, FilterReader.convert, filterReader) +class JailsReaderTestCache(LogCaptureTestCase): + + def testTestJailConfCache(self): + basedir = tempfile.mkdtemp("fail2ban_conf") + try: + shutil.rmtree(basedir) + shutil.copytree(CONFIG_DIR, basedir) + shutil.copy(CONFIG_DIR + '/jail.conf', basedir + '/jail.local') + shutil.copy(CONFIG_DIR + '/fail2ban.conf', basedir + '/fail2ban.local') + + # read whole configuration like a file2ban-client ... + configurator = Configurator() + configurator.setBaseDir(basedir) + configurator.readEarly() + configurator.getEarlyOptions() + configurator.readAll() + # from here we test a cache : + self.assertTrue(configurator.getOptions(None)) + cnt = 0 + for s in self.getLog().rsplit('\n'): + if re.match(r"^Reading files: .*jail.local", s): + cnt += 1 + # if cnt > 2: + # self.printLog() + self.assertFalse(cnt > 2, "Too many times reading of config files, cnt = %s" % cnt) + self.assertFalse(cnt <= 0) + finally: + shutil.rmtree(basedir) + + class JailsReaderTest(LogCaptureTestCase): def testProvidingBadBasedir(self): diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 912c5a90..ad504703 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -111,6 +111,7 @@ def gatherTests(regexps=None, no_network=False): tests.addTest(unittest.makeSuite(clientreadertestcase.JailReaderTest)) tests.addTest(unittest.makeSuite(clientreadertestcase.FilterReaderTest)) tests.addTest(unittest.makeSuite(clientreadertestcase.JailsReaderTest)) + tests.addTest(unittest.makeSuite(clientreadertestcase.JailsReaderTestCache)) # CSocket and AsyncServer tests.addTest(unittest.makeSuite(sockettestcase.Socket)) # Misc helpers @@ -216,5 +217,8 @@ class LogCaptureTestCase(unittest.TestCase): def _is_logged(self, s): return s in self._log.getvalue() + def getLog(self): + return self._log.getvalue() + def printLog(self): print(self._log.getvalue()) From 2a54e612382609495b50579c2a75cfb8999aaa8e Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 8 Oct 2014 15:44:32 +0200 Subject: [PATCH 8/8] config cache optimized - prevent to read the same config file inside different resources multiple times; test case: read jail file only once; --- fail2ban/client/configparserinc.py | 40 ++++++++++++++++++++++++-- fail2ban/tests/clientreadertestcase.py | 6 ++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/fail2ban/client/configparserinc.py b/fail2ban/client/configparserinc.py index ad0a255c..7aac111b 100644 --- a/fail2ban/client/configparserinc.py +++ b/fail2ban/client/configparserinc.py @@ -124,7 +124,7 @@ class SafeConfigParserWithIncludes(object): else: fileNamesFull = resource # check cache - hashv = '///'.join(fileNamesFull) + hashv = '\x01'.join(fileNamesFull) cr, ret, mtime = SCPWI.CFG_CACHE.get(hashv, (None, False, 0)) curmt = SCPWI._resource_mtime(fileNamesFull) if cr is not None and mtime == curmt: @@ -167,7 +167,7 @@ class SafeConfigParserWithIncludes(object): parser = SCPWI() try: # read without includes - parser.read(resource, get_includes = False) + parser.read(resource, get_includes=False) except UnicodeDecodeError, e: logSys.error("Error decoding config file '%s': %s" % (resource, e)) return [] @@ -232,11 +232,45 @@ after = 1.conf super(_SafeConfigParserWithIncludes, self).__init__( *args, **kwargs) + def get_defaults(self): + return self._defaults + + def get_sections(self): + return self._sections def read(self, filenames): if not isinstance(filenames, list): filenames = [ filenames ] - logSys.debug("Reading files: %s", filenames) + if len(filenames) > 1: + # read multiple configs: + ret = [] + alld = self.get_defaults() + alls = self.get_sections() + for filename in filenames: + # read single one, add to return list: + cfg = SafeConfigParserWithIncludes() + i = cfg.read(filename, get_includes=False) + if i: + ret += i + # merge defaults and all sections to self: + for (n, v) in cfg.get_defaults().items(): + alld[n] = v + for (n, s) in cfg.get_sections().items(): + if isinstance(s, dict): + s2 = alls.get(n) + if s2 is not None: + for (n, v) in s.items(): + s2[n] = v + else: + s2 = s.copy() + alls[n] = s2 + else: + alls[n] = s + + return ret + + # read one config : + logSys.debug("Reading file: %s", filenames[0]) if sys.version_info >= (3,2): # pragma: no cover return SafeConfigParser.read(self, filenames, encoding='utf-8') else: diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 68de2e8b..5c5c75bc 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -365,11 +365,11 @@ class JailsReaderTestCache(LogCaptureTestCase): self.assertTrue(configurator.getOptions(None)) cnt = 0 for s in self.getLog().rsplit('\n'): - if re.match(r"^Reading files: .*jail.local", s): + if re.match(r"^Reading files?: .*jail.local", s): cnt += 1 - # if cnt > 2: + # if cnt > 1: # self.printLog() - self.assertFalse(cnt > 2, "Too many times reading of config files, cnt = %s" % cnt) + self.assertFalse(cnt > 1, "Too many times reading of config files, cnt = %s" % cnt) self.assertFalse(cnt <= 0) finally: shutil.rmtree(basedir)