From 4642d32366e30c131d933c8bd0a519b69f4d3fff Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 26 Dec 2025 17:21:58 +0000 Subject: [PATCH] dedicated tcp-port for tricky webdav clients; there are webdav-clients (for example zotero) which fully pretend to be a graphical webbrowser, going as far as faking the firefox user-agent, which means they get the graphical login-page instead of 401 (basic-authentication challenge) these webdav-clients unfortunately also refuse to send credentials unless they get 401'd, so until now it was impossible to connect them the obvious solution of adding a suffix to links in PROPFIND responses is a nonstarter; * windows-webdav ignores the property and shows the as the filename, so this would show up in windows explorer and probably make most file operations impossible * rclone is the opposite; ignores the property (so it wouldn't even see the suffix) and builds its own URL from the so we need a new weapon: gloabl-option dav-port makes copyparty listen on another port which is dedicated to webdav-clients that otherwise don't look the part global-option p-nodav is the opposite; tags a listening-port as only accepting connections from graphical browsers, just in case closes #1142 --- copyparty/__main__.py | 4 +++- copyparty/httpcli.py | 25 +++++++++++++------------ copyparty/svchub.py | 4 ++++ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 0fdc7836..edd05f2b 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1440,7 +1440,9 @@ def add_webdav(ap): ap2.add_argument("--dav-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)") ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)") ap2.add_argument("--dav-ua1", metavar="PTN", type=u, default=r" kioworker/", help="regex of user-agents which ARE webdav-clients, and expect 401 from GET requests; disable with [\033[32mno\033[0m] or blank") + ap2.add_argument("--dav-port", metavar="P", type=int, default=0, help="additional port to listen on for misbehaving webdav clients which pretend they are graphical browsers; an alternative/supplement to dav-ua1") ap2.add_argument("--ua-nodav", metavar="PTN", type=u, default=r"^(Mozilla/|NetworkingExtension/|com\.apple\.WebKit)", help="regex of user-agents which are NOT webdav-clients") + ap2.add_argument("--p-nodav", metavar="P,P", type=u, default="", help="server-ports (comma-sep.) which are NOT webdav-clients; an alternative/supplement to ua-nodav") def add_tftp(ap): @@ -1558,7 +1560,7 @@ def add_safety(ap): ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to turn something into a dotfile") ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings") ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme/preadme.md into directory listings") - ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise)") + ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise). \033[1;31mWARNING:\033[0m Not compatible with WebDAV") ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m") ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)") ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index bc2ec68b..0c17fa8b 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -5522,18 +5522,19 @@ class HttpCli(object): # most webdav clients will not send credentials until they # get 401'd, so send a challenge if we're Absolutely Sure # that the client is not a graphical browser - if ( - rc == 403 - and self.uname == "*" - and "sec-fetch-site" not in self.headers - and self.cookies.get("js") != "y" - and ( - not self.args.ua_nodav.search(self.ua) - or (self.args.dav_ua1 and self.args.dav_ua1.search(self.ua)) - ) - ): - rc = 401 - self.out_headers["WWW-Authenticate"] = 'Basic realm="a"' + if rc == 403 and self.uname == "*": + sport = self.s.getsockname()[1] + if self.args.dav_port == sport or ( + "sec-fetch-site" not in self.headers + and self.cookies.get("js") != "y" + and sport not in self.args.p_nodav + and ( + not self.args.ua_nodav.search(self.ua) + or (self.args.dav_ua1 and self.args.dav_ua1.search(self.ua)) + ) + ): + rc = 401 + self.out_headers["WWW-Authenticate"] = 'Basic realm="a"' t = t.format(self.args.SR) qv = quotep(self.vpaths) + self.ourlq() diff --git a/copyparty/svchub.py b/copyparty/svchub.py index caa3ec9f..bdc6af1b 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -220,6 +220,10 @@ class SvcHub(object): self.log("root", t.format(args.j), c=3) args.no_fpool = True + args.p_nodav = [int(x.strip()) for x in args.p_nodav.split(",") if x] + if args.dav_port and args.dav_port not in args.p: + args.p.append(args.dav_port) + for name, arg in ( ("iobuf", "iobuf"), ("s-rd-sz", "s_rd_sz"),