"Fossies" - the Fresh Open Source Software Archive

Member "fail2ban-0.10.4/config/action.d/badips.py" (4 Oct 2018, 11476 Bytes) of package /linux/misc/fail2ban-0.10.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "badips.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 0.10.3.1_vs_0.10.4.

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