Merge pull request #996 from micahflee/basic_auth_everywhere

Switch from slugs to HTTP basic auth
This commit is contained in:
Micah Lee 2019-05-29 19:15:40 -07:00 committed by GitHub
commit 69eb7f81a3
44 changed files with 396 additions and 451 deletions

View File

@ -42,7 +42,7 @@ jobs:
- run: - run:
name: run tests name: run tests
command: | 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-3.6:
<<: *test-template <<: *test-template

View File

@ -27,6 +27,15 @@ from .web import Web
from .onion import * from .onion import *
from .onionshare import OnionShare 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): def main(cwd=None):
""" """
The main() function implements all of the logic that the command-line version of The main() function implements all of the logic that the command-line version of
@ -121,12 +130,13 @@ def main(cwd=None):
try: try:
common.settings.load() common.settings.load()
if not common.settings.get('public_mode'): if not common.settings.get('public_mode'):
web.generate_slug(common.settings.get('slug')) web.generate_password(common.settings.get('password'))
else: else:
web.slug = None web.password = None
app = OnionShare(common, onion, local_only, autostop_timer) app = OnionShare(common, onion, local_only, autostop_timer)
app.set_stealth(stealth) app.set_stealth(stealth)
app.choose_port() app.choose_port()
# Delay the startup if a startup timer was set # Delay the startup if a startup timer was set
if autostart_timer > 0: if autostart_timer > 0:
# Can't set a schedule that is later than the auto-stop timer # Can't set a schedule that is later than the auto-stop timer
@ -135,10 +145,7 @@ def main(cwd=None):
sys.exit() sys.exit()
app.start_onion_service(False, True) app.start_onion_service(False, True)
if common.settings.get('public_mode'): url = build_url(common, app, web)
url = 'http://{0:s}'.format(app.onion_host)
else:
url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug)
schedule = datetime.now() + timedelta(seconds=autostart_timer) schedule = datetime.now() + timedelta(seconds=autostart_timer)
if mode == 'receive': if mode == 'receive':
print("Files sent to you appear in this folder: {}".format(common.settings.get('data_dir'))) 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': if mode == 'website':
# Prepare files to share # Prepare files to share
print("Preparing files to publish website...")
try: try:
web.website_mode.set_file_info(filenames) web.website_mode.set_file_info(filenames)
except OSError as e: except OSError as e:
@ -198,31 +204,26 @@ def main(cwd=None):
print('') print('')
# Start OnionShare http service in new thread # 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.daemon = True
t.start() t.start()
try: # Trap Ctrl-C try: # Trap Ctrl-C
# Wait for web.generate_slug() to finish running # Wait for web.generate_password() to finish running
time.sleep(0.2) time.sleep(0.2)
# start auto-stop timer thread # start auto-stop timer thread
if app.autostop_timer > 0: if app.autostop_timer > 0:
app.autostop_timer_thread.start() 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 common.settings.get('save_private_key'):
if not common.settings.get('slug'): if not common.settings.get('password'):
common.settings.set('slug', web.slug) common.settings.set('password', web.password)
common.settings.save() common.settings.save()
# Build the URL # Build the URL
if common.settings.get('public_mode'): url = build_url(common, app, web)
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)
print('') print('')
if autostart_timer > 0: if autostart_timer > 0:

View File

@ -143,7 +143,7 @@ class Common(object):
os.makedirs(onionshare_data_dir, 0o700, True) os.makedirs(onionshare_data_dir, 0o700, True)
return onionshare_data_dir 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". Returns a random string made from two words from the wordlist, such as "deter-trig".
""" """

View File

@ -111,7 +111,7 @@ class Settings(object):
'save_private_key': False, 'save_private_key': False,
'private_key': '', 'private_key': '',
'public_mode': False, 'public_mode': False,
'slug': '', 'password': '',
'hidservauth_string': '', 'hidservauth_string': '',
'data_dir': self.build_default_data_dir(), 'data_dir': self.build_default_data_dir(),
'locale': None # this gets defined in fill_in_defaults() 'locale': None # this gets defined in fill_in_defaults()

View File

@ -31,36 +31,15 @@ class ReceiveModeWeb(object):
""" """
The web app routes for receiving files 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) self.web.add_request(self.web.REQUEST_LOAD, request.path)
r = make_response(render_template('receive.html',
if self.common.settings.get('public_mode'): static_url_path=self.web.static_url_path))
upload_action = '/upload'
else:
upload_action = '/{}/upload'.format(self.web.slug)
r = make_response(render_template(
'receive.html',
upload_action=upload_action))
return self.web.add_security_headers(r) return self.web.add_security_headers(r)
@self.web.app.route("/<slug_candidate>") @self.web.app.route("/upload", methods=['POST'])
def index(slug_candidate): def upload(ajax=False):
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):
""" """
Handle the upload files POST request, though at this point, the files have Handle the upload files POST request, though at this point, the files have
already been uploaded and saved to their correct locations. already been uploaded and saved to their correct locations.
@ -97,11 +76,7 @@ class ReceiveModeWeb(object):
return json.dumps({"error_flashes": [msg]}) return json.dumps({"error_flashes": [msg]})
else: else:
flash(msg, 'error') flash(msg, 'error')
return redirect('/')
if self.common.settings.get('public_mode'):
return redirect('/')
else:
return redirect('/{}'.format(slug_candidate))
# Note that flash strings are in English, and not translated, on purpose, # Note that flash strings are in English, and not translated, on purpose,
# to avoid leaking the locale of the OnionShare user # to avoid leaking the locale of the OnionShare user
@ -128,48 +103,22 @@ class ReceiveModeWeb(object):
if ajax: if ajax:
return json.dumps({"info_flashes": info_flashes}) return json.dumps({"info_flashes": info_flashes})
else: else:
if self.common.settings.get('public_mode'): return redirect('/')
path = '/'
else:
path = '/{}'.format(slug_candidate)
return redirect('{}'.format(path))
else: else:
if ajax: 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: else:
# It was the last upload and the timer ran out # 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) return self.web.add_security_headers(r)
@self.web.app.route("/<slug_candidate>/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("/<slug_candidate>/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']) @self.web.app.route("/upload-ajax", methods=['POST'])
def upload_ajax_public(): def upload_ajax_public():
if not self.can_upload: if not self.can_upload:
return self.web.error403() return self.web.error403()
if not self.common.settings.get('public_mode'): return upload(ajax=True)
return self.web.error404()
return upload_logic(ajax=True)
class ReceiveModeWSGIMiddleware(object): class ReceiveModeWSGIMiddleware(object):
@ -272,12 +221,8 @@ class ReceiveModeRequest(Request):
# Is this a valid upload request? # Is this a valid upload request?
self.upload_request = False self.upload_request = False
if self.method == 'POST': if self.method == 'POST':
if self.web.common.settings.get('public_mode'): if self.path == '/upload' or self.path == '/upload-ajax':
if self.path == '/upload' or self.path == '/upload-ajax': self.upload_request = True
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.upload_request: if self.upload_request:
# No errors yet # No errors yet

View File

@ -44,18 +44,8 @@ class ShareModeWeb(object):
""" """
The web app routes for sharing files The web app routes for sharing files
""" """
@self.web.app.route("/<slug_candidate>")
def index(slug_candidate):
self.web.check_slug_candidate(slug_candidate)
return index_logic()
@self.web.app.route("/") @self.web.app.route("/")
def index_public(): def index():
if not self.common.settings.get('public_mode'):
return self.web.error404()
return index_logic()
def index_logic(slug_candidate=''):
""" """
Render the template for the onionshare landing page. Render the template for the onionshare landing page.
""" """
@ -65,7 +55,8 @@ class ShareModeWeb(object):
# currently a download # currently a download
deny_download = not self.web.stay_open and self.download_in_progress deny_download = not self.web.stay_open and self.download_in_progress
if deny_download: 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) return self.web.add_security_headers(r)
# If download is allowed to continue, serve download page # If download is allowed to continue, serve download page
@ -74,38 +65,18 @@ class ShareModeWeb(object):
else: else:
self.filesize = self.download_filesize self.filesize = self.download_filesize
if self.web.slug: r = make_response(render_template(
r = make_response(render_template( 'send.html',
'send.html', file_info=self.file_info,
slug=self.web.slug, filename=os.path.basename(self.download_filename),
file_info=self.file_info, filesize=self.filesize,
filename=os.path.basename(self.download_filename), filesize_human=self.common.human_readable_filesize(self.download_filesize),
filesize=self.filesize, is_zipped=self.is_zipped,
filesize_human=self.common.human_readable_filesize(self.download_filesize), static_url_path=self.web.static_url_path))
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))
return self.web.add_security_headers(r) return self.web.add_security_headers(r)
@self.web.app.route("/<slug_candidate>/download")
def download(slug_candidate):
self.web.check_slug_candidate(slug_candidate)
return download_logic()
@self.web.app.route("/download") @self.web.app.route("/download")
def download_public(): def download():
if not self.common.settings.get('public_mode'):
return self.web.error404()
return download_logic()
def download_logic(slug_candidate=''):
""" """
Download the zip file. Download the zip file.
""" """
@ -113,7 +84,8 @@ class ShareModeWeb(object):
# currently a download # currently a download
deny_download = not self.web.stay_open and self.download_in_progress deny_download = not self.web.stay_open and self.download_in_progress
if deny_download: 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) return self.web.add_security_headers(r)
# Each download has a unique id # Each download has a unique id

View File

@ -5,11 +5,13 @@ import queue
import socket import socket
import sys import sys
import tempfile import tempfile
import requests
from distutils.version import LooseVersion as Version from distutils.version import LooseVersion as Version
from urllib.request import urlopen from urllib.request import urlopen
import flask import flask
from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version
from flask_httpauth import HTTPBasicAuth
from .. import strings from .. import strings
@ -43,6 +45,7 @@ class Web(object):
REQUEST_UPLOAD_FINISHED = 8 REQUEST_UPLOAD_FINISHED = 8
REQUEST_UPLOAD_CANCELED = 9 REQUEST_UPLOAD_CANCELED = 9
REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10 REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10
REQUEST_INVALID_PASSWORD = 11
def __init__(self, common, is_gui, mode='share'): def __init__(self, common, is_gui, mode='share'):
self.common = common self.common = common
@ -53,6 +56,9 @@ class Web(object):
static_folder=self.common.get_resource_path('static'), static_folder=self.common.get_resource_path('static'),
template_folder=self.common.get_resource_path('templates')) template_folder=self.common.get_resource_path('templates'))
self.app.secret_key = self.common.random_string(8) 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? # Verbose mode?
if self.common.verbose: if self.common.verbose:
@ -92,13 +98,14 @@ class Web(object):
] ]
self.q = queue.Queue() self.q = queue.Queue()
self.slug = None self.password = None
self.error404_count = 0
self.reset_invalid_passwords()
self.done = False self.done = False
# shutting down the server only works within the context of flask, so the easiest way to do it is over http # 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 # Keep track if the server is running
self.running = False self.running = False
@ -119,51 +126,80 @@ class Web(object):
def define_common_routes(self): 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) @self.app.errorhandler(404)
def page_not_found(e): def not_found(e):
"""
404 error page.
"""
return self.error404() return self.error404()
@self.app.route("/<slug_candidate>/shutdown") @self.app.route("/<password_candidate>/shutdown")
def shutdown(slug_candidate): def shutdown(password_candidate):
""" """
Stop the flask web server, from the context of an http request. Stop the flask web server, from the context of an http request.
""" """
self.check_shutdown_slug_candidate(slug_candidate) if password_candidate == self.shutdown_password:
self.force_shutdown() self.force_shutdown()
return "" return ""
abort(404)
@self.app.route("/noscript-xss-instructions") @self.app.route("/noscript-xss-instructions")
def noscript_xss_instructions(): def noscript_xss_instructions():
""" """
Display instructions for disabling Tor Browser's NoScript XSS setting 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) 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): def error404(self):
self.add_request(Web.REQUEST_OTHER, request.path) self.add_request(Web.REQUEST_OTHER, request.path)
if request.path != '/favicon.ico': r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404)
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)
return self.add_security_headers(r) return self.add_security_headers(r)
def error403(self): def error403(self):
self.add_request(Web.REQUEST_OTHER, request.path) 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) return self.add_security_headers(r)
def add_security_headers(self, r): def add_security_headers(self, r):
@ -179,7 +215,7 @@ class Web(object):
return True return True
return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) 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. Add a request to the queue, to communicate with the GUI.
""" """
@ -189,14 +225,26 @@ class Web(object):
'data': data 'data': data
}) })
def generate_slug(self, persistent_slug=None): def generate_password(self, persistent_password=None):
self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug)) self.common.log('Web', 'generate_password', 'persistent_password={}'.format(persistent_password))
if persistent_slug != None and persistent_slug != '': if persistent_password != None and persistent_password != '':
self.slug = persistent_slug self.password = persistent_password
self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug)) self.common.log('Web', 'generate_password', 'persistent_password sent, so password is: "{}"'.format(self.password))
else: else:
self.slug = self.common.build_slug() self.password = self.common.build_password()
self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug)) 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 + '/<path:filename>',
endpoint='static', view_func=self.app.send_static_file)
def verbose_mode(self): def verbose_mode(self):
""" """
@ -207,17 +255,9 @@ class Web(object):
log_handler.setLevel(logging.WARNING) log_handler.setLevel(logging.WARNING)
self.app.logger.addHandler(log_handler) self.app.logger.addHandler(log_handler)
def check_slug_candidate(self, slug_candidate): def reset_invalid_passwords(self):
self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate)) self.invalid_passwords_count = 0
if self.common.settings.get('public_mode'): self.invalid_passwords = []
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 force_shutdown(self): def force_shutdown(self):
""" """
@ -233,11 +273,11 @@ class Web(object):
pass pass
self.running = False 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. 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 self.stay_open = stay_open
@ -266,17 +306,11 @@ class Web(object):
# Let the mode know that the user stopped the server # Let the mode know that the user stopped the server
self.stop_q.put(True) self.stop_q.put(True)
# Reset any slug that was in use # To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown
self.slug = None # (We're putting the shutdown_password in the path as well to make routing simpler)
# To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
if self.running: if self.running:
try: requests.get('http://127.0.0.1:{}/{}/shutdown'.format(port, self.shutdown_password),
s = socket.socket() auth=requests.auth.HTTPBasicAuth('onionshare', self.password))
s.connect(('127.0.0.1', port))
s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug)) # Reset any password that was in use
except: self.password = None
try:
urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read()
except:
pass

View File

@ -3,7 +3,6 @@ import sys
import tempfile import tempfile
import mimetypes import mimetypes
from flask import Response, request, render_template, make_response, send_from_directory from flask import Response, request, render_template, make_response, send_from_directory
from flask_httpauth import HTTPBasicAuth
from .. import strings from .. import strings
@ -17,7 +16,6 @@ class WebsiteModeWeb(object):
self.common.log('WebsiteModeWeb', '__init__') self.common.log('WebsiteModeWeb', '__init__')
self.web = web self.web = web
self.auth = HTTPBasicAuth()
# Dictionary mapping file paths to filenames on disk # Dictionary mapping file paths to filenames on disk
self.files = {} self.files = {}
@ -26,8 +24,6 @@ class WebsiteModeWeb(object):
# Reset assets path # Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static') self.web.app.static_folder=self.common.get_resource_path('static')
self.users = { }
self.define_routes() self.define_routes()
def define_routes(self): def define_routes(self):
@ -35,24 +31,6 @@ class WebsiteModeWeb(object):
The web app routes for sharing a website 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('/', defaults={'path': ''})
@self.web.app.route('/<path:path>') @self.web.app.route('/<path:path>')
def path_public(path): def path_public(path):
@ -153,7 +131,8 @@ class WebsiteModeWeb(object):
r = make_response(render_template('listing.html', r = make_response(render_template('listing.html',
path=path, path=path,
files=files, files=files,
dirs=dirs)) dirs=dirs,
static_url_path=self.web.static_url_path))
return self.web.add_security_headers(r) return self.web.add_security_headers(r)
def set_file_info(self, filenames): def set_file_info(self, filenames):

View File

@ -24,7 +24,7 @@ from onionshare.common import AutoStopTimer
from ..server_status import ServerStatus from ..server_status import ServerStatus
from ..threads import OnionThread from ..threads import OnionThread
from ..threads import AutoStartTimer from ..threads import AutoStartTimer
from ..widgets import Alert from ..widgets import Alert
class Mode(QtWidgets.QWidget): class Mode(QtWidgets.QWidget):
@ -181,7 +181,7 @@ class Mode(QtWidgets.QWidget):
self.app.port = None self.app.port = None
# Start the onion thread. If this share was scheduled for a future date, # 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. # and onion address, but it will not start the WebThread yet.
if self.server_status.autostart_timer_datetime: if self.server_status.autostart_timer_datetime:
self.start_onion_thread(obtain_onion_early=True) self.start_onion_thread(obtain_onion_early=True)

View File

@ -113,7 +113,7 @@ class ReceiveMode(Mode):
""" """
# Reset web counters # Reset web counters
self.web.receive_mode.upload_count = 0 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 # Hide and reset the uploads if we have previously shared
self.reset_info_counters() self.reset_info_counters()

View File

@ -147,7 +147,7 @@ class ShareMode(Mode):
""" """
# Reset web counters # Reset web counters
self.web.share_mode.download_count = 0 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 # Hide and reset the downloads if we have previously shared
self.reset_info_counters() self.reset_info_counters()

View File

@ -142,7 +142,7 @@ class WebsiteMode(Mode):
""" """
# Reset web counters # Reset web counters
self.web.website_mode.visit_count = 0 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 # Hide and reset the downloads if we have previously shared
self.reset_info_counters() self.reset_info_counters()

View File

@ -474,8 +474,11 @@ class OnionShareGui(QtWidgets.QMainWindow):
Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"])) Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"]))
if event["type"] == Web.REQUEST_OTHER: if event["type"] == Web.REQUEST_OTHER:
if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_slug): if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_password):
self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.error404_count, strings._('other_page_loaded'), event["path"])) 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() mode.timer_callback()

View File

@ -243,8 +243,8 @@ class ServerStatus(QtWidgets.QWidget):
self.show_url() self.show_url()
if self.common.settings.get('save_private_key'): if self.common.settings.get('save_private_key'):
if not self.common.settings.get('slug'): if not self.common.settings.get('password'):
self.common.settings.set('slug', self.web.slug) self.common.settings.set('password', self.web.password)
self.common.settings.save() self.common.settings.save()
if self.common.settings.get('autostart_timer'): if self.common.settings.get('autostart_timer'):
@ -285,7 +285,7 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.setEnabled(True) self.server_button.setEnabled(True)
if self.mode == ServerStatus.MODE_SHARE: if self.mode == ServerStatus.MODE_SHARE:
self.server_button.setText(strings._('gui_share_stop_server')) 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')) self.server_button.setText(strings._('gui_share_stop_server'))
else: else:
self.server_button.setText(strings._('gui_receive_stop_server')) self.server_button.setText(strings._('gui_receive_stop_server'))
@ -420,8 +420,6 @@ class ServerStatus(QtWidgets.QWidget):
""" """
if self.common.settings.get('public_mode'): if self.common.settings.get('public_mode'):
url = 'http://{0:s}'.format(self.app.onion_host) 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: 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 return url

View File

@ -54,7 +54,7 @@ class SettingsDialog(QtWidgets.QDialog):
# General settings # 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 = QtWidgets.QCheckBox()
self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.public_mode_checkbox.setText(strings._("gui_settings_public_mode_checkbox")) 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(): if self.save_private_key_checkbox.isChecked():
settings.set('save_private_key', True) settings.set('save_private_key', True)
settings.set('private_key', self.old_settings.get('private_key')) 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')) settings.set('hidservauth_string', self.old_settings.get('hidservauth_string'))
else: else:
settings.set('save_private_key', False) settings.set('save_private_key', False)
settings.set('private_key', '') settings.set('private_key', '')
settings.set('slug', '') settings.set('password', '')
# Also unset the HidServAuth if we are removing our reusable private key # Also unset the HidServAuth if we are removing our reusable private key
settings.set('hidservauth_string', '') settings.set('hidservauth_string', '')

View File

@ -42,13 +42,16 @@ class OnionThread(QtCore.QThread):
def run(self): def run(self):
self.mode.common.log('OnionThread', 'run') 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') self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download')
if not self.mode.app.port: if not self.mode.app.port:
self.mode.app.choose_port() self.mode.app.choose_port()
if not self.mode.common.settings.get('public_mode'): if not self.mode.common.settings.get('public_mode'):
if not self.mode.web.slug: if not self.mode.web.password:
self.mode.web.generate_slug(self.mode.common.settings.get('slug')) self.mode.web.generate_password(self.mode.common.settings.get('password'))
try: try:
if self.mode.obtain_onion_early: if self.mode.obtain_onion_early:
@ -86,7 +89,7 @@ class WebThread(QtCore.QThread):
def run(self): def run(self):
self.mode.common.log('WebThread', 'run') 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() self.success.emit()

View File

@ -3,6 +3,7 @@
"not_a_readable_file": "{0:s} is not a readable file.", "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", "no_available_port": "Could not find an available port to start the onion service",
"other_page_loaded": "Address loaded", "other_page_loaded": "Address loaded",
"invalid_password_guess": "Invalid password guess",
"close_on_autostop_timer": "Stopped because auto-stop timer ran out", "close_on_autostop_timer": "Stopped because auto-stop timer ran out",
"closing_automatically": "Stopped because transfer is complete", "closing_automatically": "Stopped because transfer is complete",
"large_filesize": "Warning: Sending a large share could take hours", "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_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_quit": "Quit",
"gui_quit_warning_dont_quit": "Cancel", "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%", "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_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.", "error_ephemeral_not_supported": "OnionShare requires at least both Tor 0.2.7.1 and python3-stem 1.4.0.",

View File

@ -121,7 +121,7 @@ $(function(){
$('#uploads').append($upload_div); $('#uploads').append($upload_div);
// Send the request // Send the request
ajax.open('POST', window.location.pathname.replace(/\/$/, '') + '/upload-ajax', true); ajax.open('POST', '/upload-ajax', true);
ajax.send(formData); ajax.send(formData);
}); });
}); });

19
share/templates/401.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare: 401 Unauthorized Access</title>
<link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon" />
<link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
</head>
<body>
<div class="info-wrapper">
<div class="info">
<p><img class="logo" src="{{ static_url_path }}/img/logo_large.png" title="OnionShare"></p>
<p class="info-header">401 Unauthorized Access</p>
</div>
</div>
</body>
</html>

View File

@ -3,14 +3,14 @@
<head> <head>
<title>OnionShare: 403 Forbidden</title> <title>OnionShare: 403 Forbidden</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" /> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon" />
<link rel="stylesheet" rel="subresource" type="text/css" href="/static/css/style.css" media="all"> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
</head> </head>
<body> <body>
<div class="info-wrapper"> <div class="info-wrapper">
<div class="info"> <div class="info">
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p> <p><img class="logo" src="{{ static_url_path }}/img/logo_large.png" title="OnionShare"></p>
<p class="info-header">You are not allowed to perform that action at this time.</p> <p class="info-header">You are not allowed to perform that action at this time.</p>
</div> </div>
</div> </div>

View File

@ -3,14 +3,14 @@
<head> <head>
<title>OnionShare: 404 Not Found</title> <title>OnionShare: 404 Not Found</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon"> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="stylesheet" rel="subresource" type="text/css" href="/static/css/style.css" media="all"> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
</head> </head>
<body> <body>
<div class="info-wrapper"> <div class="info-wrapper">
<div class="info"> <div class="info">
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p> <p><img class="logo" src="{{ static_url_path }}/img/logo_large.png" title="OnionShare"></p>
<p class="info-header">404 Not Found</p> <p class="info-header">404 Not Found</p>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
<head> <head>
<title>OnionShare</title> <title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" /> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon" />
</head> </head>
<body> <body>

View File

@ -2,13 +2,13 @@
<html> <html>
<head> <head>
<title>OnionShare</title> <title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" /> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon" />
<link href="/static/css/style.css" rel="stylesheet" type="text/css" /> <link href="{{ static_url_path }}/css/style.css" rel="stylesheet" type="text/css" />
</head> </head>
<body> <body>
<header class="clearfix"> <header class="clearfix">
<img class="logo" src="/static/img/logo.png" title="OnionShare"> <img class="logo" src="{{ static_url_path }}/img/logo.png" title="OnionShare">
<h1>OnionShare</h1> <h1>OnionShare</h1>
</header> </header>
@ -22,7 +22,7 @@
{% for info in dirs %} {% for info in dirs %}
<tr> <tr>
<td> <td>
<img width="30" height="30" title="" alt="" src="/static/img/web_folder.png" /> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" />
<a href="{{ info.basename }}"> <a href="{{ info.basename }}">
{{ info.basename }} {{ info.basename }}
</a> </a>
@ -34,7 +34,7 @@
{% for info in files %} {% for info in files %}
<tr> <tr>
<td> <td>
<img width="30" height="30" title="" alt="" src="/static/img/web_file.png" /> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" />
<a href="{{ info.basename }}"> <a href="{{ info.basename }}">
{{ info.basename }} {{ info.basename }}
</a> </a>

View File

@ -2,13 +2,13 @@
<html> <html>
<head> <head>
<title>OnionShare</title> <title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon"> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="stylesheet" rel="subresource" type="text/css" href="/static/css/style.css" media="all"> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
</head> </head>
<body> <body>
<header class="clearfix"> <header class="clearfix">
<img class="logo" src="/static/img/logo.png" title="OnionShare"> <img class="logo" src="{{ static_url_path }}/img/logo.png" title="OnionShare">
<h1>OnionShare</h1> <h1>OnionShare</h1>
</header> </header>
@ -19,14 +19,14 @@
--> -->
<div id="noscript"> <div id="noscript">
<p> <p>
<img src="/static/img/warning.png" title="Warning" /><strong>Warning:</strong> Due to a bug in Tor Browser and Firefox, uploads <img src="{{ static_url_path }}/img/warning.png" title="Warning" /><strong>Warning:</strong> Due to a bug in Tor Browser and Firefox, uploads
sometimes never finish. To upload reliably, either set your Tor Browser sometimes never finish. To upload reliably, either set your Tor Browser
<a rel="noreferrer" target="_blank" href="https://tb-manual.torproject.org/en-US/security-slider/">security slider</a> <a rel="noreferrer" target="_blank" href="https://tb-manual.torproject.org/en-US/security-slider/">security slider</a>
to Standard or to Standard or
<a target="_blank" href="/noscript-xss-instructions">turn off your Tor Browser's NoScript XSS setting</a>.</p> <a target="_blank" href="/noscript-xss-instructions">turn off your Tor Browser's NoScript XSS setting</a>.</p>
</div> </div>
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p> <p><img class="logo" src="{{ static_url_path }}/img/logo_large.png" title="OnionShare"></p>
<p class="upload-header">Send Files</p> <p class="upload-header">Send Files</p>
<p class="upload-description">Select the files you want to send, then click "Send Files"...</p> <p class="upload-description">Select the files you want to send, then click "Send Files"...</p>
@ -45,14 +45,14 @@
</ul> </ul>
</div> </div>
<form id="send" method="post" enctype="multipart/form-data" action="{{ upload_action }}"> <form id="send" method="post" enctype="multipart/form-data" action="/upload">
<p><input type="file" id="file-select" name="file[]" multiple /></p> <p><input type="file" id="file-select" name="file[]" multiple /></p>
<p><button type="submit" id="send-button" class="button">Send Files</button></p> <p><button type="submit" id="send-button" class="button">Send Files</button></p>
</form> </form>
</div> </div>
<script src="/static/js/receive-noscript.js"></script> <script src="{{ static_url_path }}/js/receive-noscript.js"></script>
<script src="/static/js/jquery-3.4.0.min.js"></script> <script src="{{ static_url_path }}/js/jquery-3.4.0.min.js"></script>
<script async src="/static/js/receive.js"></script> <script async src="{{ static_url_path }}/js/receive.js"></script>
</body> </body>
</html> </html>

View File

@ -2,13 +2,13 @@
<html> <html>
<head> <head>
<title>OnionShare</title> <title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon"> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="stylesheet" rel="subresource" type="text/css" href="/static/css/style.css" media="all"> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
</head> </head>
<body> <body>
<header class="clearfix"> <header class="clearfix">
<img class="logo" src="/static/img/logo.png" title="OnionShare"> <img class="logo" src="{{ static_url_path }}/img/logo.png" title="OnionShare">
<h1>OnionShare</h1> <h1>OnionShare</h1>
</header> </header>

View File

@ -3,8 +3,8 @@
<head> <head>
<title>OnionShare</title> <title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon"> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="stylesheet" rel="subresource" type="text/css" href="/static/css/style.css" media="all"> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
<meta name="onionshare-filename" content="{{ filename }}"> <meta name="onionshare-filename" content="{{ filename }}">
<meta name="onionshare-filesize" content="{{ filesize }}"> <meta name="onionshare-filesize" content="{{ filesize }}">
</head> </head>
@ -15,14 +15,10 @@
<div class="right"> <div class="right">
<ul> <ul>
<li>Total size: <strong>{{ filesize_human }}</strong> {% if is_zipped %} (compressed){% endif %}</li> <li>Total size: <strong>{{ filesize_human }}</strong> {% if is_zipped %} (compressed){% endif %}</li>
{% if slug %}
<li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
{% else %}
<li><a class="button" href='/download'>Download Files</a></li> <li><a class="button" href='/download'>Download Files</a></li>
{% endif %}
</ul> </ul>
</div> </div>
<img class="logo" src="/static/img/logo.png" title="OnionShare"> <img class="logo" src="{{ static_url_path }}/img/logo.png" title="OnionShare">
<h1>OnionShare</h1> <h1>OnionShare</h1>
</header> </header>
@ -35,7 +31,7 @@
{% for info in file_info.dirs %} {% for info in file_info.dirs %}
<tr> <tr>
<td> <td>
<img width="30" height="30" title="" alt="" src="/static/img/web_folder.png" /> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" />
{{ info.basename }} {{ info.basename }}
</td> </td>
<td>{{ info.size_human }}</td> <td>{{ info.size_human }}</td>
@ -45,7 +41,7 @@
{% for info in file_info.files %} {% for info in file_info.files %}
<tr> <tr>
<td> <td>
<img width="30" height="30" title="" alt="" src="/static/img/web_file.png" /> <img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" />
{{ info.basename }} {{ info.basename }}
</td> </td>
<td>{{ info.size_human }}</td> <td>{{ info.size_human }}</td>
@ -53,7 +49,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<script async src="/static/js/send.js" charset="utf-8"></script> <script async src="{{ static_url_path }}/js/send.js" charset="utf-8"></script>
</body> </body>
</html> </html>

View File

@ -3,19 +3,19 @@
<head> <head>
<title>OnionShare is closed</title> <title>OnionShare is closed</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon"> <link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="stylesheet" rel="subresource" type="text/css" href="/static/css/style.css" media="all"> <link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
</head> </head>
<body> <body>
<header class="clearfix"> <header class="clearfix">
<img class="logo" src="/static/img/logo.png" title="OnionShare"> <img class="logo" src="{{ static_url_path }}/img/logo.png" title="OnionShare">
<h1>OnionShare</h1> <h1>OnionShare</h1>
</header> </header>
<div class="info-wrapper"> <div class="info-wrapper">
<div class="info"> <div class="info">
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p> <p><img class="logo" src="{{ static_url_path }}/img/logo_large.png" title="OnionShare"></p>
<p class="info-header">Thank you for using OnionShare</p> <p class="info-header">Thank you for using OnionShare</p>
<p class="info-description">You may now close this window.</p> <p class="info-description">You may now close this window.</p>
</div> </div>

View File

@ -2,8 +2,7 @@ import json
import os import os
import requests import requests
import shutil import shutil
import socket import base64
import socks
from PyQt5 import QtCore, QtTest from PyQt5 import QtCore, QtTest
@ -126,20 +125,20 @@ class GuiBaseTest(object):
if type(mode) == ReceiveMode: if type(mode) == ReceiveMode:
# Upload a file # Upload a file
files = {'file[]': open('/tmp/test.txt', 'rb')} files = {'file[]': open('/tmp/test.txt', 'rb')}
if not public_mode: url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, mode.web.slug) if public_mode:
r = requests.post(url, files=files)
else: else:
path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password))
response = requests.post(path, files=files)
QtTest.QTest.qWait(2000) QtTest.QTest.qWait(2000)
if type(mode) == ShareMode: if type(mode) == ShareMode:
# Download files # Download files
url = "http://127.0.0.1:{}/download".format(self.gui.app.port)
if public_mode: if public_mode:
url = "http://127.0.0.1:{}/download".format(self.gui.app.port) r = requests.get(url)
else: else:
url = "http://127.0.0.1:{}/{}/download".format(self.gui.app.port, mode.web.slug) r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password))
r = requests.get(url)
QtTest.QTest.qWait(2000) QtTest.QTest.qWait(2000)
# Indicator should be visible, have a value of "1" # Indicator should be visible, have a value of "1"
@ -185,17 +184,19 @@ class GuiBaseTest(object):
def web_server_is_running(self): def web_server_is_running(self):
'''Test that the web server has started''' '''Test that the web server has started'''
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try:
r = requests.get('http://127.0.0.1:{}/'.format(self.gui.app.port))
self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) self.assertTrue(True)
except requests.exceptions.ConnectionError:
self.assertTrue(False)
def have_a_slug(self, mode, public_mode): def have_a_password(self, mode, public_mode):
'''Test that we have a valid slug''' '''Test that we have a valid password'''
if not public_mode: 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: 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): def url_description_shown(self, mode):
@ -212,7 +213,7 @@ class GuiBaseTest(object):
if public_mode: if public_mode:
self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}'.format(self.gui.app.port)) self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}'.format(self.gui.app.port))
else: 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): def server_status_indicator_says_started(self, mode):
@ -225,31 +226,14 @@ class GuiBaseTest(object):
def web_page(self, mode, string, public_mode): def web_page(self, mode, string, public_mode):
'''Test that the web page contains a string''' '''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: url = "http://127.0.0.1:{}/".format(self.gui.app.port)
path = '/{}'.format(mode.server_status.web.slug) if public_mode:
r = requests.get(url)
else: else:
path = '/' r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password))
http_request = 'GET {} HTTP/1.0\r\n'.format(path) self.assertTrue(string in r.text)
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()
def history_widgets_present(self, mode): def history_widgets_present(self, mode):
@ -273,10 +257,12 @@ class GuiBaseTest(object):
def web_server_is_stopped(self): def web_server_is_stopped(self):
'''Test that the web server also stopped''' '''Test that the web server also stopped'''
QtTest.QTest.qWait(2000) QtTest.QTest.qWait(2000)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# We should be closed by now. Fail if not! try:
self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) 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): def server_status_indicator_says_closed(self, mode, stay_open):

View File

@ -7,18 +7,26 @@ from .GuiBaseTest import GuiBaseTest
class GuiReceiveTest(GuiBaseTest): class GuiReceiveTest(GuiBaseTest):
def upload_file(self, public_mode, file_to_upload, expected_basename, identical_files_at_once=False): def upload_file(self, public_mode, file_to_upload, expected_basename, identical_files_at_once=False):
'''Test that we can upload the file''' '''Test that we can upload the file'''
files = {'file[]': open(file_to_upload, 'rb')}
if not public_mode: # Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused
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)
QtTest.QTest.qWait(2000) 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 exists = False
now = datetime.now() now = datetime.now()
for i in range(10): for i in range(10):
@ -39,31 +47,28 @@ class GuiReceiveTest(GuiBaseTest):
def upload_file_should_fail(self, public_mode): 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''' '''Test that we can't upload the file when permissions are wrong, and expected content is shown'''
files = {'file[]': open('/tmp/test.txt', 'rb')} files = {'file[]': open('/tmp/test.txt', 'rb')}
if not public_mode: url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) if public_mode:
r = requests.post(url, files=files)
else: else:
path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password))
response = requests.post(path, files=files)
QtCore.QTimer.singleShot(1000, self.accept_dialog) 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): def upload_dir_permissions(self, mode=0o755):
'''Manipulate the permissions on the upload dir in between tests''' '''Manipulate the permissions on the upload dir in between tests'''
os.chmod('/tmp/OnionShare', mode) os.chmod('/tmp/OnionShare', mode)
def try_public_paths_in_non_public_mode(self): def try_without_auth_in_non_public_mode(self):
response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port)) r = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port))
self.assertEqual(response.status_code, 404) self.assertEqual(r.status_code, 401)
response = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port)) r = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port))
self.assertEqual(response.status_code, 404) self.assertEqual(r.status_code, 401)
def uploading_zero_files_shouldnt_change_ui(self, mode, public_mode): 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 you submit the receive mode form without selecting any files, the UI shouldn't get updated'''
if not public_mode: url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
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)
# What were the counts before submitting the form? # What were the counts before submitting the form?
before_in_progress_count = mode.history.in_progress_count 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) before_number_of_history_items = len(mode.history.item_list.items)
# Click submit without including any files a few times # Click submit without including any files a few times
response = requests.post(path, files={}) if public_mode:
response = requests.post(path, files={}) r = requests.post(url, files={})
response = requests.post(path, 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 # The counts shouldn't change
self.assertEqual(mode.history.in_progress_count, before_in_progress_count) self.assertEqual(mode.history.in_progress_count, before_in_progress_count)
@ -93,17 +104,17 @@ class GuiReceiveTest(GuiBaseTest):
self.settings_button_is_hidden() self.settings_button_is_hidden()
self.server_is_started(self.gui.receive_mode) self.server_is_started(self.gui.receive_mode)
self.web_server_is_running() 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.url_description_shown(self.gui.receive_mode)
self.have_copy_url_button(self.gui.receive_mode, public_mode) self.have_copy_url_button(self.gui.receive_mode, public_mode)
self.server_status_indicator_says_started(self.gui.receive_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) 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''' '''Upload files in receive mode and stop the share'''
self.run_all_receive_mode_setup_tests(public_mode) self.run_all_receive_mode_setup_tests(public_mode)
if not 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.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
self.history_widgets_present(self.gui.receive_mode) self.history_widgets_present(self.gui.receive_mode)
self.counter_incremented(self.gui.receive_mode, 1) self.counter_incremented(self.gui.receive_mode, 1)
@ -125,7 +136,7 @@ class GuiReceiveTest(GuiBaseTest):
self.server_is_started(self.gui.receive_mode) self.server_is_started(self.gui.receive_mode)
self.history_indicator(self.gui.receive_mode, public_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''' '''Attempt to upload (unwritable) files in receive mode and stop the share'''
self.run_all_receive_mode_setup_tests(public_mode) self.run_all_receive_mode_setup_tests(public_mode)
self.upload_dir_permissions(0o400) self.upload_dir_permissions(0o400)

View File

@ -2,14 +2,15 @@ import os
import requests import requests
import socks import socks
import zipfile import zipfile
import tempfile
from PyQt5 import QtCore, QtTest from PyQt5 import QtCore, QtTest
from .GuiBaseTest import GuiBaseTest from .GuiBaseTest import GuiBaseTest
class GuiShareTest(GuiBaseTest): class GuiShareTest(GuiBaseTest):
# Persistence tests # Persistence tests
def have_same_slug(self, slug): def have_same_password(self, password):
'''Test that we have the same slug''' '''Test that we have the same password'''
self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) self.assertEqual(self.gui.share_mode.server_status.web.password, password)
# Share-specific tests # Share-specific tests
@ -17,7 +18,7 @@ class GuiShareTest(GuiBaseTest):
'''Test that the number of items in the list is as expected''' '''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) self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), num)
def deleting_all_files_hides_delete_button(self): 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''' '''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)) 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 # No more files, the delete button should be hidden
self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
def add_a_file_and_delete_using_its_delete_widget(self): 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''' '''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') 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) 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) self.file_selection_widget_has_files(0)
def file_selection_widget_readd_files(self): def file_selection_widget_readd_files(self):
'''Re-add some files to the list so we can share''' '''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') 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: with open('/tmp/large_file', 'wb') as fout:
fout.write(os.urandom(size)) fout.write(os.urandom(size))
self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/large_file') self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/large_file')
def add_delete_buttons_hidden(self): def add_delete_buttons_hidden(self):
'''Test that the add and delete buttons are hidden when the server starts''' '''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.add_button.isVisible())
self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
def download_share(self, public_mode): def download_share(self, public_mode):
'''Test that we can download the share''' '''Test that we can download the share'''
s = socks.socksocket() url = "http://127.0.0.1:{}/download".format(self.gui.app.port)
s.settimeout(60)
s.connect(('127.0.0.1', self.gui.app.port))
if public_mode: if public_mode:
path = '/download' r = requests.get(url)
else: 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) tmp_file = tempfile.NamedTemporaryFile()
http_request += 'Host: 127.0.0.1\r\n' with open(tmp_file.name, 'wb') as f:
http_request += '\r\n' f.write(r.content)
s.sendall(http_request.encode('utf-8'))
with open('/tmp/download.zip', 'wb') as file_to_write: zip = zipfile.ZipFile(tmp_file.name)
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')
QtTest.QTest.qWait(2000) QtTest.QTest.qWait(2000)
self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8')) self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8'))
def hit_404(self, public_mode): def hit_401(self, public_mode):
'''Test that the server stops after too many 404s, or doesn't when in public_mode''' '''Test that the server stops after too many 401s, or doesn't when in public_mode'''
bogus_path = '/gimme' url = "http://127.0.0.1:{}/".format(self.gui.app.port)
url = "http://127.0.0.1:{}/{}".format(self.gui.app.port, bogus_path)
for _ in range(20): 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 # A nasty hack to avoid the Alert dialog that blocks the rest of the test
if not public_mode: if not public_mode:
@ -130,7 +119,7 @@ class GuiShareTest(GuiBaseTest):
self.add_a_file_and_delete_using_its_delete_widget() self.add_a_file_and_delete_using_its_delete_widget()
self.file_selection_widget_readd_files() self.file_selection_widget_readd_files()
def run_all_share_mode_started_tests(self, public_mode, startup_time=2000): def run_all_share_mode_started_tests(self, public_mode, startup_time=2000):
"""Tests in share mode after starting a share""" """Tests in share mode after starting a share"""
self.server_working_on_start_button_pressed(self.gui.share_mode) self.server_working_on_start_button_pressed(self.gui.share_mode)
@ -139,12 +128,12 @@ class GuiShareTest(GuiBaseTest):
self.settings_button_is_hidden() self.settings_button_is_hidden()
self.server_is_started(self.gui.share_mode, startup_time) self.server_is_started(self.gui.share_mode, startup_time)
self.web_server_is_running() 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.url_description_shown(self.gui.share_mode)
self.have_copy_url_button(self.gui.share_mode, public_mode) self.have_copy_url_button(self.gui.share_mode, public_mode)
self.server_status_indicator_says_started(self.gui.share_mode) self.server_status_indicator_says_started(self.gui.share_mode)
def run_all_share_mode_download_tests(self, public_mode, stay_open): def run_all_share_mode_download_tests(self, public_mode, stay_open):
"""Tests in share mode after downloading a share""" """Tests in share mode after downloading a share"""
self.web_page(self.gui.share_mode, 'Total size', public_mode) 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.server_is_started(self.gui.share_mode)
self.history_indicator(self.gui.share_mode, public_mode) self.history_indicator(self.gui.share_mode, public_mode)
def run_all_share_mode_tests(self, public_mode, stay_open): def run_all_share_mode_tests(self, public_mode, stay_open):
"""End-to-end share tests""" """End-to-end share tests"""
self.run_all_share_mode_setup_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): 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_setup_tests()
self.run_all_share_mode_started_tests(public_mode) 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.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): def run_all_share_mode_timer_tests(self, public_mode):

View File

@ -76,7 +76,7 @@ class TorGuiBaseTest(GuiBaseTest):
# Upload a file # Upload a file
files = {'file[]': open('/tmp/test.txt', 'rb')} files = {'file[]': open('/tmp/test.txt', 'rb')}
if not public_mode: 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: else:
path = 'http://{}/upload'.format(self.gui.app.onion_host) path = 'http://{}/upload'.format(self.gui.app.onion_host)
response = session.post(path, files=files) response = session.post(path, files=files)
@ -87,7 +87,7 @@ class TorGuiBaseTest(GuiBaseTest):
if public_mode: if public_mode:
path = "http://{}/download".format(self.gui.app.onion_host) path = "http://{}/download".format(self.gui.app.onion_host)
else: 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) response = session.get(path)
QtTest.QTest.qWait(4000) QtTest.QTest.qWait(4000)
@ -111,7 +111,7 @@ class TorGuiBaseTest(GuiBaseTest):
s.settimeout(60) s.settimeout(60)
s.connect((self.gui.app.onion_host, 80)) s.connect((self.gui.app.onion_host, 80))
if not public_mode: if not public_mode:
path = '/{}'.format(mode.server_status.web.slug) path = '/{}'.format(mode.server_status.web.password)
else: else:
path = '/' path = '/'
http_request = 'GET {} HTTP/1.0\r\n'.format(path) http_request = 'GET {} HTTP/1.0\r\n'.format(path)
@ -138,7 +138,7 @@ class TorGuiBaseTest(GuiBaseTest):
if public_mode: if public_mode:
self.assertEqual(clipboard.text(), 'http://{}'.format(self.gui.app.onion_host)) self.assertEqual(clipboard.text(), 'http://{}'.format(self.gui.app.onion_host))
else: 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 # Stealth tests

View File

@ -13,7 +13,7 @@ class TorGuiReceiveTest(TorGuiBaseTest):
session.proxies['http'] = 'socks5h://{}:{}'.format(socks_address, socks_port) session.proxies['http'] = 'socks5h://{}:{}'.format(socks_address, socks_port)
files = {'file[]': open(file_to_upload, 'rb')} files = {'file[]': open(file_to_upload, 'rb')}
if not public_mode: 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: else:
path = 'http://{}/upload'.format(self.gui.app.onion_host) path = 'http://{}/upload'.format(self.gui.app.onion_host)
response = session.post(path, files=files) 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.server_is_started(self.gui.receive_mode, startup_time=45000)
self.web_server_is_running() self.web_server_is_running()
self.have_an_onion_service() 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.url_description_shown(self.gui.receive_mode)
self.have_copy_url_button(self.gui.receive_mode, public_mode) self.have_copy_url_button(self.gui.receive_mode, public_mode)
self.server_status_indicator_says_started(self.gui.receive_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_working_on_start_button_pressed(self.gui.receive_mode)
self.server_is_started(self.gui.receive_mode, startup_time=45000) self.server_is_started(self.gui.receive_mode, startup_time=45000)
self.history_indicator(self.gui.receive_mode, public_mode) self.history_indicator(self.gui.receive_mode, public_mode)

View File

@ -17,7 +17,7 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
if public_mode: if public_mode:
path = "http://{}/download".format(self.gui.app.onion_host) path = "http://{}/download".format(self.gui.app.onion_host)
else: 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) response = session.get(path, stream=True)
QtTest.QTest.qWait(4000) QtTest.QTest.qWait(4000)
@ -53,7 +53,7 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
self.server_is_started(self.gui.share_mode, startup_time=45000) self.server_is_started(self.gui.share_mode, startup_time=45000)
self.web_server_is_running() self.web_server_is_running()
self.have_an_onion_service() 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.url_description_shown(self.gui.share_mode)
self.have_copy_url_button(self.gui.share_mode, public_mode) self.have_copy_url_button(self.gui.share_mode, public_mode)
self.server_status_indicator_says_started(self.gui.share_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): 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_setup_tests()
self.run_all_share_mode_started_tests(public_mode) 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 onion = self.gui.app.onion_host
self.run_all_share_mode_download_tests(public_mode, stay_open) self.run_all_share_mode_download_tests(public_mode, stay_open)
self.have_same_onion(onion) 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): def run_all_share_mode_timer_tests(self, public_mode):
"""Auto-stop timer tests in share mode""" """Auto-stop timer tests in share mode"""
self.run_all_share_mode_setup_tests() 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.autostop_timer_widget_hidden(self.gui.share_mode)
self.server_timed_out(self.gui.share_mode, 125000) self.server_timed_out(self.gui.share_mode, 125000)
self.web_server_is_stopped() self.web_server_is_stopped()

View File

@ -4,7 +4,7 @@ import unittest
from .GuiShareTest import GuiShareTest from .GuiShareTest import GuiShareTest
class Local404PublicModeRateLimitTest(unittest.TestCase, GuiShareTest): class Local401PublicModeRateLimitTest(unittest.TestCase, GuiShareTest):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
test_settings = { test_settings = {
@ -22,7 +22,7 @@ class Local404PublicModeRateLimitTest(unittest.TestCase, GuiShareTest):
def test_gui(self): def test_gui(self):
self.run_all_common_setup_tests() self.run_all_common_setup_tests()
self.run_all_share_mode_tests(True, True) self.run_all_share_mode_tests(True, True)
self.hit_404(True) self.hit_401(True)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -4,7 +4,7 @@ import unittest
from .GuiShareTest import GuiShareTest from .GuiShareTest import GuiShareTest
class Local404RateLimitTest(unittest.TestCase, GuiShareTest): class Local401RateLimitTest(unittest.TestCase, GuiShareTest):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
test_settings = { test_settings = {
@ -21,7 +21,7 @@ class Local404RateLimitTest(unittest.TestCase, GuiShareTest):
def test_gui(self): def test_gui(self):
self.run_all_common_setup_tests() self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, True) self.run_all_share_mode_tests(False, True)
self.hit_404(False) self.hit_401(False)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -20,7 +20,7 @@ class LocalReceiveModeUnwritableTest(unittest.TestCase, GuiReceiveTest):
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
def test_gui(self): def test_gui(self):
self.run_all_common_setup_tests() 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -21,7 +21,7 @@ class LocalReceivePublicModeUnwritableTest(unittest.TestCase, GuiReceiveTest):
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
def test_gui(self): def test_gui(self):
self.run_all_common_setup_tests() 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -21,7 +21,7 @@ class LocalReceiveModePublicModeTest(unittest.TestCase, GuiReceiveTest):
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
def test_gui(self): def test_gui(self):
self.run_all_common_setup_tests() self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(True, True) self.run_all_receive_mode_tests(True)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -20,7 +20,7 @@ class LocalReceiveModeTest(unittest.TestCase, GuiReceiveTest):
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
def test_gui(self): def test_gui(self):
self.run_all_common_setup_tests() self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(False, True) self.run_all_receive_mode_tests(False)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -4,12 +4,12 @@ import unittest
from .GuiShareTest import GuiShareTest from .GuiShareTest import GuiShareTest
class LocalShareModePersistentSlugTest(unittest.TestCase, GuiShareTest): class LocalShareModePersistentPasswordTest(unittest.TestCase, GuiShareTest):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
test_settings = { test_settings = {
"public_mode": False, "public_mode": False,
"slug": "", "password": "",
"save_private_key": True, "save_private_key": True,
"close_after_first_download": False, "close_after_first_download": False,
} }

View File

@ -4,13 +4,13 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest from .TorGuiShareTest import TorGuiShareTest
class ShareModePersistentSlugTest(unittest.TestCase, TorGuiShareTest): class ShareModePersistentPasswordTest(unittest.TestCase, TorGuiShareTest):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
test_settings = { test_settings = {
"use_legacy_v2_onions": True, "use_legacy_v2_onions": True,
"public_mode": False, "public_mode": False,
"slug": "", "password": "",
"save_private_key": True, "save_private_key": True,
"close_after_first_download": False, "close_after_first_download": False,
} }

View File

@ -33,13 +33,13 @@ LOG_MSG_REGEX = re.compile(r"""
^\[Jun\ 06\ 2013\ 11:05:00\] ^\[Jun\ 06\ 2013\ 11:05:00\]
\ TestModule\.<function\ TestLog\.test_output\.<locals>\.dummy_func \ TestModule\.<function\ TestLog\.test_output\.<locals>\.dummy_func
\ at\ 0x[a-f0-9]+>(:\ TEST_MSG)?$""", re.VERBOSE) \ 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 # TODO: Improve the Common tests to test it all as a single class
class TestBuildSlug: class TestBuildPassword:
@pytest.mark.parametrize('test_input,expected', ( @pytest.mark.parametrize('test_input,expected', (
# VALID, two lowercase words, separated by a hyphen # VALID, two lowercase words, separated by a hyphen
('syrup-enzyme', True), ('syrup-enzyme', True),
@ -60,8 +60,8 @@ class TestBuildSlug:
('too-many-hyphens-', False), ('too-many-hyphens-', False),
('symbols-!@#$%', False) ('symbols-!@#$%', False)
)) ))
def test_build_slug_regex(self, test_input, expected): def test_build_password_regex(self, test_input, expected):
""" Test that `SLUG_REGEX` accounts for the following patterns """ Test that `PASSWORD_REGEX` accounts for the following patterns
There are a few hyphenated words in `wordlist.txt`: There are a few hyphenated words in `wordlist.txt`:
* drop-down * drop-down
@ -69,17 +69,17 @@ class TestBuildSlug:
* t-shirt * t-shirt
* yo-yo * yo-yo
These words cause a few extra potential slug patterns: These words cause a few extra potential password patterns:
* word-word * word-word
* hyphenated-word-word * hyphenated-word-word
* word-hyphenated-word * word-hyphenated-word
* hyphenated-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): def test_build_password_unique(self, common_obj, sys_onionshare_dev_mode):
assert common_obj.build_slug() != common_obj.build_slug() assert common_obj.build_password() != common_obj.build_password()
class TestDirSize: class TestDirSize:

View File

@ -63,7 +63,7 @@ class TestSettings:
'use_legacy_v2_onions': False, 'use_legacy_v2_onions': False,
'save_private_key': False, 'save_private_key': False,
'private_key': '', 'private_key': '',
'slug': '', 'password': '',
'hidservauth_string': '', 'hidservauth_string': '',
'data_dir': os.path.expanduser('~/OnionShare'), 'data_dir': os.path.expanduser('~/OnionShare'),
'public_mode': False 'public_mode': False

View File

@ -27,8 +27,10 @@ import socket
import sys import sys
import zipfile import zipfile
import tempfile import tempfile
import base64
import pytest import pytest
from werkzeug.datastructures import Headers
from onionshare.common import Common from onionshare.common import Common
from onionshare import strings from onionshare import strings
@ -44,7 +46,7 @@ def web_obj(common_obj, mode, num_files=0):
common_obj.settings = Settings(common_obj) common_obj.settings = Settings(common_obj)
strings.load_strings(common_obj) strings.load_strings(common_obj)
web = Web(common_obj, False, mode) web = Web(common_obj, False, mode)
web.generate_slug() web.generate_password()
web.stay_open = True web.stay_open = True
web.running = True web.running = True
@ -71,22 +73,23 @@ class TestWeb:
web = web_obj(common_obj, 'share', 3) web = web_obj(common_obj, 'share', 3)
assert web.mode is 'share' assert web.mode is 'share'
with web.app.test_client() as c: with web.app.test_client() as c:
# Load 404 pages # Load / without auth
res = c.get('/') res = c.get('/')
res.get_data() 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() res.get_data()
assert res.status_code == 404 assert res.status_code == 401
# Load download page # Load / with valid auth
res = c.get('/{}'.format(web.slug)) res = c.get('/', headers=self._make_auth_headers(web.password))
res.get_data() res.get_data()
assert res.status_code == 200 assert res.status_code == 200
# Download # Download
res = c.get('/{}/download'.format(web.slug)) res = c.get('/download', headers=self._make_auth_headers(web.password))
res.get_data() res.get_data()
assert res.status_code == 200 assert res.status_code == 200
assert res.mimetype == 'application/zip' assert res.mimetype == 'application/zip'
@ -99,7 +102,7 @@ class TestWeb:
with web.app.test_client() as c: with web.app.test_client() as c:
# Download the first time # 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() res.get_data()
assert res.status_code == 200 assert res.status_code == 200
assert res.mimetype == 'application/zip' assert res.mimetype == 'application/zip'
@ -114,7 +117,7 @@ class TestWeb:
with web.app.test_client() as c: with web.app.test_client() as c:
# Download the first time # 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() res.get_data()
assert res.status_code == 200 assert res.status_code == 200
assert res.mimetype == 'application/zip' assert res.mimetype == 'application/zip'
@ -125,17 +128,18 @@ class TestWeb:
assert web.mode is 'receive' assert web.mode is 'receive'
with web.app.test_client() as c: with web.app.test_client() as c:
# Load 404 pages # Load / without auth
res = c.get('/') res = c.get('/')
res.get_data() 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() res.get_data()
assert res.status_code == 404 assert res.status_code == 401
# Load upload page # Load / with valid auth
res = c.get('/{}'.format(web.slug)) res = c.get('/', headers=self._make_auth_headers(web.password))
res.get_data() res.get_data()
assert res.status_code == 200 assert res.status_code == 200
@ -144,31 +148,37 @@ class TestWeb:
common_obj.settings.set('public_mode', True) common_obj.settings.set('public_mode', True)
with web.app.test_client() as c: with web.app.test_client() as c:
# Upload page should be accessible from / # Loading / should work without auth
res = c.get('/') res = c.get('/')
data1 = res.get_data() data1 = res.get_data()
assert res.status_code == 200 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): def test_public_mode_off(self, common_obj):
web = web_obj(common_obj, 'receive') web = web_obj(common_obj, 'receive')
common_obj.settings.set('public_mode', False) common_obj.settings.set('public_mode', False)
with web.app.test_client() as c: with web.app.test_client() as c:
# / should be a 404 # Load / without auth
res = c.get('/') res = c.get('/')
data1 = res.get_data() res.get_data()
assert res.status_code == 404 assert res.status_code == 401
# Upload page should be accessible from /[slug] # But static resources should work without auth
res = c.get('/{}'.format(web.slug)) res = c.get('{}/css/style.css'.format(web.static_url_path))
data2 = res.get_data() res.get_data()
assert res.status_code == 200 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: class TestZipWriterDefault:
@pytest.mark.parametrize('test_input', ( @pytest.mark.parametrize('test_input', (