copyparty 1.16.5__py3-none-any.whl → 1.16.7__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/httpcli.py CHANGED
@@ -141,6 +141,14 @@ A_FILE = os.stat_result(
141
141
  (0o644, -1, -1, 1, 1000, 1000, 8, 0x39230101, 0x39230101, 0x39230101)
142
142
  )
143
143
 
144
+ RE_CC = re.compile(r"[\x00-\x1f]") # search always faster
145
+ RE_HSAFE = re.compile(r"[\x00-\x1f<>\"'&]") # search always much faster
146
+ RE_HOST = re.compile(r"[^][0-9a-zA-Z.:_-]") # search faster <=17ch
147
+ RE_MHOST = re.compile(r"^[][0-9a-zA-Z.:_-]+$") # match faster >=18ch
148
+ RE_K = re.compile(r"[^0-9a-zA-Z_-]") # search faster <=17ch
149
+
150
+ UPARAM_CC_OK = set("doc move tree".split())
151
+
144
152
 
145
153
  class HttpCli(object):
146
154
  """
@@ -384,6 +392,15 @@ class HttpCli(object):
384
392
  self.host = self.headers.get("x-forwarded-host") or self.host
385
393
  trusted_xff = True
386
394
 
395
+ m = RE_HOST.search(self.host)
396
+ if m and self.host != self.args.name:
397
+ zs = self.host
398
+ t = "malicious user; illegal Host header; req(%r) host(%r) => %r"
399
+ self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)
400
+ self.cbonk(self.conn.hsrv.gmal, zs, "bad_host", "illegal Host header")
401
+ self.terse_reply(b"illegal Host header", 400)
402
+ return False
403
+
387
404
  if self.is_banned():
388
405
  return False
389
406
 
@@ -429,6 +446,16 @@ class HttpCli(object):
429
446
  self.loud_reply(t, status=400)
430
447
  return False
431
448
 
449
+ ptn_cc = RE_CC
450
+ m = ptn_cc.search(self.req)
451
+ if m:
452
+ zs = self.req
453
+ t = "malicious user; Cc in req0 %r => %r"
454
+ self.log(t % (zs, zs[m.span()[0] :]), 1)
455
+ self.cbonk(self.conn.hsrv.gmal, zs, "cc_r0", "Cc in req0")
456
+ self.terse_reply(b"", 500)
457
+ return False
458
+
432
459
  # split req into vpath + uparam
433
460
  uparam = {}
434
461
  if "?" not in self.req:
@@ -441,8 +468,8 @@ class HttpCli(object):
441
468
  self.trailing_slash = vpath.endswith("/")
442
469
  vpath = undot(vpath)
443
470
 
444
- ptn = self.conn.hsrv.ptn_cc
445
- k_safe = self.conn.hsrv.uparam_cc_ok
471
+ re_k = RE_K
472
+ k_safe = UPARAM_CC_OK
446
473
  for k in arglist.split("&"):
447
474
  if "=" in k:
448
475
  k, zs = k.split("=", 1)
@@ -452,6 +479,14 @@ class HttpCli(object):
452
479
  else:
453
480
  sv = ""
454
481
 
482
+ m = re_k.search(k)
483
+ if m:
484
+ t = "malicious user; bad char in query key; req(%r) qk(%r) => %r"
485
+ self.log(t % (self.req, k, k[m.span()[0] :]), 1)
486
+ self.cbonk(self.conn.hsrv.gmal, self.req, "bc_q", "illegal qkey")
487
+ self.terse_reply(b"", 500)
488
+ return False
489
+
455
490
  k = k.lower()
456
491
  uparam[k] = sv
457
492
 
@@ -459,23 +494,32 @@ class HttpCli(object):
459
494
  continue
460
495
 
461
496
  zs = "%s=%s" % (k, sv)
462
- m = ptn.search(zs)
497
+ m = ptn_cc.search(zs)
463
498
  if not m:
464
499
  continue
465
500
 
466
- hit = zs[m.span()[0] :]
467
- t = "malicious user; Cc in query [{}] => [{!r}]"
468
- self.log(t.format(self.req, hit), 1)
501
+ t = "malicious user; Cc in query; req(%r) qp(%r) => %r"
502
+ self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)
469
503
  self.cbonk(self.conn.hsrv.gmal, self.req, "cc_q", "Cc in query")
470
504
  self.terse_reply(b"", 500)
471
505
  return False
472
506
 
507
+ if "k" in uparam:
508
+ m = RE_K.search(uparam["k"])
509
+ if m:
510
+ zs = uparam["k"]
511
+ t = "malicious user; illegal filekey; req(%r) k(%r) => %r"
512
+ self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)
513
+ self.cbonk(self.conn.hsrv.gmal, zs, "bad_k", "illegal filekey")
514
+ self.terse_reply(b"illegal filekey", 400)
515
+ return False
516
+
473
517
  if self.is_vproxied:
474
518
  if vpath.startswith(self.args.R):
475
519
  vpath = vpath[len(self.args.R) + 1 :]
476
520
  else:
477
- t = "incorrect --rp-loc or webserver config; expected vpath starting with [{}] but got [{}]"
478
- self.log(t.format(self.args.R, vpath), 1)
521
+ t = "incorrect --rp-loc or webserver config; expected vpath starting with %r but got %r"
522
+ self.log(t % (self.args.R, vpath), 1)
479
523
 
480
524
  self.ouparam = uparam.copy()
481
525
 
@@ -513,7 +557,7 @@ class HttpCli(object):
513
557
  return self.tx_qr()
514
558
 
515
559
  if relchk(self.vpath) and (self.vpath != "*" or self.mode != "OPTIONS"):
516
- self.log("invalid relpath [{}]".format(self.vpath))
560
+ self.log("illegal relpath; req(%r) => %r" % (self.req, "/" + self.vpath))
517
561
  self.cbonk(self.conn.hsrv.gmal, self.req, "bad_vp", "invalid relpaths")
518
562
  return self.tx_404() and self.keepalive
519
563
 
@@ -593,7 +637,7 @@ class HttpCli(object):
593
637
  self.uname = idp_usr
594
638
  self.html_head += "<script>var is_idp=1</script>\n"
595
639
  else:
596
- self.log("unknown username: [%s]" % (idp_usr), 1)
640
+ self.log("unknown username: %r" % (idp_usr,), 1)
597
641
 
598
642
  if self.args.ipu and self.uname == "*":
599
643
  self.uname = self.conn.ipu_iu[self.conn.ipu_nm.map(self.ip)]
@@ -658,7 +702,7 @@ class HttpCli(object):
658
702
  origin = self.headers.get("origin", "<?>")
659
703
  proto = "https://" if self.is_https else "http://"
660
704
  guess = "modifying" if (origin and host) else "stripping"
661
- 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)"
705
+ t = "cors-reject %s because request-header Origin=%r does not match request-protocol %r and host %r based on request-header Host=%r (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)"
662
706
  self.log(t % (self.mode, origin, proto, self.host, host, guess), 3)
663
707
  raise Pebkac(403, "rejected by cors-check")
664
708
 
@@ -704,7 +748,7 @@ class HttpCli(object):
704
748
 
705
749
  if pex.code != 404 or self.do_log:
706
750
  self.log(
707
- "http%d: %s\033[0m, %s" % (pex.code, msg, self.vpath),
751
+ "http%d: %s\033[0m, %r" % (pex.code, msg, "/" + self.vpath),
708
752
  6 if em.startswith("client d/c ") else 3,
709
753
  )
710
754
 
@@ -866,12 +910,12 @@ class HttpCli(object):
866
910
  for k, zs in list(self.out_headers.items()) + self.out_headerlist:
867
911
  response.append("%s: %s" % (k, zs))
868
912
 
913
+ ptn_cc = RE_CC
869
914
  for zs in response:
870
- m = self.conn.hsrv.ptn_cc.search(zs)
915
+ m = ptn_cc.search(zs)
871
916
  if m:
872
- hit = zs[m.span()[0] :]
873
- t = "malicious user; Cc in out-hdr {!r} => [{!r}]"
874
- self.log(t.format(zs, hit), 1)
917
+ t = "malicious user; Cc in out-hdr; req(%r) hdr(%r) => %r"
918
+ self.log(t % (self.req, zs, zs[m.span()[0] :]), 1)
875
919
  self.cbonk(self.conn.hsrv.gmal, zs, "cc_hdr", "Cc in out-hdr")
876
920
  raise Pebkac(999)
877
921
 
@@ -997,7 +1041,7 @@ class HttpCli(object):
997
1041
  if not kv:
998
1042
  return ""
999
1043
 
1000
- r = ["%s=%s" % (k, quotep(zs)) if zs else k for k, zs in kv.items()]
1044
+ r = ["%s=%s" % (quotep(k), quotep(zs)) if zs else k for k, zs in kv.items()]
1001
1045
  return "?" + "&amp;".join(r)
1002
1046
 
1003
1047
  def ourlq(self) :
@@ -1113,6 +1157,8 @@ class HttpCli(object):
1113
1157
  logmsg += " [\033[36m" + rval + "\033[0m]"
1114
1158
 
1115
1159
  self.log(logmsg)
1160
+ if "%" in self.req:
1161
+ self.log(" `-- %r" % (self.vpath,))
1116
1162
 
1117
1163
  # "embedded" resources
1118
1164
  if self.vpath.startswith(".cpr"):
@@ -1147,8 +1193,8 @@ class HttpCli(object):
1147
1193
  return self.tx_res(res_path)
1148
1194
 
1149
1195
  if res_path != undot(res_path):
1150
- t = "malicious user; attempted path traversal [{}] => [{}]"
1151
- self.log(t.format(self.vpath, res_path), 1)
1196
+ t = "malicious user; attempted path traversal; req(%r) vp(%r) => %r"
1197
+ self.log(t % (self.req, "/" + self.vpath, res_path), 1)
1152
1198
  self.cbonk(self.conn.hsrv.gmal, self.req, "trav", "path traversal")
1153
1199
 
1154
1200
  self.tx_404()
@@ -1159,11 +1205,11 @@ class HttpCli(object):
1159
1205
  return True
1160
1206
 
1161
1207
  if not self.can_read and not self.can_write and not self.can_get:
1162
- t = "@{} has no access to [{}]"
1208
+ t = "@%s has no access to %r"
1163
1209
 
1164
1210
  if "on403" in self.vn.flags:
1165
1211
  t += " (on403)"
1166
- self.log(t.format(self.uname, self.vpath))
1212
+ self.log(t % (self.uname, "/" + self.vpath))
1167
1213
  ret = self.on40x(self.vn.flags["on403"], self.vn, self.rem)
1168
1214
  if ret == "true":
1169
1215
  return True
@@ -1182,7 +1228,7 @@ class HttpCli(object):
1182
1228
  if self.vpath:
1183
1229
  ptn = self.args.nonsus_urls
1184
1230
  if not ptn or not ptn.search(self.vpath):
1185
- self.log(t.format(self.uname, self.vpath))
1231
+ self.log(t % (self.uname, "/" + self.vpath))
1186
1232
 
1187
1233
  return self.tx_404(True)
1188
1234
 
@@ -1226,6 +1272,9 @@ class HttpCli(object):
1226
1272
  if "dls" in self.uparam:
1227
1273
  return self.tx_dls()
1228
1274
 
1275
+ if "ru" in self.uparam:
1276
+ return self.tx_rups()
1277
+
1229
1278
  if "h" in self.uparam:
1230
1279
  return self.tx_mounts()
1231
1280
 
@@ -1288,8 +1337,8 @@ class HttpCli(object):
1288
1337
 
1289
1338
  pw = self.ouparam.get("pw")
1290
1339
  if pw:
1291
- q_pw = "?pw=%s" % (pw,)
1292
- a_pw = "&pw=%s" % (pw,)
1340
+ q_pw = "?pw=%s" % (html_escape(pw, True, True),)
1341
+ a_pw = "&pw=%s" % (html_escape(pw, True, True),)
1293
1342
  for i in hits:
1294
1343
  i["rp"] += a_pw if "?" in i["rp"] else q_pw
1295
1344
  else:
@@ -1376,6 +1425,8 @@ class HttpCli(object):
1376
1425
  def handle_propfind(self) :
1377
1426
  if self.do_log:
1378
1427
  self.log("PFIND %s @%s" % (self.req, self.uname))
1428
+ if "%" in self.req:
1429
+ self.log(" `-- %r" % (self.vpath,))
1379
1430
 
1380
1431
  if self.args.no_dav:
1381
1432
  raise Pebkac(405, "WebDAV is disabled in server config")
@@ -1426,14 +1477,14 @@ class HttpCli(object):
1426
1477
  if depth == "infinity":
1427
1478
  # allow depth:0 from unmapped root, but require read-axs otherwise
1428
1479
  if not self.can_read and (self.vpath or self.asrv.vfs.realpath):
1429
- t = "depth:infinity requires read-access in /%s"
1430
- t = t % (self.vpath,)
1480
+ t = "depth:infinity requires read-access in %r"
1481
+ t = t % ("/" + self.vpath,)
1431
1482
  self.log(t, 3)
1432
1483
  raise Pebkac(401, t)
1433
1484
 
1434
1485
  if not stat.S_ISDIR(topdir["st"].st_mode):
1435
- t = "depth:infinity can only be used on folders; /%s is 0o%o"
1436
- t = t % (self.vpath, topdir["st"])
1486
+ t = "depth:infinity can only be used on folders; %r is 0o%o"
1487
+ t = t % ("/" + self.vpath, topdir["st"])
1437
1488
  self.log(t, 3)
1438
1489
  raise Pebkac(400, t)
1439
1490
 
@@ -1459,7 +1510,7 @@ class HttpCli(object):
1459
1510
  elif depth == "0" or not stat.S_ISDIR(st.st_mode):
1460
1511
  # propfind on a file; return as topdir
1461
1512
  if not self.can_read and not self.can_get:
1462
- self.log("inaccessible: [%s]" % (self.vpath,))
1513
+ self.log("inaccessible: %r" % ("/" + self.vpath,))
1463
1514
  raise Pebkac(401, "authenticate")
1464
1515
 
1465
1516
  elif depth == "1":
@@ -1486,7 +1537,7 @@ class HttpCli(object):
1486
1537
  raise Pebkac(412, t.format(depth, t2))
1487
1538
 
1488
1539
  if not self.can_read and not self.can_write and not fgen:
1489
- self.log("inaccessible: [%s]" % (self.vpath,))
1540
+ self.log("inaccessible: %r" % ("/" + self.vpath,))
1490
1541
  raise Pebkac(401, "authenticate")
1491
1542
 
1492
1543
  fgen = itertools.chain([topdir], fgen)
@@ -1557,12 +1608,14 @@ class HttpCli(object):
1557
1608
  def handle_proppatch(self) :
1558
1609
  if self.do_log:
1559
1610
  self.log("PPATCH %s @%s" % (self.req, self.uname))
1611
+ if "%" in self.req:
1612
+ self.log(" `-- %r" % (self.vpath,))
1560
1613
 
1561
1614
  if self.args.no_dav:
1562
1615
  raise Pebkac(405, "WebDAV is disabled in server config")
1563
1616
 
1564
1617
  if not self.can_write:
1565
- self.log("{} tried to proppatch [{}]".format(self.uname, self.vpath))
1618
+ self.log("%s tried to proppatch %r" % (self.uname, "/" + self.vpath))
1566
1619
  raise Pebkac(401, "authenticate")
1567
1620
 
1568
1621
  from xml.etree import ElementTree as ET
@@ -1609,13 +1662,15 @@ class HttpCli(object):
1609
1662
  def handle_lock(self) :
1610
1663
  if self.do_log:
1611
1664
  self.log("LOCK %s @%s" % (self.req, self.uname))
1665
+ if "%" in self.req:
1666
+ self.log(" `-- %r" % (self.vpath,))
1612
1667
 
1613
1668
  if self.args.no_dav:
1614
1669
  raise Pebkac(405, "WebDAV is disabled in server config")
1615
1670
 
1616
1671
  # win7+ deadlocks if we say no; just smile and nod
1617
1672
  if not self.can_write and "Microsoft-WebDAV" not in self.ua:
1618
- self.log("{} tried to lock [{}]".format(self.uname, self.vpath))
1673
+ self.log("%s tried to lock %r" % (self.uname, "/" + self.vpath))
1619
1674
  raise Pebkac(401, "authenticate")
1620
1675
 
1621
1676
  from xml.etree import ElementTree as ET
@@ -1644,7 +1699,7 @@ class HttpCli(object):
1644
1699
 
1645
1700
  token = str(uuid.uuid4())
1646
1701
 
1647
- if not lk.find(r"./{DAV:}depth"):
1702
+ if lk.find(r"./{DAV:}depth") is None:
1648
1703
  depth = self.headers.get("depth", "infinity")
1649
1704
  lk.append(mktnod("D:depth", depth))
1650
1705
 
@@ -1674,12 +1729,14 @@ class HttpCli(object):
1674
1729
  def handle_unlock(self) :
1675
1730
  if self.do_log:
1676
1731
  self.log("UNLOCK %s @%s" % (self.req, self.uname))
1732
+ if "%" in self.req:
1733
+ self.log(" `-- %r" % (self.vpath,))
1677
1734
 
1678
1735
  if self.args.no_dav:
1679
1736
  raise Pebkac(405, "WebDAV is disabled in server config")
1680
1737
 
1681
1738
  if not self.can_write and "Microsoft-WebDAV" not in self.ua:
1682
- self.log("{} tried to lock [{}]".format(self.uname, self.vpath))
1739
+ self.log("%s tried to lock %r" % (self.uname, "/" + self.vpath))
1683
1740
  raise Pebkac(401, "authenticate")
1684
1741
 
1685
1742
  self.send_headers(None, 204)
@@ -1691,6 +1748,8 @@ class HttpCli(object):
1691
1748
 
1692
1749
  if self.do_log:
1693
1750
  self.log("MKCOL %s @%s" % (self.req, self.uname))
1751
+ if "%" in self.req:
1752
+ self.log(" `-- %r" % (self.vpath,))
1694
1753
 
1695
1754
  try:
1696
1755
  return self._mkdir(self.vpath, True)
@@ -1742,6 +1801,8 @@ class HttpCli(object):
1742
1801
  def handle_options(self) :
1743
1802
  if self.do_log:
1744
1803
  self.log("OPTIONS %s @%s" % (self.req, self.uname))
1804
+ if "%" in self.req:
1805
+ self.log(" `-- %r" % (self.vpath,))
1745
1806
 
1746
1807
  oh = self.out_headers
1747
1808
  oh["Allow"] = ", ".join(self.conn.hsrv.mallow)
@@ -1757,10 +1818,14 @@ class HttpCli(object):
1757
1818
 
1758
1819
  def handle_delete(self) :
1759
1820
  self.log("DELETE %s @%s" % (self.req, self.uname))
1821
+ if "%" in self.req:
1822
+ self.log(" `-- %r" % (self.vpath,))
1760
1823
  return self.handle_rm([])
1761
1824
 
1762
1825
  def handle_put(self) :
1763
- self.log("PUT %s @%s" % (self.req, self.uname))
1826
+ self.log("PUT %s @%s" % (self.req, self.uname))
1827
+ if "%" in self.req:
1828
+ self.log(" `-- %r" % (self.vpath,))
1764
1829
 
1765
1830
  if not self.can_write:
1766
1831
  t = "user %s does not have write-access under /%s"
@@ -1779,6 +1844,8 @@ class HttpCli(object):
1779
1844
 
1780
1845
  def handle_post(self) :
1781
1846
  self.log("POST %s @%s" % (self.req, self.uname))
1847
+ if "%" in self.req:
1848
+ self.log(" `-- %r" % (self.vpath,))
1782
1849
 
1783
1850
  if self.headers.get("expect", "").lower() == "100-continue":
1784
1851
  try:
@@ -1823,7 +1890,7 @@ class HttpCli(object):
1823
1890
 
1824
1891
  if "save" in opt:
1825
1892
  post_sz, _, _, _, path, _ = self.dump_to_file(False)
1826
- self.log("urlform: {} bytes, {}".format(post_sz, path))
1893
+ self.log("urlform: %d bytes, %r" % (post_sz, path))
1827
1894
  elif "print" in opt:
1828
1895
  reader, _ = self.get_body_reader()
1829
1896
  buf = b""
@@ -1834,8 +1901,8 @@ class HttpCli(object):
1834
1901
 
1835
1902
  if buf:
1836
1903
  orig = buf.decode("utf-8", "replace")
1837
- t = "urlform_raw {} @ {}\n {}\n"
1838
- self.log(t.format(len(orig), self.vpath, orig))
1904
+ t = "urlform_raw %d @ %r\n %r\n"
1905
+ self.log(t % (len(orig), "/" + self.vpath, orig))
1839
1906
  try:
1840
1907
  zb = unquote(buf.replace(b"+", b" "))
1841
1908
  plain = zb.decode("utf-8", "replace")
@@ -1861,8 +1928,8 @@ class HttpCli(object):
1861
1928
  plain,
1862
1929
  )
1863
1930
 
1864
- t = "urlform_dec {} @ {}\n {}\n"
1865
- self.log(t.format(len(plain), self.vpath, plain))
1931
+ t = "urlform_dec %d @ %r\n %r\n"
1932
+ self.log(t % (len(plain), "/" + self.vpath, plain))
1866
1933
 
1867
1934
  except Exception as ex:
1868
1935
  self.log(repr(ex))
@@ -2110,7 +2177,7 @@ class HttpCli(object):
2110
2177
  try:
2111
2178
  ext = self.conn.hsrv.magician.ext(path)
2112
2179
  except Exception as ex:
2113
- self.log("filetype detection failed for [{}]: {}".format(path, ex), 6)
2180
+ self.log("filetype detection failed for %r: %s" % (path, ex), 6)
2114
2181
  ext = None
2115
2182
 
2116
2183
  if ext:
@@ -2210,8 +2277,8 @@ class HttpCli(object):
2210
2277
  def handle_stash(self, is_put ) :
2211
2278
  post_sz, sha_hex, sha_b64, remains, path, url = self.dump_to_file(is_put)
2212
2279
  spd = self._spd(post_sz)
2213
- t = "{} wrote {}/{} bytes to {} # {}"
2214
- self.log(t.format(spd, post_sz, remains, path, sha_b64[:28])) # 21
2280
+ t = "%s wrote %d/%d bytes to %r # %s"
2281
+ self.log(t % (spd, post_sz, remains, path, sha_b64[:28])) # 21
2215
2282
 
2216
2283
  ac = self.uparam.get(
2217
2284
  "want", self.headers.get("accept", "").lower().split(";")[-1]
@@ -2241,7 +2308,7 @@ class HttpCli(object):
2241
2308
  flags ,
2242
2309
  ) :
2243
2310
  now = time.time()
2244
- t = "bad-chunk: %.3f %s %s %d %s %s %s"
2311
+ t = "bad-chunk: %.3f %s %s %d %s %s %r"
2245
2312
  t = t % (now, bad_sha, good_sha, ofs, self.ip, self.uname, ap)
2246
2313
  self.log(t, 5)
2247
2314
 
@@ -2380,7 +2447,7 @@ class HttpCli(object):
2380
2447
  body = json.loads(json_buf.decode(enc, "replace"))
2381
2448
  try:
2382
2449
  zds = {k: v for k, v in body.items()}
2383
- zds["hash"] = "%d chunks" % (len(body["hash"]))
2450
+ zds["hash"] = "%d chunks" % (len(body["hash"]),)
2384
2451
  except:
2385
2452
  zds = body
2386
2453
  t = "POST len=%d type=%s ip=%s user=%s req=%r json=%s"
@@ -2424,7 +2491,7 @@ class HttpCli(object):
2424
2491
  if not bos.path.isdir(dst):
2425
2492
  bos.makedirs(dst)
2426
2493
  except OSError as ex:
2427
- self.log("makedirs failed [{}]".format(dst))
2494
+ self.log("makedirs failed %r" % (dst,))
2428
2495
  if not bos.path.isdir(dst):
2429
2496
  if ex.errno == errno.EACCES:
2430
2497
  raise Pebkac(500, "the server OS denied write-access")
@@ -2449,7 +2516,7 @@ class HttpCli(object):
2449
2516
  # strip common suffix (uploader's folder structure)
2450
2517
  vp_req, vp_vfs = vroots(self.vpath, vjoin(dbv.vpath, vrem))
2451
2518
  if not ret["purl"].startswith(vp_vfs):
2452
- t = "share-mapping failed; req=[%s] dbv=[%s] vrem=[%s] n1=[%s] n2=[%s] purl=[%s]"
2519
+ t = "share-mapping failed; req=%r dbv=%r vrem=%r n1=%r n2=%r purl=%r"
2453
2520
  zt = (self.vpath, dbv.vpath, vrem, vp_req, vp_vfs, ret["purl"])
2454
2521
  raise Pebkac(500, t % zt)
2455
2522
  ret["purl"] = vp_req + ret["purl"][len(vp_vfs) :]
@@ -2498,13 +2565,13 @@ class HttpCli(object):
2498
2565
  # search by query params
2499
2566
  q = body["q"]
2500
2567
  n = body.get("n", self.args.srch_hits)
2501
- self.log("qj: {} |{}|".format(q, n))
2568
+ self.log("qj: %r |%d|" % (q, n))
2502
2569
  hits, taglist, trunc = idx.search(self.uname, vols, q, n)
2503
2570
  msg = len(hits)
2504
2571
 
2505
2572
  idx.p_end = time.time()
2506
2573
  idx.p_dur = idx.p_end - t0
2507
- self.log("q#: {} ({:.2f}s)".format(msg, idx.p_dur))
2574
+ self.log("q#: %r (%.2fs)" % (msg, idx.p_dur))
2508
2575
 
2509
2576
  order = []
2510
2577
  for t in self.args.mte:
@@ -2615,7 +2682,7 @@ class HttpCli(object):
2615
2682
  t = "your client is sending %d bytes which is too much (server expected %d bytes at most)"
2616
2683
  raise Pebkac(400, t % (remains, maxsize))
2617
2684
 
2618
- t = "writing %s %s+%d #%d+%d %s"
2685
+ t = "writing %r %s+%d #%d+%d %s"
2619
2686
  chunkno = cstart0[0] // chunksize
2620
2687
  zs = " ".join([chashes[0][:15]] + [x[:9] for x in chashes[1:]])
2621
2688
  self.log(t % (path, cstart0, remains, chunkno, len(chashes), zs))
@@ -2727,7 +2794,7 @@ class HttpCli(object):
2727
2794
  cinf = self.headers.get("x-up2k-stat", "")
2728
2795
 
2729
2796
  spd = self._spd(postsize)
2730
- self.log("{:70} thank {}".format(spd, cinf))
2797
+ self.log("%70s thank %r" % (spd, cinf))
2731
2798
  self.reply(b"thank")
2732
2799
  return True
2733
2800
 
@@ -2806,7 +2873,7 @@ class HttpCli(object):
2806
2873
  logpwd = "%" + ub64enc(zb[:12]).decode("ascii")
2807
2874
 
2808
2875
  if pwd != "x":
2809
- self.log("invalid password: {}".format(logpwd), 3)
2876
+ self.log("invalid password: %r" % (logpwd,), 3)
2810
2877
  self.cbonk(self.conn.hsrv.gpwd, pwd, "pw", "invalid passwords")
2811
2878
 
2812
2879
  msg = "naw dude"
@@ -2841,7 +2908,7 @@ class HttpCli(object):
2841
2908
  rem = sanitize_vpath(rem, "/")
2842
2909
  fn = vfs.canonical(rem)
2843
2910
  if not fn.startswith(vfs.realpath):
2844
- self.log("invalid mkdir [%s] [%s]" % (self.gctx, vpath), 1)
2911
+ self.log("invalid mkdir %r %r" % (self.gctx, vpath), 1)
2845
2912
  raise Pebkac(422)
2846
2913
 
2847
2914
  if not nullwrite:
@@ -3006,9 +3073,9 @@ class HttpCli(object):
3006
3073
  elif bos.path.exists(abspath):
3007
3074
  try:
3008
3075
  wunlink(self.log, abspath, vfs.flags)
3009
- t = "overwriting file with new upload: %s"
3076
+ t = "overwriting file with new upload: %r"
3010
3077
  except:
3011
- t = "toctou while deleting for ?replace: %s"
3078
+ t = "toctou while deleting for ?replace: %r"
3012
3079
  self.log(t % (abspath,))
3013
3080
  else:
3014
3081
  open_args = {}
@@ -3091,7 +3158,7 @@ class HttpCli(object):
3091
3158
  f, tnam = ren_open(tnam, "wb", self.args.iobuf, **open_args)
3092
3159
  try:
3093
3160
  tabspath = os.path.join(fdir, tnam)
3094
- self.log("writing to {}".format(tabspath))
3161
+ self.log("writing to %r" % (tabspath,))
3095
3162
  sz, sha_hex, sha_b64 = copier(
3096
3163
  p_data, f, hasher, max_sz, self.args.s_wr_slp
3097
3164
  )
@@ -3276,7 +3343,7 @@ class HttpCli(object):
3276
3343
  jmsg["files"].append(jpart)
3277
3344
 
3278
3345
  vspd = self._spd(sz_total, False)
3279
- self.log("{} {}".format(vspd, msg))
3346
+ self.log("%s %r" % (vspd, msg))
3280
3347
 
3281
3348
  suf = ""
3282
3349
  if not nullwrite and self.args.write_uplog:
@@ -3537,7 +3604,7 @@ class HttpCli(object):
3537
3604
  if req == zs:
3538
3605
  return True
3539
3606
 
3540
- t = "wrong dirkey, want %s, got %s\n vp: %s\n ap: %s"
3607
+ t = "wrong dirkey, want %s, got %s\n vp: %r\n ap: %r"
3541
3608
  self.log(t % (zs, req, self.req, ap), 6)
3542
3609
  return False
3543
3610
 
@@ -3565,7 +3632,7 @@ class HttpCli(object):
3565
3632
  if req == zs:
3566
3633
  return True
3567
3634
 
3568
- t = "wrong filekey, want %s, got %s\n vp: %s\n ap: %s"
3635
+ t = "wrong filekey, want %s, got %s\n vp: %r\n ap: %r"
3569
3636
  self.log(t % (zs, req, self.req, ap), 6)
3570
3637
  return False
3571
3638
 
@@ -3613,6 +3680,7 @@ class HttpCli(object):
3613
3680
  return logues, readmes
3614
3681
 
3615
3682
  def _expand(self, txt , phs ) :
3683
+ ptn_hsafe = RE_HSAFE
3616
3684
  for ph in phs:
3617
3685
  if ph.startswith("hdr."):
3618
3686
  sv = str(self.headers.get(ph[4:], ""))
@@ -3627,10 +3695,10 @@ class HttpCli(object):
3627
3695
  elif ph == "srv.htime":
3628
3696
  sv = datetime.now(UTC).strftime("%Y-%m-%d, %H:%M:%S")
3629
3697
  else:
3630
- self.log("unknown placeholder in server config: [%s]" % (ph), 3)
3698
+ self.log("unknown placeholder in server config: [%s]" % (ph,), 3)
3631
3699
  continue
3632
3700
 
3633
- sv = self.conn.hsrv.ptn_hsafe.sub("_", sv)
3701
+ sv = ptn_hsafe.sub("_", sv)
3634
3702
  txt = txt.replace("{{%s}}" % (ph,), sv)
3635
3703
 
3636
3704
  return txt
@@ -3784,7 +3852,7 @@ class HttpCli(object):
3784
3852
  self.pipes.set(req_path, job)
3785
3853
  except Exception as ex:
3786
3854
  if getattr(ex, "errno", 0) != errno.ENOENT:
3787
- self.log("will not pipe [%s]; %s" % (ap_data, ex), 6)
3855
+ self.log("will not pipe %r; %s" % (ap_data, ex), 6)
3788
3856
  ptop = None
3789
3857
 
3790
3858
  #
@@ -4072,7 +4140,7 @@ class HttpCli(object):
4072
4140
  if lower >= data_end:
4073
4141
  if data_end:
4074
4142
  t = "pipe: uploader is too slow; aborting download at %.2f MiB"
4075
- self.log(t % (data_end / M))
4143
+ self.log(t % (data_end / M,))
4076
4144
  raise Pebkac(416, "uploader is too slow")
4077
4145
 
4078
4146
  raise Pebkac(416, "no data available yet; please retry in a bit")
@@ -4216,7 +4284,7 @@ class HttpCli(object):
4216
4284
 
4217
4285
  cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}"
4218
4286
  cdis = cdis.format(afn, ext, ufn, ext)
4219
- self.log(cdis)
4287
+ self.log(repr(cdis))
4220
4288
  self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
4221
4289
 
4222
4290
  fgen = vn.zipgen(
@@ -4878,9 +4946,9 @@ class HttpCli(object):
4878
4946
  raise Pebkac(500, "sqlite3 not found on server; unpost is disabled")
4879
4947
  raise Pebkac(500, "server busy, cannot unpost; please retry in a bit")
4880
4948
 
4881
- filt = self.uparam.get("filter") or ""
4882
- lm = "ups [{}]".format(filt)
4883
- self.log(lm)
4949
+ zs = self.uparam.get("filter") or ""
4950
+ filt = re.compile(zs, re.I) if zs else None
4951
+ lm = "ups %r" % (zs,)
4884
4952
 
4885
4953
  if self.args.shr and self.vpath.startswith(self.args.shr1):
4886
4954
  shr_dbv, shr_vrem = self.vn.get_dbv(self.rem)
@@ -4921,13 +4989,18 @@ class HttpCli(object):
4921
4989
 
4922
4990
  nfk, fk_alg = fk_vols.get(vol) or (0, 0)
4923
4991
 
4924
- q = "select sz, rd, fn, at from up where ip=? and at>?"
4992
+ n = 2000
4993
+ q = "select sz, rd, fn, at from up where ip=? and at>? order by at desc"
4925
4994
  for sz, rd, fn, at in cur.execute(q, (self.ip, lim)):
4926
4995
  vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
4927
- if filt and filt not in vp:
4996
+ if filt and not filt.search(vp):
4928
4997
  continue
4929
4998
 
4930
- rv = {"vp": quotep(vp), "sz": sz, "at": at, "nfk": nfk}
4999
+ n -= 1
5000
+ if not n:
5001
+ break
5002
+
5003
+ rv = {"vp": vp, "sz": sz, "at": at, "nfk": nfk}
4931
5004
  if nfk:
4932
5005
  rv["ap"] = vol.canonical(vjoin(rd, fn))
4933
5006
  rv["fk_alg"] = fk_alg
@@ -4938,8 +5011,12 @@ class HttpCli(object):
4938
5011
  ret = ret[:2000]
4939
5012
 
4940
5013
  ret.sort(key=lambda x: x["at"], reverse=True) # type: ignore
4941
- n = 0
4942
- for rv in ret[:11000]:
5014
+
5015
+ if len(ret) > 2000:
5016
+ ret = ret[:2000]
5017
+
5018
+ for rv in ret:
5019
+ rv["vp"] = quotep(rv["vp"])
4943
5020
  nfk = rv.pop("nfk")
4944
5021
  if not nfk:
4945
5022
  continue
@@ -4956,12 +5033,6 @@ class HttpCli(object):
4956
5033
  )
4957
5034
  rv["vp"] += "?k=" + fk[:nfk]
4958
5035
 
4959
- n += 1
4960
- if n > 2000:
4961
- break
4962
-
4963
- ret = ret[:2000]
4964
-
4965
5036
  if shr_dbv:
4966
5037
  # translate vpaths from share-target to share-url
4967
5038
  # to satisfy access checks
@@ -4984,6 +5055,126 @@ class HttpCli(object):
4984
5055
  self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
4985
5056
  return True
4986
5057
 
5058
+ def tx_rups(self) :
5059
+ if self.args.no_ups_page:
5060
+ raise Pebkac(500, "listing of recent uploads is disabled in server config")
5061
+
5062
+ idx = self.conn.get_u2idx()
5063
+ if not idx or not hasattr(idx, "p_end"):
5064
+ if not HAVE_SQLITE3:
5065
+ raise Pebkac(500, "sqlite3 not found on server; recent-uploads n/a")
5066
+ raise Pebkac(500, "server busy, cannot list recent uploads; please retry")
5067
+
5068
+ sfilt = self.uparam.get("filter") or ""
5069
+ filt = re.compile(sfilt, re.I) if sfilt else None
5070
+ lm = "ru %r" % (sfilt,)
5071
+ self.log(lm)
5072
+
5073
+ ret = []
5074
+ t0 = time.time()
5075
+ allvols = [
5076
+ x
5077
+ for x in self.asrv.vfs.all_vols.values()
5078
+ if "e2d" in x.flags and ("*" in x.axs.uread or self.uname in x.axs.uread)
5079
+ ]
5080
+ fk_vols = {
5081
+ vol: (vol.flags["fk"], 2 if "fka" in vol.flags else 1)
5082
+ for vol in allvols
5083
+ if "fk" in vol.flags and "*" not in vol.axs.uread
5084
+ }
5085
+
5086
+ for vol in allvols:
5087
+ cur = idx.get_cur(vol)
5088
+ if not cur:
5089
+ continue
5090
+
5091
+ nfk, fk_alg = fk_vols.get(vol) or (0, 0)
5092
+ adm = "*" in vol.axs.uadmin or self.uname in vol.axs.uadmin
5093
+ dots = "*" in vol.axs.udot or self.uname in vol.axs.udot
5094
+
5095
+ n = 1000
5096
+ q = "select sz, rd, fn, ip, at from up where at>0 order by at desc"
5097
+ for sz, rd, fn, ip, at in cur.execute(q):
5098
+ vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
5099
+ if filt and not filt.search(vp):
5100
+ continue
5101
+
5102
+ if not dots and "/." in vp:
5103
+ continue
5104
+
5105
+ rv = {
5106
+ "vp": vp,
5107
+ "sz": sz,
5108
+ "ip": ip,
5109
+ "at": at,
5110
+ "nfk": nfk,
5111
+ "adm": adm,
5112
+ }
5113
+ if nfk:
5114
+ rv["ap"] = vol.canonical(vjoin(rd, fn))
5115
+ rv["fk_alg"] = fk_alg
5116
+
5117
+ ret.append(rv)
5118
+ if len(ret) > 2000:
5119
+ ret.sort(key=lambda x: x["at"], reverse=True) # type: ignore
5120
+ ret = ret[:1000]
5121
+
5122
+ n -= 1
5123
+ if not n:
5124
+ break
5125
+
5126
+ ret.sort(key=lambda x: x["at"], reverse=True) # type: ignore
5127
+
5128
+ if len(ret) > 1000:
5129
+ ret = ret[:1000]
5130
+
5131
+ for rv in ret:
5132
+ rv["vp"] = quotep(rv["vp"])
5133
+ nfk = rv.pop("nfk")
5134
+ if not nfk:
5135
+ continue
5136
+
5137
+ alg = rv.pop("fk_alg")
5138
+ ap = rv.pop("ap")
5139
+ try:
5140
+ st = bos.stat(ap)
5141
+ except:
5142
+ continue
5143
+
5144
+ fk = self.gen_fk(
5145
+ alg, self.args.fk_salt, ap, st.st_size, 0 if ANYWIN else st.st_ino
5146
+ )
5147
+ rv["vp"] += "?k=" + fk[:nfk]
5148
+
5149
+ if self.args.ups_when:
5150
+ for rv in ret:
5151
+ adm = rv.pop("adm")
5152
+ if not adm:
5153
+ rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
5154
+ else:
5155
+ for rv in ret:
5156
+ adm = rv.pop("adm")
5157
+ if not adm:
5158
+ rv["ip"] = "(You)" if rv["ip"] == self.ip else "(?)"
5159
+ rv["at"] = 0
5160
+
5161
+ if self.is_vproxied:
5162
+ for v in ret:
5163
+ v["vp"] = self.args.SR + v["vp"]
5164
+
5165
+ now = time.time()
5166
+ self.log("%s #%d %.2fsec" % (lm, len(ret), now - t0))
5167
+
5168
+ ret2 = {"now": int(now), "filter": sfilt, "ups": ret}
5169
+ jtxt = json.dumps(ret2, separators=(",\n", ": "))
5170
+ if "j" in self.ouparam:
5171
+ self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
5172
+ return True
5173
+
5174
+ html = self.j2s("rups", this=self, v=jtxt)
5175
+ self.reply(html.encode("utf-8"), status=200)
5176
+ return True
5177
+
4987
5178
  def tx_shares(self) :
4988
5179
  if self.uname == "*":
4989
5180
  self.loud_reply("you're not logged in")
@@ -5689,7 +5880,7 @@ class HttpCli(object):
5689
5880
  linf = stats.get(fn) or bos.lstat(fspath)
5690
5881
  inf = bos.stat(fspath) if stat.S_ISLNK(linf.st_mode) else linf
5691
5882
  except:
5692
- self.log("broken symlink: {}".format(repr(fspath)))
5883
+ self.log("broken symlink: %r" % (fspath,))
5693
5884
  continue
5694
5885
 
5695
5886
  is_dir = stat.S_ISDIR(inf.st_mode)
@@ -5803,8 +5994,7 @@ class HttpCli(object):
5803
5994
  erd_efn = s3enc(idx.mem_cur, rd, fn)
5804
5995
  r = icur.execute(q, erd_efn)
5805
5996
  except:
5806
- t = "tag read error, {}/{}\n{}"
5807
- self.log(t.format(rd, fn, min_ex()))
5997
+ self.log("tag read error, %r / %r\n%s" % (rd, fn, min_ex()))
5808
5998
  break
5809
5999
 
5810
6000
  tags = {k: v for k, v in r}
@@ -5924,10 +6114,10 @@ class HttpCli(object):
5924
6114
  if doc.lower().endswith(".md") and "exp" in vn.flags:
5925
6115
  doctxt = self._expand(doctxt, vn.flags.get("exp_md") or [])
5926
6116
  else:
5927
- self.log("doc 2big: [{}]".format(doc), c=6)
6117
+ self.log("doc 2big: %r" % (doc,), 6)
5928
6118
  doctxt = "( size of textfile exceeds serverside limit )"
5929
6119
  else:
5930
- self.log("doc 404: [{}]".format(doc), c=6)
6120
+ self.log("doc 404: %r" % (doc,), 6)
5931
6121
  doctxt = "( textfile not found )"
5932
6122
 
5933
6123
  if doctxt is not None: