mirror of https://github.com/fail2ban/fail2ban
Merge pull request #1413 from sebres/f2b-perfom-prepare-716-cs-0.10
0.10-cs: client-server rewritten as modules / start in foreground / test coverage for cspull/1484/head
commit
bf3188c290
3
MANIFEST
3
MANIFEST
|
@ -156,8 +156,11 @@ fail2ban/client/configparserinc.py
|
||||||
fail2ban/client/configreader.py
|
fail2ban/client/configreader.py
|
||||||
fail2ban/client/configurator.py
|
fail2ban/client/configurator.py
|
||||||
fail2ban/client/csocket.py
|
fail2ban/client/csocket.py
|
||||||
|
fail2ban/client/fail2banclient.py
|
||||||
|
fail2ban/client/fail2bancmdline.py
|
||||||
fail2ban/client/fail2banreader.py
|
fail2ban/client/fail2banreader.py
|
||||||
fail2ban/client/fail2banregex.py
|
fail2ban/client/fail2banregex.py
|
||||||
|
fail2ban/client/fail2banserver.py
|
||||||
fail2ban/client/filterreader.py
|
fail2ban/client/filterreader.py
|
||||||
fail2ban/client/__init__.py
|
fail2ban/client/__init__.py
|
||||||
fail2ban/client/jailreader.py
|
fail2ban/client/jailreader.py
|
||||||
|
|
|
@ -18,458 +18,20 @@
|
||||||
# along with Fail2Ban; if not, write to the Free Software
|
# along with Fail2Ban; if not, write to the Free Software
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# 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"
|
__license__ = "GPL"
|
||||||
|
|
||||||
import getopt
|
from fail2ban.client.fail2banclient import exec_command_line, sys
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import pickle
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import signal
|
|
||||||
import socket
|
|
||||||
import string
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
from fail2ban.version import version
|
if __name__ == "__main__":
|
||||||
from fail2ban.protocol import printFormatted
|
exec_command_line(sys.argv)
|
||||||
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)
|
|
||||||
|
|
|
@ -18,123 +18,20 @@
|
||||||
# along with Fail2Ban; if not, write to the Free Software
|
# along with Fail2Ban; if not, write to the Free Software
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# 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"
|
__license__ = "GPL"
|
||||||
|
|
||||||
import getopt
|
from fail2ban.client.fail2banserver import exec_command_line, sys
|
||||||
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
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
server = Fail2banServer()
|
exec_command_line(sys.argv)
|
||||||
if server.start(sys.argv):
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
|
@ -119,9 +119,13 @@ else:
|
||||||
|
|
||||||
# Custom log format for the verbose tests runs
|
# Custom log format for the verbose tests runs
|
||||||
if verbosity > 1: # pragma: no cover
|
if verbosity > 1: # pragma: no cover
|
||||||
stdout.setFormatter(Formatter(' %(asctime)-15s %(thread)s' + fmt))
|
if verbosity > 3:
|
||||||
else: # pragma: no cover
|
fmt = ' | %(module)15.15s-%(levelno)-2d: %(funcName)-20.20s |' + fmt
|
||||||
# just prefix with the space
|
if verbosity > 2:
|
||||||
|
fmt = ' +%(relativeCreated)5d %(thread)X %(name)-25.25s %(levelname)-5.5s' + fmt
|
||||||
|
else:
|
||||||
|
fmt = ' %(asctime)-15s %(thread)X %(levelname)-5.5s' + fmt
|
||||||
|
#
|
||||||
stdout.setFormatter(Formatter(fmt))
|
stdout.setFormatter(Formatter(fmt))
|
||||||
logSys.addHandler(stdout)
|
logSys.addHandler(stdout)
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,8 @@ class Beautifier:
|
||||||
msg = "Added jail " + response
|
msg = "Added jail " + response
|
||||||
elif inC[0] == "flushlogs":
|
elif inC[0] == "flushlogs":
|
||||||
msg = "logs: " + response
|
msg = "logs: " + response
|
||||||
|
elif inC[0] == "echo":
|
||||||
|
msg = ' '.join(msg)
|
||||||
elif inC[0:1] == ['status']:
|
elif inC[0:1] == ['status']:
|
||||||
if len(inC) > 1:
|
if len(inC) > 1:
|
||||||
# Display information
|
# Display information
|
||||||
|
|
|
@ -208,7 +208,7 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes):
|
||||||
# Or it is a dict:
|
# Or it is a dict:
|
||||||
# {name: [type, default], ...}
|
# {name: [type, default], ...}
|
||||||
|
|
||||||
def getOptions(self, sec, options, pOptions=None):
|
def getOptions(self, sec, options, pOptions=None, shouldExist=False):
|
||||||
values = dict()
|
values = dict()
|
||||||
for optname in options:
|
for optname in options:
|
||||||
if isinstance(options, (list,tuple)):
|
if isinstance(options, (list,tuple)):
|
||||||
|
@ -229,6 +229,8 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes):
|
||||||
continue
|
continue
|
||||||
values[optname] = v
|
values[optname] = v
|
||||||
except NoSectionError, e:
|
except NoSectionError, e:
|
||||||
|
if shouldExist:
|
||||||
|
raise
|
||||||
# No "Definition" section or wrong basedir
|
# No "Definition" section or wrong basedir
|
||||||
logSys.error(e)
|
logSys.error(e)
|
||||||
values[optname] = optvalue
|
values[optname] = optvalue
|
||||||
|
|
|
@ -0,0 +1,463 @@
|
||||||
|
#!/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, 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):
|
||||||
|
try:
|
||||||
|
import readline
|
||||||
|
except ImportError:
|
||||||
|
raise ServerExecutionException("Readline not available")
|
||||||
|
try:
|
||||||
|
ret = True
|
||||||
|
if len(args) > 0:
|
||||||
|
ret = self.__processCommand(args)
|
||||||
|
if ret:
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class _VisualWait:
|
||||||
|
"""Small progress indication (as "wonderful visual") during waiting process
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
"""Show or step for progress indicator
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Mockup for invisible progress indication (not verbose)
|
||||||
|
"""
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
def __exit__(self, *args):
|
||||||
|
pass
|
||||||
|
def heartbeat(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def VisualWait(verbose, *args, **kwargs):
|
||||||
|
"""Wonderful visual progress indication (if verbose)
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
|
@ -0,0 +1,285 @@
|
||||||
|
#!/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).")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
#
|
||||||
|
# _exit is made to ease mocking out of the behaviour in tests,
|
||||||
|
# since method is also exposed in API via globally bound variable
|
||||||
|
@staticmethod
|
||||||
|
def _exit(code=0):
|
||||||
|
if hasattr(os, '_exit') and os._exit:
|
||||||
|
os._exit(code)
|
||||||
|
else:
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def exit(code=0):
|
||||||
|
logSys.debug("Exit with code %s", code)
|
||||||
|
Fail2banCmdLine._exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
# global exit handler:
|
||||||
|
exit = Fail2banCmdLine.exit
|
||||||
|
|
||||||
|
|
||||||
|
class ExitException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ServerExecutionException(Exception):
|
||||||
|
pass
|
|
@ -40,8 +40,13 @@ class Fail2banReader(ConfigReader):
|
||||||
ConfigReader.read(self, "fail2ban")
|
ConfigReader.read(self, "fail2ban")
|
||||||
|
|
||||||
def getEarlyOptions(self):
|
def getEarlyOptions(self):
|
||||||
opts = [["string", "socket", "/var/run/fail2ban/fail2ban.sock"],
|
opts = [
|
||||||
["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"]]
|
["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)
|
return ConfigReader.getOptions(self, "Definition", opts)
|
||||||
|
|
||||||
def getOptions(self):
|
def getOptions(self):
|
||||||
|
|
|
@ -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)
|
|
@ -119,7 +119,7 @@ class JailReader(ConfigReader):
|
||||||
defsec["fail2ban_version"] = version
|
defsec["fail2ban_version"] = version
|
||||||
|
|
||||||
# Read first options only needed for merge defaults ('known/...' from filter):
|
# 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:
|
if not self.__opts:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,12 @@ __license__ = "GPL"
|
||||||
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
def output(s):
|
||||||
|
"""Default output handler for printing protocol.
|
||||||
|
Used to ease mocking in the test cases.
|
||||||
|
"""
|
||||||
|
print(s)
|
||||||
|
|
||||||
##
|
##
|
||||||
# Describes the protocol used to communicate with the server.
|
# Describes the protocol used to communicate with the server.
|
||||||
|
|
||||||
|
@ -42,11 +48,13 @@ CSPROTO = dotdict({
|
||||||
protocol = [
|
protocol = [
|
||||||
['', "BASIC", ""],
|
['', "BASIC", ""],
|
||||||
["start", "starts the server and the jails"],
|
["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>"],
|
["reload <JAIL>", "reloads the jail <JAIL>"],
|
||||||
["stop", "stops all jails and terminate the server"],
|
["stop", "stops all jails and terminate the server"],
|
||||||
["status", "gets the current status of 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"],
|
["help", "return this output"],
|
||||||
["version", "return the server version"],
|
["version", "return the server version"],
|
||||||
['', "LOGGING", ""],
|
['', "LOGGING", ""],
|
||||||
|
@ -141,7 +149,7 @@ def printFormatted():
|
||||||
firstHeading = False
|
firstHeading = False
|
||||||
for m in protocol:
|
for m in protocol:
|
||||||
if m[0] == '' and firstHeading:
|
if m[0] == '' and firstHeading:
|
||||||
print
|
output("")
|
||||||
firstHeading = True
|
firstHeading = True
|
||||||
first = True
|
first = True
|
||||||
if len(m[0]) >= MARGIN:
|
if len(m[0]) >= MARGIN:
|
||||||
|
@ -152,7 +160,7 @@ def printFormatted():
|
||||||
first = False
|
first = False
|
||||||
else:
|
else:
|
||||||
line = ' ' * (INDENT + MARGIN) + n.strip()
|
line = ' ' * (INDENT + MARGIN) + n.strip()
|
||||||
print line
|
output(line)
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -163,20 +171,20 @@ def printWiki():
|
||||||
for m in protocol:
|
for m in protocol:
|
||||||
if m[0] == '':
|
if m[0] == '':
|
||||||
if firstHeading:
|
if firstHeading:
|
||||||
print "|}"
|
output("|}")
|
||||||
__printWikiHeader(m[1], m[2])
|
__printWikiHeader(m[1], m[2])
|
||||||
firstHeading = True
|
firstHeading = True
|
||||||
else:
|
else:
|
||||||
print "|-"
|
output("|-")
|
||||||
print "| <span style=\"white-space:nowrap;\"><tt>" + m[0] + "</tt></span> || || " + m[1]
|
output("| <span style=\"white-space:nowrap;\"><tt>" + m[0] + "</tt></span> || || " + m[1])
|
||||||
print "|}"
|
output("|}")
|
||||||
|
|
||||||
|
|
||||||
def __printWikiHeader(section, desc):
|
def __printWikiHeader(section, desc):
|
||||||
print
|
output("")
|
||||||
print "=== " + section + " ==="
|
output("=== " + section + " ===")
|
||||||
print
|
output("")
|
||||||
print desc
|
output(desc)
|
||||||
print
|
output("")
|
||||||
print "{|"
|
output("{|")
|
||||||
print "| '''Command''' || || '''Description'''"
|
output("| '''Command''' || || '''Description'''")
|
||||||
|
|
|
@ -51,6 +51,8 @@ class JailThread(Thread):
|
||||||
|
|
||||||
def __init__(self, name=None):
|
def __init__(self, name=None):
|
||||||
super(JailThread, self).__init__(name=name)
|
super(JailThread, self).__init__(name=name)
|
||||||
|
## Should going with main thread also:
|
||||||
|
self.daemon = True
|
||||||
## Control the state of the thread.
|
## Control the state of the thread.
|
||||||
self.active = False
|
self.active = False
|
||||||
## Control the idle state of the thread.
|
## Control the idle state of the thread.
|
||||||
|
|
|
@ -24,6 +24,7 @@ __author__ = "Cyril Jaquier"
|
||||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
|
|
||||||
|
import threading
|
||||||
from threading import Lock, RLock
|
from threading import Lock, RLock
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
|
@ -42,6 +43,10 @@ from ..helpers import getLogger, excepthook
|
||||||
# Gets the instance of the logger.
|
# Gets the instance of the logger.
|
||||||
logSys = getLogger(__name__)
|
logSys = getLogger(__name__)
|
||||||
|
|
||||||
|
DEF_SYSLOGSOCKET = "auto"
|
||||||
|
DEF_LOGLEVEL = "INFO"
|
||||||
|
DEF_LOGTARGET = "STDOUT"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .database import Fail2BanDb
|
from .database import Fail2BanDb
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
|
@ -49,6 +54,10 @@ except ImportError: # pragma: no cover
|
||||||
Fail2BanDb = None
|
Fail2BanDb = None
|
||||||
|
|
||||||
|
|
||||||
|
def _thread_name():
|
||||||
|
return threading.current_thread().__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
class Server:
|
class Server:
|
||||||
|
|
||||||
def __init__(self, daemon=False):
|
def __init__(self, daemon=False):
|
||||||
|
@ -58,7 +67,8 @@ class Server:
|
||||||
self.__db = None
|
self.__db = None
|
||||||
self.__daemon = daemon
|
self.__daemon = daemon
|
||||||
self.__transm = Transmitter(self)
|
self.__transm = Transmitter(self)
|
||||||
self.__asyncServer = AsyncServer(self.__transm)
|
#self.__asyncServer = AsyncServer(self.__transm)
|
||||||
|
self.__asyncServer = None
|
||||||
self.__logLevel = None
|
self.__logLevel = None
|
||||||
self.__logTarget = None
|
self.__logTarget = None
|
||||||
self.__syslogSocket = None
|
self.__syslogSocket = None
|
||||||
|
@ -67,10 +77,7 @@ class Server:
|
||||||
'FreeBSD': '/var/run/log',
|
'FreeBSD': '/var/run/log',
|
||||||
'Linux': '/dev/log',
|
'Linux': '/dev/log',
|
||||||
}
|
}
|
||||||
self.setSyslogSocket("auto")
|
self.__prev_signals = {}
|
||||||
# Set logging level
|
|
||||||
self.setLogLevel("INFO")
|
|
||||||
self.setLogTarget("STDOUT")
|
|
||||||
|
|
||||||
def __sigTERMhandler(self, signum, frame):
|
def __sigTERMhandler(self, signum, frame):
|
||||||
logSys.debug("Caught signal %d. Exiting" % signum)
|
logSys.debug("Caught signal %d. Exiting" % signum)
|
||||||
|
@ -80,27 +87,50 @@ class Server:
|
||||||
logSys.debug("Caught signal %d. Flushing logs" % signum)
|
logSys.debug("Caught signal %d. Flushing logs" % signum)
|
||||||
self.flushLogs()
|
self.flushLogs()
|
||||||
|
|
||||||
def start(self, sock, pidfile, force = False):
|
def _rebindSignal(self, s, new):
|
||||||
logSys.info("Starting Fail2ban v%s", version.version)
|
"""Bind new signal handler while storing old one in _prev_signals"""
|
||||||
|
self.__prev_signals[s] = signal.getsignal(s)
|
||||||
# Install signal handlers
|
signal.signal(s, new)
|
||||||
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
|
# First set the mask to only allow access to owner
|
||||||
os.umask(0077)
|
os.umask(0077)
|
||||||
|
# Second daemonize before logging etc, because it will close all handles:
|
||||||
if self.__daemon: # pragma: no cover
|
if self.__daemon: # pragma: no cover
|
||||||
logSys.info("Starting in daemon mode")
|
logSys.info("Starting in daemon mode")
|
||||||
ret = self.__createDaemon()
|
ret = self.__createDaemon()
|
||||||
if ret:
|
# 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")
|
logSys.info("Daemon started")
|
||||||
else:
|
|
||||||
logSys.error("Could not create daemon")
|
# Install signal handlers
|
||||||
raise ServerInitializationError("Could not create daemon")
|
if _thread_name() == '_MainThread':
|
||||||
|
for s in (signal.SIGTERM, signal.SIGINT):
|
||||||
|
self._rebindSignal(s, self.__sigTERMhandler)
|
||||||
|
self._rebindSignal(signal.SIGUSR1, self.__sigUSR1handler)
|
||||||
|
|
||||||
|
# Ensure unhandled exceptions are logged
|
||||||
|
sys.excepthook = excepthook
|
||||||
|
|
||||||
# Creates a PID file.
|
# Creates a PID file.
|
||||||
try:
|
try:
|
||||||
|
@ -114,6 +144,7 @@ class Server:
|
||||||
# Start the communication
|
# Start the communication
|
||||||
logSys.debug("Starting communication")
|
logSys.debug("Starting communication")
|
||||||
try:
|
try:
|
||||||
|
self.__asyncServer = AsyncServer(self.__transm)
|
||||||
self.__asyncServer.start(sock, force)
|
self.__asyncServer.start(sock, force)
|
||||||
except AsyncServerException, e:
|
except AsyncServerException, e:
|
||||||
logSys.error("Could not start server: %s", e)
|
logSys.error("Could not start server: %s", e)
|
||||||
|
@ -132,17 +163,25 @@ class Server:
|
||||||
# communications first (which should be ok anyways since we
|
# communications first (which should be ok anyways since we
|
||||||
# are exiting)
|
# are exiting)
|
||||||
# See https://github.com/fail2ban/fail2ban/issues/7
|
# See https://github.com/fail2ban/fail2ban/issues/7
|
||||||
|
if self.__asyncServer is not None:
|
||||||
self.__asyncServer.stop()
|
self.__asyncServer.stop()
|
||||||
|
self.__asyncServer = None
|
||||||
|
|
||||||
# Now stop all the jails
|
# Now stop all the jails
|
||||||
self.stopAllJail()
|
self.stopAllJail()
|
||||||
|
|
||||||
# Only now shutdown the logging.
|
# Only now shutdown the logging.
|
||||||
try:
|
if self.__logTarget is not None:
|
||||||
self.__loggingLock.acquire()
|
with self.__loggingLock:
|
||||||
logging.shutdown()
|
logging.shutdown()
|
||||||
finally:
|
|
||||||
self.__loggingLock.release()
|
# 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):
|
def addJail(self, name, backend):
|
||||||
self.__jails.add(name, backend, self.__db)
|
self.__jails.add(name, backend, self.__db)
|
||||||
|
@ -337,6 +376,9 @@ class Server:
|
||||||
def getBanTime(self, name):
|
def getBanTime(self, name):
|
||||||
return self.__jails[name].actions.getBanTime()
|
return self.__jails[name].actions.getBanTime()
|
||||||
|
|
||||||
|
def isStarted(self):
|
||||||
|
return self.__asyncServer is not None and self.__asyncServer.isActive()
|
||||||
|
|
||||||
def isAlive(self, jailnum=None):
|
def isAlive(self, jailnum=None):
|
||||||
if jailnum is not None and len(self.__jails) != jailnum:
|
if jailnum is not None and len(self.__jails) != jailnum:
|
||||||
return 0
|
return 0
|
||||||
|
@ -375,16 +417,15 @@ class Server:
|
||||||
# @param value the level
|
# @param value the level
|
||||||
|
|
||||||
def setLogLevel(self, value):
|
def setLogLevel(self, value):
|
||||||
|
value = value.upper()
|
||||||
|
with self.__loggingLock:
|
||||||
|
if self.__logLevel == value:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
self.__loggingLock.acquire()
|
getLogger("fail2ban").setLevel(getattr(logging, value))
|
||||||
getLogger("fail2ban").setLevel(
|
self.__logLevel = value
|
||||||
getattr(logging, value.upper()))
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ValueError("Invalid log level")
|
raise ValueError("Invalid log level %r" % value)
|
||||||
else:
|
|
||||||
self.__logLevel = value.upper()
|
|
||||||
finally:
|
|
||||||
self.__loggingLock.release()
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Get the logging level.
|
# Get the logging level.
|
||||||
|
@ -393,11 +434,8 @@ class Server:
|
||||||
# @return the log level
|
# @return the log level
|
||||||
|
|
||||||
def getLogLevel(self):
|
def getLogLevel(self):
|
||||||
try:
|
with self.__loggingLock:
|
||||||
self.__loggingLock.acquire()
|
|
||||||
return self.__logLevel
|
return self.__logLevel
|
||||||
finally:
|
|
||||||
self.__loggingLock.release()
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Sets the logging target.
|
# Sets the logging target.
|
||||||
|
@ -406,8 +444,14 @@ class Server:
|
||||||
# @param target the logging target
|
# @param target the logging target
|
||||||
|
|
||||||
def setLogTarget(self, target):
|
def setLogTarget(self, target):
|
||||||
try:
|
with self.__loggingLock:
|
||||||
self.__loggingLock.acquire()
|
# 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
|
# set a format which is simpler for console use
|
||||||
formatter = logging.Formatter("%(asctime)s %(name)-24s[%(process)d]: %(levelname)-7s %(message)s")
|
formatter = logging.Formatter("%(asctime)s %(name)-24s[%(process)d]: %(levelname)-7s %(message)s")
|
||||||
if target == "SYSLOG":
|
if target == "SYSLOG":
|
||||||
|
@ -463,7 +507,7 @@ class Server:
|
||||||
hdlr.setFormatter(formatter)
|
hdlr.setFormatter(formatter)
|
||||||
logger.addHandler(hdlr)
|
logger.addHandler(hdlr)
|
||||||
# Does not display this message at startup.
|
# Does not display this message at startup.
|
||||||
if not self.__logTarget is None:
|
if self.__logTarget is not None:
|
||||||
logSys.info("Start Fail2ban v%s", version.version)
|
logSys.info("Start Fail2ban v%s", version.version)
|
||||||
logSys.info(
|
logSys.info(
|
||||||
"Changed logging target to %s for Fail2ban v%s"
|
"Changed logging target to %s for Fail2ban v%s"
|
||||||
|
@ -475,8 +519,6 @@ class Server:
|
||||||
# Sets the logging target.
|
# Sets the logging target.
|
||||||
self.__logTarget = target
|
self.__logTarget = target
|
||||||
return True
|
return True
|
||||||
finally:
|
|
||||||
self.__loggingLock.release()
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Sets the syslog socket.
|
# Sets the syslog socket.
|
||||||
|
@ -484,24 +526,21 @@ class Server:
|
||||||
# syslogsocket is the full path to the syslog socket
|
# syslogsocket is the full path to the syslog socket
|
||||||
# @param syslogsocket the syslog socket path
|
# @param syslogsocket the syslog socket path
|
||||||
def setSyslogSocket(self, syslogsocket):
|
def setSyslogSocket(self, syslogsocket):
|
||||||
|
with self.__loggingLock:
|
||||||
|
if self.__syslogSocket == syslogsocket:
|
||||||
|
return True
|
||||||
self.__syslogSocket = syslogsocket
|
self.__syslogSocket = syslogsocket
|
||||||
# Conditionally reload, logtarget depends on socket path when SYSLOG
|
# Conditionally reload, logtarget depends on socket path when SYSLOG
|
||||||
return self.__logTarget != "SYSLOG"\
|
return self.__logTarget != "SYSLOG"\
|
||||||
or self.setLogTarget(self.__logTarget)
|
or self.setLogTarget(self.__logTarget)
|
||||||
|
|
||||||
def getLogTarget(self):
|
def getLogTarget(self):
|
||||||
try:
|
with self.__loggingLock:
|
||||||
self.__loggingLock.acquire()
|
|
||||||
return self.__logTarget
|
return self.__logTarget
|
||||||
finally:
|
|
||||||
self.__loggingLock.release()
|
|
||||||
|
|
||||||
def getSyslogSocket(self):
|
def getSyslogSocket(self):
|
||||||
try:
|
with self.__loggingLock:
|
||||||
self.__loggingLock.acquire()
|
|
||||||
return self.__syslogSocket
|
return self.__syslogSocket
|
||||||
finally:
|
|
||||||
self.__loggingLock.release()
|
|
||||||
|
|
||||||
def flushLogs(self):
|
def flushLogs(self):
|
||||||
if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']:
|
if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']:
|
||||||
|
@ -555,7 +594,7 @@ class Server:
|
||||||
# We need to set this in the parent process, so it gets inherited by the
|
# 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
|
# child process, and this makes sure that it is effect even if the parent
|
||||||
# terminates quickly.
|
# terminates quickly.
|
||||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
self._rebindSignal(signal.SIGHUP, signal.SIG_IGN)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Fork a child process so the parent can exit. This will return control
|
# Fork a child process so the parent can exit. This will return control
|
||||||
|
@ -566,7 +605,7 @@ class Server:
|
||||||
# PGID.
|
# PGID.
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
except OSError, e:
|
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.
|
if pid == 0: # The first child.
|
||||||
|
|
||||||
|
@ -587,7 +626,7 @@ class Server:
|
||||||
# preventing the daemon from ever acquiring a controlling terminal.
|
# preventing the daemon from ever acquiring a controlling terminal.
|
||||||
pid = os.fork() # Fork a second child.
|
pid = os.fork() # Fork a second child.
|
||||||
except OSError, e:
|
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.
|
if (pid == 0): # The second child.
|
||||||
# Ensure that the daemon doesn't keep any directory in use. Failure
|
# Ensure that the daemon doesn't keep any directory in use. Failure
|
||||||
|
@ -596,7 +635,8 @@ class Server:
|
||||||
else:
|
else:
|
||||||
os._exit(0) # Exit parent (the first child) of the second child.
|
os._exit(0) # Exit parent (the first child) of the second child.
|
||||||
else:
|
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,
|
# 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
|
# for the maximum number of open files to close. If it doesn't exist, use
|
||||||
|
@ -624,7 +664,7 @@ class Server:
|
||||||
os.open("/dev/null", os.O_RDONLY) # standard input (0)
|
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 output (1)
|
||||||
os.open("/dev/null", os.O_RDWR) # standard error (2)
|
os.open("/dev/null", os.O_RDWR) # standard error (2)
|
||||||
return True
|
return (True,)
|
||||||
|
|
||||||
|
|
||||||
class ServerInitializationError(Exception):
|
class ServerInitializationError(Exception):
|
||||||
|
|
|
@ -93,6 +93,8 @@ class Transmitter:
|
||||||
name = command[1]
|
name = command[1]
|
||||||
self.__server.stopJail(name)
|
self.__server.stopJail(name)
|
||||||
return None
|
return None
|
||||||
|
elif command[0] == "echo":
|
||||||
|
return command[1:]
|
||||||
elif command[0] == "sleep":
|
elif command[0] == "sleep":
|
||||||
value = command[1]
|
value = command[1]
|
||||||
time.sleep(float(value))
|
time.sleep(float(value))
|
||||||
|
|
|
@ -65,6 +65,7 @@ class SMTPActionTest(unittest.TestCase):
|
||||||
self._active = True
|
self._active = True
|
||||||
self._loop_thread = threading.Thread(
|
self._loop_thread = threading.Thread(
|
||||||
target=asyncserver.loop, kwargs={'active': lambda: self._active})
|
target=asyncserver.loop, kwargs={'active': lambda: self._active})
|
||||||
|
self._loop_thread.daemon = True
|
||||||
self._loop_thread.start()
|
self._loop_thread.start()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
|
|
@ -45,8 +45,8 @@ class CommandActionTest(LogCaptureTestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Call after every test case."""
|
"""Call after every test case."""
|
||||||
LogCaptureTestCase.tearDown(self)
|
|
||||||
self.__action.stop()
|
self.__action.stop()
|
||||||
|
LogCaptureTestCase.tearDown(self)
|
||||||
|
|
||||||
def testSubstituteRecursiveTags(self):
|
def testSubstituteRecursiveTags(self):
|
||||||
aInfo = {
|
aInfo = {
|
||||||
|
|
|
@ -0,0 +1,614 @@
|
||||||
|
# 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 os.path import join as pjoin, isdir, isfile, exists, dirname
|
||||||
|
from functools import wraps
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from ..client import fail2banclient, fail2banserver, fail2bancmdline
|
||||||
|
from ..client.fail2bancmdline import Fail2banCmdLine
|
||||||
|
from ..client.fail2banclient import 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, with_tmpdir, shutil, logging
|
||||||
|
|
||||||
|
from ..helpers import getLogger
|
||||||
|
|
||||||
|
# Gets the instance of the logger.
|
||||||
|
logSys = getLogger(__name__)
|
||||||
|
|
||||||
|
STOCK_CONF_DIR = "config"
|
||||||
|
STOCK = exists(pjoin(STOCK_CONF_DIR, 'fail2ban.conf'))
|
||||||
|
|
||||||
|
CLIENT = "fail2ban-client"
|
||||||
|
SERVER = "fail2ban-server"
|
||||||
|
BIN = 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
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Mocking .exit so we could test its correct operation.
|
||||||
|
# Two custom exceptions will be assessed to be raised in the tests
|
||||||
|
#
|
||||||
|
|
||||||
|
class ExitException(fail2bancmdline.ExitException):
|
||||||
|
"""Exception upon a normal exit"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FailExitException(fail2bancmdline.ExitException):
|
||||||
|
"""Exception upon abnormal exit"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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 = \
|
||||||
|
fail2banserver.PRODUCTION = False
|
||||||
|
|
||||||
|
|
||||||
|
def _out_file(fn):
|
||||||
|
"""Helper which outputs content of the file at HEAVYDEBUG loglevels"""
|
||||||
|
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 = pjoin(tmp, "config")
|
||||||
|
if use_stock and STOCK:
|
||||||
|
# copy config (sub-directories as alias):
|
||||||
|
def ig_dirs(dir, files):
|
||||||
|
"""Filters list of 'files' to contain only directories (under dir)"""
|
||||||
|
return [f for f in files if isdir(pjoin(dir, f))]
|
||||||
|
shutil.copytree(STOCK_CONF_DIR, cfg, ignore=ig_dirs)
|
||||||
|
os.symlink(pjoin(STOCK_CONF_DIR, "action.d"), pjoin(cfg, "action.d"))
|
||||||
|
os.symlink(pjoin(STOCK_CONF_DIR, "filter.d"), pjoin(cfg, "filter.d"))
|
||||||
|
# replace fail2ban params (database with memory):
|
||||||
|
r = re.compile(r'^dbfile\s*=')
|
||||||
|
for line in fileinput.input(pjoin(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(pjoin(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(pjoin(cfg, "fail2ban.conf"), "w")
|
||||||
|
f.write('\n'.join((
|
||||||
|
"[Definition]",
|
||||||
|
"loglevel = INFO",
|
||||||
|
"logtarget = " + logtarget,
|
||||||
|
"syslogsocket = auto",
|
||||||
|
"socket = " + pjoin(tmp, "f2b.sock"),
|
||||||
|
"pidfile = " + pjoin(tmp, "f2b.pid"),
|
||||||
|
"backend = polling",
|
||||||
|
"dbfile = :memory:",
|
||||||
|
"dbpurgeage = 1d",
|
||||||
|
"",
|
||||||
|
)))
|
||||||
|
f.close()
|
||||||
|
f = open(pjoin(cfg, "jail.conf"), "w")
|
||||||
|
f.write('\n'.join((
|
||||||
|
"[INCLUDES]", "",
|
||||||
|
"[DEFAULT]", "",
|
||||||
|
"",
|
||||||
|
)))
|
||||||
|
f.close()
|
||||||
|
if logSys.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.):
|
||||||
|
return (
|
||||||
|
"-c", cfg, "-s", pjoin(tmp, "f2b.sock"), "-p", pjoin(tmp, "f2b.pid"),
|
||||||
|
"-vv", "--logtarget", logtarget, "--loglevel", "DEBUG", "--syslogsocket", "auto",
|
||||||
|
"--timeout", str(fail2bancmdline.MAX_WAITTIME),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _kill_srv(pidfile):
|
||||||
|
logSys.debug("cleanup: %r", (pidfile, isdir(pidfile)))
|
||||||
|
if isdir(pidfile):
|
||||||
|
piddir = pidfile
|
||||||
|
pidfile = pjoin(piddir, "f2b.pid")
|
||||||
|
if not isfile(pidfile): # pragma: no cover
|
||||||
|
pidfile = pjoin(piddir, "fail2ban.pid")
|
||||||
|
|
||||||
|
if not 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()
|
||||||
|
pid = re.match(r'\S+', pid).group()
|
||||||
|
pid = int(pid)
|
||||||
|
except Exception as e: # pragma: no cover
|
||||||
|
logSys.debug(e)
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
if f is not None:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
logSys.debug("cleanup pid: %r", pid)
|
||||||
|
if pid <= 0 or pid == os.getpid(): # pragma: no cover
|
||||||
|
raise ValueError('pid %s of %s is invalid' % (pid, pidfile))
|
||||||
|
if not Utils.pid_exists(pid):
|
||||||
|
return True
|
||||||
|
## try to properly stop (have signal handler):
|
||||||
|
os.kill(pid, signal.SIGTERM)
|
||||||
|
## check still exists after small timeout:
|
||||||
|
if not Utils.wait_for(lambda: not Utils.pid_exists(pid), 1):
|
||||||
|
## try to kill hereafter:
|
||||||
|
os.kill(pid, signal.SIGKILL)
|
||||||
|
logSys.debug("cleanup: kill ready")
|
||||||
|
return not Utils.pid_exists(pid)
|
||||||
|
except Exception as e: # pragma: no cover
|
||||||
|
logSys.exception(e)
|
||||||
|
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):
|
||||||
|
|
||||||
|
_orig_exit = Fail2banCmdLine._exit
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Call before every test case."""
|
||||||
|
LogCaptureTestCase.setUp(self)
|
||||||
|
Fail2banCmdLine._exit = staticmethod(self._test_exit)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Call after every test case."""
|
||||||
|
Fail2banCmdLine._exit = self._orig_exit
|
||||||
|
LogCaptureTestCase.tearDown(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _test_exit(code=0):
|
||||||
|
if code == 0:
|
||||||
|
raise ExitException()
|
||||||
|
else:
|
||||||
|
raise FailExitException()
|
||||||
|
|
||||||
|
def _wait_for_srv(self, tmp, ready=True, startparams=None):
|
||||||
|
try:
|
||||||
|
sock = pjoin(tmp, "f2b.sock")
|
||||||
|
# wait for server (socket):
|
||||||
|
ret = Utils.wait_for(lambda: 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 = pjoin(tmp, "f2b.log")
|
||||||
|
if isfile(log):
|
||||||
|
_out_file(log)
|
||||||
|
else:
|
||||||
|
logSys.debug("No log file %s to examine details of error", log)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def execSuccess(self, startparams, *args):
|
||||||
|
raise NotImplementedError("To be defined in subclass")
|
||||||
|
|
||||||
|
def execFailed(self, startparams, *args):
|
||||||
|
raise NotImplementedError("To be defined in subclass")
|
||||||
|
|
||||||
|
#
|
||||||
|
# Common tests
|
||||||
|
#
|
||||||
|
def _testStartForeground(self, tmp, startparams, phase):
|
||||||
|
# start and wait to end (foreground):
|
||||||
|
logSys.debug("start of test worker")
|
||||||
|
phase['start'] = True
|
||||||
|
self.execSuccess(("-f",) + startparams, "start")
|
||||||
|
# end :
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
class Fail2banClientTest(Fail2banClientServerBase):
|
||||||
|
|
||||||
|
def execSuccess(self, startparams, *args):
|
||||||
|
self.assertRaises(ExitException, _exec_client,
|
||||||
|
((CLIENT,) + startparams + args))
|
||||||
|
|
||||||
|
def execFailed(self, startparams, *args):
|
||||||
|
self.assertRaises(FailExitException, _exec_client,
|
||||||
|
((CLIENT,) + startparams + args))
|
||||||
|
|
||||||
|
def testConsistency(self):
|
||||||
|
self.assertTrue(isfile(pjoin(BIN, CLIENT)))
|
||||||
|
self.assertTrue(isfile(pjoin(BIN, SERVER)))
|
||||||
|
|
||||||
|
def testClientUsage(self):
|
||||||
|
self.execSuccess((), "-h")
|
||||||
|
self.assertLogged("Usage: " + CLIENT)
|
||||||
|
self.assertLogged("Report bugs to ")
|
||||||
|
self.pruneLog()
|
||||||
|
self.execSuccess((), "-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.execSuccess(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.execSuccess(("-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.execSuccess(startparams, "echo", "TEST-ECHO")
|
||||||
|
self.execFailed(startparams, "~~unknown~cmd~failed~~")
|
||||||
|
self.pruneLog()
|
||||||
|
# start again (should fail):
|
||||||
|
self.execFailed(("-b",) + startparams, "start")
|
||||||
|
self.assertLogged("Server already running")
|
||||||
|
finally:
|
||||||
|
self.pruneLog()
|
||||||
|
# stop:
|
||||||
|
self.execSuccess(startparams, "stop")
|
||||||
|
self.assertLogged("Shutdown successful")
|
||||||
|
self.assertLogged("Exit with code 0")
|
||||||
|
|
||||||
|
self.pruneLog()
|
||||||
|
# stop again (should fail):
|
||||||
|
self.execFailed(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=pjoin(tmp, "f2b.log"))
|
||||||
|
# start (in new process, using the same python version):
|
||||||
|
cmd = (sys.executable, pjoin(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.execSuccess(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.execSuccess(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.execSuccess(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.execSuccess(startparams, "-i")
|
||||||
|
self.assertLogged("Failed during configuration: No section: '~~unknown~jail~fail~~'")
|
||||||
|
self.pruneLog()
|
||||||
|
# test reload missing jail (direct):
|
||||||
|
self.execFailed(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.execSuccess(startparams, "stop")
|
||||||
|
self.assertLogged("Shutdown successful")
|
||||||
|
self.assertLogged("Exit with code 0")
|
||||||
|
|
||||||
|
@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.execFailed((),
|
||||||
|
"--async", "-c", pjoin(tmp, "miss"), "start")
|
||||||
|
self.assertLogged("Base configuration directory " + pjoin(tmp, "miss") + " does not exist")
|
||||||
|
self.pruneLog()
|
||||||
|
|
||||||
|
## wrong socket
|
||||||
|
self.execFailed((),
|
||||||
|
"--async", "-c", pjoin(tmp, "config"), "-s", pjoin(tmp, "miss/f2b.sock"), "start")
|
||||||
|
self.assertLogged("There is no directory " + pjoin(tmp, "miss") + " to contain the socket file")
|
||||||
|
self.pruneLog()
|
||||||
|
|
||||||
|
## not running
|
||||||
|
self.execFailed((),
|
||||||
|
"-c", pjoin(tmp, "config"), "-s", pjoin(tmp, "f2b.sock"), "reload")
|
||||||
|
self.assertLogged("Could not find server")
|
||||||
|
self.pruneLog()
|
||||||
|
|
||||||
|
## already exists:
|
||||||
|
open(pjoin(tmp, "f2b.sock"), 'a').close()
|
||||||
|
self.execFailed((),
|
||||||
|
"--async", "-c", pjoin(tmp, "config"), "-s", pjoin(tmp, "f2b.sock"), "start")
|
||||||
|
self.assertLogged("Fail2ban seems to be in unexpected state (not running but the socket exists)")
|
||||||
|
self.pruneLog()
|
||||||
|
os.remove(pjoin(tmp, "f2b.sock"))
|
||||||
|
|
||||||
|
## wrong option:
|
||||||
|
self.execFailed((), "-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 execSuccess(self, startparams, *args):
|
||||||
|
self.assertRaises(ExitException, _exec_server,
|
||||||
|
((SERVER,) + startparams + args))
|
||||||
|
|
||||||
|
def execFailed(self, startparams, *args):
|
||||||
|
self.assertRaises(FailExitException, _exec_server,
|
||||||
|
((SERVER,) + startparams + args))
|
||||||
|
|
||||||
|
def testServerUsage(self):
|
||||||
|
self.execSuccess((), "-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=pjoin(tmp, "f2b.log"))
|
||||||
|
# start (in new process, using the same python version):
|
||||||
|
cmd = (sys.executable, pjoin(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.execSuccess(startparams, "echo", "TEST-ECHO")
|
||||||
|
self.execFailed(startparams, "~~unknown~cmd~failed~~")
|
||||||
|
finally:
|
||||||
|
self.pruneLog()
|
||||||
|
# stop:
|
||||||
|
self.execSuccess(startparams, "stop")
|
||||||
|
self.assertLogged("Shutdown successful")
|
||||||
|
self.assertLogged("Exit with code 0")
|
||||||
|
|
||||||
|
@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.execFailed((),
|
||||||
|
"-c", pjoin(tmp, "miss"))
|
||||||
|
self.assertLogged("Base configuration directory " + pjoin(tmp, "miss") + " does not exist")
|
||||||
|
self.pruneLog()
|
||||||
|
|
||||||
|
## wrong socket
|
||||||
|
self.execFailed((),
|
||||||
|
"-c", pjoin(tmp, "config"), "-x", "-s", pjoin(tmp, "miss/f2b.sock"))
|
||||||
|
self.assertLogged("There is no directory " + pjoin(tmp, "miss") + " to contain the socket file")
|
||||||
|
self.pruneLog()
|
||||||
|
|
||||||
|
## already exists:
|
||||||
|
open(pjoin(tmp, "f2b.sock"), 'a').close()
|
||||||
|
self.execFailed((),
|
||||||
|
"-c", pjoin(tmp, "config"), "-s", pjoin(tmp, "f2b.sock"))
|
||||||
|
self.assertLogged("Fail2ban seems to be in unexpected state (not running but the socket exists)")
|
||||||
|
self.pruneLog()
|
||||||
|
os.remove(pjoin(tmp, "f2b.sock"))
|
||||||
|
|
||||||
|
@with_tmpdir
|
||||||
|
def testKillAfterStart(self, tmp):
|
||||||
|
try:
|
||||||
|
# to prevent fork of test-cases process, start server in background via command:
|
||||||
|
startparams = _start_params(tmp, logtarget=pjoin(tmp, "f2b.log"))
|
||||||
|
# start (in new process, using the same python version):
|
||||||
|
cmd = (sys.executable, pjoin(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()
|
||||||
|
logSys.debug('Kill server ... %s', tmp)
|
||||||
|
finally:
|
||||||
|
self.assertTrue(_kill_srv(tmp))
|
||||||
|
# wait for end (kill was successful):
|
||||||
|
Utils.wait_for(lambda: not isfile(pjoin(tmp, "f2b.pid")), MAX_WAITTIME)
|
||||||
|
self.assertFalse(isfile(pjoin(tmp, "f2b.pid")))
|
||||||
|
self.assertLogged("cleanup: kill ready")
|
||||||
|
self.pruneLog()
|
||||||
|
# again:
|
||||||
|
self.assertTrue(_kill_srv(tmp))
|
||||||
|
self.assertLogged("cleanup: no pidfile for")
|
|
@ -23,19 +23,7 @@ __author__ = "Serg Brester"
|
||||||
__copyright__ = "Copyright (c) 2015 Serg G. Brester (sebres), 2008- Fail2Ban Contributors"
|
__copyright__ = "Copyright (c) 2015 Serg G. Brester (sebres), 2008- Fail2Ban Contributors"
|
||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
|
|
||||||
from __builtin__ import open as fopen
|
|
||||||
import unittest
|
|
||||||
import getpass
|
|
||||||
import os
|
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 import fail2banregex
|
||||||
from ..client.fail2banregex import Fail2banRegex, get_opt_parser, output
|
from ..client.fail2banregex import Fail2banRegex, get_opt_parser, output
|
||||||
|
|
|
@ -66,25 +66,18 @@ class TransmitterBase(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Call before every test case."""
|
"""Call before every test case."""
|
||||||
|
#super(TransmitterBase, self).setUp()
|
||||||
self.transm = self.server._Server__transm
|
self.transm = self.server._Server__transm
|
||||||
self.tmp_files = []
|
# To test thransmitter we don't need to start server...
|
||||||
sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'transmitter')
|
#self.server.start('/dev/null', '/dev/null', force=False)
|
||||||
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)
|
|
||||||
self.jailName = "TestJail1"
|
self.jailName = "TestJail1"
|
||||||
self.server.addJail(self.jailName, FAST_BACKEND)
|
self.server.addJail(self.jailName, FAST_BACKEND)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Call after every test case."""
|
"""Call after every test case."""
|
||||||
|
# stop jails, etc.
|
||||||
self.server.quit()
|
self.server.quit()
|
||||||
for f in self.tmp_files:
|
#super(TransmitterBase, self).tearDown()
|
||||||
if os.path.exists(f):
|
|
||||||
os.remove(f)
|
|
||||||
|
|
||||||
def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False):
|
def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False):
|
||||||
"""Process set/get commands and compare both return values
|
"""Process set/get commands and compare both return values
|
||||||
|
@ -166,6 +159,11 @@ class Transmitter(TransmitterBase):
|
||||||
self.server = TestServer()
|
self.server = TestServer()
|
||||||
super(Transmitter, self).setUp()
|
super(Transmitter, self).setUp()
|
||||||
|
|
||||||
|
def testServerIsNotStarted(self):
|
||||||
|
# so far isStarted only tested but not used otherwise
|
||||||
|
# and here we don't really .start server
|
||||||
|
self.assertFalse(self.server.isStarted())
|
||||||
|
|
||||||
def testStopServer(self):
|
def testStopServer(self):
|
||||||
self.assertEqual(self.transm.proceed(["stop"]), (0, None))
|
self.assertEqual(self.transm.proceed(["stop"]), (0, None))
|
||||||
|
|
||||||
|
@ -796,10 +794,10 @@ class TransmitterLogging(TransmitterBase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.server = Server()
|
self.server = Server()
|
||||||
|
super(TransmitterLogging, self).setUp()
|
||||||
self.server.setLogTarget("/dev/null")
|
self.server.setLogTarget("/dev/null")
|
||||||
self.server.setLogLevel("CRITICAL")
|
self.server.setLogLevel("CRITICAL")
|
||||||
self.server.setSyslogSocket("auto")
|
self.server.setSyslogSocket("auto")
|
||||||
super(TransmitterLogging, self).setUp()
|
|
||||||
|
|
||||||
def testLogTarget(self):
|
def testLogTarget(self):
|
||||||
logTargets = []
|
logTargets = []
|
||||||
|
@ -1000,6 +998,25 @@ class LoggingTests(LogCaptureTestCase):
|
||||||
self.assertEqual(len(x), 1)
|
self.assertEqual(len(x), 1)
|
||||||
self.assertEqual(x[0][0], RuntimeError)
|
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.assertFalse(server.isStarted())
|
||||||
|
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
|
from clientreadertestcase import ActionReader, JailReader, JailsReader, CONFIG_DIR, STOCK
|
||||||
|
|
||||||
|
|
|
@ -26,10 +26,14 @@ import logging
|
||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from ..helpers import getLogger
|
from ..helpers import getLogger
|
||||||
from ..server.ipdns import DNSUtils
|
from ..server.ipdns import DNSUtils
|
||||||
|
@ -50,6 +54,9 @@ if not CONFIG_DIR:
|
||||||
else:
|
else:
|
||||||
CONFIG_DIR = '/etc/fail2ban'
|
CONFIG_DIR = '/etc/fail2ban'
|
||||||
|
|
||||||
|
# During the test cases (or setup) use fail2ban modules from main directory:
|
||||||
|
os.putenv('PYTHONPATH', os.path.dirname(os.path.dirname(os.path.dirname(
|
||||||
|
os.path.abspath(__file__)))))
|
||||||
|
|
||||||
class F2B(optparse.Values):
|
class F2B(optparse.Values):
|
||||||
def __init__(self, opts={}):
|
def __init__(self, opts={}):
|
||||||
|
@ -71,6 +78,23 @@ class F2B(optparse.Values):
|
||||||
return wtime
|
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):
|
def initTests(opts):
|
||||||
unittest.F2B = F2B(opts)
|
unittest.F2B = F2B(opts)
|
||||||
# --fast :
|
# --fast :
|
||||||
|
@ -156,6 +180,7 @@ def gatherTests(regexps=None, opts=None):
|
||||||
from . import misctestcase
|
from . import misctestcase
|
||||||
from . import databasetestcase
|
from . import databasetestcase
|
||||||
from . import samplestestcase
|
from . import samplestestcase
|
||||||
|
from . import fail2banclienttestcase
|
||||||
from . import fail2banregextestcase
|
from . import fail2banregextestcase
|
||||||
|
|
||||||
if not regexps: # pragma: no cover
|
if not regexps: # pragma: no cover
|
||||||
|
@ -239,6 +264,9 @@ def gatherTests(regexps=None, opts=None):
|
||||||
# Filter Regex tests with sample logs
|
# Filter Regex tests with sample logs
|
||||||
tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex))
|
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
|
# bin/fail2ban-regex
|
||||||
tests.addTest(unittest.makeSuite(fail2banregextestcase.Fail2banRegexTest))
|
tests.addTest(unittest.makeSuite(fail2banregextestcase.Fail2banRegexTest))
|
||||||
|
|
||||||
|
@ -321,13 +349,16 @@ class LogCaptureTestCase(unittest.TestCase):
|
||||||
# Let's log everything into a string
|
# Let's log everything into a string
|
||||||
self._log = StringIO()
|
self._log = StringIO()
|
||||||
logSys.handlers = [logging.StreamHandler(self._log)]
|
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.handlers += self._old_handlers
|
||||||
|
logSys.debug('='*10 + ' %s ' + '='*20, self.id())
|
||||||
logSys.setLevel(getattr(logging, 'DEBUG'))
|
logSys.setLevel(getattr(logging, 'DEBUG'))
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Call after every test case."""
|
"""Call after every test case."""
|
||||||
# print "O: >>%s<<" % self._log.getvalue()
|
# print "O: >>%s<<" % self._log.getvalue()
|
||||||
|
self.pruneLog()
|
||||||
logSys = getLogger("fail2ban")
|
logSys = getLogger("fail2ban")
|
||||||
logSys.handlers = self._old_handlers
|
logSys.handlers = self._old_handlers
|
||||||
logSys.level = self._old_level
|
logSys.level = self._old_level
|
||||||
|
|
Loading…
Reference in New Issue