ENH: Add fail2ban persistent data storage

pull/480/merge^2
Steven Hiscocks 2013-12-07 23:23:28 +00:00
parent 2c1199cce0
commit bbadef847b
16 changed files with 541 additions and 18 deletions

View File

@ -47,3 +47,14 @@ socket = /var/run/fail2ban/fail2ban.sock
#
pidfile = /var/run/fail2ban/fail2ban.pid
# Options: dbfile
# Notes.: Set the file for the fail2ban persistent data to be stored.
# A value of :memory: means database is only stored in memory, and
# data is lost once fail2ban is stops.
# Values: [ FILE ] :memory: Default: /var/lib/fail2ban/fail2ban.db
dbfile = /var/lib/fail2ban/fail2ban.db
# Options: dbpurgeage
# Notes.: Sets age at which bans should be purged from the database
# Values: [ SECONDS ] Default: 86400 (24hours)
dbpurgeage = 86400

View File

@ -102,6 +102,12 @@ class Beautifier:
msg = msg + "DEBUG"
else:
msg = msg + `response`
elif inC[1] == "dbfile":
msg = "Current database file is:\n"
msg = msg + "`- " + response
elif inC[1] == "dbpurgeage":
msg = "Current database purge age is:\n"
msg = msg + "`- %iseconds" % response
elif inC[2] in ("logpath", "addlogpath", "dellogpath"):
if len(response) == 0:
msg = "No file is currently monitored"

View File

@ -45,7 +45,9 @@ class Fail2banReader(ConfigReader):
def getOptions(self):
opts = [["int", "loglevel", 1],
["string", "logtarget", "STDERR"]]
["string", "logtarget", "STDERR"],
["string", "dbfile", "/var/lib/fail2ban/fail2ban.db"],
["int", "dbpurgeage", "/var/lib/fail2ban/fail2ban.db"]]
self.__opts = ConfigReader.getOptions(self, "Definition", opts)
def convert(self):
@ -55,5 +57,9 @@ class Fail2banReader(ConfigReader):
stream.append(["set", "loglevel", self.__opts[opt]])
elif opt == "logtarget":
stream.append(["set", "logtarget", self.__opts[opt]])
elif opt == "dbfile":
stream.append(["set", "dbfile", self.__opts[opt]])
elif opt == "dbpurgeage":
stream.append(["set", "dbpurgeage", self.__opts[opt]])
return stream

View File

@ -43,6 +43,11 @@ protocol = [
["get loglevel", "gets the logging level"],
["set logtarget <TARGET>", "sets logging target to <TARGET>. Can be STDOUT, STDERR, SYSLOG or a file"],
["get logtarget", "gets logging target"],
['', "DATABASE", ""],
["set dbfile <FILE>", "set the location of fail2ban persistent datastore"],
["get dbfile", "get the location of fail2ban persistent datastore"],
["set dbpurgeage <SECONDS>", "sets the max age in <SECONDS> that history of bans will be kept"],
["get dbpurgeage", "gets the max age in seconds that history of bans will be kept"],
['', "JAIL CONTROL", ""],
["add <JAIL> <BACKEND>", "creates <JAIL> using <BACKEND>"],
["start <JAIL>", "starts the jail <JAIL>"],

254
fail2ban/server/database.py Normal file
View File

@ -0,0 +1,254 @@
# 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__ = "Steven Hiscocks"
__copyright__ = "Copyright (c) 2013 Steven Hiscocks"
__license__ = "GPL"
import logging
import sys
from threading import Lock
import sqlite3
import json
import locale
from fail2ban.server.mytime import MyTime
from fail2ban.server.ticket import FailTicket
# Gets the instance of the logger.
logSys = logging.getLogger(__name__)
if sys.version_info >= (3,):
sqlite3.register_adapter(
dict,
lambda x: json.dumps(x, ensure_ascii=False).encode(
locale.getpreferredencoding(), 'replace'))
sqlite3.register_converter(
"JSON",
lambda x: json.loads(x.decode(
locale.getpreferredencoding(), 'replace')))
else:
sqlite3.register_adapter(dict, json.dumps)
sqlite3.register_converter("JSON", json.loads)
def lockandcommit():
def wrap(f):
def func(self, *args, **kw):
with self._lock: # Threading lock
with self._db: # Auto commit and rollback on exception
return f(self, self._db.cursor(), *args, **kw)
return func
return wrap
class Fail2BanDb(object):
__version__ = 1
def __init__(self, filename, purgeAge=24*60*60):
self._lock = Lock()
try:
self._db = sqlite3.connect(
filename, check_same_thread=False,
detect_types=sqlite3.PARSE_DECLTYPES)
self._dbFilename = filename
self._purgeAge = purgeAge
logSys.info(
"Connected to fail2ban persistent database '%s'", filename)
except sqlite3.OperationalError, e:
logSys.error(
"Error connecting to fail2ban persistent database '%s': %s",
filename, e.args[0])
raise
cur = self._db.cursor()
cur.execute("PRAGMA foreign_keys = ON;")
try:
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
except sqlite3.OperationalError:
logSys.warning("New database created. Version '%i'",
self.createDb())
else:
version = cur.fetchone()[0]
if version < Fail2BanDb.__version__:
logSys.warning( "Database updated from '%i' to '%i'",
version, self.updateDb(version))
finally:
cur.close()
def getFilename(self):
return self._dbFilename
def getPurgeAge(self):
return self._purgeAge
def setPurgeAge(self, value):
self._purgeAge = int(value)
@lockandcommit()
def createDb(self, cur):
# Version info
cur.execute("CREATE TABLE fail2banDb(version INTEGER)")
cur.execute("INSERT INTO fail2banDb(version) VALUES(?)",
(Fail2BanDb.__version__, ))
# Jails
cur.execute("CREATE TABLE jails("
"name TEXT NOT NULL UNIQUE, "
"enabled INTEGER NOT NULL DEFAULT 1"
")")
cur.execute("CREATE INDEX jails_name ON jails(name)")
# Logs
cur.execute("CREATE TABLE logs("
"jail TEXT NOT NULL, "
"path TEXT, "
"firstlinemd5 TEXT, "
#TODO: systemd journal features
#"journalmatch TEXT, "
#"journlcursor TEXT, "
"lastfilepos INTEGER DEFAULT 0, "
#"lastfiletime INTEGER DEFAULT 0, " # is this easily available
"FOREIGN KEY(jail) REFERENCES jails(name) ON DELETE CASCADE, "
"UNIQUE(jail, path)"
")")
cur.execute("CREATE INDEX logs_path ON logs(path)")
cur.execute("CREATE INDEX logs_jail_path ON logs(jail, path)")
# Bans
cur.execute("CREATE TABLE bans("
"jail TEXT NOT NULL, "
"ip TEXT, "
"timeofban INTEGER NOT NULL, "
"data JSON, "
"FOREIGN KEY(jail) REFERENCES jails(name) "
")")
cur.execute(
"CREATE INDEX bans_jail_timeofban_ip ON bans(jail, timeofban)")
cur.execute("CREATE INDEX bans_jail_ip ON bans(jail, ip)")
cur.execute("CREATE INDEX bans_ip ON bans(ip)")
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
return cur.fetchone()[0]
@lockandcommit()
def updateDb(self, cur, version):
raise NotImplementedError(
"Only single version of database exists...how did you get here??")
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
return cur.fetchone()[0]
@lockandcommit()
def addJail(self, cur, jail):
cur.execute(
"INSERT OR REPLACE INTO jails(name, enabled) VALUES(?, 1)",
(jail.getName(),))
def delJail(self, jail):
return self.delJailName(jail.getName())
@lockandcommit()
def delJailName(self, cur, name):
# Will be deleted by purge as appropriate
cur.execute(
"UPDATE jails SET enabled=0 WHERE name=?", (name, ))
@lockandcommit()
def getJailNames(self, cur):
cur.execute("SELECT name FROM jails")
return set(row[0] for row in cur.fetchmany())
@lockandcommit()
def addLog(self, cur, jail, container):
lastLinePos = None
cur.execute(
"SELECT firstlinemd5, lastfilepos FROM logs "
"WHERE jail=? AND path=?",
(jail.getName(), container.getFileName()))
try:
firstLineMD5, lastLinePos = cur.fetchone()
except TypeError:
cur.execute(
"INSERT INTO logs(jail, path, firstlinemd5, lastfilepos) "
"VALUES(?, ?, ?, ?)",
(jail.getName(), container.getFileName(),
container.getHash(), container.getPos()))
else:
if container.getHash() != firstLineMD5:
self._updateLog(cur, jail, container)
lastLinePos = None
return lastLinePos
@lockandcommit()
def getLogPaths(self, cur, jail=None):
query = "SELECT path FROM logs"
queryArgs = []
if jail is not None:
query += " WHERE jail=?"
queryArgs.append(jail.getName())
cur.execute(query, queryArgs)
return set(row[0] for row in cur.fetchmany())
@lockandcommit()
def updateLog(self, cur, *args, **kwargs):
self._updateLog(cur, *args, **kwargs)
def _updateLog(self, cur, jail, container):
cur.execute(
"UPDATE logs SET firstlinemd5=?, lastfilepos=? "
"WHERE jail=? AND path=?",
(container.getHash(), container.getPos(),
jail.getName(), container.getFileName()))
@lockandcommit()
def addBan(self, cur, jail, ticket):
#TODO: Implement data parts once arbitrary match keys completed
cur.execute(
"INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)",
(jail.getName(), ticket.getIP(), ticket.getTime(),
{"matches": ticket.getMatches(),
"failures": ticket.getAttempt()}))
@lockandcommit()
def getBans(self, cur, jail=None, bantime=None):
query = "SELECT ip, timeofban, data FROM bans"
queryArgs = []
if jail is not None:
query += " WHERE jail=?"
queryArgs.append(jail.getName())
if bantime is not None:
query += " AND timeofban > ?"
queryArgs.append(MyTime.time() - bantime)
tickets = []
for ip, timeofban, data in cur.execute(query, queryArgs):
#TODO: Implement data parts once arbitrary match keys completed
tickets.append(FailTicket(ip, timeofban, data['matches']))
tickets[-1].setAttempt(data['failures'])
return tickets
@lockandcommit()
def purge(self, cur):
cur.execute(
"DELETE FROM bans WHERE timeofban < ?",
(MyTime.time() - self._purgeAge, ))
cur.execute(
"DELETE FROM jails WHERE enabled = 0 "
"AND NOT EXISTS(SELECT * FROM bans WHERE jail = jails.name)")

View File

@ -527,6 +527,9 @@ class FileFilter(Filter):
logSys.error(path + " already exists")
else:
container = FileContainer(path, self.getLogEncoding(), tail)
lastpos = self.jail.getDatabase().addLog(self.jail, container)
if lastpos and not tail:
container.setPos(lastpos)
self.__logPath.append(container)
logSys.info("Added logfile = %s" % path)
self._addLogPath(path) # backend specific
@ -546,6 +549,7 @@ class FileFilter(Filter):
for log in self.__logPath:
if log.getFileName() == path:
self.__logPath.remove(log)
self.jail.getDatabase().updateLog(self.jail, log)
logSys.info("Removed logfile = %s" % path)
self._delLogPath(path)
return
@ -644,6 +648,7 @@ class FileFilter(Filter):
break
self.processLineAndAdd(line)
container.close()
self.jail.getDatabase().updateLog(self.jail, container)
return True
def status(self):
@ -682,7 +687,7 @@ class FileContainer:
try:
firstLine = handler.readline()
# Computes the MD5 of the first line.
self.__hash = md5sum(firstLine).digest()
self.__hash = md5sum(firstLine).hexdigest()
# Start at the beginning of file if tail mode is off.
if tail:
handler.seek(0, 2)
@ -702,6 +707,15 @@ class FileContainer:
def getEncoding(self):
return self.__encoding
def getHash(self):
return self.__hash
def getPos(self):
return self.__pos
def setPos(self, value):
self.__pos = value
def open(self):
self.__handler = open(self.__filename, 'rb')
# Set the file descriptor to be FD_CLOEXEC
@ -717,7 +731,7 @@ class FileContainer:
return False
firstLine = self.__handler.readline()
# Computes the MD5 of the first line.
myHash = md5sum(firstLine).digest()
myHash = md5sum(firstLine).hexdigest()
## print "D: fn=%s hashes=%s/%s inos=%s/%s pos=%s rotate=%s" % (
## self.__filename, self.__hash, myHash, stats.st_ino, self.__ino, self.__pos,
## self.__hash != myHash or self.__ino != stats.st_ino)

View File

@ -37,7 +37,8 @@ class Jail:
# list had .index until 2.6
_BACKENDS = ['pyinotify', 'gamin', 'polling', 'systemd']
def __init__(self, name, backend = "auto"):
def __init__(self, db, name, backend = "auto"):
self.__db = db
self.setName(name)
self.__queue = Queue.Queue()
self.__filter = None
@ -118,6 +119,9 @@ class Jail:
def getName(self):
return self.__name
def getDatabase(self):
return self.__db
def getFilter(self):
return self.__filter
@ -126,6 +130,7 @@ class Jail:
def putFailTicket(self, ticket):
self.__queue.put(ticket)
self.__db.addBan(self, ticket)
def getFailTicket(self):
try:
@ -136,6 +141,9 @@ class Jail:
def start(self):
self.__filter.start()
self.__action.start()
# Restore any previous valid bans from the database
for ticket in self.__db.getBans(self, self.__action.getBanTime()):
self.__queue.put(ticket)
logSys.info("Jail '%s' started" % self.__name)
def stop(self):

View File

@ -50,13 +50,13 @@ class Jails:
# @param name The name of the jail
# @param backend The backend to use
def add(self, name, backend):
def add(self, db, name, backend):
try:
self.__lock.acquire()
if self.__jails.has_key(name):
raise DuplicateJailException(name)
else:
self.__jails[name] = Jail(name, backend)
self.__jails[name] = Jail(db, name, backend)
finally:
self.__lock.release()

View File

@ -30,6 +30,7 @@ from filter import FileFilter, JournalFilter
from transmitter import Transmitter
from asyncserver import AsyncServer
from asyncserver import AsyncServerException
from database import Fail2BanDb
from fail2ban import version
import logging, logging.handlers, sys, os, signal
@ -50,6 +51,9 @@ class Server:
# Set logging level
self.setLogLevel(3)
self.setLogTarget("STDOUT")
# Create database, initially in memory
self.setDatabase(":memory:")
def __sigTERMhandler(self, signum, frame):
logSys.debug("Caught signal %d. Exiting" % signum)
@ -117,10 +121,13 @@ class Server:
def addJail(self, name, backend):
self.__jails.add(name, backend)
self.__jails.add(self.__db, name, backend)
self.__db.addJail(self.__jails.get(name))
def delJail(self, name):
def delJail(self, name, dbDel=True):
self.__jails.remove(name)
if dbDel:
self.__db.delJailName(name)
def startJail(self, name):
try:
@ -130,13 +137,13 @@ class Server:
finally:
self.__lock.release()
def stopJail(self, name):
def stopJail(self, name, dbDel=True):
logSys.debug("Stopping jail %s" % name)
try:
self.__lock.acquire()
if self.isAlive(name):
self.__jails.get(name).stop()
self.delJail(name)
self.delJail(name, dbDel=dbDel)
finally:
self.__lock.release()
@ -145,7 +152,7 @@ class Server:
try:
self.__lock.acquire()
for jail in self.__jails.getAll():
self.stopJail(jail)
self.stopJail(jail, dbDel=False)
finally:
self.__lock.release()
@ -460,6 +467,16 @@ class Server:
finally:
self.__loggingLock.release()
def setDatabase(self, filename):
if self.__jails.size() == 0:
self.__db = Fail2BanDb(filename)
else:
raise RuntimeError(
"Cannot change database when there are jails present")
def getDatabase(self):
return self.__db
def __createDaemon(self): # pragma: no cover
""" Detach a process from the controlling terminal and run it in the
background as a daemon.

View File

@ -113,6 +113,13 @@ class Transmitter:
return self.__server.getLogTarget()
else:
raise Exception("Failed to change log target")
#Database
elif name == "dbfile":
self.__server.setDatabase(command[1])
return self.__server.getDatabase().getFilename()
elif name == "dbpurgeage":
self.__server.getDatabase().setPurgeAge(command[1])
return self.__server.getDatabase().getPurgeAge()
# Jail
elif command[1] == "idle":
if command[2] == "on":
@ -257,6 +264,11 @@ class Transmitter:
return self.__server.getLogLevel()
elif name == "logtarget":
return self.__server.getLogTarget()
#Database
elif name == "dbfile":
return self.__server.getDatabase().getFilename()
elif name == "dbpurgeage":
return self.__server.getDatabase().getPurgeAge()
# Filter
elif command[1] == "logpath":
return self.__server.getLogPath(name)

View File

@ -0,0 +1,156 @@
# 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.
# Fail2Ban developers
__copyright__ = "Copyright (c) 2013 Steven Hiscocks"
__license__ = "GPL"
import os
import unittest
import tempfile
import sqlite3
from fail2ban.server.database import Fail2BanDb
from fail2ban.server.filter import FileContainer
from fail2ban.server.mytime import MyTime
from fail2ban.server.ticket import FailTicket
from fail2ban.tests.dummyjail import DummyJail
class DatabaseTest(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
_, self.dbFilename = tempfile.mkstemp(".db", "fail2ban_")
self.db = Fail2BanDb(self.dbFilename)
def tearDown(self):
"""Call after every test case."""
# Cleanup
os.remove(self.dbFilename)
def getFilename(self):
self.assertEqual(self.dbFilename, self.db.getFilename())
def testCreateInvalidPath(self):
self.assertRaises(
sqlite3.OperationalError,
Fail2BanDb,
"/this/path/should/not/exist")
def testCreateAndReconnect(self):
self.testAddJail()
# Reconnect...
self.db = Fail2BanDb(self.dbFilename)
# and check jail of same name still present
self.assertTrue(
self.jail.getName() in self.db.getJailNames(),
"Jail not retained in Db after disconnect reconnect.")
def testUpdateDb(self):
# TODO: Currently only single version exists
pass
def testAddJail(self):
self.jail = DummyJail()
self.db.addJail(self.jail)
self.assertTrue(
self.jail.getName() in self.db.getJailNames(),
"Jail not added to database")
def testAddLog(self):
self.testAddJail() # Jail required
_, filename = tempfile.mkstemp(".log", "Fail2BanDb_")
self.fileContainer = FileContainer(filename, "utf-8")
self.db.addLog(self.jail, self.fileContainer)
self.assertTrue(filename in self.db.getLogPaths(self.jail))
def testUpdateLog(self):
self.testAddLog() # Add log file
# Write some text
filename = self.fileContainer.getFileName()
file_ = open(filename, "w")
file_.write("Some text to write which will change md5sum\n")
file_.close()
self.fileContainer.open()
self.fileContainer.readline()
self.fileContainer.close()
# Capture position which should be after line just written
lastPos = self.fileContainer.getPos()
self.assertTrue(lastPos > 0)
self.db.updateLog(self.jail, self.fileContainer)
# New FileContainer for file
self.fileContainer = FileContainer(filename, "utf-8")
self.assertEqual(self.fileContainer.getPos(), 0)
# Database should return previous position in file
self.assertEqual(
self.db.addLog(self.jail, self.fileContainer), lastPos)
# Change md5sum
file_ = open(filename, "w") # Truncate
file_.write("Some different text to change md5sum\n")
file_.close()
self.fileContainer = FileContainer(filename, "utf-8")
self.assertEqual(self.fileContainer.getPos(), 0)
# Database should be aware of md5sum change, such doesn't return
# last position in file
self.assertEqual(
self.db.addLog(self.jail, self.fileContainer), None)
def testAddBan(self):
self.testAddJail()
ticket = FailTicket("127.0.0.1", 0, [])
self.db.addBan(self.jail, ticket)
self.assertEquals(len(self.db.getBans(self.jail)), 1)
self.assertTrue(
isinstance(self.db.getBans(self.jail)[0], FailTicket))
def testPurge(self):
self.testAddJail() # Add jail
self.db.purge() # Jail enabled by default so shouldn't be purged
self.assertEqual(len(self.db.getJailNames()), 1)
self.db.delJail(self.jail)
self.db.purge() # Should remove jail
self.assertEqual(len(self.db.getJailNames()), 0)
self.testAddBan()
self.db.delJail(self.jail)
self.db.purge() # Purge should remove all bans
self.assertEqual(len(self.db.getJailNames()), 0)
self.assertEqual(len(self.db.getBans(self.jail)), 0)
# Should leave jail
self.testAddJail()
self.db.addBan(self.jail, FailTicket("127.0.0.1", MyTime.time(), []))
self.db.delJail(self.jail)
self.db.purge() # Should leave jail as ban present
self.assertEqual(len(self.db.getJailNames()), 1)
self.assertEqual(len(self.db.getBans(self.jail)), 1)

View File

@ -23,12 +23,17 @@ __copyright__ = "Copyright (c) 2012 Yaroslav Halchenko"
__license__ = "GPL"
from threading import Lock
from fail2ban.server.database import Fail2BanDb
class DummyJail(object):
"""A simple 'jail' to suck in all the tickets generated by Filter's
"""
def __init__(self):
self.lock = Lock()
self.queue = []
self.__db = Fail2BanDb(":memory:")
self.__db.addJail(self)
def __len__(self):
try:
@ -54,6 +59,15 @@ class DummyJail(object):
finally:
self.lock.release()
def setIdle(self, value):
pass
def getIdle(self):
pass
def getName(self):
return "DummyJail #%s with %d tickets" % (id(self), len(self))
def getDatabase(self):
return self.__db

View File

@ -40,6 +40,7 @@ from fail2ban.server.filter import FileFilter, DNSUtils
from fail2ban.server.failmanager import FailManager
from fail2ban.server.failmanager import FailManagerEmpty
from fail2ban.server.mytime import MyTime
from fail2ban.server.database import Fail2BanDb
from fail2ban.tests.utils import setUpMyTime, tearDownMyTime
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
@ -228,7 +229,7 @@ class LogFile(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
self.filter = FilterPoll(None)
self.filter = FilterPoll(DummyJail())
self.filter.addLogPath(LogFile.FILENAME)
def tearDown(self):
@ -251,7 +252,7 @@ class LogFileMonitor(unittest.TestCase):
self.filter = self.name = 'NA'
_, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures')
self.file = open(self.name, 'a')
self.filter = FilterPoll(None)
self.filter = FilterPoll(DummyJail())
self.filter.addLogPath(self.name)
self.filter.setActive(True)
self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>")
@ -564,7 +565,7 @@ def get_monitor_failures_testcase(Filter_):
# tail written before, so let's not copy anything yet
#_copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100)
# we should detect the failures
self.assert_correct_last_attempt(GetFailures.FAILURES_01, count=6) # was needed if we write twice above
self.assert_correct_last_attempt(GetFailures.FAILURES_01, count=3) # was needed if we write twice above
# now copy and get even more
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=100)
@ -715,7 +716,8 @@ class GetFailures(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
setUpMyTime()
self.filter = FileFilter(None)
self.jail = DummyJail()
self.filter = FileFilter(self.jail)
self.filter.setActive(True)
# TODO Test this
#self.filter.setTimeRegex("\S{3}\s{1,2}\d{1,2} \d{2}:\d{2}:\d{2}")
@ -802,7 +804,8 @@ class GetFailures(unittest.TestCase):
for useDns, output in (('yes', output_yes),
('no', output_no),
('warn', output_yes)):
filter_ = FileFilter(None, useDns=useDns)
jail = DummyJail()
filter_ = FileFilter(jail, useDns=useDns)
filter_.setActive(True)
filter_.failManager.setMaxRetry(1) # we might have just few failures
@ -916,5 +919,6 @@ class JailTests(unittest.TestCase):
def testSetBackend_gh83(self):
# smoke test
jail = Jail('test', backend='polling') # Must not fail to initiate
# Must not fail to initiate
jail = Jail(Fail2BanDb(":memory:"), 'test', backend='polling')

View File

@ -28,6 +28,7 @@ import unittest, socket, time, tempfile, os, locale, sys
from fail2ban.server.server import Server
from fail2ban.server.jail import Jail
from fail2ban.server.database import Fail2BanDb
from fail2ban.exceptions import UnknownJailException
try:
from fail2ban.server import filtersystemd
@ -168,6 +169,15 @@ class Transmitter(TransmitterBase):
# Approx 1 second delay
self.assertAlmostEqual(t1 - t0, 1, places=2)
def testDatabase(self):
_, tmpFilename = tempfile.mkstemp(".db", "Fail2Ban_")
# Jails present, cant change database
self.setGetTestNOK("dbfile", tmpFilename)
self.server.delJail(self.jailName)
self.setGetTest("dbfile", tmpFilename)
self.setGetTest("dbpurgeage", 600)
self.setGetTestNOK("dbpurgeage", "LIZARD")
def testAddJail(self):
jail2 = "TestJail2"
jail3 = "TestJail3"
@ -641,5 +651,5 @@ class JailTests(unittest.TestCase):
def testLongName(self):
# Just a smoke test for now
longname = "veryveryverylongname"
jail = Jail(longname)
jail = Jail(Fail2BanDb(":memory:"), longname)
self.assertEqual(jail.getName(), longname)

View File

@ -146,6 +146,7 @@ def gatherTests(regexps=None, no_network=False):
from fail2ban.tests import actiontestcase
from fail2ban.tests import sockettestcase
from fail2ban.tests import misctestcase
from fail2ban.tests import databasetestcase
if json:
from fail2ban.tests import samplestestcase
@ -184,6 +185,8 @@ def gatherTests(regexps=None, no_network=False):
tests.addTest(unittest.makeSuite(misctestcase.HelpersTest))
tests.addTest(unittest.makeSuite(misctestcase.SetupTest))
tests.addTest(unittest.makeSuite(misctestcase.TestsUtilsTest))
# Database
tests.addTest(unittest.makeSuite(databasetestcase.DatabaseTest))
# Filter
if not no_network:

View File

@ -131,6 +131,9 @@ setup(
('/var/run/fail2ban',
''
),
('/var/lib/fail2ban',
''
),
('/usr/share/doc/fail2ban',
['README.md', 'DEVELOP', 'doc/run-rootless.txt']
)