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 + '' : '') +