From 89aa0d64152ecec880cd145065c590bcf904db09 Mon Sep 17 00:00:00 2001 From: choltz95 Date: Tue, 2 Aug 2016 01:43:17 -0400 Subject: [PATCH 01/43] wrap progress bar --- onionshare_gui/downloads.py | 2 +- onionshare_gui/onionshare_gui.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/downloads.py b/onionshare_gui/downloads.py index 265b9bf7..7b6016c0 100644 --- a/onionshare_gui/downloads.py +++ b/onionshare_gui/downloads.py @@ -103,7 +103,7 @@ class Downloads(QtWidgets.QVBoxLayout): # add it to the list download = Download(download_id, total_bytes) self.downloads[download_id] = download - self.addWidget(download.progress_bar) + self.insertWidget(-1, download.progress_bar) def update_download(self, download_id, downloaded_bytes): """ diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 1eb508bc..bad561ac 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -112,10 +112,19 @@ class OnionShareGui(QtWidgets.QMainWindow): # main layout self.layout = QtWidgets.QVBoxLayout() + + self.downloads_layout = QtWidgets.QGroupBox() + self.downloads_layout.setLayout(self.downloads) + self.downloads_layout_container = QtWidgets.QScrollArea() + self.downloads_layout_container.setWidget(self.downloads_layout) + self.downloads_layout_container.setWidgetResizable(True) + self.downloads_layout_container.setFixedHeight(80) + self.vbar = self.downloads_layout_container.verticalScrollBar() + self.layout.addLayout(self.file_selection) self.layout.addLayout(self.server_status) self.layout.addWidget(self.filesize_warning) - self.layout.addLayout(self.downloads) + self.layout.addWidget(self.downloads_layout_container) self.layout.addLayout(self.options) central_widget = QtWidgets.QWidget() central_widget.setLayout(self.layout) @@ -210,7 +219,7 @@ class OnionShareGui(QtWidgets.QMainWindow): events.append(r) except web.queue.Empty: done = True - + self.vbar.setValue(self.vbar.maximum()) for event in events: if event["type"] == web.REQUEST_LOAD: self.status_bar.showMessage(strings._('download_page_loaded', True)) From 221bb31b68ef3f006964c132e8188fd8ddaf0ff5 Mon Sep 17 00:00:00 2001 From: choltz95 Date: Tue, 2 Aug 2016 16:43:40 +0000 Subject: [PATCH 02/43] reorganize, add comment --- onionshare_gui/downloads.py | 2 +- onionshare_gui/onionshare_gui.py | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/onionshare_gui/downloads.py b/onionshare_gui/downloads.py index 7b6016c0..7729f8c7 100644 --- a/onionshare_gui/downloads.py +++ b/onionshare_gui/downloads.py @@ -56,7 +56,7 @@ class Download(object): elapsed = time.time() - self.started if elapsed < 10: # Wait a couple of seconds for the download rate to stabilize. - # This prevents an "Windows copy dialog"-esque experience at + # This prevents a "Windows copy dialog"-esque experience at # the beginning of the download. pb_fmt = strings._('gui_download_progress_starting').format( helpers.human_readable_filesize(downloaded_bytes)) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index bad561ac..146856e3 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -98,6 +98,13 @@ class OnionShareGui(QtWidgets.QMainWindow): # downloads self.downloads = Downloads() + self.downloads_layout = QtWidgets.QGroupBox() + self.downloads_layout.setLayout(self.downloads) + self.downloads_layout_container = QtWidgets.QScrollArea() + self.downloads_layout_container.setWidget(self.downloads_layout) + self.downloads_layout_container.setWidgetResizable(True) + self.downloads_layout_container.setFixedHeight(80) + self.vbar = self.downloads_layout_container.verticalScrollBar() # options self.options = Options(web, self.app) @@ -108,19 +115,10 @@ class OnionShareGui(QtWidgets.QMainWindow): version_label = QtWidgets.QLabel('v{0:s}'.format(helpers.get_version())) version_label.setStyleSheet('color: #666666; padding: 0 10px;') self.status_bar.addPermanentWidget(version_label) - self.setStatusBar(self.status_bar) + self.setStatusBar(self.status_bar) # main layout self.layout = QtWidgets.QVBoxLayout() - - self.downloads_layout = QtWidgets.QGroupBox() - self.downloads_layout.setLayout(self.downloads) - self.downloads_layout_container = QtWidgets.QScrollArea() - self.downloads_layout_container.setWidget(self.downloads_layout) - self.downloads_layout_container.setWidgetResizable(True) - self.downloads_layout_container.setFixedHeight(80) - self.vbar = self.downloads_layout_container.verticalScrollBar() - self.layout.addLayout(self.file_selection) self.layout.addLayout(self.server_status) self.layout.addWidget(self.filesize_warning) @@ -219,7 +217,10 @@ class OnionShareGui(QtWidgets.QMainWindow): events.append(r) except web.queue.Empty: done = True + + # scroll to the bottom of the dl progress bar log pane self.vbar.setValue(self.vbar.maximum()) + for event in events: if event["type"] == web.REQUEST_LOAD: self.status_bar.showMessage(strings._('download_page_loaded', True)) From 1cd42feb456697d67980273af245fb0e1abccc19 Mon Sep 17 00:00:00 2001 From: choltz95 Date: Tue, 2 Aug 2016 23:46:13 -0400 Subject: [PATCH 03/43] very poor auto scrolling --- onionshare_gui/onionshare_gui.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 146856e3..f4019de8 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -105,6 +105,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.downloads_layout_container.setWidgetResizable(True) self.downloads_layout_container.setFixedHeight(80) self.vbar = self.downloads_layout_container.verticalScrollBar() + self.new_download = False # options self.options = Options(web, self.app) @@ -202,8 +203,13 @@ class OnionShareGui(QtWidgets.QMainWindow): def check_for_requests(self): """ Check for messages communicated from the web app, and update the GUI accordingly. - """ + """ self.update() + # scroll to the bottom of the dl progress bar log pane + # if a new download has been added + if self.new_download: + self.vbar.setValue(self.vbar.maximum()) + self.new_download = False # only check for requests if the server is running if self.server_status.status != self.server_status.STATUS_STARTED: return @@ -218,15 +224,13 @@ class OnionShareGui(QtWidgets.QMainWindow): except web.queue.Empty: done = True - # scroll to the bottom of the dl progress bar log pane - self.vbar.setValue(self.vbar.maximum()) - for event in events: if event["type"] == web.REQUEST_LOAD: self.status_bar.showMessage(strings._('download_page_loaded', True)) elif event["type"] == web.REQUEST_DOWNLOAD: self.downloads.add_download(event["data"]["id"], web.zip_filesize) + self.new_download = True elif event["type"] == web.REQUEST_RATE_LIMIT: self.stop_server() @@ -234,7 +238,7 @@ class OnionShareGui(QtWidgets.QMainWindow): elif event["type"] == web.REQUEST_PROGRESS: self.downloads.update_download(event["data"]["id"], event["data"]["bytes"]) - + # is the download complete? if event["data"]["bytes"] == web.zip_filesize: # close on finish? From 12591c2cab42fd95991b2a1dbb3ca7bd311a1fbb Mon Sep 17 00:00:00 2001 From: Srinivas Devaki Date: Wed, 24 Aug 2016 01:43:21 +0530 Subject: [PATCH 04/43] added progress bar for zipping files --- onionshare/helpers.py | 11 +++++- onionshare/web.py | 4 +- onionshare_gui/onionshare_gui.py | 66 +++++++++++++++++++++++++++++++- resources/locale/cs.json | 3 +- resources/locale/en.json | 3 +- resources/locale/eo.json | 3 +- resources/locale/fi.json | 3 +- resources/locale/it.json | 5 ++- resources/locale/nl.json | 3 +- resources/locale/tr.json | 3 +- 10 files changed, 91 insertions(+), 13 deletions(-) diff --git a/onionshare/helpers.py b/onionshare/helpers.py index 814d0207..3ec785f2 100644 --- a/onionshare/helpers.py +++ b/onionshare/helpers.py @@ -176,19 +176,26 @@ class ZipWriter(object): with. If a zip_filename is not passed in, it will use the default onionshare filename. """ - def __init__(self, zip_filename=None): + def __init__(self, zip_filename=None, processed_size_callback=None): if zip_filename: self.zip_filename = zip_filename else: self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), random_string(4, 6)) self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True) + self.processed_size_callback = processed_size_callback + if self.processed_size_callback is None: + self.processed_size_callback = lambda _: None + self._size = 0 + self.processed_size_callback(self._size) def add_file(self, filename): """ Add a file to the zip archive. """ self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED) + self._size += os.path.getsize(filename) + self.processed_size_callback(self._size) def add_dir(self, filename): """ @@ -201,6 +208,8 @@ class ZipWriter(object): if not os.path.islink(full_filename): arc_filename = full_filename[len(dir_to_strip):] self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED) + self._size += os.path.getsize(full_filename) + self.processed_size_callback(self._size) def close(self): """ diff --git a/onionshare/web.py b/onionshare/web.py index 958e8c3a..304c6148 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -31,7 +31,7 @@ zip_filename = None zip_filesize = None -def set_file_info(filenames): +def set_file_info(filenames, processed_size_callback=None): """ Using the list of filenames being shared, fill in details that the web page will need to display. This includes zipping up the file in order to @@ -58,7 +58,7 @@ def set_file_info(filenames): file_info['dirs'] = sorted(file_info['dirs'], key=lambda k: k['basename']) # zip up the files and folders - z = helpers.ZipWriter() + z = helpers.ZipWriter(processed_size_callback=processed_size_callback) for info in file_info['files']: z.add_file(info['filename']) for info in file_info['dirs']: diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 1eb508bc..d8ada3cf 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -110,6 +110,9 @@ class OnionShareGui(QtWidgets.QMainWindow): self.status_bar.addPermanentWidget(version_label) self.setStatusBar(self.status_bar) + # status bar, zip progress bar + self._zip_progress_bar = None + # main layout self.layout = QtWidgets.QVBoxLayout() self.layout.addLayout(self.file_selection) @@ -132,6 +135,9 @@ class OnionShareGui(QtWidgets.QMainWindow): Step 2 in starting the onionshare server. This displays the large filesize warning, if applicable. """ + if self._zip_progress_bar is not None: + self.status_bar.removeWidget(self._zip_progress_bar) + self._zip_progress_bar = None # warn about sending large files over Tor if web.zip_filesize >= 157286400: # 150mb self.filesize_warning.setText(strings._("large_filesize", True)) @@ -162,10 +168,22 @@ class OnionShareGui(QtWidgets.QMainWindow): t.daemon = True t.start() + self._zip_progress_bar = ZipProgressBar(0) + self.status_bar.clearMessage() + self.status_bar.insertWidget(0, self._zip_progress_bar) + # prepare the files for sending in a new thread def finish_starting_server(self): # prepare files to share - web.set_file_info(self.file_selection.file_list.filenames) + progress_bar = self._zip_progress_bar + progress_bar.total_files_size = OnionShareGui._compute_total_size( + self.file_selection.file_list.filenames) + + def _set_processed_size(x): + progress_bar.processed_size = x + + web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size) + self.app.cleanup_filenames.append(web.zip_filename) self.starting_server_step2.emit() @@ -177,11 +195,20 @@ class OnionShareGui(QtWidgets.QMainWindow): # done self.start_server_finished.emit() - self.status_bar.showMessage(strings._('gui_starting_server2', True)) t = threading.Thread(target=finish_starting_server, kwargs={'self': self}) t.daemon = True t.start() + @staticmethod + def _compute_total_size(filenames): + total_size = 0 + for filename in filenames: + if os.path.isfile(filename): + total_size += os.path.getsize(filename) + if os.path.isdir(filename): + total_size += helpers.dir_size(filename) + return total_size + def stop_server(self): """ Stop the onionshare server. @@ -268,6 +295,41 @@ class OnionShareGui(QtWidgets.QMainWindow): e.ignore() +class ZipProgressBar(QtWidgets.QProgressBar): + def __init__(self, total_files_size): + super(ZipProgressBar, self).__init__() + self.setMaximumHeight(15) + self.setValue(0) + self.setFormat(strings._('zip_progress_bar_format')) + self.setStyleSheet( + "QProgressBar::chunk { background-color: #05B8CC; }") + + self._total_files_size = total_files_size + self._processed_size = 0 + + @property + def total_files_size(self): + return self._total_files_size + + @total_files_size.setter + def total_files_size(self, val): + self._total_files_size = val + + @property + def processed_size(self): + return self._processed_size + + @processed_size.setter + def processed_size(self, val): + self._processed_size = val + if self.processed_size < self.total_files_size: + self.setValue(int((self.processed_size * 100) / self.total_files_size)) + elif self.total_files_size != 0: + self.setValue(100) + else: + self.setValue(0) + + def alert(msg, icon=QtWidgets.QMessageBox.NoIcon): """ Pop up a message in a dialog window. diff --git a/resources/locale/cs.json b/resources/locale/cs.json index 8ac426e4..70b2a66f 100644 --- a/resources/locale/cs.json +++ b/resources/locale/cs.json @@ -41,5 +41,6 @@ "gui_please_wait": "Prosím čekejte...", "error_hs_dir_cannot_create": "Nejde vytvořit složka {0:s} pro hidden service", "error_hs_dir_not_writable": "Nejde zapisovat do složky {0:s} pro hidden service", - "using_ephemeral": "Staring ephemeral Tor hidden service and awaiting publication" + "using_ephemeral": "Staring ephemeral Tor hidden service and awaiting publication", + "zip_progress_bar_format": "Crunching files: %p%" } diff --git a/resources/locale/en.json b/resources/locale/en.json index 88a0cbb3..e6552878 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -49,5 +49,6 @@ "gui_quit_warning": "Are you sure you want to quit?\nThe URL you are sharing won't exist anymore.", "gui_quit_warning_quit": "Quit", "gui_quit_warning_dont_quit": "Don't Quit", - "error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL." + "error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL.", + "zip_progress_bar_format": "Crunching files: %p%" } diff --git a/resources/locale/eo.json b/resources/locale/eo.json index 5970ab95..2fd1a14f 100644 --- a/resources/locale/eo.json +++ b/resources/locale/eo.json @@ -41,5 +41,6 @@ "gui_please_wait": "Bonvolu atendi...", "error_hs_dir_cannot_create": "Ne eblas krei hidden-service-dosierujon {0:s}", "error_hs_dir_not_writable": "Ne eblas konservi dosierojn en hidden-service-dosierujo {0:s}", - "using_ephemeral": "Staring ephemeral Tor hidden service and awaiting publication" + "using_ephemeral": "Staring ephemeral Tor hidden service and awaiting publication", + "zip_progress_bar_format": "Crunching files: %p%" } diff --git a/resources/locale/fi.json b/resources/locale/fi.json index cad0ddf7..7e1fb1df 100644 --- a/resources/locale/fi.json +++ b/resources/locale/fi.json @@ -41,5 +41,6 @@ "gui_please_wait": "Odota...", "error_hs_dir_cannot_create": "Piilopalvelulle ei pystytty luomaan hakemistoa {0:s}", "error_hs_dir_not_writable": "Piilopalvelun hakemistoon {0:s} ei voi kirjoittaa", - "using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua" + "using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua", + "zip_progress_bar_format": "Tiivistän tiedostoja: %p%" } diff --git a/resources/locale/it.json b/resources/locale/it.json index 00388980..973a0576 100644 --- a/resources/locale/it.json +++ b/resources/locale/it.json @@ -41,5 +41,6 @@ "gui_please_wait": "Attendere prego...", "error_hs_dir_cannot_create": "Impossibile create la cartella per il servizio nascosto {0:s}", "error_hs_dir_not_writable": "La cartella per il servizio nascosto {0:s} non ha i permessi di scrittura", - "using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione" -} \ No newline at end of file + "using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione", + "zip_progress_bar_format": "Elaborazione files: %p%" +} diff --git a/resources/locale/nl.json b/resources/locale/nl.json index 5da91706..da7370cf 100644 --- a/resources/locale/nl.json +++ b/resources/locale/nl.json @@ -41,5 +41,6 @@ "gui_please_wait": "Moment geduld...", "error_hs_dir_cannot_create": "Kan verborgen service map {0:s} niet aanmaken", "error_hs_dir_not_writable": "Verborgen service map {0:s} is niet schrijfbaar", - "using_ephemeral": "Kortstondige Tor hidden service gestart en in afwachting van publicatie" + "using_ephemeral": "Kortstondige Tor hidden service gestart en in afwachting van publicatie", + "zip_progress_bar_format": "Bestanden verwerken: %p%" } diff --git a/resources/locale/tr.json b/resources/locale/tr.json index 242f5038..0a953269 100644 --- a/resources/locale/tr.json +++ b/resources/locale/tr.json @@ -41,5 +41,6 @@ "gui_please_wait": "Lütfen bekleyin...", "error_hs_dir_cannot_create": "Gizli hizmet klasörü {0:s} oluşturulamıyor", "error_hs_dir_not_writable": "Gizle hizmet klasörü {0:s} yazılabilir değil", - "using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor" + "using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor", + "zip_progress_bar_format": "Dosyalar hazırlanıyor: %p%" } From a8749fe66f74051c3f30c64234b64311cd7d465c Mon Sep 17 00:00:00 2001 From: Srinivas Devaki Date: Wed, 24 Aug 2016 02:15:58 +0530 Subject: [PATCH 05/43] small refactoring --- onionshare_gui/onionshare_gui.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index d8ada3cf..149f7e87 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -168,20 +168,19 @@ class OnionShareGui(QtWidgets.QMainWindow): t.daemon = True t.start() + # add progress bar to the status bar, indicating the crunching of files. self._zip_progress_bar = ZipProgressBar(0) + self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size( + self.file_selection.file_list.filenames) self.status_bar.clearMessage() self.status_bar.insertWidget(0, self._zip_progress_bar) # prepare the files for sending in a new thread def finish_starting_server(self): - # prepare files to share - progress_bar = self._zip_progress_bar - progress_bar.total_files_size = OnionShareGui._compute_total_size( - self.file_selection.file_list.filenames) - def _set_processed_size(x): - progress_bar.processed_size = x + self._zip_progress_bar.processed_size = x + # prepare files to share web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size) self.app.cleanup_filenames.append(web.zip_filename) From 6459498da424d441539afad3dd8079028ba0e9aa Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 22 Dec 2016 13:39:32 -0800 Subject: [PATCH 06/43] Add support for Tor control port authentication --- onionshare/onion.py | 19 +++++++++++++++---- resources/locale/en.json | 2 ++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index b91105ef..b52429d3 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -20,6 +20,7 @@ along with this program. If not, see . from stem.control import Controller from stem import SocketError +from stem.connection import MissingPassword, UnreadableCookieFile import os, sys, tempfile, shutil, urllib from . import socks @@ -54,22 +55,32 @@ class Onion(object): self.cleanup_filenames = [] self.service_id = None - # connect to the tor controlport - found_tor = False - self.c = None + # if the TOR_CONTROL_PORT environment variable is set, use that + # otherwise, default to Tor Browser, Tor Messenger, and system tor ports env_port = os.environ.get('TOR_CONTROL_PORT') if env_port: ports = [int(env_port)] else: ports = [9151, 9153, 9051] + + # if the TOR_AUTHENTICATION_PASSWORD is set, use that to authenticate + password = os.environ.get('TOR_AUTHENTICATION_PASSWORD') + + # connect to the tor controlport + found_tor = False + self.c = None for port in ports: try: self.c = Controller.from_port(port=port) - self.c.authenticate() + self.c.authenticate(password) found_tor = True break except SocketError: pass + except MissingPassword: + raise NoTor(strings._("ctrlport_missing_password").format(str(ports))) + except UnreadableCookieFile: + raise NoTor(strings._("ctrlport_unreadable_cookie").format(str(ports))) if not found_tor: raise NoTor(strings._("cant_connect_ctrlport").format(str(ports))) diff --git a/resources/locale/en.json b/resources/locale/en.json index b35cf178..3d958dbf 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -2,6 +2,8 @@ "connecting_ctrlport": "Connecting to Tor control port to set up onion service on port {0:d}.", "cant_connect_ctrlport": "Can't connect to Tor control port on port {0:s}. OnionShare requires Tor Browser to be running in the background to work. If you don't have it you can get it from https://www.torproject.org/.", "cant_connect_socksport": "Can't connect to Tor SOCKS5 server on port {0:s}. OnionShare requires Tor Browser to be running in the background to work. If you don't have it you can get it from https://www.torproject.org/.", + "ctrlport_missing_password": "Connected to Tor control port on port {0:s}, but you require a password. You must have the TOR_AUTHENTICATION_PASSWORD environment variable set. Or just open Tor Browser in the background.", + "ctrlport_unreadable_cookie": "Connected to Tor control port on port {0:s}, but your user does not have permission to authenticate. You might want to add a HashedControlPassword to your torrc, and set the TOR_AUTHENTICATION_PASSWORD environment variable. Or just open Tor Browser in the background.", "preparing_files": "Preparing files to share.", "wait_for_hs": "Waiting for HS to be ready:", "wait_for_hs_trying": "Trying...", From 4e0879e230f8fa8272cd67a10af34683ef9d6a63 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 22 Dec 2016 13:43:07 -0800 Subject: [PATCH 07/43] Removing instructions from readme, and adding link to wiki instead --- README.md | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b3531938..8a3f5522 100644 --- a/README.md +++ b/README.md @@ -21,24 +21,8 @@ If you're interested in exactly what OnionShare does and does not protect agains ## Quick Start +Check out [the wiki](https://github.com/micahflee/onionshare/wiki) for information about how to use OnionShare and it's various features. + You can download OnionShare to install on your computer from . You can set up your development environment to build OnionShare yourself by following [these instructions](/BUILD.md). - -## How to Use - -Before you can share files, you need to open [Tor Browser](https://www.torproject.org/) in the background. This will provide the Tor service that OnionShare uses to start the onion service. - -Open OnionShare and drag and drop files and folders you wish to share, and click Start Sharing. It will show you a .onion URL such as `http://asxmi4q6i7pajg2b.onion/egg-cain` and copy it to your clipboard. This is the secret URL that can be used to download the file you're sharing. If you'd like multiple people to be able to download this file, uncheck the "close automatically" checkbox. - -Send this URL to the person you're trying to send the files to. If the files you're sending aren't secret, you can use normal means of sending the URL: emailing it, posting it to Facebook or Twitter, etc. If you're trying to send secret files then it's important to send this URL securely. - -The person who is receiving the files doesn't need OnionShare. All they need is to open the URL you send them in Tor Browser to be able to download the file. - -## Using the command line version - -In Linux: Just run `onionshare` from the terminal. - -In Windows: Add `C:\Program Files (x86)\OnionShare` to your PATH. Now you can run `onionshare.exe` in a command prompt. - -In Mac OS X: Run `ln -s /Applications/OnionShare.app/Contents/MacOS/onionshare /usr/local/bin`. Now you can run `onionshare` from the terminal. From c6a5515082bfa3e051c68aea6ff89ac5e06ff7cc Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 22 Dec 2016 15:15:37 -0800 Subject: [PATCH 08/43] Hide downloads progress bars until download starts, and improve the look of progress bar display --- onionshare_gui/downloads.py | 16 +++++----------- onionshare_gui/onionshare_gui.py | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/onionshare_gui/downloads.py b/onionshare_gui/downloads.py index 7729f8c7..1c54ab50 100644 --- a/onionshare_gui/downloads.py +++ b/onionshare_gui/downloads.py @@ -77,33 +77,27 @@ class Download(object): self.started) -class Downloads(QtWidgets.QVBoxLayout): +class Downloads(QtWidgets.QWidget): """ The downloads chunk of the GUI. This lists all of the active download progress bars. """ def __init__(self): super(Downloads, self).__init__() - self.downloads = {} - - # downloads label - self.downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True)) - self.downloads_label.hide() - - # add the widgets - self.addWidget(self.downloads_label) + self.layout = QtWidgets.QVBoxLayout() + self.setLayout(self.layout) def add_download(self, download_id, total_bytes): """ Add a new download progress bar. """ - self.downloads_label.show() + self.parent().show() # add it to the list download = Download(download_id, total_bytes) self.downloads[download_id] = download - self.insertWidget(-1, download.progress_bar) + self.layout.insertWidget(-1, download.progress_bar) def update_download(self, download_id, downloaded_bytes): """ diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 2f86632d..8a738d26 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -102,13 +102,12 @@ class OnionShareGui(QtWidgets.QMainWindow): # downloads self.downloads = Downloads() - self.downloads_layout = QtWidgets.QGroupBox() - self.downloads_layout.setLayout(self.downloads) - self.downloads_layout_container = QtWidgets.QScrollArea() - self.downloads_layout_container.setWidget(self.downloads_layout) - self.downloads_layout_container.setWidgetResizable(True) - self.downloads_layout_container.setFixedHeight(80) - self.vbar = self.downloads_layout_container.verticalScrollBar() + self.downloads_container = QtWidgets.QScrollArea() + self.downloads_container.setWidget(self.downloads) + self.downloads_container.setWidgetResizable(True) + self.downloads_container.setMaximumHeight(200) + self.vbar = self.downloads_container.verticalScrollBar() + self.downloads_container.hide() # downloads start out hidden self.new_download = False # options @@ -120,14 +119,14 @@ class OnionShareGui(QtWidgets.QMainWindow): version_label = QtWidgets.QLabel('v{0:s}'.format(helpers.get_version())) version_label.setStyleSheet('color: #666666; padding: 0 10px;') self.status_bar.addPermanentWidget(version_label) - self.setStatusBar(self.status_bar) + self.setStatusBar(self.status_bar) # main layout self.layout = QtWidgets.QVBoxLayout() self.layout.addLayout(self.file_selection) self.layout.addLayout(self.server_status) self.layout.addWidget(self.filesize_warning) - self.layout.addWidget(self.downloads_layout_container) + self.layout.addWidget(self.downloads_container) self.layout.addLayout(self.options) central_widget = QtWidgets.QWidget() central_widget.setLayout(self.layout) @@ -229,7 +228,7 @@ class OnionShareGui(QtWidgets.QMainWindow): def check_for_requests(self): """ Check for messages communicated from the web app, and update the GUI accordingly. - """ + """ self.update() # scroll to the bottom of the dl progress bar log pane # if a new download has been added @@ -255,6 +254,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.status_bar.showMessage(strings._('download_page_loaded', True)) elif event["type"] == web.REQUEST_DOWNLOAD: + self.downloads_container.show() # show the downloads layout self.downloads.add_download(event["data"]["id"], web.zip_filesize) self.new_download = True @@ -264,7 +264,7 @@ class OnionShareGui(QtWidgets.QMainWindow): elif event["type"] == web.REQUEST_PROGRESS: self.downloads.update_download(event["data"]["id"], event["data"]["bytes"]) - + # is the download complete? if event["data"]["bytes"] == web.zip_filesize: # close on finish? From ad0a618702d5e0395708ce9e352bb8f66c7b69ba Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 22 Dec 2016 16:19:54 -0800 Subject: [PATCH 09/43] Added dev scripts for launching onionshare and onionshare-gui from the source code tree, without having to install it --- BUILD.md | 4 ++-- dev_scripts/onionshare | 28 ++++++++++++++++++++++++++++ dev_scripts/onionshare-gui | 28 ++++++++++++++++++++++++++++ onionshare/helpers.py | 9 ++++++--- 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100755 dev_scripts/onionshare create mode 100755 dev_scripts/onionshare-gui diff --git a/BUILD.md b/BUILD.md index 668e386a..69bb4db5 100644 --- a/BUILD.md +++ b/BUILD.md @@ -20,8 +20,8 @@ sudo apt-get install -y python3-flask python3-stem python3-pyqt5 python-nautilus After that you can try both the CLI and the GUI version of OnionShare: ```sh -./install/scripts/onionshare -./install/scripts/onionshare-gui +./dev_scripts/onionshare +./dev_scripts/onionshare-gui ``` A script to build a .deb package and install OnionShare easily is also provided for your convenience: diff --git a/dev_scripts/onionshare b/dev_scripts/onionshare new file mode 100755 index 00000000..b94d1139 --- /dev/null +++ b/dev_scripts/onionshare @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2016 Micah Lee + +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 . +""" + +# Load onionshare module and resources from the source code tree +import os, sys +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.onionshare_dev_mode = True + +import onionshare +onionshare.main() diff --git a/dev_scripts/onionshare-gui b/dev_scripts/onionshare-gui new file mode 100755 index 00000000..6d610bb6 --- /dev/null +++ b/dev_scripts/onionshare-gui @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2016 Micah Lee + +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 . +""" + +# Load onionshare module and resources from the source code tree +import os, sys +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.onionshare_dev_mode = True + +import onionshare_gui +onionshare_gui.main() diff --git a/onionshare/helpers.py b/onionshare/helpers.py index 6ad8f80d..5d321c0b 100644 --- a/onionshare/helpers.py +++ b/onionshare/helpers.py @@ -34,14 +34,17 @@ def get_resource_path(filename): systemwide, and whether regardless of platform """ p = get_platform() - if p == 'Linux' and sys.argv and sys.argv[0].startswith(sys.prefix): + + if getattr(sys, 'onionshare_dev_mode', False): + # Look for resources directory relative to python file + resources_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'resources') + + elif p == 'Linux' and sys.argv and sys.argv[0].startswith(sys.prefix): # OnionShare is installed systemwide in Linux resources_dir = os.path.join(sys.prefix, 'share/onionshare') elif getattr(sys, 'frozen', False): # Check if app is "frozen" with cx_Freeze # http://cx-freeze.readthedocs.io/en/latest/faq.html#using-data-files resources_dir = os.path.join(os.path.dirname(sys.executable), 'resources') - else: # Look for resources directory relative to python file - resources_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'resources') return os.path.join(resources_dir, filename) From 8432e1ef3d62fc43abe2e86cce99b532103e18e4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 22 Dec 2016 16:56:39 -0800 Subject: [PATCH 10/43] Add support for stealth onion services in CLI version --- onionshare/onion.py | 40 +++++++++++++++++++++++++++++++++++++--- onionshare/onionshare.py | 25 ++++++++++++++++++++----- resources/locale/en.json | 8 +++++--- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index b52429d3..3da2935d 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -33,6 +33,14 @@ class NoTor(Exception): """ pass +class TorTooOld(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. + """ + pass + class Onion(object): """ Onion is an abstraction layer for connecting to the Tor control port and @@ -48,8 +56,9 @@ class Onion(object): onion services are supported. If not, it falls back to modifying the Tor configuration. """ - def __init__(self, transparent_torification=False): + def __init__(self, transparent_torification=False, stealth=False): self.transparent_torification = transparent_torification + self.stealth = stealth # files and dirs to delete on shutdown self.cleanup_filenames = [] @@ -89,17 +98,42 @@ class Onion(object): list_ephemeral_hidden_services = getattr(self.c, "list_ephemeral_hidden_services", None) self.supports_ephemeral = callable(list_ephemeral_hidden_services) and tor_version >= '0.2.7.1' + # do the versions of stem and tor that I'm using support stealth onion services? + try: + res = self.c.create_ephemeral_hidden_service({1:1}, basic_auth={'onionshare':None}, await_publication=False) + tmp_service_id = res.content()[0][2].split('=')[1] + self.c.remove_ephemeral_hidden_service(tmp_service_id) + self.supports_stealth = True + except TypeError: + # ephemeral stealth onion services are not supported + self.supports_stealth = False + def start(self, port): """ Start a onion service on port 80, pointing to the given port, and return the onion hostname. """ - print(strings._("connecting_ctrlport").format(int(port))) + self.auth_string = None + if self.stealth and not self.supports_stealth: + raise TorTooOld(strings._('error_stealth_not_supported')) + + print(strings._("config_onion_service").format(int(port))) if self.supports_ephemeral: print(strings._('using_ephemeral')) - res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication = True) + + if self.stealth: + basic_auth = {'onionshare':None} + else: + basic_auth = None + + res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth) self.service_id = res.content()[0][2].split('=')[1] onion_host = self.service_id + '.onion' + + if self.stealth: + auth_cookie = res.content()[2][2].split('=')[1].split(':')[1] + self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie) + return onion_host else: diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 6dcc5ba0..57983354 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -27,7 +27,7 @@ class OnionShare(object): OnionShare is the main application class. Pass in options and run start_onion_service and it will do the magic. """ - def __init__(self, debug=False, local_only=False, stay_open=False, transparent_torification=False): + def __init__(self, debug=False, local_only=False, stay_open=False, transparent_torification=False, stealth=False): self.port = None self.onion = None self.hidserv_dir = None @@ -49,6 +49,9 @@ class OnionShare(object): # traffic automatically goes through Tor self.transparent_torification = transparent_torification + # use stealth onion service + self.stealth = stealth + def choose_port(self): """ Pick an un-used port in the range 17600-17650 to bind to. @@ -76,10 +79,13 @@ class OnionShare(object): return if not self.onion: - self.onion = onion.Onion(self.transparent_torification) + self.onion = onion.Onion(self.transparent_torification, self.stealth) self.onion_host = self.onion.start(self.port) + if self.stealth: + self.auth_string = self.onion.auth_string + def cleanup(self): """ Shut everything down and clean up temporary files, etc. @@ -115,6 +121,7 @@ def main(cwd=None): parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only")) parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open")) parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification")) + parser.add_argument('--stealth', action='store_true', dest='stealth', help=strings._("help_stealth")) parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug")) parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename')) args = parser.parse_args() @@ -127,6 +134,7 @@ def main(cwd=None): debug = bool(args.debug) stay_open = bool(args.stay_open) transparent_torification = bool(args.transparent_torification) + stealth = bool(args.stealth) # validation valid = True @@ -139,11 +147,13 @@ def main(cwd=None): # start the onionshare app try: - app = OnionShare(debug, local_only, stay_open, transparent_torification) + app = OnionShare(debug, local_only, stay_open, transparent_torification, stealth) app.choose_port() app.start_onion_service() except onion.NoTor as e: sys.exit(e.args[0]) + except onion.TorTooOld as e: + sys.exit(e.args[0]) # prepare files to share print(strings._("preparing_files")) @@ -171,8 +181,13 @@ def main(cwd=None): # Wait for web.generate_slug() to finish running time.sleep(0.2) - print(strings._("give_this_url")) - print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) + if(stealth): + print(strings._("give_this_url_stealth")) + print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) + print(app.auth_string) + else: + print(strings._("give_this_url")) + print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) print('') print(strings._("ctrlc_to_stop")) diff --git a/resources/locale/en.json b/resources/locale/en.json index 42cdc385..1fc1e7ab 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -1,5 +1,5 @@ { - "connecting_ctrlport": "Connecting to Tor control port to set up onion service on port {0:d}.", + "config_onion_service": "Configuring onion service on port {0:d}.", "cant_connect_ctrlport": "Can't connect to Tor control port on port {0:s}. OnionShare requires Tor Browser to be running in the background to work. If you don't have it you can get it from https://www.torproject.org/.", "cant_connect_socksport": "Can't connect to Tor SOCKS5 server on port {0:s}. OnionShare requires Tor Browser to be running in the background to work. If you don't have it you can get it from https://www.torproject.org/.", "ctrlport_missing_password": "Connected to Tor control port on port {0:s}, but you require a password. You must have the TOR_AUTHENTICATION_PASSWORD environment variable set. Or just open Tor Browser in the background.", @@ -10,6 +10,7 @@ "wait_for_hs_nope": "Not ready yet.", "wait_for_hs_yup": "Ready!", "give_this_url": "Give this URL to the person you're sending the file to:", + "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:", "ctrlc_to_stop": "Press Ctrl-C to stop server", "not_a_file": "{0:s} is not a file.", "download_page_loaded": "Download page loaded", @@ -19,10 +20,10 @@ "large_filesize": "Warning: Sending large files could take hours", "error_tails_invalid_port": "Invalid value, port must be an integer", "error_tails_unknown_root": "Unknown error with Tails root process", - "help_tails_port": "Tails only: port for opening firewall, starting onion service", "help_local_only": "Do not attempt to use tor: for development only", "help_stay_open": "Keep onion service running after download has finished", "help_transparent_torification": "My system is transparently torified", + "help_stealth": "Create stealth onion service (advanced)", "help_debug": "Log errors to disk", "help_filename": "List of files or folders to share", "gui_drag_and_drop": "Drag and drop\nfiles here", @@ -52,5 +53,6 @@ "gui_quit_warning_quit": "Quit", "gui_quit_warning_dont_quit": "Don't Quit", "error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL.", - "zip_progress_bar_format": "Crunching files: %p%" + "zip_progress_bar_format": "Crunching files: %p%", + "error_stealth_not_supported": "Your versions of tor or stem are too old. You need to upgrade them to create stealth onion services." } From 447b9739f6ee308dd750a98bb39f6421ea85baf1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 22 Dec 2016 17:07:01 -0800 Subject: [PATCH 11/43] Catch all exceptions when checking for stealth support, not just TypeError, to successfully identity old versions of stem --- onionshare/onion.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 3da2935d..40fc30ae 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -99,12 +99,16 @@ class Onion(object): self.supports_ephemeral = callable(list_ephemeral_hidden_services) and tor_version >= '0.2.7.1' # do the versions of stem and tor that I'm using support stealth onion services? + if self.stealth: + self.check_for_stealth_support() + + def check_for_stealth_support(self): try: res = self.c.create_ephemeral_hidden_service({1:1}, basic_auth={'onionshare':None}, await_publication=False) tmp_service_id = res.content()[0][2].split('=')[1] self.c.remove_ephemeral_hidden_service(tmp_service_id) self.supports_stealth = True - except TypeError: + except: # ephemeral stealth onion services are not supported self.supports_stealth = False From 0512e5e84b5be6c09f30499e69f1f167c08aab00 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 22 Dec 2016 17:47:05 -0800 Subject: [PATCH 12/43] Added support for stealth onion services in the GUI version --- onionshare_gui/onionshare_gui.py | 11 +++++++++++ onionshare_gui/options.py | 25 ++++++++++++++++++++++++- onionshare_gui/server_status.py | 19 +++++++++++++++++++ resources/locale/en.json | 5 ++++- 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index a6b81018..db32a957 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -91,6 +91,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.stop_server_finished.connect(self.server_status.stop_server_finished) self.file_selection.file_list.files_updated.connect(self.server_status.update) self.server_status.url_copied.connect(self.copy_url) + self.server_status.hidservauth_copied.connect(self.copy_hidservauth) self.starting_server_step2.connect(self.start_server_step2) self.starting_server_step3.connect(self.start_server_step3) self.starting_server_error.connect(self.start_server_error) @@ -154,6 +155,9 @@ class OnionShareGui(QtWidgets.QMainWindow): # pick an available local port for the http service to listen on self.app.choose_port() + # disable the stealth option + self.options.set_stealth_enabled(False) + # start onionshare http service in new thread t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open, self.app.transparent_torification)) t.daemon = True @@ -240,6 +244,7 @@ class OnionShareGui(QtWidgets.QMainWindow): web.stop(self.app.port) self.app.cleanup() self.filesize_warning.hide() + self.options.set_stealth_enabled(True) self.stop_server_finished.emit() @staticmethod @@ -310,6 +315,12 @@ class OnionShareGui(QtWidgets.QMainWindow): """ self.status_bar.showMessage(strings._('gui_copied_url', True), 2000) + def copy_hidservauth(self): + """ + When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. + """ + self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000) + def clear_message(self): """ Clear messages from the status bar. diff --git a/onionshare_gui/options.py b/onionshare_gui/options.py index 386ea853..74645fcc 100644 --- a/onionshare_gui/options.py +++ b/onionshare_gui/options.py @@ -21,7 +21,7 @@ from PyQt5 import QtCore, QtWidgets from onionshare import strings, helpers -class Options(QtWidgets.QHBoxLayout): +class Options(QtWidgets.QVBoxLayout): """ The extra onionshare options in the GUI. """ @@ -40,8 +40,15 @@ class Options(QtWidgets.QHBoxLayout): self.close_automatically.setText(strings._("close_on_finish", True)) self.close_automatically.stateChanged.connect(self.stay_open_changed) + # stealth + self.stealth = QtWidgets.QCheckBox() + self.stealth.setCheckState(QtCore.Qt.Unchecked) + self.stealth.setText(strings._("create_stealth", True)) + self.stealth.stateChanged.connect(self.stealth_changed) + # add the widgets self.addWidget(self.close_automatically) + self.addWidget(self.stealth) def stay_open_changed(self, state): """ @@ -53,3 +60,19 @@ class Options(QtWidgets.QHBoxLayout): else: self.web.set_stay_open(True) self.app.stay_open = True + + def stealth_changed(self, state): + """ + When the 'stealth' checkbox is toggled, let the onionshare app know. + """ + if state == 2: + self.app.stealth = True + else: + self.app.stealth = False + + def set_stealth_enabled(self, enabled): + """ + You cannot toggle stealth after an onion service has started. This method + disables and re-enabled the stealth checkbox. + """ + self.stealth.setEnabled(enabled) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index ee4c5d3f..20c29a01 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -29,6 +29,7 @@ class ServerStatus(QtWidgets.QVBoxLayout): server_started = QtCore.pyqtSignal() server_stopped = QtCore.pyqtSignal() url_copied = QtCore.pyqtSignal() + hidservauth_copied = QtCore.pyqtSignal() STATUS_STOPPED = 0 STATUS_WORKING = 1 @@ -63,9 +64,12 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.url_label.setAlignment(QtCore.Qt.AlignCenter) self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True)) self.copy_url_button.clicked.connect(self.copy_url) + self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) + self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth) url_layout = QtWidgets.QHBoxLayout() url_layout.addWidget(self.url_label) url_layout.addWidget(self.copy_url_button) + url_layout.addWidget(self.copy_hidservauth_button) # add the widgets self.addLayout(server_layout) @@ -91,12 +95,18 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.url_label.show() self.copy_url_button.show() + if self.app.stealth: + self.copy_hidservauth_button.show() + else: + self.copy_hidservauth_button.hide() + # resize parent widget p = self.parentWidget() p.resize(p.sizeHint()) else: self.url_label.hide() self.copy_url_button.hide() + self.copy_hidservauth_button.hide() # button if self.file_selection.get_num_files() == 0: @@ -163,3 +173,12 @@ class ServerStatus(QtWidgets.QVBoxLayout): clipboard.setText(url) self.url_copied.emit() + + def copy_hidservauth(self): + """ + Copy the HidServAuth line to the clipboard. + """ + clipboard = self.qtapp.clipboard() + clipboard.setText(self.app.auth_string) + + self.hidservauth_copied.emit() diff --git a/resources/locale/en.json b/resources/locale/en.json index 1fc1e7ab..41a398a9 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -35,9 +35,11 @@ "gui_start_server": "Start Sharing", "gui_stop_server": "Stop Sharing", "gui_copy_url": "Copy URL", + "gui_copy_hidservauth": "Copy HidServAuth", "gui_downloads": "Downloads:", "gui_canceled": "Canceled", "gui_copied_url": "Copied URL to clipboard", + "gui_copied_hidservauth": "Copied HidServAuth line to clipboard", "gui_starting_server1": "Starting Tor onion service...", "gui_starting_server2": "Crunching files...", "gui_starting_server3": "Waiting for Tor onion service...", @@ -54,5 +56,6 @@ "gui_quit_warning_dont_quit": "Don't Quit", "error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL.", "zip_progress_bar_format": "Crunching files: %p%", - "error_stealth_not_supported": "Your versions of tor or stem are too old. You need to upgrade them to create stealth onion services." + "error_stealth_not_supported": "Your versions of tor or stem are too old. You need to upgrade them to create stealth onion services.", + "create_stealth": "Create stealth onion service (advanced)" } From 5241d756bf9a3b57f1123cae0a06a0bf77f1582c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 23 Dec 2016 19:08:18 -0800 Subject: [PATCH 13/43] Properly handle errors for using stealth onion services in the GUI, instead of crashing in the background (#144) --- onionshare/onionshare.py | 5 +++++ onionshare_gui/onionshare_gui.py | 4 ++++ onionshare_gui/options.py | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 57983354..c87b7811 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -50,7 +50,12 @@ class OnionShare(object): self.transparent_torification = transparent_torification # use stealth onion service + self.set_stealth(stealth) + + def set_stealth(self, stealth): self.stealth = stealth + if self.onion: + self.onion.stealth = stealth def choose_port(self): """ diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index db32a957..f002903a 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -176,6 +176,10 @@ class OnionShareGui(QtWidgets.QMainWindow): self.starting_server_error.emit(e.args[0]) return + except onionshare.onion.TorTooOld as e: + self.starting_server_error.emit(e.args[0]) + return + t = threading.Thread(target=start_onion_service, kwargs={'self': self}) t.daemon = True t.start() diff --git a/onionshare_gui/options.py b/onionshare_gui/options.py index 74645fcc..0f4b626c 100644 --- a/onionshare_gui/options.py +++ b/onionshare_gui/options.py @@ -66,9 +66,9 @@ class Options(QtWidgets.QVBoxLayout): When the 'stealth' checkbox is toggled, let the onionshare app know. """ if state == 2: - self.app.stealth = True + self.app.set_stealth(True) else: - self.app.stealth = False + self.app.set_stealth(False) def set_stealth_enabled(self, enabled): """ From 0061f00f49d68bcc3f48dfda5686e064663c6f87 Mon Sep 17 00:00:00 2001 From: Sigma Date: Sun, 25 Dec 2016 22:27:47 -0800 Subject: [PATCH 14/43] Support stem versions older than 1.5.0 again. Fix for issue #332 --- onionshare/onion.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 40fc30ae..05d479b2 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -130,7 +130,12 @@ class Onion(object): else: basic_auth = None - res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth) + if basic_auth != None : + res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth) + 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=True) + self.service_id = res.content()[0][2].split('=')[1] onion_host = self.service_id + '.onion' From 142387e34f7cd34fb2fd90e134229527cf999e05 Mon Sep 17 00:00:00 2001 From: Sigma Date: Mon, 26 Dec 2016 12:49:28 -0800 Subject: [PATCH 15/43] Fix for zipprogress bar qwidget access from a thread, which is invalid as qwidgets are neither reentrant nor thread safe. Fixes issue #334 --- onionshare_gui/onionshare_gui.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index f002903a..6a7da111 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -20,6 +20,7 @@ along with this program. If not, see . from __future__ import division import os, sys, subprocess, inspect, platform, argparse, threading, time, math, inspect, platform from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import pyqtSlot import onionshare from onionshare import strings, helpers, web @@ -199,7 +200,8 @@ class OnionShareGui(QtWidgets.QMainWindow): def finish_starting_server(self): # prepare files to share def _set_processed_size(x): - self._zip_progress_bar.processed_size = x + if self._zip_progress_bar != None: + self._zip_progress_bar.update_processed_size_signal.emit(x) web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size) self.app.cleanup_filenames.append(web.zip_filename) self.starting_server_step3.emit() @@ -351,6 +353,8 @@ class OnionShareGui(QtWidgets.QMainWindow): class ZipProgressBar(QtWidgets.QProgressBar): + update_processed_size_signal = QtCore.pyqtSignal(int) + def __init__(self, total_files_size): super(ZipProgressBar, self).__init__() self.setMaximumHeight(15) @@ -364,6 +368,8 @@ class ZipProgressBar(QtWidgets.QProgressBar): self._total_files_size = total_files_size self._processed_size = 0 + self.update_processed_size_signal.connect(self.update_processed_size) + @property def total_files_size(self): return self._total_files_size @@ -378,6 +384,9 @@ class ZipProgressBar(QtWidgets.QProgressBar): @processed_size.setter def processed_size(self, val): + self.update_processed_size(val) + + def update_processed_size(self, val): self._processed_size = val if self.processed_size < self.total_files_size: self.setValue(int((self.processed_size * 100) / self.total_files_size)) From 3b686b2723737d4ddc020c8485b4e62ed2d762a2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 28 Dec 2016 09:55:14 -0800 Subject: [PATCH 16/43] Put stealth option in a separate advanced group, in anticipation of other advanced options --- onionshare_gui/options.py | 13 +++++++++++-- resources/locale/en.json | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/options.py b/onionshare_gui/options.py index 0f4b626c..538f562d 100644 --- a/onionshare_gui/options.py +++ b/onionshare_gui/options.py @@ -43,12 +43,21 @@ class Options(QtWidgets.QVBoxLayout): # stealth self.stealth = QtWidgets.QCheckBox() self.stealth.setCheckState(QtCore.Qt.Unchecked) - self.stealth.setText(strings._("create_stealth", True)) + self.stealth.setText(strings._("gui_create_stealth", True)) self.stealth.stateChanged.connect(self.stealth_changed) + # advanced options group + advanced_group = QtWidgets.QGroupBox(strings._("gui_advanced_options", True)) + advanced_group.setCheckable(True) + advanced_group.setChecked(False) + advanced_group.setFlat(True) + advanced_group_layout = QtWidgets.QVBoxLayout() + advanced_group_layout.addWidget(self.stealth) + advanced_group.setLayout(advanced_group_layout) + # add the widgets self.addWidget(self.close_automatically) - self.addWidget(self.stealth) + self.addWidget(advanced_group) def stay_open_changed(self, state): """ diff --git a/resources/locale/en.json b/resources/locale/en.json index 41a398a9..3c9dcd17 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -57,5 +57,6 @@ "error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL.", "zip_progress_bar_format": "Crunching files: %p%", "error_stealth_not_supported": "Your versions of tor or stem are too old. You need to upgrade them to create stealth onion services.", - "create_stealth": "Create stealth onion service (advanced)" + "gui_advanced_options": "Advanced options", + "gui_create_stealth": "Create stealth onion service" } From 6fedeeb892e917a53325d59911f866d2a4b875b6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 28 Dec 2016 14:43:47 -0800 Subject: [PATCH 17/43] When you uncheck the advanced options checkbox, also uncheck all advanced options --- onionshare_gui/options.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/onionshare_gui/options.py b/onionshare_gui/options.py index 538f562d..f1acff25 100644 --- a/onionshare_gui/options.py +++ b/onionshare_gui/options.py @@ -51,6 +51,7 @@ class Options(QtWidgets.QVBoxLayout): advanced_group.setCheckable(True) advanced_group.setChecked(False) advanced_group.setFlat(True) + advanced_group.toggled.connect(self.advanced_options_changed) advanced_group_layout = QtWidgets.QVBoxLayout() advanced_group_layout.addWidget(self.stealth) advanced_group.setLayout(advanced_group_layout) @@ -63,12 +64,21 @@ class Options(QtWidgets.QVBoxLayout): """ When the 'close automatically' checkbox is toggled, let the web app know. """ - if state > 0: - self.web.set_stay_open(False) - self.app.stay_open = False - else: + if state == 0: self.web.set_stay_open(True) self.app.stay_open = True + else: + self.web.set_stay_open(False) + self.app.stay_open = False + + def advanced_options_changed(self, checked): + """ + When the 'advanced options' checkbox is unchecked, uncheck all advanced + options, and let the onionshare app know. + """ + if not checked: + self.stealth.setChecked(False) + self.app.set_stealth(False) def stealth_changed(self, state): """ From b928592a7ebc3a0e57989cf4493e15040fd9010f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 28 Dec 2016 14:44:25 -0800 Subject: [PATCH 18/43] Update error message language for stealth onion services not supported --- resources/locale/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/locale/en.json b/resources/locale/en.json index 3c9dcd17..46478d99 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -56,7 +56,7 @@ "gui_quit_warning_dont_quit": "Don't Quit", "error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL.", "zip_progress_bar_format": "Crunching files: %p%", - "error_stealth_not_supported": "Your versions of tor or stem are too old. You need to upgrade them to create stealth onion services.", + "error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.", "gui_advanced_options": "Advanced options", "gui_create_stealth": "Create stealth onion service" } From 1928bd80de32ed7a71de5c410e9a49e752bf5a34 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 28 Dec 2016 15:55:31 -0800 Subject: [PATCH 19/43] Add a menu bar, with a stub for Settings, and Quit --- onionshare_gui/menu.py | 48 ++++++++++++++++++++++++++++++++ onionshare_gui/onionshare_gui.py | 4 +++ resources/locale/en.json | 5 +++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 onionshare_gui/menu.py diff --git a/onionshare_gui/menu.py b/onionshare_gui/menu.py new file mode 100644 index 00000000..59f63048 --- /dev/null +++ b/onionshare_gui/menu.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2016 Micah Lee + +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 . +""" +from PyQt5 import QtCore, QtWidgets + +from onionshare import strings + +class Menu(QtWidgets.QMenuBar): + """ + OnionShare's menu bar. + """ + def __init__(self, parent=None): + super(Menu, self).__init__(parent) + + file_menu = self.addMenu(strings._('gui_menu_file_menu', True)) + + settings_action = file_menu.addAction(strings._('gui_menu_settings_action', True)) + settings_action.triggered.connect(self.settings) + quit_action = file_menu.addAction(strings._('gui_menu_quit_action', True)) + quit_action.triggered.connect(self.quit) + + def settings(self): + """ + Settings action triggered. + """ + print("Settings clicked") + + def quit(self): + """ + Quit action triggered. + """ + self.parent().qtapp.quit() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 6a7da111..83b13200 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -25,6 +25,7 @@ from PyQt5.QtCore import pyqtSlot import onionshare from onionshare import strings, helpers, web +from .menu import Menu from .file_selection import FileSelection from .server_status import ServerStatus from .downloads import Downloads @@ -70,6 +71,9 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setWindowTitle('OnionShare') self.setWindowIcon(window_icon) + # the menu bar + self.setMenuBar(Menu()) + def send_files(self, filenames=None): """ Build the GUI in send files mode. diff --git a/resources/locale/en.json b/resources/locale/en.json index 46478d99..f69e3133 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -58,5 +58,8 @@ "zip_progress_bar_format": "Crunching files: %p%", "error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.", "gui_advanced_options": "Advanced options", - "gui_create_stealth": "Create stealth onion service" + "gui_create_stealth": "Create stealth onion service", + "gui_menu_file_menu": "&File", + "gui_menu_settings_action": "&Settings", + "gui_menu_quit_action": "&Quit" } From e9865f8561b3b506dddb723c780646e976636517 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 28 Dec 2016 18:44:41 -0800 Subject: [PATCH 20/43] Designed the GUI settings dialog, but none of it does anything yet --- onionshare_gui/menu.py | 3 +- onionshare_gui/settings_dialog.py | 227 ++++++++++++++++++++++++++++++ resources/locale/en.json | 18 ++- 3 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 onionshare_gui/settings_dialog.py diff --git a/onionshare_gui/menu.py b/onionshare_gui/menu.py index 59f63048..1027c786 100644 --- a/onionshare_gui/menu.py +++ b/onionshare_gui/menu.py @@ -20,6 +20,7 @@ along with this program. If not, see . from PyQt5 import QtCore, QtWidgets from onionshare import strings +from .settings_dialog import SettingsDialog class Menu(QtWidgets.QMenuBar): """ @@ -39,7 +40,7 @@ class Menu(QtWidgets.QMenuBar): """ Settings action triggered. """ - print("Settings clicked") + SettingsDialog() def quit(self): """ diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py new file mode 100644 index 00000000..93e94c1d --- /dev/null +++ b/onionshare_gui/settings_dialog.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2016 Micah Lee + +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 . +""" +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings + +class SettingsDialog(QtWidgets.QDialog): + """ + Settings dialog. + """ + def __init__(self, parent=None): + super(SettingsDialog, self).__init__(parent) + + self.setModal(True) + self.setWindowTitle(strings._('gui_settings_window_title', True)) + + # Connection type: either automatic, control port, or socket file + + # Automatic + self.connection_type_automatic_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_automatic_option', True)) + self.connection_type_automatic_radio.toggled.connect(self.connection_type_automatic_toggled) + + # Control port + self.connection_type_control_port_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_control_port_option', True)) + self.connection_type_control_port_radio.toggled.connect(self.connection_type_control_port_toggled) + + connection_type_control_port_extras_label = QtWidgets.QLabel(strings._('gui_settings_control_port_label', True)) + self.connection_type_control_port_extras_address = QtWidgets.QLineEdit('127.0.0.1') + self.connection_type_control_port_extras_port = QtWidgets.QLineEdit('9051') + connection_type_control_port_extras_layout = QtWidgets.QHBoxLayout() + connection_type_control_port_extras_layout.addWidget(connection_type_control_port_extras_label) + connection_type_control_port_extras_layout.addWidget(self.connection_type_control_port_extras_address) + connection_type_control_port_extras_layout.addWidget(self.connection_type_control_port_extras_port) + + self.connection_type_control_port_extras = QtWidgets.QWidget() + self.connection_type_control_port_extras.setLayout(connection_type_control_port_extras_layout) + self.connection_type_control_port_extras.hide() + + # Socket file + self.connection_type_socket_file_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_socket_file_option', True)) + self.connection_type_socket_file_radio.toggled.connect(self.connection_type_socket_file_toggled) + + connection_type_socket_file_extras_label = QtWidgets.QLabel(strings._('gui_settings_socket_file_label', True)) + self.connection_type_socket_file_extras_path = QtWidgets.QLineEdit('/var/run/tor/control') + connection_type_socket_file_extras_layout = QtWidgets.QHBoxLayout() + connection_type_socket_file_extras_layout.addWidget(connection_type_socket_file_extras_label) + connection_type_socket_file_extras_layout.addWidget(self.connection_type_socket_file_extras_path) + + self.connection_type_socket_file_extras = QtWidgets.QWidget() + self.connection_type_socket_file_extras.setLayout(connection_type_socket_file_extras_layout) + self.connection_type_socket_file_extras.hide() + + # Connection type layout + connection_type_group_layout = QtWidgets.QVBoxLayout() + connection_type_group_layout.addWidget(self.connection_type_automatic_radio) + connection_type_group_layout.addWidget(self.connection_type_control_port_radio) + connection_type_group_layout.addWidget(self.connection_type_socket_file_radio) + connection_type_group_layout.addWidget(self.connection_type_control_port_extras) + connection_type_group_layout.addWidget(self.connection_type_socket_file_extras) + connection_type_group = QtWidgets.QGroupBox(strings._("gui_settings_connection_type_label", True)) + connection_type_group.setLayout(connection_type_group_layout) + + + # Authentication options + + # No authentication + self.authenticate_no_auth_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_no_auth_option', True)) + self.authenticate_no_auth_radio.toggled.connect(self.authenticate_no_auth_toggled) + + # Password + self.authenticate_password_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_password_option', True)) + self.authenticate_password_radio.toggled.connect(self.authenticate_password_toggled) + + authenticate_password_extras_label = QtWidgets.QLabel(strings._('gui_settings_password_label', True)) + self.authenticate_password_extras_password = QtWidgets.QLineEdit('') + authenticate_password_extras_layout = QtWidgets.QHBoxLayout() + authenticate_password_extras_layout.addWidget(authenticate_password_extras_label) + authenticate_password_extras_layout.addWidget(self.authenticate_password_extras_password) + + self.authenticate_password_extras = QtWidgets.QWidget() + self.authenticate_password_extras.setLayout(authenticate_password_extras_layout) + self.authenticate_password_extras.hide() + + # Cookie + self.authenticate_cookie_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_cookie_option', True)) + self.authenticate_cookie_radio.toggled.connect(self.authenticate_cookie_toggled) + + authenticate_cookie_extras_label = QtWidgets.QLabel(strings._('gui_settings_cookie_label', True)) + self.authenticate_cookie_extras_cookie_path = QtWidgets.QLineEdit('/var/run/tor/control.authcookie') + authenticate_cookie_extras_layout = QtWidgets.QHBoxLayout() + authenticate_cookie_extras_layout.addWidget(authenticate_cookie_extras_label) + authenticate_cookie_extras_layout.addWidget(self.authenticate_cookie_extras_cookie_path) + + self.authenticate_cookie_extras = QtWidgets.QWidget() + self.authenticate_cookie_extras.setLayout(authenticate_cookie_extras_layout) + self.authenticate_cookie_extras.hide() + + # Authentication options layout + authenticate_group_layout = QtWidgets.QVBoxLayout() + authenticate_group_layout.addWidget(self.authenticate_no_auth_radio) + authenticate_group_layout.addWidget(self.authenticate_password_radio) + authenticate_group_layout.addWidget(self.authenticate_cookie_radio) + authenticate_group_layout.addWidget(self.authenticate_password_extras) + authenticate_group_layout.addWidget(self.authenticate_cookie_extras) + self.authenticate_group = QtWidgets.QGroupBox(strings._("gui_settings_authenticate_label", True)) + self.authenticate_group.setLayout(authenticate_group_layout) + + + # Buttons + test_button = QtWidgets.QPushButton(strings._('gui_settings_button_test', True)) + test_button.clicked.connect(self.test_clicked) + save_button = QtWidgets.QPushButton(strings._('gui_settings_button_save', True)) + save_button.clicked.connect(self.save_clicked) + cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True)) + cancel_button.clicked.connect(self.cancel_clicked) + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addWidget(test_button) + buttons_layout.addWidget(save_button) + buttons_layout.addWidget(cancel_button) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(connection_type_group) + layout.addWidget(self.authenticate_group) + layout.addStretch() + layout.addLayout(buttons_layout) + self.setLayout(layout) + + # Set default option + self.connection_type_automatic_radio.setChecked(True) + self.authenticate_no_auth_radio.setChecked(True) + + # Show the dialog + self.exec_() + + def connection_type_automatic_toggled(self, checked): + """ + Connection type automatic was toggled. If checked, disable all other + fields. If unchecked, enable all other fields. + """ + if checked: + self.authenticate_group.setEnabled(False) + else: + self.authenticate_group.setEnabled(True) + + def connection_type_control_port_toggled(self, checked): + """ + Connection type control port was toggled. If checked, show extra fields + for Tor control address and port. If unchecked, hide those extra fields. + """ + if checked: + self.connection_type_control_port_extras.show() + else: + self.connection_type_control_port_extras.hide() + + + def connection_type_socket_file_toggled(self, checked): + """ + Connection type socket file was toggled. If checked, show extra fields + for socket file. If unchecked, hide those extra fields. + """ + if checked: + self.connection_type_socket_file_extras.show() + else: + self.connection_type_socket_file_extras.hide() + + def authenticate_no_auth_toggled(self, checked): + """ + Authentication option no authentication was toggled. + """ + pass + + def authenticate_password_toggled(self, checked): + """ + Authentication option password was toggled. If checked, show extra fields + for password auth. If unchecked, hide those extra fields. + """ + if checked: + self.authenticate_password_extras.show() + else: + self.authenticate_password_extras.hide() + + def authenticate_cookie_toggled(self, checked): + """ + Authentication option cookie was toggled. If checked, show extra fields + for cookie auth. If unchecked, hide those extra fields. + """ + if checked: + self.authenticate_cookie_extras.show() + else: + self.authenticate_cookie_extras.hide() + + def test_clicked(self): + """ + Test Settings button clicked. With the given settings, see if we can + successfully connect and authenticate to Tor. + """ + pass + + def save_clicked(self): + """ + Save button clicked. Save current settings to disk. + """ + pass + + def cancel_clicked(self): + """ + Cancel button clicked. + """ + self.close() diff --git a/resources/locale/en.json b/resources/locale/en.json index f69e3133..66494a5f 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -61,5 +61,21 @@ "gui_create_stealth": "Create stealth onion service", "gui_menu_file_menu": "&File", "gui_menu_settings_action": "&Settings", - "gui_menu_quit_action": "&Quit" + "gui_menu_quit_action": "&Quit", + "gui_settings_window_title": "Settings", + "gui_settings_connection_type_label": "How should OnionShare connect to Tor?", + "gui_settings_connection_type_automatic_option": "Attempt automatic configuration with Tor Browser", + "gui_settings_connection_type_control_port_option": "Connect using control port", + "gui_settings_connection_type_socket_file_option": "Connect using socket file", + "gui_settings_control_port_label": "Control port", + "gui_settings_socket_file_label": "Socket file", + "gui_settings_authenticate_label": "Tor authentication options", + "gui_settings_authenticate_no_auth_option": "No authentication", + "gui_settings_authenticate_password_option": "Password", + "gui_settings_authenticate_cookie_option": "Cookie", + "gui_settings_password_label": "Password", + "gui_settings_cookie_label": "Cookie path", + "gui_settings_button_test": "Test Settings", + "gui_settings_button_save": "Save", + "gui_settings_button_cancel": "Cancel" } From 38845b39cb395f1c5abfce8a98f36335b30a9dc1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 28 Dec 2016 19:52:21 -0800 Subject: [PATCH 21/43] Created a Settings object, which loads and saves settings to file, and made the Settings dialog use the settings from this object --- onionshare/onion.py | 3 ++ onionshare/settings.py | 90 +++++++++++++++++++++++++++++++ onionshare_gui/settings_dialog.py | 34 +++++++++--- 3 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 onionshare/settings.py diff --git a/onionshare/onion.py b/onionshare/onion.py index 05d479b2..90881513 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -25,6 +25,7 @@ import os, sys, tempfile, shutil, urllib from . import socks from . import helpers, strings +from .settings import Settings class NoTor(Exception): """ @@ -60,6 +61,8 @@ class Onion(object): self.transparent_torification = transparent_torification self.stealth = stealth + self.settings = Settings() + # files and dirs to delete on shutdown self.cleanup_filenames = [] self.service_id = None diff --git a/onionshare/settings.py b/onionshare/settings.py new file mode 100644 index 00000000..2bafbeef --- /dev/null +++ b/onionshare/settings.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2016 Micah Lee + +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 . +""" + +import platform, os, json + +from . import helpers + +class Settings(object): + """ + This class stores all of the settings for OnionShare, specifically for how + to connect to Tor. If it can't find the settings file, it uses the default, + which is to attempt to connect automatically using default Tor Browser + settings. + """ + def __init__(self): + self.filename = self.build_filename() + self.load() + + def build_filename(self): + """ + Returns the path of the settings file. + """ + p = platform.system() + if p == 'Windows': + appdata = os.environ['APPDATA'] + return '{}\\OnionShare\\onionshare.json'.format(appdata) + elif p == 'Darwin': + return os.path.expanduser('~/Library/Application Support/OnionShare/onionshare.json') + else: + return os.path.expanduser('~/.config/onionshare/onionshare.json') + + def load(self): + """ + Load the settings from file. + """ + default_settings = { + 'version': helpers.get_version(), + 'connection_type': 'automatic', + 'control_port_address': '127.0.0.1', + 'control_port_port': '9051', + 'socket_file_path': '/var/run/tor/control', + 'auth_type': 'no_auth', + 'auth_password': '', + 'auth_cookie_path': '/var/run/tor/control.authcookie' + } + + if os.path.exists(self.filename): + # If the settings file exists, load it + try: + self._settings = json.loads(open(self.filename, 'r').read()) + except: + # If the settings don't work, use default ones instead + self._settings = default_settings + + else: + # Otherwise, use default settings + self._settings = default_settings + + def save(self): + """ + Save settings to file. + """ + os.mkdirs(os.path.dirname(self.filename)) + open(self.filename, 'w').write(json.dumps(self._settings)) + + def get(self, key): + """ + Set a setting. + """ + return self._settings[key] + + def set(self, key, val): + self._settings[key] = val diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 93e94c1d..55a76d0b 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -20,6 +20,7 @@ along with this program. If not, see . from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings +from onionshare.settings import Settings class SettingsDialog(QtWidgets.QDialog): """ @@ -42,8 +43,8 @@ class SettingsDialog(QtWidgets.QDialog): self.connection_type_control_port_radio.toggled.connect(self.connection_type_control_port_toggled) connection_type_control_port_extras_label = QtWidgets.QLabel(strings._('gui_settings_control_port_label', True)) - self.connection_type_control_port_extras_address = QtWidgets.QLineEdit('127.0.0.1') - self.connection_type_control_port_extras_port = QtWidgets.QLineEdit('9051') + self.connection_type_control_port_extras_address = QtWidgets.QLineEdit() + self.connection_type_control_port_extras_port = QtWidgets.QLineEdit() connection_type_control_port_extras_layout = QtWidgets.QHBoxLayout() connection_type_control_port_extras_layout.addWidget(connection_type_control_port_extras_label) connection_type_control_port_extras_layout.addWidget(self.connection_type_control_port_extras_address) @@ -58,7 +59,7 @@ class SettingsDialog(QtWidgets.QDialog): self.connection_type_socket_file_radio.toggled.connect(self.connection_type_socket_file_toggled) connection_type_socket_file_extras_label = QtWidgets.QLabel(strings._('gui_settings_socket_file_label', True)) - self.connection_type_socket_file_extras_path = QtWidgets.QLineEdit('/var/run/tor/control') + self.connection_type_socket_file_extras_path = QtWidgets.QLineEdit() connection_type_socket_file_extras_layout = QtWidgets.QHBoxLayout() connection_type_socket_file_extras_layout.addWidget(connection_type_socket_file_extras_label) connection_type_socket_file_extras_layout.addWidget(self.connection_type_socket_file_extras_path) @@ -103,7 +104,7 @@ class SettingsDialog(QtWidgets.QDialog): self.authenticate_cookie_radio.toggled.connect(self.authenticate_cookie_toggled) authenticate_cookie_extras_label = QtWidgets.QLabel(strings._('gui_settings_cookie_label', True)) - self.authenticate_cookie_extras_cookie_path = QtWidgets.QLineEdit('/var/run/tor/control.authcookie') + self.authenticate_cookie_extras_cookie_path = QtWidgets.QLineEdit() authenticate_cookie_extras_layout = QtWidgets.QHBoxLayout() authenticate_cookie_extras_layout.addWidget(authenticate_cookie_extras_label) authenticate_cookie_extras_layout.addWidget(self.authenticate_cookie_extras_cookie_path) @@ -143,9 +144,28 @@ class SettingsDialog(QtWidgets.QDialog): layout.addLayout(buttons_layout) self.setLayout(layout) - # Set default option - self.connection_type_automatic_radio.setChecked(True) - self.authenticate_no_auth_radio.setChecked(True) + + # Load settings, and fill them in + self.settings = Settings() + connection_type = self.settings.get('connection_type') + if connection_type == 'automatic': + self.connection_type_automatic_radio.setChecked(True) + elif connection_type == 'control_port': + self.connect_type_control_port_radio.setChecked(True) + elif connection_type == 'socket_file': + self.connection_type_socket_file_radio.setChecked(True) + self.connection_type_control_port_extras_address.setText(self.settings.get('control_port_address')) + self.connection_type_control_port_extras_port.setText(self.settings.get('control_port_port')) + self.connection_type_socket_file_extras_path.setText(self.settings.get('socket_file_path')) + auth_type = self.settings.get('auth_type') + if auth_type == 'no_auth': + self.authenticate_no_auth_radio.setChecked(True) + elif auth_type == 'password': + self.authenticate_password_radio.setChecked(True) + elif auth_type == 'cookie': + self.authenticate_cookie_radio.setChecked(True) + self.authenticate_password_extras_password.setText(self.settings.get('auth_password')) + self.authenticate_cookie_extras_cookie_path.setText(self.settings.get('auth_cookie_path')) # Show the dialog self.exec_() From 024ad7fc2059685516a91eb2063c99806b24985e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 28 Dec 2016 19:53:02 -0800 Subject: [PATCH 22/43] Remove wrong comment --- onionshare/settings.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/onionshare/settings.py b/onionshare/settings.py index 2bafbeef..d36bb18c 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -81,9 +81,6 @@ class Settings(object): open(self.filename, 'w').write(json.dumps(self._settings)) def get(self, key): - """ - Set a setting. - """ return self._settings[key] def set(self, key, val): From e9df6e6c81e0eb3b07280c6af366f55d5d7ae02c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 28 Dec 2016 20:03:32 -0800 Subject: [PATCH 23/43] Clicking Save in the settings dialog saves settings --- onionshare/settings.py | 8 ++++++-- onionshare_gui/settings_dialog.py | 26 ++++++++++++++++++++++++-- resources/locale/en.json | 3 ++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/onionshare/settings.py b/onionshare/settings.py index d36bb18c..6ba2779d 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -20,7 +20,7 @@ along with this program. If not, see . import platform, os, json -from . import helpers +from . import strings, helpers class Settings(object): """ @@ -77,8 +77,12 @@ class Settings(object): """ Save settings to file. """ - os.mkdirs(os.path.dirname(self.filename)) + try: + os.makedirs(os.path.dirname(self.filename)) + except: + pass open(self.filename, 'w').write(json.dumps(self._settings)) + print(strings._('settings_saved').format(self.filename)) def get(self, key): return self._settings[key] diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 55a76d0b..1f1120e3 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -151,7 +151,7 @@ class SettingsDialog(QtWidgets.QDialog): if connection_type == 'automatic': self.connection_type_automatic_radio.setChecked(True) elif connection_type == 'control_port': - self.connect_type_control_port_radio.setChecked(True) + self.connection_type_control_port_radio.setChecked(True) elif connection_type == 'socket_file': self.connection_type_socket_file_radio.setChecked(True) self.connection_type_control_port_extras_address.setText(self.settings.get('control_port_address')) @@ -238,7 +238,29 @@ class SettingsDialog(QtWidgets.QDialog): """ Save button clicked. Save current settings to disk. """ - pass + if self.connection_type_automatic_radio.isChecked(): + self.settings.set('connection_type', 'automatic') + if self.connection_type_control_port_radio.isChecked(): + self.settings.set('connection_type', 'control_port') + if self.connection_type_socket_file_radio.isChecked(): + self.settings.set('connection_type', 'socket_file') + + self.settings.set('control_port_address', self.connection_type_control_port_extras_address.text()) + self.settings.set('control_port_port', self.connection_type_control_port_extras_port.text()) + self.settings.set('socket_file_path', self.connection_type_socket_file_extras_path.text()) + + if self.authenticate_no_auth_radio.isChecked(): + self.settings.set('auth_type', 'no_auth') + if self.authenticate_password_radio.isChecked(): + self.settings.set('auth_type', 'password') + if self.authenticate_cookie_radio.isChecked(): + self.settings.set('auth_type', 'cookie') + + self.settings.set('auth_password', self.authenticate_password_extras_password.text()) + self.settings.set('auth_cookie_path', self.authenticate_cookie_extras_cookie_path.text()) + + self.settings.save() + self.close() def cancel_clicked(self): """ diff --git a/resources/locale/en.json b/resources/locale/en.json index 66494a5f..90e7d3ca 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -77,5 +77,6 @@ "gui_settings_cookie_label": "Cookie path", "gui_settings_button_test": "Test Settings", "gui_settings_button_save": "Save", - "gui_settings_button_cancel": "Cancel" + "gui_settings_button_cancel": "Cancel", + "settings_saved": "Settings saved to {}" } From 940b6c647e33a3cbc0409ead0b57fb6db29be637 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 08:02:32 -0800 Subject: [PATCH 24/43] Refactor Settings object so it does not load from file by default. Make it so you can pass a Settings into Onion, to test settings --- onionshare/onion.py | 9 +++- onionshare/settings.py | 33 ++++++-------- onionshare_gui/settings_dialog.py | 76 ++++++++++++++++++------------- 3 files changed, 66 insertions(+), 52 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 90881513..22a95c19 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -57,11 +57,16 @@ class Onion(object): onion services are supported. If not, it falls back to modifying the Tor configuration. """ - def __init__(self, transparent_torification=False, stealth=False): + def __init__(self, transparent_torification=False, stealth=False, settings=False): self.transparent_torification = transparent_torification self.stealth = stealth - self.settings = Settings() + # Either use settings that are passed in, or load them from disk + if settings: + self.settings = settings + else: + self.settings = Settings() + self.settings.load() # files and dirs to delete on shutdown self.cleanup_filenames = [] diff --git a/onionshare/settings.py b/onionshare/settings.py index 6ba2779d..1fa743b3 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -31,7 +31,18 @@ class Settings(object): """ def __init__(self): self.filename = self.build_filename() - self.load() + + # These are the default settings. They will get overwritten when loading from disk + self._settings = { + 'version': helpers.get_version(), + 'connection_type': 'automatic', + 'control_port_address': '127.0.0.1', + 'control_port_port': '9051', + 'socket_file_path': '/var/run/tor/control', + 'auth_type': 'no_auth', + 'auth_password': '', + 'auth_cookie_path': '/var/run/tor/control.authcookie' + } def build_filename(self): """ @@ -50,28 +61,12 @@ class Settings(object): """ Load the settings from file. """ - default_settings = { - 'version': helpers.get_version(), - 'connection_type': 'automatic', - 'control_port_address': '127.0.0.1', - 'control_port_port': '9051', - 'socket_file_path': '/var/run/tor/control', - 'auth_type': 'no_auth', - 'auth_password': '', - 'auth_cookie_path': '/var/run/tor/control.authcookie' - } - + # If the settings file exists, load it if os.path.exists(self.filename): - # If the settings file exists, load it try: self._settings = json.loads(open(self.filename, 'r').read()) except: - # If the settings don't work, use default ones instead - self._settings = default_settings - - else: - # Otherwise, use default settings - self._settings = default_settings + pass def save(self): """ diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 1f1120e3..13db7270 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -21,6 +21,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.settings import Settings +from onionshare.onion import Onion class SettingsDialog(QtWidgets.QDialog): """ @@ -146,26 +147,28 @@ class SettingsDialog(QtWidgets.QDialog): # Load settings, and fill them in - self.settings = Settings() - connection_type = self.settings.get('connection_type') + settings = Settings() + settings.load() + + connection_type = settings.get('connection_type') if connection_type == 'automatic': self.connection_type_automatic_radio.setChecked(True) elif connection_type == 'control_port': self.connection_type_control_port_radio.setChecked(True) elif connection_type == 'socket_file': self.connection_type_socket_file_radio.setChecked(True) - self.connection_type_control_port_extras_address.setText(self.settings.get('control_port_address')) - self.connection_type_control_port_extras_port.setText(self.settings.get('control_port_port')) - self.connection_type_socket_file_extras_path.setText(self.settings.get('socket_file_path')) - auth_type = self.settings.get('auth_type') + self.connection_type_control_port_extras_address.setText(settings.get('control_port_address')) + self.connection_type_control_port_extras_port.setText(settings.get('control_port_port')) + self.connection_type_socket_file_extras_path.setText(settings.get('socket_file_path')) + auth_type = settings.get('auth_type') if auth_type == 'no_auth': self.authenticate_no_auth_radio.setChecked(True) elif auth_type == 'password': self.authenticate_password_radio.setChecked(True) elif auth_type == 'cookie': self.authenticate_cookie_radio.setChecked(True) - self.authenticate_password_extras_password.setText(self.settings.get('auth_password')) - self.authenticate_cookie_extras_cookie_path.setText(self.settings.get('auth_cookie_path')) + self.authenticate_password_extras_password.setText(settings.get('auth_password')) + self.authenticate_cookie_extras_cookie_path.setText(settings.get('auth_cookie_path')) # Show the dialog self.exec_() @@ -232,34 +235,16 @@ class SettingsDialog(QtWidgets.QDialog): Test Settings button clicked. With the given settings, see if we can successfully connect and authenticate to Tor. """ - pass + print("Testing settings") + settings = self.settings_from_fields() + onion = Onion(settings=settings) def save_clicked(self): """ Save button clicked. Save current settings to disk. """ - if self.connection_type_automatic_radio.isChecked(): - self.settings.set('connection_type', 'automatic') - if self.connection_type_control_port_radio.isChecked(): - self.settings.set('connection_type', 'control_port') - if self.connection_type_socket_file_radio.isChecked(): - self.settings.set('connection_type', 'socket_file') - - self.settings.set('control_port_address', self.connection_type_control_port_extras_address.text()) - self.settings.set('control_port_port', self.connection_type_control_port_extras_port.text()) - self.settings.set('socket_file_path', self.connection_type_socket_file_extras_path.text()) - - if self.authenticate_no_auth_radio.isChecked(): - self.settings.set('auth_type', 'no_auth') - if self.authenticate_password_radio.isChecked(): - self.settings.set('auth_type', 'password') - if self.authenticate_cookie_radio.isChecked(): - self.settings.set('auth_type', 'cookie') - - self.settings.set('auth_password', self.authenticate_password_extras_password.text()) - self.settings.set('auth_cookie_path', self.authenticate_cookie_extras_cookie_path.text()) - - self.settings.save() + settings = self.settings_from_fields() + settings.save() self.close() def cancel_clicked(self): @@ -267,3 +252,32 @@ class SettingsDialog(QtWidgets.QDialog): Cancel button clicked. """ self.close() + + def settings_from_fields(self): + """ + Return a Settings object that's full of values from the settings dialog. + """ + settings = Settings() + + if self.connection_type_automatic_radio.isChecked(): + settings.set('connection_type', 'automatic') + if self.connection_type_control_port_radio.isChecked(): + settings.set('connection_type', 'control_port') + if self.connection_type_socket_file_radio.isChecked(): + settings.set('connection_type', 'socket_file') + + settings.set('control_port_address', self.connection_type_control_port_extras_address.text()) + settings.set('control_port_port', self.connection_type_control_port_extras_port.text()) + settings.set('socket_file_path', self.connection_type_socket_file_extras_path.text()) + + if self.authenticate_no_auth_radio.isChecked(): + settings.set('auth_type', 'no_auth') + if self.authenticate_password_radio.isChecked(): + settings.set('auth_type', 'password') + if self.authenticate_cookie_radio.isChecked(): + settings.set('auth_type', 'cookie') + + settings.set('auth_password', self.authenticate_password_extras_password.text()) + settings.set('auth_cookie_path', self.authenticate_cookie_extras_cookie_path.text()) + + return settings From 52b00deb8a16597e560d44c4e16717087381ad82 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 09:57:27 -0800 Subject: [PATCH 25/43] Created an alert class, so other parts of the GUI can make alert popups --- onionshare_gui/alert.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 onionshare_gui/alert.py diff --git a/onionshare_gui/alert.py b/onionshare_gui/alert.py new file mode 100644 index 00000000..a7cadf3d --- /dev/null +++ b/onionshare_gui/alert.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2016 Micah Lee + +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 . +""" +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import helpers + +class Alert(QtWidgets.QMessageBox): + """ + An alert box dialog. + """ + def __init__(self, message, icon=QtWidgets.QMessageBox.NoIcon): + super(Alert, self).__init__(None) + self.setWindowTitle("OnionShare") + self.setWindowIcon(QtGui.QIcon(helpers.get_resource_path('images/logo.png'))) + self.setText(message) + self.setIcon(icon) + self.exec_() From 94e756ac0d3335ee594416b2a3f09f392f7c3333 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 09:58:13 -0800 Subject: [PATCH 26/43] Onion now connects to the Tor controller using the settings in Settings (except automatic still needs some work), and the settings dialog handles error when testing settings --- onionshare/onion.py | 116 +++++++++++++++++++++++------- onionshare/settings.py | 5 +- onionshare_gui/settings_dialog.py | 50 ++++--------- resources/locale/en.json | 9 ++- 4 files changed, 113 insertions(+), 67 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 22a95c19..df74ffde 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -27,6 +27,38 @@ from . import socks from . import helpers, strings from .settings import Settings +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): + """ + OnionShare connected to the Tor controller, but your user does not have permission + to access the cookie file. + """ + pass + + class NoTor(Exception): """ This exception is raised if onionshare can't find a Tor control port @@ -72,34 +104,69 @@ class Onion(object): self.cleanup_filenames = [] self.service_id = None - # if the TOR_CONTROL_PORT environment variable is set, use that - # otherwise, default to Tor Browser, Tor Messenger, and system tor ports - env_port = os.environ.get('TOR_CONTROL_PORT') - if env_port: - ports = [int(env_port)] - else: - ports = [9151, 9153, 9051] - - # if the TOR_AUTHENTICATION_PASSWORD is set, use that to authenticate - password = os.environ.get('TOR_AUTHENTICATION_PASSWORD') - - # connect to the tor controlport - found_tor = False + # Try to connect to Tor self.c = None - for port in ports: + + if self.settings.get('connection_type') == 'automatic': + # Automatically try to guess the right way to connect to Tor Browser + + # if the TOR_CONTROL_PORT environment variable is set, use that + # otherwise, default to Tor Browser, Tor Messenger, and system tor ports + env_port = os.environ.get('TOR_CONTROL_PORT') + if env_port: + ports = [int(env_port)] + else: + ports = [9151, 9153, 9051] + + # connect to the tor controlport + found_tor = False + for port in ports: + try: + self.c = Controller.from_port(port=port) + self.c.authenticate() + found_tor = True + break + except SocketError: + pass + except MissingPassword: + raise NoTor(strings._("ctrlport_missing_password").format(str(ports))) + except UnreadableCookieFile: + raise NoTor(strings._("ctrlport_unreadable_cookie").format(str(ports))) + if not found_tor: + raise NoTor(strings._("cant_connect_ctrlport").format(str(ports))) + + else: + # Use specific settings to connect to tor + + # Try connecting try: - self.c = Controller.from_port(port=port) - self.c.authenticate(password) - found_tor = True - break + if self.settings.get('connection_type') == 'control_port': + self.c = Controller.from_port(address=self.settings.get('control_port_address'), port=self.settings.get('control_port_port')) + elif self.settings.get('connection_type') == 'socket_file': + self.c = Controller.from_socket_file(path=self.settings.get('socket_file_path')) + else: + raise TorErrorInvalidSetting(strings._("settings_error_unknown")) + except SocketError: - pass + if self.settings.get('connection_type') == 'control_port': + raise TorErrorSocketPort(strings._("settings_error_socket_port").format(self.settings.get('control_port_address'), self.settings.get('control_port_port'))) + else: + raise TorErrorSocketFile(strings._("settings_error_socket_file").format(self.settings.get('socket_file_path'))) + + # Try authenticating + try: + if self.settings.get('auth_type') == 'no_auth': + self.c.authenticate() + elif self.settings.get('auth_type') == 'password': + self.c.authenticate(self.settings.get('auth_password')) + else: + raise TorErrorInvalidSetting(strings._("settings_error_unknown")) + except MissingPassword: - raise NoTor(strings._("ctrlport_missing_password").format(str(ports))) + raise TorErrorMissingPassword(strings._('settings_error_missing_password')) except UnreadableCookieFile: - raise NoTor(strings._("ctrlport_unreadable_cookie").format(str(ports))) - if not found_tor: - raise NoTor(strings._("cant_connect_ctrlport").format(str(ports))) + raise TorErrorUnreadableCookieFile(strings._('settings_error_unreadable_cookie_file')) + # do the versions of stem and tor that I'm using support ephemeral onion services? tor_version = self.c.get_version().version_str @@ -107,8 +174,7 @@ class Onion(object): self.supports_ephemeral = callable(list_ephemeral_hidden_services) and tor_version >= '0.2.7.1' # do the versions of stem and tor that I'm using support stealth onion services? - if self.stealth: - self.check_for_stealth_support() + self.check_for_stealth_support() def check_for_stealth_support(self): try: diff --git a/onionshare/settings.py b/onionshare/settings.py index 1fa743b3..9414cab9 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -37,11 +37,10 @@ class Settings(object): 'version': helpers.get_version(), 'connection_type': 'automatic', 'control_port_address': '127.0.0.1', - 'control_port_port': '9051', + 'control_port_port': 9051, 'socket_file_path': '/var/run/tor/control', 'auth_type': 'no_auth', - 'auth_password': '', - 'auth_cookie_path': '/var/run/tor/control.authcookie' + 'auth_password': '' } def build_filename(self): diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 13db7270..6116d2a5 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -21,7 +21,9 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.settings import Settings -from onionshare.onion import Onion +from onionshare.onion import Onion, TorErrorInvalidSetting, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile + +from .alert import Alert class SettingsDialog(QtWidgets.QDialog): """ @@ -100,27 +102,11 @@ class SettingsDialog(QtWidgets.QDialog): self.authenticate_password_extras.setLayout(authenticate_password_extras_layout) self.authenticate_password_extras.hide() - # Cookie - self.authenticate_cookie_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_cookie_option', True)) - self.authenticate_cookie_radio.toggled.connect(self.authenticate_cookie_toggled) - - authenticate_cookie_extras_label = QtWidgets.QLabel(strings._('gui_settings_cookie_label', True)) - self.authenticate_cookie_extras_cookie_path = QtWidgets.QLineEdit() - authenticate_cookie_extras_layout = QtWidgets.QHBoxLayout() - authenticate_cookie_extras_layout.addWidget(authenticate_cookie_extras_label) - authenticate_cookie_extras_layout.addWidget(self.authenticate_cookie_extras_cookie_path) - - self.authenticate_cookie_extras = QtWidgets.QWidget() - self.authenticate_cookie_extras.setLayout(authenticate_cookie_extras_layout) - self.authenticate_cookie_extras.hide() - # Authentication options layout authenticate_group_layout = QtWidgets.QVBoxLayout() authenticate_group_layout.addWidget(self.authenticate_no_auth_radio) authenticate_group_layout.addWidget(self.authenticate_password_radio) - authenticate_group_layout.addWidget(self.authenticate_cookie_radio) authenticate_group_layout.addWidget(self.authenticate_password_extras) - authenticate_group_layout.addWidget(self.authenticate_cookie_extras) self.authenticate_group = QtWidgets.QGroupBox(strings._("gui_settings_authenticate_label", True)) self.authenticate_group.setLayout(authenticate_group_layout) @@ -158,17 +144,14 @@ class SettingsDialog(QtWidgets.QDialog): elif connection_type == 'socket_file': self.connection_type_socket_file_radio.setChecked(True) self.connection_type_control_port_extras_address.setText(settings.get('control_port_address')) - self.connection_type_control_port_extras_port.setText(settings.get('control_port_port')) + self.connection_type_control_port_extras_port.setText(str(settings.get('control_port_port'))) self.connection_type_socket_file_extras_path.setText(settings.get('socket_file_path')) auth_type = settings.get('auth_type') if auth_type == 'no_auth': self.authenticate_no_auth_radio.setChecked(True) elif auth_type == 'password': self.authenticate_password_radio.setChecked(True) - elif auth_type == 'cookie': - self.authenticate_cookie_radio.setChecked(True) self.authenticate_password_extras_password.setText(settings.get('auth_password')) - self.authenticate_cookie_extras_cookie_path.setText(settings.get('auth_cookie_path')) # Show the dialog self.exec_() @@ -220,24 +203,20 @@ class SettingsDialog(QtWidgets.QDialog): else: self.authenticate_password_extras.hide() - def authenticate_cookie_toggled(self, checked): - """ - Authentication option cookie was toggled. If checked, show extra fields - for cookie auth. If unchecked, hide those extra fields. - """ - if checked: - self.authenticate_cookie_extras.show() - else: - self.authenticate_cookie_extras.hide() - def test_clicked(self): """ Test Settings button clicked. With the given settings, see if we can successfully connect and authenticate to Tor. """ - print("Testing settings") settings = self.settings_from_fields() - onion = Onion(settings=settings) + + try: + onion = Onion(settings=settings) + + # If an exception hasn't been raised yet, the Tor settings work + + except (TorErrorInvalidSetting, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile) as e: + Alert(e.args[0]) def save_clicked(self): """ @@ -267,17 +246,14 @@ class SettingsDialog(QtWidgets.QDialog): settings.set('connection_type', 'socket_file') settings.set('control_port_address', self.connection_type_control_port_extras_address.text()) - settings.set('control_port_port', self.connection_type_control_port_extras_port.text()) + settings.set('control_port_port', int(self.connection_type_control_port_extras_port.text())) settings.set('socket_file_path', self.connection_type_socket_file_extras_path.text()) if self.authenticate_no_auth_radio.isChecked(): settings.set('auth_type', 'no_auth') if self.authenticate_password_radio.isChecked(): settings.set('auth_type', 'password') - if self.authenticate_cookie_radio.isChecked(): - settings.set('auth_type', 'cookie') settings.set('auth_password', self.authenticate_password_extras_password.text()) - settings.set('auth_cookie_path', self.authenticate_cookie_extras_cookie_path.text()) return settings diff --git a/resources/locale/en.json b/resources/locale/en.json index 90e7d3ca..f48b90ed 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -70,7 +70,7 @@ "gui_settings_control_port_label": "Control port", "gui_settings_socket_file_label": "Socket file", "gui_settings_authenticate_label": "Tor authentication options", - "gui_settings_authenticate_no_auth_option": "No authentication", + "gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication", "gui_settings_authenticate_password_option": "Password", "gui_settings_authenticate_cookie_option": "Cookie", "gui_settings_password_label": "Password", @@ -78,5 +78,10 @@ "gui_settings_button_test": "Test Settings", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", - "settings_saved": "Settings saved to {}" + "settings_saved": "Settings saved to {}", + "settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.", + "settings_error_socket_port": "Can't connect to Tor controller on address {} with port {}.", + "settings_error_socket_file": "Can't connect to Tor controller using socket file {}.", + "settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.", + "settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user doesn't have permission to read the cookie file." } From 1c53746dd572f721e8c2e232194ebbbd6294d225 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 10:03:29 -0800 Subject: [PATCH 27/43] Display information about tor when testing settings is successful --- onionshare/onion.py | 8 +++----- onionshare_gui/settings_dialog.py | 3 ++- resources/locale/en.json | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index df74ffde..03dfde2c 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -167,16 +167,14 @@ class Onion(object): except UnreadableCookieFile: raise TorErrorUnreadableCookieFile(strings._('settings_error_unreadable_cookie_file')) + # get the tor version + self.tor_version = self.c.get_version().version_str # do the versions of stem and tor that I'm using support ephemeral onion services? - tor_version = self.c.get_version().version_str list_ephemeral_hidden_services = getattr(self.c, "list_ephemeral_hidden_services", None) - self.supports_ephemeral = callable(list_ephemeral_hidden_services) and tor_version >= '0.2.7.1' + self.supports_ephemeral = callable(list_ephemeral_hidden_services) and self.tor_version >= '0.2.7.1' # do the versions of stem and tor that I'm using support stealth onion services? - self.check_for_stealth_support() - - def check_for_stealth_support(self): try: res = self.c.create_ephemeral_hidden_service({1:1}, basic_auth={'onionshare':None}, await_publication=False) tmp_service_id = res.content()[0][2].split('=')[1] diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 6116d2a5..24cecbb3 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -214,9 +214,10 @@ class SettingsDialog(QtWidgets.QDialog): onion = Onion(settings=settings) # If an exception hasn't been raised yet, the Tor settings work + Alert(strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) except (TorErrorInvalidSetting, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile) as e: - Alert(e.args[0]) + Alert(e.args[0], QtWidgets.QMessageBox.Warning) def save_clicked(self): """ diff --git a/resources/locale/en.json b/resources/locale/en.json index f48b90ed..c69fbab8 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -83,5 +83,6 @@ "settings_error_socket_port": "Can't connect to Tor controller on address {} with port {}.", "settings_error_socket_file": "Can't connect to Tor controller using socket file {}.", "settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.", - "settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user doesn't have permission to read the cookie file." + "settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user doesn't have permission to read the cookie file.", + "settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}" } From a8381040251ecc16da982f40d03fa980dbd7f91d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 10:16:20 -0800 Subject: [PATCH 28/43] Handle new Tor controller errors in CLI --- onionshare/onion.py | 1 - onionshare/onionshare.py | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 03dfde2c..ca3a6e67 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -58,7 +58,6 @@ class TorErrorUnreadableCookieFile(Exception): """ pass - class NoTor(Exception): """ This exception is raised if onionshare can't find a Tor control port diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index c87b7811..6537f105 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -155,10 +155,11 @@ def main(cwd=None): app = OnionShare(debug, local_only, stay_open, transparent_torification, stealth) app.choose_port() app.start_onion_service() - except onion.NoTor as e: - sys.exit(e.args[0]) - except onion.TorTooOld as e: + except (onion.NoTor, onion.TorTooOld, onion.TorErrorInvalidSetting, onion.TorErrorSocketPort, onion.TorErrorSocketFile, onion.TorErrorMissingPassword, onion.TorErrorUnreadableCookieFile) as e: sys.exit(e.args[0]) + except KeyboardInterrupt: + print("") + sys.exit() # prepare files to share print(strings._("preparing_files")) From 8715838917504e26319112fbb78f03fe2dca589f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 10:34:40 -0800 Subject: [PATCH 29/43] Handle new Tor controller errors in GUI --- onionshare_gui/onionshare_gui.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 83b13200..8a5858db 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -177,11 +177,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.app.start_onion_service() self.starting_server_step2.emit() - except onionshare.onion.NoTor as e: - self.starting_server_error.emit(e.args[0]) - return - - except onionshare.onion.TorTooOld as e: + except (onionshare.onion.NoTor, onionshare.onion.TorTooOld, onionshare.onion.TorErrorInvalidSetting, onionshare.onion.TorErrorSocketPort, onionshare.onion.TorErrorSocketFile, onionshare.onion.TorErrorMissingPassword, onionshare.onion.TorErrorUnreadableCookieFile) as e: self.starting_server_error.emit(e.args[0]) return From 23d9de8d440233b3a621f29a319a15c4208b26bc Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 12:57:58 -0800 Subject: [PATCH 30/43] Make automatic settings work with with Tor Browser 6.0.8 --- onionshare/onion.py | 64 ++++++++++++++++--------------- onionshare/onionshare.py | 2 +- onionshare_gui/onionshare_gui.py | 2 +- onionshare_gui/settings_dialog.py | 4 +- resources/locale/en.json | 5 +-- 5 files changed, 39 insertions(+), 38 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index ca3a6e67..0f20a77f 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -27,6 +27,13 @@ from . import socks from . import helpers, strings from .settings import Settings +class TorErrorAutomatic(Exception): + """ + OnionShare is failing to connect and authenticate to the Tor controller, + 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. @@ -58,13 +65,6 @@ class TorErrorUnreadableCookieFile(Exception): """ pass -class NoTor(Exception): - """ - This exception is raised if onionshare can't find a Tor control port - to connect to, or if it can't find a Tor socks5 proxy to proxy though. - """ - pass - class TorTooOld(Exception): """ This exception is raised if onionshare needs to use a feature of Tor or stem @@ -109,30 +109,34 @@ class Onion(object): if self.settings.get('connection_type') == 'automatic': # Automatically try to guess the right way to connect to Tor Browser - # if the TOR_CONTROL_PORT environment variable is set, use that - # otherwise, default to Tor Browser, Tor Messenger, and system tor ports - env_port = os.environ.get('TOR_CONTROL_PORT') - if env_port: - ports = [int(env_port)] - else: - ports = [9151, 9153, 9051] + # Try connecting + try: + # If the TOR_CONTROL_PORT environment variable is set, use that + env_port = os.environ.get('TOR_CONTROL_PORT') + if env_port: + self.c = Controller.from_port(port=int(env_port)) - # connect to the tor controlport - found_tor = False - for port in ports: - try: - self.c = Controller.from_port(port=port) - self.c.authenticate() - found_tor = True - break - except SocketError: - pass - except MissingPassword: - raise NoTor(strings._("ctrlport_missing_password").format(str(ports))) - except UnreadableCookieFile: - raise NoTor(strings._("ctrlport_unreadable_cookie").format(str(ports))) - if not found_tor: - raise NoTor(strings._("cant_connect_ctrlport").format(str(ports))) + else: + # Otherwise, try default ports for Tor Browser, Tor Messenger, and system tor + found_tor = False + try: + ports = [9151, 9153, 9051] + for port in ports: + self.c = Controller.from_port(port=port) + found_tor = True + except: + pass + if not found_tor: + raise TorErrorAutomatic(strings._('settings_error_automatic')) + + except TorErrorAutomatic: + raise TorErrorAutomatic(strings._('settings_error_automatic')) + + # Try authenticating + try: + self.c.authenticate() + except: + raise TorErrorAutomatic(strings._('settings_error_automatic')) else: # Use specific settings to connect to tor diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 6537f105..9ce9b310 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -155,7 +155,7 @@ def main(cwd=None): app = OnionShare(debug, local_only, stay_open, transparent_torification, stealth) app.choose_port() app.start_onion_service() - except (onion.NoTor, onion.TorTooOld, onion.TorErrorInvalidSetting, onion.TorErrorSocketPort, onion.TorErrorSocketFile, onion.TorErrorMissingPassword, onion.TorErrorUnreadableCookieFile) as e: + except (onion.TorTooOld, onion.TorErrorInvalidSetting, onion.TorErrorAutomatic, onion.TorErrorSocketPort, onion.TorErrorSocketFile, onion.TorErrorMissingPassword, onion.TorErrorUnreadableCookieFile) as e: sys.exit(e.args[0]) except KeyboardInterrupt: print("") diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 8a5858db..16b5615c 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -177,7 +177,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.app.start_onion_service() self.starting_server_step2.emit() - except (onionshare.onion.NoTor, onionshare.onion.TorTooOld, onionshare.onion.TorErrorInvalidSetting, onionshare.onion.TorErrorSocketPort, onionshare.onion.TorErrorSocketFile, onionshare.onion.TorErrorMissingPassword, onionshare.onion.TorErrorUnreadableCookieFile) as e: + except (onionshare.onion.TorTooOld, onionshare.onion.TorErrorInvalidSetting, onionshare.onion.TorErrorAutomatic, onionshare.onion.TorErrorSocketPort, onionshare.onion.TorErrorSocketFile, onionshare.onion.TorErrorMissingPassword, onionshare.onion.TorErrorUnreadableCookieFile) as e: self.starting_server_error.emit(e.args[0]) return diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 24cecbb3..e18980de 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -21,7 +21,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.settings import Settings -from onionshare.onion import Onion, TorErrorInvalidSetting, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile +from onionshare.onion import Onion, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile from .alert import Alert @@ -216,7 +216,7 @@ class SettingsDialog(QtWidgets.QDialog): # If an exception hasn't been raised yet, the Tor settings work Alert(strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) - except (TorErrorInvalidSetting, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile) as e: + except (TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile) as e: Alert(e.args[0], QtWidgets.QMessageBox.Warning) def save_clicked(self): diff --git a/resources/locale/en.json b/resources/locale/en.json index c69fbab8..1d222a20 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -1,9 +1,5 @@ { "config_onion_service": "Configuring onion service on port {0:d}.", - "cant_connect_ctrlport": "Can't connect to Tor control port on port {0:s}. OnionShare requires Tor Browser to be running in the background to work. If you don't have it you can get it from https://www.torproject.org/.", - "cant_connect_socksport": "Can't connect to Tor SOCKS5 server on port {0:s}. OnionShare requires Tor Browser to be running in the background to work. If you don't have it you can get it from https://www.torproject.org/.", - "ctrlport_missing_password": "Connected to Tor control port on port {0:s}, but you require a password. You must have the TOR_AUTHENTICATION_PASSWORD environment variable set. Or just open Tor Browser in the background.", - "ctrlport_unreadable_cookie": "Connected to Tor control port on port {0:s}, but your user does not have permission to authenticate. You might want to add a HashedControlPassword to your torrc, and set the TOR_AUTHENTICATION_PASSWORD environment variable. Or just open Tor Browser in the background.", "preparing_files": "Preparing files to share.", "wait_for_hs": "Waiting for HS to be ready:", "wait_for_hs_trying": "Trying...", @@ -80,6 +76,7 @@ "gui_settings_button_cancel": "Cancel", "settings_saved": "Settings saved to {}", "settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.", + "settings_error_automatic": "Can't connect to Tor controller. Is Tor Browser running in the background? If you don't have it you can get it from:\nhttps://www.torproject.org/.", "settings_error_socket_port": "Can't connect to Tor controller on address {} with port {}.", "settings_error_socket_file": "Can't connect to Tor controller using socket file {}.", "settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.", From 13f1f78da7a28b325f6be1038bd2e61b34031077 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 13:36:29 -0800 Subject: [PATCH 31/43] You must connect to a socket file instead of a port for Tor Browser 6.5a6. Make automatic settings fallback to socket file if the port doesn't work (only for Linux so far, have not tested in OS X, and is not supported in Windows) --- onionshare/onion.py | 58 +++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 0f20a77f..a8f6d4e8 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -21,7 +21,7 @@ along with this program. If not, see . from stem.control import Controller from stem import SocketError from stem.connection import MissingPassword, UnreadableCookieFile -import os, sys, tempfile, shutil, urllib +import os, sys, tempfile, shutil, urllib, platform from . import socks from . import helpers, strings @@ -109,28 +109,46 @@ class Onion(object): if self.settings.get('connection_type') == 'automatic': # Automatically try to guess the right way to connect to Tor Browser - # Try connecting - try: - # If the TOR_CONTROL_PORT environment variable is set, use that - env_port = os.environ.get('TOR_CONTROL_PORT') - if env_port: - self.c = Controller.from_port(port=int(env_port)) + # Try connecting to control port + found_tor = False - else: - # Otherwise, try default ports for Tor Browser, Tor Messenger, and system tor - found_tor = False - try: - ports = [9151, 9153, 9051] - for port in ports: - self.c = Controller.from_port(port=port) - found_tor = True - except: - pass - if not found_tor: + # If the TOR_CONTROL_PORT environment variable is set, use that + env_port = os.environ.get('TOR_CONTROL_PORT') + if env_port: + try: + self.c = Controller.from_port(port=int(env_port)) + found_tor = True + except: + pass + + else: + # Otherwise, try default ports for Tor Browser, Tor Messenger, and system tor + try: + ports = [9151, 9153, 9051] + for port in ports: + self.c = Controller.from_port(port=port) + found_tor = True + except: + pass + + # If connecting to default control ports failed, so let's try + # guessing the socket file name next + if not found_tor: + try: + p = platform.system() + if p == 'Linux': + socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid()) + elif p == 'Darwin': + # TODO: figure out the unix socket path in OS X + socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid()) + elif p == 'Windows': + # Windows doesn't support unix sockets raise TorErrorAutomatic(strings._('settings_error_automatic')) - except TorErrorAutomatic: - raise TorErrorAutomatic(strings._('settings_error_automatic')) + self.c = Controller.from_socket_file(path=socket_file_path) + + except: + raise TorErrorAutomatic(strings._('settings_error_automatic')) # Try authenticating try: From 9ae6df1b0767112e3a9bbbaf76e6d9a13b996d82 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 14:23:19 -0800 Subject: [PATCH 32/43] Remove the alert function, replace it with the Alert class --- onionshare_gui/onionshare_gui.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 16b5615c..439b221a 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -30,7 +30,7 @@ from .file_selection import FileSelection from .server_status import ServerStatus from .downloads import Downloads from .options import Options - +from .alert import Alert class Application(QtWidgets.QApplication): """ @@ -238,7 +238,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ If there's an error when trying to start the onion service """ - alert(error, QtWidgets.QMessageBox.Warning) + Alert(error, QtWidgets.QMessageBox.Warning) self.server_status.stop_server() self.status_bar.clearMessage() @@ -298,7 +298,7 @@ class OnionShareGui(QtWidgets.QMainWindow): elif event["type"] == web.REQUEST_RATE_LIMIT: self.stop_server() - alert(strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical) + Alert(strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical) elif event["type"] == web.REQUEST_PROGRESS: self.downloads.update_download(event["data"]["id"], event["data"]["bytes"]) @@ -396,18 +396,6 @@ class ZipProgressBar(QtWidgets.QProgressBar): self.setValue(0) -def alert(msg, icon=QtWidgets.QMessageBox.NoIcon): - """ - Pop up a message in a dialog window. - """ - dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle("OnionShare") - dialog.setWindowIcon(window_icon) - dialog.setText(msg) - dialog.setIcon(icon) - dialog.exec_() - - def main(): """ The main() function implements all of the logic that the GUI version of onionshare uses. @@ -447,7 +435,7 @@ def main(): valid = True for filename in filenames: if not os.path.exists(filename): - alert(strings._("not_a_file", True).format(filename)) + Alert(strings._("not_a_file", True).format(filename)) valid = False if not valid: sys.exit() From bb80efa00ffeac17f1c94bb3185791fed925a15c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 29 Dec 2016 14:35:57 -0800 Subject: [PATCH 33/43] When a share is active, disable the full advanced options group, not just the stealth checkbox --- onionshare_gui/onionshare_gui.py | 4 ++-- onionshare_gui/options.py | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 439b221a..ead6a609 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -161,7 +161,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.app.choose_port() # disable the stealth option - self.options.set_stealth_enabled(False) + self.options.set_advanced_enabled(False) # start onionshare http service in new thread t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open, self.app.transparent_torification)) @@ -250,7 +250,7 @@ class OnionShareGui(QtWidgets.QMainWindow): web.stop(self.app.port) self.app.cleanup() self.filesize_warning.hide() - self.options.set_stealth_enabled(True) + self.options.set_advanced_enabled(True) self.stop_server_finished.emit() @staticmethod diff --git a/onionshare_gui/options.py b/onionshare_gui/options.py index f1acff25..7c284aa0 100644 --- a/onionshare_gui/options.py +++ b/onionshare_gui/options.py @@ -47,18 +47,18 @@ class Options(QtWidgets.QVBoxLayout): self.stealth.stateChanged.connect(self.stealth_changed) # advanced options group - advanced_group = QtWidgets.QGroupBox(strings._("gui_advanced_options", True)) - advanced_group.setCheckable(True) - advanced_group.setChecked(False) - advanced_group.setFlat(True) - advanced_group.toggled.connect(self.advanced_options_changed) + self.advanced_group = QtWidgets.QGroupBox(strings._("gui_advanced_options", True)) + self.advanced_group.setCheckable(True) + self.advanced_group.setChecked(False) + self.advanced_group.setFlat(True) + self.advanced_group.toggled.connect(self.advanced_options_changed) advanced_group_layout = QtWidgets.QVBoxLayout() advanced_group_layout.addWidget(self.stealth) - advanced_group.setLayout(advanced_group_layout) + self.advanced_group.setLayout(advanced_group_layout) # add the widgets self.addWidget(self.close_automatically) - self.addWidget(advanced_group) + self.addWidget(self.advanced_group) def stay_open_changed(self, state): """ @@ -89,9 +89,10 @@ class Options(QtWidgets.QVBoxLayout): else: self.app.set_stealth(False) - def set_stealth_enabled(self, enabled): + def set_advanced_enabled(self, enabled): """ You cannot toggle stealth after an onion service has started. This method - disables and re-enabled the stealth checkbox. + disables and re-enabled the advanced options group, including the stealth + checkbox. """ - self.stealth.setEnabled(enabled) + self.advanced_group.setEnabled(enabled) From c52bb03dc6a6775faf2653651cf7f08e64bb3896 Mon Sep 17 00:00:00 2001 From: Garrett Robinson Date: Fri, 30 Dec 2016 12:02:40 -0500 Subject: [PATCH 34/43] Revert "Replaced sanitize_html() function that was based on regex with python3's html.escape()" This reverts commit a24b4a77627824030b926265d305d27926382f25. --- onionshare/web.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index 8af634eb..d7116bb6 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -17,7 +17,7 @@ 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 . """ -import queue, mimetypes, platform, os, sys, socket, logging, html +import queue, mimetypes, platform, os, sys, socket, logging, re from urllib.request import urlopen from flask import Flask, Response, request, render_template_string, abort @@ -42,7 +42,7 @@ def set_file_info(filenames, processed_size_callback=None): file_info = {'files': [], 'dirs': []} for filename in filenames: # strips trailing '/' and sanitizes filename - basename = html.escape(os.path.basename(filename.rstrip('/'))) + basename = sanitize_html(os.path.basename(filename.rstrip('/'))) info = { 'filename': filename, 'basename': basename From ff2e0c910e7ef0f251599b09270822641279eda3 Mon Sep 17 00:00:00 2001 From: Garrett Robinson Date: Fri, 30 Dec 2016 12:04:09 -0500 Subject: [PATCH 35/43] Revert "Added a function to remove HTML from file and directory names" This reverts commit b95828973ca2f7d24b204aef8c431f99679fc906. --- onionshare/web.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index d7116bb6..e50c0ed9 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -17,7 +17,7 @@ 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 . """ -import queue, mimetypes, platform, os, sys, socket, logging, re +import queue, mimetypes, platform, os, sys, socket, logging from urllib.request import urlopen from flask import Flask, Response, request, render_template_string, abort @@ -41,11 +41,9 @@ def set_file_info(filenames, processed_size_callback=None): # build file info list file_info = {'files': [], 'dirs': []} for filename in filenames: - # strips trailing '/' and sanitizes filename - basename = sanitize_html(os.path.basename(filename.rstrip('/'))) info = { 'filename': filename, - 'basename': basename + 'basename': os.path.basename(filename.rstrip('/')) } if os.path.isfile(filename): info['size'] = os.path.getsize(filename) @@ -55,8 +53,6 @@ def set_file_info(filenames, processed_size_callback=None): info['size'] = helpers.dir_size(filename) info['size_human'] = helpers.human_readable_filesize(info['size']) file_info['dirs'].append(info) - - # sort list of files and directories by basename file_info['files'] = sorted(file_info['files'], key=lambda k: k['basename']) file_info['dirs'] = sorted(file_info['dirs'], key=lambda k: k['basename']) From 0403d3d045ac74a08f9ecd5a5eb52e4354b486f4 Mon Sep 17 00:00:00 2001 From: Garrett Robinson Date: Fri, 30 Dec 2016 12:40:05 -0500 Subject: [PATCH 36/43] Make render_template_string autoescape by default in Flask versions < 0.11 --- onionshare/web.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/onionshare/web.py b/onionshare/web.py index e50c0ed9..9f0b02cf 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -17,12 +17,29 @@ 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 . """ +from distutils.version import StrictVersion as Version import queue, mimetypes, platform, os, sys, socket, logging from urllib.request import urlopen + from flask import Flask, Response, request, render_template_string, abort +from flask import __version__ as flask_version from . import strings, helpers + +def _safe_select_jinja_autoescape(self, filename): + if filename is None: + return True + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + +# Starting in Flask 0.11, render_template_string autoescapes template variables +# by default. To prevent content injection through template variables in +# earlier versions of Flask, we force autoescaping in the Jinja2 template +# engine if we detect a Flask version with insecure default behavior. +if Version(flask_version) < Version('0.11'): + # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc + Flask.select_jinja_autoescape = _safe_select_jinja_autoescape + app = Flask(__name__) # information about the file From 47eeb547bf54b670a31e1877621b5f47162d5149 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 6 Jan 2017 11:46:41 -0800 Subject: [PATCH 37/43] Guess the default socket file path for new versions of Tor Browser in OS X --- onionshare/onion.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index a8f6d4e8..483d0195 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -108,6 +108,7 @@ class Onion(object): if self.settings.get('connection_type') == 'automatic': # Automatically try to guess the right way to connect to Tor Browser + p = platform.system() # Try connecting to control port found_tor = False @@ -131,11 +132,19 @@ class Onion(object): except: pass + # If this still didn't work, try guessing the default socket file path + socket_file_path = '' + if not found_tor: + if p == 'Darwin': + socket_file_path = os.path.expanduser('~/Library/Application Support/TorBrowser-Data/Tor/control.socket') + + self.c = Controller.from_socket_file(path=socket_file_path) + found_tor = True + # If connecting to default control ports failed, so let's try # guessing the socket file name next if not found_tor: try: - p = platform.system() if p == 'Linux': socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid()) elif p == 'Darwin': From 6e171f02c3ca54fa3f5ab3761d10cd3db3adc901 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 6 Jan 2017 11:54:42 -0800 Subject: [PATCH 38/43] Catch exceptions for guessing default Tor Browser socket file path --- onionshare/onion.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 483d0195..bb5a31af 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -135,11 +135,14 @@ class Onion(object): # If this still didn't work, try guessing the default socket file path socket_file_path = '' if not found_tor: - if p == 'Darwin': - socket_file_path = os.path.expanduser('~/Library/Application Support/TorBrowser-Data/Tor/control.socket') + try: + if p == 'Darwin': + socket_file_path = os.path.expanduser('~/Library/Application Support/TorBrowser-Data/Tor/control.socket') - self.c = Controller.from_socket_file(path=socket_file_path) - found_tor = True + self.c = Controller.from_socket_file(path=socket_file_path) + found_tor = True + except: + pass # If connecting to default control ports failed, so let's try # guessing the socket file name next From aa426957aa2569962b2a3922d0ff8a957875fcd4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 6 Jan 2017 17:44:29 -0800 Subject: [PATCH 39/43] Rewrite build documentation to be simpler --- BUILD.md | 78 ++++++++++++++++++++++++++------------------------------ 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/BUILD.md b/BUILD.md index 69bb4db5..7f899825 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,21 +1,19 @@ # Building OnionShare -## GNU/Linux - -Start by getting a copy of the source code: +Start by getting the source code: ```sh git clone https://github.com/micahflee/onionshare.git cd onionshare ``` -*For .deb-based distros (like Debian, Ubuntu, Linux Mint):* +## GNU/Linux -Then install the needed dependencies: +Install the needed dependencies: -```sh -sudo apt-get install -y python3-flask python3-stem python3-pyqt5 python-nautilus -``` +For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-flask python3-stem python3-pyqt5 python-nautilus python3-nose` + +For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 nautilus-python` After that you can try both the CLI and the GUI version of OnionShare: @@ -24,28 +22,13 @@ After that you can try both the CLI and the GUI version of OnionShare: ./dev_scripts/onionshare-gui ``` -A script to build a .deb package and install OnionShare easily is also provided for your convenience: +You can also build OnionShare packages to install: -```sh -sudo apt-get install -y build-essential fakeroot python3-all python3-stdeb dh-python python-nautilus python3-nose -./install/build_deb.sh -sudo dpkg -i deb_dist/onionshare_*.deb -``` -Note that OnionShare uses stdeb to generate Debian packages, and `python3-stdeb` is not available in Ubuntu 14.04 (Trusty). Because of this, you can't use the `build_install.sh` script to build the .deb file in versions of Ubuntu 14.04 and earlier. However, .deb files you build in later versions of Ubuntu will install and work fine in 14.04. +Create a .deb on Debian-like distros: `./install/build_deb.sh` -*For .rpm-based distros (Red Hat, Fedora, CentOS):* +Create a .rpm on Fedora-like distros: `./install/build_rpm.sh` -```sh -sudo sudo dnf install -y rpm-build python3-flask python3-stem python3-qt5 nautilus-python -./install/build_rpm.sh -sudo yum install -y dist/onionshare-*.rpm -``` - -Depending on your distribution, you may need to use `yum` instead of `dnf`. - -*For ArchLinux:* - -There is a PKBUILD available [here](https://aur.archlinux.org/packages/onionshare/) that can be used to install OnionShare. +For ArchLinux: There is a PKBUILD available [here](https://aur.archlinux.org/packages/onionshare/) that can be used to install OnionShare. ## Mac OS X @@ -65,20 +48,22 @@ Install some dependencies using pip3: sudo pip3 install flask stem ``` +After that you can try both the CLI and the GUI version of OnionShare: + +```sh +./dev_scripts/onionshare +./dev_scripts/onionshare-gui +``` + +If you want to build a Mac OS X app bundle: + Install the latest development version of cx_Freeze: * Download a [snapshot](https://bitbucket.org/anthony_tuininga/cx_freeze/downloads) of the latest development version of cx_Freeze, extract it, and cd into the folder you extracted it to * Build the package: `python3 setup.py bdist_wheel` * Install it with pip: `sudo pip3 install dist/cx_Freeze-5.0-cp35-cp35m-macosx_10_11_x86_64.whl` -Get the source code: - -```sh -git clone https://github.com/micahflee/onionshare.git -cd onionshare -``` - -To build the .app: +To build the app bundle: ```sh install/build_osx.sh @@ -86,7 +71,7 @@ install/build_osx.sh Now you should have `dist/OnionShare.app`. -To codesign and build a .pkg for distribution: +To codesign and build a pkg for distribution: ```sh install/build_osx.sh --release @@ -98,14 +83,23 @@ Now you should have `dist/OnionShare.pkg`. ### Setting up your dev environment +Download the latest Python 3.6.x, 32-bit (x86) from https://www.python.org/downloads/. I downloaded `python-3.6.0.exe`. When installing it, make sure to check the "Add Python 3.6 to PATH" checkbox on the first page of the installer. + +Open a command prompt and install dependencies with pip: `pip install flask stem PyQt5` + +Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-windows-x86-2.0.4-online.exe`. There's no need to login to a Qt account during installation. Make sure you install the latest Qt 5.x. I installed Qt 5.7. + +After that you can try both the CLI and the GUI version of OnionShare: + +``` +python dev_scripts\onionshare +python dev_scripts\onionshare-gui +``` + +If you want to build an .exe: + These instructions include adding folders to the path in Windows. To do this, go to Start and type "advanced system settings", and open "View advanced system settings" in the Control Panel. Click Environment Variables. Under "System variables" double-click on Path. From there you can add and remove folders that are available in the PATH. -Download the latest Python 3.5.x, 32-bit (x86) from https://www.python.org/downloads/. I downloaded `python-3.5.2.exe`. When installing it, make sure to check the "Add Python 3.5 to PATH" checkbox on the first page of the installer. - -Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-windows-x86-2.0.3-1-online.exe`. There's no need to login to a Qt account during installation. Make sure you install the latest Qt 5.x. - -Open a command prompt and install dependencies with pip: `pip install pypiwin32 flask stem PyQt5` - Download and install the [Microsoft Visual C++ 2008 Redistributable Package (x86)](http://www.microsoft.com/en-us/download/details.aspx?id=29). Installing cx_Freeze with support for Python 3.5 is annoying. Here are the steps (thanks https://github.com/sekrause/cx_Freeze-Wheels): From 924e3ea2e8512e18d75cb9fcb0974bf281bbf489 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 6 Jan 2017 18:07:15 -0800 Subject: [PATCH 40/43] https://micahflee.com/2015/09/why-i-say-linux-instead-of-gnulinux/ --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 7f899825..69cd7d8b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -7,7 +7,7 @@ git clone https://github.com/micahflee/onionshare.git cd onionshare ``` -## GNU/Linux +## Linux Install the needed dependencies: From c0a26b7c6c046dd8743819d576d4363d0422af12 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 6 Jan 2017 18:58:15 -0800 Subject: [PATCH 41/43] Change GPL copyright from 2016 to 2017 --- dev_scripts/onionshare | 2 +- dev_scripts/onionshare-gui | 2 +- install/scripts/onionshare | 2 +- install/scripts/onionshare-gui | 2 +- onionshare/__init__.py | 2 +- onionshare/helpers.py | 2 +- onionshare/onion.py | 2 +- onionshare/onionshare.py | 2 +- onionshare/settings.py | 2 +- onionshare/strings.py | 2 +- onionshare/web.py | 2 +- onionshare_gui/__init__.py | 2 +- onionshare_gui/alert.py | 2 +- onionshare_gui/downloads.py | 2 +- onionshare_gui/file_selection.py | 2 +- onionshare_gui/menu.py | 2 +- onionshare_gui/onionshare_gui.py | 2 +- onionshare_gui/options.py | 2 +- onionshare_gui/server_status.py | 2 +- onionshare_gui/settings_dialog.py | 2 +- resources/license.txt | 2 +- setup.py | 2 +- test/onionshare_helpers_test.py | 2 +- test/onionshare_strings_test.py | 2 +- test/onionshare_test.py | 2 +- test/test_helpers.py | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/dev_scripts/onionshare b/dev_scripts/onionshare index b94d1139..81be89f4 100755 --- a/dev_scripts/onionshare +++ b/dev_scripts/onionshare @@ -3,7 +3,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/dev_scripts/onionshare-gui b/dev_scripts/onionshare-gui index 6d610bb6..aab70404 100755 --- a/dev_scripts/onionshare-gui +++ b/dev_scripts/onionshare-gui @@ -3,7 +3,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/install/scripts/onionshare b/install/scripts/onionshare index a896d597..6a1529fe 100755 --- a/install/scripts/onionshare +++ b/install/scripts/onionshare @@ -3,7 +3,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/install/scripts/onionshare-gui b/install/scripts/onionshare-gui index 6ccdd00b..786277c4 100755 --- a/install/scripts/onionshare-gui +++ b/install/scripts/onionshare-gui @@ -3,7 +3,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 5326f50d..0a1c6c91 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare/helpers.py b/onionshare/helpers.py index 5d321c0b..832f2f38 100644 --- a/onionshare/helpers.py +++ b/onionshare/helpers.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare/onion.py b/onionshare/onion.py index bb5a31af..2e3c9211 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 9ce9b310..cec1daa5 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare/settings.py b/onionshare/settings.py index 9414cab9..7699d91f 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare/strings.py b/onionshare/strings.py index 94739efb..1637e05d 100644 --- a/onionshare/strings.py +++ b/onionshare/strings.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare/web.py b/onionshare/web.py index 9f0b02cf..5fc329aa 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 89419093..d9788c70 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare_gui/alert.py b/onionshare_gui/alert.py index a7cadf3d..5dee4d3f 100644 --- a/onionshare_gui/alert.py +++ b/onionshare_gui/alert.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare_gui/downloads.py b/onionshare_gui/downloads.py index 1c54ab50..51e12ac4 100644 --- a/onionshare_gui/downloads.py +++ b/onionshare_gui/downloads.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare_gui/file_selection.py b/onionshare_gui/file_selection.py index cf1dc759..fa8a7801 100644 --- a/onionshare_gui/file_selection.py +++ b/onionshare_gui/file_selection.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare_gui/menu.py b/onionshare_gui/menu.py index 1027c786..eb9c948e 100644 --- a/onionshare_gui/menu.py +++ b/onionshare_gui/menu.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index ead6a609..8fb9474d 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare_gui/options.py b/onionshare_gui/options.py index 7c284aa0..c4c82299 100644 --- a/onionshare_gui/options.py +++ b/onionshare_gui/options.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 20c29a01..f5b44337 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index e18980de..1c08b26c 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/resources/license.txt b/resources/license.txt index b3dc5224..1223e5a6 100644 --- a/resources/license.txt +++ b/resources/license.txt @@ -1,4 +1,4 @@ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 diff --git a/setup.py b/setup.py index 40623c73..097fb035 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/test/onionshare_helpers_test.py b/test/onionshare_helpers_test.py index d8e896c4..71a5f205 100644 --- a/test/onionshare_helpers_test.py +++ b/test/onionshare_helpers_test.py @@ -1,7 +1,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/test/onionshare_strings_test.py b/test/onionshare_strings_test.py index 7b588f29..0a48e8ca 100644 --- a/test/onionshare_strings_test.py +++ b/test/onionshare_strings_test.py @@ -2,7 +2,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/test/onionshare_test.py b/test/onionshare_test.py index a0c77fa9..9a1ebf49 100644 --- a/test/onionshare_test.py +++ b/test/onionshare_test.py @@ -1,7 +1,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 diff --git a/test/test_helpers.py b/test/test_helpers.py index b07021c5..02db1eb8 100644 --- a/test/test_helpers.py +++ b/test/test_helpers.py @@ -1,7 +1,7 @@ """ OnionShare | https://onionshare.org/ -Copyright (C) 2016 Micah Lee +Copyright (C) 2017 Micah Lee 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 From b8c7807b92efd38dedaa68d760f5aa4cbaaef3c2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 6 Jan 2017 19:00:08 -0800 Subject: [PATCH 42/43] Moved onionshare and onionshare_gui logic directly into __init__.py files --- onionshare/__init__.py | 193 ++++++++++++- onionshare/onionshare.py | 211 -------------- onionshare_gui/__init__.py | 443 ++++++++++++++++++++++++++++- onionshare_gui/onionshare_gui.py | 461 ------------------------------- 4 files changed, 634 insertions(+), 674 deletions(-) delete mode 100644 onionshare/onionshare.py delete mode 100644 onionshare_gui/onionshare_gui.py diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 0a1c6c91..cec1daa5 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -17,4 +17,195 @@ 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 . """ -from .onionshare import * + +import os, sys, time, argparse, shutil, socket, threading + +from . import strings, helpers, web, onion + +class OnionShare(object): + """ + OnionShare is the main application class. Pass in options and run + start_onion_service and it will do the magic. + """ + def __init__(self, debug=False, local_only=False, stay_open=False, transparent_torification=False, stealth=False): + self.port = None + self.onion = None + self.hidserv_dir = None + self.onion_host = None + + # files and dirs to delete on shutdown + self.cleanup_filenames = [] + + # debug mode + if debug: + web.debug_mode() + + # do not use tor -- for development + self.local_only = local_only + + # automatically close when download is finished + self.stay_open = stay_open + + # traffic automatically goes through Tor + self.transparent_torification = transparent_torification + + # use stealth onion service + self.set_stealth(stealth) + + def set_stealth(self, stealth): + self.stealth = stealth + if self.onion: + self.onion.stealth = stealth + + def choose_port(self): + """ + Pick an un-used port in the range 17600-17650 to bind to. + """ + # let the OS choose a port + tmpsock = socket.socket() + for port in range(17600, 17650): + try: + tmpsock.bind(("127.0.0.1", port)) + break + except OSError: + pass + self.port = tmpsock.getsockname()[1] + tmpsock.close() + + def start_onion_service(self): + """ + Start the onionshare onion service. + """ + if not self.port: + self.choose_port() + + if self.local_only: + self.onion_host = '127.0.0.1:{0:d}'.format(self.port) + return + + if not self.onion: + self.onion = onion.Onion(self.transparent_torification, self.stealth) + + self.onion_host = self.onion.start(self.port) + + if self.stealth: + self.auth_string = self.onion.auth_string + + def cleanup(self): + """ + Shut everything down and clean up temporary files, etc. + """ + # cleanup files + for filename in self.cleanup_filenames: + if os.path.isfile(filename): + os.remove(filename) + elif os.path.isdir(filename): + shutil.rmtree(filename) + self.cleanup_filenames = [] + + # cleanup the onion + if self.onion: + self.onion.cleanup() + + +def main(cwd=None): + """ + The main() function implements all of the logic that the command-line version of + onionshare uses. + """ + strings.load_strings(helpers) + print(strings._('version_string').format(helpers.get_version())) + + # onionshare CLI in OSX needs to change current working directory (#132) + if helpers.get_platform() == 'Darwin': + if cwd: + os.chdir(cwd) + + # parse arguments + parser = argparse.ArgumentParser() + parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only")) + parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open")) + parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification")) + parser.add_argument('--stealth', action='store_true', dest='stealth', help=strings._("help_stealth")) + parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug")) + parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename')) + args = parser.parse_args() + + filenames = args.filename + for i in range(len(filenames)): + filenames[i] = os.path.abspath(filenames[i]) + + local_only = bool(args.local_only) + debug = bool(args.debug) + stay_open = bool(args.stay_open) + transparent_torification = bool(args.transparent_torification) + stealth = bool(args.stealth) + + # validation + valid = True + for filename in filenames: + if not os.path.exists(filename): + print(strings._("not_a_file").format(filename)) + valid = False + if not valid: + sys.exit() + + # start the onionshare app + try: + app = OnionShare(debug, local_only, stay_open, transparent_torification, stealth) + app.choose_port() + app.start_onion_service() + except (onion.TorTooOld, onion.TorErrorInvalidSetting, onion.TorErrorAutomatic, onion.TorErrorSocketPort, onion.TorErrorSocketFile, onion.TorErrorMissingPassword, onion.TorErrorUnreadableCookieFile) as e: + sys.exit(e.args[0]) + except KeyboardInterrupt: + print("") + sys.exit() + + # prepare files to share + print(strings._("preparing_files")) + web.set_file_info(filenames) + app.cleanup_filenames.append(web.zip_filename) + + # warn about sending large files over Tor + if web.zip_filesize >= 157286400: # 150mb + print('') + print(strings._("large_filesize")) + print('') + + # start onionshare http service in new thread + t = threading.Thread(target=web.start, args=(app.port, app.stay_open, app.transparent_torification)) + t.daemon = True + t.start() + + try: # Trap Ctrl-C + # wait for hs, only if using old version of tor + if not app.local_only and not app.onion.supports_ephemeral: + ready = app.onion.wait_for_hs(app.onion_host) + if not ready: + sys.exit() + else: + # Wait for web.generate_slug() to finish running + time.sleep(0.2) + + if(stealth): + print(strings._("give_this_url_stealth")) + print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) + print(app.auth_string) + else: + print(strings._("give_this_url")) + print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) + print('') + print(strings._("ctrlc_to_stop")) + + # wait for app to close + while t.is_alive(): + # t.join() can't catch KeyboardInterrupt in such as Ubuntu + t.join(0.5) + except KeyboardInterrupt: + web.stop(app.port) + finally: + # shutdown + app.cleanup() + +if __name__ == '__main__': + main() diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py deleted file mode 100644 index cec1daa5..00000000 --- a/onionshare/onionshare.py +++ /dev/null @@ -1,211 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2017 Micah Lee - -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 . -""" - -import os, sys, time, argparse, shutil, socket, threading - -from . import strings, helpers, web, onion - -class OnionShare(object): - """ - OnionShare is the main application class. Pass in options and run - start_onion_service and it will do the magic. - """ - def __init__(self, debug=False, local_only=False, stay_open=False, transparent_torification=False, stealth=False): - self.port = None - self.onion = None - self.hidserv_dir = None - self.onion_host = None - - # files and dirs to delete on shutdown - self.cleanup_filenames = [] - - # debug mode - if debug: - web.debug_mode() - - # do not use tor -- for development - self.local_only = local_only - - # automatically close when download is finished - self.stay_open = stay_open - - # traffic automatically goes through Tor - self.transparent_torification = transparent_torification - - # use stealth onion service - self.set_stealth(stealth) - - def set_stealth(self, stealth): - self.stealth = stealth - if self.onion: - self.onion.stealth = stealth - - def choose_port(self): - """ - Pick an un-used port in the range 17600-17650 to bind to. - """ - # let the OS choose a port - tmpsock = socket.socket() - for port in range(17600, 17650): - try: - tmpsock.bind(("127.0.0.1", port)) - break - except OSError: - pass - self.port = tmpsock.getsockname()[1] - tmpsock.close() - - def start_onion_service(self): - """ - Start the onionshare onion service. - """ - if not self.port: - self.choose_port() - - if self.local_only: - self.onion_host = '127.0.0.1:{0:d}'.format(self.port) - return - - if not self.onion: - self.onion = onion.Onion(self.transparent_torification, self.stealth) - - self.onion_host = self.onion.start(self.port) - - if self.stealth: - self.auth_string = self.onion.auth_string - - def cleanup(self): - """ - Shut everything down and clean up temporary files, etc. - """ - # cleanup files - for filename in self.cleanup_filenames: - if os.path.isfile(filename): - os.remove(filename) - elif os.path.isdir(filename): - shutil.rmtree(filename) - self.cleanup_filenames = [] - - # cleanup the onion - if self.onion: - self.onion.cleanup() - - -def main(cwd=None): - """ - The main() function implements all of the logic that the command-line version of - onionshare uses. - """ - strings.load_strings(helpers) - print(strings._('version_string').format(helpers.get_version())) - - # onionshare CLI in OSX needs to change current working directory (#132) - if helpers.get_platform() == 'Darwin': - if cwd: - os.chdir(cwd) - - # parse arguments - parser = argparse.ArgumentParser() - parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only")) - parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open")) - parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification")) - parser.add_argument('--stealth', action='store_true', dest='stealth', help=strings._("help_stealth")) - parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug")) - parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename')) - args = parser.parse_args() - - filenames = args.filename - for i in range(len(filenames)): - filenames[i] = os.path.abspath(filenames[i]) - - local_only = bool(args.local_only) - debug = bool(args.debug) - stay_open = bool(args.stay_open) - transparent_torification = bool(args.transparent_torification) - stealth = bool(args.stealth) - - # validation - valid = True - for filename in filenames: - if not os.path.exists(filename): - print(strings._("not_a_file").format(filename)) - valid = False - if not valid: - sys.exit() - - # start the onionshare app - try: - app = OnionShare(debug, local_only, stay_open, transparent_torification, stealth) - app.choose_port() - app.start_onion_service() - except (onion.TorTooOld, onion.TorErrorInvalidSetting, onion.TorErrorAutomatic, onion.TorErrorSocketPort, onion.TorErrorSocketFile, onion.TorErrorMissingPassword, onion.TorErrorUnreadableCookieFile) as e: - sys.exit(e.args[0]) - except KeyboardInterrupt: - print("") - sys.exit() - - # prepare files to share - print(strings._("preparing_files")) - web.set_file_info(filenames) - app.cleanup_filenames.append(web.zip_filename) - - # warn about sending large files over Tor - if web.zip_filesize >= 157286400: # 150mb - print('') - print(strings._("large_filesize")) - print('') - - # start onionshare http service in new thread - t = threading.Thread(target=web.start, args=(app.port, app.stay_open, app.transparent_torification)) - t.daemon = True - t.start() - - try: # Trap Ctrl-C - # wait for hs, only if using old version of tor - if not app.local_only and not app.onion.supports_ephemeral: - ready = app.onion.wait_for_hs(app.onion_host) - if not ready: - sys.exit() - else: - # Wait for web.generate_slug() to finish running - time.sleep(0.2) - - if(stealth): - print(strings._("give_this_url_stealth")) - print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) - print(app.auth_string) - else: - print(strings._("give_this_url")) - print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) - print('') - print(strings._("ctrlc_to_stop")) - - # wait for app to close - while t.is_alive(): - # t.join() can't catch KeyboardInterrupt in such as Ubuntu - t.join(0.5) - except KeyboardInterrupt: - web.stop(app.port) - finally: - # shutdown - app.cleanup() - -if __name__ == '__main__': - main() diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index d9788c70..8fb9474d 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -17,4 +17,445 @@ 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 . """ -from .onionshare_gui import * +from __future__ import division +import os, sys, subprocess, inspect, platform, argparse, threading, time, math, inspect, platform +from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import pyqtSlot + +import onionshare +from onionshare import strings, helpers, web + +from .menu import Menu +from .file_selection import FileSelection +from .server_status import ServerStatus +from .downloads import Downloads +from .options import Options +from .alert import Alert + +class Application(QtWidgets.QApplication): + """ + This is Qt's QApplication class. It has been overridden to support threads + and the quick keyboard shortcut. + """ + def __init__(self): + platform = helpers.get_platform() + if platform == 'Linux': + self.setAttribute(QtCore.Qt.AA_X11InitThreads, True) + QtWidgets.QApplication.__init__(self, sys.argv) + self.installEventFilter(self) + + def eventFilter(self, obj, event): + if (event.type() == QtCore.QEvent.KeyPress and + event.key() == QtCore.Qt.Key_Q and + event.modifiers() == QtCore.Qt.ControlModifier): + self.quit() + return False + + +class OnionShareGui(QtWidgets.QMainWindow): + """ + OnionShareGui is the main window for the GUI that contains all of the + GUI elements. + """ + start_server_finished = QtCore.pyqtSignal() + stop_server_finished = QtCore.pyqtSignal() + starting_server_step2 = QtCore.pyqtSignal() + starting_server_step3 = QtCore.pyqtSignal() + starting_server_error = QtCore.pyqtSignal(str) + + def __init__(self, qtapp, app): + super(OnionShareGui, self).__init__() + self.qtapp = qtapp + self.app = app + + self.setWindowTitle('OnionShare') + self.setWindowIcon(window_icon) + + # the menu bar + self.setMenuBar(Menu()) + + def send_files(self, filenames=None): + """ + Build the GUI in send files mode. + Note that this is the only mode currently implemented. + """ + # file selection + self.file_selection = FileSelection() + if filenames: + for filename in filenames: + self.file_selection.file_list.add_file(filename) + + # server status + self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection) + self.server_status.server_started.connect(self.file_selection.server_started) + self.server_status.server_started.connect(self.start_server) + self.server_status.server_stopped.connect(self.file_selection.server_stopped) + self.server_status.server_stopped.connect(self.stop_server) + self.start_server_finished.connect(self.clear_message) + self.start_server_finished.connect(self.server_status.start_server_finished) + self.stop_server_finished.connect(self.server_status.stop_server_finished) + self.file_selection.file_list.files_updated.connect(self.server_status.update) + self.server_status.url_copied.connect(self.copy_url) + self.server_status.hidservauth_copied.connect(self.copy_hidservauth) + self.starting_server_step2.connect(self.start_server_step2) + self.starting_server_step3.connect(self.start_server_step3) + self.starting_server_error.connect(self.start_server_error) + + # filesize warning + self.filesize_warning = QtWidgets.QLabel() + self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;') + self.filesize_warning.hide() + + # downloads + self.downloads = Downloads() + self.downloads_container = QtWidgets.QScrollArea() + self.downloads_container.setWidget(self.downloads) + self.downloads_container.setWidgetResizable(True) + self.downloads_container.setMaximumHeight(200) + self.vbar = self.downloads_container.verticalScrollBar() + self.downloads_container.hide() # downloads start out hidden + self.new_download = False + + # options + self.options = Options(web, self.app) + + # status bar + self.status_bar = QtWidgets.QStatusBar() + self.status_bar.setSizeGripEnabled(False) + version_label = QtWidgets.QLabel('v{0:s}'.format(helpers.get_version())) + version_label.setStyleSheet('color: #666666; padding: 0 10px;') + self.status_bar.addPermanentWidget(version_label) + self.setStatusBar(self.status_bar) + + # status bar, zip progress bar + self._zip_progress_bar = None + + # main layout + self.layout = QtWidgets.QVBoxLayout() + self.layout.addLayout(self.file_selection) + self.layout.addLayout(self.server_status) + self.layout.addWidget(self.filesize_warning) + self.layout.addWidget(self.downloads_container) + self.layout.addLayout(self.options) + central_widget = QtWidgets.QWidget() + central_widget.setLayout(self.layout) + self.setCentralWidget(central_widget) + self.show() + + # check for requests frequently + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.check_for_requests) + self.timer.start(500) + + def start_server(self): + """ + Start the onionshare server. This uses multiple threads to start the Tor onion + server and the web app. + """ + # Reset web counters + web.download_count = 0 + web.error404_count = 0 + web.set_gui_mode() + + # pick an available local port for the http service to listen on + self.app.choose_port() + + # disable the stealth option + self.options.set_advanced_enabled(False) + + # start onionshare http service in new thread + t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open, self.app.transparent_torification)) + t.daemon = True + t.start() + # wait for modules in thread to load, preventing a thread-related cx_Freeze crash + time.sleep(0.2) + + # start the onion service in a new thread + def start_onion_service(self): + self.status_bar.showMessage(strings._('gui_starting_server1', True)) + try: + self.app.start_onion_service() + self.starting_server_step2.emit() + + except (onionshare.onion.TorTooOld, onionshare.onion.TorErrorInvalidSetting, onionshare.onion.TorErrorAutomatic, onionshare.onion.TorErrorSocketPort, onionshare.onion.TorErrorSocketFile, onionshare.onion.TorErrorMissingPassword, onionshare.onion.TorErrorUnreadableCookieFile) as e: + self.starting_server_error.emit(e.args[0]) + return + + t = threading.Thread(target=start_onion_service, kwargs={'self': self}) + t.daemon = True + t.start() + + def start_server_step2(self): + """ + Step 2 in starting the onionshare server. Zipping up files. + """ + # add progress bar to the status bar, indicating the crunching of files. + self._zip_progress_bar = ZipProgressBar(0) + self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size( + self.file_selection.file_list.filenames) + self.status_bar.clearMessage() + self.status_bar.insertWidget(0, self._zip_progress_bar) + + # prepare the files for sending in a new thread + def finish_starting_server(self): + # prepare files to share + def _set_processed_size(x): + if self._zip_progress_bar != None: + self._zip_progress_bar.update_processed_size_signal.emit(x) + web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size) + self.app.cleanup_filenames.append(web.zip_filename) + self.starting_server_step3.emit() + + # wait for hs + if not self.app.local_only and not self.app.onion.supports_ephemeral: + self.status_bar.showMessage(strings._('gui_starting_server3', True)) + self.app.onion.wait_for_hs(self.app.onion_host) + + # done + self.start_server_finished.emit() + + #self.status_bar.showMessage(strings._('gui_starting_server2', True)) + t = threading.Thread(target=finish_starting_server, kwargs={'self': self}) + t.daemon = True + t.start() + + def start_server_step3(self): + """ + Step 3 in starting the onionshare server. This displays the large filesize + warning, if applicable. + """ + # Remove zip progress bar + if self._zip_progress_bar is not None: + self.status_bar.removeWidget(self._zip_progress_bar) + self._zip_progress_bar = None + + # warn about sending large files over Tor + if web.zip_filesize >= 157286400: # 150mb + self.filesize_warning.setText(strings._("large_filesize", True)) + self.filesize_warning.show() + + def start_server_error(self, error): + """ + If there's an error when trying to start the onion service + """ + Alert(error, QtWidgets.QMessageBox.Warning) + self.server_status.stop_server() + self.status_bar.clearMessage() + + def stop_server(self): + """ + Stop the onionshare server. + """ + if self.server_status.status != self.server_status.STATUS_STOPPED: + web.stop(self.app.port) + self.app.cleanup() + self.filesize_warning.hide() + self.options.set_advanced_enabled(True) + self.stop_server_finished.emit() + + @staticmethod + def _compute_total_size(filenames): + total_size = 0 + for filename in filenames: + if os.path.isfile(filename): + total_size += os.path.getsize(filename) + if os.path.isdir(filename): + total_size += helpers.dir_size(filename) + return total_size + + def check_for_requests(self): + """ + Check for messages communicated from the web app, and update the GUI accordingly. + """ + self.update() + # scroll to the bottom of the dl progress bar log pane + # if a new download has been added + if self.new_download: + self.vbar.setValue(self.vbar.maximum()) + self.new_download = False + # only check for requests if the server is running + if self.server_status.status != self.server_status.STATUS_STARTED: + return + + events = [] + + done = False + while not done: + try: + r = web.q.get(False) + events.append(r) + except web.queue.Empty: + done = True + + for event in events: + if event["type"] == web.REQUEST_LOAD: + self.status_bar.showMessage(strings._('download_page_loaded', True)) + + elif event["type"] == web.REQUEST_DOWNLOAD: + self.downloads_container.show() # show the downloads layout + self.downloads.add_download(event["data"]["id"], web.zip_filesize) + self.new_download = True + + elif event["type"] == web.REQUEST_RATE_LIMIT: + self.stop_server() + Alert(strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical) + + elif event["type"] == web.REQUEST_PROGRESS: + self.downloads.update_download(event["data"]["id"], event["data"]["bytes"]) + + # is the download complete? + if event["data"]["bytes"] == web.zip_filesize: + # close on finish? + if not web.get_stay_open(): + self.server_status.stop_server() + + elif event["type"] == web.REQUEST_CANCELED: + self.downloads.cancel_download(event["data"]["id"]) + + elif event["path"] != '/favicon.ico': + self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(web.error404_count, strings._('other_page_loaded', True), event["path"])) + + def copy_url(self): + """ + When the URL gets copied to the clipboard, display this in the status bar. + """ + self.status_bar.showMessage(strings._('gui_copied_url', True), 2000) + + def copy_hidservauth(self): + """ + When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. + """ + self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000) + + def clear_message(self): + """ + Clear messages from the status bar. + """ + self.status_bar.clearMessage() + + def closeEvent(self, e): + if self.server_status.status != self.server_status.STATUS_STOPPED: + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("OnionShare") + dialog.setText(strings._('gui_quit_warning', True)) + quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole) + dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), 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() + + +class ZipProgressBar(QtWidgets.QProgressBar): + update_processed_size_signal = QtCore.pyqtSignal(int) + + def __init__(self, total_files_size): + super(ZipProgressBar, self).__init__() + self.setMaximumHeight(15) + self.setMinimumWidth(200) + self.setValue(0) + self.setFormat(strings._('zip_progress_bar_format')) + self.setStyleSheet( + "QProgressBar::chunk { background-color: #05B8CC; } " + ) + + self._total_files_size = total_files_size + self._processed_size = 0 + + self.update_processed_size_signal.connect(self.update_processed_size) + + @property + def total_files_size(self): + return self._total_files_size + + @total_files_size.setter + def total_files_size(self, val): + self._total_files_size = val + + @property + def processed_size(self): + return self._processed_size + + @processed_size.setter + def processed_size(self, val): + self.update_processed_size(val) + + def update_processed_size(self, val): + self._processed_size = val + if self.processed_size < self.total_files_size: + self.setValue(int((self.processed_size * 100) / self.total_files_size)) + elif self.total_files_size != 0: + self.setValue(100) + else: + self.setValue(0) + + +def main(): + """ + The main() function implements all of the logic that the GUI version of onionshare uses. + """ + strings.load_strings(helpers) + print(strings._('version_string').format(helpers.get_version())) + + # start the Qt app + global qtapp + qtapp = Application() + + # parse arguments + parser = argparse.ArgumentParser() + parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only")) + parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open")) + parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug")) + parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification")) + parser.add_argument('--filenames', metavar='filenames', nargs='+', help=strings._('help_filename')) + args = parser.parse_args() + + filenames = args.filenames + if filenames: + for i in range(len(filenames)): + filenames[i] = os.path.abspath(filenames[i]) + + local_only = bool(args.local_only) + stay_open = bool(args.stay_open) + debug = bool(args.debug) + transparent_torification = bool(args.transparent_torification) + + # create the onionshare icon + global window_icon + window_icon = QtGui.QIcon(helpers.get_resource_path('images/logo.png')) + + # validation + if filenames: + valid = True + for filename in filenames: + if not os.path.exists(filename): + Alert(strings._("not_a_file", True).format(filename)) + valid = False + if not valid: + sys.exit() + + # start the onionshare app + web.set_stay_open(stay_open) + web.set_transparent_torification(transparent_torification) + app = onionshare.OnionShare(debug, local_only, stay_open, transparent_torification) + + # clean up when app quits + def shutdown(): + app.cleanup() + qtapp.aboutToQuit.connect(shutdown) + + # launch the gui + gui = OnionShareGui(qtapp, app) + gui.send_files(filenames) + + # all done + sys.exit(qtapp.exec_()) + +if __name__ == '__main__': + main() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py deleted file mode 100644 index 8fb9474d..00000000 --- a/onionshare_gui/onionshare_gui.py +++ /dev/null @@ -1,461 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2017 Micah Lee - -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 . -""" -from __future__ import division -import os, sys, subprocess, inspect, platform, argparse, threading, time, math, inspect, platform -from PyQt5 import QtCore, QtWidgets, QtGui -from PyQt5.QtCore import pyqtSlot - -import onionshare -from onionshare import strings, helpers, web - -from .menu import Menu -from .file_selection import FileSelection -from .server_status import ServerStatus -from .downloads import Downloads -from .options import Options -from .alert import Alert - -class Application(QtWidgets.QApplication): - """ - This is Qt's QApplication class. It has been overridden to support threads - and the quick keyboard shortcut. - """ - def __init__(self): - platform = helpers.get_platform() - if platform == 'Linux': - self.setAttribute(QtCore.Qt.AA_X11InitThreads, True) - QtWidgets.QApplication.__init__(self, sys.argv) - self.installEventFilter(self) - - def eventFilter(self, obj, event): - if (event.type() == QtCore.QEvent.KeyPress and - event.key() == QtCore.Qt.Key_Q and - event.modifiers() == QtCore.Qt.ControlModifier): - self.quit() - return False - - -class OnionShareGui(QtWidgets.QMainWindow): - """ - OnionShareGui is the main window for the GUI that contains all of the - GUI elements. - """ - start_server_finished = QtCore.pyqtSignal() - stop_server_finished = QtCore.pyqtSignal() - starting_server_step2 = QtCore.pyqtSignal() - starting_server_step3 = QtCore.pyqtSignal() - starting_server_error = QtCore.pyqtSignal(str) - - def __init__(self, qtapp, app): - super(OnionShareGui, self).__init__() - self.qtapp = qtapp - self.app = app - - self.setWindowTitle('OnionShare') - self.setWindowIcon(window_icon) - - # the menu bar - self.setMenuBar(Menu()) - - def send_files(self, filenames=None): - """ - Build the GUI in send files mode. - Note that this is the only mode currently implemented. - """ - # file selection - self.file_selection = FileSelection() - if filenames: - for filename in filenames: - self.file_selection.file_list.add_file(filename) - - # server status - self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection) - self.server_status.server_started.connect(self.file_selection.server_started) - self.server_status.server_started.connect(self.start_server) - self.server_status.server_stopped.connect(self.file_selection.server_stopped) - self.server_status.server_stopped.connect(self.stop_server) - self.start_server_finished.connect(self.clear_message) - self.start_server_finished.connect(self.server_status.start_server_finished) - self.stop_server_finished.connect(self.server_status.stop_server_finished) - self.file_selection.file_list.files_updated.connect(self.server_status.update) - self.server_status.url_copied.connect(self.copy_url) - self.server_status.hidservauth_copied.connect(self.copy_hidservauth) - self.starting_server_step2.connect(self.start_server_step2) - self.starting_server_step3.connect(self.start_server_step3) - self.starting_server_error.connect(self.start_server_error) - - # filesize warning - self.filesize_warning = QtWidgets.QLabel() - self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;') - self.filesize_warning.hide() - - # downloads - self.downloads = Downloads() - self.downloads_container = QtWidgets.QScrollArea() - self.downloads_container.setWidget(self.downloads) - self.downloads_container.setWidgetResizable(True) - self.downloads_container.setMaximumHeight(200) - self.vbar = self.downloads_container.verticalScrollBar() - self.downloads_container.hide() # downloads start out hidden - self.new_download = False - - # options - self.options = Options(web, self.app) - - # status bar - self.status_bar = QtWidgets.QStatusBar() - self.status_bar.setSizeGripEnabled(False) - version_label = QtWidgets.QLabel('v{0:s}'.format(helpers.get_version())) - version_label.setStyleSheet('color: #666666; padding: 0 10px;') - self.status_bar.addPermanentWidget(version_label) - self.setStatusBar(self.status_bar) - - # status bar, zip progress bar - self._zip_progress_bar = None - - # main layout - self.layout = QtWidgets.QVBoxLayout() - self.layout.addLayout(self.file_selection) - self.layout.addLayout(self.server_status) - self.layout.addWidget(self.filesize_warning) - self.layout.addWidget(self.downloads_container) - self.layout.addLayout(self.options) - central_widget = QtWidgets.QWidget() - central_widget.setLayout(self.layout) - self.setCentralWidget(central_widget) - self.show() - - # check for requests frequently - self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.check_for_requests) - self.timer.start(500) - - def start_server(self): - """ - Start the onionshare server. This uses multiple threads to start the Tor onion - server and the web app. - """ - # Reset web counters - web.download_count = 0 - web.error404_count = 0 - web.set_gui_mode() - - # pick an available local port for the http service to listen on - self.app.choose_port() - - # disable the stealth option - self.options.set_advanced_enabled(False) - - # start onionshare http service in new thread - t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open, self.app.transparent_torification)) - t.daemon = True - t.start() - # wait for modules in thread to load, preventing a thread-related cx_Freeze crash - time.sleep(0.2) - - # start the onion service in a new thread - def start_onion_service(self): - self.status_bar.showMessage(strings._('gui_starting_server1', True)) - try: - self.app.start_onion_service() - self.starting_server_step2.emit() - - except (onionshare.onion.TorTooOld, onionshare.onion.TorErrorInvalidSetting, onionshare.onion.TorErrorAutomatic, onionshare.onion.TorErrorSocketPort, onionshare.onion.TorErrorSocketFile, onionshare.onion.TorErrorMissingPassword, onionshare.onion.TorErrorUnreadableCookieFile) as e: - self.starting_server_error.emit(e.args[0]) - return - - t = threading.Thread(target=start_onion_service, kwargs={'self': self}) - t.daemon = True - t.start() - - def start_server_step2(self): - """ - Step 2 in starting the onionshare server. Zipping up files. - """ - # add progress bar to the status bar, indicating the crunching of files. - self._zip_progress_bar = ZipProgressBar(0) - self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size( - self.file_selection.file_list.filenames) - self.status_bar.clearMessage() - self.status_bar.insertWidget(0, self._zip_progress_bar) - - # prepare the files for sending in a new thread - def finish_starting_server(self): - # prepare files to share - def _set_processed_size(x): - if self._zip_progress_bar != None: - self._zip_progress_bar.update_processed_size_signal.emit(x) - web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size) - self.app.cleanup_filenames.append(web.zip_filename) - self.starting_server_step3.emit() - - # wait for hs - if not self.app.local_only and not self.app.onion.supports_ephemeral: - self.status_bar.showMessage(strings._('gui_starting_server3', True)) - self.app.onion.wait_for_hs(self.app.onion_host) - - # done - self.start_server_finished.emit() - - #self.status_bar.showMessage(strings._('gui_starting_server2', True)) - t = threading.Thread(target=finish_starting_server, kwargs={'self': self}) - t.daemon = True - t.start() - - def start_server_step3(self): - """ - Step 3 in starting the onionshare server. This displays the large filesize - warning, if applicable. - """ - # Remove zip progress bar - if self._zip_progress_bar is not None: - self.status_bar.removeWidget(self._zip_progress_bar) - self._zip_progress_bar = None - - # warn about sending large files over Tor - if web.zip_filesize >= 157286400: # 150mb - self.filesize_warning.setText(strings._("large_filesize", True)) - self.filesize_warning.show() - - def start_server_error(self, error): - """ - If there's an error when trying to start the onion service - """ - Alert(error, QtWidgets.QMessageBox.Warning) - self.server_status.stop_server() - self.status_bar.clearMessage() - - def stop_server(self): - """ - Stop the onionshare server. - """ - if self.server_status.status != self.server_status.STATUS_STOPPED: - web.stop(self.app.port) - self.app.cleanup() - self.filesize_warning.hide() - self.options.set_advanced_enabled(True) - self.stop_server_finished.emit() - - @staticmethod - def _compute_total_size(filenames): - total_size = 0 - for filename in filenames: - if os.path.isfile(filename): - total_size += os.path.getsize(filename) - if os.path.isdir(filename): - total_size += helpers.dir_size(filename) - return total_size - - def check_for_requests(self): - """ - Check for messages communicated from the web app, and update the GUI accordingly. - """ - self.update() - # scroll to the bottom of the dl progress bar log pane - # if a new download has been added - if self.new_download: - self.vbar.setValue(self.vbar.maximum()) - self.new_download = False - # only check for requests if the server is running - if self.server_status.status != self.server_status.STATUS_STARTED: - return - - events = [] - - done = False - while not done: - try: - r = web.q.get(False) - events.append(r) - except web.queue.Empty: - done = True - - for event in events: - if event["type"] == web.REQUEST_LOAD: - self.status_bar.showMessage(strings._('download_page_loaded', True)) - - elif event["type"] == web.REQUEST_DOWNLOAD: - self.downloads_container.show() # show the downloads layout - self.downloads.add_download(event["data"]["id"], web.zip_filesize) - self.new_download = True - - elif event["type"] == web.REQUEST_RATE_LIMIT: - self.stop_server() - Alert(strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical) - - elif event["type"] == web.REQUEST_PROGRESS: - self.downloads.update_download(event["data"]["id"], event["data"]["bytes"]) - - # is the download complete? - if event["data"]["bytes"] == web.zip_filesize: - # close on finish? - if not web.get_stay_open(): - self.server_status.stop_server() - - elif event["type"] == web.REQUEST_CANCELED: - self.downloads.cancel_download(event["data"]["id"]) - - elif event["path"] != '/favicon.ico': - self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(web.error404_count, strings._('other_page_loaded', True), event["path"])) - - def copy_url(self): - """ - When the URL gets copied to the clipboard, display this in the status bar. - """ - self.status_bar.showMessage(strings._('gui_copied_url', True), 2000) - - def copy_hidservauth(self): - """ - When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. - """ - self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000) - - def clear_message(self): - """ - Clear messages from the status bar. - """ - self.status_bar.clearMessage() - - def closeEvent(self, e): - if self.server_status.status != self.server_status.STATUS_STOPPED: - dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle("OnionShare") - dialog.setText(strings._('gui_quit_warning', True)) - quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole) - dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), 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() - - -class ZipProgressBar(QtWidgets.QProgressBar): - update_processed_size_signal = QtCore.pyqtSignal(int) - - def __init__(self, total_files_size): - super(ZipProgressBar, self).__init__() - self.setMaximumHeight(15) - self.setMinimumWidth(200) - self.setValue(0) - self.setFormat(strings._('zip_progress_bar_format')) - self.setStyleSheet( - "QProgressBar::chunk { background-color: #05B8CC; } " - ) - - self._total_files_size = total_files_size - self._processed_size = 0 - - self.update_processed_size_signal.connect(self.update_processed_size) - - @property - def total_files_size(self): - return self._total_files_size - - @total_files_size.setter - def total_files_size(self, val): - self._total_files_size = val - - @property - def processed_size(self): - return self._processed_size - - @processed_size.setter - def processed_size(self, val): - self.update_processed_size(val) - - def update_processed_size(self, val): - self._processed_size = val - if self.processed_size < self.total_files_size: - self.setValue(int((self.processed_size * 100) / self.total_files_size)) - elif self.total_files_size != 0: - self.setValue(100) - else: - self.setValue(0) - - -def main(): - """ - The main() function implements all of the logic that the GUI version of onionshare uses. - """ - strings.load_strings(helpers) - print(strings._('version_string').format(helpers.get_version())) - - # start the Qt app - global qtapp - qtapp = Application() - - # parse arguments - parser = argparse.ArgumentParser() - parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only")) - parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open")) - parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug")) - parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification")) - parser.add_argument('--filenames', metavar='filenames', nargs='+', help=strings._('help_filename')) - args = parser.parse_args() - - filenames = args.filenames - if filenames: - for i in range(len(filenames)): - filenames[i] = os.path.abspath(filenames[i]) - - local_only = bool(args.local_only) - stay_open = bool(args.stay_open) - debug = bool(args.debug) - transparent_torification = bool(args.transparent_torification) - - # create the onionshare icon - global window_icon - window_icon = QtGui.QIcon(helpers.get_resource_path('images/logo.png')) - - # validation - if filenames: - valid = True - for filename in filenames: - if not os.path.exists(filename): - Alert(strings._("not_a_file", True).format(filename)) - valid = False - if not valid: - sys.exit() - - # start the onionshare app - web.set_stay_open(stay_open) - web.set_transparent_torification(transparent_torification) - app = onionshare.OnionShare(debug, local_only, stay_open, transparent_torification) - - # clean up when app quits - def shutdown(): - app.cleanup() - qtapp.aboutToQuit.connect(shutdown) - - # launch the gui - gui = OnionShareGui(qtapp, app) - gui.send_files(filenames) - - # all done - sys.exit(qtapp.exec_()) - -if __name__ == '__main__': - main() From c991a407e2e39cd1e72c302ffe951fe01681e96b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 7 Jan 2017 17:31:26 -0800 Subject: [PATCH 43/43] Add new AuthenticationFailure exception, and make the Onion/stem code catch more exceptions when connecting to a Tor controller --- onionshare/__init__.py | 2 +- onionshare/onion.py | 14 ++++++++++++-- onionshare_gui/__init__.py | 2 +- onionshare_gui/settings_dialog.py | 4 ++-- resources/locale/en.json | 3 ++- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index cec1daa5..8de8fa16 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -155,7 +155,7 @@ def main(cwd=None): app = OnionShare(debug, local_only, stay_open, transparent_torification, stealth) app.choose_port() app.start_onion_service() - except (onion.TorTooOld, onion.TorErrorInvalidSetting, onion.TorErrorAutomatic, onion.TorErrorSocketPort, onion.TorErrorSocketFile, onion.TorErrorMissingPassword, onion.TorErrorUnreadableCookieFile) as e: + except (onion.TorTooOld, onion.TorErrorInvalidSetting, onion.TorErrorAutomatic, onion.TorErrorSocketPort, onion.TorErrorSocketFile, onion.TorErrorMissingPassword, onion.TorErrorUnreadableCookieFile, onion.TorErrorAuthError) as e: sys.exit(e.args[0]) except KeyboardInterrupt: print("") diff --git a/onionshare/onion.py b/onionshare/onion.py index 2e3c9211..4fd5e5c9 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -20,7 +20,7 @@ along with this program. If not, see . from stem.control import Controller from stem import SocketError -from stem.connection import MissingPassword, UnreadableCookieFile +from stem.connection import MissingPassword, UnreadableCookieFile, AuthenticationFailure import os, sys, tempfile, shutil, urllib, platform from . import socks @@ -65,6 +65,13 @@ class TorErrorUnreadableCookieFile(Exception): """ pass +class TorErrorAuthError(Exception): + """ + OnionShare connected to the address and port, but can't authenticate. It's possible + that a Tor controller isn't listening on this port. + """ + pass + class TorTooOld(Exception): """ This exception is raised if onionshare needs to use a feature of Tor or stem @@ -180,12 +187,13 @@ class Onion(object): else: raise TorErrorInvalidSetting(strings._("settings_error_unknown")) - except SocketError: + except: if self.settings.get('connection_type') == 'control_port': raise TorErrorSocketPort(strings._("settings_error_socket_port").format(self.settings.get('control_port_address'), self.settings.get('control_port_port'))) else: raise TorErrorSocketFile(strings._("settings_error_socket_file").format(self.settings.get('socket_file_path'))) + # Try authenticating try: if self.settings.get('auth_type') == 'no_auth': @@ -199,6 +207,8 @@ class Onion(object): raise TorErrorMissingPassword(strings._('settings_error_missing_password')) except UnreadableCookieFile: raise TorErrorUnreadableCookieFile(strings._('settings_error_unreadable_cookie_file')) + except AuthenticationFailure: + raise TorErrorAuthError(strings._('settings_error_auth').format(self.settings.get('control_port_address'), self.settings.get('control_port_port'))) # get the tor version self.tor_version = self.c.get_version().version_str diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 8fb9474d..a49a0b7c 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -177,7 +177,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.app.start_onion_service() self.starting_server_step2.emit() - except (onionshare.onion.TorTooOld, onionshare.onion.TorErrorInvalidSetting, onionshare.onion.TorErrorAutomatic, onionshare.onion.TorErrorSocketPort, onionshare.onion.TorErrorSocketFile, onionshare.onion.TorErrorMissingPassword, onionshare.onion.TorErrorUnreadableCookieFile) as e: + except (onionshare.onion.TorTooOld, onionshare.onion.TorErrorInvalidSetting, onionshare.onion.TorErrorAutomatic, onionshare.onion.TorErrorSocketPort, onionshare.onion.TorErrorSocketFile, onionshare.onion.TorErrorMissingPassword, onionshare.onion.TorErrorUnreadableCookieFile, onionshare.onion.TorErrorAuthError) as e: self.starting_server_error.emit(e.args[0]) return diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 1c08b26c..8db649f8 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -21,7 +21,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.settings import Settings -from onionshare.onion import Onion, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile +from onionshare.onion import Onion, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError from .alert import Alert @@ -216,7 +216,7 @@ class SettingsDialog(QtWidgets.QDialog): # If an exception hasn't been raised yet, the Tor settings work Alert(strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) - except (TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile) as e: + except (TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError) as e: Alert(e.args[0], QtWidgets.QMessageBox.Warning) def save_clicked(self): diff --git a/resources/locale/en.json b/resources/locale/en.json index 1d222a20..74fe4739 100644 --- a/resources/locale/en.json +++ b/resources/locale/en.json @@ -77,8 +77,9 @@ "settings_saved": "Settings saved to {}", "settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.", "settings_error_automatic": "Can't connect to Tor controller. Is Tor Browser running in the background? If you don't have it you can get it from:\nhttps://www.torproject.org/.", - "settings_error_socket_port": "Can't connect to Tor controller on address {} with port {}.", + "settings_error_socket_port": "Can't connect to Tor controller on {}:{}.", "settings_error_socket_file": "Can't connect to Tor controller using socket file {}.", + "settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?", "settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.", "settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user doesn't have permission to read the cookie file.", "settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}"