ENH: Python based actions

Python actions are imported from action.d config folder, which have .py
file extension. This imports and creates an instance of the Action class
(Action can be a variable that points to a class of another name).
fail2ban.server.action.ActionBase is a base class which can be inherited
from or as a minimum has a subclass hook which is used to ensure any
imported actions implements the methods required.
All calls to the execAction are also wrapped in a try except such that
any errors won't cripple the jail.
Action is renamed CommandAction, to clearly distinguish it from other
actions.

Include is an example smtp.py python action for sending emails via smtp.
This is work in progress, as looking to add the <matches> and whois
elements, and also SSL/TLS support.
pull/556/head
Steven Hiscocks 2013-12-31 18:54:34 +00:00
parent 6f104638cf
commit f37c90cdba
15 changed files with 452 additions and 120 deletions

119
config/action.d/smtp.py Normal file
View File

@ -0,0 +1,119 @@
import sys
import socket
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate, formataddr
from fail2ban.server.actions import ActionBase
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'] = \
"""Hi,
The IP %(ip)s has just been banned for %(bantime)s seconds
by Fail2Ban after %(failures)i attempts against %(jailname)s.
Regards,
Fail2Ban"""
class SMTPAction(ActionBase):
def __init__(self, jail, name, initOpts):
super(SMTPAction, self).__init__(jail, name, initOpts)
if initOpts is None:
initOpts = dict() # We have defaults for everything
self.host = initOpts.get('host', "localhost:25")
#TODO: self.ssl = initOpts.get('ssl', "no") == 'yes'
self.user = initOpts.get('user', '')
self.password = initOpts.get('password', None)
self.fromname = initOpts.get('sendername', "Fail2Ban")
self.fromaddr = initOpts.get('sender', "fail2ban")
self.toaddr = initOpts.get('dest', "root")
self.smtp = smtplib.SMTP()
def _sendMessage(self, subject, text):
msg = MIMEText(text)
msg['Subject'] = subject
msg['From'] = formataddr((self.fromname, self.fromaddr))
msg['To'] = self.toaddr
msg['Date'] = formatdate()
try:
self.logSys.debug("Connected to SMTP '%s', response: %i: %s",
*self.smtp.connect(self.host))
if self.user and self.password:
smtp.login(self.user, self.password)
failed_recipients = self.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.smtp.quit()
except smtplib.SMTPServerDisconnected:
pass # Not connected
@property
def message_values(self):
return {
'jailname': self.jail.getName(),
'hostname': socket.gethostname(),
'bantime': self.jail.getAction().getBanTime(),
}
def execActionStart(self):
self._sendMessage(
"[Fail2Ban] %(jailname)s: started on %(hostname)s" %
self.message_values,
messages['start'] % self.message_values)
def execActionStop(self):
self._sendMessage(
"[Fail2Ban] %(jailname)s: stopped on %(hostname)s" %
self.message_values,
messages['stop'] % self.message_values)
def execActionBan(self, aInfo):
self._sendMessage(
"[Fail2Ban] %(jailname)s: banned %(ip)s from %(hostname)s" %
dict(self.message_values, **aInfo),
messages['ban'] % dict(self.message_values, **aInfo))
Action = SMTPAction

View File

@ -25,6 +25,7 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
import logging, re, glob, os.path
import json
from fail2ban.client.configreader import ConfigReader
from fail2ban.client.filterreader import FilterReader
@ -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.get("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

View File

@ -76,7 +76,7 @@ protocol = [
["set <JAIL> unbanip <IP>", "manually Unban <IP> in <JAIL>"],
["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"],
["set <JAIL> maxlines <LINES>", "sets the number of <LINES> to buffer for regex search for <JAIL>"],
["set <JAIL> addaction <ACT>", "adds a new action named <NAME> for <JAIL>"],
["set <JAIL> addaction <ACT> [<PYTHONFILE> <JSONOPTS>]", "adds a new action named <NAME> for <JAIL>. Optionally for a python based action, a <PYTHONFILE> and <JSONOPTS> can be specified"],
["set <JAIL> delaction <ACT>", "removes the action <NAME> from <JAIL>"],
["set <JAIL> setcinfo <ACT> <KEY> <VALUE>", "sets <VALUE> for <KEY> of the action <NAME> for <JAIL>"],
["set <JAIL> delcinfo <ACT> <KEY>", "removes <KEY> for the action <NAME> for <JAIL>"],
@ -125,13 +125,15 @@ def printFormatted():
print
firstHeading = True
first = True
for n in textwrap.wrap(m[1], WIDTH):
if len(m[0]) > MARGIN+INDENT:
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
first = False
else:
line = ' ' * (INDENT + MARGIN) + n
print line
print line.rstrip()
##
# Prints the protocol in a "mediawiki" format.

View File

@ -23,6 +23,7 @@ __license__ = "GPL"
import logging, os, subprocess, time, signal, tempfile
import threading, re
from abc import ABCMeta
#from subprocess import call
# Gets the instance of the logger.
@ -53,10 +54,63 @@ signame = dict((num, name)
# action has to be taken. A BanManager take care of the banned IP
# addresses.
class Action:
class ActionBase(object):
__metaclass__ = ABCMeta
@classmethod
def __subclasshook__(cls, C):
required = (
"getName",
"execActionStart",
"execActionStop",
"execActionBan",
"execActionUnban",
)
for method in required:
if not callable(getattr(C, method, None)):
return False
return True
def __init__(self, jail, name, initOpts=None):
self._jail = jail
self._name = name
self._logSys = logging.getLogger(
'%s.%s' % (__name__, self.__class__.__name__))
@property
def jail(self):
return self._jail
@property
def logSys(self):
return self._logSys
##
# Returns the action name.
#
# @return the name of the action
def getName(self):
return self._name
name = property(getName)
def execActionStart(self):
pass
def execActionBan(self, aInfo):
pass
def execActionUnban(self, aInfo):
pass
def execActionStop(self):
pass
class CommandAction(ActionBase):
def __init__(self, name):
self.__name = name
super(CommandAction, self).__init__(None, name)
self.__timeout = 60
self.__cInfo = dict()
## Command executed in order to initialize the system.
@ -71,22 +125,10 @@ class Action:
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
@classmethod
def __subclasshook__(cls, C):
return NotImplemented # Standard checks
##
# Sets the timeout period for commands.
#
@ -94,7 +136,7 @@ class Action:
def setTimeout(self, timeout):
self.__timeout = int(timeout)
logSys.debug("Set action %s timeout = %i" % (self.__name, timeout))
logSys.debug("Set action %s timeout = %i" % (self.getName(), timeout))
##
# Returns the action timeout period for commands.
@ -160,11 +202,11 @@ class Action:
def execActionStart(self):
if self.__cInfo:
if not Action.substituteRecursiveTags(self.__cInfo):
if not self.substituteRecursiveTags(self.__cInfo):
logSys.error("Cinfo/definitions contain self referencing definitions and cannot be resolved")
return False
startCmd = Action.replaceTag(self.__actionStart, self.__cInfo)
return Action.executeCmd(startCmd, self.__timeout)
startCmd = self.replaceTag(self.__actionStart, self.__cInfo)
return self.executeCmd(startCmd, self.__timeout)
##
# Set the "ban" command.
@ -259,8 +301,8 @@ class Action:
# @return True if the command succeeded
def execActionStop(self):
stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo)
return Action.executeCmd(stopCmd, self.__timeout)
stopCmd = self.replaceTag(self.__actionStop, self.__cInfo)
return self.executeCmd(stopCmd, self.__timeout)
##
# Sort out tag definitions within other tags
@ -270,7 +312,7 @@ class Action:
# b = <a>_3 b = 3_3
# @param tags, a dictionary
# @returns tags altered or False if there is a recursive definition
#@staticmethod
@staticmethod
def substituteRecursiveTags(tags):
t = re.compile(r'<([^ >]+)>')
for tag, value in tags.iteritems():
@ -291,15 +333,13 @@ class Action:
m = t.search(value, m.start() + 1)
tags[tag] = value
return tags
substituteRecursiveTags = staticmethod(substituteRecursiveTags)
#@staticmethod
@staticmethod
def escapeTag(tag):
for c in '\\#&;`|*?~<>^()[]{}$\'"':
if c in tag:
tag = tag.replace(c, '\\' + c)
return tag
escapeTag = staticmethod(escapeTag)
##
# Replaces tags in query with property values in aInfo.
@ -308,8 +348,8 @@ class Action:
# @param aInfo the properties
# @return a string
#@staticmethod
def replaceTag(query, aInfo):
@classmethod
def replaceTag(cls, query, aInfo):
""" Replace tags in query
"""
string = query
@ -321,12 +361,11 @@ class Action:
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("<br>", '\n')
return string
replaceTag = staticmethod(replaceTag)
##
# Executes a command with preliminary checks and substitutions.
@ -348,26 +387,26 @@ class Action:
logSys.debug("Nothing to do")
return True
checkCmd = Action.replaceTag(self.__actionCheck, self.__cInfo)
if not Action.executeCmd(checkCmd, self.__timeout):
checkCmd = self.replaceTag(self.__actionCheck, self.__cInfo)
if not self.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):
if not self.executeCmd(checkCmd, self.__timeout):
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.__cInfo)
return Action.executeCmd(realCmd, self.__timeout)
return self.executeCmd(realCmd, self.__timeout)
##
# Executes a command.
@ -381,7 +420,7 @@ class Action:
# @param realCmd the command to execute
# @return True if the command succeeded
#@staticmethod
@staticmethod
def executeCmd(realCmd, timeout=60):
logSys.debug(realCmd)
if not realCmd:
@ -440,5 +479,4 @@ class Action:
logSys.info("HINT on %i: %s"
% (retcode, msg % locals()))
return False
executeCmd = staticmethod(executeCmd)

View File

@ -25,10 +25,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
import time, logging
import os
import imp
from fail2ban.server.banmanager import BanManager
from fail2ban.server.jailthread import JailThread
from fail2ban.server.action import Action
from fail2ban.server.action import ActionBase, CommandAction
from fail2ban.server.mytime import MyTime
# Gets the instance of the logger.
@ -62,11 +64,24 @@ class Actions(JailThread):
#
# @param name The action name
def addAction(self, name):
def addAction(self, name, pythonModule=None, initOpts=None):
# Check is action name already exists
if name in [action.getName() for action in self.__actions]:
raise ValueError("Action %s already exists" % name)
action = Action(name)
if pythonModule is None:
action = CommandAction(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 implment required methods" % (
pythonModule, customActionModule.Action.__name__))
action = customActionModule.Action(self.jail, name, initOpts)
self.__actions.append(action)
##
@ -153,7 +168,11 @@ class Actions(JailThread):
def run(self):
self.setActive(True)
for action in self.__actions:
action.execActionStart()
try:
action.execActionStart()
except Exception as e:
logSys.error("Failed to start jail '%s' action '%s': %s",
self.jail.getName(), action.getName(), e)
while self._isActive():
if not self.getIdle():
#logSys.debug(self.jail.getName() + ": action")
@ -165,7 +184,11 @@ class Actions(JailThread):
time.sleep(self.getSleepTime())
self.__flushBan()
for action in self.__actions:
action.execActionStop()
try:
action.execActionStop()
except Exception as e:
logSys.error("Failed to stop jail '%s' action '%s': %s",
self.jail.getName(), action.getName(), e)
logSys.debug(self.jail.getName() + ": action terminated")
return True
@ -201,7 +224,12 @@ class Actions(JailThread):
if self.__banManager.addBanTicket(bTicket):
logSys.warning("[%s] Ban %s" % (self.jail.getName(), aInfo["ip"]))
for action in self.__actions:
action.execActionBan(aInfo)
try:
action.execActionBan(aInfo)
except Exception as e:
logSys.error(
"Failed to execute ban jail '%s' action '%s': %s",
self.jail.getName(), action.getName(), e)
return True
else:
logSys.info("[%s] %s already banned" % (self.jail.getName(),
@ -241,7 +269,12 @@ class Actions(JailThread):
aInfo["matches"] = "".join(ticket.getMatches())
logSys.warning("[%s] Unban %s" % (self.jail.getName(), aInfo["ip"]))
for action in self.__actions:
action.execActionUnban(aInfo)
try:
action.execActionUnban(aInfo)
except Exception as e:
logSys.error(
"Failed to execute unban jail '%s' action '%s': %s",
self.jail.getName(), action.getName(), e)
##

View File

@ -31,7 +31,7 @@ from fail2ban.server.datedetector import DateDetector
from fail2ban.server.datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n
from fail2ban.server.mytime import MyTime
from fail2ban.server.failregex import FailRegex, Regex, RegexException
from fail2ban.server.action import Action
from fail2ban.server.action import CommandAction
# Gets the instance of the logger.
logSys = logging.getLogger(__name__)
@ -378,9 +378,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

View File

@ -33,6 +33,7 @@ from fail2ban.server.transmitter import Transmitter
from fail2ban.server.asyncserver import AsyncServer
from fail2ban.server.asyncserver import AsyncServerException
from fail2ban.server.database import Fail2BanDb
from fail2ban.server.action import CommandAction
from fail2ban import version
# Gets the instance of the logger.
@ -278,8 +279,8 @@ class Server:
return self.__jails.getFilter(name).getMaxLines()
# Action
def addAction(self, name, value):
self.__jails.getAction(name).addAction(value)
def addAction(self, name, value, *args):
self.__jails.getAction(name).addAction(value, *args)
def getLastAction(self, name):
return self.__jails.getAction(name).getLastAction()
@ -290,14 +291,26 @@ class Server:
def delAction(self, name, value):
self.__jails.getAction(name).delAction(value)
def setCInfo(self, name, action, key, value):
self.__jails.getAction(name).getAction(action).setCInfo(key, value)
def setCInfo(self, name, actionName, key, value):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
action.setCInfo(key, value)
else:
raise TypeError("%s is not a CommandAction" % actionName)
def getCInfo(self, name, action, key):
return self.__jails.getAction(name).getAction(action).getCInfo(key)
def getCInfo(self, name, actionName, key):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
return action.getCInfo(key)
else:
raise TypeError("%s is not a CommandAction" % actionName)
def delCInfo(self, name, action, key):
self.__jails.getAction(name).getAction(action).delCInfo(key)
def delCInfo(self, name, actionName, key):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
action.delCInfo(key)
else:
raise TypeError("%s is not a CommandAction" % actionName)
def setBanTime(self, name, value):
self.__jails.getAction(name).setBanTime(value)
@ -311,41 +324,89 @@ class Server:
def getBanTime(self, name):
return self.__jails.getAction(name).getBanTime()
def setActionStart(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionStart(value)
def setActionStart(self, name, actionName, value):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
action.setActionStart(value)
else:
raise TypeError("%s is not a CommandAction" % actionName)
def getActionStart(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionStart()
def getActionStart(self, name, actionName):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
return action.getActionStart()
else:
raise TypeError("%s is not a CommandAction" % actionName)
def setActionStop(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionStop(value)
def setActionStop(self, name, actionName, value):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
action.setActionStop(value)
else:
raise TypeError("%s is not a CommandAction" % actionName)
def getActionStop(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionStop()
def getActionStop(self, name, actionName):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
return action.getActionStop()
else:
raise TypeError("%s is not a CommandAction" % actionName)
def setActionCheck(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionCheck(value)
def setActionCheck(self, name, actionName, value):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
action.setActionCheck(value)
else:
raise TypeError("%s is not a CommandAction" % actionName)
def getActionCheck(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionCheck()
def getActionCheck(self, name, actionName):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
return action.getActionCheck()
else:
raise TypeError("%s is not a CommandAction" % actionName)
def setActionBan(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionBan(value)
def setActionBan(self, name, actionName, value):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
action.setActionBan(value)
else:
raise TypeError("%s is not a CommandAction" % actionName)
def getActionBan(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionBan()
def getActionBan(self, name, actionName):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
return action.getActionBan()
else:
raise TypeError("%s is not a CommandAction" % actionName)
def setActionUnban(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionUnban(value)
def setActionUnban(self, name, actionName, value):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
action.setActionUnban(value)
else:
raise TypeError("%s is not a CommandAction" % actionName)
def getActionUnban(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionUnban()
def getActionUnban(self, name, actionName):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
return action.getActionUnban()
else:
raise TypeError("%s is not a CommandAction" % actionName)
def setActionTimeout(self, name, action, value):
self.__jails.getAction(name).getAction(action).setTimeout(value)
def setActionTimeout(self, name, actionName, value):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
action.setTimeout(value)
else:
raise TypeError("%s is not a CommandAction" % actionName)
def getActionTimeout(self, name, action):
return self.__jails.getAction(name).getAction(action).getTimeout()
def getActionTimeout(self, name, actionName):
action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
return action.getTimeout()
else:
raise TypeError("%s is not a CommandAction" % actionName)
# Status
def status(self):

View File

@ -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__)
@ -228,8 +229,10 @@ class Transmitter:
value = command[2]
return self.__server.setUnbanIP(name,value)
elif command[1] == "addaction":
value = command[2]
self.__server.addAction(name, value)
args = [command[2]]
if len(command) > 3:
args.extend([command[3], json.loads(command[4])])
self.__server.addAction(name, *args)
return self.__server.getLastAction(name).getName()
elif command[1] == "delaction":
value = command[2]

View File

@ -26,18 +26,24 @@ __license__ = "GPL"
import unittest, time
import sys, os, tempfile
from fail2ban.server.actions import Actions
from dummyjail import DummyJail
class ExecuteActions(unittest.TestCase):
from fail2ban.server.actions import Actions
from fail2ban.tests.dummyjail import DummyJail
from fail2ban.tests.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):
@ -77,3 +83,20 @@ class ExecuteActions(unittest.TestCase):
self.assertEqual(self.__actions.status(),[("Currently banned", 0 ),
("Total banned", 0 ), ("IP list", [] )])
def testAddActionPython(self):
self.__actions.addAction(
"Action", os.path.join(TEST_FILES_DIR, "action.d/action.py"), {})
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.addAction, "Action3", "/does/not/exist.py", {})

View File

@ -27,7 +27,7 @@ __license__ = "GPL"
import time
import logging, sys
from fail2ban.server.action import Action
from fail2ban.server.action import CommandAction
from fail2ban.tests.utils import LogCaptureTestCase
@ -35,7 +35,7 @@ class ExecuteAction(LogCaptureTestCase):
def setUp(self):
"""Call before every test case."""
self.__action = Action("Test")
self.__action = CommandAction("Test")
LogCaptureTestCase.setUp(self)
def tearDown(self):
@ -43,11 +43,6 @@ class ExecuteAction(LogCaptureTestCase):
LogCaptureTestCase.tearDown(self)
self.__action.execActionStop()
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 <ABC>",
}
# Recursion is bad
self.assertFalse(Action.substituteRecursiveTags({'A': '<A>'}))
self.assertFalse(Action.substituteRecursiveTags({'A': '<B>', 'B': '<A>'}))
self.assertFalse(Action.substituteRecursiveTags({'A': '<B>', 'B': '<C>', 'C': '<A>'}))
self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<A>'}))
self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<A>'}))
self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<C>', 'C': '<A>'}))
# missing tags are ok
self.assertEqual(Action.substituteRecursiveTags({'A': '<C>'}), {'A': '<C>'})
self.assertEqual(Action.substituteRecursiveTags({'A': '<C> <D> <X>','X':'fun'}), {'A': '<C> <D> fun', 'X':'fun'})
self.assertEqual(Action.substituteRecursiveTags({'A': '<C> <B>', 'B': 'cool'}), {'A': '<C> cool', 'B': 'cool'})
self.assertEqual(CommandAction.substituteRecursiveTags({'A': '<C>'}), {'A': '<C>'})
self.assertEqual(CommandAction.substituteRecursiveTags({'A': '<C> <D> <X>','X':'fun'}), {'A': '<C> <D> fun', 'X':'fun'})
self.assertEqual(CommandAction.substituteRecursiveTags({'A': '<C> <B>', 'B': 'cool'}), {'A': '<C> 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',
@ -169,20 +164,20 @@ class ExecuteAction(LogCaptureTestCase):
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
CommandAction.executeCmd('sleep 60', timeout=2) # Should take a minute
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'"))

View File

@ -0,0 +1,22 @@
from fail2ban.server.action import ActionBase
class TestAction(ActionBase):
def __init__(self, *args, **kwargs):
super(TestAction, self).__init__(*args, **kwargs)
self.logSys.debug("%s initialised" % self.__class__.__name__)
def execActionStart(self, *args, **kwargs):
self.logSys.debug("%s action start" % self.__class__.__name__)
def execActionStop(self, *args, **kwargs):
self.logSys.debug("%s action stop" % self.__class__.__name__)
def execActionBan(self, *args, **kwargs):
self.logSys.debug("%s action ban" % self.__class__.__name__)
def execActionUnban(self, *args, **kwargs):
self.logSys.debug("%s action unban" % self.__class__.__name__)
Action = TestAction

View File

@ -28,7 +28,7 @@ import shutil
from glob import glob
from utils import mbasename, TraceBack, FormatterWithTraceBack
from fail2ban.tests.utils import mbasename, TraceBack, FormatterWithTraceBack
from fail2ban.helpers import formatExceptionInfo
class HelpersTest(unittest.TestCase):

View File

@ -564,6 +564,22 @@ class Transmitter(TransmitterBase):
self.assertEqual(
self.transm.proceed(
["set", self.jailName, "delaction", "Doesn't exist"])[0],1)
self.assertEqual(
self.transm.proceed(["set", self.jailName, "addaction", action,
os.path.join(TEST_FILES_DIR, "action.d", "action.py"), "{}"]),
(0, action))
for cmd, value in zip(cmdList, cmdValueList):
self.assertTrue(
isinstance(self.transm.proceed(
["set", self.jailName, cmd, action, value])[1],
TypeError),
"set %s for python action did not raise TypeError" % cmd)
for cmd, value in zip(cmdList, cmdValueList):
self.assertTrue(
isinstance(self.transm.proceed(
["get", self.jailName, cmd, action])[1],
TypeError),
"get %s for python action did not raise TypeError" % cmd)
def testNOK(self):
self.assertEqual(self.transm.proceed(["INVALID", "COMMAND"])[0],1)

View File

@ -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-<name> --source <ip> -j DROP
echo ip=<ip>, match=<match>, time=<time> >> /var/log/fail2ban.log
.TP
Python based actions can also be used, where the file name must be \fI[actionname].py\fR. The python file must contain a variable \fIAction\fR which points to python class. This class must implement a minimum interface as described by \fIfail2ban.server.action.ActionBase\fR, which can be inherited from to ease implementation.
.SS "Action Tags"
The following tags are substituted in the actionban, actionunban and actioncheck (when called before actionban/actionunban) commands.

View File

@ -120,7 +120,7 @@ setup(
glob("config/filter.d/*.conf")
),
('/etc/fail2ban/action.d',
glob("config/action.d/*.conf")
glob("config/action.d/*.*")
),
('/etc/fail2ban/fail2ban.d',
''