diff --git a/README.md b/README.md index fcdcc8dc..2323fce6 100644 --- a/README.md +++ b/README.md @@ -834,6 +834,11 @@ on public copyparty instances with anonymous upload enabled: * unless `--no-readme` is set: by uploading/modifying a file named `readme.md` * if `move` access is granted AND none of `--no-logues`, `--no-dot-mv`, `--no-dot-ren` is set: by uploading some .html file and renaming it to `.epilogue.html` (uploading it directly is blocked) +other misc: + +* you can disable directory listings by giving accesslevel `g` instead of `r`, only accepting direct URLs to files + * combine this with volume-flag `c,fk` to generate per-file accesskeys; users which have full read-access will then see URLs with `?k=...` appended to the end, and `g` users must provide that URL including the correct key to avoid a 404 + ## gotchas diff --git a/copyparty/__main__.py b/copyparty/__main__.py index de29a921..ee472ca9 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -104,7 +104,7 @@ def ensure_cert(): cert_insec = os.path.join(E.mod, "res/insecure.pem") cert_cfg = os.path.join(E.cfg, "cert.pem") if not os.path.exists(cert_cfg): - shutil.copy2(cert_insec, cert_cfg) + shutil.copy(cert_insec, cert_cfg) try: if filecmp.cmp(cert_cfg, cert_insec): @@ -203,6 +203,11 @@ def run_argparse(argv, formatter): description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT), ) + try: + fk_salt = unicode(os.path.getmtime(os.path.join(E.cfg, "cert.pem"))) + except: + fk_salt = "hunter2" + sects = [ [ "accounts", @@ -280,6 +285,10 @@ def run_argparse(argv, formatter): \033[36mmtp=.bpm=f,audio-bpm.py\033[35m uses the "audio-bpm.py" program to generate ".bpm" tags from uploads (f = overwrite tags) \033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once + + \033[0mothers: + \033[36mfk=8\033[35m generates per-file accesskeys, + which will then be required at the "g" accesslevel \033[0m""" ), ], @@ -361,6 +370,7 @@ def run_argparse(argv, formatter): ap2 = ap.add_argument_group('safety options') ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]") ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt") + ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt") ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles") ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile") ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 296fae07..d6a905ed 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -834,6 +834,11 @@ class AuthSrv(object): if use: vol.lim = lim + for vol in vfs.all_vols.values(): + fk = vol.flags.get("fk") + if fk: + vol.flags["fk"] = int(fk) if fk is not True else 8 + for vol in vfs.all_vols.values(): if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags: vol.flags["gz"] = False # def.pk diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index f5644bda..dbc77327 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1394,7 +1394,9 @@ class HttpCli(object): # # send reply - if is_compressed or "cache" in self.uparam: + if not is_compressed and "cache" not in self.uparam: + self.out_headers.update(NO_CACHE) + else: self.out_headers.pop("Cache-Control") self.out_headers["Accept-Ranges"] = "bytes" @@ -1617,7 +1619,7 @@ class HttpCli(object): return True def tx_404(self): - m = '

404 not found

or maybe you don\'t have access -- try logging in or go home

' + m = '

404 not found  ┐( ´ -`)┌

or maybe you don\'t have access -- try logging in or go home

' html = self.j2("splash", this=self, qvpath=quotep(self.vpath), msg=m) self.reply(html.encode("utf-8"), status=404) return True @@ -1829,6 +1831,15 @@ class HttpCli(object): return self.tx_ico(rem) if not is_dir and (self.can_read or self.can_get): + if not self.can_read and "fk" in vn.flags: + correct = gen_filekey( + self.args.fk_salt, abspath, st.st_size, 0 if ANYWIN else st.st_ino + )[: vn.flags["fk"]] + got = self.uparam.get("k") + if got != correct: + self.log("wrong filekey, want {}, got {}".format(correct, got)) + return self.tx_404() + if abspath.endswith(".md") and "raw" not in self.uparam: return self.tx_md(abspath) @@ -1987,6 +1998,8 @@ class HttpCli(object): idx = self.conn.get_u2idx() icur = idx.get_cur(dbv.realpath) + add_fk = vn.flags.get("fk") + dirs = [] files = [] for fn in vfs_ls: @@ -2032,9 +2045,19 @@ class HttpCli(object): except: ext = "%" + if add_fk: + href = "{}?k={}".format( + quotep(href), + gen_filekey( + self.args.fk_salt, fspath, sz, 0 if ANYWIN else inf.st_ino + )[:add_fk], + ) + else: + href = quotep(href) + item = { "lead": margin, - "href": quotep(href), + "href": href, "name": fn, "sz": sz, "ext": ext, diff --git a/copyparty/util.py b/copyparty/util.py index 32939382..861c9fd6 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -19,7 +19,7 @@ import subprocess as sp # nosec from datetime import datetime from collections import Counter -from .__init__ import PY2, WINDOWS, ANYWIN, VT100 +from .__init__ import PY2, WINDOWS, ANYWIN, VT100, unicode from .stolen import surrogateescape FAKE_MP = False @@ -745,6 +745,14 @@ def read_header(sr): return ret[:ofs].decode("utf-8", "surrogateescape").lstrip("\r\n").split("\r\n") +def gen_filekey(salt, fspath, fsize, inode): + return base64.urlsafe_b64encode( + hashlib.sha512( + "{} {} {} {}".format(salt, fspath, fsize, inode).encode("utf-8", "replace") + ).digest() + ).decode("ascii") + + def humansize(sz, terse=False): for unit in ["B", "KiB", "MiB", "GiB", "TiB"]: if sz < 1024: