MRG: from python-actions

pull/556/head
Daniel Black 2014-01-02 10:54:14 +11:00
commit d61734b9ac
31 changed files with 604 additions and 195 deletions

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

@ -0,0 +1,144 @@
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):
def __init__(
self, jail, name, host="localhost", user=None, password=None,
sendername="Fail2Ban", sender="fail2ban", dest="root", matches=None):
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.getAction().getBanTime,
)
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()
smtp = smtplib.SMTP()
try:
self.logSys.debug("Connected to SMTP '%s', response: %i: %s",
*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:
smtp.quit()
except smtplib.SMTPServerDisconnected:
pass # Not connected
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):
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

View File

@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging, os import logging, os
from configreader import ConfigReader, DefinitionInitConfigReader
from fail2ban.client.configreader import ConfigReader, DefinitionInitConfigReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -25,9 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import glob, logging, os import glob, logging, os
from configparserinc import SafeConfigParserWithIncludes
from ConfigParser import NoOptionError, NoSectionError from ConfigParser import NoOptionError, NoSectionError
from fail2ban.client.configparserinc import SafeConfigParserWithIncludes
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -25,9 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging import logging
from configreader import ConfigReader
from fail2banreader import Fail2banReader from fail2ban.client.configreader import ConfigReader
from jailsreader import JailsReader from fail2ban.client.fail2banreader import Fail2banReader
from fail2ban.client.jailsreader import JailsReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging import logging
from configreader import ConfigReader
from fail2ban.client.configreader import ConfigReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -24,9 +24,8 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging, os, shlex from fail2ban.client.configreader import ConfigReader, DefinitionInitConfigReader
from configreader import ConfigReader, DefinitionInitConfigReader from fail2ban.server.action import CommandAction
from fail2ban.server.action import Action
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -44,7 +43,7 @@ class FilterReader(DefinitionInitConfigReader):
def convert(self): def convert(self):
stream = list() stream = list()
combinedopts = dict(list(self._opts.items()) + list(self._initOpts.items())) combinedopts = dict(list(self._opts.items()) + list(self._initOpts.items()))
opts = Action.substituteRecursiveTags(combinedopts) opts = CommandAction.substituteRecursiveTags(combinedopts)
if not opts: if not opts:
raise ValueError('recursive tag definitions unable to be resolved') raise ValueError('recursive tag definitions unable to be resolved')
for opt, value in opts.iteritems(): for opt, value in opts.iteritems():

View File

@ -25,10 +25,11 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging, re, glob, os.path import logging, re, glob, os.path
import json
from configreader import ConfigReader from fail2ban.client.configreader import ConfigReader
from filterreader import FilterReader from fail2ban.client.filterreader import FilterReader
from actionreader import ActionReader from fail2ban.client.actionreader import ActionReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -120,14 +121,26 @@ class JailReader(ConfigReader):
if not act: # skip empty actions if not act: # skip empty actions
continue continue
actName, actOpt = JailReader.extractOptions(act) actName, actOpt = JailReader.extractOptions(act)
action = ActionReader( if actName.endswith(".py"):
actName, self.__name, actOpt, basedir=self.getBaseDir()) self.__actions.append([
ret = action.read() "set",
if ret: self.__name,
action.getOptions(self.__opts) "addaction",
self.__actions.append(action) actOpt.get("actname", os.path.splitext(actName)[0]),
os.path.join(
self.getBaseDir(), "action.d", actName),
json.dumps(actOpt),
])
else: 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: except Exception, e:
logSys.error("Error in action definition " + act) logSys.error("Error in action definition " + act)
logSys.debug("Caught exception: %s" % (e,)) logSys.debug("Caught exception: %s" % (e,))
@ -193,7 +206,10 @@ class JailReader(ConfigReader):
if self.__filter: if self.__filter:
stream.extend(self.__filter.convert()) stream.extend(self.__filter.convert())
for action in self.__actions: 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]) stream.insert(0, ["add", self.__name, backend])
return stream return stream

View File

@ -25,8 +25,9 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging import logging
from configreader import ConfigReader
from jailreader import JailReader from fail2ban.client.configreader import ConfigReader
from fail2ban.client.jailreader import JailReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -76,7 +76,7 @@ protocol = [
["set <JAIL> unbanip <IP>", "manually Unban <IP> in <JAIL>"], ["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> 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> 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> 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> 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>"], ["set <JAIL> delcinfo <ACT> <KEY>", "removes <KEY> for the action <NAME> for <JAIL>"],
@ -125,13 +125,15 @@ def printFormatted():
print print
firstHeading = True firstHeading = True
first = 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: if first:
line = ' ' * INDENT + m[0] + ' ' * (MARGIN - len(m[0])) + n line = ' ' * INDENT + m[0] + ' ' * (MARGIN - len(m[0])) + n
first = False first = False
else: else:
line = ' ' * (INDENT + MARGIN) + n line = ' ' * (INDENT + MARGIN) + n
print line print line.rstrip()
## ##
# Prints the protocol in a "mediawiki" format. # Prints the protocol in a "mediawiki" format.

View File

@ -23,6 +23,8 @@ __license__ = "GPL"
import logging, os, subprocess, time, signal, tempfile import logging, os, subprocess, time, signal, tempfile
import threading, re import threading, re
from abc import ABCMeta
from collections import MutableMapping
#from subprocess import call #from subprocess import call
# Gets the instance of the logger. # Gets the instance of the logger.
@ -46,6 +48,24 @@ _RETCODE_HINTS = {
signame = dict((num, name) signame = dict((num, name)
for name, num in signal.__dict__.iteritems() if name.startswith("SIG")) for name, num in signal.__dict__.iteritems() if name.startswith("SIG"))
class CallingMap(MutableMapping):
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)
## ##
# Execute commands. # Execute commands.
# #
@ -53,10 +73,63 @@ signame = dict((num, name)
# action has to be taken. A BanManager take care of the banned IP # action has to be taken. A BanManager take care of the banned IP
# addresses. # 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):
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): def __init__(self, name):
self.__name = name super(CommandAction, self).__init__(None, name)
self.__timeout = 60 self.__timeout = 60
self.__cInfo = dict() self.__cInfo = dict()
## Command executed in order to initialize the system. ## Command executed in order to initialize the system.
@ -71,22 +144,10 @@ class Action:
self.__actionStop = '' self.__actionStop = ''
logSys.debug("Created Action") logSys.debug("Created Action")
## @classmethod
# Sets the action name. def __subclasshook__(cls, C):
# return NotImplemented # Standard checks
# @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. # Sets the timeout period for commands.
# #
@ -94,7 +155,7 @@ class Action:
def setTimeout(self, timeout): def setTimeout(self, timeout):
self.__timeout = int(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. # Returns the action timeout period for commands.
@ -160,11 +221,11 @@ class Action:
def execActionStart(self): def execActionStart(self):
if self.__cInfo: 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") logSys.error("Cinfo/definitions contain self referencing definitions and cannot be resolved")
return False return False
startCmd = Action.replaceTag(self.__actionStart, self.__cInfo) startCmd = self.replaceTag(self.__actionStart, self.__cInfo)
return Action.executeCmd(startCmd, self.__timeout) return self.executeCmd(startCmd, self.__timeout)
## ##
# Set the "ban" command. # Set the "ban" command.
@ -259,8 +320,8 @@ class Action:
# @return True if the command succeeded # @return True if the command succeeded
def execActionStop(self): def execActionStop(self):
stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo) stopCmd = self.replaceTag(self.__actionStop, self.__cInfo)
return Action.executeCmd(stopCmd, self.__timeout) return self.executeCmd(stopCmd, self.__timeout)
## ##
# Sort out tag definitions within other tags # Sort out tag definitions within other tags
@ -270,7 +331,7 @@ class Action:
# b = <a>_3 b = 3_3 # b = <a>_3 b = 3_3
# @param tags, a dictionary # @param tags, a dictionary
# @returns tags altered or False if there is a recursive definition # @returns tags altered or False if there is a recursive definition
#@staticmethod @staticmethod
def substituteRecursiveTags(tags): def substituteRecursiveTags(tags):
t = re.compile(r'<([^ >]+)>') t = re.compile(r'<([^ >]+)>')
for tag, value in tags.iteritems(): for tag, value in tags.iteritems():
@ -299,15 +360,13 @@ class Action:
#logSys.log(5, 'TAG: %s, newvalue: %s' % (tag, value)) #logSys.log(5, 'TAG: %s, newvalue: %s' % (tag, value))
tags[tag] = value tags[tag] = value
return tags return tags
substituteRecursiveTags = staticmethod(substituteRecursiveTags)
#@staticmethod @staticmethod
def escapeTag(tag): def escapeTag(tag):
for c in '\\#&;`|*?~<>^()[]{}$\'"': for c in '\\#&;`|*?~<>^()[]{}$\'"':
if c in tag: if c in tag:
tag = tag.replace(c, '\\' + c) tag = tag.replace(c, '\\' + c)
return tag return tag
escapeTag = staticmethod(escapeTag)
## ##
# Replaces tags in query with property values in aInfo. # Replaces tags in query with property values in aInfo.
@ -316,25 +375,22 @@ class Action:
# @param aInfo the properties # @param aInfo the properties
# @return a string # @return a string
#@staticmethod @classmethod
def replaceTag(query, aInfo): def replaceTag(cls, query, aInfo):
""" Replace tags in query """ Replace tags in query
""" """
string = query string = query
for tag, value in aInfo.iteritems(): for tag in aInfo:
if "<%s>" % tag in query: if "<%s>" % tag in query:
if callable(value): value = str(aInfo[tag]) # assure string
value = value()
value = str(value) # assure string
if tag.endswith('matches'): if tag.endswith('matches'):
# That one needs to be escaped since its content is # That one needs to be escaped since its content is
# out of our control # out of our control
value = Action.escapeTag(value) value = cls.escapeTag(value)
string = string.replace('<' + tag + '>', value) string = string.replace('<' + tag + '>', value)
# New line # New line
string = string.replace("<br>", '\n') string = string.replace("<br>", '\n')
return string return string
replaceTag = staticmethod(replaceTag)
## ##
# Executes a command with preliminary checks and substitutions. # Executes a command with preliminary checks and substitutions.
@ -356,26 +412,26 @@ class Action:
logSys.debug("Nothing to do") logSys.debug("Nothing to do")
return True return True
checkCmd = Action.replaceTag(self.__actionCheck, self.__cInfo) checkCmd = self.replaceTag(self.__actionCheck, self.__cInfo)
if not Action.executeCmd(checkCmd, self.__timeout): if not self.executeCmd(checkCmd, self.__timeout):
logSys.error("Invariant check failed. Trying to restore a sane" + logSys.error("Invariant check failed. Trying to restore a sane" +
" environment") " environment")
self.execActionStop() self.execActionStop()
self.execActionStart() self.execActionStart()
if not Action.executeCmd(checkCmd, self.__timeout): if not self.executeCmd(checkCmd, self.__timeout):
logSys.fatal("Unable to restore environment") logSys.fatal("Unable to restore environment")
return False return False
# Replace tags # Replace tags
if not aInfo is None: if not aInfo is None:
realCmd = Action.replaceTag(cmd, aInfo) realCmd = self.replaceTag(cmd, aInfo)
else: else:
realCmd = cmd realCmd = cmd
# Replace static fields # 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. # Executes a command.
@ -389,7 +445,7 @@ class Action:
# @param realCmd the command to execute # @param realCmd the command to execute
# @return True if the command succeeded # @return True if the command succeeded
#@staticmethod @staticmethod
def executeCmd(realCmd, timeout=60): def executeCmd(realCmd, timeout=60):
logSys.debug(realCmd) logSys.debug(realCmd)
if not realCmd: if not realCmd:
@ -448,5 +504,4 @@ class Action:
logSys.info("HINT on %i: %s" logSys.info("HINT on %i: %s"
% (retcode, msg % locals())) % (retcode, msg % locals()))
return False return False
executeCmd = staticmethod(executeCmd)

View File

@ -24,11 +24,14 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from banmanager import BanManager
from jailthread import JailThread
from action import Action
from mytime import MyTime
import time, logging import time, logging
import os
import imp
from fail2ban.server.banmanager import BanManager
from fail2ban.server.jailthread import JailThread
from fail2ban.server.action import ActionBase, CommandAction, CallingMap
from fail2ban.server.mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -61,11 +64,24 @@ class Actions(JailThread):
# #
# @param name The action name # @param name The action name
def addAction(self, name): def addAction(self, name, pythonModule=None, initOpts=None):
# Check is action name already exists # Check is action name already exists
if name in [action.getName() for action in self.__actions]: if name in [action.getName() for action in self.__actions]:
raise ValueError("Action %s already exists" % name) 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) self.__actions.append(action)
## ##
@ -152,7 +168,11 @@ class Actions(JailThread):
def run(self): def run(self):
self.setActive(True) self.setActive(True)
for action in self.__actions: 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(): while self._isActive():
if not self.getIdle(): if not self.getIdle():
#logSys.debug(self.jail.getName() + ": action") #logSys.debug(self.jail.getName() + ": action")
@ -164,7 +184,11 @@ class Actions(JailThread):
time.sleep(self.getSleepTime()) time.sleep(self.getSleepTime())
self.__flushBan() self.__flushBan()
for action in self.__actions: 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") logSys.debug(self.jail.getName() + ": action terminated")
return True return True
@ -178,7 +202,7 @@ class Actions(JailThread):
def __checkBan(self): def __checkBan(self):
ticket = self.jail.getFailTicket() ticket = self.jail.getFailTicket()
if ticket != False: if ticket != False:
aInfo = dict() aInfo = CallingMap()
bTicket = BanManager.createBanTicket(ticket) bTicket = BanManager.createBanTicket(ticket)
aInfo["ip"] = bTicket.getIP() aInfo["ip"] = bTicket.getIP()
aInfo["failures"] = bTicket.getAttempt() aInfo["failures"] = bTicket.getAttempt()
@ -200,7 +224,12 @@ class Actions(JailThread):
if self.__banManager.addBanTicket(bTicket): if self.__banManager.addBanTicket(bTicket):
logSys.warning("[%s] Ban %s" % (self.jail.getName(), aInfo["ip"])) logSys.warning("[%s] Ban %s" % (self.jail.getName(), aInfo["ip"]))
for action in self.__actions: 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 return True
else: else:
logSys.info("[%s] %s already banned" % (self.jail.getName(), logSys.info("[%s] %s already banned" % (self.jail.getName(),
@ -240,7 +269,12 @@ class Actions(JailThread):
aInfo["matches"] = "".join(ticket.getMatches()) aInfo["matches"] = "".join(ticket.getMatches())
logSys.warning("[%s] Unban %s" % (self.jail.getName(), aInfo["ip"])) logSys.warning("[%s] Unban %s" % (self.jail.getName(), aInfo["ip"]))
for action in self.__actions: 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

@ -24,10 +24,11 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from ticket import BanTicket
from threading import Lock
from mytime import MyTime
import logging import logging
from threading import Lock
from fail2ban.server.ticket import BanTicket
from fail2ban.server.mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -22,10 +22,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import sys, time, logging import sys, time, logging
from datetemplate import DatePatternRegex, DateTai64n, DateEpoch, DateISO8601
from threading import Lock from threading import Lock
from fail2ban.server.datetemplate import DatePatternRegex, DateTai64n, DateEpoch, DateISO8601
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -25,14 +25,13 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import re, time, calendar import re, time, calendar
import logging
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from mytime import MyTime from fail2ban.server.mytime import MyTime
import iso8601 from fail2ban.server import iso8601
import logging
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -24,11 +24,12 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from faildata import FailData
from ticket import FailTicket
from threading import Lock from threading import Lock
import logging import logging
from fail2ban.server.faildata import FailData
from fail2ban.server.ticket import FailTicket
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -21,18 +21,18 @@ __author__ = "Cyril Jaquier and Fail2Ban Contributors"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko"
__license__ = "GPL" __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 import logging, re, os, fcntl, time, sys, locale, codecs
from fail2ban.server.failmanager import FailManagerEmpty
from fail2ban.server.failmanager import FailManager
from fail2ban.server.ticket import FailTicket
from fail2ban.server.jailthread import JailThread
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 CommandAction
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -378,9 +378,9 @@ class Filter(JailThread):
return True return True
if self.__ignoreCommand: if self.__ignoreCommand:
command = Action.replaceTag(self.__ignoreCommand, { 'ip': ip } ) command = CommandAction.replaceTag(self.__ignoreCommand, { 'ip': ip } )
logSys.debug('ignore command: ' + command) logSys.debug('ignore command: ' + command)
return Action.executeCmd(command) return CommandAction.executeCmd(command)
return False return False

View File

@ -23,11 +23,13 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012 Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
from failmanager import FailManagerEmpty import time, logging, fcntl
from filter import FileFilter
from mytime import MyTime
import time, logging, gamin, fcntl import gamin
from fail2ban.server.failmanager import FailManagerEmpty
from fail2ban.server.filter import FileFilter
from fail2ban.server.mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -24,12 +24,12 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier; 2012 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier; 2012 Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
from failmanager import FailManagerEmpty
from filter import FileFilter
from mytime import MyTime
import time, logging, os import time, logging, os
from fail2ban.server.failmanager import FailManagerEmpty
from fail2ban.server.filter import FileFilter
from fail2ban.server.mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -24,13 +24,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Y
__license__ = "GPL" __license__ = "GPL"
import time, logging, pyinotify import time, logging, pyinotify
from distutils.version import LooseVersion from distutils.version import LooseVersion
from os.path import dirname, sep as pathsep from os.path import dirname, sep as pathsep
from failmanager import FailManagerEmpty from fail2ban.server.failmanager import FailManagerEmpty
from filter import FileFilter from fail2ban.server.filter import FileFilter
from mytime import MyTime from fail2ban.server.mytime import MyTime
if not hasattr(pyinotify, '__version__') \ if not hasattr(pyinotify, '__version__') \

View File

@ -29,9 +29,9 @@ from systemd import journal
if LooseVersion(getattr(journal, '__version__', "0")) < '204': if LooseVersion(getattr(journal, '__version__', "0")) < '204':
raise ImportError("Fail2Ban requires systemd >= 204") raise ImportError("Fail2Ban requires systemd >= 204")
from failmanager import FailManagerEmpty from fail2ban.server.failmanager import FailManagerEmpty
from filter import JournalFilter from fail2ban.server.filter import JournalFilter
from mytime import MyTime from fail2ban.server.mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.

View File

@ -25,7 +25,7 @@ __license__ = "GPL"
import Queue, logging import Queue, logging
from actions import Actions from fail2ban.server.actions import Actions
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -21,11 +21,11 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
from fail2ban.exceptions import DuplicateJailException, UnknownJailException
from jail import Jail
from threading import Lock from threading import Lock
from fail2ban.exceptions import DuplicateJailException, UnknownJailException
from fail2ban.server.jail import Jail
## ##
# Handles the jails. # Handles the jails.
# #

View File

@ -25,15 +25,17 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from threading import Lock, RLock 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 import logging, logging.handlers, sys, os, signal
from fail2ban.server.jails import Jails
from fail2ban.server.filter import FileFilter, JournalFilter
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. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -277,8 +279,8 @@ class Server:
return self.__jails.getFilter(name).getMaxLines() return self.__jails.getFilter(name).getMaxLines()
# Action # Action
def addAction(self, name, value): def addAction(self, name, value, *args):
self.__jails.getAction(name).addAction(value) self.__jails.getAction(name).addAction(value, *args)
def getLastAction(self, name): def getLastAction(self, name):
return self.__jails.getAction(name).getLastAction() return self.__jails.getAction(name).getLastAction()
@ -289,14 +291,26 @@ class Server:
def delAction(self, name, value): def delAction(self, name, value):
self.__jails.getAction(name).delAction(value) self.__jails.getAction(name).delAction(value)
def setCInfo(self, name, action, key, value): def setCInfo(self, name, actionName, key, value):
self.__jails.getAction(name).getAction(action).setCInfo(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): def getCInfo(self, name, actionName, key):
return self.__jails.getAction(name).getAction(action).getCInfo(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): def delCInfo(self, name, actionName, key):
self.__jails.getAction(name).getAction(action).delCInfo(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): def setBanTime(self, name, value):
self.__jails.getAction(name).setBanTime(value) self.__jails.getAction(name).setBanTime(value)
@ -310,41 +324,89 @@ class Server:
def getBanTime(self, name): def getBanTime(self, name):
return self.__jails.getAction(name).getBanTime() return self.__jails.getAction(name).getBanTime()
def setActionStart(self, name, action, value): def setActionStart(self, name, actionName, value):
self.__jails.getAction(name).getAction(action).setActionStart(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): def getActionStart(self, name, actionName):
return self.__jails.getAction(name).getAction(action).getActionStart() 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): def setActionStop(self, name, actionName, value):
self.__jails.getAction(name).getAction(action).setActionStop(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): def getActionStop(self, name, actionName):
return self.__jails.getAction(name).getAction(action).getActionStop() 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): def setActionCheck(self, name, actionName, value):
self.__jails.getAction(name).getAction(action).setActionCheck(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): def getActionCheck(self, name, actionName):
return self.__jails.getAction(name).getAction(action).getActionCheck() 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): def setActionBan(self, name, actionName, value):
self.__jails.getAction(name).getAction(action).setActionBan(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): def getActionBan(self, name, actionName):
return self.__jails.getAction(name).getAction(action).getActionBan() 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): def setActionUnban(self, name, actionName, value):
self.__jails.getAction(name).getAction(action).setActionUnban(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): def getActionUnban(self, name, actionName):
return self.__jails.getAction(name).getAction(action).getActionUnban() 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): def setActionTimeout(self, name, actionName, value):
self.__jails.getAction(name).getAction(action).setTimeout(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): def getActionTimeout(self, name, actionName):
return self.__jails.getAction(name).getAction(action).getTimeout() action = self.__jails.getAction(name).getAction(actionName)
if isinstance(action, CommandAction):
return action.getTimeout()
else:
raise TypeError("%s is not a CommandAction" % actionName)
# Status # Status
def status(self): def status(self):

View File

@ -25,6 +25,7 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging, time import logging, time
import json
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -228,8 +229,10 @@ class Transmitter:
value = command[2] value = command[2]
return self.__server.setUnbanIP(name,value) return self.__server.setUnbanIP(name,value)
elif command[1] == "addaction": elif command[1] == "addaction":
value = command[2] args = [command[2]]
self.__server.addAction(name, value) if len(command) > 3:
args.extend([command[3], json.loads(command[4])])
self.__server.addAction(name, *args)
return self.__server.getLastAction(name).getName() return self.__server.getLastAction(name).getName()
elif command[1] == "delaction": elif command[1] == "delaction":
value = command[2] value = command[2]

View File

@ -26,18 +26,24 @@ __license__ = "GPL"
import unittest, time import unittest, time
import sys, os, tempfile 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): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
super(ExecuteActions, self).setUp()
self.__jail = DummyJail() self.__jail = DummyJail()
self.__actions = Actions(self.__jail) self.__actions = Actions(self.__jail)
self.__tmpfile, self.__tmpfilename = tempfile.mkstemp() self.__tmpfile, self.__tmpfilename = tempfile.mkstemp()
def tearDown(self): def tearDown(self):
super(ExecuteActions, self).tearDown()
os.remove(self.__tmpfilename) os.remove(self.__tmpfilename)
def defaultActions(self): def defaultActions(self):
@ -77,3 +83,35 @@ class ExecuteActions(unittest.TestCase):
self.assertEqual(self.__actions.status(),[("Currently banned", 0 ), self.assertEqual(self.__actions.status(),[("Currently banned", 0 ),
("Total banned", 0 ), ("IP list", [] )]) ("Total banned", 0 ), ("IP list", [] )])
def testAddActionPython(self):
self.__actions.addAction(
"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.addAction, "Action3", "/does/not/exist.py", {})
# With optional argument
self.__actions.addAction(
"Action4", os.path.join(TEST_FILES_DIR, "action.d/action.py"),
{'opt1': 'value', 'opt2': 'value2'})
# With too many arguments
self.assertRaises(
TypeError, self.__actions.addAction, "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.addAction, "Action5",
os.path.join(TEST_FILES_DIR, "action.d/action.py"), {})

View File

@ -27,7 +27,7 @@ __license__ = "GPL"
import time import time
import logging, sys import logging, sys
from fail2ban.server.action import Action from fail2ban.server.action import CommandAction, CallingMap
from fail2ban.tests.utils import LogCaptureTestCase from fail2ban.tests.utils import LogCaptureTestCase
@ -35,7 +35,7 @@ class ExecuteAction(LogCaptureTestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
self.__action = Action("Test") self.__action = CommandAction("Test")
LogCaptureTestCase.setUp(self) LogCaptureTestCase.setUp(self)
def tearDown(self): def tearDown(self):
@ -43,11 +43,6 @@ class ExecuteAction(LogCaptureTestCase):
LogCaptureTestCase.tearDown(self) LogCaptureTestCase.tearDown(self)
self.__action.execActionStop() 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): def testSubstituteRecursiveTags(self):
aInfo = { aInfo = {
'HOST': "192.0.2.0", 'HOST': "192.0.2.0",
@ -55,18 +50,18 @@ class ExecuteAction(LogCaptureTestCase):
'xyz': "890 <ABC>", 'xyz': "890 <ABC>",
} }
# Recursion is bad # Recursion is bad
self.assertFalse(Action.substituteRecursiveTags({'A': '<A>'})) self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<A>'}))
self.assertFalse(Action.substituteRecursiveTags({'A': '<B>', 'B': '<A>'})) self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<A>'}))
self.assertFalse(Action.substituteRecursiveTags({'A': '<B>', 'B': '<C>', 'C': '<A>'})) self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<C>', 'C': '<A>'}))
# part recursion # Unresolveable substition
self.assertFalse(Action.substituteRecursiveTags({'A': 'to=<B> fromip=<IP>', 'C': '<B>', 'B': '<C>', 'D': ''})) self.assertFalse(CommandAction.substituteRecursiveTags({'A': 'to=<B> fromip=<IP>', 'C': '<B>', 'B': '<C>', 'D': ''}))
self.assertFalse(Action.substituteRecursiveTags({'failregex': 'to=<honeypot> fromip=<IP>', 'sweet': '<honeypot>', 'honeypot': '<sweet>', 'ignoreregex': ''})) self.assertFalse(CommandAction.substituteRecursiveTags({'failregex': 'to=<honeypot> fromip=<IP>', 'sweet': '<honeypot>', 'honeypot': '<sweet>', 'ignoreregex': ''}))
# missing tags are ok # missing tags are ok
self.assertEqual(Action.substituteRecursiveTags({'A': '<C>'}), {'A': '<C>'}) self.assertEqual(CommandAction.substituteRecursiveTags({'A': '<C>'}), {'A': '<C>'})
self.assertEqual(Action.substituteRecursiveTags({'A': '<C> <D> <X>','X':'fun'}), {'A': '<C> <D> fun', 'X':'fun'}) self.assertEqual(CommandAction.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> <B>', 'B': 'cool'}), {'A': '<C> cool', 'B': 'cool'})
# rest is just cool # rest is just cool
self.assertEqual(Action.substituteRecursiveTags(aInfo), self.assertEqual(CommandAction.substituteRecursiveTags(aInfo),
{ 'HOST': "192.0.2.0", { 'HOST': "192.0.2.0",
'ABC': '123 192.0.2.0', 'ABC': '123 192.0.2.0',
'xyz': '890 123 192.0.2.0', 'xyz': '890 123 192.0.2.0',
@ -102,15 +97,15 @@ class ExecuteAction(LogCaptureTestCase):
# Callable # Callable
self.assertEqual( self.assertEqual(
self.__action.replaceTag("09 <callable> 11", self.__action.replaceTag("09 <callme> 11",
{'callable': lambda: str(10)}), CallingMap(callme=lambda: str(10))),
"09 10 11") "09 10 11")
# As tag not present, therefore callable should not be called # As tag not present, therefore callable should not be called
# Will raise ValueError if it is # Will raise ValueError if it is
self.assertEqual( self.assertEqual(
self.__action.replaceTag("abc", self.__action.replaceTag("abc",
{'callable': lambda: int("a")}), "abc") CallingMap(callme=lambda: int("a"))), "abc")
def testExecuteActionBan(self): def testExecuteActionBan(self):
self.__action.setActionStart("touch /tmp/fail2ban.test") self.__action.setActionStart("touch /tmp/fail2ban.test")
@ -172,20 +167,31 @@ class ExecuteAction(LogCaptureTestCase):
self.assertTrue(self._is_logged('Nothing to do')) self.assertTrue(self._is_logged('Nothing to do'))
def testExecuteIncorrectCmd(self): 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"')) self.assertTrue(self._is_logged('HINT on 127: "Command not found"'))
def testExecuteTimeout(self): def testExecuteTimeout(self):
stime = time.time() 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.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 -- timed out after 2 seconds'))
self.assertTrue(self._is_logged('sleep 60 -- killed with SIGTERM')) self.assertTrue(self._is_logged('sleep 60 -- killed with SIGTERM'))
def testCaptureStdOutErr(self): 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'")) 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') 'echo "The rain in Spain stays mainly in the plain" 1>&2')
self.assertTrue(self._is_logged( self.assertTrue(self._is_logged(
"'The rain in Spain stays mainly in the plain\\n'")) "'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)

View File

@ -0,0 +1,22 @@
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__)
def execActionStart(self):
self.logSys.debug("%s action start" % self.__class__.__name__)
def execActionStop(self):
self.logSys.debug("%s action stop" % self.__class__.__name__)
def execActionBan(self, aInfo):
self.logSys.debug("%s action ban" % self.__class__.__name__)
def execActionUnban(self, aInfo):
self.logSys.debug("%s action unban" % self.__class__.__name__)
Action = TestAction

View File

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

View File

@ -564,6 +564,23 @@ class Transmitter(TransmitterBase):
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", self.jailName, "delaction", "Doesn't exist"])[0],1) ["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"),
'{"opt1": "value"}']),
(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): def testNOK(self):
self.assertEqual(self.transm.proceed(["INVALID", "COMMAND"])[0],1) 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 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 .I filter.d/*.conf filter.d/*.local
.SH DESCRIPTION .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 will try to use the following backends, in order: pyinotify, gamin, polling
.PP .PP
.SS Actions .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 .PP
.nf .nf
[ssh-iptables-ipset] [ssh-iptables-ipset]
enabled = true enabled = true
action = sendmail[name=ssh, dest=john@example.com, actname=mail-john] action = sendmail[name=ssh, dest=john@example.com, actname=mail-john]
sendmail[name=ssh, dest=paul@example.com, actname=mail-paul] 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 .fi
.SH "ACTION FILES" .SH "ACTION FILES"
@ -160,6 +162,8 @@ two commands to be executed.
actionban = iptables -I fail2ban-<name> --source <ip> -j DROP actionban = iptables -I fail2ban-<name> --source <ip> -j DROP
echo ip=<ip>, match=<match>, time=<time> >> /var/log/fail2ban.log 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" .SS "Action Tags"
The following tags are substituted in the actionban, actionunban and actioncheck (when called before actionban/actionunban) commands. 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") glob("config/filter.d/*.conf")
), ),
('/etc/fail2ban/action.d', ('/etc/fail2ban/action.d',
glob("config/action.d/*.conf") glob("config/action.d/*.*")
), ),
('/etc/fail2ban/fail2ban.d', ('/etc/fail2ban/fail2ban.d',
'' ''