Merge remote-tracking branch 'remotes/origin/_tent/cache-config-read' into ban-time-incr

pull/716/head
sebres 2014-10-20 01:37:36 +02:00
commit 8f2561e289
10 changed files with 262 additions and 241 deletions

View File

@ -29,6 +29,8 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger
provides defaults for the chain, port, protocol and name tags provides defaults for the chain, port, protocol and name tags
- Fixes: - Fixes:
* start of file2ban aborted (on slow hosts, systemd considers the server has been
timed out and kills him), see gh-824
* UTF-8 fixes in pure-ftp thanks to Johannes Weberhofer. Closes gh-806. * UTF-8 fixes in pure-ftp thanks to Johannes Weberhofer. Closes gh-806.
* systemd backend error on bad utf-8 in python3 * systemd backend error on bad utf-8 in python3
* badips.py action error when logging HTTP error raised with badips request * badips.py action error when logging HTTP error raised with badips request
@ -78,6 +80,10 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger
- Added Cloudflare API action - Added Cloudflare API action
- Enhancements - Enhancements
* Start performance of fail2ban-client (and tests) increased, start time
and cpu usage rapidly reduced. Introduced a shared storage logic, to bypass
reading lots of config files (see gh-824).
Thanks to Joost Molenaar for good catch (reported gh-820).
* Fail2ban-regex - add print-all-matched option. Closes gh-652 * Fail2ban-regex - add print-all-matched option. Closes gh-652
* Suppress fail2ban-client warnings for non-critical config options * Suppress fail2ban-client warnings for non-critical config options
* Match non "Bye Bye" disconnect messages for sshd locked account regex * Match non "Bye Bye" disconnect messages for sshd locked account regex

View File

@ -409,6 +409,7 @@ class Fail2banClient:
# TODO: get away from stew of return codes and exception # TODO: get away from stew of return codes and exception
# handling -- handle via exceptions # handling -- handle via exceptions
try: try:
self.__configurator.Reload()
self.__configurator.readAll() self.__configurator.readAll()
ret = self.__configurator.getOptions(jail) ret = self.__configurator.getOptions(jail)
self.__configurator.convertToProtocol() self.__configurator.convertToProtocol()

View File

@ -62,139 +62,11 @@ else: # pragma: no cover
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
logLevel = 7
__all__ = ['SafeConfigParserWithIncludes'] __all__ = ['SafeConfigParserWithIncludes']
class SafeConfigParserWithIncludes(object): class SafeConfigParserWithIncludes(SafeConfigParser):
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 = '\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:
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 Class adds functionality to SafeConfigParser to handle included
other configuration files (or may be urls, whatever in the future) other configuration files (or may be urls, whatever in the future)
@ -223,14 +95,99 @@ after = 1.conf
""" """
SECTION_NAME = "INCLUDES"
if sys.version_info >= (3,2): if sys.version_info >= (3,2):
# overload constructor only for fancy new Python3's # overload constructor only for fancy new Python3's
def __init__(self, *args, **kwargs): def __init__(self, share_config=None, *args, **kwargs):
kwargs = kwargs.copy() kwargs = kwargs.copy()
kwargs['interpolation'] = BasicInterpolationWithName() kwargs['interpolation'] = BasicInterpolationWithName()
kwargs['inline_comment_prefixes'] = ";" kwargs['inline_comment_prefixes'] = ";"
super(_SafeConfigParserWithIncludes, self).__init__( super(SafeConfigParserWithIncludes, self).__init__(
*args, **kwargs) *args, **kwargs)
self._cfg_share = share_config
else:
def __init__(self, share_config=None, *args, **kwargs):
SafeConfigParser.__init__(self, *args, **kwargs)
self._cfg_share = share_config
@property
def share_config(self):
return self._cfg_share
def _getSharedSCPWI(self, filename):
SCPWI = SafeConfigParserWithIncludes
# read single one, add to return list, use sharing if possible:
if self._cfg_share:
# cache/share each file as include (ex: filter.d/common could be included in each filter config):
hashv = 'inc:'+(filename if not isinstance(filename, list) else '\x01'.join(filename))
cfg, i = self._cfg_share.get(hashv, (None, None))
if cfg is None:
cfg = SCPWI(share_config=self._cfg_share)
i = cfg.read(filename, get_includes=False)
self._cfg_share[hashv] = (cfg, i)
elif logSys.getEffectiveLevel() <= logLevel:
logSys.log(logLevel, " Shared file: %s", filename)
else:
# don't have sharing:
cfg = SCPWI()
i = cfg.read(filename, get_includes=False)
return (cfg, i)
def _getIncludes(self, filenames, seen=[]):
if not isinstance(filenames, list):
filenames = [ filenames ]
# retrieve or cache include paths:
if self._cfg_share:
# cache/share include list:
hashv = 'inc-path:'+('\x01'.join(filenames))
fileNamesFull = self._cfg_share.get(hashv)
if fileNamesFull is None:
fileNamesFull = []
for filename in filenames:
fileNamesFull += self.__getIncludesUncached(filename, seen)
self._cfg_share[hashv] = fileNamesFull
return fileNamesFull
# don't have sharing:
fileNamesFull = []
for filename in filenames:
fileNamesFull += self.__getIncludesUncached(filename, seen)
return fileNamesFull
def __getIncludesUncached(self, resource, seen=[]):
"""
Given 1 config resource returns list of included files
(recursively) with the original one as well
Simple loops are taken care about
"""
SCPWI = SafeConfigParserWithIncludes
try:
parser, i = self._getSharedSCPWI(resource)
if not i:
return []
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 += self._getIncludes(r, s)
# combine lists
return newFiles[0][1] + [resource] + newFiles[1][1]
def get_defaults(self): def get_defaults(self):
return self._defaults return self._defaults
@ -238,18 +195,29 @@ after = 1.conf
def get_sections(self): def get_sections(self):
return self._sections return self._sections
def read(self, filenames): def read(self, filenames, get_includes=True):
if not isinstance(filenames, list): if not isinstance(filenames, list):
filenames = [ filenames ] filenames = [ filenames ]
if len(filenames) > 1: # retrieve (and cache) includes:
fileNamesFull = []
if get_includes:
fileNamesFull += self._getIncludes(filenames)
else:
fileNamesFull = filenames
if not fileNamesFull:
return []
logSys.info(" Loading files: %s", fileNamesFull)
if get_includes or len(fileNamesFull) > 1:
# read multiple configs: # read multiple configs:
ret = [] ret = []
alld = self.get_defaults() alld = self.get_defaults()
alls = self.get_sections() alls = self.get_sections()
for filename in filenames: for filename in fileNamesFull:
# read single one, add to return list: # read single one, add to return list, use sharing if possible:
cfg = SafeConfigParserWithIncludes() cfg, i = self._getSharedSCPWI(filename)
i = cfg.read(filename, get_includes=False)
if i: if i:
ret += i ret += i
# merge defaults and all sections to self: # merge defaults and all sections to self:
@ -267,9 +235,10 @@ after = 1.conf
return ret return ret
# read one config : # read one config :
logSys.debug("Reading file: %s", filenames[0]) if logSys.getEffectiveLevel() <= logLevel:
logSys.log(logLevel, " Reading file: %s", fileNamesFull[0])
# read file(s) :
if sys.version_info >= (3,2): # pragma: no cover if sys.version_info >= (3,2): # pragma: no cover
return SafeConfigParser.read(self, filenames, encoding='utf-8') return SafeConfigParser.read(self, fileNamesFull, encoding='utf-8')
else: else:
return SafeConfigParser.read(self, filenames) return SafeConfigParser.read(self, fileNamesFull)

View File

@ -27,14 +27,17 @@ __license__ = "GPL"
import glob, os import glob, os
from ConfigParser import NoOptionError, NoSectionError from ConfigParser import NoOptionError, NoSectionError
from .configparserinc import SafeConfigParserWithIncludes from .configparserinc import SafeConfigParserWithIncludes, logLevel
from ..helpers import getLogger from ..helpers import getLogger
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
logLevel = 6
class ConfigWrapper(): class ConfigReader():
"""Generic config reader class.
A caching adapter which automatically reuses already shared configuration.
"""
def __init__(self, use_config=None, share_config=None, **kwargs): def __init__(self, use_config=None, share_config=None, **kwargs):
# use given shared config if possible (see read): # use given shared config if possible (see read):
@ -42,68 +45,109 @@ class ConfigWrapper():
self._cfg = None self._cfg = None
if use_config is not None: if use_config is not None:
self._cfg = use_config self._cfg = use_config
else:
# share config if possible: # share config if possible:
if share_config is not None: if share_config is not None:
self._cfg_share = share_config self._cfg_share = share_config
self._cfg_share_kwargs = kwargs self._cfg_share_kwargs = kwargs
else: self._cfg_share_basedir = None
self._cfg = ConfigReader(**kwargs) elif self._cfg is None:
self._cfg = ConfigReaderUnshared(**kwargs)
def setBaseDir(self, basedir): def setBaseDir(self, basedir):
if self._cfg:
self._cfg.setBaseDir(basedir) self._cfg.setBaseDir(basedir)
else:
self._cfg_share_basedir = basedir
def getBaseDir(self): def getBaseDir(self):
if self._cfg:
return self._cfg.getBaseDir() return self._cfg.getBaseDir()
else:
return self._cfg_share_basedir
@property
def share_config(self):
return self._cfg_share
def read(self, name, once=True): def read(self, name, once=True):
# shared ? """ Overloads a default (not shared) read of config reader.
To prevent mutiple reads of config files with it includes, reads into
the config reader, if it was not yet cached/shared by 'name'.
"""
# already shared ?
if not self._cfg:
self.touch(name)
# performance feature - read once if using shared config reader:
if once and self._cfg.read_cfg_files is not None:
return self._cfg.read_cfg_files
# load:
logSys.info("Loading configs for %s under %s ", name, self._cfg.getBaseDir())
ret = self._cfg.read(name)
# save already read and return:
self._cfg.read_cfg_files = ret
return ret
def touch(self, name=''):
""" Allocates and share a config file by it name.
Automatically allocates unshared or reuses shared handle by given 'name' and
init arguments inside a given shared storage.
"""
if not self._cfg and self._cfg_share is not None: if not self._cfg and self._cfg_share is not None:
self._cfg = self._cfg_share.get(name) self._cfg = self._cfg_share.get(name)
if not self._cfg: if not self._cfg:
self._cfg = ConfigReader(**self._cfg_share_kwargs) self._cfg = ConfigReaderUnshared(share_config=self._cfg_share, **self._cfg_share_kwargs)
if self._cfg_share_basedir is not None:
self._cfg.setBaseDir(self._cfg_share_basedir)
self._cfg_share[name] = self._cfg self._cfg_share[name] = self._cfg
# performance feature - read once if using shared config reader: else:
rc = self._cfg.read_cfg_files self._cfg = ConfigReaderUnshared(**self._cfg_share_kwargs)
if once and rc.get(name) is not None:
return rc.get(name)
# read:
ret = self._cfg.read(name)
# save already read:
if once:
rc[name] = ret
return ret
def sections(self): def sections(self):
if self._cfg is not None:
return self._cfg.sections() return self._cfg.sections()
return []
def has_section(self, sec): def has_section(self, sec):
if self._cfg is not None:
return self._cfg.has_section(sec) return self._cfg.has_section(sec)
return False
def options(self, *args): def options(self, *args):
if self._cfg is not None:
return self._cfg.options(*args) return self._cfg.options(*args)
return {}
def get(self, sec, opt): def get(self, sec, opt):
if self._cfg is not None:
return self._cfg.get(sec, opt) return self._cfg.get(sec, opt)
return None
def getOptions(self, *args, **kwargs): def getOptions(self, *args, **kwargs):
if self._cfg is not None:
return self._cfg.getOptions(*args, **kwargs) return self._cfg.getOptions(*args, **kwargs)
return {}
class ConfigReaderUnshared(SafeConfigParserWithIncludes):
"""Unshared config reader (previously ConfigReader).
class ConfigReader(SafeConfigParserWithIncludes): Do not use this class (internal not shared/cached represenation).
Use ConfigReader instead.
"""
DEFAULT_BASEDIR = '/etc/fail2ban' DEFAULT_BASEDIR = '/etc/fail2ban'
def __init__(self, basedir=None): def __init__(self, basedir=None, *args, **kwargs):
SafeConfigParserWithIncludes.__init__(self) SafeConfigParserWithIncludes.__init__(self, *args, **kwargs)
self.read_cfg_files = dict() self.read_cfg_files = None
self.setBaseDir(basedir) self.setBaseDir(basedir)
def setBaseDir(self, basedir): def setBaseDir(self, basedir):
if basedir is None: if basedir is None:
basedir = ConfigReader.DEFAULT_BASEDIR # stock system location basedir = ConfigReaderUnshared.DEFAULT_BASEDIR # stock system location
self._basedir = basedir.rstrip('/') self._basedir = basedir.rstrip('/')
def getBaseDir(self): def getBaseDir(self):
@ -131,8 +175,7 @@ class ConfigReader(SafeConfigParserWithIncludes):
if len(config_files): if len(config_files):
# at least one config exists and accessible # at least one config exists and accessible
logSys.debug("Reading config files: %s", ', '.join(config_files)) logSys.debug("Reading config files: %s", ', '.join(config_files))
config_files_read = SafeConfigParserWithIncludes.read(self, 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 ] missed = [ cf for cf in config_files if cf not in config_files_read ]
if missed: if missed:
logSys.error("Could not read config files: %s", ', '.join(missed)) logSys.error("Could not read config files: %s", ', '.join(missed))
@ -158,7 +201,7 @@ class ConfigReader(SafeConfigParserWithIncludes):
# 1 -> the name of the option # 1 -> the name of the option
# 2 -> the default value for the option # 2 -> the default value for the option
def getOptions(self, sec, options, pOptions = None): def getOptions(self, sec, options, pOptions=None):
values = dict() values = dict()
for option in options: for option in options:
try: try:
@ -189,7 +232,7 @@ class ConfigReader(SafeConfigParserWithIncludes):
values[option[1]] = option[2] values[option[1]] = option[2]
return values return values
class DefinitionInitConfigReader(ConfigWrapper): class DefinitionInitConfigReader(ConfigReader):
"""Config reader for files with options grouped in [Definition] and """Config reader for files with options grouped in [Definition] and
[Init] sections. [Init] sections.
@ -201,7 +244,7 @@ class DefinitionInitConfigReader(ConfigWrapper):
_configOpts = [] _configOpts = []
def __init__(self, file_, jailName, initOpts, **kwargs): def __init__(self, file_, jailName, initOpts, **kwargs):
ConfigWrapper.__init__(self, **kwargs) ConfigReader.__init__(self, **kwargs)
self.setFile(file_) self.setFile(file_)
self.setJailName(jailName) self.setJailName(jailName)
self._initOpts = initOpts self._initOpts = initOpts
@ -220,14 +263,14 @@ class DefinitionInitConfigReader(ConfigWrapper):
return self._jailName return self._jailName
def read(self): def read(self):
return ConfigWrapper.read(self, self._file) return ConfigReader.read(self, self._file)
# needed for fail2ban-regex that doesn't need fancy directories # needed for fail2ban-regex that doesn't need fancy directories
def readexplicit(self): def readexplicit(self):
return SafeConfigParserWithIncludes.read(self, self._file) return SafeConfigParserWithIncludes.read(self, self._file)
def getOptions(self, pOpts): def getOptions(self, pOpts):
self._opts = ConfigWrapper.getOptions( self._opts = ConfigReader.getOptions(
self, "Definition", self._configOpts, pOpts) self, "Definition", self._configOpts, pOpts)
if self.has_section("Init"): if self.has_section("Init"):

View File

@ -33,11 +33,19 @@ logSys = getLogger(__name__)
class Configurator: class Configurator:
def __init__(self, force_enable=False): def __init__(self, force_enable=False, share_config=None):
self.__settings = dict() self.__settings = dict()
self.__streams = dict() self.__streams = dict()
self.__fail2ban = Fail2banReader() # always share all config readers:
self.__jails = JailsReader(force_enable=force_enable) if share_config is None:
share_config = dict()
self.__share_config = share_config
self.__fail2ban = Fail2banReader(share_config=share_config)
self.__jails = JailsReader(force_enable=force_enable, share_config=share_config)
def Reload(self):
# clear all shared handlers:
self.__share_config.clear()
def setBaseDir(self, folderName): def setBaseDir(self, folderName):
self.__fail2ban.setBaseDir(folderName) self.__fail2ban.setBaseDir(folderName)

View File

@ -24,32 +24,31 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from .configreader import ConfigWrapper from .configreader import ConfigReader
from ..helpers import getLogger from ..helpers import getLogger
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
class Fail2banReader(ConfigWrapper): class Fail2banReader(ConfigReader):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__opts = None ConfigReader.__init__(self, **kwargs)
ConfigWrapper.__init__(self, **kwargs)
def read(self): def read(self):
ConfigWrapper.read(self, "fail2ban") ConfigReader.read(self, "fail2ban")
def getEarlyOptions(self): def getEarlyOptions(self):
opts = [["string", "socket", "/var/run/fail2ban/fail2ban.sock"], opts = [["string", "socket", "/var/run/fail2ban/fail2ban.sock"],
["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"]] ["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"]]
return ConfigWrapper.getOptions(self, "Definition", opts) return ConfigReader.getOptions(self, "Definition", opts)
def getOptions(self): def getOptions(self):
opts = [["string", "loglevel", "INFO" ], opts = [["string", "loglevel", "INFO" ],
["string", "logtarget", "STDERR"], ["string", "logtarget", "STDERR"],
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"], ["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
["int", "dbpurgeage", 86400]] ["int", "dbpurgeage", 86400]]
self.__opts = ConfigWrapper.getOptions(self, "Definition", opts) self.__opts = ConfigReader.getOptions(self, "Definition", opts)
def convert(self): def convert(self):
order = {"loglevel":0, "logtarget":1, "dbfile":2, "dbpurgeage":3} order = {"loglevel":0, "logtarget":1, "dbfile":2, "dbpurgeage":3}

View File

@ -27,7 +27,7 @@ __license__ = "GPL"
import re, glob, os.path import re, glob, os.path
import json import json
from .configreader import ConfigReader, ConfigWrapper from .configreader import ConfigReaderUnshared, ConfigReader
from .filterreader import FilterReader from .filterreader import FilterReader
from .actionreader import ActionReader from .actionreader import ActionReader
from ..helpers import getLogger from ..helpers import getLogger
@ -35,19 +35,17 @@ from ..helpers import getLogger
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
class JailReader(ConfigWrapper): class JailReader(ConfigReader):
optionCRE = re.compile("^((?:\w|-|_|\.)+)(?:\[(.*)\])?$") optionCRE = re.compile("^((?:\w|-|_|\.)+)(?:\[(.*)\])?$")
optionExtractRE = re.compile( optionExtractRE = re.compile(
r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,]*))(?:,|$)') r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,]*))(?:,|$)')
def __init__(self, name, force_enable=False, cfg_share=None, **kwargs): def __init__(self, name, force_enable=False, **kwargs):
# use shared config if possible: ConfigReader.__init__(self, **kwargs)
ConfigWrapper.__init__(self, **kwargs)
self.__name = name self.__name = name
self.__filter = None self.__filter = None
self.__force_enable = force_enable self.__force_enable = force_enable
self.__cfg_share = cfg_share
self.__actions = list() self.__actions = list()
self.__opts = None self.__opts = None
@ -62,7 +60,7 @@ class JailReader(ConfigWrapper):
return self.__name return self.__name
def read(self): def read(self):
out = ConfigWrapper.read(self, "jail") out = ConfigReader.read(self, "jail")
# Before returning -- verify that requested section # Before returning -- verify that requested section
# exists at all # exists at all
if not (self.__name in self.sections()): if not (self.__name in self.sections()):
@ -110,7 +108,7 @@ class JailReader(ConfigWrapper):
["string", "ignoreip", None], ["string", "ignoreip", None],
["string", "filter", ""], ["string", "filter", ""],
["string", "action", ""]] ["string", "action", ""]]
self.__opts = ConfigWrapper.getOptions(self, self.__name, opts) self.__opts = ConfigReader.getOptions(self, self.__name, opts)
if not self.__opts: if not self.__opts:
return False return False
@ -120,7 +118,7 @@ class JailReader(ConfigWrapper):
filterName, filterOpt = JailReader.extractOptions( filterName, filterOpt = JailReader.extractOptions(
self.__opts["filter"]) self.__opts["filter"])
self.__filter = FilterReader( self.__filter = FilterReader(
filterName, self.__name, filterOpt, share_config=self.__cfg_share, basedir=self.getBaseDir()) filterName, self.__name, filterOpt, share_config=self.share_config, basedir=self.getBaseDir())
ret = self.__filter.read() ret = self.__filter.read()
if ret: if ret:
self.__filter.getOptions(self.__opts) self.__filter.getOptions(self.__opts)
@ -150,7 +148,7 @@ class JailReader(ConfigWrapper):
else: else:
action = ActionReader( action = ActionReader(
actName, self.__name, actOpt, actName, self.__name, actOpt,
share_config=self.__cfg_share, basedir=self.getBaseDir()) share_config=self.share_config, basedir=self.getBaseDir())
ret = action.read() ret = action.read()
if ret: if ret:
action.getOptions(self.__opts) action.getOptions(self.__opts)
@ -224,7 +222,7 @@ class JailReader(ConfigWrapper):
if self.__filter: if self.__filter:
stream.extend(self.__filter.convert()) stream.extend(self.__filter.convert())
for action in self.__actions: for action in self.__actions:
if isinstance(action, (ConfigReader, ConfigWrapper)): if isinstance(action, (ConfigReaderUnshared, ConfigReader)):
stream.extend(action.convert()) stream.extend(action.convert())
else: else:
stream.append(action) stream.append(action)

View File

@ -24,14 +24,14 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from .configreader import ConfigReader, ConfigWrapper from .configreader import ConfigReader
from .jailreader import JailReader from .jailreader import JailReader
from ..helpers import getLogger from ..helpers import getLogger
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
class JailsReader(ConfigWrapper): class JailsReader(ConfigReader):
def __init__(self, force_enable=False, **kwargs): def __init__(self, force_enable=False, **kwargs):
""" """
@ -41,9 +41,7 @@ class JailsReader(ConfigWrapper):
Passed to JailReader to force enable the jails. Passed to JailReader to force enable the jails.
It is for internal use It is for internal use
""" """
# use shared config if possible: ConfigReader.__init__(self, **kwargs)
ConfigWrapper.__init__(self, **kwargs)
self.__cfg_share = dict()
self.__jails = list() self.__jails = list()
self.__force_enable = force_enable self.__force_enable = force_enable
@ -52,13 +50,13 @@ class JailsReader(ConfigWrapper):
return self.__jails return self.__jails
def read(self): def read(self):
return ConfigWrapper.read(self, "jail") return ConfigReader.read(self, "jail")
def getOptions(self, section=None): def getOptions(self, section=None):
"""Reads configuration for jail(s) and adds enabled jails to __jails """Reads configuration for jail(s) and adds enabled jails to __jails
""" """
opts = [] opts = []
self.__opts = ConfigWrapper.getOptions(self, "Definition", opts) self.__opts = ConfigReader.getOptions(self, "Definition", opts)
if section is None: if section is None:
sections = self.sections() sections = self.sections()
@ -73,7 +71,7 @@ class JailsReader(ConfigWrapper):
# use the cfg_share for filter/action caching and the same config for all # use the cfg_share for filter/action caching and the same config for all
# jails (use_config=...), therefore don't read it here: # jails (use_config=...), therefore don't read it here:
jail = JailReader(sec, force_enable=self.__force_enable, jail = JailReader(sec, force_enable=self.__force_enable,
cfg_share=self.__cfg_share, use_config=self._cfg) share_config=self.share_config, use_config=self._cfg)
ret = jail.getOptions() ret = jail.getOptions()
if ret: if ret:
if jail.isEnabled(): if jail.isEnabled():

View File

@ -21,9 +21,9 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
import os, glob, shutil, tempfile, unittest, time, re import os, glob, shutil, tempfile, unittest, re, logging
from ..client.configreader import ConfigReaderUnshared
from ..client.configreader import ConfigReader from ..client import configparserinc
from ..client.jailreader import JailReader from ..client.jailreader import JailReader
from ..client.filterreader import FilterReader from ..client.filterreader import FilterReader
from ..client.jailsreader import JailsReader from ..client.jailsreader import JailsReader
@ -39,14 +39,12 @@ STOCK = os.path.exists(os.path.join('config','fail2ban.conf'))
IMPERFECT_CONFIG = os.path.join(os.path.dirname(__file__), 'config') IMPERFECT_CONFIG = os.path.join(os.path.dirname(__file__), 'config')
LAST_WRITE_TIME = 0
class ConfigReaderTest(unittest.TestCase): class ConfigReaderTest(unittest.TestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
self.d = tempfile.mkdtemp(prefix="f2b-temp") self.d = tempfile.mkdtemp(prefix="f2b-temp")
self.c = ConfigReader(basedir=self.d) self.c = ConfigReaderUnshared(basedir=self.d)
def tearDown(self): def tearDown(self):
"""Call after every test case.""" """Call after every test case."""
@ -59,8 +57,7 @@ class ConfigReaderTest(unittest.TestCase):
d_ = os.path.join(self.d, d) d_ = os.path.join(self.d, d)
if not os.path.exists(d_): if not os.path.exists(d_):
os.makedirs(d_) os.makedirs(d_)
fname = "%s/%s" % (self.d, fname) f = open("%s/%s" % (self.d, fname), "w")
f = open(fname, "w")
if value is not None: if value is not None:
f.write(""" f.write("""
[section] [section]
@ -69,14 +66,6 @@ option = %s
if content is not None: if content is not None:
f.write(content) f.write(content)
f.close() 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): def _remove(self, fname):
os.unlink("%s/%s" % (self.d, fname)) os.unlink("%s/%s" % (self.d, fname))
@ -102,6 +91,7 @@ option = %s
# raise unittest.SkipTest("Skipping on %s -- access rights are not enforced" % platform) # raise unittest.SkipTest("Skipping on %s -- access rights are not enforced" % platform)
pass pass
def testOptionalDotDDir(self): def testOptionalDotDDir(self):
self.assertFalse(self.c.read('c')) # nothing is there yet self.assertFalse(self.c.read('c')) # nothing is there yet
self._write("c.conf", "1") self._write("c.conf", "1")
@ -347,9 +337,9 @@ class FilterReaderTest(unittest.TestCase):
class JailsReaderTestCache(LogCaptureTestCase): class JailsReaderTestCache(LogCaptureTestCase):
def _readWholeConf(self, basedir, force_enable=False): def _readWholeConf(self, basedir, force_enable=False, share_config=None):
# read whole configuration like a file2ban-client ... # read whole configuration like a file2ban-client ...
configurator = Configurator(force_enable=force_enable) configurator = Configurator(force_enable=force_enable, share_config=share_config)
configurator.setBaseDir(basedir) configurator.setBaseDir(basedir)
configurator.readEarly() configurator.readEarly()
configurator.getEarlyOptions() configurator.getEarlyOptions()
@ -360,11 +350,13 @@ class JailsReaderTestCache(LogCaptureTestCase):
def _getLoggedReadCount(self, filematch): def _getLoggedReadCount(self, filematch):
cnt = 0 cnt = 0
for s in self.getLog().rsplit('\n'): for s in self.getLog().rsplit('\n'):
if re.match(r"^Reading files?: .*/"+filematch, s): if re.match(r"^\s*Reading files?: .*/"+filematch, s):
cnt += 1 cnt += 1
return cnt return cnt
def testTestJailConfCache(self): def testTestJailConfCache(self):
saved_ll = configparserinc.logLevel
configparserinc.logLevel = logging.DEBUG
basedir = tempfile.mkdtemp("fail2ban_conf") basedir = tempfile.mkdtemp("fail2ban_conf")
try: try:
shutil.rmtree(basedir) shutil.rmtree(basedir)
@ -372,8 +364,11 @@ class JailsReaderTestCache(LogCaptureTestCase):
shutil.copy(CONFIG_DIR + '/jail.conf', basedir + '/jail.local') shutil.copy(CONFIG_DIR + '/jail.conf', basedir + '/jail.local')
shutil.copy(CONFIG_DIR + '/fail2ban.conf', basedir + '/fail2ban.local') shutil.copy(CONFIG_DIR + '/fail2ban.conf', basedir + '/fail2ban.local')
# common sharing handle for this test:
share_cfg = dict()
# read whole configuration like a file2ban-client ... # read whole configuration like a file2ban-client ...
self._readWholeConf(basedir) self._readWholeConf(basedir, share_config=share_cfg)
# how many times jail.local was read: # how many times jail.local was read:
cnt = self._getLoggedReadCount('jail.local') cnt = self._getLoggedReadCount('jail.local')
# if cnt > 1: # if cnt > 1:
@ -382,7 +377,7 @@ class JailsReaderTestCache(LogCaptureTestCase):
# read whole configuration like a file2ban-client, again ... # read whole configuration like a file2ban-client, again ...
# but this time force enable all jails, to check filter and action cached also: # but this time force enable all jails, to check filter and action cached also:
self._readWholeConf(basedir, force_enable=True) self._readWholeConf(basedir, force_enable=True, share_config=share_cfg)
cnt = self._getLoggedReadCount(r'jail\.local') cnt = self._getLoggedReadCount(r'jail\.local')
# still one (no more reads): # still one (no more reads):
self.assertTrue(cnt == 1, "Unexpected count by second reading of jail files, cnt = %s" % cnt) self.assertTrue(cnt == 1, "Unexpected count by second reading of jail files, cnt = %s" % cnt)
@ -395,6 +390,7 @@ class JailsReaderTestCache(LogCaptureTestCase):
self.assertTrue(cnt == 1, "Unexpected count by reading of action files, cnt = %s" % cnt) self.assertTrue(cnt == 1, "Unexpected count by reading of action files, cnt = %s" % cnt)
finally: finally:
shutil.rmtree(basedir) shutil.rmtree(basedir)
configparserinc.logLevel = saved_ll
class JailsReaderTest(LogCaptureTestCase): class JailsReaderTest(LogCaptureTestCase):

View File

@ -25,6 +25,7 @@ __license__ = "GPL"
import logging import logging
import os import os
import re import re
import sys
import time import time
import unittest import unittest
from StringIO import StringIO from StringIO import StringIO
@ -210,6 +211,8 @@ class LogCaptureTestCase(unittest.TestCase):
# Let's log everything into a string # Let's log everything into a string
self._log = StringIO() self._log = StringIO()
logSys.handlers = [logging.StreamHandler(self._log)] logSys.handlers = [logging.StreamHandler(self._log)]
if self._old_level < logging.DEBUG: # so if HEAVYDEBUG etc -- show them!
logSys.handlers += self._old_handlers
logSys.setLevel(getattr(logging, 'DEBUG')) logSys.setLevel(getattr(logging, 'DEBUG'))
def tearDown(self): def tearDown(self):