diff --git a/copyparty/__main__.py b/copyparty/__main__.py index fd397368..30dab530 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -959,7 +959,7 @@ def add_hooks(ap): ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute CMD before a file delete") ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute CMD after a file delete") ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message") - ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="execute CMD if someone gets banned (pw/404)") + ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="execute CMD if someone gets banned (pw/404/403/url)") def add_stats(ap): @@ -1010,6 +1010,11 @@ def add_safety(ap): ap2.add_argument("--logout", metavar="H", type=float, default="8086", help="logout clients after H hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)") ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]") ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="no", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (disabled by default since turbo-up2k counts as 404s)") + ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month") + ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (422 is server fuzzing, invalid POSTs and so)") + ap2.add_argument("--ban-url", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m sus URL's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (decent replacement for --ban-404 if that can't be used)") + ap2.add_argument("--sus-urls", metavar="REGEX", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]") + ap2.add_argument("--nonsus-urls", metavar="REGEX", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]") ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for MIN minutes (and also kill its active connections) -- disable with 0") ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for B minutes; disable with [\033[32m0\033[0m]") ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index beb5c3fc..8b570151 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -625,9 +625,27 @@ class HttpCli(object): headers: Optional[dict[str, str]] = None, volsan: bool = False, ) -> bytes: - if status == 404: - g = self.conn.hsrv.g404 - if g.lim: + if status > 400 and status in (403, 404, 422): + if status == 404: + g = self.conn.hsrv.g404 + elif status == 403: + g = self.conn.hsrv.g403 + else: + g = self.conn.hsrv.g422 + + gurl = self.conn.hsrv.gurl + if ( + gurl.lim + and (not g.lim or gurl.lim < g.lim) + and self.args.sus_urls.search(self.vpath) + ): + g = self.conn.hsrv.gurl + + if g.lim and ( + g == self.conn.hsrv.g422 + or not self.args.nonsus_urls + or not self.args.nonsus_urls.search(self.vpath) + ): bonk, ip = g.bonk(self.ip, self.vpath) if bonk: xban = self.vn.flags.get("xban") @@ -642,9 +660,9 @@ class HttpCli(object): 0, self.ip, time.time(), - "404", + str(status), ): - self.log("client banned: 404s", 1) + self.log("client banned: %ss" % (status,), 1) self.conn.hsrv.bans[ip] = bonk if volsan: @@ -3260,7 +3278,7 @@ class HttpCli(object): dst = "" elif top: if not dst.startswith(top + "/"): - raise Pebkac(400, "arg funk") + raise Pebkac(422, "arg funk") dst = dst[len(top) + 1 :] diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index ca9430d4..dbfa17a8 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -107,6 +107,9 @@ class HttpSrv(object): self.ssdp: Optional["SSDPr"] = None self.gpwd = Garda(self.args.ban_pw) self.g404 = Garda(self.args.ban_404) + self.g403 = Garda(self.args.ban_403) + self.g422 = Garda(self.args.ban_422, False) + self.gurl = Garda(self.args.ban_url) self.bans: dict[str, int] = {} self.aclose: dict[str, int] = {} diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 0c669ffd..1c41cb72 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -100,11 +100,6 @@ class SvcHub(object): self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8) - # for non-http clients (ftp) - self.bans: dict[str, int] = {} - self.gpwd = Garda(self.args.ban_pw) - self.g404 = Garda(self.args.ban_404) - if args.sss or args.s >= 3: args.ss = True args.no_dav = True @@ -134,6 +129,14 @@ class SvcHub(object): if not self._process_config(): raise Exception(BAD_CFG) + # for non-http clients (ftp) + self.bans: dict[str, int] = {} + self.gpwd = Garda(self.args.ban_pw) + self.g404 = Garda(self.args.ban_404) + self.g403 = Garda(self.args.ban_403) + self.g422 = Garda(self.args.ban_422) + self.gurl = Garda(self.args.ban_url) + self.log_div = 10 ** (6 - args.log_tdec) self.log_efmt = "%02d:%02d:%02d.%0{}d".format(args.log_tdec) self.log_dfmt = "%04d-%04d-%06d.%0{}d".format(args.log_tdec) @@ -400,6 +403,18 @@ class SvcHub(object): if vs and vs.startswith("~"): setattr(al, k, os.path.expanduser(vs)) + for k in "sus_urls nonsus_urls".split(" "): + vs = getattr(al, k) + if not vs or vs == "no": + setattr(al, k, None) + else: + setattr(al, k, re.compile(vs)) + + if not al.sus_urls: + al.ban_url = "no" + elif al.ban_url == "no": + al.sus_urls = None + return True def _setlimits(self) -> None: diff --git a/copyparty/util.py b/copyparty/util.py index 1221675a..2c2666bb 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -926,7 +926,8 @@ class Magician(object): class Garda(object): """ban clients for repeated offenses""" - def __init__(self, cfg: str) -> None: + def __init__(self, cfg: str, uniq: bool = True) -> None: + self.uniq = uniq try: a, b, c = cfg.strip().split(",") self.lim = int(a) @@ -972,7 +973,7 @@ class Garda(object): # assume /64 clients; drop 4 groups ip = IPv6Address(ip).exploded[:-20] - if prev: + if prev and self.uniq: if self.prev.get(ip) == prev: return 0, ip @@ -1447,7 +1448,7 @@ class MultipartParser(object): for buf in iterable: ret += buf if len(ret) > max_len: - raise Pebkac(400, "field length is too long") + raise Pebkac(422, "field length is too long") return ret diff --git a/tests/util.py b/tests/util.py index 5cbb35d7..c51406c8 100644 --- a/tests/util.py +++ b/tests/util.py @@ -189,6 +189,8 @@ class VHttpSrv(object): self.gpwd = Garda("") self.g404 = Garda("") + self.g403 = Garda("") + self.gurl = Garda("") self.ptn_cc = re.compile(r"[\x00-\x1f]")