Merge branch 'develop' of github.com:micahflee/onionshare into develop

This commit is contained in:
emma peel 2019-07-26 11:30:20 +00:00
commit 7ab240fd7f
No known key found for this signature in database
GPG Key ID: 364E1DEA2C4F8835
54 changed files with 1041 additions and 506 deletions

View File

@ -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

View File

@ -9,7 +9,7 @@ VERSION=`cat share/version.txt`
rm -r build dist >/dev/null 2>&1
# build binary package
python3 setup.py bdist_rpm --requires="python3-flask, python3-stem, python3-qt5, python3-crypto, python3-pysocks, nautilus-python, tor, obfs4"
python3 setup.py bdist_rpm --requires="python3-flask, python3-flask-httpauth, python3-stem, python3-qt5, python3-crypto, python3-pysocks, nautilus-python, tor, obfs4"
# install it
echo ""

View File

@ -59,6 +59,7 @@ def main():
files_in(dir, 'onionshare_gui/mode') + \
files_in(dir, 'onionshare_gui/mode/share_mode') + \
files_in(dir, 'onionshare_gui/mode/receive_mode') + \
files_in(dir, 'onionshare_gui/mode/website_mode') + \
files_in(dir, 'install/scripts') + \
files_in(dir, 'tests')
pysrc = [p for p in src if p.endswith('.py')]

View File

@ -3,6 +3,7 @@ certifi==2019.3.9
chardet==3.0.4
Click==7.0
Flask==1.0.2
Flask-HTTPAuth==3.2.4
future==0.17.1
idna==2.8
itsdangerous==1.1.0

View File

@ -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
@ -51,6 +60,7 @@ def main(cwd=None):
parser.add_argument('--connect-timeout', metavar='<int>', dest='connect_timeout', default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)")
parser.add_argument('--stealth', action='store_true', dest='stealth', help="Use client authorization (advanced)")
parser.add_argument('--receive', action='store_true', dest='receive', help="Receive shares instead of sending them")
parser.add_argument('--website', action='store_true', dest='website', help="Publish a static website")
parser.add_argument('--config', metavar='config', default=False, help="Custom JSON config file location (optional)")
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help="Log OnionShare errors to stdout, and web errors to disk")
parser.add_argument('filename', metavar='filename', nargs='*', help="List of files or folders to share")
@ -68,10 +78,13 @@ def main(cwd=None):
connect_timeout = int(args.connect_timeout)
stealth = bool(args.stealth)
receive = bool(args.receive)
website = bool(args.website)
config = args.config
if receive:
mode = 'receive'
elif website:
mode = 'website'
else:
mode = 'share'
@ -117,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
@ -131,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')))
@ -168,6 +179,14 @@ def main(cwd=None):
print(e.args[0])
sys.exit()
if mode == 'website':
# Prepare files to share
try:
web.website_mode.set_file_info(filenames)
except OSError as e:
print(e.strerror)
sys.exit(1)
if mode == 'share':
# Prepare files to share
print("Compressing files.")
@ -185,29 +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)
else:
url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug)
url = build_url(common, app, web)
print('')
if autostart_timer > 0:
@ -242,7 +258,7 @@ def main(cwd=None):
if app.autostop_timer > 0:
# if the auto-stop timer was set and has run out, stop the server
if not app.autostop_timer_thread.is_alive():
if mode == 'share':
if mode == 'share' or (mode == 'website'):
# If there were no attempts to download the share, or all downloads are done, we can stop
if web.share_mode.download_count == 0 or web.done:
print("Stopped because auto-stop timer ran out")

View File

@ -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".
"""

View File

@ -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()

View File

@ -18,6 +18,9 @@ class ReceiveModeWeb(object):
self.web = web
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.can_upload = True
self.upload_count = 0
self.uploads_in_progress = []
@ -28,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("/<slug_candidate>")
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.
@ -94,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
@ -125,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("/<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'])
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):
@ -269,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

View File

@ -34,24 +34,18 @@ class ShareModeWeb(object):
# one download at a time.
self.download_in_progress = False
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.define_routes()
def define_routes(self):
"""
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("/")
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.
"""
@ -61,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
@ -70,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("/<slug_candidate>/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.
"""
@ -109,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

View File

@ -5,17 +5,19 @@ 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
from .share_mode import ShareModeWeb
from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeRequest
from .website_mode import WebsiteModeWeb
# Stub out flask's show_server_banner function, to avoiding showing warnings that
# are not applicable to OnionShare
@ -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
@ -111,57 +118,79 @@ class Web(object):
self.receive_mode = None
if self.mode == 'receive':
self.receive_mode = ReceiveModeWeb(self.common, self)
elif self.mode == 'website':
self.website_mode = WebsiteModeWeb(self.common, self)
elif self.mode == 'share':
self.share_mode = ShareModeWeb(self.common, self)
def define_common_routes(self):
"""
Common web app routes between sending and receiving
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("/<slug_candidate>/shutdown")
def shutdown(slug_candidate):
@self.app.route("/<password_candidate>/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'))
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):
@ -177,7 +206,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.
"""
@ -187,14 +216,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 + '/<path:filename>',
endpoint='static', view_func=self.app.send_static_file)
def verbose_mode(self):
"""
@ -205,17 +246,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):
"""
@ -231,11 +264,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
@ -264,17 +297,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:<port>/<shutdown_slug>/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

View File

@ -0,0 +1,181 @@
import os
import sys
import tempfile
import mimetypes
from flask import Response, request, render_template, make_response, send_from_directory
from .. import strings
class WebsiteModeWeb(object):
"""
All of the web logic for share mode
"""
def __init__(self, common, web):
self.common = common
self.common.log('WebsiteModeWeb', '__init__')
self.web = web
# Dictionary mapping file paths to filenames on disk
self.files = {}
self.visit_count = 0
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.define_routes()
def define_routes(self):
"""
The web app routes for sharing a website
"""
@self.web.app.route('/', defaults={'path': ''})
@self.web.app.route('/<path:path>')
def path_public(path):
return path_logic(path)
def path_logic(path=''):
"""
Render the onionshare website.
"""
# Each download has a unique id
visit_id = self.visit_count
self.visit_count += 1
# Tell GUI the page has been visited
self.web.add_request(self.web.REQUEST_STARTED, path, {
'id': visit_id,
'action': 'visit'
})
if path in self.files:
filesystem_path = self.files[path]
# If it's a directory
if os.path.isdir(filesystem_path):
# Is there an index.html?
index_path = os.path.join(path, 'index.html')
if index_path in self.files:
# Render it
dirname = os.path.dirname(self.files[index_path])
basename = os.path.basename(self.files[index_path])
return send_from_directory(dirname, basename)
else:
# Otherwise, render directory listing
filenames = []
for filename in os.listdir(filesystem_path):
if os.path.isdir(os.path.join(filesystem_path, filename)):
filenames.append(filename + '/')
else:
filenames.append(filename)
filenames.sort()
return self.directory_listing(path, filenames, filesystem_path)
# If it's a file
elif os.path.isfile(filesystem_path):
dirname = os.path.dirname(filesystem_path)
basename = os.path.basename(filesystem_path)
return send_from_directory(dirname, basename)
# If it's not a directory or file, throw a 404
else:
return self.web.error404()
else:
# Special case loading /
if path == '':
index_path = 'index.html'
if index_path in self.files:
# Render it
dirname = os.path.dirname(self.files[index_path])
basename = os.path.basename(self.files[index_path])
return send_from_directory(dirname, basename)
else:
# Root directory listing
filenames = list(self.root_files)
filenames.sort()
return self.directory_listing(path, filenames)
else:
# If the path isn't found, throw a 404
return self.web.error404()
def directory_listing(self, path, filenames, filesystem_path=None):
# If filesystem_path is None, this is the root directory listing
files = []
dirs = []
for filename in filenames:
if filesystem_path:
this_filesystem_path = os.path.join(filesystem_path, filename)
else:
this_filesystem_path = self.files[filename]
is_dir = os.path.isdir(this_filesystem_path)
if is_dir:
dirs.append({
'basename': filename
})
else:
size = os.path.getsize(this_filesystem_path)
size_human = self.common.human_readable_filesize(size)
files.append({
'basename': filename,
'size_human': size_human
})
r = make_response(render_template('listing.html',
path=path,
files=files,
dirs=dirs,
static_url_path=self.web.static_url_path))
return self.web.add_security_headers(r)
def set_file_info(self, filenames):
"""
Build a data structure that describes the list of files that make up
the static website.
"""
self.common.log("WebsiteModeWeb", "set_file_info")
# This is a dictionary that maps HTTP routes to filenames on disk
self.files = {}
# This is only the root files and dirs, as opposed to all of them
self.root_files = {}
# If there's just one folder, replace filenames with a list of files inside that folder
if len(filenames) == 1 and os.path.isdir(filenames[0]):
filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])]
# Loop through the files
for filename in filenames:
basename = os.path.basename(filename.rstrip('/'))
# If it's a filename, add it
if os.path.isfile(filename):
self.files[basename] = filename
self.root_files[basename] = filename
# If it's a directory, add it recursively
elif os.path.isdir(filename):
self.root_files[basename + '/'] = filename
for root, _, nested_filenames in os.walk(filename):
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
# The normalized_root should be "some_folder/foobar"
normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/')
# Add the dir itself
self.files[normalized_root + '/'] = root
# Add the files in this dir
for nested_filename in nested_filenames:
self.files[os.path.join(normalized_root, nested_filename)] = os.path.join(root, nested_filename)
return True

View File

@ -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)

View File

@ -22,7 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from ...widgets import Alert, AddFileDialog
from ..widgets import Alert, AddFileDialog
class DropHereLabel(QtWidgets.QLabel):
"""

View File

@ -341,6 +341,35 @@ class ReceiveHistoryItem(HistoryItem):
self.label.setText(self.get_canceled_label_text(self.started))
class VisitHistoryItem(HistoryItem):
"""
Download history item, for share mode
"""
def __init__(self, common, id, total_bytes):
super(VisitHistoryItem, self).__init__()
self.status = HistoryItem.STATUS_STARTED
self.common = common
self.id = id
self.visited = time.time()
self.visited_dt = datetime.fromtimestamp(self.visited)
# Label
self.label = QtWidgets.QLabel(strings._('gui_visit_started').format(self.visited_dt.strftime("%b %d, %I:%M%p")))
# Layout
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label)
self.setLayout(layout)
def update(self):
self.label.setText(self.get_finished_label_text(self.started_dt))
self.status = HistoryItem.STATUS_FINISHED
def cancel(self):
self.progress_bar.setFormat(strings._('gui_canceled'))
self.status = HistoryItem.STATUS_CANCELED
class HistoryItemList(QtWidgets.QScrollArea):
"""
List of items
@ -404,19 +433,19 @@ class HistoryItemList(QtWidgets.QScrollArea):
Reset all items, emptying the list. Override this method.
"""
for key, item in self.items.copy().items():
if item.status != HistoryItem.STATUS_STARTED:
self.items_layout.removeWidget(item)
item.close()
del self.items[key]
self.items_layout.removeWidget(item)
item.close()
del self.items[key]
class History(QtWidgets.QWidget):
"""
A history of what's happened so far in this mode. This contains an internal
object full of a scrollable list of items.
"""
def __init__(self, common, empty_image, empty_text, header_text):
def __init__(self, common, empty_image, empty_text, header_text, mode=''):
super(History, self).__init__()
self.common = common
self.mode = mode
self.setMinimumWidth(350)
@ -535,12 +564,14 @@ class History(QtWidgets.QWidget):
"""
Update the 'in progress' widget.
"""
if self.in_progress_count == 0:
image = self.common.get_resource_path('images/share_in_progress_none.png')
else:
image = self.common.get_resource_path('images/share_in_progress.png')
self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count))
self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count))
if self.mode != 'website':
if self.in_progress_count == 0:
image = self.common.get_resource_path('images/share_in_progress_none.png')
else:
image = self.common.get_resource_path('images/share_in_progress.png')
self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count))
self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count))
class ToggleHistory(QtWidgets.QPushButton):

View File

@ -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()

View File

@ -25,7 +25,7 @@ from onionshare.onion import *
from onionshare.common import Common
from onionshare.web import Web
from .file_selection import FileSelection
from ..file_selection import FileSelection
from .threads import CompressThread
from .. import Mode
from ..history import History, ToggleHistory, ShareHistoryItem
@ -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()

View File

@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import os
import random
import string
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from onionshare.onion import *
from onionshare.common import Common
from onionshare.web import Web
from ..file_selection import FileSelection
from .. import Mode
from ..history import History, ToggleHistory, VisitHistoryItem
from ...widgets import Alert
class WebsiteMode(Mode):
"""
Parts of the main window UI for sharing files.
"""
success = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
def init(self):
"""
Custom initialization for ReceiveMode.
"""
# Create the Web object
self.web = Web(self.common, True, 'website')
# File selection
self.file_selection = FileSelection(self.common, self)
if self.filenames:
for filename in self.filenames:
self.file_selection.file_list.add_file(filename)
# Server status
self.server_status.set_mode('website', self.file_selection)
self.server_status.server_started.connect(self.file_selection.server_started)
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
self.server_status.server_stopped.connect(self.update_primary_action)
self.server_status.server_canceled.connect(self.file_selection.server_stopped)
self.server_status.server_canceled.connect(self.update_primary_action)
self.file_selection.file_list.files_updated.connect(self.server_status.update)
self.file_selection.file_list.files_updated.connect(self.update_primary_action)
# Tell server_status about web, then update
self.server_status.web = self.web
self.server_status.update()
# Filesize warning
self.filesize_warning = QtWidgets.QLabel()
self.filesize_warning.setWordWrap(True)
self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning'])
self.filesize_warning.hide()
# Download history
self.history = History(
self.common,
QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/share_icon_transparent.png'))),
strings._('gui_website_mode_no_files'),
strings._('gui_all_modes_history'),
'website'
)
self.history.hide()
# Info label
self.info_label = QtWidgets.QLabel()
self.info_label.hide()
# Toggle history
self.toggle_history = ToggleHistory(
self.common, self, self.history,
QtGui.QIcon(self.common.get_resource_path('images/share_icon_toggle.png')),
QtGui.QIcon(self.common.get_resource_path('images/share_icon_toggle_selected.png'))
)
# Top bar
top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addWidget(self.info_label)
top_bar_layout.addStretch()
top_bar_layout.addWidget(self.toggle_history)
# Primary action layout
self.primary_action_layout.addWidget(self.filesize_warning)
self.primary_action.hide()
self.update_primary_action()
# Main layout
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addLayout(top_bar_layout)
self.main_layout.addLayout(self.file_selection)
self.main_layout.addWidget(self.primary_action)
self.main_layout.addWidget(self.min_width_widget)
# Wrapper layout
self.wrapper_layout = QtWidgets.QHBoxLayout()
self.wrapper_layout.addLayout(self.main_layout)
self.wrapper_layout.addWidget(self.history, stretch=1)
self.setLayout(self.wrapper_layout)
# Always start with focus on file selection
self.file_selection.setFocus()
def get_stop_server_autostop_timer_text(self):
"""
Return the string to put on the stop server button, if there's an auto-stop timer
"""
return strings._('gui_share_stop_server_autostop_timer')
def autostop_timer_finished_should_stop_server(self):
"""
The auto-stop timer expired, should we stop the server? Returns a bool
"""
self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_autostop_timer'))
return True
def start_server_custom(self):
"""
Starting the server.
"""
# Reset web counters
self.web.website_mode.visit_count = 0
self.web.reset_invalid_passwords()
# Hide and reset the downloads if we have previously shared
self.reset_info_counters()
def start_server_step2_custom(self):
"""
Step 2 in starting the server. Zipping up files.
"""
self.filenames = []
for index in range(self.file_selection.file_list.count()):
self.filenames.append(self.file_selection.file_list.item(index).filename)
# Continue
self.starting_server_step3.emit()
self.start_server_finished.emit()
def start_server_step3_custom(self):
"""
Step 3 in starting the server. Display large filesize
warning, if applicable.
"""
if self.web.website_mode.set_file_info(self.filenames):
self.success.emit()
else:
# Cancelled
pass
def start_server_error_custom(self):
"""
Start server error.
"""
if self._zip_progress_bar is not None:
self.status_bar.removeWidget(self._zip_progress_bar)
self._zip_progress_bar = None
def stop_server_custom(self):
"""
Stop server.
"""
self.filesize_warning.hide()
self.history.completed_count = 0
self.file_selection.file_list.adjustSize()
def cancel_server_custom(self):
"""
Log that the server has been cancelled
"""
self.common.log('WebsiteMode', 'cancel_server')
def handle_tor_broke_custom(self):
"""
Connection to Tor broke.
"""
self.primary_action.hide()
def handle_request_load(self, event):
"""
Handle REQUEST_LOAD event.
"""
self.system_tray.showMessage(strings._('systray_site_loaded_title'), strings._('systray_site_loaded_message'))
def handle_request_started(self, event):
"""
Handle REQUEST_STARTED event.
"""
if ( (event["path"] == '') or (event["path"].find(".htm") != -1 ) ):
item = VisitHistoryItem(self.common, event["data"]["id"], 0)
self.history.add(event["data"]["id"], item)
self.toggle_history.update_indicator(True)
self.history.completed_count += 1
self.history.update_completed()
self.system_tray.showMessage(strings._('systray_website_started_title'), strings._('systray_website_started_message'))
def on_reload_settings(self):
"""
If there were some files listed for sharing, we should be ok to re-enable
the 'Start Sharing' button now.
"""
if self.server_status.file_selection.get_num_files() > 0:
self.primary_action.show()
self.info_label.show()
def update_primary_action(self):
self.common.log('WebsiteMode', 'update_primary_action')
# Show or hide primary action layout
file_count = self.file_selection.file_list.count()
if file_count > 0:
self.primary_action.show()
self.info_label.show()
# Update the file count in the info label
total_size_bytes = 0
for index in range(self.file_selection.file_list.count()):
item = self.file_selection.file_list.item(index)
total_size_bytes += item.size_bytes
total_size_readable = self.common.human_readable_filesize(total_size_bytes)
if file_count > 1:
self.info_label.setText(strings._('gui_file_info').format(file_count, total_size_readable))
else:
self.info_label.setText(strings._('gui_file_info_single').format(file_count, total_size_readable))
else:
self.primary_action.hide()
self.info_label.hide()
def reset_info_counters(self):
"""
Set the info counters back to zero.
"""
self.history.reset()
@staticmethod
def _compute_total_size(filenames):
total_size = 0
for filename in filenames:
if os.path.isfile(filename):
total_size += os.path.getsize(filename)
if os.path.isdir(filename):
total_size += Common.dir_size(filename)
return total_size

View File

@ -25,6 +25,7 @@ from onionshare.web import Web
from .mode.share_mode import ShareMode
from .mode.receive_mode import ReceiveMode
from .mode.website_mode import WebsiteMode
from .tor_connection_dialog import TorConnectionDialog
from .settings_dialog import SettingsDialog
@ -39,6 +40,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
MODE_SHARE = 'share'
MODE_RECEIVE = 'receive'
MODE_WEBSITE = 'website'
def __init__(self, common, onion, qtapp, app, filenames, config=False, local_only=False):
super(OnionShareGui, self).__init__()
@ -92,6 +94,9 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.receive_mode_button = QtWidgets.QPushButton(strings._('gui_mode_receive_button'));
self.receive_mode_button.setFixedHeight(50)
self.receive_mode_button.clicked.connect(self.receive_mode_clicked)
self.website_mode_button = QtWidgets.QPushButton(strings._('gui_mode_website_button'));
self.website_mode_button.setFixedHeight(50)
self.website_mode_button.clicked.connect(self.website_mode_clicked)
self.settings_button = QtWidgets.QPushButton()
self.settings_button.setDefault(False)
self.settings_button.setFixedWidth(40)
@ -103,6 +108,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
mode_switcher_layout.setSpacing(0)
mode_switcher_layout.addWidget(self.share_mode_button)
mode_switcher_layout.addWidget(self.receive_mode_button)
mode_switcher_layout.addWidget(self.website_mode_button)
mode_switcher_layout.addWidget(self.settings_button)
# Server status indicator on the status bar
@ -154,6 +160,20 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.receive_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.receive_mode.set_server_active.connect(self.set_server_active)
# Website mode
self.website_mode = WebsiteMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames)
self.website_mode.init()
self.website_mode.server_status.server_started.connect(self.update_server_status_indicator)
self.website_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
self.website_mode.start_server_finished.connect(self.update_server_status_indicator)
self.website_mode.stop_server_finished.connect(self.update_server_status_indicator)
self.website_mode.stop_server_finished.connect(self.stop_server_finished)
self.website_mode.start_server_finished.connect(self.clear_message)
self.website_mode.server_status.button_clicked.connect(self.clear_message)
self.website_mode.server_status.url_copied.connect(self.copy_url)
self.website_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.website_mode.set_server_active.connect(self.set_server_active)
self.update_mode_switcher()
self.update_server_status_indicator()
@ -162,6 +182,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
contents_layout.setContentsMargins(10, 0, 10, 0)
contents_layout.addWidget(self.receive_mode)
contents_layout.addWidget(self.share_mode)
contents_layout.addWidget(self.website_mode)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
@ -199,15 +220,27 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self.mode == self.MODE_SHARE:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.website_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.receive_mode.hide()
self.share_mode.show()
self.website_mode.hide()
elif self.mode == self.MODE_WEBSITE:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.website_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
self.receive_mode.hide()
self.share_mode.hide()
self.website_mode.show()
else:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
self.website_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.share_mode.hide()
self.receive_mode.show()
self.website_mode.hide()
self.update_server_status_indicator()
@ -223,6 +256,12 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.mode = self.MODE_RECEIVE
self.update_mode_switcher()
def website_mode_clicked(self):
if self.mode != self.MODE_WEBSITE:
self.common.log('OnionShareGui', 'website_mode_clicked')
self.mode = self.MODE_WEBSITE
self.update_mode_switcher()
def update_server_status_indicator(self):
# Set the status image
if self.mode == self.MODE_SHARE:
@ -239,6 +278,17 @@ class OnionShareGui(QtWidgets.QMainWindow):
elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
self.server_status_label.setText(strings._('gui_status_indicator_share_started'))
elif self.mode == self.MODE_WEBSITE:
# Website mode
if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
self.server_status_label.setText(strings._('gui_status_indicator_share_stopped'))
elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
self.server_status_label.setText(strings._('gui_status_indicator_share_working'))
elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
self.server_status_label.setText(strings._('gui_status_indicator_share_started'))
else:
# Receive mode
if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED:
@ -317,18 +367,22 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.timer.start(500)
self.share_mode.on_reload_settings()
self.receive_mode.on_reload_settings()
self.website_mode.on_reload_settings()
self.status_bar.clearMessage()
# If we switched off the auto-stop timer setting, ensure the widget is hidden.
if not self.common.settings.get('autostop_timer'):
self.share_mode.server_status.autostop_timer_container.hide()
self.receive_mode.server_status.autostop_timer_container.hide()
self.website_mode.server_status.autostop_timer_container.hide()
# If we switched off the auto-start timer setting, ensure the widget is hidden.
if not self.common.settings.get('autostart_timer'):
self.share_mode.server_status.autostart_timer_datetime = None
self.receive_mode.server_status.autostart_timer_datetime = None
self.website_mode.server_status.autostart_timer_datetime = None
self.share_mode.server_status.autostart_timer_container.hide()
self.receive_mode.server_status.autostart_timer_container.hide()
self.website_mode.server_status.autostart_timer_container.hide()
d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
d.settings_saved.connect(reload_settings)
@ -337,6 +391,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
# When settings close, refresh the server status UI
self.share_mode.server_status.update()
self.receive_mode.server_status.update()
self.website_mode.server_status.update()
def check_for_updates(self):
"""
@ -367,10 +422,13 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.share_mode.handle_tor_broke()
self.receive_mode.handle_tor_broke()
self.website_mode.handle_tor_broke()
# Process events from the web object
if self.mode == self.MODE_SHARE:
mode = self.share_mode
elif self.mode == self.MODE_WEBSITE:
mode = self.website_mode
else:
mode = self.receive_mode
@ -416,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()
@ -450,13 +511,20 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self.mode == self.MODE_SHARE:
self.share_mode_button.show()
self.receive_mode_button.hide()
self.website_mode_button.hide()
elif self.mode == self.MODE_WEBSITE:
self.share_mode_button.hide()
self.receive_mode_button.hide()
self.website_mode_button.show()
else:
self.share_mode_button.hide()
self.receive_mode_button.show()
self.website_mode_button.hide()
else:
self.settings_button.show()
self.share_mode_button.show()
self.receive_mode_button.show()
self.website_mode_button.show()
# Disable settings menu action when server is active
self.settings_action.setEnabled(not active)
@ -466,6 +534,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
try:
if self.mode == OnionShareGui.MODE_SHARE:
server_status = self.share_mode.server_status
if self.mode == OnionShareGui.MODE_WEBSITE:
server_status = self.website_mode.server_status
else:
server_status = self.receive_mode.server_status
if server_status.status != server_status.STATUS_STOPPED:

View File

@ -39,6 +39,7 @@ class ServerStatus(QtWidgets.QWidget):
MODE_SHARE = 'share'
MODE_RECEIVE = 'receive'
MODE_WEBSITE = 'website'
STATUS_STOPPED = 0
STATUS_WORKING = 1
@ -159,7 +160,7 @@ class ServerStatus(QtWidgets.QWidget):
"""
self.mode = share_mode
if self.mode == ServerStatus.MODE_SHARE:
if (self.mode == ServerStatus.MODE_SHARE) or (self.mode == ServerStatus.MODE_WEBSITE):
self.file_selection = file_selection
self.update()
@ -207,6 +208,8 @@ class ServerStatus(QtWidgets.QWidget):
if self.mode == ServerStatus.MODE_SHARE:
self.url_description.setText(strings._('gui_share_url_description').format(info_image))
elif self.mode == ServerStatus.MODE_WEBSITE:
self.url_description.setText(strings._('gui_share_url_description').format(info_image))
else:
self.url_description.setText(strings._('gui_receive_url_description').format(info_image))
@ -240,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'):
@ -258,6 +261,8 @@ class ServerStatus(QtWidgets.QWidget):
# Button
if self.mode == ServerStatus.MODE_SHARE and self.file_selection.get_num_files() == 0:
self.server_button.hide()
elif self.mode == ServerStatus.MODE_WEBSITE and self.file_selection.get_num_files() == 0:
self.server_button.hide()
else:
self.server_button.show()
@ -266,6 +271,8 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.setEnabled(True)
if self.mode == ServerStatus.MODE_SHARE:
self.server_button.setText(strings._('gui_share_start_server'))
elif self.mode == ServerStatus.MODE_WEBSITE:
self.server_button.setText(strings._('gui_share_start_server'))
else:
self.server_button.setText(strings._('gui_receive_start_server'))
self.server_button.setToolTip('')
@ -278,6 +285,8 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.setEnabled(True)
if self.mode == ServerStatus.MODE_SHARE:
self.server_button.setText(strings._('gui_share_stop_server'))
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'))
if self.common.settings.get('autostart_timer'):
@ -412,5 +421,5 @@ class ServerStatus(QtWidgets.QWidget):
if self.common.settings.get('public_mode'):
url = 'http://{0:s}'.format(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

View File

@ -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', '')

View File

@ -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()

View File

@ -88,7 +88,8 @@ setup(
'onionshare_gui',
'onionshare_gui.mode',
'onionshare_gui.mode.share_mode',
'onionshare_gui.mode.receive_mode'
'onionshare_gui.mode.receive_mode',
'onionshare_gui.mode.website_mode'
],
include_package_data=True,
scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'],

View File

@ -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.",
@ -114,6 +115,7 @@
"gui_use_legacy_v2_onions_checkbox": "Use legacy addresses",
"gui_save_private_key_checkbox": "Use a persistent address",
"gui_share_url_description": "<b>Anyone</b> with this OnionShare address can <b>download</b> your files using the <b>Tor Browser</b>: <img src='{}' />",
"gui_website_url_description": "<b>Anyone</b> with this OnionShare address can <b>visit</b> your website using the <b>Tor Browser</b>: <img src='{}' />",
"gui_receive_url_description": "<b>Anyone</b> with this OnionShare address can <b>upload</b> files to your computer using the <b>Tor Browser</b>: <img src='{}' />",
"gui_url_label_persistent": "This share will not auto-stop.<br><br>Every subsequent share reuses the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)",
"gui_url_label_stay_open": "This share will not auto-stop.",
@ -135,6 +137,7 @@
"gui_receive_mode_warning": "Receive mode lets people upload files to your computer.<br><br><b>Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.</b>",
"gui_mode_share_button": "Share Files",
"gui_mode_receive_button": "Receive Files",
"gui_mode_website_button": "Publish Website",
"gui_settings_receiving_label": "Receiving settings",
"gui_settings_data_dir_label": "Save files to",
"gui_settings_data_dir_browse_button": "Browse",
@ -145,6 +148,8 @@
"systray_menu_exit": "Quit",
"systray_page_loaded_title": "Page Loaded",
"systray_page_loaded_message": "OnionShare address loaded",
"systray_site_loaded_title": "Site Loaded",
"systray_site_loaded_message": "OnionShare site loaded",
"systray_share_started_title": "Sharing Started",
"systray_share_started_message": "Starting to send files to someone",
"systray_share_completed_title": "Sharing Complete",
@ -153,6 +158,8 @@
"systray_share_canceled_message": "Someone canceled receiving your files",
"systray_receive_started_title": "Receiving Started",
"systray_receive_started_message": "Someone is sending files to you",
"systray_website_started_title": "Starting sharing website",
"systray_website_started_message": "Someone is visiting your website",
"gui_all_modes_history": "History",
"gui_all_modes_clear_history": "Clear All",
"gui_all_modes_transfer_started": "Started {}",
@ -165,8 +172,10 @@
"gui_all_modes_progress_eta": "{0:s}, ETA: {1:s}, %p%",
"gui_share_mode_no_files": "No Files Sent Yet",
"gui_share_mode_autostop_timer_waiting": "Waiting to finish sending",
"gui_website_mode_no_files": "No Website Shared Yet",
"gui_receive_mode_no_files": "No Files Received Yet",
"gui_receive_mode_autostop_timer_waiting": "Waiting to finish receiving",
"gui_visit_started": "Someone has visited your website {}",
"receive_mode_upload_starting": "Upload of total size {} is starting",
"days_first_letter": "d",
"hours_first_letter": "h",

View File

@ -222,20 +222,3 @@ li.info {
color: #666666;
margin: 0 0 20px 0;
}
div#noscript {
text-align: center;
color: #d709df;
padding: 1em;
line-height: 150%;
margin: 0 auto;
}
div#noscript a, div#noscript a:visited {
color: #d709df;
}
.disable-noscript-xss-wrapper {
max-width: 900px;
margin: 0 auto;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 B

View File

@ -1,2 +0,0 @@
// Hide the noscript div, because our javascript is executing
document.getElementById('noscript').style.display = 'none';

View File

@ -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);
});
});

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>
<title>OnionShare: 403 Forbidden</title>
<link href="/static/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 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/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>
</div>
</div>

View File

@ -3,14 +3,14 @@
<head>
<title>OnionShare: 404 Not Found</title>
<link href="/static/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 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/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>
</div>
</div>

View File

@ -3,7 +3,7 @@
<head>
<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>
<body>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare</title>
<link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon" />
<link href="{{ static_url_path }}/css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<header class="clearfix">
<img class="logo" src="{{ static_url_path }}/img/logo.png" title="OnionShare">
<h1>OnionShare</h1>
</header>
<table class="file-list" id="file-list">
<tr>
<th id="filename-header">Filename</th>
<th id="size-header">Size</th>
<th></th>
</tr>
{% for info in dirs %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_folder.png" />
<a href="{{ info.basename }}">
{{ info.basename }}
</a>
</td>
<td>&mdash;</td>
</tr>
{% endfor %}
{% for info in files %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="{{ static_url_path }}/img/web_file.png" />
<a href="{{ info.basename }}">
{{ info.basename }}
</a>
</td>
<td>{{ info.size_human }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>

View File

@ -2,31 +2,18 @@
<html>
<head>
<title>OnionShare</title>
<link href="/static/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 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>
<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>
</header>
<div class="upload-wrapper">
<!--
We are not using a <noscript> tag because it only works when the security slider is set to
Safest, not Safer: https://trac.torproject.org/projects/tor/ticket/29506
-->
<div id="noscript">
<p>
<img src="/static/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
<a rel="noreferrer" target="_blank" href="https://tb-manual.torproject.org/en-US/security-slider/">security slider</a>
to Standard or
<a target="_blank" href="/noscript-xss-instructions">turn off your Tor Browser's NoScript XSS setting</a>.</p>
</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-description">Select the files you want to send, then click "Send Files"...</p>
@ -45,14 +32,13 @@
</ul>
</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><button type="submit" id="send-button" class="button">Send Files</button></p>
</form>
</div>
<script src="/static/js/receive-noscript.js"></script>
<script src="/static/js/jquery-3.4.0.min.js"></script>
<script async src="/static/js/receive.js"></script>
<script src="{{ static_url_path }}/js/jquery-3.4.0.min.js"></script>
<script async src="{{ static_url_path }}/js/receive.js"></script>
</body>
</html>

View File

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="stylesheet" rel="subresource" type="text/css" href="/static/css/style.css" media="all">
</head>
<body>
<header class="clearfix">
<img class="logo" src="/static/img/logo.png" title="OnionShare">
<h1>OnionShare</h1>
</header>
<div class="disable-noscript-xss-wrapper">
<h3>Disable your Tor Browser's NoScript XSS setting</h3>
<p>If your security slider is set to Safest, JavaScript is disabled so XSS vulnerabilities won't affect you,
which makes it safe to disable NoScript's XSS protections.</p>
<p>Here is how to disable this setting:</p>
<ol>
<li>Click the menu icon in the top-right of Tor Browser and open "Add-ons"</li>
<li>Next to the NoScript add-on, click the "Preferences" button</li>
<li>Switch to the "Advanced" tab</li>
<li>Uncheck "Sanitize cross-site suspicious requests"</li>
</ol>
<p>If you'd like to learn technical details about this issue, check
<a rel="noreferrer" href="https://github.com/micahflee/onionshare/issues/899">this issue</a>
on GitHub.</p>
</div>
</body>
</html>

View File

@ -3,8 +3,8 @@
<head>
<title>OnionShare</title>
<link href="/static/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 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">
<meta name="onionshare-filename" content="{{ filename }}">
<meta name="onionshare-filesize" content="{{ filesize }}">
</head>
@ -15,14 +15,10 @@
<div class="right">
<ul>
<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>
{% endif %}
</ul>
</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>
</header>
@ -35,7 +31,7 @@
{% for info in file_info.dirs %}
<tr>
<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 }}
</td>
<td>{{ info.size_human }}</td>
@ -45,7 +41,7 @@
{% for info in file_info.files %}
<tr>
<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 }}
</td>
<td>{{ info.size_human }}</td>
@ -53,7 +49,7 @@
</tr>
{% endfor %}
</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>
</html>

View File

@ -3,19 +3,19 @@
<head>
<title>OnionShare is closed</title>
<link href="/static/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 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>
<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>
</header>
<div class="info-wrapper">
<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-description">You may now close this window.</p>
</div>

View File

@ -1,6 +1,6 @@
[DEFAULT]
Package3: onionshare
Depends3: python3, python3-flask, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python-nautilus, tor, obfs4proxy
Build-Depends: python3, python3-pytest, python3-flask, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-requests, python-nautilus, tor, obfs4proxy
Depends3: python3, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python-nautilus, tor, obfs4proxy
Build-Depends: python3, python3-pytest, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-requests, python-nautilus, tor, obfs4proxy
Suite: cosmic
X-Python3-Version: >= 3.5.3

View File

@ -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):

View File

@ -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)

View File

@ -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
@ -66,39 +67,27 @@ class GuiShareTest(GuiBaseTest):
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:
@ -139,7 +128,7 @@ 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)
@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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,14 +74,14 @@ 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):
@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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,
}

View File

@ -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,
}

View File

@ -33,13 +33,13 @@ LOG_MSG_REGEX = re.compile(r"""
^\[Jun\ 06\ 2013\ 11:05:00\]
\ TestModule\.<function\ TestLog\.test_output\.<locals>\.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:

View File

@ -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

View File

@ -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', (