copyparty 1.16.16__py3-none-any.whl → 1.16.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- copyparty/__main__.py +21 -40
- copyparty/__version__.py +2 -2
- copyparty/authsrv.py +137 -56
- copyparty/cfg.py +10 -0
- copyparty/fsutil.py +7 -5
- copyparty/ftpd.py +11 -0
- copyparty/httpcli.py +87 -33
- copyparty/mtag.py +1 -2
- copyparty/svchub.py +5 -4
- copyparty/szip.py +1 -2
- copyparty/tcpsrv.py +18 -0
- copyparty/tftpd.py +29 -8
- copyparty/up2k.py +13 -8
- copyparty/util.py +62 -3
- copyparty/web/browser.js.gz +0 -0
- copyparty/web/up2k.js.gz +0 -0
- copyparty/web/util.js.gz +0 -0
- {copyparty-1.16.16.dist-info → copyparty-1.16.18.dist-info}/METADATA +14 -3
- {copyparty-1.16.16.dist-info → copyparty-1.16.18.dist-info}/RECORD +23 -23
- {copyparty-1.16.16.dist-info → copyparty-1.16.18.dist-info}/WHEEL +1 -1
- {copyparty-1.16.16.dist-info → copyparty-1.16.18.dist-info}/entry_points.txt +0 -0
- {copyparty-1.16.16.dist-info → copyparty-1.16.18.dist-info/licenses}/LICENSE +0 -0
- {copyparty-1.16.16.dist-info → copyparty-1.16.18.dist-info}/top_level.txt +0 -0
copyparty/__main__.py
CHANGED
@@ -40,6 +40,7 @@ from .cfg import flagcats, onedash
|
|
40
40
|
from .svchub import SvcHub
|
41
41
|
from .util import (
|
42
42
|
APPLESAN_TXT,
|
43
|
+
BAD_BOTS,
|
43
44
|
DEF_EXP,
|
44
45
|
DEF_MTE,
|
45
46
|
DEF_MTH,
|
@@ -65,6 +66,7 @@ from .util import (
|
|
65
66
|
load_resource,
|
66
67
|
min_ex,
|
67
68
|
pybin,
|
69
|
+
read_utf8,
|
68
70
|
termsize,
|
69
71
|
wrap,
|
70
72
|
)
|
@@ -249,8 +251,7 @@ def get_srvname(verbose) :
|
|
249
251
|
if verbose:
|
250
252
|
lprint("using hostname from {}\n".format(fp))
|
251
253
|
try:
|
252
|
-
|
253
|
-
ret = f.read().decode("utf-8", "replace").strip()
|
254
|
+
return read_utf8(None, fp, True).strip()
|
254
255
|
except:
|
255
256
|
ret = ""
|
256
257
|
namelen = 5
|
@@ -259,47 +260,18 @@ def get_srvname(verbose) :
|
|
259
260
|
ret = re.sub("[234567=]", "", ret)[:namelen]
|
260
261
|
with open(fp, "wb") as f:
|
261
262
|
f.write(ret.encode("utf-8") + b"\n")
|
262
|
-
|
263
|
-
return ret
|
264
|
-
|
265
|
-
|
266
|
-
def get_fk_salt() :
|
267
|
-
fp = os.path.join(E.cfg, "fk-salt.txt")
|
268
|
-
try:
|
269
|
-
with open(fp, "rb") as f:
|
270
|
-
ret = f.read().strip()
|
271
|
-
except:
|
272
|
-
ret = b64enc(os.urandom(18))
|
273
|
-
with open(fp, "wb") as f:
|
274
|
-
f.write(ret + b"\n")
|
275
|
-
|
276
|
-
return ret.decode("utf-8")
|
277
|
-
|
278
|
-
|
279
|
-
def get_dk_salt() :
|
280
|
-
fp = os.path.join(E.cfg, "dk-salt.txt")
|
281
|
-
try:
|
282
|
-
with open(fp, "rb") as f:
|
283
|
-
ret = f.read().strip()
|
284
|
-
except:
|
285
|
-
ret = b64enc(os.urandom(30))
|
286
|
-
with open(fp, "wb") as f:
|
287
|
-
f.write(ret + b"\n")
|
288
|
-
|
289
|
-
return ret.decode("utf-8")
|
263
|
+
return ret
|
290
264
|
|
291
265
|
|
292
|
-
def
|
293
|
-
fp = os.path.join(E.cfg, "
|
266
|
+
def get_salt(name , nbytes ) :
|
267
|
+
fp = os.path.join(E.cfg, "%s-salt.txt" % (name,))
|
294
268
|
try:
|
295
|
-
|
296
|
-
ret = f.read().strip()
|
269
|
+
return read_utf8(None, fp, True).strip()
|
297
270
|
except:
|
298
|
-
ret = b64enc(os.urandom(
|
271
|
+
ret = b64enc(os.urandom(nbytes))
|
299
272
|
with open(fp, "wb") as f:
|
300
273
|
f.write(ret + b"\n")
|
301
|
-
|
302
|
-
return ret.decode("utf-8")
|
274
|
+
return ret.decode("utf-8")
|
303
275
|
|
304
276
|
|
305
277
|
def ensure_locale() :
|
@@ -1050,6 +1022,8 @@ def add_network(ap):
|
|
1050
1022
|
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")
|
1051
1023
|
else:
|
1052
1024
|
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
|
1025
|
+
ap2.add_argument("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
|
1026
|
+
ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
|
1053
1027
|
ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)")
|
1054
1028
|
ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=128.0, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
|
1055
1029
|
ap2.add_argument("--s-rd-sz", metavar="B", type=int, default=256*1024, help="socket read size in bytes (indirectly affects filesystem writes; recommendation: keep equal-to or lower-than \033[33m--iobuf\033[0m)")
|
@@ -1243,6 +1217,7 @@ def add_yolo(ap):
|
|
1243
1217
|
ap2 = ap.add_argument_group('yolo options')
|
1244
1218
|
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
|
1245
1219
|
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
|
1220
|
+
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
|
1246
1221
|
|
1247
1222
|
|
1248
1223
|
def add_optouts(ap):
|
@@ -1257,7 +1232,12 @@ def add_optouts(ap):
|
|
1257
1232
|
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
|
1258
1233
|
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
|
1259
1234
|
ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
|
1235
|
+
ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
|
1236
|
+
ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
|
1237
|
+
ap2.add_argument("--zipmaxt", metavar="TXT", type=u, default="", help="custom errormessage when download size exceeds max (volflag=zipmaxt)")
|
1238
|
+
ap2.add_argument("--zipmaxu", action="store_true", help="authenticated users bypass the zip size limit (volflag=zipmaxu)")
|
1260
1239
|
ap2.add_argument("--zip-who", metavar="LVL", type=int, default=3, help="who can download as zip/tar? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=zip_who)\n\033[1;31mWARNING:\033[0m if a nested volume has a more restrictive value than a parent volume, then this will be \033[33mignored\033[0m if the download is initiated from the parent, more lenient volume")
|
1240
|
+
ap2.add_argument("--ua-nozip", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from download-as-zip/tar; disable with [\033[32mno\033[0m] or blank")
|
1261
1241
|
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar; same as \033[33m--zip-who=0\033[0m")
|
1262
1242
|
ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
|
1263
1243
|
ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
|
@@ -1448,6 +1428,7 @@ def add_txt(ap):
|
|
1448
1428
|
ap2.add_argument("--exp", action="store_true", help="enable textfile expansion -- replace {{self.ip}} and such; see \033[33m--help-exp\033[0m (volflag=exp)")
|
1449
1429
|
ap2.add_argument("--exp-md", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in markdown files; add/remove stuff on the default list with +hdr_foo or /vf.scan (volflag=exp_md)")
|
1450
1430
|
ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)")
|
1431
|
+
ap2.add_argument("--ua-nodoc", metavar="PTN", type=u, default=BAD_BOTS, help="regex of user-agents to reject from viewing documents through ?doc=[...]; disable with [\033[32mno\033[0m] or blank")
|
1451
1432
|
|
1452
1433
|
|
1453
1434
|
def add_og(ap):
|
@@ -1544,9 +1525,9 @@ def run_argparse(
|
|
1544
1525
|
|
1545
1526
|
cert_path = os.path.join(E.cfg, "cert.pem")
|
1546
1527
|
|
1547
|
-
fk_salt =
|
1548
|
-
dk_salt =
|
1549
|
-
ah_salt =
|
1528
|
+
fk_salt = get_salt("fk", 18)
|
1529
|
+
dk_salt = get_salt("dk", 30)
|
1530
|
+
ah_salt = get_salt("ah", 18)
|
1550
1531
|
|
1551
1532
|
# alpine peaks at 5 threads for some reason,
|
1552
1533
|
# all others scale past that (but try to avoid SMT),
|
copyparty/__version__.py
CHANGED
copyparty/authsrv.py
CHANGED
@@ -33,6 +33,7 @@ from .util import (
|
|
33
33
|
get_df,
|
34
34
|
humansize,
|
35
35
|
odfusion,
|
36
|
+
read_utf8,
|
36
37
|
relchk,
|
37
38
|
statdir,
|
38
39
|
ub64enc,
|
@@ -64,6 +65,8 @@ SSEELOG = " ({})".format(SEE_LOG)
|
|
64
65
|
BAD_CFG = "invalid config; {}".format(SEE_LOG)
|
65
66
|
SBADCFG = " ({})".format(BAD_CFG)
|
66
67
|
|
68
|
+
PTN_U_GRP = re.compile(r"\$\{u%([+-])([^}]+)\}")
|
69
|
+
|
67
70
|
|
68
71
|
class CfgEx(Exception):
|
69
72
|
pass
|
@@ -335,22 +338,26 @@ class VFS(object):
|
|
335
338
|
log ,
|
336
339
|
realpath ,
|
337
340
|
vpath ,
|
341
|
+
vpath0 ,
|
338
342
|
axs ,
|
339
343
|
flags ,
|
340
344
|
) :
|
341
345
|
self.log = log
|
342
346
|
self.realpath = realpath # absolute path on host filesystem
|
343
347
|
self.vpath = vpath # absolute path in the virtual filesystem
|
348
|
+
self.vpath0 = vpath0 # original vpath (before idp expansion)
|
344
349
|
self.axs = axs
|
345
350
|
self.flags = flags # config options
|
346
351
|
self.root = self
|
347
352
|
self.dev = 0 # st_dev
|
353
|
+
self.badcfg1 = False
|
348
354
|
self.nodes = {} # child nodes
|
349
355
|
self.histtab = {} # all realpath->histpath
|
350
356
|
self.dbv = None # closest full/non-jump parent
|
351
357
|
self.lim = None # upload limits; only set for dbv
|
352
358
|
self.shr_src = None # source vfs+rem of a share
|
353
359
|
self.shr_files = set() # filenames to include from shr_src
|
360
|
+
self.shr_owner = "" # uname
|
354
361
|
self.aread = {}
|
355
362
|
self.awrite = {}
|
356
363
|
self.amove = {}
|
@@ -368,7 +375,7 @@ class VFS(object):
|
|
368
375
|
vp = vpath + ("/" if vpath else "")
|
369
376
|
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
370
377
|
self.all_vols = {vpath: self} # flattened recursive
|
371
|
-
self.all_nodes = {vpath: self} # also jumpvols
|
378
|
+
self.all_nodes = {vpath: self} # also jumpvols/shares
|
372
379
|
self.all_aps = [(rp, self)]
|
373
380
|
self.all_vps = [(vp, self)]
|
374
381
|
else:
|
@@ -408,7 +415,7 @@ class VFS(object):
|
|
408
415
|
for v in self.nodes.values():
|
409
416
|
v.get_all_vols(vols, nodes, aps, vps)
|
410
417
|
|
411
|
-
def add(self, src , dst ) :
|
418
|
+
def add(self, src , dst , dst0 ) :
|
412
419
|
"""get existing, or add new path to the vfs"""
|
413
420
|
assert src == "/" or not src.endswith("/") # nosec
|
414
421
|
assert not dst.endswith("/") # nosec
|
@@ -416,20 +423,22 @@ class VFS(object):
|
|
416
423
|
if "/" in dst:
|
417
424
|
# requires breadth-first population (permissions trickle down)
|
418
425
|
name, dst = dst.split("/", 1)
|
426
|
+
name0, dst0 = dst0.split("/", 1)
|
419
427
|
if name in self.nodes:
|
420
428
|
# exists; do not manipulate permissions
|
421
|
-
return self.nodes[name].add(src, dst)
|
429
|
+
return self.nodes[name].add(src, dst, dst0)
|
422
430
|
|
423
431
|
vn = VFS(
|
424
432
|
self.log,
|
425
433
|
os.path.join(self.realpath, name) if self.realpath else "",
|
426
434
|
"{}/{}".format(self.vpath, name).lstrip("/"),
|
435
|
+
"{}/{}".format(self.vpath0, name0).lstrip("/"),
|
427
436
|
self.axs,
|
428
437
|
self._copy_flags(name),
|
429
438
|
)
|
430
439
|
vn.dbv = self.dbv or self
|
431
440
|
self.nodes[name] = vn
|
432
|
-
return vn.add(src, dst)
|
441
|
+
return vn.add(src, dst, dst0)
|
433
442
|
|
434
443
|
if dst in self.nodes:
|
435
444
|
# leaf exists; return as-is
|
@@ -437,7 +446,8 @@ class VFS(object):
|
|
437
446
|
|
438
447
|
# leaf does not exist; create and keep permissions blank
|
439
448
|
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
440
|
-
|
449
|
+
vp0 = "{}/{}".format(self.vpath0, dst0).lstrip("/")
|
450
|
+
vn = VFS(self.log, src, vp, vp0, AXS(), {})
|
441
451
|
vn.dbv = self.dbv or self
|
442
452
|
self.nodes[dst] = vn
|
443
453
|
return vn
|
@@ -854,7 +864,7 @@ class AuthSrv(object):
|
|
854
864
|
self.indent = ""
|
855
865
|
|
856
866
|
# fwd-decl
|
857
|
-
self.vfs = VFS(log_func, "", "", AXS(), {})
|
867
|
+
self.vfs = VFS(log_func, "", "", "", AXS(), {})
|
858
868
|
self.acct = {} # uname->pw
|
859
869
|
self.iacct = {} # pw->uname
|
860
870
|
self.ases = {} # uname->session
|
@@ -922,7 +932,7 @@ class AuthSrv(object):
|
|
922
932
|
self,
|
923
933
|
src ,
|
924
934
|
dst ,
|
925
|
-
mount
|
935
|
+
mount ,
|
926
936
|
daxs ,
|
927
937
|
mflags ,
|
928
938
|
un_gns ,
|
@@ -938,12 +948,24 @@ class AuthSrv(object):
|
|
938
948
|
un_gn = [("", "")]
|
939
949
|
|
940
950
|
for un, gn in un_gn:
|
951
|
+
m = PTN_U_GRP.search(dst0)
|
952
|
+
if m:
|
953
|
+
req, gnc = m.groups()
|
954
|
+
hit = gnc in (un_gns.get(un) or [])
|
955
|
+
if req == "+":
|
956
|
+
if not hit:
|
957
|
+
continue
|
958
|
+
elif hit:
|
959
|
+
continue
|
960
|
+
|
941
961
|
# if ap/vp has a user/group placeholder, make sure to keep
|
942
962
|
# track so the same user/group is mapped when setting perms;
|
943
963
|
# otherwise clear un/gn to indicate it's a regular volume
|
944
964
|
|
945
965
|
src1 = src0.replace("${u}", un or "\n")
|
946
966
|
dst1 = dst0.replace("${u}", un or "\n")
|
967
|
+
src1 = PTN_U_GRP.sub(un or "\n", src1)
|
968
|
+
dst1 = PTN_U_GRP.sub(un or "\n", dst1)
|
947
969
|
if src0 == src1 and dst0 == dst1:
|
948
970
|
un = ""
|
949
971
|
|
@@ -960,7 +982,7 @@ class AuthSrv(object):
|
|
960
982
|
continue
|
961
983
|
visited.add(label)
|
962
984
|
|
963
|
-
src, dst = self._map_volume(src, dst, mount, daxs, mflags)
|
985
|
+
src, dst = self._map_volume(src, dst, dst0, mount, daxs, mflags)
|
964
986
|
if src:
|
965
987
|
ret.append((src, dst, un, gn))
|
966
988
|
if un or gn:
|
@@ -972,7 +994,8 @@ class AuthSrv(object):
|
|
972
994
|
self,
|
973
995
|
src ,
|
974
996
|
dst ,
|
975
|
-
|
997
|
+
dst0 ,
|
998
|
+
mount ,
|
976
999
|
daxs ,
|
977
1000
|
mflags ,
|
978
1001
|
) :
|
@@ -982,13 +1005,13 @@ class AuthSrv(object):
|
|
982
1005
|
|
983
1006
|
if dst in mount:
|
984
1007
|
t = "multiple filesystem-paths mounted at [/{}]:\n [{}]\n [{}]"
|
985
|
-
self.log(t.format(dst, mount[dst], src), c=1)
|
1008
|
+
self.log(t.format(dst, mount[dst][0], src), c=1)
|
986
1009
|
raise Exception(BAD_CFG)
|
987
1010
|
|
988
1011
|
if src in mount.values():
|
989
1012
|
t = "filesystem-path [{}] mounted in multiple locations:"
|
990
1013
|
t = t.format(src)
|
991
|
-
for v in [k for k, v in mount.items() if v == src] + [dst]:
|
1014
|
+
for v in [k for k, v in mount.items() if v[0] == src] + [dst]:
|
992
1015
|
t += "\n /{}".format(v)
|
993
1016
|
|
994
1017
|
self.log(t, c=3)
|
@@ -997,7 +1020,7 @@ class AuthSrv(object):
|
|
997
1020
|
if not bos.path.isdir(src):
|
998
1021
|
self.log("warning: filesystem-path does not exist: {}".format(src), 3)
|
999
1022
|
|
1000
|
-
mount[dst] = src
|
1023
|
+
mount[dst] = (src, dst0)
|
1001
1024
|
daxs[dst] = AXS()
|
1002
1025
|
mflags[dst] = {}
|
1003
1026
|
return (src, dst)
|
@@ -1058,7 +1081,7 @@ class AuthSrv(object):
|
|
1058
1081
|
grps ,
|
1059
1082
|
daxs ,
|
1060
1083
|
mflags ,
|
1061
|
-
mount
|
1084
|
+
mount ,
|
1062
1085
|
) :
|
1063
1086
|
self.line_ctr = 0
|
1064
1087
|
|
@@ -1083,7 +1106,7 @@ class AuthSrv(object):
|
|
1083
1106
|
grps ,
|
1084
1107
|
daxs ,
|
1085
1108
|
mflags ,
|
1086
|
-
mount
|
1109
|
+
mount ,
|
1087
1110
|
npass ,
|
1088
1111
|
) :
|
1089
1112
|
self.line_ctr = 0
|
@@ -1442,8 +1465,8 @@ class AuthSrv(object):
|
|
1442
1465
|
acct = {} # username:password
|
1443
1466
|
grps = {} # groupname:usernames
|
1444
1467
|
daxs = {}
|
1445
|
-
mflags = {} #
|
1446
|
-
mount
|
1468
|
+
mflags = {} # vpath:flags
|
1469
|
+
mount = {} # dst:src (vp:(ap,vp0))
|
1447
1470
|
|
1448
1471
|
self.idp_vols = {} # yolo
|
1449
1472
|
|
@@ -1522,8 +1545,8 @@ class AuthSrv(object):
|
|
1522
1545
|
# case-insensitive; normalize
|
1523
1546
|
if WINDOWS:
|
1524
1547
|
cased = {}
|
1525
|
-
for
|
1526
|
-
cased[
|
1548
|
+
for vp, (ap, vp0) in mount.items():
|
1549
|
+
cased[vp] = (absreal(ap), vp0)
|
1527
1550
|
|
1528
1551
|
mount = cased
|
1529
1552
|
|
@@ -1538,25 +1561,28 @@ class AuthSrv(object):
|
|
1538
1561
|
t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw"
|
1539
1562
|
self.log(t, 1)
|
1540
1563
|
axs = AXS()
|
1541
|
-
vfs = VFS(self.log_func, absreal("."), "", axs, {})
|
1564
|
+
vfs = VFS(self.log_func, absreal("."), "", "", axs, {})
|
1565
|
+
if not axs.uread:
|
1566
|
+
vfs.badcfg1 = True
|
1542
1567
|
elif "" not in mount:
|
1543
1568
|
# there's volumes but no root; make root inaccessible
|
1544
1569
|
zsd = {"d2d": True, "tcolor": self.args.tcolor}
|
1545
|
-
vfs = VFS(self.log_func, "", "", AXS(), zsd)
|
1570
|
+
vfs = VFS(self.log_func, "", "", "", AXS(), zsd)
|
1546
1571
|
|
1547
1572
|
maxdepth = 0
|
1548
1573
|
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
|
1549
1574
|
depth = dst.count("/")
|
1550
1575
|
assert maxdepth <= depth # nosec
|
1551
1576
|
maxdepth = depth
|
1577
|
+
src, dst0 = mount[dst]
|
1552
1578
|
|
1553
1579
|
if dst == "":
|
1554
1580
|
# rootfs was mapped; fully replaces the default CWD vfs
|
1555
|
-
vfs = VFS(self.log_func,
|
1581
|
+
vfs = VFS(self.log_func, src, dst, dst0, daxs[dst], mflags[dst])
|
1556
1582
|
continue
|
1557
1583
|
|
1558
1584
|
assert vfs # type: ignore
|
1559
|
-
zv = vfs.add(
|
1585
|
+
zv = vfs.add(src, dst, dst0)
|
1560
1586
|
zv.axs = daxs[dst]
|
1561
1587
|
zv.flags = mflags[dst]
|
1562
1588
|
zv.dbv = None
|
@@ -1590,7 +1616,8 @@ class AuthSrv(object):
|
|
1590
1616
|
if enshare:
|
1591
1617
|
import sqlite3
|
1592
1618
|
|
1593
|
-
|
1619
|
+
zsd = {"d2d": True, "tcolor": self.args.tcolor}
|
1620
|
+
shv = VFS(self.log_func, "", shr, shr, AXS(), zsd)
|
1594
1621
|
|
1595
1622
|
db_path = self.args.shr_db
|
1596
1623
|
db = sqlite3.connect(db_path)
|
@@ -1624,9 +1651,8 @@ class AuthSrv(object):
|
|
1624
1651
|
|
1625
1652
|
# don't know the abspath yet + wanna ensure the user
|
1626
1653
|
# still has the privs they granted, so nullmap it
|
1627
|
-
|
1628
|
-
|
1629
|
-
)
|
1654
|
+
vp = "%s/%s" % (shr, s_k)
|
1655
|
+
shv.nodes[s_k] = VFS(self.log_func, "", vp, vp, s_axs, shv.flags.copy())
|
1630
1656
|
|
1631
1657
|
vfs.nodes[shr] = vfs.all_vols[shr] = shv
|
1632
1658
|
for vol in shv.nodes.values():
|
@@ -1787,6 +1813,24 @@ class AuthSrv(object):
|
|
1787
1813
|
rhisttab[histp] = zv
|
1788
1814
|
vfs.histtab[zv.realpath] = histp
|
1789
1815
|
|
1816
|
+
for vol in vfs.all_vols.values():
|
1817
|
+
use = False
|
1818
|
+
for k in ["zipmaxn", "zipmaxs"]:
|
1819
|
+
try:
|
1820
|
+
zs = vol.flags[k]
|
1821
|
+
except:
|
1822
|
+
zs = getattr(self.args, k)
|
1823
|
+
if zs in ("", "0"):
|
1824
|
+
vol.flags[k] = 0
|
1825
|
+
continue
|
1826
|
+
|
1827
|
+
zf = unhumanize(zs)
|
1828
|
+
vol.flags[k + "_v"] = zf
|
1829
|
+
if zf:
|
1830
|
+
use = True
|
1831
|
+
if use:
|
1832
|
+
vol.flags["zipmax"] = True
|
1833
|
+
|
1790
1834
|
for vol in vfs.all_vols.values():
|
1791
1835
|
lim = Lim(self.log_func)
|
1792
1836
|
use = False
|
@@ -2269,22 +2313,56 @@ class AuthSrv(object):
|
|
2269
2313
|
except Pebkac:
|
2270
2314
|
self.warn_anonwrite = True
|
2271
2315
|
|
2272
|
-
|
2316
|
+
self.idp_warn = []
|
2317
|
+
self.idp_err = []
|
2273
2318
|
for idp_vp in self.idp_vols:
|
2274
|
-
|
2275
|
-
|
2276
|
-
|
2277
|
-
|
2278
|
-
|
2279
|
-
|
2280
|
-
|
2281
|
-
|
2282
|
-
|
2283
|
-
|
2284
|
-
|
2285
|
-
|
2286
|
-
|
2287
|
-
|
2319
|
+
idp_vn, _ = vfs.get(idp_vp, "*", False, False)
|
2320
|
+
idp_vp0 = idp_vn.vpath0
|
2321
|
+
|
2322
|
+
sigils = set(re.findall(r"(\${[ug][}%])", idp_vp0))
|
2323
|
+
if len(sigils) > 1:
|
2324
|
+
t = '\nWARNING: IdP-volume "/%s" created by "/%s" has multiple IdP placeholders: %s'
|
2325
|
+
self.idp_warn.append(t % (idp_vp, idp_vp0, list(sigils)))
|
2326
|
+
continue
|
2327
|
+
|
2328
|
+
sigil = sigils.pop()
|
2329
|
+
par_vp = idp_vp
|
2330
|
+
while par_vp:
|
2331
|
+
par_vp = vsplit(par_vp)[0]
|
2332
|
+
par_vn, _ = vfs.get(par_vp, "*", False, False)
|
2333
|
+
if sigil in par_vn.vpath0:
|
2334
|
+
continue # parent was spawned for and by same user
|
2335
|
+
|
2336
|
+
oth_read = []
|
2337
|
+
oth_write = []
|
2338
|
+
for usr in par_vn.axs.uread:
|
2339
|
+
if usr not in idp_vn.axs.uread:
|
2340
|
+
oth_read.append(usr)
|
2341
|
+
for usr in par_vn.axs.uwrite:
|
2342
|
+
if usr not in idp_vn.axs.uwrite:
|
2343
|
+
oth_write.append(usr)
|
2344
|
+
|
2345
|
+
if "*" in oth_read:
|
2346
|
+
taxs = "WORLD-READABLE"
|
2347
|
+
elif "*" in oth_write:
|
2348
|
+
taxs = "WORLD-WRITABLE"
|
2349
|
+
elif oth_read:
|
2350
|
+
taxs = "READABLE BY %r" % (oth_read,)
|
2351
|
+
elif oth_write:
|
2352
|
+
taxs = "WRITABLE BY %r" % (oth_write,)
|
2353
|
+
else:
|
2354
|
+
break # no sigil; not idp; safe to stop
|
2355
|
+
|
2356
|
+
t = '\nWARNING: IdP-volume "/%s" created by "/%s" has parent/grandparent "/%s" and would be %s'
|
2357
|
+
self.idp_err.append(t % (idp_vp, idp_vp0, par_vn.vpath, taxs))
|
2358
|
+
|
2359
|
+
if self.idp_warn:
|
2360
|
+
t = "WARNING! Some IdP volumes include multiple IdP placeholders; this is too complex to automatically determine if safe or not. To ensure that no users gain unintended access, please use only a single placeholder for each IdP volume."
|
2361
|
+
self.log(t + "".join(self.idp_warn), 1)
|
2362
|
+
|
2363
|
+
if self.idp_err:
|
2364
|
+
t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
|
2365
|
+
self.log(t + "".join(self.idp_err), 1)
|
2288
2366
|
|
2289
2367
|
self.vfs = vfs
|
2290
2368
|
self.acct = acct
|
@@ -2319,11 +2397,6 @@ class AuthSrv(object):
|
|
2319
2397
|
for x, y in vfs.all_vols.items()
|
2320
2398
|
if x != shr and not x.startswith(shrs)
|
2321
2399
|
}
|
2322
|
-
vfs.all_nodes = {
|
2323
|
-
x: y
|
2324
|
-
for x, y in vfs.all_nodes.items()
|
2325
|
-
if x != shr and not x.startswith(shrs)
|
2326
|
-
}
|
2327
2400
|
|
2328
2401
|
assert db and cur and cur2 and shv # type: ignore
|
2329
2402
|
for row in cur.execute("select * from sh"):
|
@@ -2353,6 +2426,7 @@ class AuthSrv(object):
|
|
2353
2426
|
else:
|
2354
2427
|
shn.ls = shn._ls
|
2355
2428
|
|
2429
|
+
shn.shr_owner = s_un
|
2356
2430
|
shn.shr_src = (s_vfs, s_rem)
|
2357
2431
|
shn.realpath = s_vfs.canonical(s_rem)
|
2358
2432
|
|
@@ -2370,7 +2444,7 @@ class AuthSrv(object):
|
|
2370
2444
|
continue # also fine
|
2371
2445
|
for zs in svn.nodes.keys():
|
2372
2446
|
# hide subvolume
|
2373
|
-
vn.nodes[zs] = VFS(self.log_func, "", "", AXS(), {})
|
2447
|
+
vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), {})
|
2374
2448
|
|
2375
2449
|
cur2.close()
|
2376
2450
|
cur.close()
|
@@ -2378,7 +2452,9 @@ class AuthSrv(object):
|
|
2378
2452
|
|
2379
2453
|
self.js_ls = {}
|
2380
2454
|
self.js_htm = {}
|
2381
|
-
for vn in self.vfs.all_nodes.
|
2455
|
+
for vp, vn in self.vfs.all_nodes.items():
|
2456
|
+
if enshare and vp.startswith(shrs):
|
2457
|
+
continue # propagates later in this func
|
2382
2458
|
vf = vn.flags
|
2383
2459
|
vn.js_ls = {
|
2384
2460
|
"idx": "e2d" in vf,
|
@@ -2435,8 +2511,12 @@ class AuthSrv(object):
|
|
2435
2511
|
|
2436
2512
|
vols = list(vfs.all_nodes.values())
|
2437
2513
|
if enshare:
|
2438
|
-
|
2439
|
-
|
2514
|
+
for vol in shv.nodes.values():
|
2515
|
+
if vol.vpath not in vfs.all_nodes:
|
2516
|
+
self.log("BUG: /%s not in all_nodes" % (vol.vpath,), 1)
|
2517
|
+
vols.append(vol)
|
2518
|
+
if shr in vfs.all_nodes:
|
2519
|
+
self.log("BUG: %s found in all_nodes" % (shr,), 1)
|
2440
2520
|
|
2441
2521
|
for vol in vols:
|
2442
2522
|
dbv = vol.get_dbv("")[0]
|
@@ -2539,8 +2619,8 @@ class AuthSrv(object):
|
|
2539
2619
|
if not bos.path.exists(ap):
|
2540
2620
|
pwdb = {}
|
2541
2621
|
else:
|
2542
|
-
|
2543
|
-
|
2622
|
+
jtxt = read_utf8(self.log, ap, True)
|
2623
|
+
pwdb = json.loads(jtxt)
|
2544
2624
|
|
2545
2625
|
pwdb = [x for x in pwdb if x[0] != uname]
|
2546
2626
|
pwdb.append((uname, self.defpw[uname], hpw))
|
@@ -2563,8 +2643,8 @@ class AuthSrv(object):
|
|
2563
2643
|
if not self.args.chpw or not bos.path.exists(ap):
|
2564
2644
|
return
|
2565
2645
|
|
2566
|
-
|
2567
|
-
|
2646
|
+
jtxt = read_utf8(self.log, ap, True)
|
2647
|
+
pwdb = json.loads(jtxt)
|
2568
2648
|
|
2569
2649
|
useen = set()
|
2570
2650
|
urst = set()
|
@@ -3060,8 +3140,9 @@ def expand_config_file(
|
|
3060
3140
|
ipath += " -> " + fp
|
3061
3141
|
ret.append("#\033[36m opening cfg file{}\033[0m".format(ipath))
|
3062
3142
|
|
3063
|
-
|
3064
|
-
|
3143
|
+
cfg_lines = read_utf8(log, fp, True).split("\n")
|
3144
|
+
if True: # diff-golf
|
3145
|
+
for oln in [x.rstrip() for x in cfg_lines]:
|
3065
3146
|
ln = oln.split(" #")[0].strip()
|
3066
3147
|
if ln.startswith("% "):
|
3067
3148
|
pad = " " * len(oln.split("%")[0])
|
copyparty/cfg.py
CHANGED
@@ -52,9 +52,11 @@ def vf_bmap() :
|
|
52
52
|
"og_s_title",
|
53
53
|
"rand",
|
54
54
|
"rss",
|
55
|
+
"wo_up_readme",
|
55
56
|
"xdev",
|
56
57
|
"xlink",
|
57
58
|
"xvol",
|
59
|
+
"zipmaxu",
|
58
60
|
):
|
59
61
|
ret[k] = k
|
60
62
|
return ret
|
@@ -101,6 +103,9 @@ def vf_vmap() :
|
|
101
103
|
"u2ts",
|
102
104
|
"ups_who",
|
103
105
|
"zip_who",
|
106
|
+
"zipmaxn",
|
107
|
+
"zipmaxs",
|
108
|
+
"zipmaxt",
|
104
109
|
):
|
105
110
|
ret[k] = k
|
106
111
|
return ret
|
@@ -169,6 +174,7 @@ flagcats = {
|
|
169
174
|
"vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)",
|
170
175
|
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
|
171
176
|
"medialinks": "return medialinks for non-up2k uploads (not hotlinks)",
|
177
|
+
"wo_up_readme": "write-only users can upload logues without getting renamed",
|
172
178
|
"rand": "force randomized filenames, 9 chars long by default",
|
173
179
|
"nrand=N": "randomized filenames are N chars long",
|
174
180
|
"u2ow=N": "overwrite existing files? 0=no 1=if-older 2=always",
|
@@ -299,6 +305,10 @@ flagcats = {
|
|
299
305
|
"rss": "allow '?rss' URL suffix (experimental)",
|
300
306
|
"ups_who=2": "restrict viewing the list of recent uploads",
|
301
307
|
"zip_who=2": "restrict access to download-as-zip/tar",
|
308
|
+
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",
|
309
|
+
"zipmaxs=2g": "reject download-as-zip if size over 2 GiB",
|
310
|
+
"zipmaxt=no": "reply with 'no' if download-as-zip exceeds max",
|
311
|
+
"zipmaxu": "zip-size-limit does not apply to authenticated users",
|
302
312
|
"nopipe": "disable race-the-beam (download unfinished uploads)",
|
303
313
|
"mv_retry": "ms-windows: timeout for renaming busy files",
|
304
314
|
"rm_retry": "ms-windows: timeout for deleting busy files",
|
copyparty/fsutil.py
CHANGED
@@ -72,7 +72,7 @@ class Fstab(object):
|
|
72
72
|
return vid
|
73
73
|
|
74
74
|
def build_fallback(self) :
|
75
|
-
self.tab = VFS(self.log_func, "idk", "/", AXS(), {})
|
75
|
+
self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
|
76
76
|
self.trusted = False
|
77
77
|
|
78
78
|
def build_tab(self) :
|
@@ -105,9 +105,10 @@ class Fstab(object):
|
|
105
105
|
|
106
106
|
tab1.sort(key=lambda x: (len(x[0]), x[0]))
|
107
107
|
path1, fs1 = tab1[0]
|
108
|
-
tab = VFS(self.log_func, fs1, path1, AXS(), {})
|
108
|
+
tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
|
109
109
|
for path, fs in tab1[1:]:
|
110
|
-
|
110
|
+
zs = path.lstrip("/")
|
111
|
+
tab.add(fs, zs, zs)
|
111
112
|
|
112
113
|
self.tab = tab
|
113
114
|
self.srctab = srctab
|
@@ -123,9 +124,10 @@ class Fstab(object):
|
|
123
124
|
if not self.trusted:
|
124
125
|
# no mtab access; have to build as we go
|
125
126
|
if "/" in rem:
|
126
|
-
|
127
|
+
zs = os.path.join(vn.vpath, rem.split("/")[0])
|
128
|
+
self.tab.add("idk", zs, zs)
|
127
129
|
if rem:
|
128
|
-
self.tab.add(nval, path)
|
130
|
+
self.tab.add(nval, path, path)
|
129
131
|
else:
|
130
132
|
vn.realpath = nval
|
131
133
|
|