Subversion Repositories configs

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
192 - 1
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
2
# vi: set ft=python sts=4 ts=4 sw=4 noet :
3
 
4
# This file is part of Fail2Ban.
5
#
6
# Fail2Ban is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# Fail2Ban is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with Fail2Ban; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19
 
20
import sys
21
if sys.version_info < (2, 7): # pragma: no cover
22
	raise ImportError("badips.py action requires Python >= 2.7")
23
import json
24
import threading
25
import logging
26
if sys.version_info >= (3, ): # pragma: 2.x no cover
27
	from urllib.request import Request, urlopen
28
	from urllib.parse import urlencode
29
	from urllib.error import HTTPError
30
else: # pragma: 3.x no cover
31
	from urllib.request import Request, urlopen
32
	from urllib.error import HTTPError
33
	from urllib.parse import urlencode
34
 
35
from fail2ban.server.actions import Actions, ActionBase, BanTicket
36
from fail2ban.helpers import splitwords, str2LogLevel
37
 
38
 
39
 
40
class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
41
	"""Fail2Ban action which reports bans to badips.com, and also
42
	blacklist bad IPs listed on badips.com by using another action's
43
	ban method.
44
 
45
	Parameters
46
	----------
47
	jail : Jail
48
		The jail which the action belongs to.
49
	name : str
50
		Name assigned to the action.
51
	category : str
52
		Valid badips.com category for reporting failures.
53
	score : int, optional
54
		Minimum score for bad IPs. Default 3.
55
	age : str, optional
56
		Age of last report for bad IPs, per badips.com syntax.
57
		Default "24h" (24 hours)
58
	banaction : str, optional
59
		Name of banaction to use for blacklisting bad IPs. If `None`,
60
		no blacklist of IPs will take place.
61
		Default `None`.
62
	bancategory : str, optional
63
		Name of category to use for blacklisting, which can differ
64
		from category used for reporting. e.g. may want to report
65
		"postfix", but want to use whole "mail" category for blacklist.
66
		Default `category`.
67
	bankey : str, optional
68
		Key issued by badips.com to retrieve personal list
69
		of blacklist IPs.
70
	updateperiod : int, optional
71
		Time in seconds between updating bad IPs blacklist.
72
		Default 900 (15 minutes)
73
	loglevel : int/str, optional
74
		Log level of the message when an IP is (un)banned.
75
		Default `DEBUG`.
76
		Can be also supplied as two-value list (comma- or space separated) to
77
		provide level of the summary message when a group of IPs is (un)banned.
78
		Example `DEBUG,INFO`.
79
	agent : str, optional
80
		User agent transmitted to server.
81
		Default `Fail2Ban/ver.`
82
 
83
	Raises
84
	------
85
	ValueError
86
		If invalid `category`, `score`, `banaction` or `updateperiod`.
87
	"""
88
 
89
	TIMEOUT = 10
90
	_badips = "https://www.badips.com"
91
	def _Request(self, url, **argv):
92
		return Request(url, headers={'User-Agent': self.agent}, **argv)
93
 
94
	def __init__(self, jail, name, category, score=3, age="24h",
95
		banaction=None, bancategory=None, bankey=None, updateperiod=900,
96
		loglevel='DEBUG', agent="Fail2Ban", timeout=TIMEOUT):
97
		super(BadIPsAction, self).__init__(jail, name)
98
 
99
		self.timeout = timeout
100
		self.agent = agent
101
		self.category = category
102
		self.score = score
103
		self.age = age
104
		self.banaction = banaction
105
		self.bancategory = bancategory or category
106
		self.bankey = bankey
107
		loglevel = splitwords(loglevel)
108
		self.sumloglevel = str2LogLevel(loglevel[-1])
109
		self.loglevel = str2LogLevel(loglevel[0])
110
		self.updateperiod = updateperiod
111
 
112
		self._bannedips = set()
113
		# Used later for threading.Timer for updating badips
114
		self._timer = None
115
 
116
	@staticmethod
117
	def isAvailable(timeout=1):
118
		try:
119
			response = urlopen(Request("/".join([BadIPsAction._badips]),
120
					headers={'User-Agent': "Fail2Ban"}), timeout=timeout)
121
			return True, ''
122
		except Exception as e: # pragma: no cover
123
			return False, e
124
 
125
	def logError(self, response, what=''): # pragma: no cover - sporadical (502: Bad Gateway, etc)
126
		messages = {}
127
		try:
128
			messages = json.loads(response.read().decode('utf-8'))
129
		except:
130
			pass
131
		self._logSys.error(
132
			"%s. badips.com response: '%s'", what,
133
				messages.get('err', 'Unknown'))
134
 
135
	def getCategories(self, incParents=False):
136
		"""Get badips.com categories.
137
 
138
		Returns
139
		-------
140
		set
141
			Set of categories.
142
 
143
		Raises
144
		------
145
		HTTPError
146
			Any issues with badips.com request.
147
		ValueError
148
			If badips.com response didn't contain necessary information
149
		"""
150
		try:
151
			response = urlopen(
152
				self._Request("/".join([self._badips, "get", "categories"])), timeout=self.timeout)
153
		except HTTPError as response: # pragma: no cover
154
			self.logError(response, "Failed to fetch categories")
155
			raise
156
		else:
157
			response_json = json.loads(response.read().decode('utf-8'))
158
			if not 'categories' in response_json:
159
				err = "badips.com response lacked categories specification. Response was: %s" \
160
				  % (response_json,)
161
				self._logSys.error(err)
162
				raise ValueError(err)
163
			categories = response_json['categories']
164
			categories_names = set(
165
				value['Name'] for value in categories)
166
			if incParents:
167
				categories_names.update(set(
168
					value['Parent'] for value in categories
169
					if "Parent" in value))
170
			return categories_names
171
 
172
	def getList(self, category, score, age, key=None):
173
		"""Get badips.com list of bad IPs.
174
 
175
		Parameters
176
		----------
177
		category : str
178
			Valid badips.com category.
179
		score : int
180
			Minimum score for bad IPs.
181
		age : str
182
			Age of last report for bad IPs, per badips.com syntax.
183
		key : str, optional
184
			Key issued by badips.com to fetch IPs reported with the
185
			associated key.
186
 
187
		Returns
188
		-------
189
		set
190
			Set of bad IPs.
191
 
192
		Raises
193
		------
194
		HTTPError
195
			Any issues with badips.com request.
196
		"""
197
		try:
198
			url = "?".join([
199
				"/".join([self._badips, "get", "list", category, str(score)]),
200
				urlencode({'age': age})])
201
			if key:
202
				url = "&".join([url, urlencode({'key': key})])
203
			self._logSys.debug('badips.com: get list, url: %r', url)
204
			response = urlopen(self._Request(url), timeout=self.timeout)
205
		except HTTPError as response: # pragma: no cover
206
			self.logError(response, "Failed to fetch bad IP list")
207
			raise
208
		else:
209
			return set(response.read().decode('utf-8').split())
210
 
211
	@property
212
	def category(self):
213
		"""badips.com category for reporting IPs.
214
		"""
215
		return self._category
216
 
217
	@category.setter
218
	def category(self, category):
219
		if category not in self.getCategories():
220
			self._logSys.error("Category name '%s' not valid. "
221
				"see badips.com for list of valid categories",
222
				category)
223
			raise ValueError("Invalid category: %s" % category)
224
		self._category = category
225
 
226
	@property
227
	def bancategory(self):
228
		"""badips.com bancategory for fetching IPs.
229
		"""
230
		return self._bancategory
231
 
232
	@bancategory.setter
233
	def bancategory(self, bancategory):
234
		if bancategory != "any" and bancategory not in self.getCategories(incParents=True):
235
			self._logSys.error("Category name '%s' not valid. "
236
				"see badips.com for list of valid categories",
237
				bancategory)
238
			raise ValueError("Invalid bancategory: %s" % bancategory)
239
		self._bancategory = bancategory
240
 
241
	@property
242
	def score(self):
243
		"""badips.com minimum score for fetching IPs.
244
		"""
245
		return self._score
246
 
247
	@score.setter
248
	def score(self, score):
249
		score = int(score)
250
		if 0 <= score <= 5:
251
			self._score = score
252
		else:
253
			raise ValueError("Score must be 0-5")
254
 
255
	@property
256
	def banaction(self):
257
		"""Jail action to use for banning/unbanning.
258
		"""
259
		return self._banaction
260
 
261
	@banaction.setter
262
	def banaction(self, banaction):
263
		if banaction is not None and banaction not in self._jail.actions:
264
			self._logSys.error("Action name '%s' not in jail '%s'",
265
				banaction, self._jail.name)
266
			raise ValueError("Invalid banaction")
267
		self._banaction = banaction
268
 
269
	@property
270
	def updateperiod(self):
271
		"""Period in seconds between banned bad IPs will be updated.
272
		"""
273
		return self._updateperiod
274
 
275
	@updateperiod.setter
276
	def updateperiod(self, updateperiod):
277
		updateperiod = int(updateperiod)
278
		if updateperiod > 0:
279
			self._updateperiod = updateperiod
280
		else:
281
			raise ValueError("Update period must be integer greater than 0")
282
 
283
	def _banIPs(self, ips):
284
		for ip in ips:
285
			try:
286
				ai = Actions.ActionInfo(BanTicket(ip), self._jail)
287
				self._jail.actions[self.banaction].ban(ai)
288
			except Exception as e:
289
				self._logSys.error(
290
					"Error banning IP %s for jail '%s' with action '%s': %s",
291
					ip, self._jail.name, self.banaction, e,
292
					exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
293
			else:
294
				self._bannedips.add(ip)
295
				self._logSys.log(self.loglevel,
296
					"Banned IP %s for jail '%s' with action '%s'",
297
					ip, self._jail.name, self.banaction)
298
 
299
	def _unbanIPs(self, ips):
300
		for ip in ips:
301
			try:
302
				ai = Actions.ActionInfo(BanTicket(ip), self._jail)
303
				self._jail.actions[self.banaction].unban(ai)
304
			except Exception as e:
305
				self._logSys.error(
306
					"Error unbanning IP %s for jail '%s' with action '%s': %s",
307
					ip, self._jail.name, self.banaction, e,
308
					exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
309
			else:
310
				self._logSys.log(self.loglevel,
311
					"Unbanned IP %s for jail '%s' with action '%s'",
312
					ip, self._jail.name, self.banaction)
313
			finally:
314
				self._bannedips.remove(ip)
315
 
316
	def start(self):
317
		"""If `banaction` set, blacklists bad IPs.
318
		"""
319
		if self.banaction is not None:
320
			self.update()
321
 
322
	def update(self):
323
		"""If `banaction` set, updates blacklisted IPs.
324
 
325
		Queries badips.com for list of bad IPs, removing IPs from the
326
		blacklist if no longer present, and adds new bad IPs to the
327
		blacklist.
328
		"""
329
		if self.banaction is not None:
330
			if self._timer:
331
				self._timer.cancel()
332
				self._timer = None
333
 
334
			try:
335
				ips = self.getList(
336
					self.bancategory, self.score, self.age, self.bankey)
337
				# Remove old IPs no longer listed
338
				s = self._bannedips - ips
339
				m = len(s)
340
				self._unbanIPs(s)
341
				# Add new IPs which are now listed
342
				s = ips - self._bannedips
343
				p = len(s)
344
				self._banIPs(s)
345
				if m != 0 or p != 0:
346
					self._logSys.log(self.sumloglevel,
347
						"Updated IPs for jail '%s' (-%d/+%d)",
348
						self._jail.name, m, p)
349
				self._logSys.debug(
350
					"Next update for jail '%' in %i seconds",
351
					self._jail.name, self.updateperiod)
352
			finally:
353
				self._timer = threading.Timer(self.updateperiod, self.update)
354
				self._timer.start()
355
 
356
	def stop(self):
357
		"""If `banaction` set, clears blacklisted IPs.
358
		"""
359
		if self.banaction is not None:
360
			if self._timer:
361
				self._timer.cancel()
362
				self._timer = None
363
			self._unbanIPs(self._bannedips.copy())
364
 
365
	def ban(self, aInfo):
366
		"""Reports banned IP to badips.com.
367
 
368
		Parameters
369
		----------
370
		aInfo : dict
371
			Dictionary which includes information in relation to
372
			the ban.
373
 
374
		Raises
375
		------
376
		HTTPError
377
			Any issues with badips.com request.
378
		"""
379
		try:
380
			url = "/".join([self._badips, "add", self.category, str(aInfo['ip'])])
381
			self._logSys.debug('badips.com: ban, url: %r', url)
382
			response = urlopen(self._Request(url), timeout=self.timeout)
383
		except HTTPError as response: # pragma: no cover
384
			self.logError(response, "Failed to ban")
385
			raise
386
		else:
387
			messages = json.loads(response.read().decode('utf-8'))
388
			self._logSys.debug(
389
				"Response from badips.com report: '%s'",
390
				messages['suc'])
391
 
392
Action = BadIPsAction