hooks: retcode 100, zmq json;

hooks returning exitcode 0 will:
* run the next hook, if any
* allow the original action, unless successive hook opposes

hooks returning exitcode 100 will:
* abort running successive hooks
* allow the original action

hooks returning anything other than 0 or 100 will:
* abort running successive hooks
* REJECT the original action

zmq can now respond with json; a dict with "rc", "rejectmsg",
"reloc" and so on, just like other hooks replying with json
This commit is contained in:
ed 2025-11-30 19:29:09 +00:00
parent ca6d3a5c16
commit 889bd3242a
10 changed files with 121 additions and 56 deletions

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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"))