Compare commits

...
35 Commits
Author SHA1 Message Date
ed 7ccebb6948 v1.20.26 2026-10-14 23:21:55 +00:00
ed ed0fcd608f th: fix .raw if not pil jxl 2026-10-14 21:55:12 +00:00
ed 9811cd3101 thx faelnor 2026-10-14 21:54:58 +00:00
ed fa8913c712 dumb 2026-10-14 21:53:36 +00:00
ed 4bd64d7456 shadowing: less false-positives 2026-10-14 21:51:52 +00:00
ed 7ce483e970 xvol: ls filter 2026-10-14 21:50:40 +00:00
ed 82eab3d2f8 xvol/xdev (GHSA-hg99-jqfp-rf76) 2026-10-14 21:49:04 +00:00
ed 0dc1cb3291 docker: drop openjpeg 2026-10-09 00:26:10 +00:00
ed 1a87dc3218 update pkgs to 1.20.25 2026-10-06 00:44:54 +00:00
ed dc3adc0f4d v1.20.25 2026-10-06 00:35:29 +00:00
ed 5f00c1631c return to ?b=u; closes #1659 2026-10-06 00:33:31 +00:00
ed e59fc05e73 add --sftp-chattr 2026-10-05 23:47:51 +00:00
ed e8de421174 shadowing + casefolding (GHSA-3c88-p299-7hvr) 2026-10-05 23:46:39 +00:00
ed 36aaa24974 file-rename instant preview on open 2026-10-05 23:02:30 +00:00
ed 5e0a88e355 ban-pw basic-auth; closes #1660 2026-10-05 22:36:30 +00:00
ed 4b283b4bfd bbox: fix #1650 (el null) 2026-10-05 22:32:49 +00:00
ed 64f574fe94 u2c: allow \ in filenames 2026-10-05 22:26:18 +00:00
Michael Rosenberg a31870cbd3 dk/fk constant-time (#1658) 2026-10-05 22:01:30 +00:00
ed 74b17374c2 DELETE: say 404 on enoent; closes #1656 2026-10-03 22:10:17 +00:00
ed 670e86228c tests: support new coreutils or whatever 2026-10-03 22:08:10 +00:00
ed 1341b3421f --smb now requres --hack-me-bro 2026-10-03 22:01:49 +00:00
ed 5961228c93 deadcode 2026-10-03 21:44:14 +00:00
ed 28f9f6f002 allow \ in filenames on linux/macos;
now possible to upload 09. t+pazolite - I still think about y%#'()_*{\\"'&36)%("'$&''&(15y!!!_??*`~.flac
2026-10-03 21:43:47 +00:00
ed 186cbf939f sftp: add fastloader;
startup on an 800mhz armv5 exceeded the 30sec timeout
2026-10-02 03:29:12 +00:00
ed 080935788b redup: skip conclusive hard/ref; closes #1652 2026-09-30 00:05:02 +00:00
ed c6918c2ce6 tests: macos 2026-09-30 00:15:08 +02:00
ed ab2299e3db add acao-re; closes #1498 2026-09-27 19:12:01 +00:00
edandTil Schmitter 334dccc55e --ih: fix nav to webroot
Co-authored-by: Til Schmitter <til.schmitter@gmail.com>
2026-09-27 18:23:08 +00:00
ed 8d7aa4f860 cosmetic 2026-09-27 18:20:47 +00:00
ed f73b37c6fa pipe-dl progress on /?h 2026-09-27 18:11:29 +00:00
ed 731dbbff84 u2c: fix exfat's http502 allergy;
addendum to 23e00c0a04
2026-09-26 23:43:26 +00:00
ed 23e00c0a04 up2k-js: fix exfat's http502 allergy;
normally, to avoid excessive handshaking, unexpected server errors are
handled by continuing onto the next chunk and dealing with it at eof

this doesn't work on filesystems that dont support sparse files, so
discard the chunk-queue and re-handshake to keep it sequential
2026-09-26 22:12:15 +00:00
ed 107dec9ed1 ifaddr: winxp fix 2026-09-24 19:31:21 +00:00
willie6666 f445bfa862 shares: delete sf entries too (#1647)
Signed-off-by: willie6666 <89621369+willie6666@users.noreply.github.com>
2026-09-20 13:12:56 +00:00
ed aff083fb28 update pkgs to 1.20.24 2026-09-19 21:39:09 +00:00
31 changed files with 655 additions and 216 deletions

No files matched your search

+11 -4
View File
@@ -572,7 +572,7 @@ if you want to grant access to all users who are logged in, the group `acct` wil
* to do the opposite, granting access to everyone who is NOT logged in. `*,-@acct` does the trick, for example `-v /srv/welcome:welcome:r,*,-@acct`
* single users can also be subtracted from a group: `@admins,-james`
anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour
anyone trying to bruteforce a password gets banned according to `--ban-pw` (assuming [real-ip](#real-ip) is configured correctly); default is 24h ban for 9 failed attempts in 1 hour
and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf`
@@ -622,6 +622,8 @@ config file example:
hiding specific subfolders by mounting another volume on top of them
> **WARNING:** shadowing is NOT bulletproof when the underlying filesystem is casefolding (case-insensitive), in other words DON'T rely on this when copyparty is running on Windows and/or MacOS. This is also unsafe when combined with certain filesystems on Linux, especially FAT32/exFAT. Pay attention to the volume-listing in the startup log; `casechk:n` indicates probably-OK, but if it says `casechk:y` then DO NOT trust shadowing to fully prevent access to a file/subfolder.
for example `-v /mnt::r -v /var/empty:web/certs:` (note: no permissions) mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
to fully unmap it from the filesystem, specify `//NULL` instead of a real path such as `/var/empty`, so for example `-v /mnt::r -v //NULL:web/certs:` ensures `/web/certs` will never be accessible by anyone
@@ -1803,12 +1805,17 @@ avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, ski
and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere
* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access, keeping permissions from outer/initial volume
* symlinks are permitted with `xvol` if they point into another volume where the user also has some sort of access
* usually keeps permissions from outer/initial volume, but don't take that for granted
these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar
these options will reduce performance; unlikely worst-case estimates are 27% reduction for directory listings, 35% for download-as-tar
* if the underlying storage is [a network disk](#using-the-cloud-as-storage) then xvol/xdev will be **very** slow (not to mention pointless/useless)
as of copyparty v1.7.0 these options also prevent file access at runtime -- in previous versions it was just hints for the indexer
> **WARNING:** the `xvol` and `xdev` options should NOT be relied on for security; these are originally and primarily intended for the [e2ds indexer](#file-indexing). Certain underlying filesystems (especially FUSE-based and casefolding ones) will be problematic, and symlinks inherently have various TOCTOUs, and there's probably also bugs in copyparty where xvol/xdev is not fully enforced for certain APIs. The developer does not care about these options and they do not receive much testing.
### periodic rescan
filesystem monitoring; if copyparty is not the only software doing stuff on your filesystem, you may want to enable periodic rescans to keep the index up to date
@@ -3047,7 +3054,7 @@ below are some tweaks roughly ordered by usefulness:
* and pypy can sometimes crash on startup with `-j0` (TODO make issue)
* if you are running the copyparty server **on Windows or Macos:**
* `--casechk=n` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
* `--casechk=n` makes it much faster, but [reduces security](https://github.com/9001/copyparty/security/advisories/GHSA-3c88-p299-7hvr) and awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
* this is the same as `casechk: n` in a config-file
+24 -12
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from __future__ import division, print_function, unicode_literals
S_VERSION = "2.23"
S_BUILD_DT = "2026-09-06"
S_VERSION = "2.25"
S_BUILD_DT = "2026-10-05"
"""
u2c.py: upload to copyparty
@@ -67,6 +67,8 @@ else:
WTF8 = "replace" if PY2 else "surrogateescape"
SEP = "{0}".format(os.sep).encode("ascii")
VT100 = platform.system() != "Windows"
@@ -249,7 +251,7 @@ class File(object):
def __init__(self, top, rel, size, lmod):
self.top = top # type: bytes
self.rel = rel.replace(b"\\", b"/") # type: bytes
self.rel = rel.replace(SEP, b"/") # type: bytes
self.size = size # type: int
self.lmod = lmod # type: float
@@ -267,6 +269,7 @@ class File(object):
self.wark = "" # type: str
self.url = "" # type: str
self.nhs = 0 # type: int
self.t_hs = 0.0 # type: float
# set by upload
self.t0_up = 0.0 # type: float
@@ -652,7 +655,7 @@ def walkdir(err, top, excl, seen):
def walkdirs(err, tops, excl):
"""recursive statdir for a list of tops, yields [top, relpath, stat]"""
sep = "{0}".format(os.sep).encode("ascii")
sep = SEP
if not VT100:
excl = excl.replace("/", r"\\")
za = []
@@ -904,6 +907,7 @@ def handshake(ar, file, search):
file.url = quotep(r["purl"].encode("utf-8", WTF8)).decode("utf-8")
file.name = r["name"]
file.wark = r["wark"]
file.t_hs = time.time()
if ar.uon and not r["hash"]:
printlink(ar, file.url, r["name"], r.get("fk"))
@@ -1027,7 +1031,7 @@ class Ctl(object):
self.up_b = 0 # num bytes handled
self.up_br = 0 # num bytes actually transferred
self.uploader_busy = 0
self.serialized = False
self.oneconn = False
self.t0 = time.time()
self.t0_up = None
@@ -1213,14 +1217,15 @@ class Ctl(object):
def hasher(self):
ptn = re.compile(self.ar.x.encode("utf-8"), re.I) if self.ar.x else None
sep = "{0}".format(os.sep).encode("ascii")
usep = os.sep
sep = SEP
prd = None
ls = {}
for top, rel, inf in self.filegen:
isdir = stat.S_ISDIR(inf.st_mode)
if self.ar.z or self.ar.drd:
rd = rel if isdir else os.path.dirname(rel)
srd = rd.decode("utf-8", "replace").replace("\\", "/").rstrip("/")
srd = rd.decode("utf-8", "replace").replace(usep, "/").rstrip("/")
if srd:
srd += "/"
if prd != rd:
@@ -1230,7 +1235,7 @@ class Ctl(object):
print(" ls ~{0}".format(srd))
zt = (
self.ar.vtop,
quotep(rd.replace(b"\\", b"/")).decode("utf-8"),
quotep(rd.replace(sep, b"/")).decode("utf-8"),
)
sc, txt = web.req("GET", "%s%s?ls&lt&dots" % zt, {})
if sc >= 400:
@@ -1403,10 +1408,10 @@ class Ctl(object):
self.recheck.append(file)
with self.mutex:
if hs and not sprs and not self.serialized:
t = "server filesystem does not support sparse files; serializing uploads\n"
if hs and not sprs and not self.oneconn:
t = "server filesystem does not support sparse files; cannot parallelize uploads\n"
eprint(t)
self.serialized = True
self.oneconn = True
for _ in range(self.ar.j - 1):
self.q_upload.put(None)
if not hs:
@@ -1418,7 +1423,7 @@ class Ctl(object):
if not file.recheck:
self.up_done(file)
if hs and file.up_c:
if hs and file.up_c and not self.oneconn:
# some chunks failed
self.up_c -= len(hs)
file.up_c -= len(hs)
@@ -1476,6 +1481,7 @@ class Ctl(object):
self.q_upload.put(fsl)
def uploader(self):
drop_hs = (0.0, "")
while True:
fsl = self.q_upload.get()
if not fsl:
@@ -1494,6 +1500,9 @@ class Ctl(object):
cids = fsl.cids
self.last_file = file
if self.oneconn and drop_hs == (file.t_hs, file.wark):
continue
with self.mutex:
if not self.uploader_busy:
self.at_upr = time.time()
@@ -1519,6 +1528,9 @@ class Ctl(object):
t = "upload failed, retrying: %s #%s+%d (%s)\n"
eprint(t % (file.name, cids[0][:8], len(cids) - 1, ex))
file.cd = time.time() + self.ar.cd
if self.oneconn:
file.ucids = []
drop_hs = (file.t_hs, file.wark)
# handshake will fix it
with self.mutex:
+2 -2
View File
@@ -3,7 +3,7 @@
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
pkgname=copyparty
pkgver="1.20.23"
pkgver="1.20.25"
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -26,7 +26,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/copyparty.conf")
install=copyparty.install
sha256sums=("c0957ce077b0494292541116b9473fdbda1e6597b5d9158b2fc470db0a1834dd")
sha256sums=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"
+2 -2
View File
@@ -2,7 +2,7 @@
pkgname=copyparty
pkgver=1.20.23
pkgver=1.20.25
pkgrel=1
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++"
arch=("any")
@@ -21,7 +21,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=("c0957ce077b0494292541116b9473fdbda1e6597b5d9158b2fc470db0a1834dd")
sha256sums=("82c68dfa42e08a59757bbfa0b1d140032141175347906e056bef83904f5c2c70")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"
+3 -3
View File
@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.20.23/copyparty-1.20.23.tar.gz",
"version": "1.20.23",
"hash": "sha256-wJV84HewSUKSVBEWuUc/29oeZZe12RWLL8Rw2woYNN0="
"url": "https://github.com/9001/copyparty/releases/download/v1.20.25/copyparty-1.20.25.tar.gz",
"version": "1.20.25",
"hash": "sha256-gsaN+kLgill1e7+gsdFAAyFBF1NHkG4Fa++DkE9cLHA="
}
+4
View File
@@ -1699,6 +1699,8 @@ def add_sftp(ap):
ap2.add_argument("--sftp-pw", action="store_true", help="allow password-authentication with sftp (not just ssh-keys)")
ap2.add_argument("--sftp-anon", metavar="TXT", type=u, default="", help="allow anonymous/unauthenticated connections with \033[33mTXT\033[0m as username")
ap2.add_argument("--sftp-hostk", metavar="FP", type=u, default=E.cfg, help="path to folder with hostkeys, for example 'ssh_host_rsa_key'; missing keys will be generated")
ap2.add_argument("--sftp-fastldr", action="store_true", help="speedhack for 'loading hostkeys' on old CPUs; only use with known-good hostkeys")
ap2.add_argument("--sftp-chattr", metavar="T", default="s", help="which attributes can be modified with chattr? [\033[32ms\033[0m]=size, [\033[32mt\033[0m]=mtime, [\033[32mp\033[0m]=permissions, [\033[32mu\033[0m]=uid/gid. Example: [\033[32mstpu\033[0m]")
ap2.add_argument("--sftp-banner", metavar="T", type=u, default="", help="bannertext to send when someone connects; can be @filepath")
ap2.add_argument("--sftp-ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m / \033[33m--ipar\033[0m. Examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
ap2.add_argument("--sftp-hs-t", metavar="SEC", type=int, default=15, help="connection handshake timeout in seconds")
@@ -1818,6 +1820,7 @@ def add_stats(ap):
def add_yolo(ap):
ap2 = ap.add_argument_group("yolo options")
ap2.add_argument("--hack-me-bro", action="store_true", help="allow enabling dangerously buggy protocols such as \033[33m--smb\033[0m / \033[33m--tftp\033[0m")
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests; \033[1;31mDANGEROUS\033[0m / LAN-only")
ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
ap2.add_argument("--allow-svg-js", action="store_true", help="allow svg images to execute javascript; default-disabled because ~nobody wants it (volflag=allow_svg_js)")
@@ -1917,6 +1920,7 @@ def add_safety(ap):
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
ap2.add_argument("--acao-re", metavar="PTN", type=u, default="", help="Access-Control-Allow-Origin; supplement to \033[33m--acao\033[0m; allow request if http-header 'Origin' matches either \033[33m--acao\033[0m or regex-\033[33mPTN\033[0m; ominous example: [\033[32m^moz-extension://.*\033[0m]")
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like \033[33m--acao\033[0m's description)")
if FULL_HELP or ANYWIN:
ap2.add_argument("--unsafe-tools", action="store_true", help="windows-only: allow running programs (ffmpeg, dcraw_emu, ...) when their location is inside a folder that can be uploaded to (dangerous due to DLL-hijacking)")
+2 -2
View File
@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 20, 24)
VERSION = (1, 20, 26)
CODENAME = "sftp is fine too"
BUILD_DT = (2026, 9, 19)
BUILD_DT = (2026, 10, 14)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
+156 -5
View File
@@ -42,6 +42,7 @@ from .util import (
odfusion,
read_utf8,
relchk,
sanitize_fn,
statdir,
ub64enc,
uncyg,
@@ -426,6 +427,7 @@ class VFS(object):
self.root = self
self.dev = 0 # st_dev
self.nodes: dict[str, VFS] = {} # child nodes
self.lnodes: Optional[dict[bytes, VFS]] = None # lowercase
self.histtab: dict[str, str] = {} # all realpath->histpath
self.dbpaths: dict[str, str] = {} # all realpath->dbpath
self.dbv: Optional[VFS] = None # closest full/non-jump parent
@@ -468,6 +470,7 @@ class VFS(object):
self.canonical = self._canonical_null
self.dcanonical = self._dcanonical_null
self.get = self._get
self.get_dbv = self._get_dbv
self.ls = self._ls
@@ -575,6 +578,11 @@ class VFS(object):
if name in self.nodes:
return self.nodes[name]._find(rem)
if self.lnodes:
zs = name.casefold().encode("ascii", "ignore")
if zs in self.lnodes:
return self.lnodes[zs]._find(rem)
return self, vpath
def can_access(
@@ -596,7 +604,7 @@ class VFS(object):
ret += "A"
return ret
def get(
def _get(
self,
vpath: str,
uname: str,
@@ -638,6 +646,42 @@ class VFS(object):
return vn, rem
def _get_xvd(
self,
vpath: str,
uname: str,
will_read: bool,
will_write: bool,
will_move: bool = False,
will_del: bool = False,
will_get: bool = False,
err: int = 403,
) -> tuple["VFS", str]:
zvr = self._get(
vpath, uname, will_read, will_write, will_move, will_del, will_get, err
)
if not zvr:
return None
vn, rem = zvr
if "xvd" in vn.flags and vn.realpath:
avn = vn.chk_ap(vn.canonical(rem))
if avn and avn.vpath == vn.vpath:
return vn, rem
if (
not avn
or (will_read and uname not in avn.axs.uread)
or (will_write and uname not in avn.axs.uwrite)
or (will_move and uname not in avn.axs.umove)
or (will_del and uname not in avn.axs.udel)
or (will_get and uname not in avn.axs.uget)
):
if not err:
return None
raise Pebkac(
err, "permission denied for [/%s]" % (vjoin(vn.vpath, rem))
)
return vn, rem
def _get_share_src(self, vrem: str) -> tuple["VFS", str]:
src = self.shr_src
if not src:
@@ -673,8 +717,6 @@ class VFS(object):
if lfn == zs.lower():
hit = zs
break
if not hit:
return True # NFC/NFD or something, can't be helped either way
if self.log:
t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r"
self.log("vfs", t % (fn, hit))
@@ -765,6 +807,32 @@ class VFS(object):
real = [x for x in real if os.path.basename(x[0]) in self.shr_files]
return abspath, real, {}
def _ls_xvd(
self,
rem: str,
uname: str,
scandir: bool,
permsets: list[list[bool]],
lstat: bool = False,
throw: bool = False,
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
dirpath, real0, virt_vis = self._ls(rem, uname, scandir, permsets, True, throw)
lns = [x for x in real0 if stat.S_ISLNK(x[1].st_mode)]
if not lns:
return dirpath, real0, virt_vis
real = [x for x in real0 if not stat.S_ISLNK(x[1].st_mode)]
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
for fn, _ in lns:
ap = absreal(os.path.join(dirpath, fn))
vn = self.chk_ap(ap)
try:
if vn.get("", uname, *ps):
real.append((fn, bos.stat(ap)))
except:
pass
real.sort()
return dirpath, real, virt_vis
def _ls(
self,
rem: str,
@@ -786,6 +854,12 @@ class VFS(object):
if not rem:
# no vfs nodes in the list of real inodes
real = [x for x in real if x[0] not in self.nodes]
if self.lnodes:
real = [
x
for x in real
if x[0].casefold().encode("ascii", "ignore") not in self.lnodes
]
dbv = self.dbv or self
for name, vn2 in sorted(self.nodes.items()):
@@ -859,12 +933,16 @@ class VFS(object):
self.log("vfs.walk", t % (seen[-1], fsroot, self.vpath, rem), 3)
return
if "xdev" in self.flags or "xvol" in self.flags:
if "xvd" in self.flags:
ps = (permsets[0] + [False] * 5)[:5] # rwmdg
rm1 = []
for le in vfs_ls:
ap = absreal(os.path.join(fsroot, le[0]))
vn2 = self.chk_ap(ap)
if not vn2 or not vn2.get("", uname, True, False, err=0):
try:
if not vn2.get("", uname, *ps):
raise Exception()
except:
rm1.append(le)
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore
@@ -951,6 +1029,22 @@ class VFS(object):
for d in [{"vp": v, "ap": a, "st": n} for v, a, n in ret2]:
yield d
def sanitize_fn(self, fn: str, rem: Any = None) -> str:
fn = sanitize_fn(fn, self.flags["fsnt"])
if rem:
return fn
if fn in self.nodes or (
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
):
raise Pebkac(400, "bad filename")
return fn
def chk_fn(self, fn: str) -> None:
if fn in self.nodes or (
self.lnodes and fn.casefold().encode("ascii", "ignore") in self.lnodes
):
raise Pebkac(400, "bad filename")
def chk_ap(self, ap: str, st: Optional[os.stat_result] = None) -> Optional["VFS"]:
aps = ap + os.sep
if "xdev" in self.flags and not ANYWIN:
@@ -2952,6 +3046,14 @@ class AuthSrv(object):
excl.extend([r"^\.", r"/\."])
vol.flags["srch_re_nodot"] = re.compile("|".join(excl))
for vol in list(vfs.all_nodes.values()):
if "xvd" in vol.flags:
for vol in vfs.all_nodes.values():
vol.get = vol._get_xvd
if vol.ls.__name__ == "_ls":
vol.ls = vol._ls_xvd
break
have_daw = False
for vol in vfs.all_nodes.values():
daw = vol.flags.get("daw") or self.args.daw
@@ -2986,6 +3088,22 @@ class AuthSrv(object):
for name in vol.axs.uread:
vol.axs.udot.add(name)
zi = 1
zs = "; you must also enable --hack-me-bro to accept the total loss of security"
if self.args.hack_me_bro:
zi = 3
zs = ""
if self.args.smb:
t = "smb is enabled; this has serious security issues which will not be fixed"
self.log(t + zs, zi)
if zs:
errors = True
if self.args.tftp:
t = "tftp is enabled; primitive protocol with primitive access-restrictions"
self.log(t, 3)
if errors:
sys.exit(1)
@@ -3290,6 +3408,39 @@ class AuthSrv(object):
cur.close()
db.close()
zb1 = zb2 = False
nodes = [self.vfs]
while nodes:
vn = nodes.pop()
nodes += list(vn.nodes.values())
if (
not vn.realpath
or "bcasechk" not in vn.flags
or (not vn.axs.uread and not vn.axs.uwrite and not vn.axs.uget)
):
continue
if ".hist" in vn.nodes and "e2d" in vn.flags:
zb1 = True
for zs, vnn in vn.nodes.items():
if zs != ".hist" and (vnn.realpath or "").startswith(vn.realpath):
t = "nested volume on casefolding filesystem: [/%s] inside [/%s]"
self.log(t % (vnn.vpath, vn.vpath), 3)
zb2 = True
if PY2:
continue
vn.lnodes = {
k.casefold().encode("ascii", "ignore"): v for k, v in vn.nodes.items()
}
if zb2 and PY2:
t = "SECURITY WARNING: due to python2, shadowing will NOT provide much security:"
self.log(t, 1)
if zb2:
t = "SECURITY WARNING: you have a case-insensitive / casefolding filesystem, and you also have nested volumes (subvolumes). This combination is NOT 100% bulletproof; it *might* be possible for someone to sneakily access a subvolume they're not supposed to have access to, if they have access to the parent volume."
self.log(t, 3)
if zb1:
t = "you have a case-insensitive / casefolding filesystem, so it is recommended to set --hist C:\\copyparty-data\\ or another safe place"
self.log(t, 3)
self.js_ls = {}
self.js_htm = ""
self.md_htm = ""
+20 -1
View File
@@ -10,7 +10,7 @@ import time
from .__init__ import ANYWIN, FREEBSD, MACOS, UNIX
from .authsrv import AXS, VFS, AuthSrv
from .bos import bos
from .util import chkcmd, json_hesc, min_ex, undot
from .util import chkcmd, fsenc, json_hesc, min_ex, runcmd, undot
if True: # pylint: disable=using-constant-test
from typing import Optional, Union
@@ -18,6 +18,11 @@ if True: # pylint: disable=using-constant-test
from .util import RootLogger, undot
PTN_FILEFRAG = re.compile(
r"^\s*[0-9]+:\s*[0-9]+\.\.\s*[0-9]+:\s*([0-9]+)\.\.\s*([0-9]+):\s*[0-9]+:\s*(.*)$"
)
class Fstab(object):
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
self.log_func = log
@@ -264,3 +269,17 @@ def ramdisk_chk(asrv: AuthSrv) -> None:
vol.flags["fsnt"] = vol.js_ls["fsnt"] = htm["fsnt"] = fs
vol.js_htm = json_hesc(json.dumps(htm))
# md_htm is dontcare; only relevant for up2k request body
def filefrag(ap: str) -> list[str]:
rc, so, se = runcmd([b"filefrag", b"-v", fsenc(ap)], timeout=10)
ptn = PTN_FILEFRAG
ret = []
for ln in so.split("\n"):
m = ptn.match(ln)
if not m:
continue
a, b, s = m.groups()
if "shared" in s.strip().split(","):
ret.append((a, b))
return ret
+22 -23
View File
@@ -15,7 +15,7 @@ from pyftpdlib.handlers import FTPHandler
from pyftpdlib.ioloop import IOLoop
from pyftpdlib.servers import FTPServer
from .__init__ import PY2, TYPE_CHECKING
from .__init__ import ANYWIN, PY2, TYPE_CHECKING
from .authsrv import VFS
from .bos import bos
from .util import (
@@ -29,7 +29,6 @@ from .util import (
ipnorm,
pybin,
read_utf8,
relchk,
runhook,
sanitize_fn,
set_fperms,
@@ -169,16 +168,23 @@ class FtpFs(AbstractedFS):
d: bool = False,
) -> tuple[str, VFS, str]:
try:
vpath = vpath.replace("\\", "/").strip("/")
rd, fn = os.path.split(vpath)
if relchk(rd):
logging.warning("malicious vpath: %s", vpath)
t = "Unsupported characters in [{}]"
raise FSE(t.format(vpath), 1)
# because:
# * ftp-spec doesn't define sep
# * windows filename limitations
# need to allow/translate illegal fn,
# but require filesystem-correct rd
if ANYWIN or ("\\" in vpath and "/" not in vpath):
vpath = vpath.replace("\\", "/")
vpath = vpath.strip("/\\")
if ANYWIN:
rd, fn = os.path.split(vpath)
fn = sanitize_fn(fn, "win")
vpath = vjoin(rd, fn)
# end of windows jank
fn = sanitize_fn(fn or "")
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
vpath = vjoin(vfs.vpath, rem)
rd, fn = os.path.split(vpath)
if (
w
and fn.lower() in vfs.flags["emb_all"]
@@ -193,18 +199,7 @@ class FtpFs(AbstractedFS):
t = "No filesystem mounted at [{}]"
raise FSE(t.format(vpath))
if "xdev" in vfs.flags or "xvol" in vfs.flags:
ap = vfs.canonical(rem)
avfs = vfs.chk_ap(ap)
t = "Permission denied in [{}]"
if not avfs:
raise FSE(t.format(vpath), 1)
cr, cw, cm, cd, _, _, _, _, _ = avfs.uaxs[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)
else:
ap = vfs.canonical(rem, False)
ap = vfs.canonical(rem, False)
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise FSE("No such file or directory", 1)
@@ -314,7 +309,11 @@ class FtpFs(AbstractedFS):
def chdir(self, path: str) -> None:
nwd = join(self.cwd, path)
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
try:
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
except Exception as ex:
raise FSE(str(ex))
if not vfs.realpath:
self.cwd = nwd
return
+48 -28
View File
@@ -17,6 +17,7 @@ import threading # typechk
import time
import uuid
from datetime import datetime
from hmac import compare_digest as ct_eq
from operator import itemgetter
import jinja2 # typechk
@@ -104,13 +105,11 @@ from .util import (
read_socket_chunked,
read_socket_unbounded,
read_utf8,
relchk,
ren_open,
runhook,
s2hms,
s3enc,
safe_mime,
sanitize_fn,
sanitize_vpath,
sendfile_kern,
sendfile_py,
@@ -738,6 +737,7 @@ class HttpCli(object):
hpw = self.asrv.ah.hash(bauth)
if self.asrv.iacct.get(hpw):
break
bauth = zs
except:
pass
@@ -885,10 +885,9 @@ class HttpCli(object):
uparam["b"] = ""
cookies["b"] = ""
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
if vn.realpath and ("xdev" in vn.flags or "xvol" in vn.flags):
ap = vn.canonical(rem)
avn = vn.chk_ap(ap)
vn, rem = self.asrv.vfs._get(self.vpath, self.uname, False, False)
if vn.realpath and "xvd" in vn.flags:
avn = vn.chk_ap(vn.canonical(rem))
else:
avn = vn
@@ -1378,6 +1377,7 @@ class HttpCli(object):
if (
self.args.pw_hdr in ih
or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins
or (self.args.acao_re and self.args.acao_re.match(origin))
):
good_origin = True
bad_hdrs = ("",)
@@ -2474,6 +2474,9 @@ class HttpCli(object):
if "%" in self.req:
self.log(" `-- %r" % (self.vpath,))
if not self.avn:
raise Pebkac(403, "no access")
if self.headers.get("expect", "").lower() == "100-continue":
try:
self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
@@ -2734,7 +2737,7 @@ class HttpCli(object):
if rnd:
fn = rand_name(fdir, fn, rnd)
fn = sanitize_fn(fn or "")
fn = vfs.sanitize_fn(fn or "", rem)
path = os.path.join(fdir, fn)
@@ -3214,19 +3217,20 @@ class HttpCli(object):
if "delete" in self.uparam:
return self.handle_rm(body)
name = undot(body["name"])
if "/" in name:
raise Pebkac(400, "your client is old; press CTRL-SHIFT-R and try again")
name = body["name"]
if "/" in name or name in (".", ".."):
raise Pebkac(400, "filename contains '/' or is invalid")
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
fsnt = vfs.flags["fsnt"]
if fsnt != "lin":
vpath = self.vpath
fsnt = self.vn.flags["fsnt"]
if not VPTL_OS and fsnt != "lin":
tl = VPTL_WIN if fsnt == "win" else VPTL_MAC
rem = rem.translate(tl)
name = name.translate(tl)
vpath = vpath.translate(tl)
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
name = vfs.sanitize_fn(name, rem)
dbv, vrem = vfs.get_dbv(rem)
name = sanitize_fn(name)
if (
not self.can_read
and self.can_write
@@ -3591,7 +3595,6 @@ class HttpCli(object):
return self.tx_404()
vfs, rem = self.asrv.vfs.get(session["vp"], self.uname, False, True)
vpath = vjoin(vfs.vpath, rem)
ap = vfs.canonical(rem)
st = bos.stat(ap)
@@ -3774,12 +3777,11 @@ class HttpCli(object):
def _mkdir(self, vpath: str, dav: bool = False) -> bool:
nullwrite = self.args.nw
self.gctx = vpath
vpath = sanitize_vpath(undot(vpath))
vpath = sanitize_vpath(undot(vpath), self.vn.flags["fsnt"])
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
if "nosub" in vfs.flags:
raise Pebkac(403, "mkdir is forbidden below this folder")
rem = sanitize_vpath(rem)
fn = vfs.canonical(rem)
if not nullwrite:
@@ -3826,10 +3828,12 @@ class HttpCli(object):
t = "you can only create %s files because you don't have the delete-permission"
raise Pebkac(400, t % (vfs.flags["rw_edit"].replace(",", "/")))
sanitized = sanitize_fn(new_file)
sanitized = vfs.sanitize_fn(new_file, rem)
fdir = vfs.canonical(rem)
fn = os.path.join(fdir, sanitized)
self.asrv.vfs.get(vjoins(vfs.vpath, rem, sanitized), self.uname, False, True)
for hn in ("xbu", "xau"):
xxu = vfs.flags.get(hn)
if xxu:
@@ -3981,12 +3985,19 @@ class HttpCli(object):
# fallthrough
fdir = fdir_base
fname = sanitize_fn(p_file or "")
fname = vfs.sanitize_fn(p_file or "", rem)
suffix = "-%.6f-%s" % (time.time(), dip)
if p_file and not nullwrite:
if rnd:
fname = rand_name(fdir, fname, rnd)
self.can_delete = (
self.uname
in self.asrv.vfs.get(
vjoin(upload_vpath, fname), self.uname, False, True
)[0].axs.udel
)
abspath = os.path.join(fdir, fname)
open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}
@@ -4308,6 +4319,7 @@ class HttpCli(object):
else:
self.redirect(
self.vpath,
suf="?b=u" if self.ouparam.get("b") == "u" else "",
msg=msg + suf,
flavor="return to",
click=False,
@@ -4343,7 +4355,7 @@ class HttpCli(object):
bos.makedirs(fp, vf=vfs.flags)
fp = os.path.join(fp, fn)
rem = "{}/{}".format(rp, fn).strip("/")
rem = vjoin(rp, fn)
dbv, vrem = vfs.get_dbv(rem)
if not self.can_delete and (
@@ -4361,6 +4373,8 @@ class HttpCli(object):
self.reply(response.encode("utf-8"))
return True
self.asrv.vfs.get(vjoin(vfs.vpath, rem), self.uname, True, True)
srv_lastmod = -1.0
srv_lastmod3 = -1
try:
@@ -4586,7 +4600,7 @@ class HttpCli(object):
ap = vn.canonical(self.rem)
zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_len]
if req == zs:
if ct_eq(req, zs):
return True
t = "wrong dirkey, want %s, got %s\n vp: %r\n ap: %r"
@@ -4614,7 +4628,7 @@ class HttpCli(object):
alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
)[:fk_len]
if req == zs:
if ct_eq(req, zs):
return True
t = "wrong filekey, want %s, got %s\n vp: %r\n ap: %r"
@@ -5267,6 +5281,8 @@ class HttpCli(object):
self.send_headers("oh_f", length=upper - lower, status=status, mime=mime)
wr_slp = self.args.s_wr_slp
wr_sz = self.args.s_wr_sz
dls = self.conn.hsrv.dls
dl_id = self.dl_id
file_size = job["size"]
chunk_size = up2k_chunksize(file_size)
num_need = -1
@@ -5286,7 +5302,7 @@ class HttpCli(object):
if job:
self.pipes.set(req_path, job)
if not job:
if not job or not job["hash"]:
t = "pipe: OK, upload has finished; yeeting remainder"
self.log(t, 2)
data_end = file_size
@@ -5383,6 +5399,9 @@ class HttpCli(object):
broken = True
break
if dl_id:
dls[dl_id] = (time.time(), lower)
if lower < upper and not broken:
with open_nolock(req_path, "rb") as f:
remains = sendfile_py(
@@ -5394,8 +5413,8 @@ class HttpCli(object):
wr_sz,
wr_slp,
not self.args.no_poll,
self.conn.hsrv.dls,
self.dl_id,
dls,
dl_id,
)
spd = self._spd((upper - lower) - remains)
@@ -6664,6 +6683,7 @@ class HttpCli(object):
act = self.uparam["eshare"]
if act == "rm":
cur.execute("delete from sh where k = ?", (skey,))
cur.execute("delete from sf where k = ?", (skey,))
if skey in self.asrv.vfs.nodes[self.args.shr.strip("/")].nodes:
reload = True
else:
@@ -7264,7 +7284,7 @@ class HttpCli(object):
if use_dirkey:
is_dk = True
elif self.can_get and "doc" in self.uparam:
zs = vjoin(self.vpath, self.uparam["doc"]) + "?v"
zs = vjoin(self.vpath, self.uparam["doc"].split("//")[-1]) + "?v"
return self.redirect(zs, flavor="redirecting to", use302=True)
elif not self.can_write:
return self.tx_404(True)
+54 -26
View File
@@ -37,7 +37,6 @@ from .util import (
ipnorm,
min_ex,
read_utf8,
relchk,
runhook,
sanitize_fn,
ub64enc,
@@ -55,6 +54,23 @@ if True: # pylint: disable=using-constant-test
SATTR = paramiko.sftp_attr.SFTPAttributes
try:
from paramiko.rsakey import serialization
_load_der_pk = serialization.load_der_private_key
def fastloader(*a, **ka):
ka0 = ka.copy()
try:
ka["unsafe_skip_rsa_key_validation"] = True
return _load_der_pk(*a, **ka)
except:
return _load_der_pk(*a, **ka0)
except:
pass
class SSH_Srv(paramiko.ServerInterface):
def __init__(self, hub: "SvcHub", addr: Any):
self.hub = hub
@@ -295,6 +311,17 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
if self.uname == LEELOO_DALLAS:
raise Exception("send her back")
cm = 0
for zs, zi in (
("s", SATTR.FLAG_SIZE),
("t", SATTR.FLAG_AMTIME),
("p", SATTR.FLAG_PERMISSIONS),
("u", SATTR.FLAG_UIDGID),
):
if zs in self.args.sftp_chattr:
cm |= zi
self.chattr_mask = cm
self.vols = [
vp
for vp, vn in self.asrv.vfs.all_vols.items()
@@ -320,15 +347,23 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
m: bool = False,
d: bool = False,
) -> tuple[str, VFS, str]:
vpath = vpath.replace(os.sep, "/").strip("/")
rd, fn = os.path.split(vpath)
if relchk(rd):
self.log("malicious vpath: %s", vpath)
raise Exception("Unsupported characters in [%s]" % (vpath,))
# because:
# * maybe some clients use \
# * windows filename limitations
# need to allow/translate illegal fn,
# but require filesystem-correct rd
if ANYWIN or ("\\" in vpath and "/" not in vpath):
vpath = vpath.replace("\\", "/")
vpath = vpath.strip("/\\")
if ANYWIN:
rd, fn = os.path.split(vpath)
fn = sanitize_fn(fn, "win")
vpath = vjoin(rd, fn)
# end of windows jank
fn = sanitize_fn(fn or "")
vpath = vjoin(rd, fn)
vn, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
vpath = vjoin(vn.vpath, rem)
rd, fn = os.path.split(vpath)
if (
w
and fn.lower() in vn.flags["emb_all"]
@@ -343,18 +378,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
# return "", vn, rem
raise OSError(errno.ENOENT, "no filesystem mounted at [/%s]" % (vpath,))
if "xdev" in vn.flags or "xvol" in vn.flags:
ap = vn.canonical(rem)
avn = vn.chk_ap(ap)
t = "Permission denied in [{}]"
if not avn:
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
cr, cw, cm, cd, _, _, _, _, _ = avn.uaxs[self.uname]
if r and not cr or w and not cw or m and not cm or d and not cd:
raise OSError(errno.EPERM, "permission denied in [/%s]" % (vpath,))
else:
ap = vn.canonical(rem, False)
ap = vn.canonical(rem, False)
if "bcasechk" in vn.flags and not vn.casechk(rem, True):
raise OSError(errno.ENOENT, "file does not exist case-sensitively")
@@ -667,8 +691,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
def _mkdir(self, vp: str, attr: SATTR) -> int:
self.log("mkdir(%s)" % (vp,))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True)
ap = vn.canonical(rem, False)
ap, vn, _ = self.v2a(vp, w=True)
bos.makedirs(ap, vf=vn.flags) # filezilla expects this
if attr is not None:
paramiko.SFTPServer.set_file_attr(ap, attr)
@@ -691,8 +714,7 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
def _rmdir(self, vp: str) -> int:
self.log("rmdir(%s)" % (vp,))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, False, will_del=True)
ap = os.path.join(vn.realpath, rem)
ap, _, _ = self.v2a(vp, d=True)
bos.rmdir(ap)
return SFTP_OK
except Pebkac as ex:
@@ -713,8 +735,8 @@ class SFTP_Srv(paramiko.SFTPServerInterface):
def _chattr(self, vp: str, attr: SATTR) -> int:
self.log("chattr(%s, %s)" % (vp, attr))
try:
vn, rem = self.asrv.vfs.get(vp, self.uname, False, True, will_del=True)
ap = os.path.join(vn.realpath, rem)
attr._flags &= self.chattr_mask
ap, _, _ = self.v2a(vp, w=True, d=True)
paramiko.SFTPServer.set_file_attr(ap, attr)
return SFTP_OK
except Pebkac as ex:
@@ -774,6 +796,10 @@ class Sftpd(object):
self.log("cannot start sftp-server; no compatible IPs in -i", 1)
return
if args.sftp_fastldr:
serialization.load_der_private_key = fastloader # type: ignore
else:
self.log("loading hostkeys...")
self.hostkeys = []
hostkeytypes = (
("ed25519", "Ed25519Key", {}), # best
@@ -801,6 +827,8 @@ class Sftpd(object):
self.hostkeys.append(pkey)
if args.sftpv:
self.log("loaded hostkey %r" % (pkey,))
if args.sftp_fastldr:
serialization.load_der_private_key = _load_der_pk # type: ignore
ips = list(ODict.fromkeys(ips)) # dedup
+8 -1
View File
@@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import ctypes
import sys
from ctypes import wintypes
if True: # pylint: disable=using-constant-test
@@ -9,6 +10,9 @@ if True: # pylint: disable=using-constant-test
from . import _shared as shared
WINVER = sys.getwindowsversion()
LE_XP = WINVER.major < 6
NO_ERROR = 0
ERROR_BUFFER_OVERFLOW = 111
MAX_ADAPTER_NAME_LENGTH = 256
@@ -83,7 +87,10 @@ def enumerate_interfaces_of_adapter(
t = "sockaddr_to_ip({}) returned None"
raise Exception(t.format(address.Address.lpSockaddr))
network_prefix = address.OnLinkPrefixLength
if LE_XP:
network_prefix = 16 if str(ip).startswith("169.254") else 24
else:
network_prefix = address.OnLinkPrefixLength
yield shared.IP(ip, network_prefix, nice_name)
+19 -28
View File
@@ -174,6 +174,7 @@ class SvcHub(object):
self.is_dut = False # running in unittest; always False
self.stopping = False
self.stopped = False
self.init_mutex = threading.Lock()
self.reload_mutex = threading.Lock()
self.retcode = 0
self.httpsrv_up = 0
@@ -535,9 +536,6 @@ class SvcHub(object):
self.tftpd: Optional[Tftpd] = None
if args.sftp or args.ftp or args.ftps or args.tftp:
Daemon(self.start_ftpd, "start_tftpd")
if args.smb:
# impacket.dcerpc is noisy about listen timeouts
sto = socket.getdefaulttimeout()
@@ -861,29 +859,19 @@ class SvcHub(object):
cur.close()
db.close()
def start_ftpd(self) -> None:
time.sleep(30)
if hasattr(self, "sftpd") and not self.sftpd:
self.restart_sftpd()
if hasattr(self, "ftpd") and not self.ftpd:
self.restart_ftpd()
if hasattr(self, "tftpd") and not self.tftpd:
self.restart_tftpd()
def restart_sftpd(self) -> None:
if not hasattr(self, "sftpd"):
return
from .sftpd import Sftpd
if self.sftpd:
return # todo
with self.init_mutex:
if self.sftpd:
return # todo
self.sftpd = Sftpd(self)
self.sftpd.run()
self.sftpd = Sftpd(self)
self.sftpd.run()
self.log("root", "started SFTPd")
def restart_ftpd(self) -> None:
@@ -892,13 +880,15 @@ class SvcHub(object):
from .ftpd import Ftpd
if self.ftpd:
return # todo
with self.init_mutex:
if self.ftpd:
return # todo
if not os.path.exists(self.args.cert):
ensure_cert(self.log, self.args)
if not os.path.exists(self.args.cert):
ensure_cert(self.log, self.args)
self.ftpd = Ftpd(self)
self.ftpd = Ftpd(self)
self.log("root", "started FTPd")
def restart_tftpd(self) -> None:
@@ -907,10 +897,11 @@ class SvcHub(object):
from .tftpd import Tftpd
if self.tftpd:
return # todo
with self.init_mutex:
if self.tftpd:
return # todo
self.tftpd = Tftpd(self)
self.tftpd = Tftpd(self)
def thr_httpsrv_up(self) -> None:
time.sleep(1 if self.args.ign_ebind_all else 5)
@@ -1290,7 +1281,7 @@ class SvcHub(object):
TH_BWRAP[:] = al.th_bwrap_b
zs = "dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
zs = "acao_re dav_ua1 lf_url sus_urls nonsus_urls ua_nodav ua_nodoc ua_nozip"
for k in zs.split(" "):
vs = getattr(al, k)
if not vs or vs == "no":
+1 -1
View File
@@ -104,7 +104,7 @@ def gen_hdr(
ret += spack(b"<LL", vsz, vsz)
# windows support (the "?" replace below too)
fn = sanitize_to(fn, VPTL_WIN)
fn = sanitize_to(fn, VPTL_WIN, "win")
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
# add ntfs (0x24) and/or unix (0x10) extrafields for utc, add z64 if requested
+5 -1
View File
@@ -275,7 +275,11 @@ class Tftpd(object):
if not vfs.realpath:
raise Exception("unmapped vfs")
return vfs, vpath, vfs.canonical(rem)
ap = vfs.canonical(rem)
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
raise Exception("file not found")
return vfs, vpath, ap
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
+3 -1
View File
@@ -102,7 +102,9 @@ class ThumbCli(object):
):
sfmt = "j"
if sfmt == "x" and not self.can_jxl:
if sfmt == "x" and (
not self.can_jxl or (not H_PIL_JXL and ext in self.fmt_raw)
):
sfmt = "w"
vf_crop = dbv.flags["crop"]
+21 -10
View File
@@ -20,7 +20,7 @@ from .__init__ import ANYWIN, PY2, TYPE_CHECKING, UNIX, WINDOWS, E
from .authsrv import LEELOO_DALLAS, REDUP_E2, SEESLOG, VFS, AuthSrv
from .bos import bos
from .cfg import vf_bmap, vf_cmap, vf_vmap
from .fsutil import Fstab
from .fsutil import Fstab, filefrag
from .mtag import MParser, MTag
from .util import (
E_FS_CRIT,
@@ -1004,7 +1004,7 @@ class Up2k(object):
t = ""
if n < 1 and "e2v" in vol.flags:
t += "+e2v"
if n < 2 and "redup" in vol.flags:
if n < 2 and vol.flags.get("redup"):
t += "+redup"
if n < 3 and "e2ts" in vol.flags:
t += "+tags"
@@ -1179,7 +1179,7 @@ class Up2k(object):
ft = "\033[0;32m{}{:.0}"
ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}"
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set zipmax zipmaxn_v zipmaxs_v"
zs = "bcasechk du_iwho emb_all emb_lgs emb_mds ext_th_d html_head html_head_d html_head_s ls_q_m put_name2 mv_re_r mv_re_t rm_re_r rm_re_t oh_f oh_g rw_edit_set srch_re_dots srch_re_nodot th_coversd th_coversl th_covers_set th_coversd_set xvd zipmax zipmaxn_v zipmaxs_v"
fx = set(zs.split())
fd = vf_bmap()
fd.update(vf_cmap())
@@ -1317,7 +1317,7 @@ class Up2k(object):
ANYWIN or UNIX or "hist_cow" in flags or "up2k.db" in os.listdir(histpath)
):
try:
if self.fstab.get(ptop)[0] != "btrfs":
if self.fstab.get(histpath)[0] != "btrfs":
raise Exception()
runcmd([b"chattr", b"-c", fsenc(histpath)], 1) # compression and...
runcmd([b"chattr", b"+C", fsenc(histpath)], 1) # no-cow are incompat
@@ -2181,10 +2181,10 @@ class Up2k(object):
for w, rd, fn in cur.execute("select w, rd, fn from up"):
w16 = w[:16]
for v2, c2 in curs:
hit = c2.execute(q2, (w16, w)).fetchone()
if not hit:
hits = c2.execute(q2, (w16, w)).fetchall()
if not hits:
continue
rd2, fn2 = hit
rd2, fn2 = hits[0]
if fn == fn2 and rd == rd2 and vol is v2:
continue
apt = ""
@@ -2196,11 +2196,22 @@ class Up2k(object):
st1 = bos.lstat(fp1)
if fp1 == fp2 or stat.S_ISLNK(st1.st_mode):
continue # self(?), or redup failed during walk
if to == "hard" and st1.st_nlink >= len(hits):
continue
ap1 = absreal(fp1)
ap2 = absreal(fp2)
st2 = bos.lstat(ap2)
if stat.S_ISLNK(st2.st_mode):
continue # dead hit
if to == "ref" and ap1 != ap2:
try:
frag1 = filefrag(ap1)
if frag1:
frag2 = filefrag(ap2)
if frag1 == frag2:
continue
except Exception as ex:
self.log("redup: filefrag failed (OK); %r" % (ex,))
for ap in (ap1,) if ap1 == ap2 else (ap1, ap2):
self.log("redup: integrity-checking %r" % (ap,))
zsl, st = self._hashlist_from_file(ap)
@@ -3539,7 +3550,7 @@ class Up2k(object):
job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath
job["prel"] = rem
job["name"] = sanitize_fn(job["name"])
job["name"] = vfs.sanitize_fn(job["name"], rem)
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
# print(json.dumps(job, sort_keys=True, indent=4))
@@ -4398,7 +4409,7 @@ class Up2k(object):
is_dir = stat.S_ISDIR(st.st_mode)
except:
# NOTE: "file not found" *sftpd
raise Pebkac(400, "file not found on disk (already deleted?)")
raise Pebkac(404, "file not found on disk (already deleted?)")
if "bcasechk" in vn.flags and not vn.casechk(rem, False):
raise Pebkac(400, "file does not exist case-sensitively")
@@ -5428,7 +5439,7 @@ class Up2k(object):
job["ptop"] = vfs.realpath
job["vtop"] = vfs.vpath
job["prel"] = rem
job["name"] = sanitize_fn(job["name"])
job["name"] = vfs.sanitize_fn(job["name"], rem)
ud2 = (vfs.vpath, job["prel"], job["name"])
if ud1 != ud2:
job["vcfg"] = vf
+54 -20
View File
@@ -365,13 +365,19 @@ if PY2:
else:
umktrans = str.maketrans
FNTL_WIN = umktrans('<>:|?*"\\/', "<>:|?*"\/")
VPTL_WIN = umktrans('<>:|?*"\\', "<>:|?*"\")
APTL_WIN = umktrans('<>:|?*"/', "<>:|?*"/")
FNTL_MAC = VPTL_MAC = APTL_MAC = umktrans(":", ":")
FNTL_OS = FNTL_WIN if ANYWIN else FNTL_MAC if MACOS else None
VPTL_MAC = umktrans(":", ":")
VPTL_OS = VPTL_WIN if ANYWIN else VPTL_MAC if MACOS else None
APTL_OS = APTL_WIN if ANYWIN else APTL_MAC if MACOS else None
BADNAMES = []
if ANYWIN:
BADNAMES += ["con", "prn", "aux", "nul"]
BADNAMES += ["com%s" % (n,) for n in range(1, 10)]
BADNAMES += ["lpt%s" % (n,) for n in range(1, 10)]
BADNAMES += ["com%s" % (n,) for n in ("¹", "²", "³")]
BADNAMES += ["lpt%s" % (n,) for n in ("¹", "²", "³")]
BADNAMESET = set(BADNAMES)
BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
@@ -463,6 +469,8 @@ IMPLICATIONS = [
["smbvvv", "smbvv"],
["smbvv", "smbv"],
["smbv", "smb"],
["xdev", "xvd"],
["xvol", "xvd"],
["zv", "zmv"],
["zv", "zsv"],
["z", "zm"],
@@ -2429,7 +2437,7 @@ def uncyg(path: str) -> str:
return "%s:\\%s" % (path[1], path[3:])
def undot(path: str) -> str:
def _undot(path: str) -> str:
ret: list[str] = []
for node in path.split("/"):
if node == "." or not node:
@@ -2445,30 +2453,56 @@ def undot(path: str) -> str:
return "/".join(ret)
def sanitize_fn(fn: str) -> str:
fn = fn.replace("\\", "/").split("/")[-1]
if APTL_OS:
fn = sanitize_to(fn, APTL_OS)
def _undot_win(path: str) -> str:
ret: list[str] = []
for node in path.replace("\\", "/").split("/"):
if node == "..":
if ret:
ret.pop()
continue
node = node.rstrip(". ")
if node:
ret.append(node)
return "/".join(ret)
if ANYWIN:
undot = _undot_win
else:
undot = _undot
def sanitize_fn(fn: str, fsnt: Optional[str] = None) -> str:
fn = fn.replace("\x00", "_").split("/")[-1]
if fsnt and fsnt != "lin":
fn = fn.translate(VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC)
fn2 = fn.replace("\\", "/")
if ANYWIN or fsnt == "win" or "/../" in fn2:
fn = fn2.split("/")[-1].rstrip(". ")
if VPTL_OS:
fn = sanitize_to(fn, VPTL_OS)
return fn.strip()
def sanitize_to(fn: str, tl: dict[int, int]) -> str:
def sanitize_to(fn: str, tl: dict[int, int], fsnt: Optional[str] = None) -> str:
fn = fn.translate(tl)
if ANYWIN:
bad = ["con", "prn", "aux", "nul"]
for n in range(1, 10):
bad += ("com%s lpt%s" % (n, n)).split(" ")
if fn.lower().split(".")[0] in bad:
if ANYWIN or fsnt == "win":
fn = fn.rstrip(". ")
if fn.lower().split(".")[0].strip() in BADNAMESET:
fn = "_" + fn
return fn
def sanitize_vpath(vp: str) -> str:
if not APTL_OS:
def sanitize_vpath(vp: str, fsnt: Optional[str] = None) -> str:
vptl = VPTL_OS
if fsnt and fsnt != "lin":
vptl = VPTL_WIN if fsnt == "win" or ANYWIN else VPTL_MAC
if not vptl:
return vp
parts = vp.replace(os.sep, "/").split("/")
ret = [sanitize_to(x, APTL_OS) for x in parts]
ret = [sanitize_to(x, vptl, fsnt) for x in parts]
return "/".join(ret)
+1
View File
@@ -705,6 +705,7 @@ window.baguetteBox = (function () {
unbind(overlay, 'touchend', touchendHandler);
unbind(document, 'focus', trapFocusInsideOverlay, true);
timer.rm(rotn);
clearTimeout(unrot_timer);
}
function prepareOverlay(gallery, userOptions, cbz) {
+3 -1
View File
@@ -4096,7 +4096,7 @@ var fileman = (function () {
if (wr)
html.push('<a href="#" class="btn">write-only</a>');
html.push('</td></tr></div');
html.push('</td></tr><table></div>');
shui.innerHTML = html.join('\n');
var sh_rand = ebi('sh_rand'),
@@ -4572,6 +4572,8 @@ var fileman = (function () {
xhr.onload = xhr.onerror = rename_cb;
xhr.send();
}
ire.oninput();
};
r.delete = function (e) {
+1 -1
View File
@@ -3008,7 +3008,7 @@ function up2k_init(subtle) {
}
else {
xhrchk(xhr, L.u_cuerr2.format(snpart, Math.ceil(t.size / chunksize), esc(t.name)), "404, target folder not found (???)", "warn", t);
if (txt.indexOf('unknown wark') == 0) {
if (!t.sprs || txt.indexOf('unknown wark') == 0) {
t.cooldown = t.coolmul = 0;
unqueue_up(t);
}
+2 -2
View File
@@ -853,7 +853,7 @@ function vjoin(p1, p2) {
if (!p2)
p2 = '';
if (p1.endsWith('/'))
if (p1 != '/' && p1.endsWith('/'))
p1 = p1.slice(0, -1);
if (p2.startsWith('/'))
@@ -865,7 +865,7 @@ function vjoin(p1, p2) {
if (!p2)
return p1;
return p1 + '/' + p2;
return p1 == '/' ? p1 + p2 : p1 + '/' + p2;
}
+74
View File
@@ -1,3 +1,77 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-1006-0035 `v1.20.25` SECURITY: shadowing on windows/mac
## ⚠️ ATTN: this release mitigates a Windows/MacOS-specific issue with shadowing
[GHSA-3c88-p299-7hvr](https://github.com/9001/copyparty/security/advisories/GHSA-3c88-p299-7hvr): due to windows/macos having case-insensitive (casefolding) filesystems, [shadowing](https://github.com/9001/copyparty/#shadowing) is not bulletproof, even with this fix
## 🧪 new features
* #1498 [--acao-re](https://copyparty.eu/cli/#g-acao-re) for allowing cross-domain access with more complex rules ab2299e3
* #1652 [redup](https://copyparty.eu/cli/#redup-help-page): detect and skip hardlinks / reflinks which are already optimally deduplicated 08093578
* [--sftp-fastldr](https://copyparty.eu/cli/#g-sftp-fastldr) to improve startup time on extremely old hardware 186cbf93
* [--sftp-chattr](https://copyparty.eu/cli/#g-sftp-chattr) to restrict chattr to certain properties e59fc05e
## 🩹 bugfixes
* GHSA-3c88-p299-7hvr e8de4211
* for further context, torvalds has some [technically accurate and colourful opinions](https://lore.kernel.org/lkml/CAHk-=wjajMJyoTv2KZdpVRoPn0LFZ94Loci37WLVXmMxDbLOjg@mail.gmail.com/) (nsfw) on how casefolding filesystems are an ongoing mistake
* on a more serious note, [the readme was updated](https://github.com/9001/copyparty/#shadowing)
* fix uploads struggling to resume from a network glitch if the server filesystem is FAT32 or exFAT 23e00c0a 731dbbff
* #1647 shares: fix cleanup of single-file shares when manually deleted before expiration (thx @willie6666!) f445bfa8
* #1660 ban-pw now more aggressive on failed basic-auth (thx @PAStheLoD!) 5e0a88e3
* #1656 webdav: return 404 instead of 400 when file didn't exist 74b17374
* fix navigating to an index.html webroot (thx @tilse!) 334dccc5
* fix IP-address detection on Windows XP 107dec9e
## 🔧 other changes
* #1658 constant-time verification of filekeys/dirkeys (thx @rozbb!) a31870cb
* prevent enabling the smb server unless `--hack-me-bro` is also enabled because the smb-server was intentionally made super-insecure so it would work with ancient outdated equipment in emergencies and should never be used otherwise 1341b342
* now possible to upload [09. t+pazolite - I still think about y%#'()_*{\\"'&36)%("'$&''&(15y!!!_??*`~.flac](https://www.youtube.com/watch?v=btPU_rluFSE) (dont click that link on acid) 28f9f6f0 64f574fe
## 🌠 fun facts
* [thx t+paz](https://a.ocv.me/pub/g/2026/10/Screenshot_20261002-221751_1.png?cache)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0919-2133 `v1.20.24` redup
## 🧪 new features
* new option [--redup](https://copyparty.eu/cli/#g-redup) (global or volflag) to convert local files between various dedup approaches fd4f4341 22915902 16a3f5d5
* turn full copies into reflinks/symlinks/hardlinks or convert between those
* run it with `--redup-dry` (dryrun) to check for unexpected consequences
* WARNING: maybe take a snapshot of your filesystem before enabling this; brand new code with little testing, read [--help-redup](https://copyparty.eu/cli/#redup-help-page) and use with care
* seems fine for converting between symlinks and reflinks, probably hardlinks too
* #1643 textfile-viewer can detect filetype from shebang (thx @botahamec!) 097b8305
* when dedup with [reflink](https://copyparty.eu/cli/#g-reflink) is enabled, then also dedup between volumes when possible (since reflink makes that safe) 735d6e00 5ddbce31
* option [-i no](https://copyparty.eu/cli/#bind-help-page) to launch copyparty without the webserver, for indexing / testing / whatever c92435c2
## 🩹 bugfixes
* if `--dedup` was enabled, then copying a file from one volume to another could dedup (create a symlink/hardlink/reflink) instead of a full copy 947d73ee
* mainly an issue for portable storage such as flashdrives, and the main reason why `--redup` was added
* the list of filenames given as input to xiu hooks did not have a trailing newline c688bb2e
* #1611 volflag `nosub` would still result in empty folders from up2k ec4a95e1
* fix `PRTY_NO_IFADDR` 6867ccc5
## 🔧 other changes
* iOS: further improved upload-speeds on buggy iOS versions 971a62cc
* looks like apple is working on fixing this bug; iOS v27 made https way more stable but http is still just as broken, so improved the workaround a bit more
* added [--help-dedup](https://copyparty.eu/cli/#dedup-help-page) and [--help-redup](https://copyparty.eu/cli/#redup-help-page) 22915902 16a3f5d5
* add a warning that the sourceforge project is not official and probably malware bfc3cff8
## 🌠 fun facts
* the [railgun roulette vip-mix](https://www.youtube.com/watch?v=mv9BmrzkiJc) has the same bpm as the [turn-signal on a honda jazz](https://a.ocv.me/pub/stuff/music/railgun-roulette-honda-remix.opus)
* on that note, [this fantasy-OPNA remake](https://www.youtube.com/watch?v=hv2hr8aPBMU) is too awesome to leave out
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2026-0906-2249 `v1.20.23` rcm once again
+1 -1
View File
@@ -2,7 +2,7 @@ FROM alpine:3.23
WORKDIR /z
ENV ver_hashwasm=4.12.0 \
ver_marked=4.3.0 \
ver_dompf=3.4.15 \
ver_dompf=3.4.17 \
ver_mde=2.18.0 \
ver_codemirror=5.65.18 \
ver_fontawesome=5.13.0 \
+8
View File
@@ -38,6 +38,14 @@ f=/z/whl/zlib_ng-0.5.1-cp312-cp312-linux_$(cat /etc/apk/arch).whl
}
rm -rf /z/whl
# drop openjpeg; unmaintained, has vulns
(cd /usr/lib/python3.*/site-packages/PIL
sed -ri /jpg_2000/d features.py
sed -ri /Jpeg2KIm/d __init__.py
sed -ri /Jpeg2KIm/d Image.py
rm -f Jpeg2KImagePlugin.py
) || true
# cleanup for flavors with python build steps (dj/iv)
rm -rf /var/cache/apk/* /root/.cache
+1 -1
View File
@@ -31,5 +31,5 @@ e9492eedaac7cfddde6d84389ef7df86c68275e8f29d838fc431d9bf379401bf6d82ec70ac024619
8ea9946d8484a6de833a97d3cd545c2b67475207944856281cffb2c513ab14bf50e2b9d31d7cca8f50a6ca8b6711fb8e45cbadabd566cab28f7d58ee058dc650 pillow-12.3.0-cp314-cp314-win_amd64.whl
6f7ed5dc47ade9ad56f6f0499bc5d8eb485bd454d47189133297fa5aa633cf70dad18656cd673318c664f96097c39003d94d9c9703c8337d62840b4564695a04 pyinstaller-6.21.0-py3-none-win_amd64.whl
0a7c1ae7063a2ee46a1c5c057fe73838d7dc5c59800e56037b3fb9dbd1c363915375f989160e44f21436e1071713682bb14473aa8112128beb670f5b110dc8e2 pyinstaller_hooks_contrib-2026.6-py3-none-any.whl
a4adbefaa216571ade7fda561ed3404c762ff03029696ce1745fd4e23b066cb49b1fef99792ce3a594a04f9260a2f05a9e2f851bcc83ed4404f00ce05709c56a python-3.14.7-amd64.exe
ccbbc7ed972a8ae3f9ace7728cf5cbff8dcb15ed2dfcd105131641f5b041cecc90adc2fc25f36333e0a2be8e56a6f702fb064da6e6fbb78cc2393333ad3e5b6f python-3.14.8-amd64.exe
bfda5996cdf908edf971df38421ed154e2f2c58f38731ad3be22a88e78c933c64efd35fbc6daf82845dc0b59b5211e925038b430749b2c851887096578c68219 setuptools-83.0.0-py3-none-any.whl
+1 -1
View File
@@ -42,7 +42,7 @@ fns=(
pillow-12.3.0-cp314-cp314-win_amd64.whl
pyinstaller-6.21.0-py3-none-win_amd64.whl
pyinstaller_hooks_contrib-2026.6-py3-none-any.whl
python-3.14.7-amd64.exe
python-3.14.8-amd64.exe
setuptools-83.0.0-py3-none-any.whl
)
[ $w7 ] && fns+=(
+13 -5
View File
@@ -2,6 +2,15 @@
set -e
td=redump-test
export TZ=Etc/UTC
rl_always=--reflink=always
rl_never=--reflink=never
[ "$(uname)" = Darwin ] && {
rl_always=
rl_never=
stat() { gstat "$@"; }
}
st_orig() { cat <<'EOF'
f1,1,'f1',3660
@@ -52,7 +61,7 @@ f9,3,'f9',3600
EOF
}
statchk() {
(cd $td;stat -c%n,%h,%N,%Y * | sort -n | diff -U9 <($1) -) && return
(cd $td;stat -c%n,%h,%N,%Y * | sort -n | tr -d \' | diff -U9 <($1|tr -d \') -) && return
cat $td/* | grep -vE '^f1f2f3f4f2f3f4f1f1$' || return
cat $td.l
exit 1
@@ -61,14 +70,13 @@ setup() {
rm -rf $td
mkdir $td
th=19700101010 #..M
tu=978310800 #unix
( cd "$td"
for n in {1..4}; do echo -n f$n > f$n; done
ln -s f2 f5
ln f3 f6
cp --reflink=always f4 f7
cp --reflink=never f1 f8
cp --reflink=never f1 f9
cp $rl_always f4 f7
cp $rl_never f1 f8
cp $rl_never f1 f9
for n in {1..9}; do touch -ht $th$n f$n; done
);statchk st_orig
}
+87 -31
View File
@@ -42,7 +42,9 @@ class TestShr(unittest.TestCase):
if edl is None:
edl = els[1]
can_dl = []
for fn in b[1]:
zs = "f1 f2 f3 f4 l1 l2 l3 l4"
known_fns = list(sorted(list(set(b[1] + zs.split()))))
for fn in known_fns:
if fn == "a.db":
continue
furl = url + "/" + fn
@@ -51,7 +53,9 @@ class TestShr(unittest.TestCase):
h, zb = self.curl(furl, True)
if h.startswith("HTTP/1.1 200 "):
can_dl.append(fn)
self.assertEqual(edl, can_dl)
elif zb.startswith(b">>fb:"):
can_dl.append(fn)
self.assertEqual(edl, list(sorted(list(set(can_dl)))))
def setUp(self):
self.td = tu.get_ramdisk()
@@ -61,12 +65,15 @@ class TestShr(unittest.TestCase):
os.mkdir("d1")
os.mkdir("d2")
os.mkdir("d2/d3")
for zs in ("d1/f1", "d2/f2", "d2/d3/f3"):
os.mkdir("d2/d3/d4")
for zs in ("d1/f1", "d2/f2", "d2/d3/f3", "d2/d3/d4/f4"):
with open(zs, "wb") as f:
f.write(zs.encode("utf-8"))
for dst in ("d1", "d2", "d2/d3"):
f.write(b">>fb:" + zs.encode("utf-8"))
for dst in ("d1", "d2", "d2/d3", "d2/d3/d4"):
src, fn = zs.rsplit("/", 1)
os.symlink(absreal(zs), dst + "/l" + fn[-1:])
os.symlink(absreal("d2/d3"), absreal("d2/ld"))
os.symlink(absreal("d2/d3"), absreal("d2/d3/d4/ld"))
db = sqlite3.connect("a.db")
with db:
@@ -92,12 +99,14 @@ class TestShr(unittest.TestCase):
shr_v=False,
)
self.cinit()
L1234 = ["l1", "l2", "l3", "l4"]
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d1", True, [[], ["f1"] + L1234], None)
self.assertLD("v1", True, [[], ["f1"] + L1234], None)
self.assertLD("d2", True, [["d3", "ld"], ["f2"] + L1234], None)
self.assertLD("d2/d3", True, [["d4"], ["f3"] + L1234], None)
self.assertLD("d2/d3/d4", True, [["ld"], ["f4"] + L1234], None)
self.assertLD("d3", True, [], [])
jt = {
@@ -105,7 +114,7 @@ class TestShr(unittest.TestCase):
"vp": ["/"],
"pw": "",
"exp": "99",
"perms": ["read"],
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
@@ -113,26 +122,48 @@ class TestShr(unittest.TestCase):
"vp": ["/d2/"],
"pw": "",
"exp": "99",
"perms": ["read"],
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
"k": "d4",
"vp": ["/d2/d3/d4/"],
"pw": "",
"exp": "99",
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
self.conn.shutdown()
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d1", True, [[], ["f1"] + L1234], None)
self.assertLD("v1", True, [[], ["f1"] + L1234], None)
self.assertLD("d2", True, [["d3", "ld"], ["f2"] + L1234], None)
self.assertLD("d2/d3", True, [["d4"], ["f3"] + L1234], None)
self.assertLD("d2/d3/d4", True, [["ld"], ["f4"] + L1234], None)
self.assertLD("d3", True, [], [])
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("shr/d2", False, [["ld"], ["f2"] + L1234], None)
self.assertLD("shr/d2/d3", False, [], None)
self.assertLD("shr/d2/d3/d4", False, [], None)
self.assertLD("shr/d4", False, [["ld"], ["f4"] + L1234], None)
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
self.assertLD("shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("shr/r/d1", False, [[], ["f1"] + L1234], None)
self.assertLD("shr/r/d2", False, [], None) # unfortunate
self.assertLD("shr/r/d2/d3", False, [], None)
self.assertLD("shr/r/d2/d3/d4", False, [], None)
self.put("shr/d2/a", 201)
os.unlink("d2/a")
self.put("shr/d2/ld/a", 201)
os.unlink("d2/d3/a")
self.put("shr/d4/ld/a", 201)
os.unlink("d2/d3/a")
self.conn.shutdown()
@@ -147,12 +178,14 @@ class TestShr(unittest.TestCase):
xvol=True,
)
self.cinit()
L1234 = ["l1", "l2", "l3", "l4"]
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d1", True, [[], ["f1"] + L1234], None)
self.assertLD("v1", True, [[], ["f1"] + L1234], None)
self.assertLD("d2", True, [["d3", "ld"], ["f2"] + L1234], None)
self.assertLD("d2/d3", True, [["d4"], ["f3"] + L1234], None)
self.assertLD("d2/d3/d4", True, [["ld"], ["f4"] + L1234], None)
self.assertLD("d3", True, [], [])
jt = {
@@ -160,7 +193,7 @@ class TestShr(unittest.TestCase):
"vp": ["/"],
"pw": "",
"exp": "99",
"perms": ["read"],
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
@@ -168,28 +201,45 @@ class TestShr(unittest.TestCase):
"vp": ["/d2/"],
"pw": "",
"exp": "99",
"perms": ["read"],
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
jt = {
"k": "d4",
"vp": ["/d2/d3/d4/"],
"pw": "",
"exp": "99",
"perms": ["read", "write"],
}
print(self.post_json("?pw=p1&share", jt)[1])
self.conn.shutdown()
self.cinit()
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
self.assertLD("d1", True, [[], ["f1"] + L1234], None)
self.assertLD("v1", True, [[], ["f1"] + L1234], None)
self.assertLD("d2", True, [["d3", "ld"], ["f2"] + L1234], None)
self.assertLD("d2/d3", True, [["d4"], ["f3"] + L1234], None)
self.assertLD("d2/d3/d4", True, [["ld"], ["f4"] + L1234], None)
self.assertLD("d3", True, [], [])
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], ["f2", "l2"])
self.assertLD("shr/d2", False, [["ld"], ["f2"] + L1234], ["f2", "l2"])
self.assertLD("shr/d2/d3", False, [], [])
self.assertLD("shr/d2/d3/d4", False, [], None)
self.assertLD("shr/d4", False, [["ld"], ["f4"] + L1234], ["f4", "l4"])
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
self.assertLD(
"shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], ["f1", "l1", "l2"]
)
self.assertLD("shr/r/d1", False, [[], ["f1"] + L1234], ["f1", "l1", "l2"])
self.assertLD("shr/r/d2", False, [], []) # unfortunate
self.assertLD("shr/r/d2/d3", False, [], [])
self.assertLD("shr/r/d2/d3/d4", False, [], None)
self.put("shr/d2/a", 201)
os.unlink("d2/a")
self.put("shr/d2/ld/a", 401)
self.put("shr/d4/ld/a", 401)
self.conn.shutdown()
@@ -227,3 +277,9 @@ class TestShr(unittest.TestCase):
print("PUT -->", buf)
HttpCli(self.conn.setbuf(buf)).run()
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
def put(self, url: str, expect: str):
h = "PUT /%s HTTP/1.1\r\nContent-Length: 1\r\n\r\n`"
HttpCli(self.conn.setbuf((h % (url,)).encode("utf-8"))).run()
h, r = self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
self.assertEqual(expect, int(h.split(" ")[1]))
+4 -3
View File
@@ -148,7 +148,7 @@ class Cfg(Namespace):
ex = "allow_flac allow_wav allow_svg_js chpw cookie_lax daw dav_auth dav_mac dav_rt dlni dothidden e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only http_no_tcp ih ihead localtime log_badxml magic md_no_br nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_dupe_m no_fnugg no_html no_lifetime no_logues no_mime no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_script no_tail no_tarcmp no_thumb no_vthumb no_u2abrt no_zip no_zls nrand nsort nw og og_no_head og_s_title ohead opds q rand re_dirsz redup_dry reflink rm_partial rmagic rss show_hist smb srch_dbg srch_excl srch_nfkc srch_icase stats ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi uqe usernames vague_403 vc ver vol_nospawn vol_or_crash wo_up_readme wopi write_uplog xdev xlink xvol zipmaxu zs"
ka.update(**{k: False for k in ex.split()})
ex = "dav_inf dedup dotpart dotsrch hist_cow hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip"
ex = "dav_inf dedup dotpart dotsrch hack_me_bro hist_cow hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump wram re_dhash see_dots plain_ip"
ka.update(**{k: True for k in ex.split()})
ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other lf_url mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip"
@@ -163,7 +163,7 @@ class Cfg(Namespace):
ex = "ac_convt au_vol dl_list du_iwho mcr mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt th_qv th_qvx ups_who ver_iwho zip_who"
ka.update(**{k: 9 for k in ex.split()})
ex = "ctl_re db_act forget_ip gauto idp_cookie idp_store k304 loris md_nhist no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"
ex = "ctl_re dav_port db_act forget_ip gauto idp_cookie idp_store k304 loris md_nhist no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri tftp theme themes turbo u2ow zipmaxn zipmaxs"
ka.update(**{k: 0 for k in ex.split()})
ex = "ah_alg bname chdir chmod_f chpw_db csp_dl csp_ui db_xattr doctitle df epilogues exit favico fika ipa ipar html_head html_head_d html_head_s idp_login idp_logout lg_sba lg_sbf log_date log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i opds_exts preadmes prologues readmes redup shr shr1 shr_site site smsg tcolor textfiles th_pregen txt_eol ufavico ufavico_h unlist up_site vc_url vname xff_src zipmaxt R RS SR"
@@ -172,7 +172,7 @@ class Cfg(Namespace):
ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary rcm rss_fmt_d rss_fmt_t spinner"
ka.update(**{k: "no" for k in ex.split()})
ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 qr_file xac xad xar xau xban xbc xbd xbr xbu xiu xm"
ex = "ext_th grp idp_h_usr idp_hm_usr ipr on403 on404 p_nodav qr_file xac xad xar xau xban xbc xbd xbr xbu xiu xm"
ka.update(**{k: [] for k in ex.split()})
ex = "exp_lg exp_md"
@@ -227,6 +227,7 @@ class Cfg(Namespace):
th_crop="y",
th_size="320x256",
th_x3="n",
ua_nodav=re.compile("\n"),
u2sort="s",
u2ts="c",
ui_filesz="1",