231 lines
6.1 KiB
Python
231 lines
6.1 KiB
Python
|
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||
|
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||
|
|
||
|
# This file is part of Fail2Ban.
|
||
|
#
|
||
|
# Fail2Ban is free software; you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU General Public License as published by
|
||
|
# the Free Software Foundation; either version 2 of the License, or
|
||
|
# (at your option) any later version.
|
||
|
#
|
||
|
# Fail2Ban is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License
|
||
|
# along with Fail2Ban; if not, write to the Free Software
|
||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
|
||
|
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)i 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):
|
||
|
"""Fail2Ban action which sends emails to inform on jail starting,
|
||
|
stopping and bans.
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self, jail, name, host="localhost", user=None, password=None,
|
||
|
sendername="Fail2Ban", sender="fail2ban", dest="root", matches=None):
|
||
|
"""Initialise action.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
jail : Jail
|
||
|
The jail which the action belongs to.
|
||
|
name : str
|
||
|
Named assigned to the action.
|
||
|
host : str, optional
|
||
|
SMTP host, of host:port format. Default host "localhost" and
|
||
|
port "25"
|
||
|
user : str, optional
|
||
|
Username used for authentication with SMTP server.
|
||
|
password : str, optional
|
||
|
Password used for authentication with SMTP server.
|
||
|
sendername : str, optional
|
||
|
Name to use for from address in email. Default "Fail2Ban".
|
||
|
sender : str, optional
|
||
|
Email address to use for from address in email.
|
||
|
Default "fail2ban".
|
||
|
dest : str, optional
|
||
|
Email addresses of intended recipient(s) in comma space ", "
|
||
|
delimited format. Default "root".
|
||
|
matches : str, optional
|
||
|
Type of matches to be included from ban in email. Can be one
|
||
|
of "matches", "ipmatches" or "ipjailmatches". Default None
|
||
|
(see man jail.conf.5).
|
||
|
"""
|
||
|
|
||
|
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.name,
|
||
|
hostname = socket.gethostname,
|
||
|
bantime = lambda: self._jail.actions.getBanTime(),
|
||
|
)
|
||
|
|
||
|
# bypass ban/unban for restored tickets
|
||
|
self.norestored = 1
|
||
|
|
||
|
def _sendMessage(self, subject, text):
|
||
|
"""Sends message based on arguments and instance's properties.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
subject : str
|
||
|
Subject of the email.
|
||
|
text : str
|
||
|
Body of the email.
|
||
|
|
||
|
Raises
|
||
|
------
|
||
|
SMTPConnectionError
|
||
|
Error on connecting to host.
|
||
|
SMTPAuthenticationError
|
||
|
Error authenticating with SMTP server.
|
||
|
SMTPException
|
||
|
See Python `smtplib` for full list of other possible
|
||
|
exceptions.
|
||
|
"""
|
||
|
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",
|
||
|
self.host, *smtp.connect(self.host))
|
||
|
if self.user and self.password: # pragma: no cover (ATM no tests covering that)
|
||
|
smtp.login(self.user, self.password)
|
||
|
failed_recipients = smtp.sendmail(
|
||
|
self.fromaddr, self.toaddr.split(", "), msg.as_string())
|
||
|
except smtplib.SMTPConnectError: # pragma: no cover
|
||
|
self._logSys.error("Error connecting to host '%s'", self.host)
|
||
|
raise
|
||
|
except smtplib.SMTPAuthenticationError: # pragma: no cover
|
||
|
self._logSys.error(
|
||
|
"Failed to authenticate with host '%s' user '%s'",
|
||
|
self.host, self.user)
|
||
|
raise
|
||
|
except smtplib.SMTPException: # pragma: no cover
|
||
|
self._logSys.error(
|
||
|
"Error sending mail to host '%s' from '%s' to '%s'",
|
||
|
self.host, self.fromaddr, self.toaddr)
|
||
|
raise
|
||
|
else:
|
||
|
if failed_recipients: # pragma: no cover
|
||
|
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._logSys.debug("Disconnected from '%s', response %i: %s",
|
||
|
self.host, *smtp.quit())
|
||
|
except smtplib.SMTPServerDisconnected: # pragma: no cover
|
||
|
pass # Not connected
|
||
|
|
||
|
def start(self):
|
||
|
"""Sends email to recipients informing that the jail has started.
|
||
|
"""
|
||
|
self._sendMessage(
|
||
|
"[Fail2Ban] %(jailname)s: started on %(hostname)s" %
|
||
|
self.message_values,
|
||
|
messages['start'] % self.message_values)
|
||
|
|
||
|
def stop(self):
|
||
|
"""Sends email to recipients informing that the jail has stopped.
|
||
|
"""
|
||
|
self._sendMessage(
|
||
|
"[Fail2Ban] %(jailname)s: stopped on %(hostname)s" %
|
||
|
self.message_values,
|
||
|
messages['stop'] % self.message_values)
|
||
|
|
||
|
def ban(self, aInfo):
|
||
|
"""Sends email to recipients informing that ban has occurred.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
aInfo : dict
|
||
|
Dictionary which includes information in relation to
|
||
|
the ban.
|
||
|
"""
|
||
|
if aInfo.get('restored'):
|
||
|
return
|
||
|
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
|