onionshare_cli 2.6.5__tar.gz → 2.7__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 (51) hide show
  1. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/PKG-INFO +21 -21
  2. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/__init__.py +37 -1
  3. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/censorship.py +1 -1
  4. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/common.py +110 -12
  5. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/meek.py +7 -1
  6. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/mode_settings.py +13 -2
  7. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/onion.py +77 -7
  8. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/onionshare.py +70 -1
  9. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/js/receive.js +7 -1
  10. onionshare_cli-2.7/onionshare_cli/resources/templates/413.html +21 -0
  11. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/receive.html +10 -0
  12. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/torrc_template +1 -0
  13. onionshare_cli-2.7/onionshare_cli/resources/torrc_template-meek +2 -0
  14. onionshare_cli-2.7/onionshare_cli/resources/torrc_template-obfs4 +8 -0
  15. onionshare_cli-2.7/onionshare_cli/resources/torrc_template-snowflake +3 -0
  16. onionshare_cli-2.7/onionshare_cli/resources/torrc_template-webtunnel +6 -0
  17. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/settings.py +31 -31
  18. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/web/receive_mode.py +373 -99
  19. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/web/share_mode.py +3 -0
  20. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/web/web.py +59 -9
  21. onionshare_cli-2.7/pyproject.toml +80 -0
  22. onionshare_cli-2.6.5/onionshare_cli/resources/torrc_template-meek_lite_azure +0 -2
  23. onionshare_cli-2.6.5/onionshare_cli/resources/torrc_template-obfs4 +0 -12
  24. onionshare_cli-2.6.5/onionshare_cli/resources/torrc_template-snowflake +0 -3
  25. onionshare_cli-2.6.5/onionshare_cli/resources/version.txt +0 -1
  26. onionshare_cli-2.6.5/pyproject.toml +0 -53
  27. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/css/style.css +0 -0
  28. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/img/ajax.gif +0 -0
  29. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/img/favicon.ico +0 -0
  30. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/img/logo.png +0 -0
  31. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/img/logo_large.png +0 -0
  32. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/img/web_file.png +0 -0
  33. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/img/web_folder.png +0 -0
  34. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/js/chat.js +0 -0
  35. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/js/jquery-3.7.1.slim.min.js +0 -0
  36. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/js/send.js +0 -0
  37. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/static/js/socket.io.min.js +0 -0
  38. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/403.html +0 -0
  39. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/404.html +0 -0
  40. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/405.html +0 -0
  41. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/500.html +0 -0
  42. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/chat.html +0 -0
  43. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/denied.html +0 -0
  44. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/listing.html +0 -0
  45. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/send.html +0 -0
  46. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/templates/thankyou.html +0 -0
  47. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/resources/wordlist.txt +0 -0
  48. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/web/__init__.py +0 -0
  49. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/web/chat_mode.py +0 -0
  50. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/web/send_base_mode.py +0 -0
  51. {onionshare_cli-2.6.5 → onionshare_cli-2.7}/onionshare_cli/web/website_mode.py +0 -0
@@ -1,29 +1,26 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.3
2
2
  Name: onionshare_cli
3
- Version: 2.6.5
4
- Summary: OnionShare lets you securely and anonymously send and receive files. It works by starting a web server, making it accessible as a Tor onion service, and generating an unguessable web address so others can download files from you, or upload files to you. It does _not_ require setting up a separate server or using a third party file-sharing service.
5
- License: GPLv3+
3
+ Version: 2.7
4
+ Summary: Securely and anonymously share files, host websites, and chat with friends using the Tor network
5
+ License: GPL-3.0-or-later
6
+ Keywords: onion,share,onionshare,tor,anonymous,web server
6
7
  Author: Micah Lee
7
8
  Author-email: micah@micahflee.com
9
+ Maintainer: Micah Lee
10
+ Maintainer-email: micah@micahflee.com
8
11
  Requires-Python: >=3.10,<4.0
9
- Classifier: Environment :: Web Environment
10
- Classifier: Framework :: Flask
11
- Classifier: Intended Audience :: End Users/Desktop
12
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
13
- Classifier: License :: Other/Proprietary License
14
- Classifier: Operating System :: OS Independent
15
12
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Programming Language :: Python :: 3.13
20
- Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Framework :: Flask
21
14
  Classifier: Topic :: Communications :: File Sharing
22
15
  Classifier: Topic :: Security :: Cryptography
16
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
17
+ Classifier: Intended Audience :: End Users/Desktop
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Environment :: Web Environment
23
20
  Requires-Dist: cffi (>=2,<3)
24
21
  Requires-Dist: click
25
22
  Requires-Dist: colorama
26
- Requires-Dist: cython (>=3.0.2,<4.0.0)
23
+ Requires-Dist: cython (>=3.0.2,<4.0)
27
24
  Requires-Dist: eventlet
28
25
  Requires-Dist: flask (==3.1.3)
29
26
  Requires-Dist: flask-compress (>=1.24,<2.0)
@@ -31,16 +28,19 @@ Requires-Dist: flask-socketio (>=5.6,<6.0)
31
28
  Requires-Dist: gevent (>=26.5.0,<27.0.0)
32
29
  Requires-Dist: gevent-websocket
33
30
  Requires-Dist: packaging (>=24)
34
- Requires-Dist: pkgconfig (>=1.6.0,<2.0.0)
31
+ Requires-Dist: pkgconfig (>=1.6.0,<2.0)
35
32
  Requires-Dist: psutil
36
- Requires-Dist: pynacl (>=1.6.2,<2.0.0)
33
+ Requires-Dist: pynacl (>=1.6.2,<2.0)
37
34
  Requires-Dist: pysocks
38
35
  Requires-Dist: qrcode (>=7.4.2,<8.0.0)
39
36
  Requires-Dist: requests[socks] (>=2.32.3)
40
- Requires-Dist: setuptools (>=80,<81)
37
+ Requires-Dist: setuptools (>=83)
41
38
  Requires-Dist: stem (==1.8.1)
42
39
  Requires-Dist: unidecode
43
40
  Requires-Dist: urllib3 (>=2.7,<3.0)
44
41
  Requires-Dist: waitress (>=3.0.1,<4.0.0)
45
- Requires-Dist: werkzeug (==3.1.8)
46
- Requires-Dist: wheel (>=0.46.2,<0.47.0)
42
+ Requires-Dist: werkzeug (==3.1.9)
43
+ Requires-Dist: wheel (>=0.46.2,<1.0)
44
+ Project-URL: Documentation, https://docs.onionshare.org/
45
+ Project-URL: Homepage, https://onionshare.org
46
+ Project-URL: Repository, https://github.com/onionshare/onionshare
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
19
19
  """
20
20
 
21
21
  import os
22
+ import signal
22
23
  import sys
23
24
  import time
24
25
  import argparse
@@ -42,6 +43,12 @@ def main(cwd=None):
42
43
  common = Common()
43
44
  common.display_banner()
44
45
 
46
+ # Allow SIGTERM (e.g. `kill`, systemd stop) to clean up like Ctrl-C does
47
+ def sigterm_handler(signum, frame):
48
+ raise KeyboardInterrupt
49
+
50
+ signal.signal(signal.SIGTERM, sigterm_handler)
51
+
45
52
  # OnionShare CLI in OSX needs to change current working directory (#132)
46
53
  if common.platform == "Darwin":
47
54
  if cwd:
@@ -159,6 +166,18 @@ def main(cwd=None):
159
166
  dest="disable_files",
160
167
  help="Receive files: Disable receiving files",
161
168
  )
169
+ parser.add_argument(
170
+ "--max-upload-size",
171
+ metavar="size",
172
+ default="0",
173
+ help="Receive files: Maximum size of an upload, e.g. 100mb (0 means no limit)",
174
+ )
175
+ parser.add_argument(
176
+ "--max-upload-size-total",
177
+ action="store_true",
178
+ dest="max_upload_size_total",
179
+ help="Receive files: Apply the maximum upload size to the entire service lifespan",
180
+ )
162
181
  # Website args
163
182
  parser.add_argument(
164
183
  "--disable_csp",
@@ -210,11 +229,24 @@ def main(cwd=None):
210
229
  webhook_url = args.webhook_url
211
230
  disable_text = args.disable_text
212
231
  disable_files = args.disable_files
232
+ max_upload_size_raw = args.max_upload_size
233
+ max_upload_size_total = args.max_upload_size_total
213
234
  disable_csp = bool(args.disable_csp)
214
235
  custom_csp = args.custom_csp
215
236
  log_filenames = bool(args.log_filenames)
216
237
  verbose = bool(args.verbose)
217
238
 
239
+ # Parse the maximum upload size, which is given in human friendly units
240
+ try:
241
+ max_upload_size = common.parse_human_readable_filesize(max_upload_size_raw)
242
+ except ValueError:
243
+ print(f"Invalid maximum upload size: {max_upload_size_raw}")
244
+ sys.exit()
245
+
246
+ if max_upload_size_total and max_upload_size <= 0:
247
+ print("You must set --max-upload-size to use --max-upload-size-total")
248
+ sys.exit()
249
+
218
250
  # Verbose mode?
219
251
  common.verbose = verbose
220
252
 
@@ -259,6 +291,8 @@ def main(cwd=None):
259
291
  mode_settings.set("receive", "webhook_url", webhook_url)
260
292
  mode_settings.set("receive", "disable_text", disable_text)
261
293
  mode_settings.set("receive", "disable_files", disable_files)
294
+ mode_settings.set("receive", "max_upload_size", max_upload_size)
295
+ mode_settings.set("receive", "max_upload_size_total", max_upload_size_total)
262
296
  if mode == "website":
263
297
  if disable_csp and custom_csp:
264
298
  print(
@@ -451,7 +485,9 @@ def main(cwd=None):
451
485
  print("")
452
486
 
453
487
  # Start OnionShare http service in new thread
454
- t = threading.Thread(target=web.start, args=(app.port,))
488
+ t = threading.Thread(
489
+ target=web.start, args=(app.port, app.unix_socket, app.unix_socket_dir)
490
+ )
455
491
  t.daemon = True
456
492
  t.start()
457
493
 
@@ -144,7 +144,7 @@ class CensorshipCircumvention(object):
144
144
  )
145
145
  data = {"country": country}
146
146
  if transports:
147
- data.append({"transports": transports})
147
+ data["transports"] = transports
148
148
  try:
149
149
  r = requests.post(
150
150
  endpoint,
@@ -17,8 +17,10 @@ GNU General Public License for more details.
17
17
  You should have received a copy of the GNU General Public License
18
18
  along with this program. If not, see <http://www.gnu.org/licenses/>.
19
19
  """
20
+
20
21
  import base64
21
22
  import hashlib
23
+ import importlib.metadata as importlib_metadata
22
24
  import importlib.resources as importlib_resources
23
25
  import os
24
26
  import platform
@@ -30,6 +32,7 @@ import threading
30
32
  import time
31
33
  import shutil
32
34
  import re
35
+ import tempfile
33
36
 
34
37
  import colorama
35
38
  from colorama import Fore, Back, Style
@@ -59,8 +62,7 @@ class Common:
59
62
  self.platform = "BSD"
60
63
 
61
64
  # The current version of OnionShare
62
- with open(self.get_resource_path("version.txt")) as f:
63
- self.version = f.read().strip()
65
+ self.version = importlib_metadata.version(__package__)
64
66
 
65
67
  def display_banner(self):
66
68
  """
@@ -326,6 +328,7 @@ class Common:
326
328
  obfs4proxy_file_path = shutil.which("obfs4proxy")
327
329
  snowflake_file_path = shutil.which("snowflake-client")
328
330
  meek_client_file_path = shutil.which("meek-client")
331
+ webtunnel_file_path = shutil.which("webtunnel-client")
329
332
  prefix = os.path.dirname(os.path.dirname(tor_path))
330
333
  tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")
331
334
  tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6")
@@ -352,6 +355,7 @@ class Common:
352
355
  obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy.exe")
353
356
  snowflake_file_path = os.path.join(base_path, "snowflake-client.exe")
354
357
  meek_client_file_path = os.path.join(base_path, "meek-client.exe")
358
+ webtunnel_file_path = os.path.join(base_path, "webtunnel-client.exe")
355
359
  tor_geo_ip_file_path = os.path.join(base_path, "geoip")
356
360
  tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
357
361
 
@@ -364,6 +368,7 @@ class Common:
364
368
  obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy")
365
369
  snowflake_file_path = os.path.join(base_path, "snowflake-client")
366
370
  meek_client_file_path = os.path.join(base_path, "meek-client")
371
+ webtunnel_file_path = os.path.join(base_path, "webtunnel-client")
367
372
  tor_geo_ip_file_path = os.path.join(base_path, "geoip")
368
373
  tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
369
374
  else:
@@ -375,6 +380,7 @@ class Common:
375
380
  obfs4proxy_file_path = shutil.which("obfs4proxy")
376
381
  snowflake_file_path = shutil.which("snowflake-client")
377
382
  meek_client_file_path = shutil.which("meek-client")
383
+ webtunnel_file_path = shutil.which("webtunnel-client")
378
384
  prefix = os.path.dirname(os.path.dirname(tor_path))
379
385
  tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")
380
386
  tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6")
@@ -386,6 +392,7 @@ class Common:
386
392
  obfs4proxy_file_path = "/usr/local/bin/obfs4proxy"
387
393
  snowflake_file_path = "/usr/local/bin/snowflake-client"
388
394
  meek_client_file_path = "/usr/local/bin/meek-client"
395
+ webtunnel_file_path = "/usr/local/bin/webtunnel-client"
389
396
 
390
397
  return (
391
398
  tor_path,
@@ -394,6 +401,7 @@ class Common:
394
401
  obfs4proxy_file_path,
395
402
  snowflake_file_path,
396
403
  meek_client_file_path,
404
+ webtunnel_file_path,
397
405
  )
398
406
 
399
407
  def build_data_dir(self):
@@ -430,12 +438,46 @@ class Common:
430
438
  os.makedirs(onionshare_data_dir, 0o700, True)
431
439
  return onionshare_data_dir
432
440
 
441
+ def build_cache_dir(self):
442
+ """
443
+ Returns the path of the OnionShare cache directory, which holds
444
+ non-essential data that can safely be deleted. On Linux and BSD this
445
+ follows the XDG Base Directory Specification (~/.cache/onionshare).
446
+ """
447
+ xdg_cache_home = os.environ.get("XDG_CACHE_HOME", "")
448
+ if not os.path.isabs(xdg_cache_home):
449
+ xdg_cache_home = os.path.expanduser("~/.cache")
450
+
451
+ onionshare_cache_dir = os.path.join(xdg_cache_home, "onionshare")
452
+
453
+ # Modify the cache dir if running tests
454
+ if getattr(sys, "onionshare_test_mode", False):
455
+ onionshare_cache_dir += "-testdata"
456
+
457
+ os.makedirs(onionshare_cache_dir, 0o700, True)
458
+ return onionshare_cache_dir
459
+
433
460
  def build_tmp_dir(self):
434
461
  """
435
462
  Returns path to a folder that can hold temporary files
436
463
  """
437
- tmp_dir = os.path.join(self.build_data_dir(), "tmp")
438
- os.makedirs(tmp_dir, 0o700, True)
464
+ if self.platform == "Darwin":
465
+ # macOS Application Support contains a space, which makes Unix
466
+ # socket targets unsuitable for Tor's ADD_ONION Port= argument.
467
+ # Keep all temporary files in the per-user system temp area.
468
+ dirname = "onionshare"
469
+ if getattr(sys, "onionshare_test_mode", False):
470
+ dirname += "-testdata"
471
+ tmp_dir = os.path.join(tempfile.gettempdir(), dirname)
472
+ else:
473
+ base_dir = (
474
+ self.build_data_dir()
475
+ if self.platform == "Windows"
476
+ else self.build_cache_dir()
477
+ )
478
+ tmp_dir = os.path.join(base_dir, "tmp")
479
+
480
+ os.makedirs(tmp_dir, mode=0o700, exist_ok=True)
439
481
  return tmp_dir
440
482
 
441
483
  def build_persistent_dir(self):
@@ -448,9 +490,16 @@ class Common:
448
490
 
449
491
  def build_tor_dir(self):
450
492
  """
451
- Returns path to the tor data directory
493
+ Returns path to the tor data directory. The tor data directory holds
494
+ non-essential data that can safely be deleted, so on Linux and BSD
495
+ it lives under the XDG cache directory (~/.cache/onionshare).
452
496
  """
453
- tor_dir = os.path.join(self.build_data_dir(), "tor_data")
497
+ if self.platform in ("Windows", "Darwin"):
498
+ base_dir = self.build_data_dir()
499
+ else:
500
+ base_dir = self.build_cache_dir()
501
+
502
+ tor_dir = os.path.join(base_dir, "tor_data")
454
503
  os.makedirs(tor_dir, 0o700, True)
455
504
  return tor_dir
456
505
 
@@ -496,11 +545,16 @@ class Common:
496
545
  snowflake_pattern = re.compile(
497
546
  r"(snowflake)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)"
498
547
  )
548
+ # WebTunnel bridge lines: "webtunnel <IP>:<port> <fingerprint> url=https://..."
549
+ webtunnel_pattern = re.compile(
550
+ r"(webtunnel)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Fa-f]+)(\s)+url=(.+)"
551
+ )
499
552
  if (
500
553
  ipv4_pattern.match(bridge)
501
554
  or ipv6_pattern.match(bridge)
502
555
  or meek_lite_pattern.match(bridge)
503
556
  or snowflake_pattern.match(bridge)
557
+ or webtunnel_pattern.match(bridge)
504
558
  ):
505
559
  valid_bridges.append(bridge)
506
560
  if valid_bridges:
@@ -533,20 +587,64 @@ class Common:
533
587
  return s[:output_len]
534
588
 
535
589
  @staticmethod
536
- def human_readable_filesize(b):
590
+ def split_filesize(b):
537
591
  """
538
- Returns filesize in a human readable format.
592
+ Splits a filesize into a human readable value and the index of its
593
+ unit, where 0 is bytes, 1 is KiB, 2 is MiB, and so on.
539
594
  """
540
595
  thresh = 1024.0
541
596
  if b < thresh:
542
- return "{:.1f} B".format(b)
543
- units = ("KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
544
- u = 0
597
+ return b, 0
598
+ u = 1
545
599
  b /= thresh
546
600
  while b >= thresh:
547
601
  b /= thresh
548
602
  u += 1
549
- return "{:.1f} {}".format(b, units[u])
603
+ return b, u
604
+
605
+ @staticmethod
606
+ def human_readable_filesize(b):
607
+ """
608
+ Returns filesize in a human readable format.
609
+ """
610
+ units = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
611
+ value, u = Common.split_filesize(b)
612
+ return "{:.1f} {}".format(value, units[u])
613
+
614
+ @staticmethod
615
+ def parse_human_readable_filesize(size_str):
616
+ """
617
+ Parse a human readable filesize such as "100mb", "1 GiB" or "512" into
618
+ an integer number of bytes. Uses base 1024, matching the KiB/MiB/GiB
619
+ units produced by human_readable_filesize(). A bare number is treated
620
+ as bytes. Raises ValueError if the string cannot be parsed.
621
+ """
622
+ match = re.match(r"^\s*([0-9]+(?:\.[0-9]+)?)\s*([a-zA-Z]*)\s*$", str(size_str))
623
+ if not match:
624
+ raise ValueError(f"Invalid filesize: {size_str}")
625
+
626
+ value = float(match.group(1))
627
+ unit = match.group(2).lower()
628
+ multipliers = {
629
+ "": 1,
630
+ "b": 1,
631
+ "k": 1024,
632
+ "kb": 1024,
633
+ "kib": 1024,
634
+ "m": 1024**2,
635
+ "mb": 1024**2,
636
+ "mib": 1024**2,
637
+ "g": 1024**3,
638
+ "gb": 1024**3,
639
+ "gib": 1024**3,
640
+ "t": 1024**4,
641
+ "tb": 1024**4,
642
+ "tib": 1024**4,
643
+ }
644
+ if unit not in multipliers:
645
+ raise ValueError(f"Unknown filesize unit: {unit}")
646
+
647
+ return int(value * multipliers[unit])
550
648
 
551
649
  @staticmethod
552
650
  def format_seconds(seconds):
@@ -48,9 +48,15 @@ class Meek(object):
48
48
  self.obfs4proxy_file_path,
49
49
  self.snowflake_file_path,
50
50
  self.meek_client_file_path,
51
+ self.webtunnel_file_path,
51
52
  ) = get_tor_paths()
52
53
 
53
54
  self.meek_proxies = {}
55
+ # The moat reflector, reached via domain fronting, that relays
56
+ # HTTPS requests to bridges.torproject.org. This is distinct from
57
+ # the CDN77 meek used as a built-in bridge, which only relays
58
+ # traffic to a Tor bridge and cannot serve the Moat API.
59
+ # https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/f6c72cec392f215a77a2aaed626bfcc53e030327
54
60
  self.meek_url = "https://1723079976.rsc.cdn77.org/"
55
61
  self.meek_front = "www.phpmyadmin.net"
56
62
  self.meek_env = {
@@ -118,7 +124,7 @@ class Meek(object):
118
124
  for line in iter(self.meek_proc.stdout.readline, b""):
119
125
  if "CMETHOD meek socks5" in line:
120
126
  self.meek_host = line.split(" ")[3].split(":")[0]
121
- self.meek_port = line.split(" ")[3].split(":")[1]
127
+ self.meek_port = line.split(" ")[3].split(":")[1].strip()
122
128
  self.common.log(
123
129
  "Meek",
124
130
  "start",
@@ -43,7 +43,7 @@ class ModeSettings:
43
43
  "persistent": {
44
44
  "mode": None,
45
45
  "enabled": False,
46
- "autostart_on_launch": False
46
+ "autostart_on_launch": False,
47
47
  },
48
48
  "general": {
49
49
  "title": None,
@@ -63,13 +63,24 @@ class ModeSettings:
63
63
  "webhook_url": None,
64
64
  "disable_text": False,
65
65
  "disable_files": False,
66
+ # Maximum size of a single upload request, in bytes. 0 means no
67
+ # limit.
68
+ "max_upload_size": 0,
69
+ # If True, apply max_upload_size to the service's entire
70
+ # lifespan: once total_upload_size reaches max_upload_size, no
71
+ # further uploads are accepted.
72
+ "max_upload_size_total": False,
73
+ # Running total of bytes received by the service, persisted so
74
+ # the lifespan limit survives restarts of a persistent service.
75
+ "total_upload_size": 0,
66
76
  },
67
77
  "website": {
68
78
  "disable_csp": False,
69
79
  "custom_csp": None,
70
80
  "log_filenames": False,
71
- "filenames": []
81
+ "filenames": [],
72
82
  },
83
+ "download": {"data_dir": self.build_default_receive_data_dir(), "poll": 0},
73
84
  "chat": {},
74
85
  }
75
86
  self._settings = {}
@@ -159,6 +159,7 @@ class Onion(object):
159
159
  self.obfs4proxy_file_path,
160
160
  self.snowflake_file_path,
161
161
  self.meek_client_file_path,
162
+ self.webtunnel_file_path,
162
163
  ) = get_tor_paths()
163
164
 
164
165
  # The tor process
@@ -190,6 +191,23 @@ class Onion(object):
190
191
  s = key_b32.decode("utf-8")
191
192
  return s
192
193
 
194
+ def private_key_to_b64(self, encoded_key):
195
+ """
196
+ Converts a base32 encoded string back to a base64 encoded string.
197
+
198
+ This is used for adding a Private Key to an onion when operating in
199
+ client mode (such as when downloading from another OnionShare)
200
+ """
201
+ # Decode the base32 string into bytes
202
+ encoded_key_b32 = encoded_key.encode("utf-8")
203
+ decoded_bytes = base64.b32decode(encoded_key_b32 + b"====")
204
+
205
+ # Convert the decoded bytes to base64
206
+ base64_key = base64.b64encode(decoded_bytes)
207
+
208
+ # Convert to string and return
209
+ return base64_key.decode("utf-8")
210
+
193
211
  def connect(
194
212
  self,
195
213
  custom_settings=None,
@@ -316,6 +334,9 @@ class Onion(object):
316
334
  torrc_template = torrc_template.replace(
317
335
  "{{snowflake_path}}", str(self.snowflake_file_path)
318
336
  )
337
+ torrc_template = torrc_template.replace(
338
+ "{{webtunnel_path}}", str(self.webtunnel_file_path)
339
+ )
319
340
 
320
341
  with open(self.tor_torrc, "w") as f:
321
342
  self.common.log("Onion", "connect", "Writing torrc template file")
@@ -356,10 +377,10 @@ class Onion(object):
356
377
  )
357
378
  ) as o:
358
379
  f.write(o.read())
359
- elif builtin_bridge_type == "meek-azure":
380
+ elif builtin_bridge_type == "meek":
360
381
  with open(
361
382
  self.common.get_resource_path(
362
- "torrc_template-meek_lite_azure"
383
+ "torrc_template-meek"
363
384
  )
364
385
  ) as o:
365
386
  f.write(o.read())
@@ -370,6 +391,18 @@ class Onion(object):
370
391
  )
371
392
  ) as o:
372
393
  f.write(o.read())
394
+ elif builtin_bridge_type == "webtunnel":
395
+ # There are no built-in WebTunnel bridges:
396
+ # BridgeDB only distributes WebTunnel bridges
397
+ # via the Moat API. Requesting them here would
398
+ # result in a torrc with UseBridges 1 and no
399
+ # Bridge lines, which tor refuses to start.
400
+ raise TorErrorInvalidSetting(
401
+ "There are no built-in WebTunnel bridges. "
402
+ "Request WebTunnel bridges from BridgeDB "
403
+ "using the 'Request a bridge' option, or "
404
+ "enter them as custom bridge lines."
405
+ )
373
406
  self.common.log(
374
407
  "Onion",
375
408
  "connect",
@@ -417,7 +450,11 @@ class Onion(object):
417
450
 
418
451
  return_code = self.tor_proc.poll()
419
452
  if return_code != None:
420
- self.common.log("Onion", "connect", f"tor process has terminated early: {return_code}")
453
+ self.common.log(
454
+ "Onion",
455
+ "connect",
456
+ f"tor process has terminated early: {return_code}",
457
+ )
421
458
 
422
459
  # Connect to the controller
423
460
  self.common.log("Onion", "connect", "authenticating to tor controller")
@@ -679,12 +716,45 @@ class Onion(object):
679
716
  else:
680
717
  return False
681
718
 
682
- def start_onion_service(self, mode, mode_settings, port, await_publication):
719
+ def add_onion_client_auth(self, service_id, private_key, key_type="x25519"):
720
+ """
721
+ Implemented from scratch here because it never made it into Stem maint branch
722
+ """
723
+ if self.supports_v3_onions and self.supports_ephemeral:
724
+ private_key64 = self.private_key_to_b64(private_key)
725
+ request = f"ONION_CLIENT_AUTH_ADD {service_id} {key_type}:{private_key64}"
726
+ response = self.c.msg(request)
727
+ self.common.log("Onion", "add_onion_client_auth", response)
728
+ return response
729
+ else:
730
+ raise TorTooOldEphemeral()
731
+
732
+ def remove_onion_client_auth(self, service_id):
733
+ """
734
+ Implemented from scratch here because it never made it into Stem maint branch
735
+ """
736
+ if self.supports_v3_onions and self.supports_ephemeral:
737
+ request = f"ONION_CLIENT_AUTH_REMOVE {service_id}"
738
+ response = self.c.msg(request)
739
+ self.common.log("Onion", "remove_onion_client_auth", response)
740
+ return response
741
+ else:
742
+ raise TorTooOldEphemeral()
743
+
744
+ def start_onion_service(
745
+ self, mode, mode_settings, port, await_publication, unix_socket=None
746
+ ):
683
747
  """
684
748
  Start a onion service on port 80, pointing to the given port, and
685
749
  return the onion hostname.
686
750
  """
687
- self.common.log("Onion", "start_onion_service", f"port={port}")
751
+ if unix_socket:
752
+ target = f"unix:{unix_socket}"
753
+ else:
754
+ target = port
755
+ self.common.log(
756
+ "Onion", "start_onion_service", f"port={port}, target={target}"
757
+ )
688
758
 
689
759
  if not self.supports_ephemeral:
690
760
  print(
@@ -736,7 +806,7 @@ class Onion(object):
736
806
  try:
737
807
  if not self.supports_stealth:
738
808
  res = self.c.create_ephemeral_hidden_service(
739
- {80: port},
809
+ {80: target},
740
810
  await_publication=await_publication,
741
811
  basic_auth=None,
742
812
  key_type=key_type,
@@ -744,7 +814,7 @@ class Onion(object):
744
814
  )
745
815
  else:
746
816
  res = self.c.create_ephemeral_hidden_service(
747
- {80: port},
817
+ {80: target},
748
818
  await_publication=await_publication,
749
819
  basic_auth=None,
750
820
  key_type=key_type,