mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
add permission 'h': folders redirect to index.html;
safest way to make copyparty like a general-purpose webserver where index.html is returned as expected yet directory listing is entirely disabled / unavailable
This commit is contained in:
parent
6420c4bd03
commit
5a968f9e47
|
@ -351,6 +351,7 @@ permissions:
|
||||||
* `d` (delete): delete files/folders
|
* `d` (delete): delete files/folders
|
||||||
* `g` (get): only download files, cannot see folder contents or zip/tar
|
* `g` (get): only download files, cannot see folder contents or zip/tar
|
||||||
* `G` (upget): same as `g` except uploaders get to see their own filekeys (see `fk` in examples below)
|
* `G` (upget): same as `g` except uploaders get to see their own filekeys (see `fk` in examples below)
|
||||||
|
* `g` (get): same as `g` except folders return their index.html, and filekeys are not necessary for index.html
|
||||||
* `a` (admin): can see uploader IPs, config-reload
|
* `a` (admin): can see uploader IPs, config-reload
|
||||||
|
|
||||||
examples:
|
examples:
|
||||||
|
|
|
@ -492,6 +492,7 @@ def get_sects():
|
||||||
"d" (delete): permanently delete files and folders
|
"d" (delete): permanently delete files and folders
|
||||||
"g" (get): download files, but cannot see folder contents
|
"g" (get): download files, but cannot see folder contents
|
||||||
"G" (upget): "get", but can see filekeys of their own uploads
|
"G" (upget): "get", but can see filekeys of their own uploads
|
||||||
|
"h" (html): "get", but folders return their index.html
|
||||||
"a" (admin): can see uploader IPs, config-reload
|
"a" (admin): can see uploader IPs, config-reload
|
||||||
|
|
||||||
too many volflags to list here, see --help-flags
|
too many volflags to list here, see --help-flags
|
||||||
|
@ -1150,7 +1151,7 @@ def add_ui(ap, retry):
|
||||||
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
|
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
|
||||||
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
|
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
|
||||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages")
|
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages")
|
||||||
ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI)")
|
ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
|
||||||
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
|
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
|
||||||
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
|
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
|
||||||
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
|
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
|
||||||
|
@ -1398,7 +1399,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||||
if re.match("c[^,]", opt):
|
if re.match("c[^,]", opt):
|
||||||
mod = True
|
mod = True
|
||||||
na.append("c," + opt[1:])
|
na.append("c," + opt[1:])
|
||||||
elif re.sub("^[rwmdgGa]*", "", opt) and "," not in opt:
|
elif re.sub("^[rwmdgGha]*", "", opt) and "," not in opt:
|
||||||
mod = True
|
mod = True
|
||||||
perm = opt[0]
|
perm = opt[0]
|
||||||
na.append(perm + "," + opt[1:])
|
na.append(perm + "," + opt[1:])
|
||||||
|
|
|
@ -67,6 +67,7 @@ class AXS(object):
|
||||||
udel: Optional[Union[list[str], set[str]]] = None,
|
udel: Optional[Union[list[str], set[str]]] = None,
|
||||||
uget: Optional[Union[list[str], set[str]]] = None,
|
uget: Optional[Union[list[str], set[str]]] = None,
|
||||||
upget: Optional[Union[list[str], set[str]]] = None,
|
upget: Optional[Union[list[str], set[str]]] = None,
|
||||||
|
uhtml: Optional[Union[list[str], set[str]]] = None,
|
||||||
uadmin: Optional[Union[list[str], set[str]]] = None,
|
uadmin: Optional[Union[list[str], set[str]]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.uread: set[str] = set(uread or [])
|
self.uread: set[str] = set(uread or [])
|
||||||
|
@ -75,10 +76,11 @@ class AXS(object):
|
||||||
self.udel: set[str] = set(udel or [])
|
self.udel: set[str] = set(udel or [])
|
||||||
self.uget: set[str] = set(uget or [])
|
self.uget: set[str] = set(uget or [])
|
||||||
self.upget: set[str] = set(upget or [])
|
self.upget: set[str] = set(upget or [])
|
||||||
|
self.uhtml: set[str] = set(uhtml or [])
|
||||||
self.uadmin: set[str] = set(uadmin or [])
|
self.uadmin: set[str] = set(uadmin or [])
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
ks = "uread uwrite umove udel uget upget uadmin".split()
|
ks = "uread uwrite umove udel uget upget uhtml uadmin".split()
|
||||||
return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),)
|
return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),)
|
||||||
|
|
||||||
|
|
||||||
|
@ -329,6 +331,7 @@ class VFS(object):
|
||||||
self.adel: dict[str, list[str]] = {}
|
self.adel: dict[str, list[str]] = {}
|
||||||
self.aget: dict[str, list[str]] = {}
|
self.aget: dict[str, list[str]] = {}
|
||||||
self.apget: dict[str, list[str]] = {}
|
self.apget: dict[str, list[str]] = {}
|
||||||
|
self.ahtml: dict[str, list[str]] = {}
|
||||||
self.aadmin: dict[str, list[str]] = {}
|
self.aadmin: dict[str, list[str]] = {}
|
||||||
|
|
||||||
if realpath:
|
if realpath:
|
||||||
|
@ -456,6 +459,7 @@ class VFS(object):
|
||||||
uname in c.upget or "*" in c.upget,
|
uname in c.upget or "*" in c.upget,
|
||||||
uname in c.uadmin or "*" in c.uadmin,
|
uname in c.uadmin or "*" in c.uadmin,
|
||||||
)
|
)
|
||||||
|
# skip uhtml because it's rarely needed
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
self,
|
self,
|
||||||
|
@ -955,7 +959,7 @@ class AuthSrv(object):
|
||||||
try:
|
try:
|
||||||
self._l(ln, 5, "volume access config:")
|
self._l(ln, 5, "volume access config:")
|
||||||
sk, sv = ln.split(":")
|
sk, sv = ln.split(":")
|
||||||
if re.sub("[rwmdgGa]", "", sk) or not sk:
|
if re.sub("[rwmdgGha]", "", sk) or not sk:
|
||||||
err = "invalid accs permissions list; "
|
err = "invalid accs permissions list; "
|
||||||
raise Exception(err)
|
raise Exception(err)
|
||||||
if " " in re.sub(", *", "", sv).strip():
|
if " " in re.sub(", *", "", sv).strip():
|
||||||
|
@ -964,7 +968,7 @@ class AuthSrv(object):
|
||||||
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
|
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
|
||||||
continue
|
continue
|
||||||
except:
|
except:
|
||||||
err += "accs entries must be 'rwmdgGa: user1, user2, ...'"
|
err += "accs entries must be 'rwmdgGha: user1, user2, ...'"
|
||||||
raise Exception(err + SBADCFG)
|
raise Exception(err + SBADCFG)
|
||||||
|
|
||||||
if cat == catf:
|
if cat == catf:
|
||||||
|
@ -1000,7 +1004,7 @@ class AuthSrv(object):
|
||||||
def _read_vol_str(
|
def _read_vol_str(
|
||||||
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
|
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
if lvl.strip("crwmdgGa"):
|
if lvl.strip("crwmdgGha"):
|
||||||
raise Exception("invalid volflag: {},{}".format(lvl, uname))
|
raise Exception("invalid volflag: {},{}".format(lvl, uname))
|
||||||
|
|
||||||
if lvl == "c":
|
if lvl == "c":
|
||||||
|
@ -1029,10 +1033,12 @@ class AuthSrv(object):
|
||||||
("w", axs.uwrite),
|
("w", axs.uwrite),
|
||||||
("m", axs.umove),
|
("m", axs.umove),
|
||||||
("d", axs.udel),
|
("d", axs.udel),
|
||||||
|
("a", axs.uadmin),
|
||||||
|
("h", axs.uhtml),
|
||||||
|
("h", axs.uget),
|
||||||
("g", axs.uget),
|
("g", axs.uget),
|
||||||
("G", axs.uget),
|
("G", axs.uget),
|
||||||
("G", axs.upget),
|
("G", axs.upget),
|
||||||
("a", axs.uadmin),
|
|
||||||
]: # b bb bbb
|
]: # b bb bbb
|
||||||
if ch in lvl:
|
if ch in lvl:
|
||||||
if un == "*":
|
if un == "*":
|
||||||
|
@ -1105,7 +1111,7 @@ class AuthSrv(object):
|
||||||
|
|
||||||
if self.args.v:
|
if self.args.v:
|
||||||
# list of src:dst:permset:permset:...
|
# list of src:dst:permset:permset:...
|
||||||
# permset is <rwmdgGa>[,username][,username] or <c>,<flag>[=args]
|
# permset is <rwmdgGha>[,username][,username] or <c>,<flag>[=args]
|
||||||
for v_str in self.args.v:
|
for v_str in self.args.v:
|
||||||
m = re_vol.match(v_str)
|
m = re_vol.match(v_str)
|
||||||
if not m:
|
if not m:
|
||||||
|
@ -1194,7 +1200,7 @@ class AuthSrv(object):
|
||||||
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||||
vol.root = vfs
|
vol.root = vfs
|
||||||
|
|
||||||
for perm in "read write move del get pget admin".split():
|
for perm in "read write move del get pget html admin".split():
|
||||||
axs_key = "u" + perm
|
axs_key = "u" + perm
|
||||||
unames = ["*"] + list(acct.keys())
|
unames = ["*"] + list(acct.keys())
|
||||||
umap: dict[str, list[str]] = {x: [] for x in unames}
|
umap: dict[str, list[str]] = {x: [] for x in unames}
|
||||||
|
@ -1216,6 +1222,7 @@ class AuthSrv(object):
|
||||||
axs.udel,
|
axs.udel,
|
||||||
axs.uget,
|
axs.uget,
|
||||||
axs.upget,
|
axs.upget,
|
||||||
|
axs.uhtml,
|
||||||
axs.uadmin,
|
axs.uadmin,
|
||||||
]:
|
]:
|
||||||
for usr in d:
|
for usr in d:
|
||||||
|
@ -1637,6 +1644,7 @@ class AuthSrv(object):
|
||||||
["delete", "udel"],
|
["delete", "udel"],
|
||||||
[" get", "uget"],
|
[" get", "uget"],
|
||||||
[" upget", "upget"],
|
[" upget", "upget"],
|
||||||
|
[" html", "uhtml"],
|
||||||
["uadmin", "uadmin"],
|
["uadmin", "uadmin"],
|
||||||
]:
|
]:
|
||||||
u = list(sorted(getattr(zv.axs, attr)))
|
u = list(sorted(getattr(zv.axs, attr)))
|
||||||
|
@ -1804,6 +1812,7 @@ class AuthSrv(object):
|
||||||
vc.udel,
|
vc.udel,
|
||||||
vc.uget,
|
vc.uget,
|
||||||
vc.upget,
|
vc.upget,
|
||||||
|
vc.uhtml,
|
||||||
vc.uadmin,
|
vc.uadmin,
|
||||||
]
|
]
|
||||||
self.log(t.format(*vs))
|
self.log(t.format(*vs))
|
||||||
|
@ -1945,6 +1954,7 @@ class AuthSrv(object):
|
||||||
"d": "udel",
|
"d": "udel",
|
||||||
"g": "uget",
|
"g": "uget",
|
||||||
"G": "upget",
|
"G": "upget",
|
||||||
|
"h": "uhtml",
|
||||||
"a": "uadmin",
|
"a": "uadmin",
|
||||||
}
|
}
|
||||||
users = {}
|
users = {}
|
||||||
|
@ -2148,7 +2158,7 @@ def upgrade_cfg_fmt(
|
||||||
else:
|
else:
|
||||||
sn = sn.replace(",", ", ")
|
sn = sn.replace(",", ", ")
|
||||||
ret.append(" " + sn)
|
ret.append(" " + sn)
|
||||||
elif sn[:1] in "rwmdgGa":
|
elif sn[:1] in "rwmdgGha":
|
||||||
if cat != catx:
|
if cat != catx:
|
||||||
cat = catx
|
cat = catx
|
||||||
ret.append(cat)
|
ret.append(cat)
|
||||||
|
|
|
@ -62,6 +62,8 @@ permdescs = {
|
||||||
"d": "delete; permanently delete files and folders",
|
"d": "delete; permanently delete files and folders",
|
||||||
"g": "get; download files, but cannot see folder contents",
|
"g": "get; download files, but cannot see folder contents",
|
||||||
"G": 'upget; same as "g" but can see filekeys of their own uploads',
|
"G": 'upget; same as "g" but can see filekeys of their own uploads',
|
||||||
|
"h": 'html; same as "g" but folders return their index.html',
|
||||||
|
"a": "admin; can see uploader IPs, config-reload",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3607,6 +3607,7 @@ class HttpCli(object):
|
||||||
self.out_headers.pop("X-Robots-Tag", None)
|
self.out_headers.pop("X-Robots-Tag", None)
|
||||||
|
|
||||||
is_dir = stat.S_ISDIR(st.st_mode)
|
is_dir = stat.S_ISDIR(st.st_mode)
|
||||||
|
fk_pass = False
|
||||||
icur = None
|
icur = None
|
||||||
if is_dir and (e2t or e2d):
|
if is_dir and (e2t or e2d):
|
||||||
idx = self.conn.get_u2idx()
|
idx = self.conn.get_u2idx()
|
||||||
|
@ -3655,8 +3656,38 @@ class HttpCli(object):
|
||||||
|
|
||||||
return self.tx_ico(rem)
|
return self.tx_ico(rem)
|
||||||
|
|
||||||
|
elif self.can_get and self.avn:
|
||||||
|
axs = self.avn.axs
|
||||||
|
if self.uname not in axs.uhtml and "*" not in axs.uhtml:
|
||||||
|
pass
|
||||||
|
elif is_dir:
|
||||||
|
for fn in ("index.htm", "index.html"):
|
||||||
|
ap2 = os.path.join(abspath, fn)
|
||||||
|
try:
|
||||||
|
st2 = bos.stat(ap2)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# might as well be extra careful
|
||||||
|
if not stat.S_ISREG(st2.st_mode):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self.trailing_slash:
|
||||||
|
return self.redirect(
|
||||||
|
self.vpath + "/", flavor="redirecting to", use302=True
|
||||||
|
)
|
||||||
|
|
||||||
|
fk_pass = True
|
||||||
|
is_dir = False
|
||||||
|
rem = vjoin(rem, fn)
|
||||||
|
vrem = vjoin(vrem, fn)
|
||||||
|
abspath = ap2
|
||||||
|
break
|
||||||
|
elif self.vpath.rsplit("/", 1)[1] in ("index.htm", "index.html"):
|
||||||
|
fk_pass = True
|
||||||
|
|
||||||
if not is_dir and (self.can_read or self.can_get):
|
if not is_dir and (self.can_read or self.can_get):
|
||||||
if not self.can_read and "fk" in vn.flags:
|
if not self.can_read and not fk_pass and "fk" in vn.flags:
|
||||||
correct = self.gen_fk(
|
correct = self.gen_fk(
|
||||||
self.args.fk_salt, abspath, st.st_size, 0 if ANYWIN else st.st_ino
|
self.args.fk_salt, abspath, st.st_size, 0 if ANYWIN else st.st_ino
|
||||||
)[: vn.flags["fk"]]
|
)[: vn.flags["fk"]]
|
||||||
|
|
Loading…
Reference in a new issue