option to change the "pw" header/uparam name;

useful to force basic-auth and such
This commit is contained in:
ed 2026-01-01 23:59:16 +00:00
parent f08cb25ccc
commit f81d80bcad
6 changed files with 38 additions and 15 deletions

View file

@ -467,6 +467,7 @@ upgrade notes
* can I link someone to a password-protected volume/file by including the password in the URL?
* yes, by adding `?pw=hunter2` to the end; replace `?` with `&` if there are parameters in the URL already, meaning it contains a `?` near the end
* if you have enabled `--usernames` then do `?pw=username:password` instead
* `?pw` can be disabled with `--pw-urlp=A` but this breaks support for many clients
* how do I stop `.hist` folders from appearing everywhere on my HDD?
* by default, a `.hist` folder is created inside each volume for the filesystem index, thumbnails, audio transcodes, and markdown document history. Use the `--hist` global-option or the `hist` volflag to move it somewhere else; see [database location](#database-location)

View file

@ -1286,6 +1286,7 @@ def add_network(ap):
ap2.add_argument("--ipar", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated).\n └─this is reverseproxy-compatible; reads client-IP from 'X-Forwarded-For' if possible, with TCP/Network IP as fallback")
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
ap2.add_argument("--cachectl", metavar="TXT", default="no-cache", help="default-value of the 'Cache-Control' response-header (controls caching in webbrowsers). Default prevents repeated downloading of the same file unless necessary (browser will ask copyparty if the file has changed). Examples: [\033[32mmax-age=604869\033[0m] will cache for 7 days, [\033[32mno-store, max-age=0\033[0m] will always redownload. (volflag=cachectl)")
ap2.add_argument("--http-vary", metavar="TXT", type=u, default="Origin, PW, Cookie", help="value of the 'Vary' response-header; a hint for caching proxies")
ap2.add_argument("--http-no-tcp", action="store_true", help="do not listen on TCP/IP for http/https; only listen on unix-domain-sockets")
if ANYWIN:
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
@ -1350,6 +1351,8 @@ def add_auth(ap):
ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
ap2.add_argument("--pw-hdr", metavar="NAME", type=u, default="pw", help="lowercase name of password-header (NAME: foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
ap2.add_argument("--pw-urlp", metavar="NAME", type=u, default="pw", help="lowercase name of password url-param (?NAME=foo); \033[1;31mWARNING:\033[0m Changing this will break support for many clients")
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)")

View file

@ -2962,9 +2962,11 @@ class AuthSrv(object):
pwds.extend([x.split(":", 1)[1] for x in pwds if ":" in x])
if pwds:
if self.ah.on:
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
zs = r"(\[H\] %s:.*|[?&]%s=)([^&]+)"
zs = zs % (self.args.pw_hdr, self.args.pw_urlp)
else:
zs = r"(\[H\] pw:.*|=)(" + "|".join(pwds) + r")([]&; ]|$)"
zs = r"(\[H\] %s:.*|=)(" % (self.args.pw_hdr,)
zs += "|".join(pwds) + r")([]&; ]|$)"
self.re_pwd = re.compile(zs)

View file

@ -328,7 +328,7 @@ class HttpCli(object):
def run(self) -> bool:
"""returns true if connection can be reused"""
self.out_headers = {
"Vary": "Origin, PW, Cookie",
"Vary": self.args.http_vary,
"Cache-Control": "no-store, max-age=0",
}
@ -682,7 +682,12 @@ class HttpCli(object):
except:
pass
self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
self.pw = (
uparam.get(self.args.pw_urlp)
or self.headers.get(self.args.pw_hdr)
or bauth
or cookie_pw
)
self.uname = (
self.asrv.sesa.get(self.pw)
or self.asrv.iacct.get(self.asrv.ah.hash(self.pw))
@ -1198,6 +1203,7 @@ class HttpCli(object):
return ""
kv = {k: zs for k, zs in self.uparam.items() if k not in rm}
# no reason to consider args.pw_urlp
if "pw" in kv:
pw = self.cookies.get("cppws") or self.cookies.get("cppwd")
if kv["pw"] == pw:
@ -1211,6 +1217,7 @@ class HttpCli(object):
return "?" + "&".join(r)
def ourlq(self) -> str:
# no reason to consider args.pw_urlp
skip = ("pw", "h", "k")
ret = []
for k, v in self.ouparam.items():
@ -1274,12 +1281,15 @@ class HttpCli(object):
proto = "https" if self.is_https else "http"
good_origins = self.args.acao + ["%s://%s" % (proto, host)]
if "pw" in ih or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins:
if (
self.args.pw_hdr in ih
or re.sub(r"(:[0-9]{1,5})?/?$", "", origin) in good_origins
):
good_origin = True
bad_hdrs = ("",)
else:
good_origin = False
bad_hdrs = ("", "pw")
bad_hdrs = ("", self.args.pw_hdr)
# '*' blocks auth through cookies / WWW-Authenticate;
# exact-match for Origin is necessary to unlock those,
@ -1526,10 +1536,11 @@ class HttpCli(object):
hits = idx.run_query(self.uname, [self.vn], uq, uv, False, False, nmax)[0]
if "pw" in self.ouparam and "nopw" not in self.ouparam:
zs = self.ouparam["pw"]
q_pw = "?pw=%s" % (quotep(zs),)
a_pw = "&pw=%s" % (quotep(zs),)
pwk = self.args.pw_urlp
if pwk in self.ouparam and "nopw" not in self.ouparam:
zs = self.ouparam[pwk]
q_pw = "?%s=%s" % (pwk, quotep(zs))
a_pw = "&%s=%s" % (pwk, quotep(zs))
for i in hits:
i["rp"] += a_pw if "?" in i["rp"] else q_pw
else:
@ -1543,8 +1554,8 @@ class HttpCli(object):
self.host,
)
feed = baseurl + self.req[1:]
if "pw" in self.ouparam and self.ouparam.get("nopw") == "a":
feed = re.sub(r"&pw=[^&]*", "", feed)
if pwk in self.ouparam and self.ouparam.get("nopw") == "a":
feed = re.sub(r"&%s=[^&]*" % (pwk,), "", feed)
if self.is_vproxied:
baseurl += self.args.RS
efeed = html_escape(feed, True, True)
@ -5308,7 +5319,7 @@ class HttpCli(object):
defpw = "dave:hunter2" if self.args.usernames else "hunter2"
vp = (self.uparam["hc"] or "").lstrip("/")
pw = self.ouparam.get("pw") or defpw
pw = self.ouparam.get(self.args.pw_urlp) or defpw
if pw in self.asrv.sesa:
pw = defpw

View file

@ -155,7 +155,11 @@ there is a static salt for all passwords;
* method `uPOST` = url-encoded post
* `FILE` = conventional HTTP file upload entry (rfc1867 et al, filename in `Content-Disposition`)
authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
clients can authenticate in the following ways; the first of these which is not blank will be used:
* url-param `&pw=foo` -- can be disabled with `--pw-urlp=A` (or renamed, if provided value is lowercase)
* then, header `PW: foo` -- can be disabled with `--pw-hdr=A` (or renamed, if provided value is lowercase)
* then, basic-auth -- can be disabled with `--no-bauth`
* then, depending on protocol, header `Cookie: cppwd=foo` on plaintext http, or header `Cookie: cppws=foo` on https
## read

View file

@ -167,7 +167,7 @@ class Cfg(Namespace):
ex = "ah_alg bname chdir chmod_f chpw_db doctitle df epilogues exit favico 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 shr tcolor textfiles txt_eol ufavico ufavico_h unlist vname xff_src zipmaxt R RS SR"
ka.update(**{k: "" for k in ex.split()})
ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl rss_fmt_d rss_fmt_t spinner"
ex = "apnd_who ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url dont_ban cachectl http_vary 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"
@ -206,6 +206,8 @@ class Cfg(Namespace):
mtp=[],
put_ck="sha512",
put_name="put-{now.6f}-{cip}.bin",
pw_hdr="pw",
pw_urlp="pw",
mv_retry="0/0",
rm_retry="0/0",
rotf_tz="UTC",