instructions
This commit is contained in:
25
fail2ban-master/fail2ban/client/__init__.py
Normal file
25
fail2ban-master/fail2ban/client/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# 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: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
104
fail2ban-master/fail2ban/client/actionreader.py
Normal file
104
fail2ban-master/fail2ban/client/actionreader.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# 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: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import os
|
||||
|
||||
from .configreader import DefinitionInitConfigReader
|
||||
from ..helpers import getLogger
|
||||
from ..server.action import CommandAction
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
|
||||
class ActionReader(DefinitionInitConfigReader):
|
||||
|
||||
_configOpts = {
|
||||
"actionstart": ["string", None],
|
||||
"actionstart_on_demand": ["bool", None],
|
||||
"actionstop": ["string", None],
|
||||
"actionflush": ["string", None],
|
||||
"actionreload": ["string", None],
|
||||
"actioncheck": ["string", None],
|
||||
"actionrepair": ["string", None],
|
||||
"actionrepair_on_unban": ["bool", None],
|
||||
"actionban": ["string", None],
|
||||
"actionprolong": ["string", None],
|
||||
"actionreban": ["string", None],
|
||||
"actionunban": ["string", None],
|
||||
"norestored": ["bool", None],
|
||||
}
|
||||
|
||||
def __init__(self, file_, jailName, initOpts, **kwargs):
|
||||
# always supply jail name as name parameter if not specified in options:
|
||||
n = initOpts.get("name")
|
||||
if n is None:
|
||||
initOpts["name"] = n = jailName
|
||||
actname = initOpts.get("actname")
|
||||
if actname is None:
|
||||
actname = file_
|
||||
# ensure we've unique action name per jail:
|
||||
if n != jailName:
|
||||
actname += n[len(jailName):] if n.startswith(jailName) else '-' + n
|
||||
initOpts["actname"] = actname
|
||||
self._name = actname
|
||||
DefinitionInitConfigReader.__init__(
|
||||
self, file_, jailName, initOpts, **kwargs)
|
||||
|
||||
def setFile(self, fileName):
|
||||
self.__file = fileName
|
||||
DefinitionInitConfigReader.setFile(self, os.path.join("action.d", fileName))
|
||||
|
||||
def getFile(self):
|
||||
return self.__file
|
||||
|
||||
def setName(self, name):
|
||||
self._name = name
|
||||
|
||||
def getName(self):
|
||||
return self._name
|
||||
|
||||
def convert(self):
|
||||
opts = self.getCombined(
|
||||
ignore=CommandAction._escapedTags | set(('timeout', 'bantime')))
|
||||
# stream-convert:
|
||||
head = ["set", self._jailName]
|
||||
stream = list()
|
||||
stream.append(head + ["addaction", self._name])
|
||||
multi = []
|
||||
for opt, optval in opts.items():
|
||||
if opt in self._configOpts and not opt.startswith('known/'):
|
||||
multi.append([opt, optval])
|
||||
if self._initOpts:
|
||||
for opt, optval in self._initOpts.items():
|
||||
if opt not in self._configOpts and not opt.startswith('known/'):
|
||||
multi.append([opt, optval])
|
||||
if len(multi) > 1:
|
||||
stream.append(["multi-set", self._jailName, "action", self._name, multi])
|
||||
elif len(multi):
|
||||
stream.append(["set", self._jailName, "action", self._name] + multi[0])
|
||||
|
||||
return stream
|
||||
272
fail2ban-master/fail2ban/client/beautifier.py
Normal file
272
fail2ban-master/fail2ban/client/beautifier.py
Normal file
@@ -0,0 +1,272 @@
|
||||
# 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__ = "Cyril Jaquier, Yaroslav Halchenko"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
import sys
|
||||
|
||||
from ..exceptions import UnknownJailException, DuplicateJailException
|
||||
from ..helpers import getLogger, logging, PREFER_ENC
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
|
||||
##
|
||||
# Beautify the output of the client.
|
||||
#
|
||||
# Fail2ban server only return unformatted return codes which need to be
|
||||
# converted into user readable messages.
|
||||
|
||||
class Beautifier:
|
||||
|
||||
stdoutEnc = PREFER_ENC
|
||||
if sys.stdout and sys.stdout.encoding is not None:
|
||||
stdoutEnc = sys.stdout.encoding
|
||||
encUtf = 1 if stdoutEnc.lower() == 'utf-8' else 0
|
||||
|
||||
def __init__(self, cmd = None):
|
||||
self.__inputCmd = cmd
|
||||
|
||||
def setInputCmd(self, cmd):
|
||||
self.__inputCmd = cmd
|
||||
|
||||
def getInputCmd(self):
|
||||
return self.__inputCmd
|
||||
|
||||
def beautify(self, response):
|
||||
logSys.log(5,
|
||||
"Beautify " + repr(response) + " with " + repr(self.__inputCmd))
|
||||
inC = self.__inputCmd
|
||||
msg = response
|
||||
try:
|
||||
if inC[0] == "ping":
|
||||
msg = "Server replied: " + response
|
||||
elif inC[0] == "version":
|
||||
msg = response
|
||||
elif inC[0] == "start":
|
||||
msg = "Jail started"
|
||||
elif inC[0] == "stop":
|
||||
if len(inC) == 1:
|
||||
if response is None:
|
||||
msg = "Shutdown successful"
|
||||
else:
|
||||
if response is None:
|
||||
msg = "Jail stopped"
|
||||
elif inC[0] == "add":
|
||||
msg = "Added jail " + response
|
||||
elif inC[0] == "flushlogs":
|
||||
msg = "logs: " + response
|
||||
elif inC[0] == "echo":
|
||||
msg = ' '.join(msg)
|
||||
elif inC[0:1] == ['status']:
|
||||
def jail_stat(response, pref=""):
|
||||
# Display jail information
|
||||
for n, res1 in enumerate(response):
|
||||
prefix1 = pref + ("`-" if n == len(response) - 1 else "|-")
|
||||
msg.append("%s %s" % (prefix1, res1[0]))
|
||||
prefix1 = pref + (" " if n == len(response) - 1 else "| ")
|
||||
for m, res2 in enumerate(res1[1]):
|
||||
prefix2 = prefix1 + ("`-" if m == len(res1[1]) - 1 else "|-")
|
||||
val = " ".join(map(str, res2[1])) if isinstance(res2[1], list) else res2[1]
|
||||
msg.append("%s %s:\t%s" % (prefix2, res2[0], val))
|
||||
if len(inC) > 1 and inC[1] != "--all":
|
||||
msg = ["Status for the jail: %s" % inC[1]]
|
||||
jail_stat(response)
|
||||
else:
|
||||
jstat = None
|
||||
if len(inC) > 1: # --all
|
||||
jstat = response[-1]
|
||||
response = response[:-1]
|
||||
msg = ["Status"]
|
||||
for n, res1 in enumerate(response):
|
||||
prefix1 = "`-" if not jstat and n == len(response) - 1 else "|-"
|
||||
val = " ".join(map(str, res1[1])) if isinstance(res1[1], list) else res1[1]
|
||||
msg.append("%s %s:\t%s" % (prefix1, res1[0], val))
|
||||
if jstat:
|
||||
msg.append("`- Status for the jails:")
|
||||
i = 0
|
||||
for n, j in jstat.items():
|
||||
i += 1
|
||||
prefix1 = "`-" if i == len(jstat) else "|-"
|
||||
msg.append(" %s Jail: %s" % (prefix1, n))
|
||||
jail_stat(j, " " if i == len(jstat) else " | ")
|
||||
msg = "\n".join(msg)
|
||||
elif inC[0:1] == ['stats'] or inC[0:1] == ['statistics']:
|
||||
chrTable = [
|
||||
['|', '-', '|', 'x', 'x', '-', '|', '-'], ## ascii
|
||||
["\u2551", "\u2550", "\u255F", "\u256B", "\u256C", "\u2569", "\u2502", "\u2500"] ## utf-8
|
||||
];
|
||||
def _statstable(response, ct):
|
||||
tophead = ["Jail", "Backend", "Filter", "Actions"]
|
||||
headers = ["", "", "cur", "tot", "cur", "tot"]
|
||||
minlens = [8, 8, 3, 3, 3, 3]
|
||||
ralign = [0, 0, 1, 1, 1, 1]
|
||||
rows = [[n, r[0], *r[1], *r[2]] for n, r in response.items()]
|
||||
lens = []
|
||||
for i in range(len(rows[0])):
|
||||
col = (len(str(s[i])) for s in rows)
|
||||
lens.append(max(minlens[i], max(col)))
|
||||
rfmt = []
|
||||
hfmt = []
|
||||
for i in range(len(rows[0])):
|
||||
f = "%%%ds" if ralign[i] else "%%-%ds"
|
||||
rfmt.append(f % lens[i])
|
||||
hfmt.append(f % lens[i])
|
||||
rfmt = [rfmt[0], rfmt[1], "%s %s %s" % (rfmt[2], ct[6], rfmt[3]), "%s %s %s" % (rfmt[4], ct[6], rfmt[5])]
|
||||
hfmt = [hfmt[0], hfmt[1], "%s %s %s" % (hfmt[2], ct[6], hfmt[3]), "%s %s %s" % (hfmt[4], ct[6], hfmt[5])]
|
||||
tlens = [lens[0], lens[1], 3 + lens[2] + lens[3], 3 + lens[4] + lens[5]]
|
||||
tfmt = [hfmt[0], hfmt[1], "%%-%ds" % (tlens[2],), "%%-%ds" % (tlens[3],)]
|
||||
tsep = tfmt[0:2]
|
||||
rfmt = (" "+ct[0]+" ").join(rfmt)
|
||||
hfmt = (" "+ct[0]+" ").join(hfmt)
|
||||
tfmt = (" "+ct[0]+" ").join(tfmt)
|
||||
tsep = (" "+ct[0]+" ").join(tsep)
|
||||
separator = ((tsep % tuple(tophead[0:2])) + " "+ct[2]+ct[7] +
|
||||
((ct[7]+ct[3]+ct[7]).join([ct[7] * n for n in tlens[2:]])) + ct[7])
|
||||
ret = []
|
||||
ret.append(" "+tfmt % tuple(["", ""]+tophead[2:]))
|
||||
ret.append(" "+separator)
|
||||
ret.append(" "+hfmt % tuple(headers))
|
||||
separator = (ct[1]+ct[4]+ct[1]).join([ct[1] * n for n in tlens]) + ct[1]
|
||||
ret.append(ct[1]+separator)
|
||||
for row in rows:
|
||||
ret.append(" "+rfmt % tuple(row))
|
||||
separator = (ct[1]+ct[5]+ct[1]).join([ct[1] * n for n in tlens]) + ct[1]
|
||||
ret.append(ct[1]+separator)
|
||||
return ret
|
||||
if not response:
|
||||
return "No jails found."
|
||||
msg = "\n".join(_statstable(response, chrTable[self.encUtf]))
|
||||
elif len(inC) < 2:
|
||||
pass # to few cmd args for below
|
||||
elif inC[1] == "syslogsocket":
|
||||
msg = "Current syslog socket is:\n"
|
||||
msg += "`- " + response
|
||||
elif inC[1] == "logtarget":
|
||||
msg = "Current logging target is:\n"
|
||||
msg += "`- " + response
|
||||
elif inC[1:2] == ['loglevel']:
|
||||
msg = "Current logging level is "
|
||||
msg += repr(logging.getLevelName(response) if isinstance(response, int) else response)
|
||||
elif inC[1] == "dbfile":
|
||||
if response is None:
|
||||
msg = "Database currently disabled"
|
||||
else:
|
||||
msg = "Current database file is:\n"
|
||||
msg += "`- " + response
|
||||
elif inC[1] == "dbpurgeage":
|
||||
if response is None:
|
||||
msg = "Database currently disabled"
|
||||
else:
|
||||
msg = "Current database purge age is:\n"
|
||||
msg += "`- %iseconds" % response
|
||||
elif len(inC) < 3:
|
||||
pass # to few cmd args for below
|
||||
elif inC[2] in ("logpath", "addlogpath", "dellogpath"):
|
||||
if len(response) == 0:
|
||||
msg = "No file is currently monitored"
|
||||
else:
|
||||
msg = "Current monitored log file(s):\n"
|
||||
for path in response[:-1]:
|
||||
msg += "|- " + path + "\n"
|
||||
msg += "`- " + response[-1]
|
||||
elif inC[2] == "logencoding":
|
||||
msg = "Current log encoding is set to:\n"
|
||||
msg += response
|
||||
elif inC[2] in ("journalmatch", "addjournalmatch", "deljournalmatch"):
|
||||
if len(response) == 0:
|
||||
msg = "No journal match filter set"
|
||||
else:
|
||||
msg = "Current match filter:\n"
|
||||
msg += ' + '.join(" ".join(res) for res in response)
|
||||
elif inC[2] == "datepattern":
|
||||
msg = "Current date pattern set to: "
|
||||
if response is None:
|
||||
msg += "Not set/required"
|
||||
elif response[0] is None:
|
||||
msg += "%s" % response[1]
|
||||
else:
|
||||
msg += "%s (%s)" % response
|
||||
elif inC[2] in ("ignoreip", "addignoreip", "delignoreip"):
|
||||
if len(response) == 0:
|
||||
msg = "No IP address/network is ignored"
|
||||
else:
|
||||
msg = "These IP addresses/networks are ignored:\n"
|
||||
for ip in response[:-1]:
|
||||
msg += "|- " + str(ip) + "\n"
|
||||
msg += "`- " + str(response[-1])
|
||||
elif inC[2] in ("failregex", "addfailregex", "delfailregex",
|
||||
"ignoreregex", "addignoreregex", "delignoreregex"):
|
||||
if len(response) == 0:
|
||||
msg = "No regular expression is defined"
|
||||
else:
|
||||
msg = "The following regular expression are defined:\n"
|
||||
c = 0
|
||||
for l in response[:-1]:
|
||||
msg += "|- [" + str(c) + "]: " + l + "\n"
|
||||
c += 1
|
||||
msg += "`- [" + str(c) + "]: " + response[-1]
|
||||
elif inC[2] == "actions":
|
||||
if len(response) == 0:
|
||||
msg = "No actions for jail %s" % inC[1]
|
||||
else:
|
||||
msg = "The jail %s has the following actions:\n" % inC[1]
|
||||
msg += ", ".join(response)
|
||||
elif inC[2] == "actionproperties":
|
||||
if len(response) == 0:
|
||||
msg = "No properties for jail %s action %s" % (
|
||||
inC[1], inC[3])
|
||||
else:
|
||||
msg = "The jail %s action %s has the following " \
|
||||
"properties:\n" % (inC[1], inC[3])
|
||||
msg += ", ".join(response)
|
||||
elif inC[2] == "actionmethods":
|
||||
if len(response) == 0:
|
||||
msg = "No methods for jail %s action %s" % (
|
||||
inC[1], inC[3])
|
||||
else:
|
||||
msg = "The jail %s action %s has the following " \
|
||||
"methods:\n" % (inC[1], inC[3])
|
||||
msg += ", ".join(response)
|
||||
elif inC[2] == "banip" and inC[0] == "get":
|
||||
if isinstance(response, list):
|
||||
sep = " " if len(inC) <= 3 else inC[3]
|
||||
if sep == "--with-time":
|
||||
sep = "\n"
|
||||
msg = sep.join(response)
|
||||
except Exception:
|
||||
logSys.warning("Beautifier error. Please report the error")
|
||||
logSys.error("Beautify %r with %r failed", response, self.__inputCmd,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
msg = repr(msg) + repr(response)
|
||||
return msg
|
||||
|
||||
def beautifyError(self, response):
|
||||
logSys.debug("Beautify (error) %r with %r", response, self.__inputCmd)
|
||||
msg = response
|
||||
if isinstance(response, UnknownJailException):
|
||||
msg = "Sorry but the jail '" + response.args[0] + "' does not exist"
|
||||
elif isinstance(response, IndexError):
|
||||
msg = "Sorry but the command is invalid"
|
||||
elif isinstance(response, DuplicateJailException):
|
||||
msg = "The jail '" + response.args[0] + "' already exists"
|
||||
return msg
|
||||
385
fail2ban-master/fail2ban/client/configparserinc.py
Normal file
385
fail2ban-master/fail2ban/client/configparserinc.py
Normal file
@@ -0,0 +1,385 @@
|
||||
# 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: Yaroslav Halchenko
|
||||
# Modified: Cyril Jaquier
|
||||
|
||||
__author__ = 'Yaroslav Halchenko, Serg G. Brester (aka sebres)'
|
||||
__copyright__ = 'Copyright (c) 2007 Yaroslav Halchenko, 2015 Serg G. Brester (aka sebres)'
|
||||
__license__ = 'GPL'
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from ..helpers import getLogger
|
||||
|
||||
# SafeConfigParser deprecated from Python 3.2 (renamed to ConfigParser)
|
||||
from configparser import ConfigParser as SafeConfigParser, BasicInterpolation, \
|
||||
InterpolationMissingOptionError, NoOptionError, NoSectionError
|
||||
|
||||
# And interpolation of __name__ was simply removed, thus we need to
|
||||
# decorate default interpolator to handle it
|
||||
class BasicInterpolationWithName(BasicInterpolation):
|
||||
"""Decorator to bring __name__ interpolation back.
|
||||
|
||||
Original handling of __name__ was removed because of
|
||||
functional deficiencies: http://bugs.python.org/issue10489
|
||||
|
||||
commit v3.2a4-105-g61f2761
|
||||
Author: Lukasz Langa <lukasz@langa.pl>
|
||||
Date: Sun Nov 21 13:41:35 2010 +0000
|
||||
|
||||
Issue #10489: removed broken `__name__` support from configparser
|
||||
|
||||
But should be fine to reincarnate for our use case
|
||||
"""
|
||||
def _interpolate_some(self, parser, option, accum, rest, section, map,
|
||||
*args, **kwargs):
|
||||
if section and not (__name__ in map):
|
||||
map = map.copy() # just to be safe
|
||||
map['__name__'] = section
|
||||
# try to wrap section options like %(section/option)s:
|
||||
parser._map_section_options(section, option, rest, map)
|
||||
return super(BasicInterpolationWithName, self)._interpolate_some(
|
||||
parser, option, accum, rest, section, map, *args, **kwargs)
|
||||
|
||||
|
||||
def _expandConfFilesWithLocal(filenames):
|
||||
"""Expands config files with local extension.
|
||||
"""
|
||||
newFilenames = []
|
||||
for filename in filenames:
|
||||
newFilenames.append(filename)
|
||||
localname = os.path.splitext(filename)[0] + '.local'
|
||||
if localname not in filenames and os.path.isfile(localname):
|
||||
newFilenames.append(localname)
|
||||
return newFilenames
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
logLevel = 7
|
||||
|
||||
|
||||
__all__ = ['SafeConfigParserWithIncludes']
|
||||
|
||||
|
||||
class SafeConfigParserWithIncludes(SafeConfigParser):
|
||||
"""
|
||||
Class adds functionality to SafeConfigParser to handle included
|
||||
other configuration files (or may be urls, whatever in the future)
|
||||
|
||||
File should have section [includes] and only 2 options implemented
|
||||
are 'files_before' and 'files_after' where files are listed 1 per
|
||||
line.
|
||||
|
||||
Example:
|
||||
|
||||
[INCLUDES]
|
||||
before = 1.conf
|
||||
3.conf
|
||||
|
||||
after = 1.conf
|
||||
|
||||
It is a simple implementation, so just basic care is taken about
|
||||
recursion. Includes preserve right order, ie new files are
|
||||
inserted to the list of read configs before original, and their
|
||||
includes correspondingly so the list should follow the leaves of
|
||||
the tree.
|
||||
|
||||
I wasn't sure what would be the right way to implement generic (aka c++
|
||||
template) so we could base at any *configparser class... so I will
|
||||
leave it for the future
|
||||
|
||||
"""
|
||||
|
||||
SECTION_NAME = "INCLUDES"
|
||||
|
||||
SECTION_OPTNAME_CRE = re.compile(r'^([\w\-]+)/([^\s>]+)$')
|
||||
|
||||
SECTION_OPTSUBST_CRE = re.compile(r'%\(([\w\-]+/([^\)]+))\)s')
|
||||
|
||||
CONDITIONAL_RE = re.compile(r"^(\w+)(\?.+)$")
|
||||
|
||||
# overload constructor only for fancy new Python3's
|
||||
def __init__(self, share_config=None, *args, **kwargs):
|
||||
kwargs = kwargs.copy()
|
||||
kwargs['interpolation'] = BasicInterpolationWithName()
|
||||
kwargs['inline_comment_prefixes'] = ";"
|
||||
super(SafeConfigParserWithIncludes, self).__init__(
|
||||
*args, **kwargs)
|
||||
self._cfg_share = share_config
|
||||
|
||||
def get_ex(self, section, option, raw=False, vars={}):
|
||||
"""Get an option value for a given section.
|
||||
|
||||
In opposite to `get`, it differentiate session-related option name like `sec/opt`.
|
||||
"""
|
||||
sopt = None
|
||||
# if option name contains section:
|
||||
if '/' in option:
|
||||
sopt = SafeConfigParserWithIncludes.SECTION_OPTNAME_CRE.search(option)
|
||||
# try get value from named section/option:
|
||||
if sopt:
|
||||
sec = sopt.group(1)
|
||||
opt = sopt.group(2)
|
||||
seclwr = sec.lower()
|
||||
if seclwr == 'known':
|
||||
# try get value firstly from known options, hereafter from current section:
|
||||
sopt = ('KNOWN/'+section, section)
|
||||
else:
|
||||
sopt = (sec,) if seclwr != 'default' else ("DEFAULT",)
|
||||
for sec in sopt:
|
||||
try:
|
||||
v = self.get(sec, opt, raw=raw)
|
||||
return v
|
||||
except (NoSectionError, NoOptionError) as e:
|
||||
pass
|
||||
# get value of section/option using given section and vars (fallback):
|
||||
v = self.get(section, option, raw=raw, vars=vars)
|
||||
return v
|
||||
|
||||
def _map_section_options(self, section, option, rest, defaults):
|
||||
"""
|
||||
Interpolates values of the section options (name syntax `%(section/option)s`).
|
||||
|
||||
Fallback: try to wrap missing default options as "default/options" resp. "known/options"
|
||||
"""
|
||||
if '/' not in rest or '%(' not in rest: # pragma: no cover
|
||||
return 0
|
||||
rplcmnt = 0
|
||||
soptrep = SafeConfigParserWithIncludes.SECTION_OPTSUBST_CRE.findall(rest)
|
||||
if not soptrep: # pragma: no cover
|
||||
return 0
|
||||
for sopt, opt in soptrep:
|
||||
if sopt not in defaults:
|
||||
sec = sopt[:~len(opt)]
|
||||
seclwr = sec.lower()
|
||||
if seclwr != 'default':
|
||||
usedef = 0
|
||||
if seclwr == 'known':
|
||||
# try get raw value from known options:
|
||||
try:
|
||||
v = self._sections['KNOWN/'+section][opt]
|
||||
except KeyError:
|
||||
# fallback to default:
|
||||
usedef = 1
|
||||
else:
|
||||
# get raw value of opt in section:
|
||||
try:
|
||||
# if section not found - ignore:
|
||||
try:
|
||||
sec = self._sections[sec]
|
||||
except KeyError: # pragma: no cover
|
||||
continue
|
||||
v = sec[opt]
|
||||
except KeyError: # pragma: no cover
|
||||
# fallback to default:
|
||||
usedef = 1
|
||||
else:
|
||||
usedef = 1
|
||||
if usedef:
|
||||
try:
|
||||
v = self._defaults[opt]
|
||||
except KeyError: # pragma: no cover
|
||||
continue
|
||||
# replacement found:
|
||||
rplcmnt = 1
|
||||
try: # set it in map-vars (consider different python versions):
|
||||
defaults[sopt] = v
|
||||
except:
|
||||
# try to set in first default map (corresponding vars):
|
||||
try:
|
||||
defaults._maps[0][sopt] = v
|
||||
except: # pragma: no cover
|
||||
# no way to update vars chain map - overwrite defaults:
|
||||
self._defaults[sopt] = v
|
||||
return rplcmnt
|
||||
|
||||
@property
|
||||
def share_config(self):
|
||||
return self._cfg_share
|
||||
|
||||
def _getSharedSCPWI(self, filename):
|
||||
SCPWI = SafeConfigParserWithIncludes
|
||||
# read single one, add to return list, use sharing if possible:
|
||||
if self._cfg_share:
|
||||
# cache/share each file as include (ex: filter.d/common could be included in each filter config):
|
||||
hashv = 'inc:'+(filename if not isinstance(filename, list) else '\x01'.join(filename))
|
||||
cfg, i = self._cfg_share.get(hashv, (None, None))
|
||||
if cfg is None:
|
||||
cfg = SCPWI(share_config=self._cfg_share)
|
||||
i = cfg.read(filename, get_includes=False)
|
||||
self._cfg_share[hashv] = (cfg, i)
|
||||
elif logSys.getEffectiveLevel() <= logLevel:
|
||||
logSys.log(logLevel, " Shared file: %s", filename)
|
||||
else:
|
||||
# don't have sharing:
|
||||
cfg = SCPWI()
|
||||
i = cfg.read(filename, get_includes=False)
|
||||
return (cfg, i)
|
||||
|
||||
def _getIncludes(self, filenames, seen=[]):
|
||||
if not isinstance(filenames, list):
|
||||
filenames = [ filenames ]
|
||||
filenames = _expandConfFilesWithLocal(filenames)
|
||||
# retrieve or cache include paths:
|
||||
if self._cfg_share:
|
||||
# cache/share include list:
|
||||
hashv = 'inc-path:'+('\x01'.join(filenames))
|
||||
fileNamesFull = self._cfg_share.get(hashv)
|
||||
if fileNamesFull is None:
|
||||
fileNamesFull = []
|
||||
for filename in filenames:
|
||||
fileNamesFull += self.__getIncludesUncached(filename, seen)
|
||||
self._cfg_share[hashv] = fileNamesFull
|
||||
return fileNamesFull
|
||||
# don't have sharing:
|
||||
fileNamesFull = []
|
||||
for filename in filenames:
|
||||
fileNamesFull += self.__getIncludesUncached(filename, seen)
|
||||
return fileNamesFull
|
||||
|
||||
def __getIncludesUncached(self, resource, seen=[]):
|
||||
"""
|
||||
Given 1 config resource returns list of included files
|
||||
(recursively) with the original one as well
|
||||
Simple loops are taken care about
|
||||
"""
|
||||
SCPWI = SafeConfigParserWithIncludes
|
||||
try:
|
||||
parser, i = self._getSharedSCPWI(resource)
|
||||
if not i:
|
||||
return []
|
||||
except UnicodeDecodeError as e:
|
||||
logSys.error("Error decoding config file '%s': %s" % (resource, e))
|
||||
return []
|
||||
|
||||
resourceDir = os.path.dirname(resource)
|
||||
|
||||
newFiles = [ ('before', []), ('after', []) ]
|
||||
if SCPWI.SECTION_NAME in parser.sections():
|
||||
for option_name, option_list in newFiles:
|
||||
if option_name in parser.options(SCPWI.SECTION_NAME):
|
||||
newResources = parser.get(SCPWI.SECTION_NAME, option_name)
|
||||
for newResource in newResources.split('\n'):
|
||||
if os.path.isabs(newResource):
|
||||
r = newResource
|
||||
else:
|
||||
r = os.path.join(resourceDir, newResource)
|
||||
if r in seen:
|
||||
continue
|
||||
s = seen + [resource]
|
||||
option_list += self._getIncludes(r, s)
|
||||
# combine lists
|
||||
return newFiles[0][1] + [resource] + newFiles[1][1]
|
||||
|
||||
def get_defaults(self):
|
||||
return self._defaults
|
||||
|
||||
def get_sections(self):
|
||||
return self._sections
|
||||
|
||||
def options(self, section, withDefault=True):
|
||||
"""Return a list of option names for the given section name.
|
||||
|
||||
Parameter `withDefault` controls the include of names from section `[DEFAULT]`
|
||||
"""
|
||||
try:
|
||||
opts = self._sections[section]
|
||||
except KeyError: # pragma: no cover
|
||||
raise NoSectionError(section)
|
||||
if withDefault:
|
||||
# mix it with defaults:
|
||||
return set(opts.keys()) | set(self._defaults)
|
||||
# only own option names:
|
||||
return list(opts.keys())
|
||||
|
||||
def read(self, filenames, get_includes=True):
|
||||
if not isinstance(filenames, list):
|
||||
filenames = [ filenames ]
|
||||
# retrieve (and cache) includes:
|
||||
fileNamesFull = []
|
||||
if get_includes:
|
||||
fileNamesFull += self._getIncludes(filenames)
|
||||
else:
|
||||
fileNamesFull = filenames
|
||||
|
||||
if not fileNamesFull:
|
||||
return []
|
||||
|
||||
logSys.info(" Loading files: %s", fileNamesFull)
|
||||
|
||||
if get_includes or len(fileNamesFull) > 1:
|
||||
# read multiple configs:
|
||||
ret = []
|
||||
alld = self.get_defaults()
|
||||
alls = self.get_sections()
|
||||
for filename in fileNamesFull:
|
||||
# read single one, add to return list, use sharing if possible:
|
||||
cfg, i = self._getSharedSCPWI(filename)
|
||||
if i:
|
||||
ret += i
|
||||
# merge defaults and all sections to self:
|
||||
alld.update(cfg.get_defaults())
|
||||
for n, s in cfg.get_sections().items():
|
||||
# conditional sections
|
||||
cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(n)
|
||||
if cond:
|
||||
n, cond = cond.groups()
|
||||
s = s.copy()
|
||||
try:
|
||||
del(s['__name__'])
|
||||
except KeyError:
|
||||
pass
|
||||
for k in list(s.keys()):
|
||||
v = s.pop(k)
|
||||
s[k + cond] = v
|
||||
s2 = alls.get(n)
|
||||
if isinstance(s2, dict):
|
||||
# save previous known values, for possible using in local interpolations later:
|
||||
self.merge_section('KNOWN/'+n,
|
||||
dict([i for i in iter(s2.items()) if i[0] in s]), '')
|
||||
# merge section
|
||||
s2.update(s)
|
||||
else:
|
||||
alls[n] = s.copy()
|
||||
|
||||
return ret
|
||||
|
||||
# read one config :
|
||||
if logSys.getEffectiveLevel() <= logLevel:
|
||||
logSys.log(logLevel, " Reading file: %s", fileNamesFull[0])
|
||||
# read file(s) :
|
||||
return SafeConfigParser.read(self, fileNamesFull, encoding='utf-8')
|
||||
|
||||
def merge_section(self, section, options, pref=None):
|
||||
alls = self.get_sections()
|
||||
try:
|
||||
sec = alls[section]
|
||||
except KeyError:
|
||||
alls[section] = sec = dict()
|
||||
if not pref:
|
||||
sec.update(options)
|
||||
return
|
||||
sk = {}
|
||||
for k, v in options.items():
|
||||
if not k.startswith(pref) and k != '__name__':
|
||||
sk[pref+k] = v
|
||||
sec.update(sk)
|
||||
|
||||
425
fail2ban-master/fail2ban/client/configreader.py
Normal file
425
fail2ban-master/fail2ban/client/configreader.py
Normal file
@@ -0,0 +1,425 @@
|
||||
# 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: Cyril Jaquier
|
||||
# Modified by: Yaroslav Halchenko (SafeConfigParserWithIncludes)
|
||||
|
||||
__author__ = "Cyril Jaquier, Yaroslav Halchenko, Serg G. Brester (aka sebres)"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2007 Yaroslav Halchenko, 2015 Serg G. Brester (aka sebres)"
|
||||
__license__ = "GPL"
|
||||
|
||||
import glob
|
||||
import os
|
||||
from configparser import NoOptionError, NoSectionError
|
||||
|
||||
from .configparserinc import sys, SafeConfigParserWithIncludes, logLevel
|
||||
from ..helpers import getLogger, _as_bool, _merge_dicts, substituteRecursiveTags
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
CONVERTER = {
|
||||
"bool": _as_bool,
|
||||
"int": int,
|
||||
}
|
||||
def _OptionsTemplateGen(options):
|
||||
"""Iterator over the options template with default options.
|
||||
|
||||
Each options entry is composed of an array or tuple with:
|
||||
[[type, name, ?default?], ...]
|
||||
Or it is a dict:
|
||||
{name: [type, default], ...}
|
||||
"""
|
||||
if isinstance(options, (list,tuple)):
|
||||
for optname in options:
|
||||
if len(optname) > 2:
|
||||
opttype, optname, optvalue = optname
|
||||
else:
|
||||
(opttype, optname), optvalue = optname, None
|
||||
yield opttype, optname, optvalue
|
||||
else:
|
||||
for optname in options:
|
||||
opttype, optvalue = options[optname]
|
||||
yield opttype, optname, optvalue
|
||||
|
||||
|
||||
class ConfigReader():
|
||||
"""Generic config reader class.
|
||||
|
||||
A caching adapter which automatically reuses already shared configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, use_config=None, share_config=None, **kwargs):
|
||||
# use given shared config if possible (see read):
|
||||
self._cfg_share = None
|
||||
self._cfg = None
|
||||
if use_config is not None:
|
||||
self._cfg = use_config
|
||||
# share config if possible:
|
||||
if share_config is not None:
|
||||
self._cfg_share = share_config
|
||||
self._cfg_share_kwargs = kwargs
|
||||
self._cfg_share_basedir = None
|
||||
elif self._cfg is None:
|
||||
self._cfg = ConfigReaderUnshared(**kwargs)
|
||||
|
||||
def setBaseDir(self, basedir):
|
||||
if self._cfg:
|
||||
self._cfg.setBaseDir(basedir)
|
||||
else:
|
||||
self._cfg_share_basedir = basedir
|
||||
|
||||
def getBaseDir(self):
|
||||
if self._cfg:
|
||||
return self._cfg.getBaseDir()
|
||||
else:
|
||||
return self._cfg_share_basedir
|
||||
|
||||
@property
|
||||
def share_config(self):
|
||||
return self._cfg_share
|
||||
|
||||
def read(self, name, once=True):
|
||||
""" Overloads a default (not shared) read of config reader.
|
||||
|
||||
To prevent multiple reads of config files with it includes, reads into
|
||||
the config reader, if it was not yet cached/shared by 'name'.
|
||||
"""
|
||||
# already shared ?
|
||||
if not self._cfg:
|
||||
self._create_unshared(name)
|
||||
# performance feature - read once if using shared config reader:
|
||||
if once and self._cfg.read_cfg_files is not None:
|
||||
return self._cfg.read_cfg_files
|
||||
|
||||
# load:
|
||||
logSys.info("Loading configs for %s under %s ", name, self._cfg.getBaseDir())
|
||||
ret = self._cfg.read(name)
|
||||
|
||||
# save already read and return:
|
||||
self._cfg.read_cfg_files = ret
|
||||
return ret
|
||||
|
||||
def _create_unshared(self, name=''):
|
||||
""" Allocates and share a config file by it name.
|
||||
|
||||
Automatically allocates unshared or reuses shared handle by given 'name' and
|
||||
init arguments inside a given shared storage.
|
||||
"""
|
||||
if not self._cfg and self._cfg_share is not None:
|
||||
self._cfg = self._cfg_share.get(name)
|
||||
if not self._cfg:
|
||||
self._cfg = ConfigReaderUnshared(share_config=self._cfg_share, **self._cfg_share_kwargs)
|
||||
if self._cfg_share_basedir is not None:
|
||||
self._cfg.setBaseDir(self._cfg_share_basedir)
|
||||
self._cfg_share[name] = self._cfg
|
||||
else:
|
||||
self._cfg = ConfigReaderUnshared(**self._cfg_share_kwargs)
|
||||
|
||||
def sections(self):
|
||||
try:
|
||||
return (n for n in self._cfg.sections() if not n.startswith('KNOWN/'))
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
def has_section(self, sec):
|
||||
try:
|
||||
return self._cfg.has_section(sec)
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def has_option(self, sec, opt, withDefault=True):
|
||||
return self._cfg.has_option(sec, opt) if withDefault \
|
||||
else opt in self._cfg._sections.get(sec, {})
|
||||
|
||||
def merge_defaults(self, d):
|
||||
self._cfg.get_defaults().update(d)
|
||||
|
||||
def merge_section(self, section, *args, **kwargs):
|
||||
try:
|
||||
return self._cfg.merge_section(section, *args, **kwargs)
|
||||
except AttributeError:
|
||||
raise NoSectionError(section)
|
||||
|
||||
def options(self, section, withDefault=False):
|
||||
"""Return a list of option names for the given section name.
|
||||
|
||||
Parameter `withDefault` controls the include of names from section `[DEFAULT]`
|
||||
"""
|
||||
try:
|
||||
return self._cfg.options(section, withDefault)
|
||||
except AttributeError:
|
||||
raise NoSectionError(section)
|
||||
|
||||
def get(self, sec, opt, raw=False, vars={}):
|
||||
try:
|
||||
return self._cfg.get(sec, opt, raw=raw, vars=vars)
|
||||
except AttributeError:
|
||||
raise NoSectionError(sec)
|
||||
|
||||
def getOptions(self, section, *args, **kwargs):
|
||||
try:
|
||||
return self._cfg.getOptions(section, *args, **kwargs)
|
||||
except AttributeError:
|
||||
raise NoSectionError(section)
|
||||
|
||||
|
||||
class ConfigReaderUnshared(SafeConfigParserWithIncludes):
|
||||
"""Unshared config reader (previously ConfigReader).
|
||||
|
||||
Do not use this class (internal not shared/cached representation).
|
||||
Use ConfigReader instead.
|
||||
"""
|
||||
|
||||
DEFAULT_BASEDIR = '/etc/fail2ban'
|
||||
|
||||
def __init__(self, basedir=None, *args, **kwargs):
|
||||
SafeConfigParserWithIncludes.__init__(self, *args, **kwargs)
|
||||
self.read_cfg_files = None
|
||||
self.setBaseDir(basedir)
|
||||
|
||||
def setBaseDir(self, basedir):
|
||||
if basedir is None:
|
||||
basedir = ConfigReaderUnshared.DEFAULT_BASEDIR # stock system location
|
||||
self._basedir = basedir.rstrip('/')
|
||||
|
||||
def getBaseDir(self):
|
||||
return self._basedir
|
||||
|
||||
def read(self, filename):
|
||||
if not os.path.exists(self._basedir):
|
||||
raise ValueError("Base configuration directory %s does not exist "
|
||||
% self._basedir)
|
||||
if filename.startswith("./"): # pragma: no cover
|
||||
filename = os.path.abspath(filename)
|
||||
basename = os.path.join(self._basedir, filename)
|
||||
logSys.debug("Reading configs for %s under %s " , filename, self._basedir)
|
||||
config_files = [ basename + ".conf" ]
|
||||
|
||||
# possible further customizations under a .conf.d directory
|
||||
config_dir = basename + '.d'
|
||||
config_files += sorted(glob.glob('%s/*.conf' % config_dir))
|
||||
|
||||
config_files.append(basename + ".local")
|
||||
|
||||
config_files += sorted(glob.glob('%s/*.local' % config_dir))
|
||||
|
||||
# choose only existing ones
|
||||
config_files = list(filter(os.path.exists, config_files))
|
||||
|
||||
if len(config_files):
|
||||
# at least one config exists and accessible
|
||||
logSys.debug("Reading config files: %s", ', '.join(config_files))
|
||||
config_files_read = SafeConfigParserWithIncludes.read(self, config_files)
|
||||
missed = [ cf for cf in config_files if cf not in config_files_read ]
|
||||
if missed:
|
||||
logSys.error("Could not read config files: %s", ', '.join(missed))
|
||||
return False
|
||||
if config_files_read:
|
||||
return True
|
||||
logSys.error("Found no accessible config files for %r under %s",
|
||||
filename, self.getBaseDir())
|
||||
return False
|
||||
else:
|
||||
logSys.error("Found no accessible config files for %r " % filename
|
||||
+ (["under %s" % self.getBaseDir(),
|
||||
"among existing ones: " + ', '.join(config_files)][bool(len(config_files))]))
|
||||
|
||||
return False
|
||||
|
||||
##
|
||||
# Read the options.
|
||||
#
|
||||
# Read the given option in the configuration file. Default values
|
||||
# are used...
|
||||
# Each options entry is composed of an array with:
|
||||
# [[type, name, default], ...]
|
||||
# Or it is a dict:
|
||||
# {name: [type, default], ...}
|
||||
|
||||
def getOptions(self, sec, options, pOptions=None, shouldExist=False, convert=True):
|
||||
values = dict()
|
||||
if pOptions is None:
|
||||
pOptions = {}
|
||||
# Get only specified options:
|
||||
for opttype, optname, optvalue in _OptionsTemplateGen(options):
|
||||
if optname in pOptions:
|
||||
continue
|
||||
try:
|
||||
v = self.get(sec, optname, vars=pOptions)
|
||||
values[optname] = v
|
||||
if convert:
|
||||
conv = CONVERTER.get(opttype)
|
||||
if conv:
|
||||
if v is None: continue
|
||||
values[optname] = conv(v)
|
||||
except NoSectionError as e:
|
||||
if shouldExist:
|
||||
raise
|
||||
# No "Definition" section or wrong basedir
|
||||
logSys.error(e)
|
||||
values[optname] = optvalue
|
||||
# TODO: validate error handling here.
|
||||
except NoOptionError:
|
||||
if not optvalue is None:
|
||||
logSys.debug("'%s' not defined in '%s'. Using default one: %r"
|
||||
% (optname, sec, optvalue))
|
||||
values[optname] = optvalue
|
||||
# elif logSys.getEffectiveLevel() <= logLevel:
|
||||
# logSys.log(logLevel, "Non essential option '%s' not defined in '%s'.", optname, sec)
|
||||
except ValueError:
|
||||
logSys.warning("Wrong value for '" + optname + "' in '" + sec +
|
||||
"'. Using default one: '" + repr(optvalue) + "'")
|
||||
values[optname] = optvalue
|
||||
return values
|
||||
|
||||
|
||||
class DefinitionInitConfigReader(ConfigReader):
|
||||
"""Config reader for files with options grouped in [Definition] and
|
||||
[Init] sections.
|
||||
|
||||
Is a base class for readers of filters and actions, where definitions
|
||||
in jails might provide custom values for options defined in [Init]
|
||||
section.
|
||||
"""
|
||||
|
||||
_configOpts = []
|
||||
|
||||
def __init__(self, file_, jailName, initOpts, **kwargs):
|
||||
ConfigReader.__init__(self, **kwargs)
|
||||
if file_.startswith("./"): # pragma: no cover
|
||||
file_ = os.path.abspath(file_)
|
||||
self.setFile(file_)
|
||||
self.setJailName(jailName)
|
||||
self._initOpts = initOpts
|
||||
self._pOpts = dict()
|
||||
self._defCache = dict()
|
||||
|
||||
def setFile(self, fileName):
|
||||
self._file = fileName
|
||||
self._initOpts = {}
|
||||
|
||||
def getFile(self):
|
||||
return self._file
|
||||
|
||||
def setJailName(self, jailName):
|
||||
self._jailName = jailName
|
||||
|
||||
def getJailName(self):
|
||||
return self._jailName
|
||||
|
||||
def read(self):
|
||||
return ConfigReader.read(self, self._file)
|
||||
|
||||
# needed for fail2ban-regex that doesn't need fancy directories
|
||||
def readexplicit(self):
|
||||
if not self._cfg:
|
||||
self._create_unshared(self._file)
|
||||
return SafeConfigParserWithIncludes.read(self._cfg, self._file)
|
||||
|
||||
def getOptions(self, pOpts, all=False):
|
||||
# overwrite static definition options with init values, supplied as
|
||||
# direct parameters from jail-config via action[xtra1="...", xtra2=...]:
|
||||
if not pOpts:
|
||||
pOpts = dict()
|
||||
if self._initOpts:
|
||||
pOpts = _merge_dicts(pOpts, self._initOpts)
|
||||
# type-convert only in combined (otherwise int/bool converting prevents substitution):
|
||||
self._opts = ConfigReader.getOptions(
|
||||
self, "Definition", self._configOpts, pOpts, convert=False)
|
||||
self._pOpts = pOpts
|
||||
if self.has_section("Init"):
|
||||
# get only own options (without options from default):
|
||||
getopt = lambda opt: self.get("Init", opt)
|
||||
for opt in self.options("Init", withDefault=False):
|
||||
if opt == '__name__': continue
|
||||
v = None
|
||||
if not opt.startswith('known/'):
|
||||
if v is None: v = getopt(opt)
|
||||
self._initOpts['known/'+opt] = v
|
||||
if opt not in self._initOpts:
|
||||
# overwrite also conditional init options (from init?... section):
|
||||
cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(opt)
|
||||
if cond:
|
||||
optc, cond = cond.groups()
|
||||
v = pOpts.get(optc, v)
|
||||
if v is None: v = getopt(opt)
|
||||
self._initOpts[opt] = v
|
||||
if all and self.has_section("Definition"):
|
||||
# merge with all definition options (and options from default),
|
||||
# bypass already converted option (so merge only new options):
|
||||
for opt in self.options("Definition"):
|
||||
if opt == '__name__' or opt in self._opts: continue
|
||||
self._opts[opt] = self.get("Definition", opt)
|
||||
|
||||
def convertOptions(self, opts, configOpts):
|
||||
"""Convert interpolated combined options to expected type.
|
||||
"""
|
||||
for opttype, optname, optvalue in _OptionsTemplateGen(configOpts):
|
||||
conv = CONVERTER.get(opttype)
|
||||
if conv:
|
||||
v = opts.get(optname)
|
||||
if v is None: continue
|
||||
try:
|
||||
opts[optname] = conv(v)
|
||||
except ValueError:
|
||||
logSys.warning("Wrong %s value %r for %r. Using default one: %r",
|
||||
opttype, v, optname, optvalue)
|
||||
opts[optname] = optvalue
|
||||
|
||||
def getCombOption(self, optname):
|
||||
"""Get combined definition option (as string) using pre-set and init
|
||||
options as preselection (values with higher precedence as specified in section).
|
||||
|
||||
Can be used only after calling of getOptions.
|
||||
"""
|
||||
try:
|
||||
return self._defCache[optname]
|
||||
except KeyError:
|
||||
try:
|
||||
v = self._cfg.get_ex("Definition", optname, vars=self._pOpts)
|
||||
except (NoSectionError, NoOptionError, ValueError):
|
||||
v = None
|
||||
self._defCache[optname] = v
|
||||
return v
|
||||
|
||||
def getCombined(self, ignore=()):
|
||||
combinedopts = self._opts
|
||||
if self._initOpts:
|
||||
combinedopts = _merge_dicts(combinedopts, self._initOpts)
|
||||
if not len(combinedopts):
|
||||
return {}
|
||||
# ignore conditional options:
|
||||
ignore = set(ignore).copy()
|
||||
for n in combinedopts:
|
||||
cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(n)
|
||||
if cond:
|
||||
n, cond = cond.groups()
|
||||
ignore.add(n)
|
||||
# substitute options already specified direct:
|
||||
opts = substituteRecursiveTags(combinedopts,
|
||||
ignore=ignore, addrepl=self.getCombOption)
|
||||
if not opts:
|
||||
raise ValueError('recursive tag definitions unable to be resolved')
|
||||
# convert options after all interpolations:
|
||||
self.convertOptions(opts, self._configOpts)
|
||||
return opts
|
||||
|
||||
def convert(self):
|
||||
raise NotImplementedError
|
||||
92
fail2ban-master/fail2ban/client/configurator.py
Normal file
92
fail2ban-master/fail2ban/client/configurator.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# 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: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
from .fail2banreader import Fail2banReader
|
||||
from .jailsreader import JailsReader
|
||||
from ..helpers import getLogger
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
|
||||
class Configurator:
|
||||
|
||||
def __init__(self, force_enable=False, share_config=None):
|
||||
self.__settings = dict()
|
||||
self.__streams = dict()
|
||||
# always share all config readers:
|
||||
if share_config is None:
|
||||
share_config = dict()
|
||||
self.__share_config = share_config
|
||||
self.__fail2ban = Fail2banReader(share_config=share_config)
|
||||
self.__jails = JailsReader(force_enable=force_enable, share_config=share_config)
|
||||
|
||||
def Reload(self):
|
||||
# clear all shared handlers:
|
||||
self.__share_config.clear()
|
||||
|
||||
def setBaseDir(self, folderName):
|
||||
self.__fail2ban.setBaseDir(folderName)
|
||||
self.__jails.setBaseDir(folderName)
|
||||
|
||||
def getBaseDir(self):
|
||||
fail2ban_basedir = self.__fail2ban.getBaseDir()
|
||||
jails_basedir = self.__jails.getBaseDir()
|
||||
if fail2ban_basedir != jails_basedir:
|
||||
logSys.error("fail2ban.conf and jails.conf readers have differing "
|
||||
"basedirs: %r and %r. "
|
||||
"Returning the one for fail2ban.conf"
|
||||
% (fail2ban_basedir, jails_basedir))
|
||||
return fail2ban_basedir
|
||||
|
||||
def readEarly(self):
|
||||
if not self.__fail2ban.read():
|
||||
raise LookupError("Read fail2ban configuration failed.")
|
||||
|
||||
def readAll(self):
|
||||
self.readEarly()
|
||||
if not self.__jails.read():
|
||||
raise LookupError("Read jails configuration failed.")
|
||||
|
||||
def getEarlyOptions(self):
|
||||
return self.__fail2ban.getEarlyOptions()
|
||||
|
||||
def getOptions(self, jail=None, updateMainOpt=None, ignoreWrong=True):
|
||||
self.__fail2ban.getOptions(updateMainOpt)
|
||||
return self.__jails.getOptions(jail, ignoreWrong=ignoreWrong)
|
||||
|
||||
def convertToProtocol(self, allow_no_files=False):
|
||||
self.__streams["general"] = self.__fail2ban.convert()
|
||||
self.__streams["jails"] = self.__jails.convert(allow_no_files=allow_no_files)
|
||||
|
||||
def getConfigStream(self):
|
||||
cmds = list()
|
||||
for opt in self.__streams["general"]:
|
||||
cmds.append(opt)
|
||||
for opt in self.__streams["jails"]:
|
||||
cmds.append(opt)
|
||||
return cmds
|
||||
|
||||
93
fail2ban-master/fail2ban/client/csocket.py
Normal file
93
fail2ban-master/fail2ban/client/csocket.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# 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: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
#from cPickle import dumps, loads, HIGHEST_PROTOCOL
|
||||
from pickle import dumps, loads, HIGHEST_PROTOCOL
|
||||
from ..protocol import CSPROTO
|
||||
import socket
|
||||
import sys
|
||||
|
||||
class CSocket:
|
||||
|
||||
def __init__(self, sock="/var/run/fail2ban/fail2ban.sock", timeout=-1):
|
||||
# Create an INET, STREAMing socket
|
||||
#self.csock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.__csock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.__deftout = self.__csock.gettimeout()
|
||||
if timeout != -1:
|
||||
self.settimeout(timeout)
|
||||
#self.csock.connect(("localhost", 2222))
|
||||
self.__csock.connect(sock)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def send(self, msg, nonblocking=False, timeout=None):
|
||||
# Convert every list member to string
|
||||
obj = dumps(list(map(CSocket.convert, msg)), HIGHEST_PROTOCOL)
|
||||
self.__csock.send(obj)
|
||||
self.__csock.send(CSPROTO.END)
|
||||
return self.receive(self.__csock, nonblocking, timeout)
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self.__csock.settimeout(timeout if timeout != -1 else self.__deftout)
|
||||
|
||||
def close(self):
|
||||
if not self.__csock:
|
||||
return
|
||||
try:
|
||||
self.__csock.sendall(CSPROTO.CLOSE + CSPROTO.END)
|
||||
self.__csock.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error: # pragma: no cover - normally unreachable
|
||||
pass
|
||||
try:
|
||||
self.__csock.close()
|
||||
except socket.error: # pragma: no cover - normally unreachable
|
||||
pass
|
||||
self.__csock = None
|
||||
|
||||
@staticmethod
|
||||
def convert(m):
|
||||
"""Convert every "unexpected" member of message to string"""
|
||||
if isinstance(m, (str, bool, int, float, list, dict, set)):
|
||||
return m
|
||||
else: # pragma: no cover
|
||||
return str(m)
|
||||
|
||||
@staticmethod
|
||||
def receive(sock, nonblocking=False, timeout=None):
|
||||
msg = CSPROTO.EMPTY
|
||||
if nonblocking: sock.setblocking(0)
|
||||
if timeout: sock.settimeout(timeout)
|
||||
bufsize = 1024
|
||||
while msg.rfind(CSPROTO.END, -32) == -1:
|
||||
chunk = sock.recv(bufsize)
|
||||
if not len(chunk):
|
||||
raise socket.error(104, 'Connection reset by peer')
|
||||
if chunk == CSPROTO.END: break
|
||||
msg = msg + chunk
|
||||
if bufsize < 32768: bufsize <<= 1
|
||||
return loads(msg)
|
||||
517
fail2ban-master/fail2ban/client/fail2banclient.py
Normal file
517
fail2ban-master/fail2ban/client/fail2banclient.py
Normal file
@@ -0,0 +1,517 @@
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
#
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
__author__ = "Fail2Ban Developers"
|
||||
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
|
||||
__license__ = "GPL"
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
import threading
|
||||
from threading import Thread
|
||||
|
||||
from ..version import version
|
||||
from .csocket import CSocket
|
||||
from .beautifier import Beautifier
|
||||
from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, ExitException, \
|
||||
logSys, exit, output
|
||||
|
||||
from ..server.utils import Utils
|
||||
|
||||
PROMPT = "fail2ban> "
|
||||
|
||||
|
||||
def _thread_name():
|
||||
return threading.current_thread().__class__.__name__
|
||||
|
||||
def input_command(): # pragma: no cover
|
||||
return input(PROMPT)
|
||||
|
||||
##
|
||||
#
|
||||
# @todo This class needs cleanup.
|
||||
|
||||
class Fail2banClient(Fail2banCmdLine, Thread):
|
||||
|
||||
def __init__(self):
|
||||
Fail2banCmdLine.__init__(self)
|
||||
Thread.__init__(self)
|
||||
self._alive = True
|
||||
self._server = None
|
||||
self._beautifier = None
|
||||
|
||||
def dispInteractive(self):
|
||||
output("Fail2Ban v" + version + " reads log file that contains password failure report")
|
||||
output("and bans the corresponding IP addresses using firewall rules.")
|
||||
output("")
|
||||
|
||||
def __sigTERMhandler(self, signum, frame): # pragma: no cover
|
||||
# Print a new line because we probably come from wait
|
||||
output("")
|
||||
logSys.warning("Caught signal %d. Exiting" % signum)
|
||||
exit(255)
|
||||
|
||||
def __ping(self, timeout=0.1):
|
||||
return self.__processCmd([["ping"] + ([timeout] if timeout != -1 else [])],
|
||||
False, timeout=timeout)
|
||||
|
||||
@property
|
||||
def beautifier(self):
|
||||
if self._beautifier:
|
||||
return self._beautifier
|
||||
self._beautifier = Beautifier()
|
||||
return self._beautifier
|
||||
|
||||
def __processCmd(self, cmd, showRet=True, timeout=-1):
|
||||
client = None
|
||||
try:
|
||||
beautifier = self.beautifier
|
||||
streamRet = True
|
||||
for c in cmd:
|
||||
beautifier.setInputCmd(c)
|
||||
try:
|
||||
if not client:
|
||||
client = CSocket(self._conf["socket"], timeout=timeout)
|
||||
elif timeout != -1:
|
||||
client.settimeout(timeout)
|
||||
if self._conf["verbose"] > 2:
|
||||
logSys.log(5, "CMD: %r", c)
|
||||
ret = client.send(c)
|
||||
if ret[0] == 0:
|
||||
logSys.log(5, "OK : %r", ret[1])
|
||||
if showRet or c[0] in ('echo', 'server-status'):
|
||||
output(beautifier.beautify(ret[1]))
|
||||
else:
|
||||
logSys.error("NOK: %r", ret[1].args)
|
||||
if showRet:
|
||||
output(beautifier.beautifyError(ret[1]))
|
||||
streamRet = False
|
||||
except socket.error as e:
|
||||
if showRet or self._conf["verbose"] > 1:
|
||||
if showRet or c[0] != "ping":
|
||||
self.__logSocketError(e, c[0] == "ping")
|
||||
else:
|
||||
logSys.log(5, " -- %s failed -- %r", c, e)
|
||||
return False
|
||||
except Exception as e: # pragma: no cover
|
||||
if showRet or self._conf["verbose"] > 1:
|
||||
if self._conf["verbose"] > 1:
|
||||
logSys.exception(e)
|
||||
else:
|
||||
logSys.error(e)
|
||||
return False
|
||||
finally:
|
||||
# prevent errors by close during shutdown (on exit command):
|
||||
if client:
|
||||
try :
|
||||
client.close()
|
||||
except Exception as e: # pragma: no cover
|
||||
if showRet or self._conf["verbose"] > 1:
|
||||
logSys.debug(e)
|
||||
if showRet or c[0] in ('echo', 'server-status'):
|
||||
sys.stdout.flush()
|
||||
return streamRet
|
||||
|
||||
def __logSocketError(self, prevError="", errorOnly=False):
|
||||
try:
|
||||
if os.access(self._conf["socket"], os.F_OK): # pragma: no cover
|
||||
# This doesn't check if path is a socket,
|
||||
# but socket.error should be raised
|
||||
if os.access(self._conf["socket"], os.W_OK):
|
||||
# Permissions look good, but socket.error was raised
|
||||
if errorOnly:
|
||||
logSys.error(prevError)
|
||||
else:
|
||||
logSys.error("%sUnable to contact server. Is it running?",
|
||||
("[%s] " % prevError) if prevError else '')
|
||||
else:
|
||||
logSys.error("Permission denied to socket: %s,"
|
||||
" (you must be root)", self._conf["socket"])
|
||||
else:
|
||||
logSys.error("Failed to access socket path: %s."
|
||||
" Is fail2ban running?",
|
||||
self._conf["socket"])
|
||||
except Exception as e: # pragma: no cover
|
||||
logSys.error("Exception while checking socket access: %s",
|
||||
self._conf["socket"])
|
||||
logSys.error(e)
|
||||
|
||||
##
|
||||
def __prepareStartServer(self):
|
||||
if self.__ping():
|
||||
logSys.error("Server already running")
|
||||
return None
|
||||
|
||||
# Read the config
|
||||
ret, stream = self.readConfig()
|
||||
# Do not continue if configuration is not 100% valid
|
||||
if not ret:
|
||||
return None
|
||||
|
||||
# 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
|
||||
|
||||
return [["server-stream", stream], ['server-status']]
|
||||
|
||||
def _set_server(self, s):
|
||||
self._server = s
|
||||
|
||||
##
|
||||
def __startServer(self, background=True):
|
||||
from .fail2banserver import Fail2banServer
|
||||
# read configuration here (in client only, in server we do that in the config-thread):
|
||||
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 (or new 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:
|
||||
phase = dict()
|
||||
self.configureServer(phase=phase, stream=stream)
|
||||
# Mark current (main) thread as daemon:
|
||||
self.daemon = True
|
||||
# Start server direct here in main thread (not fork):
|
||||
self._server = Fail2banServer.startServerDirect(self._conf, False, self._set_server)
|
||||
if not phase.get('done', False):
|
||||
if self._server: # pragma: no cover
|
||||
self._server.quit()
|
||||
self._server = None
|
||||
exit(255)
|
||||
except ExitException: # pragma: no cover
|
||||
raise
|
||||
except Exception as e: # pragma: no cover
|
||||
output("")
|
||||
logSys.error("Exception while starting server " + ("background" if background else "foreground"))
|
||||
if self._conf["verbose"] > 1:
|
||||
logSys.exception(e)
|
||||
else:
|
||||
logSys.error(e)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
def configureServer(self, nonsync=True, phase=None, stream=None):
|
||||
# if asynchronous start this operation in the new thread:
|
||||
if nonsync:
|
||||
if phase is not None:
|
||||
# event for server ready flag:
|
||||
def _server_ready():
|
||||
phase['start-ready'] = True
|
||||
logSys.log(5, ' server phase %s', phase)
|
||||
# notify waiting thread if server really ready
|
||||
self._conf['onstart'] = _server_ready
|
||||
th = Thread(target=Fail2banClient.configureServer, args=(self, False, phase, stream))
|
||||
th.daemon = True
|
||||
th.start()
|
||||
# if we need to read configuration stream:
|
||||
if stream is None and phase is not None:
|
||||
# wait, do not continue if configuration is not 100% valid:
|
||||
Utils.wait_for(lambda: phase.get('ready', None) is not None, self._conf["timeout"], 0.001)
|
||||
logSys.log(5, ' server phase %s', phase)
|
||||
if not phase.get('start', False):
|
||||
raise ServerExecutionException('Async configuration of server failed')
|
||||
return True
|
||||
# prepare: read config, check configuration is valid, etc.:
|
||||
if phase is not None:
|
||||
phase['start'] = True
|
||||
logSys.log(5, ' client phase %s', phase)
|
||||
if stream is None:
|
||||
stream = self.__prepareStartServer()
|
||||
if phase is not None:
|
||||
phase['ready'] = phase['start'] = (True if stream else False)
|
||||
logSys.log(5, ' client phase %s', phase)
|
||||
if not stream:
|
||||
return False
|
||||
# wait a little bit for phase "start-ready" before enter active waiting:
|
||||
if phase is not None:
|
||||
Utils.wait_for(lambda: phase.get('start-ready', None) is not None, 0.5, 0.001)
|
||||
phase['configure'] = (True if stream else False)
|
||||
logSys.log(5, ' client phase %s', phase)
|
||||
# configure server with config stream:
|
||||
ret = self.__processStartStreamAfterWait(stream, False)
|
||||
if phase is not None:
|
||||
phase['done'] = ret
|
||||
return ret
|
||||
|
||||
##
|
||||
# Process a command line.
|
||||
#
|
||||
# Process one command line and exit.
|
||||
# @param cmd the command line
|
||||
|
||||
def __processCommand(self, cmd):
|
||||
# wrap tuple to list (because could be modified here):
|
||||
if not isinstance(cmd, list):
|
||||
cmd = list(cmd)
|
||||
# process:
|
||||
if len(cmd) == 1 and cmd[0] == "start":
|
||||
|
||||
ret = self.__startServer(self._conf["background"])
|
||||
if not ret:
|
||||
return False
|
||||
return ret
|
||||
|
||||
elif len(cmd) >= 1 and cmd[0] == "restart":
|
||||
# if restart jail - re-operate via "reload --restart ...":
|
||||
if len(cmd) > 1:
|
||||
cmd[0:1] = ["reload", "--restart"]
|
||||
return self.__processCommand(cmd)
|
||||
# restart server:
|
||||
if self._conf.get("interactive", False):
|
||||
output(' ## stop ... ')
|
||||
self.__processCommand(['stop'])
|
||||
if not self.__waitOnServer(False): # pragma: no cover
|
||||
logSys.error("Could not stop server")
|
||||
return False
|
||||
# in interactive mode reset config, to make full-reload if there something changed:
|
||||
if self._conf.get("interactive", False):
|
||||
output(' ## load configuration ... ')
|
||||
self.resetConf()
|
||||
ret = self.initCmdLine(self._argv)
|
||||
if ret is not None:
|
||||
return ret
|
||||
if self._conf.get("interactive", False):
|
||||
output(' ## start ... ')
|
||||
return self.__processCommand(['start'])
|
||||
|
||||
elif len(cmd) >= 1 and cmd[0] == "reload":
|
||||
# reload options:
|
||||
opts = []
|
||||
while len(cmd) >= 2:
|
||||
if cmd[1] in ('--restart', "--unban", "--if-exists"):
|
||||
opts.append(cmd[1])
|
||||
del cmd[1]
|
||||
else:
|
||||
if len(cmd) > 2:
|
||||
logSys.error("Unexpected argument(s) for reload: %r", cmd[1:])
|
||||
return False
|
||||
# stop options - jail name or --all
|
||||
break
|
||||
if self.__ping(timeout=-1):
|
||||
if len(cmd) == 1 or cmd[1] == '--all':
|
||||
jail = '--all'
|
||||
ret, stream = self.readConfig()
|
||||
else:
|
||||
jail = cmd[1]
|
||||
ret, stream = self.readConfig(jail)
|
||||
# Do not continue if configuration is not 100% valid
|
||||
if not ret:
|
||||
return False
|
||||
if self._conf.get("interactive", False):
|
||||
output(' ## reload ... ')
|
||||
# Reconfigure the server
|
||||
return self.__processCmd([['reload', jail, opts, stream]], True)
|
||||
else:
|
||||
logSys.error("Could not find server")
|
||||
return False
|
||||
|
||||
elif len(cmd) > 1 and cmd[0] == "ping":
|
||||
return self.__processCmd([cmd], timeout=float(cmd[1]))
|
||||
|
||||
else:
|
||||
return self.__processCmd([cmd])
|
||||
|
||||
|
||||
def __processStartStreamAfterWait(self, *args):
|
||||
ret = False
|
||||
try:
|
||||
# Wait for the server to start
|
||||
if not self.__waitOnServer(): # pragma: no cover
|
||||
logSys.error("Could not find server, waiting failed")
|
||||
return False
|
||||
# Configure the server
|
||||
ret = self.__processCmd(*args)
|
||||
except ServerExecutionException as e: # pragma: no cover
|
||||
if self._conf["verbose"] > 1:
|
||||
logSys.exception(e)
|
||||
logSys.error("Could not start server. Maybe an old "
|
||||
"socket file is still present. Try to "
|
||||
"remove " + self._conf["socket"] + ". If "
|
||||
"you used fail2ban-client to start the "
|
||||
"server, adding the -x option will do it")
|
||||
|
||||
if not ret and self._server: # stop on error (foreground, config read in another thread):
|
||||
self._server.quit()
|
||||
self._server = None
|
||||
return ret
|
||||
|
||||
def __waitOnServer(self, alive=True, maxtime=None):
|
||||
if maxtime is None:
|
||||
maxtime = self._conf["timeout"]
|
||||
# Wait for the server to start (the server has 30 seconds to answer ping)
|
||||
starttime = time.time()
|
||||
logSys.log(5, "__waitOnServer: %r", (alive, maxtime))
|
||||
sltime = 0.0125 / 2
|
||||
test = lambda: os.path.exists(self._conf["socket"]) and self.__ping(timeout=sltime)
|
||||
with VisualWait(self._conf["verbose"]) as vis:
|
||||
while self._alive:
|
||||
runf = test()
|
||||
if runf == alive:
|
||||
return True
|
||||
waittime = time.time() - starttime
|
||||
logSys.log(5, " wait-time: %s", waittime)
|
||||
# Wonderful visual :)
|
||||
if waittime > 1:
|
||||
vis.heartbeat()
|
||||
# f end time reached:
|
||||
if waittime >= maxtime:
|
||||
raise ServerExecutionException("Failed to start server")
|
||||
# first 200ms faster:
|
||||
sltime = min(sltime * 2, 0.5 if waittime > 0.2 else 0.1)
|
||||
time.sleep(sltime)
|
||||
return False
|
||||
|
||||
def start(self, argv):
|
||||
# Install signal handlers
|
||||
_prev_signals = {}
|
||||
if _thread_name() == '_MainThread':
|
||||
for s in (signal.SIGTERM, signal.SIGINT):
|
||||
_prev_signals[s] = signal.getsignal(s)
|
||||
signal.signal(s, self.__sigTERMhandler)
|
||||
try:
|
||||
# Command line options
|
||||
if self._argv is None:
|
||||
ret = self.initCmdLine(argv)
|
||||
if ret is not None:
|
||||
if ret:
|
||||
return True
|
||||
if self._conf.get("test", False) and not self._args: # test only
|
||||
return False
|
||||
raise ServerExecutionException("Init of command line failed")
|
||||
|
||||
# Commands
|
||||
args = self._args
|
||||
|
||||
# Interactive mode
|
||||
if self._conf.get("interactive", False):
|
||||
try:
|
||||
import readline
|
||||
except ImportError:
|
||||
raise ServerExecutionException("Readline not available")
|
||||
try:
|
||||
ret = True
|
||||
if len(args) > 0:
|
||||
ret = self.__processCommand(args)
|
||||
if ret:
|
||||
readline.parse_and_bind("tab: complete")
|
||||
self.dispInteractive()
|
||||
while True:
|
||||
cmd = input_command()
|
||||
if cmd == "exit" or cmd == "quit":
|
||||
# Exit
|
||||
return True
|
||||
if cmd == "help":
|
||||
self.dispUsage()
|
||||
elif not cmd == "":
|
||||
try:
|
||||
self.__processCommand(shlex.split(cmd))
|
||||
except Exception as e: # pragma: no cover
|
||||
if self._conf["verbose"] > 1:
|
||||
logSys.exception(e)
|
||||
else:
|
||||
logSys.error(e)
|
||||
except (EOFError, KeyboardInterrupt): # pragma: no cover
|
||||
output("")
|
||||
raise
|
||||
# Single command mode
|
||||
else:
|
||||
if len(args) < 1:
|
||||
self.dispUsage()
|
||||
return False
|
||||
return self.__processCommand(args)
|
||||
except Exception as e:
|
||||
if self._conf["verbose"] > 1:
|
||||
logSys.exception(e)
|
||||
else:
|
||||
logSys.error(e)
|
||||
return False
|
||||
finally:
|
||||
self._alive = False
|
||||
for s, sh in _prev_signals.items():
|
||||
signal.signal(s, sh)
|
||||
|
||||
|
||||
class _VisualWait:
|
||||
"""Small progress indication (as "wonderful visual") during waiting process
|
||||
"""
|
||||
pos = 0
|
||||
delta = 1
|
||||
def __init__(self, maxpos=10):
|
||||
self.maxpos = maxpos
|
||||
def __enter__(self):
|
||||
return self
|
||||
def __exit__(self, *args):
|
||||
if self.pos:
|
||||
sys.stdout.write('\r'+(' '*(35+self.maxpos))+'\r')
|
||||
sys.stdout.flush()
|
||||
def heartbeat(self):
|
||||
"""Show or step for progress indicator
|
||||
"""
|
||||
if not self.pos:
|
||||
sys.stdout.write("\nINFO [#" + (' '*self.maxpos) + "] Waiting on the server...\r\x1b[8C")
|
||||
self.pos += self.delta
|
||||
if self.delta > 0:
|
||||
s = " #\x1b[1D" if self.pos > 1 else "# \x1b[2D"
|
||||
else:
|
||||
s = "\x1b[1D# \x1b[2D"
|
||||
sys.stdout.write(s)
|
||||
sys.stdout.flush()
|
||||
if self.pos > self.maxpos:
|
||||
self.delta = -1
|
||||
elif self.pos < 2:
|
||||
self.delta = 1
|
||||
class _NotVisualWait:
|
||||
"""Mockup for invisible progress indication (not verbose)
|
||||
"""
|
||||
def __enter__(self):
|
||||
return self
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
def heartbeat(self):
|
||||
pass
|
||||
|
||||
def VisualWait(verbose, *args, **kwargs):
|
||||
"""Wonderful visual progress indication (if verbose)
|
||||
"""
|
||||
return _VisualWait(*args, **kwargs) if verbose > 1 else _NotVisualWait()
|
||||
|
||||
|
||||
def exec_command_line(argv):
|
||||
client = Fail2banClient()
|
||||
# Exit with correct return value
|
||||
if client.start(argv):
|
||||
exit(0)
|
||||
else:
|
||||
exit(255)
|
||||
|
||||
349
fail2ban-master/fail2ban/client/fail2bancmdline.py
Normal file
349
fail2ban-master/fail2ban/client/fail2bancmdline.py
Normal file
@@ -0,0 +1,349 @@
|
||||
# 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
|
||||
|
||||
# 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()
|
||||
# exit after test if no commands specified (test only):
|
||||
if not len(self._args):
|
||||
if ret:
|
||||
output("OK: configuration test is successful")
|
||||
else:
|
||||
output("ERROR: test configuration failed")
|
||||
return ret
|
||||
if not ret:
|
||||
raise ServerExecutionException("ERROR: test configuration failed")
|
||||
|
||||
# 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
|
||||
85
fail2ban-master/fail2ban/client/fail2banreader.py
Normal file
85
fail2ban-master/fail2ban/client/fail2banreader.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# 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: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
from .configreader import ConfigReader
|
||||
from ..helpers import getLogger, str2LogLevel
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
|
||||
class Fail2banReader(ConfigReader):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
ConfigReader.__init__(self, **kwargs)
|
||||
|
||||
def read(self):
|
||||
return ConfigReader.read(self, "fail2ban")
|
||||
|
||||
def getEarlyOptions(self):
|
||||
opts = [
|
||||
["string", "socket", "/var/run/fail2ban/fail2ban.sock"],
|
||||
["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"],
|
||||
["string", "loglevel", "INFO"],
|
||||
["string", "logtarget", "/var/log/fail2ban.log"],
|
||||
["string", "syslogsocket", "auto"]
|
||||
]
|
||||
return ConfigReader.getOptions(self, "Definition", opts)
|
||||
|
||||
def getOptions(self, updateMainOpt=None):
|
||||
opts = [["string", "loglevel", "INFO" ],
|
||||
["string", "logtarget", "STDERR"],
|
||||
["string", "syslogsocket", "auto"],
|
||||
["string", "allowipv6", "auto"],
|
||||
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
|
||||
["int", "dbmaxmatches", None],
|
||||
["string", "dbpurgeage", "1d"]]
|
||||
self.__opts = ConfigReader.getOptions(self, "Definition", opts)
|
||||
if updateMainOpt:
|
||||
self.__opts.update(updateMainOpt)
|
||||
# check given log-level:
|
||||
str2LogLevel(self.__opts.get('loglevel', 0))
|
||||
# thread options:
|
||||
opts = [["int", "stacksize", ],
|
||||
]
|
||||
if self.has_section("Thread"):
|
||||
thopt = ConfigReader.getOptions(self, "Thread", opts)
|
||||
if thopt:
|
||||
self.__opts['thread'] = thopt
|
||||
|
||||
def convert(self):
|
||||
# Ensure logtarget/level set first so any db errors are captured
|
||||
# Also dbfile should be set before all other database options.
|
||||
# So adding order indices into items, to be stripped after sorting, upon return
|
||||
order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13,
|
||||
"allowipv6": 14,
|
||||
"dbfile":50, "dbmaxmatches":51, "dbpurgeage":51}
|
||||
stream = list()
|
||||
for opt in self.__opts:
|
||||
if opt in order:
|
||||
stream.append((order[opt], ["set", opt, self.__opts[opt]]))
|
||||
return [opt[1] for opt in sorted(stream)]
|
||||
|
||||
899
fail2ban-master/fail2ban/client/fail2banregex.py
Normal file
899
fail2ban-master/fail2ban/client/fail2banregex.py
Normal file
@@ -0,0 +1,899 @@
|
||||
# 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 can test regular expressions for "fail2ban".
|
||||
"""
|
||||
|
||||
__author__ = "Fail2Ban Developers"
|
||||
__copyright__ = """Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors
|
||||
Copyright of modifications held by their respective authors.
|
||||
Licensed under the GNU General Public License v2 (GPL).
|
||||
|
||||
Written by Cyril Jaquier <cyril.jaquier@fail2ban.org>.
|
||||
Many contributions by Yaroslav O. Halchenko, Steven Hiscocks, Sergey G. Brester (sebres)."""
|
||||
|
||||
__license__ = "GPL"
|
||||
|
||||
import getopt
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import time
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
from optparse import OptionParser, Option
|
||||
|
||||
from configparser import NoOptionError, NoSectionError, MissingSectionHeaderError
|
||||
|
||||
try: # pragma: no cover
|
||||
from ..server.filtersystemd import FilterSystemd
|
||||
except ImportError:
|
||||
FilterSystemd = None
|
||||
|
||||
from ..version import version, normVersion
|
||||
from .jailreader import FilterReader, JailReader, NoJailError
|
||||
from ..server.filter import Filter, FileContainer, MyTime
|
||||
from ..server.failregex import Regex, RegexException
|
||||
|
||||
from ..helpers import str2LogLevel, getVerbosityFormat, FormatterWithTraceBack, getLogger, \
|
||||
extractOptions, PREFER_ENC
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger("fail2ban")
|
||||
|
||||
def debuggexURL(sample, regex, multiline=False, useDns="yes"):
|
||||
args = {
|
||||
're': Regex._resolveHostTag(regex, useDns=useDns),
|
||||
'str': sample,
|
||||
'flavor': 'python'
|
||||
}
|
||||
if multiline: args['flags'] = 'm'
|
||||
return 'https://www.debuggex.com/?' + urllib.parse.urlencode(args)
|
||||
|
||||
def output(args): # pragma: no cover (overridden in test-cases)
|
||||
print(args)
|
||||
|
||||
def shortstr(s, l=53):
|
||||
"""Return shortened string
|
||||
"""
|
||||
if len(s) > l:
|
||||
return s[:l-3] + '...'
|
||||
return s
|
||||
|
||||
def pprint_list(l, header=None):
|
||||
if not len(l):
|
||||
return
|
||||
if header:
|
||||
s = "|- %s\n" % header
|
||||
else:
|
||||
s = ''
|
||||
output( s + "| " + "\n| ".join(l) + '\n`-' )
|
||||
|
||||
def journal_lines_gen(flt, myjournal): # pragma: no cover
|
||||
while True:
|
||||
try:
|
||||
entry = myjournal.get_next()
|
||||
except OSError:
|
||||
continue
|
||||
if not entry:
|
||||
break
|
||||
yield flt.formatJournalEntry(entry)
|
||||
|
||||
def dumpNormVersion(*args):
|
||||
output(normVersion())
|
||||
sys.exit(0)
|
||||
|
||||
usage = lambda: "%s [OPTIONS] <LOG> <REGEX> [IGNOREREGEX]" % sys.argv[0]
|
||||
|
||||
class _f2bOptParser(OptionParser):
|
||||
def format_help(self, *args, **kwargs):
|
||||
""" Overwritten format helper with full usage."""
|
||||
self.usage = ''
|
||||
return "Usage: " + usage() + "\n" + __doc__ + """
|
||||
LOG:
|
||||
string a string representing a log line
|
||||
filename path to a log file (/var/log/auth.log)
|
||||
systemd-journal search systemd journal (systemd-python required),
|
||||
optionally with backend parameters, see `man jail.conf`
|
||||
for usage and examples (systemd-journal[journalflags=1]).
|
||||
|
||||
REGEX:
|
||||
string a string representing a 'failregex'
|
||||
filter name of jail or filter, optionally with options (sshd[mode=aggressive])
|
||||
filename path to a filter file (filter.d/sshd.conf)
|
||||
|
||||
IGNOREREGEX:
|
||||
string a string representing an 'ignoreregex'
|
||||
\n""" + OptionParser.format_help(self, *args, **kwargs) + """\n
|
||||
Report bugs to https://github.com/fail2ban/fail2ban/issues\n
|
||||
""" + __copyright__ + "\n"
|
||||
|
||||
|
||||
def get_opt_parser():
|
||||
# use module docstring for help output
|
||||
p = _f2bOptParser(
|
||||
usage=usage(),
|
||||
version="%prog " + version)
|
||||
|
||||
p.add_options([
|
||||
Option("-c", "--config", default='/etc/fail2ban',
|
||||
help="set alternate config directory"),
|
||||
Option("-d", "--datepattern",
|
||||
help="set custom pattern used to match date/times"),
|
||||
Option("--timezone", "--TZ", action='store', default=None,
|
||||
help="set time-zone used by convert time format"),
|
||||
Option("-e", "--encoding", default=PREFER_ENC,
|
||||
help="File encoding. Default: system locale"),
|
||||
Option("-r", "--raw", action='store_true', default=False,
|
||||
help="Raw hosts, don't resolve dns"),
|
||||
Option("--usedns", action='store', default=None,
|
||||
help="DNS specified replacement of tags <HOST> in regexp "
|
||||
"('yes' - matches all form of hosts, 'no' - IP addresses only)"),
|
||||
Option("-L", "--maxlines", type=int, default=0,
|
||||
help="maxlines for multi-line regex."),
|
||||
Option("-m", "--journalmatch",
|
||||
help="journalctl style matches overriding filter file. "
|
||||
"\"systemd-journal\" only"),
|
||||
Option('-l', "--log-level",
|
||||
dest="log_level",
|
||||
default='critical',
|
||||
help="Log level for the Fail2Ban logger to use"),
|
||||
Option('-V', action="callback", callback=dumpNormVersion,
|
||||
help="get version in machine-readable short format"),
|
||||
Option('-v', '--verbose', action="count", dest="verbose",
|
||||
default=0,
|
||||
help="Increase verbosity"),
|
||||
Option("--verbosity", action="store", dest="verbose", type=int,
|
||||
help="Set numerical level of verbosity (0..4)"),
|
||||
Option("--verbose-date", "--VD", action='store_true',
|
||||
help="Verbose date patterns/regex in output"),
|
||||
Option("-D", "--debuggex", action='store_true',
|
||||
help="Produce debuggex.com urls for debugging there"),
|
||||
Option("--no-check-all", action="store_false", dest="checkAllRegex", default=True,
|
||||
help="Disable check for all regex's"),
|
||||
Option("-o", "--out", action="store", dest="out", default=None,
|
||||
help="Set token to print failure information only (row, id, ip, msg, host, ip4, ip6, dns, matches, ...)"),
|
||||
Option("-i", "--invert", action="store_true", dest="invert",
|
||||
help="Invert the sense of matching, to output non-matching lines."),
|
||||
Option("--print-no-missed", action='store_true',
|
||||
help="Do not print any missed lines"),
|
||||
Option("--print-no-ignored", action='store_true',
|
||||
help="Do not print any ignored lines"),
|
||||
Option("--print-all-matched", action='store_true',
|
||||
help="Print all matched lines"),
|
||||
Option("--print-all-missed", action='store_true',
|
||||
help="Print all missed lines, no matter how many"),
|
||||
Option("--print-all-ignored", action='store_true',
|
||||
help="Print all ignored lines, no matter how many"),
|
||||
Option("-t", "--log-traceback", action='store_true',
|
||||
help="Enrich log-messages with compressed tracebacks"),
|
||||
Option("--full-traceback", action='store_true',
|
||||
help="Either to make the tracebacks full, not compressed (as by default)"),
|
||||
])
|
||||
|
||||
return p
|
||||
|
||||
|
||||
class RegexStat(object):
|
||||
|
||||
def __init__(self, failregex):
|
||||
self._stats = 0
|
||||
self._failregex = failregex
|
||||
self._ipList = list()
|
||||
|
||||
def __str__(self):
|
||||
return "%s(%r) %d failed: %s" \
|
||||
% (self.__class__, self._failregex, self._stats, self._ipList)
|
||||
|
||||
def inc(self):
|
||||
self._stats += 1
|
||||
|
||||
def getStats(self):
|
||||
return self._stats
|
||||
|
||||
def getFailRegex(self):
|
||||
return self._failregex
|
||||
|
||||
def appendIP(self, value):
|
||||
self._ipList.append(value)
|
||||
|
||||
def getIPList(self):
|
||||
return self._ipList
|
||||
|
||||
|
||||
class LineStats(object):
|
||||
"""Just a convenience container for stats
|
||||
"""
|
||||
def __init__(self, opts):
|
||||
self.tested = self.matched = 0
|
||||
self.matched_lines = []
|
||||
self.missed = 0
|
||||
self.missed_lines = []
|
||||
self.ignored = 0
|
||||
self.ignored_lines = []
|
||||
if opts.debuggex:
|
||||
self.matched_lines_timeextracted = []
|
||||
self.missed_lines_timeextracted = []
|
||||
self.ignored_lines_timeextracted = []
|
||||
|
||||
def __str__(self):
|
||||
return "%(tested)d lines, %(ignored)d ignored, %(matched)d matched, %(missed)d missed" % self
|
||||
|
||||
# just for convenient str
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key) if hasattr(self, key) else ''
|
||||
|
||||
|
||||
class Fail2banRegex(object):
|
||||
|
||||
def __init__(self, opts):
|
||||
# set local protected members from given options:
|
||||
self.__dict__.update(dict(('_'+o,v) for o,v in opts.__dict__.items()))
|
||||
self._opts = opts
|
||||
self._maxlines_set = False # so we allow to override maxlines in cmdline
|
||||
self._datepattern_set = False
|
||||
self._journalmatch = None
|
||||
|
||||
self.share_config=dict()
|
||||
self._filter = Filter(None)
|
||||
self._prefREMatched = 0
|
||||
self._prefREGroups = list()
|
||||
self._ignoreregex = list()
|
||||
self._failregex = list()
|
||||
self._time_elapsed = None
|
||||
self._line_stats = LineStats(opts)
|
||||
|
||||
if opts.maxlines:
|
||||
self.setMaxLines(opts.maxlines)
|
||||
else:
|
||||
self._maxlines = 20
|
||||
if opts.journalmatch is not None:
|
||||
self.setJournalMatch(shlex.split(opts.journalmatch))
|
||||
if opts.timezone:
|
||||
self._filter.setLogTimeZone(opts.timezone)
|
||||
self._filter.checkFindTime = False
|
||||
if True: # not opts.out:
|
||||
MyTime.setAlternateNow(0); # accept every date (years from 19xx up to end of current century, '%ExY' and 'Exy' patterns)
|
||||
from ..server.strptime import _updateTimeRE
|
||||
_updateTimeRE()
|
||||
if opts.datepattern:
|
||||
self.setDatePattern(opts.datepattern)
|
||||
if opts.usedns:
|
||||
self._filter.setUseDns(opts.usedns)
|
||||
self._filter.returnRawHost = opts.raw
|
||||
self._filter.checkAllRegex = opts.checkAllRegex and not opts.out
|
||||
# ignore pending (without ID/IP), added to matches if it hits later (if ID/IP can be retrieved)
|
||||
self._filter.ignorePending = bool(opts.out)
|
||||
# callback to increment ignored RE's by index (during process):
|
||||
self._filter.onIgnoreRegex = self._onIgnoreRegex
|
||||
self._backend = 'auto'
|
||||
|
||||
def output(self, line):
|
||||
if not self._opts.out: output(line)
|
||||
|
||||
def encode_line(self, line):
|
||||
return line.encode(self._encoding, 'ignore')
|
||||
|
||||
def setDatePattern(self, pattern):
|
||||
if not self._datepattern_set:
|
||||
self._filter.setDatePattern(pattern)
|
||||
self._datepattern_set = True
|
||||
if pattern is not None:
|
||||
self.output( "Use datepattern : %s : %s" % (
|
||||
pattern, self._filter.getDatePattern()[1], ) )
|
||||
|
||||
def setMaxLines(self, v):
|
||||
if not self._maxlines_set:
|
||||
self._filter.setMaxLines(int(v))
|
||||
self._maxlines_set = True
|
||||
self.output( "Use maxlines : %d" % self._filter.getMaxLines() )
|
||||
|
||||
def setJournalMatch(self, v):
|
||||
self._journalmatch = v
|
||||
|
||||
def _dumpRealOptions(self, reader, fltOpt):
|
||||
realopts = {}
|
||||
combopts = reader.getCombined()
|
||||
if isinstance(reader, FilterReader):
|
||||
_get_opt = lambda k: reader.get('Definition', k)
|
||||
elif reader.filter: # JailReader for jail with filter:
|
||||
_get_opt = lambda k: reader.filter.get('Definition', k)
|
||||
else: # JailReader for jail without filter:
|
||||
_get_opt = lambda k: None
|
||||
# output all options that are specified in filter-argument as well as some special (mostly interested):
|
||||
for k in ['logtype', 'datepattern'] + list(fltOpt.keys()):
|
||||
# combined options win, but they contain only a sub-set in filter expected keys,
|
||||
# so get the rest from definition section:
|
||||
try:
|
||||
realopts[k] = combopts[k] if k in combopts else _get_opt(k)
|
||||
except NoOptionError: # pragma: no cover
|
||||
pass
|
||||
self.output("Real filter options : %r" % realopts)
|
||||
|
||||
def readRegex(self, value, regextype):
|
||||
assert(regextype in ('fail', 'ignore'))
|
||||
regex = regextype + 'regex'
|
||||
# try to check - we've case filter?[options...]?:
|
||||
basedir = self._opts.config
|
||||
fltName = value
|
||||
fltFile = None
|
||||
fltOpt = {}
|
||||
jail = None
|
||||
if regextype == 'fail':
|
||||
if re.search(r'(?ms)^/{0,3}[\w/_\-.]+(?:\[.*\])?$', value):
|
||||
try:
|
||||
fltName, fltOpt = extractOptions(value)
|
||||
if not re.search(r'(?ms)(?:/|\.(?:conf|local)$)', fltName): # name of jail?
|
||||
try:
|
||||
jail = JailReader(fltName, force_enable=True,
|
||||
share_config=self.share_config, basedir=basedir)
|
||||
jail.read()
|
||||
except NoJailError:
|
||||
jail = None
|
||||
if "." in fltName[~5:]:
|
||||
tryNames = (fltName,)
|
||||
else:
|
||||
tryNames = (fltName, fltName + '.conf', fltName + '.local')
|
||||
for fltFile in tryNames:
|
||||
if os.path.dirname(fltFile) == 'filter.d':
|
||||
fltFile = os.path.join(basedir, fltFile)
|
||||
elif not "/" in fltFile:
|
||||
if os.path.basename(basedir) == 'filter.d':
|
||||
fltFile = os.path.join(basedir, fltFile)
|
||||
else:
|
||||
fltFile = os.path.join(basedir, 'filter.d', fltFile)
|
||||
else:
|
||||
basedir = os.path.dirname(fltFile)
|
||||
if os.path.isfile(fltFile):
|
||||
break
|
||||
fltFile = None
|
||||
except Exception as e:
|
||||
output("ERROR: Wrong filter name or options: %s" % (str(e),))
|
||||
output(" while parsing: %s" % (value,))
|
||||
if self._verbose: raise(e)
|
||||
return False
|
||||
elif self._ignoreregex:
|
||||
# clear ignoreregex that could be previously loaded from filter:
|
||||
self._filter.delIgnoreRegex()
|
||||
|
||||
readercommands = None
|
||||
# if it is jail:
|
||||
if jail:
|
||||
self.output( "Use %11s jail : %s" % ('', fltName) )
|
||||
if fltOpt:
|
||||
self.output( "Use jail/flt options : %r" % fltOpt )
|
||||
if not fltOpt: fltOpt = {}
|
||||
fltOpt['backend'] = self._backend
|
||||
ret = jail.getOptions(addOpts=fltOpt)
|
||||
if not ret:
|
||||
output('ERROR: Failed to get jail for %r' % (value,))
|
||||
return False
|
||||
# show real options if expected:
|
||||
if self._verbose > 1 or logSys.getEffectiveLevel()<=logging.DEBUG:
|
||||
self._dumpRealOptions(jail, fltOpt)
|
||||
readercommands = jail.convert(allow_no_files=True)
|
||||
# if it is filter file:
|
||||
elif fltFile is not None:
|
||||
if (basedir == self._opts.config
|
||||
or os.path.basename(basedir) == 'filter.d'
|
||||
or ("." not in fltName[~5:] and "/" not in fltName)
|
||||
):
|
||||
## within filter.d folder - use standard loading algorithm to load filter completely (with .local etc.):
|
||||
if os.path.basename(basedir) == 'filter.d':
|
||||
basedir = os.path.dirname(basedir)
|
||||
fltName = os.path.splitext(os.path.basename(fltName))[0]
|
||||
self.output( "Use %11s file : %s, basedir: %s" % ('filter', fltName, basedir) )
|
||||
else:
|
||||
## foreign file - readexplicit this file and includes if possible:
|
||||
self.output( "Use %11s file : %s" % ('filter', fltName) )
|
||||
basedir = None
|
||||
if not os.path.isabs(fltName): # avoid join with "filter.d" inside FilterReader
|
||||
fltName = os.path.abspath(fltName)
|
||||
if fltOpt:
|
||||
self.output( "Use filter options : %r" % fltOpt )
|
||||
reader = FilterReader(fltName, 'fail2ban-regex-jail', fltOpt,
|
||||
share_config=self.share_config, basedir=basedir)
|
||||
ret = None
|
||||
try:
|
||||
if basedir is not None:
|
||||
ret = reader.read()
|
||||
else:
|
||||
## foreign file - readexplicit this file and includes if possible:
|
||||
reader.setBaseDir(None)
|
||||
ret = reader.readexplicit()
|
||||
except Exception as e:
|
||||
output("Wrong config file: %s" % (str(e),))
|
||||
if self._verbose: raise(e)
|
||||
if not ret:
|
||||
output( "ERROR: failed to load filter %s" % value )
|
||||
return False
|
||||
# set backend-related options (logtype):
|
||||
reader.applyAutoOptions(self._backend)
|
||||
# get, interpolate and convert options:
|
||||
reader.getOptions(None)
|
||||
# show real options if expected:
|
||||
if self._verbose > 1 or logSys.getEffectiveLevel()<=logging.DEBUG:
|
||||
self._dumpRealOptions(reader, fltOpt)
|
||||
# to stream:
|
||||
readercommands = reader.convert()
|
||||
|
||||
regex_values = {}
|
||||
if readercommands:
|
||||
for opt in readercommands:
|
||||
if opt[0] == 'multi-set':
|
||||
optval = opt[3]
|
||||
elif opt[0] == 'set':
|
||||
optval = opt[3:]
|
||||
else: # pragma: no cover
|
||||
continue
|
||||
try:
|
||||
if opt[2] == "prefregex":
|
||||
for optval in optval:
|
||||
self._filter.prefRegex = optval
|
||||
elif opt[2] == "addfailregex":
|
||||
stor = regex_values.get('fail')
|
||||
if not stor: stor = regex_values['fail'] = list()
|
||||
for optval in optval:
|
||||
stor.append(RegexStat(optval))
|
||||
#self._filter.addFailRegex(optval)
|
||||
elif opt[2] == "addignoreregex":
|
||||
stor = regex_values.get('ignore')
|
||||
if not stor: stor = regex_values['ignore'] = list()
|
||||
for optval in optval:
|
||||
stor.append(RegexStat(optval))
|
||||
#self._filter.addIgnoreRegex(optval)
|
||||
elif opt[2] == "maxlines":
|
||||
for optval in optval:
|
||||
self.setMaxLines(optval)
|
||||
elif opt[2] == "datepattern":
|
||||
for optval in optval:
|
||||
self.setDatePattern(optval)
|
||||
elif opt[2] == "addjournalmatch": # pragma: no cover
|
||||
if self._opts.journalmatch is None:
|
||||
self.setJournalMatch(optval)
|
||||
except ValueError as e: # pragma: no cover
|
||||
output( "ERROR: Invalid value for %s (%r) " \
|
||||
"read from %s: %s" % (opt[2], optval, value, e) )
|
||||
return False
|
||||
|
||||
else:
|
||||
self.output( "Use %11s line : %s" % (regex, shortstr(value)) )
|
||||
regex_values[regextype] = [RegexStat(value)]
|
||||
|
||||
for regextype, regex_values in regex_values.items():
|
||||
regex = regextype + 'regex'
|
||||
setattr(self, "_" + regex, regex_values)
|
||||
for regex in regex_values:
|
||||
getattr(
|
||||
self._filter,
|
||||
'add%sRegex' % regextype.title())(regex.getFailRegex())
|
||||
return True
|
||||
|
||||
def _onIgnoreRegex(self, idx, ignoreRegex):
|
||||
self._lineIgnored = True
|
||||
self._ignoreregex[idx].inc()
|
||||
|
||||
def testRegex(self, line, date=None):
|
||||
orgLineBuffer = self._filter._Filter__lineBuffer
|
||||
# duplicate line buffer (list can be changed inplace during processLine):
|
||||
if self._filter.getMaxLines() > 1:
|
||||
orgLineBuffer = orgLineBuffer[:]
|
||||
fullBuffer = len(orgLineBuffer) >= self._filter.getMaxLines()
|
||||
is_ignored = self._lineIgnored = False
|
||||
try:
|
||||
found = self._filter.processLine(line, date)
|
||||
lines = []
|
||||
ret = []
|
||||
for match in found:
|
||||
if not self._opts.out:
|
||||
# Append True/False flag depending if line was matched by
|
||||
# more than one regex
|
||||
match.append(len(ret)>1)
|
||||
regex = self._failregex[match[0]]
|
||||
regex.inc()
|
||||
regex.appendIP(match)
|
||||
if not match[3].get('nofail'):
|
||||
ret.append(match)
|
||||
else:
|
||||
is_ignored = True
|
||||
if self._opts.out: # (formatted) output - don't need stats:
|
||||
return None, ret, None
|
||||
# prefregex stats:
|
||||
if self._filter.prefRegex:
|
||||
pre = self._filter.prefRegex
|
||||
if pre.hasMatched():
|
||||
self._prefREMatched += 1
|
||||
if self._verbose:
|
||||
if len(self._prefREGroups) < self._maxlines:
|
||||
self._prefREGroups.append(pre.getGroups())
|
||||
else:
|
||||
if len(self._prefREGroups) == self._maxlines:
|
||||
self._prefREGroups.append('...')
|
||||
except RegexException as e: # pragma: no cover
|
||||
output( 'ERROR: %s' % e )
|
||||
return None, 0, None
|
||||
if self._filter.getMaxLines() > 1 and not self._opts.out:
|
||||
for bufLine in orgLineBuffer[int(fullBuffer):]:
|
||||
if bufLine not in self._filter._Filter__lineBuffer:
|
||||
try:
|
||||
self._line_stats.missed_lines.pop(
|
||||
self._line_stats.missed_lines.index("".join(bufLine)))
|
||||
if self._debuggex:
|
||||
self._line_stats.missed_lines_timeextracted.pop(
|
||||
self._line_stats.missed_lines_timeextracted.index(
|
||||
"".join(bufLine[::2])))
|
||||
except ValueError:
|
||||
pass
|
||||
# if buffering - add also another lines from match:
|
||||
if self._print_all_matched:
|
||||
if not self._debuggex:
|
||||
self._line_stats.matched_lines.append("".join(bufLine))
|
||||
else:
|
||||
lines.append(bufLine[0] + bufLine[2])
|
||||
self._line_stats.matched += 1
|
||||
self._line_stats.missed -= 1
|
||||
if lines: # pre-lines parsed in multiline mode (buffering)
|
||||
lines.append(self._filter.processedLine())
|
||||
line = "\n".join(lines)
|
||||
return line, ret, (is_ignored or self._lineIgnored)
|
||||
|
||||
def _prepaireOutput(self):
|
||||
"""Prepares output- and fetch-function corresponding given '--out' option (format)"""
|
||||
ofmt = self._opts.out
|
||||
if ofmt in ('id', 'fid'):
|
||||
def _out(ret):
|
||||
for r in ret:
|
||||
output(r[1])
|
||||
elif ofmt == 'ip':
|
||||
def _out(ret):
|
||||
for r in ret:
|
||||
output(r[3].get('ip', r[1]))
|
||||
elif ofmt == 'msg':
|
||||
def _out(ret):
|
||||
for r in ret:
|
||||
for r in r[3].get('matches'):
|
||||
if not isinstance(r, str):
|
||||
r = ''.join(r for r in r)
|
||||
output(r)
|
||||
elif ofmt == 'row':
|
||||
def _out(ret):
|
||||
for r in ret:
|
||||
output('[%r,\t%r,\t%r],' % (r[1],r[2],dict((k,v) for k, v in r[3].items() if k != 'matches')))
|
||||
elif '<' not in ofmt:
|
||||
def _out(ret):
|
||||
for r in ret:
|
||||
output(r[3].get(ofmt))
|
||||
else: # extended format with tags substitution:
|
||||
from ..server.actions import Actions, CommandAction, BanTicket
|
||||
def _escOut(t, v):
|
||||
# use safe escape (avoid inject on pseudo tag "\x00msg\x00"):
|
||||
if t not in ('msg',):
|
||||
return v.replace('\x00', '\\x00')
|
||||
return v
|
||||
def _out(ret):
|
||||
rows = []
|
||||
wrap = {'NL':0}
|
||||
for r in ret:
|
||||
ticket = BanTicket(r[1], time=r[2], data=r[3])
|
||||
aInfo = Actions.ActionInfo(ticket)
|
||||
# if msg tag is used - output if single line (otherwise let it as is to wrap multilines later):
|
||||
def _get_msg(self):
|
||||
if not wrap['NL'] and len(r[3].get('matches', [])) <= 1:
|
||||
return self['matches']
|
||||
else: # pseudo tag for future replacement:
|
||||
wrap['NL'] = 1
|
||||
return "\x00msg\x00"
|
||||
aInfo['msg'] = _get_msg
|
||||
# not recursive interpolation (use safe escape):
|
||||
v = CommandAction.replaceDynamicTags(ofmt, aInfo, escapeVal=_escOut)
|
||||
if wrap['NL']: # contains multiline tags (msg):
|
||||
rows.append((r, v))
|
||||
continue
|
||||
output(v)
|
||||
# wrap multiline tag (msg) interpolations to single line:
|
||||
for r, v in rows:
|
||||
for r in r[3].get('matches'):
|
||||
if not isinstance(r, str):
|
||||
r = ''.join(r for r in r)
|
||||
r = v.replace("\x00msg\x00", r)
|
||||
output(r)
|
||||
return _out
|
||||
|
||||
|
||||
def process(self, test_lines):
|
||||
t0 = time.time()
|
||||
out = None
|
||||
if self._opts.out: # get out function
|
||||
out = self._prepaireOutput()
|
||||
outinv = self._opts.invert
|
||||
for line in test_lines:
|
||||
if isinstance(line, tuple):
|
||||
line_datetimestripped, ret, is_ignored = self.testRegex(line[0], line[1])
|
||||
line = "".join(line[0])
|
||||
else:
|
||||
line = line.rstrip('\r\n')
|
||||
if line.startswith('#') or not line:
|
||||
# skip comment and empty lines
|
||||
continue
|
||||
line_datetimestripped, ret, is_ignored = self.testRegex(line)
|
||||
|
||||
if out: # (formatted) output:
|
||||
if len(ret) > 0 and not is_ignored:
|
||||
if not outinv: out(ret)
|
||||
elif outinv: # inverted output (currently only time and message as matches):
|
||||
if not len(ret): # [failRegexIndex, fid, date, fail]
|
||||
ret = [[-1, "", self._filter._Filter__lastDate, {"fid":"", "matches":[line]}]]
|
||||
out(ret)
|
||||
continue
|
||||
|
||||
if is_ignored:
|
||||
self._line_stats.ignored += 1
|
||||
if not self._print_no_ignored and (self._print_all_ignored or self._line_stats.ignored <= self._maxlines + 1):
|
||||
self._line_stats.ignored_lines.append(line)
|
||||
if self._debuggex:
|
||||
self._line_stats.ignored_lines_timeextracted.append(line_datetimestripped)
|
||||
elif len(ret) > 0:
|
||||
self._line_stats.matched += 1
|
||||
if self._print_all_matched:
|
||||
self._line_stats.matched_lines.append(line)
|
||||
if self._debuggex:
|
||||
self._line_stats.matched_lines_timeextracted.append(line_datetimestripped)
|
||||
else:
|
||||
self._line_stats.missed += 1
|
||||
if not self._print_no_missed and (self._print_all_missed or self._line_stats.missed <= self._maxlines + 1):
|
||||
self._line_stats.missed_lines.append(line)
|
||||
if self._debuggex:
|
||||
self._line_stats.missed_lines_timeextracted.append(line_datetimestripped)
|
||||
self._line_stats.tested += 1
|
||||
|
||||
self._time_elapsed = time.time() - t0
|
||||
|
||||
def printLines(self, ltype):
|
||||
lstats = self._line_stats
|
||||
assert(lstats.missed == lstats.tested - (lstats.matched + lstats.ignored))
|
||||
lines = lstats[ltype]
|
||||
l = lstats[ltype + '_lines']
|
||||
multiline = self._filter.getMaxLines() > 1
|
||||
if lines:
|
||||
header = "%s line(s):" % (ltype.capitalize(),)
|
||||
if self._debuggex:
|
||||
if ltype == 'missed' or ltype == 'matched':
|
||||
regexlist = self._failregex
|
||||
else:
|
||||
regexlist = self._ignoreregex
|
||||
l = lstats[ltype + '_lines_timeextracted']
|
||||
if lines < self._maxlines or getattr(self, '_print_all_' + ltype):
|
||||
ans = [[]]
|
||||
for arg in [l, regexlist]:
|
||||
ans = [ x + [y] for x in ans for y in arg ]
|
||||
b = [a[0] + ' | ' + a[1].getFailRegex() + ' | ' +
|
||||
debuggexURL(self.encode_line(a[0]), a[1].getFailRegex(),
|
||||
multiline, self._opts.usedns) for a in ans]
|
||||
pprint_list([x.rstrip() for x in b], header)
|
||||
else:
|
||||
output( "%s too many to print. Use --print-all-%s " \
|
||||
"to print all %d lines" % (header, ltype, lines) )
|
||||
elif lines < self._maxlines or getattr(self, '_print_all_' + ltype):
|
||||
pprint_list([x.rstrip() for x in l], header)
|
||||
else:
|
||||
output( "%s too many to print. Use --print-all-%s " \
|
||||
"to print all %d lines" % (header, ltype, lines) )
|
||||
|
||||
def printStats(self):
|
||||
if self._opts.out: return True
|
||||
output( "" )
|
||||
output( "Results" )
|
||||
output( "=======" )
|
||||
|
||||
def print_failregexes(title, failregexes):
|
||||
# Print title
|
||||
total, out = 0, []
|
||||
for cnt, failregex in enumerate(failregexes):
|
||||
match = failregex.getStats()
|
||||
total += match
|
||||
if (match or self._verbose):
|
||||
out.append("%2d) [%d] %s" % (cnt+1, match, failregex.getFailRegex()))
|
||||
|
||||
if self._verbose and len(failregex.getIPList()):
|
||||
for ip in failregex.getIPList():
|
||||
timeTuple = time.localtime(ip[2])
|
||||
timeString = time.strftime("%a %b %d %H:%M:%S %Y", timeTuple)
|
||||
out.append(
|
||||
" %s %s%s" % (
|
||||
ip[1],
|
||||
timeString,
|
||||
ip[-1] and " (multiple regex matched)" or ""))
|
||||
|
||||
output( "\n%s: %d total" % (title, total) )
|
||||
pprint_list(out, " #) [# of hits] regular expression")
|
||||
return total
|
||||
|
||||
# Print prefregex:
|
||||
if self._filter.prefRegex:
|
||||
#self._filter.prefRegex.hasMatched()
|
||||
pre = self._filter.prefRegex
|
||||
out = [pre.getRegex()]
|
||||
if self._verbose:
|
||||
for grp in self._prefREGroups:
|
||||
out.append(" %s" % (grp,))
|
||||
output( "\n%s: %d total" % ("Prefregex", self._prefREMatched) )
|
||||
pprint_list(out)
|
||||
|
||||
# Print regex's:
|
||||
total = print_failregexes("Failregex", self._failregex)
|
||||
_ = print_failregexes("Ignoreregex", self._ignoreregex)
|
||||
|
||||
|
||||
if self._filter.dateDetector is not None:
|
||||
output( "\nDate template hits:" )
|
||||
out = []
|
||||
for template in self._filter.dateDetector.templates:
|
||||
if self._verbose or template.hits:
|
||||
out.append("[%d] %s" % (template.hits, template.name))
|
||||
if self._verbose_date:
|
||||
out.append(" # weight: %.3f (%.3f), pattern: %s" % (
|
||||
template.weight, template.template.weight,
|
||||
getattr(template, 'pattern', ''),))
|
||||
out.append(" # regex: %s" % (getattr(template, 'regex', ''),))
|
||||
pprint_list(out, "[# of hits] date format")
|
||||
|
||||
output( "\nLines: %s" % self._line_stats, )
|
||||
if self._time_elapsed is not None:
|
||||
output( "[processed in %.2f sec]" % self._time_elapsed, )
|
||||
output( "" )
|
||||
|
||||
if self._print_all_matched:
|
||||
self.printLines('matched')
|
||||
if not self._print_no_ignored:
|
||||
self.printLines('ignored')
|
||||
if not self._print_no_missed:
|
||||
self.printLines('missed')
|
||||
|
||||
return True
|
||||
|
||||
def start(self, args):
|
||||
|
||||
cmd_log, cmd_regex = args[:2]
|
||||
|
||||
if cmd_log.startswith("systemd-journal"): # pragma: no cover
|
||||
self._backend = 'systemd'
|
||||
|
||||
try:
|
||||
if not self.readRegex(cmd_regex, 'fail'): # pragma: no cover
|
||||
return False
|
||||
if len(args) == 3 and not self.readRegex(args[2], 'ignore'): # pragma: no cover
|
||||
return False
|
||||
except RegexException as e:
|
||||
output( 'ERROR: %s' % e )
|
||||
return False
|
||||
|
||||
if os.path.isfile(cmd_log):
|
||||
try:
|
||||
test_lines = FileContainer(cmd_log, self._encoding, doOpen=True)
|
||||
|
||||
self.output( "Use log file : %s" % cmd_log )
|
||||
self.output( "Use encoding : %s" % self._encoding )
|
||||
except IOError as e: # pragma: no cover
|
||||
output( e )
|
||||
return False
|
||||
elif cmd_log.startswith("systemd-journal"): # pragma: no cover
|
||||
if not FilterSystemd:
|
||||
output( "Error: systemd library not found. Exiting..." )
|
||||
return False
|
||||
self.output( "Use systemd journal" )
|
||||
self.output( "Use encoding : %s" % self._encoding )
|
||||
backend, beArgs = extractOptions(cmd_log)
|
||||
flt = FilterSystemd(None, **beArgs)
|
||||
flt.setLogEncoding(self._encoding)
|
||||
myjournal = flt.getJournalReader()
|
||||
journalmatch = self._journalmatch
|
||||
self.setDatePattern(None)
|
||||
if journalmatch:
|
||||
flt.addJournalMatch(journalmatch)
|
||||
self.output( "Use journal match : %s" % " ".join(journalmatch) )
|
||||
test_lines = journal_lines_gen(flt, myjournal)
|
||||
else:
|
||||
# if single line parsing (without buffering)
|
||||
if self._filter.getMaxLines() <= 1 and '\n' not in cmd_log:
|
||||
self.output( "Use single line : %s" % shortstr(cmd_log.replace("\n", r"\n")) )
|
||||
test_lines = [ cmd_log ]
|
||||
else: # multi line parsing (with and without buffering)
|
||||
test_lines = cmd_log.split("\n")
|
||||
self.output( "Use multi line : %s line(s)" % len(test_lines) )
|
||||
for i, l in enumerate(test_lines):
|
||||
if i >= 5:
|
||||
self.output( "| ..." ); break
|
||||
self.output( "| %2.2s: %s" % (i+1, shortstr(l)) )
|
||||
self.output( "`-" )
|
||||
|
||||
self.output( "" )
|
||||
|
||||
self.process(test_lines)
|
||||
|
||||
if not self.printStats():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _loc_except_hook(exctype, value, traceback):
|
||||
if (exctype != BrokenPipeError and exctype != IOError or value.errno != 32):
|
||||
return sys.__excepthook__(exctype, value, traceback)
|
||||
# pipe seems to be closed (head / tail / etc), thus simply exit:
|
||||
sys.exit(0)
|
||||
|
||||
def exec_command_line(*args):
|
||||
sys.excepthook = _loc_except_hook; # stop on closed/broken pipe
|
||||
|
||||
logging.exitOnIOError = True
|
||||
parser = get_opt_parser()
|
||||
(opts, args) = parser.parse_args(*args)
|
||||
errors = []
|
||||
if opts.print_no_missed and opts.print_all_missed: # pragma: no cover
|
||||
errors.append("ERROR: --print-no-missed and --print-all-missed are mutually exclusive.")
|
||||
if opts.print_no_ignored and opts.print_all_ignored: # pragma: no cover
|
||||
errors.append("ERROR: --print-no-ignored and --print-all-ignored are mutually exclusive.")
|
||||
|
||||
# We need 2 or 3 parameters
|
||||
if not len(args) in (2, 3):
|
||||
errors.append("ERROR: provide both <LOG> and <REGEX>.")
|
||||
if errors:
|
||||
parser.print_help()
|
||||
sys.stderr.write("\n" + "\n".join(errors) + "\n")
|
||||
sys.exit(255)
|
||||
|
||||
if not opts.out:
|
||||
output( "" )
|
||||
output( "Running tests" )
|
||||
output( "=============" )
|
||||
output( "" )
|
||||
|
||||
# Log level (default critical):
|
||||
opts.log_level = str2LogLevel(opts.log_level)
|
||||
logSys.setLevel(opts.log_level)
|
||||
|
||||
# Add the default logging handler
|
||||
stdout = logging.StreamHandler(sys.stdout)
|
||||
|
||||
fmt = '%(levelname)-1.1s: %(message)s' if opts.verbose <= 1 else ' %(message)s'
|
||||
|
||||
if opts.log_traceback:
|
||||
Formatter = FormatterWithTraceBack
|
||||
fmt = (opts.full_traceback and ' %(tb)s' or ' %(tbc)s') + fmt
|
||||
else:
|
||||
Formatter = logging.Formatter
|
||||
|
||||
# Custom log format for the verbose tests runs
|
||||
stdout.setFormatter(Formatter(getVerbosityFormat(opts.verbose, fmt)))
|
||||
logSys.addHandler(stdout)
|
||||
|
||||
try:
|
||||
fail2banRegex = Fail2banRegex(opts)
|
||||
except Exception as e:
|
||||
if opts.verbose or logSys.getEffectiveLevel()<=logging.DEBUG:
|
||||
logSys.critical(e, exc_info=True)
|
||||
else:
|
||||
output( 'ERROR: %s' % e )
|
||||
sys.exit(255)
|
||||
|
||||
if not fail2banRegex.start(args):
|
||||
sys.exit(255)
|
||||
237
fail2ban-master/fail2ban/client/fail2banserver.py
Normal file
237
fail2ban-master/fail2ban/client/fail2banserver.py
Normal file
@@ -0,0 +1,237 @@
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
#
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
__author__ = "Fail2Ban Developers"
|
||||
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
|
||||
__license__ = "GPL"
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, \
|
||||
logSys, PRODUCTION, exit
|
||||
|
||||
SERVER = "fail2ban-server"
|
||||
|
||||
##
|
||||
# \mainpage Fail2Ban
|
||||
#
|
||||
# \section Introduction
|
||||
#
|
||||
class Fail2banServer(Fail2banCmdLine):
|
||||
|
||||
# def __init__(self):
|
||||
# Fail2banCmdLine.__init__(self)
|
||||
|
||||
##
|
||||
# Start Fail2Ban server in main thread without fork (direct, it can fork itself in Server if daemon=True).
|
||||
#
|
||||
# Start the Fail2ban server in background/foreground (daemon mode or not).
|
||||
|
||||
@staticmethod
|
||||
def startServerDirect(conf, daemon=True, setServer=None):
|
||||
logSys.debug(" direct starting of server in %s, daemon: %s", os.getpid(), daemon)
|
||||
from ..server.server import Server
|
||||
server = None
|
||||
try:
|
||||
# Start it in foreground (current thread, not new process),
|
||||
# server object will internally fork self if daemon is True
|
||||
server = Server(daemon)
|
||||
# notify caller - set server handle:
|
||||
if setServer:
|
||||
setServer(server)
|
||||
# run:
|
||||
server.start(conf["socket"],
|
||||
conf["pidfile"], conf["force"],
|
||||
conf=conf)
|
||||
except Exception as e: # pragma: no cover
|
||||
try:
|
||||
if server:
|
||||
server.quit()
|
||||
except Exception as e2:
|
||||
if conf["verbose"] > 1:
|
||||
logSys.exception(e2)
|
||||
raise
|
||||
finally:
|
||||
# notify waiting thread server ready resp. done (background execution, error case, etc):
|
||||
if conf.get('onstart'):
|
||||
conf['onstart']()
|
||||
|
||||
return server
|
||||
|
||||
##
|
||||
# Start Fail2Ban server.
|
||||
#
|
||||
# Start the Fail2ban server in daemon mode (background, start from client).
|
||||
|
||||
@staticmethod
|
||||
def startServerAsync(conf):
|
||||
# Forks the current process, don't fork if async specified (ex: test cases)
|
||||
pid = 0
|
||||
frk = not conf["async"] and PRODUCTION
|
||||
if frk: # pragma: no cover
|
||||
pid = os.fork()
|
||||
logSys.debug(" async starting of server in %s, fork: %s - %s", os.getpid(), frk, pid)
|
||||
if pid == 0:
|
||||
args = list()
|
||||
args.append(SERVER)
|
||||
# Start async (don't read config) and in background as requested.
|
||||
args.append("--async")
|
||||
args.append("-b")
|
||||
# Set the socket path.
|
||||
args.append("-s")
|
||||
args.append(conf["socket"])
|
||||
# Set the pidfile
|
||||
args.append("-p")
|
||||
args.append(conf["pidfile"])
|
||||
# Force the execution if needed.
|
||||
if conf["force"]:
|
||||
args.append("-x")
|
||||
if conf["verbose"] > 1:
|
||||
args.append("-" + "v"*(conf["verbose"]-1))
|
||||
# Logging parameters:
|
||||
for o in ('loglevel', 'logtarget', 'syslogsocket'):
|
||||
args.append("--"+o)
|
||||
args.append(conf[o])
|
||||
try:
|
||||
# Directory of client (to try the first start from current or the same directory as client, and from relative bin):
|
||||
exe = Fail2banServer.getServerPath()
|
||||
if not frk:
|
||||
# Wrapr args to use the same python version in client/server (important for multi-python systems):
|
||||
args[0] = exe
|
||||
exe = sys.executable
|
||||
args[0:0] = [exe]
|
||||
logSys.debug("Starting %r with args %r", exe, args)
|
||||
if frk: # pragma: no cover
|
||||
os.execv(exe, args)
|
||||
else:
|
||||
# use P_WAIT instead of P_NOWAIT (to prevent defunct-zomby process), it started as daemon, so parent exit fast after fork):
|
||||
ret = os.spawnv(os.P_WAIT, exe, args)
|
||||
if ret != 0: # pragma: no cover
|
||||
raise OSError(ret, "Unknown error by executing server %r with %r" % (args[1], exe))
|
||||
except OSError as e: # pragma: no cover
|
||||
if not frk: #not PRODUCTION:
|
||||
raise
|
||||
# Use the PATH env.
|
||||
logSys.warning("Initial start attempt failed (%s). Starting %r with the same args", e, SERVER)
|
||||
if frk: # pragma: no cover
|
||||
os.execvp(SERVER, args)
|
||||
|
||||
@staticmethod
|
||||
def getServerPath():
|
||||
startdir = sys.path[0]
|
||||
exe = os.path.abspath(os.path.join(startdir, SERVER))
|
||||
if not os.path.isfile(exe): # may be unresolved in test-cases, so get relative starter (client):
|
||||
startdir = os.path.dirname(sys.argv[0])
|
||||
exe = os.path.abspath(os.path.join(startdir, SERVER))
|
||||
if not os.path.isfile(exe): # may be unresolved in test-cases, so try to get relative bin-directory:
|
||||
startdir = os.path.dirname(os.path.abspath(__file__))
|
||||
startdir = os.path.join(os.path.dirname(os.path.dirname(startdir)), "bin")
|
||||
exe = os.path.abspath(os.path.join(startdir, SERVER))
|
||||
return exe
|
||||
|
||||
def _Fail2banClient(self):
|
||||
from .fail2banclient import Fail2banClient
|
||||
cli = Fail2banClient()
|
||||
cli.applyMembers(self)
|
||||
return cli
|
||||
|
||||
def start(self, argv):
|
||||
server = None
|
||||
try:
|
||||
# Command line options
|
||||
ret = self.initCmdLine(argv)
|
||||
if ret is not None:
|
||||
return ret
|
||||
|
||||
# Commands
|
||||
args = self._args
|
||||
|
||||
cli = None
|
||||
# Just start:
|
||||
if len(args) == 1 and args[0] == 'start' and not self._conf.get("interactive", False):
|
||||
pass
|
||||
else:
|
||||
# If client mode - whole processing over client:
|
||||
if len(args) or self._conf.get("interactive", False):
|
||||
cli = self._Fail2banClient()
|
||||
return cli.start(argv)
|
||||
|
||||
# Start the server, corresponding options:
|
||||
# background = True, if should be new process running in background, otherwise start in
|
||||
# foreground process will be forked in daemonize, inside of Server module.
|
||||
# nonsync = True, normally internal call only, if started from client, so configures
|
||||
# the server via asynchronous thread.
|
||||
background = self._conf["background"]
|
||||
nonsync = self._conf.get("async", False)
|
||||
|
||||
# If was started not from the client:
|
||||
if not nonsync:
|
||||
# Load requirements on demand (we need utils only when asynchronous handling):
|
||||
from ..server.utils import Utils
|
||||
# Start new thread with client to read configuration and
|
||||
# transfer it to the server:
|
||||
cli = self._Fail2banClient()
|
||||
cli._conf = self._conf
|
||||
phase = dict()
|
||||
logSys.debug('Configure via async client thread')
|
||||
cli.configureServer(phase=phase)
|
||||
|
||||
# Start server, daemonize it, etc.
|
||||
pid = os.getpid()
|
||||
server = Fail2banServer.startServerDirect(self._conf, background,
|
||||
cli._set_server if cli else None)
|
||||
# If forked - just exit other processes
|
||||
if pid != os.getpid(): # pragma: no cover
|
||||
os._exit(0)
|
||||
if cli:
|
||||
cli._server = server
|
||||
|
||||
# wait for client answer "done":
|
||||
if not nonsync and cli:
|
||||
Utils.wait_for(lambda: phase.get('done', None) is not None, self._conf["timeout"], 0.001)
|
||||
if not phase.get('done', False):
|
||||
if server: # pragma: no cover
|
||||
server.quit()
|
||||
exit(255)
|
||||
if background:
|
||||
logSys.debug('Starting server done')
|
||||
|
||||
except Exception as e:
|
||||
if self._conf["verbose"] > 1:
|
||||
logSys.exception(e)
|
||||
else:
|
||||
logSys.error(e)
|
||||
if server: # pragma: no cover
|
||||
server.quit()
|
||||
exit(255)
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def exit(code=0): # pragma: no cover
|
||||
if code != 0:
|
||||
logSys.error("Could not start %s", SERVER)
|
||||
exit(code)
|
||||
|
||||
def exec_command_line(argv):
|
||||
server = Fail2banServer()
|
||||
if server.start(argv):
|
||||
exit(0)
|
||||
else:
|
||||
exit(255)
|
||||
100
fail2ban-master/fail2ban/client/filterreader.py
Normal file
100
fail2ban-master/fail2ban/client/filterreader.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# 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: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import os
|
||||
import shlex
|
||||
|
||||
from .configreader import DefinitionInitConfigReader
|
||||
from ..helpers import getLogger
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
|
||||
class FilterReader(DefinitionInitConfigReader):
|
||||
|
||||
_configOpts = {
|
||||
"usedns": ["string", None],
|
||||
"prefregex": ["string", None],
|
||||
"ignoreregex": ["string", None],
|
||||
"failregex": ["string", None],
|
||||
"maxlines": ["int", None],
|
||||
"datepattern": ["string", None],
|
||||
"journalmatch": ["string", None],
|
||||
}
|
||||
|
||||
def setFile(self, fileName):
|
||||
self.__file = fileName
|
||||
DefinitionInitConfigReader.setFile(self, os.path.join("filter.d", fileName))
|
||||
|
||||
def getFile(self):
|
||||
return self.__file
|
||||
|
||||
def applyAutoOptions(self, backend):
|
||||
# set init option to backend-related logtype, considering
|
||||
# that the filter settings may be overwritten in its local:
|
||||
if (not self._initOpts.get('logtype') and
|
||||
not self.has_option('Definition', 'logtype', False)
|
||||
):
|
||||
self._initOpts['logtype'] = ['file','journal'][int(backend.startswith("systemd"))]
|
||||
|
||||
def convert(self):
|
||||
stream = list()
|
||||
opts = self.getCombined()
|
||||
if not len(opts):
|
||||
return stream
|
||||
return FilterReader._fillStream(stream, opts, self._jailName)
|
||||
|
||||
@staticmethod
|
||||
def _fillStream(stream, opts, jailName):
|
||||
prio0idx = 0
|
||||
for opt, value in opts.items():
|
||||
# Do not send a command if the value is not set (empty).
|
||||
if value is None: continue
|
||||
if opt in ("failregex", "ignoreregex"):
|
||||
multi = []
|
||||
for regex in value.split('\n'):
|
||||
# Do not send a command if the rule is empty.
|
||||
if regex != '':
|
||||
multi.append(regex)
|
||||
if len(multi) > 1:
|
||||
stream.append(["multi-set", jailName, "add" + opt, multi])
|
||||
elif len(multi):
|
||||
stream.append(["set", jailName, "add" + opt, multi[0]])
|
||||
elif opt in ('usedns', 'maxlines', 'prefregex'):
|
||||
# Be sure we set this options first, and usedns is before all regex(s).
|
||||
stream.insert(0 if opt == 'usedns' else prio0idx,
|
||||
["set", jailName, opt, value])
|
||||
prio0idx += 1
|
||||
elif opt == 'datepattern':
|
||||
stream.append(["set", jailName, opt, value])
|
||||
elif opt == 'journalmatch':
|
||||
for match in value.split("\n"):
|
||||
if match == '': continue
|
||||
stream.append(
|
||||
["set", jailName, "addjournalmatch"] + shlex.split(match))
|
||||
return stream
|
||||
|
||||
316
fail2ban-master/fail2ban/client/jailreader.py
Normal file
316
fail2ban-master/fail2ban/client/jailreader.py
Normal file
@@ -0,0 +1,316 @@
|
||||
# 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: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
|
||||
from .configreader import ConfigReaderUnshared, ConfigReader, NoSectionError
|
||||
from .filterreader import FilterReader
|
||||
from .actionreader import ActionReader
|
||||
from ..version import version
|
||||
from ..helpers import _merge_dicts, getLogger, extractOptions, splitWithOptions, splitwords
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
|
||||
class NoJailError(ValueError):
|
||||
pass
|
||||
|
||||
class JailReader(ConfigReader):
|
||||
|
||||
def __init__(self, name, force_enable=False, **kwargs):
|
||||
ConfigReader.__init__(self, **kwargs)
|
||||
self.__name = name
|
||||
self.__filter = None
|
||||
self.__force_enable = force_enable
|
||||
self.__actions = list()
|
||||
self.__opts = None
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return self.__opts
|
||||
|
||||
def setName(self, value):
|
||||
self.__name = value
|
||||
|
||||
def getName(self):
|
||||
return self.__name
|
||||
|
||||
def read(self):
|
||||
out = ConfigReader.read(self, "jail")
|
||||
# Before returning -- verify that requested section
|
||||
# exists at all
|
||||
if not (self.__name in self.sections()):
|
||||
raise NoJailError("Jail %r was not found among available"
|
||||
% self.__name)
|
||||
return out
|
||||
|
||||
def isEnabled(self):
|
||||
return self.__force_enable or (
|
||||
self.__opts and self.__opts.get("enabled", False))
|
||||
|
||||
@staticmethod
|
||||
def _glob(path):
|
||||
"""Given a path for glob return list of files to be passed to server.
|
||||
|
||||
Dangling symlinks are warned about and not returned
|
||||
"""
|
||||
pathList = []
|
||||
for p in glob.glob(path):
|
||||
if os.path.exists(p):
|
||||
pathList.append(p)
|
||||
else:
|
||||
logSys.warning("File %s is a dangling link, thus cannot be monitored" % p)
|
||||
return pathList
|
||||
|
||||
_configOpts1st = {
|
||||
"enabled": ["bool", False],
|
||||
"backend": ["string", "auto"],
|
||||
"filter": ["string", ""]
|
||||
}
|
||||
_configOpts = {
|
||||
"enabled": ["bool", False],
|
||||
"backend": ["string", "auto"],
|
||||
"maxretry": ["int", None],
|
||||
"maxmatches": ["int", None],
|
||||
"findtime": ["string", None],
|
||||
"bantime": ["string", None],
|
||||
"bantime.increment": ["bool", None],
|
||||
"bantime.factor": ["string", None],
|
||||
"bantime.formula": ["string", None],
|
||||
"bantime.multipliers": ["string", None],
|
||||
"bantime.maxtime": ["string", None],
|
||||
"bantime.rndtime": ["string", None],
|
||||
"bantime.overalljails": ["bool", None],
|
||||
"ignorecommand": ["string", None],
|
||||
"ignoreself": ["bool", None],
|
||||
"ignoreip": ["string", None],
|
||||
"ignorecache": ["string", None],
|
||||
"filter": ["string", ""],
|
||||
"logtimezone": ["string", None],
|
||||
"logencoding": ["string", None],
|
||||
"logpath": ["string", None],
|
||||
"skip_if_nologs": ["bool", False],
|
||||
"systemd_if_nologs": ["bool", True],
|
||||
"action": ["string", ""]
|
||||
}
|
||||
_configOpts.update(FilterReader._configOpts)
|
||||
|
||||
_ignoreOpts = set(
|
||||
['action', 'filter', 'enabled', 'backend', 'skip_if_nologs', 'systemd_if_nologs'] +
|
||||
list(FilterReader._configOpts.keys())
|
||||
)
|
||||
|
||||
def getOptions(self, addOpts=None):
|
||||
|
||||
basedir = self.getBaseDir()
|
||||
|
||||
# Before interpolation (substitution) add static options always available as default:
|
||||
self.merge_defaults({
|
||||
"fail2ban_version": version,
|
||||
"fail2ban_confpath": basedir
|
||||
})
|
||||
|
||||
try:
|
||||
|
||||
# Read first options only needed for merge defaults ('known/...' from filter):
|
||||
self.__opts = ConfigReader.getOptions(self, self.__name, self._configOpts1st,
|
||||
shouldExist=True)
|
||||
if not self.__opts: # pragma: no cover
|
||||
raise JailDefError("Init jail options failed")
|
||||
if addOpts:
|
||||
self.__opts = _merge_dicts(self.__opts, addOpts)
|
||||
|
||||
if not self.isEnabled():
|
||||
return True
|
||||
|
||||
# Read filter
|
||||
flt = self.__opts["filter"]
|
||||
if flt:
|
||||
try:
|
||||
filterName, filterOpt = extractOptions(flt)
|
||||
except ValueError as e:
|
||||
raise JailDefError("Invalid filter definition %r: %s" % (flt, e))
|
||||
if addOpts:
|
||||
filterOpt = _merge_dicts(filterOpt, addOpts)
|
||||
self.__filter = FilterReader(
|
||||
filterName, self.__name, filterOpt,
|
||||
share_config=self.share_config, basedir=basedir)
|
||||
ret = self.__filter.read()
|
||||
if not ret:
|
||||
raise JailDefError("Unable to read the filter %r" % filterName)
|
||||
# set backend-related options (logtype):
|
||||
self.__filter.applyAutoOptions(self.__opts.get('backend', ''))
|
||||
# merge options from filter as 'known/...' (all options unfiltered):
|
||||
self.__filter.getOptions(self.__opts, all=True)
|
||||
ConfigReader.merge_section(self, self.__name, self.__filter.getCombined(), 'known/')
|
||||
else:
|
||||
self.__filter = None
|
||||
logSys.warning("No filter set for jail %s" % self.__name)
|
||||
|
||||
# Read second all options (so variables like %(known/param) can be interpolated):
|
||||
self.__opts = ConfigReader.getOptions(self, self.__name, self._configOpts)
|
||||
if not self.__opts: # pragma: no cover
|
||||
raise JailDefError("Read jail options failed")
|
||||
|
||||
# cumulate filter options again (ignore given in jail):
|
||||
if self.__filter:
|
||||
self.__filter.getOptions(self.__opts)
|
||||
|
||||
# Read action
|
||||
for act in splitWithOptions(self.__opts["action"]):
|
||||
try:
|
||||
act = act.strip()
|
||||
if not act: # skip empty actions
|
||||
continue
|
||||
# join with previous line if needed (consider possible new-line):
|
||||
try:
|
||||
actName, actOpt = extractOptions(act)
|
||||
except ValueError as e:
|
||||
raise JailDefError("Invalid action definition %r: %s" % (act, e))
|
||||
if actName.endswith(".py"):
|
||||
self.__actions.append([
|
||||
"set",
|
||||
self.__name,
|
||||
"addaction",
|
||||
actOpt.pop("actname", os.path.splitext(actName)[0]),
|
||||
os.path.join(
|
||||
basedir, "action.d", actName),
|
||||
json.dumps(actOpt),
|
||||
])
|
||||
else:
|
||||
action = ActionReader(
|
||||
actName, self.__name, actOpt,
|
||||
share_config=self.share_config, basedir=basedir)
|
||||
ret = action.read()
|
||||
if ret:
|
||||
action.getOptions(self.__opts)
|
||||
self.__actions.append(action)
|
||||
else:
|
||||
raise JailDefError("Unable to read action %r" % actName)
|
||||
except JailDefError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logSys.debug("Caught exception: %s", e, exc_info=True)
|
||||
raise ValueError("Error in action definition %r: %r" % (act, e))
|
||||
if not len(self.__actions):
|
||||
logSys.warning("No actions were defined for %s" % self.__name)
|
||||
|
||||
except JailDefError as e:
|
||||
e = str(e)
|
||||
logSys.error(e)
|
||||
if not self.__opts:
|
||||
self.__opts = dict()
|
||||
self.__opts['config-error'] = e
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def filter(self):
|
||||
return self.__filter
|
||||
|
||||
def getCombined(self):
|
||||
if not self.__filter:
|
||||
return self.__opts
|
||||
return _merge_dicts(self.__opts, self.__filter.getCombined())
|
||||
|
||||
def convert(self, allow_no_files=False, systemd_if_nologs=True):
|
||||
"""Convert read before __opts to the commands stream
|
||||
|
||||
Parameters
|
||||
----------
|
||||
allow_missing : bool
|
||||
Either to allow log files to be missing entirely. Primarily is
|
||||
used for testing
|
||||
"""
|
||||
|
||||
stream = []
|
||||
stream2 = []
|
||||
e = self.__opts.get('config-error')
|
||||
if e:
|
||||
stream.extend([['config-error', "Jail '%s' skipped, because of wrong configuration: %s" % (self.__name, e)]])
|
||||
return stream
|
||||
# fill jail with filter options, using filter (only not overridden in jail):
|
||||
if self.__filter:
|
||||
stream.extend(self.__filter.convert())
|
||||
# and using options from jail:
|
||||
FilterReader._fillStream(stream, self.__opts, self.__name)
|
||||
backend = self.__opts.get('backend', 'auto')
|
||||
for opt, value in self.__opts.items():
|
||||
if opt == "logpath":
|
||||
if backend.startswith("systemd"): continue
|
||||
found_files = 0
|
||||
for path in value.split("\n"):
|
||||
path = path.rsplit(" ", 1)
|
||||
path, tail = path if len(path) > 1 else (path[0], "head")
|
||||
pathList = JailReader._glob(path)
|
||||
if len(pathList) == 0:
|
||||
logSys.notice("No file(s) found for glob %s" % path)
|
||||
for p in pathList:
|
||||
found_files += 1
|
||||
# logpath after all log-related data (backend, date-pattern, etc)
|
||||
stream2.append(
|
||||
["set", self.__name, "addlogpath", p, tail])
|
||||
if not found_files:
|
||||
msg = "Have not found any log file for '%s' jail." % self.__name
|
||||
skip_if_nologs = self.__opts.get('skip_if_nologs', False)
|
||||
# if auto and we can switch to systemd backend (only possible if jail have journalmatch):
|
||||
if backend.startswith("auto") and systemd_if_nologs and (
|
||||
self.__opts.get('systemd_if_nologs', True) and
|
||||
self.__opts.get('journalmatch', None) is not None
|
||||
):
|
||||
# switch backend to systemd:
|
||||
backend = 'systemd'
|
||||
msg += " Jail will monitor systemd journal."
|
||||
skip_if_nologs = False
|
||||
elif not allow_no_files and not skip_if_nologs:
|
||||
raise ValueError(msg)
|
||||
logSys.warning(msg)
|
||||
if skip_if_nologs:
|
||||
self.__opts['runtime-error'] = msg
|
||||
msg = "Jail '%s' skipped, because of missing log files." % (self.__name,)
|
||||
logSys.warning(msg)
|
||||
stream = [['config-error', msg]]
|
||||
return stream
|
||||
elif opt == "ignoreip":
|
||||
stream.append(["set", self.__name, "addignoreip"] + splitwords(value))
|
||||
elif opt not in JailReader._ignoreOpts:
|
||||
stream.append(["set", self.__name, opt, value])
|
||||
# consider options order (after other options):
|
||||
if stream2: stream += stream2
|
||||
for action in self.__actions:
|
||||
if isinstance(action, (ConfigReaderUnshared, ConfigReader)):
|
||||
stream.extend(action.convert())
|
||||
else:
|
||||
stream.append(action)
|
||||
stream.insert(0, ["add", self.__name, backend])
|
||||
return stream
|
||||
|
||||
class JailDefError(Exception):
|
||||
pass
|
||||
114
fail2ban-master/fail2ban/client/jailsreader.py
Normal file
114
fail2ban-master/fail2ban/client/jailsreader.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# 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: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
from .configreader import ConfigReader
|
||||
from .jailreader import JailReader
|
||||
from ..helpers import getLogger
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
|
||||
class JailsReader(ConfigReader):
|
||||
|
||||
def __init__(self, force_enable=False, **kwargs):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
force_enable : bool, optional
|
||||
Passed to JailReader to force enable the jails.
|
||||
It is for internal use
|
||||
"""
|
||||
ConfigReader.__init__(self, **kwargs)
|
||||
self.__jails = list()
|
||||
self.__force_enable = force_enable
|
||||
|
||||
@property
|
||||
def jails(self):
|
||||
return self.__jails
|
||||
|
||||
def read(self):
|
||||
self.__jails = list()
|
||||
return ConfigReader.read(self, "jail")
|
||||
|
||||
def getOptions(self, section=None, ignoreWrong=True):
|
||||
"""Reads configuration for jail(s) and adds enabled jails to __jails
|
||||
"""
|
||||
opts = []
|
||||
self.__opts = ConfigReader.getOptions(self, "Definition", opts)
|
||||
|
||||
if section is None:
|
||||
sections = self.sections()
|
||||
else:
|
||||
sections = [ section ]
|
||||
|
||||
# Get the options of all jails.
|
||||
parse_status = 0
|
||||
for sec in sections:
|
||||
if sec == 'INCLUDES':
|
||||
continue
|
||||
# use the cfg_share for filter/action caching and the same config for all
|
||||
# jails (use_config=...), therefore don't read it here:
|
||||
jail = JailReader(sec, force_enable=self.__force_enable,
|
||||
share_config=self.share_config, use_config=self._cfg)
|
||||
ret = jail.getOptions()
|
||||
if ret:
|
||||
if jail.isEnabled():
|
||||
# at least one jail was successful:
|
||||
parse_status |= 1
|
||||
# We only add enabled jails
|
||||
self.__jails.append(jail)
|
||||
else:
|
||||
logSys.error("Errors in jail %r.%s", sec, " Skipping..." if ignoreWrong else "")
|
||||
self.__jails.append(jail)
|
||||
# at least one jail was invalid:
|
||||
parse_status |= 2
|
||||
return ((ignoreWrong and parse_status & 1) or not (parse_status & 2))
|
||||
|
||||
def convert(self, allow_no_files=False, systemd_if_nologs=True):
|
||||
"""Convert read before __opts and jails to the commands stream
|
||||
|
||||
Parameters
|
||||
----------
|
||||
allow_missing : bool
|
||||
Either to allow log files to be missing entirely. Primarily is
|
||||
used for testing
|
||||
"""
|
||||
|
||||
stream = list()
|
||||
# Convert jails
|
||||
for jail in self.__jails:
|
||||
stream.extend(jail.convert(allow_no_files, systemd_if_nologs))
|
||||
# Start jails
|
||||
for jail in self.__jails:
|
||||
if not jail.options.get('config-error') and not jail.options.get('runtime-error'):
|
||||
stream.append(["start", jail.getName()])
|
||||
else:
|
||||
# just delete rtm-errors (to check next time if cached)
|
||||
jail.options.pop('runtime-error', None)
|
||||
|
||||
return stream
|
||||
|
||||
Reference in New Issue
Block a user