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 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" msg = msg + "DEBUG"
else: else:
msg = msg + `response` 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"): elif inC[2] in ("logpath", "addlogpath", "dellogpath"):
if len(response) == 0: if len(response) == 0:
msg = "No file is currently monitored" msg = "No file is currently monitored"

View File

@ -45,7 +45,9 @@ class Fail2banReader(ConfigReader):
def getOptions(self): def getOptions(self):
opts = [["int", "loglevel", 1], 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) self.__opts = ConfigReader.getOptions(self, "Definition", opts)
def convert(self): def convert(self):
@ -55,5 +57,9 @@ class Fail2banReader(ConfigReader):
stream.append(["set", "loglevel", self.__opts[opt]]) stream.append(["set", "loglevel", self.__opts[opt]])
elif opt == "logtarget": elif opt == "logtarget":
stream.append(["set", "logtarget", self.__opts[opt]]) 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 return stream

View File

@ -43,6 +43,11 @@ protocol = [
["get loglevel", "gets the logging level"], ["get loglevel", "gets the logging level"],
["set logtarget <TARGET>", "sets logging target to <TARGET>. Can be STDOUT, STDERR, SYSLOG or a file"], ["set logtarget <TARGET>", "sets logging target to <TARGET>. Can be STDOUT, STDERR, SYSLOG or a file"],
["get logtarget", "gets logging target"], ["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", ""], ['', "JAIL CONTROL", ""],
["add <JAIL> <BACKEND>", "creates <JAIL> using <BACKEND>"], ["add <JAIL> <BACKEND>", "creates <JAIL> using <BACKEND>"],
["start <JAIL>", "starts the jail <JAIL>"], ["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") logSys.error(path + " already exists")
else: else:
container = FileContainer(path, self.getLogEncoding(), tail) 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) self.__logPath.append(container)
logSys.info("Added logfile = %s" % path) logSys.info("Added logfile = %s" % path)
self._addLogPath(path) # backend specific self._addLogPath(path) # backend specific
@ -546,6 +549,7 @@ class FileFilter(Filter):
for log in self.__logPath: for log in self.__logPath:
if log.getFileName() == path: if log.getFileName() == path:
self.__logPath.remove(log) self.__logPath.remove(log)
self.jail.getDatabase().updateLog(self.jail, log)
logSys.info("Removed logfile = %s" % path) logSys.info("Removed logfile = %s" % path)
self._delLogPath(path) self._delLogPath(path)
return return
@ -644,6 +648,7 @@ class FileFilter(Filter):
break break
self.processLineAndAdd(line) self.processLineAndAdd(line)
container.close() container.close()
self.jail.getDatabase().updateLog(self.jail, container)
return True return True
def status(self): def status(self):
@ -682,7 +687,7 @@ class FileContainer:
try: try:
firstLine = handler.readline() firstLine = handler.readline()
# Computes the MD5 of the first line. # 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. # Start at the beginning of file if tail mode is off.
if tail: if tail:
handler.seek(0, 2) handler.seek(0, 2)
@ -702,6 +707,15 @@ class FileContainer:
def getEncoding(self): def getEncoding(self):
return self.__encoding 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): def open(self):
self.__handler = open(self.__filename, 'rb') self.__handler = open(self.__filename, 'rb')
# Set the file descriptor to be FD_CLOEXEC # Set the file descriptor to be FD_CLOEXEC
@ -717,7 +731,7 @@ class FileContainer:
return False return False
firstLine = self.__handler.readline() firstLine = self.__handler.readline()
# Computes the MD5 of the first line. # 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" % ( ## 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.__filename, self.__hash, myHash, stats.st_ino, self.__ino, self.__pos,
## self.__hash != myHash or self.__ino != stats.st_ino) ## self.__hash != myHash or self.__ino != stats.st_ino)

View File

@ -37,7 +37,8 @@ class Jail:
# list had .index until 2.6 # list had .index until 2.6
_BACKENDS = ['pyinotify', 'gamin', 'polling', 'systemd'] _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.setName(name)
self.__queue = Queue.Queue() self.__queue = Queue.Queue()
self.__filter = None self.__filter = None
@ -118,6 +119,9 @@ class Jail:
def getName(self): def getName(self):
return self.__name return self.__name
def getDatabase(self):
return self.__db
def getFilter(self): def getFilter(self):
return self.__filter return self.__filter
@ -126,6 +130,7 @@ class Jail:
def putFailTicket(self, ticket): def putFailTicket(self, ticket):
self.__queue.put(ticket) self.__queue.put(ticket)
self.__db.addBan(self, ticket)
def getFailTicket(self): def getFailTicket(self):
try: try:
@ -136,6 +141,9 @@ class Jail:
def start(self): def start(self):
self.__filter.start() self.__filter.start()
self.__action.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) logSys.info("Jail '%s' started" % self.__name)
def stop(self): def stop(self):

View File

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

View File

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

View File

@ -113,6 +113,13 @@ class Transmitter:
return self.__server.getLogTarget() return self.__server.getLogTarget()
else: else:
raise Exception("Failed to change log target") 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 # Jail
elif command[1] == "idle": elif command[1] == "idle":
if command[2] == "on": if command[2] == "on":
@ -257,6 +264,11 @@ class Transmitter:
return self.__server.getLogLevel() return self.__server.getLogLevel()
elif name == "logtarget": elif name == "logtarget":
return self.__server.getLogTarget() return self.__server.getLogTarget()
#Database
elif name == "dbfile":
return self.__server.getDatabase().getFilename()
elif name == "dbpurgeage":
return self.__server.getDatabase().getPurgeAge()
# Filter # Filter
elif command[1] == "logpath": elif command[1] == "logpath":
return self.__server.getLogPath(name) 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" __license__ = "GPL"
from threading import Lock from threading import Lock
from fail2ban.server.database import Fail2BanDb
class DummyJail(object): class DummyJail(object):
"""A simple 'jail' to suck in all the tickets generated by Filter's """A simple 'jail' to suck in all the tickets generated by Filter's
""" """
def __init__(self): def __init__(self):
self.lock = Lock() self.lock = Lock()
self.queue = [] self.queue = []
self.__db = Fail2BanDb(":memory:")
self.__db.addJail(self)
def __len__(self): def __len__(self):
try: try:
@ -54,6 +59,15 @@ class DummyJail(object):
finally: finally:
self.lock.release() self.lock.release()
def setIdle(self, value):
pass
def getIdle(self):
pass
def getName(self): def getName(self):
return "DummyJail #%s with %d tickets" % (id(self), len(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 FailManager
from fail2ban.server.failmanager import FailManagerEmpty from fail2ban.server.failmanager import FailManagerEmpty
from fail2ban.server.mytime import MyTime from fail2ban.server.mytime import MyTime
from fail2ban.server.database import Fail2BanDb
from fail2ban.tests.utils import setUpMyTime, tearDownMyTime from fail2ban.tests.utils import setUpMyTime, tearDownMyTime
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
@ -228,7 +229,7 @@ class LogFile(unittest.TestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
self.filter = FilterPoll(None) self.filter = FilterPoll(DummyJail())
self.filter.addLogPath(LogFile.FILENAME) self.filter.addLogPath(LogFile.FILENAME)
def tearDown(self): def tearDown(self):
@ -251,7 +252,7 @@ class LogFileMonitor(unittest.TestCase):
self.filter = self.name = 'NA' self.filter = self.name = 'NA'
_, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures') _, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures')
self.file = open(self.name, 'a') self.file = open(self.name, 'a')
self.filter = FilterPoll(None) self.filter = FilterPoll(DummyJail())
self.filter.addLogPath(self.name) self.filter.addLogPath(self.name)
self.filter.setActive(True) 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>") 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 # tail written before, so let's not copy anything yet
#_copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100) #_copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100)
# we should detect the failures # 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 # now copy and get even more
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=100) _copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=100)
@ -715,7 +716,8 @@ class GetFailures(unittest.TestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
setUpMyTime() setUpMyTime()
self.filter = FileFilter(None) self.jail = DummyJail()
self.filter = FileFilter(self.jail)
self.filter.setActive(True) self.filter.setActive(True)
# TODO Test this # TODO Test this
#self.filter.setTimeRegex("\S{3}\s{1,2}\d{1,2} \d{2}:\d{2}:\d{2}") #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), for useDns, output in (('yes', output_yes),
('no', output_no), ('no', output_no),
('warn', output_yes)): ('warn', output_yes)):
filter_ = FileFilter(None, useDns=useDns) jail = DummyJail()
filter_ = FileFilter(jail, useDns=useDns)
filter_.setActive(True) filter_.setActive(True)
filter_.failManager.setMaxRetry(1) # we might have just few failures filter_.failManager.setMaxRetry(1) # we might have just few failures
@ -916,5 +919,6 @@ class JailTests(unittest.TestCase):
def testSetBackend_gh83(self): def testSetBackend_gh83(self):
# smoke test # 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.server import Server
from fail2ban.server.jail import Jail from fail2ban.server.jail import Jail
from fail2ban.server.database import Fail2BanDb
from fail2ban.exceptions import UnknownJailException from fail2ban.exceptions import UnknownJailException
try: try:
from fail2ban.server import filtersystemd from fail2ban.server import filtersystemd
@ -168,6 +169,15 @@ class Transmitter(TransmitterBase):
# Approx 1 second delay # Approx 1 second delay
self.assertAlmostEqual(t1 - t0, 1, places=2) 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): def testAddJail(self):
jail2 = "TestJail2" jail2 = "TestJail2"
jail3 = "TestJail3" jail3 = "TestJail3"
@ -641,5 +651,5 @@ class JailTests(unittest.TestCase):
def testLongName(self): def testLongName(self):
# Just a smoke test for now # Just a smoke test for now
longname = "veryveryverylongname" longname = "veryveryverylongname"
jail = Jail(longname) jail = Jail(Fail2BanDb(":memory:"), longname)
self.assertEqual(jail.getName(), 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 actiontestcase
from fail2ban.tests import sockettestcase from fail2ban.tests import sockettestcase
from fail2ban.tests import misctestcase from fail2ban.tests import misctestcase
from fail2ban.tests import databasetestcase
if json: if json:
from fail2ban.tests import samplestestcase 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.HelpersTest))
tests.addTest(unittest.makeSuite(misctestcase.SetupTest)) tests.addTest(unittest.makeSuite(misctestcase.SetupTest))
tests.addTest(unittest.makeSuite(misctestcase.TestsUtilsTest)) tests.addTest(unittest.makeSuite(misctestcase.TestsUtilsTest))
# Database
tests.addTest(unittest.makeSuite(databasetestcase.DatabaseTest))
# Filter # Filter
if not no_network: if not no_network:

View File

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