Merge branch 'develop' into 910_flatpak

This commit is contained in:
Micah Lee 2020-04-05 20:31:18 -07:00
commit 4fdf53f493
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
108 changed files with 5432 additions and 5166 deletions

View File

@ -1,7 +1,8 @@
# Python CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-python/ for more details
#
# To run the tests, CircleCI needs these environment variables:
# QT_EMAIL - email address for a Qt account
# QT_PASSWORD - password for a Qt account
# (Unfortunately you can't install Qt without logging in.)
version: 2
workflows:
version: 2
@ -9,11 +10,12 @@ workflows:
jobs:
- test-3.6
- test-3.7
- test-3.8
jobs:
test-3.6: &test-template
docker:
- image: circleci/python:3.6.6
- image: circleci/python:3.6-buster
working_directory: ~/repo
@ -21,17 +23,25 @@ jobs:
- checkout
- run:
name: install dependencies
name: Install Qt5 binaries
command: |
sudo apt-get update
sudo apt-get install -y python3-pip python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python3-stdeb python3-all python-nautilus xvfb obfs4proxy
sudo pip3 install -r install/requirements.txt
sudo pip3 install -r install/requirements-tests.txt
sudo pip3 install pytest-cov flake8
sudo apt-get install xvfb libdbus-1-3 libxkbcommon-x11-0 libxkbcommon-x11-dev
cd ~/
wget https://download.qt.io/official_releases/qt/5.14/5.14.0/qt-opensource-linux-x64-5.14.0.run
chmod +x qt-opensource-linux-x64-5.14.0.run
xvfb-run ./qt-opensource-linux-x64-5.14.0.run --script ~/repo/.circleci/qt-installer-script.js --platform minimal --verbose
# run tests!
- run:
name: run flake tests
name: Install dependencies
command: |
sudo apt-get update
sudo apt-get install -y python3-pip xvfb
sudo pip3 install poetry flake8
poetry install
- run:
name: Run flake tests
command: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
@ -39,11 +49,16 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- run:
name: run tests
name: Run unit tests
command: |
xvfb-run -s "-screen 0 1280x1024x24" pytest --rungui --cov=onionshare --cov=onionshare_gui --cov-report=term-missing -vvv --no-qt-log tests/
xvfb-run -s "-screen 0 1280x1024x24" poetry run ./tests/run.sh --rungui
test-3.7:
<<: *test-template
docker:
- image: circleci/python:3.7.1
- image: circleci/python:3.7-buster
test-3.8:
<<: *test-template
docker:
- image: circleci/python:3.8-buster

View File

@ -0,0 +1,75 @@
function Controller() {
installer.installationFinished.connect(proceed)
}
function logCurrentPage() {
var pageName = page().objectName
var pagePrettyTitle = page().title
console.log("At page: " + pageName + " ('" + pagePrettyTitle + "')")
}
function page() {
return gui.currentPageWidget()
}
function proceed(button, delay) {
gui.clickButton(button || buttons.NextButton, delay)
}
Controller.prototype.WelcomePageCallback = function() {
logCurrentPage()
proceed(buttons.NextButton, 2000)
}
Controller.prototype.CredentialsPageCallback = function() {
logCurrentPage()
page().loginWidget.EmailLineEdit.text = installer.environmentVariable("QT_EMAIL");
page().loginWidget.PasswordLineEdit.text = installer.environmentVariable("QT_PASSWORD");
proceed()
}
Controller.prototype.IntroductionPageCallback = function() {
logCurrentPage()
proceed()
}
Controller.prototype.TargetDirectoryPageCallback = function() {
logCurrentPage()
proceed()
}
Controller.prototype.ComponentSelectionPageCallback = function() {
logCurrentPage()
page().deselectAll()
page().selectComponent("qt.qt5.5140.gcc_64")
proceed()
}
Controller.prototype.LicenseAgreementPageCallback = function() {
logCurrentPage()
page().AcceptLicenseRadioButton.checked = true
gui.clickButton(buttons.NextButton)
}
Controller.prototype.ReadyForInstallationPageCallback = function() {
logCurrentPage()
proceed()
}
Controller.prototype.PerformInstallationPageCallback = function() {
logCurrentPage()
}
Controller.prototype.FinishedPageCallback = function() {
logCurrentPage()
page().LaunchQtCreatorCheckBoxForm.launchQtCreatorCheckBox.checked = false
proceed(buttons.FinishButton)
}
Controller.prototype.DynamicTelemetryPluginFormCallback = function() {
logCurrentPage()
console.log(Object.keys(page().TelemetryPluginForm.statisticGroupBox))
var radioButtons = page().TelemetryPluginForm.statisticGroupBox
radioButtons.disableStatisticRadioButton.checked = true
proceed()
}

151
BUILD.md
View File

@ -1,3 +1,22 @@
# Index
* [Building OnionShare](#building-onionshare)
* [Linux](#linux)
* [Use newest software](#use-newest-software)
* [Use package managers](#use-package-managers)
* [macOS](#macos)
* [Windows](#windows)
* [Setting up your dev environment](#setting-up-your-dev-environment)
* [To make a .exe](#to-make-a-exe)
* [To build the installer](#to-build-the-installer)
* [Running tests](#running-tests)
* [Making releases](#making-releases)
* [Changelog, version, and signed git tag](#changelog-version-and-signed-git-tag)
* [Linux release](#linux-release)
* [macOS release](#macos-release)
* [Windows release](#windows-release)
* [Source package](#source-package)
* [Publishing the release](#publishing-the-release)
# Building OnionShare
Start by getting the source code:
@ -9,18 +28,64 @@ cd onionshare
## Linux
### Use newest software
The recommended way to develop OnionShare is to use the latest versions of all dependencies.
First, install `tor` from either the [official Debian repository](https://support.torproject.org/apt/tor-deb-repo/), or from your package manager.
Then download Qt 5.14.0 for Linux:
```sh
cd ~/Downloads
wget https://download.qt.io/official_releases/qt/5.14/5.14.0/qt-opensource-linux-x64-5.14.0.run
```
If you'd like to check to make sure you have the exact installer I have, here is the sha256 checksum:
```sh
sha256sum qt-opensource-linux-x64-5.14.0.run
4379f147c6793ec7e7349d2f9ee7d53b8ab6ea4e4edf8ee0574a75586a6a6e0e qt-opensource-linux-x64-5.14.0.run
```
Then make it executable and install Qt:
```sh
chmod +x qt-opensource-linux-x64-5.14.0.run
./qt-opensource-linux-x64-5.14.0.run
```
You have to create a Qt account and login to install Qt. Choose the default installation folder in your home directory. The only component you need is `Qt 5.14.0` > `Desktop gcc 64-bit`.
Install [poetry](https://python-poetry.org/docs/) from your package manager, or by doing `pip install --user poetry`. Then install dependencies:
```sh
poetry install
```
You can run the CLI and the GUI versions of OnionShare like this:
```sh
poetry run ./dev_scripts/onionshare
poetry run ./dev_scripts/onionshare-gui
```
### Use package managers
Alternatively, you can install dependencies from package managers.
Install the needed dependencies:
For Debian-like distros:
**For Debian-like distros:**
```
apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils
apt install -y python3-flask python3-stem python3-pyqt5 python3-crypto python3-socks python-nautilus tor obfs4proxy python3-pytest python3-pytestqt build-essential fakeroot python3-all python3-stdeb dh-python python3-flask-httpauth python3-distutils python3-psutil python3-watchdog
```
For Fedora-like distros:
**For Fedora-like distros:**
```
dnf install -y python3-flask python3-flask-httpauth python3-stem python3-qt5 python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build
dnf install -y python3-flask python3-flask-httpauth python3-stem python3-qt5 python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build python3-psutil python3-watchdog
```
After that you can try both the CLI and the GUI version of OnionShare:
@ -36,7 +101,7 @@ Create a .deb on Debian-like distros: `./install/build_deb.sh`
Create a .rpm on Fedora-like distros: `./install/build_rpm.sh`
For OpenSuSE: There are instructions for building [in the wiki](https://github.com/micahflee/onionshare/wiki/Linux-Distribution-Support#opensuse-leap-150).
For openSUSE: There are instructions for building [in the wiki](https://github.com/micahflee/onionshare/wiki/Linux-Distribution-Support#opensuse-leap-150).
For ArchLinux: There is a PKBUILD available [here](https://aur.archlinux.org/packages/onionshare/) that can be used to install OnionShare.
@ -52,24 +117,17 @@ You may also need to run the command `/Applications/Python\ 3.7/Install\ Certifi
Install Qt 5.13.1 for macOS from https://www.qt.io/offline-installers. I downloaded `qt-opensource-mac-x64-5.13.1.dmg`. In the installer, you can skip making an account, and all you need is `Qt` > `Qt 5.13.1` > `macOS`.
Now install pip dependencies. If you want to use a virtualenv, create it and activate it first:
If you don't have it already, install poetry (`pip3 install --user poetry`). Then install dependencies:
```sh
python3 -m venv venv
. venv/bin/activate
```
Then install the dependencies:
```sh
pip3 install -r install/requirements.txt
poetry install
```
#### You can run both the CLI and GUI versions of OnionShare without building an bundle
```sh
./dev_scripts/onionshare
./dev_scripts/onionshare-gui
poetry run ./dev_scripts/onionshare
poetry run ./dev_scripts/onionshare-gui
```
#### To build the app bundle
@ -94,19 +152,25 @@ Now you should have `dist/OnionShare.pkg`.
Download Python 3.7.4, 32-bit (x86) from https://www.python.org/downloads/release/python-374/. I downloaded `python-3.7.4.exe`. When installing it, make sure to check the "Add Python 3.7 to PATH" checkbox on the first page of the installer.
Open a command prompt, cd to the onionshare folder, and install dependencies with pip:
Install the Qt 5.13.1 from https://www.qt.io/offline-installers. I downloaded `qt-opensource-windows-x86-5.13.1.exe`. In the installer, you can skip making an account, and all you need `Qt` > `Qt 5.13.1` > `MSVC 2017 32-bit`.
```cmd
pip install -r install\requirements.txt
Install [poetry](https://python-poetry.org/). Open PowerShell, and run:
```
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python
```
Install the Qt 5.13.1 from https://www.qt.io/offline-installers. I downloaded `qt-opensource-windows-x86-5.13.1.exe`. In the installer, you can skip making an account, and all you need `Qt` > `Qt 5.13.1` > `MSVC 2017 32-bit`.
And add `%USERPROFILE%\.poetry\bin` to your path. Then open a command prompt and cd to the `dangerzone` folder, and install the poetry dependencies:
```
poetry install
```
After that you can try both the CLI and the GUI version of OnionShare:
```
python dev_scripts\onionshare
python dev_scripts\onionshare-gui
poetry run python dev_scripts\onionshare
poetry run python dev_scripts\onionshare-gui
```
#### If you want to build a .exe
@ -141,19 +205,6 @@ cd "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\
vcvars32.bat
```
Make sure you have a new enough `setuptools`:
```
pip install --upgrade setuptools
```
Now make sure you don't have PyInstaller installed from pip:
```
pip uninstall PyInstaller
rmdir C:\Users\user\AppData\Local\Programs\Python\Python37-32\Lib\site-packages\PyInstaller /S
```
Change to a folder where you keep source code, and clone the PyInstaller git repo and checkout the `v3.5` tag:
```
@ -164,6 +215,14 @@ git tag -v v3.5
(Note that ideally you would verify the git tag, but the PGP key that has signed the `v3.5` git tag for is not published anywhere, so this isn't possible. See [this issue](https://github.com/pyinstaller/pyinstaller/issues/4430).)
The next step is to compile the bootloader. We should do this all in dangerzone's poetry shell:
```
cd onionshare
poetry shell
cd ..\pyinstaller
```
And compile the bootloader, following [these instructions](https://pythonhosted.org/PyInstaller/bootloader-building.html). To compile, run this:
```
@ -171,11 +230,12 @@ cd bootloader
python waf distclean all --target-arch=32bit --msvc_targets=x86
```
Finally, install the PyInstaller module into your local site-packages:
Finally, install the PyInstaller module into your poetry environment:
```
cd ..
python setup.py install
exit
```
Now the next time you use PyInstaller to build OnionShare, the `.exe` file should not be flagged as malicious by anti-virus.
@ -204,28 +264,22 @@ This will prompt you to codesign three binaries and execute one unsigned binary.
# Running tests
OnionShare includes PyTest unit tests. To run the tests, first install some dependencies:
OnionShare includes PyTest unit tests. To run tests, you can run `pytest` against the `tests/` directory.
```sh
pip3 install -r install/requirements-tests.txt
```
Then you can run `pytest` against the `tests/` directory.
```sh
pytest tests/
poetry run ./tests/run.sh
```
You can run GUI tests like this:
```sh
pytest --rungui tests/
poetry run ./tests/run.sh --rungui
```
If you would like to also run the GUI unit tests in 'tor' mode, start Tor Browser in the background, then run:
```sh
pytest --rungui --runtor tests/
poetry run ./tests/run.sh --rungui --runtor
```
Keep in mind that the Tor tests take a lot longer to run than local mode, but they are also more comprehensive.
@ -233,7 +287,7 @@ Keep in mind that the Tor tests take a lot longer to run than local mode, but th
You can also choose to wrap the tests in `xvfb-run` so that a ton of OnionShare windows don't pop up on your desktop (you may need to install the `xorg-x11-server-Xvfb` package), like this:
```sh
xvfb-run pytest --rungui tests/
xvfb-run poetry run ./tests/run.sh --rungui
```
# Making releases
@ -245,8 +299,9 @@ This section documents the release process. Unless you're a core OnionShare deve
Before making a release, all of these should be complete:
* `share/version.txt` should have the correct version
* `pyproject.toml` should have the correct version
* `install/org.onionshare.OnionShare.appdata.xml` should have the correct version
* `install/onionshare.nsi` should have the correct version
* `install/onionshare.nsi` should have the correct version, for the Windows installer
* `CHANGELOG.md` should be updated to include a list of all major changes since the last release
* There must be a PGP-signed git tag for the version, e.g. for OnionShare 2.1, the tag must be `v2.1`

View File

@ -4,6 +4,7 @@ GenericName=OnionShare Client
Comment=Share a file securely and anonymously over Tor
Comment[da]=Del en fil sikkert og anonymt over Tor
Comment[de]=Teile Dateien sicher und anonym über das Tor-Netzwerk
Comment[hr]=Dijeli datoteku sigurno i anonimno preko Tora
Exec=/usr/bin/onionshare-gui
Terminal=false
Type=Application
@ -12,5 +13,6 @@ Categories=Network;FileTransfer;
Keywords=tor;anonymity;privacy;onion service;file sharing;file hosting;
Keywords[da]=tor;anonymitet;privatliv;onion-tjeneste;fildeling;filhosting;
Keywords[de]=tor;Anonymität;Privatsphäre;Onion-Service;File-Sharing;File-Hosting;
Keywords[hr]=tor;anonimnost;privatnost;Onion usluga;dijeljenje datoteka;hosting datoteka;
StartupNotify=true
StartupWMClass=onionshare

View File

@ -1,10 +0,0 @@
atomicwrites==1.3.0
attrs==19.1.0
more-itertools==7.2.0
pluggy==0.13.0
py==1.8.0
pytest==5.1.2
pytest-faulthandler==2.0.1
pytest-qt==3.2.2
six==1.12.0
urllib3==1.25.3

View File

@ -1,22 +0,0 @@
altgraph==0.16.1
certifi==2019.9.11
chardet==3.0.4
Click==7.0
Flask==1.1.1
Flask-HTTPAuth==3.3.0
future==0.17.1
idna==2.8
itsdangerous==1.1.0
Jinja2==2.10.1
macholib==1.11
MarkupSafe==1.1.1
pefile==2019.4.18
pycryptodome==3.9.0
PyInstaller==3.5
PyQt5==5.13.1
PyQt5-sip==4.19.19
PySocks==1.7.0
requests==2.22.0
stem==1.7.1
urllib3==1.25.3
Werkzeug==0.15.6

View File

@ -3,7 +3,10 @@ import sys
import json
import locale
import subprocess
import urllib
try:
import urllib.request
except:
import urllib
import gi
gi.require_version("Nautilus", "3.0")
@ -67,7 +70,10 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider):
def url2path(self, url):
file_uri = url.get_activation_uri()
arg_uri = file_uri[7:]
path = urllib.url2pathname(arg_uri)
try:
path = urllib.request.url2pathname(arg_uri)
except:
path = urllib.url2pathname(arg_uri)
return path
def exec_onionshare(self, filenames):

View File

@ -26,11 +26,12 @@ from .common import Common
from .web import Web
from .onion import *
from .onionshare import OnionShare
from .mode_settings import ModeSettings
def build_url(common, app, web):
def build_url(mode_settings, app, web):
# Build the URL
if common.settings.get("public_mode"):
if mode_settings.get("general", "public"):
return f"http://{app.onion_host}"
else:
return f"http://onionshare:{web.password}@{app.onion_host}"
@ -45,6 +46,30 @@ def main(cwd=None):
# Display OnionShare banner
print(f"OnionShare {common.version} | https://onionshare.org/")
reset='\033[0m'
purple='\33[95m'
print(purple)
print(" @@@@@@@@@ ")
print(" @@@@@@@@@@@@@@@@@@@ ")
print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ")
print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ")
print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ___ _ ")
print(" @@@@@@ @@@@@@@@@@@@@ / _ \ (_) ")
print(" @@@@ @ @@@@@@@@@@@ | | | |_ __ _ ___ _ __ ")
print(" @@@@@@@@ @@@@@@@@@@ | | | | '_ \| |/ _ \| '_ \ ")
print(" @@@@@@@@@@@@ @@@@@@@@@@ \ \_/ / | | | | (_) | | | | ")
print(" @@@@@@@@@@@@@@@@ @@@@@@@@@ \___/|_| |_|_|\___/|_| |_| ")
print(" @@@@@@@@@ @@@@@@@@@@@@@@@@ _____ _ ")
print(" @@@@@@@@@@ @@@@@@@@@@@@ / ___| | ")
print(" @@@@@@@@@@ @@@@@@@@ \ `--.| |__ __ _ _ __ ___ ")
print(" @@@@@@@@@@@ @ @@@@ `--. \ '_ \ / _` | '__/ _ \\")
print(" @@@@@@@@@@@@@ @@@@@@ /\__/ / | | | (_| | | | __/")
print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ \____/|_| |_|\__,_|_| \___|")
print(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ")
print(" @@@@@@@@@@@@@@@@@@@@@@@@@ ")
print(" @@@@@@@@@@@@@@@@@@@ ")
print(" @@@@@@@@@ ")
print(reset)
# OnionShare CLI in OSX needs to change current working directory (#132)
if common.platform == "Darwin":
@ -55,63 +80,101 @@ def main(cwd=None):
parser = argparse.ArgumentParser(
formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=28)
)
# Select modes
parser.add_argument(
"--receive", action="store_true", dest="receive", help="Receive files"
)
parser.add_argument(
"--website", action="store_true", dest="website", help="Publish website"
)
# Tor connection-related args
parser.add_argument(
"--local-only",
action="store_true",
dest="local_only",
default=False,
help="Don't use Tor (only for development)",
)
parser.add_argument(
"--stay-open",
action="store_true",
dest="stay_open",
help="Continue sharing after files have been sent",
)
parser.add_argument(
"--auto-start-timer",
metavar="<int>",
dest="autostart_timer",
default=0,
help="Schedule this share to start N seconds from now",
)
parser.add_argument(
"--auto-stop-timer",
metavar="<int>",
dest="autostop_timer",
default=0,
help="Stop sharing after a given amount of seconds",
)
parser.add_argument(
"--connect-timeout",
metavar="<int>",
metavar="SECONDS",
dest="connect_timeout",
default=120,
help="Give up connecting to Tor after a given amount of seconds (default: 120)",
)
parser.add_argument(
"--stealth",
action="store_true",
dest="stealth",
help="Use client authorization (advanced)",
)
parser.add_argument(
"--receive",
action="store_true",
dest="receive",
help="Receive shares instead of sending them",
)
parser.add_argument(
"--website",
action="store_true",
dest="website",
help="Publish a static website",
)
parser.add_argument(
"--config",
metavar="config",
default=False,
help="Custom JSON config file location (optional)",
metavar="FILENAME",
default=None,
help="Filename of custom global settings",
)
# Persistent file
parser.add_argument(
"--persistent",
metavar="FILENAME",
default=None,
help="Filename of persistent session",
)
# General args
parser.add_argument(
"--public",
action="store_true",
dest="public",
default=False,
help="Don't use a password",
)
parser.add_argument(
"--auto-start-timer",
metavar="SECONDS",
dest="autostart_timer",
default=0,
help="Start onion service at scheduled time (N seconds from now)",
)
parser.add_argument(
"--auto-stop-timer",
metavar="SECONDS",
dest="autostop_timer",
default=0,
help="Stop onion service at schedule time (N seconds from now)",
)
parser.add_argument(
"--legacy",
action="store_true",
dest="legacy",
default=False,
help="Use legacy address (v2 onion service, not recommended)",
)
parser.add_argument(
"--client-auth",
action="store_true",
dest="client_auth",
default=False,
help="Use client authorization (requires --legacy)",
)
# Share args
parser.add_argument(
"--autostop-sharing",
action="store_true",
dest="autostop_sharing",
default=True,
help="Share files: Stop sharing after files have been sent",
)
# Receive args
parser.add_argument(
"--data-dir",
metavar="data_dir",
default=None,
help="Receive files: Save files received to this directory",
)
# Website args
parser.add_argument(
"--disable_csp",
action="store_true",
dest="disable_csp",
default=False,
help="Publish website: Disable Content Security Policy header (allows your website to use third-party resources)",
)
# Other
parser.add_argument(
"-v",
"--verbose",
@ -131,16 +194,21 @@ def main(cwd=None):
for i in range(len(filenames)):
filenames[i] = os.path.abspath(filenames[i])
local_only = bool(args.local_only)
verbose = bool(args.verbose)
stay_open = bool(args.stay_open)
autostart_timer = int(args.autostart_timer)
autostop_timer = int(args.autostop_timer)
connect_timeout = int(args.connect_timeout)
stealth = bool(args.stealth)
receive = bool(args.receive)
website = bool(args.website)
config = args.config
local_only = bool(args.local_only)
connect_timeout = int(args.connect_timeout)
config_filename = args.config
persistent_filename = args.persistent
public = bool(args.public)
autostart_timer = int(args.autostart_timer)
autostop_timer = int(args.autostop_timer)
legacy = bool(args.legacy)
client_auth = bool(args.client_auth)
autostop_sharing = bool(args.autostop_sharing)
data_dir = args.data_dir
disable_csp = bool(args.disable_csp)
verbose = bool(args.verbose)
if receive:
mode = "receive"
@ -149,42 +217,86 @@ def main(cwd=None):
else:
mode = "share"
# In share an website mode, you must supply a list of filenames
if mode == "share" or mode == "website":
# Make sure filenames given if not using receiver mode
if len(filenames) == 0:
parser.print_help()
sys.exit()
# Validate filenames
valid = True
for filename in filenames:
if not os.path.isfile(filename) and not os.path.isdir(filename):
print(f"{filename} is not a valid file.")
valid = False
if not os.access(filename, os.R_OK):
print(f"{filename} is not a readable file.")
valid = False
if not valid:
sys.exit()
# Re-load settings, if a custom config was passed in
if config:
common.load_settings(config)
else:
common.load_settings()
# Verbose mode?
common.verbose = verbose
# client_auth can only be set if legacy is also set
if client_auth and not legacy:
print(
"Client authentication (--client-auth) is only supported with with legacy onion services (--legacy)"
)
sys.exit()
# Re-load settings, if a custom config was passed in
if config_filename:
common.load_settings(config_filename)
else:
common.load_settings()
# Mode settings
if persistent_filename:
mode_settings = ModeSettings(common, persistent_filename)
mode_settings.set("persistent", "enabled", True)
else:
mode_settings = ModeSettings(common)
if mode_settings.just_created:
# This means the mode settings were just created, not loaded from disk
mode_settings.set("general", "public", public)
mode_settings.set("general", "autostart_timer", autostart_timer)
mode_settings.set("general", "autostop_timer", autostop_timer)
mode_settings.set("general", "legacy", legacy)
mode_settings.set("general", "client_auth", client_auth)
if mode == "share":
mode_settings.set("share", "autostop_sharing", autostop_sharing)
if mode == "receive":
if data_dir:
mode_settings.set("receive", "data_dir", data_dir)
if mode == "website":
mode_settings.set("website", "disable_csp", disable_csp)
else:
# See what the persistent mode was
mode = mode_settings.get("persistent", "mode")
# In share and website mode, you must supply a list of filenames
if mode == "share" or mode == "website":
# Unless you passed in a persistent filename, in which case get the filenames from
# the mode settings
if persistent_filename and not mode_settings.just_created:
filenames = mode_settings.get(mode, "filenames")
else:
# Make sure filenames given if not using receiver mode
if len(filenames) == 0:
if persistent_filename:
mode_settings.delete()
parser.print_help()
sys.exit()
# Validate filenames
valid = True
for filename in filenames:
if not os.path.isfile(filename) and not os.path.isdir(filename):
print(f"{filename} is not a valid file.")
valid = False
if not os.access(filename, os.R_OK):
print(f"{filename} is not a readable file.")
valid = False
if not valid:
sys.exit()
# Create the Web object
web = Web(common, False, mode)
web = Web(common, False, mode_settings, mode)
# Start the Onion object
onion = Onion(common)
onion = Onion(common, use_tmp_dir=True)
try:
onion.connect(
custom_settings=False, config=config, connect_timeout=connect_timeout
custom_settings=False,
config=config_filename,
connect_timeout=connect_timeout,
local_only=local_only,
)
except KeyboardInterrupt:
print("")
@ -195,36 +307,35 @@ def main(cwd=None):
# Start the onionshare app
try:
common.settings.load()
if not common.settings.get("public_mode"):
web.generate_password(common.settings.get("password"))
if not mode_settings.get("general", "public"):
web.generate_password(mode_settings.get("onion", "password"))
else:
web.password = None
app = OnionShare(common, onion, local_only, autostop_timer)
app.set_stealth(stealth)
app.choose_port()
# Delay the startup if a startup timer was set
if autostart_timer > 0:
# Can't set a schedule that is later than the auto-stop timer
if app.autostop_timer > 0 and app.autostop_timer < autostart_timer:
if autostop_timer > 0 and autostop_timer < autostart_timer:
print(
"The auto-stop time can't be the same or earlier than the auto-start time. Please update it to start sharing."
)
sys.exit()
app.start_onion_service(False, True)
url = build_url(common, app, web)
app.start_onion_service(mode_settings, False, True)
url = build_url(mode_settings, app, web)
schedule = datetime.now() + timedelta(seconds=autostart_timer)
if mode == "receive":
print(
f"Files sent to you appear in this folder: {common.settings.get('data_dir')}"
f"Files sent to you appear in this folder: {mode_settings.get('receive', 'data_dir')}"
)
print("")
print(
"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."
)
print("")
if stealth:
if mode_settings.get("general", "client_auth"):
print(
f"Give this address and HidServAuth lineto your sender, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}"
)
@ -234,7 +345,7 @@ def main(cwd=None):
f"Give this address to your sender, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}"
)
else:
if stealth:
if mode_settings.get("general", "client_auth"):
print(
f"Give this address and HidServAuth line to your recipient, and tell them it won't be accessible until: {schedule.strftime('%I:%M:%S%p, %b %d, %y')}"
)
@ -248,9 +359,9 @@ def main(cwd=None):
print("Waiting for the scheduled time before starting...")
app.onion.cleanup(False)
time.sleep(autostart_timer)
app.start_onion_service()
app.start_onion_service(mode_settings)
else:
app.start_onion_service()
app.start_onion_service(mode_settings)
except KeyboardInterrupt:
print("")
sys.exit()
@ -284,10 +395,7 @@ def main(cwd=None):
print("")
# Start OnionShare http service in new thread
t = threading.Thread(
target=web.start,
args=(app.port, stay_open, common.settings.get("public_mode"), web.password),
)
t = threading.Thread(target=web.start, args=(app.port,))
t.daemon = True
t.start()
@ -300,13 +408,13 @@ def main(cwd=None):
app.autostop_timer_thread.start()
# Save the web password if we are using a persistent private key
if common.settings.get("save_private_key"):
if not common.settings.get("password"):
common.settings.set("password", web.password)
common.settings.save()
if mode_settings.get("persistent", "enabled"):
if not mode_settings.get("onion", "password"):
mode_settings.set("onion", "password", web.password)
# mode_settings.save()
# Build the URL
url = build_url(common, app, web)
url = build_url(mode_settings, app, web)
print("")
if autostart_timer > 0:
@ -314,7 +422,7 @@ def main(cwd=None):
else:
if mode == "receive":
print(
f"Files sent to you appear in this folder: {common.settings.get('data_dir')}"
f"Files sent to you appear in this folder: {mode_settings.get('receive', 'data_dir')}"
)
print("")
print(
@ -322,7 +430,7 @@ def main(cwd=None):
)
print("")
if stealth:
if mode_settings.get("general", "client_auth"):
print("Give this address and HidServAuth to the sender:")
print(url)
print(app.auth_string)
@ -330,7 +438,7 @@ def main(cwd=None):
print("Give this address to the sender:")
print(url)
else:
if stealth:
if mode_settings.get("general", "client_auth"):
print("Give this address and HidServAuth line to the recipient:")
print(url)
print(app.auth_string)

View File

@ -32,7 +32,7 @@ import time
from .settings import Settings
class Common(object):
class Common:
"""
The Common object is shared amongst all parts of OnionShare.
"""
@ -174,249 +174,46 @@ class Common(object):
else:
onionshare_data_dir = os.path.expanduser("~/.config/onionshare")
# Modify the data dir if running tests
if getattr(sys, "onionshare_test_mode", False):
onionshare_data_dir += "-testdata"
os.makedirs(onionshare_data_dir, 0o700, True)
return onionshare_data_dir
def build_password(self):
def build_tmp_dir(self):
"""
Returns a random string made from two words from the wordlist, such as "deter-trig".
Returns path to a folder that can hold temporary files
"""
tmp_dir = os.path.join(self.build_data_dir(), "tmp")
os.makedirs(tmp_dir, 0o700, True)
return tmp_dir
def build_persistent_dir(self):
"""
Returns the path to the folder that holds persistent files
"""
persistent_dir = os.path.join(self.build_data_dir(), "persistent")
os.makedirs(persistent_dir, 0o700, True)
return persistent_dir
def build_tor_dir(self):
"""
Returns path to the tor data directory
"""
tor_dir = os.path.join(self.build_data_dir(), "tor_data")
os.makedirs(tor_dir, 0o700, True)
return tor_dir
def build_password(self, word_count=2):
"""
Returns a random string made of words from the wordlist, such as "deter-trig".
"""
with open(self.get_resource_path("wordlist.txt")) as f:
wordlist = f.read().split()
r = random.SystemRandom()
return "-".join(r.choice(wordlist) for _ in range(2))
def define_css(self):
"""
This defines all of the stylesheets used in GUI mode, to avoid repeating code.
This method is only called in GUI mode.
"""
self.css = {
# OnionShareGui styles
"mode_switcher_selected_style": """
QPushButton {
color: #ffffff;
background-color: #4e064f;
border: 0;
border-right: 1px solid #69266b;
font-weight: bold;
border-radius: 0;
}""",
"mode_switcher_unselected_style": """
QPushButton {
color: #ffffff;
background-color: #601f61;
border: 0;
font-weight: normal;
border-radius: 0;
}""",
"settings_button": """
QPushButton {
background-color: #601f61;
border: 0;
border-left: 1px solid #69266b;
border-radius: 0;
}""",
"server_status_indicator_label": """
QLabel {
font-style: italic;
color: #666666;
padding: 2px;
}""",
"status_bar": """
QStatusBar {
font-style: italic;
color: #666666;
}
QStatusBar::item {
border: 0px;
}""",
# Common styles between modes and their child widgets
"mode_info_label": """
QLabel {
font-size: 12px;
color: #666666;
}
""",
"server_status_url": """
QLabel {
background-color: #ffffff;
color: #000000;
padding: 10px;
border: 1px solid #666666;
font-size: 12px;
}
""",
"server_status_url_buttons": """
QPushButton {
color: #3f7fcf;
}
""",
"server_status_button_stopped": """
QPushButton {
background-color: #5fa416;
color: #ffffff;
padding: 10px;
border: 0;
border-radius: 5px;
}""",
"server_status_button_working": """
QPushButton {
background-color: #4c8211;
color: #ffffff;
padding: 10px;
border: 0;
border-radius: 5px;
font-style: italic;
}""",
"server_status_button_started": """
QPushButton {
background-color: #d0011b;
color: #ffffff;
padding: 10px;
border: 0;
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": """
QLabel {
font-weight: bold;
font-size 14px;
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": """
QProgressBar {
border: 1px solid #4e064f;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
font-size: 14px;
}
QProgressBar::chunk {
background-color: #4e064f;
width: 10px;
}""",
"history_individual_file_timestamp_label": """
QLabel {
color: #666666;
}""",
"history_individual_file_status_code_label_2xx": """
QLabel {
color: #008800;
}""",
"history_individual_file_status_code_label_4xx": """
QLabel {
color: #cc0000;
}""",
# Share mode and child widget styles
"share_zip_progess_bar": """
QProgressBar {
border: 1px solid #4e064f;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
}
QProgressBar::chunk {
border: 0px;
background-color: #4e064f;
width: 10px;
}""",
"share_filesize_warning": """
QLabel {
padding: 10px 0;
font-weight: bold;
color: #333333;
}
""",
"share_file_selection_drop_here_label": """
QLabel {
color: #999999;
}""",
"share_file_selection_drop_count_label": """
QLabel {
color: #ffffff;
background-color: #f44449;
font-weight: bold;
padding: 5px 10px;
border-radius: 10px;
}""",
"share_file_list_drag_enter": """
FileList {
border: 3px solid #538ad0;
}
""",
"share_file_list_drag_leave": """
FileList {
border: none;
}
""",
"share_file_list_item_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
# Receive mode and child widget styles
"receive_file": """
QWidget {
background-color: #ffffff;
}
""",
"receive_file_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
# Settings dialog
"settings_version": """
QLabel {
color: #666666;
}""",
"settings_tor_status": """
QLabel {
background-color: #ffffff;
color: #000000;
padding: 10px;
}""",
"settings_whats_this": """
QLabel {
font-size: 12px;
}""",
"settings_connect_to_tor": """
QLabel {
font-style: italic;
}""",
}
return "-".join(r.choice(wordlist) for _ in range(word_count))
@staticmethod
def random_string(num_bytes, output_len=None):

142
onionshare/mode_settings.py Normal file
View File

@ -0,0 +1,142 @@
# -*- 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 pwd
import json
class ModeSettings:
"""
This stores the settings for a single instance of an OnionShare mode. In CLI there
is only one ModeSettings, and in the GUI there is a separate ModeSettings for each tab
"""
def __init__(self, common, filename=None, id=None):
self.common = common
self.default_settings = {
"onion": {
"private_key": None,
"hidservauth_string": None,
"password": None,
},
"persistent": {"mode": None, "enabled": False},
"general": {
"public": False,
"autostart_timer": False,
"autostop_timer": False,
"legacy": False,
"client_auth": False,
"service_id": None,
},
"share": {"autostop_sharing": True, "filenames": []},
"receive": {"data_dir": self.build_default_receive_data_dir()},
"website": {"disable_csp": False, "filenames": []},
}
self._settings = {}
self.just_created = False
if id:
self.id = id
else:
self.id = self.common.build_password(3)
self.load(filename)
def fill_in_defaults(self):
"""
If there are any missing settings from self._settings, replace them with
their default values.
"""
for key in self.default_settings:
if key in self._settings:
for inner_key in self.default_settings[key]:
if inner_key not in self._settings[key]:
self._settings[key][inner_key] = self.default_settings[key][
inner_key
]
else:
self._settings[key] = self.default_settings[key]
def get(self, group, key):
return self._settings[group][key]
def set(self, group, key, val):
self._settings[group][key] = val
self.common.log(
"ModeSettings", "set", f"updating {self.id}: {group}.{key} = {val}"
)
self.save()
def build_default_receive_data_dir(self):
"""
Returns the path of the default Downloads directory for receive mode.
"""
if self.common.platform == "Darwin":
# We can't use os.path.expanduser() in macOS because in the sandbox it
# returns the path to the sandboxed homedir
real_homedir = pwd.getpwuid(os.getuid()).pw_dir
return os.path.join(real_homedir, "OnionShare")
elif self.common.platform == "Windows":
# On Windows, os.path.expanduser() needs to use backslash, or else it
# retains the forward slash, which breaks opening the folder in explorer.
return os.path.expanduser("~\OnionShare")
else:
# All other OSes
return os.path.expanduser("~/OnionShare")
def load(self, filename=None):
# Load persistent settings from disk. If the file doesn't exist, create it
if filename:
self.filename = filename
else:
self.filename = os.path.join(
self.common.build_persistent_dir(), f"{self.id}.json"
)
if os.path.exists(self.filename):
try:
with open(self.filename, "r") as f:
self._settings = json.load(f)
self.fill_in_defaults()
self.common.log("ModeSettings", "load", f"loaded {self.filename}")
return
except:
pass
# If loading settings didn't work, create the settings file
self.common.log("ModeSettings", "load", f"creating {self.filename}")
self.fill_in_defaults()
self.just_created = True
def save(self):
# Save persistent setting to disk
if not self.get("persistent", "enabled"):
return
if self.filename:
with open(self.filename, "w") as file:
file.write(json.dumps(self._settings, indent=2))
def delete(self):
# Delete the file from disk
if os.path.exists(self.filename):
os.remove(self.filename)

View File

@ -150,15 +150,11 @@ class Onion(object):
is necessary for status updates to reach the GUI.
"""
def __init__(self, common):
def __init__(self, common, use_tmp_dir=False):
self.common = common
self.common.log("Onion", "__init__")
self.stealth = False
self.service_id = None
self.scheduled_key = None
self.scheduled_auth_cookie = None
self.use_tmp_dir = use_tmp_dir
# Is bundled tor supported?
if (
@ -187,11 +183,18 @@ class Onion(object):
def connect(
self,
custom_settings=False,
config=False,
custom_settings=None,
config=None,
tor_status_update_func=None,
connect_timeout=120,
local_only=False,
):
if local_only:
self.common.log(
"Onion", "connect", "--local-only, so skip trying to connect"
)
return
self.common.log("Onion", "connect")
# Either use settings that are passed in, or use them from common
@ -205,6 +208,7 @@ class Onion(object):
self.settings = self.common.settings
strings.load_strings(self.common)
# The Tor controller
self.c = None
@ -215,24 +219,30 @@ class Onion(object):
)
# Create a torrc for this session
self.tor_data_directory = tempfile.TemporaryDirectory(
dir=self.common.build_data_dir()
)
if self.use_tmp_dir:
self.tor_data_directory = tempfile.TemporaryDirectory(
dir=self.common.build_tmp_dir()
)
self.tor_data_directory_name = self.tor_data_directory.name
else:
self.tor_data_directory_name = self.common.build_tor_dir()
self.common.log(
"Onion", "connect", f"tor_data_directory={self.tor_data_directory.name}"
"Onion",
"connect",
f"tor_data_directory_name={self.tor_data_directory_name}",
)
# Create the torrc
with open(self.common.get_resource_path("torrc_template")) as f:
torrc_template = f.read()
self.tor_cookie_auth_file = os.path.join(
self.tor_data_directory.name, "cookie"
self.tor_data_directory_name, "cookie"
)
try:
self.tor_socks_port = self.common.get_available_port(1000, 65535)
except:
raise OSError(strings._("no_available_port"))
self.tor_torrc = os.path.join(self.tor_data_directory.name, "torrc")
self.tor_torrc = os.path.join(self.tor_data_directory_name, "torrc")
if self.common.platform == "Windows" or self.common.platform == "Darwin":
# Windows doesn't support unix sockets, so it must use a network port.
@ -250,11 +260,11 @@ class Onion(object):
torrc_template += "ControlSocket {{control_socket}}\n"
self.tor_control_port = None
self.tor_control_socket = os.path.join(
self.tor_data_directory.name, "control_socket"
self.tor_data_directory_name, "control_socket"
)
torrc_template = torrc_template.replace(
"{{data_directory}}", self.tor_data_directory.name
"{{data_directory}}", self.tor_data_directory_name
)
torrc_template = torrc_template.replace(
"{{control_port}}", str(self.tor_control_port)
@ -562,60 +572,45 @@ class Onion(object):
else:
return False
def start_onion_service(self, port, await_publication, save_scheduled_key=False):
def start_onion_service(self, mode_settings, port, await_publication):
"""
Start a onion service on port 80, pointing to the given port, and
return the onion hostname.
"""
self.common.log("Onion", "start_onion_service")
# Settings may have changed in the frontend but not updated in our settings object,
# such as persistence. Reload the settings now just to be sure.
self.settings.load()
self.auth_string = None
self.common.log("Onion", "start_onion_service", f"port={port}")
if not self.supports_ephemeral:
raise TorTooOld(strings._("error_ephemeral_not_supported"))
if self.stealth and not self.supports_stealth:
if mode_settings.get("general", "client_auth") and not self.supports_stealth:
raise TorTooOld(strings._("error_stealth_not_supported"))
if not save_scheduled_key:
print(f"Setting up onion service on port {port}.")
if self.stealth:
if self.settings.get("hidservauth_string"):
hidservauth_string = self.settings.get("hidservauth_string").split()[2]
basic_auth = {"onionshare": hidservauth_string}
auth_cookie = None
if mode_settings.get("general", "client_auth"):
if mode_settings.get("onion", "hidservauth_string"):
auth_cookie = mode_settings.get("onion", "hidservauth_string").split()[
2
]
if auth_cookie:
basic_auth = {"onionshare": auth_cookie}
else:
if self.scheduled_auth_cookie:
basic_auth = {"onionshare": self.scheduled_auth_cookie}
else:
basic_auth = {"onionshare": None}
# If we had neither a scheduled auth cookie or a persistent hidservauth string,
# set the cookie to 'None', which means Tor will create one for us
basic_auth = {"onionshare": None}
else:
# Not using client auth at all
basic_auth = None
if self.settings.get("private_key"):
key_content = self.settings.get("private_key")
if mode_settings.get("onion", "private_key"):
key_content = mode_settings.get("onion", "private_key")
if self.is_v2_key(key_content):
key_type = "RSA1024"
else:
# Assume it was a v3 key. Stem will throw an error if it's something illegible
key_type = "ED25519-V3"
elif self.scheduled_key:
key_content = self.scheduled_key
if self.is_v2_key(key_content):
key_type = "RSA1024"
else:
# Assume it was a v3 key. Stem will throw an error if it's something illegible
key_type = "ED25519-V3"
else:
key_type = "NEW"
# Work out if we can support v3 onion services, which are preferred
if self.supports_v3_onions and not self.settings.get(
"use_legacy_v2_onions"
):
if self.supports_v3_onions and not mode_settings.get("general", "legacy"):
key_content = "ED25519-V3"
else:
# fall back to v2 onion services
@ -626,87 +621,59 @@ class Onion(object):
if (
key_type == "NEW"
and key_content == "ED25519-V3"
and not self.settings.get("use_legacy_v2_onions")
and not mode_settings.get("general", "legacy")
):
basic_auth = None
self.stealth = False
debug_message = f"key_type={key_type}"
if key_type == "NEW":
debug_message += f", key_content={key_content}"
self.common.log("Onion", "start_onion_service", debug_message)
try:
if basic_auth != None:
res = self.c.create_ephemeral_hidden_service(
{80: port},
await_publication=await_publication,
basic_auth=basic_auth,
key_type=key_type,
key_content=key_content,
)
else:
# if the stem interface is older than 1.5.0, basic_auth isn't a valid keyword arg
res = self.c.create_ephemeral_hidden_service(
{80: port},
await_publication=await_publication,
key_type=key_type,
key_content=key_content,
)
res = self.c.create_ephemeral_hidden_service(
{80: port},
await_publication=await_publication,
basic_auth=basic_auth,
key_type=key_type,
key_content=key_content,
)
except ProtocolError as e:
raise TorErrorProtocolError(
strings._("error_tor_protocol_error").format(e.args[0])
)
self.service_id = res.service_id
onion_host = self.service_id + ".onion"
onion_host = res.service_id + ".onion"
# A new private key was generated and is in the Control port response.
if self.settings.get("save_private_key"):
if not self.settings.get("private_key"):
self.settings.set("private_key", res.private_key)
# Save the service_id
mode_settings.set("general", "service_id", res.service_id)
# If we were scheduling a future share, register the private key for later re-use
if save_scheduled_key:
self.scheduled_key = res.private_key
else:
self.scheduled_key = None
# Save the private key and hidservauth string
if not mode_settings.get("onion", "private_key"):
mode_settings.set("onion", "private_key", res.private_key)
if mode_settings.get("general", "client_auth") and not mode_settings.get(
"onion", "hidservauth_string"
):
auth_cookie = list(res.client_auth.values())[0]
auth_string = f"HidServAuth {onion_host} {auth_cookie}"
mode_settings.set("onion", "hidservauth_string", auth_string)
if self.stealth:
# Similar to the PrivateKey, the Control port only returns the ClientAuth
# in the response if it was responsible for creating the basic_auth password
# in the first place.
# If we sent the basic_auth (due to a saved hidservauth_string in the settings),
# there is no response here, so use the saved value from settings.
if self.settings.get("save_private_key"):
if self.settings.get("hidservauth_string"):
self.auth_string = self.settings.get("hidservauth_string")
else:
auth_cookie = list(res.client_auth.values())[0]
self.auth_string = f"HidServAuth {onion_host} {auth_cookie}"
self.settings.set("hidservauth_string", self.auth_string)
else:
if not self.scheduled_auth_cookie:
auth_cookie = list(res.client_auth.values())[0]
self.auth_string = f"HidServAuth {onion_host} {auth_cookie}"
if save_scheduled_key:
# Register the HidServAuth for the scheduled share
self.scheduled_auth_cookie = auth_cookie
else:
self.scheduled_auth_cookie = None
else:
self.auth_string = (
f"HidServAuth {onion_host} {self.scheduled_auth_cookie}"
)
if not save_scheduled_key:
# We've used the scheduled share's HidServAuth. Reset it to None for future shares
self.scheduled_auth_cookie = None
return onion_host
if onion_host is not None:
self.settings.save()
return onion_host
else:
raise TorErrorProtocolError(strings._("error_tor_protocol_error_unknown"))
def stop_onion_service(self, mode_settings):
"""
Stop a specific onion service
"""
onion_host = mode_settings.get("general", "service_id")
self.common.log("Onion", "stop_onion_service", f"onion host: {onion_host}")
try:
self.c.remove_ephemeral_hidden_service(
mode_settings.get("general", "service_id")
)
except:
self.common.log(
"Onion", "stop_onion_service", f"failed to remove {onion_host}"
)
def cleanup(self, stop_tor=True):
"""
@ -717,48 +684,55 @@ class Onion(object):
# Cleanup the ephemeral onion services, if we have any
try:
onions = self.c.list_ephemeral_hidden_services()
for onion in onions:
for service_id in onions:
onion_host = f"{service_id}.onion"
try:
self.common.log(
"Onion", "cleanup", f"trying to remove onion {onion}"
"Onion", "cleanup", f"trying to remove onion {onion_host}"
)
self.c.remove_ephemeral_hidden_service(onion)
self.c.remove_ephemeral_hidden_service(service_id)
except:
self.common.log(
"Onion",
"cleanup",
f"could not remove onion {onion}.. moving on anyway",
"Onion", "cleanup", f"failed to remove onion {onion_host}"
)
pass
except:
pass
self.service_id = None
if stop_tor:
# Stop tor process
if self.tor_proc:
self.tor_proc.terminate()
time.sleep(0.2)
if not self.tor_proc.poll():
if self.tor_proc.poll() == None:
self.common.log(
"Onion",
"cleanup",
"Tried to terminate tor process but it's still running",
)
try:
self.tor_proc.kill()
time.sleep(0.2)
if self.tor_proc.poll() == None:
self.common.log(
"Onion",
"cleanup",
"Tried to kill tor process but it's still running",
)
except:
pass
self.common.log(
"Onion", "cleanup", "Exception while killing tor process"
)
self.tor_proc = None
# Reset other Onion settings
self.connected_to_tor = False
self.stealth = False
try:
# Delete the temporary tor data directory
self.tor_data_directory.cleanup()
except AttributeError:
# Skip if cleanup was somehow run before connect
pass
except PermissionError:
# Skip if the directory is still open (#550)
# TODO: find a better solution
if self.use_tmp_dir:
self.tor_data_directory.cleanup()
except:
pass
def get_tor_socks_port(self):

View File

@ -42,7 +42,6 @@ class OnionShare(object):
self.hidserv_dir = None
self.onion_host = None
self.port = None
self.stealth = None
# files and dirs to delete on shutdown
self.cleanup_filenames = []
@ -55,12 +54,6 @@ class OnionShare(object):
# init auto-stop timer thread
self.autostop_timer_thread = None
def set_stealth(self, stealth):
self.common.log("OnionShare", f"set_stealth", "stealth={stealth}")
self.stealth = stealth
self.onion.stealth = stealth
def choose_port(self):
"""
Choose a random port.
@ -70,7 +63,7 @@ class OnionShare(object):
except:
raise OSError(strings._("no_available_port"))
def start_onion_service(self, await_publication=True, save_scheduled_key=False):
def start_onion_service(self, mode_settings, await_publication=True):
"""
Start the onionshare onion service.
"""
@ -87,12 +80,18 @@ class OnionShare(object):
return
self.onion_host = self.onion.start_onion_service(
self.port, await_publication, save_scheduled_key
mode_settings, self.port, await_publication
)
if self.stealth:
if mode_settings.get("general", "client_auth"):
self.auth_string = self.onion.auth_string
def stop_onion_service(self, mode_settings):
"""
Stop the onion service
"""
self.onion.stop_onion_service(mode_settings)
def cleanup(self):
"""
Shut everything down and clean up temporary files, etc.

View File

@ -106,24 +106,13 @@ class Settings(object):
"socket_file_path": "/var/run/tor/control",
"auth_type": "no_auth",
"auth_password": "",
"close_after_first_download": True,
"autostop_timer": False,
"autostart_timer": False,
"use_stealth": False,
"use_autoupdate": True,
"autoupdate_timestamp": None,
"no_bridges": True,
"tor_bridges_use_obfs4": False,
"tor_bridges_use_meek_lite_azure": False,
"tor_bridges_use_custom_bridges": "",
"use_legacy_v2_onions": False,
"save_private_key": False,
"private_key": "",
"public_mode": False,
"password": "",
"hidservauth_string": "",
"data_dir": self.build_default_data_dir(),
"csp_header_disabled": False,
"persistent_tabs": [],
"locale": None, # this gets defined in fill_in_defaults()
}
self._settings = {}
@ -163,24 +152,6 @@ class Settings(object):
"""
return os.path.join(self.common.build_data_dir(), "onionshare.json")
def build_default_data_dir(self):
"""
Returns the path of the default Downloads directory for receive mode.
"""
if self.common.platform == "Darwin":
# We can't use os.path.expanduser() in macOS because in the sandbox it
# returns the path to the sandboxed homedir
real_homedir = pwd.getpwuid(os.getuid()).pw_dir
return os.path.join(real_homedir, "OnionShare")
elif self.common.platform == "Windows":
# On Windows, os.path.expanduser() needs to use backslash, or else it
# retains the forward slash, which breaks opening the folder in explorer.
return os.path.expanduser("~\OnionShare")
else:
# All other OSes
return os.path.expanduser("~/OnionShare")
def load(self):
"""
Load the settings from file.

View File

@ -292,7 +292,7 @@ class ReceiveModeRequest(Request):
date_dir = now.strftime("%Y-%m-%d")
time_dir = now.strftime("%H.%M.%S")
self.receive_mode_dir = os.path.join(
self.web.common.settings.get("data_dir"), date_dir, time_dir
self.web.settings.get("receive", "data_dir"), date_dir, time_dir
)
# Create that directory, which shouldn't exist yet
@ -358,14 +358,9 @@ class ReceiveModeRequest(Request):
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)
),
)
)
date_str = datetime.now().strftime("%b %d, %I:%M%p")
size_str = self.web.common.human_readable_filesize(self.content_length)
print(f"{date_str}: Upload of total size {size_str} is starting")
# Don't tell the GUI that a request has started until we start receiving files
self.told_gui_about_request = False
@ -453,10 +448,10 @@ class ReceiveModeRequest(Request):
if self.previous_file != filename:
self.previous_file = filename
print(
f"\r=> {self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes'])} {filename}",
end="",
size_str = self.web.common.human_readable_filesize(
self.progress[filename]["uploaded_bytes"]
)
print(f"\r=> {size_str} {filename} ", end="")
# Update the GUI on the upload progress
if self.told_gui_about_request:

View File

@ -26,8 +26,7 @@ class SendBaseModeWeb:
self.gzip_filesize = None
self.zip_writer = None
# If "Stop After First Download" is checked (stay_open == False), only allow
# one download at a time.
# If autostop_sharing, only allow one download at a time
self.download_in_progress = False
# This tracks the history id

View File

@ -18,8 +18,8 @@ class ShareModeWeb(SendBaseModeWeb):
self.common.log("ShareModeWeb", "init")
# Allow downloading individual files if "Stop sharing after files have been sent" is unchecked
self.download_individual_files = not self.common.settings.get(
"close_after_first_download"
self.download_individual_files = not self.web.settings.get(
"share", "autostop_sharing"
)
def define_routes(self):
@ -37,7 +37,10 @@ class ShareModeWeb(SendBaseModeWeb):
# Deny new downloads if "Stop sharing after files have been sent" is checked and there is
# currently a download
deny_download = not self.web.stay_open and self.download_in_progress
deny_download = (
self.web.settings.get("share", "autostop_sharing")
and self.download_in_progress
)
if deny_download:
r = make_response(
render_template("denied.html"),
@ -60,7 +63,10 @@ class ShareModeWeb(SendBaseModeWeb):
"""
# 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
deny_download = (
self.web.settings.get("share", "autostop_sharing")
and self.download_in_progress
)
if deny_download:
r = make_response(
render_template(
@ -96,7 +102,7 @@ class ShareModeWeb(SendBaseModeWeb):
def generate():
# Starting a new download
if not self.web.stay_open:
if self.web.settings.get("share", "autostop_sharing"):
self.download_in_progress = True
chunk_size = 102400 # 100kb
@ -161,11 +167,11 @@ class ShareModeWeb(SendBaseModeWeb):
sys.stdout.write("\n")
# Download is finished
if not self.web.stay_open:
if self.web.settings.get("share", "autostop_sharing"):
self.download_in_progress = False
# Close the server, if necessary
if not self.web.stay_open and not canceled:
if self.web.settings.get("share", "autostop_sharing") and not canceled:
print("Stopped because transfer is complete")
self.web.running = False
try:

View File

@ -60,10 +60,12 @@ class Web:
REQUEST_OTHER = 13
REQUEST_INVALID_PASSWORD = 14
def __init__(self, common, is_gui, mode="share"):
def __init__(self, common, is_gui, mode_settings, mode="share"):
self.common = common
self.common.log("Web", "__init__", f"is_gui={is_gui}, mode={mode}")
self.settings = mode_settings
# The flask app
self.app = Flask(
__name__,
@ -186,7 +188,7 @@ class Web:
return None
# If public mode is disabled, require authentication
if not self.common.settings.get("public_mode"):
if not self.settings.get("general", "public"):
@self.auth.login_required
def _check_login():
@ -284,10 +286,7 @@ class Web:
for header, value in self.security_headers:
r.headers.set(header, value)
# Set a CSP header unless in website mode and the user has disabled it
if (
not self.common.settings.get("csp_header_disabled")
or self.mode != "website"
):
if not self.settings.get("website", "disable_csp") or self.mode != "website":
r.headers.set(
"Content-Security-Policy",
"default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:;",
@ -305,16 +304,14 @@ class Web:
"""
self.q.put({"type": request_type, "path": path, "data": data})
def generate_password(self, persistent_password=None):
self.common.log(
"Web", "generate_password", f"persistent_password={persistent_password}"
)
if persistent_password != None and persistent_password != "":
self.password = persistent_password
def generate_password(self, saved_password=None):
self.common.log("Web", "generate_password", f"saved_password={saved_password}")
if saved_password != None and saved_password != "":
self.password = saved_password
self.common.log(
"Web",
"generate_password",
f'persistent_password sent, so password is: "{self.password}"',
f'saved_password sent, so password is: "{self.password}"',
)
else:
self.password = self.common.build_password()
@ -349,17 +346,11 @@ class Web:
pass
self.running = False
def start(self, port, stay_open=False, public_mode=False, password=None):
def start(self, port):
"""
Start the flask web server.
"""
self.common.log(
"Web",
"start",
f"port={port}, stay_open={stay_open}, public_mode={public_mode}, password={password}",
)
self.stay_open = stay_open
self.common.log("Web", "start", f"port={port}")
# Make sure the stop_q is empty when starting a new server
while not self.stop_q.empty():
@ -389,10 +380,15 @@ class Web:
# To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown
# (We're putting the shutdown_password in the path as well to make routing simpler)
if self.running:
requests.get(
f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown",
auth=requests.auth.HTTPBasicAuth("onionshare", self.password),
)
if self.password:
requests.get(
f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown",
auth=requests.auth.HTTPBasicAuth("onionshare", self.password),
)
else:
requests.get(
f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown"
)
# Reset any password that was in use
self.password = None

View File

@ -23,14 +23,15 @@ import sys
import platform
import argparse
import signal
from .widgets import Alert
import json
import psutil
from PyQt5 import QtCore, QtWidgets
from onionshare.common import Common
from onionshare.onion import Onion
from onionshare.onionshare import OnionShare
from .onionshare_gui import OnionShareGui
from .gui_common import GuiCommon
from .widgets import Alert
from .main_window import MainWindow
class Application(QtWidgets.QApplication):
@ -60,7 +61,6 @@ def main():
The main() function implements all of the logic that the GUI version of onionshare uses.
"""
common = Common()
common.define_css()
# Display OnionShare banner
print(f"OnionShare {common.version} | https://onionshare.org/")
@ -96,12 +96,6 @@ def main():
nargs="+",
help="List of files or folders to share",
)
parser.add_argument(
"--config",
metavar="config",
default=False,
help="Custom JSON config file location (optional)",
)
args = parser.parse_args()
filenames = args.filenames
@ -109,16 +103,15 @@ def main():
for i in range(len(filenames)):
filenames[i] = os.path.abspath(filenames[i])
config = args.config
if config:
common.load_settings(config)
local_only = bool(args.local_only)
verbose = bool(args.verbose)
# Verbose mode?
common.verbose = verbose
# Attach the GUI common parts to the common object
common.gui = GuiCommon(common, qtapp, local_only)
# Validation
if filenames:
valid = True
@ -132,19 +125,50 @@ def main():
if not valid:
sys.exit()
# Start the Onion
onion = Onion(common)
# Is there another onionshare-gui running?
existing_pid = None
for proc in psutil.process_iter(attrs=["pid", "name", "cmdline"]):
if proc.info["pid"] == os.getpid():
continue
# Start the OnionShare app
app = OnionShare(common, onion, local_only)
if proc.info["name"] == "onionshare-gui" and proc.status() != "zombie":
existing_pid = proc.info["pid"]
break
else:
# Dev mode onionshare?
if proc.info["cmdline"] and len(proc.info["cmdline"]) >= 2:
if (
os.path.basename(proc.info["cmdline"][0]).lower() == "python"
and os.path.basename(proc.info["cmdline"][1]) == "onionshare-gui"
and proc.status() != "zombie"
):
existing_pid = proc.info["pid"]
break
if existing_pid:
print(f"Opening tab in existing OnionShare window (pid {proc.info['pid']})")
# Make an event for the existing OnionShare window
if filenames:
obj = {"type": "new_share_tab", "filenames": filenames}
else:
obj = {"type": "new_tab"}
# Write that event to disk
with open(common.gui.events_filename, "a") as f:
f.write(json.dumps(obj) + "\n")
return
# Launch the gui
gui = OnionShareGui(common, onion, qtapp, app, filenames, config, local_only)
main_window = MainWindow(common, filenames)
# If filenames were passed in, open them in a tab
if filenames:
main_window.tabs.new_share_tab(filenames)
# Clean up when app quits
def shutdown():
onion.cleanup()
app.cleanup()
main_window.cleanup()
qtapp.aboutToQuit.connect(shutdown)

View File

@ -0,0 +1,89 @@
# -*- 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 json
import os
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler, FileModifiedEvent
from PyQt5 import QtCore
class EventHandler(FileSystemEventHandler, QtCore.QObject):
"""
To trigger an event, write a JSON line to the events file. When that file changes,
each line will be handled as an event. Valid events are:
{"type": "new_tab"}
{"type": "new_share_tab", "filenames": ["file1", "file2"]}
"""
new_tab = QtCore.pyqtSignal()
new_share_tab = QtCore.pyqtSignal(list)
def __init__(self, common):
super(EventHandler, self).__init__()
self.common = common
def on_modified(self, event):
if (
type(event) == FileModifiedEvent
and event.src_path == self.common.gui.events_filename
):
# Read all the lines in the events, then delete it
with open(self.common.gui.events_filename, "r") as f:
lines = f.readlines()
os.remove(self.common.gui.events_filename)
self.common.log(
"EventHandler", "on_modified", f"processing {len(lines)} lines"
)
for line in lines:
try:
obj = json.loads(line)
if "type" not in obj:
self.common.log(
"EventHandler",
"on_modified",
f"event does not have a type: {obj}",
)
continue
except json.decoder.JSONDecodeError:
self.common.log(
"EventHandler", "on_modified", f"ignoring invalid line: {line}"
)
continue
if obj["type"] == "new_tab":
self.common.log("EventHandler", "on_modified", "new_tab event")
self.new_tab.emit()
elif obj["type"] == "new_share_tab":
if "filenames" in obj and type(obj["filenames"]) is list:
self.new_share_tab.emit(obj["filenames"])
else:
self.common.log(
"EventHandler",
"on_modified",
f"invalid new_share_tab event: {obj}",
)
else:
self.common.log(
"EventHandler", "on_modified", f"invalid event type: {obj}"
)

View File

@ -0,0 +1,287 @@
# -*- 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
from onionshare import strings
from onionshare.onion import Onion
class GuiCommon:
"""
The shared code for all of the OnionShare GUI.
"""
MODE_SHARE = "share"
MODE_RECEIVE = "receive"
MODE_WEBSITE = "website"
def __init__(self, common, qtapp, local_only):
self.common = common
self.qtapp = qtapp
self.local_only = local_only
# Load settings
self.common.load_settings()
# Load strings
strings.load_strings(self.common)
# Start the Onion
self.onion = Onion(common)
# Directory to watch for events
self.events_dir = os.path.join(self.common.build_data_dir(), "events")
if not os.path.exists(self.events_dir):
os.makedirs(self.events_dir, 0o700, True)
self.events_filename = os.path.join(self.events_dir, "events")
self.css = {
# OnionShareGui styles
"tab_widget_new_tab_button": """
QPushButton {
font-weight: bold;
font-size: 20px;
}""",
"mode_new_tab_button": """
QPushButton {
font-weight: bold;
font-size: 30px;
color: #601f61;
}""",
"mode_header_label": """
QLabel {
color: #ffffff;
background-color: #4e064f;
border: 0;
font-weight: bold;
font-size: 18px;
border-radius: 0;
padding: 10px 0 10px 0;
}""",
"settings_button": """
QPushButton {
border: 0;
border-radius: 0;
}""",
"server_status_indicator_label": """
QLabel {
font-style: italic;
color: #666666;
padding: 2px;
}""",
"status_bar": """
QStatusBar {
font-style: italic;
color: #666666;
}
QStatusBar::item {
border: 0px;
}""",
# Common styles between modes and their child widgets
"mode_settings_toggle_advanced": """
QPushButton {
color: #3f7fcf;
text-align: left;
}
""",
"mode_info_label": """
QLabel {
font-size: 12px;
color: #666666;
}
""",
"server_status_url": """
QLabel {
background-color: #ffffff;
color: #000000;
padding: 10px;
border: 1px solid #666666;
font-size: 12px;
}
""",
"server_status_url_buttons": """
QPushButton {
color: #3f7fcf;
}
""",
"server_status_button_stopped": """
QPushButton {
background-color: #5fa416;
color: #ffffff;
padding: 10px;
border: 0;
border-radius: 5px;
}""",
"server_status_button_working": """
QPushButton {
background-color: #4c8211;
color: #ffffff;
padding: 10px;
border: 0;
border-radius: 5px;
font-style: italic;
}""",
"server_status_button_started": """
QPushButton {
background-color: #d0011b;
color: #ffffff;
padding: 10px;
border: 0;
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": """
QLabel {
font-weight: bold;
font-size 14px;
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": """
QProgressBar {
border: 1px solid #4e064f;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
font-size: 14px;
}
QProgressBar::chunk {
background-color: #4e064f;
width: 10px;
}""",
"history_individual_file_timestamp_label": """
QLabel {
color: #666666;
}""",
"history_individual_file_status_code_label_2xx": """
QLabel {
color: #008800;
}""",
"history_individual_file_status_code_label_4xx": """
QLabel {
color: #cc0000;
}""",
# Share mode and child widget styles
"share_zip_progess_bar": """
QProgressBar {
border: 1px solid #4e064f;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
}
QProgressBar::chunk {
border: 0px;
background-color: #4e064f;
width: 10px;
}""",
"share_filesize_warning": """
QLabel {
padding: 10px 0;
font-weight: bold;
color: #333333;
}
""",
"share_file_selection_drop_here_label": """
QLabel {
color: #999999;
}""",
"share_file_selection_drop_count_label": """
QLabel {
color: #ffffff;
background-color: #f44449;
font-weight: bold;
padding: 5px 10px;
border-radius: 10px;
}""",
"share_file_list_drag_enter": """
FileList {
border: 3px solid #538ad0;
}
""",
"share_file_list_drag_leave": """
FileList {
border: none;
}
""",
"share_file_list_item_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
# Receive mode and child widget styles
"receive_file": """
QWidget {
background-color: #ffffff;
}
""",
"receive_file_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
# Settings dialog
"settings_version": """
QLabel {
color: #666666;
}""",
"settings_tor_status": """
QLabel {
background-color: #ffffff;
color: #000000;
padding: 10px;
}""",
"settings_whats_this": """
QLabel {
font-size: 12px;
}""",
"settings_connect_to_tor": """
QLabel {
font-style: italic;
}""",
}

View File

@ -0,0 +1,288 @@
# -*- 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 .tor_connection_dialog import TorConnectionDialog
from .settings_dialog import SettingsDialog
from .widgets import Alert
from .update_checker import UpdateThread
from .tab_widget import TabWidget
class MainWindow(QtWidgets.QMainWindow):
"""
MainWindow is the OnionShare main window, which contains the GUI elements, including all open tabs
"""
def __init__(self, common, filenames):
super(MainWindow, self).__init__()
self.common = common
self.common.log("MainWindow", "__init__")
# Initialize the window
self.setMinimumWidth(1040)
self.setMinimumHeight(700)
self.setWindowTitle("OnionShare")
self.setWindowIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
)
# System tray
menu = QtWidgets.QMenu()
self.settings_action = menu.addAction(strings._("gui_settings_window_title"))
self.settings_action.triggered.connect(self.open_settings)
self.help_action = menu.addAction(strings._("gui_settings_button_help"))
self.help_action.triggered.connect(lambda: SettingsDialog.help_clicked(self))
exit_action = menu.addAction(strings._("systray_menu_exit"))
exit_action.triggered.connect(self.close)
self.system_tray = QtWidgets.QSystemTrayIcon(self)
# The convention is Mac systray icons are always grayscale
if self.common.platform == "Darwin":
self.system_tray.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo_grayscale.png"))
)
else:
self.system_tray.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
)
self.system_tray.setContextMenu(menu)
self.system_tray.show()
# Status bar
self.status_bar = QtWidgets.QStatusBar()
self.status_bar.setSizeGripEnabled(False)
self.status_bar.setStyleSheet(self.common.gui.css["status_bar"])
self.setStatusBar(self.status_bar)
# Server status indicator icons
self.status_bar.server_status_image_stopped = QtGui.QImage(
self.common.get_resource_path("images/server_stopped.png")
)
self.status_bar.server_status_image_working = QtGui.QImage(
self.common.get_resource_path("images/server_working.png")
)
self.status_bar.server_status_image_started = QtGui.QImage(
self.common.get_resource_path("images/server_started.png")
)
# Server status indicator on the status bar
self.status_bar.server_status_image_label = QtWidgets.QLabel()
self.status_bar.server_status_image_label.setFixedWidth(20)
self.status_bar.server_status_label = QtWidgets.QLabel("")
self.status_bar.server_status_label.setStyleSheet(
self.common.gui.css["server_status_indicator_label"]
)
server_status_indicator_layout = QtWidgets.QHBoxLayout()
server_status_indicator_layout.addWidget(
self.status_bar.server_status_image_label
)
server_status_indicator_layout.addWidget(self.status_bar.server_status_label)
self.status_bar.server_status_indicator = QtWidgets.QWidget()
self.status_bar.server_status_indicator.setLayout(
server_status_indicator_layout
)
self.status_bar.addPermanentWidget(self.status_bar.server_status_indicator)
# Settings button
self.settings_button = QtWidgets.QPushButton()
self.settings_button.setDefault(False)
self.settings_button.setFixedSize(40, 50)
self.settings_button.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/settings.png"))
)
self.settings_button.clicked.connect(self.open_settings)
self.settings_button.setStyleSheet(self.common.gui.css["settings_button"])
self.status_bar.addPermanentWidget(self.settings_button)
# Tabs
self.tabs = TabWidget(self.common, self.system_tray, self.status_bar)
self.tabs.bring_to_front.connect(self.bring_to_front)
# If we have saved persistent tabs, try opening those
if len(self.common.settings.get("persistent_tabs")) > 0:
for mode_settings_id in self.common.settings.get("persistent_tabs"):
self.tabs.load_tab(mode_settings_id)
else:
# Start with opening the first tab
self.tabs.new_tab_clicked()
# Layout
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.tabs)
central_widget = QtWidgets.QWidget()
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
self.show()
# Start the "Connecting to Tor" dialog, which calls onion.connect()
tor_con = TorConnectionDialog(self.common)
tor_con.canceled.connect(self.tor_connection_canceled)
tor_con.open_settings.connect(self.tor_connection_open_settings)
if not self.common.gui.local_only:
tor_con.start()
self.settings_have_changed()
# After connecting to Tor, check for updates
self.check_for_updates()
# Create the close warning dialog -- the dialog widget needs to be in the constructor
# in order to test it
self.close_dialog = QtWidgets.QMessageBox()
self.close_dialog.setWindowTitle(strings._("gui_quit_warning_title"))
self.close_dialog.setText(strings._("gui_quit_warning_description"))
self.close_dialog.setIcon(QtWidgets.QMessageBox.Critical)
self.close_dialog.accept_button = self.close_dialog.addButton(
strings._("gui_quit_warning_quit"), QtWidgets.QMessageBox.AcceptRole
)
self.close_dialog.reject_button = self.close_dialog.addButton(
strings._("gui_quit_warning_cancel"), QtWidgets.QMessageBox.NoRole
)
self.close_dialog.setDefaultButton(self.close_dialog.reject_button)
def tor_connection_canceled(self):
"""
If the user cancels before Tor finishes connecting, ask if they want to
quit, or open settings.
"""
self.common.log("MainWindow", "tor_connection_canceled")
def ask():
a = Alert(
self.common,
strings._("gui_tor_connection_ask"),
QtWidgets.QMessageBox.Question,
buttons=QtWidgets.QMessageBox.NoButton,
autostart=False,
)
settings_button = QtWidgets.QPushButton(
strings._("gui_tor_connection_ask_open_settings")
)
quit_button = QtWidgets.QPushButton(
strings._("gui_tor_connection_ask_quit")
)
a.addButton(settings_button, QtWidgets.QMessageBox.AcceptRole)
a.addButton(quit_button, QtWidgets.QMessageBox.RejectRole)
a.setDefaultButton(settings_button)
a.exec_()
if a.clickedButton() == settings_button:
# Open settings
self.common.log(
"OnionShareGui",
"_tor_connection_canceled",
"Settings button clicked",
)
self.open_settings()
if a.clickedButton() == quit_button:
# Quit
self.common.log(
"OnionShareGui", "_tor_connection_canceled", "Quit button clicked"
)
# Wait 1ms for the event loop to finish, then quit
QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit)
# Wait 100ms before asking
QtCore.QTimer.singleShot(100, ask)
def tor_connection_open_settings(self):
"""
The TorConnectionDialog wants to open the Settings dialog
"""
self.common.log("MainWindow", "tor_connection_open_settings")
# Wait 1ms for the event loop to finish closing the TorConnectionDialog
QtCore.QTimer.singleShot(1, self.open_settings)
def open_settings(self):
"""
Open the SettingsDialog.
"""
self.common.log("MainWindow", "open_settings")
d = SettingsDialog(self.common)
d.settings_saved.connect(self.settings_have_changed)
d.exec_()
def settings_have_changed(self):
self.common.log("OnionShareGui", "settings_have_changed")
if self.common.gui.onion.is_authenticated():
self.status_bar.clearMessage()
# Tell each tab that settings have changed
for index in range(self.tabs.count()):
tab = self.tabs.widget(index)
tab.settings_have_changed()
def check_for_updates(self):
"""
Check for updates in a new thread, if enabled.
"""
if self.common.platform == "Windows" or self.common.platform == "Darwin":
if self.common.settings.get("use_autoupdate"):
def update_available(update_url, installed_version, latest_version):
Alert(
self.common,
strings._("update_available").format(
update_url, installed_version, latest_version
),
)
self.update_thread = UpdateThread(self.common, self.common.gui.onion)
self.update_thread.update_available.connect(update_available)
self.update_thread.start()
def bring_to_front(self):
self.common.log("MainWindow", "bring_to_front")
self.raise_()
self.activateWindow()
def closeEvent(self, e):
self.common.log("MainWindow", "closeEvent")
if self.tabs.are_tabs_active():
# Open the warning dialog
self.common.log("MainWindow", "closeEvent, opening warning dialog")
self.close_dialog.exec_()
# Close
if self.close_dialog.clickedButton() == self.close_dialog.accept_button:
self.system_tray.hide()
e.accept()
# Cancel
else:
e.ignore()
return
self.system_tray.hide()
e.accept()
def cleanup(self):
self.tabs.cleanup()
self.common.gui.onion.cleanup()

View File

@ -1,763 +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 queue
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from onionshare.web import Web
from .mode.share_mode import ShareMode
from .mode.receive_mode import ReceiveMode
from .mode.website_mode import WebsiteMode
from .tor_connection_dialog import TorConnectionDialog
from .settings_dialog import SettingsDialog
from .widgets import Alert
from .update_checker import UpdateThread
from .server_status import ServerStatus
class OnionShareGui(QtWidgets.QMainWindow):
"""
OnionShareGui is the main window for the GUI that contains all of the
GUI elements.
"""
MODE_SHARE = "share"
MODE_RECEIVE = "receive"
MODE_WEBSITE = "website"
def __init__(
self, common, onion, qtapp, app, filenames, config=False, local_only=False
):
super(OnionShareGui, self).__init__()
self.common = common
self.common.log("OnionShareGui", "__init__")
self.setMinimumWidth(820)
self.setMinimumHeight(660)
self.onion = onion
self.qtapp = qtapp
self.app = app
self.local_only = local_only
self.mode = self.MODE_SHARE
self.setWindowTitle("OnionShare")
self.setWindowIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
)
# Load settings, if a custom config was passed in
self.config = config
if self.config:
self.common.load_settings(self.config)
else:
self.common.load_settings()
strings.load_strings(self.common)
# System tray
menu = QtWidgets.QMenu()
self.settings_action = menu.addAction(strings._("gui_settings_window_title"))
self.settings_action.triggered.connect(self.open_settings)
self.help_action = menu.addAction(strings._("gui_settings_button_help"))
self.help_action.triggered.connect(lambda: SettingsDialog.help_clicked(self))
exit_action = menu.addAction(strings._("systray_menu_exit"))
exit_action.triggered.connect(self.close)
self.system_tray = QtWidgets.QSystemTrayIcon(self)
# The convention is Mac systray icons are always grayscale
if self.common.platform == "Darwin":
self.system_tray.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo_grayscale.png"))
)
else:
self.system_tray.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
)
self.system_tray.setContextMenu(menu)
self.system_tray.show()
# Mode switcher, to switch between share files and receive files
self.share_mode_button = QtWidgets.QPushButton(
strings._("gui_mode_share_button")
)
self.share_mode_button.setFixedHeight(50)
self.share_mode_button.clicked.connect(self.share_mode_clicked)
self.receive_mode_button = QtWidgets.QPushButton(
strings._("gui_mode_receive_button")
)
self.receive_mode_button.setFixedHeight(50)
self.receive_mode_button.clicked.connect(self.receive_mode_clicked)
self.website_mode_button = QtWidgets.QPushButton(
strings._("gui_mode_website_button")
)
self.website_mode_button.setFixedHeight(50)
self.website_mode_button.clicked.connect(self.website_mode_clicked)
self.settings_button = QtWidgets.QPushButton()
self.settings_button.setDefault(False)
self.settings_button.setFixedWidth(40)
self.settings_button.setFixedHeight(50)
self.settings_button.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/settings.png"))
)
self.settings_button.clicked.connect(self.open_settings)
self.settings_button.setStyleSheet(self.common.css["settings_button"])
mode_switcher_layout = QtWidgets.QHBoxLayout()
mode_switcher_layout.setSpacing(0)
mode_switcher_layout.addWidget(self.share_mode_button)
mode_switcher_layout.addWidget(self.receive_mode_button)
mode_switcher_layout.addWidget(self.website_mode_button)
mode_switcher_layout.addWidget(self.settings_button)
# Server status indicator on the status bar
self.server_status_image_stopped = QtGui.QImage(
self.common.get_resource_path("images/server_stopped.png")
)
self.server_status_image_working = QtGui.QImage(
self.common.get_resource_path("images/server_working.png")
)
self.server_status_image_started = QtGui.QImage(
self.common.get_resource_path("images/server_started.png")
)
self.server_status_image_label = QtWidgets.QLabel()
self.server_status_image_label.setFixedWidth(20)
self.server_status_label = QtWidgets.QLabel("")
self.server_status_label.setStyleSheet(
self.common.css["server_status_indicator_label"]
)
server_status_indicator_layout = QtWidgets.QHBoxLayout()
server_status_indicator_layout.addWidget(self.server_status_image_label)
server_status_indicator_layout.addWidget(self.server_status_label)
self.server_status_indicator = QtWidgets.QWidget()
self.server_status_indicator.setLayout(server_status_indicator_layout)
# Status bar
self.status_bar = QtWidgets.QStatusBar()
self.status_bar.setSizeGripEnabled(False)
self.status_bar.setStyleSheet(self.common.css["status_bar"])
self.status_bar.addPermanentWidget(self.server_status_indicator)
self.setStatusBar(self.status_bar)
# Share mode
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.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.start_server_finished.connect(
self.update_server_status_indicator
)
self.share_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.share_mode.stop_server_finished.connect(self.stop_server_finished)
self.share_mode.start_server_finished.connect(self.clear_message)
self.share_mode.server_status.button_clicked.connect(self.clear_message)
self.share_mode.server_status.url_copied.connect(self.copy_url)
self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.share_mode.set_server_active.connect(self.set_server_active)
# Receive mode
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.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.start_server_finished.connect(
self.update_server_status_indicator
)
self.receive_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.receive_mode.stop_server_finished.connect(self.stop_server_finished)
self.receive_mode.start_server_finished.connect(self.clear_message)
self.receive_mode.server_status.button_clicked.connect(self.clear_message)
self.receive_mode.server_status.url_copied.connect(self.copy_url)
self.receive_mode.server_status.hidservauth_copied.connect(
self.copy_hidservauth
)
self.receive_mode.set_server_active.connect(self.set_server_active)
# Website mode
self.website_mode = WebsiteMode(
self.common,
qtapp,
app,
self.status_bar,
self.server_status_label,
self.system_tray,
filenames,
)
self.website_mode.init()
self.website_mode.server_status.server_started.connect(
self.update_server_status_indicator
)
self.website_mode.server_status.server_stopped.connect(
self.update_server_status_indicator
)
self.website_mode.start_server_finished.connect(
self.update_server_status_indicator
)
self.website_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.website_mode.stop_server_finished.connect(self.stop_server_finished)
self.website_mode.start_server_finished.connect(self.clear_message)
self.website_mode.server_status.button_clicked.connect(self.clear_message)
self.website_mode.server_status.url_copied.connect(self.copy_url)
self.website_mode.server_status.hidservauth_copied.connect(
self.copy_hidservauth
)
self.website_mode.set_server_active.connect(self.set_server_active)
self.update_mode_switcher()
self.update_server_status_indicator()
# Layouts
contents_layout = QtWidgets.QVBoxLayout()
contents_layout.setContentsMargins(10, 0, 10, 0)
contents_layout.addWidget(self.receive_mode)
contents_layout.addWidget(self.share_mode)
contents_layout.addWidget(self.website_mode)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(mode_switcher_layout)
layout.addLayout(contents_layout)
central_widget = QtWidgets.QWidget()
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
self.show()
# The server isn't active yet
self.set_server_active(False)
# Create the timer
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.timer_callback)
# Start the "Connecting to Tor" dialog, which calls onion.connect()
tor_con = TorConnectionDialog(self.common, self.qtapp, self.onion)
tor_con.canceled.connect(self._tor_connection_canceled)
tor_con.open_settings.connect(self._tor_connection_open_settings)
if not self.local_only:
tor_con.start()
# Start the timer
self.timer.start(500)
# After connecting to Tor, check for updates
self.check_for_updates()
def update_mode_switcher(self):
# Based on the current mode, switch the mode switcher button styles,
# and show and hide widgets to switch modes
if self.mode == self.MODE_SHARE:
self.share_mode_button.setStyleSheet(
self.common.css["mode_switcher_selected_style"]
)
self.receive_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.website_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.receive_mode.hide()
self.share_mode.show()
self.website_mode.hide()
elif self.mode == self.MODE_WEBSITE:
self.share_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.receive_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.website_mode_button.setStyleSheet(
self.common.css["mode_switcher_selected_style"]
)
self.receive_mode.hide()
self.share_mode.hide()
self.website_mode.show()
else:
self.share_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.receive_mode_button.setStyleSheet(
self.common.css["mode_switcher_selected_style"]
)
self.website_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.share_mode.hide()
self.receive_mode.show()
self.website_mode.hide()
self.update_server_status_indicator()
def share_mode_clicked(self):
if self.mode != self.MODE_SHARE:
self.common.log("OnionShareGui", "share_mode_clicked")
self.mode = self.MODE_SHARE
self.update_mode_switcher()
def receive_mode_clicked(self):
if self.mode != self.MODE_RECEIVE:
self.common.log("OnionShareGui", "receive_mode_clicked")
self.mode = self.MODE_RECEIVE
self.update_mode_switcher()
def website_mode_clicked(self):
if self.mode != self.MODE_WEBSITE:
self.common.log("OnionShareGui", "website_mode_clicked")
self.mode = self.MODE_WEBSITE
self.update_mode_switcher()
def update_server_status_indicator(self):
# Set the status image
if self.mode == self.MODE_SHARE:
# Share mode
if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_stopped)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_stopped")
)
elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING:
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_working)
)
if self.share_mode.server_status.autostart_timer_datetime:
self.server_status_label.setText(
strings._("gui_status_indicator_share_scheduled")
)
else:
self.server_status_label.setText(
strings._("gui_status_indicator_share_working")
)
elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_started)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_started")
)
elif self.mode == self.MODE_WEBSITE:
# Website mode
if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_stopped)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_stopped")
)
elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING:
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_working)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_working")
)
elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_started)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_started")
)
else:
# Receive mode
if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_stopped)
)
self.server_status_label.setText(
strings._("gui_status_indicator_receive_stopped")
)
elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING:
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_working)
)
if self.receive_mode.server_status.autostart_timer_datetime:
self.server_status_label.setText(
strings._("gui_status_indicator_receive_scheduled")
)
else:
self.server_status_label.setText(
strings._("gui_status_indicator_receive_working")
)
elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_started)
)
self.server_status_label.setText(
strings._("gui_status_indicator_receive_started")
)
def stop_server_finished(self):
# When the server stopped, cleanup the ephemeral onion service
self.onion.cleanup(stop_tor=False)
def _tor_connection_canceled(self):
"""
If the user cancels before Tor finishes connecting, ask if they want to
quit, or open settings.
"""
self.common.log("OnionShareGui", "_tor_connection_canceled")
def ask():
a = Alert(
self.common,
strings._("gui_tor_connection_ask"),
QtWidgets.QMessageBox.Question,
buttons=QtWidgets.QMessageBox.NoButton,
autostart=False,
)
settings_button = QtWidgets.QPushButton(
strings._("gui_tor_connection_ask_open_settings")
)
quit_button = QtWidgets.QPushButton(
strings._("gui_tor_connection_ask_quit")
)
a.addButton(settings_button, QtWidgets.QMessageBox.AcceptRole)
a.addButton(quit_button, QtWidgets.QMessageBox.RejectRole)
a.setDefaultButton(settings_button)
a.exec_()
if a.clickedButton() == settings_button:
# Open settings
self.common.log(
"OnionShareGui",
"_tor_connection_canceled",
"Settings button clicked",
)
self.open_settings()
if a.clickedButton() == quit_button:
# Quit
self.common.log(
"OnionShareGui", "_tor_connection_canceled", "Quit button clicked"
)
# Wait 1ms for the event loop to finish, then quit
QtCore.QTimer.singleShot(1, self.qtapp.quit)
# Wait 100ms before asking
QtCore.QTimer.singleShot(100, ask)
def _tor_connection_open_settings(self):
"""
The TorConnectionDialog wants to open the Settings dialog
"""
self.common.log("OnionShareGui", "_tor_connection_open_settings")
# Wait 1ms for the event loop to finish closing the TorConnectionDialog
QtCore.QTimer.singleShot(1, self.open_settings)
def open_settings(self):
"""
Open the SettingsDialog.
"""
self.common.log("OnionShareGui", "open_settings")
def reload_settings():
self.common.log(
"OnionShareGui", "open_settings", "settings have changed, reloading"
)
self.common.settings.load()
# We might've stopped the main requests timer if a Tor connection failed.
# If we've reloaded settings, we probably succeeded in obtaining a new
# connection. If so, restart the timer.
if not self.local_only:
if self.onion.is_authenticated():
if not self.timer.isActive():
self.timer.start(500)
self.share_mode.on_reload_settings()
self.receive_mode.on_reload_settings()
self.website_mode.on_reload_settings()
self.status_bar.clearMessage()
# If we switched off the auto-stop timer setting, ensure the widget is hidden.
if not self.common.settings.get("autostop_timer"):
self.share_mode.server_status.autostop_timer_container.hide()
self.receive_mode.server_status.autostop_timer_container.hide()
self.website_mode.server_status.autostop_timer_container.hide()
# If we switched off the auto-start timer setting, ensure the widget is hidden.
if not self.common.settings.get("autostart_timer"):
self.share_mode.server_status.autostart_timer_datetime = None
self.receive_mode.server_status.autostart_timer_datetime = None
self.website_mode.server_status.autostart_timer_datetime = None
self.share_mode.server_status.autostart_timer_container.hide()
self.receive_mode.server_status.autostart_timer_container.hide()
self.website_mode.server_status.autostart_timer_container.hide()
d = SettingsDialog(
self.common, self.onion, self.qtapp, self.config, self.local_only
)
d.settings_saved.connect(reload_settings)
d.exec_()
# When settings close, refresh the server status UI
self.share_mode.server_status.update()
self.receive_mode.server_status.update()
self.website_mode.server_status.update()
def check_for_updates(self):
"""
Check for updates in a new thread, if enabled.
"""
if self.common.platform == "Windows" or self.common.platform == "Darwin":
if self.common.settings.get("use_autoupdate"):
def update_available(update_url, installed_version, latest_version):
Alert(
self.common,
strings._("update_available").format(
update_url, installed_version, latest_version
),
)
self.update_thread = UpdateThread(self.common, self.onion, self.config)
self.update_thread.update_available.connect(update_available)
self.update_thread.start()
def timer_callback(self):
"""
Check for messages communicated from the web app, and update the GUI accordingly. Also,
call ShareMode and ReceiveMode's timer_callbacks.
"""
self.update()
if not self.local_only:
# Have we lost connection to Tor somehow?
if not self.onion.is_authenticated():
self.timer.stop()
self.status_bar.showMessage(strings._("gui_tor_connection_lost"))
self.system_tray.showMessage(
strings._("gui_tor_connection_lost"),
strings._("gui_tor_connection_error_settings"),
)
self.share_mode.handle_tor_broke()
self.receive_mode.handle_tor_broke()
self.website_mode.handle_tor_broke()
# Process events from the web object
if self.mode == self.MODE_SHARE:
mode = self.share_mode
elif self.mode == self.MODE_WEBSITE:
mode = self.website_mode
else:
mode = self.receive_mode
events = []
done = False
while not done:
try:
r = mode.web.q.get(False)
events.append(r)
except queue.Empty:
done = True
for event in events:
if event["type"] == Web.REQUEST_LOAD:
mode.handle_request_load(event)
elif event["type"] == Web.REQUEST_STARTED:
mode.handle_request_started(event)
elif event["type"] == Web.REQUEST_RATE_LIMIT:
mode.handle_request_rate_limit(event)
elif event["type"] == Web.REQUEST_PROGRESS:
mode.handle_request_progress(event)
elif event["type"] == Web.REQUEST_CANCELED:
mode.handle_request_canceled(event)
elif event["type"] == Web.REQUEST_UPLOAD_FILE_RENAMED:
mode.handle_request_upload_file_renamed(event)
elif event["type"] == Web.REQUEST_UPLOAD_SET_DIR:
mode.handle_request_upload_set_dir(event)
elif event["type"] == Web.REQUEST_UPLOAD_FINISHED:
mode.handle_request_upload_finished(event)
elif event["type"] == Web.REQUEST_UPLOAD_CANCELED:
mode.handle_request_upload_canceled(event)
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_STARTED:
mode.handle_request_individual_file_started(event)
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_PROGRESS:
mode.handle_request_individual_file_progress(event)
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_CANCELED:
mode.handle_request_individual_file_canceled(event)
if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE:
Alert(
self.common,
strings._("error_cannot_create_data_dir").format(
event["data"]["receive_mode_dir"]
),
)
if event["type"] == Web.REQUEST_OTHER:
if (
event["path"] != "/favicon.ico"
and event["path"] != f"/{mode.web.shutdown_password}/shutdown"
):
self.status_bar.showMessage(
f"{strings._('other_page_loaded')}: {event['path']}"
)
if event["type"] == Web.REQUEST_INVALID_PASSWORD:
self.status_bar.showMessage(
f"[#{mode.web.invalid_passwords_count}] {strings._('incorrect_password')}: {event['data']}"
)
mode.timer_callback()
def copy_url(self):
"""
When the URL gets copied to the clipboard, display this in the status bar.
"""
self.common.log("OnionShareGui", "copy_url")
self.system_tray.showMessage(
strings._("gui_copied_url_title"), strings._("gui_copied_url")
)
def copy_hidservauth(self):
"""
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
"""
self.common.log("OnionShareGui", "copy_hidservauth")
self.system_tray.showMessage(
strings._("gui_copied_hidservauth_title"),
strings._("gui_copied_hidservauth"),
)
def clear_message(self):
"""
Clear messages from the status bar.
"""
self.status_bar.clearMessage()
def set_server_active(self, active):
"""
Disable the Settings and Receive Files buttons while an Share Files server is active.
"""
if active:
self.settings_button.hide()
if self.mode == self.MODE_SHARE:
self.share_mode_button.show()
self.receive_mode_button.hide()
self.website_mode_button.hide()
elif self.mode == self.MODE_WEBSITE:
self.share_mode_button.hide()
self.receive_mode_button.hide()
self.website_mode_button.show()
else:
self.share_mode_button.hide()
self.receive_mode_button.show()
self.website_mode_button.hide()
else:
self.settings_button.show()
self.share_mode_button.show()
self.receive_mode_button.show()
self.website_mode_button.show()
# Disable settings menu action when server is active
self.settings_action.setEnabled(not active)
def closeEvent(self, e):
self.common.log("OnionShareGui", "closeEvent")
self.system_tray.hide()
try:
if self.mode == OnionShareGui.MODE_SHARE:
server_status = self.share_mode.server_status
if self.mode == OnionShareGui.MODE_WEBSITE:
server_status = self.website_mode.server_status
else:
server_status = self.receive_mode.server_status
if server_status.status != server_status.STATUS_STOPPED:
self.common.log("OnionShareGui", "closeEvent, opening warning dialog")
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle(strings._("gui_quit_title"))
if self.mode == OnionShareGui.MODE_SHARE:
dialog.setText(strings._("gui_share_quit_warning"))
else:
dialog.setText(strings._("gui_receive_quit_warning"))
dialog.setIcon(QtWidgets.QMessageBox.Critical)
quit_button = dialog.addButton(
strings._("gui_quit_warning_quit"), QtWidgets.QMessageBox.YesRole
)
dont_quit_button = dialog.addButton(
strings._("gui_quit_warning_dont_quit"),
QtWidgets.QMessageBox.NoRole,
)
dialog.setDefaultButton(dont_quit_button)
reply = dialog.exec_()
# Quit
if reply == 0:
self.stop_server()
e.accept()
# Don't Quit
else:
e.ignore()
except:
e.accept()

View File

@ -40,18 +40,13 @@ class SettingsDialog(QtWidgets.QDialog):
settings_saved = QtCore.pyqtSignal()
def __init__(self, common, onion, qtapp, config=False, local_only=False):
def __init__(self, common):
super(SettingsDialog, self).__init__()
self.common = common
self.common.log("SettingsDialog", "__init__")
self.onion = onion
self.qtapp = qtapp
self.config = config
self.local_only = local_only
self.setModal(True)
self.setWindowTitle(strings._("gui_settings_window_title"))
self.setWindowIcon(
@ -63,273 +58,6 @@ class SettingsDialog(QtWidgets.QDialog):
# If ONIONSHARE_HIDE_TOR_SETTINGS=1, hide Tor settings in the dialog
self.hide_tor_settings = os.environ.get("ONIONSHARE_HIDE_TOR_SETTINGS") == "1"
# General settings
# Use a password or not ('public mode')
self.public_mode_checkbox = QtWidgets.QCheckBox()
self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.public_mode_checkbox.setText(
strings._("gui_settings_public_mode_checkbox")
)
public_mode_label = QtWidgets.QLabel(
strings._("gui_settings_whats_this").format(
"https://github.com/micahflee/onionshare/wiki/Public-Mode"
)
)
public_mode_label.setStyleSheet(self.common.css["settings_whats_this"])
public_mode_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
public_mode_label.setOpenExternalLinks(True)
public_mode_label.setMinimumSize(public_mode_label.sizeHint())
public_mode_layout = QtWidgets.QHBoxLayout()
public_mode_layout.addWidget(self.public_mode_checkbox)
public_mode_layout.addWidget(public_mode_label)
public_mode_layout.addStretch()
public_mode_layout.setContentsMargins(0, 0, 0, 0)
self.public_mode_widget = QtWidgets.QWidget()
self.public_mode_widget.setLayout(public_mode_layout)
# Whether or not to use an auto-start timer
self.autostart_timer_checkbox = QtWidgets.QCheckBox()
self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked)
self.autostart_timer_checkbox.setText(
strings._("gui_settings_autostart_timer_checkbox")
)
autostart_timer_label = QtWidgets.QLabel(
strings._("gui_settings_whats_this").format(
"https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Start-Timer"
)
)
autostart_timer_label.setStyleSheet(self.common.css["settings_whats_this"])
autostart_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
autostart_timer_label.setOpenExternalLinks(True)
autostart_timer_label.setMinimumSize(public_mode_label.sizeHint())
autostart_timer_layout = QtWidgets.QHBoxLayout()
autostart_timer_layout.addWidget(self.autostart_timer_checkbox)
autostart_timer_layout.addWidget(autostart_timer_label)
autostart_timer_layout.addStretch()
autostart_timer_layout.setContentsMargins(0, 0, 0, 0)
self.autostart_timer_widget = QtWidgets.QWidget()
self.autostart_timer_widget.setLayout(autostart_timer_layout)
# Whether or not to use an auto-stop timer
self.autostop_timer_checkbox = QtWidgets.QCheckBox()
self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked)
self.autostop_timer_checkbox.setText(
strings._("gui_settings_autostop_timer_checkbox")
)
autostop_timer_label = QtWidgets.QLabel(
strings._("gui_settings_whats_this").format(
"https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer"
)
)
autostop_timer_label.setStyleSheet(self.common.css["settings_whats_this"])
autostop_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
autostop_timer_label.setOpenExternalLinks(True)
autostop_timer_label.setMinimumSize(public_mode_label.sizeHint())
autostop_timer_layout = QtWidgets.QHBoxLayout()
autostop_timer_layout.addWidget(self.autostop_timer_checkbox)
autostop_timer_layout.addWidget(autostop_timer_label)
autostop_timer_layout.addStretch()
autostop_timer_layout.setContentsMargins(0, 0, 0, 0)
self.autostop_timer_widget = QtWidgets.QWidget()
self.autostop_timer_widget.setLayout(autostop_timer_layout)
# General settings layout
general_group_layout = QtWidgets.QVBoxLayout()
general_group_layout.addWidget(self.public_mode_widget)
general_group_layout.addWidget(self.autostart_timer_widget)
general_group_layout.addWidget(self.autostop_timer_widget)
general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label"))
general_group.setLayout(general_group_layout)
# Onion settings
# Label telling user to connect to Tor for onion service settings
self.connect_to_tor_label = QtWidgets.QLabel(
strings._("gui_connect_to_tor_for_onion_settings")
)
self.connect_to_tor_label.setStyleSheet(
self.common.css["settings_connect_to_tor"]
)
# Whether or not to save the Onion private key for reuse (persistent URL mode)
self.save_private_key_checkbox = QtWidgets.QCheckBox()
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.save_private_key_checkbox.setText(
strings._("gui_save_private_key_checkbox")
)
save_private_key_label = QtWidgets.QLabel(
strings._("gui_settings_whats_this").format(
"https://github.com/micahflee/onionshare/wiki/Using-a-Persistent-URL"
)
)
save_private_key_label.setStyleSheet(self.common.css["settings_whats_this"])
save_private_key_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
save_private_key_label.setOpenExternalLinks(True)
save_private_key_layout = QtWidgets.QHBoxLayout()
save_private_key_layout.addWidget(self.save_private_key_checkbox)
save_private_key_layout.addWidget(save_private_key_label)
save_private_key_layout.addStretch()
save_private_key_layout.setContentsMargins(0, 0, 0, 0)
self.save_private_key_widget = QtWidgets.QWidget()
self.save_private_key_widget.setLayout(save_private_key_layout)
# Whether or not to use legacy v2 onions
self.use_legacy_v2_onions_checkbox = QtWidgets.QCheckBox()
self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.use_legacy_v2_onions_checkbox.setText(
strings._("gui_use_legacy_v2_onions_checkbox")
)
self.use_legacy_v2_onions_checkbox.clicked.connect(
self.use_legacy_v2_onions_checkbox_clicked
)
use_legacy_v2_onions_label = QtWidgets.QLabel(
strings._("gui_settings_whats_this").format(
"https://github.com/micahflee/onionshare/wiki/Legacy-Addresses"
)
)
use_legacy_v2_onions_label.setStyleSheet(self.common.css["settings_whats_this"])
use_legacy_v2_onions_label.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
use_legacy_v2_onions_label.setOpenExternalLinks(True)
use_legacy_v2_onions_layout = QtWidgets.QHBoxLayout()
use_legacy_v2_onions_layout.addWidget(self.use_legacy_v2_onions_checkbox)
use_legacy_v2_onions_layout.addWidget(use_legacy_v2_onions_label)
use_legacy_v2_onions_layout.addStretch()
use_legacy_v2_onions_layout.setContentsMargins(0, 0, 0, 0)
self.use_legacy_v2_onions_widget = QtWidgets.QWidget()
self.use_legacy_v2_onions_widget.setLayout(use_legacy_v2_onions_layout)
# Stealth
self.stealth_checkbox = QtWidgets.QCheckBox()
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.stealth_checkbox.setText(strings._("gui_settings_stealth_option"))
self.stealth_checkbox.clicked.connect(self.stealth_checkbox_clicked_connect)
use_stealth_label = QtWidgets.QLabel(
strings._("gui_settings_whats_this").format(
"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services"
)
)
use_stealth_label.setStyleSheet(self.common.css["settings_whats_this"])
use_stealth_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
use_stealth_label.setOpenExternalLinks(True)
use_stealth_label.setMinimumSize(use_stealth_label.sizeHint())
use_stealth_layout = QtWidgets.QHBoxLayout()
use_stealth_layout.addWidget(self.stealth_checkbox)
use_stealth_layout.addWidget(use_stealth_label)
use_stealth_layout.addStretch()
use_stealth_layout.setContentsMargins(0, 0, 0, 0)
self.use_stealth_widget = QtWidgets.QWidget()
self.use_stealth_widget.setLayout(use_stealth_layout)
self.hidservauth_details = QtWidgets.QLabel(
strings._("gui_settings_stealth_hidservauth_string")
)
self.hidservauth_details.setWordWrap(True)
self.hidservauth_details.setMinimumSize(self.hidservauth_details.sizeHint())
self.hidservauth_details.hide()
self.hidservauth_copy_button = QtWidgets.QPushButton(
strings._("gui_copy_hidservauth")
)
self.hidservauth_copy_button.clicked.connect(
self.hidservauth_copy_button_clicked
)
self.hidservauth_copy_button.hide()
# Onion settings widget
onion_settings_layout = QtWidgets.QVBoxLayout()
onion_settings_layout.setContentsMargins(0, 0, 0, 0)
onion_settings_layout.addWidget(self.save_private_key_widget)
onion_settings_layout.addWidget(self.use_legacy_v2_onions_widget)
onion_settings_layout.addWidget(self.use_stealth_widget)
onion_settings_layout.addWidget(self.hidservauth_details)
onion_settings_layout.addWidget(self.hidservauth_copy_button)
self.onion_settings_widget = QtWidgets.QWidget()
self.onion_settings_widget.setLayout(onion_settings_layout)
# Onion settings layout
onion_group_layout = QtWidgets.QVBoxLayout()
onion_group_layout.addWidget(self.connect_to_tor_label)
onion_group_layout.addWidget(self.onion_settings_widget)
onion_group = QtWidgets.QGroupBox(strings._("gui_settings_onion_label"))
onion_group.setLayout(onion_group_layout)
# Sharing options
# Close after first download
self.close_after_first_download_checkbox = QtWidgets.QCheckBox()
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked)
self.close_after_first_download_checkbox.setText(
strings._("gui_settings_close_after_first_download_option")
)
individual_downloads_label = QtWidgets.QLabel(
strings._("gui_settings_individual_downloads_label")
)
# Sharing options layout
sharing_group_layout = QtWidgets.QVBoxLayout()
sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
sharing_group_layout.addWidget(individual_downloads_label)
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label"))
sharing_group.setLayout(sharing_group_layout)
# OnionShare data dir
data_dir_label = QtWidgets.QLabel(strings._("gui_settings_data_dir_label"))
self.data_dir_lineedit = QtWidgets.QLineEdit()
self.data_dir_lineedit.setReadOnly(True)
data_dir_button = QtWidgets.QPushButton(
strings._("gui_settings_data_dir_browse_button")
)
data_dir_button.clicked.connect(self.data_dir_button_clicked)
data_dir_layout = QtWidgets.QHBoxLayout()
data_dir_layout.addWidget(data_dir_label)
data_dir_layout.addWidget(self.data_dir_lineedit)
data_dir_layout.addWidget(data_dir_button)
# Receiving options layout
receiving_group_layout = QtWidgets.QVBoxLayout()
receiving_group_layout.addLayout(data_dir_layout)
receiving_group = QtWidgets.QGroupBox(strings._("gui_settings_receiving_label"))
receiving_group.setLayout(receiving_group_layout)
# Option to disable Content Security Policy (for website sharing)
self.csp_header_disabled_checkbox = QtWidgets.QCheckBox()
self.csp_header_disabled_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.csp_header_disabled_checkbox.setText(
strings._("gui_settings_csp_header_disabled_option")
)
csp_header_label = QtWidgets.QLabel(
strings._("gui_settings_whats_this").format(
"https://github.com/micahflee/onionshare/wiki/Content-Security-Policy"
)
)
csp_header_label.setStyleSheet(self.common.css["settings_whats_this"])
csp_header_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
csp_header_label.setOpenExternalLinks(True)
csp_header_label.setMinimumSize(csp_header_label.sizeHint())
csp_header_layout = QtWidgets.QHBoxLayout()
csp_header_layout.addWidget(self.csp_header_disabled_checkbox)
csp_header_layout.addWidget(csp_header_label)
csp_header_layout.addStretch()
csp_header_layout.setContentsMargins(0, 0, 0, 0)
self.csp_header_widget = QtWidgets.QWidget()
self.csp_header_widget.setLayout(csp_header_layout)
# Website settings widget
website_settings_layout = QtWidgets.QVBoxLayout()
website_settings_layout.setContentsMargins(0, 0, 0, 0)
website_settings_layout.addWidget(self.csp_header_widget)
self.website_settings_widget = QtWidgets.QWidget()
self.website_settings_widget.setLayout(website_settings_layout)
# Website mode options layout
website_group_layout = QtWidgets.QVBoxLayout()
website_group_layout.addWidget(self.website_settings_widget)
website_group = QtWidgets.QGroupBox(strings._("gui_settings_website_label"))
website_group.setLayout(website_group_layout)
# Automatic updates options
# Autoupdate
@ -346,7 +74,7 @@ class SettingsDialog(QtWidgets.QDialog):
)
self.check_for_updates_button.clicked.connect(self.check_for_updates)
# We can't check for updates if not connected to Tor
if not self.onion.connected_to_tor:
if not self.common.gui.onion.connected_to_tor:
self.check_for_updates_button.setEnabled(False)
# Autoupdate options layout
@ -673,7 +401,7 @@ class SettingsDialog(QtWidgets.QDialog):
)
self.cancel_button.clicked.connect(self.cancel_clicked)
version_label = QtWidgets.QLabel(f"OnionShare {self.common.version}")
version_label.setStyleSheet(self.common.css["settings_version"])
version_label.setStyleSheet(self.common.gui.css["settings_version"])
self.help_button = QtWidgets.QPushButton(strings._("gui_settings_button_help"))
self.help_button.clicked.connect(self.help_clicked)
buttons_layout = QtWidgets.QHBoxLayout()
@ -685,33 +413,26 @@ class SettingsDialog(QtWidgets.QDialog):
# Tor network connection status
self.tor_status = QtWidgets.QLabel()
self.tor_status.setStyleSheet(self.common.css["settings_tor_status"])
self.tor_status.setStyleSheet(self.common.gui.css["settings_tor_status"])
self.tor_status.hide()
# Layout
left_col_layout = QtWidgets.QVBoxLayout()
left_col_layout.addWidget(general_group)
left_col_layout.addWidget(onion_group)
left_col_layout.addWidget(sharing_group)
left_col_layout.addWidget(receiving_group)
left_col_layout.addWidget(website_group)
left_col_layout.addWidget(autoupdate_group)
left_col_layout.addLayout(language_layout)
left_col_layout.addStretch()
right_col_layout = QtWidgets.QVBoxLayout()
right_col_layout.addWidget(connection_type_radio_group)
right_col_layout.addLayout(connection_type_layout)
right_col_layout.addWidget(self.tor_status)
right_col_layout.addStretch()
col_layout = QtWidgets.QHBoxLayout()
col_layout.addLayout(left_col_layout)
if not self.hide_tor_settings:
col_layout.addLayout(right_col_layout)
tor_layout = QtWidgets.QVBoxLayout()
tor_layout.addWidget(connection_type_radio_group)
tor_layout.addLayout(connection_type_layout)
tor_layout.addWidget(self.tor_status)
tor_layout.addStretch()
layout = QtWidgets.QVBoxLayout()
layout.addLayout(col_layout)
if not self.hide_tor_settings:
layout.addLayout(tor_layout)
layout.addSpacing(20)
layout.addWidget(autoupdate_group)
if autoupdate_group.isVisible():
layout.addSpacing(20)
layout.addLayout(language_layout)
layout.addSpacing(20)
layout.addStretch()
layout.addLayout(buttons_layout)
self.setLayout(layout)
@ -721,67 +442,9 @@ class SettingsDialog(QtWidgets.QDialog):
def reload_settings(self):
# Load settings, and fill them in
self.old_settings = Settings(self.common, self.config)
self.old_settings = Settings(self.common)
self.old_settings.load()
close_after_first_download = self.old_settings.get("close_after_first_download")
if close_after_first_download:
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Unchecked)
csp_header_disabled = self.old_settings.get("csp_header_disabled")
if csp_header_disabled:
self.csp_header_disabled_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.csp_header_disabled_checkbox.setCheckState(QtCore.Qt.Unchecked)
autostart_timer = self.old_settings.get("autostart_timer")
if autostart_timer:
self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked)
autostop_timer = self.old_settings.get("autostop_timer")
if autostop_timer:
self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked)
save_private_key = self.old_settings.get("save_private_key")
if save_private_key:
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
use_legacy_v2_onions = self.old_settings.get("use_legacy_v2_onions")
if use_legacy_v2_onions:
self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked)
self.use_stealth_widget.show()
else:
self.use_stealth_widget.hide()
data_dir = self.old_settings.get("data_dir")
self.data_dir_lineedit.setText(data_dir)
public_mode = self.old_settings.get("public_mode")
if public_mode:
self.public_mode_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked)
use_stealth = self.old_settings.get("use_stealth")
if use_stealth:
self.stealth_checkbox.setCheckState(QtCore.Qt.Checked)
# Legacy v2 mode is forced on if Stealth is enabled
self.use_legacy_v2_onions_checkbox.setEnabled(False)
if save_private_key and self.old_settings.get("hidservauth_string") != "":
self.hidservauth_details.show()
self.hidservauth_copy_button.show()
else:
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
use_autoupdate = self.old_settings.get("use_autoupdate")
if use_autoupdate:
self.autoupdate_checkbox.setCheckState(QtCore.Qt.Checked)
@ -860,25 +523,6 @@ class SettingsDialog(QtWidgets.QDialog):
new_bridges = "".join(new_bridges)
self.tor_bridges_use_custom_textbox.setPlainText(new_bridges)
# If we're connected to Tor, show onion service settings, show label if not
if self.onion.is_authenticated():
self.connect_to_tor_label.hide()
self.onion_settings_widget.show()
# If v3 onion services are supported, allow using legacy mode
if self.onion.supports_v3_onions:
self.common.log("SettingsDialog", "__init__", "v3 onions are supported")
self.use_legacy_v2_onions_checkbox.show()
else:
self.common.log(
"SettingsDialog", "__init__", "v3 onions are not supported"
)
self.use_legacy_v2_onions_widget.hide()
self.use_legacy_v2_onions_checkbox_clicked(True)
else:
self.connect_to_tor_label.show()
self.onion_settings_widget.hide()
def connection_type_bundled_toggled(self, checked):
"""
Connection type bundled was toggled. If checked, hide authentication fields.
@ -995,55 +639,6 @@ class SettingsDialog(QtWidgets.QDialog):
else:
self.authenticate_password_extras.hide()
def hidservauth_copy_button_clicked(self):
"""
Toggle the 'Copy HidServAuth' button
to copy the saved HidServAuth to clipboard.
"""
self.common.log(
"SettingsDialog",
"hidservauth_copy_button_clicked",
"HidServAuth was copied to clipboard",
)
clipboard = self.qtapp.clipboard()
clipboard.setText(self.old_settings.get("hidservauth_string"))
def use_legacy_v2_onions_checkbox_clicked(self, checked):
"""
Show the legacy settings if the legacy mode is enabled.
"""
if checked:
self.use_stealth_widget.show()
else:
self.use_stealth_widget.hide()
def stealth_checkbox_clicked_connect(self, checked):
"""
Prevent the v2 legacy mode being switched off if stealth is enabled
"""
if checked:
self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked)
self.use_legacy_v2_onions_checkbox.setEnabled(False)
else:
self.use_legacy_v2_onions_checkbox.setEnabled(True)
def data_dir_button_clicked(self):
"""
Browse for a new OnionShare data directory
"""
data_dir = self.data_dir_lineedit.text()
selected_dir = QtWidgets.QFileDialog.getExistingDirectory(
self, strings._("gui_settings_data_dir_label"), data_dir
)
if selected_dir:
self.common.log(
"SettingsDialog",
"data_dir_button_clicked",
f"selected dir: {selected_dir}",
)
self.data_dir_lineedit.setText(selected_dir)
def test_tor_clicked(self):
"""
Test Tor Settings button clicked. With the given settings, see if we can
@ -1065,10 +660,9 @@ class SettingsDialog(QtWidgets.QDialog):
else:
tor_status_update_func = None
onion = Onion(self.common)
onion = Onion(self.common, use_tmp_dir=True)
onion.connect(
custom_settings=settings,
config=self.config,
tor_status_update_func=tor_status_update_func,
)
@ -1110,11 +704,11 @@ class SettingsDialog(QtWidgets.QDialog):
self.common.log("SettingsDialog", "check_for_updates")
# Disable buttons
self._disable_buttons()
self.qtapp.processEvents()
self.common.gui.qtapp.processEvents()
def update_timestamp():
# Update the last checked label
settings = Settings(self.common, self.config)
settings = Settings(self.common)
settings.load()
autoupdate_timestamp = settings.get("autoupdate_timestamp")
self._update_autoupdate_timestamp(autoupdate_timestamp)
@ -1157,7 +751,7 @@ class SettingsDialog(QtWidgets.QDialog):
close_forced_update_thread()
forced_update_thread = UpdateThread(
self.common, self.onion, self.config, force=True
self.common, self.onion, force=True
)
forced_update_thread.update_available.connect(update_available)
forced_update_thread.update_not_available.connect(update_not_available)
@ -1205,8 +799,8 @@ class SettingsDialog(QtWidgets.QDialog):
# If Tor isn't connected, or if Tor settings have changed, Reinitialize
# the Onion object
reboot_onion = False
if not self.local_only:
if self.onion.is_authenticated():
if not self.common.gui.local_only:
if self.common.gui.onion.is_authenticated():
self.common.log(
"SettingsDialog", "save_clicked", "Connected to Tor"
)
@ -1245,20 +839,18 @@ class SettingsDialog(QtWidgets.QDialog):
self.common.log(
"SettingsDialog", "save_clicked", "rebooting the Onion"
)
self.onion.cleanup()
self.common.gui.onion.cleanup()
tor_con = TorConnectionDialog(
self.common, self.qtapp, self.onion, settings
)
tor_con = TorConnectionDialog(self.common, settings)
tor_con.start()
self.common.log(
"SettingsDialog",
"save_clicked",
f"Onion done rebooting, connected to Tor: {self.onion.connected_to_tor}",
f"Onion done rebooting, connected to Tor: {self.common.gui.onion.connected_to_tor}",
)
if self.onion.is_authenticated() and not tor_con.wasCanceled():
if self.common.gui.onion.is_authenticated() and not tor_con.wasCanceled():
self.settings_saved.emit()
self.close()
@ -1274,7 +866,7 @@ class SettingsDialog(QtWidgets.QDialog):
Cancel button clicked.
"""
self.common.log("SettingsDialog", "cancel_clicked")
if not self.local_only and not self.onion.is_authenticated():
if not self.common.gui.local_only and not self.common.gui.onion.is_authenticated():
Alert(
self.common,
strings._("gui_tor_connection_canceled"),
@ -1301,51 +893,9 @@ class SettingsDialog(QtWidgets.QDialog):
Return a Settings object that's full of values from the settings dialog.
"""
self.common.log("SettingsDialog", "settings_from_fields")
settings = Settings(self.common, self.config)
settings = Settings(self.common)
settings.load() # To get the last update timestamp
settings.set(
"close_after_first_download",
self.close_after_first_download_checkbox.isChecked(),
)
settings.set(
"csp_header_disabled", self.csp_header_disabled_checkbox.isChecked()
)
settings.set("autostart_timer", self.autostart_timer_checkbox.isChecked())
settings.set("autostop_timer", self.autostop_timer_checkbox.isChecked())
# Complicated logic here to force v2 onion mode on or off depending on other settings
if self.use_legacy_v2_onions_checkbox.isChecked():
use_legacy_v2_onions = True
else:
use_legacy_v2_onions = False
if self.save_private_key_checkbox.isChecked():
settings.set("save_private_key", True)
settings.set("private_key", self.old_settings.get("private_key"))
settings.set("password", self.old_settings.get("password"))
settings.set(
"hidservauth_string", self.old_settings.get("hidservauth_string")
)
else:
settings.set("save_private_key", False)
settings.set("private_key", "")
settings.set("password", "")
# Also unset the HidServAuth if we are removing our reusable private key
settings.set("hidservauth_string", "")
if use_legacy_v2_onions:
settings.set("use_legacy_v2_onions", True)
else:
settings.set("use_legacy_v2_onions", False)
settings.set("data_dir", self.data_dir_lineedit.text())
settings.set("public_mode", self.public_mode_checkbox.isChecked())
settings.set("use_stealth", self.stealth_checkbox.isChecked())
# Always unset the HidServAuth if Stealth mode is unset
if not self.stealth_checkbox.isChecked():
settings.set("hidservauth_string", "")
# Language
locale_index = self.language_combobox.currentIndex()
locale = self.language_combobox.itemData(locale_index)
@ -1448,14 +998,14 @@ class SettingsDialog(QtWidgets.QDialog):
self.common.log("SettingsDialog", "closeEvent")
# On close, if Tor isn't connected, then quit OnionShare altogether
if not self.local_only:
if not self.onion.is_authenticated():
if not self.common.gui.local_only:
if not self.common.gui.onion.is_authenticated():
self.common.log(
"SettingsDialog", "closeEvent", "Closing while not connected to Tor"
)
# Wait 1ms for the event loop to finish, then quit
QtCore.QTimer.singleShot(1, self.qtapp.quit)
QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit)
def _update_autoupdate_timestamp(self, autoupdate_timestamp):
self.common.log("SettingsDialog", "_update_autoupdate_timestamp")
@ -1473,7 +1023,7 @@ class SettingsDialog(QtWidgets.QDialog):
self.tor_status.setText(
f"<strong>{strings._('connecting_to_tor')}</strong><br>{progress}% {summary}"
)
self.qtapp.processEvents()
self.common.gui.qtapp.processEvents()
if "Done" in summary:
self.tor_status.hide()
self._enable_buttons()
@ -1489,7 +1039,7 @@ class SettingsDialog(QtWidgets.QDialog):
def _enable_buttons(self):
self.common.log("SettingsDialog", "_enable_buttons")
# We can't check for updates if we're still not connected to Tor
if not self.onion.connected_to_tor:
if not self.common.gui.onion.connected_to_tor:
self.check_for_updates_button.setEnabled(False)
else:
self.check_for_updates_button.setEnabled(True)

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
@ -16,23 +17,4 @@ 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 tempfile
import os
class MockSubprocess:
def __init__(self):
self.last_call = None
def call(self, args):
self.last_call = args
def last_call_args(self):
return self.last_call
def write_tempfile(text):
path = os.path.join(tempfile.mkdtemp(), "/test-file.txt")
with open(path, "w") as f:
f.write(text)
return path
from .tab import Tab

View File

@ -23,11 +23,11 @@ from onionshare import strings
from onionshare.common import AutoStopTimer
from .history import IndividualFileHistoryItem
from .mode_settings_widget import ModeSettingsWidget
from ..server_status import ServerStatus
from ..threads import OnionThread
from ..threads import AutoStartTimer
from ..widgets import Alert
from ...threads import OnionThread, AutoStartTimer
from ...widgets import Alert
class Mode(QtWidgets.QWidget):
@ -42,43 +42,46 @@ class Mode(QtWidgets.QWidget):
starting_server_error = QtCore.pyqtSignal(str)
starting_server_early = QtCore.pyqtSignal()
set_server_active = QtCore.pyqtSignal(bool)
change_persistent = QtCore.pyqtSignal(int, bool)
def __init__(
self,
common,
qtapp,
app,
status_bar,
server_status_label,
system_tray,
filenames=None,
local_only=False,
):
def __init__(self, tab):
super(Mode, self).__init__()
self.common = common
self.qtapp = qtapp
self.app = app
self.tab = tab
self.settings = tab.settings
self.status_bar = status_bar
self.server_status_label = server_status_label
self.system_tray = system_tray
self.common = tab.common
self.qtapp = self.common.gui.qtapp
self.app = tab.app
self.filenames = filenames
self.status_bar = tab.status_bar
self.server_status_label = tab.status_bar.server_status_label
self.system_tray = tab.system_tray
self.filenames = tab.filenames
# The web object gets created in init()
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
self.startup_thread = None
# Mode settings widget
self.mode_settings_widget = ModeSettingsWidget(
self.common, self.tab, self.settings
)
self.mode_settings_widget.change_persistent.connect(self.change_persistent)
# Server status
self.server_status = ServerStatus(
self.common, self.qtapp, self.app, None, self.local_only
self.common,
self.qtapp,
self.app,
self.settings,
self.mode_settings_widget,
None,
self.common.gui.local_only,
)
self.server_status.server_started.connect(self.start_server)
self.server_status.server_stopped.connect(self.stop_server)
@ -90,18 +93,20 @@ class Mode(QtWidgets.QWidget):
self.starting_server_early.connect(self.start_server_early)
self.starting_server_error.connect(self.start_server_error)
# Header
# Note: It's up to the downstream Mode to add this to its layout
self.header_label = QtWidgets.QLabel()
self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"])
self.header_label.setAlignment(QtCore.Qt.AlignHCenter)
# 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.addWidget(self.mode_settings_widget)
self.primary_action_layout.addWidget(self.server_status)
self.primary_action = QtWidgets.QWidget()
self.primary_action.setLayout(self.primary_action_layout)
# Hack to allow a minimum width on the main layout
# Note: It's up to the downstream Mode to add this to its layout
self.min_width_widget = QtWidgets.QWidget()
self.min_width_widget.setMinimumWidth(600)
def init(self):
"""
Add custom initialization here.
@ -137,7 +142,7 @@ class Mode(QtWidgets.QWidget):
now = QtCore.QDateTime.currentDateTime()
if self.server_status.local_only:
seconds_remaining = now.secsTo(
self.server_status.autostart_timer_widget.dateTime()
self.mode_settings_widget.autostart_timer_widget.dateTime()
)
else:
seconds_remaining = now.secsTo(
@ -159,8 +164,8 @@ class Mode(QtWidgets.QWidget):
# If the auto-stop timer has stopped, stop the server
if self.server_status.status == ServerStatus.STATUS_STARTED:
if self.app.autostop_timer_thread and self.common.settings.get(
"autostop_timer"
if self.app.autostop_timer_thread and self.settings.get(
"general", "autostop_timer"
):
if self.autostop_timer_datetime_delta > 0:
now = QtCore.QDateTime.currentDateTime()
@ -207,14 +212,15 @@ class Mode(QtWidgets.QWidget):
self.common.log("Mode", "start_server")
self.start_server_custom()
self.set_server_active.emit(True)
self.app.set_stealth(self.common.settings.get("use_stealth"))
# Clear the status bar
self.status_bar.clearMessage()
self.server_status_label.setText("")
# Hide the mode settings
self.mode_settings_widget.hide()
# Ensure we always get a new random port each time we might launch an OnionThread
self.app.port = None
@ -297,7 +303,7 @@ class Mode(QtWidgets.QWidget):
self.start_server_step3_custom()
if self.common.settings.get("autostop_timer"):
if self.settings.get("general", "autostop_timer"):
# Convert the date value to seconds between now and then
now = QtCore.QDateTime.currentDateTime()
self.autostop_timer_datetime_delta = now.secsTo(
@ -385,6 +391,9 @@ class Mode(QtWidgets.QWidget):
self.set_server_active.emit(False)
self.stop_server_finished.emit()
# Show the mode settings
self.mode_settings_widget.show()
def stop_server_custom(self):
"""
Add custom initialization here.

View File

@ -22,7 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from ..widgets import Alert, AddFileDialog
from ...widgets import Alert, AddFileDialog
class DropHereLabel(QtWidgets.QLabel):
@ -50,7 +50,9 @@ class DropHereLabel(QtWidgets.QLabel):
)
else:
self.setText(strings._("gui_drag_and_drop"))
self.setStyleSheet(self.common.css["share_file_selection_drop_here_label"])
self.setStyleSheet(
self.common.gui.css["share_file_selection_drop_here_label"]
)
self.hide()
@ -75,7 +77,7 @@ class DropCountLabel(QtWidgets.QLabel):
self.setAcceptDrops(True)
self.setAlignment(QtCore.Qt.AlignCenter)
self.setText(strings._("gui_drag_and_drop"))
self.setStyleSheet(self.common.css["share_file_selection_drop_count_label"])
self.setStyleSheet(self.common.gui.css["share_file_selection_drop_count_label"])
self.hide()
def dragEnterEvent(self, event):
@ -169,7 +171,7 @@ class FileList(QtWidgets.QListWidget):
dragEnterEvent for dragging files and directories into the widget.
"""
if event.mimeData().hasUrls:
self.setStyleSheet(self.common.css["share_file_list_drag_enter"])
self.setStyleSheet(self.common.gui.css["share_file_list_drag_enter"])
count = len(event.mimeData().urls())
self.drop_count.setText(f"+{count}")
@ -189,7 +191,7 @@ class FileList(QtWidgets.QListWidget):
"""
dragLeaveEvent for dragging files and directories into the widget.
"""
self.setStyleSheet(self.common.css["share_file_list_drag_leave"])
self.setStyleSheet(self.common.gui.css["share_file_list_drag_leave"])
self.drop_count.hide()
event.accept()
self.update()
@ -217,7 +219,7 @@ class FileList(QtWidgets.QListWidget):
else:
event.ignore()
self.setStyleSheet(self.common.css["share_file_list_drag_leave"])
self.setStyleSheet(self.common.gui.css["share_file_list_drag_leave"])
self.drop_count.hide()
self.files_dropped.emit()
@ -254,7 +256,7 @@ class FileList(QtWidgets.QListWidget):
# Item's filename attribute and size labels
item.filename = filename
item_size = QtWidgets.QLabel(size_readable)
item_size.setStyleSheet(self.common.css["share_file_list_item_size"])
item_size.setStyleSheet(self.common.gui.css["share_file_list_item_size"])
item.basename = os.path.basename(filename.rstrip("/"))
# Use the basename as the method with which to sort the list
@ -381,6 +383,9 @@ class FileSelection(QtWidgets.QVBoxLayout):
# Update the file list
self.file_list.update()
# Save the latest file list to mode settings
self.save_filenames()
def add(self):
"""
Add button clicked.
@ -450,6 +455,25 @@ class FileSelection(QtWidgets.QVBoxLayout):
"""
return len(range(self.file_list.count()))
def get_filenames(self):
"""
Return the list of file and folder names
"""
filenames = []
for index in range(self.file_list.count()):
filenames.append(self.file_list.item(index).filename)
return filenames
def save_filenames(self):
"""
Save the filenames to mode settings
"""
filenames = self.get_filenames()
if self.parent.tab.mode == self.common.gui.MODE_SHARE:
self.parent.settings.set("share", "filenames", filenames)
elif self.parent.tab.mode == self.common.gui.MODE_WEBSITE:
self.parent.settings.set("website", "filenames", filenames)
def setFocus(self):
"""
Set the Qt app focus on the file selection box.

View File

@ -24,7 +24,7 @@ from datetime import datetime
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from ..widgets import Alert
from ...widgets import Alert
class HistoryItem(QtWidgets.QWidget):
@ -122,7 +122,7 @@ class ShareHistoryItem(HistoryItem):
self.progress_bar.setMaximum(total_bytes)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(
self.common.css["downloads_uploads_progress_bar"]
self.common.gui.css["downloads_uploads_progress_bar"]
)
self.progress_bar.total_bytes = total_bytes
@ -193,7 +193,7 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
# File size label
self.filesize_label = QtWidgets.QLabel()
self.filesize_label.setStyleSheet(self.common.css["receive_file_size"])
self.filesize_label.setStyleSheet(self.common.gui.css["receive_file_size"])
self.filesize_label.hide()
# Folder button
@ -290,14 +290,14 @@ class ReceiveHistoryItem(HistoryItem):
self.progress_bar.setMinimum(0)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(
self.common.css["downloads_uploads_progress_bar"]
self.common.gui.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.setStyleSheet(self.common.gui.css["receive_file"])
files_widget.setLayout(self.files_layout)
# Layout
@ -405,7 +405,7 @@ class IndividualFileHistoryItem(HistoryItem):
self.started_dt.strftime("%b %d, %I:%M%p")
)
self.timestamp_label.setStyleSheet(
self.common.css["history_individual_file_timestamp_label"]
self.common.gui.css["history_individual_file_timestamp_label"]
)
self.path_label = QtWidgets.QLabel(self.path)
self.status_code_label = QtWidgets.QLabel()
@ -417,7 +417,7 @@ class IndividualFileHistoryItem(HistoryItem):
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(
self.common.css["downloads_uploads_progress_bar"]
self.common.gui.css["downloads_uploads_progress_bar"]
)
# Text layout
@ -438,11 +438,11 @@ class IndividualFileHistoryItem(HistoryItem):
self.status_code_label.setText(str(data["status_code"]))
if data["status_code"] >= 200 and data["status_code"] < 300:
self.status_code_label.setStyleSheet(
self.common.css["history_individual_file_status_code_label_2xx"]
self.common.gui.css["history_individual_file_status_code_label_2xx"]
)
if data["status_code"] >= 400 and data["status_code"] < 500:
self.status_code_label.setStyleSheet(
self.common.css["history_individual_file_status_code_label_4xx"]
self.common.gui.css["history_individual_file_status_code_label_4xx"]
)
self.status = HistoryItem.STATUS_FINISHED
self.progress_bar.hide()
@ -464,7 +464,7 @@ class IndividualFileHistoryItem(HistoryItem):
if downloaded_bytes == self.progress_bar.total_bytes:
self.status_code_label.setText("200")
self.status_code_label.setStyleSheet(
self.common.css["history_individual_file_status_code_label_2xx"]
self.common.gui.css["history_individual_file_status_code_label_2xx"]
)
self.progress_bar.hide()
self.status = HistoryItem.STATUS_FINISHED
@ -586,19 +586,19 @@ class History(QtWidgets.QWidget):
# In progress, completed, and requests labels
self.in_progress_label = QtWidgets.QLabel()
self.in_progress_label.setStyleSheet(self.common.css["mode_info_label"])
self.in_progress_label.setStyleSheet(self.common.gui.css["mode_info_label"])
self.completed_label = QtWidgets.QLabel()
self.completed_label.setStyleSheet(self.common.css["mode_info_label"])
self.completed_label.setStyleSheet(self.common.gui.css["mode_info_label"])
self.requests_label = QtWidgets.QLabel()
self.requests_label.setStyleSheet(self.common.css["mode_info_label"])
self.requests_label.setStyleSheet(self.common.gui.css["mode_info_label"])
# Header
self.header_label = QtWidgets.QLabel(header_text)
self.header_label.setStyleSheet(self.common.css["downloads_uploads_label"])
self.header_label.setStyleSheet(self.common.gui.css["downloads_uploads_label"])
self.clear_button = QtWidgets.QPushButton(
strings._("gui_all_modes_clear_history")
)
self.clear_button.setStyleSheet(self.common.css["downloads_uploads_clear"])
self.clear_button.setStyleSheet(self.common.gui.css["downloads_uploads_clear"])
self.clear_button.setFlat(True)
self.clear_button.clicked.connect(self.reset)
header_layout = QtWidgets.QHBoxLayout()
@ -615,14 +615,16 @@ class History(QtWidgets.QWidget):
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"])
self.empty_text.setStyleSheet(
self.common.gui.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.setStyleSheet(self.common.gui.css["downloads_uploads_empty"])
self.empty.setLayout(empty_layout)
# When there are items
@ -759,7 +761,7 @@ class ToggleHistory(QtWidgets.QPushButton):
self.indicator_count = 0
self.indicator_label = QtWidgets.QLabel(parent=self)
self.indicator_label.setStyleSheet(
self.common.css["download_uploads_indicator"]
self.common.gui.css["download_uploads_indicator"]
)
self.update_indicator()

View File

@ -0,0 +1,289 @@
# -*- 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
from onionshare import strings
class ModeSettingsWidget(QtWidgets.QWidget):
"""
All of the common settings for each mode are in this widget
"""
change_persistent = QtCore.pyqtSignal(int, bool)
def __init__(self, common, tab, mode_settings):
super(ModeSettingsWidget, self).__init__()
self.common = common
self.tab = tab
self.settings = mode_settings
# Downstream Mode need to fill in this layout with its settings
self.mode_specific_layout = QtWidgets.QVBoxLayout()
# Persistent
self.persistent_checkbox = QtWidgets.QCheckBox()
self.persistent_checkbox.clicked.connect(self.persistent_checkbox_clicked)
self.persistent_checkbox.setText(strings._("mode_settings_persistent_checkbox"))
if self.settings.get("persistent", "enabled"):
self.persistent_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.persistent_checkbox.setCheckState(QtCore.Qt.Unchecked)
# Public
self.public_checkbox = QtWidgets.QCheckBox()
self.public_checkbox.clicked.connect(self.public_checkbox_clicked)
self.public_checkbox.setText(strings._("mode_settings_public_checkbox"))
if self.settings.get("general", "public"):
self.public_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.public_checkbox.setCheckState(QtCore.Qt.Unchecked)
# Whether or not to use an auto-start timer
self.autostart_timer_checkbox = QtWidgets.QCheckBox()
self.autostart_timer_checkbox.clicked.connect(
self.autostart_timer_checkbox_clicked
)
self.autostart_timer_checkbox.setText(
strings._("mode_settings_autostart_timer_checkbox")
)
if self.settings.get("general", "autostart_timer"):
self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked)
# The autostart timer widget
self.autostart_timer_widget = QtWidgets.QDateTimeEdit()
self.autostart_timer_widget.setDisplayFormat("hh:mm A MMM d, yy")
self.autostart_timer_reset()
self.autostart_timer_widget.setCurrentSection(
QtWidgets.QDateTimeEdit.MinuteSection
)
self.autostart_timer_widget.hide()
# Autostart timer layout
autostart_timer_layout = QtWidgets.QHBoxLayout()
autostart_timer_layout.setContentsMargins(0, 0, 0, 0)
autostart_timer_layout.addWidget(self.autostart_timer_checkbox)
autostart_timer_layout.addWidget(self.autostart_timer_widget)
# Whether or not to use an auto-stop timer
self.autostop_timer_checkbox = QtWidgets.QCheckBox()
self.autostop_timer_checkbox.clicked.connect(
self.autostop_timer_checkbox_clicked
)
self.autostop_timer_checkbox.setText(
strings._("mode_settings_autostop_timer_checkbox")
)
if self.settings.get("general", "autostop_timer"):
self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked)
# The autostop timer widget
self.autostop_timer_widget = QtWidgets.QDateTimeEdit()
self.autostop_timer_widget.setDisplayFormat("hh:mm A MMM d, yy")
self.autostop_timer_reset()
self.autostop_timer_widget.setCurrentSection(
QtWidgets.QDateTimeEdit.MinuteSection
)
self.autostop_timer_widget.hide()
# Autostop timer layout
autostop_timer_layout = QtWidgets.QHBoxLayout()
autostop_timer_layout.setContentsMargins(0, 0, 0, 0)
autostop_timer_layout.addWidget(self.autostop_timer_checkbox)
autostop_timer_layout.addWidget(self.autostop_timer_widget)
# Legacy address
self.legacy_checkbox = QtWidgets.QCheckBox()
self.legacy_checkbox.clicked.connect(self.legacy_checkbox_clicked)
self.legacy_checkbox.clicked.connect(self.update_ui)
self.legacy_checkbox.setText(strings._("mode_settings_legacy_checkbox"))
if self.settings.get("general", "legacy"):
self.legacy_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.legacy_checkbox.setCheckState(QtCore.Qt.Unchecked)
# Client auth
self.client_auth_checkbox = QtWidgets.QCheckBox()
self.client_auth_checkbox.clicked.connect(self.client_auth_checkbox_clicked)
self.client_auth_checkbox.clicked.connect(self.update_ui)
self.client_auth_checkbox.setText(
strings._("mode_settings_client_auth_checkbox")
)
if self.settings.get("general", "client_auth"):
self.client_auth_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.client_auth_checkbox.setCheckState(QtCore.Qt.Unchecked)
# Toggle advanced settings
self.toggle_advanced_button = QtWidgets.QPushButton()
self.toggle_advanced_button.clicked.connect(self.toggle_advanced_clicked)
self.toggle_advanced_button.setFlat(True)
self.toggle_advanced_button.setStyleSheet(
self.common.gui.css["mode_settings_toggle_advanced"]
)
# Advanced group itself
advanced_layout = QtWidgets.QVBoxLayout()
advanced_layout.setContentsMargins(0, 0, 0, 0)
advanced_layout.addLayout(autostart_timer_layout)
advanced_layout.addLayout(autostop_timer_layout)
advanced_layout.addWidget(self.legacy_checkbox)
advanced_layout.addWidget(self.client_auth_checkbox)
self.advanced_widget = QtWidgets.QWidget()
self.advanced_widget.setLayout(advanced_layout)
self.advanced_widget.hide()
layout = QtWidgets.QVBoxLayout()
layout.addLayout(self.mode_specific_layout)
layout.addWidget(self.persistent_checkbox)
layout.addWidget(self.public_checkbox)
layout.addWidget(self.advanced_widget)
layout.addWidget(self.toggle_advanced_button)
self.setLayout(layout)
self.update_ui()
def update_ui(self):
# Update text on advanced group toggle button
if self.advanced_widget.isVisible():
self.toggle_advanced_button.setText(
strings._("mode_settings_advanced_toggle_hide")
)
else:
self.toggle_advanced_button.setText(
strings._("mode_settings_advanced_toggle_show")
)
# Client auth is only a legacy option
if self.client_auth_checkbox.isChecked():
self.legacy_checkbox.setChecked(True)
self.legacy_checkbox.setEnabled(False)
else:
self.legacy_checkbox.setEnabled(True)
if self.legacy_checkbox.isChecked():
self.client_auth_checkbox.show()
else:
self.client_auth_checkbox.hide()
# If the server has been started in the past, prevent changing legacy option
if self.settings.get("onion", "private_key"):
if self.legacy_checkbox.isChecked():
# If using legacy, disable legacy and client auth options
self.legacy_checkbox.setEnabled(False)
self.client_auth_checkbox.setEnabled(False)
else:
# If using v3, hide legacy and client auth options
self.legacy_checkbox.hide()
self.client_auth_checkbox.hide()
def persistent_checkbox_clicked(self):
self.settings.set("persistent", "enabled", self.persistent_checkbox.isChecked())
self.settings.set("persistent", "mode", self.tab.mode)
self.change_persistent.emit(
self.tab.tab_id, self.persistent_checkbox.isChecked()
)
# If disabling persistence, delete the file from disk
if not self.persistent_checkbox.isChecked():
self.settings.delete()
def public_checkbox_clicked(self):
self.settings.set("general", "public", self.public_checkbox.isChecked())
def autostart_timer_checkbox_clicked(self):
self.settings.set(
"general", "autostart_timer", self.autostart_timer_checkbox.isChecked()
)
if self.autostart_timer_checkbox.isChecked():
self.autostart_timer_widget.show()
else:
self.autostart_timer_widget.hide()
def autostop_timer_checkbox_clicked(self):
self.settings.set(
"general", "autostop_timer", self.autostop_timer_checkbox.isChecked()
)
if self.autostop_timer_checkbox.isChecked():
self.autostop_timer_widget.show()
else:
self.autostop_timer_widget.hide()
def legacy_checkbox_clicked(self):
self.settings.set("general", "legacy", self.legacy_checkbox.isChecked())
def client_auth_checkbox_clicked(self):
self.settings.set(
"general", "client_auth", self.client_auth_checkbox.isChecked()
)
def toggle_advanced_clicked(self):
if self.advanced_widget.isVisible():
self.advanced_widget.hide()
else:
self.advanced_widget.show()
self.update_ui()
def autostart_timer_reset(self):
"""
Reset the auto-start timer in the UI after stopping a share
"""
if self.common.gui.local_only:
# For testing
self.autostart_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(15)
)
self.autostart_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime()
)
else:
self.autostart_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(
300
) # 5 minutes in the future
)
self.autostart_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)
def autostop_timer_reset(self):
"""
Reset the auto-stop timer in the UI after stopping a share
"""
if self.common.gui.local_only:
# For testing
self.autostop_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(15)
)
self.autostop_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime()
)
else:
self.autostop_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(300)
)
self.autostop_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)

View File

@ -24,6 +24,7 @@ from onionshare.web import Web
from ..history import History, ToggleHistory, ReceiveHistoryItem
from .. import Mode
from ....widgets import MinimumWidthWidget
class ReceiveMode(Mode):
@ -36,7 +37,28 @@ class ReceiveMode(Mode):
Custom initialization for ReceiveMode.
"""
# Create the Web object
self.web = Web(self.common, True, "receive")
self.web = Web(self.common, True, self.settings, "receive")
# Header
self.header_label.setText(strings._("gui_new_tab_receive_button"))
# Settings
data_dir_label = QtWidgets.QLabel(
strings._("mode_settings_receive_data_dir_label")
)
self.data_dir_lineedit = QtWidgets.QLineEdit()
self.data_dir_lineedit.setReadOnly(True)
self.data_dir_lineedit.setText(self.settings.get("receive", "data_dir"))
data_dir_button = QtWidgets.QPushButton(
strings._("mode_settings_receive_data_dir_browse_button")
)
data_dir_button.clicked.connect(self.data_dir_button_clicked)
data_dir_layout = QtWidgets.QHBoxLayout()
data_dir_layout.addWidget(data_dir_label)
data_dir_layout.addWidget(self.data_dir_lineedit)
data_dir_layout.addWidget(data_dir_button)
self.mode_settings_widget.mode_specific_layout.addLayout(data_dir_layout)
# Server status
self.server_status.set_mode("receive")
@ -90,14 +112,37 @@ class ReceiveMode(Mode):
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)
self.main_layout.addWidget(MinimumWidthWidget(700))
# Column layout
self.column_layout = QtWidgets.QHBoxLayout()
self.column_layout.addLayout(self.main_layout)
self.column_layout.addWidget(self.history, stretch=1)
# Wrapper layout
self.wrapper_layout = QtWidgets.QHBoxLayout()
self.wrapper_layout.addLayout(self.main_layout)
self.wrapper_layout.addWidget(self.history, stretch=1)
self.wrapper_layout = QtWidgets.QVBoxLayout()
self.wrapper_layout.addWidget(self.header_label)
self.wrapper_layout.addLayout(self.column_layout)
self.setLayout(self.wrapper_layout)
def data_dir_button_clicked(self):
"""
Browse for a new OnionShare data directory, and save to tab settings
"""
data_dir = self.data_dir_lineedit.text()
selected_dir = QtWidgets.QFileDialog.getExistingDirectory(
self, strings._("mode_settings_receive_data_dir_label"), data_dir
)
if selected_dir:
self.common.log(
"ReceiveMode",
"data_dir_button_clicked",
f"selected dir: {selected_dir}",
)
self.data_dir_lineedit.setText(selected_dir)
self.settings.set("receive", "data_dir", data_dir)
def get_stop_server_autostop_timer_text(self):
"""
Return the string to put on the stop server button, if there's an auto-stop timer

View File

@ -29,7 +29,7 @@ from ..file_selection import FileSelection
from .threads import CompressThread
from .. import Mode
from ..history import History, ToggleHistory, ShareHistoryItem
from ...widgets import Alert
from ....widgets import Alert, MinimumWidthWidget
class ShareMode(Mode):
@ -45,7 +45,27 @@ class ShareMode(Mode):
self.compress_thread = None
# Create the Web object
self.web = Web(self.common, True, "share")
self.web = Web(self.common, True, self.settings, "share")
# Header
self.header_label.setText(strings._("gui_new_tab_share_button"))
# Settings
self.autostop_sharing_checkbox = QtWidgets.QCheckBox()
self.autostop_sharing_checkbox.clicked.connect(
self.autostop_sharing_checkbox_clicked
)
self.autostop_sharing_checkbox.setText(
strings._("mode_settings_share_autostop_sharing_checkbox")
)
if self.settings.get("share", "autostop_sharing"):
self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.mode_settings_widget.mode_specific_layout.addWidget(
self.autostop_sharing_checkbox
)
# File selection
self.file_selection = FileSelection(self.common, self)
@ -69,7 +89,9 @@ class ShareMode(Mode):
# Filesize warning
self.filesize_warning = QtWidgets.QLabel()
self.filesize_warning.setWordWrap(True)
self.filesize_warning.setStyleSheet(self.common.css["share_filesize_warning"])
self.filesize_warning.setStyleSheet(
self.common.gui.css["share_filesize_warning"]
)
self.filesize_warning.hide()
# Download history
@ -119,17 +141,30 @@ class ShareMode(Mode):
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)
self.main_layout.addWidget(MinimumWidthWidget(700))
# Column layout
self.column_layout = QtWidgets.QHBoxLayout()
self.column_layout.addLayout(self.main_layout)
self.column_layout.addWidget(self.history, stretch=1)
# Wrapper layout
self.wrapper_layout = QtWidgets.QHBoxLayout()
self.wrapper_layout.addLayout(self.main_layout)
self.wrapper_layout.addWidget(self.history, stretch=1)
self.wrapper_layout = QtWidgets.QVBoxLayout()
self.wrapper_layout.addWidget(self.header_label)
self.wrapper_layout.addLayout(self.column_layout)
self.setLayout(self.wrapper_layout)
# Always start with focus on file selection
self.file_selection.setFocus()
def autostop_sharing_checkbox_clicked(self):
"""
Save autostop sharing setting to the tab settings
"""
self.settings.set(
"share", "autostop_sharing", self.autostop_sharing_checkbox.isChecked()
)
def get_stop_server_autostop_timer_text(self):
"""
Return the string to put on the stop server button, if there's an auto-stop timer
@ -141,7 +176,7 @@ class ShareMode(Mode):
The auto-stop 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 self.web.share_mode.cur_history_id == 0 or self.web.done:
if self.history.in_progress_count == 0 or self.web.done:
self.server_status.stop_server()
self.server_status_label.setText(strings._("close_on_autostop_timer"))
return True
@ -169,9 +204,7 @@ class ShareMode(Mode):
"""
# Add progress bar to the status bar, indicating the compressing of files.
self._zip_progress_bar = ZipProgressBar(self.common, 0)
self.filenames = []
for index in range(self.file_selection.file_list.count()):
self.filenames.append(self.file_selection.file_list.item(index).filename)
self.filenames = self.file_selection.get_filenames()
self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(
self.filenames
@ -278,7 +311,7 @@ class ShareMode(Mode):
self.history.update_in_progress()
# Close on finish?
if self.common.settings.get("close_after_first_download"):
if self.settings.get("share", "autostop_sharing"):
self.server_status.stop_server()
self.status_bar.clearMessage()
self.server_status_label.setText(strings._("closing_automatically"))
@ -372,7 +405,7 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setMinimumWidth(200)
self.setValue(0)
self.setFormat(strings._("zip_progress_bar_format"))
self.setStyleSheet(self.common.css["share_zip_progess_bar"])
self.setStyleSheet(self.common.gui.css["share_zip_progess_bar"])
self._total_files_size = total_files_size
self._processed_size = 0

View File

@ -31,7 +31,7 @@ from onionshare.web import Web
from ..file_selection import FileSelection
from .. import Mode
from ..history import History, ToggleHistory
from ...widgets import Alert
from ....widgets import Alert, MinimumWidthWidget
class WebsiteMode(Mode):
@ -47,7 +47,25 @@ class WebsiteMode(Mode):
Custom initialization for ReceiveMode.
"""
# Create the Web object
self.web = Web(self.common, True, "website")
self.web = Web(self.common, True, self.settings, "website")
# Header
self.header_label.setText(strings._("gui_new_tab_website_button"))
# Settings
self.disable_csp_checkbox = QtWidgets.QCheckBox()
self.disable_csp_checkbox.clicked.connect(self.disable_csp_checkbox_clicked)
self.disable_csp_checkbox.setText(
strings._("mode_settings_website_disable_csp_checkbox")
)
if self.settings.get("website", "disable_csp"):
self.disable_csp_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.disable_csp_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.mode_settings_widget.mode_specific_layout.addWidget(
self.disable_csp_checkbox
)
# File selection
self.file_selection = FileSelection(self.common, self)
@ -71,7 +89,9 @@ class WebsiteMode(Mode):
# Filesize warning
self.filesize_warning = QtWidgets.QLabel()
self.filesize_warning.setWordWrap(True)
self.filesize_warning.setStyleSheet(self.common.css["share_filesize_warning"])
self.filesize_warning.setStyleSheet(
self.common.gui.css["share_filesize_warning"]
)
self.filesize_warning.hide()
# Download history
@ -121,17 +141,30 @@ class WebsiteMode(Mode):
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)
self.main_layout.addWidget(MinimumWidthWidget(700))
# Column layout
self.column_layout = QtWidgets.QHBoxLayout()
self.column_layout.addLayout(self.main_layout)
self.column_layout.addWidget(self.history, stretch=1)
# Wrapper layout
self.wrapper_layout = QtWidgets.QHBoxLayout()
self.wrapper_layout.addLayout(self.main_layout)
self.wrapper_layout.addWidget(self.history, stretch=1)
self.wrapper_layout = QtWidgets.QVBoxLayout()
self.wrapper_layout.addWidget(self.header_label)
self.wrapper_layout.addLayout(self.column_layout)
self.setLayout(self.wrapper_layout)
# Always start with focus on file selection
self.file_selection.setFocus()
def disable_csp_checkbox_clicked(self):
"""
Save disable CSP setting to the tab settings
"""
self.settings.set(
"website", "disable_csp", self.disable_csp_checkbox.isChecked()
)
def get_stop_server_autostop_timer_text(self):
"""
Return the string to put on the stop server button, if there's an auto-stop timer

View File

@ -23,7 +23,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from .widgets import Alert
from ..widgets import Alert
class ServerStatus(QtWidgets.QWidget):
@ -39,15 +39,20 @@ class ServerStatus(QtWidgets.QWidget):
url_copied = QtCore.pyqtSignal()
hidservauth_copied = QtCore.pyqtSignal()
MODE_SHARE = "share"
MODE_RECEIVE = "receive"
MODE_WEBSITE = "website"
STATUS_STOPPED = 0
STATUS_WORKING = 1
STATUS_STARTED = 2
def __init__(self, common, qtapp, app, file_selection=None, local_only=False):
def __init__(
self,
common,
qtapp,
app,
mode_settings,
mode_settings_widget,
file_selection=None,
local_only=False,
):
super(ServerStatus, self).__init__()
self.common = common
@ -57,6 +62,8 @@ class ServerStatus(QtWidgets.QWidget):
self.qtapp = qtapp
self.app = app
self.settings = mode_settings
self.mode_settings_widget = mode_settings_widget
self.web = None
self.autostart_timer_datetime = None
@ -64,80 +71,6 @@ class ServerStatus(QtWidgets.QWidget):
self.resizeEvent(None)
# Auto-start timer layout
self.autostart_timer_label = QtWidgets.QLabel(
strings._("gui_settings_autostart_timer")
)
self.autostart_timer_widget = QtWidgets.QDateTimeEdit()
self.autostart_timer_widget.setDisplayFormat("hh:mm A MMM d, yy")
if self.local_only:
# For testing
self.autostart_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(15)
)
self.autostart_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime()
)
else:
# Set proposed timer to be 5 minutes into the future
self.autostart_timer_widget.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.autostart_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)
self.autostart_timer_widget.setCurrentSection(
QtWidgets.QDateTimeEdit.MinuteSection
)
autostart_timer_layout = QtWidgets.QHBoxLayout()
autostart_timer_layout.addWidget(self.autostart_timer_label)
autostart_timer_layout.addWidget(self.autostart_timer_widget)
# Auto-start timer container, so it can all be hidden and shown as a group
autostart_timer_container_layout = QtWidgets.QVBoxLayout()
autostart_timer_container_layout.addLayout(autostart_timer_layout)
self.autostart_timer_container = QtWidgets.QWidget()
self.autostart_timer_container.setLayout(autostart_timer_container_layout)
self.autostart_timer_container.hide()
# Auto-stop timer layout
self.autostop_timer_label = QtWidgets.QLabel(
strings._("gui_settings_autostop_timer")
)
self.autostop_timer_widget = QtWidgets.QDateTimeEdit()
self.autostop_timer_widget.setDisplayFormat("hh:mm A MMM d, yy")
if self.local_only:
# For testing
self.autostop_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(15)
)
self.autostop_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime()
)
else:
# Set proposed timer to be 5 minutes into the future
self.autostop_timer_widget.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.autostop_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)
self.autostop_timer_widget.setCurrentSection(
QtWidgets.QDateTimeEdit.MinuteSection
)
autostop_timer_layout = QtWidgets.QHBoxLayout()
autostop_timer_layout.addWidget(self.autostop_timer_label)
autostop_timer_layout.addWidget(self.autostop_timer_widget)
# Auto-stop timer container, so it can all be hidden and shown as a group
autostop_timer_container_layout = QtWidgets.QVBoxLayout()
autostop_timer_container_layout.addLayout(autostop_timer_layout)
self.autostop_timer_container = QtWidgets.QWidget()
self.autostop_timer_container.setLayout(autostop_timer_container_layout)
self.autostop_timer_container.hide()
# Server layout
self.server_button = QtWidgets.QPushButton()
self.server_button.clicked.connect(self.server_button_clicked)
@ -151,11 +84,13 @@ class ServerStatus(QtWidgets.QWidget):
self.url.setFont(url_font)
self.url.setWordWrap(True)
self.url.setMinimumSize(self.url.sizeHint())
self.url.setStyleSheet(self.common.css["server_status_url"])
self.url.setStyleSheet(self.common.gui.css["server_status_url"])
self.copy_url_button = QtWidgets.QPushButton(strings._("gui_copy_url"))
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.gui.css["server_status_url_buttons"]
)
self.copy_url_button.setMinimumHeight(65)
self.copy_url_button.clicked.connect(self.copy_url)
self.copy_hidservauth_button = QtWidgets.QPushButton(
@ -163,7 +98,7 @@ class ServerStatus(QtWidgets.QWidget):
)
self.copy_hidservauth_button.setFlat(True)
self.copy_hidservauth_button.setStyleSheet(
self.common.css["server_status_url_buttons"]
self.common.gui.css["server_status_url_buttons"]
)
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
url_buttons_layout = QtWidgets.QHBoxLayout()
@ -180,8 +115,6 @@ class ServerStatus(QtWidgets.QWidget):
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.server_button)
layout.addLayout(url_layout)
layout.addWidget(self.autostart_timer_container)
layout.addWidget(self.autostop_timer_container)
self.setLayout(layout)
def set_mode(self, share_mode, file_selection=None):
@ -190,8 +123,8 @@ class ServerStatus(QtWidgets.QWidget):
"""
self.mode = share_mode
if (self.mode == ServerStatus.MODE_SHARE) or (
self.mode == ServerStatus.MODE_WEBSITE
if (self.mode == self.common.gui.MODE_SHARE) or (
self.mode == self.common.gui.MODE_WEBSITE
):
self.file_selection = file_selection
@ -214,30 +147,6 @@ class ServerStatus(QtWidgets.QWidget):
except:
pass
def autostart_timer_reset(self):
"""
Reset the auto-start timer in the UI after stopping a share
"""
self.autostart_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(300)
)
if not self.local_only:
self.autostart_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)
def autostop_timer_reset(self):
"""
Reset the auto-stop timer in the UI after stopping a share
"""
self.autostop_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(300)
)
if not self.local_only:
self.autostop_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)
def show_url(self):
"""
Show the URL in the UI.
@ -246,11 +155,11 @@ class ServerStatus(QtWidgets.QWidget):
info_image = self.common.get_resource_path("images/info.png")
if self.mode == ServerStatus.MODE_SHARE:
if self.mode == self.common.gui.MODE_SHARE:
self.url_description.setText(
strings._("gui_share_url_description").format(info_image)
)
elif self.mode == ServerStatus.MODE_WEBSITE:
elif self.mode == self.common.gui.MODE_WEBSITE:
self.url_description.setText(
strings._("gui_website_url_description").format(info_image)
)
@ -260,9 +169,9 @@ class ServerStatus(QtWidgets.QWidget):
)
# Show a Tool Tip explaining the lifecycle of this URL
if self.common.settings.get("save_private_key"):
if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get(
"close_after_first_download"
if self.settings.get("persistent", "enabled"):
if self.mode == self.common.gui.MODE_SHARE and self.settings.get(
"share", "autostop_sharing"
):
self.url_description.setToolTip(
strings._("gui_url_label_onetime_and_persistent")
@ -270,8 +179,8 @@ class ServerStatus(QtWidgets.QWidget):
else:
self.url_description.setToolTip(strings._("gui_url_label_persistent"))
else:
if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get(
"close_after_first_download"
if self.mode == self.common.gui.MODE_SHARE and self.settings.get(
"share", "autostop_sharing"
):
self.url_description.setToolTip(strings._("gui_url_label_onetime"))
else:
@ -281,7 +190,7 @@ class ServerStatus(QtWidgets.QWidget):
self.url.show()
self.copy_url_button.show()
if self.app.stealth:
if self.settings.get("general", "client_auth"):
self.copy_hidservauth_button.show()
else:
self.copy_hidservauth_button.hide()
@ -290,6 +199,7 @@ class ServerStatus(QtWidgets.QWidget):
"""
Update the GUI elements based on the current state.
"""
self.common.log("ServerStatus", "update")
# Set the URL fields
if self.status == self.STATUS_STARTED:
# The backend Onion may have saved new settings, such as the private key.
@ -297,30 +207,34 @@ class ServerStatus(QtWidgets.QWidget):
self.common.settings.load()
self.show_url()
if self.common.settings.get("save_private_key"):
if not self.common.settings.get("password"):
self.common.settings.set("password", self.web.password)
self.common.settings.save()
if not self.settings.get("onion", "password"):
self.settings.set("onion", "password", self.web.password)
self.settings.save()
if self.common.settings.get("autostart_timer"):
self.autostart_timer_container.hide()
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.hide()
if self.settings.get("general", "autostop_timer"):
self.server_button.setToolTip(
strings._("gui_stop_server_autostop_timer_tooltip").format(
self.mode_settings_widget.autostop_timer_widget.dateTime().toString(
"h:mm AP, MMMM dd, yyyy"
)
)
)
else:
self.url_description.hide()
self.url.hide()
self.copy_url_button.hide()
self.copy_hidservauth_button.hide()
self.mode_settings_widget.update_ui()
# Button
if (
self.mode == ServerStatus.MODE_SHARE
self.mode == self.common.gui.MODE_SHARE
and self.file_selection.get_num_files() == 0
):
self.server_button.hide()
elif (
self.mode == ServerStatus.MODE_WEBSITE
self.mode == self.common.gui.MODE_WEBSITE
and self.file_selection.get_num_files() == 0
):
self.server_button.hide()
@ -329,77 +243,57 @@ class ServerStatus(QtWidgets.QWidget):
if self.status == self.STATUS_STOPPED:
self.server_button.setStyleSheet(
self.common.css["server_status_button_stopped"]
self.common.gui.css["server_status_button_stopped"]
)
self.server_button.setEnabled(True)
if self.mode == ServerStatus.MODE_SHARE:
if self.mode == self.common.gui.MODE_SHARE:
self.server_button.setText(strings._("gui_share_start_server"))
elif self.mode == ServerStatus.MODE_WEBSITE:
elif self.mode == self.common.gui.MODE_WEBSITE:
self.server_button.setText(strings._("gui_share_start_server"))
else:
self.server_button.setText(strings._("gui_receive_start_server"))
self.server_button.setToolTip("")
if self.common.settings.get("autostart_timer"):
self.autostart_timer_container.show()
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.show()
elif self.status == self.STATUS_STARTED:
self.server_button.setStyleSheet(
self.common.css["server_status_button_started"]
self.common.gui.css["server_status_button_started"]
)
self.server_button.setEnabled(True)
if self.mode == ServerStatus.MODE_SHARE:
if self.mode == self.common.gui.MODE_SHARE:
self.server_button.setText(strings._("gui_share_stop_server"))
elif self.mode == ServerStatus.MODE_WEBSITE:
elif self.mode == self.common.gui.MODE_WEBSITE:
self.server_button.setText(strings._("gui_share_stop_server"))
else:
self.server_button.setText(strings._("gui_receive_stop_server"))
if self.common.settings.get("autostart_timer"):
self.autostart_timer_container.hide()
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.hide()
self.server_button.setToolTip(
strings._("gui_stop_server_autostop_timer_tooltip").format(
self.autostop_timer_widget.dateTime().toString(
"h:mm AP, MMMM dd, yyyy"
)
)
)
elif self.status == self.STATUS_WORKING:
self.server_button.setStyleSheet(
self.common.css["server_status_button_working"]
self.common.gui.css["server_status_button_working"]
)
self.server_button.setEnabled(True)
if self.autostart_timer_datetime:
self.autostart_timer_container.hide()
self.server_button.setToolTip(
strings._("gui_start_server_autostart_timer_tooltip").format(
self.autostart_timer_widget.dateTime().toString(
self.mode_settings_widget.autostart_timer_widget.dateTime().toString(
"h:mm AP, MMMM dd, yyyy"
)
)
)
else:
self.server_button.setText(strings._("gui_please_wait"))
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.hide()
else:
self.server_button.setStyleSheet(
self.common.css["server_status_button_working"]
)
self.server_button.setEnabled(False)
self.server_button.setText(strings._("gui_please_wait"))
if self.common.settings.get("autostart_timer"):
self.autostart_timer_container.hide()
if self.settings.get("general", "autostart_timer"):
self.server_button.setToolTip(
strings._("gui_start_server_autostart_timer_tooltip").format(
self.autostart_timer_widget.dateTime().toString(
self.mode_settings_widget.autostart_timer_widget.dateTime().toString(
"h:mm AP, MMMM dd, yyyy"
)
)
)
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.hide()
else:
self.server_button.setStyleSheet(
self.common.gui.css["server_status_button_working"]
)
self.server_button.setEnabled(False)
self.server_button.setText(strings._("gui_please_wait"))
def server_button_clicked(self):
"""
@ -407,14 +301,14 @@ class ServerStatus(QtWidgets.QWidget):
"""
if self.status == self.STATUS_STOPPED:
can_start = True
if self.common.settings.get("autostart_timer"):
if self.settings.get("general", "autostart_timer"):
if self.local_only:
self.autostart_timer_datetime = (
self.autostart_timer_widget.dateTime().toPyDateTime()
self.mode_settings_widget.autostart_timer_widget.dateTime().toPyDateTime()
)
else:
self.autostart_timer_datetime = (
self.autostart_timer_widget.dateTime()
self.mode_settings_widget.autostart_timer_widget.dateTime()
.toPyDateTime()
.replace(second=0, microsecond=0)
)
@ -429,15 +323,15 @@ class ServerStatus(QtWidgets.QWidget):
strings._("gui_server_autostart_timer_expired"),
QtWidgets.QMessageBox.Warning,
)
if self.common.settings.get("autostop_timer"):
if self.settings.get("general", "autostop_timer"):
if self.local_only:
self.autostop_timer_datetime = (
self.autostop_timer_widget.dateTime().toPyDateTime()
self.mode_settings_widget.autostop_timer_widget.dateTime().toPyDateTime()
)
else:
# Get the timer chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
self.autostop_timer_datetime = (
self.autostop_timer_widget.dateTime()
self.mode_settings_widget.autostop_timer_widget.dateTime()
.toPyDateTime()
.replace(second=0, microsecond=0)
)
@ -452,7 +346,7 @@ class ServerStatus(QtWidgets.QWidget):
strings._("gui_server_autostop_timer_expired"),
QtWidgets.QMessageBox.Warning,
)
if self.common.settings.get("autostart_timer"):
if self.settings.get("general", "autostart_timer"):
if self.autostop_timer_datetime <= self.autostart_timer_datetime:
Alert(
self.common,
@ -492,8 +386,8 @@ class ServerStatus(QtWidgets.QWidget):
Stop the server.
"""
self.status = self.STATUS_WORKING
self.autostart_timer_reset()
self.autostop_timer_reset()
self.mode_settings_widget.autostart_timer_reset()
self.mode_settings_widget.autostop_timer_reset()
self.update()
self.server_stopped.emit()
@ -505,8 +399,8 @@ class ServerStatus(QtWidgets.QWidget):
"ServerStatus", "cancel_server", "Canceling the server mid-startup"
)
self.status = self.STATUS_WORKING
self.autostart_timer_reset()
self.autostop_timer_reset()
self.mode_settings_widget.autostart_timer_reset()
self.mode_settings_widget.autostop_timer_reset()
self.update()
self.server_canceled.emit()
@ -539,7 +433,7 @@ class ServerStatus(QtWidgets.QWidget):
"""
Returns the OnionShare URL.
"""
if self.common.settings.get("public_mode"):
if self.settings.get("general", "public"):
url = f"http://{self.app.onion_host}"
else:
url = f"http://onionshare:{self.web.password}@{self.app.onion_host}"

539
onionshare_gui/tab/tab.py Normal file
View File

@ -0,0 +1,539 @@
# -*- 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 queue
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from onionshare.onionshare import OnionShare
from onionshare.web import Web
from onionshare.mode_settings import ModeSettings
from .mode.share_mode import ShareMode
from .mode.receive_mode import ReceiveMode
from .mode.website_mode import WebsiteMode
from .server_status import ServerStatus
from ..widgets import Alert
class Tab(QtWidgets.QWidget):
"""
A GUI tab, you know, sort of like in a web browser
"""
change_title = QtCore.pyqtSignal(int, str)
change_icon = QtCore.pyqtSignal(int, str)
change_persistent = QtCore.pyqtSignal(int, bool)
def __init__(
self,
common,
tab_id,
system_tray,
status_bar,
mode_settings=None,
filenames=None,
):
super(Tab, self).__init__()
self.common = common
self.common.log("Tab", "__init__")
self.tab_id = tab_id
self.system_tray = system_tray
self.status_bar = status_bar
self.filenames = filenames
self.mode = None
# Start the OnionShare app
self.app = OnionShare(common, self.common.gui.onion, self.common.gui.local_only)
# Widgets to display on a new tab
self.share_button = QtWidgets.QPushButton(strings._("gui_new_tab_share_button"))
self.share_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"])
share_description = QtWidgets.QLabel(strings._("gui_new_tab_share_description"))
share_description.setWordWrap(True)
self.share_button.clicked.connect(self.share_mode_clicked)
self.receive_button = QtWidgets.QPushButton(
strings._("gui_new_tab_receive_button")
)
self.receive_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"])
self.receive_button.clicked.connect(self.receive_mode_clicked)
receive_description = QtWidgets.QLabel(
strings._("gui_new_tab_receive_description")
)
receive_description.setWordWrap(True)
self.website_button = QtWidgets.QPushButton(
strings._("gui_new_tab_website_button")
)
self.website_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"])
self.website_button.clicked.connect(self.website_mode_clicked)
website_description = QtWidgets.QLabel(
strings._("gui_new_tab_website_description")
)
website_description.setWordWrap(True)
new_tab_layout = QtWidgets.QVBoxLayout()
new_tab_layout.addStretch(1)
new_tab_layout.addWidget(self.share_button)
new_tab_layout.addWidget(share_description)
new_tab_layout.addSpacing(50)
new_tab_layout.addWidget(self.receive_button)
new_tab_layout.addWidget(receive_description)
new_tab_layout.addSpacing(50)
new_tab_layout.addWidget(self.website_button)
new_tab_layout.addWidget(website_description)
new_tab_layout.addStretch(3)
new_tab_inner = QtWidgets.QWidget()
new_tab_inner.setFixedWidth(500)
new_tab_inner.setLayout(new_tab_layout)
new_tab_outer_layout = QtWidgets.QHBoxLayout()
new_tab_outer_layout.addStretch()
new_tab_outer_layout.addWidget(new_tab_inner)
new_tab_outer_layout.addStretch()
self.new_tab = QtWidgets.QWidget()
self.new_tab.setLayout(new_tab_outer_layout)
self.new_tab.show()
# Layout
self.layout = QtWidgets.QVBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.new_tab)
self.setLayout(self.layout)
# Create the timer
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.timer_callback)
# Persistent image
self.persistent_image_label = QtWidgets.QLabel()
self.persistent_image_label.setPixmap(
QtGui.QPixmap.fromImage(
QtGui.QImage(
self.common.get_resource_path("images/persistent_enabled.png")
)
)
)
self.persistent_image_label.setFixedSize(20, 20)
# Create the close warning dialog -- the dialog widget needs to be in the constructor
# in order to test it
self.close_dialog = QtWidgets.QMessageBox()
self.close_dialog.setWindowTitle(strings._("gui_close_tab_warning_title"))
self.close_dialog.setIcon(QtWidgets.QMessageBox.Critical)
self.close_dialog.accept_button = self.close_dialog.addButton(
strings._("gui_close_tab_warning_close"), QtWidgets.QMessageBox.AcceptRole
)
self.close_dialog.reject_button = self.close_dialog.addButton(
strings._("gui_close_tab_warning_cancel"), QtWidgets.QMessageBox.RejectRole
)
self.close_dialog.setDefaultButton(self.close_dialog.reject_button)
def init(self, mode_settings=None):
if mode_settings:
# Load this tab
self.settings = mode_settings
mode = self.settings.get("persistent", "mode")
if mode == "share":
self.filenames = self.settings.get("share", "filenames")
self.share_mode_clicked()
elif mode == "receive":
self.receive_mode_clicked()
elif mode == "website":
self.filenames = self.settings.get("website", "filenames")
self.website_mode_clicked()
else:
# This is a new tab
self.settings = ModeSettings(self.common)
def share_mode_clicked(self):
self.common.log("Tab", "share_mode_clicked")
self.mode = self.common.gui.MODE_SHARE
self.new_tab.hide()
self.share_mode = ShareMode(self)
self.share_mode.change_persistent.connect(self.change_persistent)
self.layout.addWidget(self.share_mode)
self.share_mode.show()
self.share_mode.init()
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.start_server_finished.connect(
self.update_server_status_indicator
)
self.share_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.share_mode.stop_server_finished.connect(self.stop_server_finished)
self.share_mode.start_server_finished.connect(self.clear_message)
self.share_mode.server_status.button_clicked.connect(self.clear_message)
self.share_mode.server_status.url_copied.connect(self.copy_url)
self.share_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.change_title.emit(self.tab_id, strings._("gui_new_tab_share_button"))
self.update_server_status_indicator()
self.timer.start(500)
def receive_mode_clicked(self):
self.common.log("Tab", "receive_mode_clicked")
self.mode = self.common.gui.MODE_RECEIVE
self.new_tab.hide()
self.receive_mode = ReceiveMode(self)
self.receive_mode.change_persistent.connect(self.change_persistent)
self.layout.addWidget(self.receive_mode)
self.receive_mode.show()
self.receive_mode.init()
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.start_server_finished.connect(
self.update_server_status_indicator
)
self.receive_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.receive_mode.stop_server_finished.connect(self.stop_server_finished)
self.receive_mode.start_server_finished.connect(self.clear_message)
self.receive_mode.server_status.button_clicked.connect(self.clear_message)
self.receive_mode.server_status.url_copied.connect(self.copy_url)
self.receive_mode.server_status.hidservauth_copied.connect(
self.copy_hidservauth
)
self.change_title.emit(self.tab_id, strings._("gui_new_tab_receive_button"))
self.update_server_status_indicator()
self.timer.start(500)
def website_mode_clicked(self):
self.common.log("Tab", "website_mode_clicked")
self.mode = self.common.gui.MODE_WEBSITE
self.new_tab.hide()
self.website_mode = WebsiteMode(self)
self.website_mode.change_persistent.connect(self.change_persistent)
self.layout.addWidget(self.website_mode)
self.website_mode.show()
self.website_mode.init()
self.website_mode.server_status.server_started.connect(
self.update_server_status_indicator
)
self.website_mode.server_status.server_stopped.connect(
self.update_server_status_indicator
)
self.website_mode.start_server_finished.connect(
self.update_server_status_indicator
)
self.website_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.website_mode.stop_server_finished.connect(self.stop_server_finished)
self.website_mode.start_server_finished.connect(self.clear_message)
self.website_mode.server_status.button_clicked.connect(self.clear_message)
self.website_mode.server_status.url_copied.connect(self.copy_url)
self.website_mode.server_status.hidservauth_copied.connect(
self.copy_hidservauth
)
self.change_title.emit(self.tab_id, strings._("gui_new_tab_website_button"))
self.update_server_status_indicator()
self.timer.start(500)
def update_server_status_indicator(self):
# Set the status image
if self.mode == self.common.gui.MODE_SHARE:
# Share mode
if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.set_server_status_indicator_stopped(
strings._("gui_status_indicator_share_stopped")
)
elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING:
if self.share_mode.server_status.autostart_timer_datetime:
self.set_server_status_indicator_working(
strings._("gui_status_indicator_share_scheduled")
)
else:
self.set_server_status_indicator_working(
strings._("gui_status_indicator_share_working")
)
elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.set_server_status_indicator_started(
strings._("gui_status_indicator_share_started")
)
elif self.mode == self.common.gui.MODE_WEBSITE:
# Website mode
if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.set_server_status_indicator_stopped(
strings._("gui_status_indicator_share_stopped")
)
elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING:
self.set_server_status_indicator_working(
strings._("gui_status_indicator_share_working")
)
elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.set_server_status_indicator_started(
strings._("gui_status_indicator_share_started")
)
elif self.mode == self.common.gui.MODE_RECEIVE:
# Receive mode
if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.set_server_status_indicator_stopped(
strings._("gui_status_indicator_receive_stopped")
)
elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING:
if self.receive_mode.server_status.autostart_timer_datetime:
self.set_server_status_indicator_working(
strings._("gui_status_indicator_receive_scheduled")
)
else:
self.set_server_status_indicator_working(
strings._("gui_status_indicator_receive_working")
)
elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.set_server_status_indicator_started(
strings._("gui_status_indicator_receive_started")
)
def set_server_status_indicator_stopped(self, label_text):
self.change_icon.emit(self.tab_id, "images/server_stopped.png")
self.status_bar.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.status_bar.server_status_image_stopped)
)
self.status_bar.server_status_label.setText(label_text)
def set_server_status_indicator_working(self, label_text):
self.change_icon.emit(self.tab_id, "images/server_working.png")
self.status_bar.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.status_bar.server_status_image_working)
)
self.status_bar.server_status_label.setText(label_text)
def set_server_status_indicator_started(self, label_text):
self.change_icon.emit(self.tab_id, "images/server_started.png")
self.status_bar.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.status_bar.server_status_image_started)
)
self.status_bar.server_status_label.setText(label_text)
def stop_server_finished(self):
# When the server stopped, cleanup the ephemeral onion service
self.get_mode().app.stop_onion_service(self.settings)
def timer_callback(self):
"""
Check for messages communicated from the web app, and update the GUI accordingly. Also,
call ShareMode and ReceiveMode's timer_callbacks.
"""
self.update()
if not self.common.gui.local_only:
# Have we lost connection to Tor somehow?
if not self.common.gui.onion.is_authenticated():
self.timer.stop()
self.status_bar.showMessage(strings._("gui_tor_connection_lost"))
self.system_tray.showMessage(
strings._("gui_tor_connection_lost"),
strings._("gui_tor_connection_error_settings"),
)
self.get_mode().handle_tor_broke()
# Process events from the web object
mode = self.get_mode()
events = []
done = False
while not done:
try:
r = mode.web.q.get(False)
events.append(r)
except queue.Empty:
done = True
for event in events:
if event["type"] == Web.REQUEST_LOAD:
mode.handle_request_load(event)
elif event["type"] == Web.REQUEST_STARTED:
mode.handle_request_started(event)
elif event["type"] == Web.REQUEST_RATE_LIMIT:
mode.handle_request_rate_limit(event)
elif event["type"] == Web.REQUEST_PROGRESS:
mode.handle_request_progress(event)
elif event["type"] == Web.REQUEST_CANCELED:
mode.handle_request_canceled(event)
elif event["type"] == Web.REQUEST_UPLOAD_FILE_RENAMED:
mode.handle_request_upload_file_renamed(event)
elif event["type"] == Web.REQUEST_UPLOAD_SET_DIR:
mode.handle_request_upload_set_dir(event)
elif event["type"] == Web.REQUEST_UPLOAD_FINISHED:
mode.handle_request_upload_finished(event)
elif event["type"] == Web.REQUEST_UPLOAD_CANCELED:
mode.handle_request_upload_canceled(event)
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_STARTED:
mode.handle_request_individual_file_started(event)
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_PROGRESS:
mode.handle_request_individual_file_progress(event)
elif event["type"] == Web.REQUEST_INDIVIDUAL_FILE_CANCELED:
mode.handle_request_individual_file_canceled(event)
if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE:
Alert(
self.common,
strings._("error_cannot_create_data_dir").format(
event["data"]["receive_mode_dir"]
),
)
if event["type"] == Web.REQUEST_OTHER:
if (
event["path"] != "/favicon.ico"
and event["path"] != f"/{mode.web.shutdown_password}/shutdown"
):
self.status_bar.showMessage(
f"{strings._('other_page_loaded')}: {event['path']}"
)
if event["type"] == Web.REQUEST_INVALID_PASSWORD:
self.status_bar.showMessage(
f"[#{mode.web.invalid_passwords_count}] {strings._('incorrect_password')}: {event['data']}"
)
mode.timer_callback()
def copy_url(self):
"""
When the URL gets copied to the clipboard, display this in the status bar.
"""
self.common.log("Tab", "copy_url")
self.system_tray.showMessage(
strings._("gui_copied_url_title"), strings._("gui_copied_url")
)
def copy_hidservauth(self):
"""
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
"""
self.common.log("Tab", "copy_hidservauth")
self.system_tray.showMessage(
strings._("gui_copied_hidservauth_title"),
strings._("gui_copied_hidservauth"),
)
def clear_message(self):
"""
Clear messages from the status bar.
"""
self.status_bar.clearMessage()
def get_mode(self):
if self.mode:
if self.mode == self.common.gui.MODE_SHARE:
return self.share_mode
elif self.mode == self.common.gui.MODE_RECEIVE:
return self.receive_mode
else:
return self.website_mode
else:
return None
def settings_have_changed(self):
# Global settings have changed
self.common.log("Tab", "settings_have_changed")
# We might've stopped the main requests timer if a Tor connection failed. If we've reloaded
# settings, we probably succeeded in obtaining a new connection. If so, restart the timer.
if not self.common.gui.local_only:
if self.common.gui.onion.is_authenticated():
mode = self.get_mode()
if mode:
if not self.timer.isActive():
self.timer.start(500)
mode.on_reload_settings()
def close_tab(self):
self.common.log("Tab", "close_tab")
if self.mode is None:
return True
if self.settings.get("persistent", "enabled"):
dialog_text = strings._("gui_close_tab_warning_persistent_description")
else:
server_status = self.get_mode().server_status
if server_status.status == server_status.STATUS_STOPPED:
return True
else:
if self.mode == self.common.gui.MODE_SHARE:
dialog_text = strings._("gui_close_tab_warning_share_description")
elif self.mode == self.common.gui.MODE_RECEIVE:
dialog_text = strings._("gui_close_tab_warning_receive_description")
else:
dialog_text = strings._("gui_close_tab_warning_website_description")
# Open the warning dialog
self.common.log("Tab", "close_tab, opening warning dialog")
self.close_dialog.setText(dialog_text)
self.close_dialog.exec_()
# Close
if self.close_dialog.clickedButton() == self.close_dialog.accept_button:
self.common.log("Tab", "close_tab", "close, closing tab")
self.get_mode().stop_server()
self.app.cleanup()
return True
# Cancel
else:
self.common.log("Tab", "close_tab", "cancel, keeping tab open")
return False
def cleanup(self):
self.app.cleanup()

View File

@ -0,0 +1,235 @@
# -*- 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 watchdog.observers import Observer
from onionshare import strings
from onionshare.mode_settings import ModeSettings
from .tab import Tab
from .event_handler import EventHandler
class TabWidget(QtWidgets.QTabWidget):
"""
A custom tab widget, that has a "+" button for adding new tabs
"""
bring_to_front = QtCore.pyqtSignal()
def __init__(self, common, system_tray, status_bar):
super(TabWidget, self).__init__()
self.common = common
self.common.log("TabWidget", "__init__")
self.system_tray = system_tray
self.status_bar = status_bar
# Keep track of tabs in a dictionary
self.tabs = {}
self.current_tab_id = 0 # Each tab has a unique id
# Define the new tab button
self.new_tab_button = QtWidgets.QPushButton("+", parent=self)
self.new_tab_button.setFlat(True)
self.new_tab_button.setAutoFillBackground(True)
self.new_tab_button.setFixedSize(30, 30)
self.new_tab_button.clicked.connect(self.new_tab_clicked)
self.new_tab_button.setStyleSheet(
self.common.gui.css["tab_widget_new_tab_button"]
)
self.new_tab_button.setToolTip(strings._("gui_new_tab_tooltip"))
# Use a custom tab bar
tab_bar = TabBar()
tab_bar.move_new_tab_button.connect(self.move_new_tab_button)
self.setTabBar(tab_bar)
# Set up the tab widget
self.setMovable(True)
self.setTabsClosable(True)
self.setUsesScrollButtons(True)
self.tabCloseRequested.connect(self.close_tab)
self.move_new_tab_button()
# Watch the events file for changes
self.event_handler = EventHandler(common)
self.event_handler.new_tab.connect(self.add_tab)
self.event_handler.new_share_tab.connect(self.new_share_tab)
self.observer = Observer()
self.observer.schedule(self.event_handler, self.common.gui.events_dir)
self.observer.start()
def cleanup(self):
# Stop the event thread
self.observer.stop()
self.observer.join()
# Clean up each tab
for index in range(self.count()):
tab = self.widget(index)
tab.cleanup()
def move_new_tab_button(self):
# Find the width of all tabs
tabs_width = sum(
[self.tabBar().tabRect(i).width() for i in range(self.count())]
)
# The current position of the new tab button
pos = self.new_tab_button.pos()
# If there are so many tabs it scrolls, move the button to the left of the scroll buttons
if tabs_width > self.width():
pos.setX(self.width() - 61)
else:
# Otherwise move the button to the right of the tabs
pos.setX(self.tabBar().sizeHint().width())
self.new_tab_button.move(pos)
self.new_tab_button.raise_()
def new_tab_clicked(self):
# Create a new tab
self.add_tab()
def load_tab(self, mode_settings_id):
# Load the tab's mode settings
mode_settings = ModeSettings(self.common, id=mode_settings_id)
self.add_tab(mode_settings)
def new_share_tab(self, filenames):
mode_settings = ModeSettings(self.common)
mode_settings.set("persistent", "mode", "share")
mode_settings.set("share", "filenames", filenames)
self.add_tab(mode_settings)
def add_tab(self, mode_settings=None):
tab = Tab(self.common, self.current_tab_id, self.system_tray, self.status_bar)
tab.change_title.connect(self.change_title)
tab.change_icon.connect(self.change_icon)
tab.change_persistent.connect(self.change_persistent)
self.tabs[self.current_tab_id] = tab
self.current_tab_id += 1
index = self.addTab(tab, strings._("gui_new_tab"))
self.setCurrentIndex(index)
tab.init(mode_settings)
# If it's persistent, set the persistent image in the tab
self.change_persistent(tab.tab_id, tab.settings.get("persistent", "enabled"))
# Bring the window to front, in case this is being added by an event
self.bring_to_front.emit()
def change_title(self, tab_id, title):
index = self.indexOf(self.tabs[tab_id])
self.setTabText(index, title)
def change_icon(self, tab_id, icon_path):
index = self.indexOf(self.tabs[tab_id])
self.setTabIcon(index, QtGui.QIcon(self.common.get_resource_path(icon_path)))
def change_persistent(self, tab_id, is_persistent):
index = self.indexOf(self.tabs[tab_id])
if is_persistent:
self.tabBar().setTabButton(
index,
QtWidgets.QTabBar.LeftSide,
self.tabs[tab_id].persistent_image_label,
)
else:
invisible_widget = QtWidgets.QWidget()
invisible_widget.setFixedSize(0, 0)
self.tabBar().setTabButton(
index, QtWidgets.QTabBar.LeftSide, invisible_widget
)
self.save_persistent_tabs()
def save_persistent_tabs(self):
# Figure out the order of persistent tabs to save in settings
persistent_tabs = []
for index in range(self.count()):
tab = self.widget(index)
if tab.settings.get("persistent", "enabled"):
persistent_tabs.append(tab.settings.id)
# Only save if tabs have actually moved
if persistent_tabs != self.common.settings.get("persistent_tabs"):
self.common.settings.set("persistent_tabs", persistent_tabs)
self.common.settings.save()
def close_tab(self, index):
self.common.log("TabWidget", "close_tab", f"{index}")
tab = self.widget(index)
if tab.close_tab():
# If the tab is persistent, delete the settings file from disk
if tab.settings.get("persistent", "enabled"):
tab.settings.delete()
# Remove the tab
self.removeTab(index)
del self.tabs[tab.tab_id]
# If the last tab is closed, open a new one
if self.count() == 0:
self.new_tab_clicked()
self.save_persistent_tabs()
def are_tabs_active(self):
"""
See if there are active servers in any open tabs
"""
for tab_id in self.tabs:
mode = self.tabs[tab_id].get_mode()
if mode:
if mode.server_status.status != mode.server_status.STATUS_STOPPED:
return True
return False
def paintEvent(self, event):
super(TabWidget, self).paintEvent(event)
# Save the order of persistent tabs whenever a new tab is switched to -- ideally we would
# do this whenever tabs gets moved, but paintEvent is the only event that seems to get triggered
# when this happens
self.save_persistent_tabs()
def resizeEvent(self, event):
# Make sure to move new tab button on each resize
super(TabWidget, self).resizeEvent(event)
self.move_new_tab_button()
class TabBar(QtWidgets.QTabBar):
"""
A custom tab bar
"""
move_new_tab_button = QtCore.pyqtSignal()
def __init__(self):
super(TabBar, self).__init__()
def tabLayoutChange(self):
self.move_new_tab_button.emit()

View File

@ -20,7 +20,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import time
from PyQt5 import QtCore
from onionshare.onion import *
from onionshare import strings
from onionshare.onion import (
TorTooOld,
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
TorErrorSocketFile,
TorErrorMissingPassword,
TorErrorUnreadableCookieFile,
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
)
class OnionThread(QtCore.QThread):
@ -47,29 +59,28 @@ class OnionThread(QtCore.QThread):
self.mode.web.generate_static_url_path()
# Choose port and password early, because we need them to exist in advance for scheduled shares
self.mode.app.stay_open = not self.mode.common.settings.get(
"close_after_first_download"
)
if not self.mode.app.port:
self.mode.app.choose_port()
if not self.mode.common.settings.get("public_mode"):
if not self.mode.settings.get("general", "public"):
if not self.mode.web.password:
self.mode.web.generate_password(
self.mode.common.settings.get("password")
self.mode.settings.get("onion", "password")
)
try:
if self.mode.obtain_onion_early:
self.mode.app.start_onion_service(
await_publication=False, save_scheduled_key=True
self.mode.settings, await_publication=False
)
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
time.sleep(0.2)
self.success_early.emit()
# Unregister the onion so we can use it in the next OnionThread
self.mode.app.onion.cleanup(False)
self.mode.app.stop_onion_service(self.mode.settings)
else:
self.mode.app.start_onion_service(await_publication=True)
self.mode.app.start_onion_service(
self.mode.settings, await_publication=True
)
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
time.sleep(0.2)
# start onionshare http service in new thread
@ -109,12 +120,7 @@ class WebThread(QtCore.QThread):
def run(self):
self.mode.common.log("WebThread", "run")
self.mode.web.start(
self.mode.app.port,
self.mode.app.stay_open,
self.mode.common.settings.get("public_mode"),
self.mode.web.password,
)
self.mode.web.start(self.mode.app.port)
self.success.emit()

View File

@ -32,7 +32,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
open_settings = QtCore.pyqtSignal()
def __init__(self, common, qtapp, onion, custom_settings=False):
def __init__(self, common, custom_settings=False):
super(TorConnectionDialog, self).__init__(None)
self.common = common
@ -44,9 +44,6 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
self.common.log("TorConnectionDialog", "__init__")
self.qtapp = qtapp
self.onion = onion
self.setWindowTitle("OnionShare")
self.setWindowIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
@ -68,7 +65,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
def start(self):
self.common.log("TorConnectionDialog", "start")
t = TorConnectionThread(self.common, self.settings, self, self.onion)
t = TorConnectionThread(self.common, self.settings, self)
t.tor_status_update.connect(self._tor_status_update)
t.connected_to_tor.connect(self._connected_to_tor)
t.canceled_connecting_to_tor.connect(self._canceled_connecting_to_tor)
@ -81,7 +78,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
self.active = True
while self.active:
time.sleep(0.1)
self.qtapp.processEvents()
self.common.gui.qtapp.processEvents()
def _tor_status_update(self, progress, summary):
self.setValue(int(progress))
@ -99,7 +96,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
def _canceled_connecting_to_tor(self):
self.common.log("TorConnectionDialog", "_canceled_connecting_to_tor")
self.active = False
self.onion.cleanup()
self.common.gui.onion.cleanup()
# Cancel connecting to Tor
QtCore.QTimer.singleShot(1, self.cancel)
@ -131,7 +128,7 @@ class TorConnectionThread(QtCore.QThread):
canceled_connecting_to_tor = QtCore.pyqtSignal()
error_connecting_to_tor = QtCore.pyqtSignal(str)
def __init__(self, common, settings, dialog, onion):
def __init__(self, common, settings, dialog):
super(TorConnectionThread, self).__init__()
self.common = common
@ -141,15 +138,14 @@ class TorConnectionThread(QtCore.QThread):
self.settings = settings
self.dialog = dialog
self.onion = onion
def run(self):
self.common.log("TorConnectionThread", "run")
# Connect to the Onion
try:
self.onion.connect(self.settings, False, self._tor_status_update)
if self.onion.connected_to_tor:
self.common.gui.onion.connect(self.settings, False, self._tor_status_update)
if self.common.gui.onion.connected_to_tor:
self.connected_to_tor.emit()
else:
self.canceled_connecting_to_tor.emit()

View File

@ -61,19 +61,18 @@ class UpdateChecker(QtCore.QObject):
update_error = QtCore.pyqtSignal()
update_invalid_version = QtCore.pyqtSignal(str)
def __init__(self, common, onion, config=False):
def __init__(self, common, onion):
super(UpdateChecker, self).__init__()
self.common = common
self.common.log("UpdateChecker", "__init__")
self.onion = onion
self.config = config
def check(self, force=False, config=False):
def check(self, force=False):
self.common.log("UpdateChecker", "check", f"force={force}")
# Load the settings
settings = Settings(self.common, config)
settings = Settings(self.common)
settings.load()
# If force=True, then definitely check
@ -188,27 +187,26 @@ class UpdateThread(QtCore.QThread):
update_error = QtCore.pyqtSignal()
update_invalid_version = QtCore.pyqtSignal(str)
def __init__(self, common, onion, config=False, force=False):
def __init__(self, common, onion, force=False):
super(UpdateThread, self).__init__()
self.common = common
self.common.log("UpdateThread", "__init__")
self.onion = onion
self.config = config
self.force = force
def run(self):
self.common.log("UpdateThread", "run")
u = UpdateChecker(self.common, self.onion, self.config)
u = UpdateChecker(self.common, self.onion)
u.update_available.connect(self._update_available)
u.update_not_available.connect(self._update_not_available)
u.update_error.connect(self._update_error)
u.update_invalid_version.connect(self._update_invalid_version)
try:
u.check(config=self.config, force=self.force)
u.check(force=self.force)
except Exception as e:
# If update check fails, silently ignore
self.common.log("UpdateThread", "run", str(e))

View File

@ -79,3 +79,14 @@ class AddFileDialog(QtWidgets.QFileDialog):
def accept(self):
self.common.log("AddFileDialog", "accept")
QtWidgets.QDialog.accept(self)
class MinimumWidthWidget(QtWidgets.QWidget):
"""
An empty widget with a minimum width, just to force layouts to behave
"""
def __init__(self, width):
super(MinimumWidthWidget, self).__init__()
self.setMinimumWidth(width)

703
poetry.lock generated Normal file
View File

@ -0,0 +1,703 @@
[[package]]
category = "main"
description = "Python graph (network) package"
name = "altgraph"
optional = false
python-versions = "*"
version = "0.17"
[[package]]
category = "dev"
description = "Atomic file writes."
name = "atomicwrites"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.3.0"
[[package]]
category = "dev"
description = "Classes Without Boilerplate"
name = "attrs"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0"
[package.extras]
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
docs = ["sphinx", "zope.interface"]
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
optional = false
python-versions = "*"
version = "2019.11.28"
[[package]]
category = "main"
description = "Universal encoding detector for Python 2 and 3"
name = "chardet"
optional = false
python-versions = "*"
version = "3.0.4"
[[package]]
category = "main"
description = "Composable command line interface toolkit"
name = "click"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.1.1"
[[package]]
category = "dev"
description = "Cross-platform colored terminal text."
marker = "sys_platform == \"win32\""
name = "colorama"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.3"
[[package]]
category = "dev"
description = "Python 2.7 backport of the \"dis\" module from Python 3.5+"
marker = "sys_platform == \"darwin\""
name = "dis3"
optional = false
python-versions = "*"
version = "0.1.3"
[[package]]
category = "main"
description = "A simple framework for building complex web applications."
name = "flask"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.1.1"
[package.dependencies]
Jinja2 = ">=2.10.1"
Werkzeug = ">=0.15"
click = ">=5.1"
itsdangerous = ">=0.24"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
dotenv = ["python-dotenv"]
[[package]]
category = "main"
description = "Basic and Digest HTTP authentication for Flask routes"
name = "flask-httpauth"
optional = false
python-versions = "*"
version = "3.3.0"
[package.dependencies]
Flask = "*"
[[package]]
category = "main"
description = "Clean single-source support for Python 3 and 2"
name = "future"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.18.2"
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.9"
[[package]]
category = "dev"
description = "Read metadata from Python packages"
marker = "python_version < \"3.8\""
name = "importlib-metadata"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
version = "1.5.0"
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "rst.linker"]
testing = ["packaging", "importlib-resources"]
[[package]]
category = "main"
description = "Various helpers to pass data to untrusted environments and back."
name = "itsdangerous"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.0"
[[package]]
category = "main"
description = "A very fast and expressive template engine."
name = "jinja2"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.11.1"
[package.dependencies]
MarkupSafe = ">=0.23"
[package.extras]
i18n = ["Babel (>=0.8)"]
[[package]]
category = "main"
description = "Mach-O header analysis and editing"
name = "macholib"
optional = false
python-versions = "*"
version = "1.14"
[package.dependencies]
altgraph = ">=0.15"
[[package]]
category = "main"
description = "Safely add untrusted strings to HTML/XML markup."
name = "markupsafe"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.1.1"
[[package]]
category = "dev"
description = "More routines for operating on iterables, beyond itertools"
name = "more-itertools"
optional = false
python-versions = ">=3.5"
version = "8.2.0"
[[package]]
category = "dev"
description = "Core utilities for Python packages"
name = "packaging"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.3"
[package.dependencies]
pyparsing = ">=2.0.2"
six = "*"
[[package]]
category = "main"
description = "File system general utilities"
name = "pathtools"
optional = false
python-versions = "*"
version = "0.1.2"
[[package]]
category = "main"
description = "Python PE parsing module"
name = "pefile"
optional = false
python-versions = "*"
version = "2019.4.18"
[package.dependencies]
future = "*"
[[package]]
category = "dev"
description = "plugin and hook calling mechanisms for python"
name = "pluggy"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.13.1"
[package.dependencies]
[package.dependencies.importlib-metadata]
python = "<3.8"
version = ">=0.12"
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
category = "main"
description = "Cross-platform lib for process and system monitoring in Python."
name = "psutil"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "5.7.0"
[package.extras]
enum = ["enum34"]
[[package]]
category = "dev"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.8.1"
[[package]]
category = "main"
description = "Cryptographic library for Python"
name = "pycryptodome"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.9.7"
[[package]]
category = "dev"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
marker = "sys_platform == \"darwin\""
name = "pyinstaller"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "3.6"
[package.dependencies]
altgraph = "*"
dis3 = "*"
setuptools = "*"
[[package]]
category = "dev"
description = "Python parsing module"
name = "pyparsing"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "2.4.6"
[[package]]
category = "main"
description = "Python bindings for the Qt cross platform application toolkit"
name = "pyqt5"
optional = false
python-versions = ">=3.5"
version = "5.14.0"
[package.dependencies]
PyQt5-sip = ">=12.7,<13"
[[package]]
category = "main"
description = "The sip module support for PyQt5"
name = "pyqt5-sip"
optional = false
python-versions = ">=3.5"
version = "12.7.1"
[[package]]
category = "main"
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
name = "pysocks"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.7.1"
[[package]]
category = "dev"
description = "pytest: simple powerful testing with Python"
name = "pytest"
optional = false
python-versions = ">=3.5"
version = "5.4.1"
[package.dependencies]
atomicwrites = ">=1.0"
attrs = ">=17.4.0"
colorama = "*"
more-itertools = ">=4.0.0"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.5.0"
wcwidth = "*"
[package.dependencies.importlib-metadata]
python = "<3.8"
version = ">=0.12"
[package.extras]
checkqa-mypy = ["mypy (v0.761)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
category = "dev"
description = "py.test plugin that activates the fault handler module for tests (dummy package)"
name = "pytest-faulthandler"
optional = false
python-versions = "*"
version = "2.0.1"
[package.dependencies]
pytest = ">=5.0"
[[package]]
category = "dev"
description = "pytest support for PyQt and PySide applications"
name = "pytest-qt"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.3.0"
[package.dependencies]
pytest = ">=3.0.0"
[package.extras]
dev = ["pre-commit", "tox"]
doc = ["sphinx", "sphinx-rtd-theme"]
[[package]]
category = "main"
description = "Python HTTP for Humans."
name = "requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.23.0"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<4"
idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[[package]]
category = "dev"
description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.14.0"
[[package]]
category = "main"
description = "Stem is a Python controller library that allows applications to interact with Tor (https://www.torproject.org/)."
name = "stem"
optional = false
python-versions = "*"
version = "1.8.0"
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.25.8"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[[package]]
category = "main"
description = "Filesystem events monitoring"
name = "watchdog"
optional = false
python-versions = "*"
version = "0.10.2"
[package.dependencies]
pathtools = ">=0.1.1"
[package.extras]
watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"]
[[package]]
category = "dev"
description = "Measures number of Terminal column cells of wide-character codes"
name = "wcwidth"
optional = false
python-versions = "*"
version = "0.1.8"
[[package]]
category = "main"
description = "The comprehensive WSGI web application library."
name = "werkzeug"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.0.0"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
watchdog = ["watchdog"]
[[package]]
category = "dev"
description = "Backport of pathlib-compatible object wrapper for zip files"
marker = "python_version < \"3.8\""
name = "zipp"
optional = false
python-versions = ">=3.6"
version = "3.1.0"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[metadata]
content-hash = "41d68ea93701fdaa1aa56159195db7a65863e3b34cc7305ef4a3f5d02f2bdf13"
python-versions = "^3.7"
[metadata.files]
altgraph = [
{file = "altgraph-0.17-py2.py3-none-any.whl", hash = "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"},
{file = "altgraph-0.17.tar.gz", hash = "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa"},
]
atomicwrites = [
{file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"},
{file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"},
]
attrs = [
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
]
certifi = [
{file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
{file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"},
]
chardet = [
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
]
click = [
{file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
{file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
]
colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
dis3 = [
{file = "dis3-0.1.3-py2-none-any.whl", hash = "sha256:61f7720dd0d8749d23fda3d7227ce74d73da11c2fade993a67ab2f9852451b14"},
{file = "dis3-0.1.3-py3-none-any.whl", hash = "sha256:30b6412d33d738663e8ded781b138f4b01116437f0872aa56aa3adba6aeff218"},
{file = "dis3-0.1.3.tar.gz", hash = "sha256:9259b881fc1df02ed12ac25f82d4a85b44241854330b1a651e40e0c675cb2d1e"},
]
flask = [
{file = "Flask-1.1.1-py2.py3-none-any.whl", hash = "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"},
{file = "Flask-1.1.1.tar.gz", hash = "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52"},
]
flask-httpauth = [
{file = "Flask-HTTPAuth-3.3.0.tar.gz", hash = "sha256:6ef8b761332e780f9ff74d5f9056c2616f52babc1998b01d9f361a1e439e61b9"},
{file = "Flask_HTTPAuth-3.3.0-py2.py3-none-any.whl", hash = "sha256:0149953720489407e51ec24bc2f86273597b7973d71cd51f9443bd0e2a89bd72"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
idna = [
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
]
importlib-metadata = [
{file = "importlib_metadata-1.5.0-py2.py3-none-any.whl", hash = "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"},
{file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"},
]
itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
]
jinja2 = [
{file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
{file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"},
]
macholib = [
{file = "macholib-1.14-py2.py3-none-any.whl", hash = "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"},
{file = "macholib-1.14.tar.gz", hash = "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432"},
]
markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
more-itertools = [
{file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"},
{file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"},
]
packaging = [
{file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
{file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
]
pathtools = [
{file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"},
]
pefile = [
{file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
psutil = [
{file = "psutil-5.7.0-cp27-none-win32.whl", hash = "sha256:298af2f14b635c3c7118fd9183843f4e73e681bb6f01e12284d4d70d48a60953"},
{file = "psutil-5.7.0-cp27-none-win_amd64.whl", hash = "sha256:75e22717d4dbc7ca529ec5063000b2b294fc9a367f9c9ede1f65846c7955fd38"},
{file = "psutil-5.7.0-cp35-cp35m-win32.whl", hash = "sha256:f344ca230dd8e8d5eee16827596f1c22ec0876127c28e800d7ae20ed44c4b310"},
{file = "psutil-5.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e2d0c5b07c6fe5a87fa27b7855017edb0d52ee73b71e6ee368fae268605cc3f5"},
{file = "psutil-5.7.0-cp36-cp36m-win32.whl", hash = "sha256:a02f4ac50d4a23253b68233b07e7cdb567bd025b982d5cf0ee78296990c22d9e"},
{file = "psutil-5.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1413f4158eb50e110777c4f15d7c759521703bd6beb58926f1d562da40180058"},
{file = "psutil-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:d008ddc00c6906ec80040d26dc2d3e3962109e40ad07fd8a12d0284ce5e0e4f8"},
{file = "psutil-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f35ab66c6c7a9ce82ba44b1e9b1050be2a80cd4dcc3352cc108656b115c74f"},
{file = "psutil-5.7.0-cp38-cp38-win32.whl", hash = "sha256:60b86f327c198561f101a92be1995f9ae0399736b6eced8f24af41ec64fb88d4"},
{file = "psutil-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d84029b190c8a66a946e28b4d3934d2ca1528ec94764b180f7d6ea57b0e75e26"},
{file = "psutil-5.7.0.tar.gz", hash = "sha256:685ec16ca14d079455892f25bd124df26ff9137664af445563c1bd36629b5e0e"},
]
py = [
{file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
{file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
]
pycryptodome = [
{file = "pycryptodome-3.9.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:0e10f352ccbbcb5bb2dc4ecaf106564e65702a717d72ab260f9ac4c19753cfc2"},
{file = "pycryptodome-3.9.7-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9c739b7795ccf2ef1fdad8d44e539a39ad300ee6786e804ea7f0c6a786eb5343"},
{file = "pycryptodome-3.9.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9977086e0f93adb326379897437373871b80501e1d176fec63c7f46fb300c862"},
{file = "pycryptodome-3.9.7-cp27-cp27m-win32.whl", hash = "sha256:83295a3fb5cf50c48631eb5b440cb5e9832d8c14d81d1d45f4497b67a9987de8"},
{file = "pycryptodome-3.9.7-cp27-cp27m-win_amd64.whl", hash = "sha256:b1e332587b3b195542e77681389c296e1837ca01240399d88803a075447d3557"},
{file = "pycryptodome-3.9.7-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9378c309aec1f8cd8bad361ed0816a440151b97a2a3f6ffdaba1d1a1fb76873a"},
{file = "pycryptodome-3.9.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4f94368ce2d65873a87ad867eb3bf63f4ba81eb97a9ee66d38c2b71ce5a7439"},
{file = "pycryptodome-3.9.7-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:f655addaaaa9974108d4808f4150652589cada96074c87115c52e575bfcd87d5"},
{file = "pycryptodome-3.9.7-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:9a94fca11fdc161460bd8659c15b6adef45c1b20da86402256eaf3addfaab324"},
{file = "pycryptodome-3.9.7-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea83bcd9d6c03248ebd46e71ac313858e0afd5aa2fa81478c0e653242f3eb476"},
{file = "pycryptodome-3.9.7-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:07024fc364869eae8d6ac0d316e089956e6aeffe42dbdcf44fe1320d96becf7f"},
{file = "pycryptodome-3.9.7-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:426c188c83c10df71f053e04b4003b1437bae5cb37606440e498b00f160d71d0"},
{file = "pycryptodome-3.9.7-cp35-cp35m-win32.whl", hash = "sha256:d61b012baa8c2b659e9890011358455c0019a4108536b811602d2f638c40802a"},
{file = "pycryptodome-3.9.7-cp35-cp35m-win_amd64.whl", hash = "sha256:1f4752186298caf2e9ff5354f2e694d607ca7342aa313a62005235d46e28cf04"},
{file = "pycryptodome-3.9.7-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:767ad0fb5d23efc36a4d5c2fc608ac603f3de028909bcf59abc943e0d0bc5a36"},
{file = "pycryptodome-3.9.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2fbc472e0b567318fe2052281d5a8c0ae70099b446679815f655e9fbc18c3a65"},
{file = "pycryptodome-3.9.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9230fcb5d948c3fb40049bace4d33c5d254f8232c2c0bba05d2570aea3ba4520"},
{file = "pycryptodome-3.9.7-cp36-cp36m-win32.whl", hash = "sha256:8f06556a8f7ea7b1e42eff39726bb0dca1c251205debae64e6eebea3cd7b438a"},
{file = "pycryptodome-3.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:d6e1bc5c94873bec742afe2dfadce0d20445b18e75c47afc0c115b19e5dd38dd"},
{file = "pycryptodome-3.9.7-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:3ec3dc2f80f71fd0c955ce48b81bfaf8914c6f63a41a738f28885a1c4892968a"},
{file = "pycryptodome-3.9.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cff31f5a8977534f255f729d5d2467526f2b10563a30bbdade92223e0bf264bd"},
{file = "pycryptodome-3.9.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ed5761b37615a1f222c5345bbf45272ae2cf8c7dff88a4f53a1e9f977cbb6d95"},
{file = "pycryptodome-3.9.7-cp37-cp37m-win32.whl", hash = "sha256:f011cd0062e54658b7086a76f8cf0f4222812acc66e219e196ea2d0a8849d0ed"},
{file = "pycryptodome-3.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:626c0a1d4d83ec6303f970a17158114f75c3ba1736f7f2983f7b40a265861bd8"},
{file = "pycryptodome-3.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be56bde3312e022d9d1d6afa124556460ad5c844c2fc63642f6af723c098d35"},
{file = "pycryptodome-3.9.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c818dc1f3eace93ee50c2b6b5c2becf7c418fa5dd1ba6fc0ef7db279ea21d5e4"},
{file = "pycryptodome-3.9.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:09b6d6bcc01a4eb1a2b4deeff5aa602a108ec5aed8ac75ae554f97d1d7f0a5ad"},
{file = "pycryptodome-3.9.7-cp38-cp38-win32.whl", hash = "sha256:7ac729d9091ed5478af2b4a4f44f5335a98febbc008af619e4569a59fe503e40"},
{file = "pycryptodome-3.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:c109a26a21f21f695d369ff9b87f5d43e0d6c768d8384e10bc74142bed2e092e"},
{file = "pycryptodome-3.9.7.tar.gz", hash = "sha256:f1add21b6d179179b3c177c33d18a2186a09cc0d3af41ff5ed3f377360b869f2"},
]
pyinstaller = [
{file = "PyInstaller-3.6.tar.gz", hash = "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7"},
]
pyparsing = [
{file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"},
{file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"},
]
pyqt5 = [
{file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-abi3-macosx_10_6_intel.whl", hash = "sha256:895d4101f7f8c82bc728d7eb9da1c756955ce27a0c945eafe7f234dd03402853"},
{file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:a757ba71c51f428b52ba404e781e2f19b4436b2c31298b8313339d5817781b65"},
{file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:cc3529c0f7cbbe7491073458d5d15e7518ce544ad8c627f485e5db8a27fcaf61"},
{file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:0dcc128b72f83cce0fc7926c83f05a9b74b652b5eb31a4ab71693ac8829e73c8"},
{file = "PyQt5-5.14.0.tar.gz", hash = "sha256:0145a6b7de15756366decb736c349a0cb510d706c83fda5b8cd9e0557bc1da72"},
]
pyqt5-sip = [
{file = "PyQt5_sip-12.7.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:f314f31f5fd39b06897f013f425137e511d45967150eb4e424a363d8138521c6"},
{file = "PyQt5_sip-12.7.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b42021229424aa44e99b3b49520b799fd64ff6ae8b53f79f903bbd85719a28e4"},
{file = "PyQt5_sip-12.7.1-cp35-cp35m-win32.whl", hash = "sha256:6b4860c4305980db509415d0af802f111d15f92016c9422eb753bc8883463456"},
{file = "PyQt5_sip-12.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:d46b0f8effc554de52a1466b1bd80e5cb4bce635a75ac4e7ad6247c965dec5b9"},
{file = "PyQt5_sip-12.7.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3f665376d9e52faa9855c3736a66ce6d825f85c86d7774d3c393f09da23f4f86"},
{file = "PyQt5_sip-12.7.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1115728644bbadcde5fc8a16e7918bd31915a42dd6fb36b10d4afb78c582753e"},
{file = "PyQt5_sip-12.7.1-cp36-cp36m-win32.whl", hash = "sha256:cbeeae6b45234a1654657f79943f8bccd3d14b4e7496746c62cf6fbce69442c7"},
{file = "PyQt5_sip-12.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8da842d3d7bf8931d1093105fb92702276b6dbb7e801abbaaa869405d616171a"},
{file = "PyQt5_sip-12.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f4289276d355b6521dc2cc956189315da6f13adfb6bbab8f25ebd15e3bce1d4"},
{file = "PyQt5_sip-12.7.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c1e730a9eb2ec3869ed5d81b0f99f6e2460fb4d77750444c0ec183b771d798f7"},
{file = "PyQt5_sip-12.7.1-cp37-cp37m-win32.whl", hash = "sha256:b5b4906445fe980aee76f20400116b6904bf5f30d0767489c13370e42a764020"},
{file = "PyQt5_sip-12.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7ffa39763097f64de129cf5cc770a651c3f65d2466b4fe05bef2bd2efbaa38e6"},
{file = "PyQt5_sip-12.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:288c6dc18a8d6a20981c07b715b5695d9b66880778565f3792bc6e38f14f20fb"},
{file = "PyQt5_sip-12.7.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ee1a12f09d5af2304273bfd2f6b43835c1467d5ed501a6c95f5405637fa7750a"},
{file = "PyQt5_sip-12.7.1-cp38-cp38-win32.whl", hash = "sha256:8a18e6f45d482ddfe381789979d09ee13aa6450caa3a0476503891bccb3ac709"},
{file = "PyQt5_sip-12.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:e28c3abc9b62a1b7e796891648b9f14f8167b31c8e7990fae79654777252bb4d"},
{file = "PyQt5_sip-12.7.1.tar.gz", hash = "sha256:e6078f5ee7d31c102910d0c277a110e1c2a20a3fc88cd017a39e170120586d3f"},
]
pysocks = [
{file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
]
pytest = [
{file = "pytest-5.4.1-py3-none-any.whl", hash = "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172"},
{file = "pytest-5.4.1.tar.gz", hash = "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"},
]
pytest-faulthandler = [
{file = "pytest-faulthandler-2.0.1.tar.gz", hash = "sha256:ed72bbce87ac344da81eb7d882196a457d4a1026a3da4a57154dacd85cd71ae5"},
{file = "pytest_faulthandler-2.0.1-py2.py3-none-any.whl", hash = "sha256:236430ba962fd1c910d670922be55fe5b25ea9bc3fc6561a0cafbb8759e7504d"},
]
pytest-qt = [
{file = "pytest-qt-3.3.0.tar.gz", hash = "sha256:714b0bf86c5313413f2d300ac613515db3a1aef595051ab8ba2ffe619dbe8925"},
{file = "pytest_qt-3.3.0-py2.py3-none-any.whl", hash = "sha256:5f8928288f50489d83f5d38caf2d7d9fcd6e7cf769947902caa4661dc7c851e3"},
]
requests = [
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
]
six = [
{file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
{file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
]
stem = [
{file = "stem-1.8.0.tar.gz", hash = "sha256:a0b48ea6224e95f22aa34c0bc3415f0eb4667ddeae3dfb5e32a6920c185568c2"},
]
urllib3 = [
{file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"},
{file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"},
]
watchdog = [
{file = "watchdog-0.10.2.tar.gz", hash = "sha256:c560efb643faed5ef28784b2245cf8874f939569717a4a12826a173ac644456b"},
]
wcwidth = [
{file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"},
{file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"},
]
werkzeug = [
{file = "Werkzeug-1.0.0-py2.py3-none-any.whl", hash = "sha256:6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16"},
{file = "Werkzeug-1.0.0.tar.gz", hash = "sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096"},
]
zipp = [
{file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
{file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"},
]

50
pyproject.toml Normal file
View File

@ -0,0 +1,50 @@
[tool.poetry]
name = "onionshare"
version = "2.2"
description = "OnionShare lets you securely and anonymously send and receive files. It works by starting a web server, making it accessible as a Tor onion service, and generating an unguessable web address so others can download files from you, or upload files to you. It does _not_ require setting up a separate server or using a third party file-sharing service."
authors = ["Micah Lee <micah@micahflee.com>"]
license = "GPLv3+"
[tool.poetry.dependencies]
python = "^3.7"
altgraph = "*"
certifi = "*"
chardet = "*"
Click = "*"
Flask = "*"
Flask-HTTPAuth = "*"
future = "*"
idna = "*"
itsdangerous = "*"
Jinja2 = "*"
macholib = "*"
MarkupSafe = "*"
pefile = "*"
pycryptodome = "*"
PyQt5 = "5.14"
PyQt5-sip = "*"
PySocks = "*"
requests = "*"
stem = "*"
urllib3 = "*"
Werkzeug = "*"
watchdog = "*"
psutil = "*"
[tool.poetry.dev-dependencies]
atomicwrites = "*"
attrs = "*"
more-itertools = "*"
pluggy = "*"
py = "*"
pytest = "*"
pytest-faulthandler = "*"
pytest-qt = "*"
six = "*"
urllib3 = "*"
pyinstaller = {version = "*", platform = "darwin"}
setuptools = {version = "*", platform = "windows"}
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

View File

@ -69,16 +69,16 @@ classifiers = [
"Environment :: Web Environment",
]
data_files = [
("share/applications", ["install/org.onionshare.OnionShare.desktop"]),
("share/icons/hicolor/scalable/apps", ["install/org.onionshare.OnionShare.svg"]),
("share/metainfo", ["install/org.onionshare.OnionShare.appdata.xml"]),
("share/applications", ["install/org.onionshare.OnionShare.desktop"],),
("share/icons/hicolor/scalable/apps", ["install/org.onionshare.OnionShare.svg"],),
("share/metainfo", ["install/org.onionshare.OnionShare.appdata.xml"],),
("share/onionshare", file_list("share")),
("share/onionshare/images", file_list("share/images")),
("share/onionshare/locale", file_list("share/locale")),
("share/onionshare/templates", file_list("share/templates")),
("share/onionshare/static/css", file_list("share/static/css")),
("share/onionshare/static/img", file_list("share/static/img")),
("share/onionshare/static/js", file_list("share/static/js")),
("share/onionshare/templates", file_list("share/templates"),),
("share/onionshare/static/css", file_list("share/static/css"),),
("share/onionshare/static/img", file_list("share/static/img"),),
("share/onionshare/static/js", file_list("share/static/js"),),
]
if not platform.system().endswith("BSD") and platform.system() != "DragonFly":
data_files.append(
@ -105,10 +105,11 @@ setup(
"onionshare",
"onionshare.web",
"onionshare_gui",
"onionshare_gui.mode",
"onionshare_gui.mode.share_mode",
"onionshare_gui.mode.receive_mode",
"onionshare_gui.mode.website_mode",
"onionshare_gui.tab",
"onionshare_gui.tab.mode",
"onionshare_gui.tab.mode.share_mode",
"onionshare_gui.tab.mode.receive_mode",
"onionshare_gui.tab.mode.website_mode",
],
include_package_data=True,
scripts=["install/scripts/onionshare", "install/scripts/onionshare-gui"],

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -30,11 +30,6 @@
"gui_copied_hidservauth": "HidServAuth line copied to clipboard",
"gui_waiting_to_start": "Scheduled to start in {}. Click to cancel.",
"gui_please_wait": "Starting… Click to cancel.",
"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_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?",
"gui_quit_warning_quit": "Quit",
"gui_quit_warning_dont_quit": "Cancel",
"error_rate_limit": "Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.",
"zip_progress_bar_format": "Compressing: %p%",
"error_stealth_not_supported": "To use client authorization, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.",
@ -93,7 +88,7 @@
"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": "Using the Tor version that comes with OnionShare does not work in developer mode on Windows or macOS.",
"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:\n{}",
"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_unknown": "There was an unknown error with Tor",
@ -175,9 +170,39 @@
"gui_website_mode_no_files": "No Website Shared Yet",
"gui_receive_mode_no_files": "No Files Received Yet",
"gui_receive_mode_autostop_timer_waiting": "Waiting to finish receiving",
"receive_mode_upload_starting": "Upload of total size {} is starting",
"days_first_letter": "d",
"hours_first_letter": "h",
"minutes_first_letter": "m",
"seconds_first_letter": "s"
}
"seconds_first_letter": "s",
"gui_new_tab": "New Tab",
"gui_new_tab_tooltip": "Open a new tab",
"gui_new_tab_share_button": "Share Files",
"gui_new_tab_share_description": "Choose files on your computer to send to someone else. The person or people who you want to send files to will need to use Tor Browser to download them from you.",
"gui_new_tab_receive_button": "Receive Files",
"gui_new_tab_receive_description": "Turn your computer into an online dropbox. People will be able to use Tor Browser to send files to your computer.",
"gui_new_tab_website_button": "Publish Website",
"gui_new_tab_website_description": "Host a static HTML onion website from your computer.",
"gui_close_tab_warning_title": "Are you sure?",
"gui_close_tab_warning_persistent_description": "This tab is persistent. If you close it you'll lose the onion address that it's using. Are you sure you want to close it?",
"gui_close_tab_warning_share_description": "You're in the process of sending files. Are you sure you want to close this tab?",
"gui_close_tab_warning_receive_description": "You're in the process of receiving files. Are you sure you want to close this tab?",
"gui_close_tab_warning_website_description": "You're actively hosting a website. Are you sure you want to close this tab?",
"gui_close_tab_warning_close": "Close",
"gui_close_tab_warning_cancel": "Cancel",
"gui_quit_warning_title": "Are you sure?",
"gui_quit_warning_description": "Sharing is active in some of your tabs. If you quit, all of your tabs will close. Are you sure you want to quit?",
"gui_quit_warning_quit": "Quit",
"gui_quit_warning_cancel": "Cancel",
"mode_settings_advanced_toggle_show": "Show advanced settings",
"mode_settings_advanced_toggle_hide": "Hide advanced settings",
"mode_settings_persistent_checkbox": "Save this tab, and automatically open it when I open OnionShare",
"mode_settings_public_checkbox": "Don't use a password",
"mode_settings_autostart_timer_checkbox": "Start onion service at scheduled time",
"mode_settings_autostop_timer_checkbox": "Stop onion service at scheduled time",
"mode_settings_legacy_checkbox": "Use a legacy address (v2 onion service, not recommended)",
"mode_settings_client_auth_checkbox": "Use client authorization",
"mode_settings_share_autostop_sharing_checkbox": "Stop sharing after files have been sent (uncheck to allow downloading individual files)",
"mode_settings_receive_data_dir_label": "Save files to",
"mode_settings_receive_data_dir_browse_button": "Browse",
"mode_settings_website_disable_csp_checkbox": "Disable Content Security Policy header (allows your website to use third-party resources)"
}

View File

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

View File

@ -1,383 +0,0 @@
import json
import os
import requests
import shutil
import base64
from PyQt5 import QtCore, QtTest
from onionshare import strings
from onionshare.common import Common
from onionshare.settings import Settings
from onionshare.onion import Onion
from onionshare.web import Web
from onionshare_gui import Application, OnionShare, OnionShareGui
from onionshare_gui.mode.share_mode import ShareMode
from onionshare_gui.mode.receive_mode import ReceiveMode
from onionshare_gui.mode.website_mode import WebsiteMode
class GuiBaseTest(object):
@staticmethod
def set_up(test_settings):
"""Create GUI with given settings"""
# Create our test file
testfile = open("/tmp/test.txt", "w")
testfile.write("onionshare")
testfile.close()
# Create a test dir and files
if not os.path.exists("/tmp/testdir"):
testdir = os.mkdir("/tmp/testdir")
testfile = open("/tmp/testdir/test", "w")
testfile.write("onionshare")
testfile.close()
common = Common()
common.settings = Settings(common)
common.define_css()
strings.load_strings(common)
# Get all of the settings in test_settings
test_settings["data_dir"] = "/tmp/OnionShare"
for key, val in common.settings.default_settings.items():
if key not in test_settings:
test_settings[key] = val
# Start the Onion
testonion = Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
open("/tmp/settings.json", "w").write(json.dumps(test_settings))
gui = OnionShareGui(
common,
testonion,
qtapp,
app,
["/tmp/test.txt", "/tmp/testdir"],
"/tmp/settings.json",
True,
)
return gui
@staticmethod
def tear_down():
"""Clean up after tests"""
try:
os.remove("/tmp/test.txt")
os.remove("/tmp/settings.json")
os.remove("/tmp/large_file")
os.remove("/tmp/download.zip")
os.remove("/tmp/webpage")
shutil.rmtree("/tmp/testdir")
shutil.rmtree("/tmp/OnionShare")
except:
pass
def gui_loaded(self):
"""Test that the GUI actually is shown"""
self.assertTrue(self.gui.show)
def windowTitle_seen(self):
"""Test that the window title is OnionShare"""
self.assertEqual(self.gui.windowTitle(), "OnionShare")
def settings_button_is_visible(self):
"""Test that the settings button is visible"""
self.assertTrue(self.gui.settings_button.isVisible())
def settings_button_is_hidden(self):
"""Test that the settings button is hidden when the server starts"""
self.assertFalse(self.gui.settings_button.isVisible())
def server_status_bar_is_visible(self):
"""Test that the status bar is visible"""
self.assertTrue(self.gui.status_bar.isVisible())
def 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)
if type(mode) == WebsiteMode:
QtTest.QTest.mouseClick(self.gui.website_mode_button, QtCore.Qt.LeftButton)
self.assertTrue(self.gui.mode, self.gui.MODE_WEBSITE)
def 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 history_indicator(self, mode, public_mode, indicator_count="1"):
"""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")}
url = f"http://127.0.0.1:{self.gui.app.port}/upload"
if public_mode:
r = requests.post(url, files=files)
else:
r = requests.post(
url,
files=files,
auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password),
)
QtTest.QTest.qWait(2000)
if type(mode) == ShareMode:
# Download files
url = f"http://127.0.0.1:{self.gui.app.port}/download"
if public_mode:
r = requests.get(url)
else:
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password),
)
QtTest.QTest.qWait(2000)
# Indicator should be visible, have a value of "1"
self.assertTrue(mode.toggle_history.indicator_label.isVisible())
self.assertEqual(mode.toggle_history.indicator_label.text(), indicator_count)
# 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 history_is_not_visible(self, mode):
"""Test that the History section is not visible"""
self.assertFalse(mode.history.isVisible())
def history_is_visible(self, mode):
"""Test that the History section is visible"""
self.assertTrue(mode.history.isVisible())
def 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 toggle_indicator_is_reset(self, mode):
self.assertEqual(mode.toggle_history.indicator_count, 0)
self.assertFalse(mode.toggle_history.indicator_label.isVisible())
def server_status_indicator_says_starting(self, mode):
"""Test that the Server Status indicator shows we are Starting"""
self.assertEqual(
mode.server_status_label.text(),
strings._("gui_status_indicator_share_working"),
)
def server_status_indicator_says_scheduled(self, mode):
"""Test that the Server Status indicator shows we are Scheduled"""
self.assertEqual(
mode.server_status_label.text(),
strings._("gui_status_indicator_share_scheduled"),
)
def server_is_started(self, mode, startup_time=2000):
"""Test that the server has started"""
QtTest.QTest.qWait(startup_time)
# Should now be in SERVER_STARTED state
self.assertEqual(mode.server_status.status, 2)
def web_server_is_running(self):
"""Test that the web server has started"""
try:
r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/")
self.assertTrue(True)
except requests.exceptions.ConnectionError:
self.assertTrue(False)
def have_a_password(self, mode, public_mode):
"""Test that we have a valid password"""
if not public_mode:
self.assertRegex(mode.server_status.web.password, r"(\w+)-(\w+)")
else:
self.assertIsNone(mode.server_status.web.password, r"(\w+)-(\w+)")
def add_button_visible(self, mode):
"""Test that the add button should be visible"""
self.assertTrue(mode.server_status.file_selection.add_button.isVisible())
def url_description_shown(self, mode):
"""Test that the URL label is showing"""
self.assertTrue(mode.server_status.url_description.isVisible())
def have_copy_url_button(self, mode, public_mode):
"""Test that the Copy URL button is shown and that the clipboard is correct"""
self.assertTrue(mode.server_status.copy_url_button.isVisible())
QtTest.QTest.mouseClick(
mode.server_status.copy_url_button, QtCore.Qt.LeftButton
)
clipboard = self.gui.qtapp.clipboard()
if public_mode:
self.assertEqual(clipboard.text(), f"http://127.0.0.1:{self.gui.app.port}")
else:
self.assertEqual(
clipboard.text(),
f"http://onionshare:{mode.server_status.web.password}@127.0.0.1:{self.gui.app.port}",
)
def server_status_indicator_says_started(self, mode):
"""Test that the Server Status indicator shows we are started"""
if type(mode) == ReceiveMode:
self.assertEqual(
mode.server_status_label.text(),
strings._("gui_status_indicator_receive_started"),
)
if type(mode) == ShareMode:
self.assertEqual(
mode.server_status_label.text(),
strings._("gui_status_indicator_share_started"),
)
def web_page(self, mode, string, public_mode):
"""Test that the web page contains a string"""
url = f"http://127.0.0.1:{self.gui.app.port}/"
if public_mode:
r = requests.get(url)
else:
r = requests.get(
url, auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password)
)
self.assertTrue(string in r.text)
def 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 counter_incremented(self, mode, count):
"""Test that the counter has incremented"""
self.assertEqual(mode.history.completed_count, count)
def 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)
or (type(mode) == WebsiteMode)
):
QtTest.QTest.mouseClick(
mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.assertEqual(mode.server_status.status, 0)
def web_server_is_stopped(self):
"""Test that the web server also stopped"""
QtTest.QTest.qWait(2000)
try:
r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/")
self.assertTrue(False)
except requests.exceptions.ConnectionError:
self.assertTrue(True)
def server_status_indicator_says_closed(self, mode, stay_open):
"""Test that the Server Status indicator shows we closed"""
if type(mode) == ReceiveMode:
self.assertEqual(
self.gui.receive_mode.server_status_label.text(),
strings._("gui_status_indicator_receive_stopped"),
)
if type(mode) == ShareMode:
if stay_open:
self.assertEqual(
self.gui.share_mode.server_status_label.text(),
strings._("gui_status_indicator_share_stopped"),
)
else:
self.assertEqual(
self.gui.share_mode.server_status_label.text(),
strings._("closing_automatically"),
)
def clear_all_history_items(self, mode, count):
if count == 0:
QtTest.QTest.mouseClick(mode.history.clear_button, QtCore.Qt.LeftButton)
self.assertEquals(len(mode.history.item_list.items.keys()), count)
# Auto-stop timer tests
def set_timeout(self, mode, timeout):
"""Test that the timeout can be set"""
timer = QtCore.QDateTime.currentDateTime().addSecs(timeout)
mode.server_status.autostop_timer_widget.setDateTime(timer)
self.assertTrue(mode.server_status.autostop_timer_widget.dateTime(), timer)
def autostop_timer_widget_hidden(self, mode):
"""Test that the auto-stop timer widget is hidden when share has started"""
self.assertFalse(mode.server_status.autostop_timer_container.isVisible())
def 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)
# Auto-start timer tests
def set_autostart_timer(self, mode, timer):
"""Test that the timer can be set"""
schedule = QtCore.QDateTime.currentDateTime().addSecs(timer)
mode.server_status.autostart_timer_widget.setDateTime(schedule)
self.assertTrue(mode.server_status.autostart_timer_widget.dateTime(), schedule)
def autostart_timer_widget_hidden(self, mode):
"""Test that the auto-start timer widget is hidden when share has started"""
self.assertFalse(mode.server_status.autostart_timer_container.isVisible())
def scheduled_service_started(self, mode, wait):
"""Test that the server has timed out after the timer ran out"""
QtTest.QTest.qWait(wait)
# We should have started now
self.assertEqual(mode.server_status.status, 2)
def cancel_the_share(self, mode):
"""Test that we can cancel a share before it's started up """
self.server_working_on_start_button_pressed(mode)
self.server_status_indicator_says_scheduled(mode)
self.add_delete_buttons_hidden()
self.settings_button_is_hidden()
self.set_autostart_timer(mode, 10)
QtTest.QTest.mousePress(mode.server_status.server_button, QtCore.Qt.LeftButton)
QtTest.QTest.qWait(2000)
QtTest.QTest.mouseRelease(
mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.assertEqual(mode.server_status.status, 0)
self.server_is_stopped(mode, False)
self.web_server_is_stopped()
# Hack to close an Alert dialog that would otherwise block tests
def accept_dialog(self):
window = self.gui.qtapp.activeWindow()
if window:
window.close()
# 'Grouped' tests follow from here
def run_all_common_setup_tests(self):
self.gui_loaded()
self.windowTitle_seen()
self.settings_button_is_visible()
self.server_status_bar_is_visible()

View File

@ -1,169 +0,0 @@
import os
import requests
from datetime import datetime, timedelta
from PyQt5 import QtCore, QtTest
from .GuiBaseTest import GuiBaseTest
class GuiReceiveTest(GuiBaseTest):
def upload_file(
self,
public_mode,
file_to_upload,
expected_basename,
identical_files_at_once=False,
):
"""Test that we can upload the file"""
# Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused
QtTest.QTest.qWait(2000)
files = {"file[]": open(file_to_upload, "rb")}
url = f"http://127.0.0.1:{self.gui.app.port}/upload"
if public_mode:
r = requests.post(url, files=files)
if identical_files_at_once:
# Send a duplicate upload to test for collisions
r = requests.post(url, files=files)
else:
r = requests.post(
url,
files=files,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.receive_mode.web.password
),
)
if identical_files_at_once:
# Send a duplicate upload to test for collisions
r = requests.post(
url,
files=files,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.receive_mode.web.password
),
)
QtTest.QTest.qWait(2000)
# Make sure the file is within the last 10 seconds worth of fileames
exists = False
now = datetime.now()
for i in range(10):
date_dir = now.strftime("%Y-%m-%d")
if identical_files_at_once:
time_dir = now.strftime("%H.%M.%S-1")
else:
time_dir = now.strftime("%H.%M.%S")
receive_mode_dir = os.path.join(
self.gui.common.settings.get("data_dir"), date_dir, time_dir
)
expected_filename = os.path.join(receive_mode_dir, expected_basename)
if os.path.exists(expected_filename):
exists = True
break
now = now - timedelta(seconds=1)
self.assertTrue(exists)
def upload_file_should_fail(self, public_mode):
"""Test that we can't upload the file when permissions are wrong, and expected content is shown"""
files = {"file[]": open("/tmp/test.txt", "rb")}
url = f"http://127.0.0.1:{self.gui.app.port}/upload"
if public_mode:
r = requests.post(url, files=files)
else:
r = requests.post(
url,
files=files,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.receive_mode.web.password
),
)
QtCore.QTimer.singleShot(1000, self.accept_dialog)
self.assertTrue("Error uploading, please inform the OnionShare user" in r.text)
def upload_dir_permissions(self, mode=0o755):
"""Manipulate the permissions on the upload dir in between tests"""
os.chmod("/tmp/OnionShare", mode)
def try_without_auth_in_non_public_mode(self):
r = requests.post(f"http://127.0.0.1:{self.gui.app.port}/upload")
self.assertEqual(r.status_code, 401)
r = requests.get(f"http://127.0.0.1:{self.gui.app.port}/close")
self.assertEqual(r.status_code, 401)
# 'Grouped' tests follow from here
def run_all_receive_mode_setup_tests(self, public_mode):
"""Set up a share in Receive mode and start it"""
self.click_mode(self.gui.receive_mode)
self.history_is_not_visible(self.gui.receive_mode)
self.click_toggle_history(self.gui.receive_mode)
self.history_is_visible(self.gui.receive_mode)
self.server_working_on_start_button_pressed(self.gui.receive_mode)
self.server_status_indicator_says_starting(self.gui.receive_mode)
self.settings_button_is_hidden()
self.server_is_started(self.gui.receive_mode)
self.web_server_is_running()
self.have_a_password(self.gui.receive_mode, public_mode)
self.url_description_shown(self.gui.receive_mode)
self.have_copy_url_button(self.gui.receive_mode, public_mode)
self.server_status_indicator_says_started(self.gui.receive_mode)
self.web_page(
self.gui.receive_mode,
"Select the files you want to send, then click",
public_mode,
)
def run_all_receive_mode_tests(self, public_mode):
"""Upload files in receive mode and stop the share"""
self.run_all_receive_mode_setup_tests(public_mode)
if not public_mode:
self.try_without_auth_in_non_public_mode()
self.upload_file(public_mode, "/tmp/test.txt", "test.txt")
self.history_widgets_present(self.gui.receive_mode)
self.counter_incremented(self.gui.receive_mode, 1)
self.upload_file(public_mode, "/tmp/test.txt", "test.txt")
self.counter_incremented(self.gui.receive_mode, 2)
self.upload_file(public_mode, "/tmp/testdir/test", "test")
self.counter_incremented(self.gui.receive_mode, 3)
self.upload_file(public_mode, "/tmp/testdir/test", "test")
self.counter_incremented(self.gui.receive_mode, 4)
# Test uploading the same file twice at the same time, and make sure no collisions
self.upload_file(public_mode, "/tmp/test.txt", "test.txt", True)
self.counter_incremented(self.gui.receive_mode, 6)
self.history_indicator(self.gui.receive_mode, public_mode, "2")
self.server_is_stopped(self.gui.receive_mode, False)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.receive_mode, False)
self.server_working_on_start_button_pressed(self.gui.receive_mode)
self.server_is_started(self.gui.receive_mode)
self.history_indicator(self.gui.receive_mode, public_mode, "2")
def run_all_receive_mode_unwritable_dir_tests(self, public_mode):
"""Attempt to upload (unwritable) files in receive mode and stop the share"""
self.run_all_receive_mode_setup_tests(public_mode)
self.upload_dir_permissions(0o400)
self.upload_file_should_fail(public_mode)
self.server_is_stopped(self.gui.receive_mode, True)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.receive_mode, False)
self.upload_dir_permissions(0o755)
def run_all_receive_mode_timer_tests(self, public_mode):
"""Auto-stop timer tests in receive mode"""
self.run_all_receive_mode_setup_tests(public_mode)
self.set_timeout(self.gui.receive_mode, 5)
self.autostop_timer_widget_hidden(self.gui.receive_mode)
self.server_timed_out(self.gui.receive_mode, 15000)
self.web_server_is_stopped()
def run_all_clear_all_button_tests(self, public_mode):
"""Test the Clear All history button"""
self.run_all_receive_mode_setup_tests(public_mode)
self.upload_file(public_mode, "/tmp/test.txt", "test.txt")
self.history_widgets_present(self.gui.receive_mode)
self.clear_all_history_items(self.gui.receive_mode, 0)
self.upload_file(public_mode, "/tmp/test.txt", "test.txt")
self.clear_all_history_items(self.gui.receive_mode, 2)

View File

@ -1,328 +0,0 @@
import os
import requests
import socks
import zipfile
import tempfile
from PyQt5 import QtCore, QtTest
from .GuiBaseTest import GuiBaseTest
class GuiShareTest(GuiBaseTest):
# Persistence tests
def have_same_password(self, password):
"""Test that we have the same password"""
self.assertEqual(self.gui.share_mode.server_status.web.password, password)
# Share-specific tests
def file_selection_widget_has_files(self, num=2):
"""Test that the number of items in the list is as expected"""
self.assertEqual(
self.gui.share_mode.server_status.file_selection.get_num_files(), num
)
def deleting_all_files_hides_delete_button(self):
"""Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button"""
rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(
self.gui.share_mode.server_status.file_selection.file_list.item(0)
)
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, delete button should still be visible since we have one more file
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.file_selection.delete_button,
QtCore.Qt.LeftButton,
)
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(),
)
self.assertTrue(
self.gui.share_mode.server_status.file_selection.delete_button.isVisible()
)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.file_selection.delete_button,
QtCore.Qt.LeftButton,
)
# No more files, the delete button should be hidden
self.assertFalse(
self.gui.share_mode.server_status.file_selection.delete_button.isVisible()
)
def add_a_file_and_delete_using_its_delete_widget(self):
"""Test that we can also delete a file by clicking on its [X] widget"""
self.gui.share_mode.server_status.file_selection.file_list.add_file(
"/etc/hosts"
)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.file_selection.file_list.item(
0
).item_button,
QtCore.Qt.LeftButton,
)
self.file_selection_widget_has_files(0)
def file_selection_widget_read_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.file_selection_widget_has_files(2)
def add_large_file(self):
"""Add a large file to the share"""
size = 1024 * 1024 * 155
with open("/tmp/large_file", "wb") as fout:
fout.write(os.urandom(size))
self.gui.share_mode.server_status.file_selection.file_list.add_file(
"/tmp/large_file"
)
def add_delete_buttons_hidden(self):
"""Test that the add and delete buttons are hidden when the server starts"""
self.assertFalse(
self.gui.share_mode.server_status.file_selection.add_button.isVisible()
)
self.assertFalse(
self.gui.share_mode.server_status.file_selection.delete_button.isVisible()
)
def download_share(self, public_mode):
"""Test that we can download the share"""
url = f"http://127.0.0.1:{self.gui.app.port}/download"
if public_mode:
r = requests.get(url)
else:
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.share_mode.server_status.web.password
),
)
tmp_file = tempfile.NamedTemporaryFile()
with open(tmp_file.name, "wb") as f:
f.write(r.content)
zip = zipfile.ZipFile(tmp_file.name)
QtTest.QTest.qWait(2000)
self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8"))
def individual_file_is_viewable_or_not(self, public_mode, stay_open):
"""Test whether an individual file is viewable (when in stay_open mode) and that it isn't (when not in stay_open mode)"""
url = f"http://127.0.0.1:{self.gui.app.port}"
download_file_url = f"http://127.0.0.1:{self.gui.app.port}/test.txt"
if public_mode:
r = requests.get(url)
else:
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.share_mode.server_status.web.password
),
)
if stay_open:
self.assertTrue('a href="test.txt"' in r.text)
if public_mode:
r = requests.get(download_file_url)
else:
r = requests.get(
download_file_url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.share_mode.server_status.web.password
),
)
tmp_file = tempfile.NamedTemporaryFile()
with open(tmp_file.name, "wb") as f:
f.write(r.content)
with open(tmp_file.name, "r") as f:
self.assertEqual("onionshare", f.read())
else:
self.assertFalse('a href="/test.txt"' in r.text)
if public_mode:
r = requests.get(download_file_url)
else:
r = requests.get(
download_file_url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.share_mode.server_status.web.password
),
)
self.assertEqual(r.status_code, 404)
self.download_share(public_mode)
QtTest.QTest.qWait(2000)
def hit_401(self, public_mode):
"""Test that the server stops after too many 401s, or doesn't when in public_mode"""
url = f"http://127.0.0.1:{self.gui.app.port}/"
for _ in range(20):
password_guess = self.gui.common.build_password()
r = requests.get(
url, auth=requests.auth.HTTPBasicAuth("onionshare", password_guess)
)
# A nasty hack to avoid the Alert dialog that blocks the rest of the test
if not public_mode:
QtCore.QTimer.singleShot(1000, self.accept_dialog)
# In public mode, we should still be running (no rate-limiting)
if public_mode:
self.web_server_is_running()
# In non-public mode, we should be shut down (rate-limiting)
else:
self.web_server_is_stopped()
# 'Grouped' tests follow from here
def run_all_share_mode_setup_tests(self):
"""Tests in share mode prior to starting a share"""
self.click_mode(self.gui.share_mode)
self.file_selection_widget_has_files()
self.history_is_not_visible(self.gui.share_mode)
self.click_toggle_history(self.gui.share_mode)
self.history_is_visible(self.gui.share_mode)
self.deleting_all_files_hides_delete_button()
self.add_a_file_and_delete_using_its_delete_widget()
self.file_selection_widget_read_files()
def run_all_share_mode_started_tests(self, public_mode, startup_time=2000):
"""Tests in share mode after starting a share"""
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.server_status_indicator_says_starting(self.gui.share_mode)
self.add_delete_buttons_hidden()
self.settings_button_is_hidden()
self.server_is_started(self.gui.share_mode, startup_time)
self.web_server_is_running()
self.have_a_password(self.gui.share_mode, public_mode)
self.url_description_shown(self.gui.share_mode)
self.have_copy_url_button(self.gui.share_mode, public_mode)
self.server_status_indicator_says_started(self.gui.share_mode)
def run_all_share_mode_download_tests(self, public_mode, stay_open):
"""Tests in share mode after downloading a share"""
self.web_page(self.gui.share_mode, "Total size", public_mode)
self.download_share(public_mode)
self.history_widgets_present(self.gui.share_mode)
self.server_is_stopped(self.gui.share_mode, stay_open)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
self.add_button_visible(self.gui.share_mode)
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.toggle_indicator_is_reset(self.gui.share_mode)
self.server_is_started(self.gui.share_mode)
self.history_indicator(self.gui.share_mode, public_mode)
def run_all_share_mode_individual_file_download_tests(self, public_mode, stay_open):
"""Tests in share mode after downloading a share"""
self.web_page(self.gui.share_mode, "Total size", public_mode)
self.individual_file_is_viewable_or_not(public_mode, stay_open)
self.history_widgets_present(self.gui.share_mode)
self.server_is_stopped(self.gui.share_mode, stay_open)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
self.add_button_visible(self.gui.share_mode)
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.server_is_started(self.gui.share_mode)
self.history_indicator(self.gui.share_mode, public_mode)
def run_all_share_mode_tests(self, public_mode, stay_open):
"""End-to-end share tests"""
self.run_all_share_mode_setup_tests()
self.run_all_share_mode_started_tests(public_mode)
self.run_all_share_mode_download_tests(public_mode, stay_open)
def run_all_clear_all_button_tests(self, public_mode, stay_open):
"""Test the Clear All history button"""
self.run_all_share_mode_setup_tests()
self.run_all_share_mode_started_tests(public_mode)
self.individual_file_is_viewable_or_not(public_mode, stay_open)
self.history_widgets_present(self.gui.share_mode)
self.clear_all_history_items(self.gui.share_mode, 0)
self.individual_file_is_viewable_or_not(public_mode, stay_open)
self.clear_all_history_items(self.gui.share_mode, 2)
def run_all_share_mode_individual_file_tests(self, public_mode, stay_open):
"""Tests in share mode when viewing an individual file"""
self.run_all_share_mode_setup_tests()
self.run_all_share_mode_started_tests(public_mode)
self.run_all_share_mode_individual_file_download_tests(public_mode, stay_open)
def run_all_large_file_tests(self, public_mode, stay_open):
"""Same as above but with a larger file"""
self.run_all_share_mode_setup_tests()
self.add_large_file()
self.run_all_share_mode_started_tests(public_mode, startup_time=15000)
self.assertTrue(self.gui.share_mode.filesize_warning.isVisible())
self.server_is_stopped(self.gui.share_mode, stay_open)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
def run_all_share_mode_persistent_tests(self, public_mode, stay_open):
"""Same as end-to-end share tests but also test the password is the same on multiple shared"""
self.run_all_share_mode_setup_tests()
self.run_all_share_mode_started_tests(public_mode)
password = self.gui.share_mode.server_status.web.password
self.run_all_share_mode_download_tests(public_mode, stay_open)
self.have_same_password(password)
def run_all_share_mode_timer_tests(self, public_mode):
"""Auto-stop timer tests in share mode"""
self.run_all_share_mode_setup_tests()
self.set_timeout(self.gui.share_mode, 5)
self.run_all_share_mode_started_tests(public_mode)
self.autostop_timer_widget_hidden(self.gui.share_mode)
self.server_timed_out(self.gui.share_mode, 10000)
self.web_server_is_stopped()
def run_all_share_mode_autostart_timer_tests(self, public_mode):
"""Auto-start timer tests in share mode"""
self.run_all_share_mode_setup_tests()
self.set_autostart_timer(self.gui.share_mode, 5)
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.autostart_timer_widget_hidden(self.gui.share_mode)
self.server_status_indicator_says_scheduled(self.gui.share_mode)
self.web_server_is_stopped()
self.scheduled_service_started(self.gui.share_mode, 7000)
self.web_server_is_running()
def run_all_share_mode_autostop_autostart_mismatch_tests(self, public_mode):
"""Auto-stop timer tests in share mode"""
self.run_all_share_mode_setup_tests()
self.set_autostart_timer(self.gui.share_mode, 15)
self.set_timeout(self.gui.share_mode, 5)
QtCore.QTimer.singleShot(4000, self.accept_dialog)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.server_is_stopped(self.gui.share_mode, False)
def run_all_share_mode_unreadable_file_tests(self):
"""Attempt to share an unreadable file"""
self.run_all_share_mode_setup_tests()
QtCore.QTimer.singleShot(1000, self.accept_dialog)
self.gui.share_mode.server_status.file_selection.file_list.add_file(
"/tmp/nonexistent.txt"
)
self.file_selection_widget_has_files(2)

View File

@ -1,136 +0,0 @@
import json
import os
import requests
import socks
import zipfile
import tempfile
from PyQt5 import QtCore, QtTest
from onionshare import strings
from onionshare.common import Common
from onionshare.settings import Settings
from onionshare.onion import Onion
from onionshare.web import Web
from onionshare_gui import Application, OnionShare, OnionShareGui
from .GuiShareTest import GuiShareTest
class GuiWebsiteTest(GuiShareTest):
@staticmethod
def set_up(test_settings):
"""Create GUI with given settings"""
# Create our test file
testfile = open("/tmp/index.html", "w")
testfile.write(
"<html><body><p>This is a test website hosted by OnionShare</p></body></html>"
)
testfile.close()
common = Common()
common.settings = Settings(common)
common.define_css()
strings.load_strings(common)
# Get all of the settings in test_settings
test_settings["data_dir"] = "/tmp/OnionShare"
for key, val in common.settings.default_settings.items():
if key not in test_settings:
test_settings[key] = val
# Start the Onion
testonion = Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
open("/tmp/settings.json", "w").write(json.dumps(test_settings))
gui = OnionShareGui(
common,
testonion,
qtapp,
app,
["/tmp/index.html"],
"/tmp/settings.json",
True,
)
return gui
@staticmethod
def tear_down():
"""Clean up after tests"""
try:
os.remove("/tmp/index.html")
os.remove("/tmp/settings.json")
except:
pass
def view_website(self, public_mode):
"""Test that we can download the share"""
url = f"http://127.0.0.1:{self.gui.app.port}/"
if public_mode:
r = requests.get(url)
else:
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.website_mode.server_status.web.password
),
)
QtTest.QTest.qWait(2000)
self.assertTrue("This is a test website hosted by OnionShare" in r.text)
def check_csp_header(self, public_mode, csp_header_disabled):
"""Test that the CSP header is present when enabled or vice versa"""
url = f"http://127.0.0.1:{self.gui.app.port}/"
if public_mode:
r = requests.get(url)
else:
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.website_mode.server_status.web.password
),
)
QtTest.QTest.qWait(2000)
if csp_header_disabled:
self.assertFalse("Content-Security-Policy" in r.headers)
else:
self.assertTrue("Content-Security-Policy" in r.headers)
def run_all_website_mode_setup_tests(self):
"""Tests in website mode prior to starting a share"""
self.click_mode(self.gui.website_mode)
self.file_selection_widget_has_files(1)
self.history_is_not_visible(self.gui.website_mode)
self.click_toggle_history(self.gui.website_mode)
self.history_is_visible(self.gui.website_mode)
def run_all_website_mode_started_tests(self, public_mode, startup_time=2000):
"""Tests in website mode after starting a share"""
self.server_working_on_start_button_pressed(self.gui.website_mode)
self.server_status_indicator_says_starting(self.gui.website_mode)
self.add_delete_buttons_hidden()
self.settings_button_is_hidden()
self.server_is_started(self.gui.website_mode, startup_time)
self.web_server_is_running()
self.have_a_password(self.gui.website_mode, public_mode)
self.url_description_shown(self.gui.website_mode)
self.have_copy_url_button(self.gui.website_mode, public_mode)
self.server_status_indicator_says_started(self.gui.website_mode)
def run_all_website_mode_download_tests(self, public_mode):
"""Tests in website mode after viewing the site"""
self.run_all_website_mode_setup_tests()
self.run_all_website_mode_started_tests(public_mode, startup_time=2000)
self.view_website(public_mode)
self.check_csp_header(
public_mode, self.gui.common.settings.get("csp_header_disabled")
)
self.history_widgets_present(self.gui.website_mode)
self.server_is_stopped(self.gui.website_mode, False)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.website_mode, False)
self.add_button_visible(self.gui.website_mode)

View File

@ -1,333 +0,0 @@
import json
import os
import unittest
from PyQt5 import QtCore, QtTest
from onionshare import strings
from onionshare.common import Common
from onionshare.settings import Settings
from onionshare.onion import Onion
from onionshare_gui import Application, OnionShare
from onionshare_gui.settings_dialog import SettingsDialog
class OnionStub(object):
def __init__(self, is_authenticated, supports_v3_onions):
self._is_authenticated = is_authenticated
self.supports_v3_onions = supports_v3_onions
def is_authenticated(self):
return self._is_authenticated
class SettingsGuiBaseTest(object):
@staticmethod
def set_up():
"""Create the GUI"""
# Default settings for the settings GUI tests
test_settings = {
"no_bridges": False,
"tor_bridges_use_custom_bridges": "Bridge 1.2.3.4:56 EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\nBridge 5.6.7.8:910 EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\nBridge 11.12.13.14:1516 EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\n",
}
# Create our test file
testfile = open("/tmp/test.txt", "w")
testfile.write("onionshare")
testfile.close()
common = Common()
common.settings = Settings(common)
common.define_css()
strings.load_strings(common)
# Start the Onion
testonion = Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, True, 0)
for key, val in common.settings.default_settings.items():
if key not in test_settings:
test_settings[key] = val
open("/tmp/settings.json", "w").write(json.dumps(test_settings))
gui = SettingsDialog(common, testonion, qtapp, "/tmp/settings.json", True)
return gui
@staticmethod
def tear_down():
"""Clean up after tests"""
os.remove("/tmp/settings.json")
def run_settings_gui_tests(self):
self.gui.show()
# Window is shown
self.assertTrue(self.gui.isVisible())
self.assertEqual(self.gui.windowTitle(), strings._("gui_settings_window_title"))
# Check for updates button is hidden
self.assertFalse(self.gui.check_for_updates_button.isVisible())
# public mode is off
self.assertFalse(self.gui.public_mode_checkbox.isChecked())
# enable public mode
QtTest.QTest.mouseClick(
self.gui.public_mode_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.public_mode_checkbox.height() / 2),
)
self.assertTrue(self.gui.public_mode_checkbox.isChecked())
# autostop timer is off
self.assertFalse(self.gui.autostop_timer_checkbox.isChecked())
# enable autostop timer
QtTest.QTest.mouseClick(
self.gui.autostop_timer_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.autostop_timer_checkbox.height() / 2),
)
self.assertTrue(self.gui.autostop_timer_checkbox.isChecked())
# legacy mode checkbox and related widgets
if self.gui.onion.is_authenticated():
if self.gui.onion.supports_v3_onions:
# legacy mode is off
self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isChecked())
# persistence is still available, stealth is hidden and disabled
self.assertTrue(self.gui.save_private_key_widget.isVisible())
self.assertFalse(self.gui.save_private_key_checkbox.isChecked())
self.assertFalse(self.gui.use_stealth_widget.isVisible())
self.assertFalse(self.gui.stealth_checkbox.isChecked())
self.assertFalse(self.gui.hidservauth_copy_button.isVisible())
# enable legacy mode
QtTest.QTest.mouseClick(
self.gui.use_legacy_v2_onions_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.use_legacy_v2_onions_checkbox.height() / 2
),
)
self.assertTrue(self.gui.use_legacy_v2_onions_checkbox.isChecked())
self.assertTrue(self.gui.save_private_key_checkbox.isVisible())
self.assertTrue(self.gui.use_stealth_widget.isVisible())
# enable persistent mode
QtTest.QTest.mouseClick(
self.gui.save_private_key_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.save_private_key_checkbox.height() / 2
),
)
self.assertTrue(self.gui.save_private_key_checkbox.isChecked())
# enable stealth mode
QtTest.QTest.mouseClick(
self.gui.stealth_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.stealth_checkbox.height() / 2),
)
self.assertTrue(self.gui.stealth_checkbox.isChecked())
# now that stealth is enabled, we can't turn off legacy mode
self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isEnabled())
# disable stealth, persistence
QtTest.QTest.mouseClick(
self.gui.save_private_key_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.save_private_key_checkbox.height() / 2
),
)
QtTest.QTest.mouseClick(
self.gui.stealth_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.stealth_checkbox.height() / 2),
)
# legacy mode checkbox is enabled again
self.assertTrue(self.gui.use_legacy_v2_onions_checkbox.isEnabled())
# uncheck legacy mode
QtTest.QTest.mouseClick(
self.gui.use_legacy_v2_onions_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.use_legacy_v2_onions_checkbox.height() / 2
),
)
# legacy options hidden again
self.assertTrue(self.gui.save_private_key_widget.isVisible())
self.assertFalse(self.gui.use_stealth_widget.isVisible())
# re-enable legacy mode
QtTest.QTest.mouseClick(
self.gui.use_legacy_v2_onions_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.use_legacy_v2_onions_checkbox.height() / 2
),
)
else:
# legacy mode setting is hidden
self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isVisible())
# legacy options are showing
self.assertTrue(self.gui.save_private_key_widget.isVisible())
self.assertTrue(self.gui.use_stealth_widget.isVisible())
# enable them all again so that we can see the setting stick in settings.json
QtTest.QTest.mouseClick(
self.gui.save_private_key_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.save_private_key_checkbox.height() / 2),
)
QtTest.QTest.mouseClick(
self.gui.stealth_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.stealth_checkbox.height() / 2),
)
else:
# None of the onion settings should appear
self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isVisible())
self.assertFalse(self.gui.save_private_key_widget.isVisible())
self.assertFalse(self.gui.save_private_key_checkbox.isChecked())
self.assertFalse(self.gui.use_stealth_widget.isVisible())
self.assertFalse(self.gui.stealth_checkbox.isChecked())
self.assertFalse(self.gui.hidservauth_copy_button.isVisible())
# stay open toggled off, on
self.assertTrue(self.gui.close_after_first_download_checkbox.isChecked())
QtTest.QTest.mouseClick(
self.gui.close_after_first_download_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.close_after_first_download_checkbox.height() / 2
),
)
self.assertFalse(self.gui.close_after_first_download_checkbox.isChecked())
# receive mode
self.gui.data_dir_lineedit.setText("/tmp/OnionShareSettingsTest")
# bundled mode is enabled
self.assertTrue(self.gui.connection_type_bundled_radio.isEnabled())
self.assertTrue(self.gui.connection_type_bundled_radio.isChecked())
# bridge options are shown
self.assertTrue(self.gui.connection_type_bridges_radio_group.isVisible())
# bridges are set to custom
self.assertFalse(self.gui.tor_bridges_no_bridges_radio.isChecked())
self.assertTrue(self.gui.tor_bridges_use_custom_radio.isChecked())
# switch to obfs4
QtTest.QTest.mouseClick(
self.gui.tor_bridges_use_obfs4_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.tor_bridges_use_obfs4_radio.height() / 2),
)
self.assertTrue(self.gui.tor_bridges_use_obfs4_radio.isChecked())
# custom bridges are hidden
self.assertFalse(self.gui.tor_bridges_use_custom_textbox_options.isVisible())
# other modes are unchecked but enabled
self.assertTrue(self.gui.connection_type_automatic_radio.isEnabled())
self.assertTrue(self.gui.connection_type_control_port_radio.isEnabled())
self.assertTrue(self.gui.connection_type_socket_file_radio.isEnabled())
self.assertFalse(self.gui.connection_type_automatic_radio.isChecked())
self.assertFalse(self.gui.connection_type_control_port_radio.isChecked())
self.assertFalse(self.gui.connection_type_socket_file_radio.isChecked())
# enable automatic mode
QtTest.QTest.mouseClick(
self.gui.connection_type_automatic_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.connection_type_automatic_radio.height() / 2),
)
self.assertTrue(self.gui.connection_type_automatic_radio.isChecked())
# bundled is off
self.assertFalse(self.gui.connection_type_bundled_radio.isChecked())
# bridges are hidden
self.assertFalse(self.gui.connection_type_bridges_radio_group.isVisible())
# auth type is hidden in bundled or automatic mode
self.assertFalse(self.gui.authenticate_no_auth_radio.isVisible())
self.assertFalse(self.gui.authenticate_password_radio.isVisible())
# enable control port mode
QtTest.QTest.mouseClick(
self.gui.connection_type_control_port_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.connection_type_control_port_radio.height() / 2
),
)
self.assertTrue(self.gui.connection_type_control_port_radio.isChecked())
# automatic is off
self.assertFalse(self.gui.connection_type_automatic_radio.isChecked())
# auth options appear
self.assertTrue(self.gui.authenticate_no_auth_radio.isVisible())
self.assertTrue(self.gui.authenticate_password_radio.isVisible())
# enable socket mode
QtTest.QTest.mouseClick(
self.gui.connection_type_socket_file_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.connection_type_socket_file_radio.height() / 2
),
)
self.assertTrue(self.gui.connection_type_socket_file_radio.isChecked())
# control port is off
self.assertFalse(self.gui.connection_type_control_port_radio.isChecked())
# auth options are still present
self.assertTrue(self.gui.authenticate_no_auth_radio.isVisible())
self.assertTrue(self.gui.authenticate_password_radio.isVisible())
# re-enable bundled mode
QtTest.QTest.mouseClick(
self.gui.connection_type_bundled_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.connection_type_bundled_radio.height() / 2),
)
# go back to custom bridges
QtTest.QTest.mouseClick(
self.gui.tor_bridges_use_custom_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.tor_bridges_use_custom_radio.height() / 2),
)
self.assertTrue(self.gui.tor_bridges_use_custom_radio.isChecked())
self.assertTrue(self.gui.tor_bridges_use_custom_textbox.isVisible())
self.assertFalse(self.gui.tor_bridges_use_obfs4_radio.isChecked())
self.gui.tor_bridges_use_custom_textbox.setPlainText(
"94.242.249.2:83 E25A95F1DADB739F0A83EB0223A37C02FD519306\n148.251.90.59:7510 019F727CA6DCA6CA5C90B55E477B7D87981E75BC\n93.80.47.217:41727 A6A0D497D98097FCFE91D639548EE9E34C15CDD3"
)
# Test that the Settings Dialog can save the settings and close itself
QtTest.QTest.mouseClick(self.gui.save_button, QtCore.Qt.LeftButton)
self.assertFalse(self.gui.isVisible())
# Test our settings are reflected in the settings json
with open("/tmp/settings.json") as f:
data = json.load(f)
self.assertTrue(data["public_mode"])
self.assertTrue(data["autostop_timer"])
if self.gui.onion.is_authenticated():
if self.gui.onion.supports_v3_onions:
self.assertTrue(data["use_legacy_v2_onions"])
self.assertTrue(data["save_private_key"])
self.assertTrue(data["use_stealth"])
else:
self.assertFalse(data["use_legacy_v2_onions"])
self.assertFalse(data["save_private_key"])
self.assertFalse(data["use_stealth"])
self.assertEqual(data["data_dir"], "/tmp/OnionShareSettingsTest")
self.assertFalse(data["close_after_first_download"])
self.assertEqual(data["connection_type"], "bundled")
self.assertFalse(data["tor_bridges_use_obfs4"])
self.assertEqual(
data["tor_bridges_use_custom_bridges"],
"Bridge 94.242.249.2:83 E25A95F1DADB739F0A83EB0223A37C02FD519306\nBridge 148.251.90.59:7510 019F727CA6DCA6CA5C90B55E477B7D87981E75BC\nBridge 93.80.47.217:41727 A6A0D497D98097FCFE91D639548EE9E34C15CDD3\n",
)

View File

@ -1,176 +0,0 @@
import json
import os
import requests
import socks
from PyQt5 import QtCore, QtTest
from onionshare import strings
from onionshare.common import Common
from onionshare.settings import Settings
from onionshare.onion import Onion
from onionshare.web import Web
from onionshare_gui import Application, OnionShare, OnionShareGui
from onionshare_gui.mode.share_mode import ShareMode
from onionshare_gui.mode.receive_mode import ReceiveMode
from .GuiBaseTest import GuiBaseTest
class TorGuiBaseTest(GuiBaseTest):
@staticmethod
def set_up(test_settings):
"""Create GUI with given settings"""
# Create our test file
testfile = open("/tmp/test.txt", "w")
testfile.write("onionshare")
testfile.close()
# Create a test dir and files
if not os.path.exists("/tmp/testdir"):
testdir = os.mkdir("/tmp/testdir")
testfile = open("/tmp/testdir/test.txt", "w")
testfile.write("onionshare")
testfile.close()
common = Common()
common.settings = Settings(common)
common.define_css()
strings.load_strings(common)
# Get all of the settings in test_settings
test_settings["connection_type"] = "automatic"
test_settings["data_dir"] = "/tmp/OnionShare"
for key, val in common.settings.default_settings.items():
if key not in test_settings:
test_settings[key] = val
# Start the Onion
testonion = Onion(common)
global qtapp
qtapp = Application(common)
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, False)
open("/tmp/settings.json", "w").write(json.dumps(test_settings))
gui = OnionShareGui(
common,
testonion,
qtapp,
app,
["/tmp/test.txt", "/tmp/testdir"],
"/tmp/settings.json",
False,
)
return gui
def 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())
# Set up connecting to the onion
(socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port()
session = requests.session()
session.proxies = {}
session.proxies["http"] = f"socks5h://{socks_address}:{socks_port}"
if type(mode) == ReceiveMode:
# Upload a file
files = {"file[]": open("/tmp/test.txt", "rb")}
if not public_mode:
path = f"http://{self.gui.app.onion_host}/{mode.web.password}/upload"
else:
path = f"http://{self.gui.app.onion_host}/upload"
response = session.post(path, files=files)
QtTest.QTest.qWait(4000)
if type(mode) == ShareMode:
# Download files
if public_mode:
path = f"http://{self.gui.app.onion_host}/download"
else:
path = f"http://{self.gui.app.onion_host}/{mode.web.password}/download"
response = session.get(path)
QtTest.QTest.qWait(4000)
# 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 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 web_page(self, mode, string, public_mode):
"""Test that the web page contains a string"""
(socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port()
socks.set_default_proxy(socks.SOCKS5, socks_address, socks_port)
s = socks.socksocket()
s.settimeout(60)
s.connect((self.gui.app.onion_host, 80))
if not public_mode:
path = f"/{mode.server_status.web.password}"
else:
path = "/"
http_request = f"GET {path} HTTP/1.0\r\n"
http_request += f"Host: {self.gui.app.onion_host}\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 have_copy_url_button(self, mode, public_mode):
"""Test that the Copy URL button is shown and that the clipboard is correct"""
self.assertTrue(mode.server_status.copy_url_button.isVisible())
QtTest.QTest.mouseClick(
mode.server_status.copy_url_button, QtCore.Qt.LeftButton
)
clipboard = self.gui.qtapp.clipboard()
if public_mode:
self.assertEqual(clipboard.text(), f"http://{self.gui.app.onion_host}")
else:
self.assertEqual(
clipboard.text(),
f"http://{self.gui.app.onion_host}/{mode.server_status.web.password}",
)
# Stealth tests
def copy_have_hidserv_auth_button(self, mode):
"""Test that the Copy HidservAuth button is shown"""
self.assertTrue(mode.server_status.copy_hidservauth_button.isVisible())
def hidserv_auth_string(self):
"""Test the validity of the HidservAuth string"""
self.assertRegex(
self.gui.app.auth_string,
r"HidServAuth {} [a-zA-Z1-9]".format(self.gui.app.onion_host),
)
# Miscellaneous tests
def tor_killed_statusbar_message_shown(self, mode):
"""Test that the status bar message shows Tor was disconnected"""
self.gui.app.onion.c = None
QtTest.QTest.qWait(1000)
self.assertTrue(
mode.status_bar.currentMessage(), strings._("gui_tor_connection_lost")
)

View File

@ -1,61 +0,0 @@
import os
import requests
from PyQt5 import QtTest
from .TorGuiBaseTest import TorGuiBaseTest
class TorGuiReceiveTest(TorGuiBaseTest):
def upload_file(self, public_mode, file_to_upload, expected_file):
"""Test that we can upload the file"""
(socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port()
session = requests.session()
session.proxies = {}
session.proxies["http"] = f"socks5h://{socks_address}:{socks_port}"
files = {"file[]": open(file_to_upload, "rb")}
if not public_mode:
path = f"http://{self.gui.app.onion_host}/{self.gui.receive_mode.web.password}/upload"
else:
path = f"http://{self.gui.app.onion_host}/upload"
response = session.post(path, files=files)
QtTest.QTest.qWait(4000)
self.assertTrue(os.path.isfile(expected_file))
# 'Grouped' tests follow from here
def run_all_receive_mode_tests(self, public_mode, receive_allow_receiver_shutdown):
"""Run a full suite of tests in Receive mode"""
self.click_mode(self.gui.receive_mode)
self.history_is_not_visible(self.gui.receive_mode)
self.click_toggle_history(self.gui.receive_mode)
self.history_is_visible(self.gui.receive_mode)
self.server_working_on_start_button_pressed(self.gui.receive_mode)
self.server_status_indicator_says_starting(self.gui.receive_mode)
self.settings_button_is_hidden()
self.server_is_started(self.gui.receive_mode, startup_time=45000)
self.web_server_is_running()
self.have_an_onion_service()
self.have_a_password(self.gui.receive_mode, public_mode)
self.url_description_shown(self.gui.receive_mode)
self.have_copy_url_button(self.gui.receive_mode, public_mode)
self.server_status_indicator_says_started(self.gui.receive_mode)
self.web_page(
self.gui.receive_mode,
"Select the files you want to send, then click",
public_mode,
)
self.upload_file(public_mode, "/tmp/test.txt", "/tmp/OnionShare/test.txt")
self.history_widgets_present(self.gui.receive_mode)
self.counter_incremented(self.gui.receive_mode, 1)
self.upload_file(public_mode, "/tmp/test.txt", "/tmp/OnionShare/test-2.txt")
self.counter_incremented(self.gui.receive_mode, 2)
self.upload_file(public_mode, "/tmp/testdir/test", "/tmp/OnionShare/test")
self.counter_incremented(self.gui.receive_mode, 3)
self.upload_file(public_mode, "/tmp/testdir/test", "/tmp/OnionShare/test-2")
self.counter_incremented(self.gui.receive_mode, 4)
self.history_indicator(self.gui.receive_mode, public_mode)
self.server_is_stopped(self.gui.receive_mode, False)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.receive_mode, False)
self.server_working_on_start_button_pressed(self.gui.receive_mode)
self.server_is_started(self.gui.receive_mode, startup_time=45000)
self.history_indicator(self.gui.receive_mode, public_mode)

View File

@ -1,91 +0,0 @@
import requests
import zipfile
from PyQt5 import QtTest
from .TorGuiBaseTest import TorGuiBaseTest
from .GuiShareTest import GuiShareTest
class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
def download_share(self, public_mode):
"""Test downloading a share"""
# Set up connecting to the onion
(socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port()
session = requests.session()
session.proxies = {}
session.proxies["http"] = f"socks5h://{socks_address}:{socks_port}"
# Download files
if public_mode:
path = f"http://{self.gui.app.onion_host}/download"
else:
path = f"http://{self.gui.app.onion_host}/{self.gui.share_mode.web.password}/download"
response = session.get(path, stream=True)
QtTest.QTest.qWait(4000)
if response.status_code == 200:
with open("/tmp/download.zip", "wb") as file_to_write:
for chunk in response.iter_content(chunk_size=128):
file_to_write.write(chunk)
file_to_write.close()
zip = zipfile.ZipFile("/tmp/download.zip")
QtTest.QTest.qWait(4000)
self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8"))
# Persistence tests
def have_same_onion(self, onion):
"""Test that we have the same onion"""
self.assertEqual(self.gui.app.onion_host, onion)
# legacy v2 onion test
def have_v2_onion(self):
"""Test that the onion is a v2 style onion"""
self.assertRegex(self.gui.app.onion_host, r"[a-z2-7].onion")
self.assertEqual(len(self.gui.app.onion_host), 22)
# 'Grouped' tests follow from here
def run_all_share_mode_started_tests(self, public_mode):
"""Tests in share mode after starting a share"""
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.server_status_indicator_says_starting(self.gui.share_mode)
self.add_delete_buttons_hidden()
self.settings_button_is_hidden()
self.server_is_started(self.gui.share_mode, startup_time=45000)
self.web_server_is_running()
self.have_an_onion_service()
self.have_a_password(self.gui.share_mode, public_mode)
self.url_description_shown(self.gui.share_mode)
self.have_copy_url_button(self.gui.share_mode, public_mode)
self.server_status_indicator_says_started(self.gui.share_mode)
def run_all_share_mode_download_tests(self, public_mode, stay_open):
"""Tests in share mode after downloading a share"""
self.web_page(self.gui.share_mode, "Total size", public_mode)
self.download_share(public_mode)
self.history_widgets_present(self.gui.share_mode)
self.server_is_stopped(self.gui.share_mode, stay_open)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
self.add_button_visible(self.gui.share_mode)
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.server_is_started(self.gui.share_mode, startup_time=45000)
self.history_indicator(self.gui.share_mode, public_mode)
def run_all_share_mode_persistent_tests(self, public_mode, stay_open):
"""Same as end-to-end share tests but also test the password is the same on multiple shared"""
self.run_all_share_mode_setup_tests()
self.run_all_share_mode_started_tests(public_mode)
password = self.gui.share_mode.server_status.web.password
onion = self.gui.app.onion_host
self.run_all_share_mode_download_tests(public_mode, stay_open)
self.have_same_onion(onion)
self.have_same_password(password)
def run_all_share_mode_timer_tests(self, public_mode):
"""Auto-stop timer tests in share mode"""
self.run_all_share_mode_setup_tests()
self.set_timeout(self.gui.share_mode, 120)
self.run_all_share_mode_started_tests(public_mode)
self.autostop_timer_widget_hidden(self.gui.share_mode)
self.server_timed_out(self.gui.share_mode, 125000)
self.web_server_is_stopped()

View File

@ -3,6 +3,9 @@ import sys
# Force tests to look for resources in the source code tree
sys.onionshare_dev_mode = True
# Let OnionShare know the tests are running, to avoid colliding with settings files
sys.onionshare_test_mode = True
import os
import shutil
import tempfile
@ -12,6 +15,10 @@ import pytest
from onionshare import common, web, settings, strings
# The temporary directory for CLI tests
test_temp_dir = None
def pytest_addoption(parser):
parser.addoption(
"--rungui", action="store_true", default=False, help="run GUI tests"
@ -38,51 +45,60 @@ def pytest_collection_modifyitems(config, items):
@pytest.fixture
def temp_dir_1024():
def temp_dir():
"""Creates a persistent temporary directory for the CLI tests to use"""
global test_temp_dir
if not test_temp_dir:
test_temp_dir = tempfile.mkdtemp()
return test_temp_dir
@pytest.fixture
def temp_dir_1024(temp_dir):
""" 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)
new_temp_dir = tempfile.mkdtemp(dir=temp_dir)
tmp_file, tmp_file_path = tempfile.mkstemp(dir=new_temp_dir)
with open(tmp_file, "wb") as f:
f.write(b"*" * 1024)
return tmp_dir
return new_temp_dir
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture
def temp_dir_1024_delete():
def temp_dir_1024_delete(temp_dir):
""" 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 tempfile.TemporaryDirectory(dir=temp_dir) as new_temp_dir:
tmp_file, tmp_file_path = tempfile.mkstemp(dir=new_temp_dir)
with open(tmp_file, "wb") as f:
f.write(b"*" * 1024)
yield tmp_dir
yield new_temp_dir
@pytest.fixture
def temp_file_1024():
def temp_file_1024(temp_dir):
""" Create a temporary file of a particular size (1024 bytes). """
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
with tempfile.NamedTemporaryFile(delete=False, dir=temp_dir) 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():
def temp_file_1024_delete(temp_dir):
"""
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:
with tempfile.NamedTemporaryFile(dir=temp_dir) as tmp_file:
tmp_file.write(b"*" * 1024)
tmp_file.flush()
yield tmp_file.name
@ -108,7 +124,10 @@ def default_zw():
yield zw
zw.close()
tmp_dir = os.path.dirname(zw.zip_filename)
shutil.rmtree(tmp_dir)
try:
shutil.rmtree(tmp_dir, ignore_errors=True)
except:
pass
@pytest.fixture

426
tests/gui_base_test.py Normal file
View File

@ -0,0 +1,426 @@
import pytest
import unittest
import json
import os
import requests
import shutil
import base64
import tempfile
import secrets
from PyQt5 import QtCore, QtTest, QtWidgets
from onionshare import strings
from onionshare.common import Common
from onionshare.settings import Settings
from onionshare.onion import Onion
from onionshare.web import Web
from onionshare_gui import Application, MainWindow, GuiCommon
from onionshare_gui.tab.mode.share_mode import ShareMode
from onionshare_gui.tab.mode.receive_mode import ReceiveMode
from onionshare_gui.tab.mode.website_mode import WebsiteMode
class GuiBaseTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
common = Common(verbose=True)
# Delete any old test data that might exist
shutil.rmtree(common.build_data_dir(), ignore_errors=True)
qtapp = Application(common)
common.gui = GuiCommon(common, qtapp, local_only=True)
cls.gui = MainWindow(common, filenames=None)
cls.gui.qtapp = qtapp
# Create some random files to test with
cls.tmpdir = tempfile.TemporaryDirectory()
cls.tmpfiles = []
for _ in range(10):
filename = os.path.join(cls.tmpdir.name, f"{secrets.token_hex(4)}.txt")
with open(filename, "w") as file:
file.write(secrets.token_hex(10))
cls.tmpfiles.append(filename)
# A file called "test.txt"
cls.tmpfile_test = os.path.join(cls.tmpdir.name, "test.txt")
with open(cls.tmpfile_test, "w") as file:
file.write("onionshare")
# A file called "test2.txt"
cls.tmpfile_test2 = os.path.join(cls.tmpdir.name, "test2.txt")
with open(cls.tmpfile_test2, "w") as file:
file.write("onionshare2")
# A file called "index.html"
cls.tmpfile_index_html = os.path.join(cls.tmpdir.name, "index.html")
with open(cls.tmpfile_index_html, "w") as file:
file.write(
"<html><body><p>This is a test website hosted by OnionShare</p></body></html>"
)
# A large file
size = 1024 * 1024 * 155
cls.tmpfile_large = os.path.join(cls.tmpdir.name, "large_file")
with open(cls.tmpfile_large, "wb") as fout:
fout.write(os.urandom(size))
@classmethod
def tearDownClass(cls):
# Quit
cls.gui.qtapp.clipboard().clear()
QtCore.QTimer.singleShot(200, cls.gui.close_dialog.accept_button.click)
cls.gui.close()
cls.gui.cleanup()
# Shared test methods
def verify_new_tab(self, tab):
# Make sure the new tab widget is showing, and no mode has been started
QtTest.QTest.qWait(1000)
self.assertTrue(tab.new_tab.isVisible())
self.assertFalse(hasattr(tab, "share_mode"))
self.assertFalse(hasattr(tab, "receive_mode"))
self.assertFalse(hasattr(tab, "website_mode"))
def new_share_tab(self):
tab = self.gui.tabs.widget(0)
self.verify_new_tab(tab)
# Share files
tab.share_button.click()
self.assertFalse(tab.new_tab.isVisible())
self.assertTrue(tab.share_mode.isVisible())
return tab
def new_share_tab_with_files(self):
tab = self.new_share_tab()
# Add files
for filename in self.tmpfiles:
tab.share_mode.server_status.file_selection.file_list.add_file(filename)
return tab
def new_receive_tab(self):
tab = self.gui.tabs.widget(0)
self.verify_new_tab(tab)
# Receive files
tab.receive_button.click()
self.assertFalse(tab.new_tab.isVisible())
self.assertTrue(tab.receive_mode.isVisible())
return tab
def new_website_tab(self):
tab = self.gui.tabs.widget(0)
self.verify_new_tab(tab)
# Publish website
tab.website_button.click()
self.assertFalse(tab.new_tab.isVisible())
self.assertTrue(tab.website_mode.isVisible())
return tab
def new_website_tab_with_files(self):
tab = self.new_website_tab()
# Add files
for filename in self.tmpfiles:
tab.website_mode.server_status.file_selection.file_list.add_file(filename)
return tab
def close_all_tabs(self):
for _ in range(self.gui.tabs.count()):
tab = self.gui.tabs.widget(0)
QtCore.QTimer.singleShot(200, tab.close_dialog.accept_button.click)
self.gui.tabs.tabBar().tabButton(0, QtWidgets.QTabBar.RightSide).click()
def gui_loaded(self):
"""Test that the GUI actually is shown"""
self.assertTrue(self.gui.show)
def window_title_seen(self):
"""Test that the window title is OnionShare"""
self.assertEqual(self.gui.windowTitle(), "OnionShare")
def server_status_bar_is_visible(self):
"""Test that the status bar is visible"""
self.assertTrue(self.gui.status_bar.isVisible())
def mode_settings_widget_is_visible(self, tab):
"""Test that the mode settings are visible"""
self.assertTrue(tab.get_mode().mode_settings_widget.isVisible())
def mode_settings_widget_is_hidden(self, tab):
"""Test that the mode settings are hidden when the server starts"""
self.assertFalse(tab.get_mode().mode_settings_widget.isVisible())
def click_toggle_history(self, tab):
"""Test that we can toggle Download or Upload history by clicking the toggle button"""
currently_visible = tab.get_mode().history.isVisible()
tab.get_mode().toggle_history.click()
self.assertEqual(tab.get_mode().history.isVisible(), not currently_visible)
def history_indicator(self, tab, indicator_count="1"):
"""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 tab.get_mode().history.isVisible():
tab.get_mode().toggle_history.click()
self.assertFalse(tab.get_mode().history.isVisible())
# Indicator should not be visible yet
self.assertFalse(tab.get_mode().toggle_history.indicator_label.isVisible())
if type(tab.get_mode()) == ReceiveMode:
# Upload a file
files = {"file[]": open(self.tmpfiles[0], "rb")}
url = f"http://127.0.0.1:{tab.app.port}/upload"
if tab.settings.get("general", "public"):
requests.post(url, files=files)
else:
requests.post(
url,
files=files,
auth=requests.auth.HTTPBasicAuth(
"onionshare", tab.get_mode().web.password
),
)
QtTest.QTest.qWait(2000)
if type(tab.get_mode()) == ShareMode:
# Download files
url = f"http://127.0.0.1:{tab.app.port}/download"
if tab.settings.get("general", "public"):
requests.get(url)
else:
requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", tab.get_mode().web.password
),
)
QtTest.QTest.qWait(2000)
# Indicator should be visible, have a value of "1"
self.assertTrue(tab.get_mode().toggle_history.indicator_label.isVisible())
self.assertEqual(
tab.get_mode().toggle_history.indicator_label.text(), indicator_count
)
# Toggle history back on, indicator should be hidden again
tab.get_mode().toggle_history.click()
self.assertFalse(tab.get_mode().toggle_history.indicator_label.isVisible())
def history_is_not_visible(self, tab):
"""Test that the History section is not visible"""
self.assertFalse(tab.get_mode().history.isVisible())
def history_is_visible(self, tab):
"""Test that the History section is visible"""
self.assertTrue(tab.get_mode().history.isVisible())
def server_working_on_start_button_pressed(self, tab):
"""Test we can start the service"""
# Should be in SERVER_WORKING state
tab.get_mode().server_status.server_button.click()
self.assertEqual(tab.get_mode().server_status.status, 1)
def toggle_indicator_is_reset(self, tab):
self.assertEqual(tab.get_mode().toggle_history.indicator_count, 0)
self.assertFalse(tab.get_mode().toggle_history.indicator_label.isVisible())
def server_status_indicator_says_starting(self, tab):
"""Test that the Server Status indicator shows we are Starting"""
self.assertEqual(
tab.get_mode().server_status_label.text(),
strings._("gui_status_indicator_share_working"),
)
def server_status_indicator_says_scheduled(self, tab):
"""Test that the Server Status indicator shows we are Scheduled"""
self.assertEqual(
tab.get_mode().server_status_label.text(),
strings._("gui_status_indicator_share_scheduled"),
)
def server_is_started(self, tab, startup_time=2000):
"""Test that the server has started"""
QtTest.QTest.qWait(startup_time)
# Should now be in SERVER_STARTED state
self.assertEqual(tab.get_mode().server_status.status, 2)
def web_server_is_running(self, tab):
"""Test that the web server has started"""
try:
requests.get(f"http://127.0.0.1:{tab.app.port}/")
self.assertTrue(True)
except requests.exceptions.ConnectionError:
self.assertTrue(False)
def have_a_password(self, tab):
"""Test that we have a valid password"""
if not tab.settings.get("general", "public"):
self.assertRegex(tab.get_mode().server_status.web.password, r"(\w+)-(\w+)")
else:
self.assertIsNone(tab.get_mode().server_status.web.password, r"(\w+)-(\w+)")
def add_button_visible(self, tab):
"""Test that the add button should be visible"""
self.assertTrue(
tab.get_mode().server_status.file_selection.add_button.isVisible()
)
def url_description_shown(self, tab):
"""Test that the URL label is showing"""
self.assertTrue(tab.get_mode().server_status.url_description.isVisible())
def have_copy_url_button(self, tab):
"""Test that the Copy URL button is shown and that the clipboard is correct"""
self.assertTrue(tab.get_mode().server_status.copy_url_button.isVisible())
tab.get_mode().server_status.copy_url_button.click()
clipboard = tab.common.gui.qtapp.clipboard()
if tab.settings.get("general", "public"):
self.assertEqual(clipboard.text(), f"http://127.0.0.1:{tab.app.port}")
else:
self.assertEqual(
clipboard.text(),
f"http://onionshare:{tab.get_mode().server_status.web.password}@127.0.0.1:{tab.app.port}",
)
def server_status_indicator_says_started(self, tab):
"""Test that the Server Status indicator shows we are started"""
if type(tab.get_mode()) == ReceiveMode:
self.assertEqual(
tab.get_mode().server_status_label.text(),
strings._("gui_status_indicator_receive_started"),
)
if type(tab.get_mode()) == ShareMode:
self.assertEqual(
tab.get_mode().server_status_label.text(),
strings._("gui_status_indicator_share_started"),
)
def web_page(self, tab, string):
"""Test that the web page contains a string"""
url = f"http://127.0.0.1:{tab.app.port}/"
if tab.settings.get("general", "public"):
r = requests.get(url)
else:
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", tab.get_mode().web.password
),
)
self.assertTrue(string in r.text)
def history_widgets_present(self, tab):
"""Test that the relevant widgets are present in the history view after activity has taken place"""
self.assertFalse(tab.get_mode().history.empty.isVisible())
self.assertTrue(tab.get_mode().history.not_empty.isVisible())
def counter_incremented(self, tab, count):
"""Test that the counter has incremented"""
self.assertEqual(tab.get_mode().history.completed_count, count)
def server_is_stopped(self, tab):
"""Test that the server stops when we click Stop"""
if (
type(tab.get_mode()) == ReceiveMode
or (
type(tab.get_mode()) == ShareMode
and not tab.settings.get("share", "autostop_sharing")
)
or (type(tab.get_mode()) == WebsiteMode)
):
tab.get_mode().server_status.server_button.click()
self.assertEqual(tab.get_mode().server_status.status, 0)
def web_server_is_stopped(self, tab):
"""Test that the web server also stopped"""
QtTest.QTest.qWait(800)
try:
requests.get(f"http://127.0.0.1:{tab.app.port}/")
self.assertTrue(False)
except requests.exceptions.ConnectionError:
self.assertTrue(True)
def server_status_indicator_says_closed(self, tab):
"""Test that the Server Status indicator shows we closed"""
if type(tab.get_mode()) == ReceiveMode:
self.assertEqual(
tab.get_mode().server_status_label.text(),
strings._("gui_status_indicator_receive_stopped"),
)
if type(tab.get_mode()) == ShareMode:
if not tab.settings.get("share", "autostop_sharing"):
self.assertEqual(
tab.get_mode().server_status_label.text(),
strings._("gui_status_indicator_share_stopped"),
)
else:
self.assertEqual(
tab.get_mode().server_status_label.text(),
strings._("closing_automatically"),
)
def clear_all_history_items(self, tab, count):
if count == 0:
tab.get_mode().history.clear_button.click()
self.assertEqual(len(tab.get_mode().history.item_list.items.keys()), count)
def file_selection_widget_has_files(self, tab, num=3):
"""Test that the number of items in the list is as expected"""
self.assertEqual(
tab.get_mode().server_status.file_selection.get_num_files(), num
)
def add_delete_buttons_hidden(self, tab):
"""Test that the add and delete buttons are hidden when the server starts"""
self.assertFalse(
tab.get_mode().server_status.file_selection.add_button.isVisible()
)
self.assertFalse(
tab.get_mode().server_status.file_selection.delete_button.isVisible()
)
# Auto-stop timer tests
def set_timeout(self, tab, timeout):
"""Test that the timeout can be set"""
timer = QtCore.QDateTime.currentDateTime().addSecs(timeout)
tab.get_mode().mode_settings_widget.autostop_timer_widget.setDateTime(timer)
self.assertTrue(
tab.get_mode().mode_settings_widget.autostop_timer_widget.dateTime(), timer
)
def autostop_timer_widget_hidden(self, tab):
"""Test that the auto-stop timer widget is hidden when share has started"""
self.assertFalse(
tab.get_mode().mode_settings_widget.autostop_timer_widget.isVisible()
)
def server_timed_out(self, tab, 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(tab.get_mode().server_status.status, 0)
# Grouped tests follow from here
def run_all_common_setup_tests(self):
self.gui_loaded()
self.window_title_seen()
self.server_status_bar_is_visible()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class Local401PublicModeRateLimitTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"close_after_first_download": False, "public_mode": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(True, True)
self.hit_401(True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class Local401RateLimitTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, True)
self.hit_401(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,34 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from PyQt5 import QtCore, QtTest
from .GuiShareTest import GuiShareTest
class LocalQuittingDuringSharePromptsWarningTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, True)
# Prepare our auto-accept of prompt
QtCore.QTimer.singleShot(5000, self.accept_dialog)
# Try to close the app
self.gui.close()
# Server should still be running (we've been prompted first)
self.server_is_started(self.gui.share_mode, 0)
self.web_server_is_running()
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModeClearAllButtonTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_clear_all_button_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModeTimerTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": False, "autostop_timer": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_timer_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModeUnwritableTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {"receive_allow_receiver_shutdown": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_unwritable_dir_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceivePublicModeUnwritableTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": True, "receive_allow_receiver_shutdown": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_unwritable_dir_tests(True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModePublicModeTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": True, "receive_allow_receiver_shutdown": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModeTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {"receive_allow_receiver_shutdown": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from onionshare import strings
from .SettingsGuiBaseTest import SettingsGuiBaseTest, OnionStub
class SettingsGuiTest(unittest.TestCase, SettingsGuiBaseTest):
@classmethod
def setUpClass(cls):
cls.gui = SettingsGuiBaseTest.set_up()
@classmethod
def tearDownClass(cls):
SettingsGuiBaseTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui_legacy_tor(self):
self.gui.onion = OnionStub(True, False)
self.gui.reload_settings()
self.run_settings_gui_tests()
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from onionshare import strings
from .SettingsGuiBaseTest import SettingsGuiBaseTest, OnionStub
class SettingsGuiTest(unittest.TestCase, SettingsGuiBaseTest):
@classmethod
def setUpClass(cls):
cls.gui = SettingsGuiBaseTest.set_up()
@classmethod
def tearDownClass(cls):
SettingsGuiBaseTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui_no_tor(self):
self.gui.onion = OnionStub(False, False)
self.gui.reload_settings()
self.run_settings_gui_tests()
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from onionshare import strings
from .SettingsGuiBaseTest import SettingsGuiBaseTest, OnionStub
class SettingsGuiTest(unittest.TestCase, SettingsGuiBaseTest):
@classmethod
def setUpClass(cls):
cls.gui = SettingsGuiBaseTest.set_up()
@classmethod
def tearDownClass(cls):
SettingsGuiBaseTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui_v3_tor(self):
self.gui.onion = OnionStub(True, True)
self.gui.reload_settings()
self.run_settings_gui_tests()
if __name__ == "__main__":
unittest.main()

View File

@ -1,30 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeAutoStartTimerTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": False,
"autostart_timer": True,
"autostop_timer": True,
}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_autostop_autostart_mismatch_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeAutoStartTimerTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": False, "autostart_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_autostart_timer_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from PyQt5 import QtCore, QtTest
from .GuiShareTest import GuiShareTest
class LocalShareModeAutoStartTimerTooShortTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": False, "autostart_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
# Set a low timeout
self.set_autostart_timer(self.gui.share_mode, 2)
QtTest.QTest.qWait(3000)
QtCore.QTimer.singleShot(4000, self.accept_dialog)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.assertEqual(self.gui.share_mode.server_status.status, 0)
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeCancelTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"autostart_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
self.cancel_the_share(self.gui.share_mode)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeClearAllButtonTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_clear_all_button_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModePublicModeTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(True, False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeStayOpenTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeIndividualFileViewStayOpenTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_individual_file_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeIndividualFileViewTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"close_after_first_download": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_individual_file_tests(False, False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeLargeDownloadTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_large_file_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,31 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModePersistentPasswordTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": False,
"password": "",
"save_private_key": True,
"close_after_first_download": False,
}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_persistent_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeTimerTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": False, "autostop_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_timer_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from PyQt5 import QtCore, QtTest
from .GuiShareTest import GuiShareTest
class LocalShareModeTimerTooShortTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": False, "autostop_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
# Set a low timeout
self.set_timeout(self.gui.share_mode, 2)
QtTest.QTest.qWait(3000)
QtCore.QTimer.singleShot(4000, self.accept_dialog)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.assertEqual(self.gui.share_mode.server_status.status, 0)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeUnReadableFileTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_unreadable_file_tests()
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiWebsiteTest import GuiWebsiteTest
class LocalWebsiteModeCSPEnabledTest(unittest.TestCase, GuiWebsiteTest):
@classmethod
def setUpClass(cls):
test_settings = {"csp_header_disabled": False}
cls.gui = GuiWebsiteTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiWebsiteTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
# self.run_all_common_setup_tests()
self.run_all_website_mode_download_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiWebsiteTest import GuiWebsiteTest
class LocalWebsiteModeTest(unittest.TestCase, GuiWebsiteTest):
@classmethod
def setUpClass(cls):
test_settings = {"csp_header_disabled": True}
cls.gui = GuiWebsiteTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiWebsiteTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
# self.run_all_common_setup_tests()
self.run_all_website_mode_download_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,30 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
# Tests #790 regression
class ShareModeCancelSecondShareTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"close_after_first_download": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, False)
self.cancel_the_share(self.gui.share_mode)
self.server_is_stopped(self.gui.share_mode, False)
self.web_server_is_stopped()
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiReceiveTest import TorGuiReceiveTest
class ReceiveModeTest(unittest.TestCase, TorGuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": True, "receive_allow_receiver_shutdown": True}
cls.gui = TorGuiReceiveTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(True, True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiReceiveTest import TorGuiReceiveTest
class ReceiveModeTest(unittest.TestCase, TorGuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {"receive_allow_receiver_shutdown": True}
cls.gui = TorGuiReceiveTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,28 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeCancelTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"autostart_timer": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
self.cancel_the_share(self.gui.share_mode)
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModePublicModeTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(True, False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeStayOpenTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"close_after_first_download": False}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,33 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModePersistentPasswordTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"use_legacy_v2_onions": True,
"public_mode": False,
"password": "",
"save_private_key": True,
"close_after_first_download": False,
}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_persistent_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,30 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeStealthTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"use_legacy_v2_onions": True, "use_stealth": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
self.run_all_share_mode_started_tests(False)
self.hidserv_auth_string()
self.copy_have_hidserv_auth_button(self.gui.share_mode)
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeTimerTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"public_mode": False, "autostop_timer": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_timer_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeTorConnectionKilledTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {}
cls.gui = TorGuiShareTest.set_up(test_settings)
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
self.run_all_share_mode_started_tests(False)
self.tor_killed_statusbar_message_shown(self.gui.share_mode)
self.server_is_stopped(self.gui.share_mode, False)
self.web_server_is_stopped()
if __name__ == "__main__":
unittest.main()

View File

@ -1,28 +0,0 @@
#!/usr/bin/env python3
import pytest
import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeV2OnionTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {"use_legacy_v2_onions": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
TorGuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, False)
self.have_v2_onion()
if __name__ == "__main__":
unittest.main()

4
tests/pytest.ini Normal file
View File

@ -0,0 +1,4 @@
[pytest]
markers =
gui: marks tests as a GUI test
tor: marks tests as a Tor GUI test

26
tests/run.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
# The script runs python tests
# Firstly, all CLI tests are run
# Then, all the GUI tests are run individually
# to avoid segmentation fault
PARAMS=""
while [ ! $# -eq 0 ]
do
case "$1" in
--rungui)
PARAMS="$PARAMS --rungui"
;;
--runtor)
PARAMS="$PARAMS --runtor"
;;
esac
shift
done
pytest $PARAMS -vvv ./tests/test_cli*.py
for filename in ./tests/test_gui_*.py; do
pytest $PARAMS -vvv --no-qt-log $filename
done

View File

@ -23,17 +23,17 @@ import pytest
from onionshare import OnionShare
from onionshare.common import Common
from onionshare.mode_settings import ModeSettings
class MyOnion:
def __init__(self, stealth=False):
def __init__(self):
self.auth_string = "TestHidServAuth"
self.private_key = ""
self.stealth = stealth
self.scheduled_key = None
@staticmethod
def start_onion_service(self, await_publication=True, save_scheduled_key=False):
def start_onion_service(self, mode_settings_obj, await_publication=True, save_scheduled_key=False):
return "test_service_id.onion"
@ -43,38 +43,27 @@ def onionshare_obj():
return OnionShare(common, MyOnion())
@pytest.fixture
def mode_settings_obj():
common = Common()
return ModeSettings(common)
class TestOnionShare:
def test_init(self, onionshare_obj):
assert onionshare_obj.hidserv_dir is None
assert onionshare_obj.onion_host is None
assert onionshare_obj.stealth is None
assert onionshare_obj.cleanup_filenames == []
assert onionshare_obj.local_only is False
def test_set_stealth_true(self, onionshare_obj):
onionshare_obj.set_stealth(True)
assert onionshare_obj.stealth is True
assert onionshare_obj.onion.stealth is True
def test_set_stealth_false(self, onionshare_obj):
onionshare_obj.set_stealth(False)
assert onionshare_obj.stealth is False
assert onionshare_obj.onion.stealth is False
def test_start_onion_service(self, onionshare_obj):
onionshare_obj.set_stealth(False)
onionshare_obj.start_onion_service()
def test_start_onion_service(self, onionshare_obj, mode_settings_obj):
onionshare_obj.start_onion_service(mode_settings_obj)
assert 17600 <= onionshare_obj.port <= 17650
assert onionshare_obj.onion_host == "test_service_id.onion"
def test_start_onion_service_stealth(self, onionshare_obj):
onionshare_obj.set_stealth(True)
onionshare_obj.start_onion_service()
assert onionshare_obj.auth_string == "TestHidServAuth"
def test_start_onion_service_local_only(self, onionshare_obj):
def test_start_onion_service_local_only(self, onionshare_obj, mode_settings_obj):
onionshare_obj.local_only = True
onionshare_obj.start_onion_service()
onionshare_obj.start_onion_service(mode_settings_obj)
assert onionshare_obj.onion_host == "127.0.0.1:{}".format(onionshare_obj.port)
def test_cleanup(self, onionshare_obj, temp_dir_1024, temp_file_1024):

Some files were not shown because too many files have changed in this diff Show More