"Fossies" - the Fresh Open Source Software Archive

Member "fail2ban-0.11.1/fail2ban/server/action.py" (11 Jan 2020, 29617 Bytes) of package /linux/misc/fail2ban-0.11.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "action.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 0.10.5_vs_0.11.1.

    1 # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
    2 # vi: set ft=python sts=4 ts=4 sw=4 noet :
    3 
    4 # This file is part of Fail2Ban.
    5 #
    6 # Fail2Ban is free software; you can redistribute it and/or modify
    7 # it under the terms of the GNU General Public License as published by
    8 # the Free Software Foundation; either version 2 of the License, or
    9 # (at your option) any later version.
   10 #
   11 # Fail2Ban is distributed in the hope that it will be useful,
   12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   14 # GNU General Public License for more details.
   15 #
   16 # You should have received a copy of the GNU General Public License
   17 # along with Fail2Ban; if not, write to the Free Software
   18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
   19 
   20 __author__ = "Cyril Jaquier and Fail2Ban Contributors"
   21 __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Yaroslav Halchenko"
   22 __license__ = "GPL"
   23 
   24 import logging
   25 import os
   26 import re
   27 import signal
   28 import subprocess
   29 import tempfile
   30 import threading
   31 import time
   32 from abc import ABCMeta
   33 from collections import MutableMapping
   34 
   35 from .failregex import mapTag2Opt
   36 from .ipdns import DNSUtils
   37 from .mytime import MyTime
   38 from .utils import Utils
   39 from ..helpers import getLogger, _merge_copy_dicts, \
   40     splitwords, substituteRecursiveTags, uni_string, TAG_CRE, MAX_TAG_REPLACE_COUNT
   41 
   42 # Gets the instance of the logger.
   43 logSys = getLogger(__name__)
   44 
   45 # Create a lock for running system commands
   46 _cmd_lock = threading.Lock()
   47 
   48 # Specifies whether IPv6 subsystem is available:
   49 allowed_ipv6 = DNSUtils.IPv6IsAllowed
   50 
   51 # capture groups from filter for map to ticket data:
   52 FCUSTAG_CRE = re.compile(r'<F-([A-Z0-9_\-]+)>'); # currently uppercase only
   53 
   54 COND_FAMILIES = ('inet4', 'inet6')
   55 CONDITIONAL_FAM_RE = re.compile(r"^(\w+)\?(family)=(.*)$")
   56 
   57 # Special tags:
   58 DYN_REPL_TAGS = {
   59   # System-information:
   60     "fq-hostname":  lambda: str(DNSUtils.getHostname(fqdn=True)),
   61     "sh-hostname":  lambda: str(DNSUtils.getHostname(fqdn=False))
   62 }
   63 # New line, space
   64 ADD_REPL_TAGS = {
   65   "br": "\n", 
   66   "sp": " "
   67 }
   68 ADD_REPL_TAGS.update(DYN_REPL_TAGS)
   69 
   70 
   71 class CallingMap(MutableMapping, object):
   72     """A Mapping type which returns the result of callable values.
   73 
   74     `CallingMap` behaves similar to a standard python dictionary,
   75     with the exception that any values which are callable, are called
   76     and the result is returned as the value.
   77     No error handling is in place, such that any errors raised in the
   78     callable will raised as usual.
   79     Actual dictionary is stored in property `data`, and can be accessed
   80     to obtain original callable values.
   81 
   82     Attributes
   83     ----------
   84     data : dict
   85         The dictionary data which can be accessed to obtain items uncalled
   86     """
   87 
   88     CM_REPR_ITEMS = ()
   89 
   90     # immutable=True saves content between actions, without interim copying (save original on demand, recoverable via reset)
   91     __slots__ = ('data', 'storage', 'immutable', '__org_data')
   92     def __init__(self, *args, **kwargs):
   93         self.storage = dict()
   94         self.immutable = True
   95         self.data = dict(*args, **kwargs)
   96 
   97     def reset(self, immutable=True):
   98         self.storage = dict()
   99         try:
  100             self.data = self.__org_data
  101         except AttributeError:
  102             pass
  103         self.immutable = immutable
  104 
  105     def _asrepr(self, calculated=False):
  106         # be sure it is suitable as string, so use str as checker:
  107         return "%s(%r)" % (self.__class__.__name__, self._asdict(calculated, str))
  108 
  109     __repr__ = _asrepr
  110 
  111     def _asdict(self, calculated=False, checker=None):
  112         d = dict(self.data, **self.storage)
  113         if not calculated:
  114             return dict((n,v) for n,v in d.iteritems() \
  115                 if not callable(v) or n in self.CM_REPR_ITEMS)
  116         for n,v in d.items():
  117             if callable(v):
  118                 try:
  119                     # calculate:
  120                     v = self.__getitem__(n)
  121                     # convert if needed:
  122                     if checker: checker(v)
  123                     # store calculated:
  124                     d[n] = v
  125                 except: # can't calculate - just ignore it
  126                     pass
  127         return d
  128 
  129     def getRawItem(self, key):
  130         try:
  131             value = self.storage[key]
  132         except KeyError:
  133             value = self.data[key]
  134         return value
  135 
  136     def __getitem__(self, key):
  137         try:
  138             value = self.storage[key]
  139         except KeyError:
  140             value = self.data[key]
  141         if callable(value):
  142             # check arguments can be supplied to callable (for backwards compatibility):
  143             value = value(self) if hasattr(value, '__code__') and value.__code__.co_argcount else value()
  144             self.storage[key] = value
  145         return value
  146 
  147     def __setitem__(self, key, value):
  148         # mutate to copy:
  149         if self.immutable:
  150             self.storage = self.storage.copy()
  151             self.__org_data = self.data
  152             self.data = self.data.copy()
  153             self.immutable = False
  154         self.storage[key] = value
  155 
  156     def __unavailable(self, key):
  157         raise KeyError("Key %r was deleted" % key)
  158 
  159     def __delitem__(self, key):
  160         # mutate to copy:
  161         if self.immutable:
  162             self.storage = self.storage.copy()
  163             self.__org_data = self.data
  164             self.data = self.data.copy()
  165             self.immutable = False
  166         try:
  167             del self.storage[key]
  168         except KeyError:
  169             pass
  170         del self.data[key]
  171 
  172     def __iter__(self):
  173         return iter(self.data)
  174 
  175     def __len__(self):
  176         return len(self.data)
  177 
  178     def copy(self):
  179         return self.__class__(_merge_copy_dicts(self.data, self.storage))
  180 
  181 
  182 class ActionBase(object):
  183     """An abstract base class for actions in Fail2Ban.
  184 
  185     Action Base is a base definition of what methods need to be in
  186     place to create a Python based action for Fail2Ban. This class can
  187     be inherited from to ease implementation.
  188     Required methods:
  189 
  190     - __init__(jail, name)
  191     - start()
  192     - stop()
  193     - ban(aInfo)
  194     - unban(aInfo)
  195 
  196     Called when action is created, but before the jail/actions is
  197     started. This should carry out necessary methods to initialise
  198     the action but not "start" the action.
  199 
  200     Parameters
  201     ----------
  202     jail : Jail
  203         The jail in which the action belongs to.
  204     name : str
  205         Name assigned to the action.
  206 
  207     Notes
  208     -----
  209     Any additional arguments specified in `jail.conf` or passed
  210     via `fail2ban-client` will be passed as keyword arguments.
  211     """
  212     __metaclass__ = ABCMeta
  213 
  214     @classmethod
  215     def __subclasshook__(cls, C):
  216         required = (
  217             "start",
  218             "stop",
  219             "ban",
  220             "reban",
  221             "unban",
  222             )
  223         for method in required:
  224             if not callable(getattr(C, method, None)):
  225                 return False
  226         return True
  227 
  228     def __init__(self, jail, name):
  229         self._jail = jail
  230         self._name = name
  231         self._logSys = getLogger("fail2ban.%s" % self.__class__.__name__)
  232 
  233     def start(self): # pragma: no cover - abstract
  234         """Executed when the jail/action is started.
  235         """
  236         pass
  237 
  238     def stop(self): # pragma: no cover - abstract
  239         """Executed when the jail/action is stopped.
  240         """
  241         pass
  242 
  243     def ban(self, aInfo): # pragma: no cover - abstract
  244         """Executed when a ban occurs.
  245 
  246         Parameters
  247         ----------
  248         aInfo : dict
  249             Dictionary which includes information in relation to
  250             the ban.
  251         """
  252         pass
  253 
  254     def reban(self, aInfo): # pragma: no cover - abstract
  255         """Executed when a ban occurs.
  256 
  257         Parameters
  258         ----------
  259         aInfo : dict
  260             Dictionary which includes information in relation to
  261             the ban.
  262         """
  263         return self.ban(aInfo)
  264 
  265     @property
  266     def _prolongable(self): # pragma: no cover - abstract
  267         return False
  268 
  269     def unban(self, aInfo): # pragma: no cover - abstract
  270         """Executed when a ban expires.
  271 
  272         Parameters
  273         ----------
  274         aInfo : dict
  275             Dictionary which includes information in relation to
  276             the ban.
  277         """
  278         pass
  279 
  280 
  281 WRAP_CMD_PARAMS = {
  282     'timeout': 'str2seconds',
  283     'bantime': 'ignore',
  284 }
  285 
  286 class CommandAction(ActionBase):
  287     """A action which executes OS shell commands.
  288 
  289     This is the default type of action which Fail2Ban uses.
  290 
  291     Default sets all commands for actions as empty string, such
  292     no command is executed.
  293 
  294     Parameters
  295     ----------
  296     jail : Jail
  297         The jail in which the action belongs to.
  298     name : str
  299         Name assigned to the action.
  300 
  301     Attributes
  302     ----------
  303     actionban
  304     actioncheck
  305     actionreban
  306     actionreload
  307     actionrepair
  308     actionstart
  309     actionstop
  310     actionunban
  311     timeout
  312     """
  313 
  314     _escapedTags = set(('matches', 'ipmatches', 'ipjailmatches'))
  315 
  316     def clearAllParams(self):
  317         """ Clear all lists/dicts parameters (used by reloading)
  318         """
  319         self.__init = 1
  320         try:
  321             self.timeout = 60
  322             ## Command executed in order to initialize the system.
  323             self.actionstart = ''
  324             ## Command executed when ticket gets banned.
  325             self.actionban = ''
  326             self.actionreban = ''
  327             ## Command executed when ticket gets removed.
  328             self.actionunban = ''
  329             ## Command executed in order to check requirements.
  330             self.actioncheck = ''
  331             ## Command executed in order to restore sane environment in error case.
  332             self.actionrepair = ''
  333             ## Command executed in order to flush all bans at once (e. g. by stop/shutdown the system).
  334             self.actionflush = ''
  335             ## Command executed in order to stop the system.
  336             self.actionstop = ''
  337             ## Command executed in case of reloading action.
  338             self.actionreload = ''
  339         finally:
  340             self.__init = 0
  341 
  342     def __init__(self, jail, name):
  343         super(CommandAction, self).__init__(jail, name)
  344         self.__init = 1
  345         self.__properties = None
  346         self.__started = {}
  347         self.__substCache = {}
  348         self.clearAllParams()
  349         self._logSys.debug("Created %s" % self.__class__)
  350 
  351     @classmethod
  352     def __subclasshook__(cls, C):
  353         return NotImplemented # Standard checks
  354 
  355     def __setattr__(self, name, value):
  356         if not name.startswith('_') and not self.__init and not callable(value):
  357             # special case for some parameters:
  358             wrp = WRAP_CMD_PARAMS.get(name)
  359             if wrp == 'ignore': # ignore (filter) dynamic parameters
  360                 return
  361             elif wrp == 'str2seconds':
  362                 value = MyTime.str2seconds(value)
  363             # parameters changed - clear properties and substitution cache:
  364             self.__properties = None
  365             self.__substCache.clear()
  366             #self._logSys.debug("Set action %r %s = %r", self._name, name, value)
  367             self._logSys.debug("  Set %s = %r", name, value)
  368         # set:
  369         self.__dict__[name] = value
  370 
  371     __setitem__ = __setattr__
  372 
  373     def __delattr__(self, name):
  374         if not name.startswith('_'):
  375             # parameters changed - clear properties and substitution cache:
  376             self.__properties = None
  377             self.__substCache.clear()
  378             #self._logSys.debug("Unset action %r %s", self._name, name)
  379             self._logSys.debug("  Unset %s", name)
  380         # del:
  381         del self.__dict__[name]
  382 
  383     @property
  384     def _properties(self):
  385         """A dictionary of the actions properties.
  386 
  387         This is used to substitute "tags" in the commands.
  388         """
  389         # if we have a properties - return it:
  390         if self.__properties is not None:
  391             return self.__properties
  392         # otherwise retrieve:
  393         self.__properties = dict(
  394             (key, getattr(self, key))
  395             for key in dir(self)
  396             if not key.startswith("_") and not callable(getattr(self, key))
  397         )
  398         return self.__properties
  399 
  400     @property
  401     def _substCache(self):
  402         return self.__substCache
  403 
  404     def _getOperation(self, tag, family):
  405         # replace operation tag (interpolate all values), be sure family is enclosed as conditional value
  406         # (as lambda in addrepl so only if not overwritten in action):
  407         return self.replaceTag(tag, self._properties,
  408             conditional=('family='+family if family else ''),
  409             addrepl=(lambda tag:family if tag == 'family' else None),
  410             cache=self.__substCache)
  411 
  412     def _operationExecuted(self, tag, family, *args):
  413         """ Get, set or delete command of operation considering family.
  414         """
  415         key = ('__eOpCmd',tag)
  416         if not len(args): # get
  417             if not callable(family): # pragma: no cover
  418                 return self.__substCache.get(key, {}).get(family)
  419             # family as expression - use it to filter values:
  420             return [v for f, v in self.__substCache.get(key, {}).iteritems() if family(f)]
  421         cmd = args[0]
  422         if cmd: # set:
  423             try:
  424                 famd = self.__substCache[key]
  425             except KeyError:
  426                 famd = self.__substCache[key] = {}
  427             famd[family] = cmd
  428         else: # delete (given family and all other with same command):
  429             try:
  430                 famd = self.__substCache[key]
  431                 cmd = famd.pop(family)
  432                 for family, v in famd.items():
  433                     if v == cmd:
  434                         del famd[family]
  435             except KeyError: # pragma: no cover
  436                 pass
  437 
  438     def _executeOperation(self, tag, operation, family=[], afterExec=None):
  439         """Executes the operation commands (like "actionstart", "actionstop", etc).
  440 
  441         Replace the tags in the action command with actions properties
  442         and executes the resulting command.
  443         """
  444         # check valid tags in properties (raises ValueError if self recursion, etc.):
  445         res = True
  446         err = 'Script error'
  447         if not family: # all started:
  448             family = [famoper for (famoper,v) in self.__started.iteritems() if v]
  449         for famoper in family:
  450             try:
  451                 cmd = self._getOperation(tag, famoper)
  452                 ret = True
  453                 # avoid double execution of same command for both families:
  454                 if cmd and cmd not in self._operationExecuted(tag, lambda f: f != famoper):
  455                     ret = self.executeCmd(cmd, self.timeout)
  456                     res &= ret
  457                 if afterExec: afterExec(famoper, ret)
  458                 self._operationExecuted(tag, famoper, cmd if ret else None)
  459             except ValueError as e:
  460                 res = False
  461                 err = e
  462         if not res:
  463             raise RuntimeError("Error %s action %s/%s: %r" % (operation, self._jail, self._name, err))
  464         return res
  465 
  466     @property
  467     def _hasCondSection(self):
  468         v = self._properties.get('__hasCondSection')
  469         if v is not None:
  470             return v
  471         v = False
  472         for n in self._properties:
  473             if CONDITIONAL_FAM_RE.match(n):
  474                 v = True
  475                 break
  476         self._properties['__hasCondSection'] = v
  477         return v
  478 
  479     @property
  480     def _families(self):
  481         v = self._properties.get('__families')
  482         if v: return v
  483         v = self._properties.get('families')
  484         if v and not isinstance(v, (list,set)): # pragma: no cover - still unused
  485             v = splitwords(v)
  486         elif self._hasCondSection: # all conditional families:
  487             # todo: check it is needed at all # common (resp. ipv4) + ipv6 if allowed:
  488             v = ['inet4', 'inet6'] if allowed_ipv6() else ['inet4']
  489         else: # all action tags seems to be the same
  490             v = ['']
  491         self._properties['__families'] = v
  492         return v
  493 
  494     @property
  495     def _startOnDemand(self):
  496         """Checks the action depends on family (conditional)"""
  497         v = self._properties.get('actionstart_on_demand')
  498         if v is not None:
  499             return v
  500         # not set - auto-recognize (depending on conditional):
  501         v = self._hasCondSection
  502         self._properties['actionstart_on_demand'] = v
  503         return v
  504 
  505     def start(self):
  506         """Executes the "actionstart" command.
  507 
  508         Replace the tags in the action command with actions properties
  509         and executes the resulting command.
  510         """
  511         return self._start()
  512 
  513     def _start(self, family=None, forceStart=False):
  514         """Executes the "actionstart" command.
  515 
  516         Replace the tags in the action command with actions properties
  517         and executes the resulting command.
  518         """
  519         # check the action depends on family (conditional):
  520         if self._startOnDemand:
  521             if not forceStart:
  522                 return True
  523         elif not forceStart and self.__started.get(family): # pragma: no cover - normally unreachable
  524             return True
  525         family = [family] if family is not None else self._families
  526         def _started(family, ret):
  527             if ret:
  528                 self._operationExecuted('<actionstop>', family, None)
  529                 self.__started[family] = 1
  530         ret = self._executeOperation('<actionstart>', 'starting', family=family, afterExec=_started)
  531         return ret
  532 
  533     def ban(self, aInfo, cmd='<actionban>'):
  534         """Executes the given command ("actionban" or "actionreban").
  535 
  536         Replaces the tags in the action command with actions properties
  537         and ban information, and executes the resulting command.
  538 
  539         Parameters
  540         ----------
  541         aInfo : dict
  542             Dictionary which includes information in relation to
  543             the ban.
  544         """
  545         # if we should start the action on demand (conditional by family):
  546         family = aInfo.get('family', '')
  547         if self._startOnDemand:
  548             if not self.__started.get(family):
  549                 self._start(family, forceStart=True)
  550         # ban:
  551         if not self._processCmd(cmd, aInfo):
  552             raise RuntimeError("Error banning %(ip)s" % aInfo)
  553         self.__started[family] = self.__started.get(family, 0) | 3; # started and contains items
  554 
  555     @property
  556     def _prolongable(self):
  557         return (hasattr(self, 'actionprolong') and self.actionprolong 
  558             and not str(self.actionprolong).isspace())
  559     
  560     def prolong(self, aInfo):
  561         """Executes the "actionprolong" command.
  562 
  563         Replaces the tags in the action command with actions properties
  564         and ban information, and executes the resulting command.
  565 
  566         Parameters
  567         ----------
  568         aInfo : dict
  569             Dictionary which includes information in relation to
  570             the ban.
  571         """
  572         if not self._processCmd('<actionprolong>', aInfo):
  573             raise RuntimeError("Error prolonging %(ip)s" % aInfo)
  574 
  575     def unban(self, aInfo):
  576         """Executes the "actionunban" command.
  577 
  578         Replaces the tags in the action command with actions properties
  579         and ban information, and executes the resulting command.
  580 
  581         Parameters
  582         ----------
  583         aInfo : dict
  584             Dictionary which includes information in relation to
  585             the ban.
  586         """
  587         family = aInfo.get('family', '')
  588         if self.__started.get(family, 0) & 2: # contains items
  589             if not self._processCmd('<actionunban>', aInfo):
  590                 raise RuntimeError("Error unbanning %(ip)s" % aInfo)
  591 
  592     def reban(self, aInfo):
  593         """Executes the "actionreban" command if available, otherwise simply repeat "actionban".
  594 
  595         Replaces the tags in the action command with actions properties
  596         and ban information, and executes the resulting command.
  597 
  598         Parameters
  599         ----------
  600         aInfo : dict
  601             Dictionary which includes information in relation to
  602             the ban.
  603         """
  604         # re-ban:
  605         return self.ban(aInfo, '<actionreban>' if self.actionreban else '<actionban>')
  606 
  607     def flush(self):
  608         """Executes the "actionflush" command.
  609         
  610         Command executed in order to flush all bans at once (e. g. by stop/shutdown 
  611         the system), instead of unbanning of each single ticket.
  612 
  613         Replaces the tags in the action command with actions properties
  614         and executes the resulting command.
  615         """
  616         # collect started families, may be started on demand (conditional):
  617         family = [f for (f,v) in self.__started.iteritems() if v & 3 == 3]; # started and contains items
  618         # if nothing contains items:
  619         if not family: return True
  620         # flush:
  621         def _afterFlush(family, ret):
  622             if ret and self.__started.get(family):
  623                 self.__started[family] &= ~2; # no items anymore
  624         return self._executeOperation('<actionflush>', 'flushing', family=family, afterExec=_afterFlush)
  625 
  626     def stop(self):
  627         """Executes the "actionstop" command.
  628 
  629         Replaces the tags in the action command with actions properties
  630         and executes the resulting command.
  631         """
  632         return self._stop()
  633 
  634     def _stop(self, family=None):
  635         """Executes the "actionstop" command.
  636 
  637         Replaces the tags in the action command with actions properties
  638         and executes the resulting command.
  639         """
  640         # collect started families, if started on demand (conditional):
  641         if family is None:
  642             family = [f for (f,v) in self.__started.iteritems() if v]
  643             # if no started (on demand) actions:
  644             if not family: return True
  645             self.__started = {}
  646         else:
  647             try:
  648                 self.__started[family] &= 0
  649                 family = [family]
  650             except KeyError: # pragma: no cover
  651                 return True
  652         def _stopped(family, ret):
  653             if ret:
  654                 self._operationExecuted('<actionstart>', family, None)
  655         return self._executeOperation('<actionstop>', 'stopping', family=family, afterExec=_stopped)
  656 
  657     def reload(self, **kwargs):
  658         """Executes the "actionreload" command.
  659 
  660         Parameters
  661         ----------
  662         kwargs : dict
  663           Currently unused, because CommandAction do not support initOpts
  664 
  665         Replaces the tags in the action command with actions properties
  666         and executes the resulting command.
  667         """
  668         return self._executeOperation('<actionreload>', 'reloading')
  669 
  670     def consistencyCheck(self, beforeRepair=None):
  671         """Executes the invariant check with repair if expected (conditional).
  672         """
  673         ret = True
  674         # for each started family:
  675         if self.actioncheck:
  676             for (family, started) in self.__started.items():
  677                 if started and not self._invariantCheck(family, beforeRepair):
  678                     # reset started flag and command of executed operation:
  679                     self.__started[family] = 0
  680                     self._operationExecuted('<actionstart>', family, None)
  681                     ret &= False
  682         return ret
  683 
  684     ESCAPE_CRE = re.compile(r"""[\\#&;`|*?~<>^()\[\]{}$'"\n\r]""")
  685     
  686     @classmethod
  687     def escapeTag(cls, value):
  688         """Escape characters which may be used for command injection.
  689 
  690         Parameters
  691         ----------
  692         value : str
  693             A string of which characters will be escaped.
  694 
  695         Returns
  696         -------
  697         str
  698             `value` with certain characters escaped.
  699 
  700         Notes
  701         -----
  702         The following characters are escaped::
  703 
  704             \\#&;`|*?~<>^()[]{}$'"\n\r
  705 
  706         """
  707         _map2c = {'\n': 'n', '\r': 'r'}
  708         def substChar(m):
  709             c = m.group()
  710             return '\\' + _map2c.get(c, c)
  711         
  712         value = cls.ESCAPE_CRE.sub(substChar, value)
  713         return value
  714 
  715     @classmethod
  716     def replaceTag(cls, query, aInfo, conditional='', addrepl=None, cache=None):
  717         """Replaces tags in `query` with property values.
  718 
  719         Parameters
  720         ----------
  721         query : str
  722             String with tags.
  723         aInfo : dict
  724             Tags(keys) and associated values for substitution in query.
  725 
  726         Returns
  727         -------
  728         str
  729             `query` string with tags replaced.
  730         """
  731         if '<' not in query: return query
  732 
  733         # use cache if allowed:
  734         if cache is not None:
  735             ckey = (query, conditional)
  736             try:
  737                 return cache[ckey]
  738             except KeyError:
  739                 pass
  740 
  741         # **Important**: don't replace if calling map - contains dynamic values only,
  742         # no recursive tags, otherwise may be vulnerable on foreign user-input:
  743         noRecRepl = isinstance(aInfo, CallingMap)
  744         subInfo = aInfo
  745         if not noRecRepl:
  746             # substitute tags recursive (and cache if possible),
  747             # first try get cached tags dictionary:
  748             subInfo = csubkey = None
  749             if cache is not None:
  750                 csubkey = ('subst-tags', id(aInfo), conditional)
  751                 try:
  752                     subInfo = cache[csubkey]
  753                 except KeyError:
  754                     pass
  755             # interpolation of dictionary:
  756             if subInfo is None:
  757                 subInfo = substituteRecursiveTags(aInfo, conditional, ignore=cls._escapedTags,
  758                     addrepl=addrepl)
  759             # cache if possible:
  760             if csubkey is not None:
  761                 cache[csubkey] = subInfo
  762 
  763         # additional replacement as calling map:
  764         ADD_REPL_TAGS_CM = CallingMap(ADD_REPL_TAGS)
  765         # substitution callable, used by interpolation of each tag
  766         def substVal(m):
  767             tag = m.group(1)            # tagname from match
  768             value = None
  769             if conditional:
  770                 value = subInfo.get(tag + '?' + conditional)
  771             if value is None:
  772                 value = subInfo.get(tag)
  773                 if value is None:
  774                     # fallback (no or default replacement)
  775                     return ADD_REPL_TAGS_CM.get(tag, m.group())
  776             value = uni_string(value)       # assure string
  777             if tag in cls._escapedTags:
  778                 # That one needs to be escaped since its content is
  779                 # out of our control
  780                 value = cls.escapeTag(value)
  781             # replacement for tag:
  782             return value
  783 
  784         # interpolation of query:
  785         count = MAX_TAG_REPLACE_COUNT + 1
  786         while True:
  787             value = TAG_CRE.sub(substVal, query)
  788             # **Important**: no recursive replacement for tags from calling map (properties only):
  789             if noRecRepl: break
  790             # possible recursion ?
  791             if value == query or '<' not in value: break
  792             query = value
  793             count -= 1
  794             if count <= 0:
  795                 raise ValueError(
  796                     "unexpected too long replacement interpolation, "
  797                     "possible self referencing definitions in query: %s" % (query,))
  798 
  799         # cache if possible:
  800         if cache is not None:
  801             cache[ckey] = value
  802         #
  803         return value
  804 
  805     ESCAPE_CRE = re.compile(r"""[\\#&;`|*?~<>\^\(\)\[\]{}$'"\n\r]""")
  806     ESCAPE_VN_CRE = re.compile(r"\W")
  807 
  808     @classmethod
  809     def replaceDynamicTags(cls, realCmd, aInfo):
  810         """Replaces dynamical tags in `query` with property values.
  811 
  812         **Important**
  813         -------------
  814         Because this tags are dynamic resp. foreign (user) input:
  815           - values should be escaped (using "escape" as shell variable)
  816           - no recursive substitution (no interpolation for <a<b>>)
  817           - don't use cache
  818 
  819         Parameters
  820         ----------
  821         query : str
  822             String with tags.
  823         aInfo : dict
  824             Tags(keys) and associated values for substitution in query.
  825 
  826         Returns
  827         -------
  828         str
  829             shell script as string or array with tags replaced (direct or as variables).
  830         """
  831         # array for escaped vars:
  832         varsDict = dict()
  833 
  834         def escapeVal(tag, value):
  835             # if the value should be escaped:
  836             if cls.ESCAPE_CRE.search(value):
  837                 # That one needs to be escaped since its content is
  838                 # out of our control
  839                 tag = 'f2bV_%s' % cls.ESCAPE_VN_CRE.sub('_', tag)
  840                 varsDict[tag] = value # add variable
  841                 value = '$'+tag # replacement as variable
  842             # replacement for tag:
  843             return value
  844 
  845         # additional replacement as calling map:
  846         ADD_REPL_TAGS_CM = CallingMap(ADD_REPL_TAGS)
  847         # substitution callable, used by interpolation of each tag
  848         def substVal(m):
  849             tag = m.group(1)            # tagname from match
  850             try:
  851                 value = aInfo[tag]
  852             except KeyError:
  853                 # fallback (no or default replacement)
  854                 return ADD_REPL_TAGS_CM.get(tag, m.group())
  855             value = uni_string(value)       # assure string
  856             # replacement for tag:
  857             return escapeVal(tag, value)
  858         
  859         # Replace normally properties of aInfo non-recursive:
  860         realCmd = TAG_CRE.sub(substVal, realCmd)
  861 
  862         # Replace ticket options (filter capture groups) non-recursive:
  863         if '<' in realCmd:
  864             tickData = aInfo.get("F-*")
  865             if not tickData: tickData = {}
  866             def substTag(m):
  867                 tag = mapTag2Opt(m.groups()[0])
  868                 try:
  869                     value = uni_string(tickData[tag])
  870                 except KeyError:
  871                     return ""
  872                 return escapeVal("F_"+tag, value)
  873             
  874             realCmd = FCUSTAG_CRE.sub(substTag, realCmd)
  875 
  876         # build command corresponding "escaped" variables:
  877         if varsDict:
  878             realCmd = Utils.buildShellCmd(realCmd, varsDict)
  879         return realCmd
  880 
  881     @property
  882     def banEpoch(self):
  883         return getattr(self, '_banEpoch', 0)
  884     def invalidateBanEpoch(self):
  885         """Increments ban epoch of jail and this action, so already banned tickets would cause
  886         a re-ban for all tickets with previous epoch."""
  887         if self._jail is not None:
  888             self._banEpoch = self._jail.actions.banEpoch = self._jail.actions.banEpoch + 1
  889         else:
  890             self._banEpoch = self.banEpoch + 1
  891 
  892     def _invariantCheck(self, family=None, beforeRepair=None, forceStart=True):
  893         """Executes a substituted `actioncheck` command.
  894         """
  895         # for started action/family only (avoid check not started inet4 if inet6 gets broken):
  896         if not forceStart and family is not None and family not in self.__started:
  897             return 1
  898         checkCmd = self._getOperation('<actioncheck>', family)
  899         if not checkCmd or self.executeCmd(checkCmd, self.timeout):
  900             return 1
  901         # if don't need repair/restore - just return:
  902         if beforeRepair and not beforeRepair():
  903             return -1
  904         self._logSys.error(
  905             "Invariant check failed. Trying to restore a sane environment")
  906         # increment ban epoch of jail and this action (allows re-ban on already banned):
  907         self.invalidateBanEpoch()
  908         # try to find repair command, if exists - exec it:
  909         repairCmd = self._getOperation('<actionrepair>', family)
  910         if repairCmd:
  911             if not self.executeCmd(repairCmd, self.timeout):
  912                 self.__started[family] = 0
  913                 self._logSys.critical("Unable to restore environment")
  914                 return 0
  915             self.__started[family] = 1
  916         else:
  917             # no repair command, try to restart action...
  918             # [WARNING] TODO: be sure all banactions get a repair command, because
  919             #    otherwise stop/start will theoretically remove all the bans,
  920             #    but the tickets are still in BanManager, so in case of new failures
  921             #    it will not be banned, because "already banned" will happen.
  922             try:
  923                 self._stop(family)
  924             except RuntimeError: # bypass error in stop (if start/check succeeded hereafter).
  925                 pass
  926             self._start(family, forceStart=forceStart or not self._startOnDemand)
  927         if self.__started.get(family) and not self.executeCmd(checkCmd, self.timeout):
  928             self._logSys.critical("Unable to restore environment")
  929             return 0
  930         return 1
  931 
  932     def _processCmd(self, cmd, aInfo=None):
  933         """Executes a command with preliminary checks and substitutions.
  934 
  935         Before executing any commands, executes the "check" command first
  936         in order to check if pre-requirements are met. If this check fails,
  937         it tries to restore a sane environment before executing the real
  938         command.
  939 
  940         Parameters
  941         ----------
  942         cmd : str
  943             The command to execute.
  944         aInfo : dictionary
  945             Dynamic properties.
  946 
  947         Returns
  948         -------
  949         bool
  950             True if the command succeeded.
  951         """
  952         if cmd == "":
  953             self._logSys.debug("Nothing to do")
  954             return True
  955 
  956         # conditional corresponding family of the given ip:
  957         try:
  958             family = aInfo["family"]
  959         except (KeyError, TypeError):
  960             family = ''
  961 
  962         # invariant check:
  963         if self.actioncheck:
  964             # don't repair/restore if unban (no matter):
  965             def _beforeRepair():
  966                 if cmd == '<actionunban>' and not self._properties.get('actionrepair_on_unban'):
  967                     self._logSys.error("Invariant check failed. Unban is impossible.")
  968                     return False
  969                 return True
  970             # check and repair if broken:
  971             ret = self._invariantCheck(family, _beforeRepair, forceStart=(cmd != '<actionunban>'))
  972             # if not sane (and not restored) return:
  973             if ret != 1:
  974                 return False
  975 
  976         # Replace static fields
  977         realCmd = self.replaceTag(cmd, self._properties, 
  978             conditional=('family='+family if family else ''), cache=self.__substCache)
  979 
  980         # Replace dynamical tags, important - don't cache, no recursion and auto-escape here
  981         if aInfo is not None:
  982             realCmd = self.replaceDynamicTags(realCmd, aInfo)
  983         else:
  984             realCmd = cmd
  985 
  986         return self.executeCmd(realCmd, self.timeout)
  987 
  988     @staticmethod
  989     def executeCmd(realCmd, timeout=60, **kwargs):
  990         """Executes a command.
  991 
  992         Parameters
  993         ----------
  994         realCmd : str
  995             The command to execute.
  996         timeout : int
  997             The time out in seconds for the command.
  998 
  999         Returns
 1000         -------
 1001         bool
 1002             True if the command succeeded.
 1003 
 1004         Raises
 1005         ------
 1006         OSError
 1007             If command fails to be executed.
 1008         RuntimeError
 1009             If command execution times out.
 1010         """
 1011         if logSys.getEffectiveLevel() < logging.DEBUG:
 1012             logSys.log(9, realCmd)
 1013         if not realCmd:
 1014             logSys.debug("Nothing to do")
 1015             return True
 1016 
 1017         with _cmd_lock:
 1018             return Utils.executeCmd(realCmd, timeout, shell=True, output=False, **kwargs)