# -*- 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 .mode_settings_widget import ModeSettingsWidget from ..server_status import ServerStatus from ...threads import OnionThread, 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) change_persistent = QtCore.pyqtSignal(int, bool) def __init__(self, tab): super(Mode, self).__init__() self.tab = tab self.settings = tab.mode_settings self.common = tab.common self.qtapp = self.common.gui.qtapp self.app = tab.app self.status_bar = tab.status_bar self.server_status_label = tab.status_bar.server_status_label self.system_tray = tab.system_tray self.filenames = tab.filenames # The web object gets created in init() self.web = None # 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, self.settings, None, self.common.gui.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) # Mode settings widget self.mode_settings_widget = ModeSettingsWidget( self.common, self.tab.tab_id, self.tab.mode_settings ) self.mode_settings_widget.change_persistent.connect(self.change_persistent) self.mode_settings_widget.update_server_status.connect( self.server_status.update ) # Header # Note: It's up to the downstream Mode to add this to its layout self.header_label = QtWidgets.QLabel() self.header_label.setStyleSheet(self.common.gui.css["mode_header_label"]) self.header_label.setAlignment(QtCore.Qt.AlignHCenter) # Primary action # Note: It's up to the downstream Mode to add this to its layout self.primary_action_layout = QtWidgets.QVBoxLayout() self.primary_action_layout.addWidget(self.mode_settings_widget) self.primary_action_layout.addWidget(self.server_status) self.primary_action = QtWidgets.QWidget() self.primary_action.setLayout(self.primary_action_layout) 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 = ( (f"{days}{strings._('days_first_letter')}, " if days else "") + (f"{hours}{strings._('hours_first_letter')}, " if hours else "") + (f"{minutes}{strings._('minutes_first_letter')}, " if minutes else "") + f"{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.settings.get( "general", "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) # Clear the status bar self.status_bar.clearMessage() self.server_status_label.setText("") # Hide the mode settings self.mode_settings_widget.hide() # Ensure we always get a new random port each time we might launch an OnionThread self.app.port = None # 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.settings.get("general", "autostop_timer"): # Convert the date value to seconds between now and then now = QtCore.QDateTime.currentDateTime() self.autostop_timer_datetime_delta = now.secsTo( 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() # Show the mode settings self.mode_settings_widget.show() 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"])