mirror of https://github.com/fail2ban/fail2ban
347 lines
11 KiB
Python
347 lines
11 KiB
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, normVersion
|
|
from ..protocol import printFormatted
|
|
from ..helpers import getLogger, str2LogLevel, getVerbosityFormat, BrokenPipeError
|
|
|
|
# Gets the instance of the logger.
|
|
logSys = getLogger("fail2ban")
|
|
|
|
def output(s): # pragma: no cover
|
|
try:
|
|
print(s)
|
|
except (BrokenPipeError, IOError) as e: # pragma: no cover
|
|
if e.errno != 32: # closed / broken pipe
|
|
raise
|
|
|
|
# Config parameters required to start fail2ban which can be also set via command line (overwrite fail2ban.conf),
|
|
CONFIG_PARAMS = ("socket", "pidfile", "logtarget", "loglevel", "syslogsocket")
|
|
# Used to signal - we are in test cases (ex: prevents change logging params, log capturing, etc)
|
|
PRODUCTION = True
|
|
|
|
MAX_WAITTIME = 30
|
|
|
|
|
|
class Fail2banCmdLine():
|
|
|
|
def __init__(self):
|
|
self._argv = self._args = None
|
|
self._configurator = None
|
|
self.cleanConfOnly = False
|
|
self.resetConf()
|
|
|
|
def resetConf(self):
|
|
self._conf = {
|
|
"async": False,
|
|
"conf": "/etc/fail2ban",
|
|
"force": False,
|
|
"background": True,
|
|
"verbose": 1,
|
|
"socket": None,
|
|
"pidfile": None,
|
|
"timeout": MAX_WAITTIME
|
|
}
|
|
|
|
@property
|
|
def configurator(self):
|
|
if self._configurator:
|
|
return self._configurator
|
|
# New configurator
|
|
from .configurator import Configurator
|
|
self._configurator = Configurator()
|
|
# Set the configuration path
|
|
self._configurator.setBaseDir(self._conf["conf"])
|
|
return self._configurator
|
|
|
|
|
|
def applyMembers(self, obj):
|
|
for o in obj.__dict__:
|
|
self.__dict__[o] = obj.__dict__[o]
|
|
|
|
def dispVersion(self, short=False):
|
|
if not short:
|
|
output("Fail2Ban v" + version)
|
|
else:
|
|
output(normVersion())
|
|
|
|
def dispUsage(self):
|
|
""" Prints Fail2Ban command line options and exits
|
|
"""
|
|
caller = os.path.basename(self._argv[0])
|
|
output("Usage: "+caller+" [OPTIONS]" + (" <COMMAND>" if not caller.endswith('server') else ""))
|
|
output("")
|
|
output("Fail2Ban v" + version + " reads log file that contains password failure report")
|
|
output("and bans the corresponding IP addresses using firewall rules.")
|
|
output("")
|
|
output("Options:")
|
|
output(" -c, --conf <DIR> configuration directory")
|
|
output(" -s, --socket <FILE> socket path")
|
|
output(" -p, --pidfile <FILE> pidfile path")
|
|
output(" --pname <NAME> name of the process (main thread) to identify instance (default fail2ban-server)")
|
|
output(" --loglevel <LEVEL> logging level")
|
|
output(" --logtarget <TARGET> logging target, use file-name or stdout, stderr, syslog or sysout.")
|
|
output(" --syslogsocket auto|<FILE>")
|
|
output(" -d dump configuration. For debugging")
|
|
output(" --dp, --dump-pretty dump the configuration using more human readable representation")
|
|
output(" -t, --test test configuration (can be also specified with start parameters)")
|
|
output(" -i interactive mode")
|
|
output(" -v increase verbosity")
|
|
output(" -q decrease verbosity")
|
|
output(" -x force execution of the server (remove socket file)")
|
|
output(" -b start server in background (default)")
|
|
output(" -f start server in foreground")
|
|
output(" --async start server in async mode (for internal usage only, don't read configuration)")
|
|
output(" --timeout timeout to wait for the server (for internal usage only, don't read configuration)")
|
|
output(" --str2sec <STRING> convert time abbreviation format to seconds")
|
|
output(" -h, --help display this help message")
|
|
output(" -V, --version print the version (-V returns machine-readable short format)")
|
|
|
|
if not caller.endswith('server'):
|
|
output("")
|
|
output("Command:")
|
|
# Prints the protocol
|
|
printFormatted()
|
|
|
|
output("")
|
|
output("Report bugs to https://github.com/fail2ban/fail2ban/issues")
|
|
|
|
def __getCmdLineOptions(self, optList):
|
|
""" Gets the command line options
|
|
"""
|
|
for opt in optList:
|
|
o = opt[0]
|
|
if o in ("-c", "--conf"):
|
|
self._conf["conf"] = opt[1]
|
|
elif o in ("-s", "--socket"):
|
|
self._conf["socket"] = opt[1]
|
|
elif o in ("-p", "--pidfile"):
|
|
self._conf["pidfile"] = opt[1]
|
|
elif o in ("-d", "--dp", "--dump-pretty"):
|
|
self._conf["dump"] = True if o == "-d" else 2
|
|
elif o in ("-t", "--test"):
|
|
self.cleanConfOnly = True
|
|
self._conf["test"] = True
|
|
elif o == "-v":
|
|
self._conf["verbose"] += 1
|
|
elif o == "-q":
|
|
self._conf["verbose"] -= 1
|
|
elif o == "-x":
|
|
self._conf["force"] = True
|
|
elif o == "-i":
|
|
self._conf["interactive"] = True
|
|
elif o == "-b":
|
|
self._conf["background"] = True
|
|
elif o == "-f":
|
|
self._conf["background"] = False
|
|
elif o == "--async":
|
|
self._conf["async"] = True
|
|
elif o == "--timeout":
|
|
from ..server.mytime import MyTime
|
|
self._conf["timeout"] = MyTime.str2seconds(opt[1])
|
|
elif o == "--str2sec":
|
|
from ..server.mytime import MyTime
|
|
output(MyTime.str2seconds(opt[1]))
|
|
return True
|
|
elif o in ("-h", "--help"):
|
|
self.dispUsage()
|
|
return True
|
|
elif o in ("-V", "--version"):
|
|
self.dispVersion(o == "-V")
|
|
return True
|
|
elif o.startswith("--"): # other long named params (see also resetConf)
|
|
self._conf[ o[2:] ] = opt[1]
|
|
return None
|
|
|
|
def initCmdLine(self, argv):
|
|
verbose = 1
|
|
try:
|
|
# First time?
|
|
initial = (self._argv is None)
|
|
|
|
# Command line options
|
|
self._argv = argv
|
|
logSys.info("Using start params %s", argv[1:])
|
|
|
|
# Reads the command line options.
|
|
try:
|
|
cmdOpts = 'hc:s:p:xfbdtviqV'
|
|
cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'test', 'async',
|
|
'conf=', 'pidfile=', 'pname=', 'socket=',
|
|
'timeout=', 'str2sec=', 'help', 'version', 'dp', '--dump-pretty']
|
|
optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts)
|
|
except getopt.GetoptError:
|
|
self.dispUsage()
|
|
return False
|
|
|
|
ret = self.__getCmdLineOptions(optList)
|
|
if ret is not None:
|
|
return ret
|
|
|
|
logSys.debug(" conf: %r, args: %r", self._conf, self._args)
|
|
|
|
if initial and PRODUCTION: # pragma: no cover - can't test
|
|
verbose = self._conf["verbose"]
|
|
if verbose <= 0:
|
|
logSys.setLevel(logging.ERROR)
|
|
elif verbose == 1:
|
|
logSys.setLevel(logging.WARNING)
|
|
elif verbose == 2:
|
|
logSys.setLevel(logging.INFO)
|
|
elif verbose == 3:
|
|
logSys.setLevel(logging.DEBUG)
|
|
else:
|
|
logSys.setLevel(logging.HEAVYDEBUG)
|
|
# Add the default logging handler to dump to stderr
|
|
logout = logging.StreamHandler(sys.stderr)
|
|
|
|
# Custom log format for the verbose run (-1, because default verbosity here is 1):
|
|
fmt = getVerbosityFormat(verbose-1)
|
|
formatter = logging.Formatter(fmt)
|
|
# 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()
|
|
if o in conf:
|
|
self._conf[o] = conf[o]
|
|
|
|
logSys.info("Using socket file %s", self._conf["socket"])
|
|
|
|
# Check log-level before start (or transmit to server), to prevent error in background:
|
|
llev = str2LogLevel(self._conf["loglevel"])
|
|
logSys.info("Using pid file %s, [%s] logging to %s",
|
|
self._conf["pidfile"], logging.getLevelName(llev), self._conf["logtarget"])
|
|
|
|
readcfg = True
|
|
if self._conf.get("dump", False):
|
|
if readcfg:
|
|
ret, stream = self.readConfig()
|
|
readcfg = False
|
|
if stream is not None:
|
|
self.dumpConfig(stream, self._conf["dump"] == 2)
|
|
else: # pragma: no cover
|
|
output("ERROR: The configuration stream failed because of the invalid syntax.")
|
|
if not self._conf.get("test", False):
|
|
return ret
|
|
|
|
if self._conf.get("test", False):
|
|
if readcfg:
|
|
readcfg = False
|
|
ret, stream = self.readConfig()
|
|
if not ret:
|
|
raise ServerExecutionException("ERROR: test configuration failed")
|
|
# exit after test if no commands specified (test only):
|
|
if not len(self._args):
|
|
output("OK: configuration test is successful")
|
|
return ret
|
|
|
|
# Nothing to do here, process in client/server
|
|
return None
|
|
except ServerExecutionException:
|
|
raise
|
|
except Exception as e:
|
|
output("ERROR: %s" % (e,))
|
|
if verbose > 2:
|
|
logSys.exception(e)
|
|
return False
|
|
|
|
def readConfig(self, jail=None):
|
|
# Read the configuration
|
|
# TODO: get away from stew of return codes and exception
|
|
# handling -- handle via exceptions
|
|
stream = None
|
|
try:
|
|
self.configurator.Reload()
|
|
self.configurator.readAll()
|
|
ret = self.configurator.getOptions(jail, self._conf,
|
|
ignoreWrong=not self.cleanConfOnly)
|
|
self.configurator.convertToProtocol(
|
|
allow_no_files=self._conf.get("dump", False))
|
|
stream = self.configurator.getConfigStream()
|
|
except Exception as e:
|
|
logSys.error("Failed during configuration: %s" % e)
|
|
ret = False
|
|
return ret, stream
|
|
|
|
@staticmethod
|
|
def dumpConfig(cmd, pretty=False):
|
|
if pretty:
|
|
from pprint import pformat
|
|
def _output(s):
|
|
output(pformat(s, width=1000, indent=2))
|
|
else:
|
|
_output = output
|
|
for c in cmd:
|
|
_output(c)
|
|
return True
|
|
|
|
#
|
|
# _exit is made to ease mocking out of the behaviour in tests,
|
|
# since method is also exposed in API via globally bound variable
|
|
@staticmethod
|
|
def _exit(code=0):
|
|
# implicit flush without to produce broken pipe error (32):
|
|
sys.stderr.close()
|
|
try:
|
|
sys.stdout.flush()
|
|
# exit:
|
|
if hasattr(sys, 'exit') and sys.exit:
|
|
sys.exit(code)
|
|
else:
|
|
os._exit(code)
|
|
except (BrokenPipeError, IOError) as e: # pragma: no cover
|
|
if e.errno != 32: # closed / broken pipe
|
|
raise
|
|
|
|
@staticmethod
|
|
def exit(code=0):
|
|
logSys.debug("Exit with code %s", code)
|
|
# because of possible buffered output in python, we should flush it before exit:
|
|
logging.shutdown()
|
|
# exit
|
|
Fail2banCmdLine._exit(code)
|
|
|
|
|
|
# global exit handler:
|
|
exit = Fail2banCmdLine.exit
|
|
|
|
|
|
class ExitException(Exception):
|
|
pass
|
|
|
|
|
|
class ServerExecutionException(Exception):
|
|
pass
|