diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index a81c6657..9e5e4245 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -105,7 +105,7 @@ protocol = [ ["set usedns ", "sets the usedns mode for "], ["set attempt [ ... ]", "manually notify about failure"], ["set banip ... ", "manually Ban for "], -["set unbanip [--report-absent] ... ", "manually Unban in "], +["set unbanip [--report-absent] [--expr] ... ", "manually Unban in "], ["set maxretry ", "sets the number of failures before banning the host for "], ["set maxmatches ", "sets the max number of matches stored in memory per ticket in "], ["set maxlines ", "sets the number of to buffer for regex search for "], diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 74e2a615..3a0a8e3f 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -242,6 +242,33 @@ class Actions(JailThread, Mapping): return self.__checkBan(tickets) + def removeMultiBannedIP(self, ips=None, db=True, ifexists=False): + """Removes multiple banned IPs calling actions' unban method + + Parameters + ---------- + ip : list, tuple or None + The IPs as list to unban or all IPs if None + + Raises + ------ + ValueError + If at least one `ip` is not banned + """ + if ips is None: + return self.__flushBan(db) + missed = [] + cnt = 0 + for ip in ips: + try: + cnt += self.removeBannedIP(ip, db, ifexists) + except ValueError: + if not ifexists: + missed.append(ip) + if missed: + raise ValueError("not banned: %r" % missed) + return cnt + def removeBannedIP(self, ip=None, db=True, ifexists=False): """Removes banned IP calling actions' unban method @@ -250,8 +277,8 @@ class Actions(JailThread, Mapping): Parameters ---------- - ip : list, str, IPAddr or None - The IP address (or multiple IPs as list) to unban or all IPs if None + ip : str, IPAddr, TUPLE_ID representation (tuple/list) or None + The ID to unban or all IPs if None Raises ------ @@ -261,26 +288,6 @@ class Actions(JailThread, Mapping): # Unban all? if ip is None: return self.__flushBan(db) - # Multiple IPs: - if isinstance(ip, (list, tuple)): - missed = [] - cnt = 0 - for i in ip: - try: - cnt += self.removeBannedIP(i, db, ifexists) - except ValueError: - if not ifexists: - missed.append(i) - if missed: - raise ValueError("not banned: %r" % missed) - return cnt - # IPs can be represented in string format (e.g.: tuples) - is_ip_parsed = True - if isinstance(ip, str): - try: - ip = eval(ip) - except Exception: - is_ip_parsed = False # Single IP: # Always delete ip from database (also if currently not banned) if db and self._jail.database is not None: @@ -292,12 +299,12 @@ class Actions(JailThread, Mapping): self.__unBan(ticket) else: # Multiple IPs by subnet or dns: - if not is_ip_parsed and not isinstance(ip, IPAddr): + if not isinstance(ip, (IPAddr, tuple, list)): ipa = IPAddr(ip) if not ipa.isSingle: # subnet (mask/cidr) or raw (may be dns/hostname): ips = list(filter(ipa.contains, self.banManager.getBanList())) if ips: - return self.removeBannedIP(ips, db, ifexists) + return self.removeMultiBannedIP(ips, db, ifexists) # not found: msg = "%s is not banned" % str(ip) logSys.log(logging.MSG, msg) diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index bdbe88ca..beafe8d9 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -31,6 +31,7 @@ import os import signal import stat import sys +from ast import literal_eval from .observer import Observers, ObserverThread from .jails import Jails @@ -528,18 +529,26 @@ class Server: def setBanIP(self, name, value): return self.__jails[name].actions.addBannedIP(value) - def setUnbanIP(self, name=None, value=None, ifexists=True): + def setUnbanIP(self, name=None, values=None, ifexists=True, ifexpr=False): + def parseExpr(v): + try: + return literal_eval(v) + except SyntaxError: + return v if name is not None: # single jail: jails = [self.__jails[name]] else: # in all jails: jails = list(self.__jails.values()) - # unban given or all (if value is None): + # parse values if it contains an expression + if values and ifexpr: + values = map(parseExpr, values) + # unban given or all (if values is None): cnt = 0 ifexists |= (name is None) for jail in jails: - cnt += jail.actions.removeBannedIP(value, ifexists=ifexists) + cnt += jail.actions.removeMultiBannedIP(values, ifexists=ifexists) return cnt def banned(self, name=None, ids=None): diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 28931344..cee0b7a5 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -363,13 +363,17 @@ class Transmitter: value = command[2:] return self.__server.setBanIP(name,value) elif command[1] == "unbanip": + ifexpr = False ifexists = True - if command[2] != "--report-absent": - value = command[2:] - else: + offset = 2 + if "--report-absent" in command: ifexists = False - value = command[3:] - return self.__server.setUnbanIP(name, value, ifexists=ifexists) + offset += 1 + if "--expr" in command: + ifexpr = True + offset += 1 + value = command[offset:] + return self.__server.setUnbanIP(name, value, ifexists=ifexists, ifexpr=ifexpr) elif command[1] == "addaction": args = [command[2]] if len(command) > 3: