From f9502c3df35a1c8eded8eb192b09fc11e8a7880d Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 3 Aug 2025 23:27:53 +0000 Subject: [PATCH] add idp-cookie; for high-traffic / glitchy auth servers --- README.md | 2 ++ copyparty/__main__.py | 1 + copyparty/authsrv.py | 5 ++++- copyparty/httpcli.py | 16 ++++++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 22d3a0d7..f7c27be8 100644 --- a/README.md +++ b/README.md @@ -1894,6 +1894,8 @@ you can disable the built-in password-based login system, and instead replace it * the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header +* if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party + some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex) there is a [docker-compose example](./docs/examples/docker/idp-authelia-traefik) which is hopefully a good starting point (alternatively see [./docs/idp.md](./docs/idp.md) if you're the DIY type) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 9e406bea..d92089d0 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1150,6 +1150,7 @@ def add_auth(ap): ap2.add_argument("--idp-db", metavar="PATH", type=u, default=idp_db, help="where to store the known IdP users/groups (if you run multiple copyparty instances, make sure they use different DBs)") ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP") ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)") + ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)") ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app") ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins") ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index afbc24ba..44629e62 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2869,7 +2869,10 @@ class AuthSrv(object): n = [] q = "insert into us values (?,?,?)" - for uname in self.acct: + accs = list(self.acct) + if self.args.idp_h_usr and self.args.idp_cookie: + accs.extend(self.idp_accs.keys()) + for uname in accs: if uname not in ases: sid = ub64enc(os.urandom(blen)).decode("ascii") cur.execute(q, (uname, sid, int(time.time()))) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 9463e982..d562485b 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -658,6 +658,9 @@ class HttpCli(object): self.pw = "" self.uname = idp_usr self.html_head += "\n" + zs = self.asrv.ases.get(idp_usr) + if zs: + self.set_idp_cookie(zs) else: self.log("unknown username: %r" % (idp_usr,), 1) @@ -3029,6 +3032,19 @@ class HttpCli(object): return dur > 0, msg + def set_idp_cookie(self, ases) -> None: + k = "cppws" if self.is_https else "cppwd" + ck = gencookie( + k, + ases, + self.args.R, + self.args.cookie_lax, + self.is_https, + self.args.idp_cookie, + "; HttpOnly", + ) + self.out_headers["Set-Cookie"] = ck + def handle_mkdir(self) -> bool: assert self.parser # !rm new_dir = self.parser.require("name", 512)