Merge branch 'develop' into 929_download_errors

This commit is contained in:
Micah Lee 2020-12-13 11:40:25 -08:00
commit b96b83905b
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
15 changed files with 294 additions and 116 deletions

View File

@ -22,7 +22,7 @@ import os, sys, time, argparse, threading
from datetime import datetime
from datetime import timedelta
from .common import Common
from .common import Common, CannotFindTor
from .web import Web
from .onion import *
from .onionshare import OnionShare
@ -320,7 +320,15 @@ def main(cwd=None):
web = Web(common, False, mode_settings, mode)
# Start the Onion object
onion = Onion(common, use_tmp_dir=True)
try:
onion = Onion(common, use_tmp_dir=True)
except CannotFindTor:
print("You must install tor to use OnionShare from the command line")
if common.platform == "Darwin":
print("In macOS, you can do this with Homebrew (https://brew.sh):")
print(" brew install tor")
sys.exit()
try:
onion.connect(
custom_settings=False,
@ -332,7 +340,7 @@ def main(cwd=None):
print("")
sys.exit()
except Exception as e:
sys.exit(e.args[0])
sys.exit()
# Start the onionshare app
try:

View File

@ -34,6 +34,12 @@ from pkg_resources import resource_filename
from .settings import Settings
class CannotFindTor(Exception):
"""
OnionShare can't find a tor binary
"""
class Common:
"""
The Common object is shared amongst all parts of OnionShare.
@ -82,6 +88,8 @@ class Common:
def get_tor_paths(self):
if self.platform == "Linux":
tor_path = shutil.which("tor")
if not tor_path:
raise CannotFindTor()
obfs4proxy_file_path = shutil.which("obfs4proxy")
prefix = os.path.dirname(os.path.dirname(tor_path))
tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")
@ -94,6 +102,8 @@ class Common:
tor_geo_ipv6_file_path = os.path.join(base_path, "Data", "Tor", "geoip6")
elif self.platform == "Darwin":
tor_path = shutil.which("tor")
if not tor_path:
raise CannotFindTor()
obfs4proxy_file_path = shutil.which("obfs4proxy")
prefix = os.path.dirname(os.path.dirname(tor_path))
tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")

View File

@ -32,11 +32,8 @@ import getpass
import psutil
from distutils.version import LooseVersion as Version
from . import common
from .settings import Settings
# TODO: Figure out how to localize this for the GUI
class TorErrorAutomatic(Exception):
"""
@ -44,40 +41,30 @@ class TorErrorAutomatic(Exception):
using automatic settings that should work with Tor Browser.
"""
pass
class TorErrorInvalidSetting(Exception):
"""
This exception is raised if the settings just don't make sense.
"""
pass
class TorErrorSocketPort(Exception):
"""
OnionShare can't connect to the Tor controller using the supplied address and port.
"""
pass
class TorErrorSocketFile(Exception):
"""
OnionShare can't connect to the Tor controller using the supplied socket file.
"""
pass
class TorErrorMissingPassword(Exception):
"""
OnionShare connected to the Tor controller, but it requires a password.
"""
pass
class TorErrorUnreadableCookieFile(Exception):
"""
@ -85,8 +72,6 @@ class TorErrorUnreadableCookieFile(Exception):
to access the cookie file.
"""
pass
class TorErrorAuthError(Exception):
"""
@ -94,8 +79,6 @@ class TorErrorAuthError(Exception):
that a Tor controller isn't listening on this port.
"""
pass
class TorErrorProtocolError(Exception):
"""
@ -103,17 +86,17 @@ class TorErrorProtocolError(Exception):
isn't acting like a Tor controller (such as in Whonix).
"""
pass
class TorTooOld(Exception):
class TorTooOldEphemeral(Exception):
"""
This exception is raised if onionshare needs to use a feature of Tor or stem
(like stealth ephemeral onion services) but the version you have installed
is too old.
This exception is raised if the version of tor doesn't support ephemeral onion services
"""
pass
class TorTooOldStealth(Exception):
"""
This exception is raised if the version of tor doesn't support stealth onion services
"""
class BundledTorTimeout(Exception):
@ -137,6 +120,12 @@ class BundledTorBroken(Exception):
"""
class PortNotAvailable(Exception):
"""
There are no available ports for OnionShare to use, which really shouldn't ever happen
"""
class Onion(object):
"""
Onion is an abstraction layer for connecting to the Tor control port and
@ -236,7 +225,8 @@ class Onion(object):
try:
self.tor_socks_port = self.common.get_available_port(1000, 65535)
except:
raise OSError("OnionShare port not available")
print("OnionShare port not available")
raise PortNotAvailable()
self.tor_torrc = os.path.join(self.tor_data_directory_name, "torrc")
# If there is an existing OnionShare tor process, kill it
@ -268,7 +258,8 @@ class Onion(object):
try:
self.tor_control_port = self.common.get_available_port(1000, 65535)
except:
raise OSError("OnionShare port not available")
print("OnionShare port not available")
raise PortNotAvailable()
self.tor_control_socket = None
else:
# Linux and BSD can use unix sockets
@ -337,10 +328,6 @@ class Onion(object):
f.write(self.settings.get("tor_bridges_use_custom_bridges"))
f.write("\nUseBridges 1")
# Make sure the tor path is accurate
if not os.path.exists(self.tor_path):
raise BundledTorNotSupported(f"Cannot find tor binary: {self.tor_path}")
# Execute a tor subprocess
start_ts = time.time()
if self.common.platform == "Windows":
@ -375,10 +362,8 @@ class Onion(object):
self.c = Controller.from_socket_file(path=self.tor_control_socket)
self.c.authenticate()
except Exception as e:
raise BundledTorBroken(
# strings._("settings_error_bundled_tor_broken").format(e.args[0])
"OnionShare could not connect to Tor:\n{}".format(e.args[0])
)
print("OnionShare could not connect to Tor:\n{}".format(e.args[0]))
raise BundledTorBroken(e.args[0])
while True:
try:
@ -425,15 +410,16 @@ class Onion(object):
print("")
try:
self.tor_proc.terminate()
raise BundledTorTimeout(
# strings._("settings_error_bundled_tor_timeout")
print(
"Taking too long to connect to Tor. Maybe you aren't connected to the Internet, or have an inaccurate system clock?"
)
raise BundledTorTimeout()
except FileNotFoundError:
pass
elif self.settings.get("connection_type") == "automatic":
# Automatically try to guess the right way to connect to Tor Browser
automatic_error = "Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?"
# Try connecting to control port
found_tor = False
@ -485,30 +471,25 @@ class Onion(object):
)
elif self.common.platform == "Windows":
# Windows doesn't support unix sockets
raise TorErrorAutomatic(
# strings._("settings_error_automatic")
"Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?"
)
print(automatic_error)
raise TorErrorAutomatic()
self.c = Controller.from_socket_file(path=socket_file_path)
except:
raise TorErrorAutomatic(
# strings._("settings_error_automatic")
"Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?"
)
print(automatic_error)
raise TorErrorAutomatic()
# Try authenticating
try:
self.c.authenticate()
except:
raise TorErrorAutomatic(
# strings._("settings_error_automatic")
"Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?"
)
print(automatic_error)
raise TorErrorAutomatic()
else:
# Use specific settings to connect to tor
invalid_settings_error = "Can't connect to Tor controller because your settings don't make sense."
# Try connecting
try:
@ -522,27 +503,28 @@ class Onion(object):
path=self.settings.get("socket_file_path")
)
else:
raise TorErrorInvalidSetting(
# strings._("settings_error_unknown")
"Can't connect to Tor controller because your settings don't make sense."
)
print(invalid_settings_error)
raise TorErrorInvalidSetting()
except:
if self.settings.get("connection_type") == "control_port":
raise TorErrorSocketPort(
# strings._("settings_error_socket_port")
print(
"Can't connect to the Tor controller at {}:{}.".format(
self.settings.get("control_port_address"),
self.settings.get("control_port_port"),
)
)
raise TorErrorSocketPort(
self.settings.get("control_port_address"),
self.settings.get("control_port_port"),
)
else:
raise TorErrorSocketFile(
# strings._("settings_error_socket_file")
print(
"Can't connect to the Tor controller using socket file {}.".format(
self.settings.get("socket_file_path")
)
)
raise TorErrorSocketFile(self.settings.get("socket_file_path"))
# Try authenticating
try:
@ -551,29 +533,30 @@ class Onion(object):
elif self.settings.get("auth_type") == "password":
self.c.authenticate(self.settings.get("auth_password"))
else:
raise TorErrorInvalidSetting(
# strings._("settings_error_unknown")
"Can't connect to Tor controller because your settings don't make sense."
)
print(invalid_settings_error)
raise TorErrorInvalidSetting()
except MissingPassword:
raise TorErrorMissingPassword(
# strings._("settings_error_missing_password")
print(
"Connected to Tor controller, but it requires a password to authenticate."
)
raise TorErrorMissingPassword()
except UnreadableCookieFile:
raise TorErrorUnreadableCookieFile(
# strings._("settings_error_unreadable_cookie_file")
print(
"Connected to the Tor controller, but password may be wrong, or your user is not permitted to read the cookie file."
)
raise TorErrorUnreadableCookieFile()
except AuthenticationFailure:
raise TorErrorAuthError(
# strings._("settings_error_auth")
print(
"Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?".format(
self.settings.get("control_port_address"),
self.settings.get("control_port_port"),
)
)
raise TorErrorAuthError(
self.settings.get("control_port_address"),
self.settings.get("control_port_port"),
)
# If we made it this far, we should be connected to Tor
self.connected_to_tor = True
@ -628,15 +611,15 @@ class Onion(object):
self.common.log("Onion", "start_onion_service", f"port={port}")
if not self.supports_ephemeral:
raise TorTooOld(
# strings._("error_ephemeral_not_supported")
print(
"Your version of Tor is too old, ephemeral onion services are not supported"
)
raise TorTooOldEphemeral()
if mode_settings.get("general", "client_auth") and not self.supports_stealth:
raise TorTooOld(
# strings._("error_stealth_not_supported")
print(
"Your version of Tor is too old, stealth onion services are not supported"
)
raise TorTooOldStealth()
auth_cookie = None
if mode_settings.get("general", "client_auth"):
@ -693,10 +676,8 @@ class Onion(object):
)
except ProtocolError as e:
raise TorErrorProtocolError(
# strings._("error_tor_protocol_error")
"Tor error: {}".format(e.args[0])
)
print("Tor error: {}".format(e.args[0]))
raise TorErrorProtocolError(e.args[0])
onion_host = res.service_id + ".onion"

View File

@ -21,7 +21,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import os, shutil
from . import common
from .onion import TorTooOld, TorErrorProtocolError
from .common import AutoStopTimer

View File

@ -51,17 +51,7 @@ Download Tor Browser and extract the binaries:
python scripts\get-tor-windows.py
```
### Prepare the code
In order to work with the desktop app, you'll need to build a wheel of the CLI package first, and copy it into the `desktop` folder:
```sh
cd ../cli
poetry install
poetry build
cp dist/onionshare_cli-*.whl ../desktop
cd ../desktop
```
### Prepare the virtual environment
OnionShare uses [Briefcase](https://briefcase.readthedocs.io/en/latest/).
@ -86,19 +76,30 @@ While your virtual environment is active, install briefcase from pip.
pip install briefcase
```
Run OnionShare from the source tree like this (`-d` re-installs dependencies, which you'll have to do each time you update the `onionshare-cli` wheel):
In order to work with the desktop app, you'll need to build a wheel of the CLI package first, and copy it into the `desktop` folder. You'll need to re-run this script each time you change the CLI code.
```sh
python scripts/rebuild-cli.py
```
### Running OnionShare from the source code tree
Inside the virtual environment, run OnionShare like this to install all of the dependencies:
```
briefcase dev -d
```
If you want to pass arguments into `onionshare`, such as to use verbose mode:
Once you have the dependencies installed, you can run it using the `dev.sh` script, which lets you use command line arguments, such as to `--verbose` or `--local-only`:
```
cd src
python -c "import onionshare; onionshare.main()" --help
./scripts/dev.sh --help
./scripts/dev.sh -v
./scripts/dev.sh -v --local-only
```
Windows uses `scripts\dev.bat` instead.
## Running tests
Install these packages inside your virtual environment:

3
desktop/scripts/dev.bat Normal file
View File

@ -0,0 +1,3 @@
cd src
python -c "import onionshare; onionshare.main()" %*
cd ..

9
desktop/scripts/dev.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
# Run OnionShare desktop, allowing you to use command-line arguments
SCRIPTS_DIR="$( cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
cd $SCRIPTS_DIR
cd ../src
python -c "import onionshare; onionshare.main()" $@

45
desktop/scripts/rebuild-cli.py Executable file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""
This script builds the CLI python wheel, copies it to the desktop folder,
and installs it in the virtual environment.
"""
import inspect
import os
import sys
import glob
import subprocess
import shutil
def main():
# Build paths
root_path = os.path.dirname(
os.path.dirname(
os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
)
)
cli_path = os.path.join(root_path, "cli")
desktop_path = os.path.join(root_path, "desktop")
# Delete old wheels
for filename in glob.glob(os.path.join(cli_path, "dist", "*.whl")):
os.remove(filename)
# Build new wheel
subprocess.call(["poetry", "install"], cwd=cli_path)
subprocess.call(["poetry", "build"], cwd=cli_path)
wheel_filename = glob.glob(os.path.join(cli_path, "dist", "*.whl"))[0]
wheel_basename = os.path.basename(wheel_filename)
shutil.copyfile(
wheel_filename,
os.path.join(desktop_path, wheel_basename),
)
# Reinstall the new wheel
subprocess.call(["pip", "uninstall", "onionshare-cli", "-y"])
subprocess.call(["pip", "install", os.path.join(desktop_path, wheel_basename)])
if __name__ == "__main__":
main()

View File

@ -24,7 +24,22 @@ import shutil
from pkg_resources import resource_filename
from . import strings
from onionshare_cli.onion import Onion
from onionshare_cli.onion import (
Onion,
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
TorErrorSocketFile,
TorErrorMissingPassword,
TorErrorUnreadableCookieFile,
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
BundledTorBroken,
TorTooOldEphemeral,
TorTooOldStealth,
PortNotAvailable,
)
class GuiCommon:
@ -245,7 +260,7 @@ class GuiCommon:
QLabel {
text-align: center;
color: #333333;
font-size: 28px;
font-size: 25px;
}
""",
# Share mode and child widget styles
@ -377,3 +392,37 @@ class GuiCommon:
Returns the absolute path of a resource
"""
return resource_filename("onionshare", os.path.join("resources", filename))
@staticmethod
def get_translated_tor_error(e):
"""
Takes an exception defined in onion.py and returns a translated error message
"""
if type(e) is TorErrorInvalidSetting:
return strings._("settings_error_unknown")
elif type(e) is TorErrorAutomatic:
return strings._("settings_error_automatic")
elif type(e) is TorErrorSocketPort:
return strings._("settings_error_socket_port").format(e.args[0], e.args[1])
elif type(e) is TorErrorSocketFile:
return strings._("settings_error_socket_file").format(e.args[0])
elif type(e) is TorErrorMissingPassword:
return strings._("settings_error_missing_password")
elif type(e) is TorErrorUnreadableCookieFile:
return strings._("settings_error_unreadable_cookie_file")
elif type(e) is TorErrorAuthError:
return strings._("settings_error_auth").format(e.args[0], e.args[1])
elif type(e) is TorErrorProtocolError:
return strings._("error_tor_protocol_error").format(e.args[0])
elif type(e) is BundledTorTimeout:
return strings._("settings_error_bundled_tor_timeout")
elif type(e) is BundledTorBroken:
return strings._("settings_error_bundled_tor_broken").format(e.args[0])
elif type(e) is TorTooOldEphemeral:
return strings._("error_ephemeral_not_supported")
elif type(e) is TorTooOldStealth:
return strings._("error_stealth_not_supported")
elif type(e) is PortNotAvailable:
return strings._("error_port_not_available")
return None

View File

@ -189,5 +189,6 @@
"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:\n{}",
"gui_rendezvous_cleanup": "Waiting for Tor circuits to close to be sure your files have successfully transferred.\n\nThis might take a few minutes.",
"gui_rendezvous_cleanup_quit_early": "Quit Early"
"gui_rendezvous_cleanup_quit_early": "Quit Early",
"error_port_not_available": "OnionShare port not available"
}

View File

@ -27,11 +27,31 @@ import os
from onionshare_cli import common
from onionshare_cli.settings import Settings
from onionshare_cli.onion import *
from onionshare_cli.onion import (
Onion,
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
TorErrorSocketFile,
TorErrorMissingPassword,
TorErrorUnreadableCookieFile,
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
BundledTorBroken,
TorTooOldEphemeral,
TorTooOldStealth,
PortNotAvailable,
)
from . import strings
from .widgets import Alert
from .update_checker import *
from .update_checker import (
UpdateCheckerCheckError,
UpdateCheckerInvalidLatestVersion,
UpdateChecker,
UpdateThread,
)
from .tor_connection_dialog import TorConnectionDialog
from .gui_common import GuiCommon
@ -142,7 +162,7 @@ class SettingsDialog(QtWidgets.QDialog):
self.tor_geo_ip_file_path,
self.tor_geo_ipv6_file_path,
self.obfs4proxy_file_path,
) = self.common.get_tor_paths()
) = self.common.gui.get_tor_paths()
if not self.obfs4proxy_file_path or not os.path.isfile(
self.obfs4proxy_file_path
):
@ -165,7 +185,7 @@ class SettingsDialog(QtWidgets.QDialog):
self.tor_geo_ip_file_path,
self.tor_geo_ipv6_file_path,
self.obfs4proxy_file_path,
) = self.common.get_tor_paths()
) = self.common.gui.get_tor_paths()
if not self.obfs4proxy_file_path or not os.path.isfile(
self.obfs4proxy_file_path
):
@ -698,10 +718,18 @@ class SettingsDialog(QtWidgets.QDialog):
TorErrorUnreadableCookieFile,
TorErrorAuthError,
TorErrorProtocolError,
BundledTorNotSupported,
BundledTorTimeout,
BundledTorBroken,
TorTooOldEphemeral,
TorTooOldStealth,
PortNotAvailable,
) as e:
Alert(self.common, e.args[0], QtWidgets.QMessageBox.Warning)
message = self.common.gui.get_translated_tor_error(e)
Alert(
self.common,
message,
QtWidgets.QMessageBox.Warning,
)
if settings.get("connection_type") == "bundled":
self.tor_status.hide()
self._enable_buttons()

View File

@ -183,8 +183,7 @@ class Mode(QtWidgets.QWidget):
self.status_bar.clearMessage()
if not self.app.autostop_timer_thread.is_alive():
if self.autostop_timer_finished_should_stop_server():
self.server_status.stop_server()
self.autostop_timer_finished_should_stop_server()
def timer_callback_custom(self):
"""

View File

@ -53,16 +53,22 @@ class NewTabButton(QtWidgets.QPushButton):
)
self.image_label.setAlignment(QtCore.Qt.AlignCenter)
self.image_label.setStyleSheet(self.common.gui.css["new_tab_button_image"])
self.image_label.setGeometry(0, 0, self.width(), 200)
self.image_label.setGeometry(0, 0, self.width(), 190)
self.image_label.show()
# Title
self.title_label = QtWidgets.QLabel(title, parent=self)
self.title_label.setWordWrap(True)
self.title_label.setAlignment(QtCore.Qt.AlignCenter)
self.title_label.setStyleSheet(self.common.gui.css["new_tab_title_text"])
self.title_label.setGeometry(
(self.width() - 250) / 2, self.height() - 100, 250, 30
)
if self.title_label.sizeHint().width() >= 250:
self.title_label.setGeometry(
(self.width() - 250) / 2, self.height() - 120, 250, 60
)
else:
self.title_label.setGeometry(
(self.width() - 250) / 2, self.height() - 100, 250, 30
)
self.title_label.show()
# Text

View File

@ -24,7 +24,6 @@ import os
from PySide2 import QtCore
from onionshare_cli.onion import (
TorTooOld,
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
@ -34,6 +33,10 @@ from onionshare_cli.onion import (
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
BundledTorBroken,
TorTooOldEphemeral,
TorTooOldStealth,
PortNotAvailable,
)
from . import strings
@ -93,7 +96,6 @@ class OnionThread(QtCore.QThread):
self.success.emit()
except (
TorTooOld,
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
@ -103,9 +105,13 @@ class OnionThread(QtCore.QThread):
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
OSError,
BundledTorBroken,
TorTooOldEphemeral,
TorTooOldStealth,
PortNotAvailable,
) as e:
self.error.emit(e.args[0])
message = self.mode.common.gui.get_translated_tor_error(e)
self.error.emit(message)
return

View File

@ -18,9 +18,25 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import time
from PySide2 import QtCore, QtWidgets, QtGui
from onionshare_cli.onion import *
from onionshare_cli.onion import (
BundledTorCanceled,
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
TorErrorSocketFile,
TorErrorMissingPassword,
TorErrorUnreadableCookieFile,
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
BundledTorBroken,
TorTooOldEphemeral,
TorTooOldStealth,
PortNotAvailable,
)
from . import strings
from .gui_common import GuiCommon
@ -156,9 +172,26 @@ class TorConnectionThread(QtCore.QThread):
)
self.canceled_connecting_to_tor.emit()
except Exception as e:
self.common.log("TorConnectionThread", "run", f"caught exception: {e}")
self.error_connecting_to_tor.emit(str(e))
except (
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
TorErrorSocketFile,
TorErrorMissingPassword,
TorErrorUnreadableCookieFile,
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
BundledTorBroken,
TorTooOldEphemeral,
TorTooOldStealth,
PortNotAvailable,
) as e:
message = self.common.gui.get_translated_tor_error(e)
self.common.log(
"TorConnectionThread", "run", f"caught exception: {message}"
)
self.error_connecting_to_tor.emit(message)
def _tor_status_update(self, progress, summary):
self.tor_status_update.emit(progress, summary)