onionshare_cli 2.3__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 (44) hide show
  1. onionshare_cli-2.3/PKG-INFO +34 -0
  2. onionshare_cli-2.3/onionshare_cli/__init__.py +522 -0
  3. onionshare_cli-2.3/onionshare_cli/common.py +296 -0
  4. onionshare_cli-2.3/onionshare_cli/mode_settings.py +146 -0
  5. onionshare_cli-2.3/onionshare_cli/onion.py +845 -0
  6. onionshare_cli-2.3/onionshare_cli/onionshare.py +110 -0
  7. onionshare_cli-2.3/onionshare_cli/resources/static/css/style.css +393 -0
  8. onionshare_cli-2.3/onionshare_cli/resources/static/img/ajax.gif +0 -0
  9. onionshare_cli-2.3/onionshare_cli/resources/static/img/favicon.ico +0 -0
  10. onionshare_cli-2.3/onionshare_cli/resources/static/img/logo.png +0 -0
  11. onionshare_cli-2.3/onionshare_cli/resources/static/img/logo_large.png +0 -0
  12. onionshare_cli-2.3/onionshare_cli/resources/static/img/web_file.png +0 -0
  13. onionshare_cli-2.3/onionshare_cli/resources/static/img/web_folder.png +0 -0
  14. onionshare_cli-2.3/onionshare_cli/resources/static/js/chat.js +173 -0
  15. onionshare_cli-2.3/onionshare_cli/resources/static/js/jquery-3.5.1.min.js +2 -0
  16. onionshare_cli-2.3/onionshare_cli/resources/static/js/receive.js +130 -0
  17. onionshare_cli-2.3/onionshare_cli/resources/static/js/send.js +75 -0
  18. onionshare_cli-2.3/onionshare_cli/resources/static/js/socket.io.min.js +3 -0
  19. onionshare_cli-2.3/onionshare_cli/resources/templates/401.html +21 -0
  20. onionshare_cli-2.3/onionshare_cli/resources/templates/403.html +21 -0
  21. onionshare_cli-2.3/onionshare_cli/resources/templates/404.html +21 -0
  22. onionshare_cli-2.3/onionshare_cli/resources/templates/405.html +21 -0
  23. onionshare_cli-2.3/onionshare_cli/resources/templates/chat.html +46 -0
  24. onionshare_cli-2.3/onionshare_cli/resources/templates/denied.html +14 -0
  25. onionshare_cli-2.3/onionshare_cli/resources/templates/listing.html +59 -0
  26. onionshare_cli-2.3/onionshare_cli/resources/templates/receive.html +46 -0
  27. onionshare_cli-2.3/onionshare_cli/resources/templates/send.html +69 -0
  28. onionshare_cli-2.3/onionshare_cli/resources/templates/thankyou.html +27 -0
  29. onionshare_cli-2.3/onionshare_cli/resources/torrc_template +8 -0
  30. onionshare_cli-2.3/onionshare_cli/resources/torrc_template-meek_lite_amazon +2 -0
  31. onionshare_cli-2.3/onionshare_cli/resources/torrc_template-meek_lite_azure +2 -0
  32. onionshare_cli-2.3/onionshare_cli/resources/torrc_template-obfs4 +27 -0
  33. onionshare_cli-2.3/onionshare_cli/resources/version.txt +1 -0
  34. onionshare_cli-2.3/onionshare_cli/resources/wordlist.txt +7776 -0
  35. onionshare_cli-2.3/onionshare_cli/settings.py +201 -0
  36. onionshare_cli-2.3/onionshare_cli/web/__init__.py +21 -0
  37. onionshare_cli-2.3/onionshare_cli/web/chat_mode.py +169 -0
  38. onionshare_cli-2.3/onionshare_cli/web/receive_mode.py +488 -0
  39. onionshare_cli-2.3/onionshare_cli/web/send_base_mode.py +338 -0
  40. onionshare_cli-2.3/onionshare_cli/web/share_mode.py +406 -0
  41. onionshare_cli-2.3/onionshare_cli/web/web.py +424 -0
  42. onionshare_cli-2.3/onionshare_cli/web/website_mode.py +123 -0
  43. onionshare_cli-2.3/pyproject.toml +43 -0
  44. onionshare_cli-2.3/setup.py +51 -0
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.1
2
+ Name: onionshare-cli
3
+ Version: 2.3
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+
6
+ Author: Micah Lee
7
+ Author-email: micah@micahflee.com
8
+ Requires-Python: >=3.6,<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
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.6
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Topic :: Communications :: File Sharing
21
+ Classifier: Topic :: Security :: Cryptography
22
+ Requires-Dist: click
23
+ Requires-Dist: eventlet
24
+ Requires-Dist: flask
25
+ Requires-Dist: flask-httpauth
26
+ Requires-Dist: flask-socketio
27
+ Requires-Dist: psutil
28
+ Requires-Dist: pycryptodome
29
+ Requires-Dist: pysocks
30
+ Requires-Dist: requests
31
+ Requires-Dist: setuptools
32
+ Requires-Dist: stem
33
+ Requires-Dist: unidecode
34
+ Requires-Dist: urllib3
@@ -0,0 +1,522 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ OnionShare | https://onionshare.org/
4
+
5
+ Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com>
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ """
20
+
21
+ import os, sys, time, argparse, threading
22
+ from datetime import datetime
23
+ from datetime import timedelta
24
+
25
+ from .common import Common, CannotFindTor
26
+ from .web import Web
27
+ from .onion import *
28
+ from .onionshare import OnionShare
29
+ from .mode_settings import ModeSettings
30
+
31
+
32
+ def build_url(mode_settings, app, web):
33
+ # Build the URL
34
+ if mode_settings.get("general", "public"):
35
+ return f"http://{app.onion_host}"
36
+ else:
37
+ return f"http://onionshare:{web.password}@{app.onion_host}"
38
+
39
+
40
+ def main(cwd=None):
41
+ """
42
+ The main() function implements all of the logic that the command-line version of
43
+ onionshare uses.
44
+ """
45
+ common = Common()
46
+
47
+ # Display OnionShare banner
48
+ print(f"OnionShare {common.version} | https://onionshare.org/")
49
+ reset = "\033[0m"
50
+ purple = "\33[95m"
51
+ print(purple)
52
+ print(" @@@@@@@@@ ")
53
+ print(" @@@@@@@@@@@@@@@@@@@ ")
54
+ print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ")
55
+ print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ")
56
+ print(
57
+ " @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ___ _ "
58
+ )
59
+ print(
60
+ " @@@@@@ @@@@@@@@@@@@@ / _ \\ (_) "
61
+ )
62
+ print(
63
+ " @@@@ @ @@@@@@@@@@@ | | | |_ __ _ ___ _ __ "
64
+ )
65
+ print(
66
+ " @@@@@@@@ @@@@@@@@@@ | | | | '_ \\| |/ _ \\| '_ \\ "
67
+ )
68
+ print(
69
+ " @@@@@@@@@@@@ @@@@@@@@@@ \\ \\_/ / | | | | (_) | | | | "
70
+ )
71
+ print(
72
+ " @@@@@@@@@@@@@@@@ @@@@@@@@@ \\___/|_| |_|_|\\___/|_| |_| "
73
+ )
74
+ print(
75
+ " @@@@@@@@@ @@@@@@@@@@@@@@@@ _____ _ "
76
+ )
77
+ print(
78
+ " @@@@@@@@@@ @@@@@@@@@@@@ / ___| | "
79
+ )
80
+ print(
81
+ " @@@@@@@@@@ @@@@@@@@ \\ `--.| |__ __ _ _ __ ___ "
82
+ )
83
+ print(
84
+ " @@@@@@@@@@@ @ @@@@ `--. \\ '_ \\ / _` | '__/ _ \\"
85
+ )
86
+ print(
87
+ " @@@@@@@@@@@@@ @@@@@@ /\\__/ / | | | (_| | | | __/"
88
+ )
89
+ print(
90
+ " @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ \\____/|_| |_|\\__,_|_| \\___|"
91
+ )
92
+ print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ")
93
+ print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ")
94
+ print(" @@@@@@@@@@@@@@@@@@@ ")
95
+ print(" @@@@@@@@@ ")
96
+ print(reset)
97
+
98
+ # OnionShare CLI in OSX needs to change current working directory (#132)
99
+ if common.platform == "Darwin":
100
+ if cwd:
101
+ os.chdir(cwd)
102
+
103
+ # Parse arguments
104
+ parser = argparse.ArgumentParser(
105
+ formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=28)
106
+ )
107
+ # Select modes
108
+ parser.add_argument(
109
+ "--receive", action="store_true", dest="receive", help="Receive files"
110
+ )
111
+ parser.add_argument(
112
+ "--website", action="store_true", dest="website", help="Publish website"
113
+ )
114
+ parser.add_argument(
115
+ "--chat", action="store_true", dest="chat", help="Start chat server"
116
+ )
117
+ # Tor connection-related args
118
+ parser.add_argument(
119
+ "--local-only",
120
+ action="store_true",
121
+ dest="local_only",
122
+ default=False,
123
+ help="Don't use Tor (only for development)",
124
+ )
125
+ parser.add_argument(
126
+ "--connect-timeout",
127
+ metavar="SECONDS",
128
+ dest="connect_timeout",
129
+ default=120,
130
+ help="Give up connecting to Tor after a given amount of seconds (default: 120)",
131
+ )
132
+ parser.add_argument(
133
+ "--config",
134
+ metavar="FILENAME",
135
+ default=None,
136
+ help="Filename of custom global settings",
137
+ )
138
+ # Persistent file
139
+ parser.add_argument(
140
+ "--persistent",
141
+ metavar="FILENAME",
142
+ default=None,
143
+ help="Filename of persistent session",
144
+ )
145
+ # General args
146
+ parser.add_argument(
147
+ "--public",
148
+ action="store_true",
149
+ dest="public",
150
+ default=False,
151
+ help="Don't use a password",
152
+ )
153
+ parser.add_argument(
154
+ "--auto-start-timer",
155
+ metavar="SECONDS",
156
+ dest="autostart_timer",
157
+ default=0,
158
+ help="Start onion service at scheduled time (N seconds from now)",
159
+ )
160
+ parser.add_argument(
161
+ "--auto-stop-timer",
162
+ metavar="SECONDS",
163
+ dest="autostop_timer",
164
+ default=0,
165
+ help="Stop onion service at schedule time (N seconds from now)",
166
+ )
167
+ parser.add_argument(
168
+ "--legacy",
169
+ action="store_true",
170
+ dest="legacy",
171
+ default=False,
172
+ help="Use legacy address (v2 onion service, not recommended)",
173
+ )
174
+ parser.add_argument(
175
+ "--client-auth",
176
+ action="store_true",
177
+ dest="client_auth",
178
+ default=False,
179
+ help="Use client authorization (requires --legacy)",
180
+ )
181
+ # Share args
182
+ parser.add_argument(
183
+ "--no-autostop-sharing",
184
+ action="store_true",
185
+ dest="no_autostop_sharing",
186
+ default=False,
187
+ help="Share files: Continue sharing after files have been sent (default is to stop sharing)",
188
+ )
189
+ # Receive args
190
+ parser.add_argument(
191
+ "--data-dir",
192
+ metavar="data_dir",
193
+ default=None,
194
+ help="Receive files: Save files received to this directory",
195
+ )
196
+ # Website args
197
+ parser.add_argument(
198
+ "--disable_csp",
199
+ action="store_true",
200
+ dest="disable_csp",
201
+ default=False,
202
+ help="Publish website: Disable Content Security Policy header (allows your website to use third-party resources)",
203
+ )
204
+ # Other
205
+ parser.add_argument(
206
+ "-v",
207
+ "--verbose",
208
+ action="store_true",
209
+ dest="verbose",
210
+ help="Log OnionShare errors to stdout, and web errors to disk",
211
+ )
212
+ parser.add_argument(
213
+ "filename",
214
+ metavar="filename",
215
+ nargs="*",
216
+ help="List of files or folders to share",
217
+ )
218
+ args = parser.parse_args()
219
+
220
+ filenames = args.filename
221
+ for i in range(len(filenames)):
222
+ filenames[i] = os.path.abspath(filenames[i])
223
+
224
+ receive = bool(args.receive)
225
+ website = bool(args.website)
226
+ chat = bool(args.chat)
227
+ local_only = bool(args.local_only)
228
+ connect_timeout = int(args.connect_timeout)
229
+ config_filename = args.config
230
+ persistent_filename = args.persistent
231
+ public = bool(args.public)
232
+ autostart_timer = int(args.autostart_timer)
233
+ autostop_timer = int(args.autostop_timer)
234
+ legacy = bool(args.legacy)
235
+ client_auth = bool(args.client_auth)
236
+ autostop_sharing = not bool(args.no_autostop_sharing)
237
+ data_dir = args.data_dir
238
+ disable_csp = bool(args.disable_csp)
239
+ verbose = bool(args.verbose)
240
+
241
+ if receive:
242
+ mode = "receive"
243
+ elif website:
244
+ mode = "website"
245
+ elif chat:
246
+ mode = "chat"
247
+ else:
248
+ mode = "share"
249
+
250
+ # Verbose mode?
251
+ common.verbose = verbose
252
+
253
+ # client_auth can only be set if legacy is also set
254
+ if client_auth and not legacy:
255
+ print(
256
+ "Client authentication (--client-auth) is only supported with with legacy onion services (--legacy)"
257
+ )
258
+ sys.exit()
259
+
260
+ # Re-load settings, if a custom config was passed in
261
+ if config_filename:
262
+ common.load_settings(config_filename)
263
+ else:
264
+ common.load_settings()
265
+
266
+ # Mode settings
267
+ if persistent_filename:
268
+ mode_settings = ModeSettings(common, persistent_filename)
269
+ mode_settings.set("persistent", "enabled", True)
270
+ else:
271
+ mode_settings = ModeSettings(common)
272
+
273
+ if mode_settings.just_created:
274
+ # This means the mode settings were just created, not loaded from disk
275
+ mode_settings.set("general", "public", public)
276
+ mode_settings.set("general", "autostart_timer", autostart_timer)
277
+ mode_settings.set("general", "autostop_timer", autostop_timer)
278
+ mode_settings.set("general", "legacy", legacy)
279
+ mode_settings.set("general", "client_auth", client_auth)
280
+ if mode == "share":
281
+ mode_settings.set("share", "autostop_sharing", autostop_sharing)
282
+ if mode == "receive":
283
+ if data_dir:
284
+ mode_settings.set("receive", "data_dir", data_dir)
285
+ if mode == "website":
286
+ mode_settings.set("website", "disable_csp", disable_csp)
287
+ else:
288
+ # See what the persistent mode was
289
+ mode = mode_settings.get("persistent", "mode")
290
+
291
+ # In share and website mode, you must supply a list of filenames
292
+ if mode == "share" or mode == "website":
293
+ # Unless you passed in a persistent filename, in which case get the filenames from
294
+ # the mode settings
295
+ if persistent_filename and not mode_settings.just_created:
296
+ filenames = mode_settings.get(mode, "filenames")
297
+
298
+ else:
299
+ # Make sure filenames given if not using receiver mode
300
+ if len(filenames) == 0:
301
+ if persistent_filename:
302
+ mode_settings.delete()
303
+
304
+ parser.print_help()
305
+ sys.exit()
306
+
307
+ # Validate filenames
308
+ valid = True
309
+ for filename in filenames:
310
+ if not os.path.isfile(filename) and not os.path.isdir(filename):
311
+ print(f"{filename} is not a valid file.")
312
+ valid = False
313
+ if not os.access(filename, os.R_OK):
314
+ print(f"{filename} is not a readable file.")
315
+ valid = False
316
+ if not valid:
317
+ sys.exit()
318
+
319
+ # Create the Web object
320
+ web = Web(common, False, mode_settings, mode)
321
+
322
+ # Start the Onion object
323
+ try:
324
+ onion = Onion(common, use_tmp_dir=True)
325
+ except CannotFindTor:
326
+ print("You must install tor to use OnionShare from the command line")
327
+ if common.platform == "Darwin":
328
+ print("In macOS, you can do this with Homebrew (https://brew.sh):")
329
+ print(" brew install tor")
330
+ sys.exit()
331
+
332
+ try:
333
+ onion.connect(
334
+ custom_settings=False,
335
+ config=config_filename,
336
+ connect_timeout=connect_timeout,
337
+ local_only=local_only,
338
+ )
339
+ except KeyboardInterrupt:
340
+ print("")
341
+ sys.exit()
342
+ except Exception as e:
343
+ sys.exit()
344
+
345
+ # Start the onionshare app
346
+ try:
347
+ common.settings.load()
348
+ if not mode_settings.get("general", "public"):
349
+ web.generate_password(mode_settings.get("onion", "password"))
350
+ else:
351
+ web.password = None
352
+ app = OnionShare(common, onion, local_only, autostop_timer)
353
+ app.choose_port()
354
+
355
+ # Delay the startup if a startup timer was set
356
+ if autostart_timer > 0:
357
+ # Can't set a schedule that is later than the auto-stop timer
358
+ if autostop_timer > 0 and autostop_timer < autostart_timer:
359
+ print(
360
+ "The auto-stop time can't be the same or earlier than the auto-start time. Please update it to start sharing."
361
+ )
362
+ sys.exit()
363
+
364
+ app.start_onion_service(mode, mode_settings, False, True)
365
+ url = build_url(mode_settings, app, web)
366
+ schedule = datetime.now() + timedelta(seconds=autostart_timer)
367
+ if mode == "receive":
368
+ print(
369
+ f"Files sent to you appear in this folder: {mode_settings.get('receive', 'data_dir')}"
370
+ )
371
+ print("")
372
+ print(
373
+ "Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing."
374
+ )
375
+ print("")
376
+ if mode_settings.get("general", "client_auth"):
377
+ print(
378
+ f"Give this address and HidServAuth lineto your sender, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}"
379
+ )
380
+ print(app.auth_string)
381
+ else:
382
+ print(
383
+ f"Give this address to your sender, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}"
384
+ )
385
+ else:
386
+ if mode_settings.get("general", "client_auth"):
387
+ print(
388
+ f"Give this address and HidServAuth line to your recipient, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}"
389
+ )
390
+ print(app.auth_string)
391
+ else:
392
+ print(
393
+ f"Give this address to your recipient, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}"
394
+ )
395
+ print(url)
396
+ print("")
397
+ print("Waiting for the scheduled time before starting...")
398
+ app.onion.cleanup(False)
399
+ time.sleep(autostart_timer)
400
+ app.start_onion_service(mode, mode_settings)
401
+ else:
402
+ app.start_onion_service(mode, mode_settings)
403
+ except KeyboardInterrupt:
404
+ print("")
405
+ sys.exit()
406
+ except (TorTooOld, TorErrorProtocolError) as e:
407
+ print("")
408
+ print(e.args[0])
409
+ sys.exit()
410
+
411
+ if mode == "website":
412
+ # Prepare files to share
413
+ try:
414
+ web.website_mode.set_file_info(filenames)
415
+ except OSError as e:
416
+ print(e.strerror)
417
+ sys.exit(1)
418
+
419
+ if mode == "share":
420
+ # Prepare files to share
421
+ print("Compressing files.")
422
+ try:
423
+ web.share_mode.set_file_info(filenames)
424
+ app.cleanup_filenames += web.share_mode.cleanup_filenames
425
+ except OSError as e:
426
+ print(e.strerror)
427
+ sys.exit(1)
428
+
429
+ # Warn about sending large files over Tor
430
+ if web.share_mode.download_filesize >= 157286400: # 150mb
431
+ print("")
432
+ print("Warning: Sending a large share could take hours")
433
+ print("")
434
+
435
+ # Start OnionShare http service in new thread
436
+ t = threading.Thread(target=web.start, args=(app.port,))
437
+ t.daemon = True
438
+ t.start()
439
+
440
+ try: # Trap Ctrl-C
441
+ # Wait for web.generate_password() to finish running
442
+ time.sleep(0.2)
443
+
444
+ # start auto-stop timer thread
445
+ if app.autostop_timer > 0:
446
+ app.autostop_timer_thread.start()
447
+
448
+ # Save the web password if we are using a persistent private key
449
+ if mode_settings.get("persistent", "enabled"):
450
+ if not mode_settings.get("onion", "password"):
451
+ mode_settings.set("onion", "password", web.password)
452
+ # mode_settings.save()
453
+
454
+ # Build the URL
455
+ url = build_url(mode_settings, app, web)
456
+
457
+ print("")
458
+ if autostart_timer > 0:
459
+ print("Server started")
460
+ else:
461
+ if mode == "receive":
462
+ print(
463
+ f"Files sent to you appear in this folder: {mode_settings.get('receive', 'data_dir')}"
464
+ )
465
+ print("")
466
+ print(
467
+ "Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing."
468
+ )
469
+ print("")
470
+
471
+ if mode_settings.get("general", "client_auth"):
472
+ print("Give this address and HidServAuth to the sender:")
473
+ print(url)
474
+ print(app.auth_string)
475
+ else:
476
+ print("Give this address to the sender:")
477
+ print(url)
478
+ else:
479
+ if mode_settings.get("general", "client_auth"):
480
+ print("Give this address and HidServAuth line to the recipient:")
481
+ print(url)
482
+ print(app.auth_string)
483
+ else:
484
+ print("Give this address to the recipient:")
485
+ print(url)
486
+ print("")
487
+ print("Press Ctrl+C to stop the server")
488
+
489
+ # Wait for app to close
490
+ while t.is_alive():
491
+ if app.autostop_timer > 0:
492
+ # if the auto-stop timer was set and has run out, stop the server
493
+ if not app.autostop_timer_thread.is_alive():
494
+ if mode == "share" or (mode == "website"):
495
+ # If there were no attempts to download the share, or all downloads are done, we can stop
496
+ if web.share_mode.cur_history_id == 0 or web.done:
497
+ print("Stopped because auto-stop timer ran out")
498
+ web.stop(app.port)
499
+ break
500
+ if mode == "receive":
501
+ if (
502
+ web.receive_mode.cur_history_id == 0
503
+ or not web.receive_mode.uploads_in_progress
504
+ ):
505
+ print("Stopped because auto-stop timer ran out")
506
+ web.stop(app.port)
507
+ break
508
+ else:
509
+ web.receive_mode.can_upload = False
510
+ # Allow KeyboardInterrupt exception to be handled with threads
511
+ # https://stackoverflow.com/questions/3788208/python-threading-ignores-keyboardinterrupt-exception
512
+ time.sleep(0.2)
513
+ except KeyboardInterrupt:
514
+ web.stop(app.port)
515
+ finally:
516
+ # Shutdown
517
+ app.cleanup()
518
+ onion.cleanup()
519
+
520
+
521
+ if __name__ == "__main__":
522
+ main()