diff --git a/config/action.d/smtp.py b/config/action.d/smtp.py new file mode 100644 index 00000000..065a0bba --- /dev/null +++ b/config/action.d/smtp.py @@ -0,0 +1,225 @@ +# 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. + +import sys +import socket +import smtplib +from email.mime.text import MIMEText +from email.utils import formatdate, formataddr + +from fail2ban.server.actions import ActionBase, CallingMap + +messages = {} +messages['start'] = \ +"""Hi, + +The jail %(jailname)s has been started successfully. + +Regards, +Fail2Ban""" + +messages['stop'] = \ +"""Hi, + +The jail %(jailname)s has been stopped. + +Regards, +Fail2Ban""" + +messages['ban'] = {} +messages['ban']['head'] = \ +"""Hi, + +The IP %(ip)s has just been banned for %(bantime)s seconds +by Fail2Ban after %(failures)i attempts against %(jailname)s. +""" +messages['ban']['tail'] = \ +""" +Regards, +Fail2Ban""" +messages['ban']['matches'] = \ +""" +Matches for this ban: +%(matches)s +""" +messages['ban']['ipmatches'] = \ +""" +Matches for %(ip)s: +%(ipmatches)s +""" +messages['ban']['ipjailmatches'] = \ +""" +Matches for %(ip)s for jail %(jailname)s: +%(ipjailmatches)s +""" + +class SMTPAction(ActionBase): + """Fail2Ban action which sends emails to inform on jail starting, + stopping and bans. + """ + + def __init__( + self, jail, name, host="localhost", user=None, password=None, + sendername="Fail2Ban", sender="fail2ban", dest="root", matches=None): + """Initialise action. + + Parameters + ---------- + jail : Jail + The jail which the action belongs to. + name : str + Named assigned to the action. + host : str, optional + SMTP host, of host:port format. Default host "localhost" and + port "25" + user : str, optional + Username used for authentication with SMTP server. + password : str, optional + Password used for authentication with SMTP server. + sendername : str, optional + Name to use for from address in email. Default "Fail2Ban". + sender : str, optional + Email address to use for from address in email. + Default "fail2ban". + dest : str, optional + Email addresses of intended recipient(s) in comma delimited + format. Default "root". + matches : str, optional + Type of matches to be included from ban in email. Can be one + of "matches", "ipmatches" or "ipjailmatches". Default None + (see man jail.conf.5). + """ + + super(SMTPAction, self).__init__(jail, name) + + self.host = host + #TODO: self.ssl = ssl + + self.user = user + self.password =password + + self.fromname = sendername + self.fromaddr = sender + self.toaddr = dest + + self.matches = matches + + self.message_values = CallingMap( + jailname = self._jail.getName(), # Doesn't change + hostname = socket.gethostname, + bantime = self._jail.actions.getBanTime, + ) + + def _sendMessage(self, subject, text): + """Sends message based on arguments and instance's properties. + + Parameters + ---------- + subject : str + Subject of the email. + text : str + Body of the email. + + Raises + ------ + SMTPConnectionError + Error on connecting to host. + SMTPAuthenticationError + Error authenticating with SMTP server. + SMTPException + See Python `smtplib` for full list of other possible + exceptions. + """ + msg = MIMEText(text) + msg['Subject'] = subject + msg['From'] = formataddr((self.fromname, self.fromaddr)) + msg['To'] = self.toaddr + msg['Date'] = formatdate() + + smtp = smtplib.SMTP() + try: + self._logSys.debug("Connected to SMTP '%s', response: %i: %s", + self.host, *smtp.connect(self.host)) + if self.user and self.password: + smtp.login(self.user, self.password) + failed_recipients = smtp.sendmail( + self.fromaddr, self.toaddr, msg.as_string()) + except smtplib.SMTPConnectError: + self._logSys.error("Error connecting to host '%s'", self.host) + raise + except smtplib.SMTPAuthenticationError: + self._logSys.error( + "Failed to authenticate with host '%s' user '%s'", + self.host, self.user) + raise + except smtplib.SMTPException: + self._logSys.error( + "Error sending mail to host '%s' from '%s' to '%s'", + self.host, self.fromaddr, self.toaddr) + raise + else: + if failed_recipients: + self._logSys.warning( + "Email to '%s' failed to following recipients: %r", + self.toaddr, failed_recipients) + self._logSys.debug("Email '%s' successfully sent", subject) + finally: + try: + self._logSys.debug("Disconnected from '%s', response %i: %s", + self.host, *smtp.quit()) + except smtplib.SMTPServerDisconnected: + pass # Not connected + + def start(self): + """Sends email to recipients informing that the jail has started. + """ + self._sendMessage( + "[Fail2Ban] %(jailname)s: started on %(hostname)s" % + self.message_values, + messages['start'] % self.message_values) + + def stop(self): + """Sends email to recipients informing that the jail has stopped. + """ + self._sendMessage( + "[Fail2Ban] %(jailname)s: stopped on %(hostname)s" % + self.message_values, + messages['stop'] % self.message_values) + + def ban(self, aInfo): + """Sends email to recipients informing that ban has occurred. + + Parameters + ---------- + aInfo : dict + Dictionary which includes information in relation to + the ban. + """ + aInfo.update(self.message_values) + message = "".join([ + messages['ban']['head'], + messages['ban'].get(self.matches, ""), + messages['ban']['tail'] + ]) + self._sendMessage( + "[Fail2Ban] %(jailname)s: banned %(ip)s from %(hostname)s" % + aInfo, + message % aInfo) + +Action = SMTPAction diff --git a/fail2ban/client/actionreader.py b/fail2ban/client/actionreader.py index 77ca1088..b11deaba 100644 --- a/fail2ban/client/actionreader.py +++ b/fail2ban/client/actionreader.py @@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging, os -from configreader import ConfigReader, DefinitionInitConfigReader + +from .configreader import ConfigReader, DefinitionInitConfigReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -58,22 +59,20 @@ class ActionReader(DefinitionInitConfigReader): head = ["set", self._jailName] stream = list() stream.append(head + ["addaction", self._name]) + head.extend(["action", self._name]) for opt in self._opts: if opt == "actionstart": - stream.append(head + ["actionstart", self._name, self._opts[opt]]) + stream.append(head + ["actionstart", self._opts[opt]]) elif opt == "actionstop": - stream.append(head + ["actionstop", self._name, self._opts[opt]]) + stream.append(head + ["actionstop", self._opts[opt]]) elif opt == "actioncheck": - stream.append(head + ["actioncheck", self._name, self._opts[opt]]) + stream.append(head + ["actioncheck", self._opts[opt]]) elif opt == "actionban": - stream.append(head + ["actionban", self._name, self._opts[opt]]) + stream.append(head + ["actionban", self._opts[opt]]) elif opt == "actionunban": - stream.append(head + ["actionunban", self._name, self._opts[opt]]) + stream.append(head + ["actionunban", self._opts[opt]]) if self._initOpts: - if "timeout" in self._initOpts: - stream.append(head + ["timeout", self._name, self._opts["timeout"]]) - # cInfo for p in self._initOpts: - stream.append(head + ["setcinfo", self._name, p, self._initOpts[p]]) + stream.append(head + [p, self._initOpts[p]]) return stream diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 4bfa9345..724f068f 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -23,7 +23,7 @@ __license__ = "GPL" import logging -from fail2ban.exceptions import UnknownJailException, DuplicateJailException +from ..exceptions import UnknownJailException, DuplicateJailException # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -165,7 +165,23 @@ class Beautifier: msg = "No actions for jail %s" % inC[1] else: msg = "The jail %s has the following actions:\n" % inC[1] - msg += ", ".join(action.getName() for action in response) + 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) except Exception: logSys.warning("Beautifier error. Please report the error") logSys.error("Beautify " + `response` + " with " + `self.__inputCmd` + diff --git a/fail2ban/client/configreader.py b/fail2ban/client/configreader.py index ddbc48db..af81ac24 100644 --- a/fail2ban/client/configreader.py +++ b/fail2ban/client/configreader.py @@ -25,9 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import glob, logging, os -from configparserinc import SafeConfigParserWithIncludes from ConfigParser import NoOptionError, NoSectionError +from .configparserinc import SafeConfigParserWithIncludes + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/configurator.py b/fail2ban/client/configurator.py index aa64704a..4fab0b26 100644 --- a/fail2ban/client/configurator.py +++ b/fail2ban/client/configurator.py @@ -25,9 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging -from configreader import ConfigReader -from fail2banreader import Fail2banReader -from jailsreader import JailsReader + +from .configreader import ConfigReader +from .fail2banreader import Fail2banReader +from .jailsreader import JailsReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/fail2banreader.py b/fail2ban/client/fail2banreader.py index d5d7285a..5abdc141 100644 --- a/fail2ban/client/fail2banreader.py +++ b/fail2ban/client/fail2banreader.py @@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging -from configreader import ConfigReader + +from .configreader import ConfigReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/filterreader.py b/fail2ban/client/filterreader.py index d8a6dbe8..7650af08 100644 --- a/fail2ban/client/filterreader.py +++ b/fail2ban/client/filterreader.py @@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging, os, shlex -from configreader import ConfigReader, DefinitionInitConfigReader + +from .configreader import ConfigReader, DefinitionInitConfigReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index 1b6cd976..0a9d4226 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -25,10 +25,11 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging, re, glob, os.path +import json -from configreader import ConfigReader -from filterreader import FilterReader -from actionreader import ActionReader +from .configreader import ConfigReader +from .filterreader import FilterReader +from .actionreader import ActionReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -120,14 +121,26 @@ class JailReader(ConfigReader): if not act: # skip empty actions continue actName, actOpt = JailReader.extractOptions(act) - action = ActionReader( - actName, self.__name, actOpt, basedir=self.getBaseDir()) - ret = action.read() - if ret: - action.getOptions(self.__opts) - self.__actions.append(action) + if actName.endswith(".py"): + self.__actions.append([ + "set", + self.__name, + "addaction", + actOpt.pop("actname", os.path.splitext(actName)[0]), + os.path.join( + self.getBaseDir(), "action.d", actName), + json.dumps(actOpt), + ]) else: - raise AttributeError("Unable to read action") + action = ActionReader( + actName, self.__name, actOpt, + basedir=self.getBaseDir()) + ret = action.read() + if ret: + action.getOptions(self.__opts) + self.__actions.append(action) + else: + raise AttributeError("Unable to read action") except Exception, e: logSys.error("Error in action definition " + act) logSys.debug("Caught exception: %s" % (e,)) @@ -193,7 +206,10 @@ class JailReader(ConfigReader): if self.__filter: stream.extend(self.__filter.convert()) for action in self.__actions: - stream.extend(action.convert()) + if isinstance(action, ConfigReader): + stream.extend(action.convert()) + else: + stream.append(action) stream.insert(0, ["add", self.__name, backend]) return stream diff --git a/fail2ban/client/jailsreader.py b/fail2ban/client/jailsreader.py index c52f1b2b..f0794915 100644 --- a/fail2ban/client/jailsreader.py +++ b/fail2ban/client/jailsreader.py @@ -25,8 +25,9 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging -from configreader import ConfigReader -from jailreader import JailReader + +from .configreader import ConfigReader +from .jailreader import JailReader # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/exceptions.py b/fail2ban/exceptions.py index 7e933544..a7fe5ccc 100644 --- a/fail2ban/exceptions.py +++ b/fail2ban/exceptions.py @@ -29,7 +29,7 @@ __license__ = "GPL" class DuplicateJailException(Exception): pass -class UnknownJailException(Exception): +class UnknownJailException(KeyError): pass diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index 99a45d25..977cc891 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -76,16 +76,18 @@ protocol = [ ["set unbanip ", "manually Unban in "], ["set maxretry ", "sets the number of failures before banning the host for "], ["set maxlines ", "sets the number of to buffer for regex search for "], -["set addaction ", "adds a new action named for "], -["set delaction ", "removes the action from "], -["set setcinfo ", "sets for of the action for "], -["set delcinfo ", "removes for the action for "], -["set timeout ", "sets as the command timeout in seconds for the action for "], -["set actionstart ", "sets the start command of the action for "], -["set actionstop ", "sets the stop command of the action for "], -["set actioncheck ", "sets the check command of the action for "], -["set actionban ", "sets the ban command of the action for "], -["set actionunban ", "sets the unban command of the action for "], +["set addaction [ ]", "adds a new action named for . Optionally for a Python based action, a and can be specified, else will be a Command Action"], +["set delaction ", "removes the action from "], +["", "COMMAND ACTION CONFIGURATION", ""], +["set action actionstart ", "sets the start command of the action for "], +["set action actionstop ", "sets the stop command of the action for "], +["set action actioncheck ", "sets the check command of the action for "], +["set action actionban ", "sets the ban command of the action for "], +["set action actionunban ", "sets the unban command of the action for "], +["set action timeout ", "sets as the command timeout in seconds for the action for "], +["", "GENERAL ACTION CONFIGURATION", ""], +["set action ", "sets the of for the action for "], +["set action [ ]", "calls the with for the action for "], ['', "JAIL INFORMATION", ""], ["get logpath", "gets the list of the monitored files for "], ["get logencoding ", "gets the of the log files for "], @@ -100,15 +102,18 @@ protocol = [ ["get usedns", "gets the usedns setting for "], ["get maxretry", "gets the number of failures allowed for "], ["get maxlines", "gets the number of lines to buffer for "], -["get addaction", "gets the last action which has been added for "], ["get actions", "gets a list of actions for "], -["get actionstart ", "gets the start command for the action for "], -["get actionstop ", "gets the stop command for the action for "], -["get actioncheck ", "gets the check command for the action for "], -["get actionban ", "gets the ban command for the action for "], -["get actionunban ", "gets the unban command for the action for "], -["get cinfo ", "gets the value for for the action for "], -["get timeout ", "gets the command timeout in seconds for the action for "], +["", "COMMAND ACTION INFORMATION",""], +["get action actionstart", "gets the start command for the action for "], +["get action actionstop", "gets the stop command for the action for "], +["get action actioncheck", "gets the check command for the action for "], +["get action actionban", "gets the ban command for the action for "], +["get action actionunban", "gets the unban command for the action for "], +["get action timeout", "gets the command timeout in seconds for the action for "], +["", "GENERAL ACTION INFORMATION", ""], +["get actionproperties ", "gets a list of properties for the action for "], +["get actionmethods ", "gets a list of methods for the action for "], +["get action ", "gets the value of for the action for "], ] ## @@ -125,12 +130,14 @@ def printFormatted(): print firstHeading = True first = True - for n in textwrap.wrap(m[1], WIDTH): + if len(m[0]) >= MARGIN: + m[1] = ' ' * WIDTH + m[1] + for n in textwrap.wrap(m[1], WIDTH, drop_whitespace=False): if first: - line = ' ' * INDENT + m[0] + ' ' * (MARGIN - len(m[0])) + n + line = ' ' * INDENT + m[0] + ' ' * (MARGIN - len(m[0])) + n.strip() first = False else: - line = ' ' * (INDENT + MARGIN) + n + line = ' ' * (INDENT + MARGIN) + n.strip() print line ## diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 17f33a6f..20492a8a 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -23,6 +23,8 @@ __license__ = "GPL" import logging, os, subprocess, time, signal, tempfile import threading, re +from abc import ABCMeta +from collections import MutableMapping #from subprocess import call # Gets the instance of the logger. @@ -34,7 +36,7 @@ _cmd_lock = threading.Lock() # Some hints on common abnormal exit codes _RETCODE_HINTS = { 127: '"Command not found". Make sure that all commands in %(realCmd)r ' - 'are in the PATH of fail2ban-server process ' + 'are in the PATH of fail2ban-server process ' '(grep -a PATH= /proc/`pidof -x fail2ban-server`/environ). ' 'You may want to start ' '"fail2ban-server -f" separately, initiate it with ' @@ -46,232 +48,326 @@ _RETCODE_HINTS = { signame = dict((num, name) for name, num in signal.__dict__.iteritems() if name.startswith("SIG")) -## -# Execute commands. -# -# This class reads the failures from the Jail queue and decide if an -# action has to be taken. A BanManager take care of the banned IP -# addresses. +class CallingMap(MutableMapping): + """A Mapping type which returns the result of callable values. -class Action: - - def __init__(self, name): - self.__name = name - self.__timeout = 60 - self.__cInfo = dict() - ## Command executed in order to initialize the system. - self.__actionStart = '' - ## Command executed when an IP address gets banned. - self.__actionBan = '' - ## Command executed when an IP address gets removed. - self.__actionUnban = '' - ## Command executed in order to check requirements. - self.__actionCheck = '' - ## Command executed in order to stop the system. - self.__actionStop = '' - logSys.debug("Created Action") - - ## - # Sets the action name. - # - # @param name the name of the action - - def setName(self, name): - self.__name = name - - ## - # Returns the action name. - # - # @return the name of the action - - def getName(self): - return self.__name - - ## - # Sets the timeout period for commands. - # - # @param timeout timeout period in seconds - - def setTimeout(self, timeout): - self.__timeout = int(timeout) - logSys.debug("Set action %s timeout = %i" % (self.__name, timeout)) - - ## - # Returns the action timeout period for commands. - # - # @return the timeout period in seconds - - def getTimeout(self): - return self.__timeout - - ## - # Sets a "CInfo". - # - # CInfo are statically defined properties. They can be definied by - # the user and are used to set e-mail addresses, port, host or - # anything that should not change during the life of the server. - # - # @param key the property name - # @param value the property value - - def setCInfo(self, key, value): - self.__cInfo[key] = value - - ## - # Returns a "CInfo". - # - # @param key the property name - - def getCInfo(self, key): - return self.__cInfo[key] - - ## - # Removes a "CInfo". - # - # @param key the property name - - def delCInfo(self, key): - del self.__cInfo[key] - - ## - # Set the "start" command. - # - # @param value the command - - def setActionStart(self, value): - self.__actionStart = value - logSys.debug("Set actionStart = %s" % value) - - ## - # Get the "start" command. - # - # @return the command - - def getActionStart(self): - return self.__actionStart - - ## - # Executes the action "start" command. - # - # Replaces the tags in the action command with value of "cInfo" - # and executes the resulting command. - # - # @return True if the command succeeded - - def execActionStart(self): - if self.__cInfo: - if not Action.substituteRecursiveTags(self.__cInfo): - logSys.error("Cinfo/definitions contain self referencing definitions and cannot be resolved") + `CallingMap` behaves similar to a standard python dictionary, + with the exception that any values which are callable, are called + and the result is returned as the value. + No error handling is in place, such that any errors raised in the + callable will raised as usual. + Actual dictionary is stored in property `data`, and can be accessed + to obtain original callable values. + + Attributes + ---------- + data : dict + The dictionary data which can be accessed to obtain items + without callable values being called. + + """ + + def __init__(self, *args, **kwargs): + self.data = dict(*args, **kwargs) + + def __getitem__(self, key): + value = self.data[key] + if callable(value): + return value() + else: + return value + + def __setitem__(self, key, value): + self.data[key] = value + + def __delitem__(self, key): + del self.data[key] + + def __iter__(self): + return iter(self.data) + + def __len__(self): + return len(self.data) + +class ActionBase(object): + """An abstract base class for actions in Fail2Ban. + + Action Base is a base definition of what methods need to be in + place to create a Python based action for Fail2Ban. This class can + be inherited from to ease implementation. + Required methods: + - __init__(jail, name) + - start() + - stop() + - ban(aInfo) + - unban(aInfo) + """ + __metaclass__ = ABCMeta + + @classmethod + def __subclasshook__(cls, C): + required = ( + "start", + "stop", + "ban", + "unban", + ) + for method in required: + if not callable(getattr(C, method, None)): return False - startCmd = Action.replaceTag(self.__actionStart, self.__cInfo) - return Action.executeCmd(startCmd, self.__timeout) - - ## - # Set the "ban" command. - # - # @param value the command - - def setActionBan(self, value): - self.__actionBan = value - logSys.debug("Set actionBan = %s" % value) - - ## - # Get the "ban" command. - # - # @return the command - - def getActionBan(self): - return self.__actionBan - - ## - # Executes the action "ban" command. - # - # @return True if the command succeeded - - def execActionBan(self, aInfo): - return self.__processCmd(self.__actionBan, aInfo) - - ## - # Set the "unban" command. - # - # @param value the command - - def setActionUnban(self, value): - self.__actionUnban = value - logSys.debug("Set actionUnban = %s" % value) - - ## - # Get the "unban" command. - # - # @return the command - - def getActionUnban(self): - return self.__actionUnban - - ## - # Executes the action "unban" command. - # - # @return True if the command succeeded - - def execActionUnban(self, aInfo): - return self.__processCmd(self.__actionUnban, aInfo) - - ## - # Set the "check" command. - # - # @param value the command - - def setActionCheck(self, value): - self.__actionCheck = value - logSys.debug("Set actionCheck = %s" % value) - - ## - # Get the "check" command. - # - # @return the command - - def getActionCheck(self): - return self.__actionCheck - - ## - # Set the "stop" command. - # - # @param value the command - - def setActionStop(self, value): - self.__actionStop = value - logSys.debug("Set actionStop = %s" % value) - - ## - # Get the "stop" command. - # - # @return the command - - def getActionStop(self): - return self.__actionStop - - ## - # Executes the action "stop" command. - # - # Replaces the tags in the action command with value of "cInfo" - # and executes the resulting command. - # - # @return True if the command succeeded - - def execActionStop(self): - stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo) - return Action.executeCmd(stopCmd, self.__timeout) + return True - ## - # Sort out tag definitions within other tags - # - # so: becomes: - # a = 3 a = 3 - # b = _3 b = 3_3 - # @param tags, a dictionary - # @returns tags altered or False if there is a recursive definition - #@staticmethod + def __init__(self, jail, name): + """Initialise action. + + Called when action is created, but before the jail/actions is + started. This should carry out necessary methods to initialise + the action but not "start" the action. + + Parameters + ---------- + jail : Jail + The jail in which the action belongs to. + name : str + Name assigned to the action. + + Notes + ----- + Any additional arguments specified in `jail.conf` or passed + via `fail2ban-client` will be passed as keyword arguments. + """ + self._jail = jail + self._name = name + self._logSys = logging.getLogger( + '%s.%s' % (__name__, self.__class__.__name__)) + + def start(self): + """Executed when the jail/action is started. + """ + pass + + def stop(self): + """Executed when the jail/action is stopped. + """ + pass + + def ban(self, aInfo): + """Executed when a ban occurs. + + Parameters + ---------- + aInfo : dict + Dictionary which includes information in relation to + the ban. + """ + pass + + def unban(self, aInfo): + """Executed when a ban expires. + + Parameters + ---------- + aInfo : dict + Dictionary which includes information in relation to + the ban. + """ + pass + +class CommandAction(ActionBase): + """A action which executes OS shell commands. + + This is the default type of action which Fail2Ban uses. + """ + + def __init__(self, jail, name): + """Initialise action. + + Default sets all commands for actions as empty string, such + no command is executed. + + Parameters + ---------- + jail : Jail + The jail in which the action belongs to. + name : str + Name assigned to the action. + """ + + super(CommandAction, self).__init__(jail, name) + self.timeout = 60 + ## Command executed in order to initialize the system. + self.actionstart = '' + ## Command executed when an IP address gets banned. + self.actionban = '' + ## Command executed when an IP address gets removed. + self.actionunban = '' + ## Command executed in order to check requirements. + self.actioncheck = '' + ## Command executed in order to stop the system. + self.actionstop = '' + self._logSys.debug("Created %s" % self.__class__) + + @classmethod + def __subclasshook__(cls, C): + return NotImplemented # Standard checks + + @property + def timeout(self): + """Time out period in seconds for execution of commands. + """ + return self._timeout + + @timeout.setter + def timeout(self, timeout): + self._timeout = int(timeout) + self._logSys.debug("Set action %s timeout = %i" % + (self._name, self.timeout)) + + @property + def _properties(self): + """A dictionary of the actions properties. + + This is used to subsitute "tags" in the commands. + """ + return dict( + (key, getattr(self, key)) + for key in dir(self) + if not key.startswith("_") and not callable(getattr(self, key))) + + @property + def actionstart(self): + """The command executed on start of the jail/action. + """ + return self._actionstart + + @actionstart.setter + def actionstart(self, value): + self._actionstart = value + self._logSys.debug("Set actionstart = %s" % value) + + def start(self): + """Executes the "actionstart" command. + + Replace the tags in the action command with actions properties + and executes the resulting command. + """ + if (self._properties and + not self.substituteRecursiveTags(self._properties)): + self._logSys.error( + "properties contain self referencing definitions " + "and cannot be resolved") + raise RuntimeError("Error starting action") + startCmd = self.replaceTag(self.actionstart, self._properties) + if not self.executeCmd(startCmd, self.timeout): + raise RuntimeError("Error starting action") + + @property + def actionban(self): + """The command used when a ban occurs. + """ + return self._actionban + + @actionban.setter + def actionban(self, value): + self._actionban = value + self._logSys.debug("Set actionban = %s" % value) + + def ban(self, aInfo): + """Executes the "actionban" command. + + Replaces the tags in the action command with actions properties + and ban information, and executes the resulting command. + + Parameters + ---------- + aInfo : dict + Dictionary which includes information in relation to + the ban. + """ + if not self._processCmd(self.actionban, aInfo): + raise RuntimeError("Error banning %(ip)s" % aInfo) + + @property + def actionunban(self): + """The command used when an unban occurs. + """ + return self._actionunban + + @actionunban.setter + def actionunban(self, value): + self._actionunban = value + self._logSys.debug("Set actionunban = %s" % value) + + def unban(self, aInfo): + """Executes the "actionunban" command. + + Replaces the tags in the action command with actions properties + and ban information, and executes the resulting command. + + Parameters + ---------- + aInfo : dict + Dictionary which includes information in relation to + the ban. + """ + if not self._processCmd(self.actionunban, aInfo): + raise RuntimeError("Error unbanning %(ip)s" % aInfo) + + @property + def actioncheck(self): + """The command used to check the environment. + + This is used prior to a ban taking place to ensure the + environment is appropriate. If this check fails, `stop` and + `start` is executed prior to the check being called again. + """ + return self._actioncheck + + @actioncheck.setter + def actioncheck(self, value): + self._actioncheck = value + self._logSys.debug("Set actioncheck = %s" % value) + + @property + def actionstop(self): + """The command executed when the jail/actions stops. + """ + return self._actionstop + + @actionstop.setter + def actionstop(self, value): + self._actionstop = value + self._logSys.debug("Set actionstop = %s" % value) + + def stop(self): + """Executes the "actionstop" command. + + Replaces the tags in the action command with actions properties + and executes the resulting command. + """ + stopCmd = self.replaceTag(self.actionstop, self._properties) + if not self.executeCmd(stopCmd, self.timeout): + raise RuntimeError("Error stopping action") + + @staticmethod def substituteRecursiveTags(tags): + """Sort out tag definitions within other tags. + + so: becomes: + a = 3 a = 3 + b = _3 b = 3_3 + + Parameters + ---------- + tags : dict + Dictionary of tags(keys) and their values. + + Returns + ------- + dict + Dictionary of tags(keys) and their values, with tags + within the values recursively replaced. + """ t = re.compile(r'<([^ >]+)>') for tag, value in tags.iteritems(): value = str(value) @@ -291,98 +387,130 @@ class Action: m = t.search(value, m.start() + 1) tags[tag] = value return tags - substituteRecursiveTags = staticmethod(substituteRecursiveTags) - #@staticmethod - def escapeTag(tag): + @staticmethod + def escapeTag(value): + """Escape characters which may be used for command injection. + + Parameters + ---------- + value : str + A string of which characters will be escaped. + + Returns + ------- + str + `value` with certain characters escaped. + + Notes + ----- + The following characters are escaped:: + + \\#&;`|*?~<>^()[]{}$'" + + """ for c in '\\#&;`|*?~<>^()[]{}$\'"': - if c in tag: - tag = tag.replace(c, '\\' + c) - return tag - escapeTag = staticmethod(escapeTag) + if c in value: + value = value.replace(c, '\\' + c) + return value - ## - # Replaces tags in query with property values in aInfo. - # - # @param query the query string with tags - # @param aInfo the properties - # @return a string - - #@staticmethod - def replaceTag(query, aInfo): - """ Replace tags in query + @classmethod + def replaceTag(cls, query, aInfo): + """Replaces tags in `query` with property values. + + Parameters + ---------- + query : str + String with tags. + aInfo : dict + Tags(keys) and associated values for substitution in query. + + Returns + ------- + str + `query` string with tags replaced. """ string = query - for tag, value in aInfo.iteritems(): + for tag in aInfo: if "<%s>" % tag in query: - if callable(value): - value = value() - value = str(value) # assure string + value = str(aInfo[tag]) # assure string if tag.endswith('matches'): # That one needs to be escaped since its content is # out of our control - value = Action.escapeTag(value) + value = cls.escapeTag(value) string = string.replace('<' + tag + '>', value) # New line string = string.replace("
", '\n') return string - replaceTag = staticmethod(replaceTag) - - ## - # Executes a command with preliminary checks and substitutions. - # - # Before executing any commands, executes the "check" command first - # in order to check if pre-requirements are met. If this check fails, - # it tries to restore a sane environment before executing the real - # command. - # Replaces "aInfo" and "cInfo" in the query too. - # - # @param cmd The command to execute - # @param aInfo Dynamic properties - # @return True if the command succeeded - - def __processCmd(self, cmd, aInfo = None): - """ Executes an OS command. + + def _processCmd(self, cmd, aInfo = None): + """Executes a command with preliminary checks and substitutions. + + Before executing any commands, executes the "check" command first + in order to check if pre-requirements are met. If this check fails, + it tries to restore a sane environment before executing the real + command. + + Parameters + ---------- + cmd : str + The command to execute. + aInfo : dictionary + Dynamic properties. + + Returns + ------- + bool + True if the command succeeded. """ if cmd == "": - logSys.debug("Nothing to do") + self._logSys.debug("Nothing to do") return True - - checkCmd = Action.replaceTag(self.__actionCheck, self.__cInfo) - if not Action.executeCmd(checkCmd, self.__timeout): - logSys.error("Invariant check failed. Trying to restore a sane" + - " environment") - self.execActionStop() - self.execActionStart() - if not Action.executeCmd(checkCmd, self.__timeout): - logSys.fatal("Unable to restore environment") + + checkCmd = self.replaceTag(self.actioncheck, self._properties) + if not self.executeCmd(checkCmd, self.timeout): + self._logSys.error( + "Invariant check failed. Trying to restore a sane environment") + self.stop() + self.start() + if not self.executeCmd(checkCmd, self.timeout): + self._logSys.fatal("Unable to restore environment") return False # Replace tags if not aInfo is None: - realCmd = Action.replaceTag(cmd, aInfo) + realCmd = self.replaceTag(cmd, aInfo) else: realCmd = cmd # Replace static fields - realCmd = Action.replaceTag(realCmd, self.__cInfo) + realCmd = self.replaceTag(realCmd, self._properties) - return Action.executeCmd(realCmd, self.__timeout) + return self.executeCmd(realCmd, self.timeout) - ## - # Executes a command. - # - # We need a shell here because commands are mainly shell script. They - # contain pipe, redirection, etc. - # - # @todo Force the use of bash!? - # @todo Kill the command after a given timeout - # - # @param realCmd the command to execute - # @return True if the command succeeded - - #@staticmethod + @staticmethod def executeCmd(realCmd, timeout=60): + """Executes a command. + + Parameters + ---------- + realCmd : str + The command to execute. + timeout : int + The time out in seconds for the command. + + Returns + ------- + bool + True if the command succeeded. + + Raises + ------ + OSError + If command fails to be executed. + RuntimeError + If command execution times out. + """ logSys.debug(realCmd) if not realCmd: logSys.debug("Nothing to do") @@ -412,7 +540,6 @@ class Action: retcode = popen.poll() except OSError, e: logSys.error("%s -- failed with %s" % (realCmd, e)) - return False finally: _cmd_lock.release() @@ -439,6 +566,6 @@ class Action: if msg: logSys.info("HINT on %i: %s" % (retcode, msg % locals())) - return False - executeCmd = staticmethod(executeCmd) + return False + raise RuntimeError("Command execution failed: %s" % realCmd) diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index af2b4670..2426747d 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -24,94 +24,113 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -from banmanager import BanManager -from jailthread import JailThread -from action import Action -from mytime import MyTime import time, logging +import os +import imp +from collections import Mapping + +from .banmanager import BanManager +from .jailthread import JailThread +from .action import ActionBase, CommandAction, CallingMap +from .mytime import MyTime # Gets the instance of the logger. logSys = logging.getLogger(__name__) -## -# Execute commands. -# -# This class reads the failures from the Jail queue and decide if an -# action has to be taken. A BanManager take care of the banned IP -# addresses. +class Actions(JailThread, Mapping): + """Handles jail actions. + + This class handles the actions of the jail. Creation, deletion or to + actions must be done through this class. This class is based on the + Mapping type, and the `add` method must be used to add new actions. + This class also starts and stops the actions, and fetches bans from + the jail executing these bans via the actions. + """ -class Actions(JailThread): - - ## - # Constructor. - # - # Initialize the filter object with default values. - # @param jail the jail object - def __init__(self, jail): + """Initialise an empty Actions instance. + + Parameters + ---------- + jail: Jail + The jail of which the actions belongs to. + """ JailThread.__init__(self) ## The jail which contains this action. - self.jail = jail - self.__actions = list() + self._jail = jail + self._actions = dict() ## The ban manager. self.__banManager = BanManager() - - ## - # Adds an action. - # - # @param name The action name - - def addAction(self, name): + + def add(self, name, pythonModule=None, initOpts=None): + """Adds a new action. + + Add a new action if not already present, defaulting to standard + `CommandAction`, or specified Python module. + + Parameters + ---------- + name : str + The name of the action. + pythonModule : str, optional + Path to Python file which must contain `Action` class. + Default None, which means `CommandAction` is used. + initOpts : dict, optional + Options for Python Action, used as keyword arguments for + initialisation. Default None. + + Raises + ------ + ValueError + If action name already exists. + RuntimeError + If external Python module does not have `Action` class + or does not implement necessary methods as per `ActionBase` + abstract class. + """ # Check is action name already exists - if name in [action.getName() for action in self.__actions]: + if name in self._actions: raise ValueError("Action %s already exists" % name) - action = Action(name) - self.__actions.append(action) - - ## - # Removes an action. - # - # @param name The action name - - def delAction(self, name): - for action in self.__actions: - if action.getName() == name: - self.__actions.remove(action) - return - raise KeyError("Invalid Action name: %s" % name) - - ## - # Returns an action. - # - # Raises a KeyError exception if the action does not exist. - # - # @param name the action name - # @return the action - - def getAction(self, name): - for action in self.__actions: - if action.getName() == name: - return action - raise KeyError("Invalid Action name") - - ## - # Returns the last defined action. - # - # @return The last defined action. - - def getLastAction(self): - action = self.__actions.pop() - self.__actions.append(action) - return action - - ## - # Returns the list of actions - # - # @return list of actions - - def getActions(self): - return self.__actions - + if pythonModule is None: + action = CommandAction(self._jail, name) + else: + pythonModuleName = os.path.basename(pythonModule.strip(".py")) + customActionModule = imp.load_source( + pythonModuleName, pythonModule) + if not hasattr(customActionModule, "Action"): + raise RuntimeError( + "%s module does not have 'Action' class" % pythonModule) + elif not issubclass(customActionModule.Action, ActionBase): + raise RuntimeError( + "%s module %s does not implement required methods" % ( + pythonModule, customActionModule.Action.__name__)) + action = customActionModule.Action(self._jail, name, **initOpts) + self._actions[name] = action + + def __getitem__(self, name): + try: + return self._actions[name] + except KeyError: + raise KeyError("Invalid Action name: %s" % name) + + def __delitem__(self, name): + try: + del self._actions[name] + except KeyError: + raise KeyError("Invalid Action name: %s" % name) + + def __iter__(self): + return iter(self._actions) + + def __len__(self): + return len(self._actions) + + def __eq__(self, other): # Required for Threading + return False + + def __hash__(self): # Required for Threading + return id(self) + ## # Set the ban time. # @@ -128,34 +147,52 @@ class Actions(JailThread): def getBanTime(self): return self.__banManager.getBanTime() - - ## - # Remove a banned IP now, rather than waiting for it to expire, even if set to never expire. - # - # @return the IP string or 'None' if not unbanned. + def removeBannedIP(self, ip): + """Removes banned IP calling actions' unban method + + Remove a banned IP now, rather than waiting for it to expire, + even if set to never expire. + + Parameters + ---------- + ip : str + The IP address to unban + + Raises + ------ + ValueError + If `ip` is not banned + """ # Find the ticket with the IP. ticket = self.__banManager.getTicketByIP(ip) if ticket is not None: # Unban the IP. self.__unBan(ticket) - return ip - raise ValueError("IP %s is not banned" % ip) + else: + raise ValueError("IP %s is not banned" % ip) - ## - # Main loop. - # - # This function is the main loop of the thread. It checks the Jail - # queue and executes commands when an IP address is banned. - # @return True when the thread exits nicely - def run(self): + """Main loop for Threading. + + This function is the main loop of the thread. It checks the jail + queue and executes commands when an IP address is banned. + + Returns + ------- + bool + True when the thread exits nicely. + """ self.setActive(True) - for action in self.__actions: - action.execActionStart() + for name, action in self._actions.iteritems(): + try: + action.start() + except Exception as e: + logSys.error("Failed to start jail '%s' action '%s': %s", + self._jail.getName(), name, e) while self._isActive(): if not self.getIdle(): - #logSys.debug(self.jail.getName() + ": action") + #logSys.debug(self._jail.getName() + ": action") ret = self.__checkBan() if not ret: self.__checkUnBan() @@ -163,94 +200,116 @@ class Actions(JailThread): else: time.sleep(self.getSleepTime()) self.__flushBan() - for action in self.__actions: - action.execActionStop() - logSys.debug(self.jail.getName() + ": action terminated") + for name, action in self._actions.iteritems(): + try: + action.stop() + except Exception as e: + logSys.error("Failed to stop jail '%s' action '%s': %s", + self._jail.getName(), name, e) + logSys.debug(self._jail.getName() + ": action terminated") return True - ## - # Check for IP address to ban. - # - # Look in the Jail queue for FailTicket. If a ticket is available, - # it executes the "ban" command and add a ticket to the BanManager. - # @return True if an IP address get banned - def __checkBan(self): - ticket = self.jail.getFailTicket() + """Check for IP address to ban. + + Look in the jail queue for FailTicket. If a ticket is available, + it executes the "ban" command and adds a ticket to the BanManager. + + Returns + ------- + bool + True if an IP address get banned. + """ + ticket = self._jail.getFailTicket() if ticket != False: - aInfo = dict() + aInfo = CallingMap() bTicket = BanManager.createBanTicket(ticket) aInfo["ip"] = bTicket.getIP() aInfo["failures"] = bTicket.getAttempt() aInfo["time"] = bTicket.getTime() aInfo["matches"] = "\n".join(bTicket.getMatches()) - if self.jail.getDatabase() is not None: + if self._jail.getDatabase() is not None: aInfo["ipmatches"] = lambda: "\n".join( - self.jail.getDatabase().getBansMerged( + self._jail.getDatabase().getBansMerged( ip=bTicket.getIP()).getMatches()) aInfo["ipjailmatches"] = lambda: "\n".join( - self.jail.getDatabase().getBansMerged( - ip=bTicket.getIP(), jail=self.jail).getMatches()) + self._jail.getDatabase().getBansMerged( + ip=bTicket.getIP(), jail=self._jail).getMatches()) aInfo["ipfailures"] = lambda: "\n".join( - self.jail.getDatabase().getBansMerged( + self._jail.getDatabase().getBansMerged( ip=bTicket.getIP()).getAttempt()) aInfo["ipjailfailures"] = lambda: "\n".join( - self.jail.getDatabase().getBansMerged( - ip=bTicket.getIP(), jail=self.jail).getAttempt()) + self._jail.getDatabase().getBansMerged( + ip=bTicket.getIP(), jail=self._jail).getAttempt()) if self.__banManager.addBanTicket(bTicket): - logSys.warning("[%s] Ban %s" % (self.jail.getName(), aInfo["ip"])) - for action in self.__actions: - action.execActionBan(aInfo) + logSys.warning("[%s] Ban %s" % (self._jail.getName(), aInfo["ip"])) + for name, action in self._actions.iteritems(): + try: + action.ban(aInfo) + except Exception as e: + logSys.error( + "Failed to execute ban jail '%s' action '%s': %s", + self._jail.getName(), name, e) return True else: - logSys.info("[%s] %s already banned" % (self.jail.getName(), + logSys.info("[%s] %s already banned" % (self._jail.getName(), aInfo["ip"])) return False - - ## - # Check for IP address to unban. - # - # Unban IP address which are outdated. - + def __checkUnBan(self): + """Check for IP address to unban. + + Unban IP addresses which are outdated. + """ for ticket in self.__banManager.unBanList(MyTime.time()): self.__unBan(ticket) - - ## - # Flush the ban list. - # - # Unban all IP address which are still in the banning list. - + def __flushBan(self): + """Flush the ban list. + + Unban all IP address which are still in the banning list. + """ logSys.debug("Flush ban list") for ticket in self.__banManager.flushBanList(): self.__unBan(ticket) - - ## - # Unbans host corresponding to the ticket. - # - # Executes the actions in order to unban the host given in the - # ticket. - + def __unBan(self, ticket): + """Unbans host corresponding to the ticket. + + Executes the actions in order to unban the host given in the + ticket. + + Parameters + ---------- + ticket : FailTicket + Ticket of failures of which to unban + """ aInfo = dict() aInfo["ip"] = ticket.getIP() aInfo["failures"] = ticket.getAttempt() aInfo["time"] = ticket.getTime() aInfo["matches"] = "".join(ticket.getMatches()) - logSys.warning("[%s] Unban %s" % (self.jail.getName(), aInfo["ip"])) - for action in self.__actions: - action.execActionUnban(aInfo) - - - ## - # Get the status of the filter. - # - # Get some informations about the filter state such as the total - # number of failures. - # @return a list with tuple - + logSys.warning("[%s] Unban %s" % (self._jail.getName(), aInfo["ip"])) + for name, action in self._actions.iteritems(): + try: + action.unban(aInfo) + except Exception as e: + logSys.error( + "Failed to execute unban jail '%s' action '%s': %s", + self._jail.getName(), name, e) + def status(self): + """Get the status of the filter. + + Get some informations about the filter state such as the total + number of failures. + + Returns + ------- + list + List of tuple pairs, each containing a description and value + for general status information. + """ ret = [("Currently banned", self.__banManager.size()), ("Total banned", self.__banManager.getBanTotal()), ("IP list", self.__banManager.getBanList())] diff --git a/fail2ban/server/asyncserver.py b/fail2ban/server/asyncserver.py index 7f4aae58..56704510 100644 --- a/fail2ban/server/asyncserver.py +++ b/fail2ban/server/asyncserver.py @@ -27,7 +27,7 @@ __license__ = "GPL" from pickle import dumps, loads, HIGHEST_PROTOCOL import asyncore, asynchat, socket, os, logging, sys, traceback, fcntl -from fail2ban import helpers +from .. import helpers # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index ef3bf204..fc9eb948 100644 --- a/fail2ban/server/banmanager.py +++ b/fail2ban/server/banmanager.py @@ -24,10 +24,11 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -from ticket import BanTicket -from threading import Lock -from mytime import MyTime import logging +from threading import Lock + +from .ticket import BanTicket +from .mytime import MyTime # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index b0d4200c..32e4f173 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -29,8 +29,8 @@ import json import locale from functools import wraps -from fail2ban.server.mytime import MyTime -from fail2ban.server.ticket import FailTicket +from .mytime import MyTime +from .ticket import FailTicket # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/datedetector.py b/fail2ban/server/datedetector.py index 002a1070..64123a34 100644 --- a/fail2ban/server/datedetector.py +++ b/fail2ban/server/datedetector.py @@ -22,10 +22,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import sys, time, logging - -from datetemplate import DatePatternRegex, DateTai64n, DateEpoch, DateISO8601 from threading import Lock +from .datetemplate import DatePatternRegex, DateTai64n, DateEpoch, DateISO8601 + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/datetemplate.py b/fail2ban/server/datetemplate.py index 320d9dc0..729b951c 100644 --- a/fail2ban/server/datetemplate.py +++ b/fail2ban/server/datetemplate.py @@ -25,14 +25,13 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import re, time, calendar - +import logging from datetime import datetime from datetime import timedelta -from mytime import MyTime -import iso8601 +from .mytime import MyTime +from . import iso8601 -import logging logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index f021a46d..548e6adb 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -24,11 +24,12 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -from faildata import FailData -from ticket import FailTicket from threading import Lock import logging +from .faildata import FailData +from .ticket import FailTicket + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index aa534b73..b4d214d1 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -21,18 +21,17 @@ __author__ = "Cyril Jaquier and Fail2Ban Contributors" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __license__ = "GPL" -from failmanager import FailManagerEmpty -from failmanager import FailManager -from ticket import FailTicket -from jailthread import JailThread -from datedetector import DateDetector -from datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n -from mytime import MyTime -from failregex import FailRegex, Regex, RegexException -from action import Action - import logging, re, os, fcntl, time, sys, locale, codecs +from .failmanager import FailManagerEmpty, FailManager +from .ticket import FailTicket +from .jailthread import JailThread +from .datedetector import DateDetector +from .datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n +from .mytime import MyTime +from .failregex import FailRegex, Regex, RegexException +from .action import CommandAction + # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -378,9 +377,9 @@ class Filter(JailThread): return True if self.__ignoreCommand: - command = Action.replaceTag(self.__ignoreCommand, { 'ip': ip } ) + command = CommandAction.replaceTag(self.__ignoreCommand, { 'ip': ip } ) logSys.debug('ignore command: ' + command) - return Action.executeCmd(command) + return CommandAction.executeCmd(command) return False diff --git a/fail2ban/server/filtergamin.py b/fail2ban/server/filtergamin.py index b48bdd3c..6cc9c45b 100644 --- a/fail2ban/server/filtergamin.py +++ b/fail2ban/server/filtergamin.py @@ -23,11 +23,13 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012 Yaroslav Halchenko" __license__ = "GPL" -from failmanager import FailManagerEmpty -from filter import FileFilter -from mytime import MyTime +import time, logging, fcntl -import time, logging, gamin, fcntl +import gamin + +from .failmanager import FailManagerEmpty +from .filter import FileFilter +from .mytime import MyTime # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/filterpoll.py b/fail2ban/server/filterpoll.py index 7fba65e1..d2f553b2 100644 --- a/fail2ban/server/filterpoll.py +++ b/fail2ban/server/filterpoll.py @@ -24,12 +24,12 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier; 2012 Yaroslav Halchenko" __license__ = "GPL" -from failmanager import FailManagerEmpty -from filter import FileFilter -from mytime import MyTime - import time, logging, os +from .failmanager import FailManagerEmpty +from .filter import FileFilter +from .mytime import MyTime + # Gets the instance of the logger. logSys = logging.getLogger(__name__) diff --git a/fail2ban/server/filterpyinotify.py b/fail2ban/server/filterpyinotify.py index 8a83e13a..9ab8a83a 100644 --- a/fail2ban/server/filterpyinotify.py +++ b/fail2ban/server/filterpyinotify.py @@ -24,13 +24,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Y __license__ = "GPL" import time, logging, pyinotify - from distutils.version import LooseVersion from os.path import dirname, sep as pathsep -from failmanager import FailManagerEmpty -from filter import FileFilter -from mytime import MyTime +from .failmanager import FailManagerEmpty +from .filter import FileFilter +from .mytime import MyTime if not hasattr(pyinotify, '__version__') \ diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py index 180cb84b..d39ba280 100644 --- a/fail2ban/server/filtersystemd.py +++ b/fail2ban/server/filtersystemd.py @@ -29,9 +29,9 @@ from systemd import journal if LooseVersion(getattr(journal, '__version__', "0")) < '204': raise ImportError("Fail2Ban requires systemd >= 204") -from failmanager import FailManagerEmpty -from filter import JournalFilter -from mytime import MyTime +from .failmanager import FailManagerEmpty +from .filter import JournalFilter +from .mytime import MyTime # Gets the instance of the logger. diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index c7faa4d9..bdd8e89a 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -25,7 +25,7 @@ __license__ = "GPL" import Queue, logging -from actions import Actions +from .actions import Actions # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -71,7 +71,7 @@ class Jail: "%r was requested" % (b, backend)) else: logSys.info("Initiated %r backend" % b) - self.__action = Actions(self) + self.__actions = Actions(self) return # we are done except ImportError, e: logSys.debug( @@ -123,11 +123,17 @@ class Jail: def getDatabase(self): return self.__db - def getFilter(self): + @property + def filter(self): + """The filter which the jail is using to monitor log files. + """ return self.__filter - - def getAction(self): - return self.__action + + @property + def actions(self): + """Actions object used to manage actions for jail. + """ + return self.__actions def putFailTicket(self, ticket): self.__queue.put(ticket) @@ -142,36 +148,36 @@ class Jail: def start(self): self.__filter.start() - self.__action.start() + self.__actions.start() # Restore any previous valid bans from the database if self.__db is not None: for ticket in self.__db.getBans( - jail=self, bantime=self.__action.getBanTime()): + jail=self, bantime=self.__actions.getBanTime()): self.__queue.put(ticket) logSys.info("Jail '%s' started" % self.__name) def stop(self): self.__filter.stop() - self.__action.stop() + self.__actions.stop() self.__filter.join() - self.__action.join() + self.__actions.join() logSys.info("Jail '%s' stopped" % self.__name) def isAlive(self): isAlive0 = self.__filter.isAlive() - isAlive1 = self.__action.isAlive() + isAlive1 = self.__actions.isAlive() return isAlive0 or isAlive1 def setIdle(self, value): self.__filter.setIdle(value) - self.__action.setIdle(value) + self.__actions.setIdle(value) def getIdle(self): - return self.__filter.getIdle() or self.__action.getIdle() + return self.__filter.getIdle() or self.__actions.getIdle() def getStatus(self): fStatus = self.__filter.status() - aStatus = self.__action.status() + aStatus = self.__actions.status() ret = [("filter", fStatus), ("action", aStatus)] return ret diff --git a/fail2ban/server/jails.py b/fail2ban/server/jails.py index 12569d7f..7fe7a231 100644 --- a/fail2ban/server/jails.py +++ b/fail2ban/server/jails.py @@ -21,137 +21,84 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko" __license__ = "GPL" -from fail2ban.exceptions import DuplicateJailException, UnknownJailException - -from jail import Jail from threading import Lock +from collections import Mapping -## -# Handles the jails. -# -# This class handles the jails. Creation, deletion or access to a jail must be -# done through this class. This class is thread-safe which is not the case of -# the jail itself, including filter and actions. +from ..exceptions import DuplicateJailException, UnknownJailException +from .jail import Jail + + +class Jails(Mapping): + """Handles the jails. + + This class handles the jails. Creation, deletion or access to a jail + must be done through this class. This class is thread-safe which is + not the case of the jail itself, including filter and actions. This + class is based on Mapping type, and the `add` method must be used to + add additional jails. + """ -class Jails: - - ## - # Constructor. - def __init__(self): + """Initialise an empty Jails instance. + """ self.__lock = Lock() - self.__jails = dict() - - ## - # Adds a jail. - # - # Adds a new jail which should use the given backend. Raises a - # DuplicateJailException if the jail is already defined. - # @param name The name of the jail - # @param backend The backend to use - + self._jails = dict() + def add(self, name, backend, db=None): + """Adds a jail. + + Adds a new jail if not already present which should use the + given backend. + + Parameters + ---------- + name : str + The name of the jail. + backend : str + The backend to use. + + Raises + ------ + DuplicateJailException + If jail name is already present. + """ try: self.__lock.acquire() - if self.__jails.has_key(name): + if name in self._jails: raise DuplicateJailException(name) else: - self.__jails[name] = Jail(name, backend, db) - finally: - self.__lock.release() - - ## - # Removes a jail. - # - # Removes the jail name. Raise an UnknownJailException - # if the jail does not exist. - # @param name The name of the jail - - def remove(self, name): - try: - self.__lock.acquire() - if self.__jails.has_key(name): - del self.__jails[name] - else: - raise UnknownJailException(name) - finally: - self.__lock.release() - - ## - # Returns a jail. - # - # Returns the jail name. Raise an UnknownJailException - # if the jail does not exist. - # @param name The name of the jail - - def get(self, name): - try: - self.__lock.acquire() - if self.__jails.has_key(name): - jail = self.__jails[name] - return jail - else: - raise UnknownJailException(name) - finally: - self.__lock.release() - - ## - # Returns an action class instance. - # - # Returns the action object of the jail name. Raise an - # UnknownJailException if the jail does not exist. - # @param name The name of the jail - - def getAction(self, name): - try: - self.__lock.acquire() - if self.__jails.has_key(name): - action = self.__jails[name].getAction() - return action - else: - raise UnknownJailException(name) - finally: - self.__lock.release() - - ## - # Returns a filter class instance. - # - # Returns the filter object of the jail name. Raise an - # UnknownJailException if the jail does not exist. - # @param name The name of the jail - - def getFilter(self, name): - try: - self.__lock.acquire() - if self.__jails.has_key(name): - action = self.__jails[name].getFilter() - return action - else: - raise UnknownJailException(name) - finally: - self.__lock.release() - - ## - # Returns the jails. - # - # Returns a copy of the jails list. - - def getAll(self): - try: - self.__lock.acquire() - return self.__jails.copy() - finally: - self.__lock.release() - - ## - # Returns the size of the jails. - # - # Returns the number of jails. - - def size(self): - try: - self.__lock.acquire() - return len(self.__jails) + self._jails[name] = Jail(name, backend, db) finally: self.__lock.release() + def __getitem__(self, name): + try: + self.__lock.acquire() + return self._jails[name] + except KeyError: + raise UnknownJailException(name) + finally: + self.__lock.release() + + def __delitem__(self, name): + try: + self.__lock.acquire() + del self._jails[name] + except KeyError: + raise UnknownJailException(name) + finally: + self.__lock.release() + + def __len__(self): + try: + self.__lock.acquire() + return len(self._jails) + finally: + self.__lock.release() + + def __iter__(self): + try: + self.__lock.acquire() + return iter(self._jails) + finally: + self.__lock.release() diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 08e41003..3d2b54f7 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -25,15 +25,16 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" from threading import Lock, RLock -from jails import Jails -from filter import FileFilter, JournalFilter -from transmitter import Transmitter -from asyncserver import AsyncServer -from asyncserver import AsyncServerException -from database import Fail2BanDb -from fail2ban import version import logging, logging.handlers, sys, os, signal +from .jails import Jails +from .filter import FileFilter, JournalFilter +from .transmitter import Transmitter +from .asyncserver import AsyncServer, AsyncServerException +from .database import Fail2BanDb +from .action import CommandAction +from .. import version + # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -121,10 +122,10 @@ class Server: def addJail(self, name, backend): self.__jails.add(name, backend, self.__db) if self.__db is not None: - self.__db.addJail(self.__jails.get(name)) + self.__db.addJail(self.__jails[name]) def delJail(self, name): - self.__jails.remove(name) + del self.__jails[name] if self.__db is not None: self.__db.delJailName(name) @@ -132,7 +133,7 @@ class Server: try: self.__lock.acquire() if not self.isAlive(name): - self.__jails.get(name).start() + self.__jails[name].start() finally: self.__lock.release() @@ -141,7 +142,7 @@ class Server: try: self.__lock.acquire() if self.isAlive(name): - self.__jails.get(name).stop() + self.__jails[name].stop() self.delJail(name) finally: self.__lock.release() @@ -150,43 +151,43 @@ class Server: logSys.info("Stopping all jails") try: self.__lock.acquire() - for jail in self.__jails.getAll(): + for jail in self.__jails.keys(): self.stopJail(jail) finally: self.__lock.release() def isAlive(self, name): - return self.__jails.get(name).isAlive() + return self.__jails[name].isAlive() def setIdleJail(self, name, value): - self.__jails.get(name).setIdle(value) + self.__jails[name].setIdle(value) return True def getIdleJail(self, name): - return self.__jails.get(name).getIdle() + return self.__jails[name].getIdle() # Filter def addIgnoreIP(self, name, ip): - self.__jails.getFilter(name).addIgnoreIP(ip) + self.__jails[name].filter.addIgnoreIP(ip) def delIgnoreIP(self, name, ip): - self.__jails.getFilter(name).delIgnoreIP(ip) + self.__jails[name].filter.delIgnoreIP(ip) def getIgnoreIP(self, name): - return self.__jails.getFilter(name).getIgnoreIP() + return self.__jails[name].filter.getIgnoreIP() def addLogPath(self, name, fileName, tail=False): - filter_ = self.__jails.getFilter(name) + filter_ = self.__jails[name].filter if isinstance(filter_, FileFilter): filter_.addLogPath(fileName, tail) def delLogPath(self, name, fileName): - filter_ = self.__jails.getFilter(name) + filter_ = self.__jails[name].filter if isinstance(filter_, FileFilter): filter_.delLogPath(fileName) def getLogPath(self, name): - filter_ = self.__jails.getFilter(name) + filter_ = self.__jails[name].filter if isinstance(filter_, FileFilter): return [m.getFileName() for m in filter_.getLogPath()] @@ -195,17 +196,17 @@ class Server: return [] def addJournalMatch(self, name, match): # pragma: systemd no cover - filter_ = self.__jails.getFilter(name) + filter_ = self.__jails[name].filter if isinstance(filter_, JournalFilter): filter_.addJournalMatch(match) def delJournalMatch(self, name, match): # pragma: systemd no cover - filter_ = self.__jails.getFilter(name) + filter_ = self.__jails[name].filter if isinstance(filter_, JournalFilter): filter_.delJournalMatch(match) def getJournalMatch(self, name): # pragma: systemd no cover - filter_ = self.__jails.getFilter(name) + filter_ = self.__jails[name].filter if isinstance(filter_, JournalFilter): return filter_.getJournalMatch() else: @@ -213,154 +214,109 @@ class Server: return [] def setLogEncoding(self, name, encoding): - filter_ = self.__jails.getFilter(name) + filter_ = self.__jails[name].filter if isinstance(filter_, FileFilter): filter_.setLogEncoding(encoding) def getLogEncoding(self, name): - filter_ = self.__jails.getFilter(name) + filter_ = self.__jails[name].filter if isinstance(filter_, FileFilter): return filter_.getLogEncoding() def setFindTime(self, name, value): - self.__jails.getFilter(name).setFindTime(value) + self.__jails[name].filter.setFindTime(value) def getFindTime(self, name): - return self.__jails.getFilter(name).getFindTime() + return self.__jails[name].filter.getFindTime() def setDatePattern(self, name, pattern): - self.__jails.getFilter(name).setDatePattern(pattern) + self.__jails[name].filter.setDatePattern(pattern) def getDatePattern(self, name): - return self.__jails.getFilter(name).getDatePattern() + return self.__jails[name].filter.getDatePattern() def setIgnoreCommand(self, name, value): - self.__jails.getFilter(name).setIgnoreCommand(value) + self.__jails[name].filter.setIgnoreCommand(value) def getIgnoreCommand(self, name): - return self.__jails.getFilter(name).getIgnoreCommand() + return self.__jails[name].filter.getIgnoreCommand() def addFailRegex(self, name, value): - self.__jails.getFilter(name).addFailRegex(value) + self.__jails[name].filter.addFailRegex(value) def delFailRegex(self, name, index): - self.__jails.getFilter(name).delFailRegex(index) + self.__jails[name].filter.delFailRegex(index) def getFailRegex(self, name): - return self.__jails.getFilter(name).getFailRegex() + return self.__jails[name].filter.getFailRegex() def addIgnoreRegex(self, name, value): - self.__jails.getFilter(name).addIgnoreRegex(value) + self.__jails[name].filter.addIgnoreRegex(value) def delIgnoreRegex(self, name, index): - self.__jails.getFilter(name).delIgnoreRegex(index) + self.__jails[name].filter.delIgnoreRegex(index) def getIgnoreRegex(self, name): - return self.__jails.getFilter(name).getIgnoreRegex() + return self.__jails[name].filter.getIgnoreRegex() def setUseDns(self, name, value): - self.__jails.getFilter(name).setUseDns(value) + self.__jails[name].filter.setUseDns(value) def getUseDns(self, name): - return self.__jails.getFilter(name).getUseDns() + return self.__jails[name].filter.getUseDns() def setMaxRetry(self, name, value): - self.__jails.getFilter(name).setMaxRetry(value) + self.__jails[name].filter.setMaxRetry(value) def getMaxRetry(self, name): - return self.__jails.getFilter(name).getMaxRetry() + return self.__jails[name].filter.getMaxRetry() def setMaxLines(self, name, value): - self.__jails.getFilter(name).setMaxLines(value) + self.__jails[name].filter.setMaxLines(value) def getMaxLines(self, name): - return self.__jails.getFilter(name).getMaxLines() + return self.__jails[name].filter.getMaxLines() # Action - def addAction(self, name, value): - self.__jails.getAction(name).addAction(value) - - def getLastAction(self, name): - return self.__jails.getAction(name).getLastAction() + def addAction(self, name, value, *args): + self.__jails[name].actions.add(value, *args) def getActions(self, name): - return self.__jails.getAction(name).getActions() + return self.__jails[name].actions def delAction(self, name, value): - self.__jails.getAction(name).delAction(value) + del self.__jails[name].actions[value] - def setCInfo(self, name, action, key, value): - self.__jails.getAction(name).getAction(action).setCInfo(key, value) - - def getCInfo(self, name, action, key): - return self.__jails.getAction(name).getAction(action).getCInfo(key) - - def delCInfo(self, name, action, key): - self.__jails.getAction(name).getAction(action).delCInfo(key) + def getAction(self, name, value): + return self.__jails[name].actions[value] def setBanTime(self, name, value): - self.__jails.getAction(name).setBanTime(value) + self.__jails[name].actions.setBanTime(value) def setBanIP(self, name, value): - return self.__jails.getFilter(name).addBannedIP(value) + return self.__jails[name].filter.addBannedIP(value) def setUnbanIP(self, name, value): - return self.__jails.getAction(name).removeBannedIP(value) + self.__jails[name].actions.removeBannedIP(value) def getBanTime(self, name): - return self.__jails.getAction(name).getBanTime() + return self.__jails[name].actions.getBanTime() - def setActionStart(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionStart(value) - - def getActionStart(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionStart() - - def setActionStop(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionStop(value) - - def getActionStop(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionStop() - - def setActionCheck(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionCheck(value) - - def getActionCheck(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionCheck() - - def setActionBan(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionBan(value) - - def getActionBan(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionBan() - - def setActionUnban(self, name, action, value): - self.__jails.getAction(name).getAction(action).setActionUnban(value) - - def getActionUnban(self, name, action): - return self.__jails.getAction(name).getAction(action).getActionUnban() - - def setActionTimeout(self, name, action, value): - self.__jails.getAction(name).getAction(action).setTimeout(value) - - def getActionTimeout(self, name, action): - return self.__jails.getAction(name).getAction(action).getTimeout() - # Status def status(self): try: self.__lock.acquire() - jails = list(self.__jails.getAll()) + jails = list(self.__jails) jails.sort() jailList = ", ".join(jails) - ret = [("Number of jail", self.__jails.size()), + ret = [("Number of jail", len(self.__jails)), ("Jail list", jailList)] return ret finally: self.__lock.release() def statusJail(self, name): - return self.__jails.get(name).getStatus() + return self.__jails[name].getStatus() # Logging @@ -488,7 +444,7 @@ class Server: return "flushed" def setDatabase(self, filename): - if self.__jails.size() == 0: + if len(self.__jails) == 0: if filename.lower() == "none": self.__db = None else: diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 5af19c5a..0a955883 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -25,6 +25,7 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import logging, time +import json # Gets the instance of the logger. logSys = logging.getLogger(__name__) @@ -226,56 +227,29 @@ class Transmitter: return self.__server.setBanIP(name,value) elif command[1] == "unbanip": value = command[2] - return self.__server.setUnbanIP(name,value) + self.__server.setUnbanIP(name, value) + return value elif command[1] == "addaction": - value = command[2] - self.__server.addAction(name, value) - return self.__server.getLastAction(name).getName() + args = [command[2]] + if len(command) > 3: + args.extend([command[3], json.loads(command[4])]) + self.__server.addAction(name, *args) + return args[0] elif command[1] == "delaction": value = command[2] self.__server.delAction(name, value) return None - elif command[1] == "setcinfo": - act = command[2] - key = command[3] - value = " ".join(command[4:]) - self.__server.setCInfo(name, act, key, value) - return self.__server.getCInfo(name, act, key) - elif command[1] == "delcinfo": - act = command[2] - key = command[3] - self.__server.delCInfo(name, act, key) - return None - elif command[1] == "actionstart": - act = command[2] - value = " ".join(command[3:]) - self.__server.setActionStart(name, act, value) - return self.__server.getActionStart(name, act) - elif command[1] == "actionstop": - act = command[2] - value = " ".join(command[3:]) - self.__server.setActionStop(name, act, value) - return self.__server.getActionStop(name, act) - elif command[1] == "actioncheck": - act = command[2] - value = " ".join(command[3:]) - self.__server.setActionCheck(name, act, value) - return self.__server.getActionCheck(name, act) - elif command[1] == "actionban": - act = command[2] - value = " ".join(command[3:]) - self.__server.setActionBan(name, act, value) - return self.__server.getActionBan(name, act) - elif command[1] == "actionunban": - act = command[2] - value = " ".join(command[3:]) - self.__server.setActionUnban(name, act, value) - return self.__server.getActionUnban(name, act) - elif command[1] == "timeout": - act = command[2] - value = int(command[3]) - self.__server.setActionTimeout(name, act, value) - return self.__server.getActionTimeout(name, act) + elif command[1] == "action": + actionname = command[2] + actionkey = command[3] + action = self.__server.getAction(name, actionname) + if callable(getattr(action, actionkey, None)): + actionvalue = json.loads(command[4]) if len(command)>4 else {} + return getattr(action, actionkey)(**actionvalue) + else: + actionvalue = command[4] + setattr(action, actionkey, actionvalue) + return getattr(action, actionkey) raise Exception("Invalid command (no set action or not yet implemented)") def __commandGet(self, command): @@ -327,31 +301,25 @@ class Transmitter: elif command[1] == "bantime": return self.__server.getBanTime(name) elif command[1] == "actions": - return self.__server.getActions(name) - elif command[1] == "addaction": - return self.__server.getLastAction(name).getName() - elif command[1] == "actionstart": - act = command[2] - return self.__server.getActionStart(name, act) - elif command[1] == "actionstop": - act = command[2] - return self.__server.getActionStop(name, act) - elif command[1] == "actioncheck": - act = command[2] - return self.__server.getActionCheck(name, act) - elif command[1] == "actionban": - act = command[2] - return self.__server.getActionBan(name, act) - elif command[1] == "actionunban": - act = command[2] - return self.__server.getActionUnban(name, act) - elif command[1] == "cinfo": - act = command[2] - key = command[3] - return self.__server.getCInfo(name, act, key) - elif command[1] == "timeout": - act = command[2] - return self.__server.getActionTimeout(name, act) + return self.__server.getActions(name).keys() + elif command[1] == "action": + actionname = command[2] + actionvalue = command[3] + action = self.__server.getAction(name, actionname) + return getattr(action, actionvalue) + elif command[1] == "actionproperties": + actionname = command[2] + action = self.__server.getAction(name, actionname) + return [ + key for key in dir(action) + if not key.startswith("_") and + not callable(getattr(action, key))] + elif command[1] == "actionmethods": + actionname = command[2] + action = self.__server.getAction(name, actionname) + return [ + key for key in dir(action) + if not key.startswith("_") and callable(getattr(action, key))] raise Exception("Invalid command (no get action or not yet implemented)") def status(self, command): diff --git a/fail2ban/tests/actionstestcase.py b/fail2ban/tests/actionstestcase.py index 5697db37..ccc0263d 100644 --- a/fail2ban/tests/actionstestcase.py +++ b/fail2ban/tests/actionstestcase.py @@ -26,39 +26,49 @@ __license__ = "GPL" import unittest, time import sys, os, tempfile -from fail2ban.server.actions import Actions -from dummyjail import DummyJail -class ExecuteActions(unittest.TestCase): +from ..server.actions import Actions +from .dummyjail import DummyJail +from .utils import LogCaptureTestCase + +TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") + +class ExecuteActions(LogCaptureTestCase): def setUp(self): """Call before every test case.""" + super(ExecuteActions, self).setUp() self.__jail = DummyJail() self.__actions = Actions(self.__jail) self.__tmpfile, self.__tmpfilename = tempfile.mkstemp() def tearDown(self): + super(ExecuteActions, self).tearDown() os.remove(self.__tmpfilename) def defaultActions(self): - self.__actions.addAction('ip') - self.__ip = self.__actions.getAction('ip') - self.__ip.setActionStart('echo ip start 64 >> "%s"' % self.__tmpfilename ) - self.__ip.setActionBan('echo ip ban >> "%s"' % self.__tmpfilename ) - self.__ip.setActionUnban('echo ip unban >> "%s"' % self.__tmpfilename ) - self.__ip.setActionCheck('echo ip check >> "%s"' % self.__tmpfilename ) - self.__ip.setActionStop('echo ip stop >> "%s"' % self.__tmpfilename ) + self.__actions.add('ip') + self.__ip = self.__actions['ip'] + self.__ip.actionstart = 'echo ip start 64 >> "%s"' % self.__tmpfilename + self.__ip.actionban = 'echo ip ban >> "%s"' % self.__tmpfilename + self.__ip.actionunban = 'echo ip unban >> "%s"' % self.__tmpfilename + self.__ip.actioncheck = 'echo ip check >> "%s"' % self.__tmpfilename + self.__ip.actionstop = 'echo ip stop >> "%s"' % self.__tmpfilename + + def testActionsAddDuplicateName(self): + self.__actions.add('test') + self.assertRaises(ValueError, self.__actions.add, 'test') def testActionsManipulation(self): - self.__actions.addAction('test') - self.assertTrue(self.__actions.getAction('test')) - self.assertTrue(self.__actions.getLastAction()) - self.assertRaises(KeyError,self.__actions.getAction,*['nonexistant action']) - self.__actions.addAction('test1') - self.__actions.delAction('test') - self.__actions.delAction('test1') - self.assertRaises(KeyError, self.__actions.getAction, *['test']) - self.assertRaises(IndexError,self.__actions.getLastAction) + self.__actions.add('test') + self.assertTrue(self.__actions['test']) + self.assertTrue('test' in self.__actions) + self.assertFalse('nonexistant action' in self.__actions) + self.__actions.add('test1') + del self.__actions['test'] + del self.__actions['test1'] + self.assertFalse('test' in self.__actions) + self.assertEqual(len(self.__actions), 0) self.__actions.setBanTime(127) self.assertEqual(self.__actions.getBanTime(),127) @@ -77,3 +87,55 @@ class ExecuteActions(unittest.TestCase): self.assertEqual(self.__actions.status(),[("Currently banned", 0 ), ("Total banned", 0 ), ("IP list", [] )]) + + def testAddActionPython(self): + self.__actions.add( + "Action", os.path.join(TEST_FILES_DIR, "action.d/action.py"), + {'opt1': 'value'}) + + self.assertTrue(self._is_logged("TestAction initialised")) + + self.__actions.start() + time.sleep(3) + self.assertTrue(self._is_logged("TestAction action start")) + + self.__actions.stop() + self.__actions.join() + self.assertTrue(self._is_logged("TestAction action stop")) + + self.assertRaises(IOError, + self.__actions.add, "Action3", "/does/not/exist.py", {}) + + # With optional argument + self.__actions.add( + "Action4", os.path.join(TEST_FILES_DIR, "action.d/action.py"), + {'opt1': 'value', 'opt2': 'value2'}) + # With too many arguments + self.assertRaises( + TypeError, self.__actions.add, "Action5", + os.path.join(TEST_FILES_DIR, "action.d/action.py"), + {'opt1': 'value', 'opt2': 'value2', 'opt3': 'value3'}) + # Missing required argument + self.assertRaises( + TypeError, self.__actions.add, "Action5", + os.path.join(TEST_FILES_DIR, "action.d/action.py"), {}) + + def testAddPythonActionNOK(self): + self.assertRaises(RuntimeError, self.__actions.add, + "Action", os.path.join(TEST_FILES_DIR, + "action.d/action_noAction.py"), + {}) + self.assertRaises(RuntimeError, self.__actions.add, + "Action", os.path.join(TEST_FILES_DIR, + "action.d/action_nomethod.py"), + {}) + self.__actions.add( + "Action", os.path.join(TEST_FILES_DIR, + "action.d/action_errors.py"), + {}) + self.__actions.start() + time.sleep(3) + self.assertTrue(self._is_logged("Failed to start")) + self.__actions.stop() + self.__actions.join() + self.assertTrue(self._is_logged("Failed to stop")) diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 36ecc3c9..a0503fae 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -27,27 +27,22 @@ __license__ = "GPL" import time import logging, sys -from fail2ban.server.action import Action +from ..server.action import CommandAction, CallingMap -from fail2ban.tests.utils import LogCaptureTestCase +from .utils import LogCaptureTestCase -class ExecuteAction(LogCaptureTestCase): +class CommandActionTest(LogCaptureTestCase): def setUp(self): """Call before every test case.""" - self.__action = Action("Test") + self.__action = CommandAction(None, "Test") LogCaptureTestCase.setUp(self) def tearDown(self): """Call after every test case.""" LogCaptureTestCase.tearDown(self) - self.__action.execActionStop() + self.__action.stop() - def testNameChange(self): - self.assertEqual(self.__action.getName(), "Test") - self.__action.setName("Tricky Test") - self.assertEqual(self.__action.getName(), "Tricky Test") - def testSubstituteRecursiveTags(self): aInfo = { 'HOST': "192.0.2.0", @@ -55,15 +50,15 @@ class ExecuteAction(LogCaptureTestCase): 'xyz': "890 ", } # Recursion is bad - self.assertFalse(Action.substituteRecursiveTags({'A': '
'})) - self.assertFalse(Action.substituteRecursiveTags({'A': '', 'B': ''})) - self.assertFalse(Action.substituteRecursiveTags({'A': '', 'B': '', 'C': ''})) + self.assertFalse(CommandAction.substituteRecursiveTags({'A': ''})) + self.assertFalse(CommandAction.substituteRecursiveTags({'A': '', 'B': ''})) + self.assertFalse(CommandAction.substituteRecursiveTags({'A': '', 'B': '', 'C': ''})) # missing tags are ok - self.assertEqual(Action.substituteRecursiveTags({'A': ''}), {'A': ''}) - self.assertEqual(Action.substituteRecursiveTags({'A': ' ','X':'fun'}), {'A': ' fun', 'X':'fun'}) - self.assertEqual(Action.substituteRecursiveTags({'A': ' ', 'B': 'cool'}), {'A': ' cool', 'B': 'cool'}) + self.assertEqual(CommandAction.substituteRecursiveTags({'A': ''}), {'A': ''}) + self.assertEqual(CommandAction.substituteRecursiveTags({'A': ' ','X':'fun'}), {'A': ' fun', 'X':'fun'}) + self.assertEqual(CommandAction.substituteRecursiveTags({'A': ' ', 'B': 'cool'}), {'A': ' cool', 'B': 'cool'}) # rest is just cool - self.assertEqual(Action.substituteRecursiveTags(aInfo), + self.assertEqual(CommandAction.substituteRecursiveTags(aInfo), { 'HOST': "192.0.2.0", 'ABC': '123 192.0.2.0', 'xyz': '890 123 192.0.2.0', @@ -99,90 +94,102 @@ class ExecuteAction(LogCaptureTestCase): # Callable self.assertEqual( - self.__action.replaceTag("09 11", - {'callable': lambda: str(10)}), + self.__action.replaceTag("09 11", + CallingMap(callme=lambda: str(10))), "09 10 11") # As tag not present, therefore callable should not be called # Will raise ValueError if it is self.assertEqual( self.__action.replaceTag("abc", - {'callable': lambda: int("a")}), "abc") + CallingMap(callme=lambda: int("a"))), "abc") def testExecuteActionBan(self): - self.__action.setActionStart("touch /tmp/fail2ban.test") - self.assertEqual(self.__action.getActionStart(), "touch /tmp/fail2ban.test") - self.__action.setActionStop("rm -f /tmp/fail2ban.test") - self.assertEqual(self.__action.getActionStop(), 'rm -f /tmp/fail2ban.test') - self.__action.setActionBan("echo -n") - self.assertEqual(self.__action.getActionBan(), 'echo -n') - self.__action.setActionCheck("[ -e /tmp/fail2ban.test ]") - self.assertEqual(self.__action.getActionCheck(), '[ -e /tmp/fail2ban.test ]') - self.__action.setActionUnban("true") - self.assertEqual(self.__action.getActionUnban(), 'true') + self.__action.actionstart = "touch /tmp/fail2ban.test" + self.assertEqual(self.__action.actionstart, "touch /tmp/fail2ban.test") + self.__action.actionstop = "rm -f /tmp/fail2ban.test" + self.assertEqual(self.__action.actionstop, 'rm -f /tmp/fail2ban.test') + self.__action.actionban = "echo -n" + self.assertEqual(self.__action.actionban, 'echo -n') + self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]" + self.assertEqual(self.__action.actioncheck, '[ -e /tmp/fail2ban.test ]') + self.__action.actionunban = "true" + self.assertEqual(self.__action.actionunban, 'true') self.assertFalse(self._is_logged('returned')) # no action was actually executed yet - self.assertTrue(self.__action.execActionBan(None)) + self.__action.ban({'ip': None}) self.assertTrue(self._is_logged('Invariant check failed')) self.assertTrue(self._is_logged('returned successfully')) def testExecuteActionEmptyUnban(self): - self.__action.setActionUnban("") - self.assertTrue(self.__action.execActionUnban(None)) + self.__action.actionunban = "" + self.__action.unban({}) self.assertTrue(self._is_logged('Nothing to do')) def testExecuteActionStartCtags(self): - self.__action.setCInfo("HOST","192.0.2.0") - self.__action.setActionStart("touch /tmp/fail2ban.test.") - self.__action.setActionStop("rm -f /tmp/fail2ban.test.") - self.__action.setActionCheck("[ -e /tmp/fail2ban.test.192.0.2.0 ]") - self.assertTrue(self.__action.execActionStart()) + self.__action.HOST = "192.0.2.0" + self.__action.actionstart = "touch /tmp/fail2ban.test." + self.__action.actionstop = "rm -f /tmp/fail2ban.test." + self.__action.actioncheck = "[ -e /tmp/fail2ban.test.192.0.2.0 ]" + self.__action.start() def testExecuteActionCheckRestoreEnvironment(self): - self.__action.setActionStart("") - self.__action.setActionStop("rm -f /tmp/fail2ban.test") - self.__action.setActionBan("rm /tmp/fail2ban.test") - self.__action.setActionCheck("[ -e /tmp/fail2ban.test ]") - self.assertFalse(self.__action.execActionBan(None)) + self.__action.actionstart = "" + self.__action.actionstop = "rm -f /tmp/fail2ban.test" + self.__action.actionban = "rm /tmp/fail2ban.test" + self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]" + self.assertRaises(RuntimeError, self.__action.ban, {'ip': None}) self.assertTrue(self._is_logged('Unable to restore environment')) def testExecuteActionChangeCtags(self): - self.__action.setCInfo("ROST","192.0.2.0") - self.assertEqual(self.__action.getCInfo("ROST"),"192.0.2.0") - self.__action.delCInfo("ROST") - self.assertRaises(KeyError, self.__action.getCInfo, "ROST") + self.assertRaises(AttributeError, getattr, self.__action, "ROST") + self.__action.ROST = "192.0.2.0" + self.assertEqual(self.__action.ROST,"192.0.2.0") def testExecuteActionUnbanAinfo(self): aInfo = { 'ABC': "123", } - self.__action.setActionBan("touch /tmp/fail2ban.test.123") - self.__action.setActionUnban("rm /tmp/fail2ban.test.") - self.assertTrue(self.__action.execActionBan(None)) - self.assertTrue(self.__action.execActionUnban(aInfo)) + self.__action.actionban = "touch /tmp/fail2ban.test.123" + self.__action.actionunban = "rm /tmp/fail2ban.test." + self.__action.ban(aInfo) + self.__action.unban(aInfo) def testExecuteActionStartEmpty(self): - self.__action.setActionStart("") - self.assertTrue(self.__action.execActionStart()) + self.__action.actionstart = "" + self.__action.start() self.assertTrue(self._is_logged('Nothing to do')) def testExecuteIncorrectCmd(self): - Action.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null') + CommandAction.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null') self.assertTrue(self._is_logged('HINT on 127: "Command not found"')) def testExecuteTimeout(self): stime = time.time() - Action.executeCmd('sleep 60', timeout=2) # Should take a minute + # Should take a minute + self.assertRaises( + RuntimeError, CommandAction.executeCmd, 'sleep 60', timeout=2) self.assertAlmostEqual(time.time() - stime, 2, places=0) self.assertTrue(self._is_logged('sleep 60 -- timed out after 2 seconds')) self.assertTrue(self._is_logged('sleep 60 -- killed with SIGTERM')) def testCaptureStdOutErr(self): - Action.executeCmd('echo "How now brown cow"') + CommandAction.executeCmd('echo "How now brown cow"') self.assertTrue(self._is_logged("'How now brown cow\\n'")) - Action.executeCmd( + CommandAction.executeCmd( 'echo "The rain in Spain stays mainly in the plain" 1>&2') self.assertTrue(self._is_logged( "'The rain in Spain stays mainly in the plain\\n'")) + + def testCallingMap(self): + mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'), + dontcallme= "string", number=17) + + # Should work fine + self.assertEqual( + "%(callme)s okay %(dontcallme)s %(number)i" % mymap, + "10 okay string 17") + # Error will now trip, demonstrating delayed call + self.assertRaises(ValueError, lambda x: "%(error)i" % x, mymap) diff --git a/fail2ban/tests/banmanagertestcase.py b/fail2ban/tests/banmanagertestcase.py index aff16883..7dcb73a7 100644 --- a/fail2ban/tests/banmanagertestcase.py +++ b/fail2ban/tests/banmanagertestcase.py @@ -26,8 +26,8 @@ __license__ = "GPL" import unittest -from fail2ban.server.banmanager import BanManager -from fail2ban.server.ticket import BanTicket +from ..server.banmanager import BanManager +from ..server.ticket import BanTicket class AddFailure(unittest.TestCase): diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 9b2ccee8..4f197b13 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -23,13 +23,13 @@ __license__ = "GPL" import os, shutil, sys, tempfile, unittest -from fail2ban.client.configreader import ConfigReader -from fail2ban.client.jailreader import JailReader -from fail2ban.client.filterreader import FilterReader -from fail2ban.client.jailsreader import JailsReader -from fail2ban.client.actionreader import ActionReader -from fail2ban.client.configurator import Configurator -from fail2ban.tests.utils import LogCaptureTestCase +from ..client.configreader import ConfigReader +from ..client.jailreader import JailReader +from ..client.filterreader import FilterReader +from ..client.jailsreader import JailsReader +from ..client.actionreader import ActionReader +from ..client.configurator import Configurator +from .utils import LogCaptureTestCase TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") if os.path.exists(os.path.join('config','fail2ban.conf')): @@ -356,13 +356,18 @@ class JailsReaderTest(LogCaptureTestCase): ['set', 'brokenaction', 'addaction', 'brokenaction'], ['set', 'brokenaction', - 'actionban', + 'action', 'brokenaction', + 'actionban', 'hit with big stick '], - ['set', 'brokenaction', 'actionstop', 'brokenaction', ''], - ['set', 'brokenaction', 'actionstart', 'brokenaction', ''], - ['set', 'brokenaction', 'actionunban', 'brokenaction', ''], - ['set', 'brokenaction', 'actioncheck', 'brokenaction', ''], + ['set', 'brokenaction', 'action', 'brokenaction', + 'actionstop', ''], + ['set', 'brokenaction', 'action', 'brokenaction', + 'actionstart', ''], + ['set', 'brokenaction', 'action', 'brokenaction', + 'actionunban', ''], + ['set', 'brokenaction', 'action', 'brokenaction', + 'actioncheck', ''], ['add', 'parse_to_end_of_jail.conf', 'auto'], ['set', 'parse_to_end_of_jail.conf', 'usedns', 'warn'], ['set', 'parse_to_end_of_jail.conf', 'maxretry', 3], @@ -489,7 +494,7 @@ class JailsReaderTest(LogCaptureTestCase): self.assertTrue('blocktype' in action._initOpts) # Verify that we have a call to set it up blocktype_present = False - target_command = [ 'set', jail_name, 'setcinfo', action_name, 'blocktype' ] + target_command = ['set', jail_name, 'action', action_name, 'blocktype'] for command in commands: if (len(command) > 5 and command[:5] == target_command): @@ -542,6 +547,8 @@ class JailsReaderTest(LogCaptureTestCase): [testjail1] action = testaction1[actname=test1] testaction1[actname=test2] + testaction.py + testaction.py[actname=test3] filter = testfilter1 """) jailfd.close() @@ -550,8 +557,12 @@ filter = testfilter1 self.assertTrue(jails.getOptions()) comm_commands = jails.convert(allow_no_files=True) - action_names = [comm[-1] for comm in comm_commands if comm[:3] == ['set', 'testjail1', 'addaction']] + add_actions = [comm[3:] for comm in comm_commands + if comm[:3] == ['set', 'testjail1', 'addaction']] - self.assertNotEqual(len(set(action_names)), 1) + self.assertEqual(len(set(action[0] for action in add_actions)), 4) + + # Python actions should not be passed `actname` + self.assertEqual(add_actions[-1][-1], "{}") shutil.rmtree(basedir) diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index 1a2ac053..577f732f 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -28,11 +28,11 @@ import tempfile import sqlite3 import shutil -from fail2ban.server.database import Fail2BanDb -from fail2ban.server.filter import FileContainer -from fail2ban.server.mytime import MyTime -from fail2ban.server.ticket import FailTicket -from fail2ban.tests.dummyjail import DummyJail +from ..server.database import Fail2BanDb +from ..server.filter import FileContainer +from ..server.mytime import MyTime +from ..server.ticket import FailTicket +from .dummyjail import DummyJail class DatabaseTest(unittest.TestCase): diff --git a/fail2ban/tests/datedetectortestcase.py b/fail2ban/tests/datedetectortestcase.py index 301f9e5b..0077446f 100644 --- a/fail2ban/tests/datedetectortestcase.py +++ b/fail2ban/tests/datedetectortestcase.py @@ -25,10 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" import unittest, calendar, time, datetime, re, pprint -from fail2ban.server.datedetector import DateDetector -from fail2ban.server.datetemplate import DateTemplate -from fail2ban.server.iso8601 import Utc -from fail2ban.tests.utils import setUpMyTime, tearDownMyTime +from ..server.datedetector import DateDetector +from ..server.datetemplate import DateTemplate +from ..server.iso8601 import Utc +from .utils import setUpMyTime, tearDownMyTime class DateDetectorTest(unittest.TestCase): diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index fb452b01..5f31181d 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -26,8 +26,8 @@ __license__ = "GPL" import unittest, socket, time, pickle -from fail2ban.server.failmanager import FailManager, FailManagerEmpty -from fail2ban.server.ticket import FailTicket +from ..server.failmanager import FailManager, FailManagerEmpty +from ..server.ticket import FailTicket class AddFailure(unittest.TestCase): diff --git a/fail2ban/tests/files/action.d/action.py b/fail2ban/tests/files/action.d/action.py new file mode 100644 index 00000000..2dd64c3f --- /dev/null +++ b/fail2ban/tests/files/action.d/action.py @@ -0,0 +1,28 @@ + +from fail2ban.server.action import ActionBase + +class TestAction(ActionBase): + + def __init__(self, jail, name, opt1, opt2=None): + super(TestAction, self).__init__(jail, name) + self._logSys.debug("%s initialised" % self.__class__.__name__) + self.opt1 = opt1 + self.opt2 = opt2 + self._opt3 = "Hello" + + def start(self): + self._logSys.debug("%s action start" % self.__class__.__name__) + + def stop(self): + self._logSys.debug("%s action stop" % self.__class__.__name__) + + def ban(self, aInfo): + self._logSys.debug("%s action ban" % self.__class__.__name__) + + def unban(self, aInfo): + self._logSys.debug("%s action unban" % self.__class__.__name__) + + def testmethod(self, text): + return "%s %s %s" % (self._opt3, text, self.opt1) + +Action = TestAction diff --git a/fail2ban/tests/files/action.d/action_errors.py b/fail2ban/tests/files/action.d/action_errors.py new file mode 100644 index 00000000..767848c1 --- /dev/null +++ b/fail2ban/tests/files/action.d/action_errors.py @@ -0,0 +1,21 @@ + +from fail2ban.server.action import ActionBase + +class TestAction(ActionBase): + + def __init__(self, jail, name): + super(TestAction, self).__init__(jail, name) + + def start(self): + raise Exception() + + def stop(self): + raise Exception() + + def ban(self): + raise Exception() + + def unban(self): + raise Exception() + +Action = TestAction diff --git a/fail2ban/tests/files/action.d/action_noAction.py b/fail2ban/tests/files/action.d/action_noAction.py new file mode 100644 index 00000000..1aa25e67 --- /dev/null +++ b/fail2ban/tests/files/action.d/action_noAction.py @@ -0,0 +1,5 @@ + +from fail2ban.server.action import ActionBase + +class TestAction(ActionBase): + pass diff --git a/fail2ban/tests/files/action.d/action_nomethod.py b/fail2ban/tests/files/action.d/action_nomethod.py new file mode 100644 index 00000000..8b9fd981 --- /dev/null +++ b/fail2ban/tests/files/action.d/action_nomethod.py @@ -0,0 +1,12 @@ + +from fail2ban.server.action import ActionBase + +class TestAction(): + + def __init__(self, jail, name): + pass + + def start(self): + pass + +Action = TestAction diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 7f94a9f1..02dfba2f 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -34,19 +34,16 @@ try: except ImportError: journal = None -from fail2ban.server.jail import Jail -from fail2ban.server.filterpoll import FilterPoll -from fail2ban.server.filter import Filter, FileFilter, DNSUtils -from fail2ban.server.failmanager import FailManager -from fail2ban.server.failmanager import FailManagerEmpty -from fail2ban.server.mytime import MyTime -from fail2ban.tests.utils import setUpMyTime, tearDownMyTime -from fail2ban.tests.utils import mtimesleep, LogCaptureTestCase +from ..server.jail import Jail +from ..server.filterpoll import FilterPoll +from ..server.filter import Filter, FileFilter, DNSUtils +from ..server.failmanager import FailManager, FailManagerEmpty +from ..server.mytime import MyTime +from .utils import setUpMyTime, tearDownMyTime, mtimesleep, LogCaptureTestCase +from .dummyjail import DummyJail TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") -from fail2ban.tests.dummyjail import DummyJail - # yoh: per Steven Hiscocks's insight while troubleshooting # https://github.com/fail2ban/fail2ban/issues/103#issuecomment-15542836 # adding a sufficiently large buffer might help to guarantee that diff --git a/fail2ban/tests/misctestcase.py b/fail2ban/tests/misctestcase.py index 7c6e3283..04822e41 100644 --- a/fail2ban/tests/misctestcase.py +++ b/fail2ban/tests/misctestcase.py @@ -28,8 +28,8 @@ import shutil from glob import glob -from utils import mbasename, TraceBack, FormatterWithTraceBack -from fail2ban.helpers import formatExceptionInfo +from .utils import mbasename, TraceBack, FormatterWithTraceBack +from ..helpers import formatExceptionInfo class HelpersTest(unittest.TestCase): diff --git a/fail2ban/tests/samplestestcase.py b/fail2ban/tests/samplestestcase.py index 78120df5..657d3d82 100644 --- a/fail2ban/tests/samplestestcase.py +++ b/fail2ban/tests/samplestestcase.py @@ -30,9 +30,9 @@ else: import simplejson as json next = lambda x: x.next() -from fail2ban.server.filter import Filter -from fail2ban.client.filterreader import FilterReader -from fail2ban.tests.utils import setUpMyTime, tearDownMyTime +from ..server.filter import Filter +from ..client.filterreader import FilterReader +from .utils import setUpMyTime, tearDownMyTime TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") if os.path.exists('config/fail2ban.conf'): diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 66fc5b7c..dd3c740c 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -26,11 +26,11 @@ __license__ = "GPL" import unittest, socket, time, tempfile, os, locale, sys, logging -from fail2ban.server.failregex import Regex, FailRegex, RegexException -from fail2ban.server.server import Server, logSys -from fail2ban.server.jail import Jail -from fail2ban.exceptions import UnknownJailException -from fail2ban.tests.utils import LogCaptureTestCase +from ..server.failregex import Regex, FailRegex, RegexException +from ..server.server import Server, logSys +from ..server.jail import Jail +from ..exceptions import UnknownJailException +from .utils import LogCaptureTestCase #from bin.fail2ban-client import Fail2banClient try: from fail2ban.server import filtersystemd @@ -518,45 +518,38 @@ class Transmitter(TransmitterBase): self.assertEqual( self.transm.proceed(["set", self.jailName, "addaction", action]), (0, action)) - self.assertEqual( - self.transm.proceed(["get", self.jailName, "addaction"]), - (0, action)) self.assertEqual( self.transm.proceed( - ["get", self.jailName, "actions"])[1][0].getName(), + ["get", self.jailName, "actions"])[1][0], action) for cmd, value in zip(cmdList, cmdValueList): self.assertEqual( self.transm.proceed( - ["set", self.jailName, cmd, action, value]), + ["set", self.jailName, "action", action, cmd, value]), (0, value)) for cmd, value in zip(cmdList, cmdValueList): self.assertEqual( - self.transm.proceed(["get", self.jailName, cmd, action]), + self.transm.proceed(["get", self.jailName, "action", action, cmd]), (0, value)) self.assertEqual( self.transm.proceed( - ["set", self.jailName, "setcinfo", action, "KEY", "VALUE"]), + ["set", self.jailName, "action", action, "KEY", "VALUE"]), (0, "VALUE")) self.assertEqual( self.transm.proceed( - ["get", self.jailName, "cinfo", action, "KEY"]), + ["get", self.jailName, "action", action, "KEY"]), (0, "VALUE")) self.assertEqual( self.transm.proceed( - ["get", self.jailName, "cinfo", action, "InvalidKey"])[0], + ["get", self.jailName, "action", action, "InvalidKey"])[0], 1) self.assertEqual( self.transm.proceed( - ["set", self.jailName, "delcinfo", action, "KEY"]), - (0, None)) - self.assertEqual( - self.transm.proceed( - ["set", self.jailName, "timeout", action, "10"]), + ["set", self.jailName, "action", action, "timeout", "10"]), (0, 10)) self.assertEqual( self.transm.proceed( - ["get", self.jailName, "timeout", action]), + ["get", self.jailName, "action", action, "timeout"]), (0, 10)) self.assertEqual( self.transm.proceed(["set", self.jailName, "delaction", action]), @@ -565,6 +558,42 @@ class Transmitter(TransmitterBase): self.transm.proceed( ["set", self.jailName, "delaction", "Doesn't exist"])[0],1) + def testPythonActionMethodsAndProperties(self): + action = "TestCaseAction" + self.assertEqual( + self.transm.proceed(["set", self.jailName, "addaction", action, + os.path.join(TEST_FILES_DIR, "action.d", "action.py"), + '{"opt1": "value"}']), + (0, action)) + self.assertEqual( + sorted(self.transm.proceed(["get", self.jailName, + "actionproperties", action])[1]), + ['opt1', 'opt2']) + self.assertEqual( + self.transm.proceed(["get", self.jailName, "action", action, + "opt1"]), + (0, 'value')) + self.assertEqual( + self.transm.proceed(["get", self.jailName, "action", action, + "opt2"]), + (0, None)) + self.assertEqual( + sorted(self.transm.proceed(["get", self.jailName, "actionmethods", + action])[1]), + ['ban', 'start', 'stop', 'testmethod', 'unban']) + self.assertEqual( + self.transm.proceed(["set", self.jailName, "action", action, + "testmethod", '{"text": "world!"}']), + (0, 'Hello world! value')) + self.assertEqual( + self.transm.proceed(["set", self.jailName, "action", action, + "opt1", "another value"]), + (0, 'another value')) + self.assertEqual( + self.transm.proceed(["set", self.jailName, "action", action, + "testmethod", '{"text": "world!"}']), + (0, 'Hello world! another value')) + def testNOK(self): self.assertEqual(self.transm.proceed(["INVALID", "COMMAND"])[0],1) diff --git a/fail2ban/tests/sockettestcase.py b/fail2ban/tests/sockettestcase.py index 413d8081..e668be39 100644 --- a/fail2ban/tests/sockettestcase.py +++ b/fail2ban/tests/sockettestcase.py @@ -26,8 +26,8 @@ __license__ = "GPL" import unittest, time, tempfile, os, threading -from fail2ban.server.asyncserver import AsyncServer, AsyncServerException -from fail2ban.client.csocket import CSocket +from ..server.asyncserver import AsyncServer, AsyncServerException +from ..client.csocket import CSocket class Socket(unittest.TestCase): diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index b1f26bab..fba308c4 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -25,16 +25,9 @@ __license__ = "GPL" import logging, os, re, traceback, time, unittest, sys from os.path import basename, dirname from StringIO import StringIO +import json -if sys.version_info >= (2, 6): - import json -else: - try: - import simplejson as json - except ImportError: - json = None - -from fail2ban.server.mytime import MyTime +from ..server.mytime import MyTime logSys = logging.getLogger(__name__) @@ -138,19 +131,18 @@ def tearDownMyTime(): def gatherTests(regexps=None, no_network=False): # Import all the test cases here instead of a module level to # avoid circular imports - from fail2ban.tests import banmanagertestcase - from fail2ban.tests import clientreadertestcase - from fail2ban.tests import failmanagertestcase - from fail2ban.tests import filtertestcase - from fail2ban.tests import servertestcase - from fail2ban.tests import datedetectortestcase - from fail2ban.tests import actiontestcase - from fail2ban.tests import actionstestcase - from fail2ban.tests import sockettestcase - from fail2ban.tests import misctestcase - from fail2ban.tests import databasetestcase - if json: - from fail2ban.tests import samplestestcase + from . import banmanagertestcase + from . import clientreadertestcase + from . import failmanagertestcase + from . import filtertestcase + from . import servertestcase + from . import datedetectortestcase + from . import actiontestcase + from . import actionstestcase + from . import sockettestcase + from . import misctestcase + from . import databasetestcase + from . import samplestestcase if not regexps: # pragma: no cover tests = unittest.TestSuite() @@ -172,7 +164,7 @@ def gatherTests(regexps=None, no_network=False): tests.addTest(unittest.makeSuite(servertestcase.Transmitter)) tests.addTest(unittest.makeSuite(servertestcase.JailTests)) tests.addTest(unittest.makeSuite(servertestcase.RegexTests)) - tests.addTest(unittest.makeSuite(actiontestcase.ExecuteAction)) + tests.addTest(unittest.makeSuite(actiontestcase.CommandActionTest)) tests.addTest(unittest.makeSuite(actionstestcase.ExecuteActions)) # FailManager tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure)) @@ -207,30 +199,27 @@ def gatherTests(regexps=None, no_network=False): # DateDetector tests.addTest(unittest.makeSuite(datedetectortestcase.DateDetectorTest)) - if json: - # Filter Regex tests with sample logs - tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex)) - else: - logSys.warning("I: Skipping filter samples testing. No simplejson/json module") + # Filter Regex tests with sample logs + tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex)) # # Extensive use-tests of different available filters backends # - from fail2ban.server.filterpoll import FilterPoll + from ..server.filterpoll import FilterPoll filters = [FilterPoll] # always available # Additional filters available only if external modules are available # yoh: Since I do not know better way for parametric tests # with good old unittest try: - from fail2ban.server.filtergamin import FilterGamin + from ..server.filtergamin import FilterGamin filters.append(FilterGamin) except Exception, e: # pragma: no cover logSys.warning("Skipping gamin backend testing. Got exception '%s'" % e) try: - from fail2ban.server.filterpyinotify import FilterPyinotify + from ..server.filterpyinotify import FilterPyinotify filters.append(FilterPyinotify) except Exception, e: # pragma: no cover logSys.warning("I: Skipping pyinotify backend testing. Got exception '%s'" % e) @@ -239,7 +228,7 @@ def gatherTests(regexps=None, no_network=False): tests.addTest(unittest.makeSuite( filtertestcase.get_monitor_failures_testcase(Filter_))) try: # pragma: systemd no cover - from fail2ban.server.filtersystemd import FilterSystemd + from ..server.filtersystemd import FilterSystemd tests.addTest(unittest.makeSuite(filtertestcase.get_monitor_failures_journal_testcase(FilterSystemd))) except Exception, e: # pragma: no cover logSys.warning("I: Skipping systemd backend testing. Got exception '%s'" % e) diff --git a/man/jail.conf.5 b/man/jail.conf.5 index f384fc8e..9131c4b5 100644 --- a/man/jail.conf.5 +++ b/man/jail.conf.5 @@ -7,7 +7,7 @@ jail.conf \- configuration for the fail2ban server .I jail.conf / jail.local -.I action.d/*.conf action.d/*.local +.I action.d/*.conf action.d/*.local action.d/*.py .I filter.d/*.conf filter.d/*.local .SH DESCRIPTION @@ -113,13 +113,15 @@ uses systemd python library to access the systemd journal. Specifying \fBlogpath will try to use the following backends, in order: pyinotify, gamin, polling .PP .SS Actions -Each jail can be configured with only a single filter, but may have multiple actions. By default, the name of a action is the action filename. In the case where multiple of the same action are to be used, the \fBactname\fR option can be assigned to the action to avoid duplicatione.g.: +Each jail can be configured with only a single filter, but may have multiple actions. By default, the name of a action is the action filename, and in the case of Python actions, the ".py" file extension is stripped. Where multiple of the same action are to be used, the \fBactname\fR option can be assigned to the action to avoid duplication e.g.: .PP .nf [ssh-iptables-ipset] enabled = true action = sendmail[name=ssh, dest=john@example.com, actname=mail-john] sendmail[name=ssh, dest=paul@example.com, actname=mail-paul] + smtp.py[dest=chris@example.com, actname=smtp-chris] + smtp.py[dest=sally@example.com, actname=smtp-sally] .fi .SH "ACTION FILES" @@ -160,6 +162,8 @@ two commands to be executed. actionban = iptables -I fail2ban- --source -j DROP echo ip=, match=, time=