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