summaryrefslogtreecommitdiff
path: root/fail2ban/server/filterpoll.py
blob: 5905c5b5f898b287e114d537ca5bd1985715ddf2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# 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.

# Author: Cyril Jaquier, Yaroslav Halchenko
#

__author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier; 2012 Yaroslav Halchenko"
__license__ = "GPL"

import os
import time

from .failmanager import FailManagerEmpty
from .filter import FileFilter
from .mytime import MyTime
from .utils import Utils
from ..helpers import getLogger, logging


# 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 FilterPoll(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
		## The time of the last modification of the file.
		self.__prevStats = dict()
		self.__file404Cnt = dict()
		logSys.debug("Created FilterPoll")

	##
	# Add a log file path
	#
	# @param path log file path

	def _addLogPath(self, path):
		self.__prevStats[path] = (0, None, None)	 # mtime, ino, size
		self.__file404Cnt[path] = 0

	##
	# Delete a log path
	#
	# @param path the log file to delete

	def _delLogPath(self, path):
		del self.__prevStats[path]
		del self.__file404Cnt[path]

	##
	# Get a modified log path at once
	#
	def getModified(self, modlst):
		for filename in self.getLogPaths():
			if self.isModified(filename):
				modlst.append(filename)
		return modlst

	##
	# Main loop.
	#
	# This function is the main loop of the thread. It checks if the
	# file has been modified and looks for failures.
	# @return True when the thread exits nicely

	def run(self):
		while self.active:
			try:
				if logSys.getEffectiveLevel() <= 6:
					logSys.log(6, "Woke up idle=%s with %d files monitored",
							   self.idle, self.getLogCount())
				if self.idle:
					if not Utils.wait_for(lambda: not self.active or not self.idle, 
						self.sleeptime * 10, self.sleeptime
					):
						self.ticks += 1
						continue
				# Get file modification
				modlst = []
				Utils.wait_for(lambda: not self.active or self.getModified(modlst),
					self.sleeptime)
				for filename in modlst:
					self.getFailures(filename)
					self.__modified = True

				self.ticks += 1
				if self.__modified:
					try:
						while True:
							ticket = self.failManager.toBan()
							self.jail.putFailTicket(ticket)
					except FailManagerEmpty:
						self.failManager.cleanup(MyTime.time())
					self.__modified = False
			except Exception as e: # pragma: no cover
				if not self.active: # if not active - error by stop...
					break
				logSys.error("Caught unhandled exception in main cycle: %r", e,
					exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
				# incr common error counter:
				self.commonError()
		logSys.debug("[%s] filter terminated", self.jailName)
		return True

	##
	# Checks if the log file has been modified.
	#
	# Checks if the log file has been modified using os.stat().
	# @return True if log file has been modified

	def isModified(self, filename):
		try:
			logStats = os.stat(filename)
			stats = logStats.st_mtime, logStats.st_ino, logStats.st_size
			pstats = self.__prevStats.get(filename, (0))
			if logSys.getEffectiveLevel() <= 5:
				# we do not want to waste time on strftime etc if not necessary
				dt = logStats.st_mtime - pstats[0]
				logSys.log(5, "Checking %s for being modified. Previous/current stats: %s / %s. dt: %s",
				           filename, pstats, stats, dt)
				# os.system("stat %s | grep Modify" % filename)
			self.__file404Cnt[filename] = 0
			if pstats == stats:
				return False
			logSys.debug("%s has been modified", filename)
			self.__prevStats[filename] = stats
			return True
		except Exception as e:
			# still alive (may be deleted because multi-threaded):
			if not self.getLog(filename) or self.__prevStats.get(filename) is None:
				logSys.warning("Log %r seems to be down: %s", filename, e)
				return False
			# log error:
			if self.__file404Cnt[filename] < 2:
				if e.errno == 2:
					logSys.debug("Log absence detected (possibly rotation) for %s, reason: %s",
							 filename, e)
				else: # pragma: no cover
					logSys.error("Unable to get stat on %s because of: %s",
							 filename, e, 
							 exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
			# increase file and common error counters:
			self.__file404Cnt[filename] += 1
			self.commonError()
			if self.__file404Cnt[filename] > 50:
				logSys.warning("Too many errors. Remove file %r from monitoring process", filename)
				self.__file404Cnt[filename] = 0
				self.delLogPath(filename)
			return False

	def getPendingPaths(self):
		return self.__file404Cnt.keys()