Subversion Repositories configs

Rev

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