fail2ban/fail2ban/server/filterpyinotify.py

214 lines
6.3 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.
# Original author: Cyril Jaquier
__author__ = "Cyril Jaquier, Lee Clemens, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Yaroslav Halchenko"
__license__ = "GPL"
import logging
from distutils.version import LooseVersion
from os.path import dirname, sep as pathsep
import pyinotify
from .failmanager import FailManagerEmpty
from .filter import FileFilter
from .mytime import MyTime
from ..helpers import getLogger
if not hasattr(pyinotify, '__version__') \
or LooseVersion(pyinotify.__version__) < '0.8.3':
raise ImportError("Fail2Ban requires pyinotify >= 0.8.3")
# Verify that pyinotify is functional on this system
# Even though imports -- might be dysfunctional, e.g. as on kfreebsd
try:
manager = pyinotify.WatchManager()
del manager
except Exception as e:
raise ImportError("Pyinotify is probably not functional on this system: %s"
% str(e))
# Gets the instance of the logger.
logSys = getLogger(__name__)
##
# Log reader class.
#
# This class reads a log file and detects login failures or anything else
# that matches a given regular expression. This class is instantiated by
# a Jail object.
class FilterPyinotify(FileFilter):
##
# Constructor.
#
# Initialize the filter object with default values.
# @param jail the jail object
def __init__(self, jail):
FileFilter.__init__(self, jail)
self.__modified = False
# Pyinotify watch manager
self.__monitor = pyinotify.WatchManager()
self.__watches = dict()
logSys.debug("Created FilterPyinotify")
def callback(self, event, origin=''):
logSys.debug("%sCallback for Event: %s", origin, event)
path = event.pathname
if event.mask & ( pyinotify.IN_CREATE | pyinotify.IN_MOVED_TO ):
# skip directories altogether
if event.mask & pyinotify.IN_ISDIR:
logSys.debug("Ignoring creation of directory %s", path)
return
# check if that is a file we care about
if not path in self.__watches:
logSys.debug("Ignoring creation of %s we do not monitor", path)
return
else:
# we need to substitute the watcher with a new one, so first
# remove old one
self._delFileWatcher(path)
# place a new one
self._addFileWatcher(path)
self._process_file(path)
def _process_file(self, path):
"""Process a given file
TODO -- RF:
this is a common logic and must be shared/provided by FileFilter
"""
self.getFailures(path)
try:
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.dateDetector.sortTemplate()
self.__modified = False
def _addFileWatcher(self, path):
wd = self.__monitor.add_watch(path, pyinotify.IN_MODIFY)
self.__watches.update(wd)
logSys.debug("Added file watcher for %s", path)
def _delFileWatcher(self, path):
wdInt = self.__watches[path]
wd = self.__monitor.rm_watch(wdInt)
if wd[wdInt]:
del self.__watches[path]
logSys.debug("Removed file watcher for %s", path)
return True
else:
return False
##
# Add a log file path
#
# @param path log file path
def _addLogPath(self, path):
path_dir = dirname(path)
if not (path_dir in self.__watches):
# we need to watch also the directory for IN_CREATE
self.__watches.update(
self.__monitor.add_watch(path_dir, pyinotify.IN_CREATE | pyinotify.IN_MOVED_TO))
logSys.debug("Added monitor for the parent directory %s", path_dir)
self._addFileWatcher(path)
self._process_file(path)
##
# Delete a log path
#
# @param path the log file to delete
def _delLogPath(self, path):
if not self._delFileWatcher(path):
logSys.error("Failed to remove watch on path: %s", path)
path_dir = dirname(path)
if not len([k for k in self.__watches
if k.startswith(path_dir + pathsep)]):
# Remove watches for the directory
# since there is no other monitored file under this directory
wdInt = self.__watches.pop(path_dir)
self.__monitor.rm_watch(wdInt)
logSys.debug("Removed monitor for the parent directory %s", path_dir)
##
# Main loop.
#
# Since all detection is offloaded to pyinotifier -- no manual
# loop is necessary
def run(self):
self.__notifier = pyinotify.ThreadedNotifier(self.__monitor,
ProcessPyinotify(self))
self.__notifier.start()
logSys.debug("pyinotifier started for %s.", self.jail.name)
# TODO: verify that there is nothing really to be done for
# idle jails
return True
##
# Call super.stop() and then stop the 'Notifier'
def stop(self):
super(FilterPyinotify, self).stop()
# Stop the notifier thread
self.__notifier.stop()
self.__notifier.join() # to not exit before notifier does
self.__cleanup() # for pedantic ones
##
# Deallocates the resources used by pyinotify.
def __cleanup(self):
self.__notifier = None
self.__monitor = None
class ProcessPyinotify(pyinotify.ProcessEvent):
def __init__(self, FileFilter, **kargs):
#super(ProcessPyinotify, self).__init__(**kargs)
# for some reason root class _ProcessEvent is old-style (is
# not derived from object), so to play safe let's avoid super
# for now, and call superclass directly
pyinotify.ProcessEvent.__init__(self, **kargs)
self.__FileFilter = FileFilter
pass
# just need default, since using mask on watch to limit events
def process_default(self, event):
try:
self.__FileFilter.callback(event, origin='Default ')
except Exception as e:
logSys.error("Error in FilterPyinotify callback: %s",
e, exc_info=logSys.getEffectiveLevel() <= logging.DEBUG)