add optional username login; closes #511

This commit is contained in:
ed 2025-08-07 20:29:44 +00:00
parent 3c42a34f7b
commit 346515ccf1
10 changed files with 58 additions and 6 deletions

View file

@ -437,6 +437,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 `--accounts` then do `?pw=username:password` instead
* 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)
@ -1016,6 +1017,7 @@ a feed example: https://cd.ocv.me/a/d2/d22/?rss&fext=mp3
url parameters:
* `pw=hunter2` for password auth
* if you enabled `--usernames` then do `pw=username:password` instead
* `recursive` to also include subfolders
* `title=foo` changes the feed title (default: folder name)
* `fext=mp3,opus` only include mp3 and opus files (default: all)
@ -1301,6 +1303,7 @@ an FTP server can be started using `--ftp 3921`, and/or `--ftps` for explicit T
* if you enable both `ftp` and `ftps`, the port-range will be divided in half
* some older software (filezilla on debian-stable) cannot passive-mode with TLS
* login with any username + your password, or put your password in the username field
* unless you enabled `--usernames`
some recommended FTP / FTPS clients; `wark` = example password:
* https://winscp.net/eng/download.php
@ -1318,6 +1321,7 @@ click the [connect](http://127.0.0.1:3923/?hc) button in the control-panel to se
general usage:
* login with any username + your password, or put your password in the username field (password field can be empty/whatever)
* unless you enabled `--usernames`
on macos, connect from finder:
* [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/
@ -1333,6 +1337,7 @@ using the GUI (winXP or later):
* rightclick [my computer] -> [map network drive] -> Folder: `http://192.168.123.1:3923/`
* on winXP only, click the `Sign up for online storage` hyperlink instead and put the URL there
* providing your password as the username is recommended; the password field can be anything or empty
* unless you enabled `--usernames`
the webdav client that's built into windows has the following list of bugs; you can avoid all of these by connecting with rclone instead:
* win7+ doesn't actually send the password to the server when reauthenticating after a reboot unless you first try to login with an incorrect password and then switch to the correct password
@ -1390,6 +1395,7 @@ some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance:
* the smb backend is not fully integrated with vfs, meaning there could be security issues (path traversal). Please use `--smb-port` (see below) and [prisonparty](./bin/prisonparty.sh) or [bubbleparty](./bin/bubbleparty.sh)
* account passwords work per-volume as expected, and so does account permissions (read/write/move/delete), but `--smbw` must be given to allow write-access from smb
* [shadowing](#shadowing) probably works as expected but no guarantees
* not compatible with pw-hashing or `--usernames`
and some minor issues,
* clients only see the first ~400 files in big folders;
@ -2506,6 +2512,8 @@ you can provide passwords using header `PW: hunter2`, cookie `cppwd=hunter2`, ur
> for basic-authentication, all of the following are accepted: `password` / `whatever:password` / `password:whatever` (the username is ignored)
* unless you've enabled `--usernames`, then it's `PW: usr:pwd`, cookie `cppwd=usr:pwd`, url-param `?pw=usr:pwd`
NOTE: curl will not send the original filename if you use `-T` combined with url-params! Also, make sure to always leave a trailing slash in URLs unless you want to override the filename
@ -2721,6 +2729,8 @@ when generating hashes using `--ah-cli` for docker or systemd services, make sur
* inspecting the generated salt using `--show-ah-salt` in copyparty service configuration
* setting the same `--ah-salt` in both environments
> ⚠️ if you have enabled `--usernames` then provide the password as `username:password` when hashing it, for example `ed:hunter2`
## https

View file

@ -918,6 +918,9 @@ def get_sects():
copyparty will also hash and print any passwords that are non-hashed
(password which do not start with '+') and then terminate afterwards
if you have enabled --accounts then the password
must be provided as username:password for hashing
\033[36m--ah-alg\033[0m specifies the hashing algorithm and a
list of optional comma-separated arguments:
@ -1002,6 +1005,7 @@ def add_general(ap, nc, srvname):
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add account, \033[33mUSER\033[0m:\033[33mPASS\033[0m; example [\033[32med:wark\033[0m]")
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password")
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
@ -1393,7 +1397,7 @@ def add_logging(ap):
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)")
ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
ap2.add_argument("--log-badpwd", metavar="N", type=int, default=1, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")

View file

@ -2642,6 +2642,8 @@ class AuthSrv(object):
self.re_pwd = None
pwds = [re.escape(x) for x in self.iacct.keys()]
pwds.extend(list(self.sesa))
if self.args.usernames:
pwds.extend([x.split(":", 1)[1] for x in pwds if ":" in x])
if pwds:
if self.ah.on:
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
@ -2942,6 +2944,9 @@ class AuthSrv(object):
t = "minimum password length: %d characters"
return False, t % (self.args.chpw_len,)
if self.args.usernames:
pw = "%s:%s" % (uname, pw)
hpw = self.ah.hash(pw) if self.ah.on else pw
if hpw == self.acct[uname]:
@ -3033,6 +3038,12 @@ class AuthSrv(object):
self.log("chpw: " + msg, 6)
def setup_pwhash(self, acct: dict[str, str]) -> None:
if self.args.usernames:
for uname, pw in list(acct.items())[:]:
if pw.startswith("+") and len(pw) == 33:
continue
acct[uname] = "%s:%s" % (uname, pw)
self.ah = PWHash(self.args)
if not self.ah.on:
if self.args.ah_cli or self.args.ah_gen:

View file

@ -83,7 +83,12 @@ class FtpAuth(DummyAuthorizer):
uname = "*"
if username != "anonymous":
uname = ""
for zs in (password, username):
if args.usernames:
alts = ["%s:%s" % (username, password)]
else:
alts = password, username
for zs in alts:
zs = asrv.iacct.get(asrv.ah.hash(zs), "")
if zs:
uname = zs

View file

@ -2937,12 +2937,16 @@ class HttpCli(object):
def handle_chpw(self) -> bool:
assert self.parser # !rm
if self.args.usernames:
self.parser.require("uname", 64)
pwd = self.parser.require("pw", 64)
self.parser.drop()
ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)
if ok:
self.cbonk(self.conn.hsrv.gpwc, pwd, "pw", "too many password changes")
if self.args.usernames:
pwd = "%s:%s" % (self.uname, pwd)
ok, msg = self.get_pwd_cookie(pwd)
if ok:
msg = "new password OK"
@ -2955,6 +2959,13 @@ class HttpCli(object):
def handle_login(self) -> bool:
assert self.parser # !rm
if self.args.usernames:
try:
un = self.parser.require("uname", 256)
except:
un = ""
else:
un = ""
pwd = self.parser.require("cppwd", 64)
try:
uhash = self.parser.require("uhash", 256)
@ -2965,6 +2976,9 @@ class HttpCli(object):
if not pwd:
raise Pebkac(422, "password cannot be blank")
if un:
pwd = "%s:%s" % (un, pwd)
dst = self.args.SRS
if self.vpath:
dst += quotep(self.vpaths)

View file

@ -147,6 +147,10 @@ class PWHash(object):
def cli(self) -> None:
import getpass
if self.args.usernames:
t = "since you have enabled --usernames, please provide username:password"
print(t)
while True:
try:
p1 = getpass.getpass("password> ")

View file

@ -2983,8 +2983,7 @@ def justcopy(
def eol_conv(
fin: Generator[bytes, None, None],
conv: str
fin: Generator[bytes, None, None], conv: str
) -> Generator[bytes, None, None]:
crlf = conv.lower() == "crlf"
for buf in fin:

View file

@ -120,7 +120,12 @@
<div>
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
<input type="hidden" id="la" name="act" value="login" />
{% if this.args.usernames %}
<input type="text" id="lu" name="uname" placeholder=" username" size="12" />
<input type="password" id="lp" name="cppwd" placeholder=" password" size="12" />
{% else %}
<input type="password" id="lp" name="cppwd" placeholder=" password" />
{% endif %}
<input type="hidden" name="uhash" id="uhash" value="x" />
<input type="submit" id="ls" value="login" />
{% if chpw %}

View file

@ -416,7 +416,7 @@ try {
catch (ex) { }
tt.init();
var o = QS('input[name="cppwd"]');
var o = QS('input[name="uname"]') || QS('input[name="cppwd"]');
if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
o.focus();

View file

@ -143,7 +143,7 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None, **ka0):
ka = {}
ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs"
ex = "allow_flac allow_wav chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead localtime magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_fnugg no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe usernames vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs"
ka.update(**{k: False for k in ex.split()})
ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip"