mirror of
https://github.com/9001/copyparty.git
synced 2026-11-10 12:28:23 +00:00
Compare commits
37
Commits
v1.20.24
...
hovudstraum
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af7efeca8 | ||
|
|
8b4593c9a2 | ||
|
|
7ccebb6948 | ||
|
|
ed0fcd608f | ||
|
|
9811cd3101 | ||
|
|
fa8913c712 | ||
|
|
4bd64d7456 | ||
|
|
7ce483e970 | ||
|
|
82eab3d2f8 | ||
|
|
0dc1cb3291 | ||
|
|
1a87dc3218 | ||
|
|
dc3adc0f4d | ||
|
|
5f00c1631c | ||
|
|
e59fc05e73 | ||
|
|
e8de421174 | ||
|
|
36aaa24974 | ||
|
|
5e0a88e355 | ||
|
|
4b283b4bfd | ||
|
|
64f574fe94 | ||
|
|
a31870cbd3 | ||
|
|
74b17374c2 | ||
|
|
670e86228c | ||
|
|
1341b3421f | ||
|
|
5961228c93 | ||
|
|
28f9f6f002 | ||
|
|
186cbf939f | ||
|
|
080935788b | ||
|
|
c6918c2ce6 | ||
|
|
ab2299e3db | ||
|
|
334dccc55e | ||
|
|
8d7aa4f860 | ||
|
|
f73b37c6fa | ||
|
|
731dbbff84 | ||
|
|
23e00c0a04 | ||
|
|
107dec9ed1 | ||
|
|
f445bfa862 | ||
|
|
aff083fb28 |
No files matched your search
@@ -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
@@ -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<&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:
|
||||
|
||||
@@ -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.26"
|
||||
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=("ac7ea05bde8d431b32cc2ff618f2754e46bd967bf10deaa6654dac9a449a6ff9")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver=1.20.23
|
||||
pkgver=1.20.26
|
||||
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=("ac7ea05bde8d431b32cc2ff618f2754e46bd967bf10deaa6654dac9a449a6ff9")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -91,19 +91,20 @@ let
|
||||
day = builtins.substring 6 2 lastModifiedDate;
|
||||
};
|
||||
# ex: "1970" "1" "1"
|
||||
dateStringsShort = builtins.mapAttrs (_: val: toString (lib.toIntBase10 val)) dateStringsZeroPrefixed;
|
||||
dateStringsShort = builtins.mapAttrs (
|
||||
_: val: toString (lib.toIntBase10 val)
|
||||
) dateStringsZeroPrefixed;
|
||||
unstableVersion =
|
||||
if copypartyFlake == null then
|
||||
"${pinData.version}-unstable"
|
||||
else
|
||||
with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}"
|
||||
;
|
||||
with dateStringsZeroPrefixed; "${pinData.version}-unstable-${year}-${month}-${day}";
|
||||
version = if stable then pinData.version else unstableVersion;
|
||||
stableSrc = fetchurl {
|
||||
inherit (pinData) url hash;
|
||||
};
|
||||
root = ../../../..;
|
||||
unstableSrc = nix-gitignore.gitignoreSource [] root;
|
||||
unstableSrc = nix-gitignore.gitignoreSource [ ] root;
|
||||
src = if stable then stableSrc else unstableSrc;
|
||||
rev = copypartyFlake.shortRev or copypartyFlake.dirtyShortRev or "unknown";
|
||||
unstableCodename = "unstable" + (lib.optionalString (copypartyFlake != null) "-${rev}");
|
||||
@@ -125,30 +126,38 @@ buildPythonApplication {
|
||||
old_src_folder="''${folders[0]}"
|
||||
cp -r "$old_src_folder"/copyparty/web/deps copyparty/web/deps
|
||||
sed -i 's/^CODENAME =.*$/CODENAME = "${unstableCodename}"/' copyparty/__version__.py
|
||||
${lib.optionalString (copypartyFlake != null) (with dateStringsShort; ''
|
||||
sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py
|
||||
'')}
|
||||
${lib.optionalString (copypartyFlake != null) (
|
||||
with dateStringsShort;
|
||||
''
|
||||
sed -i 's/^BUILD_DT =.*$/BUILD_DT = (${year}, ${month}, ${day})/' copyparty/__version__.py
|
||||
''
|
||||
)}
|
||||
'';
|
||||
dependencies =
|
||||
[
|
||||
jinja2
|
||||
fusepy
|
||||
]
|
||||
++ lib.optional withSMB impacket
|
||||
++ lib.optional withSFTP paramiko
|
||||
++ lib.optional withFTP pyftpdlib
|
||||
++ lib.optional withFTPS pyopenssl
|
||||
++ lib.optional withTFTP partftpy
|
||||
++ lib.optional withCertgen cfssl
|
||||
++ lib.optional withThumbnails pillow
|
||||
++ lib.optional withFastThumbnails pyvips
|
||||
++ lib.optional withMediaProcessing ffmpeg
|
||||
++ lib.optional withBasicAudioMetadata mutagen
|
||||
++ lib.optional withHashedPasswords argon2-cffi
|
||||
++ lib.optional withZeroMQ pyzmq
|
||||
++ lib.optional withMagic magic
|
||||
++ (extraPythonPackages python.pkgs);
|
||||
makeWrapperArgs = [ "--prefix PATH : ${lib.makeBinPath runtimeDeps}" ];
|
||||
dependencies = [
|
||||
jinja2
|
||||
fusepy
|
||||
]
|
||||
++ lib.optional withSMB impacket
|
||||
++ lib.optional withSFTP paramiko
|
||||
++ lib.optional withFTP pyftpdlib
|
||||
++ lib.optional withFTPS pyopenssl
|
||||
++ lib.optional withTFTP partftpy
|
||||
++ lib.optional withCertgen cfssl
|
||||
++ lib.optional withThumbnails pillow
|
||||
++ lib.optional withFastThumbnails pyvips
|
||||
++ lib.optional withMediaProcessing ffmpeg
|
||||
++ lib.optional withBasicAudioMetadata mutagen
|
||||
++ lib.optional withHashedPasswords argon2-cffi
|
||||
++ lib.optional withZeroMQ pyzmq
|
||||
++ lib.optional withMagic magic
|
||||
++ (extraPythonPackages python.pkgs);
|
||||
|
||||
makeWrapperArgs = [
|
||||
"--prefix"
|
||||
"PATH"
|
||||
":"
|
||||
(lib.makeBinPath runtimeDeps)
|
||||
];
|
||||
|
||||
pyproject = true;
|
||||
build-system = [
|
||||
|
||||
@@ -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.26/copyparty-1.20.26.tar.gz",
|
||||
"version": "1.20.26",
|
||||
"hash": "sha256-rH6gW96NQxsyzC/2GPJ1Tka9lnvxDeqmZU2smkSab/k="
|
||||
}
|
||||
@@ -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)")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user