Compare commits

...

5 commits

Author SHA1 Message Date
ed
6f6b70ad04 fix misleading comments 2025-09-27 00:22:28 +00:00
ed
e187df28f2 fix markdown-expand example 2025-09-26 23:50:19 +00:00
ed
df0fa9d1b7 xbu/xau with custom message 2025-09-26 23:49:32 +00:00
ed
397ed5653b overwrite on upload with header "replace" 2025-09-26 21:35:00 +00:00
ed
9f46e4dbd7 koie layout 2025-09-26 21:17:07 +00:00
11 changed files with 273 additions and 116 deletions

View file

@ -0,0 +1,60 @@
#!/usr/bin/env python3
import json
import os
import re
import sys
_ = r"""
reject file upload (with a nice explanation why)
example usage as global config:
--xbu j,c1,bin/hooks/reject-and-explain.py
example usage as a volflag (per-volume config):
-v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reject-and-explain.py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(share filesystem-path srv/inc as volume /inc,
readable by everyone, read-write for user 'ed',
running this plugin on all uploads with the params listed below)
example usage as a volflag in a copyparty config file:
[/inc]
srv/inc
accs:
r: *
rw: ed
flags:
xbu: j,c1,bin/hooks/reject-and-explain.py
parameters explained,
xbu = execute-before-upload (can also be xau, execute-after-upload)
j = this hook needs upload information as json (not just the filename)
c1 = this hook returns json on stdout, so tell copyparty to read that
"""
def main():
inf = json.loads(sys.argv[1])
vdir, fn = os.path.split(inf["vp"])
print("inf[vp] = %r" % (inf["vp"],), file=sys.stderr)
# the following is what decides if we'll accept the upload or reject it:
# we check if the upload-folder url matches the following regex-pattern:
ok = re.search(r"(^|/)day[0-9]+$", vdir, re.IGNORECASE)
if ok:
# allow the upload
print("{}")
return
# the upload was rejected; display the following errortext:
errmsg = "Files can only be uploaded into a folder named 'DayN' where N is a number, for example 'Day573'. This file was REJECTED: "
errmsg += inf["vp"] # if you want to mention the file's url at the end of the message
print(json.dumps({"rejectmsg": errmsg}))
if __name__ == "__main__":
main()

View file

@ -956,7 +956,7 @@ def get_sects():
\033[36m{{vf.thsize}} \033[35mthumbnail size
\033[36m{{srv.itime}} \033[35mserver time in seconds
\033[36m{{srv.htime}} \033[35mserver time as YY-mm-dd, HH:MM:SS (UTC)
\033[36m{{hdr.cf_ipcountry}} \033[35mthe "CF-IPCountry" client header (probably blank)
\033[36m{{hdr.cf-ipcountry}} \033[35mthe "CF-IPCountry" client header (probably blank)
\033[0m
so the following types of placeholders can be added to the lists:
* any client header can be accessed through {{hdr.*}}

View file

@ -493,24 +493,30 @@ class FtpHandler(FTPHandler):
return
self.vfs_map[ap] = vp
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
None,
None,
self.hub.up2k,
"xbu.ftpd",
xbu,
ap,
vp,
"",
self.uname,
self.hub.asrv.vfs.get_perms(vp, self.uname),
0,
0,
self.cli_ip,
time.time(),
"",
):
raise FSE("Upload blocked by xbu server config")
if xbu:
hr = runhook(
None,
None,
self.hub.up2k,
"xbu.ftpd",
xbu,
ap,
vp,
"",
self.uname,
self.hub.asrv.vfs.get_perms(vp, self.uname),
0,
0,
self.cli_ip,
time.time(),
"",
)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "Upload blocked by xbu server config: %r" % (vp,)
self.respond("550 %s" % (t,), logging.info)
return
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
ret = FTPHandler.ftp_STOR(self, file, mode)

View file

@ -2268,8 +2268,10 @@ class HttpCli(object):
at,
"",
)
if not hr:
t = "upload blocked by xbu server config"
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r" % (vp,)
self.log(t, 1)
raise Pebkac(403, t)
if hr.get("reloc"):
@ -2301,7 +2303,11 @@ class HttpCli(object):
if (
self.can_delete
and (vfs.flags.get("daw") or "x-oc-mtime" in self.headers)
and (
vfs.flags.get("daw")
or "replace" in self.headers
or "x-oc-mtime" in self.headers
)
) or (
not bos.path.exists(os.path.join(fdir, tnam))
and not bos.path.getsize(path)
@ -2397,8 +2403,10 @@ class HttpCli(object):
at,
"",
)
if not hr:
t = "upload blocked by xau server config"
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xau server config: %r" % (vp,)
self.log(t, 1)
wunlink(self.log, path, vfs.flags)
raise Pebkac(403, t)
@ -3210,11 +3218,38 @@ class HttpCli(object):
new_file += ".md"
sanitized = sanitize_fn(new_file, "")
fdir = vfs.canonical(rem)
fn = os.path.join(fdir, sanitized)
for hn in ("xbu", "xau"):
xxu = vfs.flags.get(hn)
if xxu:
hr = runhook(
self.log,
self.conn.hsrv.broker,
None,
"%s.http.new-md" % (hn,),
xxu,
fn,
vjoin(self.vpath, sanitized),
self.host,
self.uname,
self.asrv.vfs.get_perms(self.vpath, self.uname),
time.time(),
0,
self.ip,
time.time(),
"",
)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "new-md blocked by " + hn + " server config: %r"
t = t % (vjoin(vfs.vpath, rem),)
self.log(t, 1)
raise Pebkac(403, t)
if not nullwrite:
fdir = vfs.canonical(rem)
fn = os.path.join(fdir, sanitized)
if bos.path.exists(fn):
raise Pebkac(500, "that file exists already")
@ -3344,7 +3379,7 @@ class HttpCli(object):
open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}
if "replace" in self.uparam:
if "replace" in self.uparam or "replace" in self.headers:
if not self.can_delete:
self.log("user not allowed to overwrite with ?replace")
elif bos.path.exists(abspath):
@ -3378,8 +3413,11 @@ class HttpCli(object):
at,
"",
)
if not hr:
t = "upload blocked by xbu server config"
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r"
t = t % (vjoin(upload_vpath, fname),)
self.log(t, 1)
raise Pebkac(403, t)
if hr.get("reloc"):
@ -3482,8 +3520,11 @@ class HttpCli(object):
at,
"",
)
if not hr:
t = "upload blocked by xau server config"
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xau server config: %r"
t = t % (vjoin(upload_vpath, fname),)
self.log(t, 1)
wunlink(self.log, abspath, vfs.flags)
raise Pebkac(403, t)
@ -3775,7 +3816,7 @@ class HttpCli(object):
xbu = vfs.flags.get("xbu")
if xbu:
if not runhook(
hr = runhook(
self.log,
self.conn.hsrv.broker,
None,
@ -3791,8 +3832,11 @@ class HttpCli(object):
self.ip,
time.time(),
"",
):
t = "save blocked by xbu server config"
)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "save blocked by xbu server config"
self.log(t, 1)
raise Pebkac(403, t)
@ -3819,27 +3863,31 @@ class HttpCli(object):
sha512 = sha512[:56]
xau = vfs.flags.get("xau")
if xau and not runhook(
self.log,
self.conn.hsrv.broker,
None,
"xau.http.txt",
xau,
fp,
self.vpath,
self.host,
self.uname,
self.asrv.vfs.get_perms(self.vpath, self.uname),
new_lastmod,
sz,
self.ip,
new_lastmod,
"",
):
t = "save blocked by xau server config"
self.log(t, 1)
wunlink(self.log, fp, vfs.flags)
raise Pebkac(403, t)
if xau:
hr = runhook(
self.log,
self.conn.hsrv.broker,
None,
"xau.http.txt",
xau,
fp,
self.vpath,
self.host,
self.uname,
self.asrv.vfs.get_perms(self.vpath, self.uname),
new_lastmod,
sz,
self.ip,
new_lastmod,
"",
)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "save blocked by xau server config"
self.log(t, 1)
wunlink(self.log, fp, vfs.flags)
raise Pebkac(403, t)
self.conn.hsrv.broker.say(
"up2k.hash_file",

View file

@ -246,24 +246,29 @@ class SMB(object):
ap = absreal(ap)
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
self.nlog,
None,
self.hub.up2k,
"xbu.smb",
xbu,
ap,
vpath,
"",
"",
"",
0,
0,
"1.7.6.2",
time.time(),
"",
):
yeet("blocked by xbu server config: %r" % (vpath,))
if xbu:
hr = runhook(
self.nlog,
None,
self.hub.up2k,
"xbu.smb",
xbu,
ap,
vpath,
"",
"",
"",
0,
0,
"1.7.6.2",
time.time(),
"",
)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "blocked by xbu server config: %r" % (vpath,)
yeet(t)
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
if wr:

View file

@ -363,24 +363,29 @@ class Tftpd(object):
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
self.nlog,
None,
self.hub.up2k,
"xbu.tftpd",
xbu,
ap,
vpath,
"",
"",
"",
0,
0,
"8.3.8.7",
time.time(),
"",
):
yeet("blocked by xbu server config: %r" % (vpath,))
if xbu:
hr = runhook(
self.nlog,
None,
self.hub.up2k,
"xbu.tftpd",
xbu,
ap,
vpath,
"",
"",
"",
0,
0,
"8.3.8.7",
time.time(),
"",
)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r" % (vpath,)
yeet(t)
if not self.args.tftp_nols and bos.path.isdir(ap):
return self._ls(vpath, "", 0, True)

View file

@ -3300,8 +3300,11 @@ class Up2k(object):
job["at"],
"",
)
if not hr:
t = "upload blocked by xbu server config: %r" % (dst,)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r"
t = t % (vp,)
self.log(t, 1)
raise Pebkac(403, t)
if hr.get("reloc"):
@ -3981,8 +3984,11 @@ class Up2k(object):
at or time.time(),
"",
)
if not hr:
t = "upload blocked by xau server config"
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xau server config: %r"
t = t % (djoin(vtop, rd, fn),)
self.log(t, 1)
wunlink(self.log, dst, vflags)
self.registry[ptop].pop(wark, None)
@ -5132,8 +5138,10 @@ class Up2k(object):
job["t0"],
"",
)
if not hr:
t = "upload blocked by xbu server config: %r" % (vp_chk,)
t = hr.get("rejectmsg") or ""
if t or not hr:
if not t:
t = "upload blocked by xbu server config: %r" % (vp_chk,)
self.log(t, 1)
raise Pebkac(403, t)
if hr.get("reloc"):

View file

@ -432,7 +432,7 @@ EXTS["vnd.mozilla.apng"] = "png"
MAGIC_MAP = {"jpeg": "jpg"}
DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf_ipcountry srv.itime srv.htime"
DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf-ipcountry srv.itime srv.htime"
DEF_MTE = ".files,circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash"

View file

@ -118,6 +118,26 @@ table {
.btns>a:first-child {
margin-left: 0;
}
.agr br {
display: none;
}
#lo,
.agr a,
.agr form {
margin: 0 .5em 0 0;
line-height: 4em;
}
.agr form,
.agr input {
display: inline;
padding: 0;
margin: 0;
}
#lo,
.agr input {
line-height: 1em;
font-weight: normal;
}
#msg {
margin: 3em 0;
}

View file

@ -151,19 +151,31 @@
{%- endif %}
<h1 id="cc">other stuff:</h1>
<ul>
<div class="agr">
{%- if ahttps %}
<li><a id="wb" href="{{ ahttps }}">switch to https</a></li>
<a id="wb" href="{{ ahttps }}">switch to https</a><br />
{%- endif %}
{%- if this.uname in this.args.idp_adm_set %}
<li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li>
{%- endif %}
<a id="af" href="{{ r }}/?ru">show recent uploads</a><br />
{%- if this.uname != '*' and this.args.shr %}
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
<a id="y" href="{{ r }}/?shares">edit shares</a><br />
{%- endif %}
{%- if this.uname in this.args.idp_adm_set %}
<a id="ag" href="{{ r }}/?idp">view idp cache</a><br />
{%- endif %}
<a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a><br />
{%- if this.uname != '*' and not in_shr %}
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="act" value="logout" />
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
</form>
{%- endif %}
</div>
<ul>
{%- if k304 or k304vis %}
{%- if k304 %}
<li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled)
@ -181,16 +193,6 @@
{%- endif %}
<blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
{%- endif %}
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
{%- if this.uname != '*' and not in_shr %}
<li><form method="post" enctype="multipart/form-data">
<input type="hidden" name="act" value="logout" />
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
</form></li>
{%- endif %}
</ul>
</div>

View file

@ -246,6 +246,7 @@ upload modifiers:
| `Accept: json` | `want=json` | return upload info as json; same as `?j` |
| `Rand: 4` | `rand=4` | generate random filename with 4 characters |
| `Life: 30` | `life=30` | delete file after 30 seconds |
| `Replace: 1` | `replace` | overwrite file if exists |
| `CK: no` | `ck` | disable serverside checksum (maybe faster) |
| `CK: md5` | `ck=md5` | return md5 checksum instead of sha512 |
| `CK: sha1` | `ck=sha1` | return sha1 checksum |
@ -254,7 +255,9 @@ upload modifiers:
| `CK: b2s` | `ck=b2s` | return blake2s checksum |
* `life` only has an effect if the volume has a lifetime, and the volume lifetime must be greater than the file's
* `replace` upload-modifier:
* the header `replace: 1` works for both PUT and multipart-post
* the url-param `replace` only works for multipart-post
* server behavior of `msg` can be reconfigured with `--urlform`
## admin