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.
pull/1483/head
sebres 2016-02-11 08:56:12 +01:00
parent 556ddaabd7
commit 5a053f4b74
10 changed files with 745 additions and 498 deletions

View File

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

37
bin/fail2ban-client Executable file
View File

@ -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()

37
bin/fail2ban-server Executable file
View File

@ -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()

View File

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

View File

@ -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 <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 " --loglevel <LEVEL> logging level"
print " --logtarget <FILE>|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,253 +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)
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"]
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:
@ -500,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)

View File

@ -0,0 +1,247 @@
#!/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 <cyril.jaquier@fail2ban.org>."
print "Many contributions by Yaroslav O. Halchenko <debian@onerussian.com>."
def dispUsage(self):
""" Prints Fail2Ban command line options and exits
"""
caller = os.path.basename(self._argv[0])
print "Usage: "+caller+" [OPTIONS]" + (" <COMMAND>" 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 <DIR> configuration directory"
print " -s <FILE> socket path"
print " -p <FILE> pidfile path"
print " --loglevel <LEVEL> logging level"
print " --logtarget <FILE>|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 " --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)
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
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

234
fail2ban/client/fail2banserver.py Executable file → Normal file
View File

@ -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 <cyril.jaquier@fail2ban.org>."
print "Many contributions by Yaroslav O. Halchenko <debian@onerussian.com>."
@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 <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"
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)

View File

@ -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 <JAIL>", "reloads the jail <JAIL>"],
["stop", "stops all jails and terminate the server"],
["status", "gets the current status of the server"],
["ping", "tests if the server is alive"],
["ping", "tests if the server is alive"],
["echo", "for internal usage, returns back and outputs a given string"],
["help", "return this output"],
["version", "return the server version"],
['', "LOGGING", ""],

View File

@ -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)
@ -372,16 +377,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.
@ -390,11 +394,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.
@ -480,24 +481,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']:
@ -552,7 +550,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
@ -593,7 +591,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).
@ -625,3 +623,6 @@ class Server:
class ServerInitializationError(Exception):
pass
class ServerDaemonize(Exception):
pass

View File

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