mirror of
https://github.com/9001/copyparty.git
synced 2025-12-27 10:15:16 -05:00
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:
@@ -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)
|
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
|
> **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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ def rep_server():
|
|||||||
print("copyparty says %r" % (sck.recv_string(),))
|
print("copyparty says %r" % (sck.recv_string(),))
|
||||||
reply = b"thx"
|
reply = b"thx"
|
||||||
# reply = b"return 1" # non-zero to block an upload
|
# 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)
|
sck.send(reply)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -809,7 +809,8 @@ def get_sects():
|
|||||||
\033[0m
|
\033[0m
|
||||||
hooks specified as commandline --args are appended to volflags;
|
hooks specified as commandline --args are appended to volflags;
|
||||||
each commandline --arg and volflag can be specified multiple times,
|
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:
|
optionally prefix the command with comma-sep. flags similar to -mtp:
|
||||||
|
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ class FtpHandler(FTPHandler):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "Upload blocked by xbu server config: %r" % (vp,)
|
t = "Upload blocked by xbu server config: %r" % (vp,)
|
||||||
self.respond("550 %s" % (t,), logging.info)
|
self.respond("550 %s" % (t,), logging.info)
|
||||||
|
|||||||
@@ -913,29 +913,31 @@ class HttpCli(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
xban = self.vn.flags.get("xban")
|
xban = self.vn.flags.get("xban")
|
||||||
if not xban or not runhook(
|
if xban:
|
||||||
self.log,
|
hr = runhook(
|
||||||
self.conn.hsrv.broker,
|
self.log,
|
||||||
None,
|
self.conn.hsrv.broker,
|
||||||
"xban",
|
None,
|
||||||
xban,
|
"xban",
|
||||||
self.vn.canonical(self.rem),
|
xban,
|
||||||
self.vpath,
|
self.vn.canonical(self.rem),
|
||||||
self.host,
|
self.vpath,
|
||||||
self.uname,
|
self.host,
|
||||||
"",
|
self.uname,
|
||||||
time.time(),
|
"",
|
||||||
0,
|
time.time(),
|
||||||
self.ip,
|
0,
|
||||||
time.time(),
|
self.ip,
|
||||||
[reason, reason],
|
time.time(),
|
||||||
):
|
[reason, reason],
|
||||||
self.log("client banned: %s" % (descr,), 1)
|
)
|
||||||
self.conn.hsrv.bans[ip] = bonk
|
if hr.get("rv") == 0:
|
||||||
self.conn.hsrv.nban += 1
|
return False
|
||||||
return True
|
|
||||||
|
|
||||||
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:
|
def is_banned(self) -> bool:
|
||||||
if not self.conn.bans:
|
if not self.conn.bans:
|
||||||
@@ -2386,7 +2388,7 @@ class HttpCli(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "upload blocked by xbu server config: %r" % (vp,)
|
t = "upload blocked by xbu server config: %r" % (vp,)
|
||||||
self.log(t, 1)
|
self.log(t, 1)
|
||||||
@@ -2521,7 +2523,7 @@ class HttpCli(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "upload blocked by xau server config: %r" % (vp,)
|
t = "upload blocked by xau server config: %r" % (vp,)
|
||||||
self.log(t, 1)
|
self.log(t, 1)
|
||||||
@@ -3359,7 +3361,7 @@ class HttpCli(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "new-md blocked by " + hn + " server config: %r"
|
t = "new-md blocked by " + hn + " server config: %r"
|
||||||
t = t % (vjoin(vfs.vpath, rem),)
|
t = t % (vjoin(vfs.vpath, rem),)
|
||||||
@@ -3530,7 +3532,7 @@ class HttpCli(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "upload blocked by xbu server config: %r"
|
t = "upload blocked by xbu server config: %r"
|
||||||
t = t % (vjoin(upload_vpath, fname),)
|
t = t % (vjoin(upload_vpath, fname),)
|
||||||
@@ -3637,7 +3639,7 @@ class HttpCli(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "upload blocked by xau server config: %r"
|
t = "upload blocked by xau server config: %r"
|
||||||
t = t % (vjoin(upload_vpath, fname),)
|
t = t % (vjoin(upload_vpath, fname),)
|
||||||
@@ -3950,7 +3952,7 @@ class HttpCli(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "save blocked by xbu server config"
|
t = "save blocked by xbu server config"
|
||||||
self.log(t, 1)
|
self.log(t, 1)
|
||||||
@@ -3998,7 +4000,7 @@ class HttpCli(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "save blocked by xau server config"
|
t = "save blocked by xau server config"
|
||||||
self.log(t, 1)
|
self.log(t, 1)
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ class SMB(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "blocked by xbu server config: %r" % (vpath,)
|
t = "blocked by xbu server config: %r" % (vpath,)
|
||||||
yeet(t)
|
yeet(t)
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ class Tftpd(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "upload blocked by xbu server config: %r" % (vpath,)
|
t = "upload blocked by xbu server config: %r" % (vpath,)
|
||||||
yeet(t)
|
yeet(t)
|
||||||
|
|||||||
@@ -3307,7 +3307,7 @@ class Up2k(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "upload blocked by xbu server config: %r"
|
t = "upload blocked by xbu server config: %r"
|
||||||
t = t % (vp,)
|
t = t % (vp,)
|
||||||
@@ -4003,7 +4003,7 @@ class Up2k(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "upload blocked by xau server config: %r"
|
t = "upload blocked by xau server config: %r"
|
||||||
t = t % (djoin(vtop, rd, fn),)
|
t = t % (djoin(vtop, rd, fn),)
|
||||||
@@ -4221,7 +4221,7 @@ class Up2k(object):
|
|||||||
_ = dbv.get(volpath, uname, *permsets[0])
|
_ = dbv.get(volpath, uname, *permsets[0])
|
||||||
|
|
||||||
if xbd:
|
if xbd:
|
||||||
if not runhook(
|
hr = runhook(
|
||||||
self.log,
|
self.log,
|
||||||
None,
|
None,
|
||||||
self,
|
self,
|
||||||
@@ -4237,9 +4237,12 @@ class Up2k(object):
|
|||||||
ip,
|
ip,
|
||||||
time.time(),
|
time.time(),
|
||||||
None,
|
None,
|
||||||
):
|
)
|
||||||
t = "delete blocked by xbd server config: %r"
|
t = hr.get("rejectmsg") or ""
|
||||||
self.log(t % (abspath,), 1)
|
if t or hr.get("rc") != 0:
|
||||||
|
if not t:
|
||||||
|
t = "delete blocked by xbd server config: %r" % (abspath,)
|
||||||
|
self.log(t, 1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
n_files += 1
|
n_files += 1
|
||||||
@@ -4389,7 +4392,7 @@ class Up2k(object):
|
|||||||
xbc = svn.flags.get("xbc")
|
xbc = svn.flags.get("xbc")
|
||||||
xac = dvn.flags.get("xac")
|
xac = dvn.flags.get("xac")
|
||||||
if xbc:
|
if xbc:
|
||||||
if not runhook(
|
hr = runhook(
|
||||||
self.log,
|
self.log,
|
||||||
None,
|
None,
|
||||||
self,
|
self,
|
||||||
@@ -4405,8 +4408,11 @@ class Up2k(object):
|
|||||||
ip,
|
ip,
|
||||||
time.time(),
|
time.time(),
|
||||||
None,
|
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)
|
self.log(t, 1)
|
||||||
raise Pebkac(405, t)
|
raise Pebkac(405, t)
|
||||||
|
|
||||||
@@ -4641,7 +4647,7 @@ class Up2k(object):
|
|||||||
xbr = svn.flags.get("xbr")
|
xbr = svn.flags.get("xbr")
|
||||||
xar = dvn.flags.get("xar")
|
xar = dvn.flags.get("xar")
|
||||||
if xbr:
|
if xbr:
|
||||||
if not runhook(
|
hr = runhook(
|
||||||
self.log,
|
self.log,
|
||||||
None,
|
None,
|
||||||
self,
|
self,
|
||||||
@@ -4657,8 +4663,11 @@ class Up2k(object):
|
|||||||
ip,
|
ip,
|
||||||
time.time(),
|
time.time(),
|
||||||
None,
|
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)
|
self.log(t, 1)
|
||||||
raise Pebkac(405, t)
|
raise Pebkac(405, t)
|
||||||
|
|
||||||
@@ -5163,7 +5172,7 @@ class Up2k(object):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
t = hr.get("rejectmsg") or ""
|
t = hr.get("rejectmsg") or ""
|
||||||
if t or not hr:
|
if t or hr.get("rc") != 0:
|
||||||
if not t:
|
if not t:
|
||||||
t = "upload blocked by xbu server config: %r" % (vp_chk,)
|
t = "upload blocked by xbu server config: %r" % (vp_chk,)
|
||||||
self.log(t, 1)
|
self.log(t, 1)
|
||||||
|
|||||||
@@ -3930,7 +3930,13 @@ def _runhook(
|
|||||||
zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka)
|
zi, zs = _zmq_hook(log, verbose, src, acmd[0][4:].lower(), arg, wait, sp_ka)
|
||||||
if zi:
|
if zi:
|
||||||
raise Exception("zmq says %d" % (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:
|
if sin:
|
||||||
sp_ka["sin"] = (arg + "\n").encode("utf-8", "replace")
|
sp_ka["sin"] = (arg + "\n").encode("utf-8", "replace")
|
||||||
@@ -3949,20 +3955,23 @@ def _runhook(
|
|||||||
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
|
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
|
||||||
if chk and rc:
|
if chk and rc:
|
||||||
ret["rc"] = rc
|
ret["rc"] = rc
|
||||||
retchk(rc, bcmd, err, log, 5)
|
zi = 0 if rc == 100 else rc
|
||||||
|
retchk(zi, bcmd, err, log, 5)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
ret = json.loads(v)
|
ret = json.loads(v)
|
||||||
except:
|
except:
|
||||||
ret = {}
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if "stdout" not in ret:
|
if "stdout" not in ret:
|
||||||
ret["stdout"] = v
|
ret["stdout"] = v
|
||||||
|
if "stderr" not in ret:
|
||||||
|
ret["stderr"] = err
|
||||||
if "rc" not in ret:
|
if "rc" not in ret:
|
||||||
ret["rc"] = rc
|
ret["rc"] = rc
|
||||||
except:
|
except:
|
||||||
ret = {"rc": rc, "stdout": v}
|
ret = {"rc": rc, "stdout": v, "stderr": err}
|
||||||
|
|
||||||
if wait:
|
if wait:
|
||||||
wait -= time.time() - t0
|
wait -= time.time() - t0
|
||||||
@@ -3994,6 +4003,7 @@ def runhook(
|
|||||||
verbose = args.hook_v
|
verbose = args.hook_v
|
||||||
vp = vp.replace("\\", "/")
|
vp = vp.replace("\\", "/")
|
||||||
ret = {"rc": 0}
|
ret = {"rc": 0}
|
||||||
|
stop = False
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
try:
|
try:
|
||||||
hr = _runhook(
|
hr = _runhook(
|
||||||
@@ -4001,8 +4011,6 @@ def runhook(
|
|||||||
)
|
)
|
||||||
if verbose and log:
|
if verbose and log:
|
||||||
log("hook(%s) %r => \033[32m%s" % (src, cmd, hr), 6)
|
log("hook(%s) %r => \033[32m%s" % (src, cmd, hr), 6)
|
||||||
if not hr:
|
|
||||||
return {}
|
|
||||||
for k, v in hr.items():
|
for k, v in hr.items():
|
||||||
if k in ("idx", "del") and v:
|
if k in ("idx", "del") and v:
|
||||||
if broker:
|
if broker:
|
||||||
@@ -4013,17 +4021,20 @@ def runhook(
|
|||||||
elif k == "reloc" and v:
|
elif k == "reloc" and v:
|
||||||
# idk, just take the last one ig
|
# idk, just take the last one ig
|
||||||
ret["reloc"] = v
|
ret["reloc"] = v
|
||||||
|
elif k == "rc" and v:
|
||||||
|
stop = True
|
||||||
|
ret[k] = 0 if v == 100 else v
|
||||||
elif k in ret:
|
elif k in ret:
|
||||||
if k == "rc" and v:
|
if k == "stdout" and v and not ret[k]:
|
||||||
ret[k] = v
|
|
||||||
elif k == "stdout" and v and not ret[k]:
|
|
||||||
ret[k] = v
|
ret[k] = v
|
||||||
else:
|
else:
|
||||||
ret[k] = v
|
ret[k] = v
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
(log or print)("hook: %r, %s" % (ex, ex))
|
(log or print)("hook: %r, %s" % (ex, ex))
|
||||||
if ",c," in "," + cmd:
|
if ",c," in "," + cmd:
|
||||||
return {}
|
return {"rc": 1}
|
||||||
|
break
|
||||||
|
if stop:
|
||||||
break
|
break
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -78,6 +78,41 @@ class TestHooks(tu.TC):
|
|||||||
h, b = self.curl(url_dl)
|
h, b = self.curl(url_dl)
|
||||||
self.assertEqual(b, "ok %s\n" % (url_up))
|
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):
|
def makehook(self, hs):
|
||||||
with open("h.py", "wb") as f:
|
with open("h.py", "wb") as f:
|
||||||
f.write(hs.encode("utf-8"))
|
f.write(hs.encode("utf-8"))
|
||||||
|
|||||||
Reference in New Issue
Block a user