Merge pull request #484 from grooverdan/more-more-tests

BF/TST: fix internals of jailreader and add test cases
pull/494/head^2
Daniel Black 2013-12-16 02:29:50 -08:00
commit dd79889904
10 changed files with 202 additions and 28 deletions

View File

@ -242,3 +242,7 @@ files/fail2ban-tmpfiles.conf
files/fail2ban.service files/fail2ban.service
files/ipmasq-ZZZzzz_fail2ban.rul files/ipmasq-ZZZzzz_fail2ban.rul
files/gen_badbots files/gen_badbots
testcases/config/jail.conf
testcases/config/fail2ban.conf
testcases/config/filter.d/simple.conf
testcases/config/action.d/brokenaction.conf

View File

@ -112,7 +112,7 @@ class ConfigReader(SafeConfigParserWithIncludes):
except NoSectionError, e: except NoSectionError, e:
# No "Definition" section or wrong basedir # No "Definition" section or wrong basedir
logSys.error(e) logSys.error(e)
values[option[1]] = option[2] return False
except NoOptionError: except NoOptionError:
if not option[2] is None: if not option[2] is None:
logSys.warn("'%s' not defined in '%s'. Using default one: %r" logSys.warn("'%s' not defined in '%s'. Using default one: %r"

View File

@ -35,7 +35,7 @@ logSys = logging.getLogger("fail2ban.client.config")
class JailReader(ConfigReader): class JailReader(ConfigReader):
actionCRE = re.compile("^((?:\w|-|_|\.)+)(?:\[(.*)\])?$") actionCRE = re.compile("^([\w_.-]+)(?:\[(.*)\])?$")
def __init__(self, name, force_enable=False, **kwargs): def __init__(self, name, force_enable=False, **kwargs):
ConfigReader.__init__(self, **kwargs) ConfigReader.__init__(self, **kwargs)
@ -54,7 +54,7 @@ class JailReader(ConfigReader):
return ConfigReader.read(self, "jail") return ConfigReader.read(self, "jail")
def isEnabled(self): def isEnabled(self):
return self.__force_enable or self.__opts["enabled"] return self.__force_enable or ( self.__opts and self.__opts["enabled"] )
@staticmethod @staticmethod
def _glob(path): def _glob(path):
@ -64,12 +64,10 @@ class JailReader(ConfigReader):
""" """
pathList = [] pathList = []
for p in glob.glob(path): for p in glob.glob(path):
if not os.path.exists(p): if os.path.exists(p):
logSys.warning("File %s doesn't even exist, thus cannot be monitored" % p)
elif not os.path.lexists(p):
logSys.warning("File %s is a dangling link, thus cannot be monitored" % p)
else:
pathList.append(p) pathList.append(p)
else:
logSys.warning("File %s is a dangling link, thus cannot be monitored" % p)
return pathList return pathList
def getOptions(self): def getOptions(self):
@ -86,18 +84,24 @@ class JailReader(ConfigReader):
["string", "filter", ""], ["string", "filter", ""],
["string", "action", ""]] ["string", "action", ""]]
self.__opts = ConfigReader.getOptions(self, self.__name, opts) self.__opts = ConfigReader.getOptions(self, self.__name, opts)
if not self.__opts:
return False
if self.isEnabled(): if self.isEnabled():
# Read filter # Read filter
self.__filter = FilterReader(self.__opts["filter"], self.__name, if self.__opts["filter"]:
basedir=self.getBaseDir()) self.__filter = FilterReader(self.__opts["filter"], self.__name,
ret = self.__filter.read() basedir=self.getBaseDir())
if ret: ret = self.__filter.read()
self.__filter.getOptions(self.__opts) if ret:
self.__filter.getOptions(self.__opts)
else:
logSys.error("Unable to read the filter")
return False
else: else:
logSys.error("Unable to read the filter") self.__filter = None
return False logSys.warn("No filter set for jail %s" % self.__name)
# Read action # Read action
for act in self.__opts["action"].split('\n'): for act in self.__opts["action"].split('\n'):
try: try:
@ -165,7 +169,8 @@ class JailReader(ConfigReader):
# Do not send a command if the rule is empty. # Do not send a command if the rule is empty.
if regex != '': if regex != '':
stream.append(["set", self.__name, "addignoreregex", regex]) stream.append(["set", self.__name, "addignoreregex", regex])
stream.extend(self.__filter.convert()) if self.__filter:
stream.extend(self.__filter.convert())
for action in self.__actions: for action in self.__actions:
stream.extend(action.convert()) stream.extend(action.convert())
stream.insert(0, ["add", self.__name, backend]) stream.insert(0, ["add", self.__name, backend])
@ -175,12 +180,16 @@ class JailReader(ConfigReader):
def splitAction(action): def splitAction(action):
m = JailReader.actionCRE.match(action) m = JailReader.actionCRE.match(action)
d = dict() d = dict()
mgroups = m.groups() try:
mgroups = m.groups()
except AttributeError:
raise ValueError("While reading action %s we should have got 1 or "
"2 groups. Got: 0" % action)
if len(mgroups) == 2: if len(mgroups) == 2:
action_name, action_opts = mgroups action_name, action_opts = mgroups
elif len(mgroups) == 1: elif len(mgroups) == 1: # pragma: nocover - unreachable - .* on second group always matches
action_name, action_opts = mgroups[0], None action_name, action_opts = mgroups[0], None
else: else: # pragma: nocover - unreachable - regex only can capture 2 groups
raise ValueError("While reading action %s we should have got up to " raise ValueError("While reading action %s we should have got up to "
"2 groups. Got: %r" % (action, mgroups)) "2 groups. Got: %r" % (action, mgroups))
if not action_opts is None: if not action_opts is None:

View File

@ -60,6 +60,7 @@ class JailsReader(ConfigReader):
sections = [ section ] sections = [ section ]
# Get the options of all jails. # Get the options of all jails.
parse_status = True
for sec in sections: for sec in sections:
jail = JailReader(sec, basedir=self.getBaseDir(), jail = JailReader(sec, basedir=self.getBaseDir(),
force_enable=self.__force_enable) force_enable=self.__force_enable)
@ -71,8 +72,8 @@ class JailsReader(ConfigReader):
self.__jails.append(jail) self.__jails.append(jail)
else: else:
logSys.error("Errors in jail %r. Skipping..." % sec) logSys.error("Errors in jail %r. Skipping..." % sec)
return False parse_status = False
return True return parse_status
def convert(self, allow_no_files=False): def convert(self, allow_no_files=False):
"""Convert read before __opts and jails to the commands stream """Convert read before __opts and jails to the commands stream

View File

@ -63,6 +63,12 @@ Comments: use '#' for comment lines and ';' (following a space) for inline comme
.SH DEFAULT .SH DEFAULT
The following options are applicable to all jails. Their meaning is described in the default \fIjail.conf\fR file. The following options are applicable to all jails. Their meaning is described in the default \fIjail.conf\fR file.
.TP .TP
\fBfilter\fR
.TP
\fBlogpath\fR
.TP
\fBaction\fR
.TP
\fBignoreip\fR \fBignoreip\fR
.TP .TP
\fBbantime\fR \fBbantime\fR
@ -74,6 +80,10 @@ The following options are applicable to all jails. Their meaning is described in
\fBbackend\fR \fBbackend\fR
.TP .TP
\fBusedns\fR \fBusedns\fR
.TP
\fBfailregex\fR
.TP
\fBignoreregex\fR
.SH "ACTION FILES" .SH "ACTION FILES"

View File

@ -27,6 +27,7 @@ from client.configreader import ConfigReader
from client.jailreader import JailReader from client.jailreader import JailReader
from client.jailsreader import JailsReader from client.jailsreader import JailsReader
from client.configurator import Configurator from client.configurator import Configurator
from utils import LogCaptureTestCase
class ConfigReaderTest(unittest.TestCase): class ConfigReaderTest(unittest.TestCase):
@ -106,7 +107,31 @@ option = %s
self.assertEqual(self._getoption(), 1) self.assertEqual(self._getoption(), 1)
class JailReaderTest(unittest.TestCase): class JailReaderTest(LogCaptureTestCase):
def testJailActionEmpty(self):
jail = JailReader('emptyaction', basedir=os.path.join('testcases','config'))
self.assertTrue(jail.read())
self.assertTrue(jail.getOptions())
self.assertTrue(jail.isEnabled())
self.assertTrue(self._is_logged('No filter set for jail emptyaction'))
self.assertTrue(self._is_logged('No actions were defined for emptyaction'))
def testJailActionFilterMissing(self):
jail = JailReader('missingbitsjail', basedir=os.path.join('testcases','config'))
self.assertTrue(jail.read())
self.assertFalse(jail.getOptions())
self.assertTrue(jail.isEnabled())
self.assertTrue(self._is_logged("Found no accessible config files for 'filter.d/catchallthebadies' under testcases/config"))
self.assertTrue(self._is_logged('Unable to read the filter'))
def testJailActionBrokenDef(self):
jail = JailReader('brokenactiondef', basedir=os.path.join('testcases','config'))
self.assertTrue(jail.read())
self.assertFalse(jail.getOptions())
self.assertTrue(jail.isEnabled())
self.assertTrue(self._is_logged('Error in action definition joho[foo'))
self.assertTrue(self._is_logged('Caught exception: While reading action joho[foo we should have got 1 or 2 groups. Got: 0'))
def testStockSSHJail(self): def testStockSSHJail(self):
jail = JailReader('ssh-iptables', basedir='config') # we are running tests from root project dir atm jail = JailReader('ssh-iptables', basedir='config') # we are running tests from root project dir atm
@ -114,33 +139,111 @@ class JailReaderTest(unittest.TestCase):
self.assertTrue(jail.getOptions()) self.assertTrue(jail.getOptions())
self.assertFalse(jail.isEnabled()) self.assertFalse(jail.isEnabled())
self.assertEqual(jail.getName(), 'ssh-iptables') self.assertEqual(jail.getName(), 'ssh-iptables')
jail.setName('ssh-funky-blocker')
self.assertEqual(jail.getName(), 'ssh-funky-blocker')
def testSplitAction(self): def testSplitAction(self):
action = "mail-whois[name=SSH]" action = "mail-whois[name=SSH]"
expected = ['mail-whois', {'name': 'SSH'}] expected = ['mail-whois', {'name': 'SSH'}]
result = JailReader.splitAction(action) result = JailReader.splitAction(action)
self.assertEqual(expected, result) self.assertEqual(expected, result)
self.assertEqual(['mail.who_is', {}], JailReader.splitAction("mail.who_is"))
self.assertEqual(['mail.who_is', {'a':'cat', 'b':'dog'}], JailReader.splitAction("mail.who_is[a=cat,b=dog]"))
self.assertEqual(['mail--ho_is', {}], JailReader.splitAction("mail--ho_is"))
self.assertEqual(['mail--ho_is', {}], JailReader.splitAction("mail--ho_is['s']"))
self.assertTrue(self._is_logged("Invalid argument ['s'] in ''s''"))
self.assertEqual(['mail', {'a': ','}], JailReader.splitAction("mail[a=',']"))
self.assertRaises(ValueError, JailReader.splitAction ,'mail-how[')
def testGlob(self): def testGlob(self):
d = tempfile.mkdtemp(prefix="f2b-temp") d = tempfile.mkdtemp(prefix="f2b-temp")
# Generate few files # Generate few files
# regular file # regular file
open(os.path.join(d, 'f1'), 'w').close() f1 = os.path.join(d, 'f1')
open(f1, 'w').close()
# dangling link # dangling link
os.symlink('nonexisting', os.path.join(d, 'f2'))
f2 = os.path.join(d, 'f2')
os.symlink('nonexisting',f2)
# must be only f1 # must be only f1
self.assertEqual(JailReader._glob(os.path.join(d, '*')), [os.path.join(d, 'f1')]) self.assertEqual(JailReader._glob(os.path.join(d, '*')), [f1])
# since f2 is dangling -- empty list # since f2 is dangling -- empty list
self.assertEqual(JailReader._glob(os.path.join(d, 'f2')), []) self.assertEqual(JailReader._glob(f2), [])
self.assertTrue(self._is_logged('File %s is a dangling link, thus cannot be monitored' % f2))
self.assertEqual(JailReader._glob(os.path.join(d, 'nonexisting')), [])
os.remove(f1)
os.remove(f2)
os.rmdir(d)
class JailsReaderTest(unittest.TestCase): class JailsReaderTest(LogCaptureTestCase):
def testProvidingBadBasedir(self): def testProvidingBadBasedir(self):
if not os.path.exists('/XXX'): if not os.path.exists('/XXX'):
reader = JailsReader(basedir='/XXX') reader = JailsReader(basedir='/XXX')
self.assertRaises(ValueError, reader.read) self.assertRaises(ValueError, reader.read)
def testReadTestJailConf(self):
jails = JailsReader(basedir=os.path.join('testcases','config'))
self.assertTrue(jails.read())
self.assertFalse(jails.getOptions())
self.assertRaises(ValueError, jails.convert)
comm_commands = jails.convert(allow_no_files=True)
self.maxDiff = None
self.assertEqual(sorted(comm_commands),
sorted([['add', 'emptyaction', 'auto'],
['set', 'emptyaction', 'usedns', 'warn'],
['set', 'emptyaction', 'maxretry', 3],
['set', 'emptyaction', 'findtime', 600],
['set', 'emptyaction', 'bantime', 600],
['add', 'special', 'auto'],
['set', 'special', 'usedns', 'warn'],
['set', 'special', 'maxretry', 3],
['set', 'special', 'addfailregex', '<IP>'],
['set', 'special', 'findtime', 600],
['set', 'special', 'bantime', 600],
['add', 'missinglogfiles', 'auto'],
['set', 'missinglogfiles', 'usedns', 'warn'],
['set', 'missinglogfiles', 'maxretry', 3],
['set', 'missinglogfiles', 'findtime', 600],
['set', 'missinglogfiles', 'bantime', 600],
['set', 'missinglogfiles', 'addfailregex', '<IP>'],
['add', 'brokenaction', 'auto'],
['set', 'brokenaction', 'usedns', 'warn'],
['set', 'brokenaction', 'maxretry', 3],
['set', 'brokenaction', 'findtime', 600],
['set', 'brokenaction', 'bantime', 600],
['set', 'brokenaction', 'addfailregex', '<IP>'],
['set', 'brokenaction', 'addaction', 'brokenaction'],
['set',
'brokenaction',
'actionban',
'brokenaction',
'hit with big stick <ip>'],
['set', 'brokenaction', 'actionstop', 'brokenaction', ''],
['set', 'brokenaction', 'actionstart', 'brokenaction', ''],
['set', 'brokenaction', 'actionunban', 'brokenaction', ''],
['set', 'brokenaction', 'actioncheck', 'brokenaction', ''],
['add', 'parse_to_end_of_jail.conf', 'auto'],
['set', 'parse_to_end_of_jail.conf', 'usedns', 'warn'],
['set', 'parse_to_end_of_jail.conf', 'maxretry', 3],
['set', 'parse_to_end_of_jail.conf', 'findtime', 600],
['set', 'parse_to_end_of_jail.conf', 'bantime', 600],
['set', 'parse_to_end_of_jail.conf', 'addfailregex', '<IP>'],
['start', 'emptyaction'],
['start', 'special'],
['start', 'missinglogfiles'],
['start', 'brokenaction'],
['start', 'parse_to_end_of_jail.conf'],]))
self.assertTrue(self._is_logged("Errors in jail 'missingbitsjail'. Skipping..."))
self.assertTrue(self._is_logged("No file(s) found for glob /weapons/of/mass/destruction"))
def testReadStockJailConf(self): def testReadStockJailConf(self):
jails = JailsReader(basedir='config') # we are running tests from root project dir atm jails = JailsReader(basedir='config') # we are running tests from root project dir atm
self.assertTrue(jails.read()) # opens fine self.assertTrue(jails.read()) # opens fine
@ -153,6 +256,7 @@ class JailsReaderTest(unittest.TestCase):
# We should not "read" some bogus jail # We should not "read" some bogus jail
old_comm_commands = comm_commands[:] # make a copy old_comm_commands = comm_commands[:] # make a copy
self.assertFalse(jails.getOptions("BOGUS")) self.assertFalse(jails.getOptions("BOGUS"))
self.assertTrue(self._is_logged("No section: 'BOGUS'"))
# and there should be no side-effects # and there should be no side-effects
self.assertEqual(jails.convert(), old_comm_commands) self.assertEqual(jails.convert(), old_comm_commands)

View File

@ -0,0 +1,4 @@
[Definition]
actionban = hit with big stick <ip>

View File

@ -0,0 +1,5 @@
[Definition]
# 3 = INFO
loglevel = 3

View File

@ -0,0 +1,4 @@
[Definition]
failregex = <IP>

View File

@ -0,0 +1,33 @@
[DEFAULT]
filter = simple
logpath = /non/exist
[emptyaction]
enabled = true
filter =
action =
[special]
failregex = <IP>
ignoreregex =
ignoreip =
[missinglogfiles]
logpath = /weapons/of/mass/destruction
[brokenactiondef]
enabled = true
action = joho[foo
[brokenaction]
enabled = true
action = brokenaction
[missingbitsjail]
filter = catchallthebadies
action = thefunkychickendance
[parse_to_end_of_jail.conf]
enabled = true
action =