19a5985f removed the restriction on uploading logues, as it was

too restrictive, blocking editing through webdav and ftp

but since logues and readmes can be used as helptext for users
with write-only access, it makes sense to block logue/readme
uploads from write-only users

users with write-only access can still upload any file as before,
but the filename prefix `_wo_` is added onto files named either
README.md | PREADME.md | .prologue.html | .epilogue.html

the new option `--wo-up-readme` restores previous behavior, and
will not add the filename-prefix for readmes/logues
This commit is contained in:
ed 2025-03-22 14:21:35 +00:00
parent a0ecc4d88e
commit 2525d594c5
8 changed files with 59 additions and 10 deletions

View file

@ -1222,6 +1222,7 @@ def add_yolo(ap):
ap2 = ap.add_argument_group('yolo options') ap2 = ap.add_argument_group('yolo options')
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests") ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET") ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
def add_optouts(ap): def add_optouts(ap):

View file

@ -52,6 +52,7 @@ def vf_bmap() -> dict[str, str]:
"og_s_title", "og_s_title",
"rand", "rand",
"rss", "rss",
"wo_up_readme",
"xdev", "xdev",
"xlink", "xlink",
"xvol", "xvol",
@ -173,6 +174,7 @@ flagcats = {
"vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)", "vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)",
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)", "vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
"medialinks": "return medialinks for non-up2k uploads (not hotlinks)", "medialinks": "return medialinks for non-up2k uploads (not hotlinks)",
"wo_up_readme": "write-only users can upload logues without getting renamed",
"rand": "force randomized filenames, 9 chars long by default", "rand": "force randomized filenames, 9 chars long by default",
"nrand=N": "randomized filenames are N chars long", "nrand=N": "randomized filenames are N chars long",
"u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always", "u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always",

View file

@ -19,6 +19,7 @@ from .__init__ import PY2, TYPE_CHECKING
from .authsrv import VFS from .authsrv import VFS
from .bos import bos from .bos import bos
from .util import ( from .util import (
FN_EMB,
VF_CAREFUL, VF_CAREFUL,
Daemon, Daemon,
ODict, ODict,
@ -170,6 +171,16 @@ class FtpFs(AbstractedFS):
fn = sanitize_fn(fn or "", "") fn = sanitize_fn(fn or "", "")
vpath = vjoin(rd, fn) vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if (
w
and fn.lower() in FN_EMB
and self.h.uname not in vfs.axs.uread
and "wo_up_readme" not in vfs.flags
):
fn = "_wo_" + fn
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if not vfs.realpath: if not vfs.realpath:
t = "No filesystem mounted at [{}]" t = "No filesystem mounted at [{}]"
raise FSE(t.format(vpath)) raise FSE(t.format(vpath))

View file

@ -46,6 +46,7 @@ from .util import (
APPLESAN_RE, APPLESAN_RE,
BITNESS, BITNESS,
DAV_ALLPROPS, DAV_ALLPROPS,
FN_EMB,
HAVE_SQLITE3, HAVE_SQLITE3,
HTTPCODE, HTTPCODE,
META_NOBOTS, META_NOBOTS,
@ -2550,6 +2551,16 @@ class HttpCli(object):
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
dbv, vrem = vfs.get_dbv(rem) dbv, vrem = vfs.get_dbv(rem)
name = sanitize_fn(name, "")
if (
not self.can_read
and self.can_write
and name.lower() in FN_EMB
and "wo_up_readme" not in dbv.flags
):
name = "_wo_" + name
body["name"] = name
body["vtop"] = dbv.vpath body["vtop"] = dbv.vpath
body["ptop"] = dbv.realpath body["ptop"] = dbv.realpath
body["prel"] = vrem body["prel"] = vrem

View file

@ -36,7 +36,19 @@ from partftpy.TftpShared import TftpException
from .__init__ import EXE, PY2, TYPE_CHECKING from .__init__ import EXE, PY2, TYPE_CHECKING
from .authsrv import VFS from .authsrv import VFS
from .bos import bos from .bos import bos
from .util import UTC, BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot from .util import (
FN_EMB,
UTC,
BytesIO,
Daemon,
ODict,
exclude_dotfiles,
min_ex,
runhook,
undot,
vjoin,
vsplit,
)
if True: # pylint: disable=using-constant-test if True: # pylint: disable=using-constant-test
from typing import Any, Union from typing import Any, Union
@ -244,16 +256,25 @@ class Tftpd(object):
for srv in srvs: for srv in srvs:
srv.stop() srv.stop()
def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]: def _v2a(
self, caller: str, vpath: str, perms: list, *a: Any
) -> tuple[VFS, str, str]:
vpath = vpath.replace("\\", "/").lstrip("/") vpath = vpath.replace("\\", "/").lstrip("/")
if not perms: if not perms:
perms = [True, True] perms = [True, True]
debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms) debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms)
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
if perms[1] and "*" not in vfs.axs.uread and "wo_up_readme" not in vfs.flags:
zs, fn = vsplit(vpath)
if fn.lower() in FN_EMB:
vpath = vjoin(zs, "_wo_" + fn)
vfs, rem = self.asrv.vfs.get(vpath, "*", *perms)
if not vfs.realpath: if not vfs.realpath:
raise Exception("unmapped vfs") raise Exception("unmapped vfs")
return vfs, vfs.canonical(rem)
return vfs, vpath, vfs.canonical(rem)
def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any: def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
# generate file listing if vpath is dir.txt and return as file object # generate file listing if vpath is dir.txt and return as file object
@ -331,7 +352,7 @@ class Tftpd(object):
else: else:
raise Exception("bad mode %s" % (mode,)) raise Exception("bad mode %s" % (mode,))
vfs, ap = self._v2a("open", vpath, [rd, wr]) vfs, vpath, ap = self._v2a("open", vpath, [rd, wr])
if wr: if wr:
if "*" not in vfs.axs.uwrite: if "*" not in vfs.axs.uwrite:
yeet("blocked write; folder not world-writable: /%s" % (vpath,)) yeet("blocked write; folder not world-writable: /%s" % (vpath,))
@ -368,7 +389,7 @@ class Tftpd(object):
return open(ap, mode, *a, **ka) return open(ap, mode, *a, **ka)
def _mkdir(self, vpath: str, *a) -> None: def _mkdir(self, vpath: str, *a) -> None:
vfs, ap = self._v2a("mkdir", vpath, []) vfs, _, ap = self._v2a("mkdir", vpath, [False, True])
if "*" not in vfs.axs.uwrite: if "*" not in vfs.axs.uwrite:
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,)) yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
@ -376,7 +397,7 @@ class Tftpd(object):
def _unlink(self, vpath: str) -> None: def _unlink(self, vpath: str) -> None:
# return bos.unlink(self._v2a("stat", vpath, *a)[1]) # return bos.unlink(self._v2a("stat", vpath, *a)[1])
vfs, ap = self._v2a("delete", vpath, [True, False, False, True]) vfs, _, ap = self._v2a("delete", vpath, [True, False, False, True])
try: try:
inf = bos.stat(ap) inf = bos.stat(ap)
@ -400,7 +421,7 @@ class Tftpd(object):
def _p_exists(self, vpath: str) -> bool: def _p_exists(self, vpath: str) -> bool:
try: try:
ap = self._v2a("p.exists", vpath, [False, False])[1] ap = self._v2a("p.exists", vpath, [False, False])[2]
bos.stat(ap) bos.stat(ap)
return True return True
except: except:
@ -408,7 +429,7 @@ class Tftpd(object):
def _p_isdir(self, vpath: str) -> bool: def _p_isdir(self, vpath: str) -> bool:
try: try:
st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[1]) st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[2])
ret = stat.S_ISDIR(st.st_mode) ret = stat.S_ISDIR(st.st_mode)
return ret return ret
except: except:

View file

@ -2918,7 +2918,6 @@ class Up2k(object):
if ptop not in self.registry: if ptop not in self.registry:
raise Pebkac(410, "location unavailable") raise Pebkac(410, "location unavailable")
cj["name"] = sanitize_fn(cj["name"], "")
cj["poke"] = now = self.db_act = self.vol_act[ptop] = time.time() cj["poke"] = now = self.db_act = self.vol_act[ptop] = time.time()
wark = dwark = self._get_wark(cj) wark = dwark = self._get_wark(cj)
job = None job = None
@ -3236,6 +3235,7 @@ class Up2k(object):
job["ptop"] = vfs.realpath job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath job["vtop"] = vfs.vpath
job["prel"] = rem job["prel"] = rem
job["name"] = sanitize_fn(job["name"], "")
if zvfs.vpath != vfs.vpath: if zvfs.vpath != vfs.vpath:
# print(json.dumps(job, sort_keys=True, indent=4)) # print(json.dumps(job, sort_keys=True, indent=4))
job["hash"] = cj["hash"] job["hash"] = cj["hash"]
@ -4996,6 +4996,7 @@ class Up2k(object):
job["ptop"] = vfs.realpath job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath job["vtop"] = vfs.vpath
job["prel"] = rem job["prel"] = rem
job["name"] = sanitize_fn(job["name"], "")
if zvfs.vpath != vfs.vpath: if zvfs.vpath != vfs.vpath:
self.log("xbu reloc2:%d..." % (depth,), 6) self.log("xbu reloc2:%d..." % (depth,), 6)
return self._handle_json(job, depth + 1) return self._handle_json(job, depth + 1)

View file

@ -448,6 +448,8 @@ UNHUMANIZE_UNITS = {
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1} VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
FN_EMB = set([".prologue.html", ".epilogue.html", "readme.md", "preadme.md"])
def read_ram() -> tuple[float, float]: def read_ram() -> tuple[float, float]:
a = b = 0 a = b = 0

View file

@ -129,7 +129,7 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0): def __init__(self, a=None, v=None, c=None, **ka0):
ka = {} ka = {}
ex = "chpw daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz rss smb srch_dbg srch_excl stats uqe vague_403 vc ver write_uplog xdev xlink xvol zipmaxu zs" ex = "chpw daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz rss smb srch_dbg srch_excl stats uqe vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs"
ka.update(**{k: False for k in ex.split()}) ka.update(**{k: False for k in ex.split()})
ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash plain_ip" ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash plain_ip"