diff --git a/config/action.d/smtp.py b/config/action.d/smtp.py index 4ab76d3f..4210a920 100644 --- a/config/action.d/smtp.py +++ b/config/action.d/smtp.py @@ -5,7 +5,7 @@ import smtplib from email.mime.text import MIMEText from email.utils import formatdate, formataddr -from fail2ban.server.actions import ActionBase +from fail2ban.server.actions import ActionBase, CallingMap messages = {} messages['start'] = \ @@ -24,14 +24,32 @@ The jail %(jailname)s has been stopped. Regards, Fail2Ban""" -messages['ban'] = \ +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): @@ -43,7 +61,7 @@ class SMTPAction(ActionBase): #TODO: self.ssl = initOpts.get('ssl', "no") == 'yes' self.user = initOpts.get('user', '') - self.password = initOpts.get('password', None) + self.password = initOpts.get('password') self.fromname = initOpts.get('sendername', "Fail2Ban") self.fromaddr = initOpts.get('sender', "fail2ban") @@ -51,6 +69,14 @@ class SMTPAction(ActionBase): self.smtp = smtplib.SMTP() + self.matches = initOpts.get('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 @@ -90,14 +116,6 @@ class SMTPAction(ActionBase): 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" % @@ -111,9 +129,15 @@ class SMTPAction(ActionBase): 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" % - dict(self.message_values, **aInfo), - messages['ban'] % dict(self.message_values, **aInfo)) + aInfo, + message % aInfo) Action = SMTPAction diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 209a452c..c4465503 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -24,6 +24,7 @@ __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. @@ -47,6 +48,24 @@ _RETCODE_HINTS = { signame = dict((num, name) 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. # @@ -353,11 +372,9 @@ class CommandAction(ActionBase): """ Replace tags in query """ 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 diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 73c005ef..a8d0946a 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -30,7 +30,7 @@ import imp from fail2ban.server.banmanager import BanManager from fail2ban.server.jailthread import JailThread -from fail2ban.server.action import ActionBase, CommandAction +from fail2ban.server.action import ActionBase, CommandAction, CallingMap from fail2ban.server.mytime import MyTime # Gets the instance of the logger. @@ -202,7 +202,7 @@ class Actions(JailThread): def __checkBan(self): ticket = self.jail.getFailTicket() if ticket != False: - aInfo = dict() + aInfo = CallingMap() bTicket = BanManager.createBanTicket(ticket) aInfo["ip"] = bTicket.getIP() aInfo["failures"] = bTicket.getAttempt() diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 0c7f98fb..bb212481 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -27,7 +27,7 @@ __license__ = "GPL" import time import logging, sys -from fail2ban.server.action import CommandAction +from fail2ban.server.action import CommandAction, CallingMap from fail2ban.tests.utils import LogCaptureTestCase @@ -94,15 +94,15 @@ 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") @@ -181,3 +181,11 @@ class ExecuteAction(LogCaptureTestCase): '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')) + + # Should work fine + self.assertEqual("%(callme)s okay" % mymap, "10 okay") + # Error will now trip, demonstrating delayed call + self.assertRaises(ValueError, lambda x: "%(error)i" % x, mymap)