Compare commits

..

9 Commits

Author SHA1 Message Date
ed
85a637af09 v1.8.2 2023-07-14 15:58:39 +00:00
ed
043e3c7dd6 fix traversal vulnerability GHSA-pxfv-7rr3-2qjg:
the /.cpr endpoint allowed full access to server filesystem,
unless mitigated by prisonparty
2023-07-14 15:55:49 +00:00
ed
8f59afb159 fix another race (unpost):
unposting could collide with most other database-related activities,
causing one or the other to fail.
luckily the unprotected query performed by the unpost API happens to be
very cheap, so also the most likely to fail, and would succeed upon a
manual reattempt from the UI.
even in the worst case scenario, there would be no unrecoverable damage
as the next rescan would auto-repair any resulting inconsistencies.
2023-07-14 15:21:14 +00:00
ed
77f1e51444 fix unlikely race (e2tsr):
if someone with admin rights refreshes the homepage exactly as the
directory indexer decides to `_drop_caches`, the indexer thread would
die and the up2k instance would become inoperable...
luckily the probability of hitting this by chance is absolutely minimal,
and the worst case scenario is having to restart copyparty if this
happens immediately after startup; there is no risk of database damage
2023-07-14 15:20:25 +00:00
ed
22fc4bb938 add event-hook for banning users 2023-07-13 22:29:32 +00:00
ed
50c7bba6ea volflag "nohtml" to never return html or rendered markdown from potentially unsafe volumes 2023-07-13 21:57:52 +00:00
ed
551d99b71b add permission "a" to show uploader IPs (#45) 2023-07-12 21:36:55 +00:00
ed
b54b7213a7 more thumbnailer configs available as volflags:
--th-convt = convt
--th-no-crop = nocrop
--th-size = thsize
2023-07-11 22:15:37 +00:00
ed
a14943c8de update pkgs to 1.8.1 2023-07-07 23:58:16 +00:00
17 changed files with 260 additions and 118 deletions

View File

@@ -346,6 +346,7 @@ permissions:
* `d` (delete): delete files/folders
* `g` (get): only download files, cannot see folder contents or zip/tar
* `G` (upget): same as `g` except uploaders get to see their own filekeys (see `fk` in examples below)
* `a` (admin): can see uploader IPs
examples:
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
@@ -1536,6 +1537,7 @@ some notes on hardening
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
* cors doesn't work right otherwise
* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`
safety profiles:

View File

@@ -138,6 +138,7 @@ in {
"d" (delete): permanently delete files and folders
"g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads
"a" (upget): can see uploader IPs
For example: "rwmd"

View File

@@ -1,6 +1,6 @@
# Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty
pkgver="1.8.0"
pkgver="1.8.1"
pkgrel=1
pkgdesc="Portable file sharing hub"
arch=("any")
@@ -20,7 +20,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" )
sha256sums=("e8ac0442abbf4c95428db0ad35a6965ad73038e0f384795907cde08af9e76612")
sha256sums=("f43da11ba5d1d5adf99ad642bf068042c46c23d408e7ed17b025065121abab94")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.8.0/copyparty-sfx.py",
"version": "1.8.0",
"hash": "sha256-7e0XL1r+m7e7scOsvbG9I7jk/Z24OGBuWJHxST8Ko7Y="
"url": "https://github.com/9001/copyparty/releases/download/v1.8.1/copyparty-sfx.py",
"version": "1.8.1",
"hash": "sha256-0Lf5djrgGAM+wwZP66GtSXkmRnIp3tij8j7cANeoE7o="
}

View File

@@ -492,6 +492,7 @@ def get_sects():
"d" (delete): permanently delete files and folders
"g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads
"a" (admin): can see uploader IPs
too many volflags to list here, see --help-flags
@@ -586,6 +587,7 @@ def get_sects():
\033[36mxbd\033[35m executes CMD before a file delete
\033[36mxad\033[35m executes CMD after a file delete
\033[36mxm\033[35m executes CMD on message
\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
@@ -621,6 +623,9 @@ def get_sects():
executed program on STDIN instead of as argv arguments, and
it also includes the wark (file-id/hash) as a json property
\033[36mxban\033[0m can be used to overrule / cancel a user ban event;
if the program returns 0 (true/OK) then the ban will NOT happen
except for \033[36mxm\033[0m, only one hook / one action can run at a time,
so it's recommended to use the \033[36mf\033[0m flag unless you really need
to wait for the hook to finish before continuing (without \033[36mf\033[0m
@@ -919,6 +924,7 @@ def add_hooks(ap):
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute CMD before a file delete")
ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute CMD after a file delete")
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message")
ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="execute CMD if someone gets banned (pw/404)")
def add_yolo(ap):
@@ -1007,10 +1013,10 @@ def add_thumbnail(ap):
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)")
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)")
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res")
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=int, default=60, help="conversion timeout in seconds")
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60, help="conversion timeout in seconds (volflag=convt)")
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image (volflag=nocrop)")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
@@ -1073,7 +1079,7 @@ def add_db_metadata(ap):
ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/ffprobe parsers")
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash")
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash,up_ip,.up_at")
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)",
default=".vq,.aq,vc,ac,fmt,res,.fps")
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file")
@@ -1337,11 +1343,9 @@ def main(argv: Optional[list[str]] = None) -> None:
if re.match("c[^,]", opt):
mod = True
na.append("c," + opt[1:])
elif re.sub("^[rwmdgG]*", "", opt) and "," not in opt:
elif re.sub("^[rwmdgGa]*", "", opt) and "," not in opt:
mod = True
perm = opt[0]
if perm == "a":
perm = "rw"
na.append(perm + "," + opt[1:])
else:
na.append(opt)

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 8, 1)
VERSION = (1, 8, 2)
CODENAME = "argon"
BUILD_DT = (2023, 7, 7)
BUILD_DT = (2023, 7, 14)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -62,6 +62,7 @@ class AXS(object):
udel: Optional[Union[list[str], set[str]]] = None,
uget: Optional[Union[list[str], set[str]]] = None,
upget: Optional[Union[list[str], set[str]]] = None,
uadmin: Optional[Union[list[str], set[str]]] = None,
) -> None:
self.uread: set[str] = set(uread or [])
self.uwrite: set[str] = set(uwrite or [])
@@ -69,14 +70,11 @@ class AXS(object):
self.udel: set[str] = set(udel or [])
self.uget: set[str] = set(uget or [])
self.upget: set[str] = set(upget or [])
self.uadmin: set[str] = set(uadmin or [])
def __repr__(self) -> str:
return "AXS(%s)" % (
", ".join(
"%s=%r" % (k, self.__dict__[k])
for k in "uread uwrite umove udel uget upget".split()
)
)
ks = "uread uwrite umove udel uget upget uadmin".split()
return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),)
class Lim(object):
@@ -435,8 +433,8 @@ class VFS(object):
def can_access(
self, vpath: str, uname: str
) -> tuple[bool, bool, bool, bool, bool, bool]:
"""can Read,Write,Move,Delete,Get,Upget"""
) -> tuple[bool, bool, bool, bool, bool, bool, bool]:
"""can Read,Write,Move,Delete,Get,Upget,Admin"""
if vpath:
vn, _ = self._find(undot(vpath))
else:
@@ -450,6 +448,7 @@ class VFS(object):
uname in c.udel or "*" in c.udel,
uname in c.uget or "*" in c.uget,
uname in c.upget or "*" in c.upget,
uname in c.uadmin or "*" in c.uadmin,
)
def get(
@@ -944,7 +943,7 @@ class AuthSrv(object):
try:
self._l(ln, 5, "volume access config:")
sk, sv = ln.split(":")
if re.sub("[rwmdgG]", "", sk) or not sk:
if re.sub("[rwmdgGa]", "", sk) or not sk:
err = "invalid accs permissions list; "
raise Exception(err)
if " " in re.sub(", *", "", sv).strip():
@@ -953,7 +952,7 @@ class AuthSrv(object):
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
continue
except:
err += "accs entries must be 'rwmdgG: user1, user2, ...'"
err += "accs entries must be 'rwmdgGa: user1, user2, ...'"
raise Exception(err)
if cat == catf:
@@ -989,7 +988,7 @@ class AuthSrv(object):
def _read_vol_str(
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
) -> None:
if lvl.strip("crwmdgG"):
if lvl.strip("crwmdgGa"):
raise Exception("invalid volflag: {},{}".format(lvl, uname))
if lvl == "c":
@@ -1021,6 +1020,7 @@ class AuthSrv(object):
("g", axs.uget),
("G", axs.uget),
("G", axs.upget),
("a", axs.uadmin),
]: # b bb bbb
if ch in lvl:
if un == "*":
@@ -1047,7 +1047,8 @@ class AuthSrv(object):
flags[name] = True
return
if name not in "mtp xbu xau xiu xbr xar xbd xad xm on404 on403".split():
zs = "mtp on403 on404 xbu xau xiu xbr xar xbd xad xm xban"
if name not in zs.split():
if value is True:
t = "└─add volflag [{}] = {} ({})"
else:
@@ -1092,7 +1093,7 @@ class AuthSrv(object):
if self.args.v:
# list of src:dst:permset:permset:...
# permset is <rwmdgG>[,username][,username] or <c>,<flag>[=args]
# permset is <rwmdgGa>[,username][,username] or <c>,<flag>[=args]
for v_str in self.args.v:
m = re_vol.match(v_str)
if not m:
@@ -1196,7 +1197,15 @@ class AuthSrv(object):
all_users = {}
missing_users = {}
for axs in daxs.values():
for d in [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget, axs.upget]:
for d in [
axs.uread,
axs.uwrite,
axs.umove,
axs.udel,
axs.uget,
axs.upget,
axs.uadmin,
]:
for usr in d:
all_users[usr] = 1
if usr != "*" and usr not in acct:
@@ -1420,6 +1429,10 @@ class AuthSrv(object):
if k in vol.flags:
vol.flags[k] = int(vol.flags[k])
for k in ("convt",):
if k in vol.flags:
vol.flags[k] = float(vol.flags[k])
for k1, k2 in IMPLICATIONS:
if k1 in vol.flags:
vol.flags[k2] = True
@@ -1445,7 +1458,7 @@ class AuthSrv(object):
vol.flags["mth"] = self.args.mth
# append additive args from argv to volflags
hooks = "xbu xau xiu xbr xar xbd xad xm".split()
hooks = "xbu xau xiu xbr xar xbd xad xm xban".split()
for name in "mtp on404 on403".split() + hooks:
self._read_volflag(vol.flags, name, getattr(self.args, name), True)
@@ -1468,6 +1481,10 @@ class AuthSrv(object):
hfs = [x for x in hfs if x != "f"]
ocmd = ",".join(hfs + [cmd])
if "c" not in hfs and "f" not in hfs and hn == "xban":
hfs = ["c"] + hfs
ocmd = ",".join(hfs + [cmd])
ncmds.append(ocmd)
vol.flags[hn] = ncmds
@@ -1607,6 +1624,7 @@ class AuthSrv(object):
["delete", "udel"],
[" get", "uget"],
[" upget", "upget"],
["uadmin", "uadmin"],
]:
u = list(sorted(getattr(zv.axs, attr)))
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
@@ -1752,10 +1770,19 @@ class AuthSrv(object):
raise Exception("volume not found: " + zs)
self.log(str({"users": users, "vols": vols, "flags": flags}))
t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({})"
t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({}) uadmin({})"
for k, zv in self.vfs.all_vols.items():
vc = zv.axs
vs = [k, vc.uread, vc.uwrite, vc.umove, vc.udel, vc.uget, vc.upget]
vs = [
k,
vc.uread,
vc.uwrite,
vc.umove,
vc.udel,
vc.uget,
vc.upget,
vc.uadmin,
]
self.log(t.format(*vs))
flag_v = "v" in flags
@@ -1835,7 +1862,8 @@ class AuthSrv(object):
]
csv = set("i p".split())
lst = set("c ihead mtm mtp xad xar xau xiu xbd xbr xbu xm on404 on403".split())
zs = "c ihead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm"
lst = set(zs.split())
askip = set("a v c vc cgen theme".split())
# keymap from argv to vflag
@@ -1894,6 +1922,7 @@ class AuthSrv(object):
"d": "udel",
"g": "uget",
"G": "upget",
"a": "uadmin",
}
users = {}
for pkey in perms.values():
@@ -2090,7 +2119,7 @@ def upgrade_cfg_fmt(
else:
sn = sn.replace(",", ", ")
ret.append(" " + sn)
elif sn[:1] in "rwmdgG":
elif sn[:1] in "rwmdgGa":
if cat != catx:
cat = catx
ret.append(cat)

View File

@@ -13,6 +13,7 @@ def vf_bmap() -> dict[str, str]:
"no_dedup": "copydupes",
"no_dupe": "nodupe",
"no_forget": "noforget",
"th_no_crop": "nocrop",
"dav_auth": "davauth",
"dav_rt": "davrt",
}
@@ -40,8 +41,8 @@ def vf_bmap() -> dict[str, str]:
def vf_vmap() -> dict[str, str]:
"""argv-to-volflag: simple values"""
ret = {}
for k in ("lg_sbf", "md_sbf", "unlist"):
ret = {"th_convt": "convt", "th_size": "thsize"}
for k in ("dbd", "lg_sbf", "md_sbf", "nrand", "unlist"):
ret[k] = k
return ret
@@ -49,7 +50,7 @@ def vf_vmap() -> dict[str, str]:
def vf_cmap() -> dict[str, str]:
"""argv-to-volflag: complex/lists"""
ret = {}
for k in ("dbd", "html_head", "mte", "mth", "nrand"):
for k in ("html_head", "mte", "mth"):
ret[k] = k
return ret
@@ -124,6 +125,9 @@ flagcats = {
"dvthumb": "disables video thumbnails",
"dathumb": "disables audio thumbnails (spectrograms)",
"dithumb": "disables image thumbnails",
"thsize": "thumbnail res; WxH",
"nocrop": "disable center-cropping",
"convt": "conversion timeout in seconds",
},
"handlers\n(better explained in --help-handlers)": {
"on404=PY": "handle 404s by executing PY file",
@@ -138,6 +142,7 @@ flagcats = {
"xbd=CMD": "execute CMD before a file delete",
"xad=CMD": "execute CMD after a file delete",
"xm=CMD": "execute CMD on message",
"xban=CMD": "execute CMD if someone gets banned",
},
"client and ux": {
"grid": "show grid/thumbnails by default",
@@ -151,6 +156,7 @@ flagcats = {
"sb_lg": "enable js sandbox for prologue/epilogue (default)",
"md_sbf": "list of markdown-sandbox safeguards to disable",
"lg_sbf": "list of *logue-sandbox safeguards to disable",
"nohtml": "return html and markdown as text/html",
},
"others": {
"fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission',

View File

@@ -134,6 +134,7 @@ class FtpFs(AbstractedFS):
self.can_read = self.can_write = self.can_move = False
self.can_delete = self.can_get = self.can_upget = False
self.can_admin = False
self.listdirinfo = self.listdir
self.chdir(".")
@@ -168,7 +169,7 @@ class FtpFs(AbstractedFS):
if not avfs:
raise FSE(t.format(vpath), 1)
cr, cw, cm, cd, _, _ = avfs.can_access("", self.h.uname)
cr, cw, cm, cd, _, _, _ = avfs.can_access("", self.h.uname)
if r and not cr or w and not cw or m and not cm or d and not cd:
raise FSE(t.format(vpath), 1)
@@ -243,6 +244,7 @@ class FtpFs(AbstractedFS):
self.can_delete,
self.can_get,
self.can_upget,
self.can_admin,
) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None:

View File

@@ -42,6 +42,7 @@ from .util import (
Pebkac,
UnrecvEOF,
alltrace,
absreal,
atomic_move,
exclude_dotfiles,
fsenc,
@@ -137,6 +138,8 @@ class HttpCli(object):
self.uparam: dict[str, str] = {}
self.cookies: dict[str, str] = {}
self.avn: Optional[VFS] = None
self.vn = self.asrv.vfs
self.rem = " "
self.vpath = " "
self.uname = " "
self.pw = " "
@@ -153,6 +156,7 @@ class HttpCli(object):
self.can_delete = False
self.can_get = False
self.can_upget = False
self.can_admin = False
# post
self.parser: Optional[MultipartParser] = None
# end placeholders
@@ -431,10 +435,13 @@ class HttpCli(object):
self.can_delete,
self.can_get,
self.can_upget,
self.can_admin,
) = (
avn.can_access("", self.uname) if avn else [False] * 6
)
self.avn = avn
self.vn = vn
self.rem = rem
self.s.settimeout(self.args.s_tbody or None)
@@ -570,9 +577,8 @@ class HttpCli(object):
# default to utf8 html if no content-type is set
if not mime:
mime = self.out_headers.get("Content-Type", "text/html; charset=utf-8")
mime = self.out_headers.get("Content-Type") or "text/html; charset=utf-8"
assert mime
self.out_headers["Content-Type"] = mime
for k, zs in list(self.out_headers.items()) + self.out_headerlist:
@@ -597,8 +603,22 @@ class HttpCli(object):
if g.lim:
bonk, ip = g.bonk(self.ip, self.vpath)
if bonk:
self.log("client banned: 404s", 1)
self.conn.hsrv.bans[ip] = bonk
xban = self.vn.flags.get("xban")
if not xban or not runhook(
self.log,
xban,
self.vn.canonical(self.rem),
self.vpath,
self.host,
self.uname,
time.time(),
0,
self.ip,
time.time(),
"404",
):
self.log("client banned: 404s", 1)
self.conn.hsrv.bans[ip] = bonk
if volsan:
vols = list(self.asrv.vfs.all_vols.values())
@@ -760,7 +780,14 @@ class HttpCli(object):
self.reply(b"", 301, headers=h)
return True
static_path = os.path.join(self.E.mod, "web/", self.vpath[5:])
path_base = os.path.join(self.E.mod, "web")
static_path = absreal(os.path.join(path_base, self.vpath[5:]))
if not static_path.startswith(path_base):
t = "attempted path traversal [{}] => [{}]"
self.log(t.format(self.vpath, static_path), 1)
self.tx_404()
return False
return self.tx_file(static_path)
if "cf_challenge" in self.uparam:
@@ -771,9 +798,8 @@ class HttpCli(object):
t = "@{} has no access to [{}]"
self.log(t.format(self.uname, self.vpath))
if self.avn and "on403" in self.avn.flags:
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
ret = self.on40x(vn.flags["on403"], vn, rem)
if "on403" in self.vn.flags:
ret = self.on40x(self.vn.flags["on403"], self.vn, self.rem)
if ret == "true":
return True
elif ret == "false":
@@ -782,6 +808,7 @@ class HttpCli(object):
self.log("plugin override; access permitted")
self.can_read = self.can_write = self.can_move = True
self.can_delete = self.can_get = self.can_upget = True
self.can_admin = True
else:
return self.tx_404(True)
else:
@@ -1039,9 +1066,6 @@ class HttpCli(object):
from .dxml import mkenod, mktnod, parse_xml
self.asrv.vfs.get(self.vpath, self.uname, False, False)
# abspath = vn.dcanonical(rem)
buf = b""
for rbuf in self.get_body_reader()[0]:
buf += rbuf
@@ -1098,8 +1122,7 @@ class HttpCli(object):
from .dxml import mkenod, mktnod, parse_xml
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
abspath = vn.dcanonical(rem)
abspath = self.vn.dcanonical(self.rem)
buf = b""
for rbuf in self.get_body_reader()[0]:
@@ -1313,20 +1336,17 @@ class HttpCli(object):
plain = zb.decode("utf-8", "replace")
if buf.startswith(b"msg="):
plain = plain[4:]
vfs, rem = self.asrv.vfs.get(
self.vpath, self.uname, False, False
)
xm = vfs.flags.get("xm")
xm = self.vn.flags.get("xm")
if xm:
runhook(
self.log,
xm,
vfs.canonical(rem),
self.vn.canonical(self.rem),
self.vpath,
self.host,
self.uname,
time.time(),
len(xm),
len(buf),
self.ip,
time.time(),
plain,
@@ -1998,8 +2018,22 @@ class HttpCli(object):
if g.lim:
bonk, ip = g.bonk(self.ip, pwd)
if bonk:
self.log("client banned: invalid passwords", 1)
self.conn.hsrv.bans[ip] = bonk
xban = self.vn.flags.get("xban")
if not xban or not runhook(
self.log,
xban,
self.vn.canonical(self.rem),
self.vpath,
self.host,
self.uname,
time.time(),
0,
self.ip,
time.time(),
"pw",
):
self.log("client banned: invalid passwords", 1)
self.conn.hsrv.bans[ip] = bonk
msg = "naw dude"
pwd = "x" # nosec
@@ -2728,6 +2762,9 @@ class HttpCli(object):
else:
mime = guess_mime(req_path)
if "nohtml" in self.vn.flags and "html" in mime:
mime = "text/plain; charset=utf-8"
self.out_headers["Accept-Ranges"] = "bytes"
self.send_headers(length=upper - lower, status=status, mime=mime)
@@ -3397,7 +3434,8 @@ class HttpCli(object):
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
vn = self.vn
rem = self.rem
abspath = vn.dcanonical(rem)
dbv, vrem = vn.get_dbv(rem)
@@ -3493,8 +3531,14 @@ class HttpCli(object):
self.log("wrong filekey, want {}, got {}".format(correct, got))
return self.tx_404()
if abspath.endswith(".md") and (
"v" in self.uparam or "edit" in self.uparam or "edit2" in self.uparam
if (
abspath.endswith(".md")
and "nohtml" not in vn.flags
and (
"v" in self.uparam
or "edit" in self.uparam
or "edit2" in self.uparam
)
):
return self.tx_md(abspath)
@@ -3535,6 +3579,8 @@ class HttpCli(object):
perms.append("get")
if self.can_upget:
perms.append("upget")
if self.can_admin:
perms.append("admin")
url_suf = self.urlq({}, ["k"])
is_ls = "ls" in self.uparam
@@ -3786,22 +3832,33 @@ class HttpCli(object):
if vn != dbv:
_, rd = vn.get_dbv(rd)
erd_efn = (rd, fn)
q = "select mt.k, mt.v from up inner join mt on mt.w = substr(up.w,1,16) where up.rd = ? and up.fn = ? and +mt.k != 'x'"
try:
r = icur.execute(q, (rd, fn))
r = icur.execute(q, erd_efn)
except Exception as ex:
if "database is locked" in str(ex):
break
try:
args = s3enc(idx.mem_cur, rd, fn)
r = icur.execute(q, args)
erd_efn = s3enc(idx.mem_cur, rd, fn)
r = icur.execute(q, erd_efn)
except:
t = "tag read error, {}/{}\n{}"
self.log(t.format(rd, fn, min_ex()))
break
fe["tags"] = {k: v for k, v in r}
if self.can_admin:
q = "select ip, at from up where rd=? and fn=?"
try:
zs1, zs2 = icur.execute(q, erd_efn).fetchone()
fe["tags"]["up_ip"] = zs1
fe["tags"][".up_at"] = zs2
except:
pass
_ = [tagset.add(k) for k in fe["tags"]]
if icur:

View File

@@ -13,6 +13,7 @@ import time
from queue import Queue
from .__init__ import ANYWIN, TYPE_CHECKING
from .authsrv import VFS
from .bos import bos
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
from .util import (
@@ -110,8 +111,6 @@ class ThumbSrv(object):
self.args = hub.args
self.log_func = hub.log
res = hub.args.th_size.split("x")
self.res = tuple([int(x) for x in res])
self.poke_cd = Cooldown(self.args.th_poke)
self.mutex = threading.Lock()
@@ -119,7 +118,7 @@ class ThumbSrv(object):
self.stopping = False
self.nthr = max(1, self.args.th_mt)
self.q: Queue[Optional[tuple[str, str]]] = Queue(self.nthr * 4)
self.q: Queue[Optional[tuple[str, str, VFS]]] = Queue(self.nthr * 4)
for n in range(self.nthr):
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
@@ -184,6 +183,10 @@ class ThumbSrv(object):
with self.mutex:
return not self.nthr
def getres(self, vn: VFS) -> tuple[int, int]:
w, h = vn.flags["thsize"].split("x")
return int(w), int(h)
def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:
histpath = self.asrv.vfs.histtab.get(ptop)
if not histpath:
@@ -211,7 +214,13 @@ class ThumbSrv(object):
do_conv = True
if do_conv:
self.q.put((abspath, tpath))
allvols = list(self.asrv.vfs.all_vols.values())
vn = next((x for x in allvols if x.realpath == ptop), None)
if not vn:
self.log("ptop [{}] not in {}".format(ptop, allvols), 3)
vn = self.asrv.vfs.all_aps[0][1]
self.q.put((abspath, tpath, vn))
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
while not self.stopping:
@@ -248,7 +257,7 @@ class ThumbSrv(object):
if not task:
break
abspath, tpath = task
abspath, tpath, vn = task
ext = abspath.split(".")[-1].lower()
png_ok = False
funs = []
@@ -281,7 +290,7 @@ class ThumbSrv(object):
for fun in funs:
try:
fun(abspath, ttpath)
fun(abspath, ttpath, vn)
break
except Exception as ex:
msg = "{} could not create thumbnail of {}\n{}"
@@ -315,9 +324,10 @@ class ThumbSrv(object):
with self.mutex:
self.nthr -= 1
def fancy_pillow(self, im: "Image.Image") -> "Image.Image":
def fancy_pillow(self, im: "Image.Image", vn: VFS) -> "Image.Image":
# exif_transpose is expensive (loads full image + unconditional copy)
r = max(*self.res) * 2
res = self.getres(vn)
r = max(*res) * 2
im.thumbnail((r, r), resample=Image.LANCZOS)
try:
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
@@ -331,23 +341,23 @@ class ThumbSrv(object):
if rot in rots:
im = im.transpose(rots[rot])
if self.args.th_no_crop:
im.thumbnail(self.res, resample=Image.LANCZOS)
if "nocrop" in vn.flags:
im.thumbnail(res, resample=Image.LANCZOS)
else:
iw, ih = im.size
dw, dh = self.res
dw, dh = res
res = (min(iw, dw), min(ih, dh))
im = ImageOps.fit(im, res, method=Image.LANCZOS)
return im
def conv_pil(self, abspath: str, tpath: str) -> None:
def conv_pil(self, abspath: str, tpath: str, vn: VFS) -> None:
with Image.open(fsenc(abspath)) as im:
try:
im = self.fancy_pillow(im)
im = self.fancy_pillow(im, vn)
except Exception as ex:
self.log("fancy_pillow {}".format(ex), "90")
im.thumbnail(self.res)
im.thumbnail(self.getres(vn))
fmts = ["RGB", "L"]
args = {"quality": 40}
@@ -370,12 +380,12 @@ class ThumbSrv(object):
im.save(tpath, **args)
def conv_vips(self, abspath: str, tpath: str) -> None:
def conv_vips(self, abspath: str, tpath: str, vn: VFS) -> None:
crops = ["centre", "none"]
if self.args.th_no_crop:
if "nocrop" in vn.flags:
crops = ["none"]
w, h = self.res
w, h = self.getres(vn)
kw = {"height": h, "size": "down", "intent": "relative"}
for c in crops:
@@ -389,8 +399,8 @@ class ThumbSrv(object):
img.write_to_file(tpath, Q=40)
def conv_ffmpeg(self, abspath: str, tpath: str) -> None:
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
def conv_ffmpeg(self, abspath: str, tpath: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if not ret:
return
@@ -402,12 +412,13 @@ class ThumbSrv(object):
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
scale = "scale={0}:{1}:force_original_aspect_ratio="
if self.args.th_no_crop:
if "nocrop" in vn.flags:
scale += "decrease,setsar=1:1"
else:
scale += "increase,crop={0}:{1},setsar=1:1"
bscale = scale.format(*list(self.res)).encode("utf-8")
res = self.getres(vn)
bscale = scale.format(*list(res)).encode("utf-8")
# fmt: off
cmd = [
b"ffmpeg",
@@ -439,11 +450,11 @@ class ThumbSrv(object):
]
cmd += [fsenc(tpath)]
self._run_ff(cmd)
self._run_ff(cmd, vn)
def _run_ff(self, cmd: list[bytes]) -> None:
def _run_ff(self, cmd: list[bytes], vn: VFS) -> None:
# self.log((b" ".join(cmd)).decode("utf-8"))
ret, _, serr = runcmd(cmd, timeout=self.args.th_convt)
ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"])
if not ret:
return
@@ -486,8 +497,8 @@ class ThumbSrv(object):
self.log(t + txt, c=c)
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def conv_waves(self, abspath: str, tpath: str) -> None:
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
def conv_waves(self, abspath: str, tpath: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret:
raise Exception("not audio")
@@ -512,10 +523,10 @@ class ThumbSrv(object):
# fmt: on
cmd += [fsenc(tpath)]
self._run_ff(cmd)
self._run_ff(cmd, vn)
def conv_spec(self, abspath: str, tpath: str) -> None:
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
def conv_spec(self, abspath: str, tpath: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret:
raise Exception("not audio")
@@ -555,13 +566,13 @@ class ThumbSrv(object):
]
cmd += [fsenc(tpath)]
self._run_ff(cmd)
self._run_ff(cmd, vn)
def conv_opus(self, abspath: str, tpath: str) -> None:
def conv_opus(self, abspath: str, tpath: str, vn: VFS) -> None:
if self.args.no_acode:
raise Exception("disabled in server config")
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2))
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret:
raise Exception("not audio")
@@ -597,7 +608,7 @@ class ThumbSrv(object):
fsenc(tmp_opus)
]
# fmt: on
self._run_ff(cmd)
self._run_ff(cmd, vn)
# iOS fails to play some "insufficiently complex" files
# (average file shorter than 8 seconds), so of course we
@@ -621,7 +632,7 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd)
self._run_ff(cmd, vn)
elif want_caf:
# simple remux should be safe
@@ -639,7 +650,7 @@ class ThumbSrv(object):
fsenc(tpath)
]
# fmt: on
self._run_ff(cmd)
self._run_ff(cmd, vn)
if tmp_opus != tpath:
try:

View File

@@ -24,6 +24,7 @@ from queue import Queue
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS
from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
from .bos import bos
from .cfg import vf_bmap, vf_vmap
from .fsutil import Fstab
from .mtag import MParser, MTag
from .util import (
@@ -199,7 +200,8 @@ class Up2k(object):
if self.stop:
# up-mt consistency not guaranteed if init is interrupted;
# drop caches for a full scan on next boot
self._drop_caches()
with self.mutex:
self._drop_caches()
if self.pp:
self.pp.end = True
@@ -593,7 +595,8 @@ class Up2k(object):
if self.args.re_dhash or [zv for zv in vols if "e2tsr" in zv.flags]:
self.args.re_dhash = False
self._drop_caches()
with self.mutex:
self._drop_caches()
for vol in vols:
if self.stop:
@@ -757,8 +760,9 @@ class Up2k(object):
ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}"
fx = set(("html_head",))
fdl = ("dbd", "lg_sbf", "md_sbf", "mte", "mth", "mtp", "nrand", "rand")
fd = {x: x for x in fdl}
fd = vf_bmap()
fd.update(vf_vmap())
fd = {v: k for k, v in fd.items()}
fl = {
k: v
for k, v in flags.items()
@@ -769,6 +773,9 @@ class Up2k(object):
for k, v in fl.items()
if k not in fx
]
if not a:
a = ["\033[90mall-default"]
if a:
vpath = "?"
for k, v in self.asrv.vfs.all_vols.items():
@@ -2993,7 +3000,8 @@ class Up2k(object):
permsets = [[False, True]]
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
vn, rem = vn.get_dbv(rem)
_, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem)
with self.mutex:
_, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem)
t = "you cannot delete this: "
if not dip:

View File

@@ -2427,7 +2427,7 @@ def killtree(root: int) -> None:
def runcmd(
argv: Union[list[bytes], list[str]], timeout: Optional[int] = None, **ka: Any
argv: Union[list[bytes], list[str]], timeout: Optional[float] = None, **ka: Any
) -> tuple[int, str, str]:
kill = ka.pop("kill", "t") # [t]ree [m]ain [n]one
capture = ka.pop("capture", 3) # 0=none 1=stdout 2=stderr 3=both
@@ -2480,7 +2480,7 @@ def chkcmd(argv: Union[list[bytes], list[str]], **ka: Any) -> tuple[str, str]:
return sout, serr
def mchkcmd(argv: Union[list[bytes], list[str]], timeout: int = 10) -> None:
def mchkcmd(argv: Union[list[bytes], list[str]], timeout: float = 10) -> None:
if PY2:
with open(os.devnull, "wb") as f:
rv = sp.call(argv, stdout=f, stderr=f)

View File

@@ -5780,14 +5780,18 @@ var treectl = (function () {
for (var b = 0; b < res.taglist.length; b++) {
var k = res.taglist[b],
v = (tn.tags || {})[k] || "";
v = (tn.tags || {})[k] || "",
sv = null;
if (k == ".dur") {
var sv = v ? s2ms(v) : "";
ln[ln.length - 1] += '</td><td sortv="' + v + '">' + sv;
if (k == ".dur")
sv = v ? s2ms(v) : "";
else if (k == ".up_at")
sv = v ? unix2iso(v) : "";
else {
ln.push(v);
continue;
}
ln.push(v);
ln[ln.length - 1] += '</td><td sortv="' + v + '">' + sv;
}
ln = ln.concat([tn.ext, unix2iso(tn.ts)]).join('</td><td>');
html.push(ln + '</td></tr>');
@@ -6066,7 +6070,7 @@ function apply_perms(res) {
var axs = [],
aclass = '>',
chk = ['read', 'write', 'move', 'delete', 'get'];
chk = ['read', 'write', 'move', 'delete', 'get', 'admin'];
for (var a = 0; a < chk.length; a++)
if (has(perms, chk[a]))

View File

@@ -1,3 +1,20 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0707-2220 `v1.8.1` in case of 404
## new features
* [handlers](https://github.com/9001/copyparty/tree/hovudstraum/bin/handlers); change the behavior of 404 / 403 with plugins
* makes it possible to use copyparty as a [caching proxy](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/caching-proxy.py)
* #42 add mpv + streamlink support to [very-bad-idea](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag#dangerous-plugins)
* add support for Pillow 10
* also improved text rendering in icons
* mention the [fedora package](https://github.com/9001/copyparty#fedora-package) in the readme
## bugfixes
* theme 6 (hacker) didn't show the state of some toggle-switches
* windows: keep quickedit enabled when hashing passwords interactively
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0626-0005 `v1.8.0` argon

View File

@@ -178,9 +178,9 @@ class TestVFS(unittest.TestCase):
self.assertEqual(n.realpath, os.path.join(td, "a"))
self.assertAxs(n.axs.uread, ["*"])
self.assertAxs(n.axs.uwrite, [])
perm_na = (False, False, False, False, False, False)
perm_rw = (True, True, False, False, False, False)
perm_ro = (True, False, False, False, False, False)
perm_na = (False, False, False, False, False, False, False)
perm_rw = (True, True, False, False, False, False, False)
perm_ro = (True, False, False, False, False, False, False)
self.assertEqual(vfs.can_access("/", "*"), perm_na)
self.assertEqual(vfs.can_access("/", "k"), perm_rw)
self.assertEqual(vfs.can_access("/a", "*"), perm_ro)

View File

@@ -98,7 +98,7 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None):
ka = {}
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_thumb no_vthumb no_zip nrand nw rand smb vc xdev xlink xvol"
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_thumb no_vthumb no_zip nrand nw rand smb th_no_crop vc xdev xlink xvol"
ka.update(**{k: False for k in ex.split()})
ex = "dotpart no_rescan no_sendfile no_voldump plain_ip"
@@ -107,7 +107,7 @@ class Cfg(Namespace):
ex = "css_browser hist js_browser no_forget no_hash no_idx"
ka.update(**{k: None for k in ex.split()})
ex = "s_thead s_tbody"
ex = "s_thead s_tbody th_convt"
ka.update(**{k: 9 for k in ex.split()})
ex = "df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp theme themes turbo"
@@ -116,7 +116,7 @@ class Cfg(Namespace):
ex = "ah_alg doctitle favico html_head lg_sbf log_fk md_sbf mth textfiles unlist R RS SR"
ka.update(**{k: "" for k in ex.split()})
ex = "on403 on404 xad xar xau xbd xbr xbu xiu xm"
ex = "on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()})
super(Cfg, self).__init__(
@@ -126,6 +126,7 @@ class Cfg(Namespace):
E=E,
dbd="wal",
s_wr_sz=512 * 1024,
th_size="320x256",
unpost=600,
u2sort="s",
mtp=[],