ENH: Add matches to smtp.py action

pull/556/head
Steven Hiscocks 2014-01-01 12:27:49 +00:00
parent f37c90cdba
commit 6ef911185d
4 changed files with 73 additions and 24 deletions

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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 <callable> 11",
{'callable': lambda: str(10)}),
self.__action.replaceTag("09 <callme> 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)