Rev 71 | Blame | Compare with Previous | Last modification | View Log | RSS feed
# 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.import sysif sys.version_info < (2, 7):raise ImportError("badips.py action requires Python >= 2.7")import jsonimport threadingimport loggingif sys.version_info >= (3, ):from urllib.request import Request, urlopenfrom urllib.parse import urlencodefrom urllib.error import HTTPErrorelse:from urllib2 import Request, urlopen, HTTPErrorfrom urllib import urlencodefrom fail2ban.server.actions import ActionBaseclass BadIPsAction(ActionBase):"""Fail2Ban action which reports bans to badips.com, and alsoblacklist bad IPs listed on badips.com by using another action'sban method.Parameters----------jail : JailThe jail which the action belongs to.name : strName assigned to the action.category : strValid badips.com category for reporting failures.score : int, optionalMinimum score for bad IPs. Default 3.age : str, optionalAge of last report for bad IPs, per badips.com syntax.Default "24h" (24 hours)key : str, optionalKey issued by badips.com to report bans, for later retrievalof personalised content.banaction : str, optionalName of banaction to use for blacklisting bad IPs. If `None`,no blacklist of IPs will take place.Default `None`.bancategory : str, optionalName of category to use for blacklisting, which can differfrom category used for reporting. e.g. may want to report"postfix", but want to use whole "mail" category for blacklist.Default `category`.bankey : str, optionalKey issued by badips.com to blacklist IPs reported with theassociated key.updateperiod : int, optionalTime in seconds between updating bad IPs blacklist.Default 900 (15 minutes)agent : str, optionalUser agent transmitted to server.Default `Fail2Ban/ver.`Raises------ValueErrorIf invalid `category`, `score`, `banaction` or `updateperiod`."""TIMEOUT = 10_badips = "http://www.badips.com"def _Request(self, url, **argv):return Request(url, headers={'User-Agent': self.agent}, **argv)def __init__(self, jail, name, category, score=3, age="24h", key=None,banaction=None, bancategory=None, bankey=None, updateperiod=900, agent="Fail2Ban",timeout=TIMEOUT):super(BadIPsAction, self).__init__(jail, name)self.timeout = timeoutself.agent = agentself.category = categoryself.score = scoreself.age = ageself.key = keyself.banaction = banactionself.bancategory = bancategory or categoryself.bankey = bankeyself.updateperiod = updateperiodself._bannedips = set()# Used later for threading.Timer for updating badipsself._timer = Nonedef getCategories(self, incParents=False):"""Get badips.com categories.Returns-------setSet of categories.Raises------HTTPErrorAny issues with badips.com request.ValueErrorIf badips.com response didn't contain necessary information"""try:response = urlopen(self._Request("/".join([self._badips, "get", "categories"])), timeout=self.timeout)except HTTPError as response:messages = json.loads(response.read().decode('utf-8'))self._logSys.error("Failed to fetch categories. badips.com response: '%s'",messages['err'])raiseelse:response_json = json.loads(response.read().decode('utf-8'))if not 'categories' in response_json:err = "badips.com response lacked categories specification. Response was: %s" \% (response_json,)self._logSys.error(err)raise ValueError(err)categories = response_json['categories']categories_names = set(value['Name'] for value in categories)if incParents:categories_names.update(set(value['Parent'] for value in categoriesif "Parent" in value))return categories_namesdef getList(self, category, score, age, key=None):"""Get badips.com list of bad IPs.Parameters----------category : strValid badips.com category.score : intMinimum score for bad IPs.age : strAge of last report for bad IPs, per badips.com syntax.key : str, optionalKey issued by badips.com to fetch IPs reported with theassociated key.Returns-------setSet of bad IPs.Raises------HTTPErrorAny issues with badips.com request."""try:url = "?".join(["/".join([self._badips, "get", "list", category, str(score)]),urlencode({'age': age})])if key:url = "&".join([url, urlencode({'key': key})])response = urlopen(self._Request(url), timeout=self.timeout)except HTTPError as response:messages = json.loads(response.read().decode('utf-8'))self._logSys.error("Failed to fetch bad IP list. badips.com response: '%s'",messages['err'])raiseelse:return set(response.read().decode('utf-8').split())@propertydef category(self):"""badips.com category for reporting IPs."""return self._category@category.setterdef category(self, category):if category not in self.getCategories():self._logSys.error("Category name '%s' not valid. ""see badips.com for list of valid categories",category)raise ValueError("Invalid category: %s" % category)self._category = category@propertydef bancategory(self):"""badips.com bancategory for fetching IPs."""return self._bancategory@bancategory.setterdef bancategory(self, bancategory):if bancategory not in self.getCategories(incParents=True):self._logSys.error("Category name '%s' not valid. ""see badips.com for list of valid categories",bancategory)raise ValueError("Invalid bancategory: %s" % bancategory)self._bancategory = bancategory@propertydef score(self):"""badips.com minimum score for fetching IPs."""return self._score@score.setterdef score(self, score):score = int(score)if 0 <= score <= 5:self._score = scoreelse:raise ValueError("Score must be 0-5")@propertydef banaction(self):"""Jail action to use for banning/unbanning."""return self._banaction@banaction.setterdef banaction(self, banaction):if banaction is not None and banaction not in self._jail.actions:self._logSys.error("Action name '%s' not in jail '%s'",banaction, self._jail.name)raise ValueError("Invalid banaction")self._banaction = banaction@propertydef updateperiod(self):"""Period in seconds between banned bad IPs will be updated."""return self._updateperiod@updateperiod.setterdef updateperiod(self, updateperiod):updateperiod = int(updateperiod)if updateperiod > 0:self._updateperiod = updateperiodelse:raise ValueError("Update period must be integer greater than 0")def _banIPs(self, ips):for ip in ips:try:self._jail.actions[self.banaction].ban({'ip': ip,'failures': 0,'matches': "",'ipmatches': "",'ipjailmatches': "",})except Exception as e:self._logSys.error("Error banning IP %s for jail '%s' with action '%s': %s",ip, self._jail.name, self.banaction, e,exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)else:self._bannedips.add(ip)self._logSys.info("Banned IP %s for jail '%s' with action '%s'",ip, self._jail.name, self.banaction)def _unbanIPs(self, ips):for ip in ips:try:self._jail.actions[self.banaction].unban({'ip': ip,'failures': 0,'matches': "",'ipmatches': "",'ipjailmatches': "",})except Exception as e:self._logSys.info("Error unbanning IP %s for jail '%s' with action '%s': %s",ip, self._jail.name, self.banaction, e,exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)else:self._logSys.info("Unbanned IP %s for jail '%s' with action '%s'",ip, self._jail.name, self.banaction)finally:self._bannedips.remove(ip)def start(self):"""If `banaction` set, blacklists bad IPs."""if self.banaction is not None:self.update()def update(self):"""If `banaction` set, updates blacklisted IPs.Queries badips.com for list of bad IPs, removing IPs from theblacklist if no longer present, and adds new bad IPs to theblacklist."""if self.banaction is not None:if self._timer:self._timer.cancel()self._timer = Nonetry:ips = self.getList(self.bancategory, self.score, self.age, self.bankey)# Remove old IPs no longer listedself._unbanIPs(self._bannedips - ips)# Add new IPs which are now listedself._banIPs(ips - self._bannedips)self._logSys.info("Updated IPs for jail '%s'. Update again in %i seconds",self._jail.name, self.updateperiod)finally:self._timer = threading.Timer(self.updateperiod, self.update)self._timer.start()def stop(self):"""If `banaction` set, clears blacklisted IPs."""if self.banaction is not None:if self._timer:self._timer.cancel()self._timer = Noneself._unbanIPs(self._bannedips.copy())def ban(self, aInfo):"""Reports banned IP to badips.com.Parameters----------aInfo : dictDictionary which includes information in relation tothe ban.Raises------HTTPErrorAny issues with badips.com request."""try:url = "/".join([self._badips, "add", self.category, aInfo['ip']])if self.key:url = "?".join([url, urlencode({'key': self.key})])response = urlopen(self._Request(url), timeout=self.timeout)except HTTPError as response:messages = json.loads(response.read().decode('utf-8'))self._logSys.error("Response from badips.com report: '%s'",messages['err'])raiseelse:messages = json.loads(response.read().decode('utf-8'))self._logSys.info("Response from badips.com report: '%s'",messages['suc'])Action = BadIPsAction