From 18ac830a9e63d44bb5c1bbbbe85e01235b6b14e3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 5 Mar 2018 07:45:10 -0800 Subject: [PATCH 01/36] Add command line flag for receive mode --- onionshare/__init__.py | 13 ++++++++++--- onionshare/onionshare.py | 5 ++++- onionshare_gui/__init__.py | 2 +- share/locale/en.json | 2 ++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 76d2b601..ad1d300b 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -44,9 +44,10 @@ def main(cwd=None): parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open")) parser.add_argument('--shutdown-timeout', metavar='', dest='shutdown_timeout', default=0, help=strings._("help_shutdown_timeout")) parser.add_argument('--stealth', action='store_true', dest='stealth', help=strings._("help_stealth")) - parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug")) + parser.add_argument('--receive', action='store_true', dest='receive', help=strings._("help_receive")) parser.add_argument('--config', metavar='config', default=False, help=strings._('help_config')) - parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename')) + parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug")) + parser.add_argument('filename', metavar='filename', nargs='*', help=strings._('help_filename')) args = parser.parse_args() filenames = args.filename @@ -58,8 +59,14 @@ def main(cwd=None): stay_open = bool(args.stay_open) shutdown_timeout = int(args.shutdown_timeout) stealth = bool(args.stealth) + receive = bool(args.receive) config = args.config + # Make sure filenames given if not using receiver mode + if not receive and len(filenames) == 0: + print(strings._('no_filenames')) + sys.exit() + # Debug mode? if debug: common.set_debug(debug) @@ -92,7 +99,7 @@ def main(cwd=None): # Start the onionshare app try: - app = OnionShare(onion, local_only, stay_open, shutdown_timeout) + app = OnionShare(onion, receive, local_only, stay_open, shutdown_timeout) app.set_stealth(stealth) app.start_onion_service() except KeyboardInterrupt: diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 85bfaf22..a8f2ceb0 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -27,7 +27,7 @@ class OnionShare(object): OnionShare is the main application class. Pass in options and run start_onion_service and it will do the magic. """ - def __init__(self, onion, local_only=False, stay_open=False, shutdown_timeout=0): + def __init__(self, onion, receive, local_only=False, stay_open=False, shutdown_timeout=0): common.log('OnionShare', '__init__') # The Onion object @@ -37,6 +37,9 @@ class OnionShare(object): self.onion_host = None self.stealth = None + # Receiver mode + self.receive = receive + # files and dirs to delete on shutdown self.cleanup_filenames = [] diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 24e627bb..945487fc 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -105,7 +105,7 @@ def main(): # Start the OnionShare app web.set_stay_open(stay_open) - app = OnionShare(onion, local_only, stay_open, shutdown_timeout) + app = OnionShare(onion, False, local_only, stay_open, shutdown_timeout) # Launch the gui gui = OnionShareGui(onion, qtapp, app, filenames, config) diff --git a/share/locale/en.json b/share/locale/en.json index 09ead591..e72686b8 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -10,6 +10,7 @@ "ctrlc_to_stop": "Press Ctrl-C to stop server", "not_a_file": "{0:s} is not a valid file.", "not_a_readable_file": "{0:s} is not a readable file.", + "no_filenames": "You must specify a list of files to share.", "no_available_port": "Could not start the Onion service as there was no available port.", "download_page_loaded": "Download page loaded", "other_page_loaded": "Address loaded", @@ -31,6 +32,7 @@ "help_shutdown_timeout": "Shut down the onion service after N seconds", "help_transparent_torification": "My system is transparently torified", "help_stealth": "Create stealth onion service (advanced)", + "help_receive": "Receive files instead of sending them", "help_debug": "Log application errors to stdout, and log web errors to disk", "help_filename": "List of files or folders to share", "help_config": "Path to a custom JSON config file (optional)", From 383ccb94fc047d81bddcefccd4511f11a7ebd61a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 5 Mar 2018 07:52:51 -0800 Subject: [PATCH 02/36] Add downloads_dir to settings, make it default to ~/Downloads --- onionshare/__init__.py | 2 +- onionshare/settings.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index ad1d300b..fe7f72dd 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -86,6 +86,7 @@ def main(cwd=None): settings = Settings(config) + settings.load() # Start the Onion object onion = Onion() @@ -122,7 +123,6 @@ def main(cwd=None): print('') # Start OnionShare http service in new thread - settings.load() t = threading.Thread(target=web.start, args=(app.port, app.stay_open, settings.get('slug'))) t.daemon = True t.start() diff --git a/onionshare/settings.py b/onionshare/settings.py index 545915e8..5ec2d2ae 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -70,7 +70,8 @@ class Settings(object): 'save_private_key': False, 'private_key': '', 'slug': '', - 'hidservauth_string': '' + 'hidservauth_string': '', + 'downloads_dir': self.build_default_downloads_dir() } self._settings = {} self.fill_in_defaults() @@ -97,6 +98,14 @@ class Settings(object): else: return os.path.expanduser('~/.config/onionshare/onionshare.json') + def build_default_downloads_dir(self): + """ + Returns the path of the default Downloads directory for receive mode. + """ + # TODO: Test in Windows, though it looks like it should work + # https://docs.python.org/3/library/os.path.html#os.path.expanduser + return os.path.expanduser('~/Downloads') + def load(self): """ Load the settings from file. From cd1a1d9638962dbd289088277832f0e817e95b5b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 5 Mar 2018 08:48:04 -0800 Subject: [PATCH 03/36] Web needs to know about receive mode, not the OnionShare object --- onionshare/__init__.py | 6 +++++- onionshare/onionshare.py | 5 +---- onionshare/web.py | 13 +++++++++++++ onionshare_gui/__init__.py | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index fe7f72dd..f1252f12 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -67,6 +67,10 @@ def main(cwd=None): print(strings._('no_filenames')) sys.exit() + # Tell web if receive mode is enabled + if receive: + web.set_receive_mode() + # Debug mode? if debug: common.set_debug(debug) @@ -100,7 +104,7 @@ def main(cwd=None): # Start the onionshare app try: - app = OnionShare(onion, receive, local_only, stay_open, shutdown_timeout) + app = OnionShare(onion, local_only, stay_open, shutdown_timeout) app.set_stealth(stealth) app.start_onion_service() except KeyboardInterrupt: diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index a8f2ceb0..85bfaf22 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -27,7 +27,7 @@ class OnionShare(object): OnionShare is the main application class. Pass in options and run start_onion_service and it will do the magic. """ - def __init__(self, onion, receive, local_only=False, stay_open=False, shutdown_timeout=0): + def __init__(self, onion, local_only=False, stay_open=False, shutdown_timeout=0): common.log('OnionShare', '__init__') # The Onion object @@ -37,9 +37,6 @@ class OnionShare(object): self.onion_host = None self.stealth = None - # Receiver mode - self.receive = receive - # files and dirs to delete on shutdown self.cleanup_filenames = [] diff --git a/onionshare/web.py b/onionshare/web.py index d16ca251..c7d3fae6 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -175,6 +175,19 @@ def set_gui_mode(): gui_mode = True +# Are we using receive mode? +receive_mode = False + + +def set_receive_mode(): + """ + Tell the web service that we're running in GUI mode + """ + global receive_mode + receive_mode = True + print('receive mode enabled') + + def debug_mode(): """ Turn on debugging mode, which will log flask errors to a debug file. diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 945487fc..24e627bb 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -105,7 +105,7 @@ def main(): # Start the OnionShare app web.set_stay_open(stay_open) - app = OnionShare(onion, False, local_only, stay_open, shutdown_timeout) + app = OnionShare(onion, local_only, stay_open, shutdown_timeout) # Launch the gui gui = OnionShareGui(onion, qtapp, app, filenames, config) From 08957c5145be6504907760b078045cc42d16b49c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 5 Mar 2018 08:54:20 -0800 Subject: [PATCH 04/36] Fix settings test to account for new downloads_dir setting --- test/test_onionshare_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py index e50eee41..acf5cc6a 100644 --- a/test/test_onionshare_settings.py +++ b/test/test_onionshare_settings.py @@ -67,7 +67,8 @@ class TestSettings: 'save_private_key': False, 'private_key': '', 'slug': '', - 'hidservauth_string': '' + 'hidservauth_string': '', + 'downloads_dir': os.path.expanduser('~/Downloads') } def test_fill_in_defaults(self, settings_obj): From 0cec696055cfa76494369709abebad5839180496 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 5 Mar 2018 11:06:59 -0800 Subject: [PATCH 05/36] Refactor web.py to move all the web logic into the Web class, and refactor onionshare (cli) to work with it -- but onionshare_gui is currently broken --- onionshare/__init__.py | 13 +- onionshare/web.py | 670 +++++++++++++++++-------------------- onionshare_gui/__init__.py | 3 +- 3 files changed, 320 insertions(+), 366 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index f1252f12..8d914f9c 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -20,7 +20,8 @@ along with this program. If not, see . import os, sys, time, argparse, threading -from . import strings, common, web +from . import strings, common +from .web import Web from .onion import * from .onionshare import OnionShare from .settings import Settings @@ -67,14 +68,9 @@ def main(cwd=None): print(strings._('no_filenames')) sys.exit() - # Tell web if receive mode is enabled - if receive: - web.set_receive_mode() - # Debug mode? if debug: common.set_debug(debug) - web.debug_mode() # Validation valid = True @@ -88,10 +84,13 @@ def main(cwd=None): if not valid: sys.exit() - + # Load settings settings = Settings(config) settings.load() + # Create the Web object + web = Web(debug, stay_open, False, receive) + # Start the Onion object onion = Onion() try: diff --git a/onionshare/web.py b/onionshare/web.py index c7d3fae6..3e75ba27 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -37,424 +37,378 @@ from flask import ( from . import strings, common - -def _safe_select_jinja_autoescape(self, filename): - if filename is None: - return True - return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) - -# Starting in Flask 0.11, render_template_string autoescapes template variables -# by default. To prevent content injection through template variables in -# earlier versions of Flask, we force autoescaping in the Jinja2 template -# engine if we detect a Flask version with insecure default behavior. -if Version(flask_version) < Version('0.11'): - # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc - Flask.select_jinja_autoescape = _safe_select_jinja_autoescape - -app = Flask(__name__) - -# information about the file -file_info = [] -zip_filename = None -zip_filesize = None - -security_headers = [ - ('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; script-src \'unsafe-inline\'; img-src \'self\' data:;'), - ('X-Frame-Options', 'DENY'), - ('X-Xss-Protection', '1; mode=block'), - ('X-Content-Type-Options', 'nosniff'), - ('Referrer-Policy', 'no-referrer'), - ('Server', 'OnionShare') -] - - -def set_file_info(filenames, processed_size_callback=None): +class Web(object): """ - Using the list of filenames being shared, fill in details that the web - page will need to display. This includes zipping up the file in order to - get the zip file's name and size. + The Web object is the OnionShare web server, powered by flask """ - global file_info, zip_filename, zip_filesize + def __init__(self, debug, stay_open, gui_mode, receive_mode): + # The flask app + self.app = Flask(__name__) - # build file info list - file_info = {'files': [], 'dirs': []} - for filename in filenames: - info = { - 'filename': filename, - 'basename': os.path.basename(filename.rstrip('/')) - } - if os.path.isfile(filename): - info['size'] = os.path.getsize(filename) - info['size_human'] = common.human_readable_filesize(info['size']) - file_info['files'].append(info) - if os.path.isdir(filename): - info['size'] = common.dir_size(filename) - info['size_human'] = common.human_readable_filesize(info['size']) - file_info['dirs'].append(info) - file_info['files'] = sorted(file_info['files'], key=lambda k: k['basename']) - file_info['dirs'] = sorted(file_info['dirs'], key=lambda k: k['basename']) + # Debug mode? + if debug: + self.debug_mode() - # zip up the files and folders - z = common.ZipWriter(processed_size_callback=processed_size_callback) - for info in file_info['files']: - z.add_file(info['filename']) - for info in file_info['dirs']: - z.add_dir(info['filename']) - z.close() - zip_filename = z.zip_filename - zip_filesize = os.path.getsize(zip_filename) + # Stay open after the first download? + self.stay_open = False + + # Are we running in GUI mode? + self.gui_mode = False + + # Are we using receive mode? + self.receive_mode = False -REQUEST_LOAD = 0 -REQUEST_DOWNLOAD = 1 -REQUEST_PROGRESS = 2 -REQUEST_OTHER = 3 -REQUEST_CANCELED = 4 -REQUEST_RATE_LIMIT = 5 -q = queue.Queue() + # Starting in Flask 0.11, render_template_string autoescapes template variables + # by default. To prevent content injection through template variables in + # earlier versions of Flask, we force autoescaping in the Jinja2 template + # engine if we detect a Flask version with insecure default behavior. + if Version(flask_version) < Version('0.11'): + # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc + Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape + # Information about the file + self.file_info = [] + self.zip_filename = None + self.zip_filesize = None -def add_request(request_type, path, data=None): - """ - Add a request to the queue, to communicate with the GUI. - """ - global q - q.put({ - 'type': request_type, - 'path': path, - 'data': data - }) + self.security_headers = [ + ('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; script-src \'unsafe-inline\'; img-src \'self\' data:;'), + ('X-Frame-Options', 'DENY'), + ('X-Xss-Protection', '1; mode=block'), + ('X-Content-Type-Options', 'nosniff'), + ('Referrer-Policy', 'no-referrer'), + ('Server', 'OnionShare') + ] + self.REQUEST_LOAD = 0 + self.REQUEST_DOWNLOAD = 1 + self.REQUEST_PROGRESS = 2 + self.REQUEST_OTHER = 3 + self.REQUEST_CANCELED = 4 + self.REQUEST_RATE_LIMIT = 5 + self.q = queue.Queue() -# Load and base64 encode images to pass into templates -favicon_b64 = base64.b64encode(open(common.get_resource_path('images/favicon.ico'), 'rb').read()).decode() -logo_b64 = base64.b64encode(open(common.get_resource_path('images/logo.png'), 'rb').read()).decode() -folder_b64 = base64.b64encode(open(common.get_resource_path('images/web_folder.png'), 'rb').read()).decode() -file_b64 = base64.b64encode(open(common.get_resource_path('images/web_file.png'), 'rb').read()).decode() + # Load and base64 encode images to pass into templates + self.favicon_b64 = self.base64_image('favicon.ico') + self.logo_b64 = self.base64_image('logo.png') + self.folder_b64 = self.base64_image('web_folder.png') + self.file_b64 = self.base64_image('web_file.png') -slug = None + self.slug = None + self.download_count = 0 + self.error404_count = 0 -def generate_slug(persistent_slug=''): - global slug - if persistent_slug: - slug = persistent_slug - else: - slug = common.build_slug() + # If "Stop After First Download" is checked (stay_open == False), only allow + # one download at a time. + self.download_in_progress = False -download_count = 0 -error404_count = 0 + self.done = False -stay_open = False + # If the client closes the OnionShare window while a download is in progress, + # it should immediately stop serving the file. The client_cancel global is + # used to tell the download function that the client is canceling the download. + self.client_cancel = False + # shutting down the server only works within the context of flask, so the easiest way to do it is over http + self.shutdown_slug = common.random_string(16) -def set_stay_open(new_stay_open): - """ - Set stay_open variable. - """ - global stay_open - stay_open = new_stay_open + @self.app.route("/") + def index(slug_candidate): + """ + Render the template for the onionshare landing page. + """ + self.check_slug_candidate(slug_candidate) + self.add_request(self.REQUEST_LOAD, request.path) -def get_stay_open(): - """ - Get stay_open variable. - """ - return stay_open + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not self.stay_open and self.download_in_progress + if deny_download: + r = make_response(render_template_string( + open(common.get_resource_path('html/denied.html')).read(), + favicon_b64=self.favicon_b64 + )) + for header, value in self.security_headers: + r.headers.set(header, value) + return r + # If download is allowed to continue, serve download page + r = make_response(render_template_string( + open(common.get_resource_path('html/index.html')).read(), + favicon_b64=self.favicon_b64, + logo_b64=self.logo_b64, + folder_b64=self.folder_b64, + file_b64=self.file_b64, + slug=self.slug, + file_info=self.file_info, + filename=os.path.basename(self.zip_filename), + filesize=self.zip_filesize, + filesize_human=common.human_readable_filesize(self.zip_filesize))) + for header, value in self.security_headers: + r.headers.set(header, value) + return r -# Are we running in GUI mode? -gui_mode = False + @self.app.route("//download") + def download(slug_candidate): + """ + Download the zip file. + """ + self.check_slug_candidate(slug_candidate) + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not self.stay_open and self.download_in_progress + if deny_download: + r = make_response(render_template_string( + open(common.get_resource_path('html/denied.html')).read(), + favicon_b64=self.favicon_b64 + )) + for header,value in self.security_headers: + r.headers.set(header, value) + return r -def set_gui_mode(): - """ - Tell the web service that we're running in GUI mode - """ - global gui_mode - gui_mode = True + # each download has a unique id + download_id = self.download_count + self.download_count += 1 + # prepare some variables to use inside generate() function below + # which is outside of the request context + shutdown_func = request.environ.get('werkzeug.server.shutdown') + path = request.path -# Are we using receive mode? -receive_mode = False + # tell GUI the download started + self.add_request(self.REQUEST_DOWNLOAD, path, {'id': download_id}) + dirname = os.path.dirname(self.zip_filename) + basename = os.path.basename(self.zip_filename) -def set_receive_mode(): - """ - Tell the web service that we're running in GUI mode - """ - global receive_mode - receive_mode = True - print('receive mode enabled') + def generate(): + # The user hasn't canceled the download + self.client_cancel = False + # Starting a new download + if not self.stay_open: + self.download_in_progress = True -def debug_mode(): - """ - Turn on debugging mode, which will log flask errors to a debug file. - """ - temp_dir = tempfile.gettempdir() - log_handler = logging.FileHandler( - os.path.join(temp_dir, 'onionshare_server.log')) - log_handler.setLevel(logging.WARNING) - app.logger.addHandler(log_handler) + chunk_size = 102400 # 100kb + fp = open(self.zip_filename, 'rb') + self.done = False + canceled = False + while not self.done: + # The user has canceled the download, so stop serving the file + if self.client_cancel: + self.add_request(self.REQUEST_CANCELED, path, {'id': download_id}) + break -def check_slug_candidate(slug_candidate, slug_compare=None): - if not slug_compare: - slug_compare = slug - if not hmac.compare_digest(slug_compare, slug_candidate): - abort(404) + chunk = fp.read(chunk_size) + if chunk == b'': + self.done = True + else: + try: + yield chunk + # tell GUI the progress + downloaded_bytes = fp.tell() + percent = (1.0 * downloaded_bytes / self.zip_filesize) * 100 -# If "Stop After First Download" is checked (stay_open == False), only allow -# one download at a time. -download_in_progress = False + # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) + plat = common.get_platform() + if not self.gui_mode or plat == 'Linux' or plat == 'BSD': + sys.stdout.write( + "\r{0:s}, {1:.2f}% ".format(common.human_readable_filesize(downloaded_bytes), percent)) + sys.stdout.flush() -done = False + self.add_request(self.REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes}) + self.done = False + except: + # looks like the download was canceled + self.done = True + canceled = True -@app.route("/") -def index(slug_candidate): - """ - Render the template for the onionshare landing page. - """ - check_slug_candidate(slug_candidate) + # tell the GUI the download has canceled + self.add_request(self.REQUEST_CANCELED, path, {'id': download_id}) - add_request(REQUEST_LOAD, request.path) + fp.close() - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - global stay_open, download_in_progress - deny_download = not stay_open and download_in_progress - if deny_download: - r = make_response(render_template_string( - open(common.get_resource_path('html/denied.html')).read(), - favicon_b64=favicon_b64 - )) - for header, value in security_headers: - r.headers.set(header, value) - return r + if common.get_platform() != 'Darwin': + sys.stdout.write("\n") - # If download is allowed to continue, serve download page + # Download is finished + if not self.stay_open: + self.download_in_progress = False - r = make_response(render_template_string( - open(common.get_resource_path('html/index.html')).read(), - favicon_b64=favicon_b64, - logo_b64=logo_b64, - folder_b64=folder_b64, - file_b64=file_b64, - slug=slug, - file_info=file_info, - filename=os.path.basename(zip_filename), - filesize=zip_filesize, - filesize_human=common.human_readable_filesize(zip_filesize))) - for header, value in security_headers: - r.headers.set(header, value) - return r + # Close the server, if necessary + if not self.stay_open and not canceled: + print(strings._("closing_automatically")) + if shutdown_func is None: + raise RuntimeError('Not running with the Werkzeug Server') + shutdown_func() + r = Response(generate()) + r.headers.set('Content-Length', self.zip_filesize) + r.headers.set('Content-Disposition', 'attachment', filename=basename) + for header,value in self.security_headers: + r.headers.set(header, value) + # guess content type + (content_type, _) = mimetypes.guess_type(basename, strict=False) + if content_type is not None: + r.headers.set('Content-Type', content_type) + return r -# If the client closes the OnionShare window while a download is in progress, -# it should immediately stop serving the file. The client_cancel global is -# used to tell the download function that the client is canceling the download. -client_cancel = False + @self.app.errorhandler(404) + def page_not_found(e): + """ + 404 error page. + """ + self.add_request(self.REQUEST_OTHER, request.path) + if request.path != '/favicon.ico': + self.error404_count += 1 + if self.error404_count == 20: + self.add_request(self.REQUEST_RATE_LIMIT, request.path) + force_shutdown() + print(strings._('error_rate_limit')) -@app.route("//download") -def download(slug_candidate): - """ - Download the zip file. - """ - check_slug_candidate(slug_candidate) + r = make_response(render_template_string( + open(common.get_resource_path('html/404.html')).read(), + favicon_b64=self.favicon_b64 + ), 404) + for header, value in self.security_headers: + r.headers.set(header, value) + return r - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - global stay_open, download_in_progress, done - deny_download = not stay_open and download_in_progress - if deny_download: - r = make_response(render_template_string( - open(common.get_resource_path('html/denied.html')).read(), - favicon_b64=favicon_b64 - )) - for header,value in security_headers: - r.headers.set(header, value) - return r - - global download_count - - # each download has a unique id - download_id = download_count - download_count += 1 - - # prepare some variables to use inside generate() function below - # which is outside of the request context - shutdown_func = request.environ.get('werkzeug.server.shutdown') - path = request.path - - # tell GUI the download started - add_request(REQUEST_DOWNLOAD, path, {'id': download_id}) - - dirname = os.path.dirname(zip_filename) - basename = os.path.basename(zip_filename) - - def generate(): - # The user hasn't canceled the download - global client_cancel, gui_mode - client_cancel = False - - # Starting a new download - global stay_open, download_in_progress, done - if not stay_open: - download_in_progress = True - - chunk_size = 102400 # 100kb - - fp = open(zip_filename, 'rb') - done = False - canceled = False - while not done: - # The user has canceled the download, so stop serving the file - if client_cancel: - add_request(REQUEST_CANCELED, path, {'id': download_id}) - break - - chunk = fp.read(chunk_size) - if chunk == b'': - done = True - else: - try: - yield chunk - - # tell GUI the progress - downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / zip_filesize) * 100 - - # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - plat = common.get_platform() - if not gui_mode or plat == 'Linux' or plat == 'BSD': - sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(common.human_readable_filesize(downloaded_bytes), percent)) - sys.stdout.flush() - - add_request(REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes}) - done = False - except: - # looks like the download was canceled - done = True - canceled = True - - # tell the GUI the download has canceled - add_request(REQUEST_CANCELED, path, {'id': download_id}) - - fp.close() - - if common.get_platform() != 'Darwin': - sys.stdout.write("\n") - - # Download is finished - if not stay_open: - download_in_progress = False - - # Close the server, if necessary - if not stay_open and not canceled: - print(strings._("closing_automatically")) - if shutdown_func is None: - raise RuntimeError('Not running with the Werkzeug Server') - shutdown_func() - - r = Response(generate()) - r.headers.set('Content-Length', zip_filesize) - r.headers.set('Content-Disposition', 'attachment', filename=basename) - for header,value in security_headers: - r.headers.set(header, value) - # guess content type - (content_type, _) = mimetypes.guess_type(basename, strict=False) - if content_type is not None: - r.headers.set('Content-Type', content_type) - return r - - -@app.errorhandler(404) -def page_not_found(e): - """ - 404 error page. - """ - add_request(REQUEST_OTHER, request.path) - - global error404_count - if request.path != '/favicon.ico': - error404_count += 1 - if error404_count == 20: - add_request(REQUEST_RATE_LIMIT, request.path) + @self.app.route("//shutdown") + def shutdown(slug_candidate): + """ + Stop the flask web server, from the context of an http request. + """ + check_slug_candidate(slug_candidate, shutdown_slug) force_shutdown() - print(strings._('error_rate_limit')) + return "" - r = make_response(render_template_string( - open(common.get_resource_path('html/404.html')).read(), - favicon_b64=favicon_b64 - ), 404) - for header, value in security_headers: - r.headers.set(header, value) - return r + def set_file_info(self, filenames, processed_size_callback=None): + """ + Using the list of filenames being shared, fill in details that the web + page will need to display. This includes zipping up the file in order to + get the zip file's name and size. + """ + # build file info list + self.file_info = {'files': [], 'dirs': []} + for filename in filenames: + info = { + 'filename': filename, + 'basename': os.path.basename(filename.rstrip('/')) + } + if os.path.isfile(filename): + info['size'] = os.path.getsize(filename) + info['size_human'] = common.human_readable_filesize(info['size']) + self.file_info['files'].append(info) + if os.path.isdir(filename): + info['size'] = common.dir_size(filename) + info['size_human'] = common.human_readable_filesize(info['size']) + self.file_info['dirs'].append(info) + self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) + self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) + # zip up the files and folders + z = common.ZipWriter(processed_size_callback=processed_size_callback) + for info in self.file_info['files']: + z.add_file(info['filename']) + for info in self.file_info['dirs']: + z.add_dir(info['filename']) + z.close() + self.zip_filename = z.zip_filename + self.zip_filesize = os.path.getsize(self.zip_filename) -# shutting down the server only works within the context of flask, so the easiest way to do it is over http -shutdown_slug = common.random_string(16) + def _safe_select_jinja_autoescape(self, filename): + if filename is None: + return True + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + def base64_image(self, filename): + """ + Base64-encode an image file to use data URIs in the web app + """ + return base64.b64encode(open(common.get_resource_path('images/{}'.format(filename)), 'rb').read()).decode() -@app.route("//shutdown") -def shutdown(slug_candidate): - """ - Stop the flask web server, from the context of an http request. - """ - check_slug_candidate(slug_candidate, shutdown_slug) - force_shutdown() - return "" + def add_request(self, request_type, path, data=None): + """ + Add a request to the queue, to communicate with the GUI. + """ + self.q.put({ + 'type': request_type, + 'path': path, + 'data': data + }) + def generate_slug(self, persistent_slug=''): + if persistent_slug: + self.slug = persistent_slug + else: + self.slug = common.build_slug() -def force_shutdown(): - """ - Stop the flask web server, from the context of the flask app. - """ - # shutdown the flask service - func = request.environ.get('werkzeug.server.shutdown') - if func is None: - raise RuntimeError('Not running with the Werkzeug Server') - func() + def debug_mode(self): + """ + Turn on debugging mode, which will log flask errors to a debug file. + """ + temp_dir = tempfile.gettempdir() + log_handler = logging.FileHandler( + os.path.join(temp_dir, 'onionshare_server.log')) + log_handler.setLevel(logging.WARNING) + self.app.logger.addHandler(log_handler) + def check_slug_candidate(self, slug_candidate, slug_compare=None): + if not slug_compare: + slug_compare = self.slug + if not hmac.compare_digest(slug_compare, slug_candidate): + abort(404) -def start(port, stay_open=False, persistent_slug=''): - """ - Start the flask web server. - """ - generate_slug(persistent_slug) + def force_shutdown(self): + """ + Stop the flask web server, from the context of the flask app. + """ + # shutdown the flask service + func = request.environ.get('werkzeug.server.shutdown') + if func is None: + raise RuntimeError('Not running with the Werkzeug Server') + func() - set_stay_open(stay_open) + def start(self, port, stay_open=False, persistent_slug=''): + """ + Start the flask web server. + """ + self.generate_slug(persistent_slug) - # In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220) - if os.path.exists('/usr/share/anon-ws-base-files/workstation'): - host = '0.0.0.0' - else: - host = '127.0.0.1' + self.stay_open = stay_open - app.run(host=host, port=port, threaded=True) + # In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220) + if os.path.exists('/usr/share/anon-ws-base-files/workstation'): + host = '0.0.0.0' + else: + host = '127.0.0.1' + self.app.run(host=host, port=port, threaded=True) -def stop(port): - """ - Stop the flask web server by loading /shutdown. - """ + def stop(self, port): + """ + Stop the flask web server by loading /shutdown. + """ - # If the user cancels the download, let the download function know to stop - # serving the file - global client_cancel - client_cancel = True + # If the user cancels the download, let the download function know to stop + # serving the file + self.client_cancel = True - # to stop flask, load http://127.0.0.1://shutdown - try: - s = socket.socket() - s.connect(('127.0.0.1', port)) - s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(shutdown_slug)) - except: + # to stop flask, load http://127.0.0.1://shutdown try: - urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, shutdown_slug)).read() + s = socket.socket() + s.connect(('127.0.0.1', port)) + s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(shutdown_slug)) except: - pass + try: + urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, shutdown_slug)).read() + except: + pass diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 24e627bb..a40c081f 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -22,7 +22,8 @@ import os, sys, platform, argparse from .alert import Alert from PyQt5 import QtCore, QtWidgets -from onionshare import strings, common, web +from onionshare import strings, common +from .web import Web from onionshare.onion import Onion from onionshare.onionshare import OnionShare from onionshare.settings import Settings From 4a0c6e8dcdfb6468020313f38be1496cd6cabdac Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 6 Mar 2018 00:56:40 -0800 Subject: [PATCH 06/36] Refactor OnionShareGui to use new Web class --- onionshare/web.py | 9 +++--- onionshare_gui/__init__.py | 10 ++++--- onionshare_gui/onionshare_gui.py | 51 +++++++++++++++++--------------- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index 3e75ba27..afe66d84 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -41,7 +41,7 @@ class Web(object): """ The Web object is the OnionShare web server, powered by flask """ - def __init__(self, debug, stay_open, gui_mode, receive_mode): + def __init__(self, debug, stay_open, gui_mode, receive_mode=False): # The flask app self.app = Flask(__name__) @@ -50,14 +50,13 @@ class Web(object): self.debug_mode() # Stay open after the first download? - self.stay_open = False + self.stay_open = stay_open # Are we running in GUI mode? - self.gui_mode = False + self.gui_mode = gui_mode # Are we using receive mode? - self.receive_mode = False - + self.receive_mode = receive_mode # Starting in Flask 0.11, render_template_string autoescapes template variables # by default. To prevent content injection through template variables in diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index a40c081f..205f1c82 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -23,7 +23,7 @@ from .alert import Alert from PyQt5 import QtCore, QtWidgets from onionshare import strings, common -from .web import Web +from onionshare.web import Web from onionshare.onion import Onion from onionshare.onionshare import OnionShare from onionshare.settings import Settings @@ -86,7 +86,6 @@ def main(): # Debug mode? if debug: common.set_debug(debug) - web.debug_mode() # Validation if filenames: @@ -101,15 +100,18 @@ def main(): if not valid: sys.exit() + # Create the Web object + web = Web(debug, stay_open, True) + # Start the Onion onion = Onion() # Start the OnionShare app - web.set_stay_open(stay_open) app = OnionShare(onion, local_only, stay_open, shutdown_timeout) # Launch the gui - gui = OnionShareGui(onion, qtapp, app, filenames, config) + web.stay_open = stay_open + gui = OnionShareGui(web, onion, qtapp, app, filenames, config) # Clean up when app quits def shutdown(): diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index f38dd727..947499ed 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -17,10 +17,13 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os, threading, time +import os +import threading +import time +import queue from PyQt5 import QtCore, QtWidgets, QtGui -from onionshare import strings, common, web +from onionshare import strings, common from onionshare.settings import Settings from onionshare.onion import * @@ -43,13 +46,14 @@ class OnionShareGui(QtWidgets.QMainWindow): starting_server_step3 = QtCore.pyqtSignal() starting_server_error = QtCore.pyqtSignal(str) - def __init__(self, onion, qtapp, app, filenames, config=False): + def __init__(self, web, onion, qtapp, app, filenames, config=False): super(OnionShareGui, self).__init__() self._initSystemTray() common.log('OnionShareGui', '__init__') + self.web = web self.onion = onion self.qtapp = qtapp self.app = app @@ -70,7 +74,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.file_selection.file_list.add_file(filename) # Server status - self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection, self.settings) + self.server_status = ServerStatus(self.qtapp, self.app, self.web, self.file_selection, self.settings) self.server_status.server_started.connect(self.file_selection.server_started) self.server_status.server_started.connect(self.start_server) self.server_status.server_started.connect(self.update_server_status_indicator) @@ -377,9 +381,8 @@ class OnionShareGui(QtWidgets.QMainWindow): self.server_share_status_label.setText('') # Reset web counters - web.download_count = 0 - web.error404_count = 0 - web.set_gui_mode() + self.web.download_count = 0 + self.web.error404_count = 0 # start the onion service in a new thread def start_onion_service(self): @@ -395,7 +398,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.app.stay_open = not self.settings.get('close_after_first_download') # start onionshare http service in new thread - t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open, self.settings.get('slug'))) + t = threading.Thread(target=self.web.start, args=(self.app.port, self.app.stay_open, self.settings.get('slug'))) t.daemon = True t.start() # wait for modules in thread to load, preventing a thread-related cx_Freeze crash @@ -428,8 +431,8 @@ class OnionShareGui(QtWidgets.QMainWindow): if self._zip_progress_bar != None: self._zip_progress_bar.update_processed_size_signal.emit(x) try: - web.set_file_info(self.filenames, processed_size_callback=_set_processed_size) - self.app.cleanup_filenames.append(web.zip_filename) + self.web.set_file_info(self.filenames, processed_size_callback=_set_processed_size) + self.app.cleanup_filenames.append(self.web.zip_filename) self.starting_server_step3.emit() # done @@ -455,7 +458,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self._zip_progress_bar = None # warn about sending large files over Tor - if web.zip_filesize >= 157286400: # 150mb + if self.web.zip_filesize >= 157286400: # 150mb self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.show() @@ -503,7 +506,7 @@ class OnionShareGui(QtWidgets.QMainWindow): if self.server_status.status != self.server_status.STATUS_STOPPED: try: - web.stop(self.app.port) + self.web.stop(self.app.port) except: # Probably we had no port to begin with (Onion service didn't start) pass @@ -570,33 +573,33 @@ class OnionShareGui(QtWidgets.QMainWindow): done = False while not done: try: - r = web.q.get(False) + r = self.web.q.get(False) events.append(r) - except web.queue.Empty: + except queue.Empty: done = True for event in events: - if event["type"] == web.REQUEST_LOAD: + if event["type"] == self.web.REQUEST_LOAD: self.status_bar.showMessage(strings._('download_page_loaded', True)) - elif event["type"] == web.REQUEST_DOWNLOAD: + elif event["type"] == self.web.REQUEST_DOWNLOAD: self.downloads_container.show() # show the downloads layout - self.downloads.add_download(event["data"]["id"], web.zip_filesize) + self.downloads.add_download(event["data"]["id"], self.web.zip_filesize) self.new_download = True self.downloads_in_progress += 1 self.update_downloads_in_progress(self.downloads_in_progress) if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True)) - elif event["type"] == web.REQUEST_RATE_LIMIT: + elif event["type"] == self.web.REQUEST_RATE_LIMIT: self.stop_server() Alert(strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical) - elif event["type"] == web.REQUEST_PROGRESS: + elif event["type"] == self.web.REQUEST_PROGRESS: self.downloads.update_download(event["data"]["id"], event["data"]["bytes"]) # is the download complete? - if event["data"]["bytes"] == web.zip_filesize: + if event["data"]["bytes"] == self.web.zip_filesize: if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) # Update the total 'completed downloads' info @@ -607,7 +610,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.update_downloads_in_progress(self.downloads_in_progress) # close on finish? - if not web.get_stay_open(): + if not self.web.stay_open: self.server_status.stop_server() self.status_bar.clearMessage() self.server_share_status_label.setText(strings._('closing_automatically', True)) @@ -618,7 +621,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.update_downloads_in_progress(self.downloads_in_progress) - elif event["type"] == web.REQUEST_CANCELED: + elif event["type"] == self.web.REQUEST_CANCELED: self.downloads.cancel_download(event["data"]["id"]) # Update the 'in progress downloads' info self.downloads_in_progress -= 1 @@ -627,7 +630,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True)) elif event["path"] != '/favicon.ico': - self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(web.error404_count, strings._('other_page_loaded', True), event["path"])) + self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(self.web.error404_count, strings._('other_page_loaded', True), event["path"])) # If the auto-shutdown timer has stopped, stop the server if self.server_status.status == self.server_status.STATUS_STARTED: @@ -638,7 +641,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining)) if not self.app.shutdown_timer.is_alive(): # If there were no attempts to download the share, or all downloads are done, we can stop - if web.download_count == 0 or web.done: + if self.web.download_count == 0 or self.web.done: self.server_status.stop_server() self.status_bar.clearMessage() self.server_share_status_label.setText(strings._('close_on_timeout', True)) From fa9f71465142fa9b867f94a3cdab5c856061fd17 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 6 Mar 2018 01:50:43 -0800 Subject: [PATCH 07/36] Make separate routes for send and receive modes --- onionshare/web.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/onionshare/web.py b/onionshare/web.py index afe66d84..2814616d 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -113,6 +113,17 @@ class Web(object): # shutting down the server only works within the context of flask, so the easiest way to do it is over http self.shutdown_slug = common.random_string(16) + # Define the ewb app routes + self.common_routes() + if self.receive_mode: + self.receive_routes() + else: + self.send_routes() + + def send_routes(self): + """ + The web app routes for sharing files + """ @self.app.route("/") def index(slug_candidate): """ @@ -258,6 +269,19 @@ class Web(object): r.headers.set('Content-Type', content_type) return r + def receive_routes(self): + """ + The web app routes for sharing files + """ + @self.app.route("/") + def index(slug_candidate): + return "Receive Mode" + + + def common_routes(self): + """ + Common web app routes between sending and receiving + """ @self.app.errorhandler(404) def page_not_found(e): """ From baede536321431d5a19a2bb3ae659e875c2d9c63 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 6 Mar 2018 02:06:44 -0800 Subject: [PATCH 08/36] Make separate template for send and receive mode --- onionshare/web.py | 20 +++-- share/html/receive.html | 105 +++++++++++++++++++++++++++ share/html/{index.html => send.html} | 0 3 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 share/html/receive.html rename share/html/{index.html => send.html} (100%) diff --git a/onionshare/web.py b/onionshare/web.py index 2814616d..7e488a86 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -147,7 +147,7 @@ class Web(object): # If download is allowed to continue, serve download page r = make_response(render_template_string( - open(common.get_resource_path('html/index.html')).read(), + open(common.get_resource_path('html/send.html')).read(), favicon_b64=self.favicon_b64, logo_b64=self.logo_b64, folder_b64=self.folder_b64, @@ -275,7 +275,17 @@ class Web(object): """ @self.app.route("/") def index(slug_candidate): - return "Receive Mode" + self.check_slug_candidate(slug_candidate) + + # If download is allowed to continue, serve download page + r = make_response(render_template_string( + open(common.get_resource_path('html/receive.html')).read(), + favicon_b64=self.favicon_b64, + logo_b64=self.logo_b64, + slug=self.slug)) + for header, value in self.security_headers: + r.headers.set(header, value) + return r def common_routes(self): @@ -293,7 +303,7 @@ class Web(object): self.error404_count += 1 if self.error404_count == 20: self.add_request(self.REQUEST_RATE_LIMIT, request.path) - force_shutdown() + self.force_shutdown() print(strings._('error_rate_limit')) r = make_response(render_template_string( @@ -309,8 +319,8 @@ class Web(object): """ Stop the flask web server, from the context of an http request. """ - check_slug_candidate(slug_candidate, shutdown_slug) - force_shutdown() + self.check_slug_candidate(slug_candidate, shutdown_slug) + self.force_shutdown() return "" def set_file_info(self, filenames, processed_size_callback=None): diff --git a/share/html/receive.html b/share/html/receive.html new file mode 100644 index 00000000..942ec62e --- /dev/null +++ b/share/html/receive.html @@ -0,0 +1,105 @@ + + + + OnionShare + + + + + +
+ +

OnionShare

+
+ + + diff --git a/share/html/index.html b/share/html/send.html similarity index 100% rename from share/html/index.html rename to share/html/send.html From ce852fc60a3c4d7a0b826d3f01da9fcd00302b93 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 6 Mar 2018 02:54:12 -0800 Subject: [PATCH 09/36] Create separate templates and static folder, and make the web app use both of these. Yay, now we have real static resources --- MANIFEST.in | 3 +- install/pyinstaller.spec | 3 +- onionshare/web.py | 80 ++++++++---------------- setup.py | 3 +- share/{images => static}/favicon.ico | Bin share/static/logo.png | Bin 0 -> 3824 bytes share/{images => static}/web_file.png | Bin share/{images => static}/web_folder.png | Bin share/{html => templates}/404.html | 2 +- share/{html => templates}/denied.html | 2 +- share/{html => templates}/receive.html | 4 +- share/{html => templates}/send.html | 8 +-- 12 files changed, 39 insertions(+), 66 deletions(-) rename share/{images => static}/favicon.ico (100%) create mode 100644 share/static/logo.png rename share/{images => static}/web_file.png (100%) rename share/{images => static}/web_folder.png (100%) rename share/{html => templates}/404.html (77%) rename share/{html => templates}/denied.html (79%) rename share/{html => templates}/receive.html (91%) rename share/{html => templates}/send.html (93%) diff --git a/MANIFEST.in b/MANIFEST.in index f4d1c078..c8a4d87c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,7 +4,8 @@ include BUILD.md include share/* include share/images/* include share/locale/* -include share/html/* +include share/templates/* +include share/static/* include install/onionshare.desktop include install/onionshare.appdata.xml include install/onionshare80.xpm diff --git a/install/pyinstaller.spec b/install/pyinstaller.spec index 6ca2fdbe..a4f1532a 100644 --- a/install/pyinstaller.spec +++ b/install/pyinstaller.spec @@ -20,7 +20,8 @@ a = Analysis( ('../share/torrc_template-windows', 'share'), ('../share/images/*', 'share/images'), ('../share/locale/*', 'share/locale'), - ('../share/html/*', 'share/html') + ('../share/templates/*', 'share/templates'), + ('../share/static/*', 'share/static') ], hiddenimports=[], hookspath=[], diff --git a/onionshare/web.py b/onionshare/web.py index 7e488a86..5d1d0cb8 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -26,12 +26,11 @@ import queue import socket import sys import tempfile -import base64 from distutils.version import LooseVersion as Version from urllib.request import urlopen from flask import ( - Flask, Response, request, render_template_string, abort, make_response, + Flask, Response, request, render_template, abort, make_response, __version__ as flask_version ) @@ -43,7 +42,9 @@ class Web(object): """ def __init__(self, debug, stay_open, gui_mode, receive_mode=False): # The flask app - self.app = Flask(__name__) + self.app = Flask(__name__, + static_folder=common.get_resource_path('static'), + template_folder=common.get_resource_path('templates')) # Debug mode? if debug: @@ -88,12 +89,6 @@ class Web(object): self.REQUEST_RATE_LIMIT = 5 self.q = queue.Queue() - # Load and base64 encode images to pass into templates - self.favicon_b64 = self.base64_image('favicon.ico') - self.logo_b64 = self.base64_image('logo.png') - self.folder_b64 = self.base64_image('web_folder.png') - self.file_b64 = self.base64_image('web_file.png') - self.slug = None self.download_count = 0 @@ -137,29 +132,18 @@ class Web(object): # currently a download deny_download = not self.stay_open and self.download_in_progress if deny_download: - r = make_response(render_template_string( - open(common.get_resource_path('html/denied.html')).read(), - favicon_b64=self.favicon_b64 - )) - for header, value in self.security_headers: - r.headers.set(header, value) - return r + r = make_response(render_template('denied.html')) + return self.add_security_headers(r) # If download is allowed to continue, serve download page - r = make_response(render_template_string( - open(common.get_resource_path('html/send.html')).read(), - favicon_b64=self.favicon_b64, - logo_b64=self.logo_b64, - folder_b64=self.folder_b64, - file_b64=self.file_b64, + r = make_response(render_template( + 'send.html', slug=self.slug, file_info=self.file_info, filename=os.path.basename(self.zip_filename), filesize=self.zip_filesize, filesize_human=common.human_readable_filesize(self.zip_filesize))) - for header, value in self.security_headers: - r.headers.set(header, value) - return r + return self.add_security_headers(r) @self.app.route("//download") def download(slug_candidate): @@ -172,13 +156,8 @@ class Web(object): # currently a download deny_download = not self.stay_open and self.download_in_progress if deny_download: - r = make_response(render_template_string( - open(common.get_resource_path('html/denied.html')).read(), - favicon_b64=self.favicon_b64 - )) - for header,value in self.security_headers: - r.headers.set(header, value) - return r + r = make_response(render_template('denied.html')) + return self.add_security_headers(r) # each download has a unique id download_id = self.download_count @@ -261,8 +240,7 @@ class Web(object): r = Response(generate()) r.headers.set('Content-Length', self.zip_filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) - for header,value in self.security_headers: - r.headers.set(header, value) + r = self.add_security_headers(r) # guess content type (content_type, _) = mimetypes.guess_type(basename, strict=False) if content_type is not None: @@ -278,15 +256,10 @@ class Web(object): self.check_slug_candidate(slug_candidate) # If download is allowed to continue, serve download page - r = make_response(render_template_string( - open(common.get_resource_path('html/receive.html')).read(), - favicon_b64=self.favicon_b64, - logo_b64=self.logo_b64, + r = make_response(render_template( + 'receive.html', slug=self.slug)) - for header, value in self.security_headers: - r.headers.set(header, value) - return r - + return self.add_security_headers(r) def common_routes(self): """ @@ -306,13 +279,8 @@ class Web(object): self.force_shutdown() print(strings._('error_rate_limit')) - r = make_response(render_template_string( - open(common.get_resource_path('html/404.html')).read(), - favicon_b64=self.favicon_b64 - ), 404) - for header, value in self.security_headers: - r.headers.set(header, value) - return r + r = make_response(render_template('404.html'), 404) + return self.add_security_headers(r) @self.app.route("//shutdown") def shutdown(slug_candidate): @@ -323,6 +291,14 @@ class Web(object): self.force_shutdown() return "" + def add_security_headers(self, r): + """ + Add security headers to a request + """ + for header, value in self.security_headers: + r.headers.set(header, value) + return r + def set_file_info(self, filenames, processed_size_callback=None): """ Using the list of filenames being shared, fill in details that the web @@ -362,12 +338,6 @@ class Web(object): return True return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) - def base64_image(self, filename): - """ - Base64-encode an image file to use data URIs in the web app - """ - return base64.b64encode(open(common.get_resource_path('images/{}'.format(filename)), 'rb').read()).decode() - def add_request(self, request_type, path, data=None): """ Add a request to the queue, to communicate with the GUI. diff --git a/setup.py b/setup.py index 23e1ea17..99222ef0 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,8 @@ data_files=[ (os.path.join(sys.prefix, 'share/onionshare'), file_list('share')), (os.path.join(sys.prefix, 'share/onionshare/images'), file_list('share/images')), (os.path.join(sys.prefix, 'share/onionshare/locale'), file_list('share/locale')), - (os.path.join(sys.prefix, 'share/onionshare/html'), file_list('share/html')), + (os.path.join(sys.prefix, 'share/onionshare/templates'), file_list('share/templates')), + (os.path.join(sys.prefix, 'share/onionshare/static'), file_list('share/static')) ] if platform.system() != 'OpenBSD': data_files.append(('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py'])) diff --git a/share/images/favicon.ico b/share/static/favicon.ico similarity index 100% rename from share/images/favicon.ico rename to share/static/favicon.ico diff --git a/share/static/logo.png b/share/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..43884c1f7845fa0eec517677958c6660fe79a7de GIT binary patch literal 3824 zcmVWV1gpPl`GKiyMJ`UG|VIFJTB0;B{Uy$93) zH-HMj4%7lSfhtLszHP6Mc#F{4z}d{UF~B_F1waRwqI|s*@B({)9g-}Sb|#QQCxMm) zOaih2L!9U21~vhwBw5OjH<+nWCU4p0ev z^*w?|pzp7M%D|XfjI+yWV8Fhe0ut$((f~|qvXfmx1B0}#GO%2`DtH9ORuUL%Of*r9 z7C7J)o7r|h5RP;g85|h%E71ZeM5%qh2Nux1Ws4+B3p-5%ZDyMg`1^}kg6H%3@VGrV z8XPprjnvEa$g+$q%hb#DG|G)Q8XS1s9(+DuECE~)m`n5A;-Y~~v6Rhh>j(S|cr+@& zynetegDhAETNv=e0i+E|BV%F)DV7v0=@yK=i~zVAUDSSC%e~rroG&`hjVm{}Reg)v zs#G1E zTn-m?ck9@my(1{>}R+ilS*bOR_XDY9|sD<9!h+cs{QWK@eDeU^%-gc6Azf011f+?5fzs@&n5e z1OcBnVyE+HkYkBX1GR)IZl{}Z)5o#m(26b*r@TAsUH*FXuNX`bXO&??93hPMy#nlr z$bd9B+3?v0Mm{~VO9gx7C(Bv#^Cg%PBR1jlfSv6*79ouG-3R(aV((TRS;dH_Ms&%D z`RgyfCg+))NbTYSNtXJmwt=m3Th-th6O4Rt?gKI=W^~zVfLfJuG2n~D(n5x$10f;)0vb-`<(oz^YYG{`SP!I&t($g66 z)Clr-=cCi>)HE|#O;)enWiJc~pv`Qxu;&FNK zd42eN{$O0xiRkn?^ag#jxo$|A(Pu^^_V5WwmdcyH(YN|xiP~k`p6HEw*8gTbcWUl1 z^FL;)p2Xqz4s-VSS?=Aq$KBeyG|G)Q8=UA2I+A)Pk(`oD{~z=xH9eJuTNi33AVSIV z1It-6XN_7AO8{dFFC6f^SwLQ-n%9)FwbqP>@aaXfjfItdxFf{X#mHiqZ<^kD5$kSB()=iNQ*UA>p2#H5(mFHt9Qr{)ete=Z{H#Vjp^ zK??_X=IEKQb{^1KP1d85EY&pGz-_?4sp;gQ{E$t@H!=EWqeK3D_sZQI+;p&wCVJc+ zel+4oRQ|2fzg%N1KA(@?{d@EAm54-j;MpgqlV zJKZeZyOasPoY20HpZo;ZE?uLtys`y|qE6(&?FR^=5Y+@bvSV(io67P^GG}LMe$S2X zZgBbBWi{c_aJ$RCRtOw1vlE7@cR5_FKfa!knd)e&EpKcgFEKo^55@ z7&=;8Ms&O8HZKf%K~0$C1gs+d4RKR&ZrfU9S!U_prK-ULSn&P=Z2xY<<#2`67L8PU zSV``k%-dOS<950=weQ`pH{jU8&m#=MQ1GK*I?zRHw;nS$5j?KBV- zMZxWIQ&C!>sl745h-I)v?c=G<0EPmuQEp`EzNL)G8l!nai*_tx)|<0J?n}fI#N4I1 zn%fWf;Q+Op_ZG1|=Ed!Fv+PgHq7G64US9h$6K78hxvb&=q96!dD88U&GYNpSL1}8+ zT1B7@LSC|p8JZEyuCbeYu5@!A9tdO50-s7SF{UW zEVX0VzGci=GN)bp6=`3BD4_V17Jm@n=65%>X>TthUc7DIsrdZk3W{x01e@9B_5YuU z&*y7?s6`xDFLP2k8F^4!SI->FoRGn^s24-_rG?1-b@s1$KIi$Edhz!9I(z&q%cn1I zW^F|LMJ&z6T@Zyfe{|?@HM$~SMminIeUn4JGZOH^Y36~A2UzpmnwWa=kW6Y`3Ge>u z-C)W~5%Hh6>X`9Br1sldO?D_bB{`-O3k9Me2%P@E(`;F|C2qDm|D$|zrsw!6xu6VJ z#9uzu@xXP0pRLuvvGxB!|Cq;kp+M9bbmZ;IW5?S&Vh-Lnr@!I-`R|8179xxIYtO<0 zalYt$MeHM!WCMGOtdE?3r zO#@iCbs=>Pb#bAuLV;*787MkfMDDWOsQV~BQq0=t)^L9yOLKBQ z^R=0+ZAW}HQ73Zfe-5#E-e%43F3m4x-Q0EbGWY5zq`N8NFBuIhwNUA9d=s4RKZ;$w$g`X6%c8(wWgR_Kj-3jeo|k5+!v zZrgL8pJUD3HRug`wZZis2Kk7cu2GQ~2??ACvl6DqkM{vc;(@K@_;~UtvlW;Uv{(E) z>K#}HTiAGfV++=N$jBj>5=|5zDQ>YG1wo+tn`$21eb7vre7^T{);_m3q~>)1o?DWo z`OO>a0WD0RgQFcyQhC0TIg97CtLkd)Rp#tWCfFu$V&4gTUSC8smZB&adl~t~ykD^D z$SV3-`n7px_=Mr4J(fnvv642X=u+7w>g(!B?wicYnJe2p&e6emHrQSE6Lyzf{o$g& zkKRu>?yA@o6W-44^lt#=F3sga@dey&H%6loonD8i6HycekK5xf<3hlkVrKq(^BMKC zQ6YhAEyr??vFVjfB%5328K&*GILz znzNssEk93fC*I^V@&=Enc@blBQMd-8IuwY#3f5D>iJklov(%31>f@ zP5H_4E(c!u$#Sxv&epO*7Ijo~-BWPgv4kzS4qNgcctKI5gh6RuDKq-apyXJ|_hJ_n zMWN(a2{Zc4pfs;k3wTA6EG>+>+hnx zfHBbVyZ{Z>-Q)oYs|t*HXREnHEIpUdMkz791{wdoDoH>N ze)hJj$aNqmFt*Yzdku0cKzlCYjy9u`WT`x`gEbXc8<0000 Error 404 - + + + 404 diff --git a/share/templates/denied.html b/share/templates/denied.html index 7535e0b4..39974639 100644 --- a/share/templates/denied.html +++ b/share/templates/denied.html @@ -2,16 +2,8 @@ OnionShare - - + +

OnionShare download in progress

diff --git a/share/templates/receive.html b/share/templates/receive.html index 633991a8..42fe35d2 100644 --- a/share/templates/receive.html +++ b/share/templates/receive.html @@ -2,102 +2,13 @@ OnionShare - - + +
- +

OnionShare

diff --git a/share/templates/send.html b/share/templates/send.html index 1814d214..07bd76bb 100644 --- a/share/templates/send.html +++ b/share/templates/send.html @@ -2,97 +2,8 @@ OnionShare - - + + @@ -105,7 +16,7 @@
  • Download Files
  • - +

    OnionShare

    @@ -118,7 +29,7 @@ {% for info in file_info.dirs %} - + {{ info.basename }} {{ info.size_human }} @@ -128,7 +39,7 @@ {% for info in file_info.files %} - + {{ info.basename }} {{ info.size_human }} From 649afa2fad6ad2a8a016ac633de8b4376cade016 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 6 Mar 2018 03:17:54 -0800 Subject: [PATCH 11/36] Move (optional) javascript into file, and use CSP to ban inline js --- onionshare/web.py | 2 +- share/static/js/send.js | 75 +++++++++++++++++++++++++++++++++++++++ share/templates/send.html | 73 ++----------------------------------- 3 files changed, 79 insertions(+), 71 deletions(-) create mode 100644 share/static/js/send.js diff --git a/onionshare/web.py b/onionshare/web.py index 3fa9ed7b..68684651 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -73,7 +73,7 @@ class Web(object): self.zip_filesize = None self.security_headers = [ - ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'unsafe-inline\'; img-src \'self\' data:;'), + ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), ('X-Frame-Options', 'DENY'), ('X-Xss-Protection', '1; mode=block'), ('X-Content-Type-Options', 'nosniff'), diff --git a/share/static/js/send.js b/share/static/js/send.js new file mode 100644 index 00000000..43e9892d --- /dev/null +++ b/share/static/js/send.js @@ -0,0 +1,75 @@ +// Function to convert human-readable sizes back to bytes, for sorting +function unhumanize(text) { + var powers = {'b': 0, 'k': 1, 'm': 2, 'g': 3, 't': 4}; + var regex = /(\d+(?:\.\d+)?)\s?(B|K|M|G|T)?/i; + var res = regex.exec(text); + if(res[2] === undefined) { + // Account for alphabetical words (file/dir names) + return text; + } else { + return res[1] * Math.pow(1024, powers[res[2].toLowerCase()]); + } +} +function sortTable(n) { + var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0; + table = document.getElementById("file-list"); + switching = true; + // Set the sorting direction to ascending: + dir = "asc"; + /* Make a loop that will continue until + no switching has been done: */ + while (switching) { + // Start by saying: no switching is done: + switching = false; + rows = table.getElementsByTagName("TR"); + /* Loop through all table rows (except the + first, which contains table headers): */ + for (i = 1; i < (rows.length - 1); i++) { + // Start by saying there should be no switching: + shouldSwitch = false; + /* Get the two elements you want to compare, + one from current row and one from the next: */ + x = rows[i].getElementsByTagName("TD")[n]; + y = rows[i + 1].getElementsByTagName("TD")[n]; + /* Check if the two rows should switch place, + based on the direction, asc or desc: */ + if (dir == "asc") { + if (unhumanize(x.innerHTML.toLowerCase()) > unhumanize(y.innerHTML.toLowerCase())) { + // If so, mark as a switch and break the loop: + shouldSwitch= true; + break; + } + } else if (dir == "desc") { + if (unhumanize(x.innerHTML.toLowerCase()) < unhumanize(y.innerHTML.toLowerCase())) { + // If so, mark as a switch and break the loop: + shouldSwitch= true; + break; + } + } + } + if (shouldSwitch) { + /* If a switch has been marked, make the switch + and mark that a switch has been done: */ + rows[i].parentNode.insertBefore(rows[i + 1], rows[i]); + switching = true; + // Each time a switch is done, increase this count by 1: + switchcount ++; + } else { + /* If no switching has been done AND the direction is "asc", + set the direction to "desc" and run the while loop again. */ + if (switchcount == 0 && dir == "asc") { + dir = "desc"; + switching = true; + } + } + } +} + + +// Set click handlers +document.getElementById("filename-header").addEventListener("click", function(){ + sortTable(0); +}); +document.getElementById("size-header").addEventListener("click", function(){ + sortTable(1); +}); diff --git a/share/templates/send.html b/share/templates/send.html index 07bd76bb..ba43f306 100644 --- a/share/templates/send.html +++ b/share/templates/send.html @@ -22,8 +22,8 @@ - - + + {% for info in file_info.dirs %} @@ -47,73 +47,6 @@ {% endfor %}
    FilenameSizeFilenameSize
    - + From 5d42e76eb830bb16c2a287503a642a71393bf449 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 6 Mar 2018 03:24:17 -0800 Subject: [PATCH 12/36] Show different message for receive mode than for send mode --- onionshare/__init__.py | 22 ++++++++++++++++------ share/locale/en.json | 6 ++++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 8d914f9c..3ab5cfed 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -144,13 +144,23 @@ def main(cwd=None): settings.set('slug', web.slug) settings.save() - if(stealth): - print(strings._("give_this_url_stealth")) - print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) - print(app.auth_string) + print('') + if receive: + if stealth: + print(strings._("give_this_url_receive_stealth")) + print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) + print(app.auth_string) + else: + print(strings._("give_this_url_receive")) + print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) else: - print(strings._("give_this_url")) - print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) + if stealth: + print(strings._("give_this_url_stealth")) + print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) + print(app.auth_string) + else: + print(strings._("give_this_url")) + print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) print('') print(strings._("ctrlc_to_stop")) diff --git a/share/locale/en.json b/share/locale/en.json index e72686b8..8686ea13 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -5,8 +5,10 @@ "wait_for_hs_trying": "Trying...", "wait_for_hs_nope": "Not ready yet.", "wait_for_hs_yup": "Ready!", - "give_this_url": "Give this address to the person you're sending the file to:", - "give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:", + "give_this_url": "Give this address to the person you're sending the files to:", + "give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the files to:", + "give_this_url_receive": "Give this address to the people sending you files:", + "give_this_url_receive_stealth": "Give this address and HidServAuth line to the people sending you files:", "ctrlc_to_stop": "Press Ctrl-C to stop server", "not_a_file": "{0:s} is not a valid file.", "not_a_readable_file": "{0:s} is not a readable file.", From e980bc153b601adc129cb7da0101947b2c828691 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 6 Mar 2018 05:25:49 -0800 Subject: [PATCH 13/36] Change default receive mode download directory to ~/OnionShare --- onionshare/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare/settings.py b/onionshare/settings.py index 5ec2d2ae..9b61beaf 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -104,7 +104,7 @@ class Settings(object): """ # TODO: Test in Windows, though it looks like it should work # https://docs.python.org/3/library/os.path.html#os.path.expanduser - return os.path.expanduser('~/Downloads') + return os.path.expanduser('~/OnionShare') def load(self): """ From 878dd4f880507f0e453988eca10e2ead4340b583 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 6 Mar 2018 07:40:57 -0800 Subject: [PATCH 14/36] In CLI, validate downloads_dir when starting in receive mode --- onionshare/__init__.py | 38 +++++++++++++++++++++++++++----------- share/locale/en.json | 4 +++- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 3ab5cfed..ec008f5d 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -72,22 +72,38 @@ def main(cwd=None): if debug: common.set_debug(debug) - # Validation - valid = True - for filename in filenames: - if not os.path.isfile(filename) and not os.path.isdir(filename): - print(strings._("not_a_file").format(filename)) - valid = False - if not os.access(filename, os.R_OK): - print(strings._("not_a_readable_file").format(filename)) - valid = False - if not valid: - sys.exit() + # Validate filenames + if not receive: + valid = True + for filename in filenames: + if not os.path.isfile(filename) and not os.path.isdir(filename): + print(strings._("not_a_file").format(filename)) + valid = False + if not os.access(filename, os.R_OK): + print(strings._("not_a_readable_file").format(filename)) + valid = False + if not valid: + sys.exit() # Load settings settings = Settings(config) settings.load() + # In receive mode, validate downloads dir + if receive: + valid = True + if not os.path.isdir(settings.get('downloads_dir')): + try: + os.mkdir(settings.get('downloads_dir'), 0o700) + except: + print(strings._('error_cannot_create_downloads_dir').format(settings.get('downloads_dir'))) + valid = False + if valid and not os.access(settings.get('downloads_dir'), os.W_OK): + print(strings._('error_downloads_dir_not_writable').format(settings.get('downloads_dir'))) + valid = False + if not valid: + sys.exit() + # Create the Web object web = Web(debug, stay_open, False, receive) diff --git a/share/locale/en.json b/share/locale/en.json index 8686ea13..3340afd4 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -156,5 +156,7 @@ "gui_file_info": "{} Files, {}", "gui_file_info_single": "{} File, {}", "info_in_progress_downloads_tooltip": "{} download(s) in progress", - "info_completed_downloads_tooltip": "{} download(s) completed" + "info_completed_downloads_tooltip": "{} download(s) completed", + "error_cannot_create_downloads_dir": "Error creating downloads folder: {}", + "error_downloads_dir_not_writable": "The downloads folder isn't writable: {}" } From 9a800d90b2937389004a3007e399b447b56ffb8a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 7 Mar 2018 10:12:10 -0800 Subject: [PATCH 15/36] Started designing HTML/CSS for receive mode --- share/static/css/style.css | 31 ++++++++++++++++++++++++++++++- share/static/img/logo_large.png | Bin 0 -> 9663 bytes share/templates/receive.html | 12 ++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 share/static/img/logo_large.png diff --git a/share/static/css/style.css b/share/static/css/style.css index dc20f24d..c3304f39 100644 --- a/share/static/css/style.css +++ b/share/static/css/style.css @@ -45,10 +45,11 @@ header .right ul li { font-size: 1rem; } -header .button { +.button { color: #ffffff; background-color: #4e064f; padding: 10px; + border: 0; border-radius: 5px; text-decoration: none; margin-left: 1rem; @@ -86,3 +87,31 @@ table.file-list td img { table.file-list td:last-child { width: 100%; } + +.upload-wrapper { + display: flex; + align-items: center; + justify-content: center; + min-height: 400px; +} + +.upload { + text-align: center; +} + +.upload img { + width: 120px; + height: 120px; +} + +.upload .upload-header { + font-size: 30px; + font-weight: normal; + color: #666666; + margin: 0 0 10px 0; +} + +.upload .upload-description { + color: #666666; + margin: 0 0 20px 0; +} diff --git a/share/static/img/logo_large.png b/share/static/img/logo_large.png new file mode 100644 index 0000000000000000000000000000000000000000..ee8f26acea501e10a1fd360cab892fcddfaa24e0 GIT binary patch literal 9663 zcmZ{qbx_=V^zLzYcPp?^+$m68iGsto#>t_>I&$a$moRPug2+6)e#UF5LD!4 zKKLvi=lJ@Nd|W*FtUKdKhQu69zz~cf_*#RBftvx#lugv5Fn)wr)Z^@nk`%46qJqJ; zjuPUHyWEo`EsZ++9(H03+*U&Ii}Y z8?V1i1H}~wfv0|H4}q(@q@vMUSq3pj$*}uY%o=3Y{I2If)8>DzEGXI2E_XLB4ca5r2r~5Sc6u%!?#55_Q?P!zFy-KbIolcW`ulu( zUM-NN;5s@Uq7oE8*cFQ0O_#3%85$l^l;5ys`1UM-&PyvwOM$SBSmebtK&&qPMhkfw z2?m|1Q-$Q>q@9N~K}#}Ao`Q$ESETvOr>cw!C7Zt+si4jyO5iyqnp|vrMODM=&Agg- zetpKkD+@B=mqYACJwcvDs;;&iBthJw{)3A+K4sOqMATiS_{T)5Vldw97x$fc9n6j~ zb4WkSAPe~somVH}SRQ6=JM zIL0_pDT-Bb^-FH8*j8ECv~?o~#0V=bdckCYGn=;E*wTJ6JlZlGXGZI;(RFQ114|eC zFL=L^cS+eV6iD~)DY^<1d>?4OeIVn#nr!1}LAb^4dH0pXGV)b7X`SkG!&bmIL2fWl zbQ_@$H$1#$Q*XT_zQi0%yKLuU|+t2=5l0my!XqX2;7s`RGUUNw1Ja zcH$4M-E3LM+s;<2DOP^BXD}6AT>d&n zspsc?QexsL5o5GSJwQ6Y&Z1ELVLyu3`3v}+>aKu24fPlc6J7(1DnHl+mj z8*c2_ClW-`gpksiP#8R}c`VlKygu*=BHYmxJsk5+URPdL_K#o#6f*5aOyCVf!v45N z#s`!m^K1Ea?)7K+Bya;XVr~mwde0_S6(yq(`it(X`pKm6C+tt`oi!Q3;mEihI|VUv zN8ql|wqdaYyy+>e73u3|hkvK52?qRDjRu7MVT@KBXa4Id3oPh`DzZ8azvd9Jq44N+ zJ)Luqhli3aHQ+)yyG2C(SQ-J&((at7~@p;;`ZH$ne6& z6d5F_DP^Edkwk&9NK>(W#Y^mrqQ~KaWvU@eR`Azc&v7+Ub+qkhsXbMUQo+L7DDKT_ zcRvSfvN&`4Ri!6qQ-9g4z64g4det;-rPY?1@Wn-P`o7gcDBLExIaV*Y`D3~ShavNq z;XnH>-oY*dTe6){s%~rHB%{QUO!Aze#@Ol-O-Np`vU)akmO-lzi`mPTF#5(8NwP~iFHTG-BKSd1Eq#%}(AXK>jh6H3Ad;~T}n zjh>`yr;l{rvD6h~F=X`nS9A+3QZ5aoB!$wQYx{g=YQ%WDx^I3%!VH?nH!JsTwfRd> zV&5)^^zU;oR@DTqK$;Bda zGyiIURJDUK?K8_1mrSikQLVHjwAU`qL&oExhVT305#>5pfXs!-7EN{6jzw&vX5KT5DpMjw zdU`#af>GH0=H-y)HAGj-z%<%AK1vqQWyi%o#P@?N{l~%N<0s@X<=`ery(+ ztnZ9bNFz6#AdnOYp2fzFC6WNk99~JCstm708yMzpW&L)&xnpSZ6(Xx~`o0}};VtJîW>9oFs+X@|b;p>?H z>aJ)do5o>tK~=&-T$d3ojC+&nSKqC{>OlH$~a}D$}~N=Xd^j9#cQLdo3G_!B)Iz2{dXt z&csWzuCcR==ak)tb*e5$+Box!v8_1Bi<=HvWMp@XDt2jQtNLR(yK9*-m_>-VE&-d0 zLGE)rc8)Hw1Efb{-tn1)s)IuT*ugW3AQ}twv1DZ(6JnU;n!@?CCI7umag>#HG1?b4 z%AR!kKou(!E5QTIQt`@o{g3I((<@@N(>aHzZeUstjNDQ(uV#NvqFLIqzPpkXOsO2` z+gCG1%gXb=ov$WNH5ZgV_88i3vGi_e!0&9l-Pdc4-SB!mL@jq9u+Sk(%hTD8(JmiJ zAlI_bQhbRnA%1geyQ|ncVM$R%MEssqj><=}ZErb1F>wROxPhXVRm1bQr&IozGi_G#+sY;fc z!QInBVfxGJt2GjRQfs@{+O4qUoNVtpm%m~T9FF$ouCnKBWo61CVGEW6*20W2ejbI= z2taw(JTGu~O+-YN9S(f^Xe2o1!;Lx=9w!u78XVnD>c$54|K-bV{RDZd<`oTd#liaH zc%|+NXtO^N;Z>MV5+vq=BSJHuHP?$41k!xiC6p_-*RrMT1##cBZ2zdPF1?U7TL%;Z+t8C!FiJ-DK(< zWcSqm5jm)?oaVh>Fr5NETiq_yb~-g1td17vX&z{QyB<2sJL~?4U$fIFgz@OFE=E-# zH#nh7nu>esWIh%nbTzauH>DXv6an2g*k@D6^6#7vwdSA3N<-igBP}geMT+pGikvyu z2_c=(Ru_Gglme2--SMBUzstWpD8>BP

    o&mOx8q4L-eow~!pSE$(;UzlK#2&mR4> zfIo47B|pC@H^bG#9t4L9ERFgYzODT1__n%_8;b_j`YN_-oc$e>-Pzx@PC>uSaWp6Q z*W+(#g=BtUAD@~qh-A6UonEG-We$YsVv9c5+BUzmpEbxTetQ#AMehth{1@>zS6-#K zx-R~r?h~u=VuuDrjCym`!oXAg$!@mLnz?E-+{>|)0VAWz=AYAQU+L$UuNy^?DWBg3 zO%t`aB>`Hb>ZLw4f}ZNj>a^g%4^gwdGEA(T&8}yZ!`5EIfa4!aW258SxuiE*0W$9C zUG-kqdTFLGlDs)`$d}^#4`>Ngqx8a$1xp!jL!&GKjXNA9Ka<^R^%)4dEgN|ui{O*M z%hi^(iP;$=1AY$`e>Eop5_7G=G1VMfi%%`vhtW$jg1AO)afj)qYg~5-1Df;`IXr7B zel~vdF8M_o^S;MmY&(hjL#4aTQ!^C*V-|`#S-z^Z-QC!(&pkz5L4t<5uPusv9QayM zTNO{g*YDAYi=iD6yl|mMDE1wfcbZoUy)@pbwOekwf9_hJMnc1(?zZt2C@+sVjt6AZ zLWjKwTqneFFjyLzzSG%%psxplqk&`yZXplN4Z@B5E{iQKt(GduqAfQJFp*>6(`#4) z2kW?Z5^a8)?0FJ{I@6T?G}BzJUfTuPD&u+N=JbV85;L_ejeTAj{Uai&?jh$f|8YAA zo?sOcI{0!fs=0c`?5JN2cW6#lJ8SDu%X|~=`#G1qIisevAanoCAIeEvXi{1YDFp~- z4T4Vh+&{&bPyMiNSyHcepQAthPj@K7)$5t=6`BI;#(rg~A>NMv5bfd|<5W1(YW^%X2&O(! zv5-;Th3!t<@IEK!HfwQY8uos(-H~sgDkxyQV?lCYpu~&XzgHlV?=-w5U7B@@U3v>V zQYNz40@NPX9LHbWZ!*I6(9!YoVeBUb7WZMMYu9)Bc@xW0t{mwrjes&@#qS)?Y{W@pc()lcQj_YdPF5ey1*7D$g z7kHf8{R4vGmz%}0)oAJw#S|)UdB?TiT=~1V2KniU0i`l@ZcFIG>e6$(k@)OG8=C|W zxX0o(Jh7_=@?(G(Mm{gCH+S3DnwFoPv%~9c zzJ+TFHK;i@jJHCN@Jc+6%}BgDR||_YD3Y|Y`cLL_U2Nfnq>9*m8?_&M?P4@I_&+Vz zd5$yg&+ybkqdUA{O@h?kT@aC!>I{-=8FtY|XnUc=uq0>S&oCbDW+T~#;)9ykQAlx( zaX2)>iG;TCQTvU~_QIXdgL5?Z&xuJ^|El^9z&u3D9iH~C|0|~4@Gi~rT$OtXxmlkA zh_Lm!arE5ee?O@(+%4Cl{lgI-A?GeVFw^Brx2i+`Nd44niZ!qBmg{7Wy4i7}#^dm^ z`QJN-{Wx*cnoo!ZZwOE5loh=>p})V^Y^>ncPDDiRP?=;*;`2sJe>OMQB*rsptC|zb zBaBul7PF{hWd**9j-`K!R;L5CnBYXm22>i1D{@3?LoH~0OD>OiO7^pxj@rseEpwK# zSFd$r*m#x3Yj6F5t1LJO%(WM&1`X|TOxYD{h91Fid}=h=Gx+{a1ceR%s27zA{y9%eb&S}r$WqA{+C zwU7me_<34T8w&|)()WATEQf4Mzm;Tc)w7DASK@5e(a={k7jU^~{o0~Ki=`<=68kVg&XY5N;T^zq5cc{tt}ZTL$9JJI2ha2;B*H!a_6*nO z;X4(T&iLq|=&n3z-rUhZcs}hJ6rFffaLMtH>*B=e}^brNxW> zgOykiSvG3QLYUNgf)gC4O8oZ@1|SO!ZJWidw+YBPTN4?JBA5jWkb_J*Hf7@c2wi9-6vZ`%^ zJ=z>Z=?UHmsLqV*O_tu4-OO$c(i4tUfeq%`@%(L$?l(SSFW}gUBKFQbjAjfq{-p|X z(v`+$ATj`PssGuNDwDeD@|NnGprETy=S}-Bi>lgk|6T9Gd)^BdL3EZ@+`VO5%L1+_ zq~2#O1eeeg8w|j+$zEVfpX7)Np?Eardi3&=At;m5ns~IoG^lYvWYKw5!KCSVaeV8E zj&E{J;9T8Q&6&@NbiG3OIvP}uAZhE{imyiqI34nFsj?!2A}$Q zPM~CSb_She%JkOFe0-eCc?qxR@#5Sc;6jR+xxNj2&iJWGmTMSQ{sZ$3X2Iy2at0w$Koko5{nNl{=BfPfDOM*DJk;%ZTF2AsQ`hz|bKKB?X9}6i}1~c)}+~BO2HSRAa4p1zV zOnE*UGl{pbzn^*I@*XT~Yw$kN?YO*A!hMC9jgBSxZ`(1`q~)|a*oh2)Gwk2-T_$P- z^3A0MC%WRY#mN+s^z=DCw`|~J9>;qJw1xh~PLCwM^iqtoFyKY;AJ*0TFz^kkQ=JpC zw58$0F-5G(7Q+-{v~m7&=_AX^DlWOwvspp@$ES>M6W7^Lx`GHXJxPSQoGJ1(9^8{) z;G1}aOn7Lrji!KOINNAG77UEwr11B62XQPz3>? z1P{<+H4~y#HQXHLQ#6oes1~yb6Eo9K;|Y5OmD&!G68w!M{6VDOIB}dlbK+X{Cw_H5 z#*jj<^%SOm{-xK)JK0+Tx7hmylQjz8qM3(^A61occh(x&Z{q-XH8s8M(3OTGW`Xb3 z3AymHAn8mpsgoiRr0r&qF(g0%AeN@Df$u2jJ&A%1Pi`D+M!k+P$*VW_Ai$($G?kG#AkCJFohcs=eg~tw*IIlQ>YQ>H)?~wlGySZys z%ylLd@usO}5i|q|O7DtCB+6Hfzv=%y`g`(aK3pb55L(FQ^f)rmqQ2Q;zeOSL>?BLh zPLmK;$Xu8;9o3{rQ9w*peCdVWH7+DZ|Dir>-993Mp-2;rhsQT_Ct{~XE&sdy^21Jt zT3Y~-X|u>a#-5k|o0|&{->#j&n0GNuuRRXqH0<)n=c=54w?-T#zXXB{j*_Xb`pP4z zgrdGfG2uo1AFcfD&R9()lV-o`L7bOAUR6JrC7)gx@$Am-x+?IikZ7{ooThaE_l51) zsKl@0vtlYdluaS=i5In$EbJIU|8!j>C?ZH*aOpMT4xlqKF(!?WBD@MooKZms29^~0 zktDM3W>Rkpafa=f;7-O^@kBCvzE;gT z%nmKhie#KB3raoCa{~QjJ8fY$hB;!MGuPO`HCC zLZ>Gujw-Jz%p&5~F5!}>ff+ZYzK@oxJZ;X1mbC^MSC=YBi!HHj!)-O38|R+AZ2JK# zJ?yY;5RBP8(#i-QagM@15V%W9J_t&v5?(y4AqgOZDM4bGy1I9}wlDqG!jyrJ(itKo zoXc*_E$lfu!?YtO=F_*@N+Y}`-MJQ}*6%R?e*8O4UxvR#+uXW1-{!}GjE(l*$Ik+` zWCA(lH;yz>JPcUbuqoYKCf;1sMmZutpG6Qs6z&T-5ayu_6rk{*ny*3QT>Q0{-UDd& zU-U_ucjSKsXNA#3GzMcL4QnReL4t%m(#9-y+B2=MMlfEUuQO_n`uMXBvU;g>hL|_> zAD!+GS+O*^ivLM-O)OvGIIBAo8g*`UFt4!ecAtKW`9r*En7(Qx)piIp#Bu%$s0YSg z?X?eaH3D_u0aHi4N{hSRziiI$`~&Pf>xi;MwMlA=4O)Wsuhp{OR_P0U0l397-#K3S zKP3oT^N=OeIzGzHl>GeQ_sBH-b;fm>j@uG{qyy%5EFWrQ=aBO9?d{#OgDz9`Uq|(T zHgxjwM)q$$hPTeBcqs@$vT3!6LRV_SepMMuV{6p?Kl=Gj#+$(HNkTHS`iu*n5k3Po z?Rv|Ont*#dx45I^wPPlt{bZ;dGy%$|s8Lrs)(41lfsZ?1!%}4eOzBf$91E?!phLat zqNchupr(-|gv7)!D+8ce%|-`yjfD~t=pGP*ud9=kSToOQ{nwf=-y8v`7QGoBgIaEe zh&B}qXa3c9oe_h!Qt=+K0q;p=lPpMq`{N9bdJ# z6qELV*l=%du`hQn+(nU5}5O8^vxFv@sKIogzsn5vUQjB zL)2+Xvrx_>O2`rAl&iisaKAeyf(hk;x)C-vXo(Z}5)80ymX&SG6(U}AHuSnUy2@yM zwQZnt8HzYmeKLr?@Ps1a_bmDLg*_og?M@t1p>%-5?n zQ1lyG>%F}Kk}urKwFA;u@*qL*?je?=b^7DGW+H6>09_p)Pfd2USDF4tmVxHrKSmXV z{J?kG=qt)9+H1-e9kO&BwIuF-a7#A%LsSkGOC7JG2s6WFn?WU6zOM%j$q))4Zu}i( zc2_3~zd~ZFBZyp!9At?8&=@NZ%1DG%keWn|DECJscC1(bu&Z}z^W!qh*85663ZR3~ zu~q;0$4q!?d6Fh=)h>T2eBEVa>03;FZP6KxFrs+Im|jREHg%9<`k0N?i`0#dr=qrT z`o6|q#@VpS(P~5X;_#xCp=>A@eYa}2I9|o8Nh>5VQV_u60dfa}QcA~9g#w_U>F0_D z0R8TFuQh8~vC__buEWrbBoRPv^+(D0^sxaetc*E~ux%`JTSbR6W4O6Aq!Hl$7t%Zf% zyN*Q_(thUxPR$R>s~@( z#S5egiMwpjQb{IT%L;)F@H%(EnXdDE=DT0!e~8`yz=^Nheu{#5h*P`cXI6{}>6b2R z2|bt)oaWUp?|wLOmv4x(W0OTw#!KLru=C$@usYSC;#anQ&WJEKc=LW{?fE1?MYR5D zQ{tOkYSkFCkrMxNkN~iKWVLTpUBYl-hi04Zt|GWv#1aX@TRPM2TEsFifyf?LbI+q^<~M>pa6`vsOcujNBRZa<+^pzR<>IE?1! z0_?d3o1NrC?u`MEiyGlEg^&G-YQW>c;H^ZbT{>c1^_OIie@}F?l8< z!6v2r%zjt<>v0pQYqJyx4wzNwWXBOG_llMl4SJgO=32&2I(`7s%WGTfe-OTBk=K{H#KPDie zZCxYn%w@e6oRA3pzx=?SCTbOoyiKdYMcPWkwwP1zpaj6How0FtwWhOLeg)eejAPE* zKsl0@Cj=urO4{&QfKaB*VfD))#^ddKYSkv5T!D2D%&6yC|EDK;e)OXV=>{2pg!_$o z`xI|(z_F{cz0LmXkesH?9c&?WxpOwbhwRpkRq6~MxcsNc?d054y4dTfGvi97g2o@I zZJ}O(KvXm}z2Fa23j|8Xxl7p(B-ascU%lN{bx&-JhB=@zaK_&-V~0qqdSH{qhIh6G zQhGd7jAyx%6@k9kNcSLA+ShVDT{QUu%Dbe!@*5Yuv&F8hIPm)AwMqEzt(k|X>SL|b z+l#jXSldyLnCM7?VV(lHxfn&|sDkZuJd!g?Q}zltuc_q!9K`d}V$c|f-#1V(pjPvg`&e?8trHF`<4SE3!A(Mq! zMJxvJ`3vtq08G1FQ$YdFoC!SGV_b%mb$M^n42Rzygbpz{q9hQNzQ5LlAqBKcCbgLy^QA5&bBP^!Lzqv@a;D z>h5s2_KJjKM?fvm@T6zgkkw$n*Fe@o@cqHrs&C+e+XJC=BsxcfqcGDU+(Y9~b3yYb z9O1v#rw3h}q5`5Vuan_O5aR(3loQtBheS<+mSBD8pd8ol%^W*s7DO0B0VPGD?XYHv z&n(Nyq`-1$o$5apu{)w%=wNLWFp>i0#F113MX!T=FxGdwkB7|9lhdFp1bpbX_#vh3 zc|k-h98@*fKE$`DlZ?DW^6kYe=K9E0dCuN=XT%{~y>%K|n?i^aNNWgYo)Z3CltY8j zHhfn_HUhyvxjc1T3jQ_t1fd4q?f^{WRY-JF%)EZis+&N2EOvf> zxMquVCmo?N+an$PoT_;?nft%bB288q#XftG%Ou^_Xvll(*Depqwi7%{ogz-{*o{OnionShare +

    +
    +

    +

    Send Files

    +

    Select the files you want to send, then click "Send Files"...

    +
    +

    +

    +
    +
    +
    + From 2e31db85438421ac47dda7d5b97000249d721539 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 8 Mar 2018 05:43:45 -0800 Subject: [PATCH 16/36] Fix settings test because I moved the default downloads dir --- test/test_onionshare_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py index acf5cc6a..88997749 100644 --- a/test/test_onionshare_settings.py +++ b/test/test_onionshare_settings.py @@ -68,7 +68,7 @@ class TestSettings: 'private_key': '', 'slug': '', 'hidservauth_string': '', - 'downloads_dir': os.path.expanduser('~/Downloads') + 'downloads_dir': os.path.expanduser('~/OnionShare') } def test_fill_in_defaults(self, settings_obj): From 2e6538b7f8ab9df013e0e1cb438a7c068e94d7bc Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 8 Mar 2018 05:45:07 -0800 Subject: [PATCH 17/36] Move ZipWriter from common into web, because that's the only place it's used --- onionshare/common.py | 49 ------------------ onionshare/web.py | 51 ++++++++++++++++++- test/conftest.py | 6 +-- test/test_onionshare_common.py | 58 ---------------------- test/test_onionshare_web.py | 90 ++++++++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 111 deletions(-) create mode 100644 test/test_onionshare_web.py diff --git a/onionshare/common.py b/onionshare/common.py index 0d00c7b1..218d5e6c 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -28,7 +28,6 @@ import sys import tempfile import threading import time -import zipfile debug = False @@ -226,54 +225,6 @@ def dir_size(start_path): return total_size -class ZipWriter(object): - """ - ZipWriter accepts files and directories and compresses them into a zip file - with. If a zip_filename is not passed in, it will use the default onionshare - filename. - """ - def __init__(self, zip_filename=None, processed_size_callback=None): - if zip_filename: - self.zip_filename = zip_filename - else: - self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), random_string(4, 6)) - - self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True) - self.processed_size_callback = processed_size_callback - if self.processed_size_callback is None: - self.processed_size_callback = lambda _: None - self._size = 0 - self.processed_size_callback(self._size) - - def add_file(self, filename): - """ - Add a file to the zip archive. - """ - self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED) - self._size += os.path.getsize(filename) - self.processed_size_callback(self._size) - - def add_dir(self, filename): - """ - Add a directory, and all of its children, to the zip archive. - """ - dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/' - for dirpath, dirnames, filenames in os.walk(filename): - for f in filenames: - full_filename = os.path.join(dirpath, f) - if not os.path.islink(full_filename): - arc_filename = full_filename[len(dir_to_strip):] - self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED) - self._size += os.path.getsize(full_filename) - self.processed_size_callback(self._size) - - def close(self): - """ - Close the zip archive. - """ - self.z.close() - - class close_after_seconds(threading.Thread): """ Background thread sleeps t hours and returns. diff --git a/onionshare/web.py b/onionshare/web.py index 68684651..c797a9e4 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -26,6 +26,7 @@ import queue import socket import sys import tempfile +import zipfile from distutils.version import LooseVersion as Version from urllib.request import urlopen @@ -324,7 +325,7 @@ class Web(object): self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) # zip up the files and folders - z = common.ZipWriter(processed_size_callback=processed_size_callback) + z = ZipWriter(processed_size_callback=processed_size_callback) for info in self.file_info['files']: z.add_file(info['filename']) for info in self.file_info['dirs']: @@ -415,3 +416,51 @@ class Web(object): urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, shutdown_slug)).read() except: pass + + +class ZipWriter(object): + """ + ZipWriter accepts files and directories and compresses them into a zip file + with. If a zip_filename is not passed in, it will use the default onionshare + filename. + """ + def __init__(self, zip_filename=None, processed_size_callback=None): + if zip_filename: + self.zip_filename = zip_filename + else: + self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), common.random_string(4, 6)) + + self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True) + self.processed_size_callback = processed_size_callback + if self.processed_size_callback is None: + self.processed_size_callback = lambda _: None + self._size = 0 + self.processed_size_callback(self._size) + + def add_file(self, filename): + """ + Add a file to the zip archive. + """ + self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED) + self._size += os.path.getsize(filename) + self.processed_size_callback(self._size) + + def add_dir(self, filename): + """ + Add a directory, and all of its children, to the zip archive. + """ + dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/' + for dirpath, dirnames, filenames in os.walk(filename): + for f in filenames: + full_filename = os.path.join(dirpath, f) + if not os.path.islink(full_filename): + arc_filename = full_filename[len(dir_to_strip):] + self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED) + self._size += os.path.getsize(full_filename) + self.processed_size_callback(self._size) + + def close(self): + """ + Close the zip archive. + """ + self.z.close() diff --git a/test/conftest.py b/test/conftest.py index 8f10162b..88a7c054 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -8,7 +8,7 @@ import tempfile import pytest -from onionshare import common +from onionshare import common, web @pytest.fixture def temp_dir_1024(): @@ -64,7 +64,7 @@ def temp_file_1024_delete(): # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture(scope='session') def custom_zw(): - zw = common.ZipWriter( + zw = web.ZipWriter( zip_filename=common.random_string(4, 6), processed_size_callback=lambda _: 'custom_callback' ) @@ -76,7 +76,7 @@ def custom_zw(): # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture(scope='session') def default_zw(): - zw = common.ZipWriter() + zw = web.ZipWriter() yield zw zw.close() tmp_dir = os.path.dirname(zw.zip_filename) diff --git a/test/test_onionshare_common.py b/test/test_onionshare_common.py index cb864313..66e4a808 100644 --- a/test/test_onionshare_common.py +++ b/test/test_onionshare_common.py @@ -31,12 +31,10 @@ import pytest from onionshare import common -DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$') LOG_MSG_REGEX = re.compile(r""" ^\[Jun\ 06\ 2013\ 11:05:00\] \ TestModule\.\.dummy_func \ at\ 0x[a-f0-9]+>(:\ TEST_MSG)?$""", re.VERBOSE) -RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$') SLUG_REGEX = re.compile(r'^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$') @@ -296,59 +294,3 @@ class TestSetDebug: def test_debug_false(self, set_debug_true): common.set_debug(False) assert common.debug is False - - -class TestZipWriterDefault: - @pytest.mark.parametrize('test_input', ( - 'onionshare_{}.zip'.format(''.join( - random.choice('abcdefghijklmnopqrstuvwxyz234567') for _ in range(6) - )) for _ in range(50) - )) - def test_default_zw_filename_regex(self, test_input): - assert bool(DEFAULT_ZW_FILENAME_REGEX.match(test_input)) - - def test_zw_filename(self, default_zw): - zw_filename = os.path.basename(default_zw.zip_filename) - assert bool(DEFAULT_ZW_FILENAME_REGEX.match(zw_filename)) - - def test_zipfile_filename_matches_zipwriter_filename(self, default_zw): - assert default_zw.z.filename == default_zw.zip_filename - - def test_zipfile_allow_zip64(self, default_zw): - assert default_zw.z._allowZip64 is True - - def test_zipfile_mode(self, default_zw): - assert default_zw.z.mode == 'w' - - def test_callback(self, default_zw): - assert default_zw.processed_size_callback(None) is None - - def test_add_file(self, default_zw, temp_file_1024_delete): - default_zw.add_file(temp_file_1024_delete) - zipfile_info = default_zw.z.getinfo( - os.path.basename(temp_file_1024_delete)) - - assert zipfile_info.compress_type == zipfile.ZIP_DEFLATED - assert zipfile_info.file_size == 1024 - - def test_add_directory(self, temp_dir_1024_delete, default_zw): - previous_size = default_zw._size # size before adding directory - default_zw.add_dir(temp_dir_1024_delete) - assert default_zw._size == previous_size + 1024 - - -class TestZipWriterCustom: - @pytest.mark.parametrize('test_input', ( - common.random_string( - random.randint(2, 50), - random.choice((None, random.randint(2, 50))) - ) for _ in range(50) - )) - def test_random_string_regex(self, test_input): - assert bool(RANDOM_STR_REGEX.match(test_input)) - - def test_custom_filename(self, custom_zw): - assert bool(RANDOM_STR_REGEX.match(custom_zw.zip_filename)) - - def test_custom_callback(self, custom_zw): - assert custom_zw.processed_size_callback(None) == 'custom_callback' diff --git a/test/test_onionshare_web.py b/test/test_onionshare_web.py new file mode 100644 index 00000000..55ff1f85 --- /dev/null +++ b/test/test_onionshare_web.py @@ -0,0 +1,90 @@ +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2017 Micah Lee + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import contextlib +import inspect +import io +import os +import random +import re +import socket +import sys +import zipfile + +import pytest + +from onionshare import common + +DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$') +RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$') + +class TestZipWriterDefault: + @pytest.mark.parametrize('test_input', ( + 'onionshare_{}.zip'.format(''.join( + random.choice('abcdefghijklmnopqrstuvwxyz234567') for _ in range(6) + )) for _ in range(50) + )) + def test_default_zw_filename_regex(self, test_input): + assert bool(DEFAULT_ZW_FILENAME_REGEX.match(test_input)) + + def test_zw_filename(self, default_zw): + zw_filename = os.path.basename(default_zw.zip_filename) + assert bool(DEFAULT_ZW_FILENAME_REGEX.match(zw_filename)) + + def test_zipfile_filename_matches_zipwriter_filename(self, default_zw): + assert default_zw.z.filename == default_zw.zip_filename + + def test_zipfile_allow_zip64(self, default_zw): + assert default_zw.z._allowZip64 is True + + def test_zipfile_mode(self, default_zw): + assert default_zw.z.mode == 'w' + + def test_callback(self, default_zw): + assert default_zw.processed_size_callback(None) is None + + def test_add_file(self, default_zw, temp_file_1024_delete): + default_zw.add_file(temp_file_1024_delete) + zipfile_info = default_zw.z.getinfo( + os.path.basename(temp_file_1024_delete)) + + assert zipfile_info.compress_type == zipfile.ZIP_DEFLATED + assert zipfile_info.file_size == 1024 + + def test_add_directory(self, temp_dir_1024_delete, default_zw): + previous_size = default_zw._size # size before adding directory + default_zw.add_dir(temp_dir_1024_delete) + assert default_zw._size == previous_size + 1024 + + +class TestZipWriterCustom: + @pytest.mark.parametrize('test_input', ( + common.random_string( + random.randint(2, 50), + random.choice((None, random.randint(2, 50))) + ) for _ in range(50) + )) + def test_random_string_regex(self, test_input): + assert bool(RANDOM_STR_REGEX.match(test_input)) + + def test_custom_filename(self, custom_zw): + assert bool(RANDOM_STR_REGEX.match(custom_zw.zip_filename)) + + def test_custom_callback(self, custom_zw): + assert custom_zw.processed_size_callback(None) == 'custom_callback' From 49e352d1313dcd9c8778eb0ed980d3b232f8e8ac Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 8 Mar 2018 05:50:23 -0800 Subject: [PATCH 18/36] Rename close_after_seconds class to ShutdownTimer --- onionshare/common.py | 2 +- onionshare/onionshare.py | 3 ++- onionshare_gui/onionshare_gui.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/onionshare/common.py b/onionshare/common.py index 218d5e6c..d51fcbaf 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -225,7 +225,7 @@ def dir_size(start_path): return total_size -class close_after_seconds(threading.Thread): +class ShutdownTimer(threading.Thread): """ Background thread sleeps t hours and returns. """ diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 85bfaf22..02da4b62 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -21,6 +21,7 @@ along with this program. If not, see . import os, shutil from . import common, strings +from .common import ShutdownTimer class OnionShare(object): """ @@ -74,7 +75,7 @@ class OnionShare(object): return if self.shutdown_timeout > 0: - self.shutdown_timer = common.close_after_seconds(self.shutdown_timeout) + self.shutdown_timer = ShutdownTimer(self.shutdown_timeout) self.onion_host = self.onion.start_onion_service(self.port) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 947499ed..3e7addce 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -24,6 +24,7 @@ import queue from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings, common +from onionshare.common import ShutdownTimer from onionshare.settings import Settings from onionshare.onion import * @@ -468,7 +469,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.timeout = now.secsTo(self.server_status.timeout) # Set the shutdown timeout value if self.timeout > 0: - self.app.shutdown_timer = common.close_after_seconds(self.timeout) + self.app.shutdown_timer = ShutdownTimer(self.timeout) self.app.shutdown_timer.start() # The timeout has actually already passed since the user clicked Start. Probably the Onion service took too long to start. else: From 50409167d484a6431efa646fd40d51d02e9e88b2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 8 Mar 2018 10:18:31 -0800 Subject: [PATCH 19/36] Completely refactor common to make a Common class, and pass that class down into all parts of the program --- onionshare/__init__.py | 20 +- onionshare/common.py | 331 +++++++++++------------- onionshare/onion.py | 62 ++--- onionshare/onionshare.py | 16 +- onionshare/settings.py | 18 +- onionshare/web.py | 33 +-- onionshare_gui/__init__.py | 29 ++- onionshare_gui/alert.py | 11 +- onionshare_gui/downloads.py | 21 +- onionshare_gui/file_selection.py | 42 +-- onionshare_gui/onionshare_gui.py | 111 ++++---- onionshare_gui/server_status.py | 13 +- onionshare_gui/settings_dialog.py | 87 ++++--- onionshare_gui/tor_connection_dialog.py | 36 +-- onionshare_gui/update_checker.py | 46 ++-- test/test_onionshare_common.py | 24 +- test/test_onionshare_settings.py | 2 +- 17 files changed, 458 insertions(+), 444 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index ec008f5d..1e07f11c 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -20,7 +20,8 @@ along with this program. If not, see . import os, sys, time, argparse, threading -from . import strings, common +from . import strings +from .common import Common from .web import Web from .onion import * from .onionshare import OnionShare @@ -31,11 +32,13 @@ def main(cwd=None): The main() function implements all of the logic that the command-line version of onionshare uses. """ + common = Common() + strings.load_strings(common) - print(strings._('version_string').format(common.get_version())) + print(strings._('version_string').format(common.version)) # OnionShare CLI in OSX needs to change current working directory (#132) - if common.get_platform() == 'Darwin': + if common.platform == 'Darwin': if cwd: os.chdir(cwd) @@ -69,8 +72,7 @@ def main(cwd=None): sys.exit() # Debug mode? - if debug: - common.set_debug(debug) + common.debug = debug # Validate filenames if not receive: @@ -86,7 +88,7 @@ def main(cwd=None): sys.exit() # Load settings - settings = Settings(config) + settings = Settings(common, config) settings.load() # In receive mode, validate downloads dir @@ -105,10 +107,10 @@ def main(cwd=None): sys.exit() # Create the Web object - web = Web(debug, stay_open, False, receive) + web = Web(common, stay_open, False, receive) # Start the Onion object - onion = Onion() + onion = Onion(common) try: onion.connect(settings=False, config=config) except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorNotSupported, BundledTorTimeout) as e: @@ -119,7 +121,7 @@ def main(cwd=None): # Start the onionshare app try: - app = OnionShare(onion, local_only, stay_open, shutdown_timeout) + app = OnionShare(common, onion, local_only, stay_open, shutdown_timeout) app.set_stealth(stealth) app.start_onion_service() except KeyboardInterrupt: diff --git a/onionshare/common.py b/onionshare/common.py index d51fcbaf..36848738 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -29,212 +29,197 @@ import tempfile import threading import time -debug = False - - -def log(module, func, msg=None): +class Common(object): """ - If debug mode is on, log error messages to stdout + The Common object is shared amongst all parts of OnionShare. """ - global debug - if debug: - timestamp = time.strftime("%b %d %Y %X") + def __init__(self, debug=False): + self.debug = debug - final_msg = "[{}] {}.{}".format(timestamp, module, func) - if msg: - final_msg = '{}: {}'.format(final_msg, msg) - print(final_msg) + # The platform OnionShare is running on + self.platform = platform.system() + if self.platform.endswith('BSD'): + self.platform = 'BSD' + # The current version of OnionShare + with open(self.get_resource_path('version.txt')) as f: + self.version = f.read().strip() -def set_debug(new_debug): - global debug - debug = new_debug + def log(self, module, func, msg=None): + """ + If debug mode is on, log error messages to stdout + """ + if self.debug: + timestamp = time.strftime("%b %d %Y %X") + final_msg = "[{}] {}.{}".format(timestamp, module, func) + if msg: + final_msg = '{}: {}'.format(final_msg, msg) + print(final_msg) -def get_platform(): - """ - Returns the platform OnionShare is running on. - """ - plat = platform.system() - if plat.endswith('BSD'): - plat = 'BSD' - return plat + def get_resource_path(self, filename): + """ + Returns the absolute path of a resource, regardless of whether OnionShare is installed + systemwide, and whether regardless of platform + """ + # On Windows, and in Windows dev mode, switch slashes in incoming filename to backslackes + if self.platform == 'Windows': + filename = filename.replace('/', '\\') + if getattr(sys, 'onionshare_dev_mode', False): + # Look for resources directory relative to python file + prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'share') + if not os.path.exists(prefix): + # While running tests during stdeb bdist_deb, look 3 directories up for the share folder + prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(prefix)))), 'share') -def get_resource_path(filename): - """ - Returns the absolute path of a resource, regardless of whether OnionShare is installed - systemwide, and whether regardless of platform - """ - p = get_platform() + elif self.platform == 'BSD' or self.platform == 'Linux': + # Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode + prefix = os.path.join(sys.prefix, 'share/onionshare') - # On Windows, and in Windows dev mode, switch slashes in incoming filename to backslackes - if p == 'Windows': - filename = filename.replace('/', '\\') + elif getattr(sys, 'frozen', False): + # Check if app is "frozen" + # https://pythonhosted.org/PyInstaller/#run-time-information + if self.platform == 'Darwin': + prefix = os.path.join(sys._MEIPASS, 'share') + elif self.platform == 'Windows': + prefix = os.path.join(os.path.dirname(sys.executable), 'share') - if getattr(sys, 'onionshare_dev_mode', False): - # Look for resources directory relative to python file - prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'share') - if not os.path.exists(prefix): - # While running tests during stdeb bdist_deb, look 3 directories up for the share folder - prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(prefix)))), 'share') + return os.path.join(prefix, filename) - elif p == 'BSD' or p == 'Linux': - # Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode - prefix = os.path.join(sys.prefix, 'share/onionshare') + def get_tor_paths(self): + if self.platform == 'Linux': + tor_path = '/usr/bin/tor' + tor_geo_ip_file_path = '/usr/share/tor/geoip' + tor_geo_ipv6_file_path = '/usr/share/tor/geoip6' + obfs4proxy_file_path = '/usr/bin/obfs4proxy' + elif self.platform == 'Windows': + base_path = os.path.join(os.path.dirname(os.path.dirname(self.get_resource_path(''))), 'tor') + tor_path = os.path.join(os.path.join(base_path, 'Tor'), 'tor.exe') + obfs4proxy_file_path = os.path.join(os.path.join(base_path, 'Tor'), 'obfs4proxy.exe') + tor_geo_ip_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip') + tor_geo_ipv6_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip6') + elif self.platform == 'Darwin': + base_path = os.path.dirname(os.path.dirname(os.path.dirname(self.get_resource_path('')))) + tor_path = os.path.join(base_path, 'Resources', 'Tor', 'tor') + tor_geo_ip_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip') + tor_geo_ipv6_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip6') + obfs4proxy_file_path = os.path.join(base_path, 'Resources', 'Tor', 'obfs4proxy') + elif self.platform == 'BSD': + tor_path = '/usr/local/bin/tor' + tor_geo_ip_file_path = '/usr/local/share/tor/geoip' + tor_geo_ipv6_file_path = '/usr/local/share/tor/geoip6' + obfs4proxy_file_path = '/usr/local/bin/obfs4proxy' - elif getattr(sys, 'frozen', False): - # Check if app is "frozen" - # https://pythonhosted.org/PyInstaller/#run-time-information - if p == 'Darwin': - prefix = os.path.join(sys._MEIPASS, 'share') - elif p == 'Windows': - prefix = os.path.join(os.path.dirname(sys.executable), 'share') + return (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path) - return os.path.join(prefix, filename) + def build_slug(self): + """ + Returns a random string made from two words from the wordlist, such as "deter-trig". + """ + with open(self.get_resource_path('wordlist.txt')) as f: + wordlist = f.read().split() + r = random.SystemRandom() + return '-'.join(r.choice(wordlist) for _ in range(2)) -def get_tor_paths(): - p = get_platform() - if p == 'Linux': - tor_path = '/usr/bin/tor' - tor_geo_ip_file_path = '/usr/share/tor/geoip' - tor_geo_ipv6_file_path = '/usr/share/tor/geoip6' - obfs4proxy_file_path = '/usr/bin/obfs4proxy' - elif p == 'Windows': - base_path = os.path.join(os.path.dirname(os.path.dirname(get_resource_path(''))), 'tor') - tor_path = os.path.join(os.path.join(base_path, 'Tor'), 'tor.exe') - obfs4proxy_file_path = os.path.join(os.path.join(base_path, 'Tor'), 'obfs4proxy.exe') - tor_geo_ip_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip') - tor_geo_ipv6_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip6') - elif p == 'Darwin': - base_path = os.path.dirname(os.path.dirname(os.path.dirname(get_resource_path('')))) - tor_path = os.path.join(base_path, 'Resources', 'Tor', 'tor') - tor_geo_ip_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip') - tor_geo_ipv6_file_path = os.path.join(base_path, 'Resources', 'Tor', 'geoip6') - obfs4proxy_file_path = os.path.join(base_path, 'Resources', 'Tor', 'obfs4proxy') - elif p == 'BSD': - tor_path = '/usr/local/bin/tor' - tor_geo_ip_file_path = '/usr/local/share/tor/geoip' - tor_geo_ipv6_file_path = '/usr/local/share/tor/geoip6' - obfs4proxy_file_path = '/usr/local/bin/obfs4proxy' + @staticmethod + def random_string(num_bytes, output_len=None): + """ + Returns a random string with a specified number of bytes. + """ + b = os.urandom(num_bytes) + h = hashlib.sha256(b).digest()[:16] + s = base64.b32encode(h).lower().replace(b'=', b'').decode('utf-8') + if not output_len: + return s + return s[:output_len] - return (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path) - - -def get_version(): - """ - Returns the version of OnionShare that is running. - """ - with open(get_resource_path('version.txt')) as f: - version = f.read().strip() - return version - - -def random_string(num_bytes, output_len=None): - """ - Returns a random string with a specified number of bytes. - """ - b = os.urandom(num_bytes) - h = hashlib.sha256(b).digest()[:16] - s = base64.b32encode(h).lower().replace(b'=', b'').decode('utf-8') - if not output_len: - return s - return s[:output_len] - - -def build_slug(): - """ - Returns a random string made from two words from the wordlist, such as "deter-trig". - """ - with open(get_resource_path('wordlist.txt')) as f: - wordlist = f.read().split() - - r = random.SystemRandom() - return '-'.join(r.choice(wordlist) for _ in range(2)) - - -def human_readable_filesize(b): - """ - Returns filesize in a human readable format. - """ - thresh = 1024.0 - if b < thresh: - return '{:.1f} B'.format(b) - units = ('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB') - u = 0 - b /= thresh - while b >= thresh: + @staticmethod + def human_readable_filesize(b): + """ + Returns filesize in a human readable format. + """ + thresh = 1024.0 + if b < thresh: + return '{:.1f} B'.format(b) + units = ('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB') + u = 0 b /= thresh - u += 1 - return '{:.1f} {}'.format(b, units[u]) + while b >= thresh: + b /= thresh + u += 1 + return '{:.1f} {}'.format(b, units[u]) + @staticmethod + def format_seconds(seconds): + """Return a human-readable string of the format 1d2h3m4s""" + days, seconds = divmod(seconds, 86400) + hours, seconds = divmod(seconds, 3600) + minutes, seconds = divmod(seconds, 60) -def format_seconds(seconds): - """Return a human-readable string of the format 1d2h3m4s""" - days, seconds = divmod(seconds, 86400) - hours, seconds = divmod(seconds, 3600) - minutes, seconds = divmod(seconds, 60) + human_readable = [] + if days: + human_readable.append("{:.0f}d".format(days)) + if hours: + human_readable.append("{:.0f}h".format(hours)) + if minutes: + human_readable.append("{:.0f}m".format(minutes)) + if seconds or not human_readable: + human_readable.append("{:.0f}s".format(seconds)) + return ''.join(human_readable) - human_readable = [] - if days: - human_readable.append("{:.0f}d".format(days)) - if hours: - human_readable.append("{:.0f}h".format(hours)) - if minutes: - human_readable.append("{:.0f}m".format(minutes)) - if seconds or not human_readable: - human_readable.append("{:.0f}s".format(seconds)) - return ''.join(human_readable) + @staticmethod + def estimated_time_remaining(bytes_downloaded, total_bytes, started): + now = time.time() + time_elapsed = now - started # in seconds + download_rate = bytes_downloaded / time_elapsed + remaining_bytes = total_bytes - bytes_downloaded + eta = remaining_bytes / download_rate + return format_seconds(eta) + @staticmethod + def get_available_port(min_port, max_port): + """ + Find a random available port within the given range. + """ + with socket.socket() as tmpsock: + while True: + try: + tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port))) + break + except OSError as e: + raise OSError(e) + _, port = tmpsock.getsockname() + return port -def estimated_time_remaining(bytes_downloaded, total_bytes, started): - now = time.time() - time_elapsed = now - started # in seconds - download_rate = bytes_downloaded / time_elapsed - remaining_bytes = total_bytes - bytes_downloaded - eta = remaining_bytes / download_rate - return format_seconds(eta) - - -def get_available_port(min_port, max_port): - """ - Find a random available port within the given range. - """ - with socket.socket() as tmpsock: - while True: - try: - tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port))) - break - except OSError as e: - raise OSError(e) - _, port = tmpsock.getsockname() - return port - - -def dir_size(start_path): - """ - Calculates the total size, in bytes, of all of the files in a directory. - """ - total_size = 0 - for dirpath, dirnames, filenames in os.walk(start_path): - for f in filenames: - fp = os.path.join(dirpath, f) - if not os.path.islink(fp): - total_size += os.path.getsize(fp) - return total_size + @staticmethod + def dir_size(start_path): + """ + Calculates the total size, in bytes, of all of the files in a directory. + """ + total_size = 0 + for dirpath, dirnames, filenames in os.walk(start_path): + for f in filenames: + fp = os.path.join(dirpath, f) + if not os.path.islink(fp): + total_size += os.path.getsize(fp) + return total_size class ShutdownTimer(threading.Thread): """ Background thread sleeps t hours and returns. """ - def __init__(self, time): + def __init__(self, common, time): threading.Thread.__init__(self) + + self.common = common + self.setDaemon(True) self.time = time def run(self): - log('Shutdown Timer', 'Server will shut down after {} seconds'.format(self.time)) + self.common.log('Shutdown Timer', 'Server will shut down after {} seconds'.format(self.time)) time.sleep(self.time) return 1 diff --git a/onionshare/onion.py b/onionshare/onion.py index 068648ba..4b3b0971 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -125,22 +125,22 @@ class Onion(object): call this function and pass in a status string while connecting to tor. This is necessary for status updates to reach the GUI. """ - def __init__(self): - common.log('Onion', '__init__') + def __init__(self, common): + self.common = common + + self.common.log('Onion', '__init__') self.stealth = False self.service_id = None - self.system = common.get_platform() - # Is bundled tor supported? - if (self.system == 'Windows' or self.system == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False): + if (self.common.platform == 'Windows' or self.common.platform == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False): self.bundle_tor_supported = False else: self.bundle_tor_supported = True # Set the path of the tor binary, for bundled tor - (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths() + (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths() # The tor process self.tor_proc = None @@ -149,13 +149,13 @@ class Onion(object): self.connected_to_tor = False def connect(self, settings=False, config=False, tor_status_update_func=None): - common.log('Onion', 'connect') + self.common.log('Onion', 'connect') # Either use settings that are passed in, or load them from disk if settings: self.settings = settings else: - self.settings = Settings(config) + self.settings = Settings(self.common, config) self.settings.load() # The Tor controller @@ -168,29 +168,29 @@ class Onion(object): # Create a torrc for this session self.tor_data_directory = tempfile.TemporaryDirectory() - if self.system == 'Windows': + if self.common.platform == 'Windows': # Windows needs to use network ports, doesn't support unix sockets - torrc_template = open(common.get_resource_path('torrc_template-windows')).read() + torrc_template = open(self.common.get_resource_path('torrc_template-windows')).read() try: - self.tor_control_port = common.get_available_port(1000, 65535) + self.tor_control_port = self.common.get_available_port(1000, 65535) except: raise OSError(strings._('no_available_port')) self.tor_control_socket = None self.tor_cookie_auth_file = os.path.join(self.tor_data_directory.name, 'cookie') try: - self.tor_socks_port = common.get_available_port(1000, 65535) + self.tor_socks_port = self.common.get_available_port(1000, 65535) except: raise OSError(strings._('no_available_port')) self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc') else: # Linux, Mac and BSD can use unix sockets - with open(common.get_resource_path('torrc_template')) as f: + with open(self.common.get_resource_path('torrc_template')) as f: torrc_template = f.read() self.tor_control_port = None self.tor_control_socket = os.path.join(self.tor_data_directory.name, 'control_socket') self.tor_cookie_auth_file = os.path.join(self.tor_data_directory.name, 'cookie') try: - self.tor_socks_port = common.get_available_port(1000, 65535) + self.tor_socks_port = self.common.get_available_port(1000, 65535) except: raise OSError(strings._('no_available_port')) self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc') @@ -208,17 +208,17 @@ class Onion(object): # Bridge support if self.settings.get('tor_bridges_use_obfs4'): f.write('ClientTransportPlugin obfs4 exec {}\n'.format(self.obfs4proxy_file_path)) - with open(common.get_resource_path('torrc_template-obfs4')) as o: + with open(self.common.get_resource_path('torrc_template-obfs4')) as o: for line in o: f.write(line) elif self.settings.get('tor_bridges_use_meek_lite_amazon'): f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path)) - with open(common.get_resource_path('torrc_template-meek_lite_amazon')) as o: + with open(self.common.get_resource_path('torrc_template-meek_lite_amazon')) as o: for line in o: f.write(line) elif self.settings.get('tor_bridges_use_meek_lite_azure'): f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path)) - with open(common.get_resource_path('torrc_template-meek_lite_azure')) as o: + with open(self.common.get_resource_path('torrc_template-meek_lite_azure')) as o: for line in o: f.write(line) @@ -232,7 +232,7 @@ class Onion(object): # Execute a tor subprocess start_ts = time.time() - if self.system == 'Windows': + if self.common.platform == 'Windows': # In Windows, hide console window when opening tor.exe subprocess startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW @@ -245,7 +245,7 @@ class Onion(object): # Connect to the controller try: - if self.system == 'Windows': + if self.common.platform == 'Windows': self.c = Controller.from_port(port=self.tor_control_port) self.c.authenticate() else: @@ -270,7 +270,7 @@ class Onion(object): if callable(tor_status_update_func): if not tor_status_update_func(progress, summary): # If the dialog was canceled, stop connecting to Tor - common.log('Onion', 'connect', 'tor_status_update_func returned false, canceling connecting to Tor') + self.common.log('Onion', 'connect', 'tor_status_update_func returned false, canceling connecting to Tor') print() return False @@ -322,7 +322,7 @@ class Onion(object): socket_file_path = '' if not found_tor: try: - if self.system == 'Darwin': + if self.common.platform == 'Darwin': socket_file_path = os.path.expanduser('~/Library/Application Support/TorBrowser-Data/Tor/control.socket') self.c = Controller.from_socket_file(path=socket_file_path) @@ -334,11 +334,11 @@ class Onion(object): # guessing the socket file name next if not found_tor: try: - if self.system == 'Linux' or self.system == 'BSD': + if self.common.platform == 'Linux' or self.common.platform == 'BSD': socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid()) - elif self.system == 'Darwin': + elif self.common.platform == 'Darwin': socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid()) - elif self.system == 'Windows': + elif self.common.platform == 'Windows': # Windows doesn't support unix sockets raise TorErrorAutomatic(strings._('settings_error_automatic')) @@ -424,7 +424,7 @@ class Onion(object): Start a onion service on port 80, pointing to the given port, and return the onion hostname. """ - common.log('Onion', 'start_onion_service') + self.common.log('Onion', 'start_onion_service') self.auth_string = None if not self.supports_ephemeral: @@ -447,11 +447,11 @@ class Onion(object): if self.settings.get('private_key'): key_type = "RSA1024" key_content = self.settings.get('private_key') - common.log('Onion', 'Starting a hidden service with a saved private key') + self.common.log('Onion', 'Starting a hidden service with a saved private key') else: key_type = "NEW" key_content = "RSA1024" - common.log('Onion', 'Starting a hidden service with a new private key') + self.common.log('Onion', 'Starting a hidden service with a new private key') try: if basic_auth != None: @@ -498,17 +498,17 @@ class Onion(object): """ Stop onion services that were created earlier. If there's a tor subprocess running, kill it. """ - common.log('Onion', 'cleanup') + self.common.log('Onion', 'cleanup') # Cleanup the ephemeral onion services, if we have any try: onions = self.c.list_ephemeral_hidden_services() for onion in onions: try: - common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion)) + self.common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion)) self.c.remove_ephemeral_hidden_service(onion) except: - common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion)) + self.common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion)) pass except: pass @@ -545,7 +545,7 @@ class Onion(object): """ Returns a (address, port) tuple for the Tor SOCKS port """ - common.log('Onion', 'get_tor_socks_port') + self.common.log('Onion', 'get_tor_socks_port') if self.settings.get('connection_type') == 'bundled': return ('127.0.0.1', self.tor_socks_port) diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 02da4b62..10d73751 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -28,8 +28,10 @@ class OnionShare(object): OnionShare is the main application class. Pass in options and run start_onion_service and it will do the magic. """ - def __init__(self, onion, local_only=False, stay_open=False, shutdown_timeout=0): - common.log('OnionShare', '__init__') + def __init__(self, common, onion, local_only=False, stay_open=False, shutdown_timeout=0): + self.common = common + + self.common.log('OnionShare', '__init__') # The Onion object self.onion = onion @@ -53,7 +55,7 @@ class OnionShare(object): self.shutdown_timer = None def set_stealth(self, stealth): - common.log('OnionShare', 'set_stealth', 'stealth={}'.format(stealth)) + self.common.log('OnionShare', 'set_stealth', 'stealth={}'.format(stealth)) self.stealth = stealth self.onion.stealth = stealth @@ -62,11 +64,11 @@ class OnionShare(object): """ Start the onionshare onion service. """ - common.log('OnionShare', 'start_onion_service') + self.common.log('OnionShare', 'start_onion_service') # Choose a random port try: - self.port = common.get_available_port(17600, 17650) + self.port = self.common.get_available_port(17600, 17650) except: raise OSError(strings._('no_available_port')) @@ -75,7 +77,7 @@ class OnionShare(object): return if self.shutdown_timeout > 0: - self.shutdown_timer = ShutdownTimer(self.shutdown_timeout) + self.shutdown_timer = ShutdownTimer(self.common, self.shutdown_timeout) self.onion_host = self.onion.start_onion_service(self.port) @@ -86,7 +88,7 @@ class OnionShare(object): """ Shut everything down and clean up temporary files, etc. """ - common.log('OnionShare', 'cleanup') + self.common.log('OnionShare', 'cleanup') # cleanup files for filename in self.cleanup_filenames: diff --git a/onionshare/settings.py b/onionshare/settings.py index 9b61beaf..1c7638c0 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -22,7 +22,7 @@ import json import os import platform -from . import strings, common +from . import strings class Settings(object): @@ -32,8 +32,10 @@ class Settings(object): which is to attempt to connect automatically using default Tor Browser settings. """ - def __init__(self, config=False): - common.log('Settings', '__init__') + def __init__(self, common, config=False): + self.common = common + + self.common.log('Settings', '__init__') # Default config self.filename = self.build_filename() @@ -43,11 +45,11 @@ class Settings(object): if os.path.isfile(config): self.filename = config else: - common.log('Settings', '__init__', 'Supplied config does not exist or is unreadable. Falling back to default location') + self.common.log('Settings', '__init__', 'Supplied config does not exist or is unreadable. Falling back to default location') # These are the default settings. They will get overwritten when loading from disk self.default_settings = { - 'version': common.get_version(), + 'version': self.common.version, 'connection_type': 'bundled', 'control_port_address': '127.0.0.1', 'control_port_port': 9051, @@ -110,12 +112,12 @@ class Settings(object): """ Load the settings from file. """ - common.log('Settings', 'load') + self.common.log('Settings', 'load') # If the settings file exists, load it if os.path.exists(self.filename): try: - common.log('Settings', 'load', 'Trying to load {}'.format(self.filename)) + self.common.log('Settings', 'load', 'Trying to load {}'.format(self.filename)) with open(self.filename, 'r') as f: self._settings = json.load(f) self.fill_in_defaults() @@ -126,7 +128,7 @@ class Settings(object): """ Save settings to file. """ - common.log('Settings', 'save') + self.common.log('Settings', 'save') try: os.makedirs(os.path.dirname(self.filename)) diff --git a/onionshare/web.py b/onionshare/web.py index c797a9e4..b6739bcb 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -41,14 +41,16 @@ class Web(object): """ The Web object is the OnionShare web server, powered by flask """ - def __init__(self, debug, stay_open, gui_mode, receive_mode=False): + def __init__(self, common, stay_open, gui_mode, receive_mode=False): + self.common = common + # The flask app self.app = Flask(__name__, static_folder=common.get_resource_path('static'), template_folder=common.get_resource_path('templates')) # Debug mode? - if debug: + if self.common.debug: self.debug_mode() # Stay open after the first download? @@ -107,7 +109,7 @@ class Web(object): self.client_cancel = False # shutting down the server only works within the context of flask, so the easiest way to do it is over http - self.shutdown_slug = common.random_string(16) + self.shutdown_slug = self.common.random_string(16) # Define the ewb app routes self.common_routes() @@ -143,7 +145,7 @@ class Web(object): file_info=self.file_info, filename=os.path.basename(self.zip_filename), filesize=self.zip_filesize, - filesize_human=common.human_readable_filesize(self.zip_filesize))) + filesize_human=self.common.human_readable_filesize(self.zip_filesize))) return self.add_security_headers(r) @self.app.route("//download") @@ -206,10 +208,9 @@ class Web(object): percent = (1.0 * downloaded_bytes / self.zip_filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - plat = common.get_platform() - if not self.gui_mode or plat == 'Linux' or plat == 'BSD': + if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(common.human_readable_filesize(downloaded_bytes), percent)) + "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) sys.stdout.flush() self.add_request(self.REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes}) @@ -224,7 +225,7 @@ class Web(object): fp.close() - if common.get_platform() != 'Darwin': + if self.common.platform != 'Darwin': sys.stdout.write("\n") # Download is finished @@ -315,17 +316,17 @@ class Web(object): } if os.path.isfile(filename): info['size'] = os.path.getsize(filename) - info['size_human'] = common.human_readable_filesize(info['size']) + info['size_human'] = self.common.human_readable_filesize(info['size']) self.file_info['files'].append(info) if os.path.isdir(filename): - info['size'] = common.dir_size(filename) - info['size_human'] = common.human_readable_filesize(info['size']) + info['size'] = self.common.dir_size(filename) + info['size_human'] = self.common.human_readable_filesize(info['size']) self.file_info['dirs'].append(info) self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) # zip up the files and folders - z = ZipWriter(processed_size_callback=processed_size_callback) + z = ZipWriter(self.common, processed_size_callback=processed_size_callback) for info in self.file_info['files']: z.add_file(info['filename']) for info in self.file_info['dirs']: @@ -353,7 +354,7 @@ class Web(object): if persistent_slug: self.slug = persistent_slug else: - self.slug = common.build_slug() + self.slug = self.common.build_slug() def debug_mode(self): """ @@ -424,11 +425,13 @@ class ZipWriter(object): with. If a zip_filename is not passed in, it will use the default onionshare filename. """ - def __init__(self, zip_filename=None, processed_size_callback=None): + def __init__(self, common, zip_filename=None, processed_size_callback=None): + self.common = common + if zip_filename: self.zip_filename = zip_filename else: - self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), common.random_string(4, 6)) + self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6)) self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True) self.processed_size_callback = processed_size_callback diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 205f1c82..04fe1e62 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -22,7 +22,8 @@ import os, sys, platform, argparse from .alert import Alert from PyQt5 import QtCore, QtWidgets -from onionshare import strings, common +from onionshare import strings +from onionshare.common import Common from onionshare.web import Web from onionshare.onion import Onion from onionshare.onionshare import OnionShare @@ -35,9 +36,8 @@ class Application(QtWidgets.QApplication): This is Qt's QApplication class. It has been overridden to support threads and the quick keyboard shortcut. """ - def __init__(self): - system = common.get_platform() - if system == 'Linux' or system == 'BSD': + def __init__(self, common): + if common.platform == 'Linux' or common.platform == 'BSD': self.setAttribute(QtCore.Qt.AA_X11InitThreads, True) QtWidgets.QApplication.__init__(self, sys.argv) self.installEventFilter(self) @@ -54,12 +54,14 @@ def main(): """ The main() function implements all of the logic that the GUI version of onionshare uses. """ + common = Common() + strings.load_strings(common) - print(strings._('version_string').format(common.get_version())) + print(strings._('version_string').format(common.version)) # Start the Qt app global qtapp - qtapp = Application() + qtapp = Application(common) # Parse arguments parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=48)) @@ -84,34 +86,33 @@ def main(): debug = bool(args.debug) # Debug mode? - if debug: - common.set_debug(debug) + common.debug = debug # Validation if filenames: valid = True for filename in filenames: if not os.path.isfile(filename) and not os.path.isdir(filename): - Alert(strings._("not_a_file", True).format(filename)) + Alert(self.common, strings._("not_a_file", True).format(filename)) valid = False if not os.access(filename, os.R_OK): - Alert(strings._("not_a_readable_file", True).format(filename)) + Alert(self.common, strings._("not_a_readable_file", True).format(filename)) valid = False if not valid: sys.exit() # Create the Web object - web = Web(debug, stay_open, True) + web = Web(common, stay_open, True) # Start the Onion - onion = Onion() + onion = Onion(common) # Start the OnionShare app - app = OnionShare(onion, local_only, stay_open, shutdown_timeout) + app = OnionShare(common, onion, local_only, stay_open, shutdown_timeout) # Launch the gui web.stay_open = stay_open - gui = OnionShareGui(web, onion, qtapp, app, filenames, config) + gui = OnionShareGui(common, web, onion, qtapp, app, filenames, config) # Clean up when app quits def shutdown(): diff --git a/onionshare_gui/alert.py b/onionshare_gui/alert.py index 814ff786..981225c6 100644 --- a/onionshare_gui/alert.py +++ b/onionshare_gui/alert.py @@ -19,18 +19,19 @@ along with this program. If not, see . """ from PyQt5 import QtCore, QtWidgets, QtGui -from onionshare import common - class Alert(QtWidgets.QMessageBox): """ An alert box dialog. """ - def __init__(self, message, icon=QtWidgets.QMessageBox.NoIcon, buttons=QtWidgets.QMessageBox.Ok, autostart=True): + def __init__(self, common, message, icon=QtWidgets.QMessageBox.NoIcon, buttons=QtWidgets.QMessageBox.Ok, autostart=True): super(Alert, self).__init__(None) - common.log('Alert', '__init__') + + self.common = common + + self.common.log('Alert', '__init__') self.setWindowTitle("OnionShare") - self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) + self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) self.setText(message) self.setIcon(icon) self.setStandardButtons(buttons) diff --git a/onionshare_gui/downloads.py b/onionshare_gui/downloads.py index 166f14a4..db82d30a 100644 --- a/onionshare_gui/downloads.py +++ b/onionshare_gui/downloads.py @@ -21,11 +21,13 @@ import time from PyQt5 import QtCore, QtWidgets -from onionshare import strings, common +from onionshare import strings class Download(object): - def __init__(self, download_id, total_bytes): + def __init__(self, common, download_id, total_bytes): + self.common = common + self.download_id = download_id self.started = time.time() self.total_bytes = total_bytes @@ -64,7 +66,7 @@ class Download(object): self.progress_bar.setValue(downloaded_bytes) if downloaded_bytes == self.progress_bar.total_bytes: pb_fmt = strings._('gui_download_progress_complete').format( - common.format_seconds(time.time() - self.started)) + self.common.format_seconds(time.time() - self.started)) else: elapsed = time.time() - self.started if elapsed < 10: @@ -72,10 +74,10 @@ class Download(object): # This prevents a "Windows copy dialog"-esque experience at # the beginning of the download. pb_fmt = strings._('gui_download_progress_starting').format( - common.human_readable_filesize(downloaded_bytes)) + self.common.human_readable_filesize(downloaded_bytes)) else: pb_fmt = strings._('gui_download_progress_eta').format( - common.human_readable_filesize(downloaded_bytes), + self.common.human_readable_filesize(downloaded_bytes), self.estimated_time_remaining) self.progress_bar.setFormat(pb_fmt) @@ -85,7 +87,7 @@ class Download(object): @property def estimated_time_remaining(self): - return common.estimated_time_remaining(self.downloaded_bytes, + return self.common.estimated_time_remaining(self.downloaded_bytes, self.total_bytes, self.started) @@ -95,8 +97,11 @@ class Downloads(QtWidgets.QWidget): The downloads chunk of the GUI. This lists all of the active download progress bars. """ - def __init__(self): + def __init__(self, common): super(Downloads, self).__init__() + + self.common = common + self.downloads = {} self.layout = QtWidgets.QVBoxLayout() self.setLayout(self.layout) @@ -108,7 +113,7 @@ class Downloads(QtWidgets.QWidget): self.parent().show() # add it to the list - download = Download(download_id, total_bytes) + download = Download(self.common, download_id, total_bytes) self.downloads[download_id] = download self.layout.insertWidget(-1, download.progress_bar) diff --git a/onionshare_gui/file_selection.py b/onionshare_gui/file_selection.py index 29bcc592..fbc4995b 100644 --- a/onionshare_gui/file_selection.py +++ b/onionshare_gui/file_selection.py @@ -21,21 +21,24 @@ import os from PyQt5 import QtCore, QtWidgets, QtGui from .alert import Alert -from onionshare import strings, common +from onionshare import strings class DropHereLabel(QtWidgets.QLabel): """ When there are no files or folders in the FileList yet, display the 'drop files here' message and graphic. """ - def __init__(self, parent, image=False): + def __init__(self, common, parent, image=False): self.parent = parent super(DropHereLabel, self).__init__(parent=parent) + + self.common = common + self.setAcceptDrops(True) self.setAlignment(QtCore.Qt.AlignCenter) if image: - self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/logo_transparent.png')))) + self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/logo_transparent.png')))) else: self.setText(strings._('gui_drag_and_drop', True)) self.setStyleSheet('color: #999999;') @@ -53,9 +56,12 @@ class DropCountLabel(QtWidgets.QLabel): While dragging files over the FileList, this counter displays the number of files you're dragging. """ - def __init__(self, parent): + def __init__(self, common, parent): self.parent = parent super(DropCountLabel, self).__init__(parent=parent) + + self.common = common + self.setAcceptDrops(True) self.setAlignment(QtCore.Qt.AlignCenter) self.setText(strings._('gui_drag_and_drop', True)) @@ -74,16 +80,19 @@ class FileList(QtWidgets.QListWidget): files_dropped = QtCore.pyqtSignal() files_updated = QtCore.pyqtSignal() - def __init__(self, parent=None): + def __init__(self, common, parent=None): super(FileList, self).__init__(parent) + + self.common = common + self.setAcceptDrops(True) self.setIconSize(QtCore.QSize(32, 32)) self.setSortingEnabled(True) self.setMinimumHeight(205) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - self.drop_here_image = DropHereLabel(self, True) - self.drop_here_text = DropHereLabel(self, False) - self.drop_count = DropCountLabel(self) + self.drop_here_image = DropHereLabel(self.common, self, True) + self.drop_here_text = DropHereLabel(self.common, self, False) + self.drop_count = DropCountLabel(self.common, self) self.resizeEvent(None) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) @@ -206,7 +215,7 @@ class FileList(QtWidgets.QListWidget): if filename not in filenames: if not os.access(filename, os.R_OK): - Alert(strings._("not_a_readable_file", True).format(filename)) + Alert(self.common, strings._("not_a_readable_file", True).format(filename)) return fileinfo = QtCore.QFileInfo(filename) @@ -215,10 +224,10 @@ class FileList(QtWidgets.QListWidget): if os.path.isfile(filename): size_bytes = fileinfo.size() - size_readable = common.human_readable_filesize(size_bytes) + size_readable = self.common.human_readable_filesize(size_bytes) else: - size_bytes = common.dir_size(filename) - size_readable = common.human_readable_filesize(size_bytes) + size_bytes = self.common.dir_size(filename) + size_readable = self.common.human_readable_filesize(size_bytes) # Create a new item item = QtWidgets.QListWidgetItem() @@ -245,7 +254,7 @@ class FileList(QtWidgets.QListWidget): item.item_button = QtWidgets.QPushButton() item.item_button.setDefault(False) item.item_button.setFlat(True) - item.item_button.setIcon( QtGui.QIcon(common.get_resource_path('images/file_delete.png')) ) + item.item_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/file_delete.png')) ) item.item_button.clicked.connect(delete_item) item.item_button.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) @@ -277,12 +286,15 @@ class FileSelection(QtWidgets.QVBoxLayout): The list of files and folders in the GUI, as well as buttons to add and delete the files and folders. """ - def __init__(self): + def __init__(self, common): super(FileSelection, self).__init__() + + self.common = common + self.server_on = False # File list - self.file_list = FileList() + self.file_list = FileList(self.common) self.file_list.itemSelectionChanged.connect(self.update) self.file_list.files_dropped.connect(self.update) self.file_list.files_updated.connect(self.update) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 3e7addce..04b8a066 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -24,7 +24,7 @@ import queue from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings, common -from onionshare.common import ShutdownTimer +from onionshare.common import Common, ShutdownTimer from onionshare.settings import Settings from onionshare.onion import * @@ -47,12 +47,13 @@ class OnionShareGui(QtWidgets.QMainWindow): starting_server_step3 = QtCore.pyqtSignal() starting_server_error = QtCore.pyqtSignal(str) - def __init__(self, web, onion, qtapp, app, filenames, config=False): + def __init__(self, common, web, onion, qtapp, app, filenames, config=False): super(OnionShareGui, self).__init__() - self._initSystemTray() + self.common = common + self.common.log('OnionShareGui', '__init__') - common.log('OnionShareGui', '__init__') + self._initSystemTray() self.web = web self.onion = onion @@ -60,22 +61,22 @@ class OnionShareGui(QtWidgets.QMainWindow): self.app = app self.setWindowTitle('OnionShare') - self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) + self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) self.setMinimumWidth(430) # Load settings self.config = config - self.settings = Settings(self.config) + self.settings = Settings(self.common, self.config) self.settings.load() # File selection - self.file_selection = FileSelection() + self.file_selection = FileSelection(self.common) if filenames: for filename in filenames: self.file_selection.file_list.add_file(filename) # Server status - self.server_status = ServerStatus(self.qtapp, self.app, self.web, self.file_selection, self.settings) + self.server_status = ServerStatus(self.common, self.qtapp, self.app, self.web, self.file_selection, self.settings) self.server_status.server_started.connect(self.file_selection.server_started) self.server_status.server_started.connect(self.start_server) self.server_status.server_started.connect(self.update_server_status_indicator) @@ -107,7 +108,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.filesize_warning.hide() # Downloads - self.downloads = Downloads() + self.downloads = Downloads(self.common) self.downloads_container = QtWidgets.QScrollArea() self.downloads_container.setWidget(self.downloads) self.downloads_container.setWidgetResizable(True) @@ -147,13 +148,13 @@ class OnionShareGui(QtWidgets.QMainWindow): self.settings_button.setDefault(False) self.settings_button.setFlat(True) self.settings_button.setFixedWidth(40) - self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) ) + self.settings_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/settings.png')) ) self.settings_button.clicked.connect(self.open_settings) # Server status indicator on the status bar - self.server_status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png')) - self.server_status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png')) - self.server_status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png')) + self.server_status_image_stopped = QtGui.QImage(self.common.get_resource_path('images/server_stopped.png')) + self.server_status_image_working = QtGui.QImage(self.common.get_resource_path('images/server_working.png')) + self.server_status_image_started = QtGui.QImage(self.common.get_resource_path('images/server_started.png')) self.server_status_image_label = QtWidgets.QLabel() self.server_status_image_label.setFixedWidth(20) self.server_status_label = QtWidgets.QLabel() @@ -221,7 +222,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.timer.timeout.connect(self.check_for_requests) # Start the "Connecting to Tor" dialog, which calls onion.connect() - tor_con = TorConnectionDialog(self.qtapp, self.settings, self.onion) + tor_con = TorConnectionDialog(self.common, self.qtapp, self.settings, self.onion) tor_con.canceled.connect(self._tor_connection_canceled) tor_con.open_settings.connect(self._tor_connection_open_settings) tor_con.start() @@ -244,7 +245,7 @@ class OnionShareGui(QtWidgets.QMainWindow): for index in range(self.file_selection.file_list.count()): item = self.file_selection.file_list.item(index) total_size_bytes += item.size_bytes - total_size_readable = common.human_readable_filesize(total_size_bytes) + total_size_readable = self.common.human_readable_filesize(total_size_bytes) if file_count > 1: self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable)) @@ -259,7 +260,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.adjustSize() def update_server_status_indicator(self): - common.log('OnionShareGui', 'update_server_status_indicator') + self.common.log('OnionShareGui', 'update_server_status_indicator') # Set the status image if self.server_status.status == self.server_status.STATUS_STOPPED: @@ -273,8 +274,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.server_status_label.setText(strings._('gui_status_indicator_started', True)) def _initSystemTray(self): - system = common.get_platform() - menu = QtWidgets.QMenu() self.settingsAction = menu.addAction(strings._('gui_settings_window_title', True)) self.settingsAction.triggered.connect(self.open_settings) @@ -285,10 +284,10 @@ class OnionShareGui(QtWidgets.QMainWindow): self.systemTray = QtWidgets.QSystemTrayIcon(self) # The convention is Mac systray icons are always grayscale - if system == 'Darwin': - self.systemTray.setIcon(QtGui.QIcon(common.get_resource_path('images/logo_grayscale.png'))) + if self.common.platform == 'Darwin': + self.systemTray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo_grayscale.png'))) else: - self.systemTray.setIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) + self.systemTray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) self.systemTray.setContextMenu(menu) self.systemTray.show() @@ -297,10 +296,10 @@ class OnionShareGui(QtWidgets.QMainWindow): If the user cancels before Tor finishes connecting, ask if they want to quit, or open settings. """ - common.log('OnionShareGui', '_tor_connection_canceled') + self.common.log('OnionShareGui', '_tor_connection_canceled') def ask(): - a = Alert(strings._('gui_tor_connection_ask', True), QtWidgets.QMessageBox.Question, buttons=QtWidgets.QMessageBox.NoButton, autostart=False) + a = Alert(self.common, strings._('gui_tor_connection_ask', True), QtWidgets.QMessageBox.Question, buttons=QtWidgets.QMessageBox.NoButton, autostart=False) settings_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_open_settings', True)) quit_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_quit', True)) a.addButton(settings_button, QtWidgets.QMessageBox.AcceptRole) @@ -310,12 +309,12 @@ class OnionShareGui(QtWidgets.QMainWindow): if a.clickedButton() == settings_button: # Open settings - common.log('OnionShareGui', '_tor_connection_canceled', 'Settings button clicked') + self.common.log('OnionShareGui', '_tor_connection_canceled', 'Settings button clicked') self.open_settings() if a.clickedButton() == quit_button: # Quit - common.log('OnionShareGui', '_tor_connection_canceled', 'Quit button clicked') + self.common.log('OnionShareGui', '_tor_connection_canceled', 'Quit button clicked') # Wait 1ms for the event loop to finish, then quit QtCore.QTimer.singleShot(1, self.qtapp.quit) @@ -327,7 +326,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ The TorConnectionDialog wants to open the Settings dialog """ - common.log('OnionShareGui', '_tor_connection_open_settings') + self.common.log('OnionShareGui', '_tor_connection_open_settings') # Wait 1ms for the event loop to finish closing the TorConnectionDialog QtCore.QTimer.singleShot(1, self.open_settings) @@ -336,10 +335,10 @@ class OnionShareGui(QtWidgets.QMainWindow): """ Open the SettingsDialog. """ - common.log('OnionShareGui', 'open_settings') + self.common.log('OnionShareGui', 'open_settings') def reload_settings(): - common.log('OnionShareGui', 'open_settings', 'settings have changed, reloading') + self.common.log('OnionShareGui', 'open_settings', 'settings have changed, reloading') self.settings.load() # We might've stopped the main requests timer if a Tor connection failed. # If we've reloaded settings, we probably succeeded in obtaining a new @@ -356,7 +355,7 @@ class OnionShareGui(QtWidgets.QMainWindow): if not self.settings.get('shutdown_timeout'): self.server_status.shutdown_timeout_container.hide() - d = SettingsDialog(self.onion, self.qtapp, self.config) + d = SettingsDialog(self.common, self.onion, self.qtapp, self.config) d.settings_saved.connect(reload_settings) d.exec_() @@ -368,7 +367,7 @@ class OnionShareGui(QtWidgets.QMainWindow): Start the onionshare server. This uses multiple threads to start the Tor onion server and the web app. """ - common.log('OnionShareGui', 'start_server') + self.common.log('OnionShareGui', 'start_server') self.set_server_active(True) @@ -405,8 +404,8 @@ class OnionShareGui(QtWidgets.QMainWindow): # wait for modules in thread to load, preventing a thread-related cx_Freeze crash time.sleep(0.2) - common.log('OnionshareGui', 'start_server', 'Starting an onion thread') - self.t = OnionThread(function=start_onion_service, kwargs={'self': self}) + self.common.log('OnionshareGui', 'start_server', 'Starting an onion thread') + self.t = OnionThread(self.common, function=start_onion_service, kwargs={'self': self}) self.t.daemon = True self.t.start() @@ -414,7 +413,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ Step 2 in starting the onionshare server. Zipping up files. """ - common.log('OnionShareGui', 'start_server_step2') + self.common.log('OnionShareGui', 'start_server_step2') # add progress bar to the status bar, indicating the crunching of files. self._zip_progress_bar = ZipProgressBar(0) @@ -451,7 +450,7 @@ class OnionShareGui(QtWidgets.QMainWindow): Step 3 in starting the onionshare server. This displays the large filesize warning, if applicable. """ - common.log('OnionShareGui', 'start_server_step3') + self.common.log('OnionShareGui', 'start_server_step3') # Remove zip progress bar if self._zip_progress_bar is not None: @@ -469,7 +468,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.timeout = now.secsTo(self.server_status.timeout) # Set the shutdown timeout value if self.timeout > 0: - self.app.shutdown_timer = ShutdownTimer(self.timeout) + self.app.shutdown_timer = ShutdownTimer(self.common, self.timeout) self.app.shutdown_timer.start() # The timeout has actually already passed since the user clicked Start. Probably the Onion service took too long to start. else: @@ -480,11 +479,11 @@ class OnionShareGui(QtWidgets.QMainWindow): """ If there's an error when trying to start the onion service """ - common.log('OnionShareGui', 'start_server_error') + self.common.log('OnionShareGui', 'start_server_error') self.set_server_active(False) - Alert(error, QtWidgets.QMessageBox.Warning) + Alert(self.common, error, QtWidgets.QMessageBox.Warning) self.server_status.stop_server() if self._zip_progress_bar is not None: self.status_bar.removeWidget(self._zip_progress_bar) @@ -503,7 +502,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ Stop the onionshare server. """ - common.log('OnionShareGui', 'stop_server') + self.common.log('OnionShareGui', 'stop_server') if self.server_status.status != self.server_status.STATUS_STOPPED: try: @@ -527,13 +526,12 @@ class OnionShareGui(QtWidgets.QMainWindow): """ Check for updates in a new thread, if enabled. """ - system = common.get_platform() - if system == 'Windows' or system == 'Darwin': + if self.common.platform == 'Windows' or self.common.platform == 'Darwin': if self.settings.get('use_autoupdate'): def update_available(update_url, installed_version, latest_version): - Alert(strings._("update_available", True).format(update_url, installed_version, latest_version)) + Alert(self.common, strings._("update_available", True).format(update_url, installed_version, latest_version)) - self.update_thread = UpdateThread(self.onion, self.config) + self.update_thread = UpdateThread(self.common, self.onion, self.config) self.update_thread.update_available.connect(update_available) self.update_thread.start() @@ -544,7 +542,7 @@ class OnionShareGui(QtWidgets.QMainWindow): if os.path.isfile(filename): total_size += os.path.getsize(filename) if os.path.isdir(filename): - total_size += common.dir_size(filename) + total_size += Common.dir_size(filename) return total_size def check_for_requests(self): @@ -594,7 +592,7 @@ class OnionShareGui(QtWidgets.QMainWindow): elif event["type"] == self.web.REQUEST_RATE_LIMIT: self.stop_server() - Alert(strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical) + Alert(self.common, strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical) elif event["type"] == self.web.REQUEST_PROGRESS: self.downloads.update_download(event["data"]["id"], event["data"]["bytes"]) @@ -655,7 +653,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ When the URL gets copied to the clipboard, display this in the status bar. """ - common.log('OnionShareGui', 'copy_url') + self.common.log('OnionShareGui', 'copy_url') if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True)) @@ -663,7 +661,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. """ - common.log('OnionShareGui', 'copy_hidservauth') + self.common.log('OnionShareGui', 'copy_hidservauth') if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True)) @@ -697,9 +695,9 @@ class OnionShareGui(QtWidgets.QMainWindow): Update the 'Downloads completed' info widget. """ if count == 0: - self.info_completed_downloads_image = common.get_resource_path('images/download_completed_none.png') + self.info_completed_downloads_image = self.common.get_resource_path('images/download_completed_none.png') else: - self.info_completed_downloads_image = common.get_resource_path('images/download_completed.png') + self.info_completed_downloads_image = self.common.get_resource_path('images/download_completed.png') self.info_completed_downloads_count.setText(' {1:d}'.format(self.info_completed_downloads_image, count)) self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(count)) @@ -708,17 +706,17 @@ class OnionShareGui(QtWidgets.QMainWindow): Update the 'Downloads in progress' info widget. """ if count == 0: - self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress_none.png') + self.info_in_progress_downloads_image = self.common.get_resource_path('images/download_in_progress_none.png') else: - self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress.png') + self.info_in_progress_downloads_image = self.common.get_resource_path('images/download_in_progress.png') self.info_in_progress_downloads_count.setText(' {1:d}'.format(self.info_in_progress_downloads_image, count)) self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(count)) def closeEvent(self, e): - common.log('OnionShareGui', 'closeEvent') + self.common.log('OnionShareGui', 'closeEvent') try: if self.server_status.status != self.server_status.STATUS_STOPPED: - common.log('OnionShareGui', 'closeEvent, opening warning dialog') + self.common.log('OnionShareGui', 'closeEvent, opening warning dialog') dialog = QtWidgets.QMessageBox() dialog.setWindowTitle(strings._('gui_quit_title', True)) dialog.setText(strings._('gui_quit_warning', True)) @@ -803,9 +801,12 @@ class OnionThread(QtCore.QThread): decided to cancel (in which case do not proceed with obtaining the Onion address and starting the web server). """ - def __init__(self, function, kwargs=None): + def __init__(self, common, function, kwargs=None): super(OnionThread, self).__init__() - common.log('OnionThread', '__init__') + + self.common = common + + self.common.log('OnionThread', '__init__') self.function = function if not kwargs: self.kwargs = {} @@ -813,6 +814,6 @@ class OnionThread(QtCore.QThread): self.kwargs = kwargs def run(self): - common.log('OnionThread', 'run') + self.common.log('OnionThread', 'run') self.function(**self.kwargs) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 03540415..62df81ff 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -21,7 +21,7 @@ import platform from .alert import Alert from PyQt5 import QtCore, QtWidgets, QtGui -from onionshare import strings, common, settings +from onionshare import strings class ServerStatus(QtWidgets.QWidget): """ @@ -38,8 +38,11 @@ class ServerStatus(QtWidgets.QWidget): STATUS_WORKING = 1 STATUS_STARTED = 2 - def __init__(self, qtapp, app, web, file_selection, settings): + def __init__(self, common, qtapp, app, web, file_selection, settings): super(ServerStatus, self).__init__() + + self.common = common + self.status = self.STATUS_STOPPED self.qtapp = qtapp @@ -129,7 +132,7 @@ class ServerStatus(QtWidgets.QWidget): if self.status == self.STATUS_STARTED: self.url_description.show() - info_image = common.get_resource_path('images/info.png') + info_image = self.common.get_resource_path('images/info.png') self.url_description.setText(strings._('gui_url_description', True).format(info_image)) # Show a Tool Tip explaining the lifecycle of this URL if self.settings.get('save_private_key'): @@ -212,7 +215,7 @@ class ServerStatus(QtWidgets.QWidget): self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) # If the timeout has actually passed already before the user hit Start, refuse to start the server. if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout: - Alert(strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning)) + Alert(self.common, strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning)) else: self.start_server() else: @@ -252,7 +255,7 @@ class ServerStatus(QtWidgets.QWidget): """ Cancel the server. """ - common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup') + self.common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup') self.status = self.STATUS_WORKING self.shutdown_timeout_reset() self.update() diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 7c81afc6..2bd20d84 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -34,9 +34,12 @@ class SettingsDialog(QtWidgets.QDialog): """ settings_saved = QtCore.pyqtSignal() - def __init__(self, onion, qtapp, config=False): + def __init__(self, common, onion, qtapp, config=False): super(SettingsDialog, self).__init__() - common.log('SettingsDialog', '__init__') + + self.common = common + + self.common.log('SettingsDialog', '__init__') self.onion = onion self.qtapp = qtapp @@ -44,7 +47,7 @@ class SettingsDialog(QtWidgets.QDialog): self.setModal(True) self.setWindowTitle(strings._('gui_settings_window_title', True)) - self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) + self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) self.system = platform.system() @@ -156,7 +159,7 @@ class SettingsDialog(QtWidgets.QDialog): # obfs4 option radio # if the obfs4proxy binary is missing, we can't use obfs4 transports - (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths() + (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths() if not os.path.isfile(self.obfs4proxy_file_path): self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy', True)) self.tor_bridges_use_obfs4_radio.setEnabled(False) @@ -166,7 +169,7 @@ class SettingsDialog(QtWidgets.QDialog): # meek_lite-amazon option radio # if the obfs4proxy binary is missing, we can't use meek_lite-amazon transports - (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths() + (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths() if not os.path.isfile(self.obfs4proxy_file_path): self.tor_bridges_use_meek_lite_amazon_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_amazon_radio_option_no_obfs4proxy', True)) self.tor_bridges_use_meek_lite_amazon_radio.setEnabled(False) @@ -176,7 +179,7 @@ class SettingsDialog(QtWidgets.QDialog): # meek_lite-azure option radio # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports - (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths() + (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths() if not os.path.isfile(self.obfs4proxy_file_path): self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy', True)) self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False) @@ -330,7 +333,7 @@ class SettingsDialog(QtWidgets.QDialog): self.save_button.clicked.connect(self.save_clicked) self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True)) self.cancel_button.clicked.connect(self.cancel_clicked) - version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(common.get_version())) + version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(self.common.version)) version_label.setStyleSheet('color: #666666') self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True)) self.help_button.clicked.connect(self.help_clicked) @@ -372,7 +375,7 @@ class SettingsDialog(QtWidgets.QDialog): self.cancel_button.setFocus() # Load settings, and fill them in - self.old_settings = Settings(self.config) + self.old_settings = Settings(self.common, self.config) self.old_settings.load() close_after_first_download = self.old_settings.get('close_after_first_download') @@ -470,7 +473,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Connection type bundled was toggled. If checked, hide authentication fields. """ - common.log('SettingsDialog', 'connection_type_bundled_toggled') + self.common.log('SettingsDialog', 'connection_type_bundled_toggled') if checked: self.authenticate_group.hide() self.connection_type_socks.hide() @@ -515,7 +518,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Connection type automatic was toggled. If checked, hide authentication fields. """ - common.log('SettingsDialog', 'connection_type_automatic_toggled') + self.common.log('SettingsDialog', 'connection_type_automatic_toggled') if checked: self.authenticate_group.hide() self.connection_type_socks.hide() @@ -526,7 +529,7 @@ class SettingsDialog(QtWidgets.QDialog): Connection type control port was toggled. If checked, show extra fields for Tor control address and port. If unchecked, hide those extra fields. """ - common.log('SettingsDialog', 'connection_type_control_port_toggled') + self.common.log('SettingsDialog', 'connection_type_control_port_toggled') if checked: self.authenticate_group.show() self.connection_type_control_port_extras.show() @@ -541,7 +544,7 @@ class SettingsDialog(QtWidgets.QDialog): Connection type socket file was toggled. If checked, show extra fields for socket file. If unchecked, hide those extra fields. """ - common.log('SettingsDialog', 'connection_type_socket_file_toggled') + self.common.log('SettingsDialog', 'connection_type_socket_file_toggled') if checked: self.authenticate_group.show() self.connection_type_socket_file_extras.show() @@ -554,14 +557,14 @@ class SettingsDialog(QtWidgets.QDialog): """ Authentication option no authentication was toggled. """ - common.log('SettingsDialog', 'authenticate_no_auth_toggled') + self.common.log('SettingsDialog', 'authenticate_no_auth_toggled') def authenticate_password_toggled(self, checked): """ Authentication option password was toggled. If checked, show extra fields for password auth. If unchecked, hide those extra fields. """ - common.log('SettingsDialog', 'authenticate_password_toggled') + self.common.log('SettingsDialog', 'authenticate_password_toggled') if checked: self.authenticate_password_extras.show() else: @@ -572,7 +575,7 @@ class SettingsDialog(QtWidgets.QDialog): Toggle the 'Copy HidServAuth' button to copy the saved HidServAuth to clipboard. """ - common.log('SettingsDialog', 'hidservauth_copy_button_clicked', 'HidServAuth was copied to clipboard') + self.common.log('SettingsDialog', 'hidservauth_copy_button_clicked', 'HidServAuth was copied to clipboard') clipboard = self.qtapp.clipboard() clipboard.setText(self.old_settings.get('hidservauth_string')) @@ -581,7 +584,7 @@ class SettingsDialog(QtWidgets.QDialog): Test Tor Settings button clicked. With the given settings, see if we can successfully connect and authenticate to Tor. """ - common.log('SettingsDialog', 'test_tor_clicked') + self.common.log('SettingsDialog', 'test_tor_clicked') settings = self.settings_from_fields() try: @@ -600,13 +603,13 @@ class SettingsDialog(QtWidgets.QDialog): onion.connect(settings=settings, config=self.config, tor_status_update_func=tor_status_update_func) # If an exception hasn't been raised yet, the Tor settings work - Alert(strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) + Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) # Clean up onion.cleanup() except (TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorNotSupported, BundledTorTimeout) as e: - Alert(e.args[0], QtWidgets.QMessageBox.Warning) + Alert(self.common, e.args[0], QtWidgets.QMessageBox.Warning) if settings.get('connection_type') == 'bundled': self.tor_status.hide() self._enable_buttons() @@ -615,14 +618,14 @@ class SettingsDialog(QtWidgets.QDialog): """ Check for Updates button clicked. Manually force an update check. """ - common.log('SettingsDialog', 'check_for_updates') + self.common.log('SettingsDialog', 'check_for_updates') # Disable buttons self._disable_buttons() self.qtapp.processEvents() def update_timestamp(): # Update the last checked label - settings = Settings(self.config) + settings = Settings(self.common, self.config) settings.load() autoupdate_timestamp = settings.get('autoupdate_timestamp') self._update_autoupdate_timestamp(autoupdate_timestamp) @@ -636,22 +639,22 @@ class SettingsDialog(QtWidgets.QDialog): # Check for updates def update_available(update_url, installed_version, latest_version): - Alert(strings._("update_available", True).format(update_url, installed_version, latest_version)) + Alert(self.common, strings._("update_available", True).format(update_url, installed_version, latest_version)) close_forced_update_thread() def update_not_available(): - Alert(strings._('update_not_available', True)) + Alert(self.common, strings._('update_not_available', True)) close_forced_update_thread() def update_error(): - Alert(strings._('update_error_check_error', True), QtWidgets.QMessageBox.Warning) + Alert(self.common, strings._('update_error_check_error', True), QtWidgets.QMessageBox.Warning) close_forced_update_thread() def update_invalid_version(): - Alert(strings._('update_error_invalid_latest_version', True).format(e.latest_version), QtWidgets.QMessageBox.Warning) + Alert(self.common, strings._('update_error_invalid_latest_version', True).format(e.latest_version), QtWidgets.QMessageBox.Warning) close_forced_update_thread() - forced_update_thread = UpdateThread(self.onion, self.config, force=True) + forced_update_thread = UpdateThread(self.common, self.onion, self.config, force=True) forced_update_thread.update_available.connect(update_available) forced_update_thread.update_not_available.connect(update_not_available) forced_update_thread.update_error.connect(update_error) @@ -662,7 +665,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Save button clicked. Save current settings to disk. """ - common.log('SettingsDialog', 'save_clicked') + self.common.log('SettingsDialog', 'save_clicked') settings = self.settings_from_fields() if settings: @@ -672,7 +675,7 @@ class SettingsDialog(QtWidgets.QDialog): # the Onion object reboot_onion = False if self.onion.is_authenticated(): - common.log('SettingsDialog', 'save_clicked', 'Connected to Tor') + self.common.log('SettingsDialog', 'save_clicked', 'Connected to Tor') def changed(s1, s2, keys): """ Compare the Settings objects s1 and s2 and return true if any values @@ -694,20 +697,20 @@ class SettingsDialog(QtWidgets.QDialog): reboot_onion = True else: - common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor') + self.common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor') # Tor isn't connected, so try connecting reboot_onion = True # Do we need to reinitialize Tor? if reboot_onion: # Reinitialize the Onion object - common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion') + self.common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion') self.onion.cleanup() - tor_con = TorConnectionDialog(self.qtapp, settings, self.onion) + tor_con = TorConnectionDialog(self.common, self.qtapp, settings, self.onion) tor_con.start() - common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor)) + self.common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor)) if self.onion.is_authenticated() and not tor_con.wasCanceled(): self.settings_saved.emit() @@ -721,9 +724,9 @@ class SettingsDialog(QtWidgets.QDialog): """ Cancel button clicked. """ - common.log('SettingsDialog', 'cancel_clicked') + self.common.log('SettingsDialog', 'cancel_clicked') if not self.onion.is_authenticated(): - Alert(strings._('gui_tor_connection_canceled', True), QtWidgets.QMessageBox.Warning) + Alert(self.common, strings._('gui_tor_connection_canceled', True), QtWidgets.QMessageBox.Warning) sys.exit() else: self.close() @@ -732,7 +735,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Help button clicked. """ - common.log('SettingsDialog', 'help_clicked') + self.common.log('SettingsDialog', 'help_clicked') help_site = 'https://github.com/micahflee/onionshare/wiki' QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_site)) @@ -740,8 +743,8 @@ class SettingsDialog(QtWidgets.QDialog): """ Return a Settings object that's full of values from the settings dialog. """ - common.log('SettingsDialog', 'settings_from_fields') - settings = Settings(self.config) + self.common.log('SettingsDialog', 'settings_from_fields') + settings = Settings(self.common, self.config) settings.load() # To get the last update timestamp settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked()) @@ -846,24 +849,24 @@ class SettingsDialog(QtWidgets.QDialog): new_bridges = ''.join(new_bridges) settings.set('tor_bridges_use_custom_bridges', new_bridges) else: - Alert(strings._('gui_settings_tor_bridges_invalid', True)) + Alert(self.common, strings._('gui_settings_tor_bridges_invalid', True)) settings.set('no_bridges', True) return False return settings def closeEvent(self, e): - common.log('SettingsDialog', 'closeEvent') + self.common.log('SettingsDialog', 'closeEvent') # On close, if Tor isn't connected, then quit OnionShare altogether if not self.onion.is_authenticated(): - common.log('SettingsDialog', 'closeEvent', 'Closing while not connected to Tor') + self.common.log('SettingsDialog', 'closeEvent', 'Closing while not connected to Tor') # Wait 1ms for the event loop to finish, then quit QtCore.QTimer.singleShot(1, self.qtapp.quit) def _update_autoupdate_timestamp(self, autoupdate_timestamp): - common.log('SettingsDialog', '_update_autoupdate_timestamp') + self.common.log('SettingsDialog', '_update_autoupdate_timestamp') if autoupdate_timestamp: dt = datetime.datetime.fromtimestamp(autoupdate_timestamp) @@ -880,7 +883,7 @@ class SettingsDialog(QtWidgets.QDialog): self._enable_buttons() def _disable_buttons(self): - common.log('SettingsDialog', '_disable_buttons') + self.common.log('SettingsDialog', '_disable_buttons') self.check_for_updates_button.setEnabled(False) self.connection_type_test_button.setEnabled(False) @@ -888,7 +891,7 @@ class SettingsDialog(QtWidgets.QDialog): self.cancel_button.setEnabled(False) def _enable_buttons(self): - common.log('SettingsDialog', '_enable_buttons') + self.common.log('SettingsDialog', '_enable_buttons') # We can't check for updates if we're still not connected to Tor if not self.onion.connected_to_tor: self.check_for_updates_button.setEnabled(False) diff --git a/onionshare_gui/tor_connection_dialog.py b/onionshare_gui/tor_connection_dialog.py index dc472725..6d127df9 100644 --- a/onionshare_gui/tor_connection_dialog.py +++ b/onionshare_gui/tor_connection_dialog.py @@ -19,7 +19,7 @@ along with this program. If not, see . """ from PyQt5 import QtCore, QtWidgets, QtGui -from onionshare import strings, common +from onionshare import strings from onionshare.onion import * from .alert import Alert @@ -30,16 +30,19 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): """ open_settings = QtCore.pyqtSignal() - def __init__(self, qtapp, settings, onion): + def __init__(self, common, qtapp, settings, onion): super(TorConnectionDialog, self).__init__(None) - common.log('TorConnectionDialog', '__init__') + + self.common = common + + self.common.log('TorConnectionDialog', '__init__') self.qtapp = qtapp self.settings = settings self.onion = onion self.setWindowTitle("OnionShare") - self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) + self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) self.setModal(True) self.setFixedSize(400, 150) @@ -55,9 +58,9 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): self._tor_status_update(0, '') def start(self): - common.log('TorConnectionDialog', 'start') + self.common.log('TorConnectionDialog', 'start') - t = TorConnectionThread(self, self.settings, self.onion) + t = TorConnectionThread(self.common, self, self.settings, self.onion) t.tor_status_update.connect(self._tor_status_update) t.connected_to_tor.connect(self._connected_to_tor) t.canceled_connecting_to_tor.connect(self._canceled_connecting_to_tor) @@ -77,14 +80,14 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): self.setLabelText("{}
    {}".format(strings._('connecting_to_tor', True), summary)) def _connected_to_tor(self): - common.log('TorConnectionDialog', '_connected_to_tor') + self.common.log('TorConnectionDialog', '_connected_to_tor') self.active = False # Close the dialog after connecting self.setValue(self.maximum()) def _canceled_connecting_to_tor(self): - common.log('TorConnectionDialog', '_canceled_connecting_to_tor') + self.common.log('TorConnectionDialog', '_canceled_connecting_to_tor') self.active = False self.onion.cleanup() @@ -92,12 +95,12 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): QtCore.QTimer.singleShot(1, self.cancel) def _error_connecting_to_tor(self, msg): - common.log('TorConnectionDialog', '_error_connecting_to_tor') + self.common.log('TorConnectionDialog', '_error_connecting_to_tor') self.active = False def alert_and_open_settings(): # Display the exception in an alert box - Alert("{}\n\n{}".format(msg, strings._('gui_tor_connection_error_settings', True)), QtWidgets.QMessageBox.Warning) + Alert(self.common, "{}\n\n{}".format(msg, strings._('gui_tor_connection_error_settings', True)), QtWidgets.QMessageBox.Warning) # Open settings self.open_settings.emit() @@ -113,16 +116,19 @@ class TorConnectionThread(QtCore.QThread): canceled_connecting_to_tor = QtCore.pyqtSignal() error_connecting_to_tor = QtCore.pyqtSignal(str) - def __init__(self, dialog, settings, onion): + def __init__(self, common, dialog, settings, onion): super(TorConnectionThread, self).__init__() - common.log('TorConnectionThread', '__init__') + + self.common = common + + self.common.log('TorConnectionThread', '__init__') self.dialog = dialog self.settings = settings self.onion = onion def run(self): - common.log('TorConnectionThread', 'run') + self.common.log('TorConnectionThread', 'run') # Connect to the Onion try: @@ -133,11 +139,11 @@ class TorConnectionThread(QtCore.QThread): self.canceled_connecting_to_tor.emit() except BundledTorCanceled as e: - common.log('TorConnectionThread', 'run', 'caught exception: BundledTorCanceled') + self.common.log('TorConnectionThread', 'run', 'caught exception: BundledTorCanceled') self.canceled_connecting_to_tor.emit() except Exception as e: - common.log('TorConnectionThread', 'run', 'caught exception: {}'.format(e.args[0])) + self.common.log('TorConnectionThread', 'run', 'caught exception: {}'.format(e.args[0])) self.error_connecting_to_tor.emit(str(e.args[0])) def _tor_status_update(self, progress, summary): diff --git a/onionshare_gui/update_checker.py b/onionshare_gui/update_checker.py index 8b4884a2..5dc72091 100644 --- a/onionshare_gui/update_checker.py +++ b/onionshare_gui/update_checker.py @@ -25,7 +25,7 @@ from onionshare import socks from onionshare.settings import Settings from onionshare.onion import Onion -from . import strings, common +from . import strings class UpdateCheckerCheckError(Exception): """ @@ -55,16 +55,19 @@ class UpdateChecker(QtCore.QObject): update_error = QtCore.pyqtSignal() update_invalid_version = QtCore.pyqtSignal() - def __init__(self, onion, config=False): + def __init__(self, common, onion, config=False): super(UpdateChecker, self).__init__() - common.log('UpdateChecker', '__init__') + + self.common = common + + self.common.log('UpdateChecker', '__init__') self.onion = onion self.config = config def check(self, force=False, config=False): - common.log('UpdateChecker', 'check', 'force={}'.format(force)) + self.common.log('UpdateChecker', 'check', 'force={}'.format(force)) # Load the settings - settings = Settings(config) + settings = Settings(self.common, config) settings.load() # If force=True, then definitely check @@ -87,11 +90,11 @@ class UpdateChecker(QtCore.QObject): # Check for updates if check_for_updates: - common.log('UpdateChecker', 'check', 'checking for updates') + self.common.log('UpdateChecker', 'check', 'checking for updates') # Download the latest-version file over Tor try: # User agent string includes OnionShare version and platform - user_agent = 'OnionShare {}, {}'.format(common.get_version(), platform.system()) + user_agent = 'OnionShare {}, {}'.format(self.common.version, self.common.platform) # If the update is forced, add '?force=1' to the URL, to more # accurately measure daily users @@ -104,7 +107,7 @@ class UpdateChecker(QtCore.QObject): else: onion_domain = 'elx57ue5uyfplgva.onion' - common.log('UpdateChecker', 'check', 'loading http://{}{}'.format(onion_domain, path)) + self.common.log('UpdateChecker', 'check', 'loading http://{}{}'.format(onion_domain, path)) (socks_address, socks_port) = self.onion.get_tor_socks_port() socks.set_default_proxy(socks.SOCKS5, socks_address, socks_port) @@ -122,10 +125,10 @@ class UpdateChecker(QtCore.QObject): http_response = s.recv(1024) latest_version = http_response[http_response.find(b'\r\n\r\n'):].strip().decode('utf-8') - common.log('UpdateChecker', 'check', 'latest OnionShare version: {}'.format(latest_version)) + self.common.log('UpdateChecker', 'check', 'latest OnionShare version: {}'.format(latest_version)) except Exception as e: - common.log('UpdateChecker', 'check', '{}'.format(e)) + self.common.log('UpdateChecker', 'check', '{}'.format(e)) self.update_error.emit() raise UpdateCheckerCheckError @@ -145,7 +148,7 @@ class UpdateChecker(QtCore.QObject): # Do we need to update? update_url = 'https://github.com/micahflee/onionshare/releases/tag/v{}'.format(latest_version) - installed_version = common.get_version() + installed_version = self.common.version if installed_version < latest_version: self.update_available.emit(update_url, installed_version, latest_version) return @@ -159,17 +162,20 @@ class UpdateThread(QtCore.QThread): update_error = QtCore.pyqtSignal() update_invalid_version = QtCore.pyqtSignal() - def __init__(self, onion, config=False, force=False): + def __init__(self, common, onion, config=False, force=False): super(UpdateThread, self).__init__() - common.log('UpdateThread', '__init__') + + self.common = common + + self.common.log('UpdateThread', '__init__') self.onion = onion self.config = config self.force = force def run(self): - common.log('UpdateThread', 'run') + self.common.log('UpdateThread', 'run') - u = UpdateChecker(self.onion, self.config) + u = UpdateChecker(self.common, self.onion, self.config) u.update_available.connect(self._update_available) u.update_not_available.connect(self._update_not_available) u.update_error.connect(self._update_error) @@ -179,25 +185,25 @@ class UpdateThread(QtCore.QThread): u.check(config=self.config,force=self.force) except Exception as e: # If update check fails, silently ignore - common.log('UpdateThread', 'run', '{}'.format(e)) + self.common.log('UpdateThread', 'run', '{}'.format(e)) pass def _update_available(self, update_url, installed_version, latest_version): - common.log('UpdateThread', '_update_available') + self.common.log('UpdateThread', '_update_available') self.active = False self.update_available.emit(update_url, installed_version, latest_version) def _update_not_available(self): - common.log('UpdateThread', '_update_not_available') + self.common.log('UpdateThread', '_update_not_available') self.active = False self.update_not_available.emit() def _update_error(self): - common.log('UpdateThread', '_update_error') + self.common.log('UpdateThread', '_update_error') self.active = False self.update_error.emit() def _update_invalid_version(self): - common.log('UpdateThread', '_update_invalid_version') + self.common.log('UpdateThread', '_update_invalid_version') self.active = False self.update_invalid_version.emit() diff --git a/test/test_onionshare_common.py b/test/test_onionshare_common.py index 66e4a808..ae8e4217 100644 --- a/test/test_onionshare_common.py +++ b/test/test_onionshare_common.py @@ -157,13 +157,13 @@ class TestGetAvailablePort: class TestGetPlatform: def test_darwin(self, platform_darwin): - assert common.get_platform() == 'Darwin' + assert common.platform == 'Darwin' def test_linux(self, platform_linux): - assert common.get_platform() == 'Linux' + assert common.platform == 'Linux' def test_windows(self, platform_windows): - assert common.get_platform() == 'Windows' + assert common.platform == 'Windows' # TODO: double-check these tests @@ -235,14 +235,6 @@ class TestGetTorPaths: (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path)) -class TestGetVersion: - def test_get_version(self, sys_onionshare_dev_mode): - with open(common.get_resource_path('version.txt')) as f: - version = f.read().strip() - - assert version == common.get_version() - - class TestHumanReadableFilesize: @pytest.mark.parametrize('test_input,expected', ( (1024 ** 0, '1.0 B'), @@ -284,13 +276,3 @@ class TestLog: line_one, line_two, _ = output.split('\n') assert LOG_MSG_REGEX.match(line_one) assert LOG_MSG_REGEX.match(line_two) - - -class TestSetDebug: - def test_debug_true(self, set_debug_false): - common.set_debug(True) - assert common.debug is True - - def test_debug_false(self, set_debug_true): - common.set_debug(False) - assert common.debug is False diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py index 88997749..377fba16 100644 --- a/test/test_onionshare_settings.py +++ b/test/test_onionshare_settings.py @@ -28,7 +28,7 @@ from onionshare import common, settings, strings @pytest.fixture def custom_version(monkeypatch): - monkeypatch.setattr(common, 'get_version', lambda: 'DUMMY_VERSION_1.2.3') + monkeypatch.setattr(common, 'version', 'DUMMY_VERSION_1.2.3') @pytest.fixture From c2fecf8aa47af69636eba0b46366f0e430fc8250 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 13 Mar 2018 02:22:26 -0700 Subject: [PATCH 20/36] Fix tests after refactoring Common --- onionshare/common.py | 2 +- test/conftest.py | 19 +++---- test/test_onionshare.py | 4 +- test/test_onionshare_common.py | 85 +++++++++++++++++--------------- test/test_onionshare_settings.py | 20 +++----- test/test_onionshare_strings.py | 18 +++---- test/test_onionshare_web.py | 4 +- 7 files changed, 73 insertions(+), 79 deletions(-) diff --git a/onionshare/common.py b/onionshare/common.py index 36848738..c24a1b4b 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -176,7 +176,7 @@ class Common(object): download_rate = bytes_downloaded / time_elapsed remaining_bytes = total_bytes - bytes_downloaded eta = remaining_bytes / download_rate - return format_seconds(eta) + return Common.format_seconds(eta) @staticmethod def get_available_port(min_port, max_port): diff --git a/test/conftest.py b/test/conftest.py index 88a7c054..e843bbbc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -65,7 +65,8 @@ def temp_file_1024_delete(): @pytest.yield_fixture(scope='session') def custom_zw(): zw = web.ZipWriter( - zip_filename=common.random_string(4, 6), + common.Common(), + zip_filename=common.Common.random_string(4, 6), processed_size_callback=lambda _: 'custom_callback' ) yield zw @@ -76,7 +77,7 @@ def custom_zw(): # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture(scope='session') def default_zw(): - zw = web.ZipWriter() + zw = web.ZipWriter(common.Common()) yield zw zw.close() tmp_dir = os.path.dirname(zw.zip_filename) @@ -118,16 +119,6 @@ def platform_windows(monkeypatch): monkeypatch.setattr('platform.system', lambda: 'Windows') -@pytest.fixture -def set_debug_false(monkeypatch): - monkeypatch.setattr('onionshare.common.debug', False) - - -@pytest.fixture -def set_debug_true(monkeypatch): - monkeypatch.setattr('onionshare.common.debug', True) - - @pytest.fixture def sys_argv_sys_prefix(monkeypatch): monkeypatch.setattr('sys.argv', [sys.prefix]) @@ -157,3 +148,7 @@ def time_time_100(monkeypatch): @pytest.fixture def time_strftime(monkeypatch): monkeypatch.setattr('time.strftime', lambda _: 'Jun 06 2013 11:05:00') + +@pytest.fixture +def common_obj(): + return common.Common() diff --git a/test/test_onionshare.py b/test/test_onionshare.py index 76e471bd..398fd0d3 100644 --- a/test/test_onionshare.py +++ b/test/test_onionshare.py @@ -22,6 +22,7 @@ import os import pytest from onionshare import OnionShare +from onionshare.common import Common class MyOnion: @@ -37,7 +38,8 @@ class MyOnion: @pytest.fixture def onionshare_obj(): - return OnionShare(MyOnion()) + common = Common() + return OnionShare(common, MyOnion()) class TestOnionShare: diff --git a/test/test_onionshare_common.py b/test/test_onionshare_common.py index ae8e4217..c0f9ad66 100644 --- a/test/test_onionshare_common.py +++ b/test/test_onionshare_common.py @@ -29,8 +29,6 @@ import zipfile import pytest -from onionshare import common - LOG_MSG_REGEX = re.compile(r""" ^\[Jun\ 06\ 2013\ 11:05:00\] \ TestModule\.\.dummy_func @@ -38,6 +36,9 @@ LOG_MSG_REGEX = re.compile(r""" SLUG_REGEX = re.compile(r'^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$') +# TODO: Improve the Common tests to test it all as a single class + + class TestBuildSlug: @pytest.mark.parametrize('test_input,expected', ( # VALID, two lowercase words, separated by a hyphen @@ -77,17 +78,17 @@ class TestBuildSlug: assert bool(SLUG_REGEX.match(test_input)) == expected - def test_build_slug_unique(self, sys_onionshare_dev_mode): - assert common.build_slug() != common.build_slug() + def test_build_slug_unique(self, common_obj, sys_onionshare_dev_mode): + assert common_obj.build_slug() != common_obj.build_slug() class TestDirSize: - def test_temp_dir_size(self, temp_dir_1024_delete): + def test_temp_dir_size(self, common_obj, temp_dir_1024_delete): """ dir_size() should return the total size (in bytes) of all files in a particular directory. """ - assert common.dir_size(temp_dir_1024_delete) == 1024 + assert common_obj.dir_size(temp_dir_1024_delete) == 1024 class TestEstimatedTimeRemaining: @@ -101,16 +102,16 @@ class TestEstimatedTimeRemaining: ((971, 1009, 83), '1s') )) def test_estimated_time_remaining( - self, test_input, expected, time_time_100): - assert common.estimated_time_remaining(*test_input) == expected + self, common_obj, test_input, expected, time_time_100): + assert common_obj.estimated_time_remaining(*test_input) == expected @pytest.mark.parametrize('test_input', ( (10, 20, 100), # if `time_elapsed == 0` (0, 37, 99) # if `download_rate == 0` )) - def test_raises_zero_division_error(self, test_input, time_time_100): + def test_raises_zero_division_error(self, common_obj, test_input, time_time_100): with pytest.raises(ZeroDivisionError): - common.estimated_time_remaining(*test_input) + common_obj.estimated_time_remaining(*test_input) class TestFormatSeconds: @@ -129,16 +130,16 @@ class TestFormatSeconds: (129674, '1d12h1m14s'), (56404.12, '15h40m4s') )) - def test_format_seconds(self, test_input, expected): - assert common.format_seconds(test_input) == expected + def test_format_seconds(self, common_obj, test_input, expected): + assert common_obj.format_seconds(test_input) == expected # TODO: test negative numbers? @pytest.mark.parametrize('test_input', ( 'string', lambda: None, [], {}, set() )) - def test_invalid_input_types(self, test_input): + def test_invalid_input_types(self, common_obj, test_input): with pytest.raises(TypeError): - common.format_seconds(test_input) + common_obj.format_seconds(test_input) class TestGetAvailablePort: @@ -146,29 +147,29 @@ class TestGetAvailablePort: (random.randint(1024, 1500), random.randint(1800, 2048)) for _ in range(50) )) - def test_returns_an_open_port(self, port_min, port_max): + def test_returns_an_open_port(self, common_obj, port_min, port_max): """ get_available_port() should return an open port within the range """ - port = common.get_available_port(port_min, port_max) + port = common_obj.get_available_port(port_min, port_max) assert port_min <= port <= port_max with socket.socket() as tmpsock: tmpsock.bind(('127.0.0.1', port)) class TestGetPlatform: - def test_darwin(self, platform_darwin): - assert common.platform == 'Darwin' + def test_darwin(self, platform_darwin, common_obj): + assert common_obj.platform == 'Darwin' - def test_linux(self, platform_linux): - assert common.platform == 'Linux' + def test_linux(self, platform_linux, common_obj): + assert common_obj.platform == 'Linux' - def test_windows(self, platform_windows): - assert common.platform == 'Windows' + def test_windows(self, platform_windows, common_obj): + assert common_obj.platform == 'Windows' # TODO: double-check these tests class TestGetResourcePath: - def test_onionshare_dev_mode(self, sys_onionshare_dev_mode): + def test_onionshare_dev_mode(self, common_obj, sys_onionshare_dev_mode): prefix = os.path.join( os.path.dirname( os.path.dirname( @@ -176,29 +177,29 @@ class TestGetResourcePath: inspect.getfile( inspect.currentframe())))), 'share') assert ( - common.get_resource_path(os.path.join(prefix, 'test_filename')) == + common_obj.get_resource_path(os.path.join(prefix, 'test_filename')) == os.path.join(prefix, 'test_filename')) - def test_linux(self, platform_linux, sys_argv_sys_prefix): + def test_linux(self, common_obj, platform_linux, sys_argv_sys_prefix): prefix = os.path.join(sys.prefix, 'share/onionshare') assert ( - common.get_resource_path(os.path.join(prefix, 'test_filename')) == + common_obj.get_resource_path(os.path.join(prefix, 'test_filename')) == os.path.join(prefix, 'test_filename')) - def test_frozen_darwin(self, platform_darwin, sys_frozen, sys_meipass): + def test_frozen_darwin(self, common_obj, platform_darwin, sys_frozen, sys_meipass): prefix = os.path.join(sys._MEIPASS, 'share') assert ( - common.get_resource_path(os.path.join(prefix, 'test_filename')) == + common_obj.get_resource_path(os.path.join(prefix, 'test_filename')) == os.path.join(prefix, 'test_filename')) class TestGetTorPaths: # @pytest.mark.skipif(sys.platform != 'Darwin', reason='requires MacOS') ? - def test_get_tor_paths_darwin(self, platform_darwin, sys_frozen, sys_meipass): + def test_get_tor_paths_darwin(self, platform_darwin, common_obj, sys_frozen, sys_meipass): base_path = os.path.dirname( os.path.dirname( os.path.dirname( - common.get_resource_path('')))) + common_obj.get_resource_path('')))) tor_path = os.path.join( base_path, 'Resources', 'Tor', 'tor') tor_geo_ip_file_path = os.path.join( @@ -207,20 +208,20 @@ class TestGetTorPaths: base_path, 'Resources', 'Tor', 'geoip6') obfs4proxy_file_path = os.path.join( base_path, 'Resources', 'Tor', 'obfs4proxy') - assert (common.get_tor_paths() == + assert (common_obj.get_tor_paths() == (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path)) # @pytest.mark.skipif(sys.platform != 'Linux', reason='requires Linux') ? - def test_get_tor_paths_linux(self, platform_linux): - assert (common.get_tor_paths() == + def test_get_tor_paths_linux(self, platform_linux, common_obj): + assert (common_obj.get_tor_paths() == ('/usr/bin/tor', '/usr/share/tor/geoip', '/usr/share/tor/geoip6', '/usr/bin/obfs4proxy')) # @pytest.mark.skipif(sys.platform != 'Windows', reason='requires Windows') ? - def test_get_tor_paths_windows(self, platform_windows, sys_frozen): + def test_get_tor_paths_windows(self, platform_windows, common_obj, sys_frozen): base_path = os.path.join( os.path.dirname( os.path.dirname( - common.get_resource_path(''))), 'tor') + common_obj.get_resource_path(''))), 'tor') tor_path = os.path.join( os.path.join(base_path, 'Tor'), 'tor.exe') obfs4proxy_file_path = os.path.join( @@ -231,7 +232,7 @@ class TestGetTorPaths: tor_geo_ipv6_file_path = os.path.join( os.path.join( os.path.join(base_path, 'Data'), 'Tor'), 'geoip6') - assert (common.get_tor_paths() == + assert (common_obj.get_tor_paths() == (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path)) @@ -247,8 +248,8 @@ class TestHumanReadableFilesize: (1024 ** 7, '1.0 ZiB'), (1024 ** 8, '1.0 YiB') )) - def test_human_readable_filesize(self, test_input, expected): - assert common.human_readable_filesize(test_input) == expected + def test_human_readable_filesize(self, common_obj, test_input, expected): + assert common_obj.human_readable_filesize(test_input) == expected class TestLog: @@ -263,14 +264,16 @@ class TestLog: def test_log_msg_regex(self, test_input): assert bool(LOG_MSG_REGEX.match(test_input)) - def test_output(self, set_debug_true, time_strftime): + def test_output(self, common_obj, time_strftime): def dummy_func(): pass + common_obj.debug = True + # From: https://stackoverflow.com/questions/1218933 with io.StringIO() as buf, contextlib.redirect_stdout(buf): - common.log('TestModule', dummy_func) - common.log('TestModule', dummy_func, 'TEST_MSG') + common_obj.log('TestModule', dummy_func) + common_obj.log('TestModule', dummy_func, 'TEST_MSG') output = buf.getvalue() line_one, line_two, _ = output.split('\n') diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py index 377fba16..67fd7b38 100644 --- a/test/test_onionshare_settings.py +++ b/test/test_onionshare_settings.py @@ -26,19 +26,16 @@ import pytest from onionshare import common, settings, strings -@pytest.fixture -def custom_version(monkeypatch): - monkeypatch.setattr(common, 'version', 'DUMMY_VERSION_1.2.3') - - @pytest.fixture def os_path_expanduser(monkeypatch): monkeypatch.setattr('os.path.expanduser', lambda path: path) @pytest.fixture -def settings_obj(custom_version, sys_onionshare_dev_mode, platform_linux): - return settings.Settings() +def settings_obj(sys_onionshare_dev_mode, platform_linux): + _common = common.Common() + _common.version = 'DUMMY_VERSION_1.2.3' + return settings.Settings(_common) class TestSettings: @@ -154,30 +151,27 @@ class TestSettings: def test_filename_darwin( self, - custom_version, monkeypatch, os_path_expanduser, platform_darwin): - obj = settings.Settings() + obj = settings.Settings(common.Common()) assert (obj.filename == '~/Library/Application Support/OnionShare/onionshare.json') def test_filename_linux( self, - custom_version, monkeypatch, os_path_expanduser, platform_linux): - obj = settings.Settings() + obj = settings.Settings(common.Common()) assert obj.filename == '~/.config/onionshare/onionshare.json' def test_filename_windows( self, - custom_version, monkeypatch, platform_windows): monkeypatch.setenv('APPDATA', 'C:') - obj = settings.Settings() + obj = settings.Settings(common.Common()) assert obj.filename == 'C:\\OnionShare\\onionshare.json' def test_set_custom_bridge(self, settings_obj): diff --git a/test/test_onionshare_strings.py b/test/test_onionshare_strings.py index d9fa9896..db941a26 100644 --- a/test/test_onionshare_strings.py +++ b/test/test_onionshare_strings.py @@ -22,7 +22,7 @@ import types import pytest -from onionshare import common, strings +from onionshare import strings # # Stub get_resource_path so it finds the correct path while running tests @@ -44,28 +44,28 @@ def test_underscore_is_function(): class TestLoadStrings: def test_load_strings_defaults_to_english( - self, locale_en, sys_onionshare_dev_mode): + self, common_obj, locale_en, sys_onionshare_dev_mode): """ load_strings() loads English by default """ - strings.load_strings(common) + strings.load_strings(common_obj) assert strings._('wait_for_hs') == "Waiting for HS to be ready:" def test_load_strings_loads_other_languages( - self, locale_fr, sys_onionshare_dev_mode): + self, common_obj, locale_fr, sys_onionshare_dev_mode): """ load_strings() loads other languages in different locales """ - strings.load_strings(common, "fr") + strings.load_strings(common_obj, "fr") assert strings._('wait_for_hs') == "En attente du HS:" def test_load_partial_strings( - self, locale_ru, sys_onionshare_dev_mode): - strings.load_strings(common) + self, common_obj, locale_ru, sys_onionshare_dev_mode): + strings.load_strings(common_obj) assert strings._("give_this_url") == ( "Отправьте эту ссылку тому человеку, " "которому вы хотите передать файл:") assert strings._('wait_for_hs') == "Waiting for HS to be ready:" def test_load_invalid_locale( - self, locale_invalid, sys_onionshare_dev_mode): + self, common_obj, locale_invalid, sys_onionshare_dev_mode): """ load_strings() raises a KeyError for an invalid locale """ with pytest.raises(KeyError): - strings.load_strings(common, 'XX') + strings.load_strings(common_obj, 'XX') diff --git a/test/test_onionshare_web.py b/test/test_onionshare_web.py index 55ff1f85..a80e7098 100644 --- a/test/test_onionshare_web.py +++ b/test/test_onionshare_web.py @@ -29,7 +29,7 @@ import zipfile import pytest -from onionshare import common +from onionshare.common import Common DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$') RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$') @@ -75,7 +75,7 @@ class TestZipWriterDefault: class TestZipWriterCustom: @pytest.mark.parametrize('test_input', ( - common.random_string( + Common.random_string( random.randint(2, 50), random.choice((None, random.randint(2, 50))) ) for _ in range(50) From 76d299a6c90f4fc7a899e3c19da7ac6f23c1389e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 13 Mar 2018 03:28:47 -0700 Subject: [PATCH 21/36] Move settings into the Common object, so the settings are available to all objects (including Web, which is required for receive mode) --- onionshare/__init__.py | 30 ++++++++++----------- onionshare/common.py | 9 +++++++ onionshare/onion.py | 11 ++++---- onionshare_gui/__init__.py | 1 - onionshare_gui/onionshare_gui.py | 36 ++++++++++++------------- onionshare_gui/server_status.py | 30 ++++++++++----------- onionshare_gui/settings_dialog.py | 6 ++--- onionshare_gui/tor_connection_dialog.py | 15 +++++++---- 8 files changed, 72 insertions(+), 66 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 1e07f11c..5b23ad3e 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -25,7 +25,6 @@ from .common import Common from .web import Web from .onion import * from .onionshare import OnionShare -from .settings import Settings def main(cwd=None): """ @@ -71,9 +70,6 @@ def main(cwd=None): print(strings._('no_filenames')) sys.exit() - # Debug mode? - common.debug = debug - # Validate filenames if not receive: valid = True @@ -88,20 +84,22 @@ def main(cwd=None): sys.exit() # Load settings - settings = Settings(common, config) - settings.load() + common.load_settings(config) + + # Debug mode? + common.debug = debug # In receive mode, validate downloads dir if receive: valid = True - if not os.path.isdir(settings.get('downloads_dir')): + if not os.path.isdir(common.settings.get('downloads_dir')): try: - os.mkdir(settings.get('downloads_dir'), 0o700) + os.mkdir(common.settings.get('downloads_dir'), 0o700) except: - print(strings._('error_cannot_create_downloads_dir').format(settings.get('downloads_dir'))) + print(strings._('error_cannot_create_downloads_dir').format(common.settings.get('downloads_dir'))) valid = False if valid and not os.access(settings.get('downloads_dir'), os.W_OK): - print(strings._('error_downloads_dir_not_writable').format(settings.get('downloads_dir'))) + print(strings._('error_downloads_dir_not_writable').format(common.settings.get('downloads_dir'))) valid = False if not valid: sys.exit() @@ -112,7 +110,7 @@ def main(cwd=None): # Start the Onion object onion = Onion(common) try: - onion.connect(settings=False, config=config) + onion.connect(custom_settings=False, config=config) except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorNotSupported, BundledTorTimeout) as e: sys.exit(e.args[0]) except KeyboardInterrupt: @@ -144,7 +142,7 @@ def main(cwd=None): print('') # Start OnionShare http service in new thread - t = threading.Thread(target=web.start, args=(app.port, app.stay_open, settings.get('slug'))) + t = threading.Thread(target=web.start, args=(app.port, app.stay_open, common.settings.get('slug'))) t.daemon = True t.start() @@ -157,10 +155,10 @@ def main(cwd=None): app.shutdown_timer.start() # Save the web slug if we are using a persistent private key - if settings.get('save_private_key'): - if not settings.get('slug'): - settings.set('slug', web.slug) - settings.save() + if common.settings.get('save_private_key'): + if not common.settings.get('slug'): + common.settings.set('slug', web.slug) + common.settings.save() print('') if receive: diff --git a/onionshare/common.py b/onionshare/common.py index c24a1b4b..903e4148 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -29,6 +29,8 @@ import tempfile import threading import time +from .settings import Settings + class Common(object): """ The Common object is shared amongst all parts of OnionShare. @@ -45,6 +47,13 @@ class Common(object): with open(self.get_resource_path('version.txt')) as f: self.version = f.read().strip() + def load_settings(self, config=None): + """ + Loading settings, optionally from a custom config json file. + """ + self.settings = Settings(self, config) + self.settings.load() + def log(self, module, func, msg=None): """ If debug mode is on, log error messages to stdout diff --git a/onionshare/onion.py b/onionshare/onion.py index 4b3b0971..57e407ea 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -148,15 +148,14 @@ class Onion(object): # Start out not connected to Tor self.connected_to_tor = False - def connect(self, settings=False, config=False, tor_status_update_func=None): + def connect(self, custom_settings=False, config=False, tor_status_update_func=None): self.common.log('Onion', 'connect') - # Either use settings that are passed in, or load them from disk - if settings: - self.settings = settings + # Either use settings that are passed in, or use them from common + if custom_settings: + self.settings = custom_settings else: - self.settings = Settings(self.common, config) - self.settings.load() + self.settings = self.common.settings # The Tor controller self.c = None diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 04fe1e62..e1ad8743 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -27,7 +27,6 @@ from onionshare.common import Common from onionshare.web import Web from onionshare.onion import Onion from onionshare.onionshare import OnionShare -from onionshare.settings import Settings from .onionshare_gui import OnionShareGui diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 04b8a066..a52f232a 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -25,7 +25,6 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings, common from onionshare.common import Common, ShutdownTimer -from onionshare.settings import Settings from onionshare.onion import * from .tor_connection_dialog import TorConnectionDialog @@ -66,8 +65,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # Load settings self.config = config - self.settings = Settings(self.common, self.config) - self.settings.load() + self.common.load_settings(self.config) # File selection self.file_selection = FileSelection(self.common) @@ -76,7 +74,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.file_selection.file_list.add_file(filename) # Server status - self.server_status = ServerStatus(self.common, self.qtapp, self.app, self.web, self.file_selection, self.settings) + self.server_status = ServerStatus(self.common, self.qtapp, self.app, self.web, self.file_selection) self.server_status.server_started.connect(self.file_selection.server_started) self.server_status.server_started.connect(self.start_server) self.server_status.server_started.connect(self.update_server_status_indicator) @@ -222,7 +220,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.timer.timeout.connect(self.check_for_requests) # Start the "Connecting to Tor" dialog, which calls onion.connect() - tor_con = TorConnectionDialog(self.common, self.qtapp, self.settings, self.onion) + tor_con = TorConnectionDialog(self.common, self.qtapp, self.onion) tor_con.canceled.connect(self._tor_connection_canceled) tor_con.open_settings.connect(self._tor_connection_open_settings) tor_con.start() @@ -339,7 +337,7 @@ class OnionShareGui(QtWidgets.QMainWindow): def reload_settings(): self.common.log('OnionShareGui', 'open_settings', 'settings have changed, reloading') - self.settings.load() + self.common.settings.load() # We might've stopped the main requests timer if a Tor connection failed. # If we've reloaded settings, we probably succeeded in obtaining a new # connection. If so, restart the timer. @@ -352,7 +350,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.server_status.server_button.setEnabled(True) self.status_bar.clearMessage() # If we switched off the shutdown timeout setting, ensure the widget is hidden. - if not self.settings.get('shutdown_timeout'): + if not self.common.settings.get('shutdown_timeout'): self.server_status.shutdown_timeout_container.hide() d = SettingsDialog(self.common, self.onion, self.qtapp, self.config) @@ -371,7 +369,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.set_server_active(True) - self.app.set_stealth(self.settings.get('use_stealth')) + self.app.set_stealth(self.common.settings.get('use_stealth')) # Hide and reset the downloads if we have previously shared self.downloads_container.hide() @@ -395,10 +393,10 @@ class OnionShareGui(QtWidgets.QMainWindow): return - self.app.stay_open = not self.settings.get('close_after_first_download') + self.app.stay_open = not self.common.settings.get('close_after_first_download') # start onionshare http service in new thread - t = threading.Thread(target=self.web.start, args=(self.app.port, self.app.stay_open, self.settings.get('slug'))) + t = threading.Thread(target=self.web.start, args=(self.app.port, self.app.stay_open, self.common.settings.get('slug'))) t.daemon = True t.start() # wait for modules in thread to load, preventing a thread-related cx_Freeze crash @@ -462,7 +460,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.show() - if self.settings.get('shutdown_timeout'): + if self.common.settings.get('shutdown_timeout'): # Convert the date value to seconds between now and then now = QtCore.QDateTime.currentDateTime() self.timeout = now.secsTo(self.server_status.timeout) @@ -527,7 +525,7 @@ class OnionShareGui(QtWidgets.QMainWindow): Check for updates in a new thread, if enabled. """ if self.common.platform == 'Windows' or self.common.platform == 'Darwin': - if self.settings.get('use_autoupdate'): + if self.common.settings.get('use_autoupdate'): def update_available(update_url, installed_version, latest_version): Alert(self.common, strings._("update_available", True).format(update_url, installed_version, latest_version)) @@ -558,7 +556,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.server_status.stop_server() self.server_status.server_button.setEnabled(False) self.status_bar.showMessage(strings._('gui_tor_connection_lost', True)) - if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): + if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('gui_tor_connection_lost', True), strings._('gui_tor_connection_error_settings', True)) # scroll to the bottom of the dl progress bar log pane @@ -587,7 +585,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.new_download = True self.downloads_in_progress += 1 self.update_downloads_in_progress(self.downloads_in_progress) - if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): + if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True)) elif event["type"] == self.web.REQUEST_RATE_LIMIT: @@ -599,7 +597,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # is the download complete? if event["data"]["bytes"] == self.web.zip_filesize: - if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): + if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) # Update the total 'completed downloads' info self.downloads_completed += 1 @@ -625,7 +623,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # Update the 'in progress downloads' info self.downloads_in_progress -= 1 self.update_downloads_in_progress(self.downloads_in_progress) - if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): + if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True)) elif event["path"] != '/favicon.ico': @@ -633,7 +631,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # If the auto-shutdown timer has stopped, stop the server if self.server_status.status == self.server_status.STATUS_STARTED: - if self.app.shutdown_timer and self.settings.get('shutdown_timeout'): + if self.app.shutdown_timer and self.common.settings.get('shutdown_timeout'): if self.timeout > 0: now = QtCore.QDateTime.currentDateTime() seconds_remaining = now.secsTo(self.server_status.timeout) @@ -654,7 +652,7 @@ class OnionShareGui(QtWidgets.QMainWindow): When the URL gets copied to the clipboard, display this in the status bar. """ self.common.log('OnionShareGui', 'copy_url') - if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): + if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True)) def copy_hidservauth(self): @@ -662,7 +660,7 @@ class OnionShareGui(QtWidgets.QMainWindow): When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. """ self.common.log('OnionShareGui', 'copy_hidservauth') - if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'): + if self.systemTray.supportsMessages() and self.common.settings.get('systray_notifications'): self.systemTray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True)) def clear_message(self): diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 62df81ff..ed8bc5f5 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -38,7 +38,7 @@ class ServerStatus(QtWidgets.QWidget): STATUS_WORKING = 1 STATUS_STARTED = 2 - def __init__(self, common, qtapp, app, web, file_selection, settings): + def __init__(self, common, qtapp, app, web, file_selection): super(ServerStatus, self).__init__() self.common = common @@ -50,8 +50,6 @@ class ServerStatus(QtWidgets.QWidget): self.web = web self.file_selection = file_selection - self.settings = settings - # Shutdown timeout layout self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True)) self.shutdown_timeout = QtWidgets.QDateTimeEdit() @@ -135,13 +133,13 @@ class ServerStatus(QtWidgets.QWidget): info_image = self.common.get_resource_path('images/info.png') self.url_description.setText(strings._('gui_url_description', True).format(info_image)) # Show a Tool Tip explaining the lifecycle of this URL - if self.settings.get('save_private_key'): - if self.settings.get('close_after_first_download'): + if self.common.settings.get('save_private_key'): + if self.common.settings.get('close_after_first_download'): self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True)) else: self.url_description.setToolTip(strings._('gui_url_label_persistent', True)) else: - if self.settings.get('close_after_first_download'): + if self.common.settings.get('close_after_first_download'): self.url_description.setToolTip(strings._('gui_url_label_onetime', True)) else: self.url_description.setToolTip(strings._('gui_url_label_stay_open', True)) @@ -151,12 +149,12 @@ class ServerStatus(QtWidgets.QWidget): self.copy_url_button.show() - if self.settings.get('save_private_key'): - if not self.settings.get('slug'): - self.settings.set('slug', self.web.slug) - self.settings.save() + if self.common.settings.get('save_private_key'): + if not self.common.settings.get('slug'): + self.common.settings.set('slug', self.web.slug) + self.common.settings.save() - if self.settings.get('shutdown_timeout'): + if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.hide() if self.app.stealth: @@ -183,26 +181,26 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setEnabled(True) self.server_button.setText(strings._('gui_start_server', True)) self.server_button.setToolTip('') - if self.settings.get('shutdown_timeout'): + if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.show() elif self.status == self.STATUS_STARTED: self.server_button.setStyleSheet(button_started_style) self.server_button.setEnabled(True) self.server_button.setText(strings._('gui_stop_server', True)) - if self.settings.get('shutdown_timeout'): + if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.hide() self.server_button.setToolTip(strings._('gui_stop_server_shutdown_timeout_tooltip', True).format(self.timeout)) elif self.status == self.STATUS_WORKING: self.server_button.setStyleSheet(button_working_style) self.server_button.setEnabled(True) self.server_button.setText(strings._('gui_please_wait')) - if self.settings.get('shutdown_timeout'): + if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.hide() else: self.server_button.setStyleSheet(button_working_style) self.server_button.setEnabled(False) self.server_button.setText(strings._('gui_please_wait')) - if self.settings.get('shutdown_timeout'): + if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.hide() def server_button_clicked(self): @@ -210,7 +208,7 @@ class ServerStatus(QtWidgets.QWidget): Toggle starting or stopping the server. """ if self.status == self.STATUS_STOPPED: - if self.settings.get('shutdown_timeout'): + if self.common.settings.get('shutdown_timeout'): # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) # If the timeout has actually passed already before the user hit Start, refuse to start the server. diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 2bd20d84..192815da 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -599,8 +599,8 @@ class SettingsDialog(QtWidgets.QDialog): else: tor_status_update_func = None - onion = Onion() - onion.connect(settings=settings, config=self.config, tor_status_update_func=tor_status_update_func) + onion = Onion(self.common) + onion.connect(custom_settings=settings, config=self.config, tor_status_update_func=tor_status_update_func) # If an exception hasn't been raised yet, the Tor settings work Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) @@ -707,7 +707,7 @@ class SettingsDialog(QtWidgets.QDialog): self.common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion') self.onion.cleanup() - tor_con = TorConnectionDialog(self.common, self.qtapp, settings, self.onion) + tor_con = TorConnectionDialog(self.common, self.qtapp, self.onion, settings) tor_con.start() self.common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor)) diff --git a/onionshare_gui/tor_connection_dialog.py b/onionshare_gui/tor_connection_dialog.py index 6d127df9..2ee13a66 100644 --- a/onionshare_gui/tor_connection_dialog.py +++ b/onionshare_gui/tor_connection_dialog.py @@ -30,15 +30,19 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): """ open_settings = QtCore.pyqtSignal() - def __init__(self, common, qtapp, settings, onion): + def __init__(self, common, qtapp, onion, custom_settings=False): super(TorConnectionDialog, self).__init__(None) self.common = common + if custom_settings: + self.settings = custom_settings + else: + self.settings = self.common.settings + self.common.log('TorConnectionDialog', '__init__') self.qtapp = qtapp - self.settings = settings self.onion = onion self.setWindowTitle("OnionShare") @@ -60,7 +64,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): def start(self): self.common.log('TorConnectionDialog', 'start') - t = TorConnectionThread(self.common, self, self.settings, self.onion) + t = TorConnectionThread(self.common, self.settings, self, self.onion) t.tor_status_update.connect(self._tor_status_update) t.connected_to_tor.connect(self._connected_to_tor) t.canceled_connecting_to_tor.connect(self._canceled_connecting_to_tor) @@ -116,15 +120,16 @@ class TorConnectionThread(QtCore.QThread): canceled_connecting_to_tor = QtCore.pyqtSignal() error_connecting_to_tor = QtCore.pyqtSignal(str) - def __init__(self, common, dialog, settings, onion): + def __init__(self, common, settings, dialog, onion): super(TorConnectionThread, self).__init__() self.common = common self.common.log('TorConnectionThread', '__init__') - self.dialog = dialog self.settings = settings + + self.dialog = dialog self.onion = onion def run(self): From 8e82c07039cf678749d58886a4c0004224ddfcc0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 13 Mar 2018 03:59:52 -0700 Subject: [PATCH 22/36] Fixed bug in validating downloads dir related to moving settings into common --- onionshare/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 5b23ad3e..e3a17538 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -98,7 +98,7 @@ def main(cwd=None): except: print(strings._('error_cannot_create_downloads_dir').format(common.settings.get('downloads_dir'))) valid = False - if valid and not os.access(settings.get('downloads_dir'), os.W_OK): + if valid and not os.access(common.settings.get('downloads_dir'), os.W_OK): print(strings._('error_downloads_dir_not_writable').format(common.settings.get('downloads_dir'))) valid = False if not valid: From 000d9620c19344265120f7cc155109cfc9641c56 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 13 Mar 2018 05:50:26 -0700 Subject: [PATCH 23/36] Add flash messages to receive template, and begin implementing upload POST --- onionshare/web.py | 24 +++++++++++++++++++++--- share/static/css/style.css | 12 ++++++++++++ share/templates/receive.html | 12 +++++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index b6739bcb..0027bf0f 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -31,9 +31,10 @@ from distutils.version import LooseVersion as Version from urllib.request import urlopen from flask import ( - Flask, Response, request, render_template, abort, make_response, - __version__ as flask_version + Flask, Response, request, render_template, abort, make_response, flash, + redirect, __version__ as flask_version ) +from werkzeug.utils import secure_filename from . import strings, common @@ -48,6 +49,7 @@ class Web(object): self.app = Flask(__name__, static_folder=common.get_resource_path('static'), template_folder=common.get_resource_path('templates')) + self.app.secret_key = self.common.random_string(8) # Debug mode? if self.common.debug: @@ -61,6 +63,8 @@ class Web(object): # Are we using receive mode? self.receive_mode = receive_mode + if self.receive_mode: + self.app.config['UPLOAD_FOLDER'] = self.common.settings.get('downloads_dir') # Starting in Flask 0.11, render_template_string autoescapes template variables # by default. To prevent content injection through template variables in @@ -257,12 +261,26 @@ class Web(object): def index(slug_candidate): self.check_slug_candidate(slug_candidate) - # If download is allowed to continue, serve download page r = make_response(render_template( 'receive.html', slug=self.slug)) return self.add_security_headers(r) + @self.app.route("//upload", methods=['POST']) + def upload(slug_candidate): + # Note that flash strings are on English, and not translated, on purpose, + # to avoid leaking the locale of the OnionShare user + self.check_slug_candidate(slug_candidate) + self.common.log('Web', 'upload, request.files: {}'.format(request.files)) + + # Check if the post request has the file part + if 'file' not in request.files: + flash('No files were selected to upload') + return redirect('/{}'.format(slug_candidate)) + + files = request.files['file'] + return '' + def common_routes(self): """ Common web app routes between sending and receiving diff --git a/share/static/css/style.css b/share/static/css/style.css index c3304f39..c65c11f7 100644 --- a/share/static/css/style.css +++ b/share/static/css/style.css @@ -115,3 +115,15 @@ table.file-list td:last-child { color: #666666; margin: 0 0 20px 0; } + +ul.flashes { + list-style: none; + margin: 0; + padding: 0; + color: #cc0000; +} + +ul.flashes li { + margin: 0; + padding: 10px; +} diff --git a/share/templates/receive.html b/share/templates/receive.html index 6ad3aebc..d7db31de 100644 --- a/share/templates/receive.html +++ b/share/templates/receive.html @@ -12,12 +12,22 @@

    OnionShare

    + {% with messages = get_flashed_messages() %} + {% if messages %} +
      + {% for message in messages %} +
    • {{ message }}
    • + {% endfor %} +
    + {% endif %} + {% endwith %} +

    Send Files

    Select the files you want to send, then click "Send Files"...

    -
    +

    From 01dd16d92fb8329d60558bbb8985cf342fe7c4cd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 14 Mar 2018 04:03:50 -0700 Subject: [PATCH 24/36] Uploading files works in CLI --- onionshare/web.py | 57 +++++++++++++++++++++++++++++------- share/templates/receive.html | 2 +- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index 0027bf0f..be2d1a07 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -63,8 +63,6 @@ class Web(object): # Are we using receive mode? self.receive_mode = receive_mode - if self.receive_mode: - self.app.config['UPLOAD_FOLDER'] = self.common.settings.get('downloads_dir') # Starting in Flask 0.11, render_template_string autoescapes template variables # by default. To prevent content injection through template variables in @@ -268,18 +266,55 @@ class Web(object): @self.app.route("//upload", methods=['POST']) def upload(slug_candidate): + self.check_slug_candidate(slug_candidate) + + files = request.files.getlist('file[]') + filenames = [] + for f in files: + if f.filename != '': + # Automatically rename the file, if a file of the same name already exists + filename = secure_filename(f.filename) + filenames.append(filename) + local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) + if os.path.exists(local_path): + if '.' in filename: + # Add "-i", e.g. change "foo.txt" to "foo-2.txt" + parts = filename.split('.') + name = parts[:-1] + ext = parts[-1] + + i = 2 + valid = False + while not valid: + new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + else: + # If no extension, just add "-i", e.g. change "foo" to "foo-2" + i = 2 + valid = False + while not valid: + new_filename = '{}-{}'.format(filename, i) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + + self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + f.save(local_path) + # Note that flash strings are on English, and not translated, on purpose, # to avoid leaking the locale of the OnionShare user - self.check_slug_candidate(slug_candidate) - self.common.log('Web', 'upload, request.files: {}'.format(request.files)) + if len(filenames) == 0: + flash('No files uploaded') + else: + flash('Uploaded {}'.format(', '.join(filenames))) - # Check if the post request has the file part - if 'file' not in request.files: - flash('No files were selected to upload') - return redirect('/{}'.format(slug_candidate)) - - files = request.files['file'] - return '' + return redirect('/{}'.format(slug_candidate)) def common_routes(self): """ diff --git a/share/templates/receive.html b/share/templates/receive.html index d7db31de..1ced74da 100644 --- a/share/templates/receive.html +++ b/share/templates/receive.html @@ -28,7 +28,7 @@

    Send Files

    Select the files you want to send, then click "Send Files"...

    -

    +

    From 0b10e71547f80a2f8da119534f7bb1f2fcf37ba7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 14 Mar 2018 07:35:04 -0700 Subject: [PATCH 25/36] Add receive mode warning, and print notification for each upload, in CLI mode --- onionshare/__init__.py | 3 +++ onionshare/web.py | 3 +++ share/locale/en.json | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index e3a17538..0d5156d8 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -162,6 +162,9 @@ def main(cwd=None): print('') if receive: + print(strings._('receive_mode_warning')) + print('') + if stealth: print(strings._("give_this_url_receive_stealth")) print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) diff --git a/onionshare/web.py b/onionshare/web.py index be2d1a07..495e1364 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -305,6 +305,9 @@ class Web(object): valid = True self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + print('') + print(strings._('receive_mode_received_file').format(local_path)) + print('') f.save(local_path) # Note that flash strings are on English, and not translated, on purpose, diff --git a/share/locale/en.json b/share/locale/en.json index 3340afd4..214aa32f 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -158,5 +158,7 @@ "info_in_progress_downloads_tooltip": "{} download(s) in progress", "info_completed_downloads_tooltip": "{} download(s) completed", "error_cannot_create_downloads_dir": "Error creating downloads folder: {}", - "error_downloads_dir_not_writable": "The downloads folder isn't writable: {}" + "error_downloads_dir_not_writable": "The downloads folder isn't writable: {}", + "receive_mode_warning": "Warning: Some files can hack your computer if you open them! Only open files from people you trust, or if you know what you're doing.", + "receive_mode_received_file": "Received file: {}" } From 01f86daf8f047095aab9c35fb913d55161863301 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 14 Mar 2018 08:16:09 -0700 Subject: [PATCH 26/36] In receive mode, allow uploader to close the server when they are done --- onionshare/web.py | 8 ++++++-- share/static/css/style.css | 14 ++++++++++++++ share/templates/receive.html | 27 ++++++++++++++++----------- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index 495e1364..f0c76db7 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -305,9 +305,7 @@ class Web(object): valid = True self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) - print('') print(strings._('receive_mode_received_file').format(local_path)) - print('') f.save(local_path) # Note that flash strings are on English, and not translated, on purpose, @@ -319,6 +317,12 @@ class Web(object): return redirect('/{}'.format(slug_candidate)) + @self.app.route("//close", methods=['POST']) + def close(slug_candidate): + self.check_slug_candidate(slug_candidate) + self.force_shutdown() + return "" + def common_routes(self): """ Common web app routes between sending and receiving diff --git a/share/static/css/style.css b/share/static/css/style.css index c65c11f7..6b372f22 100644 --- a/share/static/css/style.css +++ b/share/static/css/style.css @@ -56,6 +56,20 @@ header .right ul li { cursor: pointer; } +.close-button { + color: #ffffff; + background-color: #c90c0c; + padding: 10px; + border: 0; + border-radius: 5px; + text-decoration: none; + margin-left: 1rem; + cursor: pointer; + position: absolute; + right: 10px; + bottom: 10px; +} + table.file-list { width: 100%; margin: 0 auto; diff --git a/share/templates/receive.html b/share/templates/receive.html index 1ced74da..4bcf27ad 100644 --- a/share/templates/receive.html +++ b/share/templates/receive.html @@ -12,16 +12,6 @@

    OnionShare

    - {% with messages = get_flashed_messages() %} - {% if messages %} -
      - {% for message in messages %} -
    • {{ message }}
    • - {% endfor %} -
    - {% endif %} - {% endwith %} -

    @@ -29,10 +19,25 @@

    Select the files you want to send, then click "Send Files"...

    -

    +

    +
    + {% with messages = get_flashed_messages() %} + {% if messages %} +
      + {% for message in messages %} +
    • {{ message }}
    • + {% endfor %} +
    + {% endif %} + {% endwith %} +
    +
    + +
    + From 72698a7247b0379457ae1521cf9df3353dc7ad73 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 14 Mar 2018 08:30:14 -0700 Subject: [PATCH 27/36] Display a template after closing the server, and standardize the style of other simple templates --- onionshare/web.py | 3 ++- share/static/css/404.css | 6 ------ share/static/css/denied.css | 7 ------- share/templates/404.html | 7 ++++--- share/templates/closed.html | 10 ++++++++++ share/templates/denied.html | 1 - share/templates/receive.html | 4 ++-- 7 files changed, 18 insertions(+), 20 deletions(-) delete mode 100644 share/static/css/404.css delete mode 100644 share/static/css/denied.css create mode 100644 share/templates/closed.html diff --git a/onionshare/web.py b/onionshare/web.py index f0c76db7..4a6b8c71 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -321,7 +321,8 @@ class Web(object): def close(slug_candidate): self.check_slug_candidate(slug_candidate) self.force_shutdown() - return "" + r = make_response(render_template('closed.html')) + return self.add_security_headers(r) def common_routes(self): """ diff --git a/share/static/css/404.css b/share/static/css/404.css deleted file mode 100644 index e8f377ca..00000000 --- a/share/static/css/404.css +++ /dev/null @@ -1,6 +0,0 @@ -body { - background-color: #FFC4D5; - color: #FF0048; - text-align: center; - font-size: 20em; -} diff --git a/share/static/css/denied.css b/share/static/css/denied.css deleted file mode 100644 index 260a3dab..00000000 --- a/share/static/css/denied.css +++ /dev/null @@ -1,7 +0,0 @@ -body { - background-color: #222222; - color: #ffffff; - text-align: center; - font-family: sans-serif; - padding: 5em 1em; -} diff --git a/share/templates/404.html b/share/templates/404.html index 1cf9d7a2..10a4c8ef 100644 --- a/share/templates/404.html +++ b/share/templates/404.html @@ -1,9 +1,10 @@ - Error 404 + OnionsShare: Error 404 - - 404 + +

    Error 404: You probably typed the OnionShare address wrong

    + diff --git a/share/templates/closed.html b/share/templates/closed.html new file mode 100644 index 00000000..167d0efc --- /dev/null +++ b/share/templates/closed.html @@ -0,0 +1,10 @@ + + + + OnionShare is closed + + + +

    Thank you for using OnionShare

    + + diff --git a/share/templates/denied.html b/share/templates/denied.html index 39974639..5d411d62 100644 --- a/share/templates/denied.html +++ b/share/templates/denied.html @@ -3,7 +3,6 @@ OnionShare -

    OnionShare download in progress

    diff --git a/share/templates/receive.html b/share/templates/receive.html index 4bcf27ad..d1ec3b3a 100644 --- a/share/templates/receive.html +++ b/share/templates/receive.html @@ -19,7 +19,7 @@

    Select the files you want to send, then click "Send Files"...

    -

    +

    {% with messages = get_flashed_messages() %} {% if messages %} @@ -36,7 +36,7 @@
    - +
    From fee1d495634c302c065632cfe56cba5f05774434 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 14 Mar 2018 08:33:25 -0700 Subject: [PATCH 28/36] Fix bug with shutdown_slug --- onionshare/web.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index 4a6b8c71..ca0bd044 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -350,7 +350,7 @@ class Web(object): """ Stop the flask web server, from the context of an http request. """ - self.check_slug_candidate(slug_candidate, shutdown_slug) + self.check_slug_candidate(slug_candidate, self.shutdown_slug) self.force_shutdown() return "" @@ -472,10 +472,10 @@ class Web(object): try: s = socket.socket() s.connect(('127.0.0.1', port)) - s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(shutdown_slug)) + s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug)) except: try: - urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, shutdown_slug)).read() + urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read() except: pass From 929ad58ebdc527ba0ef4ec3ddb834742f70ecc9b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 14 Mar 2018 08:34:43 -0700 Subject: [PATCH 29/36] Fix bug with validating filenames from args in GUI --- onionshare_gui/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index e1ad8743..13612e77 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -92,10 +92,10 @@ def main(): valid = True for filename in filenames: if not os.path.isfile(filename) and not os.path.isdir(filename): - Alert(self.common, strings._("not_a_file", True).format(filename)) + Alert(common, strings._("not_a_file", True).format(filename)) valid = False if not os.access(filename, os.R_OK): - Alert(self.common, strings._("not_a_readable_file", True).format(filename)) + Alert(common, strings._("not_a_readable_file", True).format(filename)) valid = False if not valid: sys.exit() From aafa9b1543bf07c3714a681c698baa4377c55092 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 19 Mar 2018 02:25:22 -0700 Subject: [PATCH 30/36] Add WSGI middleware in order to capture the progress of POST request uploads --- onionshare/web.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/onionshare/web.py b/onionshare/web.py index ca0bd044..9f4c18d9 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -27,6 +27,7 @@ import socket import sys import tempfile import zipfile +import re from distutils.version import LooseVersion as Version from urllib.request import urlopen @@ -63,6 +64,9 @@ class Web(object): # Are we using receive mode? self.receive_mode = receive_mode + if self.receive_mode: + # In receive mode, use WSGI middleware to track the progess of upload POSTs + self.app.wsgi_app = UploadProgessMiddleware(self.app.wsgi_app, self) # Starting in Flask 0.11, render_template_string autoescapes template variables # by default. To prevent content injection through template variables in @@ -528,3 +532,28 @@ class ZipWriter(object): Close the zip archive. """ self.z.close() + + +class UploadProgessMiddleware(object): + def __init__(self, app, web): + self.app = app + self.web = web + + self.upload_regex = re.compile('/(.*)/upload') + + def __call__(self, environ, start_response): + # Check if this is a POST request to /[slug]/upload + valid_upload_request = False + if environ.get('REQUEST_METHOD') == 'POST': + match = self.upload_regex.match(environ.get('PATH_INFO')) + if match: + slug_candidate = match.group(1) + if hmac.compare_digest(self.web.slug, slug_candidate): + valid_upload_request = True + + # If this is a valid upload request, stream the upload + if valid_upload_request: + #print(environ.get('wsgi.input')) + pass + + return self.app(environ, start_response) From facd441caf15756a29e864b9e73e835f0d4c3184 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 21 Mar 2018 17:34:11 -0700 Subject: [PATCH 31/36] For receive mode, use a custom flask Request, and a custom TemporaryFile, in order to keep track of file upload progress --- onionshare/web.py | 77 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index 9f4c18d9..f7f02526 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -28,12 +28,13 @@ import sys import tempfile import zipfile import re +import io from distutils.version import LooseVersion as Version from urllib.request import urlopen from flask import ( - Flask, Response, request, render_template, abort, make_response, flash, - redirect, __version__ as flask_version + Flask, Response, Request, request, render_template, abort, make_response, + flash, redirect, __version__ as flask_version ) from werkzeug.utils import secure_filename @@ -66,7 +67,9 @@ class Web(object): self.receive_mode = receive_mode if self.receive_mode: # In receive mode, use WSGI middleware to track the progess of upload POSTs - self.app.wsgi_app = UploadProgessMiddleware(self.app.wsgi_app, self) + self.app.wsgi_app = UploadProgessMiddleware(self.common, self.app.wsgi_app, self) + # Use a custom Request class + self.app.request_class = ReceiveModeRequest # Starting in Flask 0.11, render_template_string autoescapes template variables # by default. To prevent content injection through template variables in @@ -535,7 +538,8 @@ class ZipWriter(object): class UploadProgessMiddleware(object): - def __init__(self, app, web): + def __init__(self, common, app, web): + self.common = common self.app = app self.web = web @@ -553,7 +557,68 @@ class UploadProgessMiddleware(object): # If this is a valid upload request, stream the upload if valid_upload_request: - #print(environ.get('wsgi.input')) - pass + length = environ.get('CONTENT_LENGTH') + self.common.log('UploadProgessMiddleware', 'upload started, {} bytes'.format(length)) return self.app(environ, start_response) + + +class ReceiveModeTemporaryFile(object): + """ + A custom TemporaryFile that tells ReceiveModeRequest every time data gets + written to it, in order to track the progress of uploads. + """ + def __init__(self, filename, update_func): + self.onionshare_filename = filename + self.onionshare_update_func = update_func + + # Create a temporary file + self.f = tempfile.TemporaryFile('wb+') + + # Make all the file-like methods and attributes actually access the + # TemporaryFile, except for write + attrs = ['close', 'closed', 'detach', 'fileno', 'flush', 'isatty', 'mode', + 'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto', + 'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell', + 'truncate', 'writable', 'writelines'] + for attr in attrs: + setattr(self, attr, getattr(self.f, attr)) + + def write(self, b): + """ + Custom write method that calls out to onionshare_update_func + """ + bytes_written = self.f.write(b) + self.onionshare_update_func(self.onionshare_filename, bytes_written) + + +class ReceiveModeRequest(Request): + """ + A custom flask Request object that keeps track of how much data has been + uploaded for each file, for receive mode. + """ + def __init__(self, environ, populate_request=True, shallow=False): + super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) + + # The total size of this request, which may include multiple files + self.total_content_length = 0 + + # A dictionary that maps filenames to the bytes uploaded so far + self.onionshare_progress = {} + + def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None): + """ + This gets called for each file that gets uploaded, and returns an file-like + writable stream. + """ + print('') + self.total_content_length = total_content_length + self.onionshare_progress[filename] = 0 + return ReceiveModeTemporaryFile(filename, self.onionshare_update_func) + + def onionshare_update_func(self, filename, length): + """ + Keep track of the bytes uploaded so far for all files. + """ + self.onionshare_progress[filename] += length + print('\r{}: {} '.format(filename, self.onionshare_progress[filename]), end='') From f7640416eb8544201d7755a4968227e70c9c6134 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 21 Mar 2018 17:51:42 -0700 Subject: [PATCH 32/36] Remove the WSGI middleware, because I'm solving the problem in a different way --- onionshare/web.py | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index f7f02526..15a68cec 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -66,9 +66,7 @@ class Web(object): # Are we using receive mode? self.receive_mode = receive_mode if self.receive_mode: - # In receive mode, use WSGI middleware to track the progess of upload POSTs - self.app.wsgi_app = UploadProgessMiddleware(self.common, self.app.wsgi_app, self) - # Use a custom Request class + # Use a custom Request class to track upload progess self.app.request_class = ReceiveModeRequest # Starting in Flask 0.11, render_template_string autoescapes template variables @@ -537,32 +535,6 @@ class ZipWriter(object): self.z.close() -class UploadProgessMiddleware(object): - def __init__(self, common, app, web): - self.common = common - self.app = app - self.web = web - - self.upload_regex = re.compile('/(.*)/upload') - - def __call__(self, environ, start_response): - # Check if this is a POST request to /[slug]/upload - valid_upload_request = False - if environ.get('REQUEST_METHOD') == 'POST': - match = self.upload_regex.match(environ.get('PATH_INFO')) - if match: - slug_candidate = match.group(1) - if hmac.compare_digest(self.web.slug, slug_candidate): - valid_upload_request = True - - # If this is a valid upload request, stream the upload - if valid_upload_request: - length = environ.get('CONTENT_LENGTH') - self.common.log('UploadProgessMiddleware', 'upload started, {} bytes'.format(length)) - - return self.app(environ, start_response) - - class ReceiveModeTemporaryFile(object): """ A custom TemporaryFile that tells ReceiveModeRequest every time data gets @@ -600,9 +572,6 @@ class ReceiveModeRequest(Request): def __init__(self, environ, populate_request=True, shallow=False): super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) - # The total size of this request, which may include multiple files - self.total_content_length = 0 - # A dictionary that maps filenames to the bytes uploaded so far self.onionshare_progress = {} @@ -612,7 +581,6 @@ class ReceiveModeRequest(Request): writable stream. """ print('') - self.total_content_length = total_content_length self.onionshare_progress[filename] = 0 return ReceiveModeTemporaryFile(filename, self.onionshare_update_func) From bd7305ab16aa6900371b0403fde9cb56c79009b3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 21 Mar 2018 18:27:42 -0700 Subject: [PATCH 33/36] Add new WSGI middleware just to attach the Web object to environ, and improve the UI of file upload progress --- onionshare/web.py | 32 +++++++++++++++++++++++++++++--- share/static/css/style.css | 1 + 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index 15a68cec..7a6a848b 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -66,6 +66,8 @@ class Web(object): # Are we using receive mode? self.receive_mode = receive_mode if self.receive_mode: + # Use custom WSGI middleware, to modify environ + self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self) # Use a custom Request class to track upload progess self.app.request_class = ReceiveModeRequest @@ -318,7 +320,8 @@ class Web(object): if len(filenames) == 0: flash('No files uploaded') else: - flash('Uploaded {}'.format(', '.join(filenames))) + for filename in filenames: + flash('Uploaded {}'.format(filename)) return redirect('/{}'.format(slug_candidate)) @@ -535,6 +538,19 @@ class ZipWriter(object): self.z.close() +class ReceiveModeWSGIMiddleware(object): + """ + Custom WSGI middleware in order to attach the Web object to environ, so + ReceiveModeRequest can access it. + """ + def __init__(self, app, web): + self.app = app + self.web = web + + def __call__(self, environ, start_response): + environ['web'] = self.web + return self.app(environ, start_response) + class ReceiveModeTemporaryFile(object): """ A custom TemporaryFile that tells ReceiveModeRequest every time data gets @@ -571,6 +587,7 @@ class ReceiveModeRequest(Request): """ def __init__(self, environ, populate_request=True, shallow=False): super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) + self.web = environ['web'] # A dictionary that maps filenames to the bytes uploaded so far self.onionshare_progress = {} @@ -580,13 +597,22 @@ class ReceiveModeRequest(Request): This gets called for each file that gets uploaded, and returns an file-like writable stream. """ - print('') + if len(self.onionshare_progress) > 0: + print('') self.onionshare_progress[filename] = 0 return ReceiveModeTemporaryFile(filename, self.onionshare_update_func) + def close(self): + """ + When closing the request, print a newline if this was a file upload. + """ + super(ReceiveModeRequest, self).close() + if len(self.onionshare_progress) > 0: + print('') + def onionshare_update_func(self, filename, length): """ Keep track of the bytes uploaded so far for all files. """ self.onionshare_progress[filename] += length - print('\r{}: {} '.format(filename, self.onionshare_progress[filename]), end='') + print('{} - {} '.format(self.web.common.human_readable_filesize(self.onionshare_progress[filename]), filename), end='\r') diff --git a/share/static/css/style.css b/share/static/css/style.css index 6b372f22..29b839a7 100644 --- a/share/static/css/style.css +++ b/share/static/css/style.css @@ -135,6 +135,7 @@ ul.flashes { margin: 0; padding: 0; color: #cc0000; + text-align: left; } ul.flashes li { From 91536ea571f30da3e2d29dd14e2e00cd61a121a2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 22 Apr 2018 17:46:14 -0700 Subject: [PATCH 34/36] Fix a few bugs that I missed when merging in develop --- onionshare/onion.py | 4 ++-- onionshare_gui/__init__.py | 2 +- onionshare_gui/onionshare_gui.py | 8 ++++---- onionshare_gui/settings_dialog.py | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 57e407ea..dc47019f 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -446,11 +446,11 @@ class Onion(object): if self.settings.get('private_key'): key_type = "RSA1024" key_content = self.settings.get('private_key') - self.common.log('Onion', 'Starting a hidden service with a saved private key') + self.common.log('Onion', 'start_onion_service', 'Starting a hidden service with a saved private key') else: key_type = "NEW" key_content = "RSA1024" - self.common.log('Onion', 'Starting a hidden service with a new private key') + self.common.log('Onion', 'start_onion_service', 'Starting a hidden service with a new private key') try: if basic_auth != None: diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 39269b07..11a5999c 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -110,7 +110,7 @@ def main(): app = OnionShare(common, onion, local_only, stay_open, shutdown_timeout) # Launch the gui - gui = OnionShareGui(onion, qtapp, app, filenames, config, local_only) + gui = OnionShareGui(common, web, onion, qtapp, app, filenames, config, local_only) # Clean up when app quits def shutdown(): diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index b5e411f2..d5a0889a 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -107,7 +107,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.filesize_warning.hide() # Downloads - self.downloads = Downloads() + self.downloads = Downloads(self.common) self.new_download = False self.downloads_in_progress = 0 self.downloads_completed = 0 @@ -118,7 +118,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.info_label.setStyleSheet('QLabel { font-size: 12px; color: #666666; }') self.info_show_downloads = QtWidgets.QToolButton() - self.info_show_downloads.setIcon(QtGui.QIcon(common.get_resource_path('images/download_window_gray.png'))) + self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) self.info_show_downloads.setCheckable(True) self.info_show_downloads.toggled.connect(self.downloads_toggled) self.info_show_downloads.setToolTip(strings._('gui_downloads_window_tooltip', True)) @@ -655,7 +655,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ When the 'Show/hide downloads' button is toggled, show or hide the downloads window. """ - common.log('OnionShareGui', 'toggle_downloads') + self.common.log('OnionShareGui', 'toggle_downloads') if checked: self.downloads.downloads_container.show() else: @@ -701,7 +701,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ self.update_downloads_completed(0) self.update_downloads_in_progress(0) - self.info_show_downloads.setIcon(QtGui.QIcon(common.get_resource_path('images/download_window_gray.png'))) + self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) self.downloads.no_downloads_label.show() self.downloads.downloads_container.resize(self.downloads.downloads_container.sizeHint()) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index e7449b51..f38f996e 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -683,7 +683,7 @@ class SettingsDialog(QtWidgets.QDialog): reboot_onion = False if not self.local_only: if self.onion.is_authenticated(): - common.log('SettingsDialog', 'save_clicked', 'Connected to Tor') + self.common.log('SettingsDialog', 'save_clicked', 'Connected to Tor') def changed(s1, s2, keys): """ Compare the Settings objects s1 and s2 and return true if any values @@ -705,20 +705,20 @@ class SettingsDialog(QtWidgets.QDialog): reboot_onion = True else: - common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor') + self.common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor') # Tor isn't connected, so try connecting reboot_onion = True # Do we need to reinitialize Tor? if reboot_onion: # Reinitialize the Onion object - common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion') + self.common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion') self.onion.cleanup() tor_con = TorConnectionDialog(self.qtapp, settings, self.onion) tor_con.start() - common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor)) + self.common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor)) if self.onion.is_authenticated() and not tor_con.wasCanceled(): self.settings_saved.emit() From aa8c07c43439819b3f4fcc4dff169e695ad84695 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 24 Apr 2018 17:18:18 -0700 Subject: [PATCH 35/36] In CLI recieve mode, tell the user where to look for uploaded files --- onionshare/__init__.py | 2 ++ share/locale/en.json | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 0d5156d8..893d83a3 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -162,6 +162,8 @@ def main(cwd=None): print('') if receive: + print(strings._('receive_mode_downloads_dir').format(common.settings.get('downloads_dir'))) + print('') print(strings._('receive_mode_warning')) print('') diff --git a/share/locale/en.json b/share/locale/en.json index 101b8a7c..525dab04 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -64,7 +64,7 @@ "gui_download_progress_complete": "%p%, Time Elapsed: {0:s}", "gui_download_progress_starting": "{0:s}, %p% (Computing ETA)", "gui_download_progress_eta": "{0:s}, ETA: {1:s}, %p%", - "version_string": "Onionshare {0:s} | https://onionshare.org/", + "version_string": "OnionShare {0:s} | https://onionshare.org/", "gui_quit_title": "Transfer in Progress", "gui_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?", "gui_quit_warning_quit": "Quit", @@ -161,6 +161,7 @@ "info_completed_downloads_tooltip": "{} download(s) completed", "error_cannot_create_downloads_dir": "Error creating downloads folder: {}", "error_downloads_dir_not_writable": "The downloads folder isn't writable: {}", + "receive_mode_downloads_dir": "Files people send you will appear in this folder: {}", "receive_mode_warning": "Warning: Some files can hack your computer if you open them! Only open files from people you trust, or if you know what you're doing.", "receive_mode_received_file": "Received file: {}" } From 0e9298794eed521490be24f582fe28720608a2b3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 24 Apr 2018 17:18:33 -0700 Subject: [PATCH 36/36] Typo --- share/templates/404.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/templates/404.html b/share/templates/404.html index 10a4c8ef..b704f9f2 100644 --- a/share/templates/404.html +++ b/share/templates/404.html @@ -1,7 +1,7 @@ - OnionsShare: Error 404 + OnionShare: Error 404