copyparty 1.15.2__py3-none-any.whl → 1.15.4__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
@@ -32,11 +32,12 @@ try:
32
32
  except:
33
33
  pass
34
34
 
35
- from .__init__ import ANYWIN, PY2, TYPE_CHECKING, EnvParams, unicode
35
+ from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode
36
36
  from .__version__ import S_VERSION
37
37
  from .authsrv import VFS # typechk
38
38
  from .bos import bos
39
39
  from .star import StreamTar
40
+ from .stolen.qrcodegen import QrCode, qr2svg
40
41
  from .sutil import StreamArc, gfilter
41
42
  from .szip import StreamZip
42
43
  from .up2k import up2k_chunksize
@@ -44,6 +45,7 @@ from .util import unquote # type: ignore
44
45
  from .util import (
45
46
  APPLESAN_RE,
46
47
  BITNESS,
48
+ DAV_ALLPROPS,
47
49
  HAVE_SQLITE3,
48
50
  HTTPCODE,
49
51
  META_NOBOTS,
@@ -67,13 +69,16 @@ from .util import (
67
69
  get_df,
68
70
  get_spd,
69
71
  guess_mime,
72
+ gzip_file_orig_sz,
70
73
  gzip_orig_sz,
74
+ has_resource,
71
75
  hashcopy,
72
76
  hidedir,
73
77
  html_bescape,
74
78
  html_escape,
75
79
  humansize,
76
80
  ipnorm,
81
+ load_resource,
77
82
  loadpy,
78
83
  log_reloc,
79
84
  min_ex,
@@ -93,6 +98,7 @@ from .util import (
93
98
  sanitize_vpath,
94
99
  sendfile_kern,
95
100
  sendfile_py,
101
+ stat_resource,
96
102
  ub64dec,
97
103
  ub64enc,
98
104
  ujoin,
@@ -418,6 +424,7 @@ class HttpCli(object):
418
424
  vpath = undot(vpath)
419
425
 
420
426
  ptn = self.conn.hsrv.ptn_cc
427
+ k_safe = self.conn.hsrv.uparam_cc_ok
421
428
  for k in arglist.split("&"):
422
429
  if "=" in k:
423
430
  k, zs = k.split("=", 1)
@@ -430,7 +437,7 @@ class HttpCli(object):
430
437
  k = k.lower()
431
438
  uparam[k] = sv
432
439
 
433
- if k in ("doc", "move", "tree"):
440
+ if k in k_safe:
434
441
  continue
435
442
 
436
443
  zs = "%s=%s" % (k, sv)
@@ -484,6 +491,9 @@ class HttpCli(object):
484
491
  self.vpath + "/" if self.trailing_slash and self.vpath else self.vpath
485
492
  )
486
493
 
494
+ if "qr" in uparam:
495
+ return self.tx_qr()
496
+
487
497
  if relchk(self.vpath) and (self.vpath != "*" or self.mode != "OPTIONS"):
488
498
  self.log("invalid relpath [{}]".format(self.vpath))
489
499
  self.cbonk(self.conn.hsrv.gmal, self.req, "bad_vp", "invalid relpaths")
@@ -1088,14 +1098,17 @@ class HttpCli(object):
1088
1098
  if self.vpath == ".cpr/metrics":
1089
1099
  return self.conn.hsrv.metrics.tx(self)
1090
1100
 
1091
- path_base = os.path.join(self.E.mod, "web")
1092
- static_path = absreal(os.path.join(path_base, self.vpath[5:]))
1093
- if static_path in self.conn.hsrv.statics:
1094
- return self.tx_file(static_path)
1101
+ res_path = "web/" + self.vpath[5:]
1102
+ if res_path in RES:
1103
+ ap = os.path.join(self.E.mod, res_path)
1104
+ if bos.path.exists(ap) or bos.path.exists(ap + ".gz"):
1105
+ return self.tx_file(ap)
1106
+ else:
1107
+ return self.tx_res(res_path)
1095
1108
 
1096
- if not static_path.startswith(path_base):
1109
+ if res_path != undot(res_path):
1097
1110
  t = "malicious user; attempted path traversal [{}] => [{}]"
1098
- self.log(t.format(self.vpath, static_path), 1)
1111
+ self.log(t.format(self.vpath, res_path), 1)
1099
1112
  self.cbonk(self.conn.hsrv.gmal, self.req, "trav", "path traversal")
1100
1113
 
1101
1114
  self.tx_404()
@@ -1190,10 +1203,6 @@ class HttpCli(object):
1190
1203
  tap = vn.canonical(rem)
1191
1204
 
1192
1205
  if "davauth" in vn.flags and self.uname == "*":
1193
- self.can_read = self.can_write = self.can_get = False
1194
-
1195
- if not self.can_read and not self.can_write and not self.can_get:
1196
- self.log("inaccessible: [%s]" % (self.vpath,))
1197
1206
  raise Pebkac(401, "authenticate")
1198
1207
 
1199
1208
  from .dxml import parse_xml
@@ -1202,6 +1211,7 @@ class HttpCli(object):
1202
1211
  # enc = "shift_jis"
1203
1212
  enc = "utf-8"
1204
1213
  uenc = enc.upper()
1214
+ props = DAV_ALLPROPS
1205
1215
 
1206
1216
  clen = int(self.headers.get("content-length", 0))
1207
1217
  if clen:
@@ -1212,33 +1222,13 @@ class HttpCli(object):
1212
1222
  break
1213
1223
 
1214
1224
  xroot = parse_xml(buf.decode(enc, "replace"))
1215
- xtag = next(x for x in xroot if x.tag.split("}")[-1] == "prop")
1216
- props_lst = [y.tag.split("}")[-1] for y in xtag]
1217
- else:
1218
- props_lst = [
1219
- "contentclass",
1220
- "creationdate",
1221
- "defaultdocument",
1222
- "displayname",
1223
- "getcontentlanguage",
1224
- "getcontentlength",
1225
- "getcontenttype",
1226
- "getlastmodified",
1227
- "href",
1228
- "iscollection",
1229
- "ishidden",
1230
- "isreadonly",
1231
- "isroot",
1232
- "isstructureddocument",
1233
- "lastaccessed",
1234
- "name",
1235
- "parentname",
1236
- "resourcetype",
1237
- "supportedlock",
1238
- ]
1225
+ xtag = next((x for x in xroot if x.tag.split("}")[-1] == "prop"), None)
1226
+ if xtag is not None:
1227
+ props = set([y.tag.split("}")[-1] for y in xtag])
1228
+ # assume <allprop/> otherwise; nobody ever gonna <propname/>
1239
1229
 
1240
- props = set(props_lst)
1241
- depth = self.headers.get("depth", "infinity").lower()
1230
+ zi = int(time.time())
1231
+ vst = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))
1242
1232
 
1243
1233
  try:
1244
1234
  topdir = {"vp": "", "st": bos.stat(tap)}
@@ -1247,10 +1237,22 @@ class HttpCli(object):
1247
1237
  raise
1248
1238
  raise Pebkac(404)
1249
1239
 
1250
- if depth == "0" or not self.can_read or not stat.S_ISDIR(topdir["st"].st_mode):
1251
- fgen = []
1240
+ fgen = []
1241
+
1242
+ depth = self.headers.get("depth", "infinity").lower()
1243
+ if depth == "infinity":
1244
+ if not self.can_read:
1245
+ t = "depth:infinity requires read-access in /%s"
1246
+ t = t % (self.vpath,)
1247
+ self.log(t, 3)
1248
+ raise Pebkac(401, t)
1249
+
1250
+ if not stat.S_ISDIR(topdir["st"].st_mode):
1251
+ t = "depth:infinity can only be used on folders; /%s is 0o%o"
1252
+ t = t % (self.vpath, topdir["st"])
1253
+ self.log(t, 3)
1254
+ raise Pebkac(400, t)
1252
1255
 
1253
- elif depth == "infinity":
1254
1256
  if not self.args.dav_inf:
1255
1257
  self.log("client wants --dav-inf", 3)
1256
1258
  zb = b'<?xml version="1.0" encoding="utf-8"?>\n<D:error xmlns:D="DAV:"><D:propfind-finite-depth/></D:error>'
@@ -1278,22 +1280,28 @@ class HttpCli(object):
1278
1280
  [[True, False]],
1279
1281
  lstat="davrt" not in vn.flags,
1280
1282
  )
1283
+ if not self.can_read:
1284
+ vfs_ls = []
1281
1285
  if not self.can_dot:
1282
1286
  names = set(exclude_dotfiles([x[0] for x in vfs_ls]))
1283
1287
  vfs_ls = [x for x in vfs_ls if x[0] in names]
1284
1288
 
1285
- zi = int(time.time())
1286
- zsr = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))
1287
- ls = [{"vp": vp, "st": st} for vp, st in vfs_ls]
1288
- ls += [{"vp": v, "st": zsr} for v in vfs_virt]
1289
- fgen = ls # type: ignore
1289
+ fgen = [{"vp": vp, "st": st} for vp, st in vfs_ls]
1290
+ fgen += [{"vp": v, "st": vst} for v in vfs_virt]
1291
+
1292
+ elif depth == "0":
1293
+ pass
1290
1294
 
1291
1295
  else:
1292
1296
  t = "invalid depth value '{}' (must be either '0' or '1'{})"
1293
1297
  t2 = " or 'infinity'" if self.args.dav_inf else ""
1294
1298
  raise Pebkac(412, t.format(depth, t2))
1295
1299
 
1296
- fgen = itertools.chain([topdir], fgen) # type: ignore
1300
+ if not self.can_read and not self.can_write and not self.can_get and not fgen:
1301
+ self.log("inaccessible: [%s]" % (self.vpath,))
1302
+ raise Pebkac(401, "authenticate")
1303
+
1304
+ fgen = itertools.chain([topdir], fgen)
1297
1305
  vtop = vjoin(self.args.R, vjoin(vn.vpath, rem))
1298
1306
 
1299
1307
  chunksz = 0x7FF8 # preferred by nginx or cf (dunno which)
@@ -1790,7 +1798,7 @@ class HttpCli(object):
1790
1798
  if rnd:
1791
1799
  fn = rand_name(fdir, fn, rnd)
1792
1800
 
1793
- fn = sanitize_fn(fn or "", "", [".prologue.html", ".epilogue.html"])
1801
+ fn = sanitize_fn(fn or "", "")
1794
1802
 
1795
1803
  path = os.path.join(fdir, fn)
1796
1804
 
@@ -2528,7 +2536,7 @@ class HttpCli(object):
2528
2536
  self.gctx = vpath
2529
2537
  vpath = undot(vpath)
2530
2538
  vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
2531
- rem = sanitize_vpath(rem, "/", [])
2539
+ rem = sanitize_vpath(rem, "/")
2532
2540
  fn = vfs.canonical(rem)
2533
2541
  if not fn.startswith(vfs.realpath):
2534
2542
  self.log("invalid mkdir [%s] [%s]" % (self.gctx, vpath), 1)
@@ -2574,7 +2582,7 @@ class HttpCli(object):
2574
2582
  if not ext or len(ext) > 5 or not self.can_delete:
2575
2583
  new_file += ".md"
2576
2584
 
2577
- sanitized = sanitize_fn(new_file, "", [])
2585
+ sanitized = sanitize_fn(new_file, "")
2578
2586
 
2579
2587
  if not nullwrite:
2580
2588
  fdir = vfs.canonical(rem)
@@ -2653,9 +2661,7 @@ class HttpCli(object):
2653
2661
  # fallthrough
2654
2662
 
2655
2663
  fdir = fdir_base
2656
- fname = sanitize_fn(
2657
- p_file or "", "", [".prologue.html", ".epilogue.html"]
2658
- )
2664
+ fname = sanitize_fn(p_file or "", "")
2659
2665
  abspath = os.path.join(fdir, fname)
2660
2666
  suffix = "-%.6f-%s" % (time.time(), dip)
2661
2667
  if p_file and not nullwrite:
@@ -3282,6 +3288,130 @@ class HttpCli(object):
3282
3288
 
3283
3289
  return txt
3284
3290
 
3291
+ def tx_res(self, req_path ) :
3292
+ status = 200
3293
+ logmsg = "{:4} {} ".format("", self.req)
3294
+ logtail = ""
3295
+
3296
+ editions = {}
3297
+ file_ts = 0
3298
+
3299
+ if has_resource(self.E, req_path):
3300
+ st = stat_resource(self.E, req_path)
3301
+ if st:
3302
+ file_ts = max(file_ts, st.st_mtime)
3303
+ editions["plain"] = req_path
3304
+
3305
+ if has_resource(self.E, req_path + ".gz"):
3306
+ st = stat_resource(self.E, req_path + ".gz")
3307
+ if st:
3308
+ file_ts = max(file_ts, st.st_mtime)
3309
+ if not st or st.st_mtime > file_ts:
3310
+ editions[".gz"] = req_path + ".gz"
3311
+
3312
+ if not editions:
3313
+ return self.tx_404()
3314
+
3315
+ #
3316
+ # if-modified
3317
+
3318
+ if file_ts > 0:
3319
+ file_lastmod, do_send = self._chk_lastmod(int(file_ts))
3320
+ self.out_headers["Last-Modified"] = file_lastmod
3321
+ if not do_send:
3322
+ status = 304
3323
+
3324
+ if self.can_write:
3325
+ self.out_headers["X-Lastmod3"] = str(int(file_ts * 1000))
3326
+ else:
3327
+ do_send = True
3328
+
3329
+ #
3330
+ # Accept-Encoding and UA decides which edition to send
3331
+
3332
+ decompress = False
3333
+ supported_editions = [
3334
+ x.strip()
3335
+ for x in self.headers.get("accept-encoding", "").lower().split(",")
3336
+ ]
3337
+ if ".gz" in editions:
3338
+ is_compressed = True
3339
+ selected_edition = ".gz"
3340
+
3341
+ if "gzip" not in supported_editions:
3342
+ decompress = True
3343
+ else:
3344
+ if re.match(r"MSIE [4-6]\.", self.ua) and " SV1" not in self.ua:
3345
+ decompress = True
3346
+
3347
+ if not decompress:
3348
+ self.out_headers["Content-Encoding"] = "gzip"
3349
+ else:
3350
+ is_compressed = False
3351
+ selected_edition = "plain"
3352
+
3353
+ res_path = editions[selected_edition]
3354
+ logmsg += "{} ".format(selected_edition.lstrip("."))
3355
+
3356
+ res = load_resource(self.E, res_path)
3357
+
3358
+ if decompress:
3359
+ file_sz = gzip_file_orig_sz(res)
3360
+ res = gzip.open(res)
3361
+ else:
3362
+ res.seek(0, os.SEEK_END)
3363
+ file_sz = res.tell()
3364
+ res.seek(0, os.SEEK_SET)
3365
+
3366
+ #
3367
+ # send reply
3368
+
3369
+ if is_compressed:
3370
+ self.out_headers["Cache-Control"] = "max-age=604869"
3371
+ else:
3372
+ self.permit_caching()
3373
+
3374
+ if "txt" in self.uparam:
3375
+ mime = "text/plain; charset={}".format(self.uparam["txt"] or "utf-8")
3376
+ elif "mime" in self.uparam:
3377
+ mime = str(self.uparam.get("mime"))
3378
+ else:
3379
+ mime = guess_mime(req_path)
3380
+
3381
+ logmsg += unicode(status) + logtail
3382
+
3383
+ if self.mode == "HEAD" or not do_send:
3384
+ res.close()
3385
+ if self.do_log:
3386
+ self.log(logmsg)
3387
+
3388
+ self.send_headers(length=file_sz, status=status, mime=mime)
3389
+ return True
3390
+
3391
+ ret = True
3392
+ self.send_headers(length=file_sz, status=status, mime=mime)
3393
+ remains = sendfile_py(
3394
+ self.log,
3395
+ 0,
3396
+ file_sz,
3397
+ res,
3398
+ self.s,
3399
+ self.args.s_wr_sz,
3400
+ self.args.s_wr_slp,
3401
+ not self.args.no_poll,
3402
+ )
3403
+ res.close()
3404
+
3405
+ if remains > 0:
3406
+ logmsg += " \033[31m" + unicode(file_sz - remains) + "\033[0m"
3407
+ ret = False
3408
+
3409
+ spd = self._spd(file_sz - remains)
3410
+ if self.do_log:
3411
+ self.log("{}, {}".format(logmsg, spd))
3412
+
3413
+ return ret
3414
+
3285
3415
  def tx_file(self, req_path , ptop = None) :
3286
3416
  status = 200
3287
3417
  logmsg = "{:4} {} ".format("", self.req)
@@ -3663,7 +3793,7 @@ class HttpCli(object):
3663
3793
  items ,
3664
3794
  ) :
3665
3795
  if self.args.no_zip:
3666
- raise Pebkac(400, "not enabled")
3796
+ raise Pebkac(400, "not enabled in server config")
3667
3797
 
3668
3798
  logmsg = "{:4} {} ".format("", self.req)
3669
3799
  self.keepalive = False
@@ -3789,6 +3919,33 @@ class HttpCli(object):
3789
3919
  self.reply(ico, mime=mime, headers={"Last-Modified": lm})
3790
3920
  return True
3791
3921
 
3922
+ def tx_qr(self):
3923
+ url = "%s://%s%s%s" % (
3924
+ "https" if self.is_https else "http",
3925
+ self.host,
3926
+ self.args.SRS,
3927
+ self.vpaths,
3928
+ )
3929
+ uhash = ""
3930
+ uparams = []
3931
+ if self.ouparam:
3932
+ for k, v in self.ouparam.items():
3933
+ if k == "qr":
3934
+ continue
3935
+ if k == "uhash":
3936
+ uhash = v
3937
+ continue
3938
+ uparams.append(k if v == "" else "%s=%s" % (k, v))
3939
+ if uparams:
3940
+ url += "?" + "&".join(uparams)
3941
+ if uhash:
3942
+ url += "#" + uhash
3943
+
3944
+ self.log("qrcode(%r)" % (url,))
3945
+ ret = qr2svg(QrCode.encode_binary(url.encode("utf-8")), 2)
3946
+ self.reply(ret.encode("utf-8"), mime="image/svg+xml")
3947
+ return True
3948
+
3792
3949
  def tx_md(self, vn , fs_path ) :
3793
3950
  logmsg = " %s @%s " % (self.req, self.uname)
3794
3951
 
@@ -3797,15 +3954,11 @@ class HttpCli(object):
3797
3954
  return self.tx_404(True)
3798
3955
 
3799
3956
  tpl = "mde" if "edit2" in self.uparam else "md"
3800
- html_path = os.path.join(self.E.mod, "web", "{}.html".format(tpl))
3801
3957
  template = self.j2j(tpl)
3802
3958
 
3803
3959
  st = bos.stat(fs_path)
3804
3960
  ts_md = st.st_mtime
3805
3961
 
3806
- st = bos.stat(html_path)
3807
- ts_html = st.st_mtime
3808
-
3809
3962
  max_sz = 1024 * self.args.txt_max
3810
3963
  sz_md = 0
3811
3964
  lead = b""
@@ -3839,7 +3992,7 @@ class HttpCli(object):
3839
3992
  fullfile = html_bescape(fullfile)
3840
3993
  sz_md = len(lead) + len(fullfile)
3841
3994
 
3842
- file_ts = int(max(ts_md, ts_html, self.E.t0))
3995
+ file_ts = int(max(ts_md, self.E.t0))
3843
3996
  file_lastmod, do_send = self._chk_lastmod(file_ts)
3844
3997
  self.out_headers["Last-Modified"] = file_lastmod
3845
3998
  self.out_headers.update(NO_CACHE)
@@ -3878,7 +4031,7 @@ class HttpCli(object):
3878
4031
  zs = template.render(**targs).encode("utf-8", "replace")
3879
4032
  html = zs.split(boundary.encode("utf-8"))
3880
4033
  if len(html) != 2:
3881
- raise Exception("boundary appears in " + html_path)
4034
+ raise Exception("boundary appears in " + tpl)
3882
4035
 
3883
4036
  self.send_headers(sz_md + len(html[0]) + len(html[1]), status)
3884
4037
 
@@ -3968,7 +4121,7 @@ class HttpCli(object):
3968
4121
  erd = quotep(rd)
3969
4122
  rds = rd.replace("/", " / ")
3970
4123
  spd = humansize(sz * fdone / td, True) + "/s"
3971
- eta = s2hms((td / fdone) - td, True)
4124
+ eta = s2hms((td / fdone) - td, True) if rem < 1 else "--"
3972
4125
  idle = s2hms(now - poke, True)
3973
4126
  ups.append((int(100 * fdone), spd, eta, idle, erd, rds, fn))
3974
4127
  except Exception as ex:
@@ -3983,6 +4136,7 @@ class HttpCli(object):
3983
4136
  "dbwt": None,
3984
4137
  }
3985
4138
 
4139
+
3986
4140
  fmt = self.uparam.get("ls", "")
3987
4141
  if not fmt and (self.ua.startswith("curl/") or self.ua.startswith("fetch")):
3988
4142
  fmt = "v"
@@ -4362,9 +4516,6 @@ class HttpCli(object):
4362
4516
  if self.uname != self.args.shr_adm:
4363
4517
  rows = [x for x in rows if x[5] == self.uname]
4364
4518
 
4365
- for x in rows:
4366
- x[1] = "yes" if x[1] else ""
4367
-
4368
4519
  html = self.j2s(
4369
4520
  "shares", this=self, shr=self.args.shr, rows=rows, now=int(time.time())
4370
4521
  )
@@ -4442,7 +4593,7 @@ class HttpCli(object):
4442
4593
  else:
4443
4594
  for zs in vps:
4444
4595
  if zs.endswith("/"):
4445
- t = "you cannot select more than one folder, or mix flies and folders in one selection"
4596
+ t = "you cannot select more than one folder, or mix files and folders in one selection"
4446
4597
  raise Pebkac(400, t)
4447
4598
  vp = vps[0].rsplit("/", 1)[0]
4448
4599
  for zs in vps:
@@ -4985,6 +5136,9 @@ class HttpCli(object):
4985
5136
  for k in ["zip", "tar"]:
4986
5137
  v = self.uparam.get(k)
4987
5138
  if v is not None and (not add_og or not og_fn):
5139
+ if is_dk and "dks" not in vn.flags:
5140
+ t = "server config does not allow download-as-zip/tar; only dk is specified, need dks too"
5141
+ raise Pebkac(403, t)
4988
5142
  return self.tx_zip(k, v, self.vpath, vn, rem, [])
4989
5143
 
4990
5144
  fsroot, vfs_ls, vfs_virt = vn.ls(
@@ -5019,14 +5173,14 @@ class HttpCli(object):
5019
5173
  except:
5020
5174
  pass
5021
5175
 
5176
+ lnames = {x.lower(): x for x in ls_names}
5177
+
5022
5178
  # show dotfiles if permitted and requested
5023
5179
  if not self.can_dot or (
5024
5180
  "dots" not in self.uparam and (is_ls or "dots" not in self.cookies)
5025
5181
  ):
5026
5182
  ls_names = exclude_dotfiles(ls_names)
5027
5183
 
5028
- lnames = {x.lower(): x for x in ls_names}
5029
-
5030
5184
  add_dk = vf.get("dk")
5031
5185
  add_fk = vf.get("fk")
5032
5186
  fk_alg = 2 if "fka" in vf else 1
@@ -5360,6 +5514,8 @@ class HttpCli(object):
5360
5514
  fmt = vn.flags.get("og_th", "j")
5361
5515
  th_base = ujoin(url_base, quotep(thumb))
5362
5516
  query = "th=%s&cache" % (fmt,)
5517
+ if use_filekey:
5518
+ query += "&k=" + self.uparam["k"]
5363
5519
  query = ub64enc(query.encode("utf-8")).decode("ascii")
5364
5520
  # discord looks at file extension, not content-type...
5365
5521
  query += "/th.jpg" if "j" in fmt else "/th.webp"
@@ -5369,7 +5525,10 @@ class HttpCli(object):
5369
5525
  j2a["og_file"] = file
5370
5526
  if og_fn:
5371
5527
  og_fn_q = quotep(og_fn)
5372
- query = ub64enc(b"raw").decode("ascii")
5528
+ query = "raw"
5529
+ if use_filekey:
5530
+ query += "&k=" + self.uparam["k"]
5531
+ query = ub64enc(query.encode("utf-8")).decode("ascii")
5373
5532
  query += "/%s" % (og_fn_q,)
5374
5533
  j2a["og_url"] = ujoin(url_base, og_fn_q)
5375
5534
  j2a["og_raw"] = j2a["og_url"] + "/.uqe/" + query
copyparty/httpconn.py CHANGED
@@ -100,9 +100,6 @@ class HttpConn(object):
100
100
  self.log_src = ("%s \033[%dm%d" % (ip, color, self.addr[1])).ljust(26)
101
101
  return self.log_src
102
102
 
103
- def respath(self, res_name ) :
104
- return os.path.join(self.E.mod, "web", res_name)
105
-
106
103
  def log(self, msg , c = 0) :
107
104
  self.log_func(self.log_src, msg, c)
108
105
 
copyparty/httpsrv.py CHANGED
@@ -66,9 +66,10 @@ from .util import (
66
66
  Magician,
67
67
  Netdev,
68
68
  NetMap,
69
- absreal,
70
69
  build_netmap,
70
+ has_resource,
71
71
  ipnorm,
72
+ load_resource,
72
73
  min_ex,
73
74
  shut_socket,
74
75
  spack,
@@ -88,6 +89,11 @@ if not hasattr(socket, "AF_UNIX"):
88
89
  setattr(socket, "AF_UNIX", -9001)
89
90
 
90
91
 
92
+ def load_jinja2_resource(E , name ):
93
+ with load_resource(E, "web/" + name, "r") as f:
94
+ return f.read()
95
+
96
+
91
97
  class HttpSrv(object):
92
98
  """
93
99
  handles incoming connections using HttpConn to process http,
@@ -150,7 +156,7 @@ class HttpSrv(object):
150
156
  self.u2idx_n = 0
151
157
 
152
158
  env = jinja2.Environment()
153
- env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web"))
159
+ env.loader = jinja2.FunctionLoader(lambda f: load_jinja2_resource(self.E, f))
154
160
  jn = [
155
161
  "splash",
156
162
  "shares",
@@ -163,18 +169,15 @@ class HttpSrv(object):
163
169
  "cf",
164
170
  ]
165
171
  self.j2 = {x: env.get_template(x + ".html") for x in jn}
166
- zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
167
- self.prism = os.path.exists(zs)
172
+ self.prism = has_resource(self.E, "web/deps/prism.js.gz")
168
173
 
169
174
  self.ipa_nm = build_netmap(self.args.ipa)
170
175
  self.xff_nm = build_netmap(self.args.xff_src)
171
176
  self.xff_lan = build_netmap("lan")
172
177
 
173
- self.statics = set()
174
- self._build_statics()
175
-
176
178
  self.ptn_cc = re.compile(r"[\x00-\x1f]")
177
179
  self.ptn_hsafe = re.compile(r"[\x00-\x1f<>\"'&]")
180
+ self.uparam_cc_ok = set("doc move tree".split())
178
181
 
179
182
  self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
180
183
  if not self.args.no_dav:
@@ -206,14 +209,6 @@ class HttpSrv(object):
206
209
  except:
207
210
  pass
208
211
 
209
- def _build_statics(self) :
210
- for dp, _, df in os.walk(os.path.join(self.E.mod, "web")):
211
- for fn in df:
212
- ap = absreal(os.path.join(dp, fn))
213
- self.statics.add(ap)
214
- if ap.endswith(".gz"):
215
- self.statics.add(ap[:-3])
216
-
217
212
  def set_netdevs(self, netdevs ) :
218
213
  ips = set()
219
214
  for ip, _ in self.bound:
copyparty/metrics.py CHANGED
@@ -88,7 +88,7 @@ class Metrics(object):
88
88
  addg("cpp_total_bans", str(self.hsrv.nban), t)
89
89
 
90
90
  if not args.nos_vst:
91
- x = self.hsrv.broker.ask("up2k.get_state")
91
+ x = self.hsrv.broker.ask("up2k.get_state", True, "")
92
92
  vs = json.loads(x.get())
93
93
 
94
94
  nvidle = 0
copyparty/smbd.py CHANGED
@@ -12,7 +12,7 @@ from types import SimpleNamespace
12
12
  from .__init__ import ANYWIN, EXE, TYPE_CHECKING
13
13
  from .authsrv import LEELOO_DALLAS, VFS
14
14
  from .bos import bos
15
- from .util import Daemon, min_ex, pybin, runhook
15
+ from .util import Daemon, absreal, min_ex, pybin, runhook, vjoin
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from .svchub import SvcHub
@@ -148,6 +148,8 @@ class SMB(object):
148
148
  def _uname(self) :
149
149
  if self.noacc:
150
150
  return LEELOO_DALLAS
151
+ if not self.asrv.acct:
152
+ return "*"
151
153
 
152
154
  try:
153
155
  # you found it! my single worst bit of code so far
@@ -186,7 +188,7 @@ class SMB(object):
186
188
  vfs, rem = self.asrv.vfs.get(vpath, uname, *perms)
187
189
  if not vfs.realpath:
188
190
  raise Exception("unmapped vfs")
189
- return vfs, vfs.canonical(rem)
191
+ return vfs, vjoin(vfs.realpath, rem)
190
192
 
191
193
  def _listdir(self, vpath , *a , **ka ) :
192
194
  vpath = vpath.replace("\\", "/").lstrip("/")
@@ -210,7 +212,7 @@ class SMB(object):
210
212
  sz = 112 * 2 # ['.', '..']
211
213
  for n, fn in enumerate(ls):
212
214
  if sz >= 64000:
213
- t = "listing only %d of %d files (%d byte) in /%s; see impacket#1433"
215
+ t = "listing only %d of %d files (%d byte) in /%s for performance; see --smb-nwa-1"
214
216
  warning(t, n, len(ls), sz, vpath)
215
217
  break
216
218
 
@@ -239,6 +241,7 @@ class SMB(object):
239
241
  t = "blocked write (no-write-acc %s): /%s @%s"
240
242
  yeet(t % (vfs.axs.uwrite, vpath, uname))
241
243
 
244
+ ap = absreal(ap)
242
245
  xbu = vfs.flags.get("xbu")
243
246
  if xbu and not runhook(
244
247
  self.nlog,
@@ -589,3 +589,20 @@ def _get_bit(x , i ) :
589
589
 
590
590
  class DataTooLongError(ValueError):
591
591
  pass
592
+
593
+
594
+ def qr2svg(qr , border ) :
595
+ parts = []
596
+ for y in range(qr.size):
597
+ sy = border + y
598
+ for x in range(qr.size):
599
+ if qr.modules[y][x]:
600
+ parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
601
+ t = """\
602
+ <?xml version="1.0" encoding="UTF-8"?>
603
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
604
+ <rect width="100%" height="100%" fill="#F7F7F7"/>
605
+ <path d="{1}" fill="#111111"/>
606
+ </svg>
607
+ """
608
+ return t.format(qr.size + border * 2, " ".join(parts))