diff --git a/bin/hooks/README.md b/bin/hooks/README.md index 9c18c55b..c0ce7bf1 100644 --- a/bin/hooks/README.md +++ b/bin/hooks/README.md @@ -4,6 +4,11 @@ these programs either take zero arguments, or a filepath (the affected file), or run copyparty with `--help-hooks` for usage details / hook type explanations (xm/xbu/xau/xiu/xbc/xac/xbr/xar/xbd/xad/xban) +in particular, if a hook is loaded into copyparty with the hook-flag `c` ("check") then its exit-code controls the action that launched the hook: +* exit-code `0` = allow the action, and/or continue running the next hook +* exit-code `100` = allow the action, and stop running any remaining consecutive hooks +* anything else = reject/prevent the original action, and don't run the remaining hooks + > **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead diff --git a/bin/zmq-recv.py b/bin/zmq-recv.py index 72fa24f7..93e0c168 100755 --- a/bin/zmq-recv.py +++ b/bin/zmq-recv.py @@ -61,6 +61,8 @@ def rep_server(): print("copyparty says %r" % (sck.recv_string(),)) reply = b"thx" # reply = b"return 1" # non-zero to block an upload + # reply = b'{"rc":1}' # or as json, that's fine too + # reply = b'{"rejectmsg":"naw dude"}' # or custom message sck.send(reply) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f8dcffb1..d6d30f1a 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -809,7 +809,8 @@ def get_sects(): \033[0m hooks specified as commandline --args are appended to volflags; each commandline --arg and volflag can be specified multiple times, - each hook will execute in order unless one returns non-zero + each hook will execute in order unless one returns non-zero, or + "100" which means "stop daisychaining and return 0 (success/OK)" optionally prefix the command with comma-sep. flags similar to -mtp: diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 12285d64..fd5d0349 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -515,7 +515,7 @@ class FtpHandler(FTPHandler): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "Upload blocked by xbu server config: %r" % (vp,) self.respond("550 %s" % (t,), logging.info) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 6e547863..ee84d45e 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -913,29 +913,31 @@ class HttpCli(object): return False xban = self.vn.flags.get("xban") - if not xban or not runhook( - self.log, - self.conn.hsrv.broker, - None, - "xban", - xban, - self.vn.canonical(self.rem), - self.vpath, - self.host, - self.uname, - "", - time.time(), - 0, - self.ip, - time.time(), - [reason, reason], - ): - self.log("client banned: %s" % (descr,), 1) - self.conn.hsrv.bans[ip] = bonk - self.conn.hsrv.nban += 1 - return True + if xban: + hr = runhook( + self.log, + self.conn.hsrv.broker, + None, + "xban", + xban, + self.vn.canonical(self.rem), + self.vpath, + self.host, + self.uname, + "", + time.time(), + 0, + self.ip, + time.time(), + [reason, reason], + ) + if hr.get("rv") == 0: + return False - return False + self.log("client banned: %s" % (descr,), 1) + self.conn.hsrv.bans[ip] = bonk + self.conn.hsrv.nban += 1 + return True def is_banned(self) -> bool: if not self.conn.bans: @@ -2386,7 +2388,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (vp,) self.log(t, 1) @@ -2521,7 +2523,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xau server config: %r" % (vp,) self.log(t, 1) @@ -3359,7 +3361,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "new-md blocked by " + hn + " server config: %r" t = t % (vjoin(vfs.vpath, rem),) @@ -3530,7 +3532,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" t = t % (vjoin(upload_vpath, fname),) @@ -3637,7 +3639,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xau server config: %r" t = t % (vjoin(upload_vpath, fname),) @@ -3950,7 +3952,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "save blocked by xbu server config" self.log(t, 1) @@ -3998,7 +4000,7 @@ class HttpCli(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "save blocked by xau server config" self.log(t, 1) diff --git a/copyparty/smbd.py b/copyparty/smbd.py index 40e4f088..0d48a733 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -265,7 +265,7 @@ class SMB(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "blocked by xbu server config: %r" % (vpath,) yeet(t) diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py index 88fd7782..e1f75c8b 100644 --- a/copyparty/tftpd.py +++ b/copyparty/tftpd.py @@ -382,7 +382,7 @@ class Tftpd(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (vpath,) yeet(t) diff --git a/copyparty/up2k.py b/copyparty/up2k.py index 4f33bb0c..a2c3b233 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -3307,7 +3307,7 @@ class Up2k(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" t = t % (vp,) @@ -4003,7 +4003,7 @@ class Up2k(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xau server config: %r" t = t % (djoin(vtop, rd, fn),) @@ -4221,7 +4221,7 @@ class Up2k(object): _ = dbv.get(volpath, uname, *permsets[0]) if xbd: - if not runhook( + hr = runhook( self.log, None, self, @@ -4237,9 +4237,12 @@ class Up2k(object): ip, time.time(), None, - ): - t = "delete blocked by xbd server config: %r" - self.log(t % (abspath,), 1) + ) + t = hr.get("rejectmsg") or "" + if t or hr.get("rc") != 0: + if not t: + t = "delete blocked by xbd server config: %r" % (abspath,) + self.log(t, 1) continue n_files += 1 @@ -4389,7 +4392,7 @@ class Up2k(object): xbc = svn.flags.get("xbc") xac = dvn.flags.get("xac") if xbc: - if not runhook( + hr = runhook( self.log, None, self, @@ -4405,8 +4408,11 @@ class Up2k(object): ip, time.time(), None, - ): - t = "copy blocked by xbr server config: %r" % (svp,) + ) + t = hr.get("rejectmsg") or "" + if t or hr.get("rc") != 0: + if not t: + t = "copy blocked by xbr server config: %r" % (svp,) self.log(t, 1) raise Pebkac(405, t) @@ -4641,7 +4647,7 @@ class Up2k(object): xbr = svn.flags.get("xbr") xar = dvn.flags.get("xar") if xbr: - if not runhook( + hr = runhook( self.log, None, self, @@ -4657,8 +4663,11 @@ class Up2k(object): ip, time.time(), None, - ): - t = "move blocked by xbr server config: %r" % (svp,) + ) + t = hr.get("rejectmsg") or "" + if t or hr.get("rc") != 0: + if not t: + t = "move blocked by xbr server config: %r" % (svp,) self.log(t, 1) raise Pebkac(405, t) @@ -5163,7 +5172,7 @@ class Up2k(object): None, ) t = hr.get("rejectmsg") or "" - if t or not hr: + if t or hr.get("rc") != 0: if not t: t = "upload blocked by xbu server config: %r" % (vp_chk,) self.log(t, 1) diff --git a/copyparty/util.py b/copyparty/util.py index 3c9ed2ab..7290dd9e 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -3930,7 +3930,13 @@ def _runhook( zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka) if zi: raise Exception("zmq says %d" % (zi,)) - return {"rc": 0, "stdout": zs} + try: + ret = json.loads(zs) + if "rc" not in ret: + ret["rc"] = 0 + return ret + except: + return {"rc": 0, "stdout": zs} if sin: sp_ka["sin"] = (arg + "\n").encode("utf-8", "replace") @@ -3949,20 +3955,23 @@ def _runhook( rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore if chk and rc: ret["rc"] = rc - retchk(rc, bcmd, err, log, 5) + zi = 0 if rc == 100 else rc + retchk(zi, bcmd, err, log, 5) else: try: ret = json.loads(v) except: - ret = {} + pass try: if "stdout" not in ret: ret["stdout"] = v + if "stderr" not in ret: + ret["stderr"] = err if "rc" not in ret: ret["rc"] = rc except: - ret = {"rc": rc, "stdout": v} + ret = {"rc": rc, "stdout": v, "stderr": err} if wait: wait -= time.time() - t0 @@ -3994,6 +4003,7 @@ def runhook( verbose = args.hook_v vp = vp.replace("\\", "/") ret = {"rc": 0} + stop = False for cmd in cmds: try: hr = _runhook( @@ -4001,8 +4011,6 @@ def runhook( ) if verbose and log: log("hook(%s) %r => \033[32m%s" % (src, cmd, hr), 6) - if not hr: - return {} for k, v in hr.items(): if k in ("idx", "del") and v: if broker: @@ -4013,17 +4021,20 @@ def runhook( elif k == "reloc" and v: # idk, just take the last one ig ret["reloc"] = v + elif k == "rc" and v: + stop = True + ret[k] = 0 if v == 100 else v elif k in ret: - if k == "rc" and v: - ret[k] = v - elif k == "stdout" and v and not ret[k]: + if k == "stdout" and v and not ret[k]: ret[k] = v else: ret[k] = v except Exception as ex: (log or print)("hook: %r, %s" % (ex, ex)) if ",c," in "," + cmd: - return {} + return {"rc": 1} + break + if stop: break return ret diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 63a525df..6b366023 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -78,6 +78,41 @@ class TestHooks(tu.TC): h, b = self.curl(url_dl) self.assertEqual(b, "ok %s\n" % (url_up)) + def test2(self): + hooktxt = "import sys\nopen('h%d','wb').close()\nsys.exit(%d)\n" + for hooktype in ("xbu", "xau"): + for upfun in (self.put, self.bup): + self.reset() + for n in [0, 1, 100]: + with open("h%d.py" % (n,), "wb") as f: + f.write((hooktxt % (n, n)).encode("utf-8")) + vcfg = [ + "012:012:A:c,H=c,h0.py:c,H=c,h1.py:c,H=c,h100.py", + "021:021:A:c,H=c,h0.py:c,H=c,h100.py:c,H=c,h1.py", + "120:120:A:c,H=c,h1.py:c,H=c,h100.py:c,H=c,h0.py", + "30:30:A:c,H=c,enoent.py:c,H=c,h100.py", # not-exist + ] + vcfg = [x.replace("H", hooktype) for x in vcfg] + self.args = Cfg(v=vcfg, a=["o:o"], e2d=True) + self.asrv = AuthSrv(self.args, self.log) + self.cinit() + scenarios = ( + ("012", False, True, True, False), + ("021", True, True, False, True), + ("120", False, False, True, False), + ("30", False, False, False, False), + ) + for (vp, ok, h0, h1, h2) in scenarios: + for zs in ("h0", "h1", "h100"): + if os.path.exists(zs): + os.unlink(zs) + vp = "%s/f" % (vp,) + h, b = upfun(vp) + self.assertEqual(ok, os.path.exists(vp)) + self.assertEqual(h0, os.path.exists("h0")) + self.assertEqual(h1, os.path.exists("h1")) + self.assertEqual(h2, os.path.exists("h100")) + def makehook(self, hs): with open("h.py", "wb") as f: f.write(hs.encode("utf-8"))