Merge pull request #1557 from sebres/_0.10/fix-reload-bug

0.10/reload-and-more: reload without restart, stability and performance fixes
pull/1563/head
Serg G. Brester 2016-09-26 15:25:36 +02:00 committed by GitHub
commit a0d8581a2c
45 changed files with 1779 additions and 672 deletions

View File

@ -12,14 +12,22 @@ ver. 0.10.0 (2016/XX/XXX) - gonna-be-released-some-time-shining
TODO: implementing of options resp. other tasks from PR #1346
### Fixes
* [grave] memory leak's fixed (gh-1277, gh-1234)
* tricky bug fix: last position of log file will be never retrieved (gh-795),
* [Grave] memory leak's fixed (gh-1277, gh-1234)
* Tricky bug fix: last position of log file will be never retrieved (gh-795),
because of CASCADE all log entries will be deleted from logs table together with jail,
if used "INSERT OR REPLACE" statement
* asyncserver (asyncore) code fixed and test cases repaired (again gh-161)
* Asyncserver (asyncore) code fixed and test cases repaired (again gh-161)
* testSocket: sporadical bug repaired - wait for server thread starts a socket (listener)
* testExecuteTimeoutWithNastyChildren: sporadical bug repaired - wait for pid file inside bash,
kill tree in any case (gh-1155)
* Fixed high-load of pyinotify-backend,
see https://github.com/fail2ban/fail2ban/issues/885#issuecomment-248964591
* Database: stability fix - repack cursor iterator as long as locked
* File filter backends: stability fix for sporadically errors - always close file
handle, otherwise may be locked (prevent log-rotate, etc.)
* Pyinotify-backend: stability fix for sporadically errors in multi-threaded
environment (without lock)
* Fixed sporadically error in testCymruInfoNxdomain, because of unsorted values
### New Features
* IPv6 support:
@ -33,18 +41,33 @@ TODO: implementing of options resp. other tasks from PR #1346
- new conditional section functionality used in config resp. includes:
- [Init?family=inet4] - IPv4 qualified hosts only
- [Init?family=inet6] - IPv6 qualified hosts only
* New reload functionality (now totally without restart, unbanning/rebanning, etc.),
see gh-1557
* Several commands extended and new commands introduced:
- `restart [--unban] [--if-exists] <JAIL>` - restarts the jail \<JAIL\>
(alias for `reload --restart ... <JAIL>`)
- `reload [--restart] [--unban] [--all]` - reloads the configuration without restarting
of the server, the option `--restart` activates completely restarting of affected jails,
thereby can unban IP addresses (if option `--unban` specified)
- `reload [--restart] [--unban] [--if-exists] <JAIL>` - reloads the jail \<JAIL\>,
or restarts it (if option `--restart` specified), at the same time unbans all IP addresses
banned in this jail, if option `--unban` specified
- `unban --all` - unbans all IP addresses (in all jails and database)
- `unban <IP> ... <IP>` - unbans \<IP\> (in all jails and database) (see gh-1388)
* New command action parameter `actionrepair` - command executed in order to restore
sane environment in error case of `actioncheck`.
### Enhancements
* huge increasing of fail2ban performance and especially test-cases performance (see gh-1109)
* datedetector: in-place reordering using hits and last used time:
* Huge increasing of fail2ban performance and especially test-cases performance (see gh-1109)
* Datedetector: in-place reordering using hits and last used time:
matchTime, template list etc. rewritten because of performance degradation
* prevent out of memory situation if many IP's makes extremely many failures (maxEntries)
* introduced string to seconds (str2seconds) for configuration entries with time,
* Prevent out of memory situation if many IP's makes extremely many failures (maxEntries)
* Introduced string to seconds (str2seconds) for configuration entries with time,
use `1h` instead of `3600`, `1d` instead of `86400`, etc
* seekToTime - prevent completely read of big files first time (after start of service),
initial seek to start time using half-interval search algorithm (see issue gh-795)
* ticket and some other modules prepared to easy merge with newest version of 'ban-time-incr'
* cache dnsToIp, ipToName to prevent long wait during retrieving of ip/name,
* Ticket and some other modules prepared to easy merge with newest version of 'ban-time-incr'
* Cache dnsToIp, ipToName to prevent long wait during retrieving of ip/name,
especially for wrong dns or lazy dns-system
* FailManager memory-optimization: increases performance,
prevents memory leakage, because don't copy failures list on some operations
@ -54,14 +77,52 @@ TODO: implementing of options resp. other tasks from PR #1346
- `-g`, `--no-gamin` to prevent running of tests that require the gamin (slow)
- `-m`, `--memory-db` - run database tests using memory instead of file
- `-i`, `--ignore` - negate [regexps] filter to ignore tests matched specified regexps
* background servicing: prevents memory leak on some platforms/python versions, using forced GC
* Background servicing: prevents memory leak on some platforms/python versions, using forced GC
in periodic intervals (latency and threshold)
* executeCmd partially moved from action to new module utils
* several functionality of class `DNSUtils` moved to new class `IPAddr`,
* Several functionality of class `DNSUtils` moved to new class `IPAddr`,
both classes moved to new module `ipdns`
* pseudo-conditional section introduced, for conditional substitution resp.
* Pseudo-conditional section introduced, for conditional substitution resp.
evaluation of parameters for different family qualified hosts,
syntax `[Section?family=inet6]` (currently use for IPv6-support only).
* All the backends were rewritten to get reload-possibility, performance increased,
so fewer greedy regarding cpu- resp. system-load now
* Numeric log-level allowed now in server (resp. fail2ban.conf);
* Implemented better error handling in some multi-threaded routines; shutdown of jails
rewritten (faster and safer, does not breaks shutdown process if some error occurred)
* Possibility for overwriting some configuration options (read with config-readers)
with command line option, e. g.:
```bash
## start server with DEBUG log-level (ignore level read from fail2ban.conf):
fail2ban-client --loglevel DEBUG start
## or
fail2ban-server -c /cfg/path --loglevel DEBUG start
## keep server log-level by reload (without restart it)
fail2ban-client --loglevel DEBUG reload
## switch log-level back to INFO:
fail2ban-client set loglevel INFO
```
* Optimized BanManager: increase performance, fewer system load, try to prevent
memory leakage:
- better ban/unban handling within actions (e.g. used dict instead of list)
- don't copy bans resp. its list on some operations;
- added new unbantime handling to relieve unBanList (prevent permanent
searching for tickets to unban)
- prefer failure-ID as identifier of the ticket to its IP (most of the time
the same, but it can be something else e.g. user name in some complex jails,
as introduced in 0.10)
* Regexp enhancements:
- build replacement of `<HOST>` substitution corresponding parameter
`usedns` - dns-part will be added only if `usedns` is not `no`,
also using fail2ban-regex
- new replacement for `<ADDR>` in opposition to `<HOST>`, for separate
usage of 2 address groups only (regardless of `usedns`), `ip4` and `ip6`
together, without host (dns)
* fail2ban-testcases:
- `assertLogged` extended with parameter wait (to wait up to specified timeout,
before we throw assert exception) + test cases rewritten using that
- added `assertDictEqual` for compatibility to early python versions (< 2.7);
- new `with_foreground_server_thread` decorator to test several client/server commands
ver. 0.9.6 (2016/XX/XX) - wanna-be-released

View File

@ -26,8 +26,11 @@ __license__ = "GPL"
import logging.handlers
# Custom debug level
# Custom debug levels
logging.TRACEDEBUG = 7
logging.HEAVYDEBUG = 5
logging.addLevelName(logging.TRACEDEBUG, 'TRACE')
logging.addLevelName(logging.HEAVYDEBUG, 'HEAVY')
"""
Below derived from:

View File

@ -38,7 +38,9 @@ class ActionReader(DefinitionInitConfigReader):
_configOpts = {
"actionstart": ["string", None],
"actionstop": ["string", None],
"actionreload": ["string", None],
"actioncheck": ["string", None],
"actionrepair": ["string", None],
"actionban": ["string", None],
"actionunban": ["string", None],
}

View File

@ -46,7 +46,7 @@ class Beautifier:
return self.__inputCmd
def beautify(self, response):
logSys.debug(
logSys.log(5,
"Beautify " + repr(response) + " with " + repr(self.__inputCmd))
inC = self.__inputCmd
msg = response

View File

@ -72,8 +72,8 @@ class Configurator:
def getEarlyOptions(self):
return self.__fail2ban.getEarlyOptions()
def getOptions(self, jail = None):
self.__fail2ban.getOptions()
def getOptions(self, jail=None, updateMainOpt=None):
self.__fail2ban.getOptions(updateMainOpt)
return self.__jails.getOptions(jail)
def convertToProtocol(self):

View File

@ -91,7 +91,7 @@ class Fail2banClient(Fail2banCmdLine, Thread):
client = CSocket(self._conf["socket"])
ret = client.send(c)
if ret[0] == 0:
logSys.debug("OK : %r", ret[1])
logSys.log(5, "OK : %r", ret[1])
if showRet or c[0] == 'echo':
output(beautifier.beautify(ret[1]))
else:
@ -104,7 +104,7 @@ class Fail2banClient(Fail2banCmdLine, Thread):
if showRet or c != ["ping"]:
self.__logSocketError()
else:
logSys.debug(" -- ping failed -- %r", e)
logSys.log(5, " -- ping failed -- %r", e)
return False
except Exception as e: # pragma: no cover
if showRet or self._conf["verbose"] > 1:
@ -226,11 +226,11 @@ class Fail2banClient(Fail2banCmdLine, Thread):
# prepare: read config, check configuration is valid, etc.:
if phase is not None:
phase['start'] = True
logSys.debug(' client phase %s', phase)
logSys.log(5, ' client phase %s', phase)
stream = self.__prepareStartServer()
if phase is not None:
phase['ready'] = phase['start'] = (True if stream else False)
logSys.debug(' client phase %s', phase)
logSys.log(5, ' client phase %s', phase)
if not stream:
return False
# configure server with config stream:
@ -246,6 +246,10 @@ class Fail2banClient(Fail2banCmdLine, Thread):
# @param cmd the command line
def __processCommand(self, cmd):
# wrap tuple to list (because could be modified here):
if not isinstance(cmd, list):
cmd = list(cmd)
# process:
if len(cmd) == 1 and cmd[0] == "start":
ret = self.__startServer(self._conf["background"])
@ -253,8 +257,12 @@ class Fail2banClient(Fail2banCmdLine, Thread):
return False
return ret
elif len(cmd) == 1 and cmd[0] == "restart":
elif len(cmd) >= 1 and cmd[0] == "restart":
# if restart jail - re-operate via "reload --restart ...":
if len(cmd) > 1:
cmd[0:1] = ["reload", "--restart"]
return self.__processCommand(cmd)
# restart server:
if self._conf.get("interactive", False):
output(' ## stop ... ')
self.__processCommand(['stop'])
@ -273,9 +281,21 @@ class Fail2banClient(Fail2banCmdLine, Thread):
return self.__processCommand(['start'])
elif len(cmd) >= 1 and cmd[0] == "reload":
# reload options:
opts = []
while len(cmd) >= 2:
if cmd[1] in ('--restart', "--unban", "--if-exists"):
opts.append(cmd[1])
del cmd[1]
else:
if len(cmd) > 2:
logSys.error("Unexpected argument(s) for reload: %r", cmd[1:])
return False
# stop options - jail name or --all
break
if self.__ping():
if len(cmd) == 1:
jail = 'all'
jail = '--all'
ret, stream = self.readConfig()
else:
jail = cmd[1]
@ -283,9 +303,10 @@ class Fail2banClient(Fail2banCmdLine, Thread):
# Do not continue if configuration is not 100% valid
if not ret:
return False
self.__processCmd([['stop', jail]], False)
# Configure the server
return self.__processCmd(stream, True)
if self._conf.get("interactive", False):
output(' ## reload ... ')
# Reconfigure the server
return self.__processCmd([['reload', jail, opts, stream]], True)
else:
logSys.error("Could not find server")
return False
@ -320,7 +341,7 @@ class Fail2banClient(Fail2banCmdLine, Thread):
maxtime = self._conf["timeout"]
# Wait for the server to start (the server has 30 seconds to answer ping)
starttime = time.time()
logSys.debug("__waitOnServer: %r", (alive, maxtime))
logSys.log(5, "__waitOnServer: %r", (alive, maxtime))
test = lambda: os.path.exists(self._conf["socket"]) and self.__ping()
with VisualWait(self._conf["verbose"]) as vis:
sltime = 0.0125 / 2

View File

@ -242,7 +242,7 @@ class Fail2banCmdLine():
try:
self.configurator.Reload()
self.configurator.readAll()
ret = self.configurator.getOptions(jail)
ret = self.configurator.getOptions(jail, self._conf)
self.configurator.convertToProtocol()
stream = self.configurator.getConfigStream()
except Exception as e:

View File

@ -25,7 +25,7 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
from .configreader import ConfigReader
from ..helpers import getLogger
from ..helpers import getLogger, str2LogLevel
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -49,13 +49,17 @@ class Fail2banReader(ConfigReader):
]
return ConfigReader.getOptions(self, "Definition", opts)
def getOptions(self):
def getOptions(self, updateMainOpt=None):
opts = [["string", "loglevel", "INFO" ],
["string", "logtarget", "STDERR"],
["string", "syslogsocket", "auto"],
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
["string", "dbpurgeage", "1d"]]
self.__opts = ConfigReader.getOptions(self, "Definition", opts)
if updateMainOpt:
self.__opts.update(updateMainOpt)
# check given log-level:
str2LogLevel(self.__opts.get('loglevel', 0))
def convert(self):
# Ensure logtarget/level set first so any db errors are captured

View File

@ -29,7 +29,6 @@ __copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halch
__license__ = "GPL"
import getopt
import locale
import logging
import os
import shlex
@ -52,7 +51,7 @@ from .filterreader import FilterReader
from ..server.filter import Filter, FileContainer
from ..server.failregex import RegexException
from ..helpers import FormatterWithTraceBack, getLogger
from ..helpers import FormatterWithTraceBack, getLogger, PREFER_ENC
# Gets the instance of the logger.
logSys = getLogger("fail2ban")
@ -127,6 +126,9 @@ Report bugs to https://github.com/fail2ban/fail2ban/issues
help="File encoding. Default: system locale"),
Option("-r", "--raw", action='store_true',
help="Raw hosts, don't resolve dns"),
Option("--usedns", action='store', default=None,
help="DNS specified replacement of tags <HOST> in regexp "
"('yes' - matches all form of hosts, 'no' - IP addresses only)"),
Option("-L", "--maxlines", type=int, default=0,
help="maxlines for multi-line regex"),
Option("-m", "--journalmatch",
@ -239,8 +241,10 @@ class Fail2banRegex(object):
if opts.encoding:
self.encoding = opts.encoding
else:
self.encoding = locale.getpreferredencoding()
self.encoding = PREFER_ENC
self.raw = True if opts.raw else False
if opts.usedns:
self._filter.setUseDns(opts.usedns)
def decode_line(self, line):
return FileContainer.decode_line('<LOG>', self.encoding, line)

View File

@ -106,7 +106,7 @@ class JailReader(ConfigReader):
["int", "maxretry", None],
["string", "findtime", None],
["string", "bantime", None],
["string", "usedns", None],
["string", "usedns", None], # be sure usedns is before all regex(s) in stream
["string", "failregex", None],
["string", "ignoreregex", None],
["string", "ignorecommand", None],

View File

@ -21,6 +21,7 @@ __author__ = "Cyril Jaquier, Arturo 'Buanzo' Busleiman, Yaroslav Halchenko"
__license__ = "GPL"
import gc
import locale
import logging
import os
import re
@ -32,6 +33,9 @@ from threading import Lock
from .server.mytime import MyTime
PREFER_ENC = locale.getpreferredencoding()
def formatExceptionInfo():
""" Consistently format exception information """
cla, exc = sys.exc_info()[:2]
@ -125,6 +129,16 @@ def getLogger(name):
name = "fail2ban.%s" % name.rpartition(".")[-1]
return logging.getLogger(name)
def str2LogLevel(value):
try:
if isinstance(value, int) or value.isdigit():
ll = int(value)
else:
ll = getattr(logging, value)
except AttributeError:
raise ValueError("Invalid log level %r" % value)
return ll
def excepthook(exctype, value, traceback):
"""Except hook used to log unhandled exceptions to Fail2Ban log
@ -144,6 +158,36 @@ def splitwords(s):
return filter(bool, map(str.strip, re.split('[ ,\n]+', s)))
#
# Following "uni_decode" function unified python independent any to string converting
#
# Typical example resp. work-case for understanding the coding/decoding issues:
#
# [isinstance('', str), isinstance(b'', str), isinstance(u'', str)]
# [True, True, False]; # -- python2
# [True, False, True]; # -- python3
#
if sys.version_info >= (3,):
def uni_decode(x, enc=PREFER_ENC, errors='strict'):
try:
if isinstance(x, bytes):
return x.decode(enc, errors)
return x
except (UnicodeDecodeError, UnicodeEncodeError): # pragma: no cover - unsure if reachable
if errors != 'strict':
raise
return uni_decode(x, enc, 'replace')
else:
def uni_decode(x, enc=PREFER_ENC, errors='strict'):
try:
if isinstance(x, unicode):
return x.encode(enc, errors)
return x
except (UnicodeDecodeError, UnicodeEncodeError): # pragma: no cover - unsure if reachable
if errors != 'strict':
raise
return uni_decode(x, enc, 'replace')
class BgService(object):
"""Background servicing

View File

@ -49,16 +49,20 @@ protocol = [
['', "BASIC", ""],
["start", "starts the server and the jails"],
["restart", "restarts the server"],
["reload", "reloads the configuration without restart"],
["reload <JAIL>", "reloads the jail <JAIL>"],
["restart [--unban] [--if-exists] <JAIL>", "restarts the jail <JAIL> (alias for 'reload --restart ... <JAIL>')"],
["reload [--restart] [--unban] [--all]", "reloads the configuration without restarting of the server, the option '--restart' activates completely restarting of affected jails, thereby can unban IP addresses (if option '--unban' specified)"],
["reload [--restart] [--unban] [--if-exists] <JAIL>", "reloads the jail <JAIL>, or restarts it (if option '--restart' specified)"],
["stop", "stops all jails and terminate the server"],
["unban --all", "unbans all IP addresses (in all jails and database)"],
["unban <IP> ... <IP>", "unbans <IP> (in all jails and database)"],
["status", "gets the current status of the server"],
["ping", "tests if the server is alive"],
["echo", "for internal usage, returns back and outputs a given string"],
["help", "return this output"],
["version", "return the server version"],
['', "LOGGING", ""],
["set loglevel <LEVEL>", "sets logging level to <LEVEL>. Levels: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG"],
["set loglevel <LEVEL>", "sets logging level to <LEVEL>. Levels: CRITICAL, ERROR, WARNING, NOTICE, INFO, "
"DEBUG, TRACEDEBUG, HEAVYDEBUG or corresponding numeric value (50-5)"],
["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"],

View File

@ -200,6 +200,9 @@ class CommandAction(ActionBase):
Attributes
----------
actionban
actioncheck
actionreload
actionrepair
actionstart
actionstop
actionunban
@ -208,22 +211,35 @@ class CommandAction(ActionBase):
_escapedTags = set(('matches', 'ipmatches', 'ipjailmatches'))
timeout = 60
## Command executed in order to initialize the system.
actionstart = ''
## Command executed when an IP address gets banned.
actionban = ''
## Command executed when an IP address gets removed.
actionunban = ''
## Command executed in order to check requirements.
actioncheck = ''
## Command executed in order to stop the system.
actionstop = ''
def clearAllParams(self):
""" Clear all lists/dicts parameters (used by reloading)
"""
self.__init = 1
try:
self.timeout = 60
## Command executed in order to initialize the system.
self.actionstart = ''
## Command executed when an IP address gets banned.
self.actionban = ''
## Command executed when an IP address gets removed.
self.actionunban = ''
## Command executed in order to check requirements.
self.actioncheck = ''
## Command executed in order to restore sane environment in error case.
self.actionrepair = ''
## Command executed in order to stop the system.
self.actionstop = ''
## Command executed in case of reloading action.
self.actionreload = ''
finally:
self.__init = 0
def __init__(self, jail, name):
super(CommandAction, self).__init__(jail, name)
self.__init = 1
self.__properties = None
self.__substCache = {}
self.clearAllParams()
self._logSys.debug("Created %s" % self.__class__)
@classmethod
@ -231,7 +247,7 @@ class CommandAction(ActionBase):
return NotImplemented # Standard checks
def __setattr__(self, name, value):
if not name.startswith('_') and not callable(value):
if not name.startswith('_') and not self.__init and not callable(value):
# special case for some pasrameters:
if name in ('timeout', 'bantime'):
value = str(MyTime.str2seconds(value))
@ -264,28 +280,38 @@ class CommandAction(ActionBase):
def _substCache(self):
return self.__substCache
def _executeOperation(self, tag, operation):
"""Executes the operation commands (like "actionstart", "actionstop", etc).
Replace the tags in the action command with actions properties
and executes the resulting command.
"""
# check valid tags in properties (raises ValueError if self recursion, etc.):
res = True
try:
# common (resp. ipv4):
startCmd = self.replaceTag(tag, self._properties,
conditional='family=inet4', cache=self.__substCache)
if startCmd:
res &= self.executeCmd(startCmd, self.timeout)
# start ipv6 actions if available:
if allowed_ipv6:
startCmd6 = self.replaceTag(tag, self._properties,
conditional='family=inet6', cache=self.__substCache)
if startCmd6 and startCmd6 != startCmd:
res &= self.executeCmd(startCmd6, self.timeout)
if not res:
raise RuntimeError("Error %s action %s/%s" % (operation, self._jail, self._name,))
except ValueError as e:
raise RuntimeError("Error %s action %s/%s: %r" % (operation, self._jail, self._name, e))
def start(self):
"""Executes the "actionstart" command.
Replace the tags in the action command with actions properties
and executes the resulting command.
"""
# check valid tags in properties (raises ValueError if self recursion, etc.):
try:
# common (resp. ipv4):
startCmd = self.replaceTag('<actionstart>', self._properties,
conditional='family=inet4', cache=self.__substCache)
res = self.executeCmd(startCmd, self.timeout)
# start ipv6 actions if available:
if allowed_ipv6:
startCmd6 = self.replaceTag('<actionstart>', self._properties,
conditional='family=inet6', cache=self.__substCache)
if startCmd6 != startCmd:
res &= self.executeCmd(startCmd6, self.timeout)
if not res:
raise RuntimeError("Error starting action %s/%s" % (self._jail, self._name,))
except ValueError as e:
raise RuntimeError("Error starting action %s/%s: %r" % (self._jail, self._name, e))
return self._executeOperation('<actionstart>', 'starting')
def ban(self, aInfo):
"""Executes the "actionban" command.
@ -323,18 +349,20 @@ class CommandAction(ActionBase):
Replaces the tags in the action command with actions properties
and executes the resulting command.
"""
# common (resp. ipv4):
stopCmd = self.replaceTag('<actionstop>', self._properties,
conditional='family=inet4', cache=self.__substCache)
res = self.executeCmd(stopCmd, self.timeout)
# ipv6 actions if available:
if allowed_ipv6:
stopCmd6 = self.replaceTag('<actionstop>', self._properties,
conditional='family=inet6', cache=self.__substCache)
if stopCmd6 != stopCmd:
res &= self.executeCmd(stopCmd6, self.timeout)
if not res:
raise RuntimeError("Error stopping action")
return self._executeOperation('<actionstop>', 'stopping')
def reload(self, **kwargs):
"""Executes the "actionreload" command.
Parameters
----------
kwargs : dict
Currently unused, because CommandAction do not support initOpts
Replaces the tags in the action command with actions properties
and executes the resulting command.
"""
return self._executeOperation('<actionreload>', 'reloading')
@classmethod
def substituteRecursiveTags(cls, inptags, conditional=''):
@ -520,14 +548,28 @@ class CommandAction(ActionBase):
checkCmd = self.replaceTag('<actioncheck>', self._properties,
conditional=conditional, cache=self.__substCache)
if not self.executeCmd(checkCmd, self.timeout):
self._logSys.error(
"Invariant check failed. Trying to restore a sane environment")
self.stop()
self.start()
if checkCmd:
if not self.executeCmd(checkCmd, self.timeout):
self._logSys.critical("Unable to restore environment")
return False
self._logSys.error(
"Invariant check failed. Trying to restore a sane environment")
# try to find repair command, if exists - exec it:
repairCmd = self.replaceTag('<actionrepair>', self._properties,
conditional=conditional, cache=self.__substCache)
if repairCmd:
if not self.executeCmd(repairCmd, self.timeout):
self._logSys.critical("Unable to restore environment")
return False
else:
# no repair command, try to restart action...
# [WARNING] TODO: be sure all banactions get a repair command, because
# otherwise stop/start will theoretically remove all the bans,
# but the tickets are still in BanManager, so in case of new failures
# it will not be banned, because "already banned" will happen.
self.stop()
self.start()
if not self.executeCmd(checkCmd, self.timeout):
self._logSys.critical("Unable to restore environment")
return False
# Replace static fields
realCmd = self.replaceTag(cmd, self._properties,

View File

@ -36,7 +36,7 @@ from collections import Mapping
try:
from collections import OrderedDict
except ImportError:
OrderedDict = None
OrderedDict = dict
from .banmanager import BanManager
from .jailthread import JailThread
@ -81,14 +81,11 @@ class Actions(JailThread, Mapping):
JailThread.__init__(self)
## The jail which contains this action.
self._jail = jail
if OrderedDict is not None:
self._actions = OrderedDict()
else:
self._actions = dict()
self._actions = OrderedDict()
## The ban manager.
self.__banManager = BanManager()
def add(self, name, pythonModule=None, initOpts=None):
def add(self, name, pythonModule=None, initOpts=None, reload=False):
"""Adds a new action.
Add a new action if not already present, defaulting to standard
@ -116,7 +113,17 @@ class Actions(JailThread, Mapping):
"""
# Check is action name already exists
if name in self._actions:
raise ValueError("Action %s already exists" % name)
if not reload:
raise ValueError("Action %s already exists" % name)
# don't create new action if reload supported:
action = self._actions[name]
if hasattr(action, 'reload'):
# don't execute reload right now, reload after all parameters are actualized
if hasattr(action, 'clearAllParams'):
action.clearAllParams()
self._reload_actions[name] = initOpts
return
## Create new action:
if pythonModule is None:
action = CommandAction(self._jail, name)
else:
@ -138,6 +145,27 @@ class Actions(JailThread, Mapping):
action = customActionModule.Action(self._jail, name, **initOpts)
self._actions[name] = action
def reload(self, begin=True):
""" Begin or end of reloading resp. refreshing of all parameters
"""
if begin:
self._reload_actions = dict()
else:
if hasattr(self, '_reload_actions'):
# reload actions after all parameters set via stream:
for name, initOpts in self._reload_actions.iteritems():
if name in self._actions:
self._actions[name].reload(**initOpts if initOpts else {})
# remove obsolete actions (untouched by reload process):
delacts = OrderedDict((name, action) for name, action in self._actions.iteritems()
if name not in self._reload_actions)
if len(delacts):
# unban all tickets using remove action only:
self.__flushBan(db=False, actions=delacts)
# stop and remove it:
self.stopActions(actions=delacts)
delattr(self, '_reload_actions')
def __getitem__(self, name):
try:
return self._actions[name]
@ -180,7 +208,7 @@ class Actions(JailThread, Mapping):
def getBanTime(self):
return self.__banManager.getBanTime()
def removeBannedIP(self, ip):
def removeBannedIP(self, ip=None, db=True, ifexists=False):
"""Removes banned IP calling actions' unban method
Remove a banned IP now, rather than waiting for it to expire,
@ -188,24 +216,50 @@ class Actions(JailThread, Mapping):
Parameters
----------
ip : str or IPAddr
The IP address to unban
ip : str or IPAddr or None
The IP address to unban or all IPs if None
Raises
------
ValueError
If `ip` is not banned
"""
# Unban all?
if ip is None:
return self.__flushBan(db)
# Single IP:
# Always delete ip from database (also if currently not banned)
if self._jail.database is not None:
if db and self._jail.database is not None:
self._jail.database.delBan(self._jail, ip)
# Find the ticket with the IP.
ticket = self.__banManager.getTicketByIP(ip)
ticket = self.__banManager.getTicketByID(ip)
if ticket is not None:
# Unban the IP.
self.__unBan(ticket)
else:
raise ValueError("IP %s is not banned" % ip)
if ifexists:
return 0
raise ValueError("%s is not banned" % ip)
return 1
def stopActions(self, actions=None):
"""Stops the actions in reverse sequence (optionally filtered)
"""
if actions is None:
actions = self._actions
revactions = actions.items()
revactions.reverse()
for name, action in revactions:
try:
action.stop()
except Exception as e:
logSys.error("Failed to stop jail '%s' action '%s': %s",
self._jail.name, name, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
del self._actions[name]
logSys.debug("%s: action %s terminated", self._jail.name, name)
def run(self):
"""Main loop for Threading.
@ -227,22 +281,14 @@ class Actions(JailThread, Mapping):
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
while self.active:
if self.idle:
time.sleep(self.sleeptime)
Utils.wait_for(lambda: not self.active or not self.idle,
self.sleeptime * 10, self.sleeptime)
continue
if not Utils.wait_for(self.__checkBan, self.sleeptime):
if not Utils.wait_for(lambda: not self.active or self.__checkBan(), self.sleeptime):
self.__checkUnBan()
self.__flushBan()
actions = self._actions.items()
actions.reverse()
for name, action in actions:
try:
action.stop()
except Exception as e:
logSys.error("Failed to stop jail '%s' action '%s': %s",
self._jail.name, name, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
logSys.debug(self._jail.name + ": action terminated")
self.stopActions()
return True
def __getBansMerged(self, mi, overalljails=False):
@ -295,8 +341,11 @@ class Actions(JailThread, Mapping):
bool
True if an IP address get banned.
"""
ticket = self._jail.getFailTicket()
if ticket:
cnt = 0
while cnt < 100:
ticket = self._jail.getFailTicket()
if not ticket:
break
aInfo = CallingMap()
bTicket = BanManager.createBanTicket(ticket)
ip = bTicket.getIP()
@ -311,8 +360,10 @@ class Actions(JailThread, Mapping):
aInfo["ipjailmatches"] = lambda: "\n".join(mi4ip().getMatches())
aInfo["ipfailures"] = lambda: mi4ip(True).getAttempt()
aInfo["ipjailfailures"] = lambda: mi4ip().getAttempt()
if self.__banManager.addBanTicket(bTicket):
logSys.notice("[%s] Ban %s" % (self._jail.name, aInfo["ip"]))
reason = {}
if self.__banManager.addBanTicket(bTicket, reason=reason):
cnt += 1
logSys.notice("[%s] %sBan %s", self._jail.name, ('' if not bTicket.restored else 'Restore '), ip)
for name, action in self._actions.iteritems():
try:
action.ban(aInfo.copy())
@ -322,30 +373,68 @@ class Actions(JailThread, Mapping):
"info '%r': %s",
self._jail.name, name, aInfo, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
return True
# after all actions are processed set banned flag:
bTicket.banned = True
else:
logSys.notice("[%s] %s already banned" % (self._jail.name,
aInfo["ip"]))
return False
bTicket = reason['ticket']
# if already banned (otherwise still process some action)
if bTicket.banned:
# compare time of failure occurrence with time ticket was really banned:
diftm = ticket.getTime() - bTicket.getTime()
# log already banned with following level:
# DEBUG - before 3 seconds - certain interval for it, because of possible latency by recognizing in backends, etc.
# NOTICE - before 60 seconds - may still occurre if action are slow, or very high load in backend,
# WARNING - after 60 seconds - very long time, something may be wrong
ll = logging.DEBUG if diftm < 3 \
else logging.NOTICE if diftm < 60 \
else logging.WARNING
logSys.log(ll, "[%s] %s already banned", self._jail.name, ip)
if cnt:
logSys.debug("Banned %s / %s, %s ticket(s) in %r", cnt,
self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name)
return cnt
def __checkUnBan(self):
"""Check for IP address to unban.
Unban IP addresses which are outdated.
"""
for ticket in self.__banManager.unBanList(MyTime.time()):
lst = self.__banManager.unBanList(MyTime.time())
for ticket in lst:
self.__unBan(ticket)
cnt = len(lst)
if cnt:
logSys.debug("Unbanned %s, %s ticket(s) in %r",
cnt, self.__banManager.size(), self._jail.name)
return cnt
def __flushBan(self):
def __flushBan(self, db=False, actions=None):
"""Flush the ban list.
Unban all IP address which are still in the banning list.
"""
logSys.debug("Flush ban list")
for ticket in self.__banManager.flushBanList():
self.__unBan(ticket)
def __unBan(self, ticket):
If actions specified, don't flush list - just execute unban for
given actions (reload, obsolete resp. removed actions).
"""
if actions is None:
logSys.debug("Flush ban list")
lst = self.__banManager.flushBanList()
else:
lst = iter(self.__banManager)
cnt = 0
for ticket in lst:
# delete ip from database also:
if db and self._jail.database is not None:
ip = str(ticket.getIP())
self._jail.database.delBan(self._jail, ip)
# unban ip:
self.__unBan(ticket, actions=actions)
cnt += 1
logSys.debug("Unbanned %s, %s ticket(s) in %r",
cnt, self.__banManager.size(), self._jail.name)
return cnt
def __unBan(self, ticket, actions=None):
"""Unbans host corresponding to the ticket.
Executes the actions in order to unban the host given in the
@ -356,14 +445,20 @@ class Actions(JailThread, Mapping):
ticket : FailTicket
Ticket of failures of which to unban
"""
if actions is None:
unbactions = self._actions
else:
unbactions = actions
aInfo = dict()
aInfo["ip"] = ticket.getIP()
aInfo["failures"] = ticket.getAttempt()
aInfo["time"] = ticket.getTime()
aInfo["matches"] = "".join(ticket.getMatches())
logSys.notice("[%s] Unban %s" % (self._jail.name, aInfo["ip"]))
for name, action in self._actions.iteritems():
if actions is None:
logSys.notice("[%s] Unban %s", self._jail.name, aInfo["ip"])
for name, action in unbactions.iteritems():
try:
logSys.debug("[%s] action %r: unban %s", self._jail.name, name, aInfo["ip"])
action.unban(aInfo.copy())
except Exception as e:
logSys.error(

View File

@ -67,23 +67,28 @@ class RequestHandler(asynchat.async_chat):
# This method is called once we have a complete request.
def found_terminator(self):
# Pop whole buffer
message = self.__buffer
self.__buffer = []
# Joins the buffer items.
message = CSPROTO.EMPTY.join(message)
# Closes the channel if close was received
if message == CSPROTO.CLOSE:
self.close_when_done()
return
# Deserialize
message = loads(message)
# Gives the message to the transmitter.
message = self.__transmitter.proceed(message)
# Serializes the response.
message = dumps(message, HIGHEST_PROTOCOL)
# Sends the response to the client.
self.push(message + CSPROTO.END)
try:
# Pop whole buffer
message = self.__buffer
self.__buffer = []
# Joins the buffer items.
message = CSPROTO.EMPTY.join(message)
# Closes the channel if close was received
if message == CSPROTO.CLOSE:
self.close_when_done()
return
# Deserialize
message = loads(message)
# Gives the message to the transmitter.
message = self.__transmitter.proceed(message)
# Serializes the response.
message = dumps(message, HIGHEST_PROTOCOL)
# Sends the response to the client.
self.push(message + CSPROTO.END)
except Exception as e: # pragma: no cover
logSys.error("Caught unhandled exception: %r", e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
def handle_error(self):
e1, e2 = formatExceptionInfo()
@ -199,6 +204,7 @@ class AsyncServer(asyncore.dispatcher):
def close(self):
stopflg = False
if self.__active:
self.__loop = False
asyncore.dispatcher.close(self)
@ -206,11 +212,13 @@ class AsyncServer(asyncore.dispatcher):
# for the server leaves loop, before remove socket
if threading.current_thread() != self.__worker:
Utils.wait_for(lambda: not self.__active, 1)
stopflg = True
# Remove socket (file) only if it was created:
if self.__init and os.path.exists(self.__sock):
self._remove_sock()
logSys.debug("Removed socket file " + self.__sock)
logSys.debug("Socket shutdown")
if stopflg:
logSys.debug("Socket shutdown")
self.__active = False
##

View File

@ -51,11 +51,13 @@ class BanManager:
## Mutex used to protect the ban list.
self.__lock = Lock()
## The ban list.
self.__banList = list()
self.__banList = dict()
## The amount of time an IP address gets banned.
self.__banTime = 600
## Total number of banned IP address
self.__banTotal = 0
## The time for next unban process (for performance and load reasons):
self.__nextUnbanTime = BanTicket.MAX_TIME
##
# Set the ban time.
@ -64,11 +66,8 @@ class BanManager:
# @param value the time
def setBanTime(self, value):
try:
self.__lock.acquire()
with self.__lock:
self.__banTime = int(value)
finally:
self.__lock.release()
##
# Get the ban time.
@ -77,11 +76,8 @@ class BanManager:
# @return the time
def getBanTime(self):
try:
self.__lock.acquire()
with self.__lock:
return self.__banTime
finally:
self.__lock.release()
##
# Set the total number of banned address.
@ -89,11 +85,8 @@ class BanManager:
# @param value total number
def setBanTotal(self, value):
try:
self.__lock.acquire()
with self.__lock:
self.__banTotal = value
finally:
self.__lock.release()
##
# Get the total number of banned address.
@ -101,11 +94,8 @@ class BanManager:
# @return the total number
def getBanTotal(self):
try:
self.__lock.acquire()
with self.__lock:
return self.__banTotal
finally:
self.__lock.release()
##
# Returns a copy of the IP list.
@ -113,11 +103,17 @@ class BanManager:
# @return IP list
def getBanList(self):
try:
self.__lock.acquire()
return [m.getIP() for m in self.__banList]
finally:
self.__lock.release()
with self.__lock:
return self.__banList.keys()
##
# Returns a iterator to ban list (used in reload, so idle).
#
# @return ban list iterator
def __iter__(self):
with self.__lock:
return self.__banList.itervalues()
##
# Returns normalized value
@ -149,7 +145,7 @@ class BanManager:
return return_dict
self.__lock.acquire()
try:
for banData in self.__banList:
for banData in self.__banList.values():
ip = banData.getIP()
# Reference: http://www.team-cymru.org/Services/ip-to-asn.html#dns
question = ip.getPTR(
@ -260,30 +256,33 @@ class BanManager:
# @param ticket the ticket
# @return True if the IP address is not in the ban list
def addBanTicket(self, ticket):
try:
self.__lock.acquire()
def addBanTicket(self, ticket, reason={}):
eob = ticket.getEndOfBanTime(self.__banTime)
with self.__lock:
# check already banned
for oldticket in self.__banList:
if ticket.getIP() == oldticket.getIP():
# if already permanent
btold, told = oldticket.getBanTime(self.__banTime), oldticket.getTime()
if btold == -1:
return False
# if given time is less than already banned time
btnew, tnew = ticket.getBanTime(self.__banTime), ticket.getTime()
if btnew != -1 and tnew + btnew <= told + btold:
return False
fid = ticket.getID()
oldticket = self.__banList.get(fid)
if oldticket:
reason['ticket'] = oldticket
# if new time for end of ban is larger than already banned end-time:
if eob > oldticket.getEndOfBanTime(self.__banTime):
# we have longest ban - set new (increment) ban time
oldticket.setTime(tnew)
oldticket.setBanTime(btnew)
return False
# not yet banned - add new
self.__banList.append(ticket)
reason['prolong'] = 1
btm = ticket.getBanTime(self.__banTime)
# if not permanent:
if btm != -1:
diftm = ticket.getTime() - oldticket.getTime()
if diftm > 0:
btm += diftm
oldticket.setBanTime(btm)
return False
# not yet banned - add new one:
self.__banList[fid] = ticket
self.__banTotal += 1
# correct next unban time:
if self.__nextUnbanTime > eob:
self.__nextUnbanTime = eob
return True
finally:
self.__lock.release()
##
# Get the size of the ban list.
@ -291,11 +290,7 @@ class BanManager:
# @return the size
def size(self):
try:
self.__lock.acquire()
return len(self.__banList)
finally:
self.__lock.release()
return len(self.__banList)
##
# Check if a ticket is in the list.
@ -306,10 +301,7 @@ class BanManager:
# @return True if a ticket already exists
def _inBanList(self, ticket):
for i in self.__banList:
if ticket.getIP() == i.getIP():
return True
return False
return ticket.getID() in self.__banList
##
# Get the list of IP address to unban.
@ -319,22 +311,39 @@ class BanManager:
# @return the list of ticket to unban
def unBanList(self, time):
try:
self.__lock.acquire()
with self.__lock:
# Permanent banning
if self.__banTime < 0:
return list()
# Gets the list of ticket to remove.
unBanList = [ticket for ticket in self.__banList if ticket.isTimedOut(time, self.__banTime)]
# Check next unban time:
if self.__nextUnbanTime > time:
return list()
# Gets the list of ticket to remove (thereby correct next unban time).
unBanList = {}
self.__nextUnbanTime = BanTicket.MAX_TIME
for fid,ticket in self.__banList.iteritems():
# current time greater as end of ban - timed out:
eob = ticket.getEndOfBanTime(self.__banTime)
if time > eob:
unBanList[fid] = ticket
elif self.__nextUnbanTime > eob:
self.__nextUnbanTime = eob
# Removes tickets.
self.__banList = [ticket for ticket in self.__banList
if ticket not in unBanList]
if len(unBanList):
if len(unBanList) / 2.0 <= len(self.__banList) / 3.0:
# few as 2/3 should be removed - remove particular items:
for fid in unBanList.iterkeys():
del self.__banList[fid]
else:
# create new dictionary without items to be deleted:
self.__banList = dict((fid,ticket) for fid,ticket in self.__banList.iteritems() \
if fid not in unBanList)
return unBanList
finally:
self.__lock.release()
# return list of tickets:
return unBanList.values()
##
# Flush the ban list.
@ -343,28 +352,21 @@ class BanManager:
# @return the complete ban list
def flushBanList(self):
try:
self.__lock.acquire()
uBList = self.__banList
self.__banList = list()
with self.__lock:
uBList = self.__banList.values()
self.__banList = dict()
return uBList
finally:
self.__lock.release()
##
# Gets the ticket for the specified IP.
# Gets the ticket for the specified ID (most of the time it is IP-address).
#
# @return the ticket for the IP or False.
def getTicketByIP(self, ip):
try:
self.__lock.acquire()
# Find the ticket the IP goes with and return it
for i, ticket in enumerate(self.__banList):
if ticket.getIP() == ip:
# Return the ticket after removing (popping)
# if from the ban list.
return self.__banList.pop(i)
finally:
self.__lock.release()
# @return the ticket or False.
def getTicketByID(self, fid):
with self.__lock:
try:
# Return the ticket after removing (popping)
# if from the ban list.
return self.__banList.pop(fid)
except KeyError:
pass
return None # if none found

View File

@ -22,7 +22,6 @@ __copyright__ = "Copyright (c) 2013 Steven Hiscocks"
__license__ = "GPL"
import json
import locale
import shutil
import sqlite3
import sys
@ -32,7 +31,7 @@ from threading import RLock
from .mytime import MyTime
from .ticket import FailTicket
from ..helpers import getLogger
from ..helpers import getLogger, PREFER_ENC
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -41,7 +40,7 @@ if sys.version_info >= (3,):
def _json_dumps_safe(x):
try:
x = json.dumps(x, ensure_ascii=False).encode(
locale.getpreferredencoding(), 'replace')
PREFER_ENC, 'replace')
except Exception as e: # pragma: no cover
logSys.error('json dumps failed: %s', e)
x = '{}'
@ -50,7 +49,7 @@ if sys.version_info >= (3,):
def _json_loads_safe(x):
try:
x = json.loads(x.decode(
locale.getpreferredencoding(), 'replace'))
PREFER_ENC, 'replace'))
except Exception as e: # pragma: no cover
logSys.error('json loads failed: %s', e)
x = {}
@ -62,14 +61,14 @@ else:
elif isinstance(x, list):
return [_normalize(element) for element in x]
elif isinstance(x, unicode):
return x.encode(locale.getpreferredencoding())
return x.encode(PREFER_ENC)
else:
return x
def _json_dumps_safe(x):
try:
x = json.dumps(_normalize(x), ensure_ascii=False).decode(
locale.getpreferredencoding(), 'replace')
PREFER_ENC, 'replace')
except Exception as e: # pragma: no cover
logSys.error('json dumps failed: %s', e)
x = '{}'
@ -78,7 +77,7 @@ else:
def _json_loads_safe(x):
try:
x = _normalize(json.loads(x.decode(
locale.getpreferredencoding(), 'replace')))
PREFER_ENC, 'replace')))
except Exception as e: # pragma: no cover
logSys.error('json loads failed: %s', e)
x = {}
@ -212,7 +211,7 @@ class Fail2BanDb(object):
if newversion == Fail2BanDb.__version__:
logSys.warning( "Database updated from '%i' to '%i'",
version, newversion)
else:
else: # pragma: no cover
logSys.error( "Database update failed to achieve version '%i'"
": updated from '%i' to '%i'",
Fail2BanDb.__version__, version, newversion)
@ -223,6 +222,11 @@ class Fail2BanDb(object):
cur.execute("PRAGMA journal_mode = MEMORY")
cur.close()
def close(self):
logSys.debug("Close connection to database ...")
self._db.close()
logSys.info("Connection to database closed.")
@property
def filename(self):
"""File name of SQLite3 database file.
@ -477,7 +481,8 @@ class Fail2BanDb(object):
queryArgs.append(str(ip))
query += " ORDER BY ip, timeofban desc"
return cur.execute(query, queryArgs)
# repack iterator as long as in lock:
return list(cur.execute(query, queryArgs))
def getBans(self, **kwargs):
"""Get bans from the database.
@ -576,6 +581,43 @@ class Fail2BanDb(object):
self._bansMergedCache[cacheKey] = tickets if ip is None else ticket
return tickets if ip is None else ticket
def _getCurrentBans(self, cur, jail = None, ip = None, forbantime=None, fromtime=None):
if fromtime is None:
fromtime = MyTime.time()
queryArgs = []
if jail is not None:
query = "SELECT ip, timeofban, data FROM bans WHERE jail=?"
queryArgs.append(jail.name)
else:
query = "SELECT ip, max(timeofban), data FROM bans WHERE 1"
if ip is not None:
query += " AND ip=?"
queryArgs.append(ip)
if forbantime is not None:
query += " AND timeofban > ?"
queryArgs.append(fromtime - forbantime)
if ip is None:
query += " GROUP BY ip ORDER BY ip, timeofban DESC"
cur = self._db.cursor()
return cur.execute(query, queryArgs)
def getCurrentBans(self, jail = None, ip = None, forbantime=None, fromtime=None):
tickets = []
ticket = None
with self._lock:
results = list(self._getCurrentBans(self._db.cursor(),
jail=jail, ip=ip, forbantime=forbantime, fromtime=fromtime))
if results:
for banip, timeofban, data in results:
# logSys.debug('restore ticket %r, %r, %r', banip, timeofban, data)
ticket = FailTicket(banip, timeofban, data=data)
# logSys.debug('restored ticket: %r', ticket)
tickets.append(ticket)
return tickets if ip is None else ticket
@commitandrollback
def purge(self, cur):
"""Purge old bans, jails and log files from database.

View File

@ -40,11 +40,11 @@ class Regex:
# avoid construction of invalid object.
# @param value the regular expression
def __init__(self, regex):
def __init__(self, regex, **kwargs):
self._matchCache = None
# Perform shortcuts expansions.
# Resolve "<HOST>" tag using default regular expression for host:
regex = Regex._resolveHostTag(regex)
regex = Regex._resolveHostTag(regex, **kwargs)
# Replace "<SKIPLINES>" with regular expression for multiple lines.
regexSplit = regex.split("<SKIPLINES>")
regex = regexSplit[0]
@ -69,22 +69,29 @@ class Regex:
# @return the replaced regular expression as string
@staticmethod
def _resolveHostTag(regex):
# 3 groups instead of <HOST> - separated ipv4, ipv6 and host
regex = regex.replace("<HOST>",
r"""(?:(?:::f{4,6}:)?(?P<ip4>(?:\d{1,3}\.){3}\d{1,3})|\[?(?P<ip6>(?:[0-9a-fA-F]{1,4}::?|::){1,7}(?:[0-9a-fA-F]{1,4}|(?<=:):))\]?|(?P<dns>[\w\-.^_]*\w))""")
def _resolveHostTag(regex, useDns="yes"):
# separated ipv4:
r_host = []
r = r"""(?:::f{4,6}:)?(?P<ip4>(?:\d{1,3}\.){3}\d{1,3})"""
regex = regex.replace("<IP4>", r); # self closed
regex = regex.replace("<F-IP4/>", r); # closed
r_host.append(r)
# separated ipv6:
r = r"""(?P<ip6>(?:[0-9a-fA-F]{1,4}::?|::){1,7}(?:[0-9a-fA-F]{1,4}?|(?<=:):))"""
regex = regex.replace("<IP6>", r); # self closed
regex = regex.replace("<F-IP6/>", r); # closed
r_host.append(r"""\[?%s\]?""" % (r,)); # enclose ipv6 in optional [] in host-regex
# 2 address groups instead of <ADDR> - in opposition to `<HOST>`,
# for separate usage of 2 address groups only (regardless of `usedns`), `ip4` and `ip6` together
regex = regex.replace("<ADDR>", "(?:%s)" % ("|".join(r_host),))
# separated dns:
r = r"""(?P<dns>[\w\-.^_]*\w)"""
regex = regex.replace("<DNS>", r); # self closed
regex = regex.replace("<F-DNS/>", r); # closed
if useDns not in ("no",):
r_host.append(r)
# 3 groups instead of <HOST> - separated ipv4, ipv6 and host (dns)
regex = regex.replace("<HOST>", "(?:%s)" % ("|".join(r_host),))
# default failure-id as no space tag:
regex = regex.replace("<F-ID/>", r"""(?P<fid>\S+)"""); # closed
# default failure port, like 80 or http :
@ -249,9 +256,9 @@ class FailRegex(Regex):
# avoid construction of invalid object.
# @param value the regular expression
def __init__(self, regex):
def __init__(self, regex, **kwargs):
# Initializes the parent.
Regex.__init__(self, regex)
Regex.__init__(self, regex, **kwargs)
# Check for group "dns", "ip4", "ip6", "fid"
if not [grp for grp in FAILURE_ID_GROPS if grp in self._regexObj.groupindex]:
raise RegexException("No failure-id group in '%s'" % self._regex)

View File

@ -24,7 +24,6 @@ __license__ = "GPL"
import codecs
import datetime
import fcntl
import locale
import logging
import os
import re
@ -40,7 +39,7 @@ from .datetemplate import DatePatternRegex, DateEpoch, DateTai64n
from .mytime import MyTime
from .failregex import FailRegex, Regex, RegexException
from .action import CommandAction
from ..helpers import getLogger
from ..helpers import getLogger, PREFER_ENC
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -87,9 +86,10 @@ class Filter(JailThread):
## External command
self.__ignoreCommand = False
## Default or preferred encoding (to decode bytes from file or journal):
self.__encoding = locale.getpreferredencoding()
## Error counter
self.__errors = 0
self.__encoding = PREFER_ENC
## Error counter (protected, so can be used in filter implementations)
## if it reached 100 (at once), run-cycle will go idle
self._errors = 0
## Ticks counter
self.ticks = 0
@ -100,6 +100,31 @@ class Filter(JailThread):
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, self.jail)
@property
def jailName(self):
return (self.jail is not None and self.jail.name or "~jailless~")
def clearAllParams(self):
""" Clear all lists/dicts parameters (used by reloading)
"""
self.delFailRegex()
self.delIgnoreRegex()
self.delIgnoreIP()
def reload(self, begin=True):
""" Begin or end of reloading resp. refreshing of all parameters
"""
if begin:
self.clearAllParams()
if hasattr(self, 'getLogPaths'):
self._reload_logs = dict((k, 1) for k in self.getLogPaths())
else:
if hasattr(self, '_reload_logs'):
# if it was not reloaded - remove obsolete log file:
for path in self._reload_logs:
self.delLogPath(path)
delattr(self, '_reload_logs')
##
# Add a regular expression which matches the failure.
#
@ -109,18 +134,23 @@ class Filter(JailThread):
def addFailRegex(self, value):
try:
regex = FailRegex(value)
regex = FailRegex(value, useDns=self.__useDns)
self.__failRegex.append(regex)
if "\n" in regex.getRegex() and not self.getMaxLines() > 1:
logSys.warning(
"Mutliline regex set for jail '%s' "
"but maxlines not greater than 1")
"Mutliline regex set for jail %r "
"but maxlines not greater than 1", self.jailName)
except RegexException as e:
logSys.error(e)
raise e
def delFailRegex(self, index):
def delFailRegex(self, index=None):
try:
# clear all:
if index is None:
del self.__failRegex[:]
return
# delete by index:
del self.__failRegex[index]
except IndexError:
logSys.error("Cannot remove regular expression. Index %d is not "
@ -146,14 +176,19 @@ class Filter(JailThread):
def addIgnoreRegex(self, value):
try:
regex = Regex(value)
regex = Regex(value, useDns=self.__useDns)
self.__ignoreRegex.append(regex)
except RegexException as e:
logSys.error(e)
raise e
def delIgnoreRegex(self, index):
def delIgnoreRegex(self, index=None):
try:
# clear all:
if index is None:
del self.__ignoreRegex[:]
return
# delete by index:
del self.__ignoreRegex[index]
except IndexError:
logSys.error("Cannot remove regular expression. Index %d is not "
@ -203,7 +238,7 @@ class Filter(JailThread):
value = MyTime.str2seconds(value)
self.__findTime = value
self.failManager.setMaxTime(value)
logSys.info("Set findtime = %s" % value)
logSys.info(" findtime: %s", value)
##
# Get the time needed to find a failure.
@ -232,10 +267,10 @@ class Filter(JailThread):
template = DatePatternRegex(pattern)
self.dateDetector = DateDetector()
self.dateDetector.appendTemplate(template)
logSys.info("Date pattern set to `%r`: `%s`" %
(pattern, template.name))
logSys.debug("Date pattern regex for %r: %s" %
(pattern, template.regex))
logSys.info(" date pattern `%r`: `%s`",
pattern, template.name)
logSys.debug(" date pattern regex for %r: %s",
pattern, template.regex)
##
# Get the date detector pattern, or Default Detectors if not changed
@ -261,7 +296,7 @@ class Filter(JailThread):
def setMaxRetry(self, value):
self.failManager.setMaxRetry(value)
logSys.info("Set maxRetry = %s" % value)
logSys.info(" maxRetry: %s", value)
##
# Get the maximum retry value.
@ -280,7 +315,7 @@ class Filter(JailThread):
if int(value) <= 0:
raise ValueError("maxlines must be integer greater than zero")
self.__lineBufferSize = int(value)
logSys.info("Set maxlines = %i" % self.__lineBufferSize)
logSys.info(" maxLines: %i", self.__lineBufferSize)
##
# Get the maximum line buffer size.
@ -297,10 +332,10 @@ class Filter(JailThread):
def setLogEncoding(self, encoding):
if encoding.lower() == "auto":
encoding = locale.getpreferredencoding()
encoding = PREFER_ENC
codecs.lookup(encoding) # Raise LookupError if invalid codec
self.__encoding = encoding
logSys.info("Set jail log file encoding to %s" % encoding)
logSys.info(" encoding: %s" % encoding)
return encoding
##
@ -375,16 +410,21 @@ class Filter(JailThread):
ip = IPAddr(ipstr)
# log and append to ignore list
logSys.debug("Add %r to ignore list (%r)", ip, ipstr)
logSys.debug(" Add %r to ignore list (%r)", ip, ipstr)
self.__ignoreIpList.append(ip)
def delIgnoreIP(self, ip):
logSys.debug("Remove %r from ignore list", ip)
def delIgnoreIP(self, ip=None):
# clear all:
if ip is None:
del self.__ignoreIpList[:]
return
# delete by ip:
logSys.debug(" Remove %r from ignore list", ip)
self.__ignoreIpList.remove(ip)
def logIgnoreIp(self, ip, log_ignore, ignore_source="unknown source"):
if log_ignore:
logSys.info("[%s] Ignore %s by %s" % (self.jail.name, ip, ignore_source))
logSys.info("[%s] Ignore %s by %s" % (self.jailName, ip, ignore_source))
def getIgnoreIP(self):
return self.__ignoreIpList
@ -415,29 +455,6 @@ class Filter(JailThread):
return False
if sys.version_info >= (3,):
@staticmethod
def uni_decode(x, enc, errors='strict'):
try:
if isinstance(x, bytes):
return x.decode(enc, errors)
return x
except (UnicodeDecodeError, UnicodeEncodeError): # pragma: no cover - unsure if reachable
if errors != 'strict':
raise
return uni_decode(x, enc, 'replace')
else:
@staticmethod
def uni_decode(x, enc, errors='strict'):
try:
if isinstance(x, unicode):
return x.encode(enc, errors)
return x
except (UnicodeDecodeError, UnicodeEncodeError): # pragma: no cover - unsure if reachable
if errors != 'strict':
raise
return uni_decode(x, enc, 'replace')
def processLine(self, line, date=None, returnRawHost=False,
checkAllRegex=False, checkFindTime=False):
"""Split the time portion from log msg and return findFailures on them
@ -478,24 +495,28 @@ class Filter(JailThread):
if self.inIgnoreIPList(ip, log_ignore=True):
continue
logSys.info(
"[%s] Found %s - %s", self.jail.name, ip, datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S")
"[%s] Found %s - %s", self.jailName, ip, datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S")
)
tick = FailTicket(ip, unixTime, lines, data=fail)
self.failManager.addFailure(tick)
# reset (halve) error counter (successfully processed line):
if self.__errors:
self.__errors //= 2
if self._errors:
self._errors //= 2
except Exception as e:
logSys.error("Failed to process line: %r, caught exception: %r", line, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# incr error counter, stop processing (going idle) after 100th error :
self.__errors += 1
# sleep a little bit (to get around time-related errors):
time.sleep(self.sleeptime)
if self.__errors >= 100:
logSys.error("Too many errors at once (%s), going idle", self.__errors)
self.__errors //= 2
self.idle = True
# incr common error counter:
self.commonError()
def commonError(self):
# incr error counter, stop processing (going idle) after 100th error :
self._errors += 1
# sleep a little bit (to get around time-related errors):
time.sleep(self.sleeptime)
if self._errors >= 100:
logSys.error("Too many errors at once (%s), going idle", self._errors)
self._errors //= 2
self.idle = True
##
# Returns true if the line should be ignored.
@ -657,7 +678,10 @@ class FileFilter(Filter):
def addLogPath(self, path, tail=False, autoSeek=True):
if path in self.__logs:
logSys.error(path + " already exists")
if hasattr(self, '_reload_logs') and path in self._reload_logs:
del self._reload_logs[path]
else:
logSys.error(path + " already exists")
else:
log = FileContainer(path, self.getLogEncoding(), tail)
db = self.jail.database
@ -666,7 +690,7 @@ class FileFilter(Filter):
if lastpos and not tail:
log.setPos(lastpos)
self.__logs[path] = log
logSys.info("Added logfile = %s (pos = %s, hash = %s)" , path, log.getPos(), log.getHash())
logSys.info("Added logfile: %r (pos = %s, hash = %s)" , path, log.getPos(), log.getHash())
if autoSeek:
# if default, seek to "current time" - "find time":
if isinstance(autoSeek, bool):
@ -692,7 +716,7 @@ class FileFilter(Filter):
db = self.jail.database
if db is not None:
db.updateLog(self.jail, log)
logSys.info("Removed logfile = %s" % path)
logSys.info("Removed logfile: %r" % path)
self._delLogPath(path)
return
@ -759,49 +783,48 @@ class FileFilter(Filter):
if log is None:
logSys.error("Unable to get failures in " + filename)
return False
# Try to open log file.
# We should always close log (file), otherwise may be locked (log-rotate, etc.)
try:
has_content = log.open()
# see http://python.org/dev/peps/pep-3151/
except IOError as e:
logSys.error("Unable to open %s" % filename)
logSys.exception(e)
return False
except OSError as e: # pragma: no cover - requires race condition to tigger this
logSys.error("Error opening %s" % filename)
logSys.exception(e)
return False
except Exception as e: # pragma: no cover - Requires implemention error in FileContainer to generate
logSys.error("Internal error in FileContainer open method - please report as a bug to https://github.com/fail2ban/fail2ban/issues")
logSys.exception(e)
return False
# seek to find time for first usage only (prevent performance decline with polling of big files)
if self.__autoSeek.get(filename):
startTime = self.__autoSeek[filename]
del self.__autoSeek[filename]
# prevent completely read of big files first time (after start of service),
# initial seek to start time using half-interval search algorithm:
# Try to open log file.
try:
self.seekToTime(log, startTime)
except Exception as e: # pragma: no cover
logSys.error("Error during seek to start time in \"%s\"", filename)
raise
has_content = log.open()
# see http://python.org/dev/peps/pep-3151/
except IOError as e:
logSys.error("Unable to open %s" % filename)
logSys.exception(e)
return False
except OSError as e: # pragma: no cover - requires race condition to tigger this
logSys.error("Error opening %s" % filename)
logSys.exception(e)
return False
except Exception as e: # pragma: no cover - Requires implemention error in FileContainer to generate
logSys.error("Internal error in FileContainer open method - please report as a bug to https://github.com/fail2ban/fail2ban/issues")
logSys.exception(e)
return False
# yoh: has_content is just a bool, so do not expect it to
# change -- loop is exited upon break, and is not entered at
# all if upon container opening that one was empty. If we
# start reading tested to be empty container -- race condition
# might occur leading at least to tests failures.
while has_content:
line = log.readline()
if not line or not self.active:
# The jail reached the bottom or has been stopped
break
self.processLineAndAdd(line)
log.close()
# seek to find time for first usage only (prevent performance decline with polling of big files)
if self.__autoSeek.get(filename):
startTime = self.__autoSeek[filename]
del self.__autoSeek[filename]
# prevent completely read of big files first time (after start of service),
# initial seek to start time using half-interval search algorithm:
try:
self.seekToTime(log, startTime)
except Exception as e: # pragma: no cover
logSys.error("Error during seek to start time in \"%s\"", filename)
raise
logSys.exception(e)
return False
if has_content:
while not self.idle:
line = log.readline()
if not line or not self.active:
# The jail reached the bottom or has been stopped
break
self.processLineAndAdd(line)
finally:
log.close()
db = self.jail.database
if db is not None:
db.updateLog(self.jail, log)
@ -1055,10 +1078,14 @@ _decode_line_warn = {}
class JournalFilter(Filter): # pragma: systemd no cover
def clearAllParams(self):
super(JournalFilter, self).clearAllParams()
self.delJournalMatch()
def addJournalMatch(self, match): # pragma: no cover - Base class, not used
pass
def delJournalMatch(self, match): # pragma: no cover - Base class, not used
def delJournalMatch(self, match=None): # pragma: no cover - Base class, not used
pass
def getJournalMatch(self, match): # pragma: no cover - Base class, not used

View File

@ -124,14 +124,15 @@ class FilterGamin(FileFilter):
while self.active:
if self.idle:
# wait a little bit here for not idle, to prevent hi-load:
if not Utils.wait_for(lambda: not self.idle,
if not Utils.wait_for(lambda: not self.active or not self.idle,
self.sleeptime * 10, self.sleeptime
):
self.ticks += 1
continue
Utils.wait_for(self._handleEvents, self.sleeptime)
Utils.wait_for(lambda: not self.active or self._handleEvents(),
self.sleeptime)
self.ticks += 1
logSys.debug(self.jail.name + ": filter terminated")
logSys.debug("[%s] filter terminated", self.jailName)
return True
def stop(self):

View File

@ -31,7 +31,7 @@ from .failmanager import FailManagerEmpty
from .filter import FileFilter
from .mytime import MyTime
from .utils import Utils
from ..helpers import getLogger
from ..helpers import getLogger, logging
# Gets the instance of the logger.
@ -101,14 +101,15 @@ class FilterPoll(FileFilter):
logSys.log(6, "Woke up idle=%s with %d files monitored",
self.idle, self.getLogCount())
if self.idle:
if not Utils.wait_for(lambda: not self.idle,
if not Utils.wait_for(lambda: not self.active or not self.idle,
self.sleeptime * 10, self.sleeptime
):
self.ticks += 1
continue
# Get file modification
modlst = []
Utils.wait_for(lambda: self.getModified(modlst), self.sleeptime)
Utils.wait_for(lambda: not self.active or self.getModified(modlst),
self.sleeptime)
for filename in modlst:
self.getFailures(filename)
self.__modified = True
@ -122,9 +123,7 @@ class FilterPoll(FileFilter):
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.__modified = False
logSys.debug(
(self.jail is not None and self.jail.name or "jailless") +
" filter terminated")
logSys.debug("[%s] filter terminated", self.jailName)
return True
##
@ -137,28 +136,34 @@ class FilterPoll(FileFilter):
try:
logStats = os.stat(filename)
stats = logStats.st_mtime, logStats.st_ino, logStats.st_size
pstats = self.__prevStats.get(filename, ())
self.__file404Cnt[filename] = 0
if logSys.getEffectiveLevel() <= 7:
pstats = self.__prevStats.get(filename, (0))
if logSys.getEffectiveLevel() <= 5:
# we do not want to waste time on strftime etc if not necessary
dt = logStats.st_mtime - pstats[0]
logSys.log(7, "Checking %s for being modified. Previous/current stats: %s / %s. dt: %s",
logSys.log(5, "Checking %s for being modified. Previous/current stats: %s / %s. dt: %s",
filename, pstats, stats, dt)
# os.system("stat %s | grep Modify" % filename)
self.__file404Cnt[filename] = 0
if pstats == stats:
return False
logSys.debug("%s has been modified", filename)
self.__prevStats[filename] = stats
return True
except OSError as e:
logSys.error("Unable to get stat on %s because of: %s"
% (filename, e))
except Exception as e:
# stil alive (may be deleted because multi-threaded):
if not self.getLog(filename):
logSys.warning("Log %r seems to be down: %s", filename, e)
return
# log error:
if self.__file404Cnt[filename] < 2:
logSys.error("Unable to get stat on %s because of: %s",
filename, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# increase file and common error counters:
self.__file404Cnt[filename] += 1
if self.__file404Cnt[filename] > 2:
logSys.warning("Too many errors. Setting the jail idle")
if self.jail is not None:
self.jail.idle = True
else:
logSys.warning("No jail is assigned to %s" % self)
self.commonError()
if self.__file404Cnt[filename] > 50:
logSys.warning("Too many errors. Remove file %r from monitoring process", filename)
self.__file404Cnt[filename] = 0
self.delLogPath(filename)
return False

View File

@ -76,7 +76,7 @@ class FilterPyinotify(FileFilter):
logSys.debug("Created FilterPyinotify")
def callback(self, event, origin=''):
logSys.debug("%sCallback for Event: %s", origin, event)
logSys.log(7, "[%s] %sCallback for Event: %s", self.jailName, origin, event)
path = event.pathname
if event.mask & ( pyinotify.IN_CREATE | pyinotify.IN_MOVED_TO ):
# skip directories altogether
@ -119,14 +119,15 @@ class FilterPyinotify(FileFilter):
logSys.debug("Added file watcher for %s", path)
def _delFileWatcher(self, path):
wdInt = self.__watches[path]
wd = self.__monitor.rm_watch(wdInt)
if wd[wdInt]:
del self.__watches[path]
logSys.debug("Removed file watcher for %s", path)
return True
else:
return False
try:
wdInt = self.__watches.pop(path)
wd = self.__monitor.rm_watch(wdInt)
if wd[wdInt]:
logSys.debug("Removed file watcher for %s", path)
return True
except KeyError: # pragma: no cover
pass
return False
##
# Add a log file path
@ -158,8 +159,11 @@ class FilterPyinotify(FileFilter):
if k.startswith(path_dir + pathsep)]):
# Remove watches for the directory
# since there is no other monitored file under this directory
wdInt = self.__watches.pop(path_dir)
self.__monitor.rm_watch(wdInt)
try:
wdInt = self.__watches.pop(path_dir)
self.__monitor.rm_watch(wdInt)
except KeyError: # pragma: no cover
pass
logSys.debug("Removed monitor for the parent directory %s", path_dir)
# pyinotify.ProcessEvent default handler:
@ -174,7 +178,7 @@ class FilterPyinotify(FileFilter):
# slow check events while idle:
def __check_events(self, *args, **kwargs):
if self.idle:
if Utils.wait_for(lambda: not self.idle,
if Utils.wait_for(lambda: not self.active or not self.idle,
self.sleeptime * 10, self.sleeptime
):
pass
@ -190,11 +194,12 @@ class FilterPyinotify(FileFilter):
def run(self):
prcevent = pyinotify.ProcessEvent()
prcevent.process_default = self.__process_default
## timeout for pyinotify must be set in milliseconds (our time values are floats contain seconds)
self.__notifier = pyinotify.ThreadedNotifier(self.__monitor,
prcevent, timeout=self.sleeptime)
prcevent, timeout=self.sleeptime * 1000)
self.__notifier.check_events = self.__check_events
self.__notifier.start()
logSys.debug("pyinotifier started for %s.", self.jail.name)
logSys.debug("[%s] filter started (pyinotifier)", self.jailName)
return True
##
@ -202,15 +207,22 @@ class FilterPyinotify(FileFilter):
def stop(self):
super(FilterPyinotify, self).stop()
# Stop the notifier thread
self.__notifier.stop()
self.__notifier.join() # to not exit before notifier does
self.__cleanup() # for pedantic ones
##
# Wait for exit with cleanup.
def join(self):
self.__cleanup()
super(FilterPyinotify, self).join()
logSys.debug("[%s] filter terminated (pyinotifier)", self.jailName)
##
# Deallocates the resources used by pyinotify.
def __cleanup(self):
self.__notifier = None
if self.__notifier:
self.__notifier.join() # to not exit before notifier does
self.__notifier = None
self.__monitor = None

View File

@ -34,7 +34,7 @@ from .failmanager import FailManagerEmpty
from .filter import JournalFilter, Filter
from .mytime import MyTime
from .utils import Utils
from ..helpers import getLogger, logging, splitwords
from ..helpers import getLogger, logging, splitwords, uni_decode
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -130,7 +130,8 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
self.resetJournalMatches()
raise
else:
logSys.info("Added journal match for: %r", " ".join(match))
logSys.info("[%s] Added journal match for: %r", self.jailName,
" ".join(match))
##
# Reset a journal match filter called on removal or failure
#
@ -138,7 +139,7 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
def resetJournalMatches(self):
self.__journal.flush_matches()
logSys.debug("Flushed all journal matches")
logSys.debug("[%s] Flushed all journal matches", self.jailName)
match_copy = self.__matches[:]
self.__matches = []
try:
@ -154,13 +155,20 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
#
# @param match journalctl syntax matches
def delJournalMatch(self, match):
if match in self.__matches:
def delJournalMatch(self, match=None):
# clear all:
if match is None:
if not self.__matches:
return
del self.__matches[:]
# delete by index:
elif match in self.__matches:
del self.__matches[self.__matches.index(match)]
self.resetJournalMatches()
else:
raise ValueError("Match not found")
logSys.info("Removed journal match for: %r" % " ".join(match))
raise ValueError("Match %r not found" % match)
self.resetJournalMatches()
logSys.info("[%s] Removed journal match for: %r", self.jailName,
match if match else '*')
##
# Get current journal match filter
@ -170,10 +178,6 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
def getJournalMatch(self):
return self.__matches
def uni_decode(self, x):
v = Filter.uni_decode(x, self.getLogEncoding())
return v
##
# Format journal log entry into syslog style
#
@ -182,16 +186,16 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
def formatJournalEntry(self, logentry):
# Be sure, all argument of line tuple should have the same type:
uni_decode = self.uni_decode
enc = self.getLogEncoding()
logelements = []
v = logentry.get('_HOSTNAME')
if v:
logelements.append(uni_decode(v))
logelements.append(uni_decode(v, enc))
v = logentry.get('SYSLOG_IDENTIFIER')
if not v:
v = logentry.get('_COMM')
if v:
logelements.append(uni_decode(v))
logelements.append(uni_decode(v, enc))
v = logentry.get('SYSLOG_PID')
if not v:
v = logentry.get('_PID')
@ -206,16 +210,16 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
logelements.append("[%12.6f]" % monotonic.total_seconds())
msg = logentry.get('MESSAGE','')
if isinstance(msg, list):
logelements.append(" ".join(uni_decode(v) for v in msg))
logelements.append(" ".join(uni_decode(v, enc) for v in msg))
else:
logelements.append(uni_decode(msg))
logelements.append(uni_decode(msg, enc))
logline = " ".join(logelements)
date = logentry.get('_SOURCE_REALTIME_TIMESTAMP',
logentry.get('__REALTIME_TIMESTAMP'))
logSys.debug("Read systemd journal entry: %r" %
"".join([date.isoformat(), logline]))
logSys.log(5, "[%s] Read systemd journal entry: %s %s", self.jailName,
date.isoformat(), logline)
## use the same type for 1st argument:
return ((logline[:0], date.isoformat(), logline),
time.mktime(date.timetuple()) + date.microsecond/1.0E6)
@ -252,40 +256,64 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
while self.active:
# wait for records (or for timeout in sleeptime seconds):
self.__journal.wait(self.sleeptime)
if self.idle:
# because journal.wait will returns immediatelly if we have records in journal,
# just wait a little bit here for not idle, to prevent hi-load:
if not Utils.wait_for(lambda: not self.idle,
self.sleeptime * 10, self.sleeptime
):
try:
## todo: find better method as wait_for to break (e.g. notify) journal.wait(self.sleeptime),
## don't use `journal.close()` for it, because in some python/systemd implementation it may
## cause abnormal program termination
#self.__journal.wait(self.sleeptime) != journal.NOP
##
## wait for entries without sleep in intervals, because "sleeping" in journal.wait:
Utils.wait_for(lambda: not self.active or \
self.__journal.wait(Utils.DEFAULT_SLEEP_INTERVAL) != journal.NOP,
self.sleeptime, 0.00001)
if self.idle:
# because journal.wait will returns immediatelly if we have records in journal,
# just wait a little bit here for not idle, to prevent hi-load:
if not Utils.wait_for(lambda: not self.active or not self.idle,
self.sleeptime * 10, self.sleeptime
):
self.ticks += 1
continue
self.__modified = 0
while self.active:
logentry = None
try:
logentry = self.__journal.get_next()
except OSError as e:
logSys.error("Error reading line from systemd journal: %s",
e, exc_info=logSys.getEffectiveLevel() <= logging.DEBUG)
self.ticks += 1
continue
self.__modified = 0
while self.active:
logentry = None
try:
logentry = self.__journal.get_next()
except OSError as e:
logSys.error("Error reading line from systemd journal: %s",
e, exc_info=logSys.getEffectiveLevel() <= logging.DEBUG)
self.ticks += 1
if logentry:
self.processLineAndAdd(
*self.formatJournalEntry(logentry))
self.__modified += 1
if self.__modified >= 100: # todo: should be configurable
if logentry:
self.processLineAndAdd(
*self.formatJournalEntry(logentry))
self.__modified += 1
if self.__modified >= 100: # todo: should be configurable
break
else:
break
else:
if self.__modified:
try:
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
except Exception as e: # pragma: no cover
if not self.active: # if not active - error by stop...
break
if self.__modified:
try:
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
logSys.error("Caught unhandled exception in main cycle: %r", e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# incr common error counter:
self.commonError()
logSys.debug("[%s] filter terminated", self.jailName)
# close journal:
try:
if self.__journal:
self.__journal.close()
except Exception as e: # pragma: no cover
logSys.error("Close journal failed: %r", e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
logSys.debug((self.jail is not None and self.jail.name
or "jailless") +" filter terminated")
return True

View File

@ -28,7 +28,7 @@ import Queue
from .actions import Actions
from ..client.jailreader import JailReader
from ..helpers import getLogger
from ..helpers import getLogger, MyTime
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -79,6 +79,7 @@ class Jail(object):
logSys.info("Creating new jail '%s'" % self.name)
if backend is not None:
self._setBackend(backend)
self.backend = backend
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, self.name)
@ -193,7 +194,7 @@ class Jail(object):
Used by filter to add a failure for banning.
"""
self.__queue.put(ticket)
if self.database is not None:
if not ticket.restored and self.database is not None:
self.database.addBan(self, ticket)
def getFailTicket(self):
@ -202,34 +203,66 @@ class Jail(object):
Used by actions to get a failure for banning.
"""
try:
return self.__queue.get(False)
ticket = self.__queue.get(False)
return ticket
except Queue.Empty:
return False
def restoreCurrentBans(self):
"""Restore any previous valid bans from the database.
"""
try:
if self.database is not None:
forbantime = self.actions.getBanTime()
for ticket in self.database.getCurrentBans(jail=self, forbantime=forbantime):
#logSys.debug('restored ticket: %s', ticket)
if not self.filter.inIgnoreIPList(ticket.getIP(), log_ignore=True):
# mark ticked was restored from database - does not put it again into db:
ticket.restored = True
# correct start time / ban time (by the same end of ban):
btm = ticket.getBanTime(forbantime)
diftm = MyTime.time() - ticket.getTime()
if btm != -1 and diftm > 0:
btm -= diftm
# ignore obsolete tickets:
if btm != -1 and btm <= 0:
continue
ticket.setTime(MyTime.time())
ticket.setBanTime(btm)
self.putFailTicket(ticket)
except Exception as e: # pragma: no cover
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
def start(self):
"""Start the jail, by starting filter and actions threads.
Once stated, also queries the persistent database to reinstate
any valid bans.
"""
logSys.debug("Starting jail %r", self.name)
self.filter.start()
self.actions.start()
# Restore any previous valid bans from the database
if self.database is not None:
for ticket in self.database.getBansMerged(
jail=self, bantime=self.actions.getBanTime()):
if not self.filter.inIgnoreIPList(ticket.getIP(), log_ignore=True):
self.__queue.put(ticket)
logSys.info("Jail '%s' started" % self.name)
self.restoreCurrentBans()
logSys.info("Jail %r started", self.name)
def stop(self):
def stop(self, stop=True, join=True):
"""Stop the jail, by stopping filter and actions threads.
"""
self.filter.stop()
self.actions.stop()
self.filter.join()
self.actions.join()
logSys.info("Jail '%s' stopped" % self.name)
if stop:
logSys.debug("Stopping jail %r", self.name)
for obj in (self.filter, self.actions):
try:
## signal to stop filter / actions:
if stop:
obj.stop()
## wait for end of threads:
if join:
obj.join()
except Exception as e:
logSys.error("Stop %r of jail %r failed: %s", obj, self.name, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
if join:
logSys.info("Jail %r stopped", self.name)
def isAlive(self):
"""Check jail "isAlive" by checking filter and actions threads.

View File

@ -62,14 +62,15 @@ class Jails(Mapping):
DuplicateJailException
If jail name is already present.
"""
try:
self.__lock.acquire()
with self.__lock:
if name in self._jails:
raise DuplicateJailException(name)
if noduplicates:
raise DuplicateJailException(name)
else:
self._jails[name] = Jail(name, backend, db)
finally:
self.__lock.release()
def exists(self, name):
return name in self._jails
def __getitem__(self, name):
try:

View File

@ -53,8 +53,8 @@ class JailThread(Thread):
super(JailThread, self).__init__(name=name)
## Should going with main thread also:
self.daemon = True
## Control the state of the thread.
self.active = False
## Control the state of the thread (None - was not started, True - active, False - stopped).
self.active = None
## Control the idle state of the thread.
self.idle = False
## The time the thread sleeps in the loop.
@ -93,3 +93,14 @@ class JailThread(Thread):
"""Abstract - Called when thread starts, thread stops when returns.
"""
pass
def join(self):
""" Safer join, that could be called also for not started (or ended) threads (used for cleanup).
"""
## if cleanup needed - create derivate and call it before join...
## if was really started - should call join:
if self.active is not None:
super(JailThread, self).join()

View File

@ -38,7 +38,7 @@ from .filter import FileFilter, JournalFilter
from .transmitter import Transmitter
from .asyncserver import AsyncServer, AsyncServerException
from .. import version
from ..helpers import getLogger, excepthook
from ..helpers import getLogger, str2LogLevel, excepthook
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -67,6 +67,7 @@ class Server:
self.__db = None
self.__daemon = daemon
self.__transm = Transmitter(self)
self.__reload_state = {}
#self.__asyncServer = AsyncServer(self.__transm)
self.__asyncServer = None
self.__logLevel = None
@ -170,6 +171,12 @@ class Server:
# Now stop all the jails
self.stopAllJail()
# Explicit close database (server can leave in a thread,
# so delayed GC can prevent commiting changes)
if self.__db:
self.__db.close()
self.__db = None
# Only now shutdown the logging.
if self.__logTarget is not None:
with self.__loggingLock:
@ -184,41 +191,111 @@ class Server:
self.quit = lambda: False
def addJail(self, name, backend):
self.__jails.add(name, backend, self.__db)
addflg = True
if self.__reload_state.get(name) and self.__jails.exists(name):
jail = self.__jails[name]
# if backend switch - restart instead of reload:
if jail.backend == backend:
addflg = False
logSys.info("Reload jail %r", name)
# prevent to reload the same jail twice (temporary keep it in state, needed to commit reload):
self.__reload_state[name] = None
else:
logSys.info("Restart jail %r (reason: %r != %r)", name, jail.backend, backend)
self.delJail(name, stop=True)
# prevent to start the same jail twice (no reload more - restart):
del self.__reload_state[name]
if addflg:
self.__jails.add(name, backend, self.__db)
if self.__db is not None:
self.__db.addJail(self.__jails[name])
def delJail(self, name):
if self.__db is not None:
self.__db.delJail(self.__jails[name])
del self.__jails[name]
def delJail(self, name, stop=True, join=True):
jail = self.__jails[name]
if join or jail.isAlive():
jail.stop(stop=stop, join=join)
if join:
if self.__db is not None:
self.__db.delJail(jail)
del self.__jails[name]
def startJail(self, name):
try:
self.__lock.acquire()
if not self.__jails[name].isAlive():
self.__jails[name].start()
finally:
self.__lock.release()
with self.__lock:
jail = self.__jails[name]
if not jail.isAlive():
jail.start()
elif name in self.__reload_state:
logSys.info("Jail %r reloaded", name)
del self.__reload_state[name]
if jail.idle:
jail.idle = False
def stopJail(self, name):
logSys.debug("Stopping jail %s" % name)
try:
self.__lock.acquire()
if self.__jails[name].isAlive():
self.__jails[name].stop()
self.delJail(name)
finally:
self.__lock.release()
with self.__lock:
self.delJail(name, stop=True)
def stopAllJail(self):
logSys.info("Stopping all jails")
try:
self.__lock.acquire()
for jail in self.__jails.keys():
self.stopJail(jail)
finally:
self.__lock.release()
with self.__lock:
# 1st stop all jails (signal and stop actions/filter thread):
for name in self.__jails.keys():
self.delJail(name, stop=True, join=False)
# 2nd wait for end and delete jails:
for name in self.__jails.keys():
self.delJail(name, stop=False, join=True)
def reloadJails(self, name, opts, begin):
if begin:
# begin reload:
if self.__reload_state and (name == '--all' or self.__reload_state.get(name)):
raise ValueError('Reload already in progress')
logSys.info("Reload " + (("jail %s" % name) if name != '--all' else "all jails"))
with self.__lock:
# if single jail:
if name != '--all':
jail = None
# test jail exists (throws exception if not):
if "--if-exists" not in opts or self.__jails.exists(name):
jail = self.__jails[name]
if jail:
# first unban all ips (will be not restored after (re)start):
if "--unban" in opts:
self.setUnbanIP(name)
# stop if expected:
if "--restart" in opts:
self.stopJail(name)
else:
# first unban all ips (will be not restored after (re)start):
if "--unban" in opts:
self.setUnbanIP()
# stop if expected:
if "--restart" in opts:
self.stopAllJail()
# first set all affected jail(s) to idle and reset filter regex and other lists/dicts:
for jn, jail in self.__jails.iteritems():
if name == '--all' or jn == name:
jail.idle = True
self.__reload_state[jn] = jail
jail.filter.reload(begin=True)
jail.actions.reload(begin=True)
pass
else:
# end reload, all affected (or new) jails have already all new parameters (via stream) and (re)started:
with self.__lock:
deljails = []
for jn, jail in self.__jails.iteritems():
# still in reload state:
if jn in self.__reload_state:
# remove jails that are not reloaded (untouched, so not in new configuration)
deljails.append(jn)
else:
# commit (reload was finished):
jail.filter.reload(begin=False)
jail.actions.reload(begin=False)
for jn in deljails:
self.delJail(jn)
self.__reload_state = {}
logSys.info("Reload finished.")
def setIdleJail(self, name, value):
self.__jails[name].idle = value
@ -309,7 +386,7 @@ class Server:
logSys.debug(" failregex: %r", value)
flt.addFailRegex(value)
def delFailRegex(self, name, index):
def delFailRegex(self, name, index=None):
self.__jails[name].filter.delFailRegex(index)
def getFailRegex(self, name):
@ -351,7 +428,9 @@ class Server:
# Action
def addAction(self, name, value, *args):
self.__jails[name].actions.add(value, *args)
## create (or reload) jail action:
self.__jails[name].actions.add(value, *args,
reload=name in self.__reload_state)
def getActions(self, name):
return self.__jails[name].actions
@ -368,8 +447,20 @@ class Server:
def setBanIP(self, name, value):
return self.__jails[name].filter.addBannedIP(value)
def setUnbanIP(self, name, value):
self.__jails[name].actions.removeBannedIP(value)
def setUnbanIP(self, name=None, value=None):
if name is not None:
# in all jails:
jails = [self.__jails[name]]
else:
# single jail:
jails = self.__jails.values()
# unban given or all (if value is None):
cnt = 0
for jail in jails:
cnt += jail.actions.removeBannedIP(value, ifexists=(name is None))
if value and not cnt:
logSys.info("%s is not banned", value)
return cnt
def getBanTime(self, name):
return self.__jails[name].actions.getBanTime()
@ -419,11 +510,11 @@ class Server:
with self.__loggingLock:
if self.__logLevel == value:
return
try:
getLogger("fail2ban").setLevel(getattr(logging, value))
self.__logLevel = value
except AttributeError:
raise ValueError("Invalid log level %r" % value)
ll = str2LogLevel(value)
# don't change real log-level if running from the test cases:
getLogger("fail2ban").setLevel(
ll if DEF_LOGTARGET != "INHERITED" or ll < logging.DEBUG else DEF_LOGLEVEL)
self.__logLevel = value
##
# Get the logging level.

View File

@ -34,8 +34,13 @@ from .mytime import MyTime
logSys = getLogger(__name__)
class Ticket:
class Ticket(object):
MAX_TIME = 0X7FFFFFFFFFFF ;# 4461763-th year
RESTORED = 0x01
BANNED = 0x08
def __init__(self, ip=None, time=None, matches=None, data={}, ticket=None):
"""Ticket constructor
@ -49,13 +54,12 @@ class Ticket:
self._banCount = 0;
self._banTime = None;
self._time = time if time is not None else MyTime.time()
self._data = {'matches': [], 'failures': 0}
self._data.update(data)
self._data = {'matches': matches or [], 'failures': 0}
if data is not None:
self._data.update(data)
if ticket:
# ticket available - copy whole information from ticket:
self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__)
else:
self._data['matches'] = matches or []
def __str__(self):
return "%s: ip=%s time=%s #attempts=%d matches=%r" % \
@ -94,8 +98,8 @@ class Ticket:
def setBanTime(self, value):
self._banTime = value;
def getBanTime(self, defaultBT = None):
return (self._banTime if not self._banTime is None else defaultBT);
def getBanTime(self, defaultBT=None):
return (self._banTime if self._banTime is not None else defaultBT)
def setBanCount(self, value):
self._banCount = value;
@ -106,8 +110,16 @@ class Ticket:
def getBanCount(self):
return self._banCount;
def isTimedOut(self, time, defaultBT = None):
bantime = (self._banTime if not self._banTime is None else defaultBT);
def getEndOfBanTime(self, defaultBT=None):
bantime = (self._banTime if self._banTime is not None else defaultBT)
# permanent
if bantime == -1:
return Ticket.MAX_TIME
# unban time (end of ban):
return self._time + bantime
def isTimedOut(self, time, defaultBT=None):
bantime = (self._banTime if self._banTime is not None else defaultBT)
# permanent
if bantime == -1:
return False
@ -126,6 +138,26 @@ class Ticket:
def getMatches(self):
return self._data.get('matches', [])
@property
def restored(self):
return self._flags & Ticket.RESTORED
@restored.setter
def restored(self, value):
if value:
self._flags |= Ticket.RESTORED
else:
self._flags &= ~(Ticket.RESTORED)
@property
def banned(self):
return self._flags & Ticket.BANNED
@banned.setter
def banned(self, value):
if value:
self._flags |= Ticket.BANNED
else:
self._flags &= ~(Ticket.BANNED)
def setData(self, *args, **argv):
# if overwrite - set data and filter None values:
if len(args) == 1:

View File

@ -27,7 +27,7 @@ __license__ = "GPL"
import time
import json
from ..helpers import getLogger
from ..helpers import getLogger, logging
from .. import version
# Gets the instance of the logger.
@ -52,13 +52,14 @@ class Transmitter:
def proceed(self, command):
# Deserialize object
logSys.debug("Command: %r", command)
logSys.log(5, "Command: %r", command)
try:
ret = self.__commandHandler(command)
ack = 0, ret
except Exception as e:
logSys.warning("Command %r has failed. Received %r"
% (command, e))
logSys.warning("Command %r has failed. Received %r",
command, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
ack = 1, e
return ack
@ -72,8 +73,8 @@ class Transmitter:
return "pong"
elif command[0] == "add":
name = command[1]
if name == "all":
raise Exception("Reserved name")
if name == "--all":
raise Exception("Reserved name %r" % (name,))
try:
backend = command[2]
except IndexError:
@ -87,12 +88,31 @@ class Transmitter:
elif command[0] == "stop":
if len(command) == 1:
self.__server.quit()
elif command[1] == "all":
elif command[1] == "--all":
self.__server.stopAllJail()
else:
name = command[1]
self.__server.stopJail(name)
return None
elif command[0] == "reload":
opts = command[1:3]
try:
self.__server.reloadJails(*opts, begin=True)
for cmd in command[3]:
self.__commandHandler(cmd)
finally:
self.__server.reloadJails(*opts, begin=False)
return 'OK'
elif len(command) >= 2 and command[0] == "unban":
# unban in all jails:
value = command[1:]
# if all ips:
if len(value) == 1 and value[0] == "--all":
self.__server.setUnbanIP()
return
for value in value:
self.__server.setUnbanIP(None, value)
return None
elif command[0] == "echo":
return command[1:]
elif command[0] == "sleep":
@ -265,7 +285,7 @@ class Transmitter:
action = self.__server.getAction(name, actionname)
if multiple:
for cmd in command[3]:
logSys.debug(" %r", cmd)
logSys.log(5, " %r", cmd)
actionkey = cmd[0]
if callable(getattr(action, actionkey, None)):
actionvalue = json.loads(cmd[1]) if len(cmd)>1 else {}

View File

@ -21,8 +21,14 @@ __author__ = "Serg G. Brester (sebres) and Fail2Ban Contributors"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Yaroslav Halchenko, 2012-2015 Serg G. Brester"
__license__ = "GPL"
import logging, os, fcntl, subprocess, time, signal
from ..helpers import getLogger
import fcntl
import logging
import os
import signal
import subprocess
import sys
import time
from ..helpers import getLogger, uni_decode
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -46,8 +52,8 @@ class Utils():
"""Utilities provide diverse static methods like executes OS shell commands, etc.
"""
DEFAULT_SLEEP_TIME = 0.1
DEFAULT_SLEEP_INTERVAL = 0.01
DEFAULT_SLEEP_TIME = 2
DEFAULT_SLEEP_INTERVAL = 0.2
class Cache(object):
@ -179,7 +185,7 @@ class Utils():
if stdout is not None and stdout != '' and std_level >= logSys.getEffectiveLevel():
logSys.log(std_level, "%s -- stdout:", realCmd)
for l in stdout.splitlines():
logSys.log(std_level, " -- stdout: %r", l)
logSys.log(std_level, " -- stdout: %r", uni_decode(l))
popen.stdout.close()
if popen.stderr:
try:
@ -191,7 +197,7 @@ class Utils():
if stderr is not None and stderr != '' and std_level >= logSys.getEffectiveLevel():
logSys.log(std_level, "%s -- stderr:", realCmd)
for l in stderr.splitlines():
logSys.log(std_level, " -- stderr: %r", l)
logSys.log(std_level, " -- stderr: %r", uni_decode(l))
popen.stderr.close()
success = False

View File

@ -277,6 +277,24 @@ class CommandActionTest(LogCaptureTestCase):
self.assertRaises(RuntimeError, self.__action.ban, {'ip': None})
self.assertLogged('Unable to restore environment')
def testExecuteActionCheckRepairEnvironment(self):
self.__action.actionstart = ""
self.__action.actionstop = ""
self.__action.actionban = "rm /tmp/fail2ban.test"
self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.__action.actionrepair = "echo 'repair ...'; touch /tmp/fail2ban.test"
# 1st time with success repair:
self.__action.ban({'ip': None})
self.assertLogged("Invariant check failed. Trying", "echo 'repair ...'", all=True)
self.pruneLog()
# 2nd time failed (not really repaired):
self.__action.actionrepair = "echo 'repair ...'"
self.assertRaises(RuntimeError, self.__action.ban, {'ip': None})
self.assertLogged(
"Invariant check failed. Trying",
"echo 'repair ...'",
"Unable to restore environment", all=True)
def testExecuteActionChangeCtags(self):
self.assertRaises(AttributeError, getattr, self.__action, "ROST")
self.__action.ROST = "192.0.2.0"
@ -294,7 +312,12 @@ class CommandActionTest(LogCaptureTestCase):
def testExecuteActionStartEmpty(self):
self.__action.actionstart = ""
self.__action.start()
self.assertTrue(self.__action.executeCmd(""))
self.assertLogged('Nothing to do')
self.pruneLog()
self.assertTrue(self.__action._processCmd(""))
self.assertLogged('Nothing to do')
self.pruneLog()
def testExecuteIncorrectCmd(self):
CommandAction.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null')
@ -376,11 +399,11 @@ class CommandActionTest(LogCaptureTestCase):
def testCaptureStdOutErr(self):
CommandAction.executeCmd('echo "How now brown cow"')
self.assertLogged("stdout: 'How now brown cow'\n", "stdout: b'How now brown cow'\n")
self.assertLogged("stdout: 'How now brown cow'\n")
CommandAction.executeCmd(
'echo "The rain in Spain stays mainly in the plain" 1>&2')
self.assertLogged(
"stderr: 'The rain in Spain stays mainly in the plain'\n", "stderr: b'The rain in Spain stays mainly in the plain'\n")
"stderr: 'The rain in Spain stays mainly in the plain'\n")
def testCallingMap(self):
mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'),

View File

@ -28,7 +28,6 @@ import unittest
from ..server.banmanager import BanManager
from ..server.ticket import BanTicket
from .utils import assert_dict_equal
class AddFailure(unittest.TestCase):
def setUp(self):
@ -53,11 +52,15 @@ class AddFailure(unittest.TestCase):
self.assertEqual(self.__banManager.size(), 1)
def testAddDuplicateWithTime(self):
defBanTime = self.__banManager.getBanTime()
prevEndOfBanTime = 0
# add again a duplicate :
# 1) with newer start time and the same ban time
# 0) with same start time and the same (default) ban time
# 1) with newer start time and the same (default) ban time
# 2) with same start time and longer ban time
# 3) with permanent ban time (-1)
for tnew, btnew in (
(1167605999.0, None),
(1167605999.0 + 100, None),
(1167605999.0, 24*60*60),
(1167605999.0, -1),
@ -70,10 +73,15 @@ class AddFailure(unittest.TestCase):
self.assertFalse(self.__banManager.addBanTicket(ticket2))
self.assertEqual(self.__banManager.size(), 1)
# pop ticket and check it was prolonged :
banticket = self.__banManager.getTicketByIP(ticket2.getIP())
self.assertEqual(banticket.getTime(), ticket2.getTime())
self.assertEqual(banticket.getTime(), ticket2.getTime())
self.assertEqual(banticket.getBanTime(), ticket2.getBanTime(self.__banManager.getBanTime()))
banticket = self.__banManager.getTicketByID(ticket2.getID())
self.assertEqual(banticket.getEndOfBanTime(defBanTime), ticket2.getEndOfBanTime(defBanTime))
self.assertTrue(banticket.getEndOfBanTime(defBanTime) > prevEndOfBanTime)
prevEndOfBanTime = ticket1.getEndOfBanTime(defBanTime)
# but the start time should not be changed (+ 100 is ignored):
self.assertEqual(banticket.getTime(), 1167605999.0)
# if prolong to permanent, it should also have permanent ban time:
if btnew == -1:
self.assertEqual(banticket.getBanTime(defBanTime), -1)
def testInListOK(self):
self.assertTrue(self.__banManager.addBanTicket(self.__ticket))
@ -87,9 +95,28 @@ class AddFailure(unittest.TestCase):
def testUnban(self):
btime = self.__banManager.getBanTime()
stime = self.__ticket.getTime()
self.assertTrue(self.__banManager.addBanTicket(self.__ticket))
self.assertTrue(self.__banManager._inBanList(self.__ticket))
self.assertEqual(self.__banManager.unBanList(self.__ticket.getTime() + btime + 1), [self.__ticket])
self.assertEqual(self.__banManager.unBanList(stime), [])
self.assertEqual(self.__banManager.unBanList(stime + btime + 1), [self.__ticket])
self.assertEqual(self.__banManager.size(), 0)
## again, but now we will prolong ban-time and then try to unban again (1st too early):
self.assertTrue(self.__banManager.addBanTicket(self.__ticket))
# prolong ban:
ticket = BanTicket(self.__ticket.getID(), stime + 600)
self.assertFalse(self.__banManager.addBanTicket(ticket))
# try unban too early:
self.assertEqual(len(self.__banManager.unBanList(stime + btime + 1)), 0)
# try unban using correct time:
self.assertEqual(len(self.__banManager.unBanList(stime + btime + 600 + 1)), 1)
## again, but now we test removing tickets particular (to test < 2/3-rule):
for i in range(5):
ticket = BanTicket('193.168.0.%s' % i, stime)
ticket.setBanTime(ticket.getBanTime(btime) + i*10)
self.assertTrue(self.__banManager.addBanTicket(ticket))
self.assertEqual(len(self.__banManager.unBanList(stime + btime + 1*10 + 1)), 2)
self.assertEqual(len(self.__banManager.unBanList(stime + btime + 5*10 + 1)), 3)
self.assertEqual(self.__banManager.size(), 0)
def testUnbanPermanent(self):
@ -122,7 +149,7 @@ class StatusExtendedCymruInfo(unittest.TestCase):
def testCymruInfo(self):
cymru_info = self.__banManager.getBanListExtendedCymruInfo()
assert_dict_equal(cymru_info,
self.assertDictEqual(cymru_info,
{"asn": [self.__asn],
"country": [self.__country],
"rir": [self.__rir]})
@ -149,7 +176,7 @@ class StatusExtendedCymruInfo(unittest.TestCase):
ticket = BanTicket("0.0.0.0", 1167605999.0)
self.assertTrue(self.__banManager.addBanTicket(ticket))
cymru_info = self.__banManager.getBanListExtendedCymruInfo()
assert_dict_equal(cymru_info,
self.assertDictEqual(cymru_info,
{"asn": ["nxdomain"],
"country": ["nxdomain"],
"rir": ["nxdomain"]})
@ -160,7 +187,7 @@ class StatusExtendedCymruInfo(unittest.TestCase):
ticket = BanTicket("10.0.0.0", 1167606000.0)
self.assertTrue(self.__banManager.addBanTicket(ticket))
cymru_info = self.__banManager.getBanListExtendedCymruInfo()
assert_dict_equal(cymru_info,
{"asn": ["nxdomain", "4565",],
"country": ["nxdomain", "unknown"],
"rir": ["nxdomain", "other"]})
self.assertDictEqual(dict((k, sorted(v)) for k, v in cymru_info.iteritems()),
{"asn": sorted(["nxdomain", "4565",]),
"country": sorted(["nxdomain", "unknown"]),
"rir": sorted(["nxdomain", "other"])})

View File

@ -353,6 +353,11 @@ class DatabaseTest(LogCaptureTestCase):
# be returned
tickets = self.db.getBansMerged(bantime=-1)
self.assertEqual(len(tickets), 2)
# getCurrentBans:
tickets = self.db.getCurrentBans(jail=self.jail)
self.assertEqual(len(tickets), 2)
ticket = self.db.getCurrentBans(jail=None, ip="127.0.0.1");
self.assertEqual(ticket.getIP(), "127.0.0.1")
def testActionWithDB(self):
# test action together with database functionality

View File

@ -41,8 +41,9 @@ from ..client.fail2banclient import exec_command_line as _exec_client, VisualWai
from ..client.fail2banserver import Fail2banServer, exec_command_line as _exec_server
from .. import protocol
from ..server import server
from ..server.mytime import MyTime
from ..server.utils import Utils
from .utils import LogCaptureTestCase, with_tmpdir, shutil, logging
from .utils import LogCaptureTestCase, logSys as DefLogSys, with_tmpdir, shutil, logging
from ..helpers import getLogger
@ -57,6 +58,7 @@ SERVER = "fail2ban-server"
BIN = dirname(Fail2banServer.getServerPath())
MAX_WAITTIME = 30 if not unittest.F2B.fast else 5
MID_WAITTIME = MAX_WAITTIME
##
# Several wrappers and settings for proper testing:
@ -68,7 +70,8 @@ fail2bancmdline.logSys = \
fail2banclient.logSys = \
fail2banserver.logSys = logSys
server.DEF_LOGTARGET = "/dev/null"
SRV_DEF_LOGTARGET = server.DEF_LOGTARGET
SRV_DEF_LOGLEVEL = server.DEF_LOGLEVEL
def _test_output(*args):
logSys.info(args[0])
@ -110,17 +113,25 @@ fail2bancmdline.PRODUCTION = \
fail2banserver.PRODUCTION = False
def _out_file(fn):
def _out_file(fn, handle=logSys.debug):
"""Helper which outputs content of the file at HEAVYDEBUG loglevels"""
logSys.debug('---- ' + fn + ' ----')
handle('---- ' + fn + ' ----')
for line in fileinput.input(fn):
line = line.rstrip('\n')
logSys.debug(line)
logSys.debug('-'*30)
handle(line)
handle('-'*30)
def _start_params(tmp, use_stock=False, logtarget="/dev/null"):
def _write_file(fn, mode, *lines):
f = open(fn, mode)
f.write('\n'.join(lines))
f.close()
def _start_params(tmp, use_stock=False, logtarget="/dev/null", db=":memory:"):
cfg = pjoin(tmp, "config")
if db == 'auto':
db = pjoin(tmp, "f2b-db.sqlite3")
if use_stock and STOCK:
# copy config (sub-directories as alias):
def ig_dirs(dir, files):
@ -146,8 +157,7 @@ def _start_params(tmp, use_stock=False, logtarget="/dev/null"):
else:
# just empty config directory without anything (only fail2ban.conf/jail.conf):
os.mkdir(cfg)
f = open(pjoin(cfg, "fail2ban.conf"), "w")
f.write('\n'.join((
_write_file(pjoin(cfg, "fail2ban.conf"), "w",
"[Definition]",
"loglevel = INFO",
"logtarget = " + logtarget,
@ -155,19 +165,16 @@ def _start_params(tmp, use_stock=False, logtarget="/dev/null"):
"socket = " + pjoin(tmp, "f2b.sock"),
"pidfile = " + pjoin(tmp, "f2b.pid"),
"backend = polling",
"dbfile = :memory:",
"dbfile = " + db,
"dbpurgeage = 1d",
"",
)))
f.close()
f = open(pjoin(cfg, "jail.conf"), "w")
f.write('\n'.join((
)
_write_file(pjoin(cfg, "jail.conf"), "w",
"[INCLUDES]", "",
"[DEFAULT]", "",
"",
)))
f.close()
if logSys.level < logging.DEBUG: # if HEAVYDEBUG
)
if DefLogSys.level < logging.DEBUG: # if HEAVYDEBUG
_out_file(pjoin(cfg, "fail2ban.conf"))
_out_file(pjoin(cfg, "jail.conf"))
# parameters (sock/pid and config, increase verbosity, set log, etc.):
@ -237,19 +244,78 @@ def with_kill_srv(f):
_kill_srv(pidfile)
return wrapper
def with_foreground_server_thread(startextra={}):
"""Helper to decorate tests uses foreground server (as thread), started directly in test-cases
To be used only in subclasses
"""
def _deco_wrapper(f):
@with_tmpdir
@wraps(f)
def wrapper(self, tmp, *args, **kwargs):
th = None
phase = dict()
try:
# started directly here, so prevent overwrite test cases logger with "INHERITED"
startparams = _start_params(tmp, logtarget="INHERITED", **startextra)
# because foreground block execution - start it in thread:
th = Thread(
name="_TestCaseWorker",
target=self._testStartForeground,
args=(tmp, startparams, phase)
)
th.daemon = True
th.start()
try:
# wait for start thread:
Utils.wait_for(lambda: phase.get('start', None) is not None, MAX_WAITTIME)
self.assertTrue(phase.get('start', None))
# wait for server (socket and ready):
self._wait_for_srv(tmp, True, startparams=startparams)
DefLogSys.info('=== within server: begin ===')
self.pruneLog()
# several commands to server in body of decorated function:
return f(self, tmp, startparams, *args, **kwargs)
finally:
DefLogSys.info('=== within server: end. ===')
self.pruneLog()
# stop:
self.execSuccess(startparams, "stop")
# wait for end:
Utils.wait_for(lambda: phase.get('end', None) is not None, MAX_WAITTIME)
self.assertTrue(phase.get('end', None))
self.assertLogged("Shutdown successful", "Exiting Fail2ban")
finally:
if th:
# we start client/server directly in current process (new thread),
# so don't kill (same process) - if success, just wait for end of worker:
if phase.get('end', None):
th.join()
return wrapper
return _deco_wrapper
class Fail2banClientServerBase(LogCaptureTestCase):
_orig_exit = Fail2banCmdLine._exit
def _setLogLevel(self, *args, **kwargs):
pass
def setUp(self):
"""Call before every test case."""
LogCaptureTestCase.setUp(self)
# prevent to switch the logging in the test cases (use inherited one):
server.DEF_LOGTARGET = "INHERITED"
server.DEF_LOGLEVEL = DefLogSys.level
Fail2banCmdLine._exit = staticmethod(self._test_exit)
def tearDown(self):
"""Call after every test case."""
Fail2banCmdLine._exit = self._orig_exit
# restore server log target:
server.DEF_LOGTARGET = SRV_DEF_LOGTARGET
server.DEF_LOGLEVEL = SRV_DEF_LOGLEVEL
LogCaptureTestCase.tearDown(self)
@staticmethod
@ -303,47 +369,12 @@ class Fail2banClientServerBase(LogCaptureTestCase):
phase['end'] = True
logSys.debug("end of test worker")
@with_tmpdir
def testStartForeground(self, tmp):
# intended to be ran only in subclasses
th = None
phase = dict()
try:
# started directly here, so prevent overwrite test cases logger with "INHERITED"
startparams = _start_params(tmp, logtarget="INHERITED")
# because foreground block execution - start it in thread:
th = Thread(
name="_TestCaseWorker",
target=self._testStartForeground,
args=(tmp, startparams, phase)
)
th.daemon = True
th.start()
try:
# wait for start thread:
Utils.wait_for(lambda: phase.get('start', None) is not None, MAX_WAITTIME)
self.assertTrue(phase.get('start', None))
# wait for server (socket and ready):
self._wait_for_srv(tmp, True, startparams=startparams)
self.pruneLog()
# several commands to server:
self.execSuccess(startparams, "ping")
self.execFailed(startparams, "~~unknown~cmd~failed~~")
self.execSuccess(startparams, "echo", "TEST-ECHO")
finally:
self.pruneLog()
# stop:
self.execSuccess(startparams, "stop")
# wait for end:
Utils.wait_for(lambda: phase.get('end', None) is not None, MAX_WAITTIME)
self.assertTrue(phase.get('end', None))
self.assertLogged("Shutdown successful", "Exiting Fail2ban")
finally:
if th:
# we start client/server directly in current process (new thread),
# so don't kill (same process) - if success, just wait for end of worker:
if phase.get('end', None):
th.join()
@with_foreground_server_thread()
def testStartForeground(self, tmp, startparams):
# several commands to server:
self.execSuccess(startparams, "ping")
self.execFailed(startparams, "~~unknown~cmd~failed~~")
self.execSuccess(startparams, "echo", "TEST-ECHO")
class Fail2banClientTest(Fail2banClientServerBase):
@ -508,6 +539,24 @@ class Fail2banClientTest(Fail2banClientServerBase):
self.assertLogged("Usage: ")
self.pruneLog()
@with_tmpdir
def testClientFailCommands(self, tmp):
# started directly here, so prevent overwrite test cases logger with "INHERITED"
startparams = _start_params(tmp, logtarget="INHERITED")
# not started:
self.execFailed(startparams,
"reload", "jail")
self.assertLogged("Could not find server")
self.pruneLog()
# unexpected arg:
self.execFailed(startparams,
"--async", "reload", "--xxx", "jail")
self.assertLogged("Unexpected argument(s) for reload:")
self.pruneLog()
def testVisualWait(self):
sleeptime = 0.035
for verbose in (2, 0):
@ -612,3 +661,315 @@ class Fail2banServerTest(Fail2banClientServerBase):
# again:
self.assertTrue(_kill_srv(tmp))
self.assertLogged("cleanup: no pidfile for")
@with_foreground_server_thread(startextra={'db': 'auto'})
def testServerReloadTest(self, tmp, startparams):
# Very complicated test-case, that expected running server (foreground in thread).
#
# In this test-case, each phase is related from previous one,
# so it cannot be splitted in multiple test cases.
# Additionaly many log-messages used as ready-sign (to wait for end of phase).
#
# Used file database (instead of :memory:), to restore bans and log-file positions,
# after restart/reload between phases.
cfg = pjoin(tmp, "config")
test1log = pjoin(tmp, "test1.log")
test2log = pjoin(tmp, "test2.log")
test3log = pjoin(tmp, "test3.log")
os.mkdir(pjoin(cfg, "action.d"))
def _write_action_cfg(actname="test-action1", allow=True,
start="", reload="", ban="", unban="", stop=""):
fn = pjoin(cfg, "action.d", "%s.conf" % actname)
if not allow:
os.remove(fn)
return
_write_file(fn, "w",
"[Definition]",
"actionstart = echo '[<name>] %s: ** start'" % actname, start,
"actionreload = echo '[<name>] %s: .. reload'" % actname, reload,
"actionban = echo '[<name>] %s: ++ ban <ip>'" % actname, ban,
"actionunban = echo '[<name>] %s: -- unban <ip>'" % actname, unban,
"actionstop = echo '[<name>] %s: __ stop'" % actname, stop,
)
if DefLogSys.level <= logging.DEBUG: # if DEBUG
_out_file(fn)
def _write_jail_cfg(enabled=(1, 2), actions=()):
_write_file(pjoin(cfg, "jail.conf"), "w",
"[INCLUDES]", "",
"[DEFAULT]", "",
"usedns = no",
"maxretry = 3",
"findtime = 10m",
"failregex = ^\s*failure (401|403) from <HOST>",
"",
"[test-jail1]", "backend = polling", "filter =",
"action = ",
" test-action1[name='%(__name__)s']" if 1 in actions else "",
" test-action2[name='%(__name__)s']" if 2 in actions else "",
"logpath = " + test1log,
" " + test2log if 2 in enabled else "",
" " + test3log if 2 in enabled else "",
"failregex = ^\s*failure (401|403) from <HOST>",
" ^\s*error (401|403) from <HOST>" if 2 in enabled else "",
"enabled = true" if 1 in enabled else "",
"",
"[test-jail2]", "backend = polling", "filter =",
"action =",
"logpath = " + test2log,
"enabled = true" if 2 in enabled else "",
)
if DefLogSys.level <= logging.DEBUG: # if DEBUG
_out_file(pjoin(cfg, "jail.conf"))
# create default test actions:
_write_action_cfg(actname="test-action1")
_write_action_cfg(actname="test-action2")
_write_jail_cfg(enabled=[1], actions=[1,2])
_write_file(test1log, "w", *((str(int(MyTime.time())) + " failure 401 from 192.0.2.1: test 1",) * 3))
_write_file(test2log, "w")
_write_file(test3log, "w")
# reload and wait for ban:
self.pruneLog("[test-phase 1a]")
if DefLogSys.level < logging.DEBUG: # if HEAVYDEBUG
_out_file(test1log)
self.execSuccess(startparams, "reload")
self.assertLogged(
"Reload finished.",
"1 ticket(s) in 'test-jail1", all=True, wait=MID_WAITTIME)
self.assertLogged("Added logfile: %r" % test1log)
self.assertLogged("[test-jail1] Ban 192.0.2.1")
# test actions started:
self.assertLogged(
"stdout: '[test-jail1] test-action1: ** start'",
"stdout: '[test-jail1] test-action2: ** start'", all=True)
# enable both jails, 3 logs for jail1, etc...
# truncate test-log - we should not find unban/ban again by reload:
self.pruneLog("[test-phase 1b]")
_write_jail_cfg(actions=[1,2])
_write_file(test1log, "w+")
if DefLogSys.level < logging.DEBUG: # if HEAVYDEBUG
_out_file(test1log)
self.execSuccess(startparams, "reload")
self.assertLogged("Reload finished.", all=True, wait=MID_WAITTIME)
# test not unbanned / banned again:
self.assertNotLogged(
"[test-jail1] Unban 192.0.2.1",
"[test-jail1] Ban 192.0.2.1", all=True)
# test 2 new log files:
self.assertLogged(
"Added logfile: %r" % test2log,
"Added logfile: %r" % test3log, all=True)
# test actions reloaded:
self.assertLogged(
"stdout: '[test-jail1] test-action1: .. reload'",
"stdout: '[test-jail1] test-action2: .. reload'", all=True)
# test 1 new jail:
self.assertLogged(
"Creating new jail 'test-jail2'",
"Jail 'test-jail2' started", all=True)
# update action1, delete action2 (should be stopped via configuration)...
self.pruneLog("[test-phase 2a]")
_write_jail_cfg(actions=[1])
_write_action_cfg(actname="test-action1",
start= " echo '[<name>] %s: started.'" % "test-action1",
reload=" echo '[<name>] %s: reloaded.'" % "test-action1",
stop= " echo '[<name>] %s: stopped.'" % "test-action1")
self.execSuccess(startparams, "reload")
self.assertLogged("Reload finished.", all=True, wait=MID_WAITTIME)
# test not unbanned / banned again:
self.assertNotLogged(
"[test-jail1] Unban 192.0.2.1",
"[test-jail1] Ban 192.0.2.1", all=True)
# no new log files:
self.assertNotLogged("Added logfile:")
# test action reloaded (update):
self.assertLogged(
"stdout: '[test-jail1] test-action1: .. reload'",
"stdout: '[test-jail1] test-action1: reloaded.'", all=True)
# test stopped action unbans:
self.assertLogged(
"stdout: '[test-jail1] test-action2: -- unban 192.0.2.1'")
# test action stopped:
self.assertLogged(
"stdout: '[test-jail1] test-action2: __ stop'")
self.assertNotLogged(
"stdout: '[test-jail1] test-action1: -- unban 192.0.2.1'")
# don't need both actions anymore:
_write_action_cfg(actname="test-action1", allow=False)
_write_action_cfg(actname="test-action2", allow=False)
_write_jail_cfg(actions=[])
# write new failures:
self.pruneLog("[test-phase 2b]")
_write_file(test2log, "w+", *(
(str(int(MyTime.time())) + " error 403 from 192.0.2.2: test 2",) * 3 +
(str(int(MyTime.time())) + " error 403 from 192.0.2.3: test 2",) * 3 +
(str(int(MyTime.time())) + " failure 401 from 192.0.2.4: test 2",) * 3 +
(str(int(MyTime.time())) + " failure 401 from 192.0.2.8: test 2",) * 3
))
if DefLogSys.level < logging.DEBUG: # if HEAVYDEBUG
_out_file(test2log)
# test all will be found in jail1 and one in jail2:
self.assertLogged(
"2 ticket(s) in 'test-jail2",
"5 ticket(s) in 'test-jail1", all=True, wait=MID_WAITTIME)
self.assertLogged(
"[test-jail1] Ban 192.0.2.2",
"[test-jail1] Ban 192.0.2.3",
"[test-jail1] Ban 192.0.2.4",
"[test-jail1] Ban 192.0.2.8",
"[test-jail2] Ban 192.0.2.4",
"[test-jail2] Ban 192.0.2.8", all=True)
# test ips at all not visible for jail2:
self.assertNotLogged(
"[test-jail2] Found 192.0.2.2",
"[test-jail2] Ban 192.0.2.2",
"[test-jail2] Found 192.0.2.3",
"[test-jail2] Ban 192.0.2.3", all=True)
# rotate logs:
_write_file(test1log, "w+")
_write_file(test2log, "w+")
# restart jail without unban all:
self.pruneLog("[test-phase 2c]")
self.execSuccess(startparams,
"restart", "test-jail2")
self.assertLogged(
"Reload finished.",
"Restore Ban",
"2 ticket(s) in 'test-jail2", all=True, wait=MID_WAITTIME)
# stop/start and unban/restore ban:
self.assertLogged(
"Jail 'test-jail2' stopped",
"Jail 'test-jail2' started",
"[test-jail2] Unban 192.0.2.4",
"[test-jail2] Unban 192.0.2.8",
"[test-jail2] Restore Ban 192.0.2.4",
"[test-jail2] Restore Ban 192.0.2.8", all=True
)
# restart jail with unban all:
self.pruneLog("[test-phase 2d]")
self.execSuccess(startparams,
"restart", "--unban", "test-jail2")
self.assertLogged(
"Reload finished.",
"Jail 'test-jail2' started", all=True, wait=MID_WAITTIME)
self.assertLogged(
"Jail 'test-jail2' stopped",
"Jail 'test-jail2' started",
"[test-jail2] Unban 192.0.2.4",
"[test-jail2] Unban 192.0.2.8", all=True
)
# no more ban (unbanned all):
self.assertNotLogged(
"[test-jail2] Ban 192.0.2.4",
"[test-jail2] Ban 192.0.2.8", all=True
)
# reload jail1 without restart (without ban/unban):
self.pruneLog("[test-phase 3]")
self.execSuccess(startparams, "reload", "test-jail1")
self.assertLogged(
"Reload finished.", all=True, wait=MID_WAITTIME)
self.assertLogged(
"Reload jail 'test-jail1'",
"Jail 'test-jail1' reloaded", all=True)
self.assertNotLogged(
"Reload jail 'test-jail2'",
"Jail 'test-jail2' reloaded",
"Jail 'test-jail1' started", all=True
)
# whole reload, but this time with jail1 only (jail2 should be stopped via configuration):
self.pruneLog("[test-phase 4]")
_write_jail_cfg(enabled=[1])
self.execSuccess(startparams, "reload")
self.assertLogged("Reload finished.", all=True, wait=MID_WAITTIME)
# test both jails should be reloaded:
self.assertLogged(
"Reload jail 'test-jail1'")
# test jail2 goes down:
self.assertLogged(
"Stopping jail 'test-jail2'",
"Jail 'test-jail2' stopped", all=True)
# test 2 log files removed:
self.assertLogged(
"Removed logfile: %r" % test2log,
"Removed logfile: %r" % test3log, all=True)
# now write failures again and check already banned (jail1 was alive the whole time) and new bans occurred (jail1 was alive the whole time):
self.pruneLog("[test-phase 5]")
_write_file(test1log, "w+", *(
(str(int(MyTime.time())) + " failure 401 from 192.0.2.1: test 5",) * 3 +
(str(int(MyTime.time())) + " error 403 from 192.0.2.5: test 5",) * 3 +
(str(int(MyTime.time())) + " failure 401 from 192.0.2.6: test 5",) * 3
))
if DefLogSys.level < logging.DEBUG: # if HEAVYDEBUG
_out_file(test1log)
self.assertLogged(
"6 ticket(s) in 'test-jail1",
"[test-jail1] 192.0.2.1 already banned", all=True, wait=MID_WAITTIME)
# test "failure" regexp still available:
self.assertLogged(
"[test-jail1] Found 192.0.2.1",
"[test-jail1] Found 192.0.2.6",
"[test-jail1] 192.0.2.1 already banned",
"[test-jail1] Ban 192.0.2.6", all=True)
# test "error" regexp no more available:
self.assertNotLogged("[test-jail1] Found 192.0.2.5")
# unban single ips:
self.pruneLog("[test-phase 6]")
self.execSuccess(startparams,
"--async", "unban", "192.0.2.5", "192.0.2.6")
self.assertLogged(
"192.0.2.5 is not banned",
"[test-jail1] Unban 192.0.2.6", all=True
)
# reload all (one jail) with unban all:
self.pruneLog("[test-phase 7]")
self.execSuccess(startparams,
"reload", "--unban")
self.assertLogged("Reload finished.", all=True, wait=MID_WAITTIME)
# reloads unbanned all:
self.assertLogged(
"Jail 'test-jail1' reloaded",
"[test-jail1] Unban 192.0.2.1",
"[test-jail1] Unban 192.0.2.2",
"[test-jail1] Unban 192.0.2.3",
"[test-jail1] Unban 192.0.2.4", all=True
)
# no restart occurred, no more ban (unbanned all using option "--unban"):
self.assertNotLogged(
"Jail 'test-jail1' stopped",
"Jail 'test-jail1' started",
"[test-jail1] Ban 192.0.2.1",
"[test-jail1] Ban 192.0.2.2",
"[test-jail1] Ban 192.0.2.3",
"[test-jail1] Ban 192.0.2.4", all=True
)
# several small cases (cover several parts):
self.pruneLog("[test-phase end-1]")
# wrong jail (not-started):
self.execFailed(startparams,
"--async", "reload", "test-jail2")
self.assertLogged("the jail 'test-jail2' does not exist")
self.pruneLog()
# unavailable jail (but exit 0), using --if-exists option:
self.execSuccess(startparams,
"--async", "reload", "--if-exists", "test-jail2")
self.assertNotLogged(
"Creating new jail 'test-jail2'",
"Jail 'test-jail2' started", all=True)
self.pruneLog()

View File

@ -131,6 +131,15 @@ class Fail2banRegexTest(LogCaptureTestCase):
self.assertTrue(fail2banRegex.start(opts, args))
self.assertLogged('Lines: 19 lines, 0 ignored, 16 matched, 3 missed')
def testDirectRE_1raw_noDns(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"--print-all-matched", "--raw", "--usedns=no",
Fail2banRegexTest.FILENAME_01,
Fail2banRegexTest.RE_00
)
self.assertTrue(fail2banRegex.start(opts, args))
self.assertLogged('Lines: 19 lines, 0 ignored, 13 matched, 6 missed')
def testDirectRE_2(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"--print-all-matched",

View File

@ -38,11 +38,11 @@ except ImportError:
from ..server.jail import Jail
from ..server.filterpoll import FilterPoll
from ..server.filter import Filter, FileFilter, FileContainer, locale
from ..server.filter import Filter, FileFilter, FileContainer
from ..server.failmanager import FailManagerEmpty
from ..server.ipdns import DNSUtils, IPAddr
from ..server.mytime import MyTime
from ..server.utils import Utils
from ..server.utils import Utils, uni_decode
from .utils import setUpMyTime, tearDownMyTime, mtimesleep, LogCaptureTestCase
from .dummyjail import DummyJail
@ -311,8 +311,7 @@ class BasicFilter(unittest.TestCase):
b'Fail for "g\xc3\xb6ran" from 192.0.2.1'
):
# join should work if all arguments have the same type:
enc = locale.getpreferredencoding()
"".join([Filter.uni_decode(v, enc) for v in (a1, a2, a3)])
"".join([uni_decode(v) for v in (a1, a2, a3)])
class IgnoreIP(LogCaptureTestCase):

View File

@ -35,7 +35,7 @@ from StringIO import StringIO
from utils import LogCaptureTestCase, logSys as DefLogSys
from ..helpers import formatExceptionInfo, mbasename, TraceBack, FormatterWithTraceBack, getLogger
from ..helpers import formatExceptionInfo, mbasename, TraceBack, FormatterWithTraceBack, getLogger, uni_decode
from ..helpers import splitwords
from ..server.datedetector import DateDetector
from ..server.datetemplate import DatePatternRegex
@ -74,16 +74,14 @@ class HelpersTest(unittest.TestCase):
if sys.version_info >= (2,7):
def _sh_call(cmd):
import subprocess, locale
import subprocess
ret = subprocess.check_output(cmd, shell=True)
if sys.version_info >= (3,):
ret = ret.decode(locale.getpreferredencoding(), 'replace')
return str(ret).rstrip()
return uni_decode(ret).rstrip()
else:
def _sh_call(cmd):
import subprocess
ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout.read()
return str(ret).rstrip()
return uni_decode(ret).rstrip()
def _getSysPythonVersion():
return _sh_call("fail2ban-python -c 'import sys; print(tuple(sys.version_info))'")
@ -286,6 +284,10 @@ class TestsUtilsTest(LogCaptureTestCase):
self.assertLogged, 'test_zyx', 'zyx', all=False)
self._testAssertionErrorRE(r"All of the .* were found present in the log",
self.assertNotLogged, 'test', 'xyz', all=False)
## assertDictEqual:
self.assertDictEqual({'A': [1, 2]}, {'A': [1, 2]})
self.assertRaises(AssertionError, self.assertDictEqual,
{'A': [1, 2]}, {'A': [2, 1]})
def testFormatterWithTraceBack(self):
strout = StringIO()

View File

@ -28,7 +28,6 @@ import unittest
import time
import tempfile
import os
import locale
import sys
import platform
@ -40,7 +39,7 @@ from ..server.jail import Jail
from ..server.jailthread import JailThread
from ..server.utils import Utils
from .utils import LogCaptureTestCase
from ..helpers import getLogger
from ..helpers import getLogger, PREFER_ENC
from .. import version
try:
@ -240,7 +239,7 @@ class Transmitter(TransmitterBase):
self.transm.proceed(["add", self.jailName, "polling"])[0], 1)
# All name is reserved
self.assertEqual(
self.transm.proceed(["add", "all", "polling"])[0], 1)
self.transm.proceed(["add", "--all", "polling"])[0], 1)
def testStartStopJail(self):
self.assertEqual(
@ -267,7 +266,7 @@ class Transmitter(TransmitterBase):
self.assertTrue( Utils.wait_for(
lambda: self.server.isAlive(2) and not isinstance(self.transm.proceed(["status", self.jailName]), RuntimeError),
3) )
self.assertEqual(self.transm.proceed(["stop", "all"]), (0, None))
self.assertEqual(self.transm.proceed(["stop", "--all"]), (0, None))
self.assertTrue( Utils.wait_for( lambda: not len(self.server._Server__jails), 3) )
self.assertNotIn(self.jailName, self.server._Server__jails)
self.assertNotIn("TestJail2", self.server._Server__jails)
@ -354,7 +353,7 @@ class Transmitter(TransmitterBase):
def testJailLogEncoding(self):
self.setGetTest("logencoding", "UTF-8", jail=self.jailName)
self.setGetTest("logencoding", "ascii", jail=self.jailName)
self.setGetTest("logencoding", "auto", locale.getpreferredencoding(),
self.setGetTest("logencoding", "auto", PREFER_ENC,
jail=self.jailName)
self.setGetTestNOK("logencoding", "Monkey", jail=self.jailName)
@ -843,6 +842,8 @@ class TransmitterLogging(TransmitterBase):
def testLogLevel(self):
self.setGetTest("loglevel", "HEAVYDEBUG")
self.setGetTest("loglevel", "TRACEDEBUG")
self.setGetTest("loglevel", "9")
self.setGetTest("loglevel", "DEBUG")
self.setGetTest("loglevel", "INFO")
self.setGetTest("loglevel", "NOTICE")

View File

@ -108,6 +108,24 @@ class TicketTests(unittest.TestCase):
self.assertEqual(ft2.getLastTime(), ft.getLastTime())
self.assertEqual(ft2.getBanTime(), ft.getBanTime())
def testTicketFlags(self):
flags = ('restored', 'banned')
ticket = Ticket('test', 0)
trueflags = []
for v in (True, False, True):
for f in flags:
setattr(ticket, f, v)
if v:
trueflags.append(f)
else:
trueflags.remove(f)
for f2 in flags:
self.assertEqual(bool(getattr(ticket, f2)), f2 in trueflags)
## inherite props from another tockets:
ticket = FailTicket(ticket=ticket)
for f2 in flags:
self.assertTrue(bool(getattr(ticket, f2)))
def testTicketData(self):
t = BanTicket('193.168.0.128', None, ['first', 'second'])
# expand data (no overwrites, matches are available) :

View File

@ -119,6 +119,7 @@ def getOptParser(doc=""):
def initProcess(opts):
# Logger:
global logSys
logSys = getLogger("fail2ban")
# Numerical level of verbosity corresponding to a log "level"
@ -242,6 +243,9 @@ def initTests(opts):
raise unittest.SkipTest('Skip test because of "--fast"')
unittest.F2B.SkipIfFast = F2B_SkipIfFast
else:
# smaller inertance inside test-cases (litle speedup):
Utils.DEFAULT_SLEEP_TIME = 0.25
Utils.DEFAULT_SLEEP_INTERVAL = 0.025
# sleep intervals are large - use replacement for sleep to check time to sleep:
_org_sleep = time.sleep
def _new_sleep(v):
@ -462,6 +466,20 @@ def gatherTests(regexps=None, opts=None):
# Forwards compatibility of unittest.TestCase for some early python versions
#
if not hasattr(unittest.TestCase, 'assertDictEqual'):
import difflib, pprint
def assertDictEqual(self, d1, d2, msg=None):
self.assert_(isinstance(d1, dict), 'First argument is not a dictionary')
self.assert_(isinstance(d2, dict), 'Second argument is not a dictionary')
if d1 != d2:
standardMsg = '%r != %r' % (d1, d2)
diff = ('\n' + '\n'.join(difflib.ndiff(
pprint.pformat(d1).splitlines(),
pprint.pformat(d2).splitlines())))
msg = msg or (standardMsg + diff)
self.fail(msg)
unittest.TestCase.assertDictEqual = assertDictEqual
if not hasattr(unittest.TestCase, 'assertRaisesRegexp'):
def assertRaisesRegexp(self, exccls, regexp, fun, *args, **kwargs):
try:
@ -577,7 +595,8 @@ class LogCaptureTestCase(unittest.TestCase):
print("")
logSys.handlers += self._old_handlers
logSys.debug('='*10 + ' %s ' + '='*20, self.id())
logSys.setLevel(logging.DEBUG)
else:
logSys.setLevel(logging.DEBUG)
def tearDown(self):
"""Call after every test case."""
@ -587,8 +606,21 @@ class LogCaptureTestCase(unittest.TestCase):
logSys.handlers = self._old_handlers
logSys.level = self._old_level
def _is_logged(self, s):
return s in self._log.getvalue()
def _is_logged(self, *s, **kwargs):
logged = self._log.getvalue()
if not kwargs.get('all', False):
# at least one entry should be found:
for s_ in s:
if s_ in logged:
return True
if True: # pragma: no cover
return False
else:
# each entry should be found:
for s_ in s:
if s_ not in logged: # pragma: no cover
return False
return True
def assertLogged(self, *s, **kwargs):
"""Assert that one of the strings was logged
@ -602,19 +634,23 @@ class LogCaptureTestCase(unittest.TestCase):
Test should succeed if string (or any of the listed) is present in the log
all : boolean (default False) if True should fail if any of s not logged
"""
logged = self._log.getvalue()
wait = kwargs.get('wait', None)
if wait:
res = Utils.wait_for(lambda: self._is_logged(*s, **kwargs), wait)
else:
res = self._is_logged(*s, **kwargs)
if not kwargs.get('all', False):
# at least one entry should be found:
for s_ in s:
if s_ in logged:
return
if True: # pragma: no cover
if not res: # pragma: no cover
logged = self._log.getvalue()
self.fail("None among %r was found in the log: ===\n%s===" % (s, logged))
else:
# each entry should be found:
for s_ in s:
if s_ not in logged: # pragma: no cover
self.fail("%r was not found in the log: ===\n%s===" % (s_, logged))
if not res: # pragma: no cover
logged = self._log.getvalue()
for s_ in s:
if s_ not in logged:
self.fail("%r was not found in the log: ===\n%s===" % (s_, logged))
def assertNotLogged(self, *s, **kwargs):
"""Assert that strings were not logged
@ -638,8 +674,10 @@ class LogCaptureTestCase(unittest.TestCase):
if s_ in logged: # pragma: no cover
self.fail("%r was found in the log: ===\n%s===" % (s_, logged))
def pruneLog(self):
def pruneLog(self, logphase=None):
self._log.truncate(0)
if logphase:
logSys.debug('='*5 + ' %s ' + '='*5, logphase)
def getLog(self):
return self._log.getvalue()
@ -649,9 +687,3 @@ class LogCaptureTestCase(unittest.TestCase):
pid_exists = Utils.pid_exists
# Python 2.6 compatibility. in 2.7 assertDictEqual
def assert_dict_equal(a, b):
assert isinstance(a, dict), "Object is not dictionary: %r" % a
assert isinstance(b, dict), "Object is not dictionary: %r" % b
assert a==b, "Dictionaries differ:\n%r !=\n%r" % (a, b)

View File

@ -1,12 +1,12 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.44.1.
.TH FAIL2BAN-CLIENT "1" "July 2016" "fail2ban-client v0.10.0a1" "User Commands"
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3.
.TH FAIL2BAN-CLIENT "1" "September 2016" "fail2ban-client v0.10.0a2" "User Commands"
.SH NAME
fail2ban-client \- configure and control the server
.SH SYNOPSIS
.B fail2ban-client
[\fIOPTIONS\fR] \fI<COMMAND>\fR
[\fI\,OPTIONS\/\fR] \fI\,<COMMAND>\/\fR
.SH DESCRIPTION
Fail2Ban v0.10.0a1 reads log file that contains password failure report
Fail2Ban v0.10.0a2 reads log file that contains password failure report
and bans the corresponding IP addresses using firewall rules.
.SH OPTIONS
.TP
@ -68,17 +68,36 @@ starts the server and the jails
\fBrestart\fR
restarts the server
.TP
\fBreload\fR
reloads the configuration without
restart
\fBrestart [\-\-unban] [\-\-if\-exists] <JAIL>\fR
restarts the jail <JAIL> (alias
for 'reload \fB\-\-restart\fR ... <JAIL>')
.TP
\fBreload <JAIL>\fR
reloads the jail <JAIL>
\fBreload [\-\-restart] [\-\-unban] [\-\-all]\fR
reloads the configuration without
restarting of the server, the
option '\-\-restart' activates
completely restarting of affected
jails, thereby can unban IP
addresses (if option '\-\-unban'
specified)
.TP
\fBreload [\-\-restart] [\-\-unban] [\-\-if\-exists] <JAIL>\fR
reloads the jail <JAIL>, or
restarts it (if option '\-\-restart'
specified)
.TP
\fBstop\fR
stops all jails and terminate the
server
.TP
\fBunban \fB\-\-all\fR\fR
unbans all IP addresses (in all
jails and database)
.TP
\fBunban <IP> ... <IP>\fR
unbans <IP> (in all jails and
database)
.TP
\fBstatus\fR
gets the current status of the
server
@ -101,7 +120,9 @@ LOGGING
\fBset loglevel <LEVEL>\fR
sets logging level to <LEVEL>.
Levels: CRITICAL, ERROR, WARNING,
NOTICE, INFO, DEBUG
NOTICE, INFO, DEBUG, TRACEDEBUG,
HEAVYDEBUG or corresponding
numeric value (50\-5)
.TP
\fBget loglevel\fR
gets the logging level
@ -248,9 +269,8 @@ for <JAIL>
\fBset <JAIL> maxlines <LINES>\fR
sets the number of <LINES> to
buffer for regex search for <JAIL>
.IP
set <JAIL> addaction <ACT>[ <PYTHONFILE> <JSONKWARGS>]
.IP
.TP
\fBset <JAIL> addaction <ACT>[ <PYTHONFILE> <JSONKWARGS>]\fR
adds a new action named <ACT> for
<JAIL>. Optionally for a Python
based action, a <PYTHONFILE> and
@ -262,45 +282,38 @@ removes the action <ACT> from
<JAIL>
.IP
COMMAND ACTION CONFIGURATION
.IP
set <JAIL> action <ACT> actionstart <CMD>
.IP
.TP
\fBset <JAIL> action <ACT> actionstart <CMD>\fR
sets the start command <CMD> of
the action <ACT> for <JAIL>
.IP
set <JAIL> action <ACT> actionstop <CMD> sets the stop command <CMD> of the
.IP
.TP
\fBset <JAIL> action <ACT> actionstop <CMD> sets the stop command <CMD> of the\fR
action <ACT> for <JAIL>
.IP
set <JAIL> action <ACT> actioncheck <CMD>
.IP
.TP
\fBset <JAIL> action <ACT> actioncheck <CMD>\fR
sets the check command <CMD> of
the action <ACT> for <JAIL>
.TP
\fBset <JAIL> action <ACT> actionban <CMD>\fR
sets the ban command <CMD> of the
action <ACT> for <JAIL>
.IP
set <JAIL> action <ACT> actionunban <CMD>
.IP
.TP
\fBset <JAIL> action <ACT> actionunban <CMD>\fR
sets the unban command <CMD> of
the action <ACT> for <JAIL>
.IP
set <JAIL> action <ACT> timeout <TIMEOUT>
.IP
.TP
\fBset <JAIL> action <ACT> timeout <TIMEOUT>\fR
sets <TIMEOUT> as the command
timeout in seconds for the action
<ACT> for <JAIL>
.IP
GENERAL ACTION CONFIGURATION
.IP
set <JAIL> action <ACT> <PROPERTY> <VALUE>
.IP
.TP
\fBset <JAIL> action <ACT> <PROPERTY> <VALUE>\fR
sets the <VALUE> of <PROPERTY> for
the action <ACT> for <JAIL>
.IP
set <JAIL> action <ACT> <METHOD>[ <JSONKWARGS>]
.IP
.TP
\fBset <JAIL> action <ACT> <METHOD>[ <JSONKWARGS>]\fR
calls the <METHOD> with
<JSONKWARGS> for the action <ACT>
for <JAIL>

View File

@ -1,10 +1,10 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.44.1.
.TH FAIL2BAN-REGEX "1" "July 2016" "fail2ban-regex 0.10.0a1" "User Commands"
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3.
.TH FAIL2BAN-REGEX "1" "September 2016" "fail2ban-regex 0.10.0a2" "User Commands"
.SH NAME
fail2ban-regex \- test Fail2ban "failregex" option
.SH SYNOPSIS
.B fail2ban-regex
[\fIOPTIONS\fR] \fI<LOG> <REGEX> \fR[\fIIGNOREREGEX\fR]
[\fI\,OPTIONS\/\fR] \fI\,<LOG> <REGEX> \/\fR[\fI\,IGNOREREGEX\/\fR]
.SH DESCRIPTION
Fail2Ban reads log file that contains password failure report
and bans the corresponding IP addresses using firewall rules.
@ -16,7 +16,7 @@ string
a string representing a log line
.TP
filename
path to a log file (\fI/var/log/auth.log\fP)
path to a log file (\fI\,/var/log/auth.log\/\fP)
.TP
"systemd\-journal"
search systemd journal (systemd\-python required)
@ -42,23 +42,28 @@ show program's version number and exit
\fB\-h\fR, \fB\-\-help\fR
show this help message and exit
.TP
\fB\-d\fR DATEPATTERN, \fB\-\-datepattern\fR=\fIDATEPATTERN\fR
\fB\-d\fR DATEPATTERN, \fB\-\-datepattern\fR=\fI\,DATEPATTERN\/\fR
set custom pattern used to match date/times
.TP
\fB\-e\fR ENCODING, \fB\-\-encoding\fR=\fIENCODING\fR
\fB\-e\fR ENCODING, \fB\-\-encoding\fR=\fI\,ENCODING\/\fR
File encoding. Default: system locale
.TP
\fB\-r\fR, \fB\-\-raw\fR
Raw hosts, don't resolve dns
.TP
\fB\-L\fR MAXLINES, \fB\-\-maxlines\fR=\fIMAXLINES\fR
\fB\-\-usedns\fR=\fI\,USEDNS\/\fR
DNS specified replacement of tags <HOST> in regexp
('yes' \- matches all form of hosts, 'no' \- IP
addresses only)
.TP
\fB\-L\fR MAXLINES, \fB\-\-maxlines\fR=\fI\,MAXLINES\/\fR
maxlines for multi\-line regex
.TP
\fB\-m\fR JOURNALMATCH, \fB\-\-journalmatch\fR=\fIJOURNALMATCH\fR
\fB\-m\fR JOURNALMATCH, \fB\-\-journalmatch\fR=\fI\,JOURNALMATCH\/\fR
journalctl style matches overriding filter file.
"systemd\-journal" only
.TP
\fB\-l\fR LOG_LEVEL, \fB\-\-log\-level\fR=\fILOG_LEVEL\fR
\fB\-l\fR LOG_LEVEL, \fB\-\-log\-level\fR=\fI\,LOG_LEVEL\/\fR
Log level for the Fail2Ban logger to use
.TP
\fB\-v\fR, \fB\-\-verbose\fR

View File

@ -1,12 +1,12 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.44.1.
.TH FAIL2BAN-SERVER "1" "July 2016" "fail2ban-server v0.10.0a1" "User Commands"
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3.
.TH FAIL2BAN-SERVER "1" "September 2016" "fail2ban-server v0.10.0a2" "User Commands"
.SH NAME
fail2ban-server \- start the server
.SH SYNOPSIS
.B fail2ban-server
[\fIOPTIONS\fR]
[\fI\,OPTIONS\/\fR]
.SH DESCRIPTION
Fail2Ban v0.10.0a1 reads log file that contains password failure report
Fail2Ban v0.10.0a2 reads log file that contains password failure report
and bans the corresponding IP addresses using firewall rules.
.SH OPTIONS
.TP

View File

@ -1,10 +1,10 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.44.1.
.TH FAIL2BAN-TESTCASES "1" "July 2016" "fail2ban-testcases 0.10.0a1" "User Commands"
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3.
.TH FAIL2BAN-TESTCASES "1" "September 2016" "fail2ban-testcases 0.10.0a2" "User Commands"
.SH NAME
fail2ban-testcases \- run Fail2Ban unit-tests
.SH SYNOPSIS
.B fail2ban-testcases
[\fIOPTIONS\fR] [\fIregexps\fR]
[\fI\,OPTIONS\/\fR] [\fI\,regexps\/\fR]
.SH DESCRIPTION
Script to run Fail2Ban tests battery
.SH OPTIONS
@ -15,9 +15,15 @@ show program's version number and exit
\fB\-h\fR, \fB\-\-help\fR
show this help message and exit
.TP
\fB\-l\fR LOG_LEVEL, \fB\-\-log\-level\fR=\fILOG_LEVEL\fR
\fB\-l\fR LOG_LEVEL, \fB\-\-log\-level\fR=\fI\,LOG_LEVEL\/\fR
Log level for the logger to use during running tests
.TP
\fB\-v\fR VERBOSITY, \fB\-\-verbosity\fR=\fI\,VERBOSITY\/\fR
Set numerical level of verbosity (0..4)
.TP
\fB\-\-log\-direct\fR
Prevent lazy logging inside tests
.TP
\fB\-n\fR, \fB\-\-no\-network\fR
Do not run tests that require the network
.TP

View File

@ -127,7 +127,7 @@ These files have one section, [Definition].
The items that can be set are:
.TP
.B loglevel
verbosity level of log output: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG. Default: ERROR
verbosity level of log output: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, TRACEDEBUG, HEAVYDEBUG or corresponding numeric value (50-5). Default: ERROR (equal 40)
.TP
.B logtarget
log target: filename, SYSLOG, STDERR or STDOUT. Default: STDERR