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/__init__.py +55 -0
- copyparty/__main__.py +15 -12
- copyparty/__version__.py +2 -2
- copyparty/cert.py +20 -8
- copyparty/cfg.py +1 -1
- copyparty/ftpd.py +1 -1
- copyparty/httpcli.py +227 -68
- copyparty/httpconn.py +0 -3
- copyparty/httpsrv.py +10 -15
- copyparty/metrics.py +1 -1
- copyparty/smbd.py +6 -3
- copyparty/stolen/qrcodegen.py +17 -0
- copyparty/szip.py +1 -1
- copyparty/u2idx.py +1 -0
- copyparty/up2k.py +31 -13
- copyparty/util.py +145 -64
- copyparty/web/a/partyfuse.py +233 -357
- copyparty/web/a/u2c.py +249 -153
- copyparty/web/browser.css.gz +0 -0
- copyparty/web/browser.html +3 -6
- copyparty/web/browser.js.gz +0 -0
- copyparty/web/deps/fuse.py +1064 -0
- copyparty/web/deps/marked.js.gz +0 -0
- copyparty/web/shares.css.gz +0 -0
- copyparty/web/shares.html +7 -4
- copyparty/web/shares.js.gz +0 -0
- copyparty/web/splash.html +12 -12
- copyparty/web/splash.js.gz +0 -0
- copyparty/web/svcs.html +1 -1
- copyparty/web/ui.css.gz +0 -0
- copyparty/web/util.js.gz +0 -0
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/METADATA +9 -8
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/RECORD +37 -36
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/WHEEL +1 -1
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/LICENSE +0 -0
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/entry_points.txt +0 -0
- {copyparty-1.15.2.dist-info → copyparty-1.15.4.dist-info}/top_level.txt +0 -0
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
|
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
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
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
|
1109
|
+
if res_path != undot(res_path):
|
1097
1110
|
t = "malicious user; attempted path traversal [{}] => [{}]"
|
1098
|
-
self.log(t.format(self.vpath,
|
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
|
-
|
1217
|
-
|
1218
|
-
|
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
|
-
|
1241
|
-
|
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
|
-
|
1251
|
-
|
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
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
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
|
-
|
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 "", ""
|
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,
|
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 " +
|
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
|
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 =
|
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.
|
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
|
-
|
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
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.
|
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
|
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,
|
copyparty/stolen/qrcodegen.py
CHANGED
@@ -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))
|