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.mime.text import MIMEText
from email.utils import formatdate, formataddr from email.utils import formatdate, formataddr
from fail2ban.server.actions import ActionBase from fail2ban.server.actions import ActionBase, CallingMap
messages = {} messages = {}
messages['start'] = \ messages['start'] = \
@ -24,14 +24,32 @@ The jail %(jailname)s has been stopped.
Regards, Regards,
Fail2Ban""" Fail2Ban"""
messages['ban'] = \ messages['ban'] = {}
messages['ban']['head'] = \
"""Hi, """Hi,
The IP %(ip)s has just been banned for %(bantime)s seconds The IP %(ip)s has just been banned for %(bantime)s seconds
by Fail2Ban after %(failures)i attempts against %(jailname)s. by Fail2Ban after %(failures)i attempts against %(jailname)s.
"""
messages['ban']['tail'] = \
"""
Regards, Regards,
Fail2Ban""" 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): class SMTPAction(ActionBase):
@ -43,7 +61,7 @@ class SMTPAction(ActionBase):
#TODO: self.ssl = initOpts.get('ssl', "no") == 'yes' #TODO: self.ssl = initOpts.get('ssl', "no") == 'yes'
self.user = initOpts.get('user', '') self.user = initOpts.get('user', '')
self.password = initOpts.get('password', None) self.password = initOpts.get('password')
self.fromname = initOpts.get('sendername', "Fail2Ban") self.fromname = initOpts.get('sendername', "Fail2Ban")
self.fromaddr = initOpts.get('sender', "fail2ban") self.fromaddr = initOpts.get('sender', "fail2ban")
@ -51,6 +69,14 @@ class SMTPAction(ActionBase):
self.smtp = smtplib.SMTP() 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): def _sendMessage(self, subject, text):
msg = MIMEText(text) msg = MIMEText(text)
msg['Subject'] = subject msg['Subject'] = subject
@ -90,14 +116,6 @@ class SMTPAction(ActionBase):
except smtplib.SMTPServerDisconnected: except smtplib.SMTPServerDisconnected:
pass # Not connected pass # Not connected
@property
def message_values(self):
return {
'jailname': self.jail.getName(),
'hostname': socket.gethostname(),
'bantime': self.jail.getAction().getBanTime(),
}
def execActionStart(self): def execActionStart(self):
self._sendMessage( self._sendMessage(
"[Fail2Ban] %(jailname)s: started on %(hostname)s" % "[Fail2Ban] %(jailname)s: started on %(hostname)s" %
@ -111,9 +129,15 @@ class SMTPAction(ActionBase):
messages['stop'] % self.message_values) messages['stop'] % self.message_values)
def execActionBan(self, aInfo): def execActionBan(self, aInfo):
aInfo.update(self.message_values)
message = "".join([
messages['ban']['head'],
messages['ban'].get(self.matches, ""),
messages['ban']['tail']
])
self._sendMessage( self._sendMessage(
"[Fail2Ban] %(jailname)s: banned %(ip)s from %(hostname)s" % "[Fail2Ban] %(jailname)s: banned %(ip)s from %(hostname)s" %
dict(self.message_values, **aInfo), aInfo,
messages['ban'] % dict(self.message_values, **aInfo)) message % aInfo)
Action = SMTPAction Action = SMTPAction

View File

@ -24,6 +24,7 @@ __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 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.
@ -47,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.
# #
@ -353,11 +372,9 @@ class CommandAction(ActionBase):
""" 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

View File

@ -30,7 +30,7 @@ import imp
from fail2ban.server.banmanager import BanManager from fail2ban.server.banmanager import BanManager
from fail2ban.server.jailthread import JailThread 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 from fail2ban.server.mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.
@ -202,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()

View File

@ -27,7 +27,7 @@ __license__ = "GPL"
import time import time
import logging, sys import logging, sys
from fail2ban.server.action import CommandAction from fail2ban.server.action import CommandAction, CallingMap
from fail2ban.tests.utils import LogCaptureTestCase from fail2ban.tests.utils import LogCaptureTestCase
@ -94,15 +94,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")
@ -181,3 +181,11 @@ class ExecuteAction(LogCaptureTestCase):
'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'))
# 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)