Fix conflicts

This commit is contained in:
Miguel Jacq 2018-10-11 13:04:48 +11:00
commit 86b537a4a6
No known key found for this signature in database
GPG Key ID: EEA4341C6D97A0B6
95 changed files with 6523 additions and 1996 deletions

8
.github/CODEOWNERS vendored
View File

@ -1 +1,9 @@
* @micahflee * @micahflee
# localization
/share/locale/ @emmapeel2
# tests
/tests/ @mig5
/tests_gui_local/ @mig5
/tests_gui_tor/ @mig5

View File

@ -1,6 +1,6 @@
language: python language: python
# sudo: required dist: trusty
dist: bionic sudo: required
python: python:
- "3.6" - "3.6"
- "3.6-dev" - "3.6-dev"
@ -8,14 +8,16 @@ python:
- "nightly" - "nightly"
# command to install dependencies # command to install dependencies
install: install:
- sudo apt-get update && sudo apt-get install python3-pyqt5
- pip install -r install/requirements.txt - pip install -r install/requirements.txt
- pip install pytest-cov coveralls flake8 - pip install -r install/requirements-tests.txt
- pip install pytest-cov flake8
before_script: before_script:
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# command to run tests # run CLI tests and local GUI tests
script: pytest --cov=onionshare test/ script:
after_success: - pytest --cov=onionshare tests/
- coveralls - cd tests_gui_local/ && xvfb-run ./run_unit_tests.sh

View File

@ -11,11 +11,11 @@ cd onionshare
Install the needed dependencies: Install the needed dependencies:
For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-flask python3-stem python3-pyqt5 python-nautilus python3-pytest tor obfs4proxy python3-cryptography python3-crypto python3-nacl python3-pip python3-socks python3-sha3` For Debian-like distros: `apt install -y python3-flask python3-stem python3-pyqt5 python3-cryptography python3-crypto python3-nacl python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python`
On some older versions of Debian you may need to install pysha3 with `pip3 install pysha3` if python3-sha3 is not available. On some older versions of Debian you may need to install pysha3 with `pip3 install pysha3` if python3-sha3 is not available.
For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 python3-pytest nautilus-python tor obfs4 python3-pynacl python3-cryptography python3-crypto python3-pip python3-pysocks` For Fedora-like distros: `dnf install -y python3-flask python3-stem python3-qt5 python3-pynacl python3-cryptography python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build`
After that you can try both the CLI and the GUI version of OnionShare: After that you can try both the CLI and the GUI version of OnionShare:
@ -137,8 +137,30 @@ This will prompt you to codesign three binaries and execute one unsigned binary.
## Tests ## Tests
OnionShare includes PyTest unit tests. To run the tests: OnionShare includes PyTest unit tests. To run the tests, first install some dependencies:
```sh ```sh
pytest test/ pip3 install -r install/requirements-tests.txt
``` ```
If you'd like to run the CLI-based tests that Travis runs:
```sh
pytest tests/
```
If you would like to run the GUI unit tests in 'local only mode':
```sh
cd tests_gui_local/
./run_unit_tests.sh
```
If you would like to run the GUI unit tests in 'tor' (bundled) mode:
```sh
cd tests_gui_tor/
./run_unit_tests.sh
```
Keep in mind that the Tor tests take a lot longer to run than local mode, but they are also more comprehensive.

View File

@ -10,4 +10,4 @@ include install/onionshare.desktop
include install/onionshare.appdata.xml include install/onionshare.appdata.xml
include install/onionshare80.xpm include install/onionshare80.xpm
include install/scripts/onionshare-nautilus.py include install/scripts/onionshare-nautilus.py
include test/*.py include tests/*.py

14
dev_scripts/run_all_tests.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
ROOT="$( dirname $(cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd ))"
# CLI tests
cd $ROOT
pytest tests/
# Local GUI tests
cd $ROOT/tests_gui_local
./run_unit_tests.sh
# Tor GUI tests
cd $ROOT/tests_gui_tor
./run_unit_tests.sh

View File

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

View File

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

View File

@ -35,9 +35,9 @@ import subprocess
import requests import requests
def main(): def main():
dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/TorBrowser-8.0-osx64_en-US.dmg' dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0.1/TorBrowser-8.0.1-osx64_en-US.dmg'
dmg_filename = 'TorBrowser-8.0-osx64_en-US.dmg' dmg_filename = 'TorBrowser-8.0.1-osx64_en-US.dmg'
expected_dmg_sha256 = '15603ae7b3a1942863c98acc92f509e4409db48fe22c9acae6b15c9cb9bf3088' expected_dmg_sha256 = 'fb1be2a0f850a65bae38747c3abbf9061742c5d7799e1693405078aaf38d2b08'
# Build paths # Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))

View File

@ -33,9 +33,9 @@ import subprocess
import requests import requests
def main(): def main():
exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/torbrowser-install-8.0_en-US.exe' exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0.1/torbrowser-install-8.0.1_en-US.exe'
exe_filename = 'torbrowser-install-8.0_en-US.exe' exe_filename = 'torbrowser-install-8.0.1_en-US.exe'
expected_exe_sha256 = '0682b44eff5877dfc2fe2fdd5b46e678d47adad86d564e7cb6654c5f60eb1ed2' expected_exe_sha256 = 'bdf81d4282b991a6425c213c7b03b3f5c1f17bb02986b7fe9a1891e577e51639'
# Build paths # Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
working_path = os.path.join(os.path.join(root_path, 'build'), 'tor') working_path = os.path.join(os.path.join(root_path, 'build'), 'tor')

View File

@ -0,0 +1,11 @@
atomicwrites==1.2.1
attrs==18.2.0
more-itertools==4.3.0
pluggy==0.6.0
py==1.6.0
pytest==3.4.2
pytest-faulthandler==1.5.0
pytest-ordering==0.5
pytest-qt==3.1.0
six==1.11.0
urllib3==1.23

View File

@ -65,13 +65,18 @@ def main(cwd=None):
receive = bool(args.receive) receive = bool(args.receive)
config = args.config config = args.config
if receive:
mode = 'receive'
else:
mode = 'share'
# Make sure filenames given if not using receiver mode # Make sure filenames given if not using receiver mode
if not receive and len(filenames) == 0: if mode == 'share' and len(filenames) == 0:
print(strings._('no_filenames')) parser.print_help()
sys.exit() sys.exit()
# Validate filenames # Validate filenames
if not receive: if mode == 'share':
valid = True valid = True
for filename in filenames: for filename in filenames:
if not os.path.isfile(filename) and not os.path.isdir(filename): if not os.path.isfile(filename) and not os.path.isdir(filename):
@ -90,7 +95,7 @@ def main(cwd=None):
common.debug = debug common.debug = debug
# Create the Web object # Create the Web object
web = Web(common, False, receive) web = Web(common, False, mode)
# Start the Onion object # Start the Onion object
onion = Onion(common) onion = Onion(common)
@ -116,20 +121,21 @@ def main(cwd=None):
print(e.args[0]) print(e.args[0])
sys.exit() sys.exit()
# Prepare files to share if mode == 'share':
print(strings._("preparing_files")) # Prepare files to share
try: print(strings._("preparing_files"))
web.set_file_info(filenames) try:
app.cleanup_filenames.append(web.zip_filename) web.share_mode.set_file_info(filenames)
except OSError as e: app.cleanup_filenames += web.share_mode.cleanup_filenames
print(e.strerror) except OSError as e:
sys.exit(1) print(e.strerror)
sys.exit(1)
# Warn about sending large files over Tor # Warn about sending large files over Tor
if web.zip_filesize >= 157286400: # 150mb if web.share_mode.download_filesize >= 157286400: # 150mb
print('') print('')
print(strings._("large_filesize")) print(strings._("large_filesize"))
print('') print('')
# Start OnionShare http service in new thread # Start OnionShare http service in new thread
t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), common.settings.get('slug'))) t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), common.settings.get('slug')))
@ -157,7 +163,7 @@ def main(cwd=None):
url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug)
print('') print('')
if receive: if mode == 'receive':
print(strings._('receive_mode_downloads_dir').format(common.settings.get('downloads_dir'))) print(strings._('receive_mode_downloads_dir').format(common.settings.get('downloads_dir')))
print('') print('')
print(strings._('receive_mode_warning')) print(strings._('receive_mode_warning'))
@ -186,11 +192,12 @@ def main(cwd=None):
if app.shutdown_timeout > 0: if app.shutdown_timeout > 0:
# if the shutdown timer was set and has run out, stop the server # if the shutdown timer was set and has run out, stop the server
if not app.shutdown_timer.is_alive(): if not app.shutdown_timer.is_alive():
# If there were no attempts to download the share, or all downloads are done, we can stop if mode == 'share':
if web.download_count == 0 or web.done: # If there were no attempts to download the share, or all downloads are done, we can stop
print(strings._("close_on_timeout")) if web.share_mode.download_count == 0 or web.done:
web.stop(app.port) print(strings._("close_on_timeout"))
break web.stop(app.port)
break
# Allow KeyboardInterrupt exception to be handled with threads # Allow KeyboardInterrupt exception to be handled with threads
# https://stackoverflow.com/questions/3788208/python-threading-ignores-keyboardinterrupt-exception # https://stackoverflow.com/questions/3788208/python-threading-ignores-keyboardinterrupt-exception
time.sleep(0.2) time.sleep(0.2)

View File

@ -211,6 +211,7 @@ class Common(object):
color: #000000; color: #000000;
padding: 10px; padding: 10px;
border: 1px solid #666666; border: 1px solid #666666;
font-size: 12px;
} }
""", """,
@ -248,11 +249,46 @@ class Common(object):
border-radius: 5px; border-radius: 5px;
}""", }""",
'downloads_uploads_empty': """
QWidget {
background-color: #ffffff;
border: 1px solid #999999;
}
QWidget QLabel {
background-color: none;
border: 0px;
}
""",
'downloads_uploads_empty_text': """
QLabel {
color: #999999;
}""",
'downloads_uploads_label': """ 'downloads_uploads_label': """
QLabel { QLabel {
font-weight: bold; font-weight: bold;
font-size 14px; font-size 14px;
text-align: center; text-align: center;
background-color: none;
border: none;
}""",
'downloads_uploads_clear': """
QPushButton {
color: #3f7fcf;
}
""",
'download_uploads_indicator': """
QLabel {
color: #ffffff;
background-color: #f44449;
font-weight: bold;
font-size: 10px;
padding: 2px;
border-radius: 7px;
text-align: center;
}""", }""",
'downloads_uploads_progress_bar': """ 'downloads_uploads_progress_bar': """
@ -261,7 +297,7 @@ class Common(object):
background-color: #ffffff !important; background-color: #ffffff !important;
text-align: center; text-align: center;
color: #9b9b9b; color: #9b9b9b;
font-size: 12px; font-size: 14px;
} }
QProgressBar::chunk { QProgressBar::chunk {
background-color: #4e064f; background-color: #4e064f;
@ -433,7 +469,7 @@ class Common(object):
tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port))) tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port)))
break break
except OSError as e: except OSError as e:
raise OSError(e) pass
_, port = tmpsock.getsockname() _, port = tmpsock.getsockname()
return port return port

View File

@ -402,6 +402,8 @@ class Onion(object):
# ephemeral stealth onion services are not supported # ephemeral stealth onion services are not supported
self.supports_stealth = False self.supports_stealth = False
# Does this version of Tor support next-gen ('v3') onions?
self.supports_next_gen_onions = self.tor_version > Version('0.3.3.1')
def is_authenticated(self): def is_authenticated(self):
""" """
@ -427,7 +429,6 @@ class Onion(object):
raise TorTooOld(strings._('error_stealth_not_supported')) raise TorTooOld(strings._('error_stealth_not_supported'))
print(strings._("config_onion_service").format(int(port))) print(strings._("config_onion_service").format(int(port)))
print(strings._('using_ephemeral'))
if self.stealth: if self.stealth:
if self.settings.get('hidservauth_string'): if self.settings.get('hidservauth_string'):

View File

@ -1,846 +0,0 @@
# -*- 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 hmac
import logging
import mimetypes
import os
import queue
import socket
import sys
import tempfile
import zipfile
import re
import io
from distutils.version import LooseVersion as Version
from urllib.request import urlopen
from datetime import datetime
import flask
from flask import (
Flask, Response, Request, request, render_template, abort, make_response,
flash, redirect, __version__ as flask_version
)
from werkzeug.utils import secure_filename
from . import strings
from .common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable
# Stub out flask's show_server_banner function, to avoiding showing warnings that
# are not applicable to OnionShare
def stubbed_show_server_banner(env, debug, app_import_path, eager_loading):
pass
flask.cli.show_server_banner = stubbed_show_server_banner
class Web(object):
"""
The Web object is the OnionShare web server, powered by flask
"""
REQUEST_LOAD = 0
REQUEST_STARTED = 1
REQUEST_PROGRESS = 2
REQUEST_OTHER = 3
REQUEST_CANCELED = 4
REQUEST_RATE_LIMIT = 5
REQUEST_CLOSE_SERVER = 6
REQUEST_UPLOAD_FILE_RENAMED = 7
REQUEST_UPLOAD_FINISHED = 8
REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9
REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10
def __init__(self, common, gui_mode, receive_mode=False):
self.common = common
# The flask app
self.app = Flask(__name__,
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)
# Debug mode?
if self.common.debug:
self.debug_mode()
# Are we running in GUI mode?
self.gui_mode = gui_mode
# Are we using receive mode?
self.receive_mode = receive_mode
if self.receive_mode:
# Use custom WSGI middleware, to modify environ
self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self)
# Use a custom Request class to track upload progess
self.app.request_class = ReceiveModeRequest
# Starting in Flask 0.11, render_template_string autoescapes template variables
# by default. To prevent content injection through template variables in
# earlier versions of Flask, we force autoescaping in the Jinja2 template
# engine if we detect a Flask version with insecure default behavior.
if Version(flask_version) < Version('0.11'):
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape
# Information about the file
self.file_info = []
self.zip_filename = None
self.zip_filesize = None
self.security_headers = [
('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'),
('X-Frame-Options', 'DENY'),
('X-Xss-Protection', '1; mode=block'),
('X-Content-Type-Options', 'nosniff'),
('Referrer-Policy', 'no-referrer'),
('Server', 'OnionShare')
]
self.q = queue.Queue()
self.slug = None
self.download_count = 0
self.upload_count = 0
self.error404_count = 0
# If "Stop After First Download" is checked (stay_open == False), only allow
# one download at a time.
self.download_in_progress = False
self.done = False
# If the client closes the OnionShare window while a download is in progress,
# it should immediately stop serving the file. The client_cancel global is
# used to tell the download function that the client is canceling the download.
self.client_cancel = False
# shutting down the server only works within the context of flask, so the easiest way to do it is over http
self.shutdown_slug = self.common.random_string(16)
# Keep track if the server is running
self.running = False
# Define the ewb app routes
self.common_routes()
if self.receive_mode:
self.receive_routes()
else:
self.send_routes()
def send_routes(self):
"""
The web app routes for sharing files
"""
@self.app.route("/<slug_candidate>")
def index(slug_candidate):
self.check_slug_candidate(slug_candidate)
return index_logic()
@self.app.route("/")
def index_public():
if not self.common.settings.get('public_mode'):
return self.error404()
return index_logic()
def index_logic(slug_candidate=''):
"""
Render the template for the onionshare landing page.
"""
self.add_request(Web.REQUEST_LOAD, request.path)
# Deny new downloads if "Stop After First Download" is checked and there is
# currently a download
deny_download = not self.stay_open and self.download_in_progress
if deny_download:
r = make_response(render_template('denied.html'))
return self.add_security_headers(r)
# If download is allowed to continue, serve download page
if self.slug:
r = make_response(render_template(
'send.html',
slug=self.slug,
file_info=self.file_info,
filename=os.path.basename(self.zip_filename),
filesize=self.zip_filesize,
filesize_human=self.common.human_readable_filesize(self.zip_filesize)))
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.zip_filename),
filesize=self.zip_filesize,
filesize_human=self.common.human_readable_filesize(self.zip_filesize)))
return self.add_security_headers(r)
@self.app.route("/<slug_candidate>/download")
def download(slug_candidate):
self.check_slug_candidate(slug_candidate)
return download_logic()
@self.app.route("/download")
def download_public():
if not self.common.settings.get('public_mode'):
return self.error404()
return download_logic()
def download_logic(slug_candidate=''):
"""
Download the zip file.
"""
# Deny new downloads if "Stop After First Download" is checked and there is
# currently a download
deny_download = not self.stay_open and self.download_in_progress
if deny_download:
r = make_response(render_template('denied.html'))
return self.add_security_headers(r)
# Each download has a unique id
download_id = self.download_count
self.download_count += 1
# Prepare some variables to use inside generate() function below
# which is outside of the request context
shutdown_func = request.environ.get('werkzeug.server.shutdown')
path = request.path
# Tell GUI the download started
self.add_request(Web.REQUEST_STARTED, path, {
'id': download_id}
)
dirname = os.path.dirname(self.zip_filename)
basename = os.path.basename(self.zip_filename)
def generate():
# The user hasn't canceled the download
self.client_cancel = False
# Starting a new download
if not self.stay_open:
self.download_in_progress = True
chunk_size = 102400 # 100kb
fp = open(self.zip_filename, 'rb')
self.done = False
canceled = False
while not self.done:
# The user has canceled the download, so stop serving the file
if self.client_cancel:
self.add_request(Web.REQUEST_CANCELED, path, {
'id': download_id
})
break
chunk = fp.read(chunk_size)
if chunk == b'':
self.done = True
else:
try:
yield chunk
# tell GUI the progress
downloaded_bytes = fp.tell()
percent = (1.0 * downloaded_bytes / self.zip_filesize) * 100
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD':
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
sys.stdout.flush()
self.add_request(Web.REQUEST_PROGRESS, path, {
'id': download_id,
'bytes': downloaded_bytes
})
self.done = False
except:
# looks like the download was canceled
self.done = True
canceled = True
# tell the GUI the download has canceled
self.add_request(Web.REQUEST_CANCELED, path, {
'id': download_id
})
fp.close()
if self.common.platform != 'Darwin':
sys.stdout.write("\n")
# Download is finished
if not self.stay_open:
self.download_in_progress = False
# Close the server, if necessary
if not self.stay_open and not canceled:
print(strings._("closing_automatically"))
self.running = False
try:
if shutdown_func is None:
raise RuntimeError('Not running with the Werkzeug Server')
shutdown_func()
except:
pass
r = Response(generate())
r.headers.set('Content-Length', self.zip_filesize)
r.headers.set('Content-Disposition', 'attachment', filename=basename)
r = self.add_security_headers(r)
# guess content type
(content_type, _) = mimetypes.guess_type(basename, strict=False)
if content_type is not None:
r.headers.set('Content-Type', content_type)
return r
def receive_routes(self):
"""
The web app routes for receiving files
"""
def index_logic():
self.add_request(Web.REQUEST_LOAD, request.path)
if self.common.settings.get('public_mode'):
upload_action = '/upload'
close_action = '/close'
else:
upload_action = '/{}/upload'.format(self.slug)
close_action = '/{}/close'.format(self.slug)
r = make_response(render_template(
'receive.html',
upload_action=upload_action,
close_action=close_action,
receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown')))
return self.add_security_headers(r)
@self.app.route("/<slug_candidate>")
def index(slug_candidate):
self.check_slug_candidate(slug_candidate)
return index_logic()
@self.app.route("/")
def index_public():
if not self.common.settings.get('public_mode'):
return self.error404()
return index_logic()
def upload_logic(slug_candidate=''):
"""
Upload files.
"""
# Make sure downloads_dir exists
valid = True
try:
self.common.validate_downloads_dir()
except DownloadsDirErrorCannotCreate:
self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path)
print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir')))
valid = False
except DownloadsDirErrorNotWritable:
self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path)
print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir')))
valid = False
if not valid:
flash('Error uploading, please inform the OnionShare user', 'error')
if self.common.settings.get('public_mode'):
return redirect('/')
else:
return redirect('/{}'.format(slug_candidate))
files = request.files.getlist('file[]')
filenames = []
print('')
for f in files:
if f.filename != '':
# Automatically rename the file, if a file of the same name already exists
filename = secure_filename(f.filename)
filenames.append(filename)
local_path = os.path.join(self.common.settings.get('downloads_dir'), filename)
if os.path.exists(local_path):
if '.' in filename:
# Add "-i", e.g. change "foo.txt" to "foo-2.txt"
parts = filename.split('.')
name = parts[:-1]
ext = parts[-1]
i = 2
valid = False
while not valid:
new_filename = '{}-{}.{}'.format('.'.join(name), i, ext)
local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
if os.path.exists(local_path):
i += 1
else:
valid = True
else:
# If no extension, just add "-i", e.g. change "foo" to "foo-2"
i = 2
valid = False
while not valid:
new_filename = '{}-{}'.format(filename, i)
local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
if os.path.exists(local_path):
i += 1
else:
valid = True
basename = os.path.basename(local_path)
if f.filename != basename:
# Tell the GUI that the file has changed names
self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, {
'id': request.upload_id,
'old_filename': f.filename,
'new_filename': basename
})
self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
print(strings._('receive_mode_received_file').format(local_path))
f.save(local_path)
# Note that flash strings are on English, and not translated, on purpose,
# to avoid leaking the locale of the OnionShare user
if len(filenames) == 0:
flash('No files uploaded', 'info')
else:
for filename in filenames:
flash('Sent {}'.format(filename), 'info')
if self.common.settings.get('public_mode'):
return redirect('/')
else:
return redirect('/{}'.format(slug_candidate))
@self.app.route("/<slug_candidate>/upload", methods=['POST'])
def upload(slug_candidate):
self.check_slug_candidate(slug_candidate)
return upload_logic(slug_candidate)
@self.app.route("/upload", methods=['POST'])
def upload_public():
if not self.common.settings.get('public_mode'):
return self.error404()
return upload_logic()
def close_logic(slug_candidate=''):
if self.common.settings.get('receive_allow_receiver_shutdown'):
self.force_shutdown()
r = make_response(render_template('closed.html'))
self.add_request(Web.REQUEST_CLOSE_SERVER, request.path)
return self.add_security_headers(r)
else:
return redirect('/{}'.format(slug_candidate))
@self.app.route("/<slug_candidate>/close", methods=['POST'])
def close(slug_candidate):
self.check_slug_candidate(slug_candidate)
return close_logic(slug_candidate)
@self.app.route("/close", methods=['POST'])
def close_public():
if not self.common.settings.get('public_mode'):
return self.error404()
return close_logic()
def common_routes(self):
"""
Common web app routes between sending and receiving
"""
@self.app.errorhandler(404)
def page_not_found(e):
"""
404 error page.
"""
return self.error404()
@self.app.route("/<slug_candidate>/shutdown")
def shutdown(slug_candidate):
"""
Stop the flask web server, from the context of an http request.
"""
self.check_shutdown_slug_candidate(slug_candidate)
self.force_shutdown()
return ""
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(strings._('error_rate_limit'))
r = make_response(render_template('404.html'), 404)
return self.add_security_headers(r)
def add_security_headers(self, r):
"""
Add security headers to a request
"""
for header, value in self.security_headers:
r.headers.set(header, value)
return r
def set_file_info(self, filenames, processed_size_callback=None):
"""
Using the list of filenames being shared, fill in details that the web
page will need to display. This includes zipping up the file in order to
get the zip file's name and size.
"""
# build file info list
self.file_info = {'files': [], 'dirs': []}
for filename in filenames:
info = {
'filename': filename,
'basename': os.path.basename(filename.rstrip('/'))
}
if os.path.isfile(filename):
info['size'] = os.path.getsize(filename)
info['size_human'] = self.common.human_readable_filesize(info['size'])
self.file_info['files'].append(info)
if os.path.isdir(filename):
info['size'] = self.common.dir_size(filename)
info['size_human'] = self.common.human_readable_filesize(info['size'])
self.file_info['dirs'].append(info)
self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename'])
self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename'])
# zip up the files and folders
z = ZipWriter(self.common, processed_size_callback=processed_size_callback)
for info in self.file_info['files']:
z.add_file(info['filename'])
for info in self.file_info['dirs']:
z.add_dir(info['filename'])
z.close()
self.zip_filename = z.zip_filename
self.zip_filesize = os.path.getsize(self.zip_filename)
def _safe_select_jinja_autoescape(self, filename):
if filename is None:
return True
return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
def add_request(self, request_type, path, data=None):
"""
Add a request to the queue, to communicate with the GUI.
"""
self.q.put({
'type': request_type,
'path': path,
'data': data
})
def generate_slug(self, persistent_slug=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))
else:
self.slug = self.common.build_slug()
self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug))
def debug_mode(self):
"""
Turn on debugging mode, which will log flask errors to a debug file.
"""
temp_dir = tempfile.gettempdir()
log_handler = logging.FileHandler(
os.path.join(temp_dir, 'onionshare_server.log'))
log_handler.setLevel(logging.WARNING)
self.app.logger.addHandler(log_handler)
def check_slug_candidate(self, slug_candidate):
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 force_shutdown(self):
"""
Stop the flask web server, from the context of the flask app.
"""
# Shutdown the flask service
try:
func = request.environ.get('werkzeug.server.shutdown')
if func is None:
raise RuntimeError('Not running with the Werkzeug Server')
func()
except:
pass
self.running = False
def start(self, port, stay_open=False, public_mode=False, persistent_slug=None):
"""
Start the flask web server.
"""
self.common.log('Web', 'start', 'port={}, stay_open={}, persistent_slug={}'.format(port, stay_open, persistent_slug))
if not public_mode:
self.generate_slug(persistent_slug)
self.stay_open = stay_open
# In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
host = '0.0.0.0'
else:
host = '127.0.0.1'
self.running = True
self.app.run(host=host, port=port, threaded=True)
def stop(self, port):
"""
Stop the flask web server by loading /shutdown.
"""
# If the user cancels the download, let the download function know to stop
# serving the file
self.client_cancel = True
# To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
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
class ZipWriter(object):
"""
ZipWriter accepts files and directories and compresses them into a zip file
with. If a zip_filename is not passed in, it will use the default onionshare
filename.
"""
def __init__(self, common, zip_filename=None, processed_size_callback=None):
self.common = common
if zip_filename:
self.zip_filename = zip_filename
else:
self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6))
self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True)
self.processed_size_callback = processed_size_callback
if self.processed_size_callback is None:
self.processed_size_callback = lambda _: None
self._size = 0
self.processed_size_callback(self._size)
def add_file(self, filename):
"""
Add a file to the zip archive.
"""
self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
self._size += os.path.getsize(filename)
self.processed_size_callback(self._size)
def add_dir(self, filename):
"""
Add a directory, and all of its children, to the zip archive.
"""
dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/'
for dirpath, dirnames, filenames in os.walk(filename):
for f in filenames:
full_filename = os.path.join(dirpath, f)
if not os.path.islink(full_filename):
arc_filename = full_filename[len(dir_to_strip):]
self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
self._size += os.path.getsize(full_filename)
self.processed_size_callback(self._size)
def close(self):
"""
Close the zip archive.
"""
self.z.close()
class ReceiveModeWSGIMiddleware(object):
"""
Custom WSGI middleware in order to attach the Web object to environ, so
ReceiveModeRequest can access it.
"""
def __init__(self, app, web):
self.app = app
self.web = web
def __call__(self, environ, start_response):
environ['web'] = self.web
return self.app(environ, start_response)
class ReceiveModeTemporaryFile(object):
"""
A custom TemporaryFile that tells ReceiveModeRequest every time data gets
written to it, in order to track the progress of uploads.
"""
def __init__(self, filename, write_func, close_func):
self.onionshare_filename = filename
self.onionshare_write_func = write_func
self.onionshare_close_func = close_func
# Create a temporary file
self.f = tempfile.TemporaryFile('wb+')
# Make all the file-like methods and attributes actually access the
# TemporaryFile, except for write
attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode',
'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto',
'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell',
'truncate', 'writable', 'writelines']
for attr in attrs:
setattr(self, attr, getattr(self.f, attr))
def write(self, b):
"""
Custom write method that calls out to onionshare_write_func
"""
bytes_written = self.f.write(b)
self.onionshare_write_func(self.onionshare_filename, bytes_written)
def close(self):
"""
Custom close method that calls out to onionshare_close_func
"""
self.f.close()
self.onionshare_close_func(self.onionshare_filename)
class ReceiveModeRequest(Request):
"""
A custom flask Request object that keeps track of how much data has been
uploaded for each file, for receive mode.
"""
def __init__(self, environ, populate_request=True, shallow=False):
super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
self.web = environ['web']
# Is this a valid upload request?
self.upload_request = False
if self.method == 'POST':
if self.path == '/{}/upload'.format(self.web.slug):
self.upload_request = True
else:
if self.web.common.settings.get('public_mode'):
if self.path == '/upload':
self.upload_request = True
if self.upload_request:
# A dictionary that maps filenames to the bytes uploaded so far
self.progress = {}
# Create an upload_id, attach it to the request
self.upload_id = self.web.upload_count
self.web.upload_count += 1
# Figure out the content length
try:
self.content_length = int(self.headers['Content-Length'])
except:
self.content_length = 0
print("{}: {}".format(
datetime.now().strftime("%b %d, %I:%M%p"),
strings._("receive_mode_upload_starting").format(self.web.common.human_readable_filesize(self.content_length))
))
# Tell the GUI
self.web.add_request(Web.REQUEST_STARTED, self.path, {
'id': self.upload_id,
'content_length': self.content_length
})
self.previous_file = None
def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None):
"""
This gets called for each file that gets uploaded, and returns an file-like
writable stream.
"""
if self.upload_request:
self.progress[filename] = {
'uploaded_bytes': 0,
'complete': False
}
return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func)
def close(self):
"""
Closing the request.
"""
super(ReceiveModeRequest, self).close()
if self.upload_request:
# Inform the GUI that the upload has finished
self.web.add_request(Web.REQUEST_UPLOAD_FINISHED, self.path, {
'id': self.upload_id
})
def file_write_func(self, filename, length):
"""
This function gets called when a specific file is written to.
"""
if self.upload_request:
self.progress[filename]['uploaded_bytes'] += length
if self.previous_file != filename:
if self.previous_file is not None:
print('')
self.previous_file = filename
print('\r=> {:15s} {}'.format(
self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']),
filename
), end='')
# Update the GUI on the upload progress
self.web.add_request(Web.REQUEST_PROGRESS, self.path, {
'id': self.upload_id,
'progress': self.progress
})
def file_close_func(self, filename):
"""
This function gets called when a specific file is closed.
"""
self.progress[filename]['complete'] = True

View File

@ -0,0 +1,21 @@
# -*- 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/>.
"""
from .web import Web

View File

@ -0,0 +1,325 @@
import os
import tempfile
from datetime import datetime
from flask import Request, request, render_template, make_response, flash, redirect
from werkzeug.utils import secure_filename
from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable
from .. import strings
class ReceiveModeWeb(object):
"""
All of the web logic for receive mode
"""
def __init__(self, common, web):
self.common = common
self.common.log('ReceiveModeWeb', '__init__')
self.web = web
self.upload_count = 0
self.define_routes()
def define_routes(self):
"""
The web app routes for receiving files
"""
def index_logic():
self.web.add_request(self.web.REQUEST_LOAD, request.path)
if self.common.settings.get('public_mode'):
upload_action = '/upload'
close_action = '/close'
else:
upload_action = '/{}/upload'.format(self.web.slug)
close_action = '/{}/close'.format(self.web.slug)
r = make_response(render_template(
'receive.html',
upload_action=upload_action,
close_action=close_action,
receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown')))
return self.web.add_security_headers(r)
@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 upload_logic(slug_candidate=''):
"""
Upload files.
"""
# Make sure downloads_dir exists
valid = True
try:
self.common.validate_downloads_dir()
except DownloadsDirErrorCannotCreate:
self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path)
print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir')))
valid = False
except DownloadsDirErrorNotWritable:
self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path)
print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir')))
valid = False
if not valid:
flash('Error uploading, please inform the OnionShare user', 'error')
if self.common.settings.get('public_mode'):
return redirect('/')
else:
return redirect('/{}'.format(slug_candidate))
files = request.files.getlist('file[]')
filenames = []
print('')
for f in files:
if f.filename != '':
# Automatically rename the file, if a file of the same name already exists
filename = secure_filename(f.filename)
filenames.append(filename)
local_path = os.path.join(self.common.settings.get('downloads_dir'), filename)
if os.path.exists(local_path):
if '.' in filename:
# Add "-i", e.g. change "foo.txt" to "foo-2.txt"
parts = filename.split('.')
name = parts[:-1]
ext = parts[-1]
i = 2
valid = False
while not valid:
new_filename = '{}-{}.{}'.format('.'.join(name), i, ext)
local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
if os.path.exists(local_path):
i += 1
else:
valid = True
else:
# If no extension, just add "-i", e.g. change "foo" to "foo-2"
i = 2
valid = False
while not valid:
new_filename = '{}-{}'.format(filename, i)
local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename)
if os.path.exists(local_path):
i += 1
else:
valid = True
basename = os.path.basename(local_path)
if f.filename != basename:
# Tell the GUI that the file has changed names
self.web.add_request(self.web.REQUEST_UPLOAD_FILE_RENAMED, request.path, {
'id': request.upload_id,
'old_filename': f.filename,
'new_filename': basename
})
self.common.log('ReceiveModeWeb', 'define_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
print(strings._('receive_mode_received_file').format(local_path))
f.save(local_path)
# Note that flash strings are on English, and not translated, on purpose,
# to avoid leaking the locale of the OnionShare user
if len(filenames) == 0:
flash('No files uploaded', 'info')
else:
for filename in filenames:
flash('Sent {}'.format(filename), 'info')
if self.common.settings.get('public_mode'):
return redirect('/')
else:
return redirect('/{}'.format(slug_candidate))
@self.web.app.route("/<slug_candidate>/upload", methods=['POST'])
def upload(slug_candidate):
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.common.settings.get('public_mode'):
return self.web.error404()
return upload_logic()
def close_logic(slug_candidate=''):
if self.common.settings.get('receive_allow_receiver_shutdown'):
self.web.force_shutdown()
r = make_response(render_template('closed.html'))
self.web.add_request(self.web.REQUEST_CLOSE_SERVER, request.path)
return self.web.add_security_headers(r)
else:
return redirect('/{}'.format(slug_candidate))
@self.web.app.route("/<slug_candidate>/close", methods=['POST'])
def close(slug_candidate):
self.web.check_slug_candidate(slug_candidate)
return close_logic(slug_candidate)
@self.web.app.route("/close", methods=['POST'])
def close_public():
if not self.common.settings.get('public_mode'):
return self.web.error404()
return close_logic()
class ReceiveModeWSGIMiddleware(object):
"""
Custom WSGI middleware in order to attach the Web object to environ, so
ReceiveModeRequest can access it.
"""
def __init__(self, app, web):
self.app = app
self.web = web
def __call__(self, environ, start_response):
environ['web'] = self.web
return self.app(environ, start_response)
class ReceiveModeTemporaryFile(object):
"""
A custom TemporaryFile that tells ReceiveModeRequest every time data gets
written to it, in order to track the progress of uploads.
"""
def __init__(self, filename, write_func, close_func):
self.onionshare_filename = filename
self.onionshare_write_func = write_func
self.onionshare_close_func = close_func
# Create a temporary file
self.f = tempfile.TemporaryFile('wb+')
# Make all the file-like methods and attributes actually access the
# TemporaryFile, except for write
attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode',
'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto',
'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell',
'truncate', 'writable', 'writelines']
for attr in attrs:
setattr(self, attr, getattr(self.f, attr))
def write(self, b):
"""
Custom write method that calls out to onionshare_write_func
"""
bytes_written = self.f.write(b)
self.onionshare_write_func(self.onionshare_filename, bytes_written)
def close(self):
"""
Custom close method that calls out to onionshare_close_func
"""
self.f.close()
self.onionshare_close_func(self.onionshare_filename)
class ReceiveModeRequest(Request):
"""
A custom flask Request object that keeps track of how much data has been
uploaded for each file, for receive mode.
"""
def __init__(self, environ, populate_request=True, shallow=False):
super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
self.web = environ['web']
# Is this a valid upload request?
self.upload_request = False
if self.method == 'POST':
if self.path == '/{}/upload'.format(self.web.slug):
self.upload_request = True
else:
if self.web.common.settings.get('public_mode'):
if self.path == '/upload':
self.upload_request = True
if self.upload_request:
# A dictionary that maps filenames to the bytes uploaded so far
self.progress = {}
# Create an upload_id, attach it to the request
self.upload_id = self.web.receive_mode.upload_count
self.web.receive_mode.upload_count += 1
# Figure out the content length
try:
self.content_length = int(self.headers['Content-Length'])
except:
self.content_length = 0
print("{}: {}".format(
datetime.now().strftime("%b %d, %I:%M%p"),
strings._("receive_mode_upload_starting").format(self.web.common.human_readable_filesize(self.content_length))
))
# Tell the GUI
self.web.add_request(self.web.REQUEST_STARTED, self.path, {
'id': self.upload_id,
'content_length': self.content_length
})
self.previous_file = None
def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None):
"""
This gets called for each file that gets uploaded, and returns an file-like
writable stream.
"""
if self.upload_request:
self.progress[filename] = {
'uploaded_bytes': 0,
'complete': False
}
return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func)
def close(self):
"""
Closing the request.
"""
super(ReceiveModeRequest, self).close()
if self.upload_request:
# Inform the GUI that the upload has finished
self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, {
'id': self.upload_id
})
def file_write_func(self, filename, length):
"""
This function gets called when a specific file is written to.
"""
if self.upload_request:
self.progress[filename]['uploaded_bytes'] += length
if self.previous_file != filename:
if self.previous_file is not None:
print('')
self.previous_file = filename
print('\r=> {:15s} {}'.format(
self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']),
filename
), end='')
# Update the GUI on the upload progress
self.web.add_request(self.web.REQUEST_PROGRESS, self.path, {
'id': self.upload_id,
'progress': self.progress
})
def file_close_func(self, filename):
"""
This function gets called when a specific file is closed.
"""
self.progress[filename]['complete'] = True

View File

@ -0,0 +1,384 @@
import os
import sys
import tempfile
import zipfile
import mimetypes
import gzip
from flask import Response, request, render_template, make_response
from .. import strings
class ShareModeWeb(object):
"""
All of the web logic for share mode
"""
def __init__(self, common, web):
self.common = common
self.common.log('ShareModeWeb', '__init__')
self.web = web
# Information about the file to be shared
self.file_info = []
self.is_zipped = False
self.download_filename = None
self.download_filesize = None
self.gzip_filename = None
self.gzip_filesize = None
self.zip_writer = None
self.download_count = 0
# If "Stop After First Download" is checked (stay_open == False), only allow
# one download at a time.
self.download_in_progress = False
# If the client closes the OnionShare window while a download is in progress,
# it should immediately stop serving the file. The client_cancel global is
# used to tell the download function that the client is canceling the download.
self.client_cancel = False
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=''):
"""
Render the template for the onionshare landing page.
"""
self.web.add_request(self.web.REQUEST_LOAD, request.path)
# Deny new downloads if "Stop After First Download" is checked and there is
# 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'))
return self.web.add_security_headers(r)
# If download is allowed to continue, serve download page
if self.should_use_gzip():
self.filesize = self.gzip_filesize
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))
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=''):
"""
Download the zip file.
"""
# Deny new downloads if "Stop After First Download" is checked and there is
# 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'))
return self.web.add_security_headers(r)
# Each download has a unique id
download_id = self.download_count
self.download_count += 1
# Prepare some variables to use inside generate() function below
# which is outside of the request context
shutdown_func = request.environ.get('werkzeug.server.shutdown')
path = request.path
# If this is a zipped file, then serve as-is. If it's not zipped, then,
# if the http client supports gzip compression, gzip the file first
# and serve that
use_gzip = self.should_use_gzip()
if use_gzip:
file_to_download = self.gzip_filename
self.filesize = self.gzip_filesize
else:
file_to_download = self.download_filename
self.filesize = self.download_filesize
# Tell GUI the download started
self.web.add_request(self.web.REQUEST_STARTED, path, {
'id': download_id,
'use_gzip': use_gzip
})
basename = os.path.basename(self.download_filename)
def generate():
# The user hasn't canceled the download
self.client_cancel = False
# Starting a new download
if not self.web.stay_open:
self.download_in_progress = True
chunk_size = 102400 # 100kb
fp = open(file_to_download, 'rb')
self.web.done = False
canceled = False
while not self.web.done:
# The user has canceled the download, so stop serving the file
if self.client_cancel:
self.web.add_request(self.web.REQUEST_CANCELED, path, {
'id': download_id
})
break
chunk = fp.read(chunk_size)
if chunk == b'':
self.web.done = True
else:
try:
yield chunk
# tell GUI the progress
downloaded_bytes = fp.tell()
percent = (1.0 * downloaded_bytes / self.filesize) * 100
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD':
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
sys.stdout.flush()
self.web.add_request(self.web.REQUEST_PROGRESS, path, {
'id': download_id,
'bytes': downloaded_bytes
})
self.web.done = False
except:
# looks like the download was canceled
self.web.done = True
canceled = True
# tell the GUI the download has canceled
self.web.add_request(self.web.REQUEST_CANCELED, path, {
'id': download_id
})
fp.close()
if self.common.platform != 'Darwin':
sys.stdout.write("\n")
# Download is finished
if not self.web.stay_open:
self.download_in_progress = False
# Close the server, if necessary
if not self.web.stay_open and not canceled:
print(strings._("closing_automatically"))
self.web.running = False
try:
if shutdown_func is None:
raise RuntimeError('Not running with the Werkzeug Server')
shutdown_func()
except:
pass
r = Response(generate())
if use_gzip:
r.headers.set('Content-Encoding', 'gzip')
r.headers.set('Content-Length', self.filesize)
r.headers.set('Content-Disposition', 'attachment', filename=basename)
r = self.web.add_security_headers(r)
# guess content type
(content_type, _) = mimetypes.guess_type(basename, strict=False)
if content_type is not None:
r.headers.set('Content-Type', content_type)
return r
def set_file_info(self, filenames, processed_size_callback=None):
"""
Using the list of filenames being shared, fill in details that the web
page will need to display. This includes zipping up the file in order to
get the zip file's name and size.
"""
self.common.log("ShareModeWeb", "set_file_info")
self.web.cancel_compression = False
self.cleanup_filenames = []
# build file info list
self.file_info = {'files': [], 'dirs': []}
for filename in filenames:
info = {
'filename': filename,
'basename': os.path.basename(filename.rstrip('/'))
}
if os.path.isfile(filename):
info['size'] = os.path.getsize(filename)
info['size_human'] = self.common.human_readable_filesize(info['size'])
self.file_info['files'].append(info)
if os.path.isdir(filename):
info['size'] = self.common.dir_size(filename)
info['size_human'] = self.common.human_readable_filesize(info['size'])
self.file_info['dirs'].append(info)
self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename'])
self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename'])
# Check if there's only 1 file and no folders
if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0:
self.download_filename = self.file_info['files'][0]['filename']
self.download_filesize = self.file_info['files'][0]['size']
# Compress the file with gzip now, so we don't have to do it on each request
self.gzip_filename = tempfile.mkstemp('wb+')[1]
self._gzip_compress(self.download_filename, self.gzip_filename, 6, processed_size_callback)
self.gzip_filesize = os.path.getsize(self.gzip_filename)
# Make sure the gzip file gets cleaned up when onionshare stops
self.cleanup_filenames.append(self.gzip_filename)
self.is_zipped = False
else:
# Zip up the files and folders
self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback)
self.download_filename = self.zip_writer.zip_filename
for info in self.file_info['files']:
self.zip_writer.add_file(info['filename'])
# Canceling early?
if self.web.cancel_compression:
self.zip_writer.close()
return False
for info in self.file_info['dirs']:
if not self.zip_writer.add_dir(info['filename']):
return False
self.zip_writer.close()
self.download_filesize = os.path.getsize(self.download_filename)
# Make sure the zip file gets cleaned up when onionshare stops
self.cleanup_filenames.append(self.zip_writer.zip_filename)
self.is_zipped = True
return True
def should_use_gzip(self):
"""
Should we use gzip for this browser?
"""
return (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower())
def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None):
"""
Compress a file with gzip, without loading the whole thing into memory
Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
"""
bytes_processed = 0
blocksize = 1 << 16 # 64kB
with open(input_filename, 'rb') as input_file:
output_file = gzip.open(output_filename, 'wb', level)
while True:
if processed_size_callback is not None:
processed_size_callback(bytes_processed)
block = input_file.read(blocksize)
if len(block) == 0:
break
output_file.write(block)
bytes_processed += blocksize
output_file.close()
class ZipWriter(object):
"""
ZipWriter accepts files and directories and compresses them into a zip file
with. If a zip_filename is not passed in, it will use the default onionshare
filename.
"""
def __init__(self, common, zip_filename=None, processed_size_callback=None):
self.common = common
self.cancel_compression = False
if zip_filename:
self.zip_filename = zip_filename
else:
self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6))
self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True)
self.processed_size_callback = processed_size_callback
if self.processed_size_callback is None:
self.processed_size_callback = lambda _: None
self._size = 0
self.processed_size_callback(self._size)
def add_file(self, filename):
"""
Add a file to the zip archive.
"""
self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
self._size += os.path.getsize(filename)
self.processed_size_callback(self._size)
def add_dir(self, filename):
"""
Add a directory, and all of its children, to the zip archive.
"""
dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/'
for dirpath, dirnames, filenames in os.walk(filename):
for f in filenames:
# Canceling early?
if self.cancel_compression:
return False
full_filename = os.path.join(dirpath, f)
if not os.path.islink(full_filename):
arc_filename = full_filename[len(dir_to_strip):]
self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
self._size += os.path.getsize(full_filename)
self.processed_size_callback(self._size)
return True
def close(self):
"""
Close the zip archive.
"""
self.z.close()

252
onionshare/web/web.py Normal file
View File

@ -0,0 +1,252 @@
import hmac
import logging
import os
import queue
import socket
import sys
import tempfile
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 .. import strings
from .share_mode import ShareModeWeb
from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest
# Stub out flask's show_server_banner function, to avoiding showing warnings that
# are not applicable to OnionShare
def stubbed_show_server_banner(env, debug, app_import_path, eager_loading):
pass
flask.cli.show_server_banner = stubbed_show_server_banner
class Web(object):
"""
The Web object is the OnionShare web server, powered by flask
"""
REQUEST_LOAD = 0
REQUEST_STARTED = 1
REQUEST_PROGRESS = 2
REQUEST_OTHER = 3
REQUEST_CANCELED = 4
REQUEST_RATE_LIMIT = 5
REQUEST_CLOSE_SERVER = 6
REQUEST_UPLOAD_FILE_RENAMED = 7
REQUEST_UPLOAD_FINISHED = 8
REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9
REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10
def __init__(self, common, is_gui, mode='share'):
self.common = common
self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode))
# The flask app
self.app = Flask(__name__,
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)
# Debug mode?
if self.common.debug:
self.debug_mode()
# Are we running in GUI mode?
self.is_gui = is_gui
# Are we using receive mode?
self.mode = mode
if self.mode == 'receive':
# Use custom WSGI middleware, to modify environ
self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self)
# Use a custom Request class to track upload progess
self.app.request_class = ReceiveModeRequest
# Starting in Flask 0.11, render_template_string autoescapes template variables
# by default. To prevent content injection through template variables in
# earlier versions of Flask, we force autoescaping in the Jinja2 template
# engine if we detect a Flask version with insecure default behavior.
if Version(flask_version) < Version('0.11'):
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape
self.security_headers = [
('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'),
('X-Frame-Options', 'DENY'),
('X-Xss-Protection', '1; mode=block'),
('X-Content-Type-Options', 'nosniff'),
('Referrer-Policy', 'no-referrer'),
('Server', 'OnionShare')
]
self.q = queue.Queue()
self.slug = None
self.error404_count = 0
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)
# Keep track if the server is running
self.running = False
# Define the web app routes
self.define_common_routes()
# Create the mode web object, which defines its own routes
self.share_mode = None
self.receive_mode = None
if self.mode == 'receive':
self.receive_mode = ReceiveModeWeb(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
"""
@self.app.errorhandler(404)
def page_not_found(e):
"""
404 error page.
"""
return self.error404()
@self.app.route("/<slug_candidate>/shutdown")
def shutdown(slug_candidate):
"""
Stop the flask web server, from the context of an http request.
"""
self.check_shutdown_slug_candidate(slug_candidate)
self.force_shutdown()
return ""
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(strings._('error_rate_limit'))
r = make_response(render_template('404.html'), 404)
return self.add_security_headers(r)
def add_security_headers(self, r):
"""
Add security headers to a request
"""
for header, value in self.security_headers:
r.headers.set(header, value)
return r
def _safe_select_jinja_autoescape(self, filename):
if filename is None:
return True
return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
def add_request(self, request_type, path, data=None):
"""
Add a request to the queue, to communicate with the GUI.
"""
self.q.put({
'type': request_type,
'path': path,
'data': data
})
def generate_slug(self, persistent_slug=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))
else:
self.slug = self.common.build_slug()
self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug))
def debug_mode(self):
"""
Turn on debugging mode, which will log flask errors to a debug file.
"""
temp_dir = tempfile.gettempdir()
log_handler = logging.FileHandler(
os.path.join(temp_dir, 'onionshare_server.log'))
log_handler.setLevel(logging.WARNING)
self.app.logger.addHandler(log_handler)
def check_slug_candidate(self, slug_candidate):
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 force_shutdown(self):
"""
Stop the flask web server, from the context of the flask app.
"""
# Shutdown the flask service
try:
func = request.environ.get('werkzeug.server.shutdown')
if func is None:
raise RuntimeError('Not running with the Werkzeug Server')
func()
except:
pass
self.running = False
def start(self, port, stay_open=False, public_mode=False, persistent_slug=None):
"""
Start the flask web server.
"""
self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, persistent_slug={}'.format(port, stay_open, public_mode, persistent_slug))
if not public_mode:
self.generate_slug(persistent_slug)
self.stay_open = stay_open
# In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
host = '0.0.0.0'
else:
host = '127.0.0.1'
self.running = True
self.app.run(host=host, port=port, threaded=True)
def stop(self, port):
"""
Stop the flask web server by loading /shutdown.
"""
if self.mode == 'share':
# If the user cancels the download, let the download function know to stop
# serving the file
self.share_mode.client_cancel = True
# To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
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

View File

@ -18,7 +18,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
from __future__ import division from __future__ import division
import os, sys, platform, argparse import os
import sys
import platform
import argparse
import signal
from .widgets import Alert from .widgets import Alert
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
@ -58,6 +62,10 @@ def main():
strings.load_strings(common) strings.load_strings(common)
print(strings._('version_string').format(common.version)) print(strings._('version_string').format(common.version))
# Allow Ctrl-C to smoothly quit the program instead of throwing an exception
# https://stackoverflow.com/questions/42814093/how-to-handle-ctrlc-in-python-app-with-pyqt
signal.signal(signal.SIGINT, signal.SIG_DFL)
# Start the Qt app # Start the Qt app
global qtapp global qtapp
qtapp = Application(common) qtapp = Application(common)

View File

@ -17,16 +17,14 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
import time
import threading
from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings from onionshare import strings
from onionshare.common import ShutdownTimer from onionshare.common import ShutdownTimer
from .server_status import ServerStatus from ..server_status import ServerStatus
from .onion_thread import OnionThread from ..threads import OnionThread
from .widgets import Alert from ..widgets import Alert
class Mode(QtWidgets.QWidget): class Mode(QtWidgets.QWidget):
""" """
@ -39,7 +37,7 @@ class Mode(QtWidgets.QWidget):
starting_server_error = QtCore.pyqtSignal(str) starting_server_error = QtCore.pyqtSignal(str)
set_server_active = QtCore.pyqtSignal(bool) set_server_active = QtCore.pyqtSignal(bool)
def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None): def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False):
super(Mode, self).__init__() super(Mode, self).__init__()
self.common = common self.common = common
self.qtapp = qtapp self.qtapp = qtapp
@ -51,13 +49,18 @@ class Mode(QtWidgets.QWidget):
self.filenames = filenames self.filenames = filenames
self.setMinimumWidth(450)
# The web object gets created in init() # The web object gets created in init()
self.web = None self.web = None
# Local mode is passed from OnionShareGui
self.local_only = local_only
# Threads start out as None
self.onion_thread = None
self.web_thread = None
# Server status # Server status
self.server_status = ServerStatus(self.common, self.qtapp, self.app) self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only)
self.server_status.server_started.connect(self.start_server) self.server_status.server_started.connect(self.start_server)
self.server_status.server_stopped.connect(self.stop_server) self.server_status.server_stopped.connect(self.stop_server)
self.server_status.server_canceled.connect(self.cancel_server) self.server_status.server_canceled.connect(self.cancel_server)
@ -67,16 +70,17 @@ class Mode(QtWidgets.QWidget):
self.starting_server_step3.connect(self.start_server_step3) self.starting_server_step3.connect(self.start_server_step3)
self.starting_server_error.connect(self.start_server_error) self.starting_server_error.connect(self.start_server_error)
# Primary action layout # Primary action
# Note: It's up to the downstream Mode to add this to its layout
self.primary_action_layout = QtWidgets.QVBoxLayout() self.primary_action_layout = QtWidgets.QVBoxLayout()
self.primary_action_layout.addWidget(self.server_status) self.primary_action_layout.addWidget(self.server_status)
self.primary_action = QtWidgets.QWidget() self.primary_action = QtWidgets.QWidget()
self.primary_action.setLayout(self.primary_action_layout) self.primary_action.setLayout(self.primary_action_layout)
# Layout # Hack to allow a minimum width on the main layout
self.layout = QtWidgets.QVBoxLayout() # Note: It's up to the downstream Mode to add this to its layout
self.layout.addWidget(self.primary_action) self.min_width_widget = QtWidgets.QWidget()
self.setLayout(self.layout) self.min_width_widget.setMinimumWidth(600)
def init(self): def init(self):
""" """
@ -138,34 +142,11 @@ class Mode(QtWidgets.QWidget):
self.status_bar.clearMessage() self.status_bar.clearMessage()
self.server_status_label.setText('') self.server_status_label.setText('')
# Start the onion service in a new thread
def start_onion_service(self):
# Choose a port for the web app
self.app.choose_port()
# Start http service in new thread
t = threading.Thread(target=self.web.start, args=(self.app.port, not self.common.settings.get('close_after_first_download'), self.common.settings.get('public_mode'), self.common.settings.get('slug')))
t.daemon = True
t.start()
# Wait for the web app slug to generate before continuing
if not self.common.settings.get('public_mode'):
while self.web.slug == None:
time.sleep(0.1)
# Now start the onion service
try:
self.app.start_onion_service()
self.starting_server_step2.emit()
except Exception as e:
self.starting_server_error.emit(e.args[0])
return
self.common.log('Mode', 'start_server', 'Starting an onion thread') self.common.log('Mode', 'start_server', 'Starting an onion thread')
self.t = OnionThread(self.common, function=start_onion_service, kwargs={'self': self}) self.onion_thread = OnionThread(self)
self.t.daemon = True self.onion_thread.success.connect(self.starting_server_step2.emit)
self.t.start() self.onion_thread.error.connect(self.starting_server_error.emit)
self.onion_thread.start()
def start_server_custom(self): def start_server_custom(self):
""" """
@ -243,10 +224,22 @@ class Mode(QtWidgets.QWidget):
""" """
Cancel the server while it is preparing to start Cancel the server while it is preparing to start
""" """
if self.t: self.cancel_server_custom()
self.t.quit()
if self.onion_thread:
self.common.log('Mode', 'cancel_server: quitting onion thread')
self.onion_thread.quit()
if self.web_thread:
self.common.log('Mode', 'cancel_server: quitting web thread')
self.web_thread.quit()
self.stop_server() self.stop_server()
def cancel_server_custom(self):
"""
Add custom initialization here.
"""
pass
def stop_server(self): def stop_server(self):
""" """
Stop the onionshare server. Stop the onionshare server.

View File

@ -0,0 +1,548 @@
# -*- 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 time
import subprocess
import os
from datetime import datetime
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from ..widgets import Alert
class HistoryItem(QtWidgets.QWidget):
"""
The base history item
"""
def __init__(self):
super(HistoryItem, self).__init__()
def update(self):
pass
def cancel(self):
pass
class DownloadHistoryItem(HistoryItem):
"""
Download history item, for share mode
"""
def __init__(self, common, id, total_bytes):
super(DownloadHistoryItem, self).__init__()
self.common = common
self.id = id
self.total_bytes = total_bytes
self.downloaded_bytes = 0
self.started = time.time()
self.started_dt = datetime.fromtimestamp(self.started)
# Label
self.label = QtWidgets.QLabel(strings._('gui_download_in_progress').format(self.started_dt.strftime("%b %d, %I:%M%p")))
# Progress bar
self.progress_bar = QtWidgets.QProgressBar()
self.progress_bar.setTextVisible(True)
self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(total_bytes)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
self.progress_bar.total_bytes = total_bytes
# Layout
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.progress_bar)
self.setLayout(layout)
# Start at 0
self.update(0)
def update(self, downloaded_bytes):
self.downloaded_bytes = downloaded_bytes
self.progress_bar.setValue(downloaded_bytes)
if downloaded_bytes == self.progress_bar.total_bytes:
pb_fmt = strings._('gui_download_upload_progress_complete').format(
self.common.format_seconds(time.time() - self.started))
else:
elapsed = time.time() - self.started
if elapsed < 10:
# Wait a couple of seconds for the download rate to stabilize.
# This prevents a "Windows copy dialog"-esque experience at
# the beginning of the download.
pb_fmt = strings._('gui_download_upload_progress_starting').format(
self.common.human_readable_filesize(downloaded_bytes))
else:
pb_fmt = strings._('gui_download_upload_progress_eta').format(
self.common.human_readable_filesize(downloaded_bytes),
self.estimated_time_remaining)
self.progress_bar.setFormat(pb_fmt)
def cancel(self):
self.progress_bar.setFormat(strings._('gui_canceled'))
@property
def estimated_time_remaining(self):
return self.common.estimated_time_remaining(self.downloaded_bytes,
self.total_bytes,
self.started)
class UploadHistoryItemFile(QtWidgets.QWidget):
def __init__(self, common, filename):
super(UploadHistoryItemFile, self).__init__()
self.common = common
self.common.log('UploadHistoryItemFile', '__init__', 'filename: {}'.format(filename))
self.filename = filename
self.started = datetime.now()
# Filename label
self.filename_label = QtWidgets.QLabel(self.filename)
self.filename_label_width = self.filename_label.width()
# File size label
self.filesize_label = QtWidgets.QLabel()
self.filesize_label.setStyleSheet(self.common.css['receive_file_size'])
self.filesize_label.hide()
# Folder button
folder_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/open_folder.png')))
folder_icon = QtGui.QIcon(folder_pixmap)
self.folder_button = QtWidgets.QPushButton()
self.folder_button.clicked.connect(self.open_folder)
self.folder_button.setIcon(folder_icon)
self.folder_button.setIconSize(folder_pixmap.rect().size())
self.folder_button.setFlat(True)
self.folder_button.hide()
# Layouts
layout = QtWidgets.QHBoxLayout()
layout.addWidget(self.filename_label)
layout.addWidget(self.filesize_label)
layout.addStretch()
layout.addWidget(self.folder_button)
self.setLayout(layout)
def update(self, uploaded_bytes, complete):
self.filesize_label.setText(self.common.human_readable_filesize(uploaded_bytes))
self.filesize_label.show()
if complete:
self.folder_button.show()
def rename(self, new_filename):
self.filename = new_filename
self.filename_label.setText(self.filename)
def open_folder(self):
"""
Open the downloads folder, with the file selected, in a cross-platform manner
"""
self.common.log('UploadHistoryItemFile', 'open_folder')
abs_filename = os.path.join(self.common.settings.get('downloads_dir'), self.filename)
# Linux
if self.common.platform == 'Linux' or self.common.platform == 'BSD':
try:
# If nautilus is available, open it
subprocess.Popen(['nautilus', abs_filename])
except:
Alert(self.common, strings._('gui_open_folder_error_nautilus').format(abs_filename))
# macOS
elif self.common.platform == 'Darwin':
subprocess.call(['open', '-R', abs_filename])
# Windows
elif self.common.platform == 'Windows':
subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)])
class UploadHistoryItem(HistoryItem):
def __init__(self, common, id, content_length):
super(UploadHistoryItem, self).__init__()
self.common = common
self.id = id
self.content_length = content_length
self.started = datetime.now()
# Label
self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress', True).format(self.started.strftime("%b %d, %I:%M%p")))
# Progress bar
self.progress_bar = QtWidgets.QProgressBar()
self.progress_bar.setTextVisible(True)
self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
self.progress_bar.setMinimum(0)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
# This layout contains file widgets
self.files_layout = QtWidgets.QVBoxLayout()
self.files_layout.setContentsMargins(0, 0, 0, 0)
files_widget = QtWidgets.QWidget()
files_widget.setStyleSheet(self.common.css['receive_file'])
files_widget.setLayout(self.files_layout)
# Layout
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.progress_bar)
layout.addWidget(files_widget)
layout.addStretch()
self.setLayout(layout)
# We're also making a dictionary of file widgets, to make them easier to access
self.files = {}
def update(self, data):
"""
Using the progress from Web, update the progress bar and file size labels
for each file
"""
if data['action'] == 'progress':
total_uploaded_bytes = 0
for filename in data['progress']:
total_uploaded_bytes += data['progress'][filename]['uploaded_bytes']
# Update the progress bar
self.progress_bar.setMaximum(self.content_length)
self.progress_bar.setValue(total_uploaded_bytes)
elapsed = datetime.now() - self.started
if elapsed.seconds < 10:
pb_fmt = strings._('gui_download_upload_progress_starting').format(
self.common.human_readable_filesize(total_uploaded_bytes))
else:
estimated_time_remaining = self.common.estimated_time_remaining(
total_uploaded_bytes,
self.content_length,
self.started.timestamp())
pb_fmt = strings._('gui_download_upload_progress_eta').format(
self.common.human_readable_filesize(total_uploaded_bytes),
estimated_time_remaining)
# Using list(progress) to avoid "RuntimeError: dictionary changed size during iteration"
for filename in list(data['progress']):
# Add a new file if needed
if filename not in self.files:
self.files[filename] = UploadHistoryItemFile(self.common, filename)
self.files_layout.addWidget(self.files[filename])
# Update the file
self.files[filename].update(data['progress'][filename]['uploaded_bytes'], data['progress'][filename]['complete'])
elif data['action'] == 'rename':
self.files[data['old_filename']].rename(data['new_filename'])
self.files[data['new_filename']] = self.files.pop(data['old_filename'])
elif data['action'] == 'finished':
# Hide the progress bar
self.progress_bar.hide()
# Change the label
self.ended = self.started = datetime.now()
if self.started.year == self.ended.year and self.started.month == self.ended.month and self.started.day == self.ended.day:
if self.started.hour == self.ended.hour and self.started.minute == self.ended.minute:
text = strings._('gui_upload_finished', True).format(
self.started.strftime("%b %d, %I:%M%p")
)
else:
text = strings._('gui_upload_finished_range', True).format(
self.started.strftime("%b %d, %I:%M%p"),
self.ended.strftime("%I:%M%p")
)
else:
text = strings._('gui_upload_finished_range', True).format(
self.started.strftime("%b %d, %I:%M%p"),
self.ended.strftime("%b %d, %I:%M%p")
)
self.label.setText(text)
class HistoryItemList(QtWidgets.QScrollArea):
"""
List of items
"""
def __init__(self, common):
super(HistoryItemList, self).__init__()
self.common = common
self.items = {}
# The layout that holds all of the items
self.items_layout = QtWidgets.QVBoxLayout()
self.items_layout.setContentsMargins(0, 0, 0, 0)
self.items_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize)
# Wrapper layout that also contains a stretch
wrapper_layout = QtWidgets.QVBoxLayout()
wrapper_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize)
wrapper_layout.addLayout(self.items_layout)
wrapper_layout.addStretch()
# The internal widget of the scroll area
widget = QtWidgets.QWidget()
widget.setLayout(wrapper_layout)
self.setWidget(widget)
self.setWidgetResizable(True)
# Other scroll area settings
self.setBackgroundRole(QtGui.QPalette.Light)
self.verticalScrollBar().rangeChanged.connect(self.resizeScroll)
def resizeScroll(self, minimum, maximum):
"""
Scroll to the bottom of the window when the range changes.
"""
self.verticalScrollBar().setValue(maximum)
def add(self, id, item):
"""
Add a new item. Override this method.
"""
self.items[id] = item
self.items_layout.addWidget(item)
def update(self, id, data):
"""
Update an item. Override this method.
"""
self.items[id].update(data)
def cancel(self, id):
"""
Cancel an item. Override this method.
"""
self.items[id].cancel()
def reset(self):
"""
Reset all items, emptying the list. Override this method.
"""
for item in self.items.values():
self.items_layout.removeWidget(item)
item.close()
self.items = {}
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):
super(History, self).__init__()
self.common = common
self.setMinimumWidth(350)
# In progress and completed counters
self.in_progress_count = 0
self.completed_count = 0
# In progress and completed labels
self.in_progress_label = QtWidgets.QLabel()
self.in_progress_label.setStyleSheet(self.common.css['mode_info_label'])
self.completed_label = QtWidgets.QLabel()
self.completed_label.setStyleSheet(self.common.css['mode_info_label'])
# Header
self.header_label = QtWidgets.QLabel(header_text)
self.header_label.setStyleSheet(self.common.css['downloads_uploads_label'])
clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True))
clear_button.setStyleSheet(self.common.css['downloads_uploads_clear'])
clear_button.setFlat(True)
clear_button.clicked.connect(self.reset)
header_layout = QtWidgets.QHBoxLayout()
header_layout.addWidget(self.header_label)
header_layout.addStretch()
header_layout.addWidget(self.in_progress_label)
header_layout.addWidget(self.completed_label)
header_layout.addWidget(clear_button)
# When there are no items
self.empty_image = QtWidgets.QLabel()
self.empty_image.setAlignment(QtCore.Qt.AlignCenter)
self.empty_image.setPixmap(empty_image)
self.empty_text = QtWidgets.QLabel(empty_text)
self.empty_text.setAlignment(QtCore.Qt.AlignCenter)
self.empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text'])
empty_layout = QtWidgets.QVBoxLayout()
empty_layout.addStretch()
empty_layout.addWidget(self.empty_image)
empty_layout.addWidget(self.empty_text)
empty_layout.addStretch()
self.empty = QtWidgets.QWidget()
self.empty.setStyleSheet(self.common.css['downloads_uploads_empty'])
self.empty.setLayout(empty_layout)
# When there are items
self.item_list = HistoryItemList(self.common)
self.not_empty_layout = QtWidgets.QVBoxLayout()
self.not_empty_layout.addLayout(header_layout)
self.not_empty_layout.addWidget(self.item_list)
self.not_empty = QtWidgets.QWidget()
self.not_empty.setLayout(self.not_empty_layout)
# Layout
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.empty)
layout.addWidget(self.not_empty)
self.setLayout(layout)
# Reset once at the beginning
self.reset()
def add(self, id, item):
"""
Add a new item.
"""
self.common.log('History', 'add', 'id: {}, item: {}'.format(id, item))
# Hide empty, show not empty
self.empty.hide()
self.not_empty.show()
# Add it to the list
self.item_list.add(id, item)
def update(self, id, data):
"""
Update an item.
"""
self.item_list.update(id, data)
def cancel(self, id):
"""
Cancel an item.
"""
self.item_list.cancel(id)
def reset(self):
"""
Reset all items.
"""
self.item_list.reset()
# Hide not empty, show empty
self.not_empty.hide()
self.empty.show()
# Reset counters
self.completed_count = 0
self.in_progress_count = 0
self.update_completed()
self.update_in_progress()
def update_completed(self):
"""
Update the 'completed' widget.
"""
if self.completed_count == 0:
image = self.common.get_resource_path('images/share_completed_none.png')
else:
image = self.common.get_resource_path('images/share_completed.png')
self.completed_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.completed_count))
self.completed_label.setToolTip(strings._('history_completed_tooltip').format(self.completed_count))
def update_in_progress(self):
"""
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', True).format(self.in_progress_count))
class ToggleHistory(QtWidgets.QPushButton):
"""
Widget for toggling showing or hiding the history, as well as keeping track
of the indicator counter if it's hidden
"""
def __init__(self, common, current_mode, history_widget, icon, selected_icon):
super(ToggleHistory, self).__init__()
self.common = common
self.current_mode = current_mode
self.history_widget = history_widget
self.icon = icon
self.selected_icon = selected_icon
# Toggle button
self.setDefault(False)
self.setFixedWidth(35)
self.setFixedHeight(30)
self.setFlat(True)
self.setIcon(icon)
self.clicked.connect(self.toggle_clicked)
# Keep track of indicator
self.indicator_count = 0
self.indicator_label = QtWidgets.QLabel(parent=self)
self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator'])
self.update_indicator()
def update_indicator(self, increment=False):
"""
Update the display of the indicator count. If increment is True, then
only increment the counter if Downloads is hidden.
"""
if increment and not self.history_widget.isVisible():
self.indicator_count += 1
self.indicator_label.setText("{}".format(self.indicator_count))
if self.indicator_count == 0:
self.indicator_label.hide()
else:
size = self.indicator_label.sizeHint()
self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height())
self.indicator_label.show()
def toggle_clicked(self):
"""
Toggle showing and hiding the history widget
"""
self.common.log('ToggleHistory', 'toggle_clicked')
if self.history_widget.isVisible():
self.history_widget.hide()
self.setIcon(self.icon)
self.setFlat(True)
else:
self.history_widget.show()
self.setIcon(self.selected_icon)
self.setFlat(False)
# Reset the indicator count
self.indicator_count = 0
self.update_indicator()

View File

@ -0,0 +1,196 @@
# -*- 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/>.
"""
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from onionshare.web import Web
from ..history import History, ToggleHistory, UploadHistoryItem
from .. import Mode
class ReceiveMode(Mode):
"""
Parts of the main window UI for receiving files.
"""
def init(self):
"""
Custom initialization for ReceiveMode.
"""
# Create the Web object
self.web = Web(self.common, True, 'receive')
# Server status
self.server_status.set_mode('receive')
self.server_status.server_started_finished.connect(self.update_primary_action)
self.server_status.server_stopped.connect(self.update_primary_action)
self.server_status.server_canceled.connect(self.update_primary_action)
# Tell server_status about web, then update
self.server_status.web = self.web
self.server_status.update()
# Upload history
self.history = History(
self.common,
QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/uploads_transparent.png'))),
strings._('gui_no_uploads'),
strings._('gui_uploads')
)
self.history.hide()
# Toggle history
self.toggle_history = ToggleHistory(
self.common, self, self.history,
QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')),
QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png'))
)
# Receive mode warning
receive_warning = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True))
receive_warning.setMinimumHeight(80)
receive_warning.setWordWrap(True)
# Top bar
top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addStretch()
top_bar_layout.addWidget(self.toggle_history)
# Main layout
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addLayout(top_bar_layout)
self.main_layout.addWidget(receive_warning)
self.main_layout.addWidget(self.primary_action)
self.main_layout.addStretch()
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)
self.setLayout(self.wrapper_layout)
def get_stop_server_shutdown_timeout_text(self):
"""
Return the string to put on the stop server button, if there's a shutdown timeout
"""
return strings._('gui_receive_stop_server_shutdown_timeout', True)
def timeout_finished_should_stop_server(self):
"""
The shutdown timer expired, should we stop the server? Returns a bool
"""
# TODO: wait until the final upload is done before stoppign the server?
return True
def start_server_custom(self):
"""
Starting the server.
"""
# Reset web counters
self.web.receive_mode.upload_count = 0
self.web.error404_count = 0
# Hide and reset the uploads if we have previously shared
self.reset_info_counters()
def start_server_step2_custom(self):
"""
Step 2 in starting the server.
"""
# Continue
self.starting_server_step3.emit()
self.start_server_finished.emit()
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_page_loaded_title', True), strings._('systray_upload_page_loaded_message', True))
def handle_request_started(self, event):
"""
Handle REQUEST_STARTED event.
"""
item = UploadHistoryItem(self.common, event["data"]["id"], event["data"]["content_length"])
self.history.add(event["data"]["id"], item)
self.toggle_history.update_indicator(True)
self.history.in_progress_count += 1
self.history.update_in_progress()
self.system_tray.showMessage(strings._('systray_upload_started_title', True), strings._('systray_upload_started_message', True))
def handle_request_progress(self, event):
"""
Handle REQUEST_PROGRESS event.
"""
self.history.update(event["data"]["id"], {
'action': 'progress',
'progress': event["data"]["progress"]
})
def handle_request_close_server(self, event):
"""
Handle REQUEST_CLOSE_SERVER event.
"""
self.stop_server()
self.system_tray.showMessage(strings._('systray_close_server_title', True), strings._('systray_close_server_message', True))
def handle_request_upload_file_renamed(self, event):
"""
Handle REQUEST_UPLOAD_FILE_RENAMED event.
"""
self.history.update(event["data"]["id"], {
'action': 'rename',
'old_filename': event["data"]["old_filename"],
'new_filename': event["data"]["new_filename"]
})
def handle_request_upload_finished(self, event):
"""
Handle REQUEST_UPLOAD_FINISHED event.
"""
self.history.update(event["data"]["id"], {
'action': 'finished'
})
self.history.completed_count += 1
self.history.in_progress_count -= 1
self.history.update_completed()
self.history.update_in_progress()
def on_reload_settings(self):
"""
We should be ok to re-enable the 'Start Receive Mode' button now.
"""
self.primary_action.show()
def reset_info_counters(self):
"""
Set the info counters back to zero.
"""
self.history.reset()
def update_primary_action(self):
self.common.log('ReceiveMode', 'update_primary_action')

View File

@ -17,7 +17,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
import threading
import os import os
from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5 import QtCore, QtWidgets, QtGui
@ -27,9 +26,11 @@ from onionshare.common import Common
from onionshare.web import Web from onionshare.web import Web
from .file_selection import FileSelection from .file_selection import FileSelection
from .downloads import Downloads from .threads import CompressThread
from ..mode import Mode from .. import Mode
from ..widgets import Alert from ..history import History, ToggleHistory, DownloadHistoryItem
from ...widgets import Alert
class ShareMode(Mode): class ShareMode(Mode):
""" """
@ -39,8 +40,11 @@ class ShareMode(Mode):
""" """
Custom initialization for ReceiveMode. Custom initialization for ReceiveMode.
""" """
# Threads start out as None
self.compress_thread = None
# Create the Web object # Create the Web object
self.web = Web(self.common, True, False) self.web = Web(self.common, True, 'share')
# File selection # File selection
self.file_selection = FileSelection(self.common) self.file_selection = FileSelection(self.common)
@ -67,40 +71,31 @@ class ShareMode(Mode):
self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning']) self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning'])
self.filesize_warning.hide() self.filesize_warning.hide()
# Downloads # Download history
self.downloads = Downloads(self.common) self.history = History(
self.downloads_in_progress = 0 self.common,
self.downloads_completed = 0 QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png'))),
strings._('gui_no_downloads'),
strings._('gui_downloads')
)
self.history.hide()
# Information about share, and show downloads button # Info label
self.info_label = QtWidgets.QLabel() self.info_label = QtWidgets.QLabel()
self.info_label.setStyleSheet(self.common.css['mode_info_label']) self.info_label.hide()
self.info_show_downloads = QtWidgets.QToolButton() # Toggle history
self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) self.toggle_history = ToggleHistory(
self.info_show_downloads.setCheckable(True) self.common, self, self.history,
self.info_show_downloads.toggled.connect(self.downloads_toggled) QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')),
self.info_show_downloads.setToolTip(strings._('gui_downloads_window_tooltip', True)) QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png'))
)
self.info_in_progress_downloads_count = QtWidgets.QLabel() # Top bar
self.info_in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label']) top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addWidget(self.info_label)
self.info_completed_downloads_count = QtWidgets.QLabel() top_bar_layout.addStretch()
self.info_completed_downloads_count.setStyleSheet(self.common.css['mode_info_label']) top_bar_layout.addWidget(self.toggle_history)
self.update_downloads_completed()
self.update_downloads_in_progress()
self.info_layout = QtWidgets.QHBoxLayout()
self.info_layout.addWidget(self.info_label)
self.info_layout.addStretch()
self.info_layout.addWidget(self.info_in_progress_downloads_count)
self.info_layout.addWidget(self.info_completed_downloads_count)
self.info_layout.addWidget(self.info_show_downloads)
self.info_widget = QtWidgets.QWidget()
self.info_widget.setLayout(self.info_layout)
self.info_widget.hide()
# Primary action layout # Primary action layout
self.primary_action_layout.addWidget(self.filesize_warning) self.primary_action_layout.addWidget(self.filesize_warning)
@ -110,9 +105,18 @@ class ShareMode(Mode):
# Status bar, zip progress bar # Status bar, zip progress bar
self._zip_progress_bar = None self._zip_progress_bar = None
# Layout # Main layout
self.layout.insertLayout(0, self.file_selection) self.main_layout = QtWidgets.QVBoxLayout()
self.layout.insertWidget(0, self.info_widget) 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)
self.setLayout(self.wrapper_layout)
# Always start with focus on file selection # Always start with focus on file selection
self.file_selection.setFocus() self.file_selection.setFocus()
@ -128,7 +132,7 @@ class ShareMode(Mode):
The shutdown timer expired, should we stop the server? Returns a bool The shutdown timer expired, should we stop the server? Returns a bool
""" """
# If there were no attempts to download the share, or all downloads are done, we can stop # If there were no attempts to download the share, or all downloads are done, we can stop
if self.web.download_count == 0 or self.web.done: if self.web.share_mode.download_count == 0 or self.web.done:
self.server_status.stop_server() self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_timeout', True)) self.server_status_label.setText(strings._('close_on_timeout', True))
return True return True
@ -142,7 +146,7 @@ class ShareMode(Mode):
Starting the server. Starting the server.
""" """
# Reset web counters # Reset web counters
self.web.download_count = 0 self.web.share_mode.download_count = 0
self.web.error404_count = 0 self.web.error404_count = 0
# Hide and reset the downloads if we have previously shared # Hide and reset the downloads if we have previously shared
@ -161,28 +165,13 @@ class ShareMode(Mode):
self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(self.filenames) self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(self.filenames)
self.status_bar.insertWidget(0, self._zip_progress_bar) self.status_bar.insertWidget(0, self._zip_progress_bar)
# Prepare the files for sending in a new thread # prepare the files for sending in a new thread
def finish_starting_server(self): self.compress_thread = CompressThread(self)
# Prepare files to share self.compress_thread.success.connect(self.starting_server_step3.emit)
def _set_processed_size(x): self.compress_thread.success.connect(self.start_server_finished.emit)
if self._zip_progress_bar != None: self.compress_thread.error.connect(self.starting_server_error.emit)
self._zip_progress_bar.update_processed_size_signal.emit(x) self.server_status.server_canceled.connect(self.compress_thread.cancel)
self.compress_thread.start()
try:
self.web.set_file_info(self.filenames, processed_size_callback=_set_processed_size)
self.app.cleanup_filenames.append(self.web.zip_filename)
# Only continue if the server hasn't been canceled
if self.server_status.status != self.server_status.STATUS_STOPPED:
self.starting_server_step3.emit()
self.start_server_finished.emit()
except OSError as e:
self.starting_server_error.emit(e.strerror)
return
t = threading.Thread(target=finish_starting_server, kwargs={'self': self})
t.daemon = True
t.start()
def start_server_step3_custom(self): def start_server_step3_custom(self):
""" """
@ -195,7 +184,7 @@ class ShareMode(Mode):
self._zip_progress_bar = None self._zip_progress_bar = None
# Warn about sending large files over Tor # Warn about sending large files over Tor
if self.web.zip_filesize >= 157286400: # 150mb if self.web.share_mode.download_filesize >= 157286400: # 150mb
self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.setText(strings._("large_filesize", True))
self.filesize_warning.show() self.filesize_warning.show()
@ -217,17 +206,24 @@ class ShareMode(Mode):
self._zip_progress_bar = None self._zip_progress_bar = None
self.filesize_warning.hide() self.filesize_warning.hide()
self.downloads_in_progress = 0 self.history.in_progress_count = 0
self.downloads_completed = 0 self.history.completed_count = 0
self.update_downloads_in_progress() self.history.update_in_progress()
self.file_selection.file_list.adjustSize() self.file_selection.file_list.adjustSize()
def cancel_server_custom(self):
"""
Stop the compression thread on cancel
"""
if self.compress_thread:
self.common.log('ShareMode', 'cancel_server: quitting compress thread')
self.compress_thread.quit()
def handle_tor_broke_custom(self): def handle_tor_broke_custom(self):
""" """
Connection to Tor broke. Connection to Tor broke.
""" """
self.primary_action.hide() self.primary_action.hide()
self.info_widget.hide()
def handle_request_load(self, event): def handle_request_load(self, event):
""" """
@ -239,9 +235,16 @@ class ShareMode(Mode):
""" """
Handle REQUEST_STARTED event. Handle REQUEST_STARTED event.
""" """
self.downloads.add(event["data"]["id"], self.web.zip_filesize) if event["data"]["use_gzip"]:
self.downloads_in_progress += 1 filesize = self.web.share_mode.gzip_filesize
self.update_downloads_in_progress() else:
filesize = self.web.share_mode.download_filesize
item = DownloadHistoryItem(self.common, event["data"]["id"], filesize)
self.history.add(event["data"]["id"], item)
self.toggle_history.update_indicator(True)
self.history.in_progress_count += 1
self.history.update_in_progress()
self.system_tray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True)) self.system_tray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True))
@ -249,18 +252,17 @@ class ShareMode(Mode):
""" """
Handle REQUEST_PROGRESS event. Handle REQUEST_PROGRESS event.
""" """
self.downloads.update(event["data"]["id"], event["data"]["bytes"]) self.history.update(event["data"]["id"], event["data"]["bytes"])
# Is the download complete? # Is the download complete?
if event["data"]["bytes"] == self.web.zip_filesize: if event["data"]["bytes"] == self.web.share_mode.filesize:
self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True))
# Update the total 'completed downloads' info # Update completed and in progress labels
self.downloads_completed += 1 self.history.completed_count += 1
self.update_downloads_completed() self.history.in_progress_count -= 1
# Update the 'in progress downloads' info self.history.update_completed()
self.downloads_in_progress -= 1 self.history.update_in_progress()
self.update_downloads_in_progress()
# Close on finish? # Close on finish?
if self.common.settings.get('close_after_first_download'): if self.common.settings.get('close_after_first_download'):
@ -269,19 +271,19 @@ class ShareMode(Mode):
self.server_status_label.setText(strings._('closing_automatically', True)) self.server_status_label.setText(strings._('closing_automatically', True))
else: else:
if self.server_status.status == self.server_status.STATUS_STOPPED: if self.server_status.status == self.server_status.STATUS_STOPPED:
self.downloads.cancel(event["data"]["id"]) self.history.cancel(event["data"]["id"])
self.downloads_in_progress = 0 self.history.in_progress_count = 0
self.update_downloads_in_progress() self.history.update_in_progress()
def handle_request_canceled(self, event): def handle_request_canceled(self, event):
""" """
Handle REQUEST_CANCELED event. Handle REQUEST_CANCELED event.
""" """
self.downloads.cancel(event["data"]["id"]) self.history.cancel(event["data"]["id"])
# Update the 'in progress downloads' info # Update in progress count
self.downloads_in_progress -= 1 self.history.in_progress_count -= 1
self.update_downloads_in_progress() self.history.update_in_progress()
self.system_tray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True)) self.system_tray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True))
def on_reload_settings(self): def on_reload_settings(self):
@ -291,14 +293,16 @@ class ShareMode(Mode):
""" """
if self.server_status.file_selection.get_num_files() > 0: if self.server_status.file_selection.get_num_files() > 0:
self.primary_action.show() self.primary_action.show()
self.info_widget.show() self.info_label.show()
def update_primary_action(self): def update_primary_action(self):
self.common.log('ShareMode', 'update_primary_action')
# Show or hide primary action layout # Show or hide primary action layout
file_count = self.file_selection.file_list.count() file_count = self.file_selection.file_list.count()
if file_count > 0: if file_count > 0:
self.primary_action.show() self.primary_action.show()
self.info_widget.show() self.info_label.show()
# Update the file count in the info label # Update the file count in the info label
total_size_bytes = 0 total_size_bytes = 0
@ -314,54 +318,13 @@ class ShareMode(Mode):
else: else:
self.primary_action.hide() self.primary_action.hide()
self.info_widget.hide() self.info_label.hide()
# Resize window
self.adjustSize()
def downloads_toggled(self, checked):
"""
When the 'Show/hide downloads' button is toggled, show or hide the downloads window.
"""
self.common.log('ShareMode', 'toggle_downloads')
if checked:
self.downloads.show()
else:
self.downloads.hide()
def reset_info_counters(self): def reset_info_counters(self):
""" """
Set the info counters back to zero. Set the info counters back to zero.
""" """
self.downloads_completed = 0 self.history.reset()
self.downloads_in_progress = 0
self.update_downloads_completed()
self.update_downloads_in_progress()
self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png')))
self.downloads.reset()
def update_downloads_completed(self):
"""
Update the 'Downloads completed' info widget.
"""
if self.downloads_completed == 0:
image = self.common.get_resource_path('images/share_completed_none.png')
else:
image = self.common.get_resource_path('images/share_completed.png')
self.info_completed_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.downloads_completed))
self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(self.downloads_completed))
def update_downloads_in_progress(self):
"""
Update the 'Downloads in progress' info widget.
"""
if self.downloads_in_progress == 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.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_green.png')))
self.info_in_progress_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.downloads_in_progress))
self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.downloads_in_progress))
@staticmethod @staticmethod
def _compute_total_size(filenames): def _compute_total_size(filenames):
@ -410,6 +373,7 @@ class ZipProgressBar(QtWidgets.QProgressBar):
def update_processed_size(self, val): def update_processed_size(self, val):
self._processed_size = val self._processed_size = val
if self.processed_size < self.total_files_size: if self.processed_size < self.total_files_size:
self.setValue(int((self.processed_size * 100) / self.total_files_size)) self.setValue(int((self.processed_size * 100) / self.total_files_size))
elif self.total_files_size != 0: elif self.total_files_size != 0:

View File

@ -22,7 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings from onionshare import strings
from ..widgets import Alert, AddFileDialog from ...widgets import Alert, AddFileDialog
class DropHereLabel(QtWidgets.QLabel): class DropHereLabel(QtWidgets.QLabel):
""" """
@ -89,7 +89,7 @@ class FileList(QtWidgets.QListWidget):
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.setIconSize(QtCore.QSize(32, 32)) self.setIconSize(QtCore.QSize(32, 32))
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.setMinimumHeight(205) self.setMinimumHeight(160)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.drop_here_image = DropHereLabel(self.common, self, True) self.drop_here_image = DropHereLabel(self.common, self, True)
self.drop_here_text = DropHereLabel(self.common, self, False) self.drop_here_text = DropHereLabel(self.common, self, False)
@ -261,6 +261,7 @@ class FileList(QtWidgets.QListWidget):
# Item info widget, with a white background # Item info widget, with a white background
item_info_layout = QtWidgets.QHBoxLayout() item_info_layout = QtWidgets.QHBoxLayout()
item_info_layout.setContentsMargins(0, 0, 0, 0)
item_info_layout.addWidget(item_size) item_info_layout.addWidget(item_size)
item_info_layout.addWidget(item.item_button) item_info_layout.addWidget(item.item_button)
item_info = QtWidgets.QWidget() item_info = QtWidgets.QWidget()

View File

@ -0,0 +1,63 @@
# -*- 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/>.
"""
from PyQt5 import QtCore
class CompressThread(QtCore.QThread):
"""
Compresses files to be shared
"""
success = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
def __init__(self, mode):
super(CompressThread, self).__init__()
self.mode = mode
self.mode.common.log('CompressThread', '__init__')
# prepare files to share
def set_processed_size(self, x):
if self.mode._zip_progress_bar != None:
self.mode._zip_progress_bar.update_processed_size_signal.emit(x)
def run(self):
self.mode.common.log('CompressThread', 'run')
try:
if self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size):
self.success.emit()
else:
# Cancelled
pass
self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames
except OSError as e:
self.error.emit(e.strerror)
def cancel(self):
self.mode.common.log('CompressThread', 'cancel')
# Let the Web and ZipWriter objects know that we're canceling compression early
self.mode.web.cancel_compression = True
try:
self.mode.web.zip_writer.cancel_compression = True
except AttributeError:
# we never made it as far as creating a ZipWriter object
pass

View File

@ -1,45 +0,0 @@
# -*- 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/>.
"""
from PyQt5 import QtCore
class OnionThread(QtCore.QThread):
"""
A QThread for starting our Onion Service.
By using QThread rather than threading.Thread, we are able
to call quit() or terminate() on the startup if the user
decided to cancel (in which case do not proceed with obtaining
the Onion address and starting the web server).
"""
def __init__(self, common, function, kwargs=None):
super(OnionThread, self).__init__()
self.common = common
self.common.log('OnionThread', '__init__')
self.function = function
if not kwargs:
self.kwargs = {}
else:
self.kwargs = kwargs
def run(self):
self.common.log('OnionThread', 'run')
self.function(**self.kwargs)

View File

@ -23,8 +23,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings from onionshare import strings
from onionshare.web import Web from onionshare.web import Web
from .share_mode import ShareMode from .mode.share_mode import ShareMode
from .receive_mode import ReceiveMode from .mode.receive_mode import ReceiveMode
from .tor_connection_dialog import TorConnectionDialog from .tor_connection_dialog import TorConnectionDialog
from .settings_dialog import SettingsDialog from .settings_dialog import SettingsDialog
@ -45,6 +45,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.common = common self.common = common
self.common.log('OnionShareGui', '__init__') self.common.log('OnionShareGui', '__init__')
self.setMinimumWidth(820)
self.setMinimumHeight(660)
self.onion = onion self.onion = onion
self.qtapp = qtapp self.qtapp = qtapp
@ -55,7 +57,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.setWindowTitle('OnionShare') self.setWindowTitle('OnionShare')
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.setMinimumWidth(450)
# Load settings # Load settings
self.config = config self.config = config
@ -66,7 +67,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.settings_action = menu.addAction(strings._('gui_settings_window_title', True)) self.settings_action = menu.addAction(strings._('gui_settings_window_title', True))
self.settings_action.triggered.connect(self.open_settings) self.settings_action.triggered.connect(self.open_settings)
help_action = menu.addAction(strings._('gui_settings_button_help', True)) help_action = menu.addAction(strings._('gui_settings_button_help', True))
help_action.triggered.connect(SettingsDialog.help_clicked) help_action.triggered.connect(SettingsDialog.open_help)
exit_action = menu.addAction(strings._('systray_menu_exit', True)) exit_action = menu.addAction(strings._('systray_menu_exit', True))
exit_action.triggered.connect(self.close) exit_action.triggered.connect(self.close)
@ -121,7 +122,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.setStatusBar(self.status_bar) self.setStatusBar(self.status_bar)
# Share mode # Share mode
self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames) self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames, self.local_only)
self.share_mode.init() self.share_mode.init()
self.share_mode.server_status.server_started.connect(self.update_server_status_indicator) self.share_mode.server_status.server_started.connect(self.update_server_status_indicator)
self.share_mode.server_status.server_stopped.connect(self.update_server_status_indicator) self.share_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
@ -135,7 +136,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.share_mode.set_server_active.connect(self.set_server_active) self.share_mode.set_server_active.connect(self.set_server_active)
# Receive mode # Receive mode
self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray) self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, None, self.local_only)
self.receive_mode.init() self.receive_mode.init()
self.receive_mode.server_status.server_started.connect(self.update_server_status_indicator) self.receive_mode.server_status.server_started.connect(self.update_server_status_indicator)
self.receive_mode.server_status.server_stopped.connect(self.update_server_status_indicator) self.receive_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
@ -153,7 +154,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Layouts # Layouts
contents_layout = QtWidgets.QVBoxLayout() contents_layout = QtWidgets.QVBoxLayout()
contents_layout.setContentsMargins(10, 10, 10, 10) contents_layout.setContentsMargins(10, 0, 10, 0)
contents_layout.addWidget(self.receive_mode) contents_layout.addWidget(self.receive_mode)
contents_layout.addWidget(self.share_mode) contents_layout.addWidget(self.share_mode)
@ -194,8 +195,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) 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.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.share_mode.show()
self.receive_mode.hide() self.receive_mode.hide()
self.share_mode.show()
else: else:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) 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.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
@ -205,9 +206,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.update_server_status_indicator() self.update_server_status_indicator()
# Wait 1ms for the event loop to finish, then adjust size
QtCore.QTimer.singleShot(1, self.adjustSize)
def share_mode_clicked(self): def share_mode_clicked(self):
if self.mode != self.MODE_SHARE: if self.mode != self.MODE_SHARE:
self.common.log('OnionShareGui', 'share_mode_clicked') self.common.log('OnionShareGui', 'share_mode_clicked')

View File

@ -1,237 +0,0 @@
# -*- 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/>.
"""
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from onionshare.web import Web
from .uploads import Uploads
from ..mode import Mode
class ReceiveMode(Mode):
"""
Parts of the main window UI for receiving files.
"""
def init(self):
"""
Custom initialization for ReceiveMode.
"""
# Create the Web object
self.web = Web(self.common, True, True)
# Server status
self.server_status.set_mode('receive')
self.server_status.server_started_finished.connect(self.update_primary_action)
self.server_status.server_stopped.connect(self.update_primary_action)
self.server_status.server_canceled.connect(self.update_primary_action)
# Tell server_status about web, then update
self.server_status.web = self.web
self.server_status.update()
# Downloads
self.uploads = Uploads(self.common)
self.uploads_in_progress = 0
self.uploads_completed = 0
self.new_upload = False # For scrolling to the bottom of the uploads list
# Information about share, and show uploads button
self.info_show_uploads = QtWidgets.QToolButton()
self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png')))
self.info_show_uploads.setCheckable(True)
self.info_show_uploads.toggled.connect(self.uploads_toggled)
self.info_show_uploads.setToolTip(strings._('gui_uploads_window_tooltip', True))
self.info_in_progress_uploads_count = QtWidgets.QLabel()
self.info_in_progress_uploads_count.setStyleSheet(self.common.css['mode_info_label'])
self.info_completed_uploads_count = QtWidgets.QLabel()
self.info_completed_uploads_count.setStyleSheet(self.common.css['mode_info_label'])
self.update_uploads_completed()
self.update_uploads_in_progress()
self.info_layout = QtWidgets.QHBoxLayout()
self.info_layout.addStretch()
self.info_layout.addWidget(self.info_in_progress_uploads_count)
self.info_layout.addWidget(self.info_completed_uploads_count)
self.info_layout.addWidget(self.info_show_uploads)
self.info_widget = QtWidgets.QWidget()
self.info_widget.setLayout(self.info_layout)
self.info_widget.hide()
# Receive mode info
self.receive_info = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True))
self.receive_info.setMinimumHeight(80)
self.receive_info.setWordWrap(True)
# Layout
self.layout.insertWidget(0, self.receive_info)
self.layout.insertWidget(0, self.info_widget)
def get_stop_server_shutdown_timeout_text(self):
"""
Return the string to put on the stop server button, if there's a shutdown timeout
"""
return strings._('gui_receive_stop_server_shutdown_timeout', True)
def timeout_finished_should_stop_server(self):
"""
The shutdown timer expired, should we stop the server? Returns a bool
"""
# TODO: wait until the final upload is done before stoppign the server?
return True
def start_server_custom(self):
"""
Starting the server.
"""
# Reset web counters
self.web.upload_count = 0
self.web.error404_count = 0
# Hide and reset the uploads if we have previously shared
self.reset_info_counters()
def start_server_step2_custom(self):
"""
Step 2 in starting the server.
"""
# Continue
self.starting_server_step3.emit()
self.start_server_finished.emit()
def handle_tor_broke_custom(self):
"""
Connection to Tor broke.
"""
self.primary_action.hide()
self.info_widget.hide()
def handle_request_load(self, event):
"""
Handle REQUEST_LOAD event.
"""
self.system_tray.showMessage(strings._('systray_page_loaded_title', True), strings._('systray_upload_page_loaded_message', True))
def handle_request_started(self, event):
"""
Handle REQUEST_STARTED event.
"""
self.uploads.add(event["data"]["id"], event["data"]["content_length"])
self.uploads_in_progress += 1
self.update_uploads_in_progress()
self.system_tray.showMessage(strings._('systray_upload_started_title', True), strings._('systray_upload_started_message', True))
def handle_request_progress(self, event):
"""
Handle REQUEST_PROGRESS event.
"""
self.uploads.update(event["data"]["id"], event["data"]["progress"])
def handle_request_close_server(self, event):
"""
Handle REQUEST_CLOSE_SERVER event.
"""
self.stop_server()
self.system_tray.showMessage(strings._('systray_close_server_title', True), strings._('systray_close_server_message', True))
def handle_request_upload_file_renamed(self, event):
"""
Handle REQUEST_UPLOAD_FILE_RENAMED event.
"""
self.uploads.rename(event["data"]["id"], event["data"]["old_filename"], event["data"]["new_filename"])
def handle_request_upload_finished(self, event):
"""
Handle REQUEST_UPLOAD_FINISHED event.
"""
self.uploads.finished(event["data"]["id"])
# Update the total 'completed uploads' info
self.uploads_completed += 1
self.update_uploads_completed()
# Update the 'in progress uploads' info
self.uploads_in_progress -= 1
self.update_uploads_in_progress()
def on_reload_settings(self):
"""
We should be ok to re-enable the 'Start Receive Mode' button now.
"""
self.primary_action.show()
self.info_widget.show()
def reset_info_counters(self):
"""
Set the info counters back to zero.
"""
self.uploads_completed = 0
self.uploads_in_progress = 0
self.update_uploads_completed()
self.update_uploads_in_progress()
self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png')))
self.uploads.reset()
def update_uploads_completed(self):
"""
Update the 'Uploads completed' info widget.
"""
if self.uploads_completed == 0:
image = self.common.get_resource_path('images/share_completed_none.png')
else:
image = self.common.get_resource_path('images/share_completed.png')
self.info_completed_uploads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.uploads_completed))
self.info_completed_uploads_count.setToolTip(strings._('info_completed_uploads_tooltip', True).format(self.uploads_completed))
def update_uploads_in_progress(self):
"""
Update the 'Uploads in progress' info widget.
"""
if self.uploads_in_progress == 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.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_green.png')))
self.info_in_progress_uploads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.uploads_in_progress))
self.info_in_progress_uploads_count.setToolTip(strings._('info_in_progress_uploads_tooltip', True).format(self.uploads_in_progress))
def update_primary_action(self):
self.common.log('ReceiveMode', 'update_primary_action')
# Show the info widget when the server is active
if self.server_status.status == self.server_status.STATUS_STARTED:
self.info_widget.show()
else:
self.info_widget.hide()
# Resize window
self.adjustSize()
def uploads_toggled(self, checked):
"""
When the 'Show/hide uploads' button is toggled, show or hide the uploads window.
"""
self.common.log('ReceiveMode', 'toggle_uploads')
if checked:
self.uploads.show()
else:
self.uploads.hide()

View File

@ -1,310 +0,0 @@
# -*- 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 subprocess
from datetime import datetime
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from ..widgets import Alert
class File(QtWidgets.QWidget):
def __init__(self, common, filename):
super(File, self).__init__()
self.common = common
self.common.log('File', '__init__', 'filename: {}'.format(filename))
self.filename = filename
self.started = datetime.now()
# Filename label
self.filename_label = QtWidgets.QLabel(self.filename)
self.filename_label_width = self.filename_label.width()
# File size label
self.filesize_label = QtWidgets.QLabel()
self.filesize_label.setStyleSheet(self.common.css['receive_file_size'])
self.filesize_label.hide()
# Folder button
folder_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/open_folder.png')))
folder_icon = QtGui.QIcon(folder_pixmap)
self.folder_button = QtWidgets.QPushButton()
self.folder_button.clicked.connect(self.open_folder)
self.folder_button.setIcon(folder_icon)
self.folder_button.setIconSize(folder_pixmap.rect().size())
self.folder_button.setFlat(True)
self.folder_button.hide()
# Layouts
layout = QtWidgets.QHBoxLayout()
layout.addWidget(self.filename_label)
layout.addWidget(self.filesize_label)
layout.addStretch()
layout.addWidget(self.folder_button)
self.setLayout(layout)
def update(self, uploaded_bytes, complete):
self.filesize_label.setText(self.common.human_readable_filesize(uploaded_bytes))
self.filesize_label.show()
if complete:
self.folder_button.show()
def rename(self, new_filename):
self.filename = new_filename
self.filename_label.setText(self.filename)
def open_folder(self):
"""
Open the downloads folder, with the file selected, in a cross-platform manner
"""
self.common.log('File', 'open_folder')
abs_filename = os.path.join(self.common.settings.get('downloads_dir'), self.filename)
# Linux
if self.common.platform == 'Linux' or self.common.platform == 'BSD':
try:
# If nautilus is available, open it
subprocess.Popen(['nautilus', abs_filename])
except:
Alert(self.common, strings._('gui_open_folder_error_nautilus').format(abs_filename))
# macOS
elif self.common.platform == 'Darwin':
subprocess.call(['open', '-R', abs_filename])
# Windows
elif self.common.platform == 'Windows':
subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)])
class Upload(QtWidgets.QWidget):
def __init__(self, common, upload_id, content_length):
super(Upload, self).__init__()
self.common = common
self.upload_id = upload_id
self.content_length = content_length
self.started = datetime.now()
# Label
self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress', True).format(self.started.strftime("%b %d, %I:%M%p")))
# Progress bar
self.progress_bar = QtWidgets.QProgressBar()
self.progress_bar.setTextVisible(True)
self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
self.progress_bar.setMinimum(0)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
# This layout contains file widgets
self.files_layout = QtWidgets.QVBoxLayout()
self.files_layout.setContentsMargins(0, 0, 0, 0)
files_widget = QtWidgets.QWidget()
files_widget.setStyleSheet(self.common.css['receive_file'])
files_widget.setLayout(self.files_layout)
# Layout
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.progress_bar)
layout.addWidget(files_widget)
layout.addStretch()
self.setLayout(layout)
# We're also making a dictionary of file widgets, to make them easier to access
self.files = {}
def update(self, progress):
"""
Using the progress from Web, update the progress bar and file size labels
for each file
"""
total_uploaded_bytes = 0
for filename in progress:
total_uploaded_bytes += progress[filename]['uploaded_bytes']
# Update the progress bar
self.progress_bar.setMaximum(self.content_length)
self.progress_bar.setValue(total_uploaded_bytes)
elapsed = datetime.now() - self.started
if elapsed.seconds < 10:
pb_fmt = strings._('gui_download_upload_progress_starting').format(
self.common.human_readable_filesize(total_uploaded_bytes))
else:
estimated_time_remaining = self.common.estimated_time_remaining(
total_uploaded_bytes,
self.content_length,
self.started.timestamp())
pb_fmt = strings._('gui_download_upload_progress_eta').format(
self.common.human_readable_filesize(total_uploaded_bytes),
estimated_time_remaining)
# Using list(progress) to avoid "RuntimeError: dictionary changed size during iteration"
for filename in list(progress):
# Add a new file if needed
if filename not in self.files:
self.files[filename] = File(self.common, filename)
self.files_layout.addWidget(self.files[filename])
# Update the file
self.files[filename].update(progress[filename]['uploaded_bytes'], progress[filename]['complete'])
def rename(self, old_filename, new_filename):
self.files[old_filename].rename(new_filename)
self.files[new_filename] = self.files.pop(old_filename)
def finished(self):
# Hide the progress bar
self.progress_bar.hide()
# Change the label
self.ended = self.started = datetime.now()
if self.started.year == self.ended.year and self.started.month == self.ended.month and self.started.day == self.ended.day:
if self.started.hour == self.ended.hour and self.started.minute == self.ended.minute:
text = strings._('gui_upload_finished', True).format(
self.started.strftime("%b %d, %I:%M%p")
)
else:
text = strings._('gui_upload_finished_range', True).format(
self.started.strftime("%b %d, %I:%M%p"),
self.ended.strftime("%I:%M%p")
)
else:
text = strings._('gui_upload_finished_range', True).format(
self.started.strftime("%b %d, %I:%M%p"),
self.ended.strftime("%b %d, %I:%M%p")
)
self.label.setText(text)
class Uploads(QtWidgets.QScrollArea):
"""
The uploads chunk of the GUI. This lists all of the active upload
progress bars, as well as information about each upload.
"""
def __init__(self, common):
super(Uploads, self).__init__()
self.common = common
self.common.log('Uploads', '__init__')
self.resizeEvent = None
self.uploads = {}
self.setWindowTitle(strings._('gui_uploads', True))
self.setWidgetResizable(True)
self.setMaximumHeight(600)
self.setMinimumHeight(150)
self.setMinimumWidth(350)
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint)
self.vbar = self.verticalScrollBar()
self.vbar.rangeChanged.connect(self.resizeScroll)
uploads_label = QtWidgets.QLabel(strings._('gui_uploads', True))
uploads_label.setStyleSheet(self.common.css['downloads_uploads_label'])
self.no_uploads_label = QtWidgets.QLabel(strings._('gui_no_uploads', True))
self.uploads_layout = QtWidgets.QVBoxLayout()
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
layout.addWidget(uploads_label)
layout.addWidget(self.no_uploads_label)
layout.addLayout(self.uploads_layout)
layout.addStretch()
widget.setLayout(layout)
self.setWidget(widget)
def resizeScroll(self, minimum, maximum):
"""
Scroll to the bottom of the window when the range changes.
"""
self.vbar.setValue(maximum)
def add(self, upload_id, content_length):
"""
Add a new upload.
"""
self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length))
# Hide the no_uploads_label
self.no_uploads_label.hide()
# Add it to the list
upload = Upload(self.common, upload_id, content_length)
self.uploads[upload_id] = upload
self.uploads_layout.addWidget(upload)
def update(self, upload_id, progress):
"""
Update the progress of an upload.
"""
self.uploads[upload_id].update(progress)
def rename(self, upload_id, old_filename, new_filename):
"""
Rename a file, which happens if the filename already exists in downloads_dir.
"""
self.uploads[upload_id].rename(old_filename, new_filename)
def finished(self, upload_id):
"""
An upload has finished.
"""
self.uploads[upload_id].finished()
def cancel(self, upload_id):
"""
Update an upload progress bar to show that it has been canceled.
"""
self.common.log('Uploads', 'cancel', 'upload_id: {}'.format(upload_id))
self.uploads[upload_id].cancel()
def reset(self):
"""
Reset the uploads back to zero
"""
self.common.log('Uploads', 'reset')
for upload in self.uploads.values():
self.uploads_layout.removeWidget(upload)
self.uploads = {}
self.no_uploads_label.show()
self.resize(self.sizeHint())
def resizeEvent(self, event):
width = self.frameGeometry().width()
try:
for upload in self.uploads.values():
for item in upload.files.values():
if item.filename_label_width > width:
item.filename_label.setText(item.filename[:25] + '[...]')
item.adjustSize()
if width > item.filename_label_width:
item.filename_label.setText(item.filename)
except:
pass

View File

@ -44,7 +44,7 @@ class ServerStatus(QtWidgets.QWidget):
STATUS_WORKING = 1 STATUS_WORKING = 1
STATUS_STARTED = 2 STATUS_STARTED = 2
def __init__(self, common, qtapp, app, file_selection=None): def __init__(self, common, qtapp, app, file_selection=None, local_only=False):
super(ServerStatus, self).__init__() super(ServerStatus, self).__init__()
self.common = common self.common = common
@ -56,17 +56,23 @@ class ServerStatus(QtWidgets.QWidget):
self.app = app self.app = app
self.web = None self.web = None
self.local_only = local_only
self.resizeEvent(None) self.resizeEvent(None)
# Shutdown timeout layout # Shutdown timeout layout
self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True)) self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
self.shutdown_timeout = QtWidgets.QDateTimeEdit() self.shutdown_timeout = QtWidgets.QDateTimeEdit()
# Set proposed timeout to be 5 minutes into the future
self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy") self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy")
self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) if self.local_only:
# Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 2 min from now # For testing
self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15))
self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime())
else:
# Set proposed timeout to be 5 minutes into the future
self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
# Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now
self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection)
shutdown_timeout_layout = QtWidgets.QHBoxLayout() shutdown_timeout_layout = QtWidgets.QHBoxLayout()
shutdown_timeout_layout.addWidget(self.shutdown_timeout_label) shutdown_timeout_layout.addWidget(self.shutdown_timeout_label)
@ -84,20 +90,20 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.clicked.connect(self.server_button_clicked) self.server_button.clicked.connect(self.server_button_clicked)
# URL layout # URL layout
url_font = QtGui.QFont() url_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
self.url_description = QtWidgets.QLabel() self.url_description = QtWidgets.QLabel()
self.url_description.setWordWrap(True) self.url_description.setWordWrap(True)
self.url_description.setMinimumHeight(50) self.url_description.setMinimumHeight(50)
self.url = QtWidgets.QLabel() self.url = QtWidgets.QLabel()
self.url.setFont(url_font) self.url.setFont(url_font)
self.url.setWordWrap(True) self.url.setWordWrap(True)
self.url.setMinimumHeight(65)
self.url.setMinimumSize(self.url.sizeHint()) self.url.setMinimumSize(self.url.sizeHint())
self.url.setStyleSheet(self.common.css['server_status_url']) self.url.setStyleSheet(self.common.css['server_status_url'])
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True)) self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True))
self.copy_url_button.setFlat(True) self.copy_url_button.setFlat(True)
self.copy_url_button.setStyleSheet(self.common.css['server_status_url_buttons']) self.copy_url_button.setStyleSheet(self.common.css['server_status_url_buttons'])
self.copy_url_button.setMinimumHeight(65)
self.copy_url_button.clicked.connect(self.copy_url) self.copy_url_button.clicked.connect(self.copy_url)
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
self.copy_hidservauth_button.setFlat(True) self.copy_hidservauth_button.setFlat(True)
@ -136,12 +142,12 @@ class ServerStatus(QtWidgets.QWidget):
When the widget is resized, try and adjust the display of a v3 onion URL. When the widget is resized, try and adjust the display of a v3 onion URL.
""" """
try: try:
self.get_url() # Wrap the URL label
url_length=len(self.get_url()) url_length=len(self.get_url())
if url_length > 60: if url_length > 60:
width = self.frameGeometry().width() width = self.frameGeometry().width()
if width < 530: if width < 530:
wrapped_onion_url = textwrap.fill(self.get_url(), 50) wrapped_onion_url = textwrap.fill(self.get_url(), 46)
self.url.setText(wrapped_onion_url) self.url.setText(wrapped_onion_url)
else: else:
self.url.setText(self.get_url()) self.url.setText(self.get_url())
@ -154,7 +160,8 @@ class ServerStatus(QtWidgets.QWidget):
Reset the timeout in the UI after stopping a share Reset the timeout in the UI after stopping a share
""" """
self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) if not self.local_only:
self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
def update(self): def update(self):
""" """
@ -255,8 +262,11 @@ class ServerStatus(QtWidgets.QWidget):
""" """
if self.status == self.STATUS_STOPPED: if self.status == self.STATUS_STOPPED:
if self.common.settings.get('shutdown_timeout'): if self.common.settings.get('shutdown_timeout'):
# Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen if self.local_only:
self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) self.timeout = self.shutdown_timeout.dateTime().toPyDateTime()
else:
# Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
# If the timeout has actually passed already before the user hit Start, refuse to start the server. # If the timeout has actually passed already before the user hit Start, refuse to start the server.
if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout: if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout:
Alert(self.common, strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning)) Alert(self.common, strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning))

View File

@ -746,7 +746,7 @@ class SettingsDialog(QtWidgets.QDialog):
onion.connect(custom_settings=settings, config=self.config, tor_status_update_func=tor_status_update_func) onion.connect(custom_settings=settings, config=self.config, tor_status_update_func=tor_status_update_func)
# If an exception hasn't been raised yet, the Tor settings work # If an exception hasn't been raised yet, the Tor settings work
Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth, onion.supports_next_gen_onions))
# Clean up # Clean up
onion.cleanup() onion.cleanup()
@ -883,8 +883,12 @@ class SettingsDialog(QtWidgets.QDialog):
Help button clicked. Help button clicked.
""" """
self.common.log('SettingsDialog', 'help_clicked') self.common.log('SettingsDialog', 'help_clicked')
help_site = 'https://github.com/micahflee/onionshare/wiki' SettingsDialog.open_help()
QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_site))
@staticmethod
def open_help():
help_url = 'https://github.com/micahflee/onionshare/wiki'
QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_url))
def settings_from_fields(self): def settings_from_fields(self):
""" """

View File

@ -1,157 +0,0 @@
# -*- 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 time
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
class Download(object):
def __init__(self, common, download_id, total_bytes):
self.common = common
self.download_id = download_id
self.started = time.time()
self.total_bytes = total_bytes
self.downloaded_bytes = 0
# Progress bar
self.progress_bar = QtWidgets.QProgressBar()
self.progress_bar.setTextVisible(True)
self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(total_bytes)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
self.progress_bar.total_bytes = total_bytes
# Start at 0
self.update(0)
def update(self, downloaded_bytes):
self.downloaded_bytes = downloaded_bytes
self.progress_bar.setValue(downloaded_bytes)
if downloaded_bytes == self.progress_bar.total_bytes:
pb_fmt = strings._('gui_download_upload_progress_complete').format(
self.common.format_seconds(time.time() - self.started))
else:
elapsed = time.time() - self.started
if elapsed < 10:
# Wait a couple of seconds for the download rate to stabilize.
# This prevents a "Windows copy dialog"-esque experience at
# the beginning of the download.
pb_fmt = strings._('gui_download_upload_progress_starting').format(
self.common.human_readable_filesize(downloaded_bytes))
else:
pb_fmt = strings._('gui_download_upload_progress_eta').format(
self.common.human_readable_filesize(downloaded_bytes),
self.estimated_time_remaining)
self.progress_bar.setFormat(pb_fmt)
def cancel(self):
self.progress_bar.setFormat(strings._('gui_canceled'))
@property
def estimated_time_remaining(self):
return self.common.estimated_time_remaining(self.downloaded_bytes,
self.total_bytes,
self.started)
class Downloads(QtWidgets.QScrollArea):
"""
The downloads chunk of the GUI. This lists all of the active download
progress bars.
"""
def __init__(self, common):
super(Downloads, self).__init__()
self.common = common
self.downloads = {}
self.setWindowTitle(strings._('gui_downloads', True))
self.setWidgetResizable(True)
self.setMaximumHeight(600)
self.setMinimumHeight(150)
self.setMinimumWidth(350)
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint)
self.vbar = self.verticalScrollBar()
self.vbar.rangeChanged.connect(self.resizeScroll)
downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True))
downloads_label.setStyleSheet(self.common.css['downloads_uploads_label'])
self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True))
self.downloads_layout = QtWidgets.QVBoxLayout()
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
layout.addWidget(downloads_label)
layout.addWidget(self.no_downloads_label)
layout.addLayout(self.downloads_layout)
layout.addStretch()
widget.setLayout(layout)
self.setWidget(widget)
def resizeScroll(self, minimum, maximum):
"""
Scroll to the bottom of the window when the range changes.
"""
self.vbar.setValue(maximum)
def add(self, download_id, total_bytes):
"""
Add a new download progress bar.
"""
# Hide the no_downloads_label
self.no_downloads_label.hide()
# Add it to the list
download = Download(self.common, download_id, total_bytes)
self.downloads[download_id] = download
self.downloads_layout.addWidget(download.progress_bar)
def update(self, download_id, downloaded_bytes):
"""
Update the progress of a download progress bar.
"""
self.downloads[download_id].update(downloaded_bytes)
def cancel(self, download_id):
"""
Update a download progress bar to show that it has been canceled.
"""
self.downloads[download_id].cancel()
def reset(self):
"""
Reset the downloads back to zero
"""
for download in self.downloads.values():
self.downloads_layout.removeWidget(download.progress_bar)
download.progress_bar.close()
self.downloads = {}
self.no_downloads_label.show()
self.resize(self.sizeHint())

77
onionshare_gui/threads.py Normal file
View File

@ -0,0 +1,77 @@
# -*- 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 time
from PyQt5 import QtCore
from onionshare.onion import *
class OnionThread(QtCore.QThread):
"""
Starts the onion service, and waits for it to finish
"""
success = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
def __init__(self, mode):
super(OnionThread, self).__init__()
self.mode = mode
self.mode.common.log('OnionThread', '__init__')
# allow this thread to be terminated
self.setTerminationEnabled()
def run(self):
self.mode.common.log('OnionThread', 'run')
self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download')
# start onionshare http service in new thread
self.mode.web_thread = WebThread(self.mode)
self.mode.web_thread.start()
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
time.sleep(0.2)
try:
self.mode.app.start_onion_service()
self.success.emit()
except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e:
self.error.emit(e.args[0])
return
class WebThread(QtCore.QThread):
"""
Starts the web service
"""
success = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
def __init__(self, mode):
super(WebThread, self).__init__()
self.mode = mode
self.mode.common.log('WebThread', '__init__')
def run(self):
self.mode.common.log('WebThread', 'run')
self.mode.app.choose_port()
self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.common.settings.get('slug'))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

View File

@ -65,7 +65,14 @@ setup(
description=description, long_description=long_description, description=description, long_description=long_description,
author=author, author_email=author_email, author=author, author_email=author_email,
url=url, license=license, keywords=keywords, url=url, license=license, keywords=keywords,
packages=['onionshare', 'onionshare_gui'], packages=[
'onionshare',
'onionshare.web',
'onionshare_gui',
'onionshare_gui.mode',
'onionshare_gui.mode.share_mode',
'onionshare_gui.mode.receive_mode'
],
include_package_data=True, include_package_data=True,
scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'], scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'],
data_files=data_files data_files=data_files

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 B

BIN
share/images/downloads.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 B

BIN
share/images/uploads.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,7 +1,6 @@
{ {
"config_onion_service": "Nastavuji onion service na portu {0:d}.", "config_onion_service": "Nastavuji onion service na portu {0:d}.",
"preparing_files": "Připravuji soubory ke sdílení.", "preparing_files": "Připravuji soubory ke sdílení.",
"wait_for_hs": "Čekám na HS až bude připravena:",
"give_this_url": "Dejte tuto URL osobě, které dané soubory posíláte:", "give_this_url": "Dejte tuto URL osobě, které dané soubory posíláte:",
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:", "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
"ctrlc_to_stop": "Stiskněte Ctrl-C pro zastavení serveru", "ctrlc_to_stop": "Stiskněte Ctrl-C pro zastavení serveru",
@ -27,7 +26,6 @@
"gui_copied_url": "URL zkopírováno do schránky", "gui_copied_url": "URL zkopírováno do schránky",
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard", "gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
"gui_please_wait": "Prosím čekejte...", "gui_please_wait": "Prosím čekejte...",
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
"gui_download_upload_progress_complete": "%p%, Uplynulý čas: {0:s}", "gui_download_upload_progress_complete": "%p%, Uplynulý čas: {0:s}",
"gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)",
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",

View File

@ -1,7 +1,6 @@
{ {
"config_onion_service": "Konfigurerer onion-tjeneste på port {0:d}.", "config_onion_service": "Konfigurerer onion-tjeneste på port {0:d}.",
"preparing_files": "Forbereder filer som skal deles.", "preparing_files": "Forbereder filer som skal deles.",
"wait_for_hs": "Venter på at HS bliver klar:",
"give_this_url": "Giv denne URL til personen du sender filen til:", "give_this_url": "Giv denne URL til personen du sender filen til:",
"give_this_url_stealth": "Giv denne URL og HidServAuth-linje til personen du sender filen til:", "give_this_url_stealth": "Giv denne URL og HidServAuth-linje til personen du sender filen til:",
"ctrlc_to_stop": "Tryk på Ctrl-C for at stoppe serveren", "ctrlc_to_stop": "Tryk på Ctrl-C for at stoppe serveren",
@ -40,7 +39,6 @@
"gui_copied_url": "Kopierede URL til udklipsholder", "gui_copied_url": "Kopierede URL til udklipsholder",
"gui_copied_hidservauth": "Kopierede HidServAuth-linje til udklipsholder", "gui_copied_hidservauth": "Kopierede HidServAuth-linje til udklipsholder",
"gui_please_wait": "Vent venligst...", "gui_please_wait": "Vent venligst...",
"using_ephemeral": "Starter kortvarig Tor onion-tjeneste og afventer udgivelse",
"gui_download_upload_progress_complete": "%p%, tid forløbet: {0:s}", "gui_download_upload_progress_complete": "%p%, tid forløbet: {0:s}",
"gui_download_upload_progress_starting": "{0:s}, %p% (udregner anslået ankomsttid)", "gui_download_upload_progress_starting": "{0:s}, %p% (udregner anslået ankomsttid)",
"gui_download_upload_progress_eta": "{0:s}, anslået ankomsttid: {1:s}, %p%", "gui_download_upload_progress_eta": "{0:s}, anslået ankomsttid: {1:s}, %p%",
@ -53,9 +51,7 @@
"error_stealth_not_supported": "For at oprette usynlige onion-tjenester, skal du mindst have Tor 0.2.9.1-alpha (eller Tor Browser 6.5) og mindst python3-stem 1.5.0.", "error_stealth_not_supported": "For at oprette usynlige onion-tjenester, skal du mindst have Tor 0.2.9.1-alpha (eller Tor Browser 6.5) og mindst python3-stem 1.5.0.",
"error_ephemeral_not_supported": "OnionShare kræver mindst Tor 0.2.7.1 og mindst python3-stem 1.4.0.", "error_ephemeral_not_supported": "OnionShare kræver mindst Tor 0.2.7.1 og mindst python3-stem 1.4.0.",
"gui_settings_window_title": "Indstillinger", "gui_settings_window_title": "Indstillinger",
"gui_settings_stealth_label": "Usynlig (avanceret)",
"gui_settings_stealth_option": "Opret usynlige onion-tjenester", "gui_settings_stealth_option": "Opret usynlige onion-tjenester",
"gui_settings_stealth_option_details": "Det gør OnionShare mere sikker, men også mere besværlig for modtageren at oprette forbindelse til den.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">Mere information</a>.",
"gui_settings_stealth_hidservauth_string": "Du har gemt den private nøgle til at blive brugt igen, så din HidServAuth-streng bruges også igen.\nKlik nedenfor, for at kopiere HidServAuth.", "gui_settings_stealth_hidservauth_string": "Du har gemt den private nøgle til at blive brugt igen, så din HidServAuth-streng bruges også igen.\nKlik nedenfor, for at kopiere HidServAuth.",
"gui_settings_autoupdate_label": "Søg efter opdateringer", "gui_settings_autoupdate_label": "Søg efter opdateringer",
"gui_settings_autoupdate_option": "Giv mig besked når der findes opdateringer", "gui_settings_autoupdate_option": "Giv mig besked når der findes opdateringer",

View File

@ -1,6 +1,5 @@
{ {
"preparing_files": "Dateien werden vorbereitet.", "preparing_files": "Dateien werden vorbereitet.",
"wait_for_hs": "Warte auf HS:",
"give_this_url": "Geben Sie diese URL der Person, der Sie die Datei zusenden möchten:", "give_this_url": "Geben Sie diese URL der Person, der Sie die Datei zusenden möchten:",
"ctrlc_to_stop": "Drücken Sie Strg+C um den Server anzuhalten", "ctrlc_to_stop": "Drücken Sie Strg+C um den Server anzuhalten",
"not_a_file": "{0:s} ist keine Datei.", "not_a_file": "{0:s} ist keine Datei.",

View File

@ -1,21 +1,19 @@
{ {
"config_onion_service": "Configuring onion service on port {0:d}.", "config_onion_service": "Setting up onion service on port {0:d}.",
"preparing_files": "Preparing files to share.", "preparing_files": "Compressing files.",
"wait_for_hs": "Waiting for HS to be ready:", "give_this_url": "Give this address to the recipient:",
"give_this_url": "Give this address to the person you're sending the file to:", "give_this_url_stealth": "Give this address and HidServAuth line to the recipient:",
"give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:", "give_this_url_receive": "Give this address to the sender:",
"give_this_url_receive": "Give this address to the people sending you files:", "give_this_url_receive_stealth": "Give this address and HidServAuth to the sender:",
"give_this_url_receive_stealth": "Give this address and HidServAuth line to the people sending you files:",
"ctrlc_to_stop": "Press Ctrl+C to stop the server", "ctrlc_to_stop": "Press Ctrl+C to stop the server",
"not_a_file": "{0:s} is not a valid file.", "not_a_file": "{0:s} is not a valid file.",
"not_a_readable_file": "{0:s} is not a readable file.", "not_a_readable_file": "{0:s} is not a readable file.",
"no_filenames": "You must specify a list of files to share.", "no_available_port": "Could not find an available port to start the onion service",
"no_available_port": "Could not start the Onion service as there was no available port.",
"other_page_loaded": "Address loaded", "other_page_loaded": "Address loaded",
"close_on_timeout": "Stopped because timer expired", "close_on_timeout": "Stopped because auto-stop timer ran out",
"closing_automatically": "Stopped because download finished", "closing_automatically": "Stopped because download finished",
"timeout_download_still_running": "Waiting for download to complete", "timeout_download_still_running": "Waiting for download to complete",
"large_filesize": "Warning: Sending large files could take hours", "large_filesize": "Warning: Sending a large share could take hours",
"systray_menu_exit": "Quit", "systray_menu_exit": "Quit",
"systray_download_started_title": "OnionShare Download Started", "systray_download_started_title": "OnionShare Download Started",
"systray_download_started_message": "A user started downloading your files", "systray_download_started_message": "A user started downloading your files",
@ -25,69 +23,67 @@
"systray_download_canceled_message": "The user canceled the download", "systray_download_canceled_message": "The user canceled the download",
"systray_upload_started_title": "OnionShare Upload Started", "systray_upload_started_title": "OnionShare Upload Started",
"systray_upload_started_message": "A user started uploading files to your computer", "systray_upload_started_message": "A user started uploading files to your computer",
"help_local_only": "Do not attempt to use Tor: For development only", "help_local_only": "Don't use Tor (only for development)",
"help_stay_open": "Keep onion service running after download has finished", "help_stay_open": "Keep sharing after first download",
"help_shutdown_timeout": "Shut down the onion service after N seconds", "help_shutdown_timeout": "Stop sharing after a given amount of seconds",
"help_stealth": "Create stealth onion service (advanced)", "help_stealth": "Use client authorization (advanced)",
"help_receive": "Receive files instead of sending them", "help_receive": "Receive shares instead of sending them",
"help_debug": "Log application errors to stdout, and log web errors to disk", "help_debug": "Log OnionShare errors to stdout, and web errors to disk",
"help_filename": "List of files or folders to share", "help_filename": "List of files or folders to share",
"help_config": "Path to a custom JSON config file (optional)", "help_config": "Custom JSON config file location (optional)",
"gui_drag_and_drop": "Drag and drop files and folders\nto start sharing", "gui_drag_and_drop": "Drag and drop files and folders\nto start sharing",
"gui_add": "Add", "gui_add": "Add",
"gui_delete": "Delete", "gui_delete": "Delete",
"gui_choose_items": "Choose", "gui_choose_items": "Choose",
"gui_share_start_server": "Start Sharing", "gui_share_start_server": "Start sharing",
"gui_share_stop_server": "Stop Sharing", "gui_share_stop_server": "Stop sharing",
"gui_share_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)", "gui_share_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)",
"gui_share_stop_server_shutdown_timeout_tooltip": "Share will expire automatically at {}", "gui_share_stop_server_shutdown_timeout_tooltip": "Auto-stop timer ends at {}",
"gui_receive_start_server": "Start Receive Mode", "gui_receive_start_server": "Start Receive Mode",
"gui_receive_stop_server": "Stop Receive Mode", "gui_receive_stop_server": "Stop Receive Mode",
"gui_receive_stop_server_shutdown_timeout": "Stop Receive Mode ({}s remaining)", "gui_receive_stop_server_shutdown_timeout": "Stop Receive Mode ({}s remaining)",
"gui_receive_stop_server_shutdown_timeout_tooltip": "Receive mode will expire automatically at {}", "gui_receive_stop_server_shutdown_timeout_tooltip": "Auto-stop timer ends at {}",
"gui_copy_url": "Copy Address", "gui_copy_url": "Copy Address",
"gui_copy_hidservauth": "Copy HidServAuth", "gui_copy_hidservauth": "Copy HidServAuth",
"gui_downloads": "Download History", "gui_downloads": "Download History",
"gui_downloads_window_tooltip": "Show/hide downloads", "gui_no_downloads": "No Downloads Yet",
"gui_no_downloads": "No downloads yet.",
"gui_canceled": "Canceled", "gui_canceled": "Canceled",
"gui_copied_url_title": "Copied OnionShare address", "gui_copied_url_title": "Copied OnionShare Address",
"gui_copied_url": "The OnionShare address has been copied to clipboard", "gui_copied_url": "OnionShare address copied to clipboard",
"gui_copied_hidservauth_title": "Copied HidServAuth", "gui_copied_hidservauth_title": "Copied HidServAuth",
"gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard", "gui_copied_hidservauth": "HidServAuth line copied to clipboard",
"gui_please_wait": "Starting… Click to cancel", "gui_please_wait": "Starting… Click to cancel.",
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication", "gui_download_upload_progress_complete": "%p%, {0:s} elapsed.",
"gui_download_upload_progress_complete": "%p%, Time Elapsed: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (calculating)",
"gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)",
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
"version_string": "OnionShare {0:s} | https://onionshare.org/", "version_string": "OnionShare {0:s} | https://onionshare.org/",
"gui_quit_title": "Transfer in Progress", "gui_quit_title": "Not so fast",
"gui_share_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?", "gui_share_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?",
"gui_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?", "gui_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?",
"gui_quit_warning_quit": "Quit", "gui_quit_warning_quit": "Quit",
"gui_quit_warning_dont_quit": "Cancel", "gui_quit_warning_dont_quit": "Cancel",
"error_rate_limit": "An attacker might be trying to guess your address. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new address.", "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 receipient a new address to share.",
"zip_progress_bar_format": "Compressing files: %p%", "zip_progress_bar_format": "Compressing: %p%",
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.", "error_stealth_not_supported": "To use client authorization, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.",
"error_ephemeral_not_supported": "OnionShare requires at least Tor 0.2.7.1 and at least python3-stem 1.4.0.", "error_ephemeral_not_supported": "OnionShare requires at least both Tor 0.2.7.1 and python3-stem 1.4.0.",
"gui_settings_window_title": "Settings", "gui_settings_window_title": "Settings",
"gui_settings_whats_this": "<a href='{0:s}'>what's this?</a>", "gui_settings_whats_this": "<a href='{0:s}'>What's this?</a>",
"gui_settings_stealth_option": "Create stealth onion services (legacy)", "gui_settings_stealth_option": "Use client authorization (legacy)",
"gui_settings_stealth_hidservauth_string": "You have saved the private key for reuse, so your HidServAuth string is also reused.\nClick below to copy the HidServAuth.", "gui_settings_stealth_hidservauth_string": "Having saved your private key for reuse, means you can now\nclick to copy your HidServAuth.",
"gui_settings_autoupdate_label": "Check for upgrades", "gui_settings_autoupdate_label": "Check for new version",
"gui_settings_autoupdate_option": "Notify me when upgrades are available", "gui_settings_autoupdate_option": "Notify me when a new version is available",
"gui_settings_autoupdate_timestamp": "Last checked: {}", "gui_settings_autoupdate_timestamp": "Last checked: {}",
"gui_settings_autoupdate_timestamp_never": "Never", "gui_settings_autoupdate_timestamp_never": "Never",
"gui_settings_autoupdate_check_button": "Check For Upgrades", "gui_settings_autoupdate_check_button": "Check for New Version",
"gui_settings_general_label": "General settings", "gui_settings_general_label": "General settings",
"gui_settings_sharing_label": "Sharing settings", "gui_settings_sharing_label": "Sharing settings",
"gui_settings_close_after_first_download_option": "Stop sharing after first download", "gui_settings_close_after_first_download_option": "Stop sharing after first download",
"gui_settings_connection_type_label": "How should OnionShare connect to Tor?", "gui_settings_connection_type_label": "How should OnionShare connect to Tor?",
"gui_settings_connection_type_bundled_option": "Use the Tor version that is bundled with OnionShare", "gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare",
"gui_settings_connection_type_automatic_option": "Attempt automatic configuration with Tor Browser", "gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser",
"gui_settings_connection_type_control_port_option": "Connect using control port", "gui_settings_connection_type_control_port_option": "Connect using control port",
"gui_settings_connection_type_socket_file_option": "Connect using socket file", "gui_settings_connection_type_socket_file_option": "Connect using socket file",
"gui_settings_connection_type_test_button": "Test Tor Settings", "gui_settings_connection_type_test_button": "Test Connection to Tor",
"gui_settings_control_port_label": "Control port", "gui_settings_control_port_label": "Control port",
"gui_settings_socket_file_label": "Socket file", "gui_settings_socket_file_label": "Socket file",
"gui_settings_socks_label": "SOCKS port", "gui_settings_socks_label": "SOCKS port",
@ -95,77 +91,77 @@
"gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication", "gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication",
"gui_settings_authenticate_password_option": "Password", "gui_settings_authenticate_password_option": "Password",
"gui_settings_password_label": "Password", "gui_settings_password_label": "Password",
"gui_settings_tor_bridges": "Tor Bridge support", "gui_settings_tor_bridges": "Tor bridge support",
"gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges", "gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges",
"gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports", "gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports",
"gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)", "gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)",
"gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports", "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports",
"gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)", "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)",
"gui_settings_meek_lite_expensive_warning": "Warning: the meek_lite bridges are very costly for the Tor Project to run!<br><br>You should only use meek_lite bridges if you are having trouble connecting to Tor directly, via obfs4 transports or other normal bridges.", "gui_settings_meek_lite_expensive_warning": "Warning: The meek_lite bridges are very costly for the Tor Project to run.<br><br>Only use them if unable to connect to Tor directly, via obfs4 transports, or other normal bridges.",
"gui_settings_tor_bridges_custom_radio_option": "Use custom bridges", "gui_settings_tor_bridges_custom_radio_option": "Use custom bridges",
"gui_settings_tor_bridges_custom_label": "You can get bridges from <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>", "gui_settings_tor_bridges_custom_label": "You can get bridges from <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>",
"gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to work.\nPlease try again by double-checking them or adding other ones.", "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.",
"gui_settings_button_save": "Save", "gui_settings_button_save": "Save",
"gui_settings_button_cancel": "Cancel", "gui_settings_button_cancel": "Cancel",
"gui_settings_button_help": "Help", "gui_settings_button_help": "Help",
"gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer", "gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer",
"gui_settings_shutdown_timeout": "Stop the share at:", "gui_settings_shutdown_timeout": "Stop the share at:",
"settings_saved": "Settings saved to {}", "settings_saved": "Settings saved in {}",
"settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.", "settings_error_unknown": "Can't connect to Tor controller because your settings don't make sense.",
"settings_error_automatic": "Can't connect to Tor controller. Is Tor Browser running in the background? If you don't have it you can get it from:\nhttps://www.torproject.org/.", "settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?",
"settings_error_socket_port": "Can't connect to Tor controller on {}:{}.", "settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.",
"settings_error_socket_file": "Can't connect to Tor controller using socket file {}.", "settings_error_socket_file": "Can't connect to the Tor controller using socket file {}.",
"settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?", "settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?",
"settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.", "settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.",
"settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user lacks permission to read the cookie file.", "settings_error_unreadable_cookie_file": "Connected to the Tor controller, but password may be wrong, or your user is not permitted to read the cookie file.",
"settings_error_bundled_tor_not_supported": "Use of the Tor version bundled with OnionShare is not supported when using developer mode on Windows or macOS.", "settings_error_bundled_tor_not_supported": "Using the Tor version that comes with OnionShare does not work in developer mode on Windows or macOS.",
"settings_error_bundled_tor_timeout": "Connecting to Tor is taking too long. Maybe your computer is offline, or your system clock isn't accurate.", "settings_error_bundled_tor_timeout": "Taking too long to connect to Tor. Maybe you aren't connected to the Internet, or have an inaccurate system clock?",
"settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}", "settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}",
"settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}", "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.\nSupports next-gen .onion addresses: {}.",
"error_tor_protocol_error": "There was an error with Tor: {}", "error_tor_protocol_error": "There was an error with Tor: {}",
"error_tor_protocol_error_unknown": "There was an unknown error with Tor", "error_tor_protocol_error_unknown": "There was an unknown error with Tor",
"error_invalid_private_key": "This private key type is unsupported", "error_invalid_private_key": "This private key type is unsupported",
"connecting_to_tor": "Connecting to the Tor network", "connecting_to_tor": "Connecting to the Tor network",
"update_available": "A new version of OnionShare is available. <a href='{}'>Click here</a> to download it.<br><br>Installed version: {}<br>Latest version: {}", "update_available": "New OnionShare out. <a href='{}'>Click here</a> to get it.<br><br>You are using {} and the latest is {}.",
"update_error_check_error": "Error checking for updates: Maybe you're not connected to Tor, or maybe the OnionShare website is down.", "update_error_check_error": "Could not check for new versions: The OnionShare website is saying the latest version is the unrecognizable '{}'…",
"update_error_invalid_latest_version": "Error checking for updates: The OnionShare website responded saying the latest version is '{}', but that doesn't appear to be a valid version string.", "update_error_invalid_latest_version": "Could not check for new version: Maybe you're not connected to Tor, or the OnionShare website is down?",
"update_not_available": "You are running the latest version of OnionShare.", "update_not_available": "You are running the latest OnionShare.",
"gui_tor_connection_ask": "Would you like to open OnionShare settings to troubleshoot connecting to Tor?", "gui_tor_connection_ask": "Open the settings to sort out connection to Tor?",
"gui_tor_connection_ask_open_settings": "Open Settings", "gui_tor_connection_ask_open_settings": "Yes",
"gui_tor_connection_ask_quit": "Quit", "gui_tor_connection_ask_quit": "Quit",
"gui_tor_connection_error_settings": "Try adjusting how OnionShare connects to the Tor network in Settings.", "gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.",
"gui_tor_connection_canceled": "OnionShare could not connect to Tor.\n\nMake sure you're connected to the Internet, then re-open OnionShare to set up the Tor connection.", "gui_tor_connection_canceled": "Could not connect to Tor.\n\nEnsure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.",
"gui_tor_connection_lost": "Disconnected from Tor.", "gui_tor_connection_lost": "Disconnected from Tor.",
"gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.", "gui_server_started_after_timeout": "The auto-stop timer ran out before the server started.\nPlease make a new share.",
"gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.", "gui_server_timeout_expired": "The auto-stop timer already ran out.\nPlease update it to start sharing.",
"share_via_onionshare": "Share via OnionShare", "share_via_onionshare": "OnionShare it",
"gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses",
"gui_save_private_key_checkbox": "Use a persistent address (legacy)", "gui_save_private_key_checkbox": "Use a persistent address (legacy)",
"gui_share_url_description": "<b>Anyone</b> with this link can <b>download</b> your files using the <b>Tor Browser</b>: <img src='{}' />", "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_receive_url_description": "<b>Anyone</b> with this link can <b>upload</b> files to your computer 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 expire automatically unless a timer is set.<br><br>Every share will have the same address (to use one-time addresses, disable persistence in Settings)", "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 expire automatically unless a timer is set.", "gui_url_label_stay_open": "This share will not auto-stop.",
"gui_url_label_onetime": "This share will expire after the first download", "gui_url_label_onetime": "This share will stop after first completion.",
"gui_url_label_onetime_and_persistent": "This share will expire after the first download<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)", "gui_url_label_onetime_and_persistent": "This share will not auto-stop.<br><br>Every subsequent share will reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)",
"gui_status_indicator_share_stopped": "Ready to Share", "gui_status_indicator_share_stopped": "Ready to share",
"gui_status_indicator_share_working": "Starting…", "gui_status_indicator_share_working": "Starting…",
"gui_status_indicator_share_started": "Sharing", "gui_status_indicator_share_started": "Sharing",
"gui_status_indicator_receive_stopped": "Ready to Receive", "gui_status_indicator_receive_stopped": "Ready to receive",
"gui_status_indicator_receive_working": "Starting…", "gui_status_indicator_receive_working": "Starting…",
"gui_status_indicator_receive_started": "Receiving", "gui_status_indicator_receive_started": "Receiving",
"gui_file_info": "{} Files, {}", "gui_file_info": "{} files, {}",
"gui_file_info_single": "{} File, {}", "gui_file_info_single": "{} file, {}",
"info_in_progress_downloads_tooltip": "{} download(s) in progress", "history_in_progress_tooltip": "{} in progress",
"info_completed_downloads_tooltip": "{} download(s) completed", "history_completed_tooltip": "{} completed",
"info_in_progress_uploads_tooltip": "{} upload(s) in progress", "info_in_progress_uploads_tooltip": "{} upload(s) in progress",
"info_completed_uploads_tooltip": "{} upload(s) completed", "info_completed_uploads_tooltip": "{} upload(s) completed",
"error_cannot_create_downloads_dir": "Error creating downloads folder: {}", "error_cannot_create_downloads_dir": "Could not create receive mode folder: {}",
"error_downloads_dir_not_writable": "The downloads folder isn't writable: {}", "error_downloads_dir_not_writable": "The receive mode folder is write protected: {}",
"receive_mode_downloads_dir": "Files people send you will appear in this folder: {}", "receive_mode_downloads_dir": "Files sent to you appear in this folder: {}",
"receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer. Some files can hack your computer if you open them! Only open files from people you trust, or if you know what you're doing.", "receive_mode_warning": "Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.",
"gui_receive_mode_warning": "<b>Some files can hack your computer if you open them!</b><br>Only open files from people you trust, or if you know what you're doing.", "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>",
"receive_mode_upload_starting": "Upload of total size {} is starting", "receive_mode_upload_starting": "Upload of total size {} is starting",
"receive_mode_received_file": "Received file: {}", "receive_mode_received_file": "Received: {}",
"gui_mode_share_button": "Share Files", "gui_mode_share_button": "Share Files",
"gui_mode_receive_button": "Receive Files", "gui_mode_receive_button": "Receive Files",
"gui_settings_receiving_label": "Receiving settings", "gui_settings_receiving_label": "Receiving settings",
@ -179,10 +175,11 @@
"systray_download_page_loaded_message": "A user loaded the download page", "systray_download_page_loaded_message": "A user loaded the download page",
"systray_upload_page_loaded_message": "A user loaded the upload page", "systray_upload_page_loaded_message": "A user loaded the upload page",
"gui_uploads": "Upload History", "gui_uploads": "Upload History",
"gui_uploads_window_tooltip": "Show/hide uploads", "gui_no_uploads": "No Uploads Yet",
"gui_no_uploads": "No uploads yet.", "gui_clear_history": "Clear All",
"gui_upload_in_progress": "Upload Started {}", "gui_upload_in_progress": "Upload Started {}",
"gui_upload_finished_range": "Uploaded {} to {}", "gui_upload_finished_range": "Uploaded {} to {}",
"gui_upload_finished": "Uploaded {}", "gui_upload_finished": "Uploaded {}",
"gui_open_folder_error_nautilus": "Cannot open folder the because nautilus is not available. You can find this file here: {}" "gui_download_in_progress": "Download Started {}",
"gui_open_folder_error_nautilus": "Cannot open folder because nautilus is not available. The file is here: {}"
} }

View File

@ -1,7 +1,6 @@
{ {
"config_onion_service": "Agordas onion service je pordo {0:d}.", "config_onion_service": "Agordas onion service je pordo {0:d}.",
"preparing_files": "Preparas dosierojn por kundivido.", "preparing_files": "Preparas dosierojn por kundivido.",
"wait_for_hs": "Atendas al hidden sevice por esti preta:",
"give_this_url": "Donu ĉi tiun URL al la persono al kiu vi sendas la dosieron:", "give_this_url": "Donu ĉi tiun URL al la persono al kiu vi sendas la dosieron:",
"give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:", "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
"ctrlc_to_stop": "Presu Ctrl-C por halti la servilon", "ctrlc_to_stop": "Presu Ctrl-C por halti la servilon",
@ -27,7 +26,6 @@
"gui_copied_url": "URL kopiita en tondujon", "gui_copied_url": "URL kopiita en tondujon",
"gui_copied_hidservauth": "Copied HidServAuth line to clipboard", "gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
"gui_please_wait": "Bonvolu atendi...", "gui_please_wait": "Bonvolu atendi...",
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
"gui_download_upload_progress_complete": "%p%, Tempo pasinta: {0:s}", "gui_download_upload_progress_complete": "%p%, Tempo pasinta: {0:s}",
"gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)",
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",

View File

@ -1,6 +1,5 @@
{ {
"preparing_files": "Preparando los archivos para compartir.", "preparing_files": "Preparando los archivos para compartir.",
"wait_for_hs": "Esperando a que HS esté listo:",
"give_this_url": "Entregue esta URL a la persona a la que está enviando el archivo:", "give_this_url": "Entregue esta URL a la persona a la que está enviando el archivo:",
"ctrlc_to_stop": "Pulse Ctrl-C para detener el servidor", "ctrlc_to_stop": "Pulse Ctrl-C para detener el servidor",
"not_a_file": "{0:s} no es un archivo.", "not_a_file": "{0:s} no es un archivo.",

View File

@ -1,6 +1,5 @@
{ {
"preparing_files": "Valmistellaan tiedostoja jaettavaksi.", "preparing_files": "Valmistellaan tiedostoja jaettavaksi.",
"wait_for_hs": "Odotetaan piilopalvelun valmistumista:",
"give_this_url": "Anna tämä URL-osoite henkilölle, jolle lähetät tiedostot:", "give_this_url": "Anna tämä URL-osoite henkilölle, jolle lähetät tiedostot:",
"ctrlc_to_stop": "Näppäin Ctrl-C pysäyttää palvelimen", "ctrlc_to_stop": "Näppäin Ctrl-C pysäyttää palvelimen",
"not_a_file": "{0:s} Ei ole tiedosto.", "not_a_file": "{0:s} Ei ole tiedosto.",
@ -22,6 +21,5 @@
"gui_canceled": "Peruutettu", "gui_canceled": "Peruutettu",
"gui_copied_url": "URL-osoite kopioitu leikepöydälle", "gui_copied_url": "URL-osoite kopioitu leikepöydälle",
"gui_please_wait": "Odota...", "gui_please_wait": "Odota...",
"using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua",
"zip_progress_bar_format": "Tiivistän tiedostoja: %p%" "zip_progress_bar_format": "Tiivistän tiedostoja: %p%"
} }

View File

@ -1,6 +1,5 @@
{ {
"preparing_files": "Préparation des fichiers à partager.", "preparing_files": "Préparation des fichiers à partager.",
"wait_for_hs": "En attente du HS:",
"give_this_url": "Donnez cette URL à la personne qui doit recevoir le fichier :", "give_this_url": "Donnez cette URL à la personne qui doit recevoir le fichier :",
"ctrlc_to_stop": "Ctrl-C arrête le serveur", "ctrlc_to_stop": "Ctrl-C arrête le serveur",
"not_a_file": "{0:s} n'est pas un fichier.", "not_a_file": "{0:s} n'est pas un fichier.",

View File

@ -1,6 +1,5 @@
{ {
"preparing_files": "Preparazione dei files da condividere.", "preparing_files": "Preparazione dei files da condividere.",
"wait_for_hs": "In attesa che l'HS sia pronto:",
"give_this_url": "Dai questo URL alla persona a cui vuoi inviare il file:", "give_this_url": "Dai questo URL alla persona a cui vuoi inviare il file:",
"ctrlc_to_stop": "Premi Ctrl-C per fermare il server", "ctrlc_to_stop": "Premi Ctrl-C per fermare il server",
"not_a_file": "{0:s} non è un file.", "not_a_file": "{0:s} non è un file.",
@ -22,6 +21,5 @@
"gui_canceled": "Cancellati", "gui_canceled": "Cancellati",
"gui_copied_url": "URL Copiato nella clipboard", "gui_copied_url": "URL Copiato nella clipboard",
"gui_please_wait": "Attendere prego...", "gui_please_wait": "Attendere prego...",
"using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione",
"zip_progress_bar_format": "Elaborazione files: %p%" "zip_progress_bar_format": "Elaborazione files: %p%"
} }

View File

@ -1,7 +1,6 @@
{ {
"config_onion_service": "Onion service configureren op poort {0:d}.", "config_onion_service": "Onion service configureren op poort {0:d}.",
"preparing_files": "Bestanden om te delen aan het voorbereiden.", "preparing_files": "Bestanden om te delen aan het voorbereiden.",
"wait_for_hs": "Wachten op gereed zijn van HS:",
"give_this_url": "Geef deze URL aan de persoon aan wie je dit bestand verzend:", "give_this_url": "Geef deze URL aan de persoon aan wie je dit bestand verzend:",
"give_this_url_stealth": "Geef deze URL en de HidServAuth regel aan de persoon aan wie je dit bestand verzend:", "give_this_url_stealth": "Geef deze URL en de HidServAuth regel aan de persoon aan wie je dit bestand verzend:",
"ctrlc_to_stop": "Druk Ctrl-C om de server te stoppen", "ctrlc_to_stop": "Druk Ctrl-C om de server te stoppen",
@ -38,7 +37,6 @@
"gui_copied_url": "URL gekopieerd naar klembord", "gui_copied_url": "URL gekopieerd naar klembord",
"gui_copied_hidservauth": "HidServAuth regel gekopieerd naar klembord", "gui_copied_hidservauth": "HidServAuth regel gekopieerd naar klembord",
"gui_please_wait": "Moment geduld...", "gui_please_wait": "Moment geduld...",
"using_ephemeral": "Kortstondige Tor onion service gestart en in afwachting van publicatie",
"gui_download_upload_progress_complete": "%p%, Tijd verstreken: {0:s}", "gui_download_upload_progress_complete": "%p%, Tijd verstreken: {0:s}",
"gui_download_upload_progress_starting": "{0:s}, %p% (ETA berekenen)", "gui_download_upload_progress_starting": "{0:s}, %p% (ETA berekenen)",
"gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%",
@ -51,9 +49,7 @@
"error_stealth_not_supported": "Om een geheime onion service te maken heb je minstens Tor 0.2.9.1-alpha (of Tor Browser 6.5) en minstens python3-stem 1.5.0 nodig.", "error_stealth_not_supported": "Om een geheime onion service te maken heb je minstens Tor 0.2.9.1-alpha (of Tor Browser 6.5) en minstens python3-stem 1.5.0 nodig.",
"error_ephemeral_not_supported": "OnionShare vereist minstens Tor 0.2.7.1 en minstens python3-stem 1.4.0.", "error_ephemeral_not_supported": "OnionShare vereist minstens Tor 0.2.7.1 en minstens python3-stem 1.4.0.",
"gui_settings_window_title": "Instellingen", "gui_settings_window_title": "Instellingen",
"gui_settings_stealth_label": "Stealth (geavanceerd)",
"gui_settings_stealth_option": "Maak stealth onion services", "gui_settings_stealth_option": "Maak stealth onion services",
"gui_settings_stealth_option_details": "Dit maakt OnionShare veiliger, maar ook lastiger voor de ontvanger om te verbinden.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">Meer informatie</a>.",
"gui_settings_autoupdate_label": "Controleer voor updates", "gui_settings_autoupdate_label": "Controleer voor updates",
"gui_settings_autoupdate_option": "Notificeer me als er updates beschikbaar zijn", "gui_settings_autoupdate_option": "Notificeer me als er updates beschikbaar zijn",
"gui_settings_autoupdate_timestamp": "Laatste controle: {}", "gui_settings_autoupdate_timestamp": "Laatste controle: {}",

View File

@ -1,6 +1,5 @@
{ {
"preparing_files": "Paylaşmak için dosyalar hazırlanıyor.", "preparing_files": "Paylaşmak için dosyalar hazırlanıyor.",
"wait_for_hs": "GH hazır olması bekleniyor:",
"give_this_url": "Dosyayı gönderdiğin kişiye bu URL'i verin:", "give_this_url": "Dosyayı gönderdiğin kişiye bu URL'i verin:",
"ctrlc_to_stop": "Sunucuyu durdurmak için, Ctrl-C basın", "ctrlc_to_stop": "Sunucuyu durdurmak için, Ctrl-C basın",
"not_a_file": "{0:s} dosya değil.", "not_a_file": "{0:s} dosya değil.",
@ -22,6 +21,5 @@
"gui_canceled": "İptal edilen", "gui_canceled": "İptal edilen",
"gui_copied_url": "Panoya kopyalanan URL", "gui_copied_url": "Panoya kopyalanan URL",
"gui_please_wait": "Lütfen bekleyin...", "gui_please_wait": "Lütfen bekleyin...",
"using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor",
"zip_progress_bar_format": "Dosyalar hazırlanıyor: %p%" "zip_progress_bar_format": "Dosyalar hazırlanıyor: %p%"
} }

View File

@ -10,18 +10,18 @@
<body> <body>
<header class="clearfix"> <header class="clearfix">
<div class="right"> <div class="right">
<ul> <ul>
<li>Total size: <strong>{{ filesize_human }}</strong> (compressed)</li> <li>Total size: <strong>{{ filesize_human }}</strong> {% if is_zipped %} (compressed){% endif %}</li>
{% if slug %} {% if slug %}
<li><a class="button" href='/{{ slug }}/download'>Download Files</a></li> <li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
{% else %} {% else %}
<li><a class="button" href='/download'>Download Files</a></li> <li><a class="button" href='/download'>Download Files</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
<img class="logo" src="/static/img/logo.png" title="OnionShare"> <img class="logo" src="/static/img/logo.png" title="OnionShare">
<h1>OnionShare</h1> <h1>OnionShare</h1>
</header> </header>
<table class="file-list" id="file-list"> <table class="file-list" id="file-list">

View File

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

View File

@ -64,7 +64,7 @@ def temp_file_1024_delete():
# pytest > 2.9 only needs @pytest.fixture # pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session') @pytest.yield_fixture(scope='session')
def custom_zw(): def custom_zw():
zw = web.ZipWriter( zw = web.share_mode.ZipWriter(
common.Common(), common.Common(),
zip_filename=common.Common.random_string(4, 6), zip_filename=common.Common.random_string(4, 6),
processed_size_callback=lambda _: 'custom_callback' processed_size_callback=lambda _: 'custom_callback'
@ -77,7 +77,7 @@ def custom_zw():
# pytest > 2.9 only needs @pytest.fixture # pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session') @pytest.yield_fixture(scope='session')
def default_zw(): def default_zw():
zw = web.ZipWriter(common.Common()) zw = web.share_mode.ZipWriter(common.Common())
yield zw yield zw
zw.close() zw.close()
tmp_dir = os.path.dirname(zw.zip_filename) tmp_dir = os.path.dirname(zw.zip_filename)

View File

@ -47,22 +47,14 @@ class TestLoadStrings:
self, common_obj, locale_en, sys_onionshare_dev_mode): self, common_obj, locale_en, sys_onionshare_dev_mode):
""" load_strings() loads English by default """ """ load_strings() loads English by default """
strings.load_strings(common_obj) strings.load_strings(common_obj)
assert strings._('wait_for_hs') == "Waiting for HS to be ready:" assert strings._('preparing_files') == "Compressing files."
def test_load_strings_loads_other_languages( def test_load_strings_loads_other_languages(
self, common_obj, locale_fr, sys_onionshare_dev_mode): self, common_obj, locale_fr, sys_onionshare_dev_mode):
""" load_strings() loads other languages in different locales """ """ load_strings() loads other languages in different locales """
strings.load_strings(common_obj, "fr") strings.load_strings(common_obj, "fr")
assert strings._('wait_for_hs') == "En attente du HS:" assert strings._('preparing_files') == "Préparation des fichiers à partager."
def test_load_partial_strings(
self, common_obj, locale_ru, sys_onionshare_dev_mode):
strings.load_strings(common_obj)
assert strings._("give_this_url") == (
"Отправьте эту ссылку тому человеку, "
"которому вы хотите передать файл:")
assert strings._('wait_for_hs') == "Waiting for HS to be ready:"
def test_load_invalid_locale( def test_load_invalid_locale(
self, common_obj, locale_invalid, sys_onionshare_dev_mode): self, common_obj, locale_invalid, sys_onionshare_dev_mode):

View File

@ -38,11 +38,11 @@ DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$')
RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$') RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$')
def web_obj(common_obj, receive_mode, num_files=0): def web_obj(common_obj, mode, num_files=0):
""" Creates a Web object, in either share mode or receive mode, ready for testing """ """ Creates a Web object, in either share mode or receive mode, ready for testing """
common_obj.load_settings() common_obj.load_settings()
web = Web(common_obj, False, receive_mode) web = Web(common_obj, False, mode)
web.generate_slug() web.generate_slug()
web.stay_open = True web.stay_open = True
web.running = True web.running = True
@ -50,14 +50,14 @@ def web_obj(common_obj, receive_mode, num_files=0):
web.app.testing = True web.app.testing = True
# Share mode # Share mode
if not receive_mode: if mode == 'share':
# Add files # Add files
files = [] files = []
for i in range(num_files): for i in range(num_files):
with tempfile.NamedTemporaryFile(delete=False) as tmp_file: with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
tmp_file.write(b'*' * 1024) tmp_file.write(b'*' * 1024)
files.append(tmp_file.name) files.append(tmp_file.name)
web.set_file_info(files) web.share_mode.set_file_info(files)
# Receive mode # Receive mode
else: else:
pass pass
@ -67,8 +67,8 @@ def web_obj(common_obj, receive_mode, num_files=0):
class TestWeb: class TestWeb:
def test_share_mode(self, common_obj): def test_share_mode(self, common_obj):
web = web_obj(common_obj, False, 3) web = web_obj(common_obj, 'share', 3)
assert web.receive_mode is False assert web.mode is 'share'
with web.app.test_client() as c: with web.app.test_client() as c:
# Load 404 pages # Load 404 pages
res = c.get('/') res = c.get('/')
@ -91,7 +91,7 @@ class TestWeb:
assert res.mimetype == 'application/zip' assert res.mimetype == 'application/zip'
def test_share_mode_close_after_first_download_on(self, common_obj, temp_file_1024): def test_share_mode_close_after_first_download_on(self, common_obj, temp_file_1024):
web = web_obj(common_obj, False, 3) web = web_obj(common_obj, 'share', 3)
web.stay_open = False web.stay_open = False
assert web.running == True assert web.running == True
@ -106,7 +106,7 @@ class TestWeb:
assert web.running == False assert web.running == False
def test_share_mode_close_after_first_download_off(self, common_obj, temp_file_1024): def test_share_mode_close_after_first_download_off(self, common_obj, temp_file_1024):
web = web_obj(common_obj, False, 3) web = web_obj(common_obj, 'share', 3)
web.stay_open = True web.stay_open = True
assert web.running == True assert web.running == True
@ -120,8 +120,8 @@ class TestWeb:
assert web.running == True assert web.running == True
def test_receive_mode(self, common_obj): def test_receive_mode(self, common_obj):
web = web_obj(common_obj, True) web = web_obj(common_obj, 'receive')
assert web.receive_mode is True assert web.mode is 'receive'
with web.app.test_client() as c: with web.app.test_client() as c:
# Load 404 pages # Load 404 pages
@ -139,7 +139,7 @@ class TestWeb:
assert res.status_code == 200 assert res.status_code == 200
def test_receive_mode_allow_receiver_shutdown_on(self, common_obj): def test_receive_mode_allow_receiver_shutdown_on(self, common_obj):
web = web_obj(common_obj, True) web = web_obj(common_obj, 'receive')
common_obj.settings.set('receive_allow_receiver_shutdown', True) common_obj.settings.set('receive_allow_receiver_shutdown', True)
@ -154,7 +154,7 @@ class TestWeb:
assert web.running == False assert web.running == False
def test_receive_mode_allow_receiver_shutdown_off(self, common_obj): def test_receive_mode_allow_receiver_shutdown_off(self, common_obj):
web = web_obj(common_obj, True) web = web_obj(common_obj, 'receive')
common_obj.settings.set('receive_allow_receiver_shutdown', False) common_obj.settings.set('receive_allow_receiver_shutdown', False)
@ -167,9 +167,9 @@ class TestWeb:
# Should redirect to index, and server should still be running # Should redirect to index, and server should still be running
assert res.status_code == 302 assert res.status_code == 302
assert web.running == True assert web.running == True
def test_public_mode_on(self, common_obj): def test_public_mode_on(self, common_obj):
web = web_obj(common_obj, True) web = web_obj(common_obj, 'receive')
common_obj.settings.set('public_mode', True) common_obj.settings.set('public_mode', True)
with web.app.test_client() as c: with web.app.test_client() as c:
@ -182,9 +182,9 @@ class TestWeb:
res = c.get('/{}'.format(web.slug)) res = c.get('/{}'.format(web.slug))
data2 = res.get_data() data2 = res.get_data()
assert res.status_code == 404 assert res.status_code == 404
def test_public_mode_off(self, common_obj): def test_public_mode_off(self, common_obj):
web = web_obj(common_obj, True) web = web_obj(common_obj, 'receive')
common_obj.settings.set('public_mode', False) common_obj.settings.set('public_mode', False)
with web.app.test_client() as c: with web.app.test_client() as c:

View File

@ -0,0 +1 @@
from .commontests import CommonTests

View File

@ -0,0 +1,291 @@
import os
import requests
import socket
import socks
import zipfile
from PyQt5 import QtCore, QtTest
from onionshare import strings
from onionshare_gui.mode.receive_mode import ReceiveMode
from onionshare_gui.mode.share_mode import ShareMode
class CommonTests(object):
def test_gui_loaded(self):
'''Test that the GUI actually is shown'''
self.assertTrue(self.gui.show)
def test_windowTitle_seen(self):
'''Test that the window title is OnionShare'''
self.assertEqual(self.gui.windowTitle(), 'OnionShare')
def test_settings_button_is_visible(self):
'''Test that the settings button is visible'''
self.assertTrue(self.gui.settings_button.isVisible())
def test_server_status_bar_is_visible(self):
'''Test that the status bar is visible'''
self.assertTrue(self.gui.status_bar.isVisible())
def test_click_mode(self, mode):
'''Test that we can switch Mode by clicking the button'''
if type(mode) == ReceiveMode:
QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton)
self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE)
if type(mode) == ShareMode:
QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton)
self.assertTrue(self.gui.mode, self.gui.MODE_SHARE)
def test_click_toggle_history(self, mode):
'''Test that we can toggle Download or Upload history by clicking the toggle button'''
currently_visible = mode.history.isVisible()
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
self.assertEqual(mode.history.isVisible(), not currently_visible)
def test_history_indicator(self, mode, public_mode):
'''Test that we can make sure the history is toggled off, do an action, and the indiciator works'''
# Make sure history is toggled off
if mode.history.isVisible():
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
self.assertFalse(mode.history.isVisible())
# Indicator should not be visible yet
self.assertFalse(mode.toggle_history.indicator_label.isVisible())
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)
else:
path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
response = requests.post(path, files=files)
QtTest.QTest.qWait(2000)
if type(mode) == ShareMode:
# Download files
if public_mode:
url = "http://127.0.0.1:{}/download".format(self.gui.app.port)
else:
url = "http://127.0.0.1:{}/{}/download".format(self.gui.app.port, self.gui.share_mode.web.slug)
r = requests.get(url)
QtTest.QTest.qWait(2000)
# Indicator should be visible, have a value of "1"
self.assertTrue(mode.toggle_history.indicator_label.isVisible())
self.assertEqual(mode.toggle_history.indicator_label.text(), "1")
# Toggle history back on, indicator should be hidden again
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
self.assertFalse(mode.toggle_history.indicator_label.isVisible())
def test_history_is_not_visible(self, mode):
'''Test that the History section is not visible'''
self.assertFalse(mode.history.isVisible())
def test_history_is_visible(self, mode):
'''Test that the History section is visible'''
self.assertTrue(mode.history.isVisible())
def test_server_working_on_start_button_pressed(self, mode):
'''Test we can start the service'''
# Should be in SERVER_WORKING state
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
self.assertEqual(mode.server_status.status, 1)
def test_server_status_indicator_says_starting(self, mode):
'''Test that the Server Status indicator shows we are Starting'''
self.assertEquals(mode.server_status_label.text(), strings._('gui_status_indicator_share_working'))
def test_settings_button_is_hidden(self):
'''Test that the settings button is hidden when the server starts'''
self.assertFalse(self.gui.settings_button.isVisible())
def test_a_server_is_started(self, mode):
'''Test that the server has started'''
QtTest.QTest.qWait(2000)
# Should now be in SERVER_STARTED state
self.assertEqual(mode.server_status.status, 2)
def test_a_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)
def test_have_a_slug(self, mode, public_mode):
'''Test that we have a valid slug'''
if not public_mode:
self.assertRegex(mode.server_status.web.slug, r'(\w+)-(\w+)')
else:
self.assertIsNone(mode.server_status.web.slug, r'(\w+)-(\w+)')
def test_url_description_shown(self, mode):
'''Test that the URL label is showing'''
self.assertTrue(mode.server_status.url_description.isVisible())
def test_have_copy_url_button(self, mode):
'''Test that the Copy URL button is shown'''
self.assertTrue(mode.server_status.copy_url_button.isVisible())
def test_server_status_indicator_says_started(self, mode):
'''Test that the Server Status indicator shows we are started'''
if type(mode) == ReceiveMode:
self.assertEquals(mode.server_status_label.text(), strings._('gui_status_indicator_receive_started'))
if type(mode) == ShareMode:
self.assertEquals(mode.server_status_label.text(), strings._('gui_status_indicator_share_started'))
def test_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)
else:
path = '/'
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()
def test_history_widgets_present(self, mode):
'''Test that the relevant widgets are present in the history view after activity has taken place'''
self.assertFalse(mode.history.empty.isVisible())
self.assertTrue(mode.history.not_empty.isVisible())
def test_counter_incremented(self, mode, count):
'''Test that the counter has incremented'''
self.assertEquals(mode.history.completed_count, count)
def test_server_is_stopped(self, mode, stay_open):
'''Test that the server stops when we click Stop'''
if type(mode) == ReceiveMode or (type(mode) == ShareMode and stay_open):
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
self.assertEquals(mode.server_status.status, 0)
def test_web_service_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)
def test_server_status_indicator_says_closed(self, mode, stay_open):
'''Test that the Server Status indicator shows we closed'''
if type(mode) == ReceiveMode:
self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True))
if type(mode) == ShareMode:
if stay_open:
self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True))
else:
self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True))
# Auto-stop timer tests
def test_set_timeout(self, mode, timeout):
'''Test that the timeout can be set'''
timer = QtCore.QDateTime.currentDateTime().addSecs(timeout)
mode.server_status.shutdown_timeout.setDateTime(timer)
self.assertTrue(mode.server_status.shutdown_timeout.dateTime(), timer)
def test_timeout_widget_hidden(self, mode):
'''Test that the timeout widget is hidden when share has started'''
self.assertFalse(mode.server_status.shutdown_timeout_container.isVisible())
def test_server_timed_out(self, mode, wait):
'''Test that the server has timed out after the timer ran out'''
QtTest.QTest.qWait(wait)
# We should have timed out now
self.assertEqual(mode.server_status.status, 0)
# Receive-specific tests
def test_upload_file(self, public_mode, expected_file):
'''Test that we can upload the file'''
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)
else:
path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
response = requests.post(path, files=files)
QtTest.QTest.qWait(2000)
self.assertTrue(os.path.isfile(expected_file))
# Share-specific tests
def test_file_selection_widget_has_a_file(self):
'''Test that the number of files in the list is 1'''
self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1)
def test_deleting_only_file_hides_delete_button(self):
'''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button'''
rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0))
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center())
# Delete button should be visible
self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
# Click delete, and since there's no more files, the delete button should be hidden
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton)
self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
def test_add_a_file_and_delete_using_its_delete_widget(self):
'''Test that we can also delete a file by clicking on its [X] widget'''
self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts')
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton)
self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 0)
def test_file_selection_widget_readd_files(self):
'''Re-add some files to the list so we can share'''
self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts')
self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt')
self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2)
def test_add_delete_buttons_hidden(self):
'''Test that the add and delete buttons are hidden when the server starts'''
self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible())
self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
def test_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))
if public_mode:
path = '/download'
else:
path = '{}/download'.format(self.gui.share_mode.web.slug)
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/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')
QtTest.QTest.qWait(2000)
self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8'))
def test_add_button_visible(self):
'''Test that the add button should be visible'''
self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible())

160
tests_gui_local/conftest.py Normal file
View File

@ -0,0 +1,160 @@
import sys
# Force tests to look for resources in the source code tree
sys.onionshare_dev_mode = True
import os
import shutil
import tempfile
import pytest
from onionshare import common, web, settings
@pytest.fixture
def temp_dir_1024():
""" Create a temporary directory that has a single file of a
particular size (1024 bytes).
"""
tmp_dir = tempfile.mkdtemp()
tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
with open(tmp_file, 'wb') as f:
f.write(b'*' * 1024)
return tmp_dir
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture
def temp_dir_1024_delete():
""" Create a temporary directory that has a single file of a
particular size (1024 bytes). The temporary directory (including
the file inside) will be deleted after fixture usage.
"""
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
with open(tmp_file, 'wb') as f:
f.write(b'*' * 1024)
yield tmp_dir
@pytest.fixture
def temp_file_1024():
""" Create a temporary file of a particular size (1024 bytes). """
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
tmp_file.write(b'*' * 1024)
return tmp_file.name
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture
def temp_file_1024_delete():
"""
Create a temporary file of a particular size (1024 bytes).
The temporary file will be deleted after fixture usage.
"""
with tempfile.NamedTemporaryFile() as tmp_file:
tmp_file.write(b'*' * 1024)
tmp_file.flush()
yield tmp_file.name
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
def custom_zw():
zw = web.share_mode.ZipWriter(
common.Common(),
zip_filename=common.Common.random_string(4, 6),
processed_size_callback=lambda _: 'custom_callback'
)
yield zw
zw.close()
os.remove(zw.zip_filename)
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
def default_zw():
zw = web.share_mode.ZipWriter(common.Common())
yield zw
zw.close()
tmp_dir = os.path.dirname(zw.zip_filename)
shutil.rmtree(tmp_dir)
@pytest.fixture
def locale_en(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('en_US', 'UTF-8'))
@pytest.fixture
def locale_fr(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('fr_FR', 'UTF-8'))
@pytest.fixture
def locale_invalid(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('xx_XX', 'UTF-8'))
@pytest.fixture
def locale_ru(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('ru_RU', 'UTF-8'))
@pytest.fixture
def platform_darwin(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Darwin')
@pytest.fixture # (scope="session")
def platform_linux(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Linux')
@pytest.fixture
def platform_windows(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Windows')
@pytest.fixture
def sys_argv_sys_prefix(monkeypatch):
monkeypatch.setattr('sys.argv', [sys.prefix])
@pytest.fixture
def sys_frozen(monkeypatch):
monkeypatch.setattr('sys.frozen', True, raising=False)
@pytest.fixture
def sys_meipass(monkeypatch):
monkeypatch.setattr(
'sys._MEIPASS', os.path.expanduser('~'), raising=False)
@pytest.fixture # (scope="session")
def sys_onionshare_dev_mode(monkeypatch):
monkeypatch.setattr('sys.onionshare_dev_mode', True, raising=False)
@pytest.fixture
def time_time_100(monkeypatch):
monkeypatch.setattr('time.time', lambda: 100)
@pytest.fixture
def time_strftime(monkeypatch):
monkeypatch.setattr('time.strftime', lambda _: 'Jun 06 2013 11:05:00')
@pytest.fixture
def common_obj():
return common.Common()
@pytest.fixture
def settings_obj(sys_onionshare_dev_mode, platform_linux):
_common = common.Common()
_common.version = 'DUMMY_VERSION_1.2.3'
return settings.Settings(_common)

View File

@ -0,0 +1,190 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
os.remove('/tmp/OnionShare/test.txt')
os.remove('/tmp/OnionShare/test-2.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=6)
def test_click_mode(self):
CommonTests.test_click_mode(self, self.gui.receive_mode)
@pytest.mark.run(order=6)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, self.gui.receive_mode)
@pytest.mark.run(order=7)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, self.gui.receive_mode)
@pytest.mark.run(order=8)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, self.gui.receive_mode)
@pytest.mark.run(order=8)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.receive_mode)
@pytest.mark.run(order=9)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, self.gui.receive_mode)
@pytest.mark.run(order=10)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=11)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, self.gui.receive_mode)
@pytest.mark.run(order=12)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=14)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, self.gui.receive_mode, False)
@pytest.mark.run(order=15)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, self.gui.receive_mode)
@pytest.mark.run(order=16)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, self.gui.receive_mode)
@pytest.mark.run(order=17)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, self.gui.receive_mode)
@pytest.mark.run(order=18)
def test_web_page(self):
CommonTests.test_web_page(self, self.gui.receive_mode, 'Select the files you want to send, then click', False)
@pytest.mark.run(order=19)
def test_upload_file(self):
CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test.txt')
@pytest.mark.run(order=20)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, self.gui.receive_mode)
@pytest.mark.run(order=21)
def test_counter_incremented(self):
CommonTests.test_counter_incremented(self, self.gui.receive_mode, 1)
@pytest.mark.run(order=22)
def test_upload_same_file_is_renamed(self):
CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test-2.txt')
@pytest.mark.run(order=23)
def test_upload_count_incremented_again(self):
CommonTests.test_counter_incremented(self, self.gui.receive_mode, 2)
@pytest.mark.run(order=24)
def test_history_indicator(self):
CommonTests.test_history_indicator(self, self.gui.receive_mode, False)
@pytest.mark.run(order=25)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, self.gui.receive_mode, False)
@pytest.mark.run(order=26)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=27)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, self.gui.receive_mode, False)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,193 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": True,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
try:
os.remove('/tmp/test.txt')
os.remove('/tmp/OnionShare/test.txt')
os.remove('/tmp/OnionShare/test-2.txt')
except:
pass
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_click_mode(self):
CommonTests.test_click_mode(self, self.gui.receive_mode)
@pytest.mark.run(order=6)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, self.gui.receive_mode)
@pytest.mark.run(order=7)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, self.gui.receive_mode)
@pytest.mark.run(order=8)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, self.gui.receive_mode)
@pytest.mark.run(order=9)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.receive_mode)
@pytest.mark.run(order=10)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, self.gui.receive_mode)
@pytest.mark.run(order=11)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=12)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, self.gui.receive_mode)
@pytest.mark.run(order=13)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=14)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, self.gui.receive_mode, True)
@pytest.mark.run(order=15)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, self.gui.receive_mode)
@pytest.mark.run(order=16)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, self.gui.receive_mode)
@pytest.mark.run(order=17)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, self.gui.receive_mode)
@pytest.mark.run(order=18)
def test_web_page(self):
CommonTests.test_web_page(self, self.gui.receive_mode, 'Select the files you want to send, then click', True)
@pytest.mark.run(order=19)
def test_upload_file(self):
CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test.txt')
@pytest.mark.run(order=20)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, self.gui.receive_mode)
@pytest.mark.run(order=21)
def test_counter_incremented(self):
CommonTests.test_counter_incremented(self, self.gui.receive_mode, 1)
@pytest.mark.run(order=22)
def test_upload_same_file_is_renamed(self):
CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test-2.txt')
@pytest.mark.run(order=23)
def test_upload_count_incremented_again(self):
CommonTests.test_counter_incremented(self, self.gui.receive_mode, 2)
@pytest.mark.run(order=24)
def test_history_indicator(self):
CommonTests.test_history_indicator(self, self.gui.receive_mode, True)
@pytest.mark.run(order=25)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, self.gui.receive_mode, False)
@pytest.mark.run(order=26)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=27)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, self.gui.receive_mode, False)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,199 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, self.gui.share_mode)
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, self.gui.share_mode)
@pytest.mark.run(order=10)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=11)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=12)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=13)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
@pytest.mark.run(order=14)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
@pytest.mark.run(order=15)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=16)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=17)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
@pytest.mark.run(order=18)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=19)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, self.gui.share_mode, False)
@pytest.mark.run(order=20)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, self.gui.share_mode)
@pytest.mark.run(order=21)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, self.gui.share_mode)
@pytest.mark.run(order=22)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode)
@pytest.mark.run(order=23)
def test_web_page(self):
CommonTests.test_web_page(self, self.gui.share_mode, 'Total size', False)
@pytest.mark.run(order=24)
def test_download_share(self):
CommonTests.test_download_share(self, False)
@pytest.mark.run(order=25)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, self.gui.share_mode)
@pytest.mark.run(order=26)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, self.gui.share_mode, False)
@pytest.mark.run(order=27)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=28)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, False)
@pytest.mark.run(order=29)
def test_add_button_visible(self):
CommonTests.test_add_button_visible(self)
@pytest.mark.run(order=30)
def test_history_indicator(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
CommonTests.test_history_indicator(self, self.gui.share_mode, False)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,199 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": True,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, self.gui.share_mode)
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, self.gui.share_mode)
@pytest.mark.run(order=10)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=11)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=12)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=13)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
@pytest.mark.run(order=14)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
@pytest.mark.run(order=15)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=16)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=17)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
@pytest.mark.run(order=18)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=19)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, self.gui.share_mode, True)
@pytest.mark.run(order=20)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, self.gui.share_mode)
@pytest.mark.run(order=21)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, self.gui.share_mode)
@pytest.mark.run(order=22)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode)
@pytest.mark.run(order=23)
def test_web_page(self):
CommonTests.test_web_page(self, self.gui.share_mode, 'Total size', True)
@pytest.mark.run(order=24)
def test_download_share(self):
CommonTests.test_download_share(self, True)
@pytest.mark.run(order=25)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, self.gui.share_mode)
@pytest.mark.run(order=26)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, self.gui.share_mode, False)
@pytest.mark.run(order=27)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=28)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, False)
@pytest.mark.run(order=29)
def test_add_button_visible(self):
CommonTests.test_add_button_visible(self)
@pytest.mark.run(order=30)
def test_history_indicator(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
CommonTests.test_history_indicator(self, self.gui.share_mode, True)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,211 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": False,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": True,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, self.gui.share_mode)
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, self.gui.share_mode)
@pytest.mark.run(order=10)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=11)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=12)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=13)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
@pytest.mark.run(order=14)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
@pytest.mark.run(order=15)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=16)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=17)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
@pytest.mark.run(order=18)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=19)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, self.gui.share_mode, True)
@pytest.mark.run(order=20)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, self.gui.share_mode)
@pytest.mark.run(order=21)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, self.gui.share_mode)
@pytest.mark.run(order=22)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode)
@pytest.mark.run(order=23)
def test_web_page(self):
CommonTests.test_web_page(self, self.gui.share_mode, 'Total size', True)
@pytest.mark.run(order=24)
def test_download_share(self):
CommonTests.test_download_share(self, True)
@pytest.mark.run(order=25)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, self.gui.share_mode)
@pytest.mark.run(order=26)
def test_counter_incremented(self):
CommonTests.test_counter_incremented(self, self.gui.share_mode, 1)
@pytest.mark.run(order=27)
def test_download_share_again(self):
CommonTests.test_download_share(self, True)
@pytest.mark.run(order=28)
def test_counter_incremented_again(self):
CommonTests.test_counter_incremented(self, self.gui.share_mode, 2)
@pytest.mark.run(order=29)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, self.gui.share_mode, True)
@pytest.mark.run(order=30)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=31)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, True)
@pytest.mark.run(order=32)
def test_add_button_visible(self):
CommonTests.test_add_button_visible(self)
@pytest.mark.run(order=33)
def test_history_indicator(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
CommonTests.test_history_indicator(self, self.gui.share_mode, True)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,175 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
slug = ''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": True,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, self.gui.share_mode)
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, self.gui.share_mode)
@pytest.mark.run(order=10)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
@pytest.mark.run(order=11)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
@pytest.mark.run(order=12)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=13)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
@pytest.mark.run(order=14)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=15)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, self.gui.share_mode, False)
global slug
slug = self.gui.share_mode.server_status.web.slug
@pytest.mark.run(order=16)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, self.gui.share_mode)
@pytest.mark.run(order=17)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, self.gui.share_mode, True)
@pytest.mark.run(order=18)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=19)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, self.gui.share_mode, True)
@pytest.mark.run(order=20)
def test_server_started_again(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
@pytest.mark.run(order=21)
def test_have_same_slug(self):
'''Test that we have the same slug'''
self.assertEqual(self.gui.share_mode.server_status.web.slug, slug)
@pytest.mark.run(order=22)
def test_server_is_stopped_again(self):
CommonTests.test_server_is_stopped(self, self.gui.share_mode, True)
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=23)
def test_history_indicator(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
CommonTests.test_history_indicator(self, self.gui.share_mode, False)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,136 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": True,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, self.gui.share_mode)
@pytest.mark.run(order=8)
def test_set_timeout(self):
CommonTests.test_set_timeout(self, self.gui.share_mode, 5)
@pytest.mark.run(order=9)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, self.gui.share_mode)
@pytest.mark.run(order=10)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, self.gui.share_mode)
@pytest.mark.run(order=11)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, self.gui.share_mode)
@pytest.mark.run(order=12)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=13)
def test_timeout_widget_hidden(self):
CommonTests.test_timeout_widget_hidden(self, self.gui.share_mode)
@pytest.mark.run(order=14)
def test_timeout(self):
CommonTests.test_server_timed_out(self, self.gui.share_mode, 10000)
@pytest.mark.run(order=15)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,5 @@
#!/bin/bash
for test in `ls -1 | egrep ^onionshare_`; do
pytest $test -vvv || exit 1
done

View File

View File

@ -0,0 +1,61 @@
import os
import requests
import socket
import socks
import zipfile
from PyQt5 import QtCore, QtTest
from onionshare import strings
from tests_gui_local import CommonTests as LocalCommonTests
class CommonTests(LocalCommonTests):
def test_a_server_is_started(self, mode):
'''Test that the server has started (overriding from local tests to wait for longer)'''
QtTest.QTest.qWait(45000)
# Should now be in SERVER_STARTED state
if mode == 'receive':
self.assertEqual(self.gui.receive_mode.server_status.status, 2)
if mode == 'share':
self.assertEqual(self.gui.share_mode.server_status.status, 2)
def test_have_an_onion_service(self):
'''Test that we have a valid Onion URL'''
self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion')
def test_cancel_the_share(self, mode):
'''Test that we can cancel this share before it's started up '''
if mode == 'share':
QtTest.QTest.mousePress(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton)
QtTest.QTest.qWait(1000)
QtTest.QTest.mouseRelease(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton)
self.assertEqual(self.gui.share_mode.server_status.status, 0)
if mode == 'receive':
QtTest.QTest.mousePress(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton)
QtTest.QTest.qWait(1000)
QtTest.QTest.mouseRelease(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton)
self.assertEqual(self.gui.receive_mode.server_status.status, 0)
# Stealth tests
def test_copy_have_hidserv_auth_button(self, mode):
'''Test that the Copy HidservAuth button is shown'''
if mode == 'share':
self.assertTrue(self.gui.share_mode.server_status.copy_hidservauth_button.isVisible())
if mode == 'receive':
self.assertTrue(self.gui.receive_mode.server_status.copy_hidservauth_button.isVisible())
def test_hidserv_auth_string(self):
'''Test the validity of the HidservAuth string'''
self.assertRegex(self.gui.app.auth_string, r'HidServAuth %s [a-zA-Z1-9]' % self.gui.app.onion_host)
# Miscellaneous tests
def test_tor_killed_statusbar_message_shown(self, mode):
'''Test that the status bar message shows Tor was disconnected'''
self.gui.app.onion.cleanup(stop_tor=True)
QtTest.QTest.qWait(2500)
if mode == 'share':
self.assertTrue(self.gui.share_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True))
if mode == 'receive':
self.assertTrue(self.gui.receive_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True))

160
tests_gui_tor/conftest.py Normal file
View File

@ -0,0 +1,160 @@
import sys
# Force tests to look for resources in the source code tree
sys.onionshare_dev_mode = True
import os
import shutil
import tempfile
import pytest
from onionshare import common, web, settings
@pytest.fixture
def temp_dir_1024():
""" Create a temporary directory that has a single file of a
particular size (1024 bytes).
"""
tmp_dir = tempfile.mkdtemp()
tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
with open(tmp_file, 'wb') as f:
f.write(b'*' * 1024)
return tmp_dir
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture
def temp_dir_1024_delete():
""" Create a temporary directory that has a single file of a
particular size (1024 bytes). The temporary directory (including
the file inside) will be deleted after fixture usage.
"""
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
with open(tmp_file, 'wb') as f:
f.write(b'*' * 1024)
yield tmp_dir
@pytest.fixture
def temp_file_1024():
""" Create a temporary file of a particular size (1024 bytes). """
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
tmp_file.write(b'*' * 1024)
return tmp_file.name
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture
def temp_file_1024_delete():
"""
Create a temporary file of a particular size (1024 bytes).
The temporary file will be deleted after fixture usage.
"""
with tempfile.NamedTemporaryFile() as tmp_file:
tmp_file.write(b'*' * 1024)
tmp_file.flush()
yield tmp_file.name
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
def custom_zw():
zw = web.share_mode.ZipWriter(
common.Common(),
zip_filename=common.Common.random_string(4, 6),
processed_size_callback=lambda _: 'custom_callback'
)
yield zw
zw.close()
os.remove(zw.zip_filename)
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
def default_zw():
zw = web.share_mode.ZipWriter(common.Common())
yield zw
zw.close()
tmp_dir = os.path.dirname(zw.zip_filename)
shutil.rmtree(tmp_dir)
@pytest.fixture
def locale_en(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('en_US', 'UTF-8'))
@pytest.fixture
def locale_fr(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('fr_FR', 'UTF-8'))
@pytest.fixture
def locale_invalid(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('xx_XX', 'UTF-8'))
@pytest.fixture
def locale_ru(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('ru_RU', 'UTF-8'))
@pytest.fixture
def platform_darwin(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Darwin')
@pytest.fixture # (scope="session")
def platform_linux(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Linux')
@pytest.fixture
def platform_windows(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Windows')
@pytest.fixture
def sys_argv_sys_prefix(monkeypatch):
monkeypatch.setattr('sys.argv', [sys.prefix])
@pytest.fixture
def sys_frozen(monkeypatch):
monkeypatch.setattr('sys.frozen', True, raising=False)
@pytest.fixture
def sys_meipass(monkeypatch):
monkeypatch.setattr(
'sys._MEIPASS', os.path.expanduser('~'), raising=False)
@pytest.fixture # (scope="session")
def sys_onionshare_dev_mode(monkeypatch):
monkeypatch.setattr('sys.onionshare_dev_mode', True, raising=False)
@pytest.fixture
def time_time_100(monkeypatch):
monkeypatch.setattr('time.time', lambda: 100)
@pytest.fixture
def time_strftime(monkeypatch):
monkeypatch.setattr('time.strftime', lambda _: 'Jun 06 2013 11:05:00')
@pytest.fixture
def common_obj():
return common.Common()
@pytest.fixture
def settings_obj(sys_onionshare_dev_mode, platform_linux):
_common = common.Common()
_common.version = 'DUMMY_VERSION_1.2.3'
return settings.Settings(_common)

View File

@ -0,0 +1,197 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=6)
def test_info_widget_is_visible(self):
CommonTests.test_info_widget_is_visible(self, 'share')
@pytest.mark.run(order=7)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'share')
@pytest.mark.run(order=8)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=9)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=10)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=11)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=12)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=13)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=14)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=15)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=16)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=17)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'share', False)
@pytest.mark.run(order=18)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
@pytest.mark.run(order=19)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, 'share')
@pytest.mark.run(order=20)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, 'share')
@pytest.mark.run(order=21)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'share')
@pytest.mark.run(order=22)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'share', True)
@pytest.mark.run(order=23)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=24)
def test_server_working_on_start_button_pressed_round2(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=25)
def test_server_status_indicator_says_starting_round2(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=26)
def test_cancel_the_share(self):
CommonTests.test_cancel_the_share(self, 'share')
@pytest.mark.run(order=27)
def test_server_is_stopped_round2(self):
CommonTests.test_server_is_stopped(self, 'share', False)
@pytest.mark.run(order=28)
def test_web_service_is_stopped_round2(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=29)
def test_add_button_visible(self):
CommonTests.test_add_button_visible(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,190 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
os.remove('/tmp/OnionShare/test.txt')
os.remove('/tmp/OnionShare/test-2.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=6)
def test_click_mode(self):
CommonTests.test_click_mode(self, 'receive')
@pytest.mark.run(order=6)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'receive')
@pytest.mark.run(order=7)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'receive')
@pytest.mark.run(order=8)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'receive')
@pytest.mark.run(order=8)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'receive')
@pytest.mark.run(order=9)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'receive')
@pytest.mark.run(order=10)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=11)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'receive')
@pytest.mark.run(order=12)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=14)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'receive', False)
@pytest.mark.run(order=15)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
@pytest.mark.run(order=20)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, 'receive')
@pytest.mark.run(order=21)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, 'receive')
@pytest.mark.run(order=22)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'receive')
@pytest.mark.run(order=23)
def test_web_page(self):
CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', False)
@pytest.mark.run(order=24)
def test_upload_file(self):
CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test.txt')
@pytest.mark.run(order=25)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, 'receive')
@pytest.mark.run(order=26)
def test_counter_incremented(self):
CommonTests.test_counter_incremented(self, 'receive', 1)
@pytest.mark.run(order=27)
def test_upload_same_file_is_renamed(self):
CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test-2.txt')
@pytest.mark.run(order=28)
def test_upload_count_incremented_again(self):
CommonTests.test_counter_incremented(self, 'receive', 2)
@pytest.mark.run(order=29)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'receive', False)
@pytest.mark.run(order=30)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=31)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, 'receive', False)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,190 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": True,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
os.remove('/tmp/OnionShare/test.txt')
os.remove('/tmp/OnionShare/test-2.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_click_mode(self):
CommonTests.test_click_mode(self, 'receive')
@pytest.mark.run(order=6)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'receive')
@pytest.mark.run(order=7)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'receive')
@pytest.mark.run(order=8)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'receive')
@pytest.mark.run(order=9)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'receive')
@pytest.mark.run(order=10)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'receive')
@pytest.mark.run(order=11)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=12)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'receive')
@pytest.mark.run(order=13)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=14)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'receive', True)
@pytest.mark.run(order=15)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
@pytest.mark.run(order=20)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, 'receive')
@pytest.mark.run(order=21)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, 'receive')
@pytest.mark.run(order=22)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'receive')
@pytest.mark.run(order=23)
def test_web_page(self):
CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', True)
@pytest.mark.run(order=24)
def test_upload_file(self):
CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test.txt')
@pytest.mark.run(order=25)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, 'receive')
@pytest.mark.run(order=26)
def test_counter_incremented(self):
CommonTests.test_counter_incremented(self, 'receive', 1)
@pytest.mark.run(order=27)
def test_upload_same_file_is_renamed(self):
CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test-2.txt')
@pytest.mark.run(order=28)
def test_upload_count_incremented_again(self):
CommonTests.test_counter_incremented(self, 'receive', 2)
@pytest.mark.run(order=29)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'receive', False)
@pytest.mark.run(order=30)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=31)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, 'receive', False)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,149 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=8)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=9)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=10)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=11)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=12)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=13)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=14)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=16)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=17)
def test_cancel_the_share(self):
CommonTests.test_cancel_the_share(self, 'share')
@pytest.mark.run(order=18)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'share', False)
@pytest.mark.run(order=19)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=20)
def test_add_button_visible(self):
CommonTests.test_add_button_visible(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,193 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=6)
def test_info_widget_shows_less(self):
CommonTests.test_info_widget_shows_less(self, 'share')
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'share')
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'share')
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'share')
@pytest.mark.run(order=10)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=11)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=12)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=13)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=14)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=15)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=16)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=17)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'share', False)
@pytest.mark.run(order=18)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
@pytest.mark.run(order=19)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, 'share')
@pytest.mark.run(order=20)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, 'share')
@pytest.mark.run(order=21)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'share')
@pytest.mark.run(order=22)
def test_web_page(self):
CommonTests.test_web_page(self, 'share', 'Total size', False)
@pytest.mark.run(order=23)
def test_download_share(self):
CommonTests.test_download_share(self, False)
@pytest.mark.run(order=24)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, 'share')
@pytest.mark.run(order=25)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'share', False)
@pytest.mark.run(order=26)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=27)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, 'share', False)
@pytest.mark.run(order=28)
def test_add_button_visible(self):
CommonTests.test_add_button_visible(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,201 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": True,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=6)
def test_info_widget_shows_less(self):
CommonTests.test_info_widget_shows_less(self, 'share')
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'share')
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'share')
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'share')
@pytest.mark.run(order=10)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=11)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=12)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=13)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=14)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=15)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=16)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=17)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=18)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=19)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'share', True)
@pytest.mark.run(order=20)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
@pytest.mark.run(order=21)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, 'share')
@pytest.mark.run(order=22)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, 'share')
@pytest.mark.run(order=23)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'share')
@pytest.mark.run(order=24)
def test_web_page(self):
CommonTests.test_web_page(self, 'share', 'Total size', True)
@pytest.mark.run(order=25)
def test_download_share(self):
CommonTests.test_download_share(self, True)
@pytest.mark.run(order=26)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, 'share')
@pytest.mark.run(order=27)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'share', False)
@pytest.mark.run(order=28)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=29)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, 'share', False)
@pytest.mark.run(order=30)
def test_add_button_visible(self):
CommonTests.test_add_button_visible(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,213 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": False,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": True,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=6)
def test_info_widget_shows_less(self):
CommonTests.test_info_widget_shows_less(self, 'share')
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'share')
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'share')
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'share')
@pytest.mark.run(order=10)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=11)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=12)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=13)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=14)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=15)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=16)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=17)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=18)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=19)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'share', True)
@pytest.mark.run(order=20)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
@pytest.mark.run(order=21)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, 'share')
@pytest.mark.run(order=22)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, 'share')
@pytest.mark.run(order=23)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'share')
@pytest.mark.run(order=24)
def test_web_page(self):
CommonTests.test_web_page(self, 'share', 'Total size', True)
@pytest.mark.run(order=25)
def test_download_share(self):
CommonTests.test_download_share(self, True)
@pytest.mark.run(order=26)
def test_history_widgets_present(self):
CommonTests.test_history_widgets_present(self, 'share')
@pytest.mark.run(order=27)
def test_counter_incremented(self):
CommonTests.test_counter_incremented(self, 'share', 1)
@pytest.mark.run(order=28)
def test_download_share_again(self):
CommonTests.test_download_share(self, True)
@pytest.mark.run(order=29)
def test_counter_incremented_again(self):
CommonTests.test_counter_incremented(self, 'share', 2)
@pytest.mark.run(order=30)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'share', True)
@pytest.mark.run(order=31)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=32)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, 'share', True)
@pytest.mark.run(order=33)
def test_add_button_visible(self):
CommonTests.test_add_button_visible(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,185 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
slug = ''
onion_host = ''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": True,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=6)
def test_info_widget_shows_less(self):
CommonTests.test_info_widget_shows_less(self, 'share')
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'share')
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'share')
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'share')
@pytest.mark.run(order=10)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=11)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=12)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=13)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=14)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=15)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'share', False)
global slug
slug = self.gui.share_mode.server_status.web.slug
@pytest.mark.run(order=16)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
global onion_host
onion_host = self.gui.app.onion_host
@pytest.mark.run(order=17)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'share')
@pytest.mark.run(order=18)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'share', True)
@pytest.mark.run(order=19)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
@pytest.mark.run(order=20)
def test_server_status_indicator_says_closed(self):
CommonTests.test_server_status_indicator_says_closed(self, 'share', True)
@pytest.mark.run(order=21)
def test_server_started_again(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
CommonTests.test_server_status_indicator_says_starting(self, 'share')
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=22)
def test_have_same_slug(self):
'''Test that we have the same slug'''
self.assertEqual(self.gui.share_mode.server_status.web.slug, slug)
@pytest.mark.run(order=23)
def test_have_same_onion(self):
'''Test that we have the same onion'''
self.assertEqual(self.gui.app.onion_host, onion_host)
@pytest.mark.run(order=24)
def test_server_is_stopped_again(self):
CommonTests.test_server_is_stopped(self, 'share', True)
CommonTests.test_web_service_is_stopped(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,180 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": True,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=6)
def test_info_widget_shows_less(self):
CommonTests.test_info_widget_shows_less(self, 'share')
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'share')
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'share')
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'share')
@pytest.mark.run(order=10)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=11)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=12)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=13)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=14)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=15)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=16)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=17)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=18)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=19)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'share', False)
@pytest.mark.run(order=20)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
@pytest.mark.run(order=21)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, 'share')
@pytest.mark.run(order=22)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, 'share')
@pytest.mark.run(order=23)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'share')
@pytest.mark.run(order=24)
def test_copy_have_hidserv_auth_button(self):
CommonTests.test_copy_have_hidserv_auth_button(self, 'share')
@pytest.mark.run(order=25)
def test_hidserv_auth_string(self):
CommonTests.test_hidserv_auth_string(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,185 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=6)
def test_info_widget_shows_less(self):
CommonTests.test_info_widget_shows_less(self, 'share')
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'share')
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'share')
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'share')
@pytest.mark.run(order=10)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=11)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=12)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=13)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=14)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=15)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=16)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=17)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=18)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=19)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'share', False)
@pytest.mark.run(order=20)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
@pytest.mark.run(order=21)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, 'share')
@pytest.mark.run(order=22)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, 'share')
@pytest.mark.run(order=23)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'share')
@pytest.mark.run(order=24)
def test_tor_killed_statusbar_message_shown(self):
CommonTests.test_tor_killed_statusbar_message_shown(self, 'share')
@pytest.mark.run(order=25)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'share', False)
@pytest.mark.run(order=26)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,148 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": True,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=6)
def test_info_widget_shows_less(self):
CommonTests.test_info_widget_shows_less(self, 'share')
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'share')
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'share')
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'share')
@pytest.mark.run(order=10)
def test_set_timeout(self):
CommonTests.test_set_timeout(self, 'share', 120)
@pytest.mark.run(order=11)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=12)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=13)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=14)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=15)
def test_timeout_widget_hidden(self):
CommonTests.test_timeout_widget_hidden(self, 'share')
@pytest.mark.run(order=16)
def test_timeout(self):
CommonTests.test_server_timed_out(self, 'share', 125000)
@pytest.mark.run(order=17)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,185 @@
#!/usr/bin/env python3
import os
import sys
import unittest
import pytest
import json
from PyQt5 import QtWidgets
from onionshare.common import Common
from onionshare.web import Web
from onionshare import onion, strings
from onionshare_gui import *
from .commontests import CommonTests
class OnionShareGuiTest(unittest.TestCase):
'''Test the OnionShare GUI'''
@classmethod
def setUpClass(cls):
'''Create the GUI'''
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile.close()
common = Common()
common.define_css()
# Start the Onion
strings.load_strings(common)
testonion = onion.Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, True)
test_settings = {
"auth_password": "",
"auth_type": "no_auth",
"autoupdate_timestamp": "",
"close_after_first_download": True,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"downloads_dir": "/tmp/OnionShare",
"hidservauth_string": "",
"no_bridges": True,
"private_key": "",
"public_mode": False,
"receive_allow_receiver_shutdown": True,
"save_private_key": False,
"shutdown_timeout": False,
"slug": "",
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"systray_notifications": True,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_meek_lite_amazon": False,
"tor_bridges_use_custom_bridges": "",
"tor_bridges_use_obfs4": False,
"use_stealth": False,
"use_legacy_v2_onions": False,
"use_autoupdate": True,
"version": "1.3.1"
}
testsettings = '/tmp/testsettings.json'
open(testsettings, 'w').write(json.dumps(test_settings))
cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False)
@classmethod
def tearDownClass(cls):
'''Clean up after tests'''
os.remove('/tmp/test.txt')
@pytest.mark.run(order=1)
def test_gui_loaded(self):
CommonTests.test_gui_loaded(self)
@pytest.mark.run(order=2)
def test_windowTitle_seen(self):
CommonTests.test_windowTitle_seen(self)
@pytest.mark.run(order=3)
def test_settings_button_is_visible(self):
CommonTests.test_settings_button_is_visible(self)
@pytest.mark.run(order=4)
def test_server_status_bar_is_visible(self):
CommonTests.test_server_status_bar_is_visible(self)
@pytest.mark.run(order=5)
def test_file_selection_widget_has_a_file(self):
CommonTests.test_file_selection_widget_has_a_file(self)
@pytest.mark.run(order=6)
def test_info_widget_shows_less(self):
CommonTests.test_info_widget_shows_less(self, 'share')
@pytest.mark.run(order=7)
def test_history_is_not_visible(self):
CommonTests.test_history_is_not_visible(self, 'share')
@pytest.mark.run(order=8)
def test_click_toggle_history(self):
CommonTests.test_click_toggle_history(self, 'share')
@pytest.mark.run(order=9)
def test_history_is_visible(self):
CommonTests.test_history_is_visible(self, 'share')
@pytest.mark.run(order=10)
def test_deleting_only_file_hides_delete_button(self):
CommonTests.test_deleting_only_file_hides_delete_button(self)
@pytest.mark.run(order=11)
def test_add_a_file_and_delete_using_its_delete_widget(self):
CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self)
@pytest.mark.run(order=12)
def test_file_selection_widget_readd_files(self):
CommonTests.test_file_selection_widget_readd_files(self)
@pytest.mark.run(order=13)
def test_server_working_on_start_button_pressed(self):
CommonTests.test_server_working_on_start_button_pressed(self, 'share')
@pytest.mark.run(order=14)
def test_server_status_indicator_says_starting(self):
CommonTests.test_server_status_indicator_says_starting(self, 'share')
@pytest.mark.run(order=15)
def test_add_delete_buttons_hidden(self):
CommonTests.test_add_delete_buttons_hidden(self)
@pytest.mark.run(order=16)
def test_settings_button_is_hidden(self):
CommonTests.test_settings_button_is_hidden(self)
@pytest.mark.run(order=17)
def test_a_server_is_started(self):
CommonTests.test_a_server_is_started(self, 'share')
@pytest.mark.run(order=18)
def test_a_web_server_is_running(self):
CommonTests.test_a_web_server_is_running(self)
@pytest.mark.run(order=19)
def test_have_a_slug(self):
CommonTests.test_have_a_slug(self, 'share', False)
@pytest.mark.run(order=20)
def test_have_an_onion(self):
CommonTests.test_have_an_onion_service(self)
@pytest.mark.run(order=21)
def test_url_description_shown(self):
CommonTests.test_url_description_shown(self, 'share')
@pytest.mark.run(order=22)
def test_have_copy_url_button(self):
CommonTests.test_have_copy_url_button(self, 'share')
@pytest.mark.run(order=23)
def test_server_status_indicator_says_started(self):
CommonTests.test_server_status_indicator_says_started(self, 'share')
@pytest.mark.run(order=24)
def test_tor_killed_statusbar_message_shown(self):
CommonTests.test_tor_killed_statusbar_message_shown(self, 'share')
@pytest.mark.run(order=25)
def test_server_is_stopped(self):
CommonTests.test_server_is_stopped(self, 'share', False)
@pytest.mark.run(order=26)
def test_web_service_is_stopped(self):
CommonTests.test_web_service_is_stopped(self)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,5 @@
#!/bin/bash
for test in `ls -1 | egrep ^onionshare_`; do
pytest $test -vvv || exit 1
done