diff --git a/copyparty/__main__.py b/copyparty/__main__.py
index 29259b17..8846d5e1 100644
--- a/copyparty/__main__.py
+++ b/copyparty/__main__.py
@@ -634,12 +634,12 @@ def get_sects():
\033[36mxban\033[35m executes CMD if someone gets banned
\033[0m
can be defined as --args or volflags; for example \033[36m
- --xau notify-send
- -v .::r:c,xau=notify-send
+ --xau foo.py
+ -v .::r:c,xau=bar.py
\033[0m
- commands specified as --args are appended to volflags;
- each --arg and volflag can be specified multiple times,
- each command will execute in order unless one returns non-zero
+ 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
optionally prefix the command with comma-sep. flags similar to -mtp:
@@ -650,6 +650,10 @@ def get_sects():
\033[36mtN\033[35m sets an N sec timeout before the command is abandoned
\033[36miN\033[35m xiu only: volume must be idle for N sec (default = 5)
+ \033[36mar\033[35m only run hook if user has read-access
+ \033[36marw\033[35m only run hook if user has read-write-access
+ \033[36marwmd\033[35m ...and so on... (doesn't work for xiu or xban)
+
\033[36mkt\033[35m kills the entire process tree on timeout (default),
\033[36mkm\033[35m kills just the main process
\033[36mkn\033[35m lets it continue running until copyparty is terminated
@@ -659,6 +663,21 @@ def get_sects():
\033[36mc2\033[35m show only stdout
\033[36mc3\033[35m mute all process otput
\033[0m
+ examples:
+
+ \033[36m--xm some.py\033[35m runs \033[33msome.py msgtxt\033[35m on each 📟 message;
+ \033[33mmsgtxt\033[35m is the message that was written into the web-ui
+
+ \033[36m--xm j,some.py\033[35m runs \033[33msome.py jsontext\033[35m on each 📟 message;
+ \033[33mjsontext\033[35m is the message info (ip, user, ..., msg-text)
+
+ \033[36m--xm aw,j,some.py\033[35m requires user to have write-access
+
+ \033[36m--xm aw,,notify-send,hey,--\033[35m shows an OS alert on linux;
+ the \033[33m,,\033[35m stops copyparty from reading the rest as flags and
+ the \033[33m--\033[35m stops notify-send from reading the message as args
+ and the alert will be "hey" followed by the messagetext
+ \033[0m
each hook is executed once for each event, except for \033[36mxiu\033[0m
which builds up a backlog of uploads, running the hook just once
as soon as the volume has been idle for iN seconds (5 by default)
@@ -685,7 +704,10 @@ def get_sects():
\033[36mstash\033[35m dumps the data to file and returns length + checksum
\033[36msave,get\033[35m dumps to file and returns the page like a GET
\033[36mprint,get\033[35m prints the data in the log and returns GET
- (leave out the ",get" to return an error instead)
+ (leave out the ",get" to return an error instead)\033[0m
+
+ note that the \033[35m--xm\033[0m hook will only run if \033[35m--urlform\033[0m
+ is either \033[36mprint\033[0m or the default \033[36mprint,get\033[0m
"""
),
],
diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py
index 815fac23..9dff0dad 100644
--- a/copyparty/authsrv.py
+++ b/copyparty/authsrv.py
@@ -477,6 +477,13 @@ class VFS(object):
)
# skip uhtml because it's rarely needed
+ def get_perms(self, vpath: str, uname: str) -> str:
+ zbl = self.can_access(vpath, uname)
+ ret = "".join(ch for ch, ok in zip("rwmdgGa.", zbl) if ok)
+ if "rwmd" in ret and "a." in ret:
+ ret += "A"
+ return ret
+
def get(
self,
vpath: str,
diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py
index 1e1fa577..fdedab44 100644
--- a/copyparty/ftpd.py
+++ b/copyparty/ftpd.py
@@ -470,9 +470,10 @@ class FtpHandler(FTPHandler):
None,
xbu,
ap,
- vfs.canonical(rem),
+ vp,
"",
self.uname,
+ self.hub.asrv.vfs.get_perms(vp, self.uname),
0,
0,
self.cli_ip,
diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py
index 77f4a23d..30df2ddf 100644
--- a/copyparty/httpcli.py
+++ b/copyparty/httpcli.py
@@ -699,6 +699,7 @@ class HttpCli(object):
self.vpath,
self.host,
self.uname,
+ "",
time.time(),
0,
self.ip,
@@ -1635,6 +1636,7 @@ class HttpCli(object):
self.vpath,
self.host,
self.uname,
+ self.asrv.vfs.get_perms(self.vpath, self.uname),
time.time(),
len(buf),
self.ip,
@@ -1784,6 +1786,7 @@ class HttpCli(object):
self.vpath,
self.host,
self.uname,
+ self.asrv.vfs.get_perms(self.vpath, self.uname),
at,
remains,
self.ip,
@@ -1874,6 +1877,7 @@ class HttpCli(object):
self.vpath,
self.host,
self.uname,
+ self.asrv.vfs.get_perms(self.vpath, self.uname),
mt,
post_sz,
self.ip,
@@ -2556,6 +2560,7 @@ class HttpCli(object):
self.vpath,
self.host,
self.uname,
+ self.asrv.vfs.get_perms(self.vpath, self.uname),
at,
0,
self.ip,
@@ -2619,6 +2624,7 @@ class HttpCli(object):
self.vpath,
self.host,
self.uname,
+ self.asrv.vfs.get_perms(self.vpath, self.uname),
at,
sz,
self.ip,
@@ -2863,6 +2869,7 @@ class HttpCli(object):
self.vpath,
self.host,
self.uname,
+ self.asrv.vfs.get_perms(self.vpath, self.uname),
time.time(),
0,
self.ip,
@@ -2901,6 +2908,7 @@ class HttpCli(object):
self.vpath,
self.host,
self.uname,
+ self.asrv.vfs.get_perms(self.vpath, self.uname),
new_lastmod,
sz,
self.ip,
diff --git a/copyparty/smbd.py b/copyparty/smbd.py
index 6a096265..a5f6681b 100644
--- a/copyparty/smbd.py
+++ b/copyparty/smbd.py
@@ -240,7 +240,7 @@ class SMB(object):
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
- self.nlog, xbu, ap, vpath, "", "", 0, 0, "1.7.6.2", 0, ""
+ self.nlog, xbu, ap, vpath, "", "", "", 0, 0, "1.7.6.2", 0, ""
):
yeet("blocked by xbu server config: " + vpath)
diff --git a/copyparty/tftpd.py b/copyparty/tftpd.py
index c46a8fd5..04409f9d 100644
--- a/copyparty/tftpd.py
+++ b/copyparty/tftpd.py
@@ -328,7 +328,7 @@ class Tftpd(object):
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
- self.nlog, xbu, ap, vpath, "", "", 0, 0, "8.3.8.7", 0, ""
+ self.nlog, xbu, ap, vpath, "", "", "", 0, 0, "8.3.8.7", 0, ""
):
yeet("blocked by xbu server config: " + vpath)
diff --git a/copyparty/up2k.py b/copyparty/up2k.py
index 4d209dba..d1e0e7e6 100644
--- a/copyparty/up2k.py
+++ b/copyparty/up2k.py
@@ -2770,6 +2770,7 @@ class Up2k(object):
job["vtop"],
job["host"],
job["user"],
+ self.asrv.vfs.get_perms(job["vtop"], job["user"]),
job["lmod"],
job["size"],
job["addr"],
@@ -3297,6 +3298,7 @@ class Up2k(object):
djoin(vtop, rd, fn),
host,
usr,
+ self.asrv.vfs.get_perms(djoin(vtop, rd, fn), usr),
int(ts),
sz,
ip,
@@ -3496,6 +3498,7 @@ class Up2k(object):
vpath,
"",
uname,
+ self.asrv.vfs.get_perms(vpath, uname),
stl.st_mtime,
st.st_size,
ip,
@@ -3529,6 +3532,7 @@ class Up2k(object):
vpath,
"",
uname,
+ self.asrv.vfs.get_perms(vpath, uname),
stl.st_mtime,
st.st_size,
ip,
@@ -3661,7 +3665,18 @@ class Up2k(object):
xar = dvn.flags.get("xar")
if xbr:
if not runhook(
- self.log, xbr, sabs, svp, "", uname, stl.st_mtime, st.st_size, "", 0, ""
+ self.log,
+ xbr,
+ sabs,
+ svp,
+ "",
+ uname,
+ self.asrv.vfs.get_perms(svp, uname),
+ stl.st_mtime,
+ st.st_size,
+ "",
+ 0,
+ "",
):
t = "move blocked by xbr server config: {}".format(svp)
self.log(t, 1)
@@ -3686,7 +3701,20 @@ class Up2k(object):
self.rescan_cond.notify_all()
if xar:
- runhook(self.log, xar, dabs, dvp, "", uname, 0, 0, "", 0, "")
+ runhook(
+ self.log,
+ xar,
+ dabs,
+ dvp,
+ "",
+ uname,
+ self.asrv.vfs.get_perms(dvp, uname),
+ 0,
+ 0,
+ "",
+ 0,
+ "",
+ )
return "k"
@@ -3785,7 +3813,20 @@ class Up2k(object):
wunlink(self.log, sabs, svn.flags)
if xar:
- runhook(self.log, xar, dabs, dvp, "", uname, 0, 0, "", 0, "")
+ runhook(
+ self.log,
+ xar,
+ dabs,
+ dvp,
+ "",
+ uname,
+ self.asrv.vfs.get_perms(dvp, uname),
+ 0,
+ 0,
+ "",
+ 0,
+ "",
+ )
return "k"
@@ -4074,6 +4115,7 @@ class Up2k(object):
vp_chk,
job["host"],
job["user"],
+ self.asrv.vfs.get_perms(vp_chk, job["user"]),
int(job["lmod"]),
job["size"],
job["addr"],
diff --git a/copyparty/util.py b/copyparty/util.py
index c08f831e..a117755a 100644
--- a/copyparty/util.py
+++ b/copyparty/util.py
@@ -2992,7 +2992,8 @@ def retchk(
def _parsehook(
log: Optional["NamedLogger"], cmd: str
-) -> tuple[bool, bool, bool, float, dict[str, Any], str]:
+) -> tuple[str, bool, bool, bool, float, dict[str, Any], list[str]]:
+ areq = ""
chk = False
fork = False
jtxt = False
@@ -3017,8 +3018,12 @@ def _parsehook(
cap = int(arg[1:]) # 0=none 1=stdout 2=stderr 3=both
elif arg.startswith("k"):
kill = arg[1:] # [t]ree [m]ain [n]one
+ elif arg.startswith("a"):
+ areq = arg[1:] # required perms
elif arg.startswith("i"):
pass
+ elif not arg:
+ break
else:
t = "hook: invalid flag {} in {}"
(log or print)(t.format(arg, ocmd))
@@ -3045,9 +3050,11 @@ def _parsehook(
"capture": cap,
}
- cmd = os.path.expandvars(os.path.expanduser(cmd))
+ argv = cmd.split(",") if "," in cmd else [cmd]
- return chk, fork, jtxt, wait, sp_ka, cmd
+ argv[0] = os.path.expandvars(os.path.expanduser(argv[0]))
+
+ return areq, chk, fork, jtxt, wait, sp_ka, argv
def runihook(
@@ -3056,10 +3063,9 @@ def runihook(
vol: "VFS",
ups: list[tuple[str, int, int, str, str, str, int]],
) -> bool:
- ocmd = cmd
- chk, fork, jtxt, wait, sp_ka, cmd = _parsehook(log, cmd)
- bcmd = [sfsenc(cmd)]
- if cmd.endswith(".py"):
+ _, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
+ bcmd = [sfsenc(x) for x in acmd]
+ if acmd[0].endswith(".py"):
bcmd = [sfsenc(pybin)] + bcmd
vps = [vjoin(*list(s3dec(x[3], x[4]))) for x in ups]
@@ -3084,7 +3090,7 @@ def runihook(
t0 = time.time()
if fork:
- Daemon(runcmd, ocmd, [bcmd], ka=sp_ka)
+ Daemon(runcmd, cmd, bcmd, ka=sp_ka)
else:
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
if chk and rc:
@@ -3105,14 +3111,20 @@ def _runhook(
vp: str,
host: str,
uname: str,
+ perms: str,
mt: float,
sz: int,
ip: str,
at: float,
txt: str,
) -> bool:
- ocmd = cmd
- chk, fork, jtxt, wait, sp_ka, cmd = _parsehook(log, cmd)
+ areq, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
+ if areq:
+ for ch in areq:
+ if ch not in perms:
+ t = "user %s not allowed to run hook %s; need perms %s, have %s"
+ log(t % (uname, cmd, areq, perms))
+ return True # fallthrough to next hook
if jtxt:
ja = {
"ap": ap,
@@ -3123,21 +3135,22 @@ def _runhook(
"at": at or time.time(),
"host": host,
"user": uname,
+ "perms": perms,
"txt": txt,
}
arg = json.dumps(ja)
else:
arg = txt or ap
- acmd = [cmd, arg]
- if cmd.endswith(".py"):
+ acmd += [arg]
+ if acmd[0].endswith(".py"):
acmd = [pybin] + acmd
bcmd = [fsenc(x) if x == ap else sfsenc(x) for x in acmd]
t0 = time.time()
if fork:
- Daemon(runcmd, ocmd, [bcmd], ka=sp_ka)
+ Daemon(runcmd, cmd, [bcmd], ka=sp_ka)
else:
rc, v, err = runcmd(bcmd, **sp_ka) # type: ignore
if chk and rc:
@@ -3158,6 +3171,7 @@ def runhook(
vp: str,
host: str,
uname: str,
+ perms: str,
mt: float,
sz: int,
ip: str,
@@ -3167,7 +3181,7 @@ def runhook(
vp = vp.replace("\\", "/")
for cmd in cmds:
try:
- if not _runhook(log, cmd, ap, vp, host, uname, mt, sz, ip, at, txt):
+ if not _runhook(log, cmd, ap, vp, host, uname, perms, mt, sz, ip, at, txt):
return False
except Exception as ex:
(log or print)("hook: {}".format(ex))
diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js
index c7d202f6..3e0ba755 100644
--- a/copyparty/web/browser.js
+++ b/copyparty/web/browser.js
@@ -1064,7 +1064,7 @@ ebi('ops').innerHTML = (
'🎈' +
'📂' +
'📝' +
- '📟' +
+ '📟' +
'🎺' +
'⚙️' +
(IE ? '' + L.ot_noie + '' : '') +