copyparty 1.16.16__tar.gz → 1.16.17__tar.gz

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 (124) hide show
  1. {copyparty-1.16.16 → copyparty-1.16.17}/PKG-INFO +12 -2
  2. {copyparty-1.16.16 → copyparty-1.16.17}/README.md +11 -1
  3. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/__main__.py +15 -40
  4. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/__version__.py +2 -2
  5. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/authsrv.py +137 -56
  6. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/cfg.py +8 -0
  7. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/fsutil.py +7 -5
  8. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/httpcli.py +68 -32
  9. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/svchub.py +2 -2
  10. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/up2k.py +8 -5
  11. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/util.py +32 -0
  12. copyparty-1.16.17/copyparty/web/browser.js.gz +0 -0
  13. copyparty-1.16.17/copyparty/web/up2k.js.gz +0 -0
  14. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/util.js.gz +0 -0
  15. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty.egg-info/PKG-INFO +12 -2
  16. copyparty-1.16.16/copyparty/web/browser.js.gz +0 -0
  17. copyparty-1.16.16/copyparty/web/up2k.js.gz +0 -0
  18. {copyparty-1.16.16 → copyparty-1.16.17}/LICENSE +0 -0
  19. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/__init__.py +0 -0
  20. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/bos/__init__.py +0 -0
  21. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/bos/bos.py +0 -0
  22. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/bos/path.py +0 -0
  23. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/broker_mp.py +0 -0
  24. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/broker_mpw.py +0 -0
  25. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/broker_thr.py +0 -0
  26. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/broker_util.py +0 -0
  27. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/cert.py +0 -0
  28. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/dxml.py +0 -0
  29. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/ftpd.py +0 -0
  30. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/httpconn.py +0 -0
  31. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/httpsrv.py +0 -0
  32. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/ico.py +0 -0
  33. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/mdns.py +0 -0
  34. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/metrics.py +0 -0
  35. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/mtag.py +0 -0
  36. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/multicast.py +0 -0
  37. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/pwhash.py +0 -0
  38. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/res/COPYING.txt +0 -0
  39. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/res/__init__.py +0 -0
  40. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/res/insecure.pem +0 -0
  41. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/smbd.py +0 -0
  42. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/ssdp.py +0 -0
  43. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/star.py +0 -0
  44. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/__init__.py +0 -0
  45. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/dnslib/__init__.py +0 -0
  46. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/dnslib/bimap.py +0 -0
  47. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/dnslib/bit.py +0 -0
  48. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/dnslib/buffer.py +0 -0
  49. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/dnslib/dns.py +0 -0
  50. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/dnslib/label.py +0 -0
  51. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/dnslib/lex.py +0 -0
  52. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/dnslib/ranges.py +0 -0
  53. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/ifaddr/__init__.py +0 -0
  54. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/ifaddr/_posix.py +0 -0
  55. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/ifaddr/_shared.py +0 -0
  56. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/ifaddr/_win32.py +0 -0
  57. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/qrcodegen.py +0 -0
  58. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/stolen/surrogateescape.py +0 -0
  59. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/sutil.py +0 -0
  60. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/szip.py +0 -0
  61. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/tcpsrv.py +0 -0
  62. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/tftpd.py +0 -0
  63. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/th_cli.py +0 -0
  64. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/th_srv.py +0 -0
  65. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/u2idx.py +0 -0
  66. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/a/__init__.py +0 -0
  67. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/a/partyfuse.py +0 -0
  68. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/a/u2c.py +0 -0
  69. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/a/webdav-cfg.bat +0 -0
  70. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/baguettebox.js.gz +0 -0
  71. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/browser.css.gz +0 -0
  72. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/browser.html +0 -0
  73. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/browser2.html +0 -0
  74. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/cf.html +0 -0
  75. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/dbg-audio.js.gz +0 -0
  76. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/dd/2.png +0 -0
  77. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/dd/3.png +0 -0
  78. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/dd/4.png +0 -0
  79. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/dd/5.png +0 -0
  80. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/dd/__init__.py +0 -0
  81. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/__init__.py +0 -0
  82. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/busy.mp3.gz +0 -0
  83. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/easymde.css.gz +0 -0
  84. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/easymde.js.gz +0 -0
  85. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/fuse.py +0 -0
  86. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/marked.js.gz +0 -0
  87. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/mini-fa.css.gz +0 -0
  88. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/mini-fa.woff +0 -0
  89. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/prism.css.gz +0 -0
  90. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/prism.js.gz +0 -0
  91. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/prismd.css.gz +0 -0
  92. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/scp.woff2 +0 -0
  93. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/sha512.ac.js.gz +0 -0
  94. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/deps/sha512.hw.js.gz +0 -0
  95. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/md.css.gz +0 -0
  96. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/md.html +0 -0
  97. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/md.js.gz +0 -0
  98. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/md2.css.gz +0 -0
  99. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/md2.js.gz +0 -0
  100. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/mde.css.gz +0 -0
  101. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/mde.html +0 -0
  102. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/mde.js.gz +0 -0
  103. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/msg.css.gz +0 -0
  104. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/msg.html +0 -0
  105. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/rups.css.gz +0 -0
  106. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/rups.html +0 -0
  107. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/rups.js.gz +0 -0
  108. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/shares.css.gz +0 -0
  109. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/shares.html +0 -0
  110. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/shares.js.gz +0 -0
  111. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/splash.css.gz +0 -0
  112. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/splash.html +0 -0
  113. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/splash.js.gz +0 -0
  114. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/svcs.html +0 -0
  115. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/svcs.js.gz +0 -0
  116. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/ui.css.gz +0 -0
  117. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty/web/w.hash.js.gz +0 -0
  118. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty.egg-info/SOURCES.txt +0 -0
  119. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty.egg-info/dependency_links.txt +0 -0
  120. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty.egg-info/entry_points.txt +0 -0
  121. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty.egg-info/requires.txt +0 -0
  122. {copyparty-1.16.16 → copyparty-1.16.17}/copyparty.egg-info/top_level.txt +0 -0
  123. {copyparty-1.16.16 → copyparty-1.16.17}/pyproject.toml +0 -0
  124. {copyparty-1.16.16 → copyparty-1.16.17}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: copyparty
3
- Version: 1.16.16
3
+ Version: 1.16.17
4
4
  Summary: Portable file server with accelerated resumable uploads, deduplication, WebDAV, FTP, zeroconf, media indexer, video thumbnails, audio transcoding, and write-only folders
5
5
  Author-email: ed <copyparty@ocv.me>
6
6
  License: MIT
@@ -157,6 +157,7 @@ turn almost any device into a file server with resumable uploads/downloads using
157
157
  * [custom mimetypes](#custom-mimetypes) - change the association of a file extension
158
158
  * [GDPR compliance](#GDPR-compliance) - imagine using copyparty professionally...
159
159
  * [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out
160
+ * [feature beefybits](#feature-beefybits) - force-enable incompatible features
160
161
  * [packages](#packages) - the party might be closer than you think
161
162
  * [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
162
163
  * [fedora package](#fedora-package) - does not exist yet
@@ -1893,7 +1894,7 @@ tell search engines you don't wanna be indexed, either using the good old [robo
1893
1894
  * volflag `[...]:c,norobots` does the same thing for that single volume
1894
1895
  * volflag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally
1895
1896
 
1896
- also, `--force-js` disables the plain HTML folder listing, making things harder to parse for search engines
1897
+ also, `--force-js` disables the plain HTML folder listing, making things harder to parse for *some* search engines -- note that crawlers which understand javascript (such as google) will not be affected
1897
1898
 
1898
1899
 
1899
1900
  ## themes
@@ -2194,6 +2195,15 @@ buggy feature? rip it out by setting any of the following environment variables
2194
2195
  example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
2195
2196
 
2196
2197
 
2198
+ ### feature beefybits
2199
+
2200
+ force-enable features with known issues on your OS/env by setting any of the following environment variables, also affectionately known as `fuckitbits` or `hail-mary-bits`
2201
+
2202
+ | env-var | what it does |
2203
+ | ------------------------ | ------------ |
2204
+ | `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms |
2205
+
2206
+
2197
2207
  # packages
2198
2208
 
2199
2209
  the party might be closer than you think
@@ -100,6 +100,7 @@ turn almost any device into a file server with resumable uploads/downloads using
100
100
  * [custom mimetypes](#custom-mimetypes) - change the association of a file extension
101
101
  * [GDPR compliance](#GDPR-compliance) - imagine using copyparty professionally...
102
102
  * [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out
103
+ * [feature beefybits](#feature-beefybits) - force-enable incompatible features
103
104
  * [packages](#packages) - the party might be closer than you think
104
105
  * [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
105
106
  * [fedora package](#fedora-package) - does not exist yet
@@ -1836,7 +1837,7 @@ tell search engines you don't wanna be indexed, either using the good old [robo
1836
1837
  * volflag `[...]:c,norobots` does the same thing for that single volume
1837
1838
  * volflag `[...]:c,robots` ALLOWS search-engine crawling for that volume, even if `--no-robots` is set globally
1838
1839
 
1839
- also, `--force-js` disables the plain HTML folder listing, making things harder to parse for search engines
1840
+ also, `--force-js` disables the plain HTML folder listing, making things harder to parse for *some* search engines -- note that crawlers which understand javascript (such as google) will not be affected
1840
1841
 
1841
1842
 
1842
1843
  ## themes
@@ -2137,6 +2138,15 @@ buggy feature? rip it out by setting any of the following environment variables
2137
2138
  example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
2138
2139
 
2139
2140
 
2141
+ ### feature beefybits
2142
+
2143
+ force-enable features with known issues on your OS/env by setting any of the following environment variables, also affectionately known as `fuckitbits` or `hail-mary-bits`
2144
+
2145
+ | env-var | what it does |
2146
+ | ------------------------ | ------------ |
2147
+ | `PRTY_FORCE_MP` | force-enable multiprocessing (real multithreading) on MacOS and other broken platforms |
2148
+
2149
+
2140
2150
  # packages
2141
2151
 
2142
2152
  the party might be closer than you think
@@ -65,6 +65,7 @@ from .util import (
65
65
  load_resource,
66
66
  min_ex,
67
67
  pybin,
68
+ read_utf8,
68
69
  termsize,
69
70
  wrap,
70
71
  )
@@ -249,8 +250,7 @@ def get_srvname(verbose) :
249
250
  if verbose:
250
251
  lprint("using hostname from {}\n".format(fp))
251
252
  try:
252
- with open(fp, "rb") as f:
253
- ret = f.read().decode("utf-8", "replace").strip()
253
+ return read_utf8(None, fp, True).strip()
254
254
  except:
255
255
  ret = ""
256
256
  namelen = 5
@@ -259,47 +259,18 @@ def get_srvname(verbose) :
259
259
  ret = re.sub("[234567=]", "", ret)[:namelen]
260
260
  with open(fp, "wb") as f:
261
261
  f.write(ret.encode("utf-8") + b"\n")
262
-
263
- return ret
264
-
265
-
266
- def get_fk_salt() :
267
- fp = os.path.join(E.cfg, "fk-salt.txt")
268
- try:
269
- with open(fp, "rb") as f:
270
- ret = f.read().strip()
271
- except:
272
- ret = b64enc(os.urandom(18))
273
- with open(fp, "wb") as f:
274
- f.write(ret + b"\n")
275
-
276
- return ret.decode("utf-8")
277
-
278
-
279
- def get_dk_salt() :
280
- fp = os.path.join(E.cfg, "dk-salt.txt")
281
- try:
282
- with open(fp, "rb") as f:
283
- ret = f.read().strip()
284
- except:
285
- ret = b64enc(os.urandom(30))
286
- with open(fp, "wb") as f:
287
- f.write(ret + b"\n")
288
-
289
- return ret.decode("utf-8")
262
+ return ret
290
263
 
291
264
 
292
- def get_ah_salt() :
293
- fp = os.path.join(E.cfg, "ah-salt.txt")
265
+ def get_salt(name , nbytes ) :
266
+ fp = os.path.join(E.cfg, "%s-salt.txt" % (name,))
294
267
  try:
295
- with open(fp, "rb") as f:
296
- ret = f.read().strip()
268
+ return read_utf8(None, fp, True).strip()
297
269
  except:
298
- ret = b64enc(os.urandom(18))
270
+ ret = b64enc(os.urandom(nbytes))
299
271
  with open(fp, "wb") as f:
300
272
  f.write(ret + b"\n")
301
-
302
- return ret.decode("utf-8")
273
+ return ret.decode("utf-8")
303
274
 
304
275
 
305
276
  def ensure_locale() :
@@ -1257,6 +1228,10 @@ def add_optouts(ap):
1257
1228
  ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
1258
1229
  ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
1259
1230
  ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
1231
+ ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
1232
+ ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
1233
+ ap2.add_argument("--zipmaxt", metavar="TXT", type=u, default="", help="custom errormessage when download size exceeds max (volflag=zipmaxt)")
1234
+ ap2.add_argument("--zipmaxu", action="store_true", help="authenticated users bypass the zip size limit (volflag=zipmaxu)")
1260
1235
  ap2.add_argument("--zip-who", metavar="LVL", type=int, default=3, help="who can download as zip/tar? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=authenticated-with-read-access, [\033[32m3\033[0m]=everyone-with-read-access (volflag=zip_who)\n\033[1;31mWARNING:\033[0m if a nested volume has a more restrictive value than a parent volume, then this will be \033[33mignored\033[0m if the download is initiated from the parent, more lenient volume")
1261
1236
  ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar; same as \033[33m--zip-who=0\033[0m")
1262
1237
  ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
@@ -1544,9 +1519,9 @@ def run_argparse(
1544
1519
 
1545
1520
  cert_path = os.path.join(E.cfg, "cert.pem")
1546
1521
 
1547
- fk_salt = get_fk_salt()
1548
- dk_salt = get_dk_salt()
1549
- ah_salt = get_ah_salt()
1522
+ fk_salt = get_salt("fk", 18)
1523
+ dk_salt = get_salt("dk", 30)
1524
+ ah_salt = get_salt("ah", 18)
1550
1525
 
1551
1526
  # alpine peaks at 5 threads for some reason,
1552
1527
  # all others scale past that (but try to avoid SMT),
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 16, 16)
3
+ VERSION = (1, 16, 17)
4
4
  CODENAME = "COPYparty"
5
- BUILD_DT = (2025, 2, 28)
5
+ BUILD_DT = (2025, 3, 16)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
@@ -33,6 +33,7 @@ from .util import (
33
33
  get_df,
34
34
  humansize,
35
35
  odfusion,
36
+ read_utf8,
36
37
  relchk,
37
38
  statdir,
38
39
  ub64enc,
@@ -64,6 +65,8 @@ SSEELOG = " ({})".format(SEE_LOG)
64
65
  BAD_CFG = "invalid config; {}".format(SEE_LOG)
65
66
  SBADCFG = " ({})".format(BAD_CFG)
66
67
 
68
+ PTN_U_GRP = re.compile(r"\$\{u%([+-])([^}]+)\}")
69
+
67
70
 
68
71
  class CfgEx(Exception):
69
72
  pass
@@ -335,22 +338,26 @@ class VFS(object):
335
338
  log ,
336
339
  realpath ,
337
340
  vpath ,
341
+ vpath0 ,
338
342
  axs ,
339
343
  flags ,
340
344
  ) :
341
345
  self.log = log
342
346
  self.realpath = realpath # absolute path on host filesystem
343
347
  self.vpath = vpath # absolute path in the virtual filesystem
348
+ self.vpath0 = vpath0 # original vpath (before idp expansion)
344
349
  self.axs = axs
345
350
  self.flags = flags # config options
346
351
  self.root = self
347
352
  self.dev = 0 # st_dev
353
+ self.badcfg1 = False
348
354
  self.nodes = {} # child nodes
349
355
  self.histtab = {} # all realpath->histpath
350
356
  self.dbv = None # closest full/non-jump parent
351
357
  self.lim = None # upload limits; only set for dbv
352
358
  self.shr_src = None # source vfs+rem of a share
353
359
  self.shr_files = set() # filenames to include from shr_src
360
+ self.shr_owner = "" # uname
354
361
  self.aread = {}
355
362
  self.awrite = {}
356
363
  self.amove = {}
@@ -368,7 +375,7 @@ class VFS(object):
368
375
  vp = vpath + ("/" if vpath else "")
369
376
  self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
370
377
  self.all_vols = {vpath: self} # flattened recursive
371
- self.all_nodes = {vpath: self} # also jumpvols
378
+ self.all_nodes = {vpath: self} # also jumpvols/shares
372
379
  self.all_aps = [(rp, self)]
373
380
  self.all_vps = [(vp, self)]
374
381
  else:
@@ -408,7 +415,7 @@ class VFS(object):
408
415
  for v in self.nodes.values():
409
416
  v.get_all_vols(vols, nodes, aps, vps)
410
417
 
411
- def add(self, src , dst ) :
418
+ def add(self, src , dst , dst0 ) :
412
419
  """get existing, or add new path to the vfs"""
413
420
  assert src == "/" or not src.endswith("/") # nosec
414
421
  assert not dst.endswith("/") # nosec
@@ -416,20 +423,22 @@ class VFS(object):
416
423
  if "/" in dst:
417
424
  # requires breadth-first population (permissions trickle down)
418
425
  name, dst = dst.split("/", 1)
426
+ name0, dst0 = dst0.split("/", 1)
419
427
  if name in self.nodes:
420
428
  # exists; do not manipulate permissions
421
- return self.nodes[name].add(src, dst)
429
+ return self.nodes[name].add(src, dst, dst0)
422
430
 
423
431
  vn = VFS(
424
432
  self.log,
425
433
  os.path.join(self.realpath, name) if self.realpath else "",
426
434
  "{}/{}".format(self.vpath, name).lstrip("/"),
435
+ "{}/{}".format(self.vpath0, name0).lstrip("/"),
427
436
  self.axs,
428
437
  self._copy_flags(name),
429
438
  )
430
439
  vn.dbv = self.dbv or self
431
440
  self.nodes[name] = vn
432
- return vn.add(src, dst)
441
+ return vn.add(src, dst, dst0)
433
442
 
434
443
  if dst in self.nodes:
435
444
  # leaf exists; return as-is
@@ -437,7 +446,8 @@ class VFS(object):
437
446
 
438
447
  # leaf does not exist; create and keep permissions blank
439
448
  vp = "{}/{}".format(self.vpath, dst).lstrip("/")
440
- vn = VFS(self.log, src, vp, AXS(), {})
449
+ vp0 = "{}/{}".format(self.vpath0, dst0).lstrip("/")
450
+ vn = VFS(self.log, src, vp, vp0, AXS(), {})
441
451
  vn.dbv = self.dbv or self
442
452
  self.nodes[dst] = vn
443
453
  return vn
@@ -854,7 +864,7 @@ class AuthSrv(object):
854
864
  self.indent = ""
855
865
 
856
866
  # fwd-decl
857
- self.vfs = VFS(log_func, "", "", AXS(), {})
867
+ self.vfs = VFS(log_func, "", "", "", AXS(), {})
858
868
  self.acct = {} # uname->pw
859
869
  self.iacct = {} # pw->uname
860
870
  self.ases = {} # uname->session
@@ -922,7 +932,7 @@ class AuthSrv(object):
922
932
  self,
923
933
  src ,
924
934
  dst ,
925
- mount ,
935
+ mount ,
926
936
  daxs ,
927
937
  mflags ,
928
938
  un_gns ,
@@ -938,12 +948,24 @@ class AuthSrv(object):
938
948
  un_gn = [("", "")]
939
949
 
940
950
  for un, gn in un_gn:
951
+ m = PTN_U_GRP.search(dst0)
952
+ if m:
953
+ req, gnc = m.groups()
954
+ hit = gnc in (un_gns.get(un) or [])
955
+ if req == "+":
956
+ if not hit:
957
+ continue
958
+ elif hit:
959
+ continue
960
+
941
961
  # if ap/vp has a user/group placeholder, make sure to keep
942
962
  # track so the same user/group is mapped when setting perms;
943
963
  # otherwise clear un/gn to indicate it's a regular volume
944
964
 
945
965
  src1 = src0.replace("${u}", un or "\n")
946
966
  dst1 = dst0.replace("${u}", un or "\n")
967
+ src1 = PTN_U_GRP.sub(un or "\n", src1)
968
+ dst1 = PTN_U_GRP.sub(un or "\n", dst1)
947
969
  if src0 == src1 and dst0 == dst1:
948
970
  un = ""
949
971
 
@@ -960,7 +982,7 @@ class AuthSrv(object):
960
982
  continue
961
983
  visited.add(label)
962
984
 
963
- src, dst = self._map_volume(src, dst, mount, daxs, mflags)
985
+ src, dst = self._map_volume(src, dst, dst0, mount, daxs, mflags)
964
986
  if src:
965
987
  ret.append((src, dst, un, gn))
966
988
  if un or gn:
@@ -972,7 +994,8 @@ class AuthSrv(object):
972
994
  self,
973
995
  src ,
974
996
  dst ,
975
- mount ,
997
+ dst0 ,
998
+ mount ,
976
999
  daxs ,
977
1000
  mflags ,
978
1001
  ) :
@@ -982,13 +1005,13 @@ class AuthSrv(object):
982
1005
 
983
1006
  if dst in mount:
984
1007
  t = "multiple filesystem-paths mounted at [/{}]:\n [{}]\n [{}]"
985
- self.log(t.format(dst, mount[dst], src), c=1)
1008
+ self.log(t.format(dst, mount[dst][0], src), c=1)
986
1009
  raise Exception(BAD_CFG)
987
1010
 
988
1011
  if src in mount.values():
989
1012
  t = "filesystem-path [{}] mounted in multiple locations:"
990
1013
  t = t.format(src)
991
- for v in [k for k, v in mount.items() if v == src] + [dst]:
1014
+ for v in [k for k, v in mount.items() if v[0] == src] + [dst]:
992
1015
  t += "\n /{}".format(v)
993
1016
 
994
1017
  self.log(t, c=3)
@@ -997,7 +1020,7 @@ class AuthSrv(object):
997
1020
  if not bos.path.isdir(src):
998
1021
  self.log("warning: filesystem-path does not exist: {}".format(src), 3)
999
1022
 
1000
- mount[dst] = src
1023
+ mount[dst] = (src, dst0)
1001
1024
  daxs[dst] = AXS()
1002
1025
  mflags[dst] = {}
1003
1026
  return (src, dst)
@@ -1058,7 +1081,7 @@ class AuthSrv(object):
1058
1081
  grps ,
1059
1082
  daxs ,
1060
1083
  mflags ,
1061
- mount ,
1084
+ mount ,
1062
1085
  ) :
1063
1086
  self.line_ctr = 0
1064
1087
 
@@ -1083,7 +1106,7 @@ class AuthSrv(object):
1083
1106
  grps ,
1084
1107
  daxs ,
1085
1108
  mflags ,
1086
- mount ,
1109
+ mount ,
1087
1110
  npass ,
1088
1111
  ) :
1089
1112
  self.line_ctr = 0
@@ -1442,8 +1465,8 @@ class AuthSrv(object):
1442
1465
  acct = {} # username:password
1443
1466
  grps = {} # groupname:usernames
1444
1467
  daxs = {}
1445
- mflags = {} # moutpoint:flags
1446
- mount = {} # dst:src (mountpoint:realpath)
1468
+ mflags = {} # vpath:flags
1469
+ mount = {} # dst:src (vp:(ap,vp0))
1447
1470
 
1448
1471
  self.idp_vols = {} # yolo
1449
1472
 
@@ -1522,8 +1545,8 @@ class AuthSrv(object):
1522
1545
  # case-insensitive; normalize
1523
1546
  if WINDOWS:
1524
1547
  cased = {}
1525
- for k, v in mount.items():
1526
- cased[k] = absreal(v)
1548
+ for vp, (ap, vp0) in mount.items():
1549
+ cased[vp] = (absreal(ap), vp0)
1527
1550
 
1528
1551
  mount = cased
1529
1552
 
@@ -1538,25 +1561,28 @@ class AuthSrv(object):
1538
1561
  t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw"
1539
1562
  self.log(t, 1)
1540
1563
  axs = AXS()
1541
- vfs = VFS(self.log_func, absreal("."), "", axs, {})
1564
+ vfs = VFS(self.log_func, absreal("."), "", "", axs, {})
1565
+ if not axs.uread:
1566
+ vfs.badcfg1 = True
1542
1567
  elif "" not in mount:
1543
1568
  # there's volumes but no root; make root inaccessible
1544
1569
  zsd = {"d2d": True, "tcolor": self.args.tcolor}
1545
- vfs = VFS(self.log_func, "", "", AXS(), zsd)
1570
+ vfs = VFS(self.log_func, "", "", "", AXS(), zsd)
1546
1571
 
1547
1572
  maxdepth = 0
1548
1573
  for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
1549
1574
  depth = dst.count("/")
1550
1575
  assert maxdepth <= depth # nosec
1551
1576
  maxdepth = depth
1577
+ src, dst0 = mount[dst]
1552
1578
 
1553
1579
  if dst == "":
1554
1580
  # rootfs was mapped; fully replaces the default CWD vfs
1555
- vfs = VFS(self.log_func, mount[dst], dst, daxs[dst], mflags[dst])
1581
+ vfs = VFS(self.log_func, src, dst, dst0, daxs[dst], mflags[dst])
1556
1582
  continue
1557
1583
 
1558
1584
  assert vfs # type: ignore
1559
- zv = vfs.add(mount[dst], dst)
1585
+ zv = vfs.add(src, dst, dst0)
1560
1586
  zv.axs = daxs[dst]
1561
1587
  zv.flags = mflags[dst]
1562
1588
  zv.dbv = None
@@ -1590,7 +1616,8 @@ class AuthSrv(object):
1590
1616
  if enshare:
1591
1617
  import sqlite3
1592
1618
 
1593
- shv = VFS(self.log_func, "", shr, AXS(), {})
1619
+ zsd = {"d2d": True, "tcolor": self.args.tcolor}
1620
+ shv = VFS(self.log_func, "", shr, shr, AXS(), zsd)
1594
1621
 
1595
1622
  db_path = self.args.shr_db
1596
1623
  db = sqlite3.connect(db_path)
@@ -1624,9 +1651,8 @@ class AuthSrv(object):
1624
1651
 
1625
1652
  # don't know the abspath yet + wanna ensure the user
1626
1653
  # still has the privs they granted, so nullmap it
1627
- shv.nodes[s_k] = VFS(
1628
- self.log_func, "", "%s/%s" % (shr, s_k), s_axs, shv.flags.copy()
1629
- )
1654
+ vp = "%s/%s" % (shr, s_k)
1655
+ shv.nodes[s_k] = VFS(self.log_func, "", vp, vp, s_axs, shv.flags.copy())
1630
1656
 
1631
1657
  vfs.nodes[shr] = vfs.all_vols[shr] = shv
1632
1658
  for vol in shv.nodes.values():
@@ -1787,6 +1813,24 @@ class AuthSrv(object):
1787
1813
  rhisttab[histp] = zv
1788
1814
  vfs.histtab[zv.realpath] = histp
1789
1815
 
1816
+ for vol in vfs.all_vols.values():
1817
+ use = False
1818
+ for k in ["zipmaxn", "zipmaxs"]:
1819
+ try:
1820
+ zs = vol.flags[k]
1821
+ except:
1822
+ zs = getattr(self.args, k)
1823
+ if zs in ("", "0"):
1824
+ vol.flags[k] = 0
1825
+ continue
1826
+
1827
+ zf = unhumanize(zs)
1828
+ vol.flags[k + "_v"] = zf
1829
+ if zf:
1830
+ use = True
1831
+ if use:
1832
+ vol.flags["zipmax"] = True
1833
+
1790
1834
  for vol in vfs.all_vols.values():
1791
1835
  lim = Lim(self.log_func)
1792
1836
  use = False
@@ -2269,22 +2313,56 @@ class AuthSrv(object):
2269
2313
  except Pebkac:
2270
2314
  self.warn_anonwrite = True
2271
2315
 
2272
- idp_err = "WARNING! The following IdP volumes are mounted directly below another volume where anonymous users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by anonymous users UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
2316
+ self.idp_warn = []
2317
+ self.idp_err = []
2273
2318
  for idp_vp in self.idp_vols:
2274
- parent_vp = vsplit(idp_vp)[0]
2275
- vn, _ = vfs.get(parent_vp, "*", False, False)
2276
- zs = (
2277
- "READABLE"
2278
- if "*" in vn.axs.uread
2279
- else "WRITABLE"
2280
- if "*" in vn.axs.uwrite
2281
- else ""
2282
- )
2283
- if zs:
2284
- t = '\nWARNING: Volume "/%s" appears below "/%s" and would be WORLD-%s'
2285
- idp_err += t % (idp_vp, vn.vpath, zs)
2286
- if "\n" in idp_err:
2287
- self.log(idp_err, 1)
2319
+ idp_vn, _ = vfs.get(idp_vp, "*", False, False)
2320
+ idp_vp0 = idp_vn.vpath0
2321
+
2322
+ sigils = set(re.findall(r"(\${[ug][}%])", idp_vp0))
2323
+ if len(sigils) > 1:
2324
+ t = '\nWARNING: IdP-volume "/%s" created by "/%s" has multiple IdP placeholders: %s'
2325
+ self.idp_warn.append(t % (idp_vp, idp_vp0, list(sigils)))
2326
+ continue
2327
+
2328
+ sigil = sigils.pop()
2329
+ par_vp = idp_vp
2330
+ while par_vp:
2331
+ par_vp = vsplit(par_vp)[0]
2332
+ par_vn, _ = vfs.get(par_vp, "*", False, False)
2333
+ if sigil in par_vn.vpath0:
2334
+ continue # parent was spawned for and by same user
2335
+
2336
+ oth_read = []
2337
+ oth_write = []
2338
+ for usr in par_vn.axs.uread:
2339
+ if usr not in idp_vn.axs.uread:
2340
+ oth_read.append(usr)
2341
+ for usr in par_vn.axs.uwrite:
2342
+ if usr not in idp_vn.axs.uwrite:
2343
+ oth_write.append(usr)
2344
+
2345
+ if "*" in oth_read:
2346
+ taxs = "WORLD-READABLE"
2347
+ elif "*" in oth_write:
2348
+ taxs = "WORLD-WRITABLE"
2349
+ elif oth_read:
2350
+ taxs = "READABLE BY %r" % (oth_read,)
2351
+ elif oth_write:
2352
+ taxs = "WRITABLE BY %r" % (oth_write,)
2353
+ else:
2354
+ break # no sigil; not idp; safe to stop
2355
+
2356
+ t = '\nWARNING: IdP-volume "/%s" created by "/%s" has parent/grandparent "/%s" and would be %s'
2357
+ self.idp_err.append(t % (idp_vp, idp_vp0, par_vn.vpath, taxs))
2358
+
2359
+ if self.idp_warn:
2360
+ t = "WARNING! Some IdP volumes include multiple IdP placeholders; this is too complex to automatically determine if safe or not. To ensure that no users gain unintended access, please use only a single placeholder for each IdP volume."
2361
+ self.log(t + "".join(self.idp_warn), 1)
2362
+
2363
+ if self.idp_err:
2364
+ t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
2365
+ self.log(t + "".join(self.idp_err), 1)
2288
2366
 
2289
2367
  self.vfs = vfs
2290
2368
  self.acct = acct
@@ -2319,11 +2397,6 @@ class AuthSrv(object):
2319
2397
  for x, y in vfs.all_vols.items()
2320
2398
  if x != shr and not x.startswith(shrs)
2321
2399
  }
2322
- vfs.all_nodes = {
2323
- x: y
2324
- for x, y in vfs.all_nodes.items()
2325
- if x != shr and not x.startswith(shrs)
2326
- }
2327
2400
 
2328
2401
  assert db and cur and cur2 and shv # type: ignore
2329
2402
  for row in cur.execute("select * from sh"):
@@ -2353,6 +2426,7 @@ class AuthSrv(object):
2353
2426
  else:
2354
2427
  shn.ls = shn._ls
2355
2428
 
2429
+ shn.shr_owner = s_un
2356
2430
  shn.shr_src = (s_vfs, s_rem)
2357
2431
  shn.realpath = s_vfs.canonical(s_rem)
2358
2432
 
@@ -2370,7 +2444,7 @@ class AuthSrv(object):
2370
2444
  continue # also fine
2371
2445
  for zs in svn.nodes.keys():
2372
2446
  # hide subvolume
2373
- vn.nodes[zs] = VFS(self.log_func, "", "", AXS(), {})
2447
+ vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), {})
2374
2448
 
2375
2449
  cur2.close()
2376
2450
  cur.close()
@@ -2378,7 +2452,9 @@ class AuthSrv(object):
2378
2452
 
2379
2453
  self.js_ls = {}
2380
2454
  self.js_htm = {}
2381
- for vn in self.vfs.all_nodes.values():
2455
+ for vp, vn in self.vfs.all_nodes.items():
2456
+ if enshare and vp.startswith(shrs):
2457
+ continue # propagates later in this func
2382
2458
  vf = vn.flags
2383
2459
  vn.js_ls = {
2384
2460
  "idx": "e2d" in vf,
@@ -2435,8 +2511,12 @@ class AuthSrv(object):
2435
2511
 
2436
2512
  vols = list(vfs.all_nodes.values())
2437
2513
  if enshare:
2438
- vols.append(shv)
2439
- vols.extend(list(shv.nodes.values()))
2514
+ for vol in shv.nodes.values():
2515
+ if vol.vpath not in vfs.all_nodes:
2516
+ self.log("BUG: /%s not in all_nodes" % (vol.vpath,), 1)
2517
+ vols.append(vol)
2518
+ if shr in vfs.all_nodes:
2519
+ self.log("BUG: %s found in all_nodes" % (shr,), 1)
2440
2520
 
2441
2521
  for vol in vols:
2442
2522
  dbv = vol.get_dbv("")[0]
@@ -2539,8 +2619,8 @@ class AuthSrv(object):
2539
2619
  if not bos.path.exists(ap):
2540
2620
  pwdb = {}
2541
2621
  else:
2542
- with open(ap, "r", encoding="utf-8") as f:
2543
- pwdb = json.load(f)
2622
+ jtxt = read_utf8(self.log, ap, True)
2623
+ pwdb = json.loads(jtxt)
2544
2624
 
2545
2625
  pwdb = [x for x in pwdb if x[0] != uname]
2546
2626
  pwdb.append((uname, self.defpw[uname], hpw))
@@ -2563,8 +2643,8 @@ class AuthSrv(object):
2563
2643
  if not self.args.chpw or not bos.path.exists(ap):
2564
2644
  return
2565
2645
 
2566
- with open(ap, "r", encoding="utf-8") as f:
2567
- pwdb = json.load(f)
2646
+ jtxt = read_utf8(self.log, ap, True)
2647
+ pwdb = json.loads(jtxt)
2568
2648
 
2569
2649
  useen = set()
2570
2650
  urst = set()
@@ -3060,8 +3140,9 @@ def expand_config_file(
3060
3140
  ipath += " -> " + fp
3061
3141
  ret.append("#\033[36m opening cfg file{}\033[0m".format(ipath))
3062
3142
 
3063
- with open(fp, "rb") as f:
3064
- for oln in [x.decode("utf-8").rstrip() for x in f]:
3143
+ cfg_lines = read_utf8(log, fp, True).split("\n")
3144
+ if True: # diff-golf
3145
+ for oln in [x.rstrip() for x in cfg_lines]:
3065
3146
  ln = oln.split(" #")[0].strip()
3066
3147
  if ln.startswith("% "):
3067
3148
  pad = " " * len(oln.split("%")[0])
@@ -55,6 +55,7 @@ def vf_bmap() :
55
55
  "xdev",
56
56
  "xlink",
57
57
  "xvol",
58
+ "zipmaxu",
58
59
  ):
59
60
  ret[k] = k
60
61
  return ret
@@ -101,6 +102,9 @@ def vf_vmap() :
101
102
  "u2ts",
102
103
  "ups_who",
103
104
  "zip_who",
105
+ "zipmaxn",
106
+ "zipmaxs",
107
+ "zipmaxt",
104
108
  ):
105
109
  ret[k] = k
106
110
  return ret
@@ -299,6 +303,10 @@ flagcats = {
299
303
  "rss": "allow '?rss' URL suffix (experimental)",
300
304
  "ups_who=2": "restrict viewing the list of recent uploads",
301
305
  "zip_who=2": "restrict access to download-as-zip/tar",
306
+ "zipmaxn=9k": "reject download-as-zip if more than 9000 files",
307
+ "zipmaxs=2g": "reject download-as-zip if size over 2 GiB",
308
+ "zipmaxt=no": "reply with 'no' if download-as-zip exceeds max",
309
+ "zipmaxu": "zip-size-limit does not apply to authenticated users",
302
310
  "nopipe": "disable race-the-beam (download unfinished uploads)",
303
311
  "mv_retry": "ms-windows: timeout for renaming busy files",
304
312
  "rm_retry": "ms-windows: timeout for deleting busy files",