Merge remote-tracking branch 'remotes/gh-origin/f2b-perfom-prepare-716-cs-0.10' into 0.10-full

pull/1460/head
sebres 2016-06-09 22:13:48 +02:00
commit e3ab10196e
21 changed files with 1800 additions and 683 deletions

View File

@ -156,8 +156,11 @@ fail2ban/client/configparserinc.py
fail2ban/client/configreader.py
fail2ban/client/configurator.py
fail2ban/client/csocket.py
fail2ban/client/fail2banclient.py
fail2ban/client/fail2bancmdline.py
fail2ban/client/fail2banreader.py
fail2ban/client/fail2banregex.py
fail2ban/client/fail2banserver.py
fail2ban/client/filterreader.py
fail2ban/client/__init__.py
fail2ban/client/jailreader.py

View File

@ -18,458 +18,20 @@
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
__author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
"""
Fail2Ban reads log file that contains password failure report
and bans the corresponding IP addresses using firewall rules.
This tools starts/stops fail2ban server or does client/server communication,
to change/read parameters of the server or jails.
"""
__author__ = "Fail2Ban Developers"
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
__license__ = "GPL"
import getopt
import logging
import os
import pickle
import re
import shlex
import signal
import socket
import string
import sys
import time
from fail2ban.client.fail2banclient import exec_command_line, sys
from fail2ban.version import version
from fail2ban.protocol import printFormatted
from fail2ban.client.csocket import CSocket
from fail2ban.client.configurator import Configurator
from fail2ban.client.beautifier import Beautifier
from fail2ban.helpers import getLogger
# Gets the instance of the logger.
logSys = getLogger("fail2ban")
##
#
# @todo This class needs cleanup.
class Fail2banClient:
SERVER = "fail2ban-server"
PROMPT = "fail2ban> "
def __init__(self):
self.__argv = None
self.__stream = None
self.__configurator = Configurator()
self.__conf = dict()
self.__conf["conf"] = "/etc/fail2ban"
self.__conf["dump"] = False
self.__conf["force"] = False
self.__conf["background"] = True
self.__conf["verbose"] = 1
self.__conf["interactive"] = False
self.__conf["socket"] = None
self.__conf["pidfile"] = None
def dispVersion(self):
print "Fail2Ban v" + version
print
print "Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors"
print "Copyright of modifications held by their respective authors."
print "Licensed under the GNU General Public License v2 (GPL)."
print
print "Written by Cyril Jaquier <cyril.jaquier@fail2ban.org>."
print "Many contributions by Yaroslav O. Halchenko <debian@onerussian.com>."
def dispUsage(self):
""" Prints Fail2Ban command line options and exits
"""
print "Usage: "+self.__argv[0]+" [OPTIONS] <COMMAND>"
print
print "Fail2Ban v" + version + " reads log file that contains password failure report"
print "and bans the corresponding IP addresses using firewall rules."
print
print "Options:"
print " -c <DIR> configuration directory"
print " -s <FILE> socket path"
print " -p <FILE> pidfile path"
print " -d dump configuration. For debugging"
print " -i interactive mode"
print " -v increase verbosity"
print " -q decrease verbosity"
print " -x force execution of the server (remove socket file)"
print " -b start server in background (default)"
print " -f start server in foreground (note that the client forks once itself)"
print " -h, --help display this help message"
print " -V, --version print the version"
print
print "Command:"
# Prints the protocol
printFormatted()
print
print "Report bugs to https://github.com/fail2ban/fail2ban/issues"
def dispInteractive(self):
print "Fail2Ban v" + version + " reads log file that contains password failure report"
print "and bans the corresponding IP addresses using firewall rules."
print
def __sigTERMhandler(self, signum, frame):
# Print a new line because we probably come from wait
print
logSys.warning("Caught signal %d. Exiting" % signum)
sys.exit(-1)
def __getCmdLineOptions(self, optList):
""" Gets the command line options
"""
for opt in optList:
if opt[0] == "-c":
self.__conf["conf"] = opt[1]
elif opt[0] == "-s":
self.__conf["socket"] = opt[1]
elif opt[0] == "-p":
self.__conf["pidfile"] = opt[1]
elif opt[0] == "-d":
self.__conf["dump"] = True
elif opt[0] == "-v":
self.__conf["verbose"] = self.__conf["verbose"] + 1
elif opt[0] == "-q":
self.__conf["verbose"] = self.__conf["verbose"] - 1
elif opt[0] == "-x":
self.__conf["force"] = True
elif opt[0] == "-i":
self.__conf["interactive"] = True
elif opt[0] == "-b":
self.__conf["background"] = True
elif opt[0] == "-f":
self.__conf["background"] = False
elif opt[0] in ["-h", "--help"]:
self.dispUsage()
sys.exit(0)
elif opt[0] in ["-V", "--version"]:
self.dispVersion()
sys.exit(0)
def __ping(self):
return self.__processCmd([["ping"]], False)
def __processCmd(self, cmd, showRet = True):
client = None
try:
beautifier = Beautifier()
streamRet = True
for c in cmd:
beautifier.setInputCmd(c)
try:
if not client:
client = CSocket(self.__conf["socket"])
ret = client.send(c)
if ret[0] == 0:
logSys.debug("OK : " + `ret[1]`)
if showRet:
print beautifier.beautify(ret[1])
else:
logSys.error("NOK: " + `ret[1].args`)
if showRet:
print beautifier.beautifyError(ret[1])
streamRet = False
except socket.error:
if showRet:
self.__logSocketError()
return False
except Exception, e:
if showRet:
logSys.error(e)
return False
finally:
if client:
client.close()
return streamRet
def __logSocketError(self):
try:
if os.access(self.__conf["socket"], os.F_OK):
# This doesn't check if path is a socket,
# but socket.error should be raised
if os.access(self.__conf["socket"], os.W_OK):
# Permissions look good, but socket.error was raised
logSys.error("Unable to contact server. Is it running?")
else:
logSys.error("Permission denied to socket: %s,"
" (you must be root)", self.__conf["socket"])
else:
logSys.error("Failed to access socket path: %s."
" Is fail2ban running?",
self.__conf["socket"])
except Exception as e:
logSys.error("Exception while checking socket access: %s",
self.__conf["socket"])
logSys.error(e)
##
# Process a command line.
#
# Process one command line and exit.
# @param cmd the command line
def __processCommand(self, cmd):
if len(cmd) == 1 and cmd[0] == "start":
if self.__ping():
logSys.error("Server already running")
return False
else:
# Read the config
ret = self.__readConfig()
# Do not continue if configuration is not 100% valid
if not ret:
return False
# verify that directory for the socket file exists
socket_dir = os.path.dirname(self.__conf["socket"])
if not os.path.exists(socket_dir):
logSys.error(
"There is no directory %s to contain the socket file %s."
% (socket_dir, self.__conf["socket"]))
return False
if not os.access(socket_dir, os.W_OK | os.X_OK):
logSys.error(
"Directory %s exists but not accessible for writing"
% (socket_dir,))
return False
# Start the server
self.__startServerAsync(self.__conf["socket"],
self.__conf["pidfile"],
self.__conf["force"],
self.__conf["background"])
try:
# Wait for the server to start
self.__waitOnServer()
# Configure the server
self.__processCmd(self.__stream, False)
return True
except ServerExecutionException:
logSys.error("Could not start server. Maybe an old "
"socket file is still present. Try to "
"remove " + self.__conf["socket"] + ". If "
"you used fail2ban-client to start the "
"server, adding the -x option will do it")
return False
elif len(cmd) == 1 and cmd[0] == "reload":
if self.__ping():
ret = self.__readConfig()
# Do not continue if configuration is not 100% valid
if not ret:
return False
self.__processCmd([['stop', 'all']], False)
# Configure the server
return self.__processCmd(self.__stream, False)
else:
logSys.error("Could not find server")
return False
elif len(cmd) == 2 and cmd[0] == "reload":
if self.__ping():
jail = cmd[1]
ret = self.__readConfig(jail)
# 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(self.__stream, False)
else:
logSys.error("Could not find server")
return False
else:
return self.__processCmd([cmd])
##
# Start Fail2Ban server.
#
# Start the Fail2ban server in daemon mode.
def __startServerAsync(self, socket, pidfile, force = False, background = True):
# Forks the current process.
pid = os.fork()
if pid == 0:
args = list()
args.append(self.SERVER)
# Set the socket path.
args.append("-s")
args.append(socket)
# Set the pidfile
args.append("-p")
args.append(pidfile)
# Force the execution if needed.
if force:
args.append("-x")
# Start in foreground mode if requested.
if background:
args.append("-b")
else:
args.append("-f")
try:
# Use the current directory.
exe = os.path.abspath(os.path.join(sys.path[0], self.SERVER))
logSys.debug("Starting %r with args %r" % (exe, args))
os.execv(exe, args)
except OSError:
try:
# Use the PATH env.
logSys.warning("Initial start attempt failed. Starting %r with the same args" % (self.SERVER,))
os.execvp(self.SERVER, args)
except OSError:
logSys.error("Could not start %s" % self.SERVER)
os.exit(-1)
def __waitOnServer(self):
# Wait for the server to start
cnt = 0
if self.__conf["verbose"] > 1:
pos = 0
delta = 1
mask = "[ ]"
while not self.__ping():
# Wonderful visual :)
if self.__conf["verbose"] > 1:
pos += delta
sys.stdout.write("\rINFO " + mask[:pos] + '#' + mask[pos+1:] +
" Waiting on the server...")
sys.stdout.flush()
if pos > len(mask)-3:
delta = -1
elif pos < 2:
delta = 1
# The server has 30 seconds to start.
if cnt >= 300:
if self.__conf["verbose"] > 1:
sys.stdout.write('\n')
raise ServerExecutionException("Failed to start server")
time.sleep(0.1)
cnt += 1
if self.__conf["verbose"] > 1:
sys.stdout.write('\n')
def start(self, argv):
# Command line options
self.__argv = argv
# Install signal handlers
signal.signal(signal.SIGTERM, self.__sigTERMhandler)
signal.signal(signal.SIGINT, self.__sigTERMhandler)
# Reads the command line options.
try:
cmdOpts = 'hc:s:p:xfbdviqV'
cmdLongOpts = ['help', 'version']
optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts)
except getopt.GetoptError:
self.dispUsage()
return False
self.__getCmdLineOptions(optList)
verbose = self.__conf["verbose"]
if verbose <= 0:
logSys.setLevel(logging.ERROR)
elif verbose == 1:
logSys.setLevel(logging.WARNING)
elif verbose == 2:
logSys.setLevel(logging.INFO)
elif verbose == 3:
logSys.setLevel(logging.DEBUG)
else:
logSys.setLevel(logging.HEAVYDEBUG)
# Add the default logging handler to dump to stderr
logout = logging.StreamHandler(sys.stderr)
# set a format which is simpler for console use
formatter = logging.Formatter('%(levelname)-6s %(message)s')
# tell the handler to use this format
logout.setFormatter(formatter)
logSys.addHandler(logout)
# Set the configuration path
self.__configurator.setBaseDir(self.__conf["conf"])
# Set socket path
self.__configurator.readEarly()
conf = self.__configurator.getEarlyOptions()
if self.__conf["socket"] is None:
self.__conf["socket"] = conf["socket"]
if self.__conf["pidfile"] is None:
self.__conf["pidfile"] = conf["pidfile"]
logSys.info("Using socket file " + self.__conf["socket"])
if self.__conf["dump"]:
ret = self.__readConfig()
self.dumpConfig(self.__stream)
return ret
# Interactive mode
if self.__conf["interactive"]:
try:
import readline
except ImportError:
logSys.error("Readline not available")
return False
try:
ret = True
if len(args) > 0:
ret = self.__processCommand(args)
if ret:
readline.parse_and_bind("tab: complete")
self.dispInteractive()
while True:
cmd = raw_input(self.PROMPT)
if cmd == "exit" or cmd == "quit":
# Exit
return True
if cmd == "help":
self.dispUsage()
elif not cmd == "":
try:
self.__processCommand(shlex.split(cmd))
except Exception, e:
logSys.error(e)
except (EOFError, KeyboardInterrupt):
print
return True
# Single command mode
else:
if len(args) < 1:
self.dispUsage()
return False
return self.__processCommand(args)
def __readConfig(self, jail=None):
# Read the configuration
# TODO: get away from stew of return codes and exception
# handling -- handle via exceptions
try:
self.__configurator.Reload()
self.__configurator.readAll()
ret = self.__configurator.getOptions(jail)
self.__configurator.convertToProtocol()
self.__stream = self.__configurator.getConfigStream()
except Exception, e:
logSys.error("Failed during configuration: %s" % e)
ret = False
return ret
@staticmethod
def dumpConfig(cmd):
for c in cmd:
print c
return True
class ServerExecutionException(Exception):
pass
if __name__ == "__main__": # pragma: no cover - can't test main
client = Fail2banClient()
# Exit with correct return value
if client.start(sys.argv):
sys.exit(0)
else:
sys.exit(-1)
if __name__ == "__main__":
exec_command_line(sys.argv)

View File

@ -18,123 +18,20 @@
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
__author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
"""
Fail2Ban reads log file that contains password failure report
and bans the corresponding IP addresses using firewall rules.
This tools starts/stops fail2ban server or does client/server communication,
to change/read parameters of the server or jails.
"""
__author__ = "Fail2Ban Developers"
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
__license__ = "GPL"
import getopt
import os
import sys
from fail2ban.version import version
from fail2ban.server.server import Server
from fail2ban.helpers import getLogger
# Gets the instance of the logger.
logSys = getLogger("fail2ban")
##
# \mainpage Fail2Ban
#
# \section Introduction
#
# Fail2ban is designed to protect your server against brute force attacks.
# Its first goal was to protect a SSH server.
class Fail2banServer:
def __init__(self):
self.__server = None
self.__argv = None
self.__conf = dict()
self.__conf["background"] = True
self.__conf["force"] = False
self.__conf["socket"] = "/var/run/fail2ban/fail2ban.sock"
self.__conf["pidfile"] = "/var/run/fail2ban/fail2ban.pid"
def dispVersion(self):
print "Fail2Ban v" + version
print
print "Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors"
print "Copyright of modifications held by their respective authors."
print "Licensed under the GNU General Public License v2 (GPL)."
print
print "Written by Cyril Jaquier <cyril.jaquier@fail2ban.org>."
print "Many contributions by Yaroslav O. Halchenko <debian@onerussian.com>."
def dispUsage(self):
""" Prints Fail2Ban command line options and exits
"""
print "Usage: "+self.__argv[0]+" [OPTIONS]"
print
print "Fail2Ban v" + version + " reads log file that contains password failure report"
print "and bans the corresponding IP addresses using firewall rules."
print
print "Only use this command for debugging purpose. Start the server with"
print "fail2ban-client instead. The default behaviour is to start the server"
print "in background."
print
print "Options:"
print " -b start in background"
print " -f start in foreground"
print " -s <FILE> socket path"
print " -p <FILE> pidfile path"
print " -x force execution of the server (remove socket file)"
print " -h, --help display this help message"
print " -V, --version print the version"
print
print "Report bugs to https://github.com/fail2ban/fail2ban/issues"
def __getCmdLineOptions(self, optList):
""" Gets the command line options
"""
for opt in optList:
if opt[0] == "-b":
self.__conf["background"] = True
if opt[0] == "-f":
self.__conf["background"] = False
if opt[0] == "-s":
self.__conf["socket"] = opt[1]
if opt[0] == "-p":
self.__conf["pidfile"] = opt[1]
if opt[0] == "-x":
self.__conf["force"] = True
if opt[0] in ["-h", "--help"]:
self.dispUsage()
sys.exit(0)
if opt[0] in ["-V", "--version"]:
self.dispVersion()
sys.exit(0)
def start(self, argv):
# Command line options
self.__argv = argv
# Reads the command line options.
try:
cmdOpts = 'bfs:p:xhV'
cmdLongOpts = ['help', 'version']
optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts)
except getopt.GetoptError:
self.dispUsage()
sys.exit(-1)
self.__getCmdLineOptions(optList)
try:
self.__server = Server(self.__conf["background"])
self.__server.start(self.__conf["socket"],
self.__conf["pidfile"],
self.__conf["force"])
return True
except Exception, e:
logSys.exception(e)
self.__server.quit()
return False
from fail2ban.client.fail2banserver import exec_command_line, sys
if __name__ == "__main__":
server = Fail2banServer()
if server.start(sys.argv):
sys.exit(0)
else:
sys.exit(-1)
exec_command_line(sys.argv)

View File

@ -119,10 +119,14 @@ else:
# Custom log format for the verbose tests runs
if verbosity > 1: # pragma: no cover
stdout.setFormatter(Formatter(' %(asctime)-15s %(thread)s' + fmt))
else: # pragma: no cover
# just prefix with the space
stdout.setFormatter(Formatter(fmt))
if verbosity > 3:
fmt = ' | %(module)15.15s-%(levelno)-2d: %(funcName)-20.20s |' + fmt
if verbosity > 2:
fmt = ' +%(relativeCreated)5d %(thread)X %(levelname)-5.5s' + fmt
else:
fmt = ' %(asctime)-15s %(thread)X %(levelname)-5.5s' + fmt
#
stdout.setFormatter(Formatter(fmt))
logSys.addHandler(stdout)
#
@ -130,7 +134,7 @@ logSys.addHandler(stdout)
#
if not opts.log_level or opts.log_level != 'critical': # pragma: no cover
print("Fail2ban %s test suite. Python %s. Please wait..." \
% (version, str(sys.version).replace('\n', '')))
% (version, str(sys.version).replace('\n', '')))
tests = gatherTests(regexps, opts)
#

View File

@ -68,6 +68,8 @@ class Beautifier:
msg = "Added jail " + response
elif inC[0] == "flushlogs":
msg = "logs: " + response
elif inC[0] == "echo":
msg = ' '.join(msg)
elif inC[0:1] == ['status']:
if len(inC) > 1:
# Display information

View File

@ -208,7 +208,7 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes):
# Or it is a dict:
# {name: [type, default], ...}
def getOptions(self, sec, options, pOptions=None):
def getOptions(self, sec, options, pOptions=None, shouldExist=False):
values = dict()
for optname in options:
if isinstance(options, (list,tuple)):
@ -229,6 +229,8 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes):
continue
values[optname] = v
except NoSectionError, e:
if shouldExist:
raise
# No "Definition" section or wrong basedir
logSys.error(e)
values[optname] = optvalue

462
fail2ban/client/fail2banclient.py Executable file
View File

@ -0,0 +1,462 @@
#!/usr/bin/python
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
# vi: set ft=python sts=4 ts=4 sw=4 noet :
#
# This file is part of Fail2Ban.
#
# Fail2Ban is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Fail2Ban is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
__author__ = "Fail2Ban Developers"
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
__license__ = "GPL"
import os
import shlex
import signal
import socket
import sys
import time
import threading
from threading import Thread
from ..version import version
from .csocket import CSocket
from .beautifier import Beautifier
from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, ExitException, \
logSys, PRODUCTION, exit, output
PROMPT = "fail2ban> "
def _thread_name():
return threading.current_thread().__class__.__name__
def input_command(): # pragma: no cover
return raw_input(PROMPT)
##
#
# @todo This class needs cleanup.
class Fail2banClient(Fail2banCmdLine, Thread):
def __init__(self):
Fail2banCmdLine.__init__(self)
Thread.__init__(self)
self._alive = True
self._server = None
self._beautifier = None
def dispInteractive(self):
output("Fail2Ban v" + version + " reads log file that contains password failure report")
output("and bans the corresponding IP addresses using firewall rules.")
output("")
def __sigTERMhandler(self, signum, frame): # pragma: no cover
# Print a new line because we probably come from wait
output("")
logSys.warning("Caught signal %d. Exiting" % signum)
exit(-1)
def __ping(self):
return self.__processCmd([["ping"]], False)
@property
def beautifier(self):
if self._beautifier:
return self._beautifier
self._beautifier = Beautifier()
return self._beautifier
def __processCmd(self, cmd, showRet=True):
client = None
try:
beautifier = self.beautifier
streamRet = True
for c in cmd:
beautifier.setInputCmd(c)
try:
if not client:
client = CSocket(self._conf["socket"])
ret = client.send(c)
if ret[0] == 0:
logSys.debug("OK : %r", ret[1])
if showRet or c[0] == 'echo':
output(beautifier.beautify(ret[1]))
else:
logSys.error("NOK: %r", ret[1].args)
if showRet:
output(beautifier.beautifyError(ret[1]))
streamRet = False
except socket.error as e:
if showRet or self._conf["verbose"] > 1:
if showRet or c != ["ping"]:
self.__logSocketError()
else:
logSys.debug(" -- ping failed -- %r", e)
return False
except Exception as e: # pragma: no cover
if showRet or self._conf["verbose"] > 1:
if self._conf["verbose"] > 1:
logSys.exception(e)
else:
logSys.error(e)
return False
finally:
# prevent errors by close during shutdown (on exit command):
if client:
try :
client.close()
except Exception as e:
if showRet or self._conf["verbose"] > 1:
logSys.debug(e)
if showRet or c[0] == 'echo':
sys.stdout.flush()
return streamRet
def __logSocketError(self):
try:
if os.access(self._conf["socket"], os.F_OK): # pragma: no cover
# This doesn't check if path is a socket,
# but socket.error should be raised
if os.access(self._conf["socket"], os.W_OK):
# Permissions look good, but socket.error was raised
logSys.error("Unable to contact server. Is it running?")
else:
logSys.error("Permission denied to socket: %s,"
" (you must be root)", self._conf["socket"])
else:
logSys.error("Failed to access socket path: %s."
" Is fail2ban running?",
self._conf["socket"])
except Exception as e: # pragma: no cover
logSys.error("Exception while checking socket access: %s",
self._conf["socket"])
logSys.error(e)
##
def __prepareStartServer(self):
if self.__ping():
logSys.error("Server already running")
return None
# Read the config
ret, stream = self.readConfig()
# Do not continue if configuration is not 100% valid
if not ret:
return None
# verify that directory for the socket file exists
socket_dir = os.path.dirname(self._conf["socket"])
if not os.path.exists(socket_dir):
logSys.error(
"There is no directory %s to contain the socket file %s."
% (socket_dir, self._conf["socket"]))
return None
if not os.access(socket_dir, os.W_OK | os.X_OK): # pragma: no cover
logSys.error(
"Directory %s exists but not accessible for writing"
% (socket_dir,))
return None
# Check already running
if not self._conf["force"] and os.path.exists(self._conf["socket"]):
logSys.error("Fail2ban seems to be in unexpected state (not running but the socket exists)")
return None
stream.append(['echo', 'Server ready'])
return stream
##
def __startServer(self, background=True):
from .fail2banserver import Fail2banServer
stream = self.__prepareStartServer()
self._alive = True
if not stream:
return False
# Start the server or just initialize started one:
try:
if background:
# Start server daemon as fork of client process:
Fail2banServer.startServerAsync(self._conf)
# Send config stream to server:
if not self.__processStartStreamAfterWait(stream, False):
return False
else:
# In foreground mode we should make server/client communication in different threads:
th = Thread(target=Fail2banClient.__processStartStreamAfterWait, args=(self, stream, False))
th.daemon = True
th.start()
# Mark current (main) thread as daemon:
self.setDaemon(True)
# Start server direct here in main thread (not fork):
self._server = Fail2banServer.startServerDirect(self._conf, False)
except ExitException: # pragma: no cover
pass
except Exception as e: # pragma: no cover
output("")
logSys.error("Exception while starting server " + ("background" if background else "foreground"))
if self._conf["verbose"] > 1:
logSys.exception(e)
else:
logSys.error(e)
return False
return True
##
def configureServer(self, async=True, phase=None):
# if asynchron start this operation in the new thread:
if async:
th = Thread(target=Fail2banClient.configureServer, args=(self, False, phase))
th.daemon = True
return th.start()
# prepare: read config, check configuration is valid, etc.:
if phase is not None:
phase['start'] = True
logSys.debug('-- 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)
if not stream:
return False
# configure server with config stream:
ret = self.__processStartStreamAfterWait(stream, False)
if phase is not None:
phase['done'] = ret
return ret
##
# Process a command line.
#
# Process one command line and exit.
# @param cmd the command line
def __processCommand(self, cmd):
if len(cmd) == 1 and cmd[0] == "start":
ret = self.__startServer(self._conf["background"])
if not ret:
return False
return ret
elif len(cmd) == 1 and cmd[0] == "restart":
if self._conf.get("interactive", False):
output(' ## stop ... ')
self.__processCommand(['stop'])
if not self.__waitOnServer(False): # pragma: no cover
logSys.error("Could not stop server")
return False
# in interactive mode reset config, to make full-reload if there something changed:
if self._conf.get("interactive", False):
output(' ## load configuration ... ')
self.resetConf()
ret = self.initCmdLine(self._argv)
if ret is not None:
return ret
if self._conf.get("interactive", False):
output(' ## start ... ')
return self.__processCommand(['start'])
elif len(cmd) >= 1 and cmd[0] == "reload":
if self.__ping():
if len(cmd) == 1:
jail = 'all'
ret, stream = self.readConfig()
else:
jail = cmd[1]
ret, stream = self.readConfig(jail)
# 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)
else:
logSys.error("Could not find server")
return False
else:
return self.__processCmd([cmd])
def __processStartStreamAfterWait(self, *args):
try:
# Wait for the server to start
if not self.__waitOnServer(): # pragma: no cover
logSys.error("Could not find server, waiting failed")
return False
# Configure the server
self.__processCmd(*args)
except ServerExecutionException as e: # pragma: no cover
if self._conf["verbose"] > 1:
logSys.exception(e)
logSys.error("Could not start server. Maybe an old "
"socket file is still present. Try to "
"remove " + self._conf["socket"] + ". If "
"you used fail2ban-client to start the "
"server, adding the -x option will do it")
if self._server:
self._server.quit()
return False
return True
def __waitOnServer(self, alive=True, maxtime=None):
if maxtime is None:
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))
test = lambda: os.path.exists(self._conf["socket"]) and self.__ping()
with VisualWait(self._conf["verbose"]) as vis:
sltime = 0.0125 / 2
while self._alive:
runf = test()
if runf == alive:
return True
now = time.time()
# Wonderful visual :)
if now > starttime + 1:
vis.heartbeat()
# f end time reached:
if now - starttime >= maxtime:
raise ServerExecutionException("Failed to start server")
sltime = min(sltime * 2, 0.5)
time.sleep(sltime)
return False
def start(self, argv):
# Install signal handlers
_prev_signals = {}
if _thread_name() == '_MainThread':
for s in (signal.SIGTERM, signal.SIGINT):
_prev_signals[s] = signal.getsignal(s)
signal.signal(s, self.__sigTERMhandler)
try:
# Command line options
if self._argv is None:
ret = self.initCmdLine(argv)
if ret is not None:
if ret:
return True
raise ServerExecutionException("Init of command line failed")
# Commands
args = self._args
# Interactive mode
if self._conf.get("interactive", False):
# no readline in test:
if PRODUCTION: # pragma: no cover
try:
import readline
except ImportError:
raise ServerExecutionException("Readline not available")
try:
ret = True
if len(args) > 0:
ret = self.__processCommand(args)
if ret:
if PRODUCTION: # pragma: no cover
readline.parse_and_bind("tab: complete")
self.dispInteractive()
while True:
cmd = input_command()
if cmd == "exit" or cmd == "quit":
# Exit
return True
if cmd == "help":
self.dispUsage()
elif not cmd == "":
try:
self.__processCommand(shlex.split(cmd))
except Exception, e: # pragma: no cover
if self._conf["verbose"] > 1:
logSys.exception(e)
else:
logSys.error(e)
except (EOFError, KeyboardInterrupt): # pragma: no cover
output("")
raise
# Single command mode
else:
if len(args) < 1:
self.dispUsage()
return False
return self.__processCommand(args)
except Exception as e:
if self._conf["verbose"] > 1:
logSys.exception(e)
else:
logSys.error(e)
return False
finally:
self._alive = False
for s, sh in _prev_signals.iteritems():
signal.signal(s, sh)
##
# Wonderful visual :)
#
class _VisualWait:
pos = 0
delta = 1
def __init__(self, maxpos=10):
self.maxpos = maxpos
def __enter__(self):
return self
def __exit__(self, *args):
if self.pos:
sys.stdout.write('\r'+(' '*(35+self.maxpos))+'\r')
sys.stdout.flush()
def heartbeat(self):
if not self.pos:
sys.stdout.write("\nINFO [#" + (' '*self.maxpos) + "] Waiting on the server...\r\x1b[8C")
self.pos += self.delta
if self.delta > 0:
s = " #\x1b[1D" if self.pos > 1 else "# \x1b[2D"
else:
s = "\x1b[1D# \x1b[2D"
sys.stdout.write(s)
sys.stdout.flush()
if self.pos > self.maxpos:
self.delta = -1
elif self.pos < 2:
self.delta = 1
class _NotVisualWait:
def __enter__(self):
return self
def __exit__(self, *args):
pass
def heartbeat(self):
pass
def VisualWait(verbose, *args, **kwargs):
return _VisualWait(*args, **kwargs) if verbose > 1 else _NotVisualWait()
def exec_command_line(argv):
client = Fail2banClient()
# Exit with correct return value
if client.start(argv):
exit(0)
else:
exit(-1)

View File

@ -0,0 +1,279 @@
#!/usr/bin/python
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
# vi: set ft=python sts=4 ts=4 sw=4 noet :
#
# This file is part of Fail2Ban.
#
# Fail2Ban is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Fail2Ban is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
__author__ = "Fail2Ban Developers"
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
__license__ = "GPL"
import getopt
import logging
import os
import sys
from ..version import version
from ..protocol import printFormatted
from ..helpers import getLogger
# Gets the instance of the logger.
logSys = getLogger("fail2ban")
def output(s): # pragma: no cover
print(s)
CONFIG_PARAMS = ("socket", "pidfile", "logtarget", "loglevel", "syslogsocket",)
# Used to signal - we are in test cases (ex: prevents change logging params, log capturing, etc)
PRODUCTION = True
MAX_WAITTIME = 30
class Fail2banCmdLine():
def __init__(self):
self._argv = self._args = None
self._configurator = None
self.resetConf()
def resetConf(self):
self._conf = {
"async": False,
"conf": "/etc/fail2ban",
"force": False,
"background": True,
"verbose": 1,
"socket": None,
"pidfile": None,
"timeout": MAX_WAITTIME
}
@property
def configurator(self):
if self._configurator:
return self._configurator
# New configurator
from .configurator import Configurator
self._configurator = Configurator()
# Set the configuration path
self._configurator.setBaseDir(self._conf["conf"])
return self._configurator
def applyMembers(self, obj):
for o in obj.__dict__:
self.__dict__[o] = obj.__dict__[o]
def dispVersion(self):
output("Fail2Ban v" + version)
output("")
output("Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors")
output("Copyright of modifications held by their respective authors.")
output("Licensed under the GNU General Public License v2 (GPL).")
output("")
output("Written by Cyril Jaquier <cyril.jaquier@fail2ban.org>.")
output("Many contributions by Yaroslav O. Halchenko <debian@onerussian.com>.")
def dispUsage(self):
""" Prints Fail2Ban command line options and exits
"""
caller = os.path.basename(self._argv[0])
output("Usage: "+caller+" [OPTIONS]" + (" <COMMAND>" if not caller.endswith('server') else ""))
output("")
output("Fail2Ban v" + version + " reads log file that contains password failure report")
output("and bans the corresponding IP addresses using firewall rules.")
output("")
output("Options:")
output(" -c <DIR> configuration directory")
output(" -s <FILE> socket path")
output(" -p <FILE> pidfile path")
output(" --loglevel <LEVEL> logging level")
output(" --logtarget <FILE>|STDOUT|STDERR|SYSLOG")
output(" --syslogsocket auto|<FILE>")
output(" -d dump configuration. For debugging")
output(" -i interactive mode")
output(" -v increase verbosity")
output(" -q decrease verbosity")
output(" -x force execution of the server (remove socket file)")
output(" -b start server in background (default)")
output(" -f start server in foreground")
output(" --async start server in async mode (for internal usage only, don't read configuration)")
output(" --timeout timeout to wait for the server (for internal usage only, don't read configuration)")
output(" -h, --help display this help message")
output(" -V, --version print the version")
if not caller.endswith('server'):
output("")
output("Command:")
# Prints the protocol
printFormatted()
output("")
output("Report bugs to https://github.com/fail2ban/fail2ban/issues")
def __getCmdLineOptions(self, optList):
""" Gets the command line options
"""
for opt in optList:
o = opt[0]
if o == "-c":
self._conf["conf"] = opt[1]
elif o == "-s":
self._conf["socket"] = opt[1]
elif o == "-p":
self._conf["pidfile"] = opt[1]
elif o.startswith("--log") or o.startswith("--sys"):
self._conf[ o[2:] ] = opt[1]
elif o == "-d":
self._conf["dump"] = True
elif o == "-v":
self._conf["verbose"] += 1
elif o == "-q":
self._conf["verbose"] -= 1
elif o == "-x":
self._conf["force"] = True
elif o == "-i":
self._conf["interactive"] = True
elif o == "-b":
self._conf["background"] = True
elif o == "-f":
self._conf["background"] = False
elif o == "--async":
self._conf["async"] = True
elif o == "-timeout":
from ..mytime import MyTime
self._conf["timeout"] = MyTime.str2seconds(opt[1])
elif o in ["-h", "--help"]:
self.dispUsage()
return True
elif o in ["-V", "--version"]:
self.dispVersion()
return True
return None
def initCmdLine(self, argv):
verbose = 1
try:
# First time?
initial = (self._argv is None)
# Command line options
self._argv = argv
logSys.info("Using start params %s", argv[1:])
# Reads the command line options.
try:
cmdOpts = 'hc:s:p:xfbdviqV'
cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'async', 'timeout=', 'help', 'version']
optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts)
except getopt.GetoptError:
self.dispUsage()
return False
ret = self.__getCmdLineOptions(optList)
if ret is not None:
return ret
logSys.debug("-- conf: %r, args: %r", self._conf, self._args)
if initial and PRODUCTION: # pragma: no cover - can't test
verbose = self._conf["verbose"]
if verbose <= 0:
logSys.setLevel(logging.ERROR)
elif verbose == 1:
logSys.setLevel(logging.WARNING)
elif verbose == 2:
logSys.setLevel(logging.INFO)
elif verbose == 3:
logSys.setLevel(logging.DEBUG)
else:
logSys.setLevel(logging.HEAVYDEBUG)
# Add the default logging handler to dump to stderr
logout = logging.StreamHandler(sys.stderr)
# set a format which is simpler for console use
formatter = logging.Formatter('%(levelname)-6s %(message)s')
# tell the handler to use this format
logout.setFormatter(formatter)
logSys.addHandler(logout)
# Set expected parameters (like socket, pidfile, etc) from configuration,
# if those not yet specified, in which read configuration only if needed here:
conf = None
for o in CONFIG_PARAMS:
if self._conf.get(o, None) is None:
if not conf:
self.configurator.readEarly()
conf = self.configurator.getEarlyOptions()
self._conf[o] = conf[o]
logSys.info("Using socket file %s", self._conf["socket"])
logSys.info("Using pid file %s, [%s] logging to %s",
self._conf["pidfile"], self._conf["loglevel"], self._conf["logtarget"])
if self._conf.get("dump", False):
ret, stream = self.readConfig()
self.dumpConfig(stream)
return ret
# Nothing to do here, process in client/server
return None
except Exception as e:
output("ERROR: %s" % (e,))
if verbose > 2:
logSys.exception(e)
return False
def readConfig(self, jail=None):
# Read the configuration
# TODO: get away from stew of return codes and exception
# handling -- handle via exceptions
stream = None
try:
self.configurator.Reload()
self.configurator.readAll()
ret = self.configurator.getOptions(jail)
self.configurator.convertToProtocol()
stream = self.configurator.getConfigStream()
except Exception, e:
logSys.error("Failed during configuration: %s" % e)
ret = False
return ret, stream
@staticmethod
def dumpConfig(cmd):
for c in cmd:
output(c)
return True
@staticmethod
def exit(code=0): # pragma: no cover - can't test
logSys.debug("Exit with code %s", code)
if os._exit:
os._exit(code)
else:
sys.exit(code)
# global exit handler:
exit = Fail2banCmdLine.exit
class ExitException(Exception):
pass
class ServerExecutionException(Exception):
pass

View File

@ -40,8 +40,13 @@ class Fail2banReader(ConfigReader):
ConfigReader.read(self, "fail2ban")
def getEarlyOptions(self):
opts = [["string", "socket", "/var/run/fail2ban/fail2ban.sock"],
["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"]]
opts = [
["string", "socket", "/var/run/fail2ban/fail2ban.sock"],
["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"],
["string", "loglevel", "INFO"],
["string", "logtarget", "/var/log/fail2ban.log"],
["string", "syslogsocket", "auto"]
]
return ConfigReader.getOptions(self, "Definition", opts)
def getOptions(self):

View File

@ -0,0 +1,226 @@
#!/usr/bin/python
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
# vi: set ft=python sts=4 ts=4 sw=4 noet :
#
# This file is part of Fail2Ban.
#
# Fail2Ban is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Fail2Ban is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
__author__ = "Fail2Ban Developers"
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
__license__ = "GPL"
import os
import sys
from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, \
logSys, PRODUCTION, exit
SERVER = "fail2ban-server"
##
# \mainpage Fail2Ban
#
# \section Introduction
#
class Fail2banServer(Fail2banCmdLine):
# def __init__(self):
# Fail2banCmdLine.__init__(self)
##
# Start Fail2Ban server in main thread without fork (direct, it can fork itself in Server if daemon=True).
#
# Start the Fail2ban server in background/foreground (daemon mode or not).
@staticmethod
def startServerDirect(conf, daemon=True):
logSys.debug("-- direct starting of server in %s, deamon: %s", os.getpid(), daemon)
from ..server.server import Server
server = None
try:
# Start it in foreground (current thread, not new process),
# server object will internally fork self if daemon is True
server = Server(daemon)
server.start(conf["socket"],
conf["pidfile"], conf["force"],
conf=conf)
except Exception as e: # pragma: no cover
try:
if server:
server.quit()
except Exception as e2:
if conf["verbose"] > 1:
logSys.exception(e2)
raise
return server
##
# Start Fail2Ban server.
#
# Start the Fail2ban server in daemon mode (background, start from client).
@staticmethod
def startServerAsync(conf):
# Forks the current process, don't fork if async specified (ex: test cases)
pid = 0
frk = not conf["async"] and PRODUCTION
if frk: # pragma: no cover
pid = os.fork()
logSys.debug("-- async starting of server in %s, fork: %s - %s", os.getpid(), frk, pid)
if pid == 0:
args = list()
args.append(SERVER)
# Start async (don't read config) and in background as requested.
args.append("--async")
args.append("-b")
# Set the socket path.
args.append("-s")
args.append(conf["socket"])
# Set the pidfile
args.append("-p")
args.append(conf["pidfile"])
# Force the execution if needed.
if conf["force"]:
args.append("-x")
# Logging parameters:
for o in ('loglevel', 'logtarget', 'syslogsocket'):
args.append("--"+o)
args.append(conf[o])
try:
# Directory of client (to try the first start from current or the same directory as client, and from relative bin):
exe = Fail2banServer.getServerPath()
if not frk:
# Wrapr args to use the same python version in client/server (important for multi-python systems):
args[0] = exe
exe = sys.executable
args[0:0] = [exe]
logSys.debug("Starting %r with args %r", exe, args)
if frk: # pragma: no cover
os.execv(exe, args)
else:
# use P_WAIT instead of P_NOWAIT (to prevent defunct-zomby process), it startet as daemon, so parent exit fast after fork):
ret = os.spawnv(os.P_WAIT, exe, args)
if ret != 0: # pragma: no cover
raise OSError(ret, "Unknown error by executing server %r with %r" % (args[1], exe))
except OSError as e: # pragma: no cover
if not frk: #not PRODUCTION:
raise
# Use the PATH env.
logSys.warning("Initial start attempt failed (%s). Starting %r with the same args", e, SERVER)
if frk: # pragma: no cover
os.execvp(SERVER, args)
@staticmethod
def getServerPath():
startdir = sys.path[0]
exe = os.path.abspath(os.path.join(startdir, SERVER))
if not os.path.isfile(exe): # may be uresolved in test-cases, so get relative starter (client):
startdir = os.path.dirname(sys.argv[0])
exe = os.path.abspath(os.path.join(startdir, SERVER))
if not os.path.isfile(exe): # may be uresolved in test-cases, so try to get relative bin-directory:
startdir = os.path.dirname(os.path.abspath(__file__))
startdir = os.path.join(os.path.dirname(os.path.dirname(startdir)), "bin")
exe = os.path.abspath(os.path.join(startdir, SERVER))
return exe
def _Fail2banClient(self):
from .fail2banclient import Fail2banClient
cli = Fail2banClient()
cli.applyMembers(self)
return cli
def start(self, argv):
# Command line options
ret = self.initCmdLine(argv)
if ret is not None:
return ret
# Commands
args = self._args
cli = None
# Just start:
if len(args) == 1 and args[0] == 'start' and not self._conf.get("interactive", False):
pass
else:
# If client mode - whole processing over client:
if len(args) or self._conf.get("interactive", False):
cli = self._Fail2banClient()
return cli.start(argv)
# Start the server:
server = None
try:
from ..server.utils import Utils
# background = True, if should be new process running in background, otherwise start in foreground
# process will be forked in daemonize, inside of Server module.
# async = True, if started from client, should...
background = self._conf["background"]
async = self._conf.get("async", False)
# If was started not from the client:
if not async:
# Start new thread with client to read configuration and
# transfer it to the server:
cli = self._Fail2banClient()
phase = dict()
logSys.debug('Configure via async client thread')
cli.configureServer(async=True, phase=phase)
# wait, do not continue if configuration is not 100% valid:
Utils.wait_for(lambda: phase.get('ready', None) is not None, self._conf["timeout"])
if not phase.get('start', False):
raise ServerExecutionException('Async configuration of server failed')
# Start server, daemonize it, etc.
pid = os.getpid()
server = Fail2banServer.startServerDirect(self._conf, background)
# If forked - just exit other processes
if pid != os.getpid(): # pragma: no cover
os._exit(0)
if cli:
cli._server = server
# wait for client answer "done":
if not async and cli:
Utils.wait_for(lambda: phase.get('done', None) is not None, self._conf["timeout"])
if not phase.get('done', False):
if server: # pragma: no cover
server.quit()
exit(-1)
logSys.debug('Starting server done')
except Exception, e:
if self._conf["verbose"] > 1:
logSys.exception(e)
else:
logSys.error(e)
if server: # pragma: no cover
server.quit()
exit(-1)
return True
@staticmethod
def exit(code=0): # pragma: no cover
if code != 0:
logSys.error("Could not start %s", SERVER)
exit(code)
def exec_command_line(argv):
server = Fail2banServer()
if server.start(argv):
exit(0)
else:
exit(-1)

View File

@ -119,7 +119,7 @@ class JailReader(ConfigReader):
defsec["fail2ban_version"] = version
# Read first options only needed for merge defaults ('known/...' from filter):
self.__opts = ConfigReader.getOptions(self, self.__name, opts1st)
self.__opts = ConfigReader.getOptions(self, self.__name, opts1st, shouldExist=True)
if not self.__opts:
return False

View File

@ -26,6 +26,9 @@ __license__ = "GPL"
import textwrap
def output(s):
print(s)
##
# Describes the protocol used to communicate with the server.
@ -42,11 +45,13 @@ CSPROTO = dotdict({
protocol = [
['', "BASIC", ""],
["start", "starts the server and the jails"],
["reload", "reloads the configuration"],
["restart", "restarts the server"],
["reload", "reloads the configuration without restart"],
["reload <JAIL>", "reloads the jail <JAIL>"],
["stop", "stops all jails and terminate the server"],
["status", "gets the current status of the server"],
["ping", "tests if the server is alive"],
["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", ""],
@ -141,7 +146,7 @@ def printFormatted():
firstHeading = False
for m in protocol:
if m[0] == '' and firstHeading:
print
output("")
firstHeading = True
first = True
if len(m[0]) >= MARGIN:
@ -152,7 +157,7 @@ def printFormatted():
first = False
else:
line = ' ' * (INDENT + MARGIN) + n.strip()
print line
output(line)
##
@ -163,20 +168,20 @@ def printWiki():
for m in protocol:
if m[0] == '':
if firstHeading:
print "|}"
output("|}")
__printWikiHeader(m[1], m[2])
firstHeading = True
else:
print "|-"
print "| <span style=\"white-space:nowrap;\"><tt>" + m[0] + "</tt></span> || || " + m[1]
print "|}"
output("|-")
output("| <span style=\"white-space:nowrap;\"><tt>" + m[0] + "</tt></span> || || " + m[1])
output("|}")
def __printWikiHeader(section, desc):
print
print "=== " + section + " ==="
print
print desc
print
print "{|"
print "| '''Command''' || || '''Description'''"
output("")
output("=== " + section + " ===")
output("")
output(desc)
output("")
output("{|")
output("| '''Command''' || || '''Description'''")

View File

@ -51,6 +51,8 @@ class JailThread(Thread):
def __init__(self, name=None):
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 idle state of the thread.

View File

@ -24,6 +24,7 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
import threading
from threading import Lock, RLock
import logging
import logging.handlers
@ -42,6 +43,10 @@ from ..helpers import getLogger, excepthook
# Gets the instance of the logger.
logSys = getLogger(__name__)
DEF_SYSLOGSOCKET = "auto"
DEF_LOGLEVEL = "INFO"
DEF_LOGTARGET = "STDOUT"
try:
from .database import Fail2BanDb
except ImportError: # pragma: no cover
@ -49,6 +54,10 @@ except ImportError: # pragma: no cover
Fail2BanDb = None
def _thread_name():
return threading.current_thread().__class__.__name__
class Server:
def __init__(self, daemon = False):
@ -58,7 +67,8 @@ class Server:
self.__db = None
self.__daemon = daemon
self.__transm = Transmitter(self)
self.__asyncServer = AsyncServer(self.__transm)
#self.__asyncServer = AsyncServer(self.__transm)
self.__asyncServer = None
self.__logLevel = None
self.__logTarget = None
self.__syslogSocket = None
@ -67,10 +77,7 @@ class Server:
'FreeBSD': '/var/run/log',
'Linux': '/dev/log',
}
self.setSyslogSocket("auto")
# Set logging level
self.setLogLevel("INFO")
self.setLogTarget("STDOUT")
self.__prev_signals = {}
def __sigTERMhandler(self, signum, frame):
logSys.debug("Caught signal %d. Exiting" % signum)
@ -80,28 +87,45 @@ class Server:
logSys.debug("Caught signal %d. Flushing logs" % signum)
self.flushLogs()
def start(self, sock, pidfile, force = False):
logSys.info("Starting Fail2ban v%s", version.version)
# Install signal handlers
signal.signal(signal.SIGTERM, self.__sigTERMhandler)
signal.signal(signal.SIGINT, self.__sigTERMhandler)
signal.signal(signal.SIGUSR1, self.__sigUSR1handler)
# Ensure unhandled exceptions are logged
sys.excepthook = excepthook
def start(self, sock, pidfile, force=False, conf={}):
# First set the mask to only allow access to owner
os.umask(0077)
# Second daemonize before logging etc, because it will close all handles:
if self.__daemon: # pragma: no cover
logSys.info("Starting in daemon mode")
ret = self.__createDaemon()
if ret:
logSys.info("Daemon started")
else:
logSys.error("Could not create daemon")
raise ServerInitializationError("Could not create daemon")
# If forked parent - return here (parent process will configure server later):
if ret is None:
return False
# If error:
if not ret[0]:
err = "Could not create daemon %s", ret[1:]
logSys.error(err)
raise ServerInitializationError(err)
# We are daemon.
# Set all logging parameters (or use default if not specified):
self.setSyslogSocket(conf.get("syslogsocket",
self.__syslogSocket if self.__syslogSocket is not None else DEF_SYSLOGSOCKET))
self.setLogLevel(conf.get("loglevel",
self.__logLevel if self.__logLevel is not None else DEF_LOGLEVEL))
self.setLogTarget(conf.get("logtarget",
self.__logTarget if self.__logTarget is not None else DEF_LOGTARGET))
logSys.info("-"*50)
logSys.info("Starting Fail2ban v%s", version.version)
if self.__daemon: # pragma: no cover
logSys.info("Daemon started")
# Install signal handlers
if _thread_name() == '_MainThread':
for s in (signal.SIGTERM, signal.SIGINT, signal.SIGUSR1):
self.__prev_signals[s] = signal.getsignal(s)
signal.signal(s, self.__sigTERMhandler if s != signal.SIGUSR1 else self.__sigUSR1handler)
# Ensure unhandled exceptions are logged
sys.excepthook = excepthook
# Creates a PID file.
try:
logSys.debug("Creating PID file %s" % pidfile)
@ -114,6 +138,7 @@ class Server:
# Start the communication
logSys.debug("Starting communication")
try:
self.__asyncServer = AsyncServer(self.__transm)
self.__asyncServer.start(sock, force)
except AsyncServerException, e:
logSys.error("Could not start server: %s", e)
@ -132,17 +157,25 @@ class Server:
# communications first (which should be ok anyways since we
# are exiting)
# See https://github.com/fail2ban/fail2ban/issues/7
self.__asyncServer.stop()
if self.__asyncServer is not None:
self.__asyncServer.stop()
self.__asyncServer = None
# Now stop all the jails
self.stopAllJail()
# Only now shutdown the logging.
try:
self.__loggingLock.acquire()
logging.shutdown()
finally:
self.__loggingLock.release()
if self.__logTarget is not None:
with self.__loggingLock:
logging.shutdown()
# Restore default signal handlers:
if _thread_name() == '_MainThread':
for s, sh in self.__prev_signals.iteritems():
signal.signal(s, sh)
# Prevent to call quit twice:
self.quit = lambda: False
def addJail(self, name, backend):
self.__jails.add(name, backend, self.__db)
@ -337,6 +370,9 @@ class Server:
def getBanTime(self, name):
return self.__jails[name].actions.getBanTime()
def isStarted(self):
self.__asyncServer.isActive()
def isAlive(self, jailnum=None):
if jailnum is not None and len(self.__jails) != jailnum:
return 0
@ -375,16 +411,15 @@ class Server:
# @param value the level
def setLogLevel(self, value):
try:
self.__loggingLock.acquire()
getLogger("fail2ban").setLevel(
getattr(logging, value.upper()))
except AttributeError:
raise ValueError("Invalid log level")
else:
self.__logLevel = value.upper()
finally:
self.__loggingLock.release()
value = value.upper()
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")
##
# Get the logging level.
@ -393,11 +428,8 @@ class Server:
# @return the log level
def getLogLevel(self):
try:
self.__loggingLock.acquire()
with self.__loggingLock:
return self.__logLevel
finally:
self.__loggingLock.release()
##
# Sets the logging target.
@ -406,8 +438,14 @@ class Server:
# @param target the logging target
def setLogTarget(self, target):
try:
self.__loggingLock.acquire()
with self.__loggingLock:
# don't set new handlers if already the same
# or if "INHERITED" (foreground worker of the test cases, to prevent stop logging):
if self.__logTarget == target:
return True
if target == "INHERITED":
self.__logTarget = target
return True
# set a format which is simpler for console use
formatter = logging.Formatter("%(asctime)s %(name)-24s[%(process)d]: %(levelname)-7s %(message)s")
if target == "SYSLOG":
@ -475,8 +513,6 @@ class Server:
# Sets the logging target.
self.__logTarget = target
return True
finally:
self.__loggingLock.release()
##
# Sets the syslog socket.
@ -484,24 +520,21 @@ class Server:
# syslogsocket is the full path to the syslog socket
# @param syslogsocket the syslog socket path
def setSyslogSocket(self, syslogsocket):
self.__syslogSocket = syslogsocket
# Conditionally reload, logtarget depends on socket path when SYSLOG
return self.__logTarget != "SYSLOG"\
or self.setLogTarget(self.__logTarget)
with self.__loggingLock:
if self.__syslogSocket == syslogsocket:
return True
self.__syslogSocket = syslogsocket
# Conditionally reload, logtarget depends on socket path when SYSLOG
return self.__logTarget != "SYSLOG"\
or self.setLogTarget(self.__logTarget)
def getLogTarget(self):
try:
self.__loggingLock.acquire()
with self.__loggingLock:
return self.__logTarget
finally:
self.__loggingLock.release()
def getSyslogSocket(self):
try:
self.__loggingLock.acquire()
with self.__loggingLock:
return self.__syslogSocket
finally:
self.__loggingLock.release()
def flushLogs(self):
if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']:
@ -555,8 +588,10 @@ class Server:
# We need to set this in the parent process, so it gets inherited by the
# child process, and this makes sure that it is effect even if the parent
# terminates quickly.
signal.signal(signal.SIGHUP, signal.SIG_IGN)
for s in (signal.SIGHUP,):
self.__prev_signals[s] = signal.getsignal(s)
signal.signal(s, signal.SIG_IGN)
try:
# Fork a child process so the parent can exit. This will return control
# to the command line or shell. This is required so that the new process
@ -566,7 +601,7 @@ class Server:
# PGID.
pid = os.fork()
except OSError, e:
return((e.errno, e.strerror)) # ERROR (return a tuple)
return (False, (e.errno, e.strerror)) # ERROR (return a tuple)
if pid == 0: # The first child.
@ -587,7 +622,7 @@ class Server:
# preventing the daemon from ever acquiring a controlling terminal.
pid = os.fork() # Fork a second child.
except OSError, e:
return((e.errno, e.strerror)) # ERROR (return a tuple)
return (False, (e.errno, e.strerror)) # ERROR (return a tuple)
if (pid == 0): # The second child.
# Ensure that the daemon doesn't keep any directory in use. Failure
@ -596,8 +631,9 @@ class Server:
else:
os._exit(0) # Exit parent (the first child) of the second child.
else:
os._exit(0) # Exit parent of the first child.
# Signal to exit, parent of the first child.
return None
# Close all open files. Try the system configuration variable, SC_OPEN_MAX,
# for the maximum number of open files to close. If it doesn't exist, use
# the default value (configurable).
@ -624,7 +660,7 @@ class Server:
os.open("/dev/null", os.O_RDONLY) # standard input (0)
os.open("/dev/null", os.O_RDWR) # standard output (1)
os.open("/dev/null", os.O_RDWR) # standard error (2)
return True
return (True,)
class ServerInitializationError(Exception):

View File

@ -93,6 +93,8 @@ class Transmitter:
name = command[1]
self.__server.stopJail(name)
return None
elif command[0] == "echo":
return command[1:]
elif command[0] == "sleep":
value = command[1]
time.sleep(float(value))

View File

@ -65,6 +65,7 @@ class SMTPActionTest(unittest.TestCase):
self._active = True
self._loop_thread = threading.Thread(
target=asyncserver.loop, kwargs={'active': lambda: self._active})
self._loop_thread.daemon = True
self._loop_thread.start()
def tearDown(self):

View File

@ -45,8 +45,8 @@ class CommandActionTest(LogCaptureTestCase):
def tearDown(self):
"""Call after every test case."""
LogCaptureTestCase.tearDown(self)
self.__action.stop()
LogCaptureTestCase.tearDown(self)
def testSubstituteRecursiveTags(self):
aInfo = {

View File

@ -0,0 +1,598 @@
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
# vi: set ft=python sts=4 ts=4 sw=4 noet :
# This file is part of Fail2Ban.
#
# Fail2Ban is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Fail2Ban is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Fail2Ban developers
__author__ = "Serg Brester"
__copyright__ = "Copyright (c) 2014- Serg G. Brester (sebres), 2008- Fail2Ban Contributors"
__license__ = "GPL"
import fileinput
import os
import re
import sys
import time
import signal
import unittest
from functools import wraps
from threading import Thread
from ..client import fail2banclient, fail2banserver, fail2bancmdline
from ..client.fail2banclient import Fail2banClient, exec_command_line as _exec_client, VisualWait
from ..client.fail2banserver import Fail2banServer, exec_command_line as _exec_server
from .. import protocol
from ..server import server
from ..server.utils import Utils
from .utils import LogCaptureTestCase, logSys, with_tmpdir, shutil, logging
STOCK_CONF_DIR = "config"
STOCK = os.path.exists(os.path.join(STOCK_CONF_DIR,'fail2ban.conf'))
CLIENT = "fail2ban-client"
SERVER = "fail2ban-server"
BIN = os.path.dirname(Fail2banServer.getServerPath())
MAX_WAITTIME = 30 if not unittest.F2B.fast else 5
##
# Several wrappers and settings for proper testing:
#
fail2bancmdline.MAX_WAITTIME = MAX_WAITTIME-1
fail2bancmdline.logSys = \
fail2banclient.logSys = \
fail2banserver.logSys = logSys
server.DEF_LOGTARGET = "/dev/null"
def _test_output(*args):
logSys.info(args[0])
fail2bancmdline.output = \
fail2banclient.output = \
fail2banserver.output = \
protocol.output = _test_output
def _test_exit(code=0):
logSys.debug("Exit with code %s", code)
if code == 0:
raise ExitException()
else:
raise FailExitException()
fail2bancmdline.exit = \
fail2banclient.exit = \
fail2banserver.exit = _test_exit
INTERACT = []
def _test_input_command(*args):
if len(INTERACT):
#logSys.debug('--- interact command: %r', INTERACT[0])
return INTERACT.pop(0)
else:
return "exit"
fail2banclient.input_command = _test_input_command
# prevents change logging params, log capturing, etc:
fail2bancmdline.PRODUCTION = \
fail2banclient.PRODUCTION = \
fail2banserver.PRODUCTION = False
class ExitException(fail2bancmdline.ExitException):
pass
class FailExitException(fail2bancmdline.ExitException):
pass
def _out_file(fn): # pragma: no cover
logSys.debug('---- ' + fn + ' ----')
for line in fileinput.input(fn):
line = line.rstrip('\n')
logSys.debug(line)
logSys.debug('-'*30)
def _start_params(tmp, use_stock=False, logtarget="/dev/null"):
cfg = tmp+"/config"
if use_stock and STOCK:
# copy config (sub-directories as alias):
def ig_dirs(dir, files):
return [f for f in files if os.path.isdir(os.path.join(dir, f))]
shutil.copytree(STOCK_CONF_DIR, cfg, ignore=ig_dirs)
os.symlink(STOCK_CONF_DIR+"/action.d", cfg+"/action.d")
os.symlink(STOCK_CONF_DIR+"/filter.d", cfg+"/filter.d")
# replace fail2ban params (database with memory):
r = re.compile(r'^dbfile\s*=')
for line in fileinput.input(cfg+"/fail2ban.conf", inplace=True):
line = line.rstrip('\n')
if r.match(line):
line = "dbfile = :memory:"
print(line)
# replace jail params (polling as backend to be fast in initialize):
r = re.compile(r'^backend\s*=')
for line in fileinput.input(cfg+"/jail.conf", inplace=True):
line = line.rstrip('\n')
if r.match(line):
line = "backend = polling"
print(line)
else:
# just empty config directory without anything (only fail2ban.conf/jail.conf):
os.mkdir(cfg)
f = open(cfg+"/fail2ban.conf", "w")
f.write('\n'.join((
"[Definition]",
"loglevel = INFO",
"logtarget = " + logtarget,
"syslogsocket = auto",
"socket = "+tmp+"/f2b.sock",
"pidfile = "+tmp+"/f2b.pid",
"backend = polling",
"dbfile = :memory:",
"dbpurgeage = 1d",
"",
)))
f.close()
f = open(cfg+"/jail.conf", "w")
f.write('\n'.join((
"[INCLUDES]", "",
"[DEFAULT]", "",
"",
)))
f.close()
if logSys.level < logging.DEBUG: # if HEAVYDEBUG
_out_file(cfg+"/fail2ban.conf")
_out_file(cfg+"/jail.conf")
# parameters (sock/pid and config, increase verbosity, set log, etc.):
return ("-c", cfg, "-s", tmp+"/f2b.sock", "-p", tmp+"/f2b.pid",
"-vv", "--logtarget", logtarget, "--loglevel", "DEBUG", "--syslogsocket", "auto",
"--timeout", str(fail2bancmdline.MAX_WAITTIME),
)
def _kill_srv(pidfile): # pragma: no cover
def _pid_exists(pid):
try:
os.kill(pid, 0)
return True
except OSError:
return False
logSys.debug("-- cleanup: %r", (pidfile, os.path.isdir(pidfile)))
if os.path.isdir(pidfile):
piddir = pidfile
pidfile = piddir + "/f2b.pid"
if not os.path.isfile(pidfile):
pidfile = piddir + "/fail2ban.pid"
if not os.path.isfile(pidfile):
logSys.debug("--- cleanup: no pidfile for %r", piddir)
return True
f = pid = None
try:
logSys.debug("--- cleanup pidfile: %r", pidfile)
f = open(pidfile)
pid = f.read().split()[1]
pid = int(pid)
logSys.debug("--- cleanup pid: %r", pid)
if pid <= 0:
raise ValueError('pid %s of %s is invalid' % (pid, pidfile))
if not _pid_exists(pid):
return True
## try to preper stop (have signal handler):
os.kill(pid, signal.SIGTERM)
## check still exists after small timeout:
if not Utils.wait_for(lambda: not _pid_exists(pid), 1):
## try to kill hereafter:
os.kill(pid, signal.SIGKILL)
return not _pid_exists(pid)
except Exception as e:
logSys.debug(e)
finally:
if f is not None:
f.close()
return True
def with_kill_srv(f):
"""Helper to decorate tests which receive in the last argument tmpdir to pass to kill_srv
To be used in tandem with @with_tmpdir
"""
@wraps(f)
def wrapper(self, *args):
pidfile = args[-1]
try:
return f(self, *args)
finally:
_kill_srv(pidfile)
return wrapper
class Fail2banClientServerBase(LogCaptureTestCase):
def setUp(self):
"""Call before every test case."""
LogCaptureTestCase.setUp(self)
def tearDown(self):
"""Call after every test case."""
LogCaptureTestCase.tearDown(self)
def _wait_for_srv(self, tmp, ready=True, startparams=None):
try:
sock = tmp+"/f2b.sock"
# wait for server (socket):
ret = Utils.wait_for(lambda: os.path.exists(sock), MAX_WAITTIME)
if not ret:
raise Exception('Unexpected: Socket file does not exists.\nStart failed: %r' % (startparams,))
if ready:
# wait for communication with worker ready:
ret = Utils.wait_for(lambda: "Server ready" in self.getLog(), MAX_WAITTIME)
if not ret:
raise Exception('Unexpected: Server ready was not found.\nStart failed: %r' % (startparams,))
except: # pragma: no cover
log = tmp+"/f2b.log"
if os.path.isfile(log):
_out_file(log)
else:
logSys.debug("No log file %s to examine details of error", log)
raise
class Fail2banClientTest(Fail2banClientServerBase):
def testConsistency(self):
self.assertTrue(os.path.isfile(os.path.join(os.path.join(BIN), CLIENT)))
self.assertTrue(os.path.isfile(os.path.join(os.path.join(BIN), SERVER)))
def testClientUsage(self):
self.assertRaises(ExitException, _exec_client,
(CLIENT, "-h",))
self.assertLogged("Usage: " + CLIENT)
self.assertLogged("Report bugs to ")
self.pruneLog()
self.assertRaises(ExitException, _exec_client,
(CLIENT, "-vq", "-V",))
self.assertLogged("Fail2Ban v" + fail2bancmdline.version)
@with_tmpdir
def testClientDump(self, tmp):
# use here the stock configuration (if possible)
startparams = _start_params(tmp, True)
self.assertRaises(ExitException, _exec_client,
((CLIENT,) + startparams + ("-vvd",)))
self.assertLogged("Loading files")
self.assertLogged("logtarget")
@with_tmpdir
@with_kill_srv
def testClientStartBackgroundInside(self, tmp):
# use once the stock configuration (to test starting also)
startparams = _start_params(tmp, True)
# start:
self.assertRaises(ExitException, _exec_client,
(CLIENT, "-b") + startparams + ("start",))
# wait for server (socket and ready):
self._wait_for_srv(tmp, True, startparams=startparams)
self.assertLogged("Server ready")
self.assertLogged("Exit with code 0")
try:
self.assertRaises(ExitException, _exec_client,
(CLIENT,) + startparams + ("echo", "TEST-ECHO",))
self.assertRaises(FailExitException, _exec_client,
(CLIENT,) + startparams + ("~~unknown~cmd~failed~~",))
self.pruneLog()
# start again (should fail):
self.assertRaises(FailExitException, _exec_client,
(CLIENT, "-b") + startparams + ("start",))
self.assertLogged("Server already running")
finally:
self.pruneLog()
# stop:
self.assertRaises(ExitException, _exec_client,
(CLIENT,) + startparams + ("stop",))
self.assertLogged("Shutdown successful")
self.assertLogged("Exit with code 0")
self.pruneLog()
# stop again (should fail):
self.assertRaises(FailExitException, _exec_client,
(CLIENT,) + startparams + ("stop",))
self.assertLogged("Failed to access socket path")
self.assertLogged("Is fail2ban running?")
@with_tmpdir
@with_kill_srv
def testClientStartBackgroundCall(self, tmp):
global INTERACT
startparams = _start_params(tmp, logtarget=tmp+"/f2b.log")
# start (in new process, using the same python version):
cmd = (sys.executable, os.path.join(os.path.join(BIN), CLIENT))
logSys.debug('Start %s ...', cmd)
cmd = cmd + startparams + ("--async", "start",)
ret = Utils.executeCmd(cmd, timeout=MAX_WAITTIME, shell=False, output=True)
self.assertTrue(len(ret) and ret[0])
# wait for server (socket and ready):
self._wait_for_srv(tmp, True, startparams=cmd)
self.assertLogged("Server ready")
self.pruneLog()
try:
# echo from client (inside):
self.assertRaises(ExitException, _exec_client,
(CLIENT,) + startparams + ("echo", "TEST-ECHO",))
self.assertLogged("TEST-ECHO")
self.assertLogged("Exit with code 0")
self.pruneLog()
# interactive client chat with started server:
INTERACT += [
"echo INTERACT-ECHO",
"status",
"exit"
]
self.assertRaises(ExitException, _exec_client,
(CLIENT,) + startparams + ("-i",))
self.assertLogged("INTERACT-ECHO")
self.assertLogged("Status", "Number of jail:")
self.assertLogged("Exit with code 0")
self.pruneLog()
# test reload and restart over interactive client:
INTERACT += [
"reload",
"restart",
"exit"
]
self.assertRaises(ExitException, _exec_client,
(CLIENT,) + startparams + ("-i",))
self.assertLogged("Reading config files:")
self.assertLogged("Shutdown successful")
self.assertLogged("Server ready")
self.assertLogged("Exit with code 0")
self.pruneLog()
# test reload missing jail (interactive):
INTERACT += [
"reload ~~unknown~jail~fail~~",
"exit"
]
self.assertRaises(ExitException, _exec_client,
(CLIENT,) + startparams + ("-i",))
self.assertLogged("Failed during configuration: No section: '~~unknown~jail~fail~~'")
self.pruneLog()
# test reload missing jail (direct):
self.assertRaises(FailExitException, _exec_client,
(CLIENT,) + startparams + ("reload", "~~unknown~jail~fail~~"))
self.assertLogged("Failed during configuration: No section: '~~unknown~jail~fail~~'")
self.assertLogged("Exit with code -1")
self.pruneLog()
finally:
self.pruneLog()
# stop:
self.assertRaises(ExitException, _exec_client,
(CLIENT,) + startparams + ("stop",))
self.assertLogged("Shutdown successful")
self.assertLogged("Exit with code 0")
def _testClientStartForeground(self, tmp, startparams, phase):
# start and wait to end (foreground):
logSys.debug("-- start of test worker")
phase['start'] = True
self.assertRaises(fail2bancmdline.ExitException, _exec_client,
(CLIENT, "-f") + startparams + ("start",))
# end :
phase['end'] = True
logSys.debug("-- end of test worker")
@with_tmpdir
def testClientStartForeground(self, tmp):
th = None
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:
phase = dict()
th = Thread(name="_TestCaseWorker",
target=Fail2banClientTest._testClientStartForeground, args=(self, 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.assertRaises(ExitException, _exec_client,
(CLIENT,) + startparams + ("ping",))
self.assertRaises(FailExitException, _exec_client,
(CLIENT,) + startparams + ("~~unknown~cmd~failed~~",))
self.assertRaises(ExitException, _exec_client,
(CLIENT,) + startparams + ("echo", "TEST-ECHO",))
finally:
self.pruneLog()
# stop:
self.assertRaises(ExitException, _exec_client,
(CLIENT,) + 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:
_kill_srv(tmp)
if th:
th.join()
@with_tmpdir
@with_kill_srv
def testClientFailStart(self, tmp):
# started directly here, so prevent overwrite test cases logger with "INHERITED"
startparams = _start_params(tmp, logtarget="INHERITED")
## wrong config directory
self.assertRaises(FailExitException, _exec_client,
(CLIENT, "--async", "-c", tmp+"/miss", "start",))
self.assertLogged("Base configuration directory " + tmp+"/miss" + " does not exist")
self.pruneLog()
## wrong socket
self.assertRaises(FailExitException, _exec_client,
(CLIENT, "--async", "-c", tmp+"/config", "-s", tmp+"/miss/f2b.sock", "start",))
self.assertLogged("There is no directory " + tmp+"/miss" + " to contain the socket file")
self.pruneLog()
## not running
self.assertRaises(FailExitException, _exec_client,
(CLIENT, "-c", tmp+"/config", "-s", tmp+"/f2b.sock", "reload",))
self.assertLogged("Could not find server")
self.pruneLog()
## already exists:
open(tmp+"/f2b.sock", 'a').close()
self.assertRaises(FailExitException, _exec_client,
(CLIENT, "--async", "-c", tmp+"/config", "-s", tmp+"/f2b.sock", "start",))
self.assertLogged("Fail2ban seems to be in unexpected state (not running but the socket exists)")
self.pruneLog()
os.remove(tmp+"/f2b.sock")
## wrong option:
self.assertRaises(FailExitException, _exec_client,
(CLIENT, "-s",))
self.assertLogged("Usage: ")
self.pruneLog()
def testVisualWait(self):
sleeptime = 0.035
for verbose in (2, 0):
cntr = 15
with VisualWait(verbose, 5) as vis:
while cntr:
vis.heartbeat()
if verbose and not unittest.F2B.fast:
time.sleep(sleeptime)
cntr -= 1
class Fail2banServerTest(Fail2banClientServerBase):
def testServerUsage(self):
self.assertRaises(ExitException, _exec_server,
(SERVER, "-h",))
self.assertLogged("Usage: " + SERVER)
self.assertLogged("Report bugs to ")
@with_tmpdir
@with_kill_srv
def testServerStartBackground(self, tmp):
# to prevent fork of test-cases process, start server in background via command:
startparams = _start_params(tmp, logtarget=tmp+"/f2b.log")
# start (in new process, using the same python version):
cmd = (sys.executable, os.path.join(os.path.join(BIN), SERVER))
logSys.debug('Start %s ...', cmd)
cmd = cmd + startparams + ("-b",)
ret = Utils.executeCmd(cmd, timeout=MAX_WAITTIME, shell=False, output=True)
self.assertTrue(len(ret) and ret[0])
# wait for server (socket and ready):
self._wait_for_srv(tmp, True, startparams=cmd)
self.assertLogged("Server ready")
self.pruneLog()
try:
self.assertRaises(ExitException, _exec_server,
(SERVER,) + startparams + ("echo", "TEST-ECHO",))
self.assertRaises(FailExitException, _exec_server,
(SERVER,) + startparams + ("~~unknown~cmd~failed~~",))
finally:
self.pruneLog()
# stop:
self.assertRaises(ExitException, _exec_server,
(SERVER,) + startparams + ("stop",))
self.assertLogged("Shutdown successful")
self.assertLogged("Exit with code 0")
def _testServerStartForeground(self, tmp, startparams, phase):
# start and wait to end (foreground):
logSys.debug("-- start of test worker")
phase['start'] = True
self.assertRaises(fail2bancmdline.ExitException, _exec_server,
(SERVER, "-f") + startparams + ("start",))
# end :
phase['end'] = True
logSys.debug("-- end of test worker")
@with_tmpdir
def testServerStartForeground(self, tmp):
th = None
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:
phase = dict()
th = Thread(name="_TestCaseWorker",
target=Fail2banServerTest._testServerStartForeground, args=(self, 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.assertRaises(ExitException, _exec_server,
(SERVER,) + startparams + ("ping",))
self.assertRaises(FailExitException, _exec_server,
(SERVER,) + startparams + ("~~unknown~cmd~failed~~",))
self.assertRaises(ExitException, _exec_server,
(SERVER,) + startparams + ("echo", "TEST-ECHO",))
finally:
self.pruneLog()
# stop:
self.assertRaises(ExitException, _exec_server,
(SERVER,) + 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:
_kill_srv(tmp)
if th:
th.join()
@with_tmpdir
@with_kill_srv
def testServerFailStart(self, tmp):
# started directly here, so prevent overwrite test cases logger with "INHERITED"
startparams = _start_params(tmp, logtarget="INHERITED")
## wrong config directory
self.assertRaises(FailExitException, _exec_server,
(SERVER, "-c", tmp+"/miss",))
self.assertLogged("Base configuration directory " + tmp+"/miss" + " does not exist")
self.pruneLog()
## wrong socket
self.assertRaises(FailExitException, _exec_server,
(SERVER, "-c", tmp+"/config", "-x", "-s", tmp+"/miss/f2b.sock",))
self.assertLogged("There is no directory " + tmp+"/miss" + " to contain the socket file")
self.pruneLog()
## already exists:
open(tmp+"/f2b.sock", 'a').close()
self.assertRaises(FailExitException, _exec_server,
(SERVER, "-c", tmp+"/config", "-s", tmp+"/f2b.sock",))
self.assertLogged("Fail2ban seems to be in unexpected state (not running but the socket exists)")
self.pruneLog()
os.remove(tmp+"/f2b.sock")

View File

@ -23,19 +23,7 @@ __author__ = "Serg Brester"
__copyright__ = "Copyright (c) 2015 Serg G. Brester (sebres), 2008- Fail2Ban Contributors"
__license__ = "GPL"
from __builtin__ import open as fopen
import unittest
import getpass
import os
import sys
import time
import tempfile
import uuid
try:
from systemd import journal
except ImportError:
journal = None
from ..client import fail2banregex
from ..client.fail2banregex import Fail2banRegex, get_opt_parser, output

View File

@ -66,25 +66,18 @@ class TransmitterBase(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
#super(TransmitterBase, self).setUp()
self.transm = self.server._Server__transm
self.tmp_files = []
sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'transmitter')
os.close(sock_fd)
self.tmp_files.append(sock_name)
pidfile_fd, pidfile_name = tempfile.mkstemp(
'fail2ban.pid', 'transmitter')
os.close(pidfile_fd)
self.tmp_files.append(pidfile_name)
self.server.start(sock_name, pidfile_name, force=False)
# To test thransmitter we don't need to start server...
#self.server.start('/dev/null', '/dev/null', force=False)
self.jailName = "TestJail1"
self.server.addJail(self.jailName, FAST_BACKEND)
def tearDown(self):
"""Call after every test case."""
# stop jails, etc.
self.server.quit()
for f in self.tmp_files:
if os.path.exists(f):
os.remove(f)
#super(TransmitterBase, self).tearDown()
def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False):
"""Process set/get commands and compare both return values
@ -796,10 +789,10 @@ class TransmitterLogging(TransmitterBase):
def setUp(self):
self.server = Server()
super(TransmitterLogging, self).setUp()
self.server.setLogTarget("/dev/null")
self.server.setLogLevel("CRITICAL")
self.server.setSyslogSocket("auto")
super(TransmitterLogging, self).setUp()
def testLogTarget(self):
logTargets = []
@ -1000,6 +993,24 @@ class LoggingTests(LogCaptureTestCase):
self.assertEqual(len(x), 1)
self.assertEqual(x[0][0], RuntimeError)
def testStartFailedSockExists(self):
tmp_files = []
sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'f2b-test')
os.close(sock_fd)
tmp_files.append(sock_name)
pidfile_fd, pidfile_name = tempfile.mkstemp('fail2ban.pid', 'f2b-test')
os.close(pidfile_fd)
tmp_files.append(pidfile_name)
server = TestServer()
try:
server.start(sock_name, pidfile_name, force=False)
self.assertLogged("Server already running")
finally:
server.quit()
for f in tmp_files:
if os.path.exists(f):
os.remove(f)
from clientreadertestcase import ActionReader, JailReader, JailsReader, CONFIG_DIR, STOCK

View File

@ -26,10 +26,14 @@ import logging
import optparse
import os
import re
import tempfile
import shutil
import sys
import time
import unittest
from StringIO import StringIO
from functools import wraps
from ..helpers import getLogger
from ..server.ipdns import DNSUtils
@ -50,6 +54,10 @@ if not CONFIG_DIR:
else:
CONFIG_DIR = '/etc/fail2ban'
# In not installed env (setup, test-cases) use fail2ban modules from main directory:
if 1 or os.environ.get('PYTHONPATH', None) is None:
os.putenv('PYTHONPATH', os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)))))
class F2B(optparse.Values):
def __init__(self, opts={}):
@ -71,6 +79,23 @@ class F2B(optparse.Values):
return wtime
def with_tmpdir(f):
"""Helper decorator to create a temporary directory
Directory gets removed after function returns, regardless
if exception was thrown of not
"""
@wraps(f)
def wrapper(self, *args, **kwargs):
tmp = tempfile.mkdtemp(prefix="f2b-temp")
try:
return f(self, tmp, *args, **kwargs)
finally:
# clean up
shutil.rmtree(tmp)
return wrapper
def initTests(opts):
unittest.F2B = F2B(opts)
# --fast :
@ -156,6 +181,7 @@ def gatherTests(regexps=None, opts=None):
from . import misctestcase
from . import databasetestcase
from . import samplestestcase
from . import fail2banclienttestcase
from . import fail2banregextestcase
if not regexps: # pragma: no cover
@ -239,6 +265,9 @@ def gatherTests(regexps=None, opts=None):
# Filter Regex tests with sample logs
tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex))
# bin/fail2ban-client, bin/fail2ban-server
tests.addTest(unittest.makeSuite(fail2banclienttestcase.Fail2banClientTest))
tests.addTest(unittest.makeSuite(fail2banclienttestcase.Fail2banServerTest))
# bin/fail2ban-regex
tests.addTest(unittest.makeSuite(fail2banregextestcase.Fail2banRegexTest))
@ -321,13 +350,16 @@ class LogCaptureTestCase(unittest.TestCase):
# Let's log everything into a string
self._log = StringIO()
logSys.handlers = [logging.StreamHandler(self._log)]
if self._old_level < logging.DEBUG: # so if HEAVYDEBUG etc -- show them!
if self._old_level <= logging.DEBUG: # so if DEBUG etc -- show them (and log it in travis)!
print("")
logSys.handlers += self._old_handlers
logSys.debug('='*10 + ' %s ' + '='*20, self.id())
logSys.setLevel(getattr(logging, 'DEBUG'))
def tearDown(self):
"""Call after every test case."""
# print "O: >>%s<<" % self._log.getvalue()
self.pruneLog()
logSys = getLogger("fail2ban")
logSys.handlers = self._old_handlers
logSys.level = self._old_level