diff --git a/README.md b/README.md index c8d50dbf..76109973 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ built in Norway ๐ณ๐ด with contributions from [not-norway](https://github.com * [other flags](#other-flags) * [descript.ion](#description) - add a description to each file in a folder * [dothidden](#dothidden) - cosmetically hide specific files in a folder + * [thumbnail pregen](#thumbnail-pregen) - if you want to pre-generate everything on startup * [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else * [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload * [metadata from xattrs](#metadata-from-xattrs) - unix extended file attributes @@ -1360,6 +1361,7 @@ serverlog is sent to stdout by default (but logging to a file is also possible) * [-q](https://copyparty.eu/cli/#g-q) disables logging to stdout, and may improve performance a little bit * combine it with `-lo logfolder/cpp-%Y-%m-%d.txt` to log to a file instead * the `%Y-%m-%d` makes it create a new logfile every day, with the date as filename + * global-option [--rlo](https://copyparty.eu/cli/#rlo-help-page) decides what happens if the filename is taken * `-lo whatever.txt` can be used without `-q` to log to both at the same time * by default, the logfile will have colors if the terminal does (usually the case) * use the [textfile-viewer](https://github.com/user-attachments/assets/8a828947-2fae-4df9-bd2a-3de46f42d478) or `less -R` in a terminal to see colors correctly @@ -3127,9 +3129,10 @@ when generating hashes using `--ah-cli` for docker or systemd services, make sur ## https -both HTTP and HTTPS are accepted by default, but letting a [reverse proxy](#reverse-proxy) handle the https/tls/ssl would be better (probably more secure by default) +both HTTP and HTTPS are accepted by default, but please ignore copyparty's built-in https/tls support and instead use a [reverse proxy](#reverse-proxy) to handle https/tls/ssl -copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well -- but note that HTTP/1 is usually faster than both HTTP/2 and HTTP/3 +* reverseproxies do a better job following [best practices](https://cipherlist.eu/) meaning they are more secure, and probably also have higher performance +* also, copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well -- but note that HTTP/1 is usually faster than both HTTP/2 and HTTP/3 if [cfssl](https://github.com/cloudflare/cfssl/releases/latest) is installed, copyparty will automatically create a CA and server-cert on startup * the certs are written to `--crt-dir` for distribution, see `--help` for the other `--crt` options @@ -3141,6 +3144,11 @@ to install cfssl on windows: * rename them to `cfssl.exe`, `cfssljson.exe`, `cfssl-certinfo.exe` * put them in PATH, for example inside `c:\windows\system32` +if you really wanna give copyparty an existing TLS certificate then do one of the following: +* `--no-crt --cert server.pem` where `server.pem` is a concatenation of key + cert + chain (in that order), or... +* `--no-crt --cert server.crt --certkey server.key` where `server.key` is the key, and `server.crt` is a concatenation of cert + chain (in that order) +* file-extensions don't matter, but all files are expected to be [PEM-style](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/res/insecure.pem) + # recovering from crashes diff --git a/bin/hooks/README.md b/bin/hooks/README.md index c0ce7bf1..04574248 100644 --- a/bin/hooks/README.md +++ b/bin/hooks/README.md @@ -38,6 +38,10 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin * this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty +# more upload stuff +* combine [reloc-by-wark-xbu.py](reloc-by-wark-xbu.py) and [reloc-by-wark-xau.py](reloc-by-wark-xau.py) to rename uploads to the checksum of the file contents + + # on message * [wget.py](wget.py) lets you download files by POSTing URLs to copyparty * [wget-i.py](wget-i.py) is an import-safe modification of this hook (starts 140x faster, but higher chance of bugs) diff --git a/bin/hooks/reloc-by-wark-xau.py b/bin/hooks/reloc-by-wark-xau.py new file mode 100644 index 00000000..3779a350 --- /dev/null +++ b/bin/hooks/reloc-by-wark-xau.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +import os +import sys + + +_ = r""" +rename incoming uploads according to the "wark" (the file identifier) +which is basically but not exactly a sha512 hash of the file contents + +NOTE: this does NOT work with up2k uploads (dragdrop into browser); + combine this hook with reloc-by-wark-xbu.py to fix that + +example usage as global config: + -e2d --xau I,c,bin/hooks/reloc-by-wark-xau.py + +parameters explained, + e2d = enable up2k database (mandatory for xau hooks) + xau = execute before upload + I = import this hook for performance; do not fork / subprocess + c = "check"; reject upload if this hook crashes due to a bug + +example usage as a volflag (per-volume config): + -v srv/inc:inc:r:rw,ed:c,e2d,xau=I,c,bin/hooks/reloc-by-wark-xau.py + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + (share filesystem-path srv/inc as volume /inc, + readable by everyone, read-write for user 'ed', + running this plugin on all uploads with the params explained above) + +example usage as a volflag in a copyparty config file: + [/inc] + srv/inc + accs: + r: * + rw: ed + flags: + e2d, xau: I,c,bin/hooks/reloc-by-wark-xau.py +""" + + +def main(inf): + if inf.get("wark"): + # this is an up2k upload; nothing to be done, just bail + return {} + + abspath = inf["ap"] # filesystem path to the uploaded file + + # we don't have the wark yet so need to calculate it; + # generating a regular sha512 would of course be much easier, + # but then filenames would be different depending on how the + # file was uploaded (laaame) so let's do it the hard way + + # use the standard up2k-salt which nobody ever changes: + salt = "hunter2" + + # to generate the wark we'll need some functions from copyparty; + # follow the trail to the copyparty module and grab them from there: + + import inspect + + libpath = inspect.getfile(inf["log"]) + libpath = os.path.dirname(os.path.dirname(libpath)) + sys.path.insert(0, libpath) + + from copyparty.up2k import up2k_hashlist_from_file, up2k_wark_from_hashlist + + chunklist, st = up2k_hashlist_from_file(abspath) + wark = up2k_wark_from_hashlist(salt, st.st_size, chunklist) + + # okay nice + # the rest of the code below is just copied from reloc-by-wark-xbu.py + # ------------------------------------------------------------------------- + + # grab the original filename from the vpath... + vdir, fn = os.path.split(inf["vp"]) + + # ...to retain the original file extension, if any + try: + fn, ext = fn.rsplit(".", 1) + except: + ext = "" + + # use the first 16 characters; 12 bytes of entropy, + # roughly one collision for every 26 million files + fn = wark[:16] + + if ext: + ext = ext.lower() + fn += "." + ext + + return {"reloc": {"fn": fn}} diff --git a/bin/hooks/reloc-by-wark-xbu.py b/bin/hooks/reloc-by-wark-xbu.py new file mode 100644 index 00000000..8576ad59 --- /dev/null +++ b/bin/hooks/reloc-by-wark-xbu.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +import os + + +_ = r""" +rename incoming uploads according to the "wark" (the file identifier) +which is basically but not exactly a sha512 hash of the file contents + +NOTE: this only works for up2k uploads (dragdrop into browser); + combine this with reloc-by-wark-xau.py to cover the other protocols + +example usage as global config: + -e2d --xbu I,c,bin/hooks/reloc-by-wark-xbu.py + +parameters explained, + e2d = enable up2k database (mandatory for xbu hooks) + xbu = execute before upload + I = import this hook for performance; do not fork / subprocess + c = "check"; reject upload if this hook crashes due to a bug + +example usage as a volflag (per-volume config): + -v srv/inc:inc:r:rw,ed:c,e2d,xbu=I,c,bin/hooks/reloc-by-wark-xbu.py + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + (share filesystem-path srv/inc as volume /inc, + readable by everyone, read-write for user 'ed', + running this plugin on all uploads with the params explained above) + +example usage as a volflag in a copyparty config file: + [/inc] + srv/inc + accs: + r: * + rw: ed + flags: + e2d, xbu: I,c,bin/hooks/reloc-by-wark-xbu.py +""" + + +def main(inf): + wark = inf.get("wark") + if not wark: + # not an up2k upload, so we don't have the hash; + + # option 1: let upload proceed with original filename + return {} + + # option 2: reject the upload + return {"rejectmsg": "only up2k uploads are allowed in this volume"} + + # grab the original filename from the vpath... + vdir, fn = os.path.split(inf["vp"]) + + # ...to retain the original file extension, if any + try: + fn, ext = fn.rsplit(".", 1) + except: + ext = "" + + # use the first 16 characters; 12 bytes of entropy, + # roughly one collision for every 26 million files + fn = wark[:16] + + if ext: + ext = ext.lower() + fn += "." + ext + + return {"reloc": {"fn": fn}} diff --git a/bin/u2c.py b/bin/u2c.py index 25877a96..f0e3f8de 100755 --- a/bin/u2c.py +++ b/bin/u2c.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 from __future__ import print_function, unicode_literals -S_VERSION = "2.19" -S_BUILD_DT = "2026-01-18" +S_VERSION = "2.20" +S_BUILD_DT = "2026-04-22" """ u2c.py: upload to copyparty @@ -1184,7 +1184,7 @@ class Ctl(object): handshake(self.ar, file, False) def cleanup_vt100(self): - if VT100: + if VT100 and not self.ar.ns: ss.scroll_region(None) else: eprint("\033]9;4;0\033\\") diff --git a/contrib/package/arch/PKGBUILD b/contrib/package/arch/PKGBUILD index 062c794e..3983488a 100644 --- a/contrib/package/arch/PKGBUILD +++ b/contrib/package/arch/PKGBUILD @@ -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.13" +pkgver="1.20.14" pkgrel=1 pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, SFTP, FTP, TFTP, zeroconf, media indexer, thumbnails++" arch=("any") @@ -24,7 +24,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}/copyparty.conf" ) -sha256sums=("b2af9250f7ef97a5df26df412ee082c6d2be0f0cd31d579b4fbb6aa2f3e5c271") +sha256sums=("8783dc8390be17673d306f424e7a28dd9f9b4fce005e35734d30c1b296707c12") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/makedeb-mpr/PKGBUILD b/contrib/package/makedeb-mpr/PKGBUILD index 347f9f67..770398d1 100644 --- a/contrib/package/makedeb-mpr/PKGBUILD +++ b/contrib/package/makedeb-mpr/PKGBUILD @@ -2,7 +2,7 @@ pkgname=copyparty -pkgver=1.20.13 +pkgver=1.20.14 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=("b2af9250f7ef97a5df26df412ee082c6d2be0f0cd31d579b4fbb6aa2f3e5c271") +sha256sums=("8783dc8390be17673d306f424e7a28dd9f9b4fce005e35734d30c1b296707c12") build() { cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web" diff --git a/contrib/package/nix/copyparty/pin.json b/contrib/package/nix/copyparty/pin.json index bff86f37..7ed8f8f1 100644 --- a/contrib/package/nix/copyparty/pin.json +++ b/contrib/package/nix/copyparty/pin.json @@ -1,5 +1,5 @@ { - "url": "https://github.com/9001/copyparty/releases/download/v1.20.13/copyparty-1.20.13.tar.gz", - "version": "1.20.13", - "hash": "sha256-sq+SUPfvl6XfJt9BLuCCxtK+DwzTHVebT7tqovPlwnE=" + "url": "https://github.com/9001/copyparty/releases/download/v1.20.14/copyparty-1.20.14.tar.gz", + "version": "1.20.14", + "hash": "sha256-h4Pcg5C+F2c9MG9CTnoo3Z+bT84AXjVzTTDBspZwfBI=" } \ No newline at end of file diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f7363f1f..9d335801 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -70,7 +70,10 @@ from .util import ( expand_osenv_noop, expand_osenv_s, has_resource, + list_ips, + list_nics, load_resource, + lprint, min_ex, pybin, read_utf8, @@ -98,7 +101,6 @@ except: HAVE_SSL = False u = unicode -printed: list[str] = [] zsid = uuid.uuid4().urn[4:] CFG_DEF = [os.environ.get("PRTY_CONFIG", "")] @@ -174,16 +176,6 @@ class BasicDodge11874( super(BasicDodge11874, self).__init__(*args, **kwargs) -def lprint(*a: Any, **ka: Any) -> None: - eol = ka.pop("end", "\n") - txt: str = " ".join(unicode(x) for x in a) + eol - printed.append(txt) - if not VT100: - txt = RE_ANSI.sub("", txt) - - print(txt, end="", **ka) - - def warn(msg: str) -> None: lprint("\033[1mwarning:\033[0;33m {}\033[0m\n".format(msg)) @@ -644,6 +636,9 @@ def get_sects(): \033[32m-i fd:\033[33m3\033[0m uses the socket passed to copyparty on file descriptor 3 \033[33m-p\033[0m (tcp ports) is ignored for unix-sockets and FDs + + \033[33m--list-nics\033[0m shows all network adapters (also offline ones); + \033[33m--list-ips\033[0m shows all LAN IPs """ ), ], @@ -969,6 +964,7 @@ def get_sects(): values for --urlform: \033[36mstash\033[35m dumps the data to file and returns length + checksum \033[36msave,get\033[35m dumps to file and returns the page like a GET + \033[36mget\033[35m ignores the message and returns the page like a GET \033[36mprint \033[35m prints the data to log and returns an error \033[36mprint,xm \033[35m prints the data to log and returns --xm output \033[36mprint,get\033[35m prints the data to log and returns GET\033[0m @@ -1029,6 +1025,23 @@ def get_sects(): """ ), ], + [ + "rlo", + "logrotate format", + dedent( + """ + a logrotate-counter is added if the logfile filename is taken; + by default at the end, unless \033[32m%R\033[0m is somewhere in the \033[36m-lo\033[0m pattern, + for example: -lo /var/log/cpp/%Y-%m-%d%R.txt + + \033[36m--rlo\033[0m configures the logrotate format; examples: + .1 = when necessary, append a dot followed by a single digit + .1! = counter is always added, even when not necessary + -3 = a hyphen followed by three-digit counter + (blank) = disable counter; overwrite existing logfile + """ + ), + ], [ "ls", "volume inspection", @@ -1216,7 +1229,7 @@ def add_general(ap, nc, srvname): ap2.add_argument("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)") ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS) ap2.add_argument("--site", metavar="URL", type=u, default="", help="public URL to assume when creating links; example: [\033[32mhttps://example.com/\033[0m]") - ap2.add_argument("--env-expand", metavar="N", type=int, default=-1, help="syntax to expect for environment-variables to expand in config-files; [\033[32m0\033[0m]=disable, [\033[32m1\033[0m]=$VAR (old syntax (scary)), [\033[32m2\033[0m]=${VAR} (new syntax (recommended))") + ap2.add_argument("--env-expand", metavar="N", type=int, default=-1, help="expand environment-variables in config-files? [\033[32m0\033[0m]=no, [\033[32m1\033[0m]=$VAR (old scary syntax), [\033[32m2\033[0m]=${VAR} (new recommended syntax); default is new-syntax with panic if old-syntax is seen") ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]") ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit") ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)") @@ -1350,13 +1363,16 @@ def add_network(ap): ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0.0, help="debug: socket write delay in seconds") ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0.0, help="debug: response delay in seconds") ap2.add_argument("--rsp-jtr", metavar="SEC", type=float, default=0.0, help="debug: response delay, random duration 0..\033[33mSEC\033[0m") + ap2.add_argument("--list-nics", action="store_true", help="debug: list detected network adapters") + ap2.add_argument("--list-ips", action="store_true", help="debug: list detected LAN IPs") def add_tls(ap, cert_path): ap2 = ap.add_argument_group("SSL/TLS options") ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext") ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls") - ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain") + ap2.add_argument("--cert", metavar="PATH", type=u, default=cert_path, help="path to file containing a concatenation of TLS key and certificate chain (if \033[33m--certkey\033[0m is not set), or just the certificate chain (if \033[33m--certkey\033[0m is set)") + ap2.add_argument("--certkey", metavar="PATH", type=u, default="", help="path to file containing just the certificate key; if this is set, then \033[33m--cert\033[0m should only contain the certificate chain") ap2.add_argument("--ssl-ver", metavar="LIST", type=u, default="", help="set allowed ssl/tls versions; [\033[32mhelp\033[0m] shows available versions; default is what your python version considers safe") ap2.add_argument("--ciphers", metavar="LIST", type=u, default="", help="set allowed ssl/tls ciphers; [\033[32mhelp\033[0m] shows available ciphers") ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info") @@ -1588,10 +1604,10 @@ def add_stats(ap): def add_yolo(ap): ap2 = ap.add_argument_group("yolo options") - ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests") + ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests; \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("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI") - ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET") + ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET -- \033[1;31mDANGEROUS\033[0m, removes csrf protection") ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)") ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets") @@ -1689,6 +1705,7 @@ def add_logging(ap): ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages") ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)") ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color") + ap2.add_argument("--rlo", metavar="TXT", type=u, default=".1", help="logrotate counter format; see \033[33m--help-rlo\033[0m") ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster") @@ -1888,7 +1905,7 @@ def add_ui(ap, retry: int): ap2.add_argument("--grid", action="store_true", default="true", help="show grid/thumbnails by default (volflag=grid)") ap2.add_argument("--gsel", action="store_true", default="true", help="select files in grid by ctrl-click (volflag=gsel)") ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC") - ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="4", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)") + ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="4", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, 6, 6c, 7, 7c, fuzzy (see UI)") ap2.add_argument("--gauto", metavar="PERCENT", type=int, default=0, help="switch to gridview if more than \033[33mPERCENT\033[0m of files are pics/vids; 0=disabled") ap2.add_argument("--rcm", metavar="TXT", default="yy", help="rightclick-menu; two yes/no options: 1st y/n is enable-custom-menu, 2nd y/n is enable-double") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...") @@ -2114,6 +2131,14 @@ def main(argv: Optional[list[str]] = None) -> None: print("\n".join("%8s %s" % (k, v) for k, v in sorted(MIMES.items()))) sys.exit(0) + if "--list-ips" in argv: + print("\n".join(str(x) for x in sorted(list_ips()))) + sys.exit(0) + + if "--list-nics" in argv: + print("\n".join(str(x) for x in sorted(list_nics(True).items()))) + sys.exit(0) + if EXE: print("pybin: {}\n".format(pybin), end="") @@ -2301,7 +2326,7 @@ def main(argv: Optional[list[str]] = None) -> None: # signal.signal(signal.SIGINT, sighandler) - SvcHub(al, dal, argv, "".join(printed)).run() + SvcHub(al, dal, argv).run() if __name__ == "__main__": diff --git a/copyparty/__version__.py b/copyparty/__version__.py index a7229506..9d2458c4 100644 --- a/copyparty/__version__.py +++ b/copyparty/__version__.py @@ -1,8 +1,8 @@ # coding: utf-8 -VERSION = (1, 20, 13) +VERSION = (1, 20, 14) CODENAME = "sftp is fine too" -BUILD_DT = (2026, 3, 23) +BUILD_DT = (2026, 4, 24) S_VERSION = ".".join(map(str, VERSION)) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) diff --git a/copyparty/cert.py b/copyparty/cert.py index 18859536..54c523a9 100644 --- a/copyparty/cert.py +++ b/copyparty/cert.py @@ -51,9 +51,12 @@ def ensure_cert(log: "RootLogger", args) -> None: with open(args.cert, "wb") as f: f.write(cert_insec) + if args.certkey and not os.path.isfile(args.certkey): + raise Exception("certificate-key file does not exist: " + args.certkey) + with open(args.cert, "rb") as f: buf = f.read() - o1 = buf.find(b" PRIVATE KEY-") + o1 = buf.find(b" PRIVATE KEY-") if not args.certkey else 0 o2 = buf.find(b" CERTIFICATE-") m = "unsupported certificate format: " if o1 < 0: @@ -252,7 +255,7 @@ def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]): if args.http_only: return - if args.no_crt or not HAVE_CFSSL: + if args.no_crt or args.certkey or not HAVE_CFSSL: ensure_cert(log, args) return diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 3912ade9..ccbd423c 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -616,6 +616,8 @@ class Ftpd(object): print(t.format(pybin)) sys.exit(1) + if self.args.certkey: + h1.keyfile = self.args.certkey h1.certfile = self.args.cert h1.tls_control_required = True h1.tls_data_required = True diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 8b95507b..74540e7c 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -4897,15 +4897,8 @@ class HttpCli(object): dl_id, ) sent = (eof - ofs) - remains - ofs = eof - remains - f.seek(ofs) - - try: - st2 = os.stat(open_args[0]) - if st.st_ino == st2.st_ino: - st = st2 # for filesize - except: - pass + f.seek(eof - remains) + ofs = f.tell() gone = 0 unsent = False @@ -4925,6 +4918,7 @@ class HttpCli(object): t_fd = t_ka = now self.s.sendall(buf) sent += len(buf) + ofs += len(buf) unsent = False dls[dl_id] = (time.time(), sent) continue @@ -4935,14 +4929,9 @@ class HttpCli(object): self.s.send(b"\x00") if t_fd < now - sec_fd: try: - st2 = os.stat(open_args[0]) - szd = st2.st_size - st.st_size - if ( - st2.st_ino != st.st_ino - or st2.st_size < sent - or szd < 0 - or unsent - ): + st2 = os.stat(abspath) + szd = st2.st_size - ofs + if st2.st_ino != st.st_ino or szd < 0 or unsent: assert f # !rm # open new file before closing previous to avoid toctous (open may fail; cannot null f before) f2 = open_nolock(*open_args) @@ -4950,15 +4939,15 @@ class HttpCli(object): f = f2 f.seek(0, os.SEEK_END) eof = f.tell() - if eof < sent: - ofs = sent = 0 # shrunk; send from start + if eof < ofs: + ofs = 0 # shrunk; send from start zb = b"\n\n*** file size decreased -- rewinding to the start of the file ***\n\n" self.s.sendall(zb) if ofs0 < 0 and eof > -ofs0: ofs = eof + ofs0 - else: - ofs = sent # just new fd? resume from same ofs + # else: probably just new fd; resume from same ofs f.seek(ofs) + ofs = f.tell() self.log("reopened at byte %d: %r" % (ofs, abspath), 6) unsent = False gone = 0 @@ -5846,6 +5835,7 @@ class HttpCli(object): excl, target = (target.split("/", 1) + [""])[:2] sub = self.gen_tree("/".join([top, excl]).strip("/"), target, dk) ret["k" + quotep(excl)] = sub + dk = "" vfs = self.asrv.vfs dk_sz = False @@ -5885,16 +5875,16 @@ class HttpCli(object): else: dirs = exclude_dotfiles(dirs) - dirs = [quotep(x) for x in dirs if x != excl] - if dk_sz and fsroot: kdirs = [] fsroot_ = os.path.join(fsroot, "") - for dn in dirs: + for dn in [x for x in dirs if x != excl]: ap = fsroot_ + dn zs = self.gen_fk(2, self.args.dk_salt, ap, 0, 0)[:dk_sz] - kdirs.append(dn + "?k=" + zs) + kdirs.append(quotep(dn) + "?k=" + zs) dirs = kdirs + else: + dirs = [quotep(x) for x in dirs if x != excl] if vfs_virt: for x in vfs_virt: diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py index 30d9d87d..a463943f 100644 --- a/copyparty/httpconn.py +++ b/copyparty/httpconn.py @@ -157,7 +157,7 @@ class HttpConn(object): try: assert ssl # type: ignore # !rm ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - ctx.load_cert_chain(self.args.cert) + ctx.load_cert_chain(self.args.cert, self.args.certkey) if self.args.ssl_ver: ctx.options &= ~self.args.ssl_flags_en ctx.options |= self.args.ssl_flags_de diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 5f7441c3..fc0a9db9 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -57,6 +57,7 @@ from .util import ( HAVE_PSUTIL, HAVE_SQLITE3, HAVE_ZMQ, + LOG, RE_ANSI, URL_BUG, UTC, @@ -74,6 +75,7 @@ from .util import ( load_ipr, load_ipu, lock_file, + lprinted, min_ex, mp, odfusion, @@ -126,7 +128,6 @@ class SvcHub(object): args: argparse.Namespace, dargs: argparse.Namespace, argv: list[str], - printed: str, ) -> None: self.args = args self.dargs = dargs @@ -208,8 +209,24 @@ class SvcHub(object): else: self.log = self._log_enabled + self.lo1 = self.lo2 = "" if args.lo: - self._setup_logfile(printed) + if "%" in args.lo and "%R" not in args.lo: + args.lo += "%R" + if not args.rlo: + args.lo = args.lo.replace("%R", "") + try: + self.lo1, self.lo2 = args.lo.split("%R") + except: + self.lo1 = args.lo + try: + self.rot_fmt = "%%s%s%%0%sd%s" % (args.rlo[:1], args.rlo[1:2], self.lo2) + except: + self.rot_fmt = "%s.%d" + self._setup_logfile() + + LOG[0] = self.log + lprinted[:] = [] lg = logging.getLogger() lh = HLog(self.log) @@ -1229,6 +1246,9 @@ class SvcHub(object): for x in [x.split(" ") for x in al.sftp_key or []] } + if not al.certkey: + al.certkey = None + mte = ODict.fromkeys(DEF_MTE.split(","), True) al.mte = odfusion(mte, al.mte) @@ -1353,7 +1373,7 @@ class SvcHub(object): def _logname(self) -> str: dt = datetime.now(self.tz) - fn = str(self.args.lo) + fn = str(self.lo1) for fs in "YmdHMS": fs = "%" + fs if fs in fn: @@ -1361,16 +1381,18 @@ class SvcHub(object): return fn - def _setup_logfile(self, printed: str) -> None: - base_fn = fn = sel_fn = self._logname() - do_xz = fn.lower().endswith(".xz") - if fn != self.args.lo: - ctr = 0 + def _setup_logfile(self) -> None: + base_fn = fn = self._logname() + sel_fn = fn + self.lo2 + do_xz = sel_fn.lower().endswith(".xz") + if "%R" in self.args.lo: # yup this is a race; if started sufficiently concurrently, two # copyparties can grab the same logfile (considered and ignored) - while os.path.exists(sel_fn): - ctr += 1 - sel_fn = "{}.{}".format(fn, ctr) + for n in range(9999): + if n or "!" in self.args.rlo: + sel_fn = self.rot_fmt % (fn, n) + if not os.path.exists(sel_fn): + break fn = sel_fn try: @@ -1401,7 +1423,7 @@ class SvcHub(object): argv = ['"{}"'.format(x) for x in argv] msg = "[+] opened logfile [{}]\n".format(fn) - printed += msg + printed = "".join(lprinted) + msg t = "t0: {:.3f}\nargv: {}\n\n{}" lh.write(t.format(self.E.t0, " ".join(argv), printed)) self.logf = lh @@ -1673,7 +1695,7 @@ class SvcHub(object): def _set_next_day(self, dt: datetime) -> None: if self.cday and self.logf and self.logf_base_fn != self._logname(): self.logf.close() - self._setup_logfile("") + self._setup_logfile() self.cday = dt.day self.cmon = dt.month diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index 73fc7448..1518381d 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -23,6 +23,7 @@ from .util import ( atomic_move, chkcmd, get_adapters, + list_nics, min_ex, sunpack, termsize, @@ -461,23 +462,7 @@ class TcpSrv(object): def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]: listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))] - nics = get_adapters(True) - eps: dict[str, Netdev] = {} - for nic in nics: - for nip in nic.ips: - ipa = nip.ip[0] if ":" in str(nip.ip) else nip.ip - sip = "{}/{}".format(ipa, nip.network_prefix) - nd = Netdev(sip, nic.index or 0, nic.nice_name, "") - eps[sip] = nd - try: - idx = socket.if_nametoindex(nd.name) - if idx and idx != nd.idx: - t = "netdev idx mismatch; ifaddr={} cpython={}" - self.log("tcpsrv", t.format(nd.idx, idx), 3) - nd.idx = idx - except: - pass - + eps = list_nics() netlist = str(sorted(eps.items())) if netlist == self.netlist and self.netdevs: return {} diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index eb039e3f..397f055d 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -789,6 +789,10 @@ class ThumbSrv(object): if not ret: return + if "vc" not in ret and "ac" in ret: + # audio in a video trenchcoat + return self.conv_spec(abspath, tpath, fmt, vn) + ext = abspath.rsplit(".")[-1].lower() if ext in ["h264", "h265"] or ext in self.fmt_ffi: seek: list[bytes] = [] diff --git a/copyparty/up2k.py b/copyparty/up2k.py index c3d7901d..6245085a 100644 --- a/copyparty/up2k.py +++ b/copyparty/up2k.py @@ -3357,7 +3357,7 @@ class Up2k(object): job["size"], job["addr"], job["at"], - None, + [dwark], ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: @@ -4069,7 +4069,7 @@ class Up2k(object): sz, ip, at or time.time(), - None, + [dwark], ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: @@ -5251,7 +5251,7 @@ class Up2k(object): job["size"], job["addr"], job["t0"], - None, + [job["dwrk"]], ) t = hr.get("rejectmsg") or "" if t or hr.get("rc") != 0: @@ -5708,6 +5708,31 @@ def up2k_chunksize(filesize: int) -> int: stepsize *= mul +def up2k_hashlist_from_file(path: str) -> tuple[list[str], os.stat_result]: + """not used by copyparty itself, only by some hooks""" + st = bos.stat(path) + fsz = st.st_size + csz = up2k_chunksize(fsz) + ret = [] + with open(fsenc(path), "rb", 256 * 1024) as f: + while fsz > 0: + hashobj = hashlib.sha512() + rem = min(csz, fsz) + fsz -= rem + while rem > 0: + buf = f.read(min(rem, 64 * 1024)) + if not buf: + raise Exception("EOF at " + str(f.tell())) + + hashobj.update(buf) + rem -= len(buf) + + digest = hashobj.digest()[:33] + ret.append(ub64enc(digest).decode("ascii")) + + return ret, st + + def up2k_wark_from_hashlist(salt: str, filesize: int, hashes: list[str]) -> str: """server-reproducible file identifier, independent of name or location""" values = [salt, str(filesize)] + hashes diff --git a/copyparty/util.py b/copyparty/util.py index c8a8dfc1..1f7b9cec 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -62,6 +62,20 @@ def noop(*a, **ka): pass +def lprint(*a: "Any", **ka: "Any") -> None: + eol = ka.pop("end", "\n") + txt = " ".join(unicode(x) for x in a) + eol + lprinted.append(txt) + if not VT100 and "\033" in txt: + txt = RE_ANSI.sub("", txt) + + print(txt, end="", **ka) + + +lprinted: list[str] = [] +LOG: list["Callable[..., None]"] = [lprint] + + try: from datetime import datetime, timezone @@ -788,7 +802,7 @@ def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool) if log: log(t, 3) else: - print(t) + LOG[0]("#", t) return buf.decode("utf-8", "replace") t = "ERROR: The file [%s] is not using the UTF-8 character encoding, and cannot be loaded. The first unreadable character was byte %r at offset %d. Please convert this file to UTF-8 by opening the file in your text-editor and saving it as UTF-8." @@ -796,7 +810,7 @@ def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool) if log: log(t, 3) else: - print(t) + LOG[0]("#", t) raise NotUTF8(t) @@ -1573,12 +1587,18 @@ def _expand_osenv_c(txt) -> str: ret = zsl[0] for v in zsl[1:]: if "}" not in v: - raise Exception("missing '}' after %r in config-value %r" % (v, txt)) + t = "missing '}' after %r in config-value %r" % (v, txt) + LOG[0]("ERROR:", t) + raise Exception(t) a, b = v.split("}", 1) try: ret += os.environ[a] + b + continue except: - raise Exception("env-var %r not defined; config-value %r" % (a, txt)) + pass + t = "env-var %r not defined; config-value %r" % (a, txt) + LOG[0]("ERROR:", t) + raise Exception(t) return ret @@ -1595,8 +1615,24 @@ def expand_osenv_cs(txt) -> str: b = expand_osenv_s(txt) if a == b: return a - t = "config-value %r is using the old syntax for environment-variables; choose one of the following options:\noption 1: update the config-value to the new syntax, ${VAR} instead of $VAR or %%VAR%%\noption 2: tell copyparty to allow the old syntax with global-option --env-expand 1 (risky)\noption 3: tell copyparty to only use the new syntax (and not expand this variable) with global-option --env-expand 2\noption 4: disable all environment-variable expansions with PRTY_NO_ENVEXPAND=1 or global-option --env-expand 0" - raise Exception(t % (txt,)) + + t = "config-value %r is using old syntax for environment-variables; choose one of the following:\noption 1: update the config-value to the new syntax; ${VAR} instead of $VAR or %%VAR%%\noption 2: allow and expand old-syntax with global-option --env-expand 1 (risky)\noption 3: ignore/disable expansion of old-syntax with global-option --env-expand 2\noption 4: disable all env-var expansions by setting env-var PRTY_NO_ENVEXPAND=1" + t = t % (txt,) + LOG[0]("WARNING:", t) + + try: + _, _ = txt.split("$") + zs = r"\$(LOGS_DIRECTORY|XDG_[A-Z]+_HOME|XDG_[A-Z]+_DIR)\b" + txt = re.sub(zs, r"${\1}", txt) + + a = expand_osenv_c(txt) + b = expand_osenv_s(txt) + if a == b: + return a + except: + pass + + raise Exception(t) def rice_tid() -> str: @@ -3140,6 +3176,31 @@ def list_ips() -> list[str]: return list(ret) +def list_nics(alll: bool = False) -> dict[str, Netdev]: + nics = get_adapters(alll) + eps: dict[str, Netdev] = {} + for nic in nics: + name = nic.nice_name + try: + idx = socket.if_nametoindex(name) + if idx and idx != nic.index: + LOG[0]("#", "nic-idx mismatch; ifaddr=%r libc=%r" % (nic.index, idx), 3) + except: + idx = nic.index + + for nip in nic.ips: + ipa = nip.ip[0] if ":" in str(nip.ip) else nip.ip + sip = "%s/%s" % (ipa, nip.network_prefix) + nd = Netdev(sip, idx or 0, name, "") + eps[sip] = nd + + if alll and not nic.ips: + zs = "no-ip-%s" % (idx,) + eps[zs] = Netdev(zs, idx or 0, name, "") + + return eps + + def build_netmap(csv: str, defer_mutex: bool = False): csv = csv.lower().strip() @@ -4097,8 +4158,11 @@ def _runhook( "src": src, } if txt: - ja["txt"] = txt[0] - ja["body"] = txt[1] + if src in ("xm", "xban"): + ja["txt"] = txt[0] + ja["body"] = txt[1] + else: + ja["wark"] = txt[0] # acshually the dwark but less confusing if imp: ja["log"] = log mod = loadpy(acmd[0], False) @@ -4211,7 +4275,7 @@ def runhook( else: ret[k] = v except Exception as ex: - (log or print)("hook: %r, %s" % (ex, ex)) + (log or print)("hook failed; %s:\n%s" % (ex, min_ex())) if ",c," in "," + cmd: return {"rc": 1} break diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index e4cbb15e..da8d4809 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1227,16 +1227,20 @@ ebi('op_cfg').innerHTML = ( '