mirror of https://github.com/fail2ban/fail2ban
Merge pull request #1726 from sebres/0.10-grave-fix-escape-tags-1st
0.10 fix escape tagspull/1410/head
commit
1e6787877a
|
@ -32,7 +32,7 @@ from ..helpers import getLogger
|
||||||
if sys.version_info >= (3,2):
|
if sys.version_info >= (3,2):
|
||||||
|
|
||||||
# SafeConfigParser deprecated from Python 3.2 (renamed to ConfigParser)
|
# SafeConfigParser deprecated from Python 3.2 (renamed to ConfigParser)
|
||||||
from configparser import ConfigParser as SafeConfigParser, \
|
from configparser import ConfigParser as SafeConfigParser, NoSectionError, \
|
||||||
BasicInterpolation
|
BasicInterpolation
|
||||||
|
|
||||||
# And interpolation of __name__ was simply removed, thus we need to
|
# And interpolation of __name__ was simply removed, thus we need to
|
||||||
|
@ -60,7 +60,7 @@ if sys.version_info >= (3,2):
|
||||||
parser, option, accum, rest, section, map, depth)
|
parser, option, accum, rest, section, map, depth)
|
||||||
|
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
from ConfigParser import SafeConfigParser
|
from ConfigParser import SafeConfigParser, NoSectionError
|
||||||
|
|
||||||
# Gets the instance of the logger.
|
# Gets the instance of the logger.
|
||||||
logSys = getLogger(__name__)
|
logSys = getLogger(__name__)
|
||||||
|
@ -200,6 +200,21 @@ after = 1.conf
|
||||||
def get_sections(self):
|
def get_sections(self):
|
||||||
return self._sections
|
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:
|
||||||
|
raise NoSectionError(section)
|
||||||
|
if withDefault:
|
||||||
|
# mix it with defaults:
|
||||||
|
return set(opts.keys()) | set(self._defaults)
|
||||||
|
# only own option names:
|
||||||
|
return opts.keys()
|
||||||
|
|
||||||
def read(self, filenames, get_includes=True):
|
def read(self, filenames, get_includes=True):
|
||||||
if not isinstance(filenames, list):
|
if not isinstance(filenames, list):
|
||||||
filenames = [ filenames ]
|
filenames = [ filenames ]
|
||||||
|
|
|
@ -109,33 +109,44 @@ class ConfigReader():
|
||||||
self._cfg = ConfigReaderUnshared(**self._cfg_share_kwargs)
|
self._cfg = ConfigReaderUnshared(**self._cfg_share_kwargs)
|
||||||
|
|
||||||
def sections(self):
|
def sections(self):
|
||||||
if self._cfg is not None:
|
try:
|
||||||
return self._cfg.sections()
|
return self._cfg.sections()
|
||||||
|
except AttributeError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def has_section(self, sec):
|
def has_section(self, sec):
|
||||||
if self._cfg is not None:
|
try:
|
||||||
return self._cfg.has_section(sec)
|
return self._cfg.has_section(sec)
|
||||||
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def merge_section(self, *args, **kwargs):
|
def merge_section(self, section, *args, **kwargs):
|
||||||
if self._cfg is not None:
|
try:
|
||||||
return self._cfg.merge_section(*args, **kwargs)
|
return self._cfg.merge_section(section, *args, **kwargs)
|
||||||
|
except AttributeError:
|
||||||
|
raise NoSectionError(section)
|
||||||
|
|
||||||
def options(self, *args):
|
def options(self, section, withDefault=False):
|
||||||
if self._cfg is not None:
|
"""Return a list of option names for the given section name.
|
||||||
return self._cfg.options(*args)
|
|
||||||
return {}
|
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={}):
|
def get(self, sec, opt, raw=False, vars={}):
|
||||||
if self._cfg is not None:
|
try:
|
||||||
return self._cfg.get(sec, opt, raw=raw, vars=vars)
|
return self._cfg.get(sec, opt, raw=raw, vars=vars)
|
||||||
return None
|
except AttributeError:
|
||||||
|
raise NoSectionError(sec)
|
||||||
|
|
||||||
def getOptions(self, *args, **kwargs):
|
def getOptions(self, section, *args, **kwargs):
|
||||||
if self._cfg is not None:
|
try:
|
||||||
return self._cfg.getOptions(*args, **kwargs)
|
return self._cfg.getOptions(section, *args, **kwargs)
|
||||||
return {}
|
except AttributeError:
|
||||||
|
raise NoSectionError(section)
|
||||||
|
|
||||||
|
|
||||||
class ConfigReaderUnshared(SafeConfigParserWithIncludes):
|
class ConfigReaderUnshared(SafeConfigParserWithIncludes):
|
||||||
|
@ -297,23 +308,35 @@ class DefinitionInitConfigReader(ConfigReader):
|
||||||
self._create_unshared(self._file)
|
self._create_unshared(self._file)
|
||||||
return SafeConfigParserWithIncludes.read(self._cfg, self._file)
|
return SafeConfigParserWithIncludes.read(self._cfg, self._file)
|
||||||
|
|
||||||
def getOptions(self, pOpts):
|
def getOptions(self, pOpts, all=False):
|
||||||
# overwrite static definition options with init values, supplied as
|
# overwrite static definition options with init values, supplied as
|
||||||
# direct parameters from jail-config via action[xtra1="...", xtra2=...]:
|
# direct parameters from jail-config via action[xtra1="...", xtra2=...]:
|
||||||
if self._initOpts:
|
|
||||||
if not pOpts:
|
if not pOpts:
|
||||||
pOpts = dict()
|
pOpts = dict()
|
||||||
|
if self._initOpts:
|
||||||
pOpts = _merge_dicts(pOpts, self._initOpts)
|
pOpts = _merge_dicts(pOpts, self._initOpts)
|
||||||
self._opts = ConfigReader.getOptions(
|
self._opts = ConfigReader.getOptions(
|
||||||
self, "Definition", self._configOpts, pOpts)
|
self, "Definition", self._configOpts, pOpts)
|
||||||
self._pOpts = pOpts
|
self._pOpts = pOpts
|
||||||
if self.has_section("Init"):
|
if self.has_section("Init"):
|
||||||
for opt in self.options("Init"):
|
# get only own options (without options from default):
|
||||||
v = self.get("Init", opt)
|
getopt = lambda opt: self.get("Init", opt)
|
||||||
if not opt.startswith('known/') and opt != '__name__':
|
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
|
self._initOpts['known/'+opt] = v
|
||||||
if not opt in self._initOpts:
|
if opt not in self._initOpts:
|
||||||
|
if v is None: v = getopt(opt)
|
||||||
self._initOpts[opt] = v
|
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 _convert_to_boolean(self, value):
|
def _convert_to_boolean(self, value):
|
||||||
return value.lower() in ("1", "yes", "true", "on")
|
return value.lower() in ("1", "yes", "true", "on")
|
||||||
|
@ -336,12 +359,12 @@ class DefinitionInitConfigReader(ConfigReader):
|
||||||
|
|
||||||
def getCombined(self, ignore=()):
|
def getCombined(self, ignore=()):
|
||||||
combinedopts = self._opts
|
combinedopts = self._opts
|
||||||
ignore = set(ignore).copy()
|
|
||||||
if self._initOpts:
|
if self._initOpts:
|
||||||
combinedopts = _merge_dicts(self._opts, self._initOpts)
|
combinedopts = _merge_dicts(combinedopts, self._initOpts)
|
||||||
if not len(combinedopts):
|
if not len(combinedopts):
|
||||||
return {}
|
return {}
|
||||||
# ignore conditional options:
|
# ignore conditional options:
|
||||||
|
ignore = set(ignore).copy()
|
||||||
for n in combinedopts:
|
for n in combinedopts:
|
||||||
cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(n)
|
cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(n)
|
||||||
if cond:
|
if cond:
|
||||||
|
|
|
@ -139,11 +139,11 @@ class JailReader(ConfigReader):
|
||||||
filterName, self.__name, filterOpt,
|
filterName, self.__name, filterOpt,
|
||||||
share_config=self.share_config, basedir=self.getBaseDir())
|
share_config=self.share_config, basedir=self.getBaseDir())
|
||||||
ret = self.__filter.read()
|
ret = self.__filter.read()
|
||||||
# merge options from filter as 'known/...':
|
|
||||||
self.__filter.getOptions(self.__opts)
|
|
||||||
ConfigReader.merge_section(self, self.__name, self.__filter.getCombined(), 'known/')
|
|
||||||
if not ret:
|
if not ret:
|
||||||
raise JailDefError("Unable to read the filter %r" % filterName)
|
raise JailDefError("Unable to read the filter %r" % filterName)
|
||||||
|
# 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:
|
else:
|
||||||
self.__filter = None
|
self.__filter = None
|
||||||
logSys.warning("No filter set for jail %s" % self.__name)
|
logSys.warning("No filter set for jail %s" % self.__name)
|
||||||
|
|
|
@ -453,7 +453,7 @@ class CommandAction(ActionBase):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def replaceTag(cls, query, aInfo, conditional='', cache=None, substRec=True):
|
def replaceTag(cls, query, aInfo, conditional='', cache=None):
|
||||||
"""Replaces tags in `query` with property values.
|
"""Replaces tags in `query` with property values.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -481,9 +481,8 @@ class CommandAction(ActionBase):
|
||||||
# **Important**: don't replace if calling map - contains dynamic values only,
|
# **Important**: don't replace if calling map - contains dynamic values only,
|
||||||
# no recursive tags, otherwise may be vulnerable on foreign user-input:
|
# no recursive tags, otherwise may be vulnerable on foreign user-input:
|
||||||
noRecRepl = isinstance(aInfo, CallingMap)
|
noRecRepl = isinstance(aInfo, CallingMap)
|
||||||
if noRecRepl:
|
|
||||||
subInfo = aInfo
|
subInfo = aInfo
|
||||||
else:
|
if not noRecRepl:
|
||||||
# substitute tags recursive (and cache if possible),
|
# substitute tags recursive (and cache if possible),
|
||||||
# first try get cached tags dictionary:
|
# first try get cached tags dictionary:
|
||||||
subInfo = csubkey = None
|
subInfo = csubkey = None
|
||||||
|
@ -534,13 +533,86 @@ class CommandAction(ActionBase):
|
||||||
"unexpected too long replacement interpolation, "
|
"unexpected too long replacement interpolation, "
|
||||||
"possible self referencing definitions in query: %s" % (query,))
|
"possible self referencing definitions in query: %s" % (query,))
|
||||||
|
|
||||||
|
|
||||||
# cache if possible:
|
# cache if possible:
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
cache[ckey] = value
|
cache[ckey] = value
|
||||||
#
|
#
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
ESCAPE_CRE = re.compile(r"""[\\#&;`|*?~<>\^\(\)\[\]{}$'"\n\r]""")
|
||||||
|
ESCAPE_VN_CRE = re.compile(r"\W")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def replaceDynamicTags(cls, realCmd, aInfo):
|
||||||
|
"""Replaces dynamical tags in `query` with property values.
|
||||||
|
|
||||||
|
**Important**
|
||||||
|
-------------
|
||||||
|
Because this tags are dynamic resp. foreign (user) input:
|
||||||
|
- values should be escaped (using "escape" as shell variable)
|
||||||
|
- no recursive substitution (no interpolation for <a<b>>)
|
||||||
|
- don't use cache
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
query : str
|
||||||
|
String with tags.
|
||||||
|
aInfo : dict
|
||||||
|
Tags(keys) and associated values for substitution in query.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
shell script as string or array with tags replaced (direct or as variables).
|
||||||
|
"""
|
||||||
|
# array for escaped vars:
|
||||||
|
varsDict = dict()
|
||||||
|
|
||||||
|
def escapeVal(tag, value):
|
||||||
|
# if the value should be escaped:
|
||||||
|
if cls.ESCAPE_CRE.search(value):
|
||||||
|
# That one needs to be escaped since its content is
|
||||||
|
# out of our control
|
||||||
|
tag = 'f2bV_%s' % cls.ESCAPE_VN_CRE.sub('_', tag)
|
||||||
|
varsDict[tag] = value # add variable
|
||||||
|
value = '$'+tag # replacement as variable
|
||||||
|
# replacement for tag:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# substitution callable, used by interpolation of each tag
|
||||||
|
def substVal(m):
|
||||||
|
tag = m.group(1) # tagname from match
|
||||||
|
try:
|
||||||
|
value = aInfo[tag]
|
||||||
|
except KeyError:
|
||||||
|
# fallback (no or default replacement)
|
||||||
|
return ADD_REPL_TAGS.get(tag, m.group())
|
||||||
|
value = str(value) # assure string
|
||||||
|
# replacement for tag:
|
||||||
|
return escapeVal(tag, value)
|
||||||
|
|
||||||
|
# Replace normally properties of aInfo non-recursive:
|
||||||
|
realCmd = TAG_CRE.sub(substVal, realCmd)
|
||||||
|
|
||||||
|
# Replace ticket options (filter capture groups) non-recursive:
|
||||||
|
if '<' in realCmd:
|
||||||
|
tickData = aInfo.get("F-*")
|
||||||
|
if not tickData: tickData = {}
|
||||||
|
def substTag(m):
|
||||||
|
tag = mapTag2Opt(m.groups()[0])
|
||||||
|
try:
|
||||||
|
value = str(tickData[tag])
|
||||||
|
except KeyError:
|
||||||
|
return ""
|
||||||
|
return escapeVal("F_"+tag, value)
|
||||||
|
|
||||||
|
realCmd = FCUSTAG_CRE.sub(substTag, realCmd)
|
||||||
|
|
||||||
|
# build command corresponding "escaped" variables:
|
||||||
|
if varsDict:
|
||||||
|
realCmd = Utils.buildShellCmd(realCmd, varsDict)
|
||||||
|
return realCmd
|
||||||
|
|
||||||
def _processCmd(self, cmd, aInfo=None, conditional=''):
|
def _processCmd(self, cmd, aInfo=None, conditional=''):
|
||||||
"""Executes a command with preliminary checks and substitutions.
|
"""Executes a command with preliminary checks and substitutions.
|
||||||
|
|
||||||
|
@ -605,21 +677,9 @@ class CommandAction(ActionBase):
|
||||||
realCmd = self.replaceTag(cmd, self._properties,
|
realCmd = self.replaceTag(cmd, self._properties,
|
||||||
conditional=conditional, cache=self.__substCache)
|
conditional=conditional, cache=self.__substCache)
|
||||||
|
|
||||||
# Replace dynamical tags (don't use cache here)
|
# Replace dynamical tags, important - don't cache, no recursion and auto-escape here
|
||||||
if aInfo is not None:
|
if aInfo is not None:
|
||||||
realCmd = self.replaceTag(realCmd, aInfo, conditional=conditional)
|
realCmd = self.replaceDynamicTags(realCmd, aInfo)
|
||||||
# Replace ticket options (filter capture groups) non-recursive:
|
|
||||||
if '<' in realCmd:
|
|
||||||
tickData = aInfo.get("F-*")
|
|
||||||
if not tickData: tickData = {}
|
|
||||||
def substTag(m):
|
|
||||||
tn = mapTag2Opt(m.groups()[0])
|
|
||||||
try:
|
|
||||||
return str(tickData[tn])
|
|
||||||
except KeyError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
realCmd = FCUSTAG_CRE.sub(substTag, realCmd)
|
|
||||||
else:
|
else:
|
||||||
realCmd = cmd
|
realCmd = cmd
|
||||||
|
|
||||||
|
@ -653,8 +713,5 @@ class CommandAction(ActionBase):
|
||||||
logSys.debug("Nothing to do")
|
logSys.debug("Nothing to do")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
_cmd_lock.acquire()
|
with _cmd_lock:
|
||||||
try:
|
|
||||||
return Utils.executeCmd(realCmd, timeout, shell=True, output=False, **kwargs)
|
return Utils.executeCmd(realCmd, timeout, shell=True, output=False, **kwargs)
|
||||||
finally:
|
|
||||||
_cmd_lock.release()
|
|
||||||
|
|
|
@ -290,6 +290,7 @@ class Actions(JailThread, Mapping):
|
||||||
|
|
||||||
AI_DICT = {
|
AI_DICT = {
|
||||||
"ip": lambda self: self.__ticket.getIP(),
|
"ip": lambda self: self.__ticket.getIP(),
|
||||||
|
"family": lambda self: self['ip'].familyStr,
|
||||||
"ip-rev": lambda self: self['ip'].getPTR(''),
|
"ip-rev": lambda self: self['ip'].getPTR(''),
|
||||||
"ip-host": lambda self: self['ip'].getHost(),
|
"ip-host": lambda self: self['ip'].getHost(),
|
||||||
"fid": lambda self: self.__ticket.getID(),
|
"fid": lambda self: self.__ticket.getID(),
|
||||||
|
|
|
@ -261,6 +261,11 @@ class IPAddr(object):
|
||||||
def family(self):
|
def family(self):
|
||||||
return self._family
|
return self._family
|
||||||
|
|
||||||
|
FAM2STR = {socket.AF_INET: 'inet4', socket.AF_INET6: 'inet6'}
|
||||||
|
@property
|
||||||
|
def familyStr(self):
|
||||||
|
return IPAddr.FAM2STR.get(self._family)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plen(self):
|
def plen(self):
|
||||||
return self._plen
|
return self._plen
|
||||||
|
|
|
@ -28,7 +28,7 @@ import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from ..helpers import getLogger, uni_decode
|
from ..helpers import getLogger, _merge_dicts, uni_decode
|
||||||
|
|
||||||
if sys.version_info >= (3, 3):
|
if sys.version_info >= (3, 3):
|
||||||
import importlib.machinery
|
import importlib.machinery
|
||||||
|
@ -60,6 +60,7 @@ class Utils():
|
||||||
DEFAULT_SLEEP_TIME = 2
|
DEFAULT_SLEEP_TIME = 2
|
||||||
DEFAULT_SLEEP_INTERVAL = 0.2
|
DEFAULT_SLEEP_INTERVAL = 0.2
|
||||||
DEFAULT_SHORT_INTERVAL = 0.001
|
DEFAULT_SHORT_INTERVAL = 0.001
|
||||||
|
DEFAULT_SHORTEST_INTERVAL = DEFAULT_SHORT_INTERVAL / 100
|
||||||
|
|
||||||
|
|
||||||
class Cache(object):
|
class Cache(object):
|
||||||
|
@ -116,7 +117,31 @@ class Utils():
|
||||||
return flags
|
return flags
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def executeCmd(realCmd, timeout=60, shell=True, output=False, tout_kill_tree=True, success_codes=(0,)):
|
def buildShellCmd(realCmd, varsDict):
|
||||||
|
"""Generates new shell command as array, contains map as variables to
|
||||||
|
arguments statement (varsStat), the command (realCmd) used this variables and
|
||||||
|
the list of the arguments, mapped from varsDict
|
||||||
|
|
||||||
|
Example:
|
||||||
|
buildShellCmd('echo "V2: $v2, V1: $v1"', {"v1": "val 1", "v2": "val 2", "vUnused": "unused var"})
|
||||||
|
returns:
|
||||||
|
['v1=$0 v2=$1 vUnused=$2 \necho "V2: $v2, V1: $v1"', 'val 1', 'val 2', 'unused var']
|
||||||
|
"""
|
||||||
|
# build map as array of vars and command line array:
|
||||||
|
varsStat = ""
|
||||||
|
if not isinstance(realCmd, list):
|
||||||
|
realCmd = [realCmd]
|
||||||
|
i = len(realCmd)-1
|
||||||
|
for k, v in varsDict.iteritems():
|
||||||
|
varsStat += "%s=$%s " % (k, i)
|
||||||
|
realCmd.append(v)
|
||||||
|
i += 1
|
||||||
|
realCmd[0] = varsStat + "\n" + realCmd[0]
|
||||||
|
return realCmd
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def executeCmd(realCmd, timeout=60, shell=True, output=False, tout_kill_tree=True,
|
||||||
|
success_codes=(0,), varsDict=None):
|
||||||
"""Executes a command.
|
"""Executes a command.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -131,6 +156,8 @@ class Utils():
|
||||||
output : bool
|
output : bool
|
||||||
If output is True, the function returns tuple (success, stdoutdata, stderrdata, returncode).
|
If output is True, the function returns tuple (success, stdoutdata, stderrdata, returncode).
|
||||||
If False, just indication of success is returned
|
If False, just indication of success is returned
|
||||||
|
varsDict: dict
|
||||||
|
variables supplied to the command (or to the shell script)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -146,10 +173,18 @@ class Utils():
|
||||||
"""
|
"""
|
||||||
stdout = stderr = None
|
stdout = stderr = None
|
||||||
retcode = None
|
retcode = None
|
||||||
popen = None
|
popen = env = None
|
||||||
|
if varsDict:
|
||||||
|
if shell:
|
||||||
|
# build map as array of vars and command line array:
|
||||||
|
realCmd = Utils.buildShellCmd(realCmd, varsDict)
|
||||||
|
else: # pragma: no cover - currently unused
|
||||||
|
env = _merge_dicts(os.environ, varsDict)
|
||||||
|
realCmdId = id(realCmd)
|
||||||
|
logCmd = lambda level: logSys.log(level, "%x -- exec: %s", realCmdId, realCmd)
|
||||||
try:
|
try:
|
||||||
popen = subprocess.Popen(
|
popen = subprocess.Popen(
|
||||||
realCmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell,
|
realCmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, env=env,
|
||||||
preexec_fn=os.setsid # so that killpg does not kill our process
|
preexec_fn=os.setsid # so that killpg does not kill our process
|
||||||
)
|
)
|
||||||
# wait with timeout for process has terminated:
|
# wait with timeout for process has terminated:
|
||||||
|
@ -158,13 +193,15 @@ class Utils():
|
||||||
def _popen_wait_end():
|
def _popen_wait_end():
|
||||||
retcode = popen.poll()
|
retcode = popen.poll()
|
||||||
return (True, retcode) if retcode is not None else None
|
return (True, retcode) if retcode is not None else None
|
||||||
retcode = Utils.wait_for(_popen_wait_end, timeout, Utils.DEFAULT_SHORT_INTERVAL)
|
# popen.poll is fast operation so we can use the shortest sleep interval:
|
||||||
|
retcode = Utils.wait_for(_popen_wait_end, timeout, Utils.DEFAULT_SHORTEST_INTERVAL)
|
||||||
if retcode:
|
if retcode:
|
||||||
retcode = retcode[1]
|
retcode = retcode[1]
|
||||||
# if timeout:
|
# if timeout:
|
||||||
if retcode is None:
|
if retcode is None:
|
||||||
logSys.error("%s -- timed out after %s seconds." %
|
if logCmd: logCmd(logging.ERROR); logCmd = None
|
||||||
(realCmd, timeout))
|
logSys.error("%x -- timed out after %s seconds." %
|
||||||
|
(realCmdId, timeout))
|
||||||
pgid = os.getpgid(popen.pid)
|
pgid = os.getpgid(popen.pid)
|
||||||
# if not tree - first try to terminate and then kill, otherwise - kill (-9) only:
|
# if not tree - first try to terminate and then kill, otherwise - kill (-9) only:
|
||||||
os.killpg(pgid, signal.SIGTERM) # Terminate the process
|
os.killpg(pgid, signal.SIGTERM) # Terminate the process
|
||||||
|
@ -179,54 +216,56 @@ class Utils():
|
||||||
if retcode is None and not Utils.pid_exists(pgid): # pragma: no cover
|
if retcode is None and not Utils.pid_exists(pgid): # pragma: no cover
|
||||||
retcode = signal.SIGKILL
|
retcode = signal.SIGKILL
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
if logCmd: logCmd(logging.ERROR); logCmd = None
|
||||||
stderr = "%s -- failed with %s" % (realCmd, e)
|
stderr = "%s -- failed with %s" % (realCmd, e)
|
||||||
logSys.error(stderr)
|
logSys.error(stderr)
|
||||||
if not popen:
|
if not popen:
|
||||||
return False if not output else (False, stdout, stderr, retcode)
|
return False if not output else (False, stdout, stderr, retcode)
|
||||||
|
|
||||||
std_level = logging.DEBUG if retcode in success_codes else logging.ERROR
|
std_level = logging.DEBUG if retcode in success_codes else logging.ERROR
|
||||||
|
if std_level > logSys.getEffectiveLevel():
|
||||||
|
if logCmd: logCmd(std_level-1); logCmd = None
|
||||||
# if we need output (to return or to log it):
|
# if we need output (to return or to log it):
|
||||||
if output or std_level >= logSys.getEffectiveLevel():
|
if output or std_level >= logSys.getEffectiveLevel():
|
||||||
|
|
||||||
# if was timeouted (killed/terminated) - to prevent waiting, set std handles to non-blocking mode.
|
# if was timeouted (killed/terminated) - to prevent waiting, set std handles to non-blocking mode.
|
||||||
if popen.stdout:
|
if popen.stdout:
|
||||||
try:
|
try:
|
||||||
if retcode is None or retcode < 0:
|
if retcode is None or retcode < 0:
|
||||||
Utils.setFBlockMode(popen.stdout, False)
|
Utils.setFBlockMode(popen.stdout, False)
|
||||||
stdout = popen.stdout.read()
|
stdout = popen.stdout.read()
|
||||||
except IOError as e:
|
except IOError as e: # pragma: no cover
|
||||||
logSys.error(" ... -- failed to read stdout %s", e)
|
logSys.error(" ... -- failed to read stdout %s", e)
|
||||||
if stdout is not None and stdout != '' and std_level >= logSys.getEffectiveLevel():
|
if stdout is not None and stdout != '' and std_level >= logSys.getEffectiveLevel():
|
||||||
logSys.log(std_level, "%s -- stdout:", realCmd)
|
|
||||||
for l in stdout.splitlines():
|
for l in stdout.splitlines():
|
||||||
logSys.log(std_level, " -- stdout: %r", uni_decode(l))
|
logSys.log(std_level, "%x -- stdout: %r", realCmdId, uni_decode(l))
|
||||||
popen.stdout.close()
|
popen.stdout.close()
|
||||||
if popen.stderr:
|
if popen.stderr:
|
||||||
try:
|
try:
|
||||||
if retcode is None or retcode < 0:
|
if retcode is None or retcode < 0:
|
||||||
Utils.setFBlockMode(popen.stderr, False)
|
Utils.setFBlockMode(popen.stderr, False)
|
||||||
stderr = popen.stderr.read()
|
stderr = popen.stderr.read()
|
||||||
except IOError as e:
|
except IOError as e: # pragma: no cover
|
||||||
logSys.error(" ... -- failed to read stderr %s", e)
|
logSys.error(" ... -- failed to read stderr %s", e)
|
||||||
if stderr is not None and stderr != '' and std_level >= logSys.getEffectiveLevel():
|
if stderr is not None and stderr != '' and std_level >= logSys.getEffectiveLevel():
|
||||||
logSys.log(std_level, "%s -- stderr:", realCmd)
|
|
||||||
for l in stderr.splitlines():
|
for l in stderr.splitlines():
|
||||||
logSys.log(std_level, " -- stderr: %r", uni_decode(l))
|
logSys.log(std_level, "%x -- stderr: %r", realCmdId, uni_decode(l))
|
||||||
popen.stderr.close()
|
popen.stderr.close()
|
||||||
|
|
||||||
success = False
|
success = False
|
||||||
if retcode in success_codes:
|
if retcode in success_codes:
|
||||||
logSys.debug("%-.40s -- returned successfully %i", realCmd, retcode)
|
logSys.debug("%x -- returned successfully %i", realCmdId, retcode)
|
||||||
success = True
|
success = True
|
||||||
elif retcode is None:
|
elif retcode is None:
|
||||||
logSys.error("%-.40s -- unable to kill PID %i", realCmd, popen.pid)
|
logSys.error("%x -- unable to kill PID %i", realCmdId, popen.pid)
|
||||||
elif retcode < 0 or retcode > 128:
|
elif retcode < 0 or retcode > 128:
|
||||||
# dash would return negative while bash 128 + n
|
# dash would return negative while bash 128 + n
|
||||||
sigcode = -retcode if retcode < 0 else retcode - 128
|
sigcode = -retcode if retcode < 0 else retcode - 128
|
||||||
logSys.error("%-.40s -- killed with %s (return code: %s)",
|
logSys.error("%x -- killed with %s (return code: %s)",
|
||||||
realCmd, signame.get(sigcode, "signal %i" % sigcode), retcode)
|
realCmdId, signame.get(sigcode, "signal %i" % sigcode), retcode)
|
||||||
else:
|
else:
|
||||||
msg = _RETCODE_HINTS.get(retcode, None)
|
msg = _RETCODE_HINTS.get(retcode, None)
|
||||||
logSys.error("%-.40s -- returned %i", realCmd, retcode)
|
logSys.error("%x -- returned %i", realCmdId, retcode)
|
||||||
if msg:
|
if msg:
|
||||||
logSys.info("HINT on %i: %s", retcode, msg % locals())
|
logSys.info("HINT on %i: %s", retcode, msg % locals())
|
||||||
if output:
|
if output:
|
||||||
|
@ -290,7 +329,7 @@ class Utils():
|
||||||
return e.errno == errno.EPERM
|
return e.errno == errno.EPERM
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
else:
|
else: # pragma : no cover (no windows currently supported)
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pid_exists(pid):
|
def pid_exists(pid):
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
|
@ -389,6 +389,51 @@ class CommandActionTest(LogCaptureTestCase):
|
||||||
self.assertLogged('Nothing to do')
|
self.assertLogged('Nothing to do')
|
||||||
self.pruneLog()
|
self.pruneLog()
|
||||||
|
|
||||||
|
def testExecuteWithVars(self):
|
||||||
|
self.assertTrue(self.__action.executeCmd(
|
||||||
|
r'''printf %b "foreign input:\n'''
|
||||||
|
r''' -- $f2bV_A --\n'''
|
||||||
|
r''' -- $f2bV_B --\n'''
|
||||||
|
r''' -- $(echo -n $f2bV_C) --''' # echo just replaces \n to test it as single line
|
||||||
|
r'''"''',
|
||||||
|
varsDict={
|
||||||
|
'f2bV_A': 'I\'m a hacker; && $(echo $f2bV_B)',
|
||||||
|
'f2bV_B': 'I"m very bad hacker',
|
||||||
|
'f2bV_C': '`Very | very\n$(bad & worst hacker)`'
|
||||||
|
}))
|
||||||
|
self.assertLogged(r"""foreign input:""",
|
||||||
|
' -- I\'m a hacker; && $(echo $f2bV_B) --',
|
||||||
|
' -- I"m very bad hacker --',
|
||||||
|
' -- `Very | very $(bad & worst hacker)` --', all=True)
|
||||||
|
|
||||||
|
def testExecuteReplaceEscapeWithVars(self):
|
||||||
|
self.__action.actionban = 'echo "** ban <ip>, reason: <reason> ...\\n<matches>"'
|
||||||
|
self.__action.actionunban = 'echo "** unban <ip>"'
|
||||||
|
self.__action.actionstop = 'echo "** stop monitoring"'
|
||||||
|
matches = [
|
||||||
|
'<actionunban>',
|
||||||
|
'" Hooray! #',
|
||||||
|
'`I\'m cool script kiddy',
|
||||||
|
'`I`m very cool > /here-is-the-path/to/bin/.x-attempt.sh',
|
||||||
|
'<actionstop>',
|
||||||
|
]
|
||||||
|
aInfo = {
|
||||||
|
'ip': '192.0.2.1',
|
||||||
|
'reason': 'hacking attempt ( he thought he knows how f2b internally works ;)',
|
||||||
|
'matches': '\n'.join(matches)
|
||||||
|
}
|
||||||
|
self.pruneLog()
|
||||||
|
self.__action.ban(aInfo)
|
||||||
|
self.assertLogged(
|
||||||
|
'** ban %s' % aInfo['ip'], aInfo['reason'], *matches, all=True)
|
||||||
|
self.assertNotLogged(
|
||||||
|
'** unban %s' % aInfo['ip'], '** stop monitoring', all=True)
|
||||||
|
self.pruneLog()
|
||||||
|
self.__action.unban(aInfo)
|
||||||
|
self.__action.stop()
|
||||||
|
self.assertLogged(
|
||||||
|
'** unban %s' % aInfo['ip'], '** stop monitoring', all=True)
|
||||||
|
|
||||||
def testExecuteIncorrectCmd(self):
|
def testExecuteIncorrectCmd(self):
|
||||||
CommandAction.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null')
|
CommandAction.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null')
|
||||||
self.assertLogged('HINT on 127: "Command not found"')
|
self.assertLogged('HINT on 127: "Command not found"')
|
||||||
|
@ -400,8 +445,9 @@ class CommandActionTest(LogCaptureTestCase):
|
||||||
self.assertFalse(CommandAction.executeCmd('sleep 30', timeout=timeout))
|
self.assertFalse(CommandAction.executeCmd('sleep 30', timeout=timeout))
|
||||||
# give a test still 1 second, because system could be too busy
|
# give a test still 1 second, because system could be too busy
|
||||||
self.assertTrue(time.time() >= stime + timeout and time.time() <= stime + timeout + 1)
|
self.assertTrue(time.time() >= stime + timeout and time.time() <= stime + timeout + 1)
|
||||||
self.assertLogged('sleep 30 -- timed out after')
|
self.assertLogged('sleep 30', ' -- timed out after', all=True)
|
||||||
self.assertLogged('sleep 30 -- killed with SIGTERM')
|
self.assertLogged(' -- killed with SIGTERM',
|
||||||
|
' -- killed with SIGKILL')
|
||||||
|
|
||||||
def testExecuteTimeoutWithNastyChildren(self):
|
def testExecuteTimeoutWithNastyChildren(self):
|
||||||
# temporary file for a nasty kid shell script
|
# temporary file for a nasty kid shell script
|
||||||
|
@ -457,9 +503,9 @@ class CommandActionTest(LogCaptureTestCase):
|
||||||
# Verify that the process itself got killed
|
# Verify that the process itself got killed
|
||||||
self.assertTrue(Utils.wait_for(lambda: not pid_exists(cpid), 3))
|
self.assertTrue(Utils.wait_for(lambda: not pid_exists(cpid), 3))
|
||||||
self.assertLogged('my pid ', 'Resource temporarily unavailable')
|
self.assertLogged('my pid ', 'Resource temporarily unavailable')
|
||||||
self.assertLogged('timed out')
|
self.assertLogged(' -- timed out')
|
||||||
self.assertLogged('killed with SIGTERM',
|
self.assertLogged(' -- killed with SIGTERM',
|
||||||
'killed with SIGKILL')
|
' -- killed with SIGKILL')
|
||||||
os.unlink(tmpFilename)
|
os.unlink(tmpFilename)
|
||||||
os.unlink(tmpFilename + '.pid')
|
os.unlink(tmpFilename + '.pid')
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from ..client.configreader import ConfigReader, ConfigReaderUnshared
|
from ..client.configreader import ConfigReader, ConfigReaderUnshared, NoSectionError
|
||||||
from ..client import configparserinc
|
from ..client import configparserinc
|
||||||
from ..client.jailreader import JailReader
|
from ..client.jailreader import JailReader
|
||||||
from ..client.filterreader import FilterReader
|
from ..client.filterreader import FilterReader
|
||||||
|
@ -317,6 +317,16 @@ class JailReaderTest(LogCaptureTestCase):
|
||||||
self.assertLogged('File %s is a dangling link, thus cannot be monitored' % f2)
|
self.assertLogged('File %s is a dangling link, thus cannot be monitored' % f2)
|
||||||
self.assertEqual(JailReader._glob(os.path.join(d, 'nonexisting')), [])
|
self.assertEqual(JailReader._glob(os.path.join(d, 'nonexisting')), [])
|
||||||
|
|
||||||
|
def testCommonFunction(self):
|
||||||
|
c = ConfigReader(share_config={})
|
||||||
|
# test common functionalities (no shared, without read of config):
|
||||||
|
self.assertEqual(c.sections(), [])
|
||||||
|
self.assertFalse(c.has_section('test'))
|
||||||
|
self.assertRaises(NoSectionError, c.merge_section, 'test', {})
|
||||||
|
self.assertRaises(NoSectionError, c.options, 'test')
|
||||||
|
self.assertRaises(NoSectionError, c.get, 'test', 'any')
|
||||||
|
self.assertRaises(NoSectionError, c.getOptions, 'test', {})
|
||||||
|
|
||||||
|
|
||||||
class FilterReaderTest(unittest.TestCase):
|
class FilterReaderTest(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -712,6 +722,7 @@ class JailsReaderTest(LogCaptureTestCase):
|
||||||
self.assertEqual(opts['socket'], '/var/run/fail2ban/fail2ban.sock')
|
self.assertEqual(opts['socket'], '/var/run/fail2ban/fail2ban.sock')
|
||||||
self.assertEqual(opts['pidfile'], '/var/run/fail2ban/fail2ban.pid')
|
self.assertEqual(opts['pidfile'], '/var/run/fail2ban/fail2ban.pid')
|
||||||
|
|
||||||
|
configurator.readAll()
|
||||||
configurator.getOptions()
|
configurator.getOptions()
|
||||||
configurator.convertToProtocol()
|
configurator.convertToProtocol()
|
||||||
commands = configurator.getConfigStream()
|
commands = configurator.getConfigStream()
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
#[INCLUDES]
|
#[INCLUDES]
|
||||||
#before = common.conf
|
#before = common.conf
|
||||||
|
|
||||||
[Definition]
|
[DEFAULT]
|
||||||
failregex = failure test 1 (filter.d/test.conf) <HOST>
|
_daemon = default
|
||||||
|
|
||||||
|
[Definition]
|
||||||
|
where = conf
|
||||||
|
failregex = failure <_daemon> <one> (filter.d/test.%(where)s) <HOST>
|
||||||
|
|
||||||
|
[Init]
|
||||||
|
# test parameter, should be overriden in jail by "filter=test[one=1,...]"
|
||||||
|
one = *1*
|
||||||
|
|
|
@ -2,6 +2,15 @@
|
||||||
#before = common.conf
|
#before = common.conf
|
||||||
|
|
||||||
[Definition]
|
[Definition]
|
||||||
|
# overwrite default daemon, additionally it should be accessible in jail with "%(known/_daemon)s":
|
||||||
|
_daemon = test
|
||||||
|
# interpolate previous regex (from test.conf) + new 2nd + dynamical substitution) of "two" an "where":
|
||||||
failregex = %(known/failregex)s
|
failregex = %(known/failregex)s
|
||||||
failure test 2 (filter.d/test.local) <HOST>
|
failure %(_daemon)s <two> (filter.d/test.<where>) <HOST>
|
||||||
|
# parameter "two" should be specified in jail by "filter=test[..., two=2]"
|
||||||
|
|
||||||
|
[Init]
|
||||||
|
# this parameter can be used in jail with "%(known/three)s":
|
||||||
|
three = 3
|
||||||
|
# this parameter "where" does not overwrite "where" in definition of test.conf (dynamical values only):
|
||||||
|
where = local
|
|
@ -15,9 +15,9 @@ ignoreip =
|
||||||
|
|
||||||
[test-known-interp]
|
[test-known-interp]
|
||||||
enabled = true
|
enabled = true
|
||||||
filter = test
|
filter = test[one=1,two=2]
|
||||||
failregex = %(known/failregex)s
|
failregex = %(known/failregex)s
|
||||||
failure test 3 (jail.local) <HOST>
|
failure %(known/_daemon)s %(known/three)s (jail.local) <HOST>
|
||||||
|
|
||||||
[missinglogfiles]
|
[missinglogfiles]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
|
@ -1679,7 +1679,7 @@ class ServerConfigReaderTests(LogCaptureTestCase):
|
||||||
# complain --
|
# complain --
|
||||||
('j-complain-abuse',
|
('j-complain-abuse',
|
||||||
'complain['
|
'complain['
|
||||||
'name=%(__name__)s, grepopts="-m 1", grepmax=2, mailcmd="mail -s Hostname: <ip-host> - ",' +
|
'name=%(__name__)s, grepopts="-m 1", grepmax=2, mailcmd="mail -s \'Hostname: <ip-host>, family: <family>\' - ",' +
|
||||||
# test reverse ip:
|
# test reverse ip:
|
||||||
'debug=1,' +
|
'debug=1,' +
|
||||||
# 2 logs to test grep from multiple logs:
|
# 2 logs to test grep from multiple logs:
|
||||||
|
@ -1694,14 +1694,14 @@ class ServerConfigReaderTests(LogCaptureTestCase):
|
||||||
'testcase01.log:Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 87.142.124.10',
|
'testcase01.log:Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 87.142.124.10',
|
||||||
'testcase01a.log:Dec 31 11:55:01 [sshd] error: PAM: Authentication failure for test from 87.142.124.10',
|
'testcase01a.log:Dec 31 11:55:01 [sshd] error: PAM: Authentication failure for test from 87.142.124.10',
|
||||||
# both abuse mails should be separated with space:
|
# both abuse mails should be separated with space:
|
||||||
'mail -s Hostname: test-host - Abuse from 87.142.124.10 abuse-1@abuse-test-server abuse-2@abuse-test-server',
|
'mail -s Hostname: test-host, family: inet4 - Abuse from 87.142.124.10 abuse-1@abuse-test-server abuse-2@abuse-test-server',
|
||||||
),
|
),
|
||||||
'ip6-ban': (
|
'ip6-ban': (
|
||||||
# test reverse ip:
|
# test reverse ip:
|
||||||
'try to resolve 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.abuse-contacts.abusix.org',
|
'try to resolve 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.abuse-contacts.abusix.org',
|
||||||
'Lines containing failures of 2001:db8::1 (max 2)',
|
'Lines containing failures of 2001:db8::1 (max 2)',
|
||||||
# both abuse mails should be separated with space:
|
# both abuse mails should be separated with space:
|
||||||
'mail -s Hostname: test-host - Abuse from 2001:db8::1 abuse-1@abuse-test-server abuse-2@abuse-test-server',
|
'mail -s Hostname: test-host, family: inet6 - Abuse from 2001:db8::1 abuse-1@abuse-test-server abuse-2@abuse-test-server',
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue