# -*- coding: utf-8 -*- """ OnionShare | https://onionshare.org/ Copyright (C) 2014-2018 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 from onionshare.common import AutoStopTimer from .history import IndividualFileHistoryItem from ..server_status import ServerStatus from ..threads import OnionThread from ..threads import AutoStartTimer from ..widgets import Alert class Mode(QtWidgets.QWidget): """ The class that all modes inherit from """ 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) starting_server_early = QtCore.pyqtSignal() set_server_active = QtCore.pyqtSignal(bool) def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False): super(Mode, self).__init__() self.common = common self.qtapp = qtapp self.app = app self.status_bar = status_bar self.server_status_label = server_status_label self.system_tray = system_tray self.filenames = filenames # The web object gets created in init() self.web = None # Local mode is passed from OnionShareGui self.local_only = local_only # Threads start out as None self.onion_thread = None self.web_thread = None self.startup_thread = None # Server status self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only) self.server_status.server_started.connect(self.start_server) self.server_status.server_stopped.connect(self.stop_server) self.server_status.server_canceled.connect(self.cancel_server) self.start_server_finished.connect(self.server_status.start_server_finished) self.stop_server_finished.connect(self.server_status.stop_server_finished) self.starting_server_step2.connect(self.start_server_step2) self.starting_server_step3.connect(self.start_server_step3) self.starting_server_early.connect(self.start_server_early) self.starting_server_error.connect(self.start_server_error) # Primary action # Note: It's up to the downstream Mode to add this to its layout self.primary_action_layout = QtWidgets.QVBoxLayout() self.primary_action_layout.addWidget(self.server_status) self.primary_action = QtWidgets.QWidget() self.primary_action.setLayout(self.primary_action_layout) # Hack to allow a minimum width on the main layout # Note: It's up to the downstream Mode to add this to its layout self.min_width_widget = QtWidgets.QWidget() self.min_width_widget.setMinimumWidth(600) def init(self): """ Add custom initialization here. """ pass def human_friendly_time(self, secs): """ Returns a human-friendly time delta from given seconds. """ days = secs//86400 hours = (secs - days*86400)//3600 minutes = (secs - days*86400 - hours*3600)//60 seconds = secs - days*86400 - hours*3600 - minutes*60 if not seconds: seconds = '0' result = ("{0}{1}, ".format(days, strings._('days_first_letter')) if days else "") + \ ("{0}{1}, ".format(hours, strings._('hours_first_letter')) if hours else "") + \ ("{0}{1}, ".format(minutes, strings._('minutes_first_letter')) if minutes else "") + \ "{0}{1}".format(seconds, strings._('seconds_first_letter')) return result def timer_callback(self): """ This method is called regularly on a timer. """ # If this is a scheduled share, display the countdown til the share starts if self.server_status.status == ServerStatus.STATUS_WORKING: if self.server_status.autostart_timer_datetime: now = QtCore.QDateTime.currentDateTime() if self.server_status.local_only: seconds_remaining = now.secsTo(self.server_status.autostart_timer_widget.dateTime()) else: seconds_remaining = now.secsTo(self.server_status.autostart_timer_datetime.replace(second=0, microsecond=0)) # Update the server button if seconds_remaining > 0: self.server_status.server_button.setText(strings._('gui_waiting_to_start').format(self.human_friendly_time(seconds_remaining))) else: self.server_status.server_button.setText(strings._('gui_please_wait')) # If the auto-stop timer has stopped, stop the server if self.server_status.status == ServerStatus.STATUS_STARTED: if self.app.autostop_timer_thread and self.common.settings.get('autostop_timer'): if self.autostop_timer_datetime_delta > 0: now = QtCore.QDateTime.currentDateTime() seconds_remaining = now.secsTo(self.server_status.autostop_timer_datetime) # Update the server button server_button_text = self.get_stop_server_autostop_timer_text() self.server_status.server_button.setText(server_button_text.format(self.human_friendly_time(seconds_remaining))) self.status_bar.clearMessage() if not self.app.autostop_timer_thread.is_alive(): if self.autostop_timer_finished_should_stop_server(): self.server_status.stop_server() def timer_callback_custom(self): """ Add custom timer code. """ pass def get_stop_server_autostop_timer_text(self): """ Return the string to put on the stop server button, if there's an auto-stop timer """ pass def autostop_timer_finished_should_stop_server(self): """ The auto-stop timer expired, should we stop the server? Returns a bool """ pass def start_server(self): """ Start the onionshare server. This uses multiple threads to start the Tor onion server and the web app. """ self.common.log('Mode', 'start_server') self.start_server_custom() self.set_server_active.emit(True) self.app.set_stealth(self.common.settings.get('use_stealth')) # Clear the status bar self.status_bar.clearMessage() self.server_status_label.setText('') # Ensure we always get a new random port each time we might launch an OnionThread self.app.port = None # Start the onion thread. If this share was scheduled for a future date, # the OnionThread will start and exit 'early' to obtain the port, password # and onion address, but it will not start the WebThread yet. if self.server_status.autostart_timer_datetime: self.start_onion_thread(obtain_onion_early=True) else: self.start_onion_thread() # If scheduling a share, delay starting the real share if self.server_status.autostart_timer_datetime: self.common.log('Mode', 'start_server', 'Starting auto-start timer') self.startup_thread = AutoStartTimer(self) # Once the timer has finished, start the real share, with a WebThread self.startup_thread.success.connect(self.start_scheduled_service) self.startup_thread.error.connect(self.start_server_error) self.startup_thread.canceled = False self.startup_thread.start() def start_onion_thread(self, obtain_onion_early=False): self.common.log('Mode', 'start_server', 'Starting an onion thread') self.obtain_onion_early = obtain_onion_early self.onion_thread = OnionThread(self) self.onion_thread.success.connect(self.starting_server_step2.emit) self.onion_thread.success_early.connect(self.starting_server_early.emit) self.onion_thread.error.connect(self.starting_server_error.emit) self.onion_thread.start() def start_scheduled_service(self, obtain_onion_early=False): # We start a new OnionThread with the saved scheduled key from settings self.common.settings.load() self.obtain_onion_early = obtain_onion_early self.common.log('Mode', 'start_server', 'Starting a scheduled onion thread') self.onion_thread = OnionThread(self) self.onion_thread.success.connect(self.starting_server_step2.emit) self.onion_thread.error.connect(self.starting_server_error.emit) self.onion_thread.start() def start_server_custom(self): """ Add custom initialization here. """ pass def start_server_early(self): """ An 'early' start of an onion service in order to obtain the onion address for a scheduled start. Shows the onion address in the UI in advance of actually starting the share. """ self.server_status.show_url() def start_server_step2(self): """ Step 2 in starting the onionshare server. """ self.common.log('Mode', 'start_server_step2') self.start_server_step2_custom() # Nothing to do here. # start_server_step2_custom has call these to move on: # self.starting_server_step3.emit() # self.start_server_finished.emit() def start_server_step2_custom(self): """ Add custom initialization here. """ pass def start_server_step3(self): """ Step 3 in starting the onionshare server. """ self.common.log('Mode', 'start_server_step3') self.start_server_step3_custom() if self.common.settings.get('autostop_timer'): # Convert the date value to seconds between now and then now = QtCore.QDateTime.currentDateTime() self.autostop_timer_datetime_delta = now.secsTo(self.server_status.autostop_timer_datetime) # Start the auto-stop timer if self.autostop_timer_datetime_delta > 0: self.app.autostop_timer_thread = AutoStopTimer(self.common, self.autostop_timer_datetime_delta) self.app.autostop_timer_thread.start() # The auto-stop timer has actually already passed since the user clicked Start. Probably the Onion service took too long to start. else: self.stop_server() self.start_server_error(strings._('gui_server_started_after_autostop_timer')) def start_server_step3_custom(self): """ Add custom initialization here. """ pass def start_server_error(self, error): """ If there's an error when trying to start the onion service """ self.common.log('Mode', 'start_server_error') Alert(self.common, error, QtWidgets.QMessageBox.Warning) self.set_server_active.emit(False) self.server_status.stop_server() self.status_bar.clearMessage() self.start_server_error_custom() def start_server_error_custom(self): """ Add custom initialization here. """ pass def cancel_server(self): """ Cancel the server while it is preparing to start """ self.cancel_server_custom() if self.startup_thread: self.common.log('Mode', 'cancel_server: quitting startup thread') self.startup_thread.canceled = True self.app.onion.scheduled_key = None self.app.onion.scheduled_auth_cookie = None self.startup_thread.quit() if self.onion_thread: self.common.log('Mode', 'cancel_server: quitting onion thread') self.onion_thread.quit() if self.web_thread: self.common.log('Mode', 'cancel_server: quitting web thread') self.web_thread.quit() self.stop_server() def cancel_server_custom(self): """ Add custom initialization here. """ pass def stop_server(self): """ Stop the onionshare server. """ self.common.log('Mode', 'stop_server') if self.server_status.status != ServerStatus.STATUS_STOPPED: try: self.web.stop(self.app.port) except: # Probably we had no port to begin with (Onion service didn't start) pass self.app.cleanup() self.stop_server_custom() self.set_server_active.emit(False) self.stop_server_finished.emit() def stop_server_custom(self): """ Add custom initialization here. """ pass def handle_tor_broke(self): """ Handle connection from Tor breaking. """ if self.server_status.status != ServerStatus.STATUS_STOPPED: self.server_status.stop_server() self.handle_tor_broke_custom() def handle_tor_broke_custom(self): """ Add custom initialization here. """ pass # Handle web server events def handle_request_load(self, event): """ Handle REQUEST_LOAD event. """ pass def handle_request_started(self, event): """ Handle REQUEST_STARTED event. """ pass def handle_request_rate_limit(self, event): """ Handle REQUEST_RATE_LIMIT event. """ self.stop_server() Alert(self.common, strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical) def handle_request_progress(self, event): """ Handle REQUEST_PROGRESS event. """ pass def handle_request_canceled(self, event): """ Handle REQUEST_CANCELED event. """ pass def handle_request_upload_file_renamed(self, event): """ Handle REQUEST_UPLOAD_FILE_RENAMED event. """ pass def handle_request_upload_set_dir(self, event): """ Handle REQUEST_UPLOAD_SET_DIR event. """ pass def handle_request_upload_finished(self, event): """ Handle REQUEST_UPLOAD_FINISHED event. """ pass def handle_request_upload_canceled(self, event): """ Handle REQUEST_UPLOAD_CANCELED event. """ pass def handle_request_individual_file_started(self, event): """ Handle REQUEST_INDVIDIDUAL_FILES_STARTED event. Used in both Share and Website modes, so implemented here. """ self.toggle_history.update_indicator(True) self.history.requests_count += 1 self.history.update_requests() item = IndividualFileHistoryItem(self.common, event["data"], event["path"]) self.history.add(event["data"]["id"], item) def handle_request_individual_file_progress(self, event): """ Handle REQUEST_INDVIDIDUAL_FILES_PROGRESS event. Used in both Share and Website modes, so implemented here. """ self.history.update(event["data"]["id"], event["data"]["bytes"]) if self.server_status.status == self.server_status.STATUS_STOPPED: self.history.cancel(event["data"]["id"]) def handle_request_individual_file_canceled(self, event): """ Handle REQUEST_INDVIDIDUAL_FILES_CANCELED event. Used in both Share and Website modes, so implemented here. """ self.history.cancel(event["data"]["id"])