diff --git a/.circleci/config.yml b/.circleci/config.yml index accbc808..3a63d743 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ jobs: - run: name: run tests command: | - xvfb-run pytest --rungui --cov=onionshare --cov=onionshare_gui --cov-report=term-missing -vvv tests/ + xvfb-run -s "-screen 0 1280x1024x24" pytest --rungui --cov=onionshare --cov=onionshare_gui --cov-report=term-missing -vvv --no-qt-log tests/ test-3.6: <<: *test-template diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 1b099a1d..7a1bf170 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -27,6 +27,15 @@ from .web import Web from .onion import * from .onionshare import OnionShare + +def build_url(common, app, web): + # Build the URL + if common.settings.get('public_mode'): + return 'http://{0:s}'.format(app.onion_host) + else: + return 'http://onionshare:{0:s}@{1:s}'.format(web.password, app.onion_host) + + def main(cwd=None): """ The main() function implements all of the logic that the command-line version of @@ -121,12 +130,13 @@ def main(cwd=None): try: common.settings.load() if not common.settings.get('public_mode'): - web.generate_slug(common.settings.get('slug')) + web.generate_password(common.settings.get('password')) else: - web.slug = None + web.password = None app = OnionShare(common, onion, local_only, autostop_timer) app.set_stealth(stealth) app.choose_port() + # Delay the startup if a startup timer was set if autostart_timer > 0: # Can't set a schedule that is later than the auto-stop timer @@ -135,10 +145,7 @@ def main(cwd=None): sys.exit() app.start_onion_service(False, True) - if common.settings.get('public_mode'): - url = 'http://{0:s}'.format(app.onion_host) - else: - url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) + url = build_url(common, app, web) schedule = datetime.now() + timedelta(seconds=autostart_timer) if mode == 'receive': print("Files sent to you appear in this folder: {}".format(common.settings.get('data_dir'))) @@ -174,7 +181,6 @@ def main(cwd=None): if mode == 'website': # Prepare files to share - print("Preparing files to publish website...") try: web.website_mode.set_file_info(filenames) except OSError as e: @@ -198,31 +204,26 @@ def main(cwd=None): print('') # Start OnionShare http service in new thread - t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), web.slug)) + t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), web.password)) t.daemon = True t.start() try: # Trap Ctrl-C - # Wait for web.generate_slug() to finish running + # Wait for web.generate_password() to finish running time.sleep(0.2) # start auto-stop timer thread if app.autostop_timer > 0: app.autostop_timer_thread.start() - # Save the web slug if we are using a persistent private key + # Save the web password if we are using a persistent private key if common.settings.get('save_private_key'): - if not common.settings.get('slug'): - common.settings.set('slug', web.slug) + if not common.settings.get('password'): + common.settings.set('password', web.password) common.settings.save() # Build the URL - if common.settings.get('public_mode'): - url = 'http://{0:s}'.format(app.onion_host) - elif mode == 'website': - url = 'http://onionshare:{0:s}@{1:s}'.format(web.slug, app.onion_host) - else: - url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) + url = build_url(common, app, web) print('') if autostart_timer > 0: diff --git a/onionshare/common.py b/onionshare/common.py index 325f11d4..9b871f04 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -143,7 +143,7 @@ class Common(object): os.makedirs(onionshare_data_dir, 0o700, True) return onionshare_data_dir - def build_slug(self): + def build_password(self): """ Returns a random string made from two words from the wordlist, such as "deter-trig". """ diff --git a/onionshare/settings.py b/onionshare/settings.py index 16b64a05..762c6dc2 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -111,7 +111,7 @@ class Settings(object): 'save_private_key': False, 'private_key': '', 'public_mode': False, - 'slug': '', + 'password': '', 'hidservauth_string': '', 'data_dir': self.build_default_data_dir(), 'locale': None # this gets defined in fill_in_defaults() diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index e7f3b3ae..3f848d2f 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -31,36 +31,15 @@ class ReceiveModeWeb(object): """ The web app routes for receiving files """ - def index_logic(): + @self.web.app.route("/") + def index(): self.web.add_request(self.web.REQUEST_LOAD, request.path) - - if self.common.settings.get('public_mode'): - upload_action = '/upload' - else: - upload_action = '/{}/upload'.format(self.web.slug) - - r = make_response(render_template( - 'receive.html', - upload_action=upload_action)) + r = make_response(render_template('receive.html', + static_url_path=self.web.static_url_path)) return self.web.add_security_headers(r) - @self.web.app.route("/") - def index(slug_candidate): - if not self.can_upload: - return self.web.error403() - self.web.check_slug_candidate(slug_candidate) - return index_logic() - - @self.web.app.route("/") - def index_public(): - if not self.can_upload: - return self.web.error403() - if not self.common.settings.get('public_mode'): - return self.web.error404() - return index_logic() - - - def upload_logic(slug_candidate='', ajax=False): + @self.web.app.route("/upload", methods=['POST']) + def upload(ajax=False): """ Handle the upload files POST request, though at this point, the files have already been uploaded and saved to their correct locations. @@ -97,11 +76,7 @@ class ReceiveModeWeb(object): return json.dumps({"error_flashes": [msg]}) else: flash(msg, 'error') - - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) + return redirect('/') # Note that flash strings are in English, and not translated, on purpose, # to avoid leaking the locale of the OnionShare user @@ -128,48 +103,22 @@ class ReceiveModeWeb(object): if ajax: return json.dumps({"info_flashes": info_flashes}) else: - if self.common.settings.get('public_mode'): - path = '/' - else: - path = '/{}'.format(slug_candidate) - return redirect('{}'.format(path)) + return redirect('/') else: if ajax: - return json.dumps({"new_body": render_template('thankyou.html')}) + return json.dumps({ + "new_body": render_template('thankyou.html', static_url_path=self.web.static_url_path) + }) else: # It was the last upload and the timer ran out - r = make_response(render_template('thankyou.html')) + r = make_response(render_template('thankyou.html'), static_url_path=self.web.static_url_path) return self.web.add_security_headers(r) - @self.web.app.route("//upload", methods=['POST']) - def upload(slug_candidate): - if not self.can_upload: - return self.web.error403() - self.web.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate) - - @self.web.app.route("/upload", methods=['POST']) - def upload_public(): - if not self.can_upload: - return self.web.error403() - if not self.common.settings.get('public_mode'): - return self.web.error404() - return upload_logic() - - @self.web.app.route("//upload-ajax", methods=['POST']) - def upload_ajax(slug_candidate): - if not self.can_upload: - return self.web.error403() - self.web.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate, ajax=True) - @self.web.app.route("/upload-ajax", methods=['POST']) def upload_ajax_public(): if not self.can_upload: return self.web.error403() - if not self.common.settings.get('public_mode'): - return self.web.error404() - return upload_logic(ajax=True) + return upload(ajax=True) class ReceiveModeWSGIMiddleware(object): @@ -272,12 +221,8 @@ class ReceiveModeRequest(Request): # Is this a valid upload request? self.upload_request = False if self.method == 'POST': - if self.web.common.settings.get('public_mode'): - if self.path == '/upload' or self.path == '/upload-ajax': - self.upload_request = True - else: - if self.path == '/{}/upload'.format(self.web.slug) or self.path == '/{}/upload-ajax'.format(self.web.slug): - self.upload_request = True + if self.path == '/upload' or self.path == '/upload-ajax': + self.upload_request = True if self.upload_request: # No errors yet diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index a0c8dc90..0dfa7e0a 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -44,18 +44,8 @@ class ShareModeWeb(object): """ The web app routes for sharing files """ - @self.web.app.route("/") - def index(slug_candidate): - self.web.check_slug_candidate(slug_candidate) - return index_logic() - @self.web.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.web.error404() - return index_logic() - - def index_logic(slug_candidate=''): + def index(): """ Render the template for the onionshare landing page. """ @@ -65,7 +55,8 @@ class ShareModeWeb(object): # currently a download deny_download = not self.web.stay_open and self.download_in_progress if deny_download: - r = make_response(render_template('denied.html')) + r = make_response(render_template('denied.html'), + static_url_path=self.web.static_url_path) return self.web.add_security_headers(r) # If download is allowed to continue, serve download page @@ -74,38 +65,18 @@ class ShareModeWeb(object): else: self.filesize = self.download_filesize - if self.web.slug: - r = make_response(render_template( - 'send.html', - slug=self.web.slug, - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - else: - # If download is allowed to continue, serve download page - r = make_response(render_template( - 'send.html', - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) + r = make_response(render_template( + 'send.html', + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + filesize=self.filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped, + static_url_path=self.web.static_url_path)) return self.web.add_security_headers(r) - @self.web.app.route("//download") - def download(slug_candidate): - self.web.check_slug_candidate(slug_candidate) - return download_logic() - @self.web.app.route("/download") - def download_public(): - if not self.common.settings.get('public_mode'): - return self.web.error404() - return download_logic() - - def download_logic(slug_candidate=''): + def download(): """ Download the zip file. """ @@ -113,7 +84,8 @@ class ShareModeWeb(object): # currently a download deny_download = not self.web.stay_open and self.download_in_progress if deny_download: - r = make_response(render_template('denied.html')) + r = make_response(render_template('denied.html', + static_url_path=self.web.static_url_path)) return self.web.add_security_headers(r) # Each download has a unique id diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 0ba8c6b3..1e040b54 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -5,11 +5,13 @@ import queue import socket import sys import tempfile +import requests from distutils.version import LooseVersion as Version from urllib.request import urlopen import flask from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version +from flask_httpauth import HTTPBasicAuth from .. import strings @@ -43,6 +45,7 @@ class Web(object): REQUEST_UPLOAD_FINISHED = 8 REQUEST_UPLOAD_CANCELED = 9 REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10 + REQUEST_INVALID_PASSWORD = 11 def __init__(self, common, is_gui, mode='share'): self.common = common @@ -53,6 +56,9 @@ class Web(object): static_folder=self.common.get_resource_path('static'), template_folder=self.common.get_resource_path('templates')) self.app.secret_key = self.common.random_string(8) + self.generate_static_url_path() + self.auth = HTTPBasicAuth() + self.auth.error_handler(self.error401) # Verbose mode? if self.common.verbose: @@ -92,13 +98,14 @@ class Web(object): ] self.q = queue.Queue() - self.slug = None - self.error404_count = 0 + self.password = None + + self.reset_invalid_passwords() self.done = 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 = self.common.random_string(16) + self.shutdown_password = self.common.random_string(16) # Keep track if the server is running self.running = False @@ -119,51 +126,80 @@ class Web(object): def define_common_routes(self): """ - Common web app routes between sending, receiving and website modes. + Common web app routes between all modes. """ + + @self.auth.get_password + def get_pw(username): + if username == 'onionshare': + return self.password + else: + return None + + @self.app.before_request + def conditional_auth_check(): + # Allow static files without basic authentication + if(request.path.startswith(self.static_url_path + '/')): + return None + + # If public mode is disabled, require authentication + if not self.common.settings.get('public_mode'): + @self.auth.login_required + def _check_login(): + return None + + return _check_login() + @self.app.errorhandler(404) - def page_not_found(e): - """ - 404 error page. - """ + def not_found(e): return self.error404() - @self.app.route("//shutdown") - def shutdown(slug_candidate): + @self.app.route("//shutdown") + def shutdown(password_candidate): """ Stop the flask web server, from the context of an http request. """ - self.check_shutdown_slug_candidate(slug_candidate) - self.force_shutdown() - return "" + if password_candidate == self.shutdown_password: + self.force_shutdown() + return "" + abort(404) @self.app.route("/noscript-xss-instructions") def noscript_xss_instructions(): """ Display instructions for disabling Tor Browser's NoScript XSS setting """ - r = make_response(render_template('receive_noscript_xss.html')) + r = make_response(render_template('receive_noscript_xss.html', + static_url_path=self.static_url_path)) return self.add_security_headers(r) + def error401(self): + auth = request.authorization + if auth: + if auth['username'] == 'onionshare' and auth['password'] not in self.invalid_passwords: + print('Invalid password guess: {}'.format(auth['password'])) + self.add_request(Web.REQUEST_INVALID_PASSWORD, data=auth['password']) + + self.invalid_passwords.append(auth['password']) + self.invalid_passwords_count += 1 + + if self.invalid_passwords_count == 20: + self.add_request(Web.REQUEST_RATE_LIMIT) + self.force_shutdown() + print("Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.") + + r = make_response(render_template('401.html', static_url_path=self.static_url_path), 401) + return self.add_security_headers(r) + def error404(self): self.add_request(Web.REQUEST_OTHER, request.path) - if request.path != '/favicon.ico': - self.error404_count += 1 - - # In receive mode, with public mode enabled, skip rate limiting 404s - if not self.common.settings.get('public_mode'): - if self.error404_count == 20: - self.add_request(Web.REQUEST_RATE_LIMIT, request.path) - self.force_shutdown() - print("Someone has made too many wrong attempts on your address, which means they could be trying to guess it, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.") - - r = make_response(render_template('404.html'), 404) + r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404) return self.add_security_headers(r) def error403(self): self.add_request(Web.REQUEST_OTHER, request.path) - r = make_response(render_template('403.html'), 403) + r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403) return self.add_security_headers(r) def add_security_headers(self, r): @@ -179,7 +215,7 @@ class Web(object): return True return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) - def add_request(self, request_type, path, data=None): + def add_request(self, request_type, path=None, data=None): """ Add a request to the queue, to communicate with the GUI. """ @@ -189,14 +225,26 @@ class Web(object): 'data': data }) - def generate_slug(self, persistent_slug=None): - self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug)) - if persistent_slug != None and persistent_slug != '': - self.slug = persistent_slug - self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug)) + def generate_password(self, persistent_password=None): + self.common.log('Web', 'generate_password', 'persistent_password={}'.format(persistent_password)) + if persistent_password != None and persistent_password != '': + self.password = persistent_password + self.common.log('Web', 'generate_password', 'persistent_password sent, so password is: "{}"'.format(self.password)) else: - self.slug = self.common.build_slug() - self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug)) + self.password = self.common.build_password() + self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password)) + + def generate_static_url_path(self): + # The static URL path has a 128-bit random number in it to avoid having name + # collisions with files that might be getting shared + self.static_url_path = '/static_{}'.format(self.common.random_string(16)) + self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(self.static_url_path)) + + # Update the flask route to handle the new static URL path + self.app.static_url_path = self.static_url_path + self.app.add_url_rule( + self.static_url_path + '/', + endpoint='static', view_func=self.app.send_static_file) def verbose_mode(self): """ @@ -207,17 +255,9 @@ class Web(object): log_handler.setLevel(logging.WARNING) self.app.logger.addHandler(log_handler) - def check_slug_candidate(self, slug_candidate): - self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate)) - if self.common.settings.get('public_mode'): - abort(404) - if not hmac.compare_digest(self.slug, slug_candidate): - abort(404) - - def check_shutdown_slug_candidate(self, slug_candidate): - self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate)) - if not hmac.compare_digest(self.shutdown_slug, slug_candidate): - abort(404) + def reset_invalid_passwords(self): + self.invalid_passwords_count = 0 + self.invalid_passwords = [] def force_shutdown(self): """ @@ -233,11 +273,11 @@ class Web(object): pass self.running = False - def start(self, port, stay_open=False, public_mode=False, slug=None): + def start(self, port, stay_open=False, public_mode=False, password=None): """ Start the flask web server. """ - self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, slug={}'.format(port, stay_open, public_mode, slug)) + self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, password={}'.format(port, stay_open, public_mode, password)) self.stay_open = stay_open @@ -266,17 +306,11 @@ class Web(object): # Let the mode know that the user stopped the server self.stop_q.put(True) - # Reset any slug that was in use - self.slug = None - - # To stop flask, load http://127.0.0.1://shutdown + # To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown + # (We're putting the shutdown_password in the path as well to make routing simpler) if self.running: - 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(self.shutdown_slug)) - except: - try: - urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read() - except: - pass + requests.get('http://127.0.0.1:{}/{}/shutdown'.format(port, self.shutdown_password), + auth=requests.auth.HTTPBasicAuth('onionshare', self.password)) + + # Reset any password that was in use + self.password = None diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index 39f41b3e..d2cd6db9 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -3,7 +3,6 @@ import sys import tempfile import mimetypes from flask import Response, request, render_template, make_response, send_from_directory -from flask_httpauth import HTTPBasicAuth from .. import strings @@ -17,7 +16,6 @@ class WebsiteModeWeb(object): self.common.log('WebsiteModeWeb', '__init__') self.web = web - self.auth = HTTPBasicAuth() # Dictionary mapping file paths to filenames on disk self.files = {} @@ -26,8 +24,6 @@ class WebsiteModeWeb(object): # Reset assets path self.web.app.static_folder=self.common.get_resource_path('static') - self.users = { } - self.define_routes() def define_routes(self): @@ -35,24 +31,6 @@ class WebsiteModeWeb(object): The web app routes for sharing a website """ - @self.auth.get_password - def get_pw(username): - self.users['onionshare'] = self.web.slug - - if username in self.users: - return self.users.get(username) - else: - return None - - @self.web.app.before_request - def conditional_auth_check(): - if not self.common.settings.get('public_mode'): - @self.auth.login_required - def _check_login(): - return None - - return _check_login() - @self.web.app.route('/', defaults={'path': ''}) @self.web.app.route('/') def path_public(path): @@ -153,7 +131,8 @@ class WebsiteModeWeb(object): r = make_response(render_template('listing.html', path=path, files=files, - dirs=dirs)) + dirs=dirs, + static_url_path=self.web.static_url_path)) return self.web.add_security_headers(r) def set_file_info(self, filenames): diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/mode/__init__.py index 8f5ff32b..e92e36f8 100644 --- a/onionshare_gui/mode/__init__.py +++ b/onionshare_gui/mode/__init__.py @@ -24,7 +24,7 @@ from onionshare.common import AutoStopTimer from ..server_status import ServerStatus from ..threads import OnionThread -from ..threads import AutoStartTimer +from ..threads import AutoStartTimer from ..widgets import Alert class Mode(QtWidgets.QWidget): @@ -181,7 +181,7 @@ class Mode(QtWidgets.QWidget): self.app.port = None # Start the onion thread. If this share was scheduled for a future date, - # the OnionThread will start and exit 'early' to obtain the port, slug + # the OnionThread will start and exit 'early' to obtain the port, password # and onion address, but it will not start the WebThread yet. if self.server_status.autostart_timer_datetime: self.start_onion_thread(obtain_onion_early=True) diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index 4c0b49ba..dbc0bc73 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -113,7 +113,7 @@ class ReceiveMode(Mode): """ # Reset web counters self.web.receive_mode.upload_count = 0 - self.web.error404_count = 0 + self.web.reset_invalid_passwords() # Hide and reset the uploads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 1ee40ca3..143fd577 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -147,7 +147,7 @@ class ShareMode(Mode): """ # Reset web counters self.web.share_mode.download_count = 0 - self.web.error404_count = 0 + self.web.reset_invalid_passwords() # Hide and reset the downloads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index 50d72564..50af4725 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -142,7 +142,7 @@ class WebsiteMode(Mode): """ # Reset web counters self.web.website_mode.visit_count = 0 - self.web.error404_count = 0 + self.web.reset_invalid_passwords() # Hide and reset the downloads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 6dec82b2..bed86895 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -474,8 +474,11 @@ class OnionShareGui(QtWidgets.QMainWindow): Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"])) if event["type"] == Web.REQUEST_OTHER: - if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_slug): - self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.error404_count, strings._('other_page_loaded'), event["path"])) + if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_password): + self.status_bar.showMessage('{0:s}: {1:s}'.format(strings._('other_page_loaded'), event["path"])) + + if event["type"] == Web.REQUEST_INVALID_PASSWORD: + self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.invalid_passwords_count, strings._('invalid_password_guess'), event["data"])) mode.timer_callback() diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index e8385e64..3a6e31cc 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -243,8 +243,8 @@ class ServerStatus(QtWidgets.QWidget): self.show_url() if self.common.settings.get('save_private_key'): - if not self.common.settings.get('slug'): - self.common.settings.set('slug', self.web.slug) + if not self.common.settings.get('password'): + self.common.settings.set('password', self.web.password) self.common.settings.save() if self.common.settings.get('autostart_timer'): @@ -285,7 +285,7 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setEnabled(True) if self.mode == ServerStatus.MODE_SHARE: self.server_button.setText(strings._('gui_share_stop_server')) - if self.mode == ServerStatus.MODE_WEBSITE: + elif self.mode == ServerStatus.MODE_WEBSITE: self.server_button.setText(strings._('gui_share_stop_server')) else: self.server_button.setText(strings._('gui_receive_stop_server')) @@ -420,8 +420,6 @@ class ServerStatus(QtWidgets.QWidget): """ if self.common.settings.get('public_mode'): url = 'http://{0:s}'.format(self.app.onion_host) - elif self.mode == ServerStatus.MODE_WEBSITE: - url = 'http://onionshare:{0:s}@{1:s}'.format(self.web.slug, self.app.onion_host) else: - url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug) + url = 'http://onionshare:{0:s}@{1:s}'.format(self.web.password, self.app.onion_host) return url diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 3c0b83f4..ae5f5acf 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -54,7 +54,7 @@ class SettingsDialog(QtWidgets.QDialog): # General settings - # Use a slug or not ('public mode') + # Use a password or not ('public mode') self.public_mode_checkbox = QtWidgets.QCheckBox() self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) self.public_mode_checkbox.setText(strings._("gui_settings_public_mode_checkbox")) @@ -968,12 +968,12 @@ class SettingsDialog(QtWidgets.QDialog): if self.save_private_key_checkbox.isChecked(): settings.set('save_private_key', True) settings.set('private_key', self.old_settings.get('private_key')) - settings.set('slug', self.old_settings.get('slug')) + settings.set('password', self.old_settings.get('password')) settings.set('hidservauth_string', self.old_settings.get('hidservauth_string')) else: settings.set('save_private_key', False) settings.set('private_key', '') - settings.set('slug', '') + settings.set('password', '') # Also unset the HidServAuth if we are removing our reusable private key settings.set('hidservauth_string', '') diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 26a9ee6b..57e0f0af 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -42,13 +42,16 @@ class OnionThread(QtCore.QThread): def run(self): self.mode.common.log('OnionThread', 'run') - # Choose port and slug early, because we need them to exist in advance for scheduled shares + # Make a new static URL path for each new share + self.mode.web.generate_static_url_path() + + # Choose port and password early, because we need them to exist in advance for scheduled shares self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') if not self.mode.app.port: self.mode.app.choose_port() if not self.mode.common.settings.get('public_mode'): - if not self.mode.web.slug: - self.mode.web.generate_slug(self.mode.common.settings.get('slug')) + if not self.mode.web.password: + self.mode.web.generate_password(self.mode.common.settings.get('password')) try: if self.mode.obtain_onion_early: @@ -86,7 +89,7 @@ class WebThread(QtCore.QThread): def run(self): self.mode.common.log('WebThread', 'run') - self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.web.slug) + self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.web.password) self.success.emit() diff --git a/share/locale/en.json b/share/locale/en.json index 7183e734..2063a415 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -3,6 +3,7 @@ "not_a_readable_file": "{0:s} is not a readable file.", "no_available_port": "Could not find an available port to start the onion service", "other_page_loaded": "Address loaded", + "invalid_password_guess": "Invalid password guess", "close_on_autostop_timer": "Stopped because auto-stop timer ran out", "closing_automatically": "Stopped because transfer is complete", "large_filesize": "Warning: Sending a large share could take hours", @@ -34,7 +35,7 @@ "gui_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?", "gui_quit_warning_quit": "Quit", "gui_quit_warning_dont_quit": "Cancel", - "error_rate_limit": "Someone has made too many wrong attempts on your address, which means they could be trying to guess it, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.", + "error_rate_limit": "Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.", "zip_progress_bar_format": "Compressing: %p%", "error_stealth_not_supported": "To use client authorization, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.", "error_ephemeral_not_supported": "OnionShare requires at least both Tor 0.2.7.1 and python3-stem 1.4.0.", diff --git a/share/static/js/receive.js b/share/static/js/receive.js index c29c726c..cbd60954 100644 --- a/share/static/js/receive.js +++ b/share/static/js/receive.js @@ -121,7 +121,7 @@ $(function(){ $('#uploads').append($upload_div); // Send the request - ajax.open('POST', window.location.pathname.replace(/\/$/, '') + '/upload-ajax', true); + ajax.open('POST', '/upload-ajax', true); ajax.send(formData); }); }); diff --git a/share/templates/401.html b/share/templates/401.html new file mode 100644 index 00000000..dc50f534 --- /dev/null +++ b/share/templates/401.html @@ -0,0 +1,19 @@ + + + + + OnionShare: 401 Unauthorized Access + + + + + +
+
+

+

401 Unauthorized Access

+
+
+ + + diff --git a/share/templates/403.html b/share/templates/403.html index f3ea4e0e..2ebab09a 100644 --- a/share/templates/403.html +++ b/share/templates/403.html @@ -3,14 +3,14 @@ OnionShare: 403 Forbidden - - + +
-

+

You are not allowed to perform that action at this time.

diff --git a/share/templates/404.html b/share/templates/404.html index 1c5d7d2d..375c125d 100644 --- a/share/templates/404.html +++ b/share/templates/404.html @@ -3,14 +3,14 @@ OnionShare: 404 Not Found - - + +
-

+

404 Not Found

diff --git a/share/templates/denied.html b/share/templates/denied.html index 94fb379b..ad4d0b21 100644 --- a/share/templates/denied.html +++ b/share/templates/denied.html @@ -3,7 +3,7 @@ OnionShare - + diff --git a/share/templates/listing.html b/share/templates/listing.html index 8883eea9..e394f842 100644 --- a/share/templates/listing.html +++ b/share/templates/listing.html @@ -2,13 +2,13 @@ OnionShare - - + +
- +

OnionShare

@@ -22,7 +22,7 @@ {% for info in dirs %} - + {{ info.basename }} @@ -34,7 +34,7 @@ {% for info in files %} - + {{ info.basename }} diff --git a/share/templates/receive.html b/share/templates/receive.html index 4f207a03..23242501 100644 --- a/share/templates/receive.html +++ b/share/templates/receive.html @@ -2,13 +2,13 @@ OnionShare - - + +
- +

OnionShare

@@ -19,14 +19,14 @@ -->

- Warning: Due to a bug in Tor Browser and Firefox, uploads + Warning: Due to a bug in Tor Browser and Firefox, uploads sometimes never finish. To upload reliably, either set your Tor Browser security slider to Standard or turn off your Tor Browser's NoScript XSS setting.

-

+

Send Files

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

@@ -45,14 +45,14 @@ -
+

- - - + + + diff --git a/share/templates/receive_noscript_xss.html b/share/templates/receive_noscript_xss.html index bce78524..84d35ba1 100644 --- a/share/templates/receive_noscript_xss.html +++ b/share/templates/receive_noscript_xss.html @@ -2,13 +2,13 @@ OnionShare - - + +
- +

OnionShare

diff --git a/share/templates/send.html b/share/templates/send.html index 2a56829a..e0076c0f 100644 --- a/share/templates/send.html +++ b/share/templates/send.html @@ -3,8 +3,8 @@ OnionShare - - + + @@ -15,14 +15,10 @@
  • Total size: {{ filesize_human }} {% if is_zipped %} (compressed){% endif %}
  • - {% if slug %} -
  • Download Files
  • - {% else %}
  • Download Files
  • - {% endif %}
- +

OnionShare

@@ -35,7 +31,7 @@ {% for info in file_info.dirs %} - + {{ info.basename }} {{ info.size_human }} @@ -45,7 +41,7 @@ {% for info in file_info.files %} - + {{ info.basename }} {{ info.size_human }} @@ -53,7 +49,7 @@ {% endfor %} - + diff --git a/share/templates/thankyou.html b/share/templates/thankyou.html index c4b39cde..b7e2b97c 100644 --- a/share/templates/thankyou.html +++ b/share/templates/thankyou.html @@ -3,19 +3,19 @@ OnionShare is closed - - + +
- +

OnionShare

-

+

Thank you for using OnionShare

You may now close this window.

diff --git a/tests/GuiBaseTest.py b/tests/GuiBaseTest.py index d3fc9945..2f340396 100644 --- a/tests/GuiBaseTest.py +++ b/tests/GuiBaseTest.py @@ -2,8 +2,7 @@ import json import os import requests import shutil -import socket -import socks +import base64 from PyQt5 import QtCore, QtTest @@ -126,20 +125,20 @@ class GuiBaseTest(object): if type(mode) == ReceiveMode: # Upload a file files = {'file[]': open('/tmp/test.txt', 'rb')} - if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, mode.web.slug) + url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + if public_mode: + r = requests.post(url, files=files) else: - path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) - response = requests.post(path, files=files) + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) QtTest.QTest.qWait(2000) if type(mode) == ShareMode: # Download files + url = "http://127.0.0.1:{}/download".format(self.gui.app.port) if public_mode: - url = "http://127.0.0.1:{}/download".format(self.gui.app.port) + r = requests.get(url) else: - url = "http://127.0.0.1:{}/{}/download".format(self.gui.app.port, mode.web.slug) - r = requests.get(url) + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) QtTest.QTest.qWait(2000) # Indicator should be visible, have a value of "1" @@ -185,17 +184,19 @@ class GuiBaseTest(object): def web_server_is_running(self): '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + try: + r = requests.get('http://127.0.0.1:{}/'.format(self.gui.app.port)) + self.assertTrue(True) + except requests.exceptions.ConnectionError: + self.assertTrue(False) - def have_a_slug(self, mode, public_mode): - '''Test that we have a valid slug''' + def have_a_password(self, mode, public_mode): + '''Test that we have a valid password''' if not public_mode: - self.assertRegex(mode.server_status.web.slug, r'(\w+)-(\w+)') + self.assertRegex(mode.server_status.web.password, r'(\w+)-(\w+)') else: - self.assertIsNone(mode.server_status.web.slug, r'(\w+)-(\w+)') + self.assertIsNone(mode.server_status.web.password, r'(\w+)-(\w+)') def url_description_shown(self, mode): @@ -212,7 +213,7 @@ class GuiBaseTest(object): if public_mode: self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}'.format(self.gui.app.port)) else: - self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}/{}'.format(self.gui.app.port, mode.server_status.web.slug)) + self.assertEqual(clipboard.text(), 'http://onionshare:{}@127.0.0.1:{}'.format(mode.server_status.web.password, self.gui.app.port)) def server_status_indicator_says_started(self, mode): @@ -225,31 +226,14 @@ class GuiBaseTest(object): def web_page(self, mode, string, public_mode): '''Test that the web page contains a string''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - if not public_mode: - path = '/{}'.format(mode.server_status.web.slug) + url = "http://127.0.0.1:{}/".format(self.gui.app.port) + if public_mode: + r = requests.get(url) else: - path = '/' + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) - http_request = 'GET {} HTTP/1.0\r\n'.format(path) - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/webpage', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - f = open('/tmp/webpage') - self.assertTrue(string in f.read()) - f.close() + self.assertTrue(string in r.text) def history_widgets_present(self, mode): @@ -273,10 +257,12 @@ class GuiBaseTest(object): def web_server_is_stopped(self): '''Test that the web server also stopped''' QtTest.QTest.qWait(2000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + try: + r = requests.get('http://127.0.0.1:{}/'.format(self.gui.app.port)) + self.assertTrue(False) + except requests.exceptions.ConnectionError: + self.assertTrue(True) def server_status_indicator_says_closed(self, mode, stay_open): diff --git a/tests/GuiReceiveTest.py b/tests/GuiReceiveTest.py index 40c3de95..c4bfa884 100644 --- a/tests/GuiReceiveTest.py +++ b/tests/GuiReceiveTest.py @@ -7,18 +7,26 @@ from .GuiBaseTest import GuiBaseTest class GuiReceiveTest(GuiBaseTest): def upload_file(self, public_mode, file_to_upload, expected_basename, identical_files_at_once=False): '''Test that we can upload the file''' - files = {'file[]': open(file_to_upload, 'rb')} - if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) - else: - path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) - response = requests.post(path, files=files) - if identical_files_at_once: - # Send a duplicate upload to test for collisions - response = requests.post(path, files=files) + + # Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused QtTest.QTest.qWait(2000) - # Make sure the file is within the last 10 seconds worth of filenames + files = {'file[]': open(file_to_upload, 'rb')} + url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + if public_mode: + r = requests.post(url, files=files) + if identical_files_at_once: + # Send a duplicate upload to test for collisions + r = requests.post(url, files=files) + else: + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password)) + if identical_files_at_once: + # Send a duplicate upload to test for collisions + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password)) + + QtTest.QTest.qWait(2000) + + # Make sure the file is within the last 10 seconds worth of fileames exists = False now = datetime.now() for i in range(10): @@ -39,31 +47,28 @@ class GuiReceiveTest(GuiBaseTest): def upload_file_should_fail(self, public_mode): '''Test that we can't upload the file when permissions are wrong, and expected content is shown''' files = {'file[]': open('/tmp/test.txt', 'rb')} - if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) + url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + if public_mode: + r = requests.post(url, files=files) else: - path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) - response = requests.post(path, files=files) + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password)) QtCore.QTimer.singleShot(1000, self.accept_dialog) - self.assertTrue('Error uploading, please inform the OnionShare user' in response.text) + self.assertTrue('Error uploading, please inform the OnionShare user' in r.text) def upload_dir_permissions(self, mode=0o755): '''Manipulate the permissions on the upload dir in between tests''' os.chmod('/tmp/OnionShare', mode) - def try_public_paths_in_non_public_mode(self): - response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port)) - self.assertEqual(response.status_code, 404) - response = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port)) - self.assertEqual(response.status_code, 404) + def try_without_auth_in_non_public_mode(self): + r = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port)) + self.assertEqual(r.status_code, 401) + r = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port)) + self.assertEqual(r.status_code, 401) def uploading_zero_files_shouldnt_change_ui(self, mode, public_mode): '''If you submit the receive mode form without selecting any files, the UI shouldn't get updated''' - if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) - else: - path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) # What were the counts before submitting the form? before_in_progress_count = mode.history.in_progress_count @@ -71,9 +76,15 @@ class GuiReceiveTest(GuiBaseTest): before_number_of_history_items = len(mode.history.item_list.items) # Click submit without including any files a few times - response = requests.post(path, files={}) - response = requests.post(path, files={}) - response = requests.post(path, files={}) + if public_mode: + r = requests.post(url, files={}) + r = requests.post(url, files={}) + r = requests.post(url, files={}) + else: + auth = requests.auth.HTTPBasicAuth('onionshare', mode.web.password) + r = requests.post(url, files={}, auth=auth) + r = requests.post(url, files={}, auth=auth) + r = requests.post(url, files={}, auth=auth) # The counts shouldn't change self.assertEqual(mode.history.in_progress_count, before_in_progress_count) @@ -93,17 +104,17 @@ class GuiReceiveTest(GuiBaseTest): self.settings_button_is_hidden() self.server_is_started(self.gui.receive_mode) self.web_server_is_running() - self.have_a_slug(self.gui.receive_mode, public_mode) + self.have_a_password(self.gui.receive_mode, public_mode) self.url_description_shown(self.gui.receive_mode) self.have_copy_url_button(self.gui.receive_mode, public_mode) self.server_status_indicator_says_started(self.gui.receive_mode) self.web_page(self.gui.receive_mode, 'Select the files you want to send, then click', public_mode) - def run_all_receive_mode_tests(self, public_mode, receive_allow_receiver_shutdown): + def run_all_receive_mode_tests(self, public_mode): '''Upload files in receive mode and stop the share''' self.run_all_receive_mode_setup_tests(public_mode) if not public_mode: - self.try_public_paths_in_non_public_mode() + self.try_without_auth_in_non_public_mode() self.upload_file(public_mode, '/tmp/test.txt', 'test.txt') self.history_widgets_present(self.gui.receive_mode) self.counter_incremented(self.gui.receive_mode, 1) @@ -125,7 +136,7 @@ class GuiReceiveTest(GuiBaseTest): self.server_is_started(self.gui.receive_mode) self.history_indicator(self.gui.receive_mode, public_mode) - def run_all_receive_mode_unwritable_dir_tests(self, public_mode, receive_allow_receiver_shutdown): + def run_all_receive_mode_unwritable_dir_tests(self, public_mode): '''Attempt to upload (unwritable) files in receive mode and stop the share''' self.run_all_receive_mode_setup_tests(public_mode) self.upload_dir_permissions(0o400) diff --git a/tests/GuiShareTest.py b/tests/GuiShareTest.py index 29661712..64e57b9f 100644 --- a/tests/GuiShareTest.py +++ b/tests/GuiShareTest.py @@ -2,14 +2,15 @@ import os import requests import socks import zipfile +import tempfile from PyQt5 import QtCore, QtTest from .GuiBaseTest import GuiBaseTest class GuiShareTest(GuiBaseTest): # Persistence tests - def have_same_slug(self, slug): - '''Test that we have the same slug''' - self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) + def have_same_password(self, password): + '''Test that we have the same password''' + self.assertEqual(self.gui.share_mode.server_status.web.password, password) # Share-specific tests @@ -17,7 +18,7 @@ class GuiShareTest(GuiBaseTest): '''Test that the number of items in the list is as expected''' self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), num) - + def deleting_all_files_hides_delete_button(self): '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) @@ -35,14 +36,14 @@ class GuiShareTest(GuiBaseTest): # No more files, the delete button should be hidden self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) - + def add_a_file_and_delete_using_its_delete_widget(self): '''Test that we can also delete a file by clicking on its [X] widget''' self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) self.file_selection_widget_has_files(0) - + def file_selection_widget_readd_files(self): '''Re-add some files to the list so we can share''' self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') @@ -56,49 +57,37 @@ class GuiShareTest(GuiBaseTest): with open('/tmp/large_file', 'wb') as fout: fout.write(os.urandom(size)) self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/large_file') - + def add_delete_buttons_hidden(self): '''Test that the add and delete buttons are hidden when the server starts''' self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) - + def download_share(self, public_mode): '''Test that we can download the share''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - + url = "http://127.0.0.1:{}/download".format(self.gui.app.port) if public_mode: - path = '/download' + r = requests.get(url) else: - path = '{}/download'.format(self.gui.share_mode.web.slug) + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password)) - http_request = 'GET {} HTTP/1.0\r\n'.format(path) - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) + tmp_file = tempfile.NamedTemporaryFile() + with open(tmp_file.name, 'wb') as f: + f.write(r.content) - with open('/tmp/download.zip', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - zip = zipfile.ZipFile('/tmp/download.zip') + zip = zipfile.ZipFile(tmp_file.name) QtTest.QTest.qWait(2000) self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8')) - def hit_404(self, public_mode): - '''Test that the server stops after too many 404s, or doesn't when in public_mode''' - bogus_path = '/gimme' - url = "http://127.0.0.1:{}/{}".format(self.gui.app.port, bogus_path) + def hit_401(self, public_mode): + '''Test that the server stops after too many 401s, or doesn't when in public_mode''' + url = "http://127.0.0.1:{}/".format(self.gui.app.port) for _ in range(20): - r = requests.get(url) + password_guess = self.gui.common.build_password() + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', password_guess)) # A nasty hack to avoid the Alert dialog that blocks the rest of the test if not public_mode: @@ -130,7 +119,7 @@ class GuiShareTest(GuiBaseTest): self.add_a_file_and_delete_using_its_delete_widget() self.file_selection_widget_readd_files() - + def run_all_share_mode_started_tests(self, public_mode, startup_time=2000): """Tests in share mode after starting a share""" self.server_working_on_start_button_pressed(self.gui.share_mode) @@ -139,12 +128,12 @@ class GuiShareTest(GuiBaseTest): self.settings_button_is_hidden() self.server_is_started(self.gui.share_mode, startup_time) self.web_server_is_running() - self.have_a_slug(self.gui.share_mode, public_mode) + self.have_a_password(self.gui.share_mode, public_mode) self.url_description_shown(self.gui.share_mode) self.have_copy_url_button(self.gui.share_mode, public_mode) self.server_status_indicator_says_started(self.gui.share_mode) - + def run_all_share_mode_download_tests(self, public_mode, stay_open): """Tests in share mode after downloading a share""" self.web_page(self.gui.share_mode, 'Total size', public_mode) @@ -158,7 +147,7 @@ class GuiShareTest(GuiBaseTest): self.server_is_started(self.gui.share_mode) self.history_indicator(self.gui.share_mode, public_mode) - + def run_all_share_mode_tests(self, public_mode, stay_open): """End-to-end share tests""" self.run_all_share_mode_setup_tests() @@ -178,12 +167,12 @@ class GuiShareTest(GuiBaseTest): def run_all_share_mode_persistent_tests(self, public_mode, stay_open): - """Same as end-to-end share tests but also test the slug is the same on multiple shared""" + """Same as end-to-end share tests but also test the password is the same on multiple shared""" self.run_all_share_mode_setup_tests() self.run_all_share_mode_started_tests(public_mode) - slug = self.gui.share_mode.server_status.web.slug + password = self.gui.share_mode.server_status.web.password self.run_all_share_mode_download_tests(public_mode, stay_open) - self.have_same_slug(slug) + self.have_same_password(password) def run_all_share_mode_timer_tests(self, public_mode): diff --git a/tests/TorGuiBaseTest.py b/tests/TorGuiBaseTest.py index 8bd963bd..3f9952d0 100644 --- a/tests/TorGuiBaseTest.py +++ b/tests/TorGuiBaseTest.py @@ -76,7 +76,7 @@ class TorGuiBaseTest(GuiBaseTest): # Upload a file files = {'file[]': open('/tmp/test.txt', 'rb')} if not public_mode: - path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, mode.web.slug) + path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, mode.web.password) else: path = 'http://{}/upload'.format(self.gui.app.onion_host) response = session.post(path, files=files) @@ -87,7 +87,7 @@ class TorGuiBaseTest(GuiBaseTest): if public_mode: path = "http://{}/download".format(self.gui.app.onion_host) else: - path = "http://{}/{}/download".format(self.gui.app.onion_host, mode.web.slug) + path = "http://{}/{}/download".format(self.gui.app.onion_host, mode.web.password) response = session.get(path) QtTest.QTest.qWait(4000) @@ -111,7 +111,7 @@ class TorGuiBaseTest(GuiBaseTest): s.settimeout(60) s.connect((self.gui.app.onion_host, 80)) if not public_mode: - path = '/{}'.format(mode.server_status.web.slug) + path = '/{}'.format(mode.server_status.web.password) else: path = '/' http_request = 'GET {} HTTP/1.0\r\n'.format(path) @@ -138,7 +138,7 @@ class TorGuiBaseTest(GuiBaseTest): if public_mode: self.assertEqual(clipboard.text(), 'http://{}'.format(self.gui.app.onion_host)) else: - self.assertEqual(clipboard.text(), 'http://{}/{}'.format(self.gui.app.onion_host, mode.server_status.web.slug)) + self.assertEqual(clipboard.text(), 'http://{}/{}'.format(self.gui.app.onion_host, mode.server_status.web.password)) # Stealth tests diff --git a/tests/TorGuiReceiveTest.py b/tests/TorGuiReceiveTest.py index a21dd4fc..601f34b6 100644 --- a/tests/TorGuiReceiveTest.py +++ b/tests/TorGuiReceiveTest.py @@ -13,7 +13,7 @@ class TorGuiReceiveTest(TorGuiBaseTest): session.proxies['http'] = 'socks5h://{}:{}'.format(socks_address, socks_port) files = {'file[]': open(file_to_upload, 'rb')} if not public_mode: - path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, self.gui.receive_mode.web.slug) + path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, self.gui.receive_mode.web.password) else: path = 'http://{}/upload'.format(self.gui.app.onion_host) response = session.post(path, files=files) @@ -35,7 +35,7 @@ class TorGuiReceiveTest(TorGuiBaseTest): self.server_is_started(self.gui.receive_mode, startup_time=45000) self.web_server_is_running() self.have_an_onion_service() - self.have_a_slug(self.gui.receive_mode, public_mode) + self.have_a_password(self.gui.receive_mode, public_mode) self.url_description_shown(self.gui.receive_mode) self.have_copy_url_button(self.gui.receive_mode, public_mode) self.server_status_indicator_says_started(self.gui.receive_mode) @@ -56,4 +56,3 @@ class TorGuiReceiveTest(TorGuiBaseTest): self.server_working_on_start_button_pressed(self.gui.receive_mode) self.server_is_started(self.gui.receive_mode, startup_time=45000) self.history_indicator(self.gui.receive_mode, public_mode) - diff --git a/tests/TorGuiShareTest.py b/tests/TorGuiShareTest.py index 36efacd1..352707eb 100644 --- a/tests/TorGuiShareTest.py +++ b/tests/TorGuiShareTest.py @@ -17,7 +17,7 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest): if public_mode: path = "http://{}/download".format(self.gui.app.onion_host) else: - path = "http://{}/{}/download".format(self.gui.app.onion_host, self.gui.share_mode.web.slug) + path = "http://{}/{}/download".format(self.gui.app.onion_host, self.gui.share_mode.web.password) response = session.get(path, stream=True) QtTest.QTest.qWait(4000) @@ -53,7 +53,7 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest): self.server_is_started(self.gui.share_mode, startup_time=45000) self.web_server_is_running() self.have_an_onion_service() - self.have_a_slug(self.gui.share_mode, public_mode) + self.have_a_password(self.gui.share_mode, public_mode) self.url_description_shown(self.gui.share_mode) self.have_copy_url_button(self.gui.share_mode, public_mode) self.server_status_indicator_says_started(self.gui.share_mode) @@ -74,16 +74,16 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest): def run_all_share_mode_persistent_tests(self, public_mode, stay_open): - """Same as end-to-end share tests but also test the slug is the same on multiple shared""" + """Same as end-to-end share tests but also test the password is the same on multiple shared""" self.run_all_share_mode_setup_tests() self.run_all_share_mode_started_tests(public_mode) - slug = self.gui.share_mode.server_status.web.slug + password = self.gui.share_mode.server_status.web.password onion = self.gui.app.onion_host self.run_all_share_mode_download_tests(public_mode, stay_open) self.have_same_onion(onion) - self.have_same_slug(slug) + self.have_same_password(password) + - def run_all_share_mode_timer_tests(self, public_mode): """Auto-stop timer tests in share mode""" self.run_all_share_mode_setup_tests() @@ -92,4 +92,3 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest): self.autostop_timer_widget_hidden(self.gui.share_mode) self.server_timed_out(self.gui.share_mode, 125000) self.web_server_is_stopped() - diff --git a/tests/local_onionshare_404_public_mode_skips_ratelimit_test.py b/tests/local_onionshare_401_public_mode_skips_ratelimit_test.py similarity index 87% rename from tests/local_onionshare_404_public_mode_skips_ratelimit_test.py rename to tests/local_onionshare_401_public_mode_skips_ratelimit_test.py index 4fad5532..f06ea37b 100644 --- a/tests/local_onionshare_404_public_mode_skips_ratelimit_test.py +++ b/tests/local_onionshare_401_public_mode_skips_ratelimit_test.py @@ -4,7 +4,7 @@ import unittest from .GuiShareTest import GuiShareTest -class Local404PublicModeRateLimitTest(unittest.TestCase, GuiShareTest): +class Local401PublicModeRateLimitTest(unittest.TestCase, GuiShareTest): @classmethod def setUpClass(cls): test_settings = { @@ -22,7 +22,7 @@ class Local404PublicModeRateLimitTest(unittest.TestCase, GuiShareTest): def test_gui(self): self.run_all_common_setup_tests() self.run_all_share_mode_tests(True, True) - self.hit_404(True) + self.hit_401(True) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_404_triggers_ratelimit_test.py b/tests/local_onionshare_401_triggers_ratelimit_test.py similarity index 87% rename from tests/local_onionshare_404_triggers_ratelimit_test.py rename to tests/local_onionshare_401_triggers_ratelimit_test.py index 49be0f5b..4100657b 100644 --- a/tests/local_onionshare_404_triggers_ratelimit_test.py +++ b/tests/local_onionshare_401_triggers_ratelimit_test.py @@ -4,7 +4,7 @@ import unittest from .GuiShareTest import GuiShareTest -class Local404RateLimitTest(unittest.TestCase, GuiShareTest): +class Local401RateLimitTest(unittest.TestCase, GuiShareTest): @classmethod def setUpClass(cls): test_settings = { @@ -21,7 +21,7 @@ class Local404RateLimitTest(unittest.TestCase, GuiShareTest): def test_gui(self): self.run_all_common_setup_tests() self.run_all_share_mode_tests(False, True) - self.hit_404(False) + self.hit_401(False) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py b/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py index 5737bae3..26feacc3 100644 --- a/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py +++ b/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py @@ -20,7 +20,7 @@ class LocalReceiveModeUnwritableTest(unittest.TestCase, GuiReceiveTest): @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") def test_gui(self): self.run_all_common_setup_tests() - self.run_all_receive_mode_unwritable_dir_tests(False, True) + self.run_all_receive_mode_unwritable_dir_tests(False) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py b/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py index e6024352..601c4bd2 100644 --- a/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py +++ b/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py @@ -21,7 +21,7 @@ class LocalReceivePublicModeUnwritableTest(unittest.TestCase, GuiReceiveTest): @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") def test_gui(self): self.run_all_common_setup_tests() - self.run_all_receive_mode_unwritable_dir_tests(True, True) + self.run_all_receive_mode_unwritable_dir_tests(True) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_public_mode_test.py b/tests/local_onionshare_receive_mode_upload_public_mode_test.py index 885ae4fe..1f3a8331 100644 --- a/tests/local_onionshare_receive_mode_upload_public_mode_test.py +++ b/tests/local_onionshare_receive_mode_upload_public_mode_test.py @@ -21,7 +21,7 @@ class LocalReceiveModePublicModeTest(unittest.TestCase, GuiReceiveTest): @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") def test_gui(self): self.run_all_common_setup_tests() - self.run_all_receive_mode_tests(True, True) + self.run_all_receive_mode_tests(True) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_test.py b/tests/local_onionshare_receive_mode_upload_test.py index 3d23730c..16036893 100644 --- a/tests/local_onionshare_receive_mode_upload_test.py +++ b/tests/local_onionshare_receive_mode_upload_test.py @@ -20,7 +20,7 @@ class LocalReceiveModeTest(unittest.TestCase, GuiReceiveTest): @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") def test_gui(self): self.run_all_common_setup_tests() - self.run_all_receive_mode_tests(False, True) + self.run_all_receive_mode_tests(False) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_share_mode_slug_persistent_test.py b/tests/local_onionshare_share_mode_password_persistent_test.py similarity index 87% rename from tests/local_onionshare_share_mode_slug_persistent_test.py rename to tests/local_onionshare_share_mode_password_persistent_test.py index 58e1cfeb..5b515ca1 100644 --- a/tests/local_onionshare_share_mode_slug_persistent_test.py +++ b/tests/local_onionshare_share_mode_password_persistent_test.py @@ -4,12 +4,12 @@ import unittest from .GuiShareTest import GuiShareTest -class LocalShareModePersistentSlugTest(unittest.TestCase, GuiShareTest): +class LocalShareModePersistentPasswordTest(unittest.TestCase, GuiShareTest): @classmethod def setUpClass(cls): test_settings = { "public_mode": False, - "slug": "", + "password": "", "save_private_key": True, "close_after_first_download": False, } diff --git a/tests/onionshare_share_mode_persistent_test.py b/tests/onionshare_share_mode_persistent_test.py index d0fb5b22..0e461f7e 100644 --- a/tests/onionshare_share_mode_persistent_test.py +++ b/tests/onionshare_share_mode_persistent_test.py @@ -4,13 +4,13 @@ import unittest from .TorGuiShareTest import TorGuiShareTest -class ShareModePersistentSlugTest(unittest.TestCase, TorGuiShareTest): +class ShareModePersistentPasswordTest(unittest.TestCase, TorGuiShareTest): @classmethod def setUpClass(cls): test_settings = { "use_legacy_v2_onions": True, "public_mode": False, - "slug": "", + "password": "", "save_private_key": True, "close_after_first_download": False, } diff --git a/tests/test_onionshare_common.py b/tests/test_onionshare_common.py index f975dce7..d5e67381 100644 --- a/tests/test_onionshare_common.py +++ b/tests/test_onionshare_common.py @@ -33,13 +33,13 @@ LOG_MSG_REGEX = re.compile(r""" ^\[Jun\ 06\ 2013\ 11:05:00\] \ TestModule\.\.dummy_func \ at\ 0x[a-f0-9]+>(:\ TEST_MSG)?$""", re.VERBOSE) -SLUG_REGEX = re.compile(r'^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$') +PASSWORD_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: +class TestBuildPassword: @pytest.mark.parametrize('test_input,expected', ( # VALID, two lowercase words, separated by a hyphen ('syrup-enzyme', True), @@ -60,8 +60,8 @@ class TestBuildSlug: ('too-many-hyphens-', False), ('symbols-!@#$%', False) )) - def test_build_slug_regex(self, test_input, expected): - """ Test that `SLUG_REGEX` accounts for the following patterns + def test_build_password_regex(self, test_input, expected): + """ Test that `PASSWORD_REGEX` accounts for the following patterns There are a few hyphenated words in `wordlist.txt`: * drop-down @@ -69,17 +69,17 @@ class TestBuildSlug: * t-shirt * yo-yo - These words cause a few extra potential slug patterns: + These words cause a few extra potential password patterns: * word-word * hyphenated-word-word * word-hyphenated-word * hyphenated-word-hyphenated-word """ - assert bool(SLUG_REGEX.match(test_input)) == expected + assert bool(PASSWORD_REGEX.match(test_input)) == expected - def test_build_slug_unique(self, common_obj, sys_onionshare_dev_mode): - assert common_obj.build_slug() != common_obj.build_slug() + def test_build_password_unique(self, common_obj, sys_onionshare_dev_mode): + assert common_obj.build_password() != common_obj.build_password() class TestDirSize: diff --git a/tests/test_onionshare_settings.py b/tests/test_onionshare_settings.py index bcc2f7cb..05878899 100644 --- a/tests/test_onionshare_settings.py +++ b/tests/test_onionshare_settings.py @@ -63,7 +63,7 @@ class TestSettings: 'use_legacy_v2_onions': False, 'save_private_key': False, 'private_key': '', - 'slug': '', + 'password': '', 'hidservauth_string': '', 'data_dir': os.path.expanduser('~/OnionShare'), 'public_mode': False diff --git a/tests/test_onionshare_web.py b/tests/test_onionshare_web.py index 0c29859b..313dbcea 100644 --- a/tests/test_onionshare_web.py +++ b/tests/test_onionshare_web.py @@ -27,8 +27,10 @@ import socket import sys import zipfile import tempfile +import base64 import pytest +from werkzeug.datastructures import Headers from onionshare.common import Common from onionshare import strings @@ -44,7 +46,7 @@ def web_obj(common_obj, mode, num_files=0): common_obj.settings = Settings(common_obj) strings.load_strings(common_obj) web = Web(common_obj, False, mode) - web.generate_slug() + web.generate_password() web.stay_open = True web.running = True @@ -71,22 +73,23 @@ class TestWeb: web = web_obj(common_obj, 'share', 3) assert web.mode is 'share' with web.app.test_client() as c: - # Load 404 pages + # Load / without auth res = c.get('/') res.get_data() - assert res.status_code == 404 + assert res.status_code == 401 - res = c.get('/invalidslug'.format(web.slug)) + # Load / with invalid auth + res = c.get('/', headers=self._make_auth_headers('invalid')) res.get_data() - assert res.status_code == 404 + assert res.status_code == 401 - # Load download page - res = c.get('/{}'.format(web.slug)) + # Load / with valid auth + res = c.get('/', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 # Download - res = c.get('/{}/download'.format(web.slug)) + res = c.get('/download', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 assert res.mimetype == 'application/zip' @@ -99,7 +102,7 @@ class TestWeb: with web.app.test_client() as c: # Download the first time - res = c.get('/{}/download'.format(web.slug)) + res = c.get('/download', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 assert res.mimetype == 'application/zip' @@ -114,7 +117,7 @@ class TestWeb: with web.app.test_client() as c: # Download the first time - res = c.get('/{}/download'.format(web.slug)) + res = c.get('/download', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 assert res.mimetype == 'application/zip' @@ -125,17 +128,18 @@ class TestWeb: assert web.mode is 'receive' with web.app.test_client() as c: - # Load 404 pages + # Load / without auth res = c.get('/') res.get_data() - assert res.status_code == 404 + assert res.status_code == 401 - res = c.get('/invalidslug'.format(web.slug)) + # Load / with invalid auth + res = c.get('/', headers=self._make_auth_headers('invalid')) res.get_data() - assert res.status_code == 404 + assert res.status_code == 401 - # Load upload page - res = c.get('/{}'.format(web.slug)) + # Load / with valid auth + res = c.get('/', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 @@ -144,31 +148,37 @@ class TestWeb: common_obj.settings.set('public_mode', True) with web.app.test_client() as c: - # Upload page should be accessible from / + # Loading / should work without auth res = c.get('/') data1 = res.get_data() assert res.status_code == 200 - # /[slug] should be a 404 - res = c.get('/{}'.format(web.slug)) - data2 = res.get_data() - assert res.status_code == 404 - def test_public_mode_off(self, common_obj): web = web_obj(common_obj, 'receive') common_obj.settings.set('public_mode', False) with web.app.test_client() as c: - # / should be a 404 + # Load / without auth res = c.get('/') - data1 = res.get_data() - assert res.status_code == 404 + res.get_data() + assert res.status_code == 401 - # Upload page should be accessible from /[slug] - res = c.get('/{}'.format(web.slug)) - data2 = res.get_data() + # But static resources should work without auth + res = c.get('{}/css/style.css'.format(web.static_url_path)) + res.get_data() assert res.status_code == 200 + # Load / with valid auth + res = c.get('/', headers=self._make_auth_headers(web.password)) + res.get_data() + assert res.status_code == 200 + + def _make_auth_headers(self, password): + auth = base64.b64encode(b'onionshare:'+password.encode()).decode() + h = Headers() + h.add('Authorization', 'Basic ' + auth) + return h + class TestZipWriterDefault: @pytest.mark.parametrize('test_input', (