From 4d696d69a035902eadaa2e8449d8ec08cccb359d Mon Sep 17 00:00:00 2001 From: sebres Date: Thu, 11 Feb 2016 08:56:12 +0100 Subject: [PATCH] starting of the server (and client/server communication behavior during start and daemonize) completely rewritten: - client/server functionality moved away from bin and using now the common interface (introduced in fail2bancmdline); - start in foreground fixed; - server can act as client corresponding command line; - command "restart" added: in opposite to "reload" in reality restarts the server (new process); - several client/server bugs during starting process fixed. --- MANIFEST | 4 + bin/fail2ban-client | 37 ++ bin/fail2ban-server | 37 ++ fail2ban/client/beautifier.py | 2 + fail2ban/client/fail2banclient.py | 578 +++++++++++------------------ fail2ban/client/fail2bancmdline.py | 245 ++++++++++++ fail2ban/client/fail2banserver.py | 234 +++++++----- fail2ban/protocol.py | 6 +- fail2ban/server/asyncserver.py | 39 +- fail2ban/server/server.py | 95 ++--- fail2ban/server/transmitter.py | 2 + 11 files changed, 775 insertions(+), 504 deletions(-) create mode 100755 bin/fail2ban-client create mode 100755 bin/fail2ban-server mode change 100755 => 100644 fail2ban/client/fail2banclient.py create mode 100644 fail2ban/client/fail2bancmdline.py mode change 100755 => 100644 fail2ban/client/fail2banserver.py diff --git a/MANIFEST b/MANIFEST index fb70bb4b..f77caad6 100644 --- a/MANIFEST +++ b/MANIFEST @@ -165,7 +165,11 @@ fail2ban/client/configparserinc.py fail2ban/client/configreader.py fail2ban/client/configurator.py fail2ban/client/csocket.py +fail2ban/client/fail2banclient.py +fail2ban/client/fail2bancmdline.py fail2ban/client/fail2banreader.py +fail2ban/client/fail2banregex.py +fail2ban/client/fail2banserver.py fail2ban/client/filterreader.py fail2ban/client/jailreader.py fail2ban/client/jailsreader.py diff --git a/bin/fail2ban-client b/bin/fail2ban-client new file mode 100755 index 00000000..19e76a98 --- /dev/null +++ b/bin/fail2ban-client @@ -0,0 +1,37 @@ +#!/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. + +""" +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" + +from fail2ban.client.fail2banclient import exec_command_line + +if __name__ == "__main__": + exec_command_line() diff --git a/bin/fail2ban-server b/bin/fail2ban-server new file mode 100755 index 00000000..8e64d865 --- /dev/null +++ b/bin/fail2ban-server @@ -0,0 +1,37 @@ +#!/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. + +""" +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" + +from fail2ban.client.fail2banserver import exec_command_line + +if __name__ == "__main__": + exec_command_line() diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 812fbe65..08ff484d 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -68,6 +68,8 @@ class Beautifier: msg = "Added jail " + response elif inC[0] == "flushlogs": msg = "logs: " + response + elif inC[0] == "echo": + msg = ' '.join(msg) elif inC[0:1] == ['status']: if len(inC) > 1: # Display information diff --git a/fail2ban/client/fail2banclient.py b/fail2ban/client/fail2banclient.py old mode 100755 new mode 100644 index 4a1bab39..7adbab95 --- a/fail2ban/client/fail2banclient.py +++ b/fail2ban/client/fail2banclient.py @@ -1,7 +1,7 @@ #!/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 @@ -17,99 +17,38 @@ # 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__ = "Cyril Jaquier" -__copyright__ = "Copyright (c) 2004 Cyril Jaquier" +__author__ = "Fail2Ban Developers" +__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester" __license__ = "GPL" -import getopt -import logging import os -import pickle -import re import shlex import signal import socket -import string import sys import time -from fail2ban.version import version -from fail2ban.protocol import printFormatted -from fail2ban.client.csocket import CSocket -from fail2ban.client.configurator import Configurator -from fail2ban.client.beautifier import Beautifier -from fail2ban.helpers import getLogger +from threading import Thread -# Gets the instance of the logger. -logSys = getLogger("fail2ban") +from ..version import version +from .csocket import CSocket +from .beautifier import Beautifier +from .fail2bancmdline import Fail2banCmdLine, logSys, exit ## # # @todo This class needs cleanup. -class Fail2banClient: +class Fail2banClient(Fail2banCmdLine, Thread): - SERVER = "fail2ban-server" PROMPT = "fail2ban> " def __init__(self): - self.__server = None - 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 ." - print "Many contributions by Yaroslav O. Halchenko ." - - 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 "Options:" - print " -c configuration directory" - print " -s socket path" - print " -p pidfile path" - print " --loglevel logging level" - print " --logtarget |STDOUT|STDERR|SYSLOG" - print " --syslogsocket auto|file" - 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" - 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" + Fail2banCmdLine.__init__(self) + Thread.__init__(self) + self._alive = True + self._server = None + self._beautifier = None def dispInteractive(self): print "Fail2Ban v" + version + " reads log file that contains password failure report" @@ -120,58 +59,32 @@ class Fail2banClient: # 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].startswith("--log") or opt[0].startswith("--sys"): - self.__conf[ opt[0][2:] ] = 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) + exit(-1) def __ping(self): return self.__processCmd([["ping"]], False) - def __processCmd(self, cmd, showRet = True): + @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 = Beautifier() + beautifier = self.beautifier streamRet = True for c in cmd: beautifier.setInputCmd(c) try: if not client: - client = CSocket(self.__conf["socket"]) + client = CSocket(self._conf["socket"]) ret = client.send(c) if ret[0] == 0: logSys.debug("OK : " + `ret[1]`) - if showRet: + if showRet or c[0] == 'echo': print beautifier.beautify(ret[1]) else: logSys.error("NOK: " + `ret[1].args`) @@ -179,38 +92,126 @@ class Fail2banClient: print beautifier.beautifyError(ret[1]) streamRet = False except socket.error: - if showRet: + if showRet or self._conf["verbose"] > 1: self.__logSocketError() return False except Exception, e: - if showRet: + if showRet or self._conf["verbose"] > 1: logSys.error(e) return False finally: if client: client.close() + if showRet or c[0] == 'echo': + sys.stdout.flush() return streamRet def __logSocketError(self): try: - if os.access(self.__conf["socket"], os.F_OK): + 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): + 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"]) + " (you must be root)", self._conf["socket"]) else: logSys.error("Failed to access socket path: %s." " Is fail2ban running?", - self.__conf["socket"]) + self._conf["socket"]) except Exception as e: logSys.error("Exception while checking socket access: %s", - self.__conf["socket"]) + 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): + 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: + Thread(target=Fail2banClient.__processStartStreamAfterWait, args=(self, stream, False)).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 Exception as e: + print + logSys.error("Exception while starting server foreground") + logSys.error(e) + finally: + self._alive = False + + return True + + ## + def configureServer(self, async=True, phase=None): + # if asynchron start this operation in the new thread: + if async: + return Thread(target=Fail2banClient.configureServer, args=(self, False, phase)).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. # @@ -219,251 +220,101 @@ class Fail2banClient: def __processCommand(self, cmd): if len(cmd) == 1 and cmd[0] == "start": - if self.__ping(): - logSys.error("Server already running") + + ret = self.__startServer(self._conf["background"]) + if not ret: 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 + return ret - # 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 socket exists)") - return False + elif len(cmd) == 1 and cmd[0] == "restart": - # Start the server - t = None - if self.__conf["background"]: - # Start server daemon as fork of client process: - self.__startServerAsync() - # Send config stream to server: - return self.__processStartStreamAfterWait() + if self._conf.get("interactive", False): + print(' ## stop ... ') + self.__processCommand(['stop']) + self.__waitOnServer(False) + # in interactive mode reset config, to make full-reload if there something changed: + if self._conf.get("interactive", False): + print(' ## load configuration ... ') + self.resetConf() + ret = self.initCmdLine(self._argv) + if ret is not None: + return ret + if self._conf.get("interactive", False): + print(' ## 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: - # In foreground mode we should start server/client communication in other thread: - from threading import Thread - t = Thread(target=Fail2banClient.__processStartStreamAfterWait, args=(self,)) - t.start() - # Start server direct here in main thread: - try: - self.__startServerDirect() - except KeyboardInterrupt: - None - - return True - - 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) + 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(self.__stream, False) + return self.__processCmd(stream, True) else: logSys.error("Could not find server") return False + else: return self.__processCmd([cmd]) - def __processStartStreamAfterWait(self): + def __processStartStreamAfterWait(self, *args): try: # Wait for the server to start self.__waitOnServer() - # Configure the server - self.__processCmd(self.__stream, False) + # Configure the server + self.__processCmd(*args) except ServerExecutionException: logSys.error("Could not start server. Maybe an old " "socket file is still present. Try to " - "remove " + self.__conf["socket"] + ". If " + "remove " + self._conf["socket"] + ". If " "you used fail2ban-client to start the " "server, adding the -x option will do it") - if not self.__conf["background"]: - self.__server.quit() - sys.exit(-1) + if self._server: + self._server.quit() + exit(-1) return False return True - - ## - # Start Fail2Ban server in main thread without fork (foreground). - # - # Start the Fail2ban server in foreground (daemon mode or not). - - def __startServerDirect(self): - from fail2ban.server.server import Server - try: - self.__server = Server(False) - self.__server.start(self.__conf["socket"], - self.__conf["pidfile"], self.__conf["force"], - conf=self.__conf) - except Exception, e: - logSys.exception(e) - if self.__server: - self.__server.quit() - sys.exit(-1) - - - ## - # Start Fail2Ban server. - # - # Start the Fail2ban server in daemon mode. - - def __startServerAsync(self): - # 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(self.__conf["socket"]) - # Set the pidfile - args.append("-p") - args.append(self.__conf["pidfile"]) - # Force the execution if needed. - if self.__conf["force"]: - args.append("-x") - # Start in background as requested. - args.append("-b") - - 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 __waitOnServer(self, alive=True, maxtime=30): + # Wait for the server to start (the server has 30 seconds to answer ping) + starttime = time.time() + with VisualWait(self._conf["verbose"]) as vis: + while self._alive and not self.__ping() == alive or ( + not alive and os.path.exists(self._conf["socket"]) + ): + 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") + time.sleep(0.1) 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 = ['loglevel', 'logtarget', 'syslogsocket', 'help', 'version'] - optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts) - except getopt.GetoptError: - self.dispUsage() - return False + # Command line options + if self._argv is None: + ret = self.initCmdLine(argv) + if ret is not None: + return ret - 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) - else: - logSys.setLevel(logging.DEBUG) - # 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"] - if self.__conf.get("logtarget", None) is None: - self.__conf["logtarget"] = conf["logtarget"] - if self.__conf.get("loglevel", None) is None: - self.__conf["loglevel"] = conf["loglevel"] - if self.__conf.get("syslogsocket", None) is None: - self.__conf["syslogsocket"] = conf["syslogsocket"] - - 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["dump"]: - ret = self.__readConfig() - self.dumpConfig(self.__stream) - return ret + # Commands + args = self._args # Interactive mode - if self.__conf["interactive"]: + if self._conf.get("interactive", False): try: import readline except ImportError: @@ -498,35 +349,56 @@ class Fail2banClient: 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 + +## +# Wonderful visual :) +# + +class _VisualWait: + pos = 0 + delta = 1 + maxpos = 10 + def __enter__(self): + return self + def __exit__(self, *args): + if self.pos: + sys.stdout.write('\r'+(' '*(35+self.maxpos))+'\r') + sys.stdout.flush() + def heartbeat(self): + if not self.pos: + sys.stdout.write("\nINFO [#" + (' '*self.maxpos) + "] Waiting on the server...\r\x1b[8C") + self.pos += self.delta + if self.delta > 0: + s = " #\x1b[1D" if self.pos > 1 else "# \x1b[2D" + else: + s = "\x1b[1D# \x1b[2D" + sys.stdout.write(s) + sys.stdout.flush() + if self.pos > self.maxpos: + self.delta = -1 + elif self.pos < 2: + self.delta = 1 +class _NotVisualWait: + def __enter__(self): + return self + def __exit__(self, *args): + pass + def heartbeat(self): + pass + +def VisualWait(verbose): + return _VisualWait() if verbose > 1 else _NotVisualWait() + + +def exec_command_line(): # pragma: no cover - can't test main client = Fail2banClient() # Exit with correct return value if client.start(sys.argv): - sys.exit(0) + exit(0) else: - sys.exit(-1) + exit(-1) + diff --git a/fail2ban/client/fail2bancmdline.py b/fail2ban/client/fail2bancmdline.py new file mode 100644 index 00000000..3628f695 --- /dev/null +++ b/fail2ban/client/fail2bancmdline.py @@ -0,0 +1,245 @@ +#!/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") + +CONFIG_PARAMS = ("socket", "pidfile", "logtarget", "loglevel", "syslogsocket",) + + +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 + } + + @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): + 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 ." + print "Many contributions by Yaroslav O. Halchenko ." + + def dispUsage(self): + """ Prints Fail2Ban command line options and exits + """ + caller = os.path.basename(self._argv[0]) + print "Usage: "+caller+" [OPTIONS]" + (" " if not caller.endswith('server') else "") + 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 configuration directory" + print " -s socket path" + print " -p pidfile path" + print " --loglevel logging level" + print " --logtarget |STDOUT|STDERR|SYSLOG" + print " --syslogsocket auto|" + 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" + print " --async start server in async mode (for internal usage only, don't read configuration)" + print " -h, --help display this help message" + print " -V, --version print the version" + + if not caller.endswith('server'): + print + print "Command:" + # Prints the protocol + printFormatted() + + print + print "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 == "--async": + self._conf["async"] = True + 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 in ["-h", "--help"]: + self.dispUsage() + exit(0) + elif o in ["-V", "--version"]: + self.dispVersion() + exit(0) + + def initCmdLine(self, argv): + # First time? + initial = (self._argv is None) + + # Command line options + self._argv = argv + + # Reads the command line options. + try: + cmdOpts = 'hc:s:p:xfbdviqV' + cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'async', 'help', 'version'] + optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts) + except getopt.GetoptError: + self.dispUsage() + exit(-1) + + self.__getCmdLineOptions(optList) + + if initial: + 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) + else: + logSys.setLevel(logging.DEBUG) + # 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 + + 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: + print c + return True + + @staticmethod + def exit(code=0): + logSys.debug("Exit with code %s", code) + if os._exit: + os._exit(code) + else: + sys.exit(code) + +# global exit handler: +exit = Fail2banCmdLine.exit \ No newline at end of file diff --git a/fail2ban/client/fail2banserver.py b/fail2ban/client/fail2banserver.py old mode 100755 new mode 100644 index 0b8b6418..6c1dd694 --- a/fail2ban/client/fail2banserver.py +++ b/fail2ban/client/fail2banserver.py @@ -1,7 +1,7 @@ #!/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 @@ -17,125 +17,171 @@ # 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__ = "Cyril Jaquier" -__copyright__ = "Copyright (c) 2004 Cyril Jaquier" +__author__ = "Fail2Ban Developers" +__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester" __license__ = "GPL" -import getopt import os import sys -from fail2ban.version import version -from fail2ban.server.server import Server -from fail2ban.helpers import getLogger - -# Gets the instance of the logger. -logSys = getLogger("fail2ban") +from ..version import version +from ..server.server import Server, ServerDaemonize +from ..server.utils import Utils +from .fail2bancmdline import Fail2banCmdLine, logSys, exit +SERVER = "fail2ban-server" ## # \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(Fail2banCmdLine): -class Fail2banServer: + # def __init__(self): + # Fail2banCmdLine.__init__(self) - 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" + ## + # Start Fail2Ban server in main thread without fork (foreground). + # + # Start the Fail2ban server in foreground (daemon mode or not). - 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 ." - print "Many contributions by Yaroslav O. Halchenko ." + @staticmethod + def startServerDirect(conf, daemon=True): + 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 ServerDaemonize: + pass + except Exception, e: + logSys.exception(e) + if server: + server.quit() + exit(-1) - 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 socket path" - print " -p 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" + return server - 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) + ## + # Start Fail2Ban server. + # + # Start the Fail2ban server in daemon mode (background, start from client). + + @staticmethod + def startServerAsync(conf): + # Forks the current process. + pid = os.fork() + 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: + # Use the current directory. + exe = os.path.abspath(os.path.join(sys.path[0], 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", SERVER) + os.execvp(SERVER, args) + except OSError: + exit(-1) + + def _Fail2banClient(self): + from .fail2banclient import Fail2banClient + cli = Fail2banClient() + cli.applyMembers(self) + return cli def start(self, argv): # Command line options - self.__argv = argv + ret = self.initCmdLine(argv) + if ret is not None: + return ret - # Reads the command line options. + # Commands + args = self._args + + cli = None + # 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: - cmdOpts = 'bfs:p:xhV' - cmdLongOpts = ['help', 'version'] - optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts) - except getopt.GetoptError: - self.dispUsage() - sys.exit(-1) + # async = True, if started from client, should fork, daemonize, etc... + # background = True, if should start in new process, otherwise start in foreground + async = self._conf.get("async", False) + background = self._conf["background"] + # 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 up to 30 seconds, do not continue if configuration is not 100% valid: + Utils.wait_for(lambda: phase.get('ready', None) is not None, 30) + if not phase.get('start', False): + return False - self.__getCmdLineOptions(optList) + # Start server, daemonize it, etc. + if async or not background: + server = Fail2banServer.startServerDirect(self._conf, background) + else: + Fail2banServer.startServerAsync(self._conf) + 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, 30) + if not phase.get('done', False): + if server: + server.quit() + exit(-1) + logSys.debug('Starting server done') - 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) - if self.__server: - self.__server.quit() - return False + if server: + server.quit() + exit(-1) -if __name__ == "__main__": + 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(): # pragma: no cover - can't test main server = Fail2banServer() if server.start(sys.argv): - sys.exit(0) + exit(0) else: - sys.exit(-1) + exit(-1) diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index 5d9fdd65..857d5fa6 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -42,11 +42,13 @@ CSPROTO = dotdict({ protocol = [ ['', "BASIC", ""], ["start", "starts the server and the jails"], -["reload", "reloads the configuration"], +["restart", "restarts the server"], +["reload", "reloads the configuration without restart"], ["reload ", "reloads the jail "], ["stop", "stops all jails and terminate the server"], ["status", "gets the current status of the server"], -["ping", "tests if the server is alive"], +["ping", "tests if the server is alive"], +["echo", "for internal usage, returns back and outputs a given string"], ["help", "return this output"], ["version", "return the server version"], ['', "LOGGING", ""], diff --git a/fail2ban/server/asyncserver.py b/fail2ban/server/asyncserver.py index ad37544a..6454ef1c 100644 --- a/fail2ban/server/asyncserver.py +++ b/fail2ban/server/asyncserver.py @@ -95,6 +95,7 @@ def loop(active, timeout=None, use_poll=False): # Use poll instead of loop, because of recognition of active flag, # because of loop timeout mistake: different in poll and poll2 (sec vs ms), # and to prevent sporadical errors like EBADF 'Bad file descriptor' etc. (see gh-161) + errCount = 0 if timeout is None: timeout = Utils.DEFAULT_SLEEP_TIME poll = asyncore.poll @@ -107,11 +108,20 @@ def loop(active, timeout=None, use_poll=False): while active(): try: poll(timeout) + if errCount: + errCount -= 1 except Exception as e: # pragma: no cover - if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') - logSys.info('Server connection was closed: %s', str(e)) - else: - logSys.error('Server connection was closed: %s', str(e)) + if not active(): + break + errCount += 1 + if errCount < 20: + if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') + logSys.info('Server connection was closed: %s', str(e)) + else: + logSys.error('Server connection was closed: %s', str(e)) + elif errCount == 20: + logSys.info('Too many errors - stop logging connection errors') + logSys.exception(e) ## @@ -162,7 +172,7 @@ class AsyncServer(asyncore.dispatcher): logSys.error("Fail2ban seems to be already running") if force: logSys.warning("Forcing execution of the server") - os.remove(sock) + self._remove_sock() else: raise AsyncServerException("Server already running") # Creates the socket. @@ -175,20 +185,22 @@ class AsyncServer(asyncore.dispatcher): AsyncServer.__markCloseOnExec(self.socket) self.listen(1) # Sets the init flag. - self.__init = self.__active = True + self.__init = self.__loop = self.__active = True # Event loop as long as active: - loop(lambda: self.__active) + loop(lambda: self.__loop) + self.__active = False # Cleanup all self.stop() def close(self): if self.__active: + self.__loop = False asyncore.dispatcher.close(self) # Remove socket (file) only if it was created: if self.__init and os.path.exists(self.__sock): logSys.debug("Removed socket file " + self.__sock) - os.remove(self.__sock) + self._remove_sock() logSys.debug("Socket shutdown") self.__active = False @@ -201,6 +213,17 @@ class AsyncServer(asyncore.dispatcher): def isActive(self): return self.__active + + ## + # Safe remove (in multithreaded mode): + + def _remove_sock(self): + try: + os.remove(self.__sock) + except OSError as e: + if e.errno != errno.ENOENT: + raise + ## # Marks socket as close-on-exec to avoid leaking file descriptors when # running actions involving command execution. diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 923b6ba3..72279b2d 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -67,6 +67,11 @@ class Server: 'FreeBSD': '/var/run/log', 'Linux': '/dev/log', } + # todo: remove that, if test cases are fixed + self.setSyslogSocket("auto") + # Set logging level + self.setLogLevel("INFO") + self.setLogTarget("STDOUT") def __sigTERMhandler(self, signum, frame): logSys.debug("Caught signal %d. Exiting" % signum) @@ -77,13 +82,27 @@ class Server: self.flushLogs() def start(self, sock, pidfile, force=False, conf={}): - # First set all logging parameters: - self.setSyslogSocket(conf.get("syslogsocket", "auto")) - self.setLogLevel(conf.get("loglevel", "INFO")) - self.setLogTarget(conf.get("logtarget", "STDOUT")) + # First set the mask to only allow access to owner + os.umask(0077) + # Second daemonize before logging etc, because it will close all handles: + if self.__daemon: # pragma: no cover + logSys.info("Starting in daemon mode") + ret = self.__createDaemon() + if not ret: + logSys.error("Could not create daemon") + raise ServerInitializationError("Could not create daemon") + + # Set all logging parameters (or use default if not specified): + self.setSyslogSocket(conf.get("syslogsocket", self.__syslogSocket)) + self.setLogLevel(conf.get("loglevel", self.__logLevel)) + self.setLogTarget(conf.get("logtarget", self.__logTarget)) + logSys.info("-"*50) logSys.info("Starting Fail2ban v%s", version.version) + if self.__daemon: # pragma: no cover + logSys.info("Daemon started") + # Install signal handlers signal.signal(signal.SIGTERM, self.__sigTERMhandler) signal.signal(signal.SIGINT, self.__sigTERMhandler) @@ -92,17 +111,6 @@ class Server: # Ensure unhandled exceptions are logged sys.excepthook = excepthook - # First set the mask to only allow access to owner - os.umask(0077) - if self.__daemon: # pragma: no cover - logSys.info("Starting in daemon mode") - ret = self.__createDaemon() - if ret: - logSys.info("Daemon started") - else: - logSys.error("Could not create daemon") - raise ServerInitializationError("Could not create daemon") - # Creates a PID file. try: logSys.debug("Creating PID file %s" % pidfile) @@ -139,11 +147,8 @@ class Server: self.stopAllJail() # Only now shutdown the logging. - try: - self.__loggingLock.acquire() + with self.__loggingLock: logging.shutdown() - finally: - self.__loggingLock.release() def addJail(self, name, backend): self.__jails.add(name, backend, self.__db) @@ -362,16 +367,15 @@ class Server: # @param value the level def setLogLevel(self, value): - try: - self.__loggingLock.acquire() - getLogger("fail2ban").setLevel( - getattr(logging, value.upper())) - except AttributeError: - raise ValueError("Invalid log level") - else: - self.__logLevel = value.upper() - finally: - self.__loggingLock.release() + value = value.upper() + with self.__loggingLock: + if self.__logLevel == value: + return + try: + getLogger("fail2ban").setLevel(getattr(logging, value)) + self.__logLevel = value + except AttributeError: + raise ValueError("Invalid log level") ## # Get the logging level. @@ -380,11 +384,8 @@ class Server: # @return the log level def getLogLevel(self): - try: - self.__loggingLock.acquire() + with self.__loggingLock: return self.__logLevel - finally: - self.__loggingLock.release() ## # Sets the logging target. @@ -470,24 +471,21 @@ class Server: # syslogsocket is the full path to the syslog socket # @param syslogsocket the syslog socket path def setSyslogSocket(self, syslogsocket): - self.__syslogSocket = syslogsocket - # Conditionally reload, logtarget depends on socket path when SYSLOG - return self.__logTarget != "SYSLOG"\ - or self.setLogTarget(self.__logTarget) + with self.__loggingLock: + if self.__syslogSocket == syslogsocket: + return True + self.__syslogSocket = syslogsocket + # Conditionally reload, logtarget depends on socket path when SYSLOG + return self.__logTarget != "SYSLOG"\ + or self.setLogTarget(self.__logTarget) def getLogTarget(self): - try: - self.__loggingLock.acquire() + with self.__loggingLock: return self.__logTarget - finally: - self.__loggingLock.release() def getSyslogSocket(self): - try: - self.__loggingLock.acquire() + with self.__loggingLock: return self.__syslogSocket - finally: - self.__loggingLock.release() def flushLogs(self): if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']: @@ -542,7 +540,7 @@ class Server: # child process, and this makes sure that it is effect even if the parent # terminates quickly. signal.signal(signal.SIGHUP, signal.SIG_IGN) - + try: # Fork a child process so the parent can exit. This will return control # to the command line or shell. This is required so that the new process @@ -583,7 +581,7 @@ class Server: os._exit(0) # Exit parent (the first child) of the second child. else: os._exit(0) # Exit parent of the first child. - + # Close all open files. Try the system configuration variable, SC_OPEN_MAX, # for the maximum number of open files to close. If it doesn't exist, use # the default value (configurable). @@ -615,3 +613,6 @@ class Server: class ServerInitializationError(Exception): pass + +class ServerDaemonize(Exception): + pass \ No newline at end of file diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 4c4c32f7..2194f591 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -93,6 +93,8 @@ class Transmitter: name = command[1] self.__server.stopJail(name) return None + elif command[0] == "echo": + return command[1:] elif command[0] == "sleep": value = command[1] time.sleep(float(value))