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 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
- with open(fp, "rb") as f:
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 get_ah_salt() :
293
- fp = os.path.join(E.cfg, "ah-salt.txt")
266
+ def get_salt(name , nbytes ) :
267
+ fp = os.path.join(E.cfg, "%s-salt.txt" % (name,))
294
268
  try:
295
- with open(fp, "rb") as f:
296
- ret = f.read().strip()
269
+ return read_utf8(None, fp, True).strip()
297
270
  except:
298
- ret = b64enc(os.urandom(18))
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 = get_fk_salt()
1548
- dk_salt = get_dk_salt()
1549
- ah_salt = get_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
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 16, 16)
3
+ VERSION = (1, 16, 18)
4
4
  CODENAME = "COPYparty"
5
- BUILD_DT = (2025, 2, 28)
5
+ BUILD_DT = (2025, 3, 23)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
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
- vn = VFS(self.log, src, vp, AXS(), {})
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
- mount ,
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 = {} # moutpoint:flags
1446
- mount = {} # dst:src (mountpoint:realpath)
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 k, v in mount.items():
1526
- cased[k] = absreal(v)
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, mount[dst], dst, daxs[dst], mflags[dst])
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(mount[dst], dst)
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
- shv = VFS(self.log_func, "", shr, AXS(), {})
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
- shv.nodes[s_k] = VFS(
1628
- self.log_func, "", "%s/%s" % (shr, s_k), s_axs, shv.flags.copy()
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
- idp_err = "WARNING! The following IdP volumes are mounted directly below another volume where anonymous 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 anonymous users 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."
2316
+ self.idp_warn = []
2317
+ self.idp_err = []
2273
2318
  for idp_vp in self.idp_vols:
2274
- parent_vp = vsplit(idp_vp)[0]
2275
- vn, _ = vfs.get(parent_vp, "*", False, False)
2276
- zs = (
2277
- "READABLE"
2278
- if "*" in vn.axs.uread
2279
- else "WRITABLE"
2280
- if "*" in vn.axs.uwrite
2281
- else ""
2282
- )
2283
- if zs:
2284
- t = '\nWARNING: Volume "/%s" appears below "/%s" and would be WORLD-%s'
2285
- idp_err += t % (idp_vp, vn.vpath, zs)
2286
- if "\n" in idp_err:
2287
- self.log(idp_err, 1)
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.values():
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
- vols.append(shv)
2439
- vols.extend(list(shv.nodes.values()))
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
- with open(ap, "r", encoding="utf-8") as f:
2543
- pwdb = json.load(f)
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
- with open(ap, "r", encoding="utf-8") as f:
2567
- pwdb = json.load(f)
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
- with open(fp, "rb") as f:
3064
- for oln in [x.decode("utf-8").rstrip() for x in f]:
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
- tab.add(fs, path.lstrip("/"))
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
- self.tab.add("idk", os.path.join(vn.vpath, rem.split("/")[0]))
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