copyparty 1.10.2__py3-none-any.whl → 1.11.1__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.
Files changed (45) hide show
  1. copyparty/__main__.py +16 -6
  2. copyparty/__version__.py +3 -3
  3. copyparty/authsrv.py +365 -66
  4. copyparty/cfg.py +3 -0
  5. copyparty/ftpd.py +2 -2
  6. copyparty/httpcli.py +113 -48
  7. copyparty/httpconn.py +4 -1
  8. copyparty/httpsrv.py +8 -3
  9. copyparty/metrics.py +3 -0
  10. copyparty/multicast.py +1 -1
  11. copyparty/smbd.py +1 -1
  12. copyparty/svchub.py +55 -14
  13. copyparty/tftpd.py +11 -9
  14. copyparty/up2k.py +143 -49
  15. copyparty/util.py +52 -8
  16. copyparty/web/baguettebox.js.gz +0 -0
  17. copyparty/web/browser.css.gz +0 -0
  18. copyparty/web/browser.html +1 -1
  19. copyparty/web/browser.js.gz +0 -0
  20. copyparty/web/browser2.html +1 -1
  21. copyparty/web/deps/easymde.css.gz +0 -0
  22. copyparty/web/deps/easymde.js.gz +0 -0
  23. copyparty/web/md.css.gz +0 -0
  24. copyparty/web/md.html +1 -1
  25. copyparty/web/md.js.gz +0 -0
  26. copyparty/web/md2.css.gz +0 -0
  27. copyparty/web/md2.js.gz +0 -0
  28. copyparty/web/mde.css.gz +0 -0
  29. copyparty/web/mde.html +1 -1
  30. copyparty/web/mde.js.gz +0 -0
  31. copyparty/web/msg.css.gz +0 -0
  32. copyparty/web/msg.html +1 -1
  33. copyparty/web/splash.css.gz +0 -0
  34. copyparty/web/splash.html +4 -2
  35. copyparty/web/splash.js.gz +0 -0
  36. copyparty/web/svcs.html +1 -1
  37. copyparty/web/ui.css.gz +0 -0
  38. copyparty/web/up2k.js.gz +0 -0
  39. copyparty/web/util.js.gz +0 -0
  40. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/METADATA +62 -20
  41. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/RECORD +45 -43
  42. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/WHEEL +1 -1
  43. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/LICENSE +0 -0
  44. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/entry_points.txt +0 -0
  45. {copyparty-1.10.2.dist-info → copyparty-1.11.1.dist-info}/top_level.txt +0 -0
copyparty/cfg.py CHANGED
@@ -66,6 +66,7 @@ def vf_vmap() :
66
66
  "rm_retry",
67
67
  "sort",
68
68
  "unlist",
69
+ "u2abort",
69
70
  "u2ts",
70
71
  ):
71
72
  ret[k] = k
@@ -116,6 +117,7 @@ flagcats = {
116
117
  "hardlink": "does dedup with hardlinks instead of symlinks",
117
118
  "neversymlink": "disables symlink fallback; full copy instead",
118
119
  "copydupes": "disables dedup, always saves full copies of dupes",
120
+ "sparse": "force use of sparse files, mainly for s3-backed storage",
119
121
  "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
120
122
  "nosub": "forces all uploads into the top folder of the vfs",
121
123
  "magic": "enables filetype detection for nameless uploads",
@@ -130,6 +132,7 @@ flagcats = {
130
132
  "rand": "force randomized filenames, 9 chars long by default",
131
133
  "nrand=N": "randomized filenames are N chars long",
132
134
  "u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time",
135
+ "u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk",
133
136
  "sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
134
137
  "df=1g": "ensure 1 GiB free disk space",
135
138
  },
copyparty/ftpd.py CHANGED
@@ -295,7 +295,7 @@ class FtpFs(AbstractedFS):
295
295
 
296
296
  vp = join(self.cwd, path).lstrip("/")
297
297
  try:
298
- self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False)
298
+ self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False, False)
299
299
  except Exception as ex:
300
300
  raise FSE(str(ex))
301
301
 
@@ -405,7 +405,7 @@ class FtpHandler(FTPHandler):
405
405
  if cip.startswith("::ffff:"):
406
406
  cip = cip[7:]
407
407
 
408
- if self.args.ftp_ipa_re and not self.args.ftp_ipa_re.match(cip):
408
+ if self.args.ftp_ipa_nm and not self.args.ftp_ipa_nm.map(cip):
409
409
  logging.warning("client rejected (--ftp-ipa): %s", cip)
410
410
  self.connected = False
411
411
  conn.close()
copyparty/httpcli.py CHANGED
@@ -166,16 +166,12 @@ class HttpCli(object):
166
166
  self.can_dot = False
167
167
  self.out_headerlist = []
168
168
  self.out_headers = {}
169
- self.html_head = " "
170
169
  # post
171
170
  self.parser = None
172
171
  # end placeholders
173
172
 
174
173
  self.bufsz = 1024 * 32
175
- h = self.args.html_head
176
- if self.args.no_robots:
177
- h = META_NOBOTS + (("\n" + h) if h else "")
178
- self.html_head = h
174
+ self.html_head = ""
179
175
 
180
176
  def log(self, msg , c = 0) :
181
177
  ptn = self.asrv.re_pwd
@@ -227,13 +223,11 @@ class HttpCli(object):
227
223
  "Vary": "Origin, PW, Cookie",
228
224
  "Cache-Control": "no-store, max-age=0",
229
225
  }
230
- if self.args.no_robots:
231
- self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
232
226
 
233
- if self.is_banned():
227
+ if self.args.early_ban and self.is_banned():
234
228
  return False
235
229
 
236
- if self.args.ipa_re and not self.args.ipa_re.match(self.conn.addr[0]):
230
+ if self.conn.ipa_nm and not self.conn.ipa_nm.map(self.conn.addr[0]):
237
231
  self.log("client rejected (--ipa)", 3)
238
232
  self.terse_reply(b"", 500)
239
233
  return False
@@ -296,6 +290,7 @@ class HttpCli(object):
296
290
  zs = "%s:%s" % self.s.getsockname()[:2]
297
291
  self.host = zs[7:] if zs.startswith("::ffff:") else zs
298
292
 
293
+ trusted_xff = False
299
294
  n = self.args.rproxy
300
295
  if n:
301
296
  zso = self.headers.get(self.args.xff_hdr)
@@ -312,21 +307,26 @@ class HttpCli(object):
312
307
  self.log(t.format(self.args.rproxy, zso), c=3)
313
308
 
314
309
  pip = self.conn.addr[0]
315
- if self.args.xff_re and not self.args.xff_re.match(pip):
316
- t = 'got header "%s" from untrusted source "%s" claiming the true client ip is "%s" (raw value: "%s"); if you trust this, you must allowlist this proxy with "--xff-src=%s"'
310
+ xffs = self.conn.xff_nm
311
+ if xffs and not xffs.map(pip):
312
+ t = 'got header "%s" from untrusted source "%s" claiming the true client ip is "%s" (raw value: "%s"); if you trust this, you must allowlist this proxy with "--xff-src=%s"%s'
317
313
  if self.headers.get("cf-connecting-ip"):
318
- t += " Alternatively, if you are behind cloudflare, it is better to specify these two instead: --xff-hdr=cf-connecting-ip --xff-src=any"
314
+ t += ' Note: if you are behind cloudflare, then this default header is not a good choice; please first make sure your local reverse-proxy (if any) does not allow non-cloudflare IPs from providing cf-* headers, and then add this additional global setting: "--xff-hdr=cf-connecting-ip"'
315
+ else:
316
+ t += ' Note: depending on your reverse-proxy, and/or WAF, and/or other intermediates, you may want to read the true client IP from another header by also specifying "--xff-hdr=SomeOtherHeader"'
319
317
  zs = (
320
318
  ".".join(pip.split(".")[:2]) + "."
321
319
  if "." in pip
322
320
  else ":".join(pip.split(":")[:4]) + ":"
323
- )
324
- self.log(t % (self.args.xff_hdr, pip, cli_ip, zso, zs), 3)
321
+ ) + "0.0/16"
322
+ zs2 = ' or "--xff-src=lan"' if self.conn.xff_lan.map(pip) else ""
323
+ self.log(t % (self.args.xff_hdr, pip, cli_ip, zso, zs, zs2), 3)
325
324
  else:
326
325
  self.ip = cli_ip
327
326
  self.is_vproxied = bool(self.args.R)
328
327
  self.log_src = self.conn.set_rproxy(self.ip)
329
328
  self.host = self.headers.get("x-forwarded-host") or self.host
329
+ trusted_xff = True
330
330
 
331
331
  if self.is_banned():
332
332
  return False
@@ -454,9 +454,56 @@ class HttpCli(object):
454
454
 
455
455
  if self.args.idp_h_usr:
456
456
  self.pw = ""
457
- self.uname = self.headers.get(self.args.idp_h_usr) or "*"
458
- if self.uname not in self.asrv.vfs.aread:
459
- self.log("unknown username: [%s]" % (self.uname), 1)
457
+ idp_usr = self.headers.get(self.args.idp_h_usr) or ""
458
+ if idp_usr:
459
+ idp_grp = (
460
+ self.headers.get(self.args.idp_h_grp) or ""
461
+ if self.args.idp_h_grp
462
+ else ""
463
+ )
464
+
465
+ if not trusted_xff:
466
+ pip = self.conn.addr[0]
467
+ xffs = self.conn.xff_nm
468
+ trusted_xff = xffs and xffs.map(pip)
469
+
470
+ trusted_key = (
471
+ not self.args.idp_h_key
472
+ ) or self.args.idp_h_key in self.headers
473
+
474
+ if trusted_key and trusted_xff:
475
+ self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp)
476
+ else:
477
+ if not trusted_key:
478
+ t = 'the idp-h-key header ("%s") is not present in the request; will NOT trust the other headers saying that the client\'s username is "%s" and group is "%s"'
479
+ self.log(t % (self.args.idp_h_key, idp_usr, idp_grp), 3)
480
+
481
+ if not trusted_xff:
482
+ t = 'got IdP headers from untrusted source "%s" claiming the client\'s username is "%s" and group is "%s"; if you trust this, you must allowlist this proxy with "--xff-src=%s"%s'
483
+ if not self.args.idp_h_key:
484
+ t += " Note: you probably also want to specify --idp-h-key <SECRET-HEADER-NAME> for additional security"
485
+
486
+ pip = self.conn.addr[0]
487
+ zs = (
488
+ ".".join(pip.split(".")[:2]) + "."
489
+ if "." in pip
490
+ else ":".join(pip.split(":")[:4]) + ":"
491
+ ) + "0.0/16"
492
+ zs2 = (
493
+ ' or "--xff-src=lan"' if self.conn.xff_lan.map(pip) else ""
494
+ )
495
+ self.log(t % (pip, idp_usr, idp_grp, zs, zs2), 3)
496
+
497
+ idp_usr = "*"
498
+ idp_grp = ""
499
+
500
+ if idp_usr in self.asrv.vfs.aread:
501
+ self.uname = idp_usr
502
+ self.html_head += "<script>var is_idp=1</script>\n"
503
+ else:
504
+ self.log("unknown username: [%s]" % (idp_usr), 1)
505
+ self.uname = "*"
506
+ else:
460
507
  self.uname = "*"
461
508
  else:
462
509
  self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
@@ -506,6 +553,10 @@ class HttpCli(object):
506
553
 
507
554
  self.s.settimeout(self.args.s_tbody or None)
508
555
 
556
+ if "norobots" in vn.flags:
557
+ self.html_head += META_NOBOTS
558
+ self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
559
+
509
560
  try:
510
561
  cors_k = self._cors()
511
562
  if self.mode in ("GET", "HEAD"):
@@ -514,9 +565,13 @@ class HttpCli(object):
514
565
  return self.handle_options() and self.keepalive
515
566
 
516
567
  if not cors_k:
568
+ host = self.headers.get("host", "<?>")
517
569
  origin = self.headers.get("origin", "<?>")
518
- self.log("cors-reject {} from {}".format(self.mode, origin), 3)
519
- raise Pebkac(403, "no surfing")
570
+ proto = "https://" if self.is_https else "http://"
571
+ guess = "modifying" if (origin and host) else "stripping"
572
+ t = "cors-reject %s because request-header Origin='%s' does not match request-protocol '%s' and host '%s' based on request-header Host='%s' (note: if this request is not malicious, check if your reverse-proxy is accidentally %s request headers, in particular 'Origin', for example by running copyparty with --ihead='*' to show all request headers)"
573
+ self.log(t % (self.mode, origin, proto, self.host, host, guess), 3)
574
+ raise Pebkac(403, "rejected by cors-check")
520
575
 
521
576
  # getattr(self.mode) is not yet faster than this
522
577
  if self.mode == "POST":
@@ -647,7 +702,11 @@ class HttpCli(object):
647
702
 
648
703
  def k304(self) :
649
704
  k304 = self.cookies.get("k304")
650
- return k304 == "y" or ("; Trident/" in self.ua and not k304)
705
+ return (
706
+ k304 == "y"
707
+ or (self.args.k304 == 2 and k304 != "n")
708
+ or ("; Trident/" in self.ua and not k304)
709
+ )
651
710
 
652
711
  def send_headers(
653
712
  self,
@@ -2823,11 +2882,11 @@ class HttpCli(object):
2823
2882
  logtail = ""
2824
2883
 
2825
2884
  #
2826
- # if request is for foo.js, check if we have foo.js.{gz,br}
2885
+ # if request is for foo.js, check if we have foo.js.gz
2827
2886
 
2828
2887
  file_ts = 0.0
2829
2888
  editions = {}
2830
- for ext in ["", ".gz", ".br"]:
2889
+ for ext in ("", ".gz"):
2831
2890
  try:
2832
2891
  fs_path = req_path + ext
2833
2892
  st = bos.stat(fs_path)
@@ -2872,12 +2931,7 @@ class HttpCli(object):
2872
2931
  x.strip()
2873
2932
  for x in self.headers.get("accept-encoding", "").lower().split(",")
2874
2933
  ]
2875
- if ".br" in editions and "br" in supported_editions:
2876
- is_compressed = True
2877
- selected_edition = ".br"
2878
- fs_path, file_sz = editions[".br"]
2879
- self.out_headers["Content-Encoding"] = "br"
2880
- elif ".gz" in editions:
2934
+ if ".gz" in editions:
2881
2935
  is_compressed = True
2882
2936
  selected_edition = ".gz"
2883
2937
  fs_path, file_sz = editions[".gz"]
@@ -2893,13 +2947,8 @@ class HttpCli(object):
2893
2947
  is_compressed = False
2894
2948
  selected_edition = "plain"
2895
2949
 
2896
- try:
2897
- fs_path, file_sz = editions[selected_edition]
2898
- logmsg += "{} ".format(selected_edition.lstrip("."))
2899
- except:
2900
- # client is old and we only have .br
2901
- # (could make brotli a dep to fix this but it's not worth)
2902
- raise Pebkac(404)
2950
+ fs_path, file_sz = editions[selected_edition]
2951
+ logmsg += "{} ".format(selected_edition.lstrip("."))
2903
2952
 
2904
2953
  #
2905
2954
  # partial
@@ -3339,6 +3388,8 @@ class HttpCli(object):
3339
3388
  self.reply(zb, mime="text/plain; charset=utf-8")
3340
3389
  return True
3341
3390
 
3391
+ self.html_head += self.vn.flags.get("html_head", "")
3392
+
3342
3393
  html = self.j2s(
3343
3394
  "splash",
3344
3395
  this=self,
@@ -3354,6 +3405,7 @@ class HttpCli(object):
3354
3405
  dbwt=vs["dbwt"],
3355
3406
  url_suf=suf,
3356
3407
  k304=self.k304(),
3408
+ k304vis=self.args.k304 > 0,
3357
3409
  ver=S_VERSION if self.args.ver else "",
3358
3410
  ahttps="" if self.is_https else "https://" + self.host + self.req,
3359
3411
  )
@@ -3362,7 +3414,7 @@ class HttpCli(object):
3362
3414
 
3363
3415
  def set_k304(self) :
3364
3416
  v = self.uparam["k304"].lower()
3365
- if v == "y":
3417
+ if v in "yn":
3366
3418
  dur = 86400 * 299
3367
3419
  else:
3368
3420
  dur = 0
@@ -3545,9 +3597,6 @@ class HttpCli(object):
3545
3597
  return ret
3546
3598
 
3547
3599
  def tx_ups(self) :
3548
- if not self.args.unpost:
3549
- raise Pebkac(403, "the unpost feature is disabled in server config")
3550
-
3551
3600
  idx = self.conn.get_u2idx()
3552
3601
  if not idx or not hasattr(idx, "p_end"):
3553
3602
  raise Pebkac(500, "sqlite3 is not available on the server; cannot unpost")
@@ -3565,7 +3614,20 @@ class HttpCli(object):
3565
3614
  if "fk" in vol.flags
3566
3615
  and (self.uname in vol.axs.uread or self.uname in vol.axs.upget)
3567
3616
  }
3568
- for vol in self.asrv.vfs.all_vols.values():
3617
+
3618
+ x = self.conn.hsrv.broker.ask(
3619
+ "up2k.get_unfinished_by_user", self.uname, self.ip
3620
+ )
3621
+ uret = x.get()
3622
+
3623
+ if not self.args.unpost:
3624
+ allvols = []
3625
+ else:
3626
+ allvols = list(self.asrv.vfs.all_vols.values())
3627
+
3628
+ allvols = [x for x in allvols if "e2d" in x.flags]
3629
+
3630
+ for vol in allvols:
3569
3631
  cur = idx.get_cur(vol.realpath)
3570
3632
  if not cur:
3571
3633
  continue
@@ -3617,9 +3679,13 @@ class HttpCli(object):
3617
3679
  for v in ret:
3618
3680
  v["vp"] = self.args.SR + v["vp"]
3619
3681
 
3620
- jtxt = json.dumps(ret, indent=2, sort_keys=True).encode("utf-8", "replace")
3621
- self.log("{} #{} {:.2f}sec".format(lm, len(ret), time.time() - t0))
3622
- self.reply(jtxt, mime="application/json")
3682
+ if not allvols:
3683
+ ret = [{"kinshi": 1}]
3684
+
3685
+ jtxt = '{"u":%s,"c":%s}' % (uret, json.dumps(ret, indent=0))
3686
+ zi = len(uret.split('\n"pd":')) - 1
3687
+ self.log("%s #%d+%d %.2fsec" % (lm, zi, len(ret), time.time() - t0))
3688
+ self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
3623
3689
  return True
3624
3690
 
3625
3691
  def handle_rm(self, req ) :
@@ -3634,11 +3700,12 @@ class HttpCli(object):
3634
3700
  elif self.is_vproxied:
3635
3701
  req = [x[len(self.args.SR) :] for x in req]
3636
3702
 
3703
+ unpost = "unpost" in self.uparam
3637
3704
  nlim = int(self.uparam.get("lim") or 0)
3638
3705
  lim = [nlim, nlim] if nlim else []
3639
3706
 
3640
3707
  x = self.conn.hsrv.broker.ask(
3641
- "up2k.handle_rm", self.uname, self.ip, req, lim, False
3708
+ "up2k.handle_rm", self.uname, self.ip, req, lim, False, unpost
3642
3709
  )
3643
3710
  self.loud_reply(x.get())
3644
3711
  return True
@@ -3776,11 +3843,9 @@ class HttpCli(object):
3776
3843
  e2d = "e2d" in vn.flags
3777
3844
  e2t = "e2t" in vn.flags
3778
3845
 
3779
- self.html_head = vn.flags.get("html_head", "")
3780
- if vn.flags.get("norobots") or "b" in self.uparam:
3846
+ self.html_head += vn.flags.get("html_head", "")
3847
+ if "b" in self.uparam:
3781
3848
  self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
3782
- else:
3783
- self.out_headers.pop("X-Robots-Tag", None)
3784
3849
 
3785
3850
  is_dir = stat.S_ISDIR(st.st_mode)
3786
3851
  fk_pass = False
copyparty/httpconn.py CHANGED
@@ -23,7 +23,7 @@ from .mtag import HAVE_FFMPEG
23
23
  from .th_cli import ThumbCli
24
24
  from .th_srv import HAVE_PIL, HAVE_VIPS
25
25
  from .u2idx import U2idx
26
- from .util import HMaccas, shut_socket
26
+ from .util import HMaccas, NetMap, shut_socket
27
27
 
28
28
  if TYPE_CHECKING:
29
29
  from .httpsrv import HttpSrv
@@ -52,6 +52,9 @@ class HttpConn(object):
52
52
  self.E = self.args.E
53
53
  self.asrv = hsrv.asrv # mypy404
54
54
  self.u2fh = hsrv.u2fh # mypy404
55
+ self.ipa_nm = hsrv.ipa_nm
56
+ self.xff_nm = hsrv.xff_nm
57
+ self.xff_lan = hsrv.xff_lan # type: ignore
55
58
  self.iphash = hsrv.broker.iphash
56
59
  self.bans = hsrv.bans
57
60
  self.aclose = hsrv.aclose
copyparty/httpsrv.py CHANGED
@@ -67,6 +67,7 @@ from .util import (
67
67
  Netdev,
68
68
  NetMap,
69
69
  absreal,
70
+ build_netmap,
70
71
  ipnorm,
71
72
  min_ex,
72
73
  shut_socket,
@@ -99,7 +100,7 @@ class HttpSrv(object):
99
100
  self.t0 = time.time()
100
101
  nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else ""
101
102
  self.magician = Magician()
102
- self.nm = NetMap([], {})
103
+ self.nm = NetMap([], [])
103
104
  self.ssdp = None
104
105
  self.gpwd = Garda(self.args.ban_pw)
105
106
  self.g404 = Garda(self.args.ban_404)
@@ -146,6 +147,10 @@ class HttpSrv(object):
146
147
  zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
147
148
  self.prism = os.path.exists(zs)
148
149
 
150
+ self.ipa_nm = build_netmap(self.args.ipa)
151
+ self.xff_nm = build_netmap(self.args.xff_src)
152
+ self.xff_lan = build_netmap("lan")
153
+
149
154
  self.statics = set()
150
155
  self._build_statics()
151
156
 
@@ -187,7 +192,7 @@ class HttpSrv(object):
187
192
  for fn in df:
188
193
  ap = absreal(os.path.join(dp, fn))
189
194
  self.statics.add(ap)
190
- if ap.endswith(".gz") or ap.endswith(".br"):
195
+ if ap.endswith(".gz"):
191
196
  self.statics.add(ap[:-3])
192
197
 
193
198
  def set_netdevs(self, netdevs ) :
@@ -195,7 +200,7 @@ class HttpSrv(object):
195
200
  for ip, _ in self.bound:
196
201
  ips.add(ip)
197
202
 
198
- self.nm = NetMap(list(ips), netdevs)
203
+ self.nm = NetMap(list(ips), list(netdevs))
199
204
 
200
205
  def start_threads(self, n ) :
201
206
  self.tp_nthr += n
copyparty/metrics.py CHANGED
@@ -206,6 +206,9 @@ class Metrics(object):
206
206
  try:
207
207
  x = self.hsrv.broker.ask("up2k.get_unfinished")
208
208
  xs = x.get()
209
+ if not xs:
210
+ raise Exception("up2k mutex acquisition timed out")
211
+
209
212
  xj = json.loads(xs)
210
213
  for ptop, (nbytes, nfiles) in xj.items():
211
214
  tnbytes += nbytes
copyparty/multicast.py CHANGED
@@ -107,7 +107,7 @@ class MCast(object):
107
107
  )
108
108
 
109
109
  ips = [x for x in ips if x not in ("::1", "127.0.0.1")]
110
- ips = find_prefix(ips, netdevs)
110
+ ips = find_prefix(ips, list(netdevs))
111
111
 
112
112
  on = self.on[:]
113
113
  off = self.off[:]
copyparty/smbd.py CHANGED
@@ -337,7 +337,7 @@ class SMB(object):
337
337
  yeet("blocked delete (no-del-acc): " + vpath)
338
338
 
339
339
  vpath = vpath.replace("\\", "/").lstrip("/")
340
- self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False)
340
+ self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False, False)
341
341
 
342
342
  def _utime(self, vpath , times ) :
343
343
  if not self.args.smbw:
copyparty/svchub.py CHANGED
@@ -22,7 +22,7 @@ from datetime import datetime, timedelta
22
22
  # print(currentframe().f_lineno)
23
23
 
24
24
 
25
- from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
25
+ from .__init__ import ANYWIN, E, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
26
26
  from .authsrv import BAD_CFG, AuthSrv
27
27
  from .cert import ensure_cert
28
28
  from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
@@ -43,6 +43,7 @@ from .util import (
43
43
  ODict,
44
44
  alltrace,
45
45
  ansi_re,
46
+ build_netmap,
46
47
  min_ex,
47
48
  mp,
48
49
  odfusion,
@@ -88,7 +89,7 @@ class SvcHub(object):
88
89
  self.stopping = False
89
90
  self.stopped = False
90
91
  self.reload_req = False
91
- self.reloading = False
92
+ self.reloading = 0
92
93
  self.stop_cond = threading.Condition()
93
94
  self.nsigs = 3
94
95
  self.retcode = 0
@@ -148,6 +149,8 @@ class SvcHub(object):
148
149
  lg.handlers = [lh]
149
150
  lg.setLevel(logging.DEBUG)
150
151
 
152
+ self._check_env()
153
+
151
154
  if args.stackmon:
152
155
  start_stackmon(args.stackmon, 0)
153
156
 
@@ -379,6 +382,17 @@ class SvcHub(object):
379
382
 
380
383
  Daemon(self.sd_notify, "sd-notify")
381
384
 
385
+ def _check_env(self) :
386
+ try:
387
+ files = os.listdir(E.cfg)
388
+ except:
389
+ files = []
390
+
391
+ hits = [x for x in files if x.lower().endswith(".conf")]
392
+ if hits:
393
+ t = "WARNING: found config files in [%s]: %s\n config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)"
394
+ self.log("root", t % (E.cfg, ", ".join(hits)), 3)
395
+
382
396
  def _process_config(self) :
383
397
  al = self.args
384
398
 
@@ -459,12 +473,11 @@ class SvcHub(object):
459
473
 
460
474
  al.xff_hdr = al.xff_hdr.lower()
461
475
  al.idp_h_usr = al.idp_h_usr.lower()
462
- # al.idp_h_grp = al.idp_h_grp.lower()
476
+ al.idp_h_grp = al.idp_h_grp.lower()
477
+ al.idp_h_key = al.idp_h_key.lower()
463
478
 
464
- al.xff_re = self._ipa2re(al.xff_src)
465
- al.ipa_re = self._ipa2re(al.ipa)
466
- al.ftp_ipa_re = self._ipa2re(al.ftp_ipa or al.ipa)
467
- al.tftp_ipa_re = self._ipa2re(al.tftp_ipa or al.ipa)
479
+ al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa)
480
+ al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa)
468
481
 
469
482
  mte = ODict.fromkeys(DEF_MTE.split(","), True)
470
483
  al.mte = odfusion(mte, al.mte)
@@ -481,6 +494,18 @@ class SvcHub(object):
481
494
  if ptn:
482
495
  setattr(self.args, k, re.compile(ptn))
483
496
 
497
+ for k in ["idp_gsep"]:
498
+ ptn = getattr(self.args, k)
499
+ if "]" in ptn:
500
+ ptn = "]" + ptn.replace("]", "")
501
+ if "[" in ptn:
502
+ ptn = ptn.replace("[", "") + "["
503
+ if "-" in ptn:
504
+ ptn = ptn.replace("-", "") + "-"
505
+
506
+ ptn = ptn.replace("\\", "\\\\").replace("^", "\\^")
507
+ setattr(self.args, k, re.compile("[%s]" % (ptn,)))
508
+
484
509
  try:
485
510
  zf1, zf2 = self.args.rm_retry.split("/")
486
511
  self.args.rm_re_t = float(zf1)
@@ -656,21 +681,37 @@ class SvcHub(object):
656
681
  self.log("root", "ssdp startup failed;\n" + min_ex(), 3)
657
682
 
658
683
  def reload(self) :
659
- if self.reloading:
660
- return "cannot reload; already in progress"
684
+ with self.up2k.mutex:
685
+ if self.reloading:
686
+ return "cannot reload; already in progress"
687
+ self.reloading = 1
661
688
 
662
- self.reloading = True
663
689
  Daemon(self._reload, "reloading")
664
690
  return "reload initiated"
665
691
 
666
- def _reload(self) :
667
- self.log("root", "reload scheduled")
692
+ def _reload(self, rescan_all_vols = True) :
668
693
  with self.up2k.mutex:
694
+ if self.reloading != 1:
695
+ return
696
+ self.reloading = 2
697
+ self.log("root", "reloading config")
669
698
  self.asrv.reload()
670
- self.up2k.reload()
699
+ self.up2k.reload(rescan_all_vols)
671
700
  self.broker.reload()
701
+ self.reloading = 0
702
+
703
+ def _reload_blocking(self, rescan_all_vols = True) :
704
+ while True:
705
+ with self.up2k.mutex:
706
+ if self.reloading < 2:
707
+ self.reloading = 1
708
+ break
709
+ time.sleep(0.05)
710
+
711
+ # try to handle multiple pending IdP reloads at once:
712
+ time.sleep(0.2)
672
713
 
673
- self.reloading = False
714
+ self._reload(rescan_all_vols=rescan_all_vols)
674
715
 
675
716
  def stop_thr(self) :
676
717
  while not self.stop_req:
copyparty/tftpd.py CHANGED
@@ -52,17 +52,16 @@ def noop(*a, **ka) :
52
52
 
53
53
  def _serverInitial(self, pkt , raddress , rport ) :
54
54
  info("connection from %s:%s", raddress, rport)
55
- ret = _orig_serverInitial(self, pkt, raddress, rport)
56
- ptn = _hub[0].args.tftp_ipa_re
57
- if ptn and not ptn.match(raddress):
55
+ ret = _sinitial[0](self, pkt, raddress, rport)
56
+ nm = _hub[0].args.tftp_ipa_nm
57
+ if nm and not nm.map(raddress):
58
58
  yeet("client rejected (--tftp-ipa): %s" % (raddress,))
59
59
  return ret
60
60
 
61
61
 
62
- # patch ipa-check into partftpd
62
+ # patch ipa-check into partftpd (part 1/2)
63
63
  _hub = []
64
- _orig_serverInitial = TftpStates.TftpServerState.serverInitial
65
- TftpStates.TftpServerState.serverInitial = _serverInitial
64
+ _sinitial = []
66
65
 
67
66
 
68
67
  class Tftpd(object):
@@ -95,8 +94,6 @@ class Tftpd(object):
95
94
  cbak = []
96
95
  if not self.args.tftp_no_fast and not EXE:
97
96
  try:
98
- import inspect
99
-
100
97
  ptn = re.compile(r"(^\s*)log\.debug\(.*\)$")
101
98
  for C in Cs:
102
99
  cbak.append(C.__dict__)
@@ -113,6 +110,11 @@ class Tftpd(object):
113
110
  for C in Cs:
114
111
  C.log.debug = noop
115
112
 
113
+ # patch ipa-check into partftpd (part 2/2)
114
+ _sinitial[:] = []
115
+ _sinitial.append(TftpStates.TftpServerState.serverInitial)
116
+ TftpStates.TftpServerState.serverInitial = _serverInitial
117
+
116
118
  # patch vfs into partftpy
117
119
  TftpContexts.open = self._open
118
120
  TftpStates.open = self._open
@@ -357,7 +359,7 @@ class Tftpd(object):
357
359
  yeet("attempted delete of non-empty file")
358
360
 
359
361
  vpath = vpath.replace("\\", "/").lstrip("/")
360
- self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False)
362
+ self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False, False)
361
363
 
362
364
  def _access(self, *a ) :
363
365
  return True