diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 2f7f423c..5acc98ad 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -34,7 +34,7 @@ from collections import MutableMapping from .ipdns import asip from .mytime import MyTime from .utils import Utils -from ..helpers import getLogger, substituteRecursiveTags +from ..helpers import getLogger, substituteRecursiveTags, TAG_CRE, MAX_TAG_REPLACE_COUNT # Gets the instance of the logger. logSys = getLogger(__name__) @@ -399,33 +399,71 @@ class CommandAction(ActionBase): str `query` string with tags replaced. """ + if '<' not in query: return query + # use cache if allowed: if cache is not None: ckey = (query, conditional) - string = cache.get(ckey) - if string is not None: - return string - # replace: - string = query - aInfo = substituteRecursiveTags(aInfo, conditional, ignore=cls._escapedTags) - for tag in aInfo: - if "<%s>" % tag in query: - value = aInfo.get(tag + '?' + conditional) - if value is None: - value = aInfo.get(tag) - value = str(value) # assure string - if tag in cls._escapedTags: - # That one needs to be escaped since its content is - # out of our control - value = cls.escapeTag(value) - string = string.replace('<' + tag + '>', value) - # New line, space - string = reduce(lambda s, kv: s.replace(*kv), (("
", '\n'), ("", " ")), string) - # cache if properties: + value = cache.get(ckey) + if value is not None: + return value + + # first try get cached tags dictionary: + subInfo = csubkey = None if cache is not None: - cache[ckey] = string + csubkey = ('subst-tags', id(aInfo), conditional) + subInfo = cache.get(csubkey) + # interpolation of dictionary: + if subInfo is None: + subInfo = substituteRecursiveTags(aInfo, conditional, ignore=cls._escapedTags) + # New line, space + for (tag, value) in (("br", '\n'), ("sp", " ")): + if subInfo.get(tag) is None: subInfo[tag] = value + # cache if possible: + if csubkey is not None: + cache[csubkey] = subInfo + + # substitution callable, used by interpolation of each tag + repeatSubst = {} + def substVal(m): + tag = m.group(1) # tagname from match + value = None + if conditional: + value = subInfo.get(tag + '?' + conditional) + if value is None: + value = subInfo.get(tag) + if value is None: + return m.group() # fallback (no replacement) + value = str(value) # assure string + if tag in cls._escapedTags: + # That one needs to be escaped since its content is + # out of our control + value = cls.escapeTag(value) + # possible contains tags: + if '<' in value: + repeatSubst[1] = True + return value + + # interpolation of query: + count = MAX_TAG_REPLACE_COUNT + 1 + while True: + repeatSubst = {} + value = TAG_CRE.sub(substVal, query) + # possible recursion ? + if not repeatSubst or value == query: break + query = value + count -= 1 + if count <= 0: # pragma: no cover - almost impossible (because resolved above) + raise ValueError( + "unexpected too long replacement interpolation, " + "possible self referencing definitions in query: %s" % (query,)) + + + # cache if possible: + if cache is not None: + cache[ckey] = value # - return string + return value def _processCmd(self, cmd, aInfo=None, conditional=''): """Executes a command with preliminary checks and substitutions. @@ -491,7 +529,7 @@ class CommandAction(ActionBase): realCmd = self.replaceTag(cmd, self._properties, conditional=conditional, cache=self.__substCache) - # Replace tags + # Replace dynamical tags (don't use cache here) if aInfo is not None: realCmd = self.replaceTag(realCmd, aInfo, conditional=conditional) else: diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 8a74201c..38f576b1 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -217,10 +217,10 @@ class CommandActionTest(LogCaptureTestCase): self.__action.replaceTag(" ''", self.__action._properties, conditional="family=inet6", cache=cache), "Text 890-567 text 567 '567'") - self.assertEqual(len(cache) if cache is not None else -1, 3) + self.assertTrue(len(cache) >= 3) # set one parameter - internal properties and cache should be reseted: setattr(self.__action, 'xyz', "000-") - self.assertEqual(len(cache) if cache is not None else -1, 0) + self.assertEqual(len(cache), 0) # test againg, should have 000 instead of 890: for i in range(2): self.assertEqual( @@ -235,7 +235,7 @@ class CommandActionTest(LogCaptureTestCase): self.__action.replaceTag(" ''", self.__action._properties, conditional="family=inet6", cache=cache), "Text 000-567 text 567 '567'") - self.assertEqual(len(cache), 3) + self.assertTrue(len(cache) >= 3) def testExecuteActionBan(self):