mirror of https://github.com/fail2ban/fail2ban
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
parent
6f104638cf
commit
f37c90cdba
|
@ -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
|
|
@ -25,6 +25,7 @@ __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 fail2ban.client.configreader import ConfigReader
|
from fail2ban.client.configreader import ConfigReader
|
||||||
from fail2ban.client.filterreader import FilterReader
|
from fail2ban.client.filterreader import FilterReader
|
||||||
|
@ -120,8 +121,20 @@ 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)
|
||||||
|
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:
|
||||||
action = ActionReader(
|
action = ActionReader(
|
||||||
actName, self.__name, actOpt, basedir=self.getBaseDir())
|
actName, self.__name, actOpt,
|
||||||
|
basedir=self.getBaseDir())
|
||||||
ret = action.read()
|
ret = action.read()
|
||||||
if ret:
|
if ret:
|
||||||
action.getOptions(self.__opts)
|
action.getOptions(self.__opts)
|
||||||
|
@ -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:
|
||||||
|
if isinstance(action, ConfigReader):
|
||||||
stream.extend(action.convert())
|
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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -23,6 +23,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 subprocess import call
|
#from subprocess import call
|
||||||
|
|
||||||
# Gets the instance of the logger.
|
# 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
|
# 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, 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):
|
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,21 +125,9 @@ 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 +136,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 +202,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 +301,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 +312,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():
|
||||||
|
@ -291,15 +333,13 @@ class Action:
|
||||||
m = t.search(value, m.start() + 1)
|
m = t.search(value, m.start() + 1)
|
||||||
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.
|
||||||
|
@ -308,8 +348,8 @@ 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
|
||||||
|
@ -321,12 +361,11 @@ class Action:
|
||||||
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.
|
||||||
|
@ -348,26 +387,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.
|
||||||
|
@ -381,7 +420,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:
|
||||||
|
@ -440,5 +479,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)
|
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
|
|
||||||
import time, logging
|
import time, logging
|
||||||
|
import os
|
||||||
|
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 Action
|
from fail2ban.server.action import ActionBase, CommandAction
|
||||||
from fail2ban.server.mytime import MyTime
|
from fail2ban.server.mytime import MyTime
|
||||||
|
|
||||||
# Gets the instance of the logger.
|
# Gets the instance of the logger.
|
||||||
|
@ -62,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)
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -153,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:
|
||||||
|
try:
|
||||||
action.execActionStart()
|
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")
|
||||||
|
@ -165,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:
|
||||||
|
try:
|
||||||
action.execActionStop()
|
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
|
||||||
|
|
||||||
|
@ -201,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:
|
||||||
|
try:
|
||||||
action.execActionBan(aInfo)
|
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(),
|
||||||
|
@ -241,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:
|
||||||
|
try:
|
||||||
action.execActionUnban(aInfo)
|
action.execActionUnban(aInfo)
|
||||||
|
except Exception as e:
|
||||||
|
logSys.error(
|
||||||
|
"Failed to execute unban jail '%s' action '%s': %s",
|
||||||
|
self.jail.getName(), action.getName(), e)
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -31,7 +31,7 @@ from fail2ban.server.datedetector import DateDetector
|
||||||
from fail2ban.server.datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n
|
from fail2ban.server.datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n
|
||||||
from fail2ban.server.mytime import MyTime
|
from fail2ban.server.mytime import MyTime
|
||||||
from fail2ban.server.failregex import FailRegex, Regex, RegexException
|
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.
|
# 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
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ from fail2ban.server.transmitter import Transmitter
|
||||||
from fail2ban.server.asyncserver import AsyncServer
|
from fail2ban.server.asyncserver import AsyncServer
|
||||||
from fail2ban.server.asyncserver import AsyncServerException
|
from fail2ban.server.asyncserver import AsyncServerException
|
||||||
from fail2ban.server.database import Fail2BanDb
|
from fail2ban.server.database import Fail2BanDb
|
||||||
|
from fail2ban.server.action import CommandAction
|
||||||
from fail2ban import version
|
from fail2ban import version
|
||||||
|
|
||||||
# Gets the instance of the logger.
|
# Gets the instance of the logger.
|
||||||
|
@ -278,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()
|
||||||
|
@ -290,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)
|
||||||
|
@ -311,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):
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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,20 @@ 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"), {})
|
||||||
|
|
||||||
|
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", {})
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
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,15 +50,15 @@ 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>'}))
|
||||||
# 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',
|
||||||
|
@ -169,20 +164,20 @@ 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'"))
|
||||||
|
|
|
@ -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
|
|
@ -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):
|
||||||
|
|
|
@ -564,6 +564,22 @@ 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"), "{}"]),
|
||||||
|
(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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue