Subversion Repositories configs

Rev

Rev 71 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
34 - 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):
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, ):
27
	from urllib.request import Request, urlopen
28
	from urllib.parse import urlencode
29
	from urllib.error import HTTPError
30
else:
31
	from urllib2 import Request, urlopen, HTTPError
32
	from urllib import urlencode
33
 
34
from fail2ban.server.actions import ActionBase
35
 
39 - 36
 
34 - 37
class BadIPsAction(ActionBase):
38
	"""Fail2Ban action which reports bans to badips.com, and also
39
	blacklist bad IPs listed on badips.com by using another action's
40
	ban method.
41
 
42
	Parameters
43
	----------
44
	jail : Jail
45
		The jail which the action belongs to.
46
	name : str
47
		Name assigned to the action.
48
	category : str
49
		Valid badips.com category for reporting failures.
50
	score : int, optional
51
		Minimum score for bad IPs. Default 3.
52
	age : str, optional
53
		Age of last report for bad IPs, per badips.com syntax.
54
		Default "24h" (24 hours)
55
	key : str, optional
56
		Key issued by badips.com to report bans, for later retrieval
57
		of personalised content.
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 blacklist IPs reported with the
69
		associated key.
70
	updateperiod : int, optional
71
		Time in seconds between updating bad IPs blacklist.
72
		Default 900 (15 minutes)
71 - 73
	agent : str, optional
74
		User agent transmitted to server.
75
		Default `Fail2Ban/ver.`
34 - 76
 
77
	Raises
78
	------
79
	ValueError
80
		If invalid `category`, `score`, `banaction` or `updateperiod`.
81
	"""
82
 
87 - 83
	TIMEOUT = 10
34 - 84
	_badips = "http://www.badips.com"
71 - 85
	def _Request(self, url, **argv):
86
		return Request(url, headers={'User-Agent': self.agent}, **argv)
34 - 87
 
88
	def __init__(self, jail, name, category, score=3, age="24h", key=None,
87 - 89
		banaction=None, bancategory=None, bankey=None, updateperiod=900, agent="Fail2Ban",
90
		timeout=TIMEOUT):
34 - 91
		super(BadIPsAction, self).__init__(jail, name)
92
 
87 - 93
		self.timeout = timeout
71 - 94
		self.agent = agent
34 - 95
		self.category = category
96
		self.score = score
97
		self.age = age
98
		self.key = key
99
		self.banaction = banaction
100
		self.bancategory = bancategory or category
101
		self.bankey = bankey
102
		self.updateperiod = updateperiod
103
 
104
		self._bannedips = set()
105
		# Used later for threading.Timer for updating badips
106
		self._timer = None
107
 
108
	def getCategories(self, incParents=False):
109
		"""Get badips.com categories.
110
 
111
		Returns
112
		-------
113
		set
114
			Set of categories.
115
 
116
		Raises
117
		------
118
		HTTPError
119
			Any issues with badips.com request.
120
		ValueError
121
			If badips.com response didn't contain necessary information
122
		"""
123
		try:
124
			response = urlopen(
87 - 125
				self._Request("/".join([self._badips, "get", "categories"])), timeout=self.timeout)
34 - 126
		except HTTPError as response:
127
			messages = json.loads(response.read().decode('utf-8'))
128
			self._logSys.error(
129
				"Failed to fetch categories. badips.com response: '%s'",
130
				messages['err'])
131
			raise
132
		else:
133
			response_json = json.loads(response.read().decode('utf-8'))
134
			if not 'categories' in response_json:
135
				err = "badips.com response lacked categories specification. Response was: %s" \
136
				  % (response_json,)
137
				self._logSys.error(err)
138
				raise ValueError(err)
139
			categories = response_json['categories']
140
			categories_names = set(
141
				value['Name'] for value in categories)
142
			if incParents:
143
				categories_names.update(set(
144
					value['Parent'] for value in categories
145
					if "Parent" in value))
146
			return categories_names
147
 
148
	def getList(self, category, score, age, key=None):
149
		"""Get badips.com list of bad IPs.
150
 
151
		Parameters
152
		----------
153
		category : str
154
			Valid badips.com category.
155
		score : int
156
			Minimum score for bad IPs.
157
		age : str
158
			Age of last report for bad IPs, per badips.com syntax.
159
		key : str, optional
160
			Key issued by badips.com to fetch IPs reported with the
161
			associated key.
162
 
163
		Returns
164
		-------
165
		set
166
			Set of bad IPs.
167
 
168
		Raises
169
		------
170
		HTTPError
171
			Any issues with badips.com request.
172
		"""
173
		try:
174
			url = "?".join([
175
				"/".join([self._badips, "get", "list", category, str(score)]),
176
				urlencode({'age': age})])
177
			if key:
178
				url = "&".join([url, urlencode({'key': key})])
87 - 179
			response = urlopen(self._Request(url), timeout=self.timeout)
34 - 180
		except HTTPError as response:
181
			messages = json.loads(response.read().decode('utf-8'))
182
			self._logSys.error(
183
				"Failed to fetch bad IP list. badips.com response: '%s'",
184
				messages['err'])
185
			raise
186
		else:
187
			return set(response.read().decode('utf-8').split())
188
 
189
	@property
190
	def category(self):
191
		"""badips.com category for reporting IPs.
192
		"""
193
		return self._category
194
 
195
	@category.setter
196
	def category(self, category):
197
		if category not in self.getCategories():
198
			self._logSys.error("Category name '%s' not valid. "
199
				"see badips.com for list of valid categories",
200
				category)
201
			raise ValueError("Invalid category: %s" % category)
202
		self._category = category
203
 
204
	@property
205
	def bancategory(self):
206
		"""badips.com bancategory for fetching IPs.
207
		"""
208
		return self._bancategory
209
 
210
	@bancategory.setter
211
	def bancategory(self, bancategory):
212
		if bancategory not in self.getCategories(incParents=True):
213
			self._logSys.error("Category name '%s' not valid. "
214
				"see badips.com for list of valid categories",
215
				bancategory)
216
			raise ValueError("Invalid bancategory: %s" % bancategory)
217
		self._bancategory = bancategory
218
 
219
	@property
220
	def score(self):
221
		"""badips.com minimum score for fetching IPs.
222
		"""
223
		return self._score
224
 
225
	@score.setter
226
	def score(self, score):
227
		score = int(score)
228
		if 0 <= score <= 5:
229
			self._score = score
230
		else:
231
			raise ValueError("Score must be 0-5")
232
 
233
	@property
234
	def banaction(self):
235
		"""Jail action to use for banning/unbanning.
236
		"""
237
		return self._banaction
238
 
239
	@banaction.setter
240
	def banaction(self, banaction):
241
		if banaction is not None and banaction not in self._jail.actions:
242
			self._logSys.error("Action name '%s' not in jail '%s'",
243
				banaction, self._jail.name)
244
			raise ValueError("Invalid banaction")
245
		self._banaction = banaction
246
 
247
	@property
248
	def updateperiod(self):
249
		"""Period in seconds between banned bad IPs will be updated.
250
		"""
251
		return self._updateperiod
252
 
253
	@updateperiod.setter
254
	def updateperiod(self, updateperiod):
255
		updateperiod = int(updateperiod)
256
		if updateperiod > 0:
257
			self._updateperiod = updateperiod
258
		else:
259
			raise ValueError("Update period must be integer greater than 0")
260
 
261
	def _banIPs(self, ips):
262
		for ip in ips:
263
			try:
264
				self._jail.actions[self.banaction].ban({
265
					'ip': ip,
266
					'failures': 0,
267
					'matches': "",
268
					'ipmatches': "",
269
					'ipjailmatches': "",
270
				})
271
			except Exception as e:
272
				self._logSys.error(
273
					"Error banning IP %s for jail '%s' with action '%s': %s",
274
					ip, self._jail.name, self.banaction, e,
275
					exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
276
			else:
277
				self._bannedips.add(ip)
278
				self._logSys.info(
279
					"Banned IP %s for jail '%s' with action '%s'",
280
					ip, self._jail.name, self.banaction)
281
 
282
	def _unbanIPs(self, ips):
283
		for ip in ips:
284
			try:
285
				self._jail.actions[self.banaction].unban({
286
					'ip': ip,
287
					'failures': 0,
288
					'matches': "",
289
					'ipmatches': "",
290
					'ipjailmatches': "",
291
				})
292
			except Exception as e:
293
				self._logSys.info(
294
					"Error unbanning IP %s for jail '%s' with action '%s': %s",
295
					ip, self._jail.name, self.banaction, e,
296
					exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
297
			else:
298
				self._logSys.info(
299
					"Unbanned IP %s for jail '%s' with action '%s'",
300
					ip, self._jail.name, self.banaction)
301
			finally:
302
				self._bannedips.remove(ip)
303
 
304
	def start(self):
305
		"""If `banaction` set, blacklists bad IPs.
306
		"""
307
		if self.banaction is not None:
308
			self.update()
309
 
310
	def update(self):
311
		"""If `banaction` set, updates blacklisted IPs.
312
 
313
		Queries badips.com for list of bad IPs, removing IPs from the
314
		blacklist if no longer present, and adds new bad IPs to the
315
		blacklist.
316
		"""
317
		if self.banaction is not None:
318
			if self._timer:
319
				self._timer.cancel()
320
				self._timer = None
321
 
322
			try:
323
				ips = self.getList(
324
					self.bancategory, self.score, self.age, self.bankey)
325
				# Remove old IPs no longer listed
326
				self._unbanIPs(self._bannedips - ips)
327
				# Add new IPs which are now listed
328
				self._banIPs(ips - self._bannedips)
329
 
330
				self._logSys.info(
331
					"Updated IPs for jail '%s'. Update again in %i seconds",
332
					self._jail.name, self.updateperiod)
333
			finally:
334
				self._timer = threading.Timer(self.updateperiod, self.update)
335
				self._timer.start()
336
 
337
	def stop(self):
338
		"""If `banaction` set, clears blacklisted IPs.
339
		"""
340
		if self.banaction is not None:
341
			if self._timer:
342
				self._timer.cancel()
343
				self._timer = None
344
			self._unbanIPs(self._bannedips.copy())
345
 
346
	def ban(self, aInfo):
347
		"""Reports banned IP to badips.com.
348
 
349
		Parameters
350
		----------
351
		aInfo : dict
352
			Dictionary which includes information in relation to
353
			the ban.
354
 
355
		Raises
356
		------
357
		HTTPError
358
			Any issues with badips.com request.
359
		"""
360
		try:
361
			url = "/".join([self._badips, "add", self.category, aInfo['ip']])
362
			if self.key:
363
				url = "?".join([url, urlencode({'key': self.key})])
87 - 364
			response = urlopen(self._Request(url), timeout=self.timeout)
34 - 365
		except HTTPError as response:
366
			messages = json.loads(response.read().decode('utf-8'))
367
			self._logSys.error(
368
				"Response from badips.com report: '%s'",
369
				messages['err'])
370
			raise
371
		else:
372
			messages = json.loads(response.read().decode('utf-8'))
373
			self._logSys.info(
374
				"Response from badips.com report: '%s'",
375
				messages['suc'])
376
 
377
Action = BadIPsAction