diff options
author | Tomás Touceda <chiiph@leap.se> | 2014-05-16 16:18:59 -0300 |
---|---|---|
committer | Tomás Touceda <chiiph@leap.se> | 2014-05-16 16:18:59 -0300 |
commit | 4c550c558dcb554b3ea1bc0246492e39e8532886 (patch) | |
tree | 3a18999e62eb18091d002e6c1c7222846a92947b /src/leap/bitmask | |
parent | 81715dc47d77934c4f67d2527a56c28f58f0345d (diff) | |
parent | d52daa106a97562e1cc67c5a2f242f2ef9884508 (diff) |
Merge branch 'release-0.5.1'0.5.1
Diffstat (limited to 'src/leap/bitmask')
31 files changed, 2365 insertions, 1358 deletions
diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 02e27123..e413ab4c 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -76,6 +76,16 @@ def sigint_handler(*args, **kwargs): mainwindow = args[0] mainwindow.quit() +def sigterm_handler(*args, **kwargs): + """ + Signal handler for SIGTERM. + This handler is actually passed to twisted reactor + """ + logger = kwargs.get('logger', None) + if logger: + logger.debug("SIGTERM catched. shutting down...") + mainwindow = args[0] + mainwindow.quit() def add_logger_handlers(debug=False, logfile=None, replace_stdout=True): """ @@ -200,7 +210,7 @@ def main(): debug = opts.debug logfile = opts.log_file mail_logfile = opts.mail_log_file - openvpn_verb = opts.openvpn_verb + start_hidden = opts.start_hidden ############################################################# # Given how paths and bundling works, we need to delay the imports @@ -213,6 +223,8 @@ def main(): flags.MAIL_LOGFILE = mail_logfile flags.APP_VERSION_CHECK = opts.app_version_check flags.API_VERSION_CHECK = opts.api_version_check + flags.OPENVPN_VERBOSITY = opts.openvpn_verb + flags.SKIP_WIZARD_CHECKS = opts.skip_wizard_checks flags.CA_CERT_FILE = opts.ca_cert_file @@ -306,12 +318,15 @@ def main(): window = MainWindow( lambda: twisted_main.quit(app), - openvpn_verb=openvpn_verb, - bypass_checks=bypass_checks) + bypass_checks=bypass_checks, + start_hidden=start_hidden) sigint_window = partial(sigint_handler, window, logger=logger) signal.signal(signal.SIGINT, sigint_window) + # callable used in addSystemEventTrigger to handle SIGTERM + sigterm_window = partial(sigterm_handler, window, logger=logger) + if IS_MAC: window.raise_() @@ -322,6 +337,12 @@ def main(): l = LoopingCall(QtCore.QCoreApplication.processEvents, 0, 10) l.start(0.01) + + # SIGTERM can't be handled the same way SIGINT is, since it's + # caught by twisted. See _handleSignals method in + # twisted/internet/base.py#L1150. So, addSystemEventTrigger + # reactor's method is used. + reactor.addSystemEventTrigger('before', 'shutdown', sigterm_window) reactor.run() if __name__ == "__main__": diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index 45ea451c..a2df465d 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -17,11 +17,15 @@ """ Backend for everything """ +import commands import logging +import os +import time from functools import partial from Queue import Queue, Empty +from twisted.internet import reactor from twisted.internet import threads, defer from twisted.internet.task import LoopingCall from twisted.python import log @@ -29,9 +33,19 @@ from twisted.python import log import zope.interface from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.crypto.srpauth import SRPAuth from leap.bitmask.crypto.srpregister import SRPRegister +from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.provider import get_provider_path from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.services.eip import eipconfig +from leap.bitmask.services.eip import get_openvpn_management +from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper + +from leap.bitmask.services.eip import vpnlauncher, vpnprocess +from leap.bitmask.services.eip import linuxvpnlauncher, darwinvpnlauncher + +from leap.common import certs as leap_certs # Frontend side from PySide import QtCore @@ -39,6 +53,26 @@ from PySide import QtCore logger = logging.getLogger(__name__) +def get_provider_config(config, domain): + """ + Return the ProviderConfig object for the given domain. + If it is already loaded in `config`, then don't reload. + + :param config: a ProviderConfig object + :type conig: ProviderConfig + :param domain: the domain which config is required. + :type domain: unicode + + :returns: True if the config was loaded successfully, False otherwise. + :rtype: bool + """ + # TODO: see ProviderConfig.get_provider_config + if (not config.loaded() or config.get_domain() != domain): + config.load(get_provider_path(domain)) + + return config.loaded() + + class ILEAPComponent(zope.interface.Interface): """ Interface that every component for the backend should comply to @@ -54,7 +88,7 @@ class ILEAPService(ILEAPComponent): def start(self): """ - Starts the service. + Start the service. """ pass @@ -66,13 +100,13 @@ class ILEAPService(ILEAPComponent): def terminate(self): """ - Terminates the service, not necessarily in a nice way. + Terminate the service, not necessarily in a nice way. """ pass def status(self): """ - Returns a json object with the current status for the service. + Return a json object with the current status for the service. :rtype: object (list, str, dict) """ @@ -83,7 +117,7 @@ class ILEAPService(ILEAPComponent): def set_configs(self, keyval): """ - Sets the config parameters for this Service. + Set the config parameters for this Service. :param keyval: values to configure :type keyval: dict, {str: str} @@ -92,7 +126,7 @@ class ILEAPService(ILEAPComponent): def get_configs(self, keys): """ - Returns the configuration values for the list of keys. + Return the configuration values for the list of keys. :param keys: keys to retrieve :type keys: list of str @@ -109,8 +143,6 @@ class Provider(object): zope.interface.implements(ILEAPComponent) - PROBLEM_SIGNAL = "prov_problem_with_provider" - def __init__(self, signaler=None, bypass_checks=False): """ Constructor for the Provider component @@ -123,7 +155,6 @@ class Provider(object): certificates at bootstrap :type bypass_checks: bool """ - object.__init__(self) self.key = "provider" self._provider_bootstrapper = ProviderBootstrapper(signaler, bypass_checks) @@ -132,7 +163,7 @@ class Provider(object): def setup_provider(self, provider): """ - Initiates the setup for a provider + Initiate the setup for a provider :param provider: URL for the provider :type provider: unicode @@ -166,19 +197,15 @@ class Provider(object): """ d = None - # If there's no loaded provider or - # we want to connect to other provider... - if (not self._provider_config.loaded() or - self._provider_config.get_domain() != provider): - self._provider_config.load(get_provider_path(provider)) - - if self._provider_config.loaded(): + config = self._provider_config + if get_provider_config(config, provider): d = self._provider_bootstrapper.run_provider_setup_checks( self._provider_config, download_if_needed=True) else: if self._signaler is not None: - self._signaler.signal(self.PROBLEM_SIGNAL) + self._signaler.signal( + self._signaler.PROV_PROBLEM_WITH_PROVIDER_KEY) logger.error("Could not load provider configuration.") self._login_widget.set_enabled(True) @@ -202,10 +229,8 @@ class Register(object): back to the frontend :type signaler: Signaler """ - object.__init__(self) self.key = "register" self._signaler = signaler - self._provider_config = ProviderConfig() def register_user(self, domain, username, password): """ @@ -221,23 +246,406 @@ class Register(object): :returns: the defer for the operation running in a thread. :rtype: twisted.internet.defer.Deferred """ - # If there's no loaded provider or - # we want to connect to other provider... - if (not self._provider_config.loaded() or - self._provider_config.get_domain() != domain): - self._provider_config.load(get_provider_path(domain)) - - if self._provider_config.loaded(): + config = ProviderConfig() + if get_provider_config(config, domain): srpregister = SRPRegister(signaler=self._signaler, - provider_config=self._provider_config) + provider_config=config) return threads.deferToThread( partial(srpregister.register_user, username, password)) else: if self._signaler is not None: - self._signaler.signal(self._signaler.srp_registration_failed) + self._signaler.signal(self._signaler.SRP_REGISTRATION_FAILED) logger.error("Could not load provider configuration.") +class EIP(object): + """ + Interfaces with setup and launch of EIP + """ + + zope.interface.implements(ILEAPService) + + def __init__(self, signaler=None): + """ + Constructor for the EIP component + + :param signaler: Object in charge of handling communication + back to the frontend + :type signaler: Signaler + """ + self.key = "eip" + self._signaler = signaler + self._eip_bootstrapper = EIPBootstrapper(signaler) + self._eip_setup_defer = None + self._provider_config = ProviderConfig() + + self._vpn = vpnprocess.VPN(signaler=signaler) + + def setup_eip(self, domain, skip_network=False): + """ + Initiate the setup for a provider + + :param domain: URL for the provider + :type domain: unicode + :param skip_network: Whether checks that involve network should be done + or not + :type skip_network: bool + + :returns: the defer for the operation running in a thread. + :rtype: twisted.internet.defer.Deferred + """ + config = self._provider_config + if get_provider_config(config, domain): + if skip_network: + return defer.Deferred() + eb = self._eip_bootstrapper + d = eb.run_eip_setup_checks(self._provider_config, + download_if_needed=True) + self._eip_setup_defer = d + return d + else: + raise Exception("No provider setup loaded") + + def cancel_setup_eip(self): + """ + Cancel the ongoing setup eip defer (if any). + """ + d = self._eip_setup_defer + if d is not None: + d.cancel() + + def _start_eip(self): + """ + Start EIP + """ + provider_config = self._provider_config + eip_config = eipconfig.EIPConfig() + domain = provider_config.get_domain() + + loaded = eipconfig.load_eipconfig_if_needed( + provider_config, eip_config, domain) + + if not loaded: + if self._signaler is not None: + self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED) + logger.error("Tried to start EIP but cannot find any " + "available provider!") + return + + host, port = get_openvpn_management() + self._vpn.start(eipconfig=eip_config, + providerconfig=provider_config, + socket_host=host, socket_port=port) + + def start(self): + """ + Start the service. + """ + signaler = self._signaler + + if not self._provider_config.loaded(): + # This means that the user didn't call setup_eip first. + self._signaler.signal(signaler.BACKEND_BAD_CALL, "EIP.start(), " + "no provider loaded") + return + + try: + self._start_eip() + except vpnprocess.OpenVPNAlreadyRunning: + signaler.signal(signaler.EIP_OPENVPN_ALREADY_RUNNING) + except vpnprocess.AlienOpenVPNAlreadyRunning: + signaler.signal(signaler.EIP_ALIEN_OPENVPN_ALREADY_RUNNING) + except vpnlauncher.OpenVPNNotFoundException: + signaler.signal(signaler.EIP_OPENVPN_NOT_FOUND_ERROR) + except vpnlauncher.VPNLauncherException: + # TODO: this seems to be used for 'gateway not found' only. + # see vpnlauncher.py + signaler.signal(signaler.EIP_VPN_LAUNCHER_EXCEPTION) + except linuxvpnlauncher.EIPNoPolkitAuthAgentAvailable: + signaler.signal(signaler.EIP_NO_POLKIT_AGENT_ERROR) + except linuxvpnlauncher.EIPNoPkexecAvailable: + signaler.signal(signaler.EIP_NO_PKEXEC_ERROR) + except darwinvpnlauncher.EIPNoTunKextLoaded: + signaler.signal(signaler.EIP_NO_TUN_KEXT_ERROR) + except Exception as e: + logger.error("Unexpected problem: {0!r}".format(e)) + else: + # TODO: are we connected here? + signaler.signal(signaler.EIP_CONNECTED) + + def stop(self, shutdown=False): + """ + Stop the service. + """ + self._vpn.terminate(shutdown) + if IS_LINUX: + self._wait_for_firewall_down() + + def _wait_for_firewall_down(self): + """ + Wait for the firewall to come down. + """ + # Due to how we delay the resolvconf action in linux. + # XXX this *has* to wait for a reasonable lapse, since we have some + # delay in vpn.terminate. + # For a better solution it should be signaled from backend that + # everything is clear to proceed, or a timeout happened. + MAX_FW_WAIT_RETRIES = 25 + FW_WAIT_STEP = 0.5 + + retry = 0 + + fw_up_cmd = "pkexec /usr/sbin/bitmask-root firewall isup" + fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256 + + while retry < MAX_FW_WAIT_RETRIES: + if fw_is_down(): + return + else: + time.sleep(FW_WAIT_STEP) + retry += 1 + logger.warning("After waiting, firewall is not down... " + "You might experience lack of connectivity") + + def terminate(self): + """ + Terminate the service, not necessarily in a nice way. + """ + self._vpn.killit() + + def status(self): + """ + Return a json object with the current status for the service. + + :rtype: object (list, str, dict) + """ + # XXX: Use a namedtuple or a specific object instead of a json + # object, since parsing it will be problematic otherwise. + # It has to be something easily serializable though. + pass + + def _provider_is_initialized(self, domain): + """ + Return whether the given domain is initialized or not. + + :param domain: the domain to check + :type domain: str + + :returns: True if is initialized, False otherwise. + :rtype: bool + """ + eipconfig_path = eipconfig.get_eipconfig_path(domain, relative=False) + if os.path.isfile(eipconfig_path): + return True + else: + return False + + def get_initialized_providers(self, domains): + """ + Signal a list of the given domains and if they are initialized or not. + + :param domains: the list of domains to check. + :type domain: list of str + + Signals: + eip_get_initialized_providers -> list of tuple(unicode, bool) + """ + filtered_domains = [] + for domain in domains: + is_initialized = self._provider_is_initialized(domain) + filtered_domains.append((domain, is_initialized)) + + if self._signaler is not None: + self._signaler.signal(self._signaler.EIP_GET_INITIALIZED_PROVIDERS, + filtered_domains) + + def get_gateways_list(self, domain): + """ + Signal a list of gateways for the given provider. + + :param domain: the domain to get the gateways. + :type domain: str + + Signals: + eip_get_gateways_list -> list of unicode + eip_get_gateways_list_error + eip_uninitialized_provider + """ + if not self._provider_is_initialized(domain): + if self._signaler is not None: + self._signaler.signal( + self._signaler.EIP_UNINITIALIZED_PROVIDER) + return + + eip_config = eipconfig.EIPConfig() + provider_config = ProviderConfig.get_provider_config(domain) + + api_version = provider_config.get_api_version() + eip_config.set_api_version(api_version) + eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain)) + + # check for other problems + if not eip_loaded or provider_config is None: + if self._signaler is not None: + self._signaler.signal( + self._signaler.EIP_GET_GATEWAYS_LIST_ERROR) + return + + gateways = eipconfig.VPNGatewaySelector(eip_config).get_gateways_list() + + if self._signaler is not None: + self._signaler.signal( + self._signaler.EIP_GET_GATEWAYS_LIST, gateways) + + def can_start(self, domain): + """ + Signal whether it has everything that is needed to run EIP or not + + :param domain: the domain for the provider to check + :type domain: str + + Signals: + eip_can_start + eip_cannot_start + """ + try: + eip_config = eipconfig.EIPConfig() + provider_config = ProviderConfig.get_provider_config(domain) + + api_version = provider_config.get_api_version() + eip_config.set_api_version(api_version) + eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain)) + + # check for other problems + if not eip_loaded or provider_config is None: + raise Exception("Cannot load provider and eip config, cannot " + "autostart") + + client_cert_path = eip_config.\ + get_client_cert_path(provider_config, about_to_download=False) + + if leap_certs.should_redownload(client_cert_path): + raise Exception("The client should redownload the certificate," + " cannot autostart") + + if not os.path.isfile(client_cert_path): + raise Exception("Can't find the certificate, cannot autostart") + + if self._signaler is not None: + self._signaler.signal(self._signaler.EIP_CAN_START) + except Exception as e: + logger.exception(e) + if self._signaler is not None: + self._signaler.signal(self._signaler.EIP_CANNOT_START) + + +class Authenticate(object): + """ + Interfaces with setup and bootstrapping operations for a provider + """ + + zope.interface.implements(ILEAPComponent) + + def __init__(self, signaler=None): + """ + Constructor for the Authenticate component + + :param signaler: Object in charge of handling communication + back to the frontend + :type signaler: Signaler + """ + self.key = "authenticate" + self._signaler = signaler + self._srp_auth = SRPAuth(ProviderConfig(), self._signaler) + + def login(self, domain, username, password): + """ + Execute the whole authentication process for a user + + :param domain: the domain where we need to authenticate. + :type domain: unicode + :param username: username for this session + :type username: str + :param password: password for this user + :type password: str + + :returns: the defer for the operation running in a thread. + :rtype: twisted.internet.defer.Deferred + """ + config = ProviderConfig() + if get_provider_config(config, domain): + self._srp_auth = SRPAuth(config, self._signaler) + self._login_defer = self._srp_auth.authenticate(username, password) + return self._login_defer + else: + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_AUTH_ERROR) + logger.error("Could not load provider configuration.") + + def cancel_login(self): + """ + Cancel the ongoing login defer (if any). + """ + d = self._login_defer + if d is not None: + d.cancel() + + def change_password(self, current_password, new_password): + """ + Change the user's password. + + :param current_password: the current password of the user. + :type current_password: str + :param new_password: the new password for the user. + :type new_password: str + + :returns: a defer to interact with. + :rtype: twisted.internet.defer.Deferred + """ + if not self._is_logged_in(): + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) + return + + return self._srp_auth.change_password(current_password, new_password) + + def logout(self): + """ + Log out the current session. + Expects a session_id to exists, might raise AssertionError + """ + if not self._is_logged_in(): + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) + return + + self._srp_auth.logout() + + def _is_logged_in(self): + """ + Return whether the user is logged in or not. + + :rtype: bool + """ + return (self._srp_auth is not None and + self._srp_auth.is_authenticated()) + + def get_logged_in_status(self): + """ + Signal if the user is currently logged in or not. + """ + if self._signaler is None: + return + + signal = None + if self._is_logged_in(): + signal = self._signaler.SRP_STATUS_LOGGED_IN + else: + signal = self._signaler.SRP_STATUS_NOT_LOGGED_IN + + self._signaler.signal(signal) + + class Signaler(QtCore.QObject): """ Signaler object, handles converting string commands to Qt signals. @@ -269,6 +677,65 @@ class Signaler(QtCore.QObject): srp_registration_failed = QtCore.Signal(object) srp_registration_taken = QtCore.Signal(object) + # Signals for EIP bootstrapping + eip_config_ready = QtCore.Signal(object) + eip_client_certificate_ready = QtCore.Signal(object) + + eip_cancelled_setup = QtCore.Signal(object) + + # Signals for SRPAuth + srp_auth_ok = QtCore.Signal(object) + srp_auth_error = QtCore.Signal(object) + srp_auth_server_error = QtCore.Signal(object) + srp_auth_connection_error = QtCore.Signal(object) + srp_auth_bad_user_or_password = QtCore.Signal(object) + srp_logout_ok = QtCore.Signal(object) + srp_logout_error = QtCore.Signal(object) + srp_password_change_ok = QtCore.Signal(object) + srp_password_change_error = QtCore.Signal(object) + srp_password_change_badpw = QtCore.Signal(object) + srp_not_logged_in_error = QtCore.Signal(object) + srp_status_logged_in = QtCore.Signal(object) + srp_status_not_logged_in = QtCore.Signal(object) + + # Signals for EIP + eip_connected = QtCore.Signal(object) + eip_disconnected = QtCore.Signal(object) + eip_connection_died = QtCore.Signal(object) + eip_connection_aborted = QtCore.Signal(object) + + # EIP problems + eip_no_polkit_agent_error = QtCore.Signal(object) + eip_no_tun_kext_error = QtCore.Signal(object) + eip_no_pkexec_error = QtCore.Signal(object) + eip_openvpn_not_found_error = QtCore.Signal(object) + eip_openvpn_already_running = QtCore.Signal(object) + eip_alien_openvpn_already_running = QtCore.Signal(object) + eip_vpn_launcher_exception = QtCore.Signal(object) + + eip_get_gateways_list = QtCore.Signal(object) + eip_get_gateways_list_error = QtCore.Signal(object) + eip_uninitialized_provider = QtCore.Signal(object) + eip_get_initialized_providers = QtCore.Signal(object) + + # signals from parsing openvpn output + eip_network_unreachable = QtCore.Signal(object) + eip_process_restart_tls = QtCore.Signal(object) + eip_process_restart_ping = QtCore.Signal(object) + + # signals from vpnprocess.py + eip_state_changed = QtCore.Signal(dict) + eip_status_changed = QtCore.Signal(dict) + eip_process_finished = QtCore.Signal(int) + + # signals whether the needed files to start EIP exist or not + eip_can_start = QtCore.Signal(object) + eip_cannot_start = QtCore.Signal(object) + + # This signal is used to warn the backend user that is doing something + # wrong + backend_bad_call = QtCore.Signal(object) + #################### # These will exist both in the backend AND the front end. # The frontend might choose to not "interpret" all the signals @@ -288,6 +755,53 @@ class Signaler(QtCore.QObject): SRP_REGISTRATION_FINISHED = "srp_registration_finished" SRP_REGISTRATION_FAILED = "srp_registration_failed" SRP_REGISTRATION_TAKEN = "srp_registration_taken" + SRP_AUTH_OK = "srp_auth_ok" + SRP_AUTH_ERROR = "srp_auth_error" + SRP_AUTH_SERVER_ERROR = "srp_auth_server_error" + SRP_AUTH_CONNECTION_ERROR = "srp_auth_connection_error" + SRP_AUTH_BAD_USER_OR_PASSWORD = "srp_auth_bad_user_or_password" + SRP_LOGOUT_OK = "srp_logout_ok" + SRP_LOGOUT_ERROR = "srp_logout_error" + SRP_PASSWORD_CHANGE_OK = "srp_password_change_ok" + SRP_PASSWORD_CHANGE_ERROR = "srp_password_change_error" + SRP_PASSWORD_CHANGE_BADPW = "srp_password_change_badpw" + SRP_NOT_LOGGED_IN_ERROR = "srp_not_logged_in_error" + SRP_STATUS_LOGGED_IN = "srp_status_logged_in" + SRP_STATUS_NOT_LOGGED_IN = "srp_status_not_logged_in" + + EIP_CONFIG_READY = "eip_config_ready" + EIP_CLIENT_CERTIFICATE_READY = "eip_client_certificate_ready" + EIP_CANCELLED_SETUP = "eip_cancelled_setup" + + EIP_CONNECTED = "eip_connected" + EIP_DISCONNECTED = "eip_disconnected" + EIP_CONNECTION_DIED = "eip_connection_died" + EIP_CONNECTION_ABORTED = "eip_connection_aborted" + EIP_NO_POLKIT_AGENT_ERROR = "eip_no_polkit_agent_error" + EIP_NO_TUN_KEXT_ERROR = "eip_no_tun_kext_error" + EIP_NO_PKEXEC_ERROR = "eip_no_pkexec_error" + EIP_OPENVPN_NOT_FOUND_ERROR = "eip_openvpn_not_found_error" + EIP_OPENVPN_ALREADY_RUNNING = "eip_openvpn_already_running" + EIP_ALIEN_OPENVPN_ALREADY_RUNNING = "eip_alien_openvpn_already_running" + EIP_VPN_LAUNCHER_EXCEPTION = "eip_vpn_launcher_exception" + + EIP_GET_GATEWAYS_LIST = "eip_get_gateways_list" + EIP_GET_GATEWAYS_LIST_ERROR = "eip_get_gateways_list_error" + EIP_UNINITIALIZED_PROVIDER = "eip_uninitialized_provider" + EIP_GET_INITIALIZED_PROVIDERS = "eip_get_initialized_providers" + + EIP_NETWORK_UNREACHABLE = "eip_network_unreachable" + EIP_PROCESS_RESTART_TLS = "eip_process_restart_tls" + EIP_PROCESS_RESTART_PING = "eip_process_restart_ping" + + EIP_STATE_CHANGED = "eip_state_changed" + EIP_STATUS_CHANGED = "eip_status_changed" + EIP_PROCESS_FINISHED = "eip_process_finished" + + EIP_CAN_START = "eip_can_start" + EIP_CANNOT_START = "eip_cannot_start" + + BACKEND_BAD_CALL = "backend_bad_call" def __init__(self): """ @@ -311,6 +825,54 @@ class Signaler(QtCore.QObject): self.SRP_REGISTRATION_FINISHED, self.SRP_REGISTRATION_FAILED, self.SRP_REGISTRATION_TAKEN, + + self.EIP_CONFIG_READY, + self.EIP_CLIENT_CERTIFICATE_READY, + self.EIP_CANCELLED_SETUP, + + self.EIP_CONNECTED, + self.EIP_DISCONNECTED, + self.EIP_CONNECTION_DIED, + self.EIP_CONNECTION_ABORTED, + self.EIP_NO_POLKIT_AGENT_ERROR, + self.EIP_NO_TUN_KEXT_ERROR, + self.EIP_NO_PKEXEC_ERROR, + self.EIP_OPENVPN_NOT_FOUND_ERROR, + self.EIP_OPENVPN_ALREADY_RUNNING, + self.EIP_ALIEN_OPENVPN_ALREADY_RUNNING, + self.EIP_VPN_LAUNCHER_EXCEPTION, + + self.EIP_GET_GATEWAYS_LIST, + self.EIP_GET_GATEWAYS_LIST_ERROR, + self.EIP_UNINITIALIZED_PROVIDER, + self.EIP_GET_INITIALIZED_PROVIDERS, + + self.EIP_NETWORK_UNREACHABLE, + self.EIP_PROCESS_RESTART_TLS, + self.EIP_PROCESS_RESTART_PING, + + self.EIP_STATE_CHANGED, + self.EIP_STATUS_CHANGED, + self.EIP_PROCESS_FINISHED, + + self.EIP_CAN_START, + self.EIP_CANNOT_START, + + self.SRP_AUTH_OK, + self.SRP_AUTH_ERROR, + self.SRP_AUTH_SERVER_ERROR, + self.SRP_AUTH_CONNECTION_ERROR, + self.SRP_AUTH_BAD_USER_OR_PASSWORD, + self.SRP_LOGOUT_OK, + self.SRP_LOGOUT_ERROR, + self.SRP_PASSWORD_CHANGE_OK, + self.SRP_PASSWORD_CHANGE_ERROR, + self.SRP_PASSWORD_CHANGE_BADPW, + self.SRP_NOT_LOGGED_IN_ERROR, + self.SRP_STATUS_LOGGED_IN, + self.SRP_STATUS_NOT_LOGGED_IN, + + self.BACKEND_BAD_CALL, ] for sig in signals: @@ -332,7 +894,6 @@ class Signaler(QtCore.QObject): # Right now it emits Qt signals. The backend version of this # will do zmq.send_multipart, and the frontend version will be # similar to this - log.msg("Signaling %s :: %s" % (key, data)) # for some reason emitting 'None' gives a segmentation fault. if data is None: @@ -341,7 +902,7 @@ class Signaler(QtCore.QObject): try: self._signals[key].emit(data) except KeyError: - log.msg("Unknown key for signal %s!" % (key,)) + log.err("Unknown key for signal %s!" % (key,)) class Backend(object): @@ -356,8 +917,6 @@ class Backend(object): """ Constructor for the backend. """ - object.__init__(self) - # Components map for the commands received self._components = {} @@ -370,6 +929,8 @@ class Backend(object): # Component registration self._register(Provider(self._signaler, bypass_checks)) self._register(Register(self._signaler)) + self._register(Authenticate(self._signaler)) + self._register(EIP(self._signaler)) # We have a looping call on a thread executing all the # commands in queue. Right now this queue is an actual Queue @@ -398,9 +959,17 @@ class Backend(object): """ Stops the looping call and tries to cancel all the defers. """ + reactor.callLater(2, self._stop) + + def _stop(self): + """ + Delayed stopping of worker. Called from `stop`. + """ log.msg("Stopping worker...") if self._lc.running: self._lc.stop() + else: + logger.warning("Looping call is not running, cannot stop") while len(self._ongoing_defers) > 0: d = self._ongoing_defers.pop() d.cancel() @@ -476,14 +1045,241 @@ class Backend(object): # send_multipart and this backend class will be really simple. def setup_provider(self, provider): + """ + Initiate the setup for a provider. + + :param provider: URL for the provider + :type provider: unicode + + Signals: + prov_unsupported_client + prov_unsupported_api + prov_name_resolution -> { PASSED_KEY: bool, ERROR_KEY: str } + prov_https_connection -> { PASSED_KEY: bool, ERROR_KEY: str } + prov_download_provider_info -> { PASSED_KEY: bool, ERROR_KEY: str } + """ self._call_queue.put(("provider", "setup_provider", None, provider)) def cancel_setup_provider(self): + """ + Cancel the ongoing setup provider (if any). + """ self._call_queue.put(("provider", "cancel_setup_provider", None)) def provider_bootstrap(self, provider): + """ + Second stage of bootstrapping for a provider. + + :param provider: URL for the provider + :type provider: unicode + + Signals: + prov_problem_with_provider + prov_download_ca_cert -> {PASSED_KEY: bool, ERROR_KEY: str} + prov_check_ca_fingerprint -> {PASSED_KEY: bool, ERROR_KEY: str} + prov_check_api_certificate -> {PASSED_KEY: bool, ERROR_KEY: str} + """ self._call_queue.put(("provider", "bootstrap", None, provider)) def register_user(self, provider, username, password): + """ + Register a user using the domain and password given as parameters. + + :param domain: the domain we need to register the user. + :type domain: unicode + :param username: the user name + :type username: unicode + :param password: the password for the username + :type password: unicode + + Signals: + srp_registration_finished + srp_registration_taken + srp_registration_failed + """ self._call_queue.put(("register", "register_user", None, provider, username, password)) + + def setup_eip(self, provider, skip_network=False): + """ + Initiate the setup for a provider + + :param provider: URL for the provider + :type provider: unicode + :param skip_network: Whether checks that involve network should be done + or not + :type skip_network: bool + + Signals: + eip_config_ready -> {PASSED_KEY: bool, ERROR_KEY: str} + eip_client_certificate_ready -> {PASSED_KEY: bool, ERROR_KEY: str} + eip_cancelled_setup + """ + self._call_queue.put(("eip", "setup_eip", None, provider, + skip_network)) + + def cancel_setup_eip(self): + """ + Cancel the ongoing setup EIP (if any). + """ + self._call_queue.put(("eip", "cancel_setup_eip", None)) + + def start_eip(self): + """ + Start the EIP service. + + Signals: + backend_bad_call + eip_alien_openvpn_already_running + eip_connected + eip_connection_aborted + eip_network_unreachable + eip_no_pkexec_error + eip_no_polkit_agent_error + eip_no_tun_kext_error + eip_openvpn_already_running + eip_openvpn_not_found_error + eip_process_finished + eip_process_restart_ping + eip_process_restart_tls + eip_state_changed -> str + eip_status_changed -> tuple of str (download, upload) + eip_vpn_launcher_exception + """ + self._call_queue.put(("eip", "start", None)) + + def stop_eip(self, shutdown=False): + """ + Stop the EIP service. + + :param shutdown: + :type shutdown: bool + """ + self._call_queue.put(("eip", "stop", None, shutdown)) + + def terminate_eip(self): + """ + Terminate the EIP service, not necessarily in a nice way. + """ + self._call_queue.put(("eip", "terminate", None)) + + def eip_get_gateways_list(self, domain): + """ + Signal a list of gateways for the given provider. + + :param domain: the domain to get the gateways. + :type domain: str + + # TODO discuss how to document the expected result object received of + # the signal + :signal type: list of str + + Signals: + eip_get_gateways_list -> list of unicode + eip_get_gateways_list_error + eip_uninitialized_provider + """ + self._call_queue.put(("eip", "get_gateways_list", None, domain)) + + def eip_get_initialized_providers(self, domains): + """ + Signal a list of the given domains and if they are initialized or not. + + :param domains: the list of domains to check. + :type domain: list of str + + Signals: + eip_get_initialized_providers -> list of tuple(unicode, bool) + + """ + self._call_queue.put(("eip", "get_initialized_providers", + None, domains)) + + def eip_can_start(self, domain): + """ + Signal whether it has everything that is needed to run EIP or not + + :param domain: the domain for the provider to check + :type domain: str + + Signals: + eip_can_start + eip_cannot_start + """ + self._call_queue.put(("eip", "can_start", + None, domain)) + + def login(self, provider, username, password): + """ + Execute the whole authentication process for a user + + :param domain: the domain where we need to authenticate. + :type domain: unicode + :param username: username for this session + :type username: str + :param password: password for this user + :type password: str + + Signals: + srp_auth_error + srp_auth_ok + srp_auth_bad_user_or_password + srp_auth_server_error + srp_auth_connection_error + srp_auth_error + """ + self._call_queue.put(("authenticate", "login", None, provider, + username, password)) + + def logout(self): + """ + Log out the current session. + + Signals: + srp_logout_ok + srp_logout_error + srp_not_logged_in_error + """ + self._call_queue.put(("authenticate", "logout", None)) + + def cancel_login(self): + """ + Cancel the ongoing login (if any). + """ + self._call_queue.put(("authenticate", "cancel_login", None)) + + def change_password(self, current_password, new_password): + """ + Change the user's password. + + :param current_password: the current password of the user. + :type current_password: str + :param new_password: the new password for the user. + :type new_password: str + + Signals: + srp_not_logged_in_error + srp_password_change_ok + srp_password_change_badpw + srp_password_change_error + """ + self._call_queue.put(("authenticate", "change_password", None, + current_password, new_password)) + + def get_logged_in_status(self): + """ + Signal if the user is currently logged in or not. + + Signals: + srp_status_logged_in + srp_status_not_logged_in + """ + self._call_queue.put(("authenticate", "get_logged_in_status", None)) + + ########################################################################### + # XXX HACK: this section is meant to be a place to hold methods and + # variables needed in the meantime while we migrate all to the backend. + + def get_provider_config(self): + provider_config = self._components["provider"]._provider_config + return provider_config diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py index 5d8bc9b3..6b70659d 100644 --- a/src/leap/bitmask/config/flags.py +++ b/src/leap/bitmask/config/flags.py @@ -46,7 +46,12 @@ API_VERSION_CHECK = True # Used for skipping soledad bootstrapping/syncs. OFFLINE = False - # CA cert path # used to allow self signed certs in requests that needs SSL CA_CERT_FILE = None + +# OpenVPN verbosity level +OPENVPN_VERBOSITY = 1 + +# Skip the checks in the wizard, use for testing purposes only! +SKIP_WIZARD_CHECKS = False diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 7cf7e55a..192a9d5c 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -17,6 +17,7 @@ import binascii import logging +import threading import sys import requests @@ -28,8 +29,8 @@ from simplejson.decoder import JSONDecodeError from functools import partial from requests.adapters import HTTPAdapter -from PySide import QtCore from twisted.internet import threads +from twisted.internet.defer import CancelledError from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.util import request_helpers as reqhelper @@ -117,12 +118,12 @@ class SRPAuthNoSessionId(SRPAuthenticationError): pass -class SRPAuth(QtCore.QObject): +class SRPAuth(object): """ SRPAuth singleton """ - class __impl(QtCore.QObject): + class __impl(object): """ Implementation of the SRPAuth interface """ @@ -135,19 +136,21 @@ class SRPAuth(QtCore.QObject): USER_SALT_KEY = 'user[password_salt]' AUTHORIZATION_KEY = "Authorization" - def __init__(self, provider_config): + def __init__(self, provider_config, signaler=None): """ Constructor for SRPAuth implementation - :param server: Server to which we will authenticate - :type server: str + :param provider_config: ProviderConfig needed to authenticate. + :type provider_config: ProviderConfig + :param signaler: Signaler object used to receive notifications + from the backend + :type signaler: Signaler """ - QtCore.QObject.__init__(self) - leap_assert(provider_config, "We need a provider config to authenticate") self._provider_config = provider_config + self._signaler = signaler self._settings = LeapSettings() # **************************************************** # @@ -162,11 +165,11 @@ class SRPAuth(QtCore.QObject): self._reset_session() self._session_id = None - self._session_id_lock = QtCore.QMutex() + self._session_id_lock = threading.Lock() self._uuid = None - self._uuid_lock = QtCore.QMutex() + self._uuid_lock = threading.Lock() self._token = None - self._token_lock = QtCore.QMutex() + self._token_lock = threading.Lock() self._srp_user = None self._srp_a = None @@ -448,7 +451,7 @@ class SRPAuth(QtCore.QObject): def _threader(self, cb, res, *args, **kwargs): return threads.deferToThread(cb, res, *args, **kwargs) - def change_password(self, current_password, new_password): + def _change_password(self, current_password, new_password): """ Changes the password for the currently logged user if the current password match. @@ -499,6 +502,43 @@ class SRPAuth(QtCore.QObject): self._password = new_password + def change_password(self, current_password, new_password): + """ + Changes the password for the currently logged user if the current + password match. + It requires to be authenticated. + + :param current_password: the current password for the logged user. + :type current_password: str + :param new_password: the new password for the user + :type new_password: str + """ + d = threads.deferToThread( + self._change_password, current_password, new_password) + d.addCallback(self._change_password_ok) + d.addErrback(self._change_password_error) + + def _change_password_ok(self, _): + """ + Password change callback. + """ + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_OK) + + def _change_password_error(self, failure): + """ + Password change errback. + """ + logger.debug( + "Error changing password. Failure: {0}".format(failure)) + if self._signaler is None: + return + + if failure.check(SRPAuthBadUserOrPassword): + self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_BADPW) + else: + self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_ERROR) + def authenticate(self, username, password): """ Executes the whole authentication process for a user @@ -539,8 +579,49 @@ class SRPAuth(QtCore.QObject): d.addCallback(partial(self._threader, self._verify_session)) + d.addCallback(self._authenticate_ok) + d.addErrback(self._authenticate_error) return d + def _authenticate_ok(self, _): + """ + Callback that notifies that the authentication was successful. + + :param _: IGNORED, output from the previous callback (None) + :type _: IGNORED + """ + logger.debug("Successful login!") + self._signaler.signal(self._signaler.SRP_AUTH_OK) + + def _authenticate_error(self, failure): + """ + Error handler for the srpauth.authenticate method. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + """ + logger.error("Error logging in, {0!r}".format(failure)) + + signal = None + if failure.check(CancelledError): + logger.debug("Defer cancelled.") + failure.trap(Exception) + return + + if self._signaler is None: + return + + if failure.check(SRPAuthBadUserOrPassword): + signal = self._signaler.SRP_AUTH_BAD_USER_OR_PASSWORD + elif failure.check(SRPAuthConnectionError): + signal = self._signaler.SRP_AUTH_CONNECTION_ERROR + elif failure.check(SRPAuthenticationError): + signal = self._signaler.SRP_AUTH_SERVER_ERROR + else: + signal = self._signaler.SRP_AUTH_ERROR + + self._signaler.signal(signal) + def logout(self): """ Logs out the current session. @@ -565,6 +646,8 @@ class SRPAuth(QtCore.QObject): except Exception as e: logger.warning("Something went wrong with the logout: %r" % (e,)) + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_LOGOUT_ERROR) raise else: self.set_session_id(None) @@ -573,51 +656,65 @@ class SRPAuth(QtCore.QObject): # Also reset the session self._session = self._fetcher.session() logger.debug("Successfully logged out.") + if self._signaler is not None: + self._signaler.signal(self._signaler.SRP_LOGOUT_OK) def set_session_id(self, session_id): - QtCore.QMutexLocker(self._session_id_lock) - self._session_id = session_id + with self._session_id_lock: + self._session_id = session_id def get_session_id(self): - QtCore.QMutexLocker(self._session_id_lock) - return self._session_id + with self._session_id_lock: + return self._session_id def set_uuid(self, uuid): - QtCore.QMutexLocker(self._uuid_lock) - full_uid = "%s@%s" % ( - self._username, self._provider_config.get_domain()) - if uuid is not None: # avoid removing the uuid from settings - self._settings.set_uuid(full_uid, uuid) - self._uuid = uuid + with self._uuid_lock: + full_uid = "%s@%s" % ( + self._username, self._provider_config.get_domain()) + if uuid is not None: # avoid removing the uuid from settings + self._settings.set_uuid(full_uid, uuid) + self._uuid = uuid def get_uuid(self): - QtCore.QMutexLocker(self._uuid_lock) - return self._uuid + with self._uuid_lock: + return self._uuid def set_token(self, token): - QtCore.QMutexLocker(self._token_lock) - self._token = token + with self._token_lock: + self._token = token def get_token(self): - QtCore.QMutexLocker(self._token_lock) - return self._token + with self._token_lock: + return self._token - __instance = None + def is_authenticated(self): + """ + Return whether the user is authenticated or not. - authentication_finished = QtCore.Signal() - logout_ok = QtCore.Signal() - logout_error = QtCore.Signal() + :rtype: bool + """ + user = self._srp_user + if user is not None: + return user.authenticated() - def __init__(self, provider_config): - """ - Creates a singleton instance if needed + return False + + __instance = None + + def __init__(self, provider_config, signaler=None): """ - QtCore.QObject.__init__(self) + Create a singleton instance if needed + :param provider_config: ProviderConfig needed to authenticate. + :type provider_config: ProviderConfig + :param signaler: Signaler object used to send notifications + from the backend + :type signaler: Signaler + """ # Check whether we already have an instance if SRPAuth.__instance is None: # Create and remember instance - SRPAuth.__instance = SRPAuth.__impl(provider_config) + SRPAuth.__instance = SRPAuth.__impl(provider_config, signaler) # Store instance reference as the only member in the handle self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance @@ -642,9 +739,16 @@ class SRPAuth(QtCore.QObject): """ username = username.lower() d = self.__instance.authenticate(username, password) - d.addCallback(self._gui_notify) return d + def is_authenticated(self): + """ + Return whether the user is authenticated or not. + + :rtype: bool + """ + return self.__instance.is_authenticated() + def change_password(self, current_password, new_password): """ Changes the user's password. @@ -657,8 +761,7 @@ class SRPAuth(QtCore.QObject): :returns: a defer to interact with. :rtype: twisted.internet.defer.Deferred """ - d = threads.deferToThread( - self.__instance.change_password, current_password, new_password) + d = self.__instance.change_password(current_password, new_password) return d def get_username(self): @@ -672,16 +775,6 @@ class SRPAuth(QtCore.QObject): return None return self.__instance._username - def _gui_notify(self, _): - """ - Callback that notifies the UI with the proper signal. - - :param _: IGNORED, output from the previous callback (None) - :type _: IGNORED - """ - logger.debug("Successful login!") - self.authentication_finished.emit() - def get_session_id(self): return self.__instance.get_session_id() @@ -699,9 +792,7 @@ class SRPAuth(QtCore.QObject): try: self.__instance.logout() logger.debug("Logout success") - self.logout_ok.emit() return True except Exception as e: logger.debug("Logout error: {0!r}".format(e)) - self.logout_error.emit() return False diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py index 4c52db42..f03dc469 100644 --- a/src/leap/bitmask/crypto/srpregister.py +++ b/src/leap/bitmask/crypto/srpregister.py @@ -46,8 +46,6 @@ class SRPRegister(QtCore.QObject): STATUS_TAKEN = 422 STATUS_ERROR = -999 # Custom error status - registration_finished = QtCore.Signal(bool, object) - def __init__(self, signaler=None, provider_config=None, register_path="users"): """ diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py index cbc8c3e3..be6b4410 100644 --- a/src/leap/bitmask/gui/advanced_key_management.py +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -30,12 +30,17 @@ from ui_advanced_key_management import Ui_AdvancedKeyManagement logger = logging.getLogger(__name__) -class AdvancedKeyManagement(QtGui.QWidget): +class AdvancedKeyManagement(QtGui.QDialog): """ Advanced Key Management """ - def __init__(self, user, keymanager, soledad): + def __init__(self, parent, has_mx, user, keymanager, soledad): """ + :param parent: parent object of AdvancedKeyManagement. + :parent type: QWidget + :param has_mx: defines whether the current provider provides email or + not. + :type has_mx: bool :param user: the current logged in user. :type user: unicode :param keymanager: the existing keymanager instance @@ -43,7 +48,7 @@ class AdvancedKeyManagement(QtGui.QWidget): :param soledad: a loaded instance of Soledad :type soledad: Soledad """ - QtGui.QWidget.__init__(self) + QtGui.QDialog.__init__(self, parent) self.ui = Ui_AdvancedKeyManagement() self.ui.setupUi(self) @@ -52,13 +57,18 @@ class AdvancedKeyManagement(QtGui.QWidget): self.ui.pbImportKeys.setVisible(False) # if Soledad is not started yet + if not has_mx: + msg = self.tr("The provider that you are using " + "does not support {0}.") + msg = msg.format(get_service_display_name(MX_SERVICE)) + self._disable_ui(msg) + return + + # if Soledad is not started yet if sameProxiedObjects(soledad, None): - self.ui.gbMyKeyPair.setEnabled(False) - self.ui.gbStoredPublicKeys.setEnabled(False) - msg = self.tr("<span style='color:#0000FF;'>NOTE</span>: " - "To use this, you need to enable/start {0}.") + msg = self.tr("To use this, you need to enable/start {0}.") msg = msg.format(get_service_display_name(MX_SERVICE)) - self.ui.lblStatus.setText(msg) + self._disable_ui(msg) return # XXX: since import is disabled this is no longer a dangerous feature. # else: @@ -90,6 +100,18 @@ class AdvancedKeyManagement(QtGui.QWidget): self._list_keys() + def _disable_ui(self, msg): + """ + Disable the UI and set a note in the status bar. + + :param msg: note to display in the status bar. + :type msg: unicode + """ + self.ui.gbMyKeyPair.setEnabled(False) + self.ui.gbStoredPublicKeys.setEnabled(False) + msg = self.tr("<span style='color:#0000FF;'>NOTE</span>: ") + msg + self.ui.lblStatus.setText(msg) + def _import_keys(self): """ Imports the user's key pair. diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index dcaa8b1e..530cd38d 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -18,17 +18,13 @@ """ EIP Preferences window """ -import os import logging from functools import partial from PySide import QtCore, QtGui from leap.bitmask.config.leapsettings import LeapSettings -from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.gui.ui_eippreferences import Ui_EIPPreferences -from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector -from leap.bitmask.services.eip.eipconfig import get_eipconfig_path logger = logging.getLogger(__name__) @@ -37,17 +33,20 @@ class EIPPreferencesWindow(QtGui.QDialog): """ Window that displays the EIP preferences. """ - def __init__(self, parent, domain): + def __init__(self, parent, domain, backend): """ :param parent: parent object of the EIPPreferencesWindow. :type parent: QWidget :param domain: the selected by default domain. :type domain: unicode + :param backend: Backend being used + :type backend: Backend """ QtGui.QDialog.__init__(self, parent) self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") self._settings = LeapSettings() + self._backend = backend # Load UI self.ui = Ui_EIPPreferences() @@ -61,7 +60,11 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.cbGateways.currentIndexChanged[unicode].connect( lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) - self._add_configured_providers(domain) + self._selected_domain = domain + self._configured_providers = [] + + self._backend_connect() + self._add_configured_providers() def _set_providers_gateway_status(self, status, success=False, error=False): @@ -85,35 +88,49 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.lblProvidersGatewayStatus.setVisible(True) self.ui.lblProvidersGatewayStatus.setText(status) - def _add_configured_providers(self, domain=None): + def _add_configured_providers(self): """ Add the client's configured providers to the providers combo boxes. + """ + providers = self._settings.get_configured_providers() + if not providers: + return - :param domain: the domain to be selected by default. - :type domain: unicode + self._backend.eip_get_initialized_providers(providers) + + @QtCore.Slot(list) + def _load_providers_in_combo(self, providers): + """ + TRIGGERS: + Signaler.eip_get_initialized_providers + + Add the client's configured providers to the providers combo boxes. + + :param providers: the list of providers to add and whether each one is + initialized or not. + :type providers: list of tuples (str, bool) """ self.ui.cbProvidersGateway.clear() - providers = self._settings.get_configured_providers() if not providers: self.ui.gbGatewaySelector.setEnabled(False) return - for provider in providers: + for provider, is_initialized in providers: label = provider - eip_config_path = get_eipconfig_path(provider, relative=False) - if not os.path.isfile(eip_config_path): - label = provider + self.tr(" (uninitialized)") + if not is_initialized: + label += self.tr(" (uninitialized)") self.ui.cbProvidersGateway.addItem(label, userData=provider) # Select provider by name + domain = self._selected_domain if domain is not None: provider_index = self.ui.cbProvidersGateway.findText( domain, QtCore.Qt.MatchStartsWith) self.ui.cbProvidersGateway.setCurrentIndex(provider_index) + @QtCore.Slot(str) def _save_selected_gateway(self, provider): """ - SLOT TRIGGERS: self.ui.pbSaveGateway.clicked @@ -136,9 +153,9 @@ class EIPPreferencesWindow(QtGui.QDialog): "Gateway settings for provider '{0}' saved.").format(provider) self._set_providers_gateway_status(msg, success=True) + @QtCore.Slot(int) def _populate_gateways(self, domain_idx): """ - SLOT TRIGGERS: self.ui.cbProvidersGateway.currentIndexChanged[unicode] @@ -155,18 +172,27 @@ class EIPPreferencesWindow(QtGui.QDialog): return domain = self.ui.cbProvidersGateway.itemData(domain_idx) + self._selected_domain = domain - if not os.path.isfile(get_eipconfig_path(domain, relative=False)): - self._set_providers_gateway_status( - self.tr("This is an uninitialized provider, " - "please log in first."), - error=True) - self.ui.pbSaveGateway.setEnabled(False) - self.ui.cbGateways.setEnabled(False) - return - else: - self.ui.pbSaveGateway.setEnabled(True) - self.ui.cbGateways.setEnabled(True) + self._backend.eip_get_gateways_list(domain) + + @QtCore.Slot(list) + def _update_gateways_list(self, gateways): + """ + TRIGGERS: + Signaler.eip_get_gateways_list + + :param gateways: a list of gateways + :type gateways: list of unicode + + Add the available gateways and select the one stored in configuration + file. + """ + self.ui.pbSaveGateway.setEnabled(True) + self.ui.cbGateways.setEnabled(True) + + self.ui.cbGateways.clear() + self.ui.cbGateways.addItem(self.AUTOMATIC_GATEWAY_LABEL) try: # disconnect previously connected save method @@ -175,31 +201,13 @@ class EIPPreferencesWindow(QtGui.QDialog): pass # Signal was not connected # set the proper connection for the 'save' button + domain = self._selected_domain save_gateway = partial(self._save_selected_gateway, domain) self.ui.pbSaveGateway.clicked.connect(save_gateway) - eip_config = EIPConfig() - provider_config = ProviderConfig.get_provider_config(domain) - - api_version = provider_config.get_api_version() - eip_config.set_api_version(api_version) - eip_loaded = eip_config.load(get_eipconfig_path(domain)) - - if not eip_loaded or provider_config is None: - self._set_providers_gateway_status( - self.tr("There was a problem with configuration files."), - error=True) - return - - gateways = VPNGatewaySelector(eip_config).get_gateways_list() - logger.debug(gateways) - - self.ui.cbGateways.clear() - self.ui.cbGateways.addItem(self.AUTOMATIC_GATEWAY_LABEL) + selected_gateway = self._settings.get_selected_gateway( + self._selected_domain) - # Add the available gateways and - # select the one stored in configuration file. - selected_gateway = self._settings.get_selected_gateway(domain) index = 0 for idx, (gw_name, gw_ip) in enumerate(gateways): gateway = "{0} ({1})".format(gw_name, gw_ip) @@ -208,3 +216,42 @@ class EIPPreferencesWindow(QtGui.QDialog): index = idx + 1 self.ui.cbGateways.setCurrentIndex(index) + + @QtCore.Slot() + def _gateways_list_error(self): + """ + TRIGGERS: + Signaler.eip_get_gateways_list_error + + An error has occurred retrieving the gateway list so we inform the + user. + """ + self._set_providers_gateway_status( + self.tr("There was a problem with configuration files."), + error=True) + self.ui.pbSaveGateway.setEnabled(False) + self.ui.cbGateways.setEnabled(False) + + @QtCore.Slot() + def _gateways_list_uninitialized(self): + """ + TRIGGERS: + Signaler.eip_uninitialized_provider + + The requested provider in not initialized yet, so we give the user an + error msg. + """ + self._set_providers_gateway_status( + self.tr("This is an uninitialized provider, please log in first."), + error=True) + self.ui.pbSaveGateway.setEnabled(False) + self.ui.cbGateways.setEnabled(False) + + def _backend_connect(self): + sig = self._backend.signaler + sig.eip_get_gateways_list.connect(self._update_gateways_list) + sig.eip_get_gateways_list_error.connect(self._gateways_list_error) + sig.eip_uninitialized_provider.connect( + self._gateways_list_uninitialized) + sig.eip_get_initialized_providers.connect( + self._load_providers_in_combo) diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 19942d9d..ca28b8bf 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -25,7 +25,6 @@ from functools import partial from PySide import QtCore, QtGui from leap.bitmask.services.eip.connection import EIPConnection -from leap.bitmask.services.eip.vpnprocess import VPNManager from leap.bitmask.services import get_service_display_name, EIP_SERVICE from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.util.averages import RateMovingAverage @@ -89,17 +88,18 @@ class EIPStatusWidget(QtGui.QWidget): self.ui.btnUpload.clicked.connect(onclicked) self.ui.btnDownload.clicked.connect(onclicked) + @QtCore.Slot() def _on_VPN_status_clicked(self): """ - SLOT - TRIGGER: self.ui.btnUpload.clicked - self.ui.btnDownload.clicked + TRIGGERS: + self.ui.btnUpload.clicked + self.ui.btnDownload.clicked Toggles between rate and total throughput display for vpn status figures. """ self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES - self.update_vpn_status(None) # refresh + self.update_vpn_status() # refresh def _set_traffic_rates(self): """ @@ -117,7 +117,7 @@ class EIPStatusWidget(QtGui.QWidget): """ self._up_rate.reset() self._down_rate.reset() - self.update_vpn_status(None) + self.update_vpn_status() def _update_traffic_rates(self, up, down): """ @@ -260,11 +260,12 @@ class EIPStatusWidget(QtGui.QWidget): self._service_name, self.tr("disabled"))) # Replace EIP tray menu with an action that displays a "disabled" text - menu = self._systray.contextMenu() - menu.insertAction( - self._eip_status_menu.menuAction(), - self._eip_disabled_action) - self._eip_status_menu.menuAction().setVisible(False) + if self.isVisible(): + menu = self._systray.contextMenu() + menu.insertAction( + self._eip_status_menu.menuAction(), + self._eip_disabled_action) + self._eip_status_menu.menuAction().setVisible(False) @QtCore.Slot() def enable_eip_start(self): @@ -278,7 +279,8 @@ class EIPStatusWidget(QtGui.QWidget): # Restore the eip action menu menu = self._systray.contextMenu() menu.removeAction(self._eip_disabled_action) - self._eip_status_menu.menuAction().setVisible(True) + if self.isVisible(): + self._eip_status_menu.menuAction().setVisible(True) # XXX disable (later) -------------------------- def set_eip_status(self, status, error=False): @@ -348,23 +350,27 @@ class EIPStatusWidget(QtGui.QWidget): self.tr("Traffic is being routed in the clear")) self.ui.lblEIPStatus.show() - def update_vpn_status(self, data): + @QtCore.Slot(dict) + def update_vpn_status(self, data=None): """ - SLOT - TRIGGER: VPN.status_changed + TRIGGERS: + Signaler.eip_status_changed - Updates the download/upload labels based on the data provided - by the VPN thread. + Updates the download/upload labels based on the data provided by the + VPN thread. + If data is None, we just will refresh the display based on the previous + data. - :param data: a dictionary with the tcp/udp write and read totals. - If data is None, we just will refresh the display based - on the previous data. - :type data: dict + :param data: a tuple with download/upload totals (download, upload). + :type data: tuple """ - if data: - upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0") - download = float(data[VPNManager.TCPUDP_READ_KEY] or "0") - self._update_traffic_rates(upload, download) + if data is not None: + try: + upload, download = map(float, data) + self._update_traffic_rates(upload, download) + except Exception: + # discard invalid data + return if self.DISPLAY_TRAFFIC_RATES: uprate, downrate = self._get_traffic_rates() @@ -379,39 +385,42 @@ class EIPStatusWidget(QtGui.QWidget): self.ui.btnUpload.setText(upload_str) self.ui.btnDownload.setText(download_str) - def update_vpn_state(self, data): + @QtCore.Slot(dict) + def update_vpn_state(self, vpn_state): """ - SLOT - TRIGGER: VPN.state_changed + TRIGGERS: + Signaler.eip_state_changed Updates the displayed VPN state based on the data provided by the VPN thread. + :param vpn_state: the state of the VPN + :type vpn_state: dict + Emits: - If the status is connected, we emit EIPConnection.qtsigs. + If the vpn_state is connected, we emit EIPConnection.qtsigs. connected_signal """ - status = data[VPNManager.STATUS_STEP_KEY] - self.set_eip_status_icon(status) - if status == "CONNECTED": + self.set_eip_status_icon(vpn_state) + if vpn_state == "CONNECTED": self.ui.eip_bandwidth.show() self.ui.lblEIPStatus.hide() # XXX should be handled by the state machine too. self.eip_connection_connected.emit() - # XXX should lookup status map in EIPConnection - elif status == "AUTH": + # XXX should lookup vpn_state map in EIPConnection + elif vpn_state == "AUTH": self.set_eip_status(self.tr("Authenticating...")) - elif status == "GET_CONFIG": + elif vpn_state == "GET_CONFIG": self.set_eip_status(self.tr("Retrieving configuration...")) - elif status == "WAIT": + elif vpn_state == "WAIT": self.set_eip_status(self.tr("Waiting to start...")) - elif status == "ASSIGN_IP": + elif vpn_state == "ASSIGN_IP": self.set_eip_status(self.tr("Assigning IP")) - elif status == "RECONNECTING": + elif vpn_state == "RECONNECTING": self.set_eip_status(self.tr("Reconnecting...")) - elif status == "ALREADYRUNNING": + elif vpn_state == "ALREADYRUNNING": # Put the following calls in Qt's event queue, otherwise # the UI won't update properly QtCore.QTimer.singleShot( @@ -419,7 +428,7 @@ class EIPStatusWidget(QtGui.QWidget): msg = self.tr("Unable to start VPN, it's already running.") QtCore.QTimer.singleShot(0, partial(self.set_eip_status, msg)) else: - self.set_eip_status(status) + self.set_eip_status(vpn_state) def set_eip_icon(self, icon): """ diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index 9f396574..f19b172f 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -21,14 +21,14 @@ History log window import logging import cgi -from PySide import QtGui +from PySide import QtCore, QtGui from twisted.internet import threads from ui_loggerwindow import Ui_LoggerWindow from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY from leap.bitmask.util.leap_log_handler import LeapLogHandler -from leap.bitmask.util.pastebin import PastebinAPI, PastebinError +from leap.bitmask.util import pastebin from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -203,10 +203,10 @@ class LoggerWindow(QtGui.QDialog): Send content to pastebin and return the link. """ content = self._current_history - pb = PastebinAPI() + pb = pastebin.PastebinAPI() link = pb.paste(PASTEBIN_API_DEV_KEY, content, paste_name="Bitmask log", - paste_expire_date='1W') + paste_expire_date='1M') # convert to 'raw' link link = "http://pastebin.com/raw.php?i=" + link.split('/')[-1] @@ -220,12 +220,16 @@ class LoggerWindow(QtGui.QDialog): :param link: the recently created pastebin link. :type link: str """ + self._set_pastebin_sending(False) msg = self.tr("Your pastebin link <a href='{0}'>{0}</a>") msg = msg.format(link) - show_info = lambda: QtGui.QMessageBox.information( - self, self.tr("Pastebin OK"), msg) - self._set_pastebin_sending(False) - self.reactor.callLater(0, show_info) + + # We save the dialog in an instance member to avoid dialog being + # deleted right after we exit this method + self._msgBox = msgBox = QtGui.QMessageBox( + QtGui.QMessageBox.Information, self.tr("Pastebin OK"), msg) + msgBox.setWindowModality(QtCore.Qt.NonModal) + msgBox.show() def pastebin_err(failure): """ @@ -234,13 +238,19 @@ class LoggerWindow(QtGui.QDialog): :param failure: the failure that triggered the errback. :type failure: twisted.python.failure.Failure """ + self._set_pastebin_sending(False) logger.error(repr(failure)) + msg = self.tr("Sending logs to Pastebin failed!") - show_err = lambda: QtGui.QMessageBox.critical( - self, self.tr("Pastebin Error"), msg) - self._set_pastebin_sending(False) - self.reactor.callLater(0, show_err) - failure.trap(PastebinError) + if failure.check(pastebin.PostLimitError): + msg = self.tr('Maximum posts per day reached') + + # We save the dialog in an instance member to avoid dialog being + # deleted right after we exit this method + self._msgBox = msgBox = QtGui.QMessageBox( + QtGui.QMessageBox.Critical, self.tr("Pastebin Error"), msg) + msgBox.setWindowModality(QtCore.Qt.NonModal) + msgBox.show() self._set_pastebin_sending(True) d = threads.deferToThread(do_pastebin) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index 4a483c32..ac7ad878 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -259,12 +259,16 @@ class LoginWidget(QtGui.QWidget): """ self.ui.lnPassword.setFocus() - def _current_provider_changed(self, param): + @QtCore.Slot(int) + def _current_provider_changed(self, idx): """ - SLOT - TRIGGERS: self.ui.cmbProviders.currentIndexChanged + TRIGGERS: + self.ui.cmbProviders.currentIndexChanged + + :param idx: the index of the new selected item + :type idx: int """ - if param == (self.ui.cmbProviders.count() - 1): + if idx == (self.ui.cmbProviders.count() - 1): self.show_wizard.emit() # Leave the previously selected provider in the combobox prev_provider = 0 @@ -274,7 +278,7 @@ class LoginWidget(QtGui.QWidget): self.ui.cmbProviders.setCurrentIndex(prev_provider) self.ui.cmbProviders.blockSignals(False) else: - self._selected_provider_index = param + self._selected_provider_index = idx def start_login(self): """ diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 44a138e2..d3346780 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -184,10 +184,10 @@ class MailStatusWidget(QtGui.QWidget): leap_assert_type(action_mail_status, QtGui.QAction) self._action_mail_status = action_mail_status + @QtCore.Slot() def set_soledad_failed(self): """ - SLOT - TRIGGER: + TRIGGERS: SoledadBootstrapper.soledad_failed This method is called whenever soledad has a failure. @@ -195,10 +195,10 @@ class MailStatusWidget(QtGui.QWidget): msg = self.tr("There was an unexpected problem with Soledad.") self._set_mail_status(msg, ready=-1) + @QtCore.Slot() def set_soledad_invalid_auth_token(self): """ - SLOT - TRIGGER: + TRIGGERS: SoledadBootstrapper.soledad_invalid_token This method is called when the auth token is invalid @@ -250,10 +250,11 @@ class MailStatusWidget(QtGui.QWidget): """ self._soledad_event.emit(req) + @QtCore.Slot(object) def _mail_handle_soledad_events_slot(self, req): """ - SLOT - TRIGGER: _mail_handle_soledad_events + TRIGGERS: + _mail_handle_soledad_events Reacts to an Soledad event @@ -284,10 +285,11 @@ class MailStatusWidget(QtGui.QWidget): """ self._keymanager_event.emit(req) + @QtCore.Slot(object) def _mail_handle_keymanager_events_slot(self, req): """ - SLOT - TRIGGER: _mail_handle_keymanager_events + TRIGGERS: + _mail_handle_keymanager_events Reacts to an KeyManager event @@ -330,10 +332,11 @@ class MailStatusWidget(QtGui.QWidget): """ self._smtp_event.emit(req) + @QtCore.Slot(object) def _mail_handle_smtp_events_slot(self, req): """ - SLOT - TRIGGER: _mail_handle_smtp_events + TRIGGERS: + _mail_handle_smtp_events Reacts to an SMTP event @@ -364,10 +367,11 @@ class MailStatusWidget(QtGui.QWidget): """ self._imap_event.emit(req) + @QtCore.Slot(object) def _mail_handle_imap_events_slot(self, req): """ - SLOT - TRIGGER: _mail_handle_imap_events + TRIGGERS: + _mail_handle_imap_events Reacts to an IMAP event diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 5abfaa67..e3848c46 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -19,6 +19,7 @@ Main window for Bitmask. """ import logging import socket +import time from threading import Condition from datetime import datetime @@ -26,7 +27,6 @@ from datetime import datetime from PySide import QtCore, QtGui from zope.proxy import ProxyBase, setProxiedObject from twisted.internet import reactor, threads -from twisted.internet.defer import CancelledError from leap.bitmask import __version__ as VERSION from leap.bitmask import __version_hash__ as VERSION_HASH @@ -34,19 +34,16 @@ from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.crypto import srpauth -from leap.bitmask.crypto.srpauth import SRPAuth - -from leap.bitmask.gui.loggerwindow import LoggerWindow +from leap.bitmask.gui import statemachines from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement -from leap.bitmask.gui.login import LoginWidget -from leap.bitmask.gui.preferenceswindow import PreferencesWindow from leap.bitmask.gui.eip_preferenceswindow import EIPPreferencesWindow -from leap.bitmask.gui import statemachines from leap.bitmask.gui.eip_status import EIPStatusWidget +from leap.bitmask.gui.loggerwindow import LoggerWindow +from leap.bitmask.gui.login import LoginWidget from leap.bitmask.gui.mail_status import MailStatusWidget -from leap.bitmask.gui.wizard import Wizard +from leap.bitmask.gui.preferenceswindow import PreferencesWindow from leap.bitmask.gui.systray import SysTray +from leap.bitmask.gui.wizard import Wizard from leap.bitmask import provider from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX @@ -59,20 +56,7 @@ from leap.bitmask.services import get_service_display_name from leap.bitmask.services.mail import conductor as mail_conductor from leap.bitmask.services import EIP_SERVICE, MX_SERVICE -from leap.bitmask.services.eip import eipconfig -from leap.bitmask.services.eip import get_openvpn_management -from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper from leap.bitmask.services.eip.connection import EIPConnection -from leap.bitmask.services.eip.vpnprocess import VPN -from leap.bitmask.services.eip.vpnprocess import OpenVPNAlreadyRunning -from leap.bitmask.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning - -from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException -from leap.bitmask.services.eip.vpnlauncher import OpenVPNNotFoundException -from leap.bitmask.services.eip.linuxvpnlauncher import EIPNoPkexecAvailable -from leap.bitmask.services.eip.linuxvpnlauncher import \ - EIPNoPolkitAuthAgentAvailable -from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded from leap.bitmask.services.soledad.soledadbootstrapper import \ SoledadBootstrapper @@ -99,11 +83,6 @@ class MainWindow(QtGui.QMainWindow): """ Main window for login and presenting status updates to the user """ - - # StackedWidget indexes - LOGIN_INDEX = 0 - EIP_STATUS_INDEX = 1 - # Signals eip_needs_login = QtCore.Signal([]) offline_mode_bypass_login = QtCore.Signal([]) @@ -122,9 +101,7 @@ class MainWindow(QtGui.QMainWindow): # We give each service some time to come to a halt before forcing quit SERVICE_STOP_TIMEOUT = 20 - def __init__(self, quit_callback, - openvpn_verb=1, - bypass_checks=False): + def __init__(self, quit_callback, bypass_checks=False, start_hidden=False): """ Constructor for the client main window @@ -132,10 +109,12 @@ class MainWindow(QtGui.QMainWindow): the application. :type quit_callback: callable - :param bypass_checks: Set to true if the app should bypass - first round of checks for CA - certificates at bootstrap + :param bypass_checks: Set to true if the app should bypass first round + of checks for CA certificates at bootstrap :type bypass_checks: bool + :param start_hidden: Set to true if the app should not show the window + but just the tray. + :type start_hidden: bool """ QtGui.QMainWindow.__init__(self) @@ -190,31 +169,30 @@ class MainWindow(QtGui.QMainWindow): # XXX this should be handled by EIP Conductor self._eip_connection.qtsigs.connecting_signal.connect( - self._start_eip) + self._start_EIP) self._eip_connection.qtsigs.disconnecting_signal.connect( self._stop_eip) self._eip_status.eip_connection_connected.connect( - self._on_eip_connected) + self._on_eip_connection_connected) self._eip_status.eip_connection_connected.connect( self._maybe_run_soledad_setup_checks) self.offline_mode_bypass_login.connect( self._maybe_run_soledad_setup_checks) - self.eip_needs_login.connect( - self._eip_status.disable_eip_start) - self.eip_needs_login.connect( - self._disable_eip_start_action) + self.eip_needs_login.connect(self._eip_status.disable_eip_start) + self.eip_needs_login.connect(self._disable_eip_start_action) + + self._trying_to_start_eip = False # This is loaded only once, there's a bug when doing that more # than once # XXX HACK!! But we need it as long as we are using # provider_config in here - self._provider_config = ( - self._backend._components["provider"]._provider_config) + self._provider_config = self._backend.get_provider_config() + # Used for automatic start of EIP self._provisional_provider_config = ProviderConfig() - self._eip_config = eipconfig.EIPConfig() self._already_started_eip = False self._already_started_soledad = False @@ -224,35 +202,9 @@ class MainWindow(QtGui.QMainWindow): self._logged_user = None self._logged_in_offline = False + self._backend_connected_signals = {} self._backend_connect() - # This thread is similar to the provider bootstrapper - self._eip_bootstrapper = EIPBootstrapper() - - # EIP signals ---- move to eip conductor. - # TODO change the name of "download_config" signal to - # something less confusing (config_ready maybe) - self._eip_bootstrapper.download_config.connect( - self._eip_intermediate_stage) - self._eip_bootstrapper.download_client_certificate.connect( - self._finish_eip_bootstrap) - - self._vpn = VPN(openvpn_verb=openvpn_verb) - - # connect vpn process signals - self._vpn.qtsigs.state_changed.connect( - self._eip_status.update_vpn_state) - self._vpn.qtsigs.status_changed.connect( - self._eip_status.update_vpn_status) - self._vpn.qtsigs.process_finished.connect( - self._eip_finished) - self._vpn.qtsigs.network_unreachable.connect( - self._on_eip_network_unreachable) - self._vpn.qtsigs.process_restart_tls.connect( - self._do_eip_restart) - self._vpn.qtsigs.process_restart_ping.connect( - self._do_eip_restart) - self._soledad_bootstrapper = SoledadBootstrapper() self._soledad_bootstrapper.download_config.connect( self._soledad_intermediate_stage) @@ -260,8 +212,6 @@ class MainWindow(QtGui.QMainWindow): self._soledad_bootstrapped_stage) self._soledad_bootstrapper.local_only_ready.connect( self._soledad_bootstrapped_stage) - self._soledad_bootstrapper.soledad_timeout.connect( - self._retry_soledad_connection) self._soledad_bootstrapper.soledad_invalid_auth_token.connect( self._mail_status.set_soledad_invalid_auth_token) self._soledad_bootstrapper.soledad_failed.connect( @@ -307,6 +257,8 @@ class MainWindow(QtGui.QMainWindow): # self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences) self._enabled_services = [] + self._ui_mx_visible = True + self._ui_eip_visible = True # last minute UI manipulations @@ -342,6 +294,7 @@ class MainWindow(QtGui.QMainWindow): self._logger_window = None self._bypass_checks = bypass_checks + self._start_hidden = start_hidden # We initialize Soledad and Keymanager instances as # transparent proxies, so we can pass the reference freely @@ -349,7 +302,6 @@ class MainWindow(QtGui.QMainWindow): self._soledad = ProxyBase(None) self._keymanager = ProxyBase(None) - self._login_defer = None self._soledad_defer = None self._mail_conductor = mail_conductor.MailConductor( @@ -372,7 +324,7 @@ class MainWindow(QtGui.QMainWindow): if self._first_run(): self._wizard_firstrun = True - self._backend_disconnect() + self._disconnect_and_untrack() self._wizard = Wizard(backend=self._backend, bypass_checks=bypass_checks) # Give this window time to finish init and then show the wizard @@ -384,46 +336,151 @@ class MainWindow(QtGui.QMainWindow): # so this has to be done after eip_machine is started self._finish_init() + def _not_logged_in_error(self): + """ + Handle the 'not logged in' backend error if we try to do an operation + that requires to be logged in. + """ + logger.critical("You are trying to do an operation that requires " + "log in first.") + QtGui.QMessageBox.critical( + self, self.tr("Application error"), + self.tr("You are trying to do an operation " + "that requires logging in first.")) + + def _connect_and_track(self, signal, method): + """ + Helper to connect signals and keep track of them. + + :param signal: the signal to connect to. + :type signal: QtCore.Signal + :param method: the method to call when the signal is triggered. + :type method: callable, Slot or Signal + """ + self._backend_connected_signals[signal] = method + signal.connect(method) + + def _backend_bad_call(self, data): + """ + Callback for debugging bad backend calls + + :param data: data from the backend about the problem + :type data: str + """ + logger.error("Bad call to the backend:") + logger.error(data) + def _backend_connect(self): """ Helper to connect to backend signals """ sig = self._backend.signaler - sig.prov_name_resolution.connect(self._intermediate_stage) - sig.prov_https_connection.connect(self._intermediate_stage) - sig.prov_download_ca_cert.connect(self._intermediate_stage) - sig.prov_download_provider_info.connect(self._load_provider_config) - sig.prov_check_api_certificate.connect(self._provider_config_loaded) + sig.backend_bad_call.connect(self._backend_bad_call) + + self._connect_and_track(sig.prov_name_resolution, + self._intermediate_stage) + self._connect_and_track(sig.prov_https_connection, + self._intermediate_stage) + self._connect_and_track(sig.prov_download_ca_cert, + self._intermediate_stage) + + self._connect_and_track(sig.prov_download_provider_info, + self._load_provider_config) + self._connect_and_track(sig.prov_check_api_certificate, + self._provider_config_loaded) + + self._connect_and_track(sig.prov_problem_with_provider, + self._login_problem_provider) + + self._connect_and_track(sig.prov_cancelled_setup, + self._set_login_cancelled) + + # Login signals + self._connect_and_track(sig.srp_auth_ok, self._authentication_finished) + + auth_error = ( + lambda: self._authentication_error(self.tr("Unknown error."))) + self._connect_and_track(sig.srp_auth_error, auth_error) - # Only used at login, no need to disconnect this like we do - # with the other - sig.prov_problem_with_provider.connect(self._login_problem_provider) + auth_server_error = ( + lambda: self._authentication_error( + self.tr("There was a server problem with authentication."))) + self._connect_and_track(sig.srp_auth_server_error, auth_server_error) + auth_connection_error = ( + lambda: self._authentication_error( + self.tr("Could not establish a connection."))) + self._connect_and_track(sig.srp_auth_connection_error, + auth_connection_error) + + auth_bad_user_or_password = ( + lambda: self._authentication_error( + self.tr("Invalid username or password."))) + self._connect_and_track(sig.srp_auth_bad_user_or_password, + auth_bad_user_or_password) + + # Logout signals + self._connect_and_track(sig.srp_logout_ok, self._logout_ok) + self._connect_and_track(sig.srp_logout_error, self._logout_error) + + self._connect_and_track(sig.srp_not_logged_in_error, + self._not_logged_in_error) + + # EIP bootstrap signals + self._connect_and_track(sig.eip_config_ready, + self._eip_intermediate_stage) + self._connect_and_track(sig.eip_client_certificate_ready, + self._finish_eip_bootstrap) + + # We don't want to disconnect some signals so don't track them: sig.prov_unsupported_client.connect(self._needs_update) sig.prov_unsupported_api.connect(self._incompatible_api) - sig.prov_cancelled_setup.connect(self._set_login_cancelled) - - def _backend_disconnect(self): - """ - Helper to disconnect from backend signals. + # EIP start signals + sig.eip_openvpn_already_running.connect( + self._on_eip_openvpn_already_running) + sig.eip_alien_openvpn_already_running.connect( + self._on_eip_alien_openvpn_already_running) + sig.eip_openvpn_not_found_error.connect( + self._on_eip_openvpn_not_found_error) + sig.eip_vpn_launcher_exception.connect( + self._on_eip_vpn_launcher_exception) + sig.eip_no_polkit_agent_error.connect( + self._on_eip_no_polkit_agent_error) + sig.eip_no_pkexec_error.connect(self._on_eip_no_pkexec_error) + sig.eip_no_tun_kext_error.connect(self._on_eip_no_tun_kext_error) + + sig.eip_state_changed.connect(self._eip_status.update_vpn_state) + sig.eip_status_changed.connect(self._eip_status.update_vpn_status) + sig.eip_process_finished.connect(self._eip_finished) + sig.eip_network_unreachable.connect(self._on_eip_network_unreachable) + sig.eip_process_restart_tls.connect(self._do_eip_restart) + sig.eip_process_restart_ping.connect(self._do_eip_restart) + + sig.eip_can_start.connect(self._backend_can_start_eip) + sig.eip_cannot_start.connect(self._backend_cannot_start_eip) + + def _disconnect_and_untrack(self): + """ + Helper to disconnect the tracked signals. Some signals are emitted from the wizard, and we want to ignore those. """ - sig = self._backend.signaler - sig.prov_name_resolution.disconnect(self._intermediate_stage) - sig.prov_https_connection.disconnect(self._intermediate_stage) - sig.prov_download_ca_cert.disconnect(self._intermediate_stage) + for signal, method in self._backend_connected_signals.items(): + try: + signal.disconnect(method) + except RuntimeError: + pass # Signal was not connected - sig.prov_download_provider_info.disconnect(self._load_provider_config) - sig.prov_check_api_certificate.disconnect(self._provider_config_loaded) + self._backend_connected_signals = {} + @QtCore.Slot() def _rejected_wizard(self): """ - SLOT - TRIGGERS: self._wizard.rejected + TRIGGERS: + self._wizard.rejected Called if the wizard has been cancelled or closed before finishing. @@ -444,12 +501,12 @@ class MainWindow(QtGui.QMainWindow): if self._wizard_firstrun: self._finish_init() + @QtCore.Slot() def _launch_wizard(self): """ - SLOT TRIGGERS: - self._login_widget.show_wizard - self.ui.action_wizard.triggered + self._login_widget.show_wizard + self.ui.action_wizard.triggered Also called in first run. @@ -457,7 +514,7 @@ class MainWindow(QtGui.QMainWindow): there. """ if self._wizard is None: - self._backend_disconnect() + self._disconnect_and_untrack() self._wizard = Wizard(backend=self._backend, bypass_checks=self._bypass_checks) self._wizard.accepted.connect(self._finish_init) @@ -472,11 +529,11 @@ class MainWindow(QtGui.QMainWindow): self._wizard.finished.connect(self._wizard_finished) self._settings.set_skip_first_run(True) + @QtCore.Slot() def _wizard_finished(self): """ - SLOT - TRIGGERS - self._wizard.finished + TRIGGERS: + self._wizard.finished Called when the wizard has finished. """ @@ -497,11 +554,11 @@ class MainWindow(QtGui.QMainWindow): return h return None + @QtCore.Slot() def _show_logger_window(self): """ - SLOT TRIGGERS: - self.ui.action_show_logs.triggered + self.ui.action_show_logs.triggered Displays the window with the history of messages logged until now and displays the new ones on arrival. @@ -518,9 +575,9 @@ class MainWindow(QtGui.QMainWindow): else: self._logger_window.setVisible(not self._logger_window.isVisible()) + @QtCore.Slot() def _show_AKM(self): """ - SLOT TRIGGERS: self.ui.action_advanced_key_management.triggered @@ -528,30 +585,38 @@ class MainWindow(QtGui.QMainWindow): """ domain = self._login_widget.get_selected_provider() logged_user = "{0}@{1}".format(self._logged_user, domain) - self._akm = AdvancedKeyManagement( - logged_user, self._keymanager, self._soledad) - self._akm.show() + has_mx = True + if self._logged_user is not None: + provider_config = self._get_best_provider_config() + has_mx = provider_config.provides_mx() + + akm = AdvancedKeyManagement( + self, has_mx, logged_user, self._keymanager, self._soledad) + akm.show() + + @QtCore.Slot() def _show_preferences(self): """ - SLOT TRIGGERS: - self.ui.btnPreferences.clicked (disabled for now) - self.ui.action_preferences + self.ui.btnPreferences.clicked (disabled for now) + self.ui.action_preferences Displays the preferences window. """ + user = self._login_widget.get_user() + prov = self._login_widget.get_selected_provider() preferences = PreferencesWindow( - self, self._srp_auth, self._provider_config, self._soledad, - self._login_widget.get_selected_provider()) + self, self._backend, self._provider_config, self._soledad, + user, prov) self.soledad_ready.connect(preferences.set_soledad_ready) preferences.show() preferences.preferences_saved.connect(self._update_eip_enabled_status) + @QtCore.Slot() def _update_eip_enabled_status(self): """ - SLOT TRIGGER: PreferencesWindow.preferences_saved @@ -563,17 +628,43 @@ class MainWindow(QtGui.QMainWindow): """ settings = self._settings default_provider = settings.get_defaultprovider() + + if default_provider is None: + logger.warning("Trying toupdate eip enabled status but there's no" + " default provider. Disabling EIP for the time" + " being...") + self._backend_cannot_start_eip() + return + + self._trying_to_start_eip = settings.get_autostart_eip() + self._backend.eip_can_start(default_provider) + + # If we don't want to start eip, we leave everything + # initialized to quickly start it + if not self._trying_to_start_eip: + self._backend.setup_eip(default_provider, skip_network=True) + + def _backend_can_start_eip(self): + """ + TRIGGER: + self._backend.signaler.eip_can_start + + If EIP can be started right away, and the client is configured + to do so, start it. Otherwise it leaves everything in place + for the user to click Turn ON. + """ + settings = self._settings + default_provider = settings.get_defaultprovider() enabled_services = [] if default_provider is not None: enabled_services = settings.get_enabled_services(default_provider) eip_enabled = False if EIP_SERVICE in enabled_services: - should_autostart = settings.get_autostart_eip() - if should_autostart and default_provider is not None: + eip_enabled = True + if default_provider is not None: self._eip_status.enable_eip_start() self._eip_status.set_eip_status("") - eip_enabled = True else: # we don't have an usable provider # so the user needs to log in first @@ -583,19 +674,44 @@ class MainWindow(QtGui.QMainWindow): self._eip_status.disable_eip_start() self._eip_status.set_eip_status(self.tr("Disabled")) - return eip_enabled + if eip_enabled and self._trying_to_start_eip: + self._trying_to_start_eip = False + self._try_autostart_eip() + + def _backend_cannot_start_eip(self): + """ + TRIGGER: + self._backend.signaler.eip_cannot_start + If EIP can't be started right away, get the UI to what it + needs to look like and waits for a proper login/eip bootstrap. + """ + settings = self._settings + default_provider = settings.get_defaultprovider() + enabled_services = [] + if default_provider is not None: + enabled_services = settings.get_enabled_services(default_provider) + + if EIP_SERVICE in enabled_services: + # we don't have a usable provider + # so the user needs to log in first + self._eip_status.disable_eip_start() + else: + self._stop_eip() + self._eip_status.disable_eip_start() + self._eip_status.set_eip_status(self.tr("Disabled")) + + @QtCore.Slot() def _show_eip_preferences(self): """ - SLOT TRIGGERS: - self.ui.btnEIPPreferences.clicked - self.ui.action_eip_preferences (disabled for now) + self.ui.btnEIPPreferences.clicked + self.ui.action_eip_preferences (disabled for now) Displays the EIP preferences window. """ domain = self._login_widget.get_selected_provider() - EIPPreferencesWindow(self, domain).show() + EIPPreferencesWindow(self, domain, self._backend).show() # # updates @@ -610,22 +726,27 @@ class MainWindow(QtGui.QMainWindow): """ self.new_updates.emit(req) + @QtCore.Slot(object) def _react_to_new_updates(self, req): """ - SLOT - TRIGGER: self._new_updates_available + TRIGGERS: + self.new_updates Displays the new updates label and sets the updates_content + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest """ self.moveToThread(QtCore.QCoreApplication.instance().thread()) self.ui.lblNewUpdates.setVisible(True) self.ui.btnMore.setVisible(True) self._updates_content = req.content + @QtCore.Slot() def _updates_details(self): """ - SLOT - TRIGGER: self.ui.btnMore.clicked + TRIGGERS: + self.ui.btnMore.clicked Parses and displays the updates details """ @@ -649,11 +770,11 @@ class MainWindow(QtGui.QMainWindow): self.tr("Updates available"), msg) + @QtCore.Slot() def _finish_init(self): """ - SLOT TRIGGERS: - self._wizard.accepted + self._wizard.accepted Also called at the end of the constructor if not first run. @@ -666,11 +787,13 @@ class MainWindow(QtGui.QMainWindow): providers = self._settings.get_configured_providers() self._login_widget.set_providers(providers) self._show_systray() - self.show() - if IS_MAC: - self.raise_() - self._hide_unsupported_services() + if not self._start_hidden: + self.show() + if IS_MAC: + self.raise_() + + self._show_hide_unsupported_services() if self._wizard: possible_username = self._wizard.get_username() @@ -696,7 +819,7 @@ class MainWindow(QtGui.QMainWindow): self._wizard = None self._backend_connect() else: - self._try_autostart_eip() + self._update_eip_enabled_status() domain = self._settings.get_provider() if domain is not None: @@ -712,7 +835,7 @@ class MainWindow(QtGui.QMainWindow): if self._login_widget.load_user_from_keyring(saved_user): self._login() - def _hide_unsupported_services(self): + def _show_hide_unsupported_services(self): """ Given a set of configured providers, it creates a set of available services among all of them and displays the service @@ -733,8 +856,38 @@ class MainWindow(QtGui.QMainWindow): for service in provider_config.get_services(): services.add(service) - self.ui.eipWidget.setVisible(EIP_SERVICE in services) - self.ui.mailWidget.setVisible(MX_SERVICE in services) + self._set_eip_visible(EIP_SERVICE in services) + self._set_mx_visible(MX_SERVICE in services) + + def _set_mx_visible(self, visible): + """ + Change the visibility of MX_SERVICE related UI components. + + :param visible: whether the components should be visible or not. + :type visible: bool + """ + # only update visibility if it is something to change + if self._ui_mx_visible ^ visible: + self.ui.mailWidget.setVisible(visible) + self.ui.lineUnderEmail.setVisible(visible) + self._action_mail_status.setVisible(visible) + self._ui_mx_visible = visible + + def _set_eip_visible(self, visible): + """ + Change the visibility of EIP_SERVICE related UI components. + + :param visible: whether the components should be visible or not. + :type visible: bool + """ + # NOTE: we use xor to avoid the code being run if the visibility hasn't + # changed. This is meant to avoid the eip menu being displayed floating + # around at start because the systray isn't rendered yet. + if self._ui_eip_visible ^ visible: + self.ui.eipWidget.setVisible(visible) + self.ui.lineUnderEIP.setVisible(visible) + self._eip_menu.setVisible(visible) + self._ui_eip_visible = visible def _set_label_offline(self): """ @@ -771,7 +924,7 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addSeparator() eip_status_label = "{0}: {1}".format(self._eip_name, self.tr("OFF")) - eip_menu = systrayMenu.addMenu(eip_status_label) + self._eip_menu = eip_menu = systrayMenu.addMenu(eip_status_label) eip_menu.addAction(self._action_eip_startstop) self._eip_status.set_eip_status_menu(eip_menu) systrayMenu.addSeparator() @@ -787,10 +940,21 @@ class MainWindow(QtGui.QMainWindow): self._mail_status.set_systray(self._systray) self._eip_status.set_systray(self._systray) + if self._start_hidden: + hello = lambda: self._systray.showMessage( + self.tr('Hello!'), + self.tr('Bitmask has started in the tray.')) + # we wait for the systray to be ready + reactor.callLater(1, hello) + + @QtCore.Slot(int) def _tray_activated(self, reason=None): """ - SLOT - TRIGGER: self._systray.activated + TRIGGERS: + self._systray.activated + + :param reason: the reason why the tray got activated. + :type reason: int Displays the context menu from the tray icon """ @@ -817,10 +981,11 @@ class MainWindow(QtGui.QMainWindow): visible = self.isVisible() and self.isActiveWindow() self._action_visible.setText(get_action(visible)) + @QtCore.Slot() def _toggle_visible(self): """ - SLOT - TRIGGER: self._action_visible.triggered + TRIGGERS: + self._action_visible.triggered Toggles the window visibility """ @@ -864,10 +1029,11 @@ class MainWindow(QtGui.QMainWindow): if state is not None: self.restoreState(state) + @QtCore.Slot() def _about(self): """ - SLOT - TRIGGERS: self.ui.action_about_leap.triggered + TRIGGERS: + self.ui.action_about_leap.triggered Display the About Bitmask dialog """ @@ -891,10 +1057,11 @@ class MainWindow(QtGui.QMainWindow): "<a href='https://leap.se'>More about LEAP" "</a>") % (VERSION, VERSION_HASH[:10], greet)) + @QtCore.Slot() def _help(self): """ - SLOT - TRIGGERS: self.ui.action_help.triggered + TRIGGERS: + self.ui.action_help.triggered Display the Bitmask help dialog. """ @@ -991,10 +1158,11 @@ class MainWindow(QtGui.QMainWindow): provider = self._login_widget.get_selected_provider() self._backend.setup_provider(provider) + @QtCore.Slot(dict) def _load_provider_config(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_download_provider_info + TRIGGERS: + self._backend.signaler.prov_download_provider_info Once the provider config has been downloaded, this loads the self._provider_config instance with it and starts the second @@ -1009,8 +1177,9 @@ class MainWindow(QtGui.QMainWindow): self._backend.provider_bootstrap(selected_provider) else: logger.error(data[self._backend.ERROR_KEY]) - self._login_widget.set_enabled(True) + self._login_problem_provider() + @QtCore.Slot() def _login_problem_provider(self): """ Warns the user about a problem with the provider during login. @@ -1019,11 +1188,11 @@ class MainWindow(QtGui.QMainWindow): self.tr("Unable to login: Problem with provider")) self._login_widget.set_enabled(True) + @QtCore.Slot() def _login(self): """ - SLOT TRIGGERS: - self._login_widget.login + self._login_widget.login Starts the login sequence. Which involves bootstrapping the selected provider if the selection is valid (not empty), then @@ -1043,50 +1212,33 @@ class MainWindow(QtGui.QMainWindow): self.offline_mode_bypass_login.emit() else: leap_assert(self._provider_config, "We need a provider config") + self.ui.action_create_new_account.setEnabled(False) if self._login_widget.start_login(): self._download_provider_config() - def _login_errback(self, failure): + @QtCore.Slot(unicode) + def _authentication_error(self, msg): """ - Error handler for the srpauth.authenticate method. + TRIGGERS: + Signaler.srp_auth_error + Signaler.srp_auth_server_error + Signaler.srp_auth_connection_error + Signaler.srp_auth_bad_user_or_password - :param failure: failure object that Twisted generates - :type failure: twisted.python.failure.Failure - """ - # NOTE: this behavior needs to be managed through the signaler, - # as we are doing with the prov_cancelled_setup signal. - # After we move srpauth to the backend, we need to update this. - logger.error("Error logging in, {0!r}".format(failure)) - - if failure.check(CancelledError): - logger.debug("Defer cancelled.") - failure.trap(Exception) - self._set_login_cancelled() - return - elif failure.check(srpauth.SRPAuthBadUserOrPassword): - msg = self.tr("Invalid username or password.") - elif failure.check(srpauth.SRPAuthBadStatusCode, - srpauth.SRPAuthenticationError, - srpauth.SRPAuthVerificationFailed, - srpauth.SRPAuthNoSessionId, - srpauth.SRPAuthNoSalt, srpauth.SRPAuthNoB, - srpauth.SRPAuthBadDataFromServer, - srpauth.SRPAuthJSONDecodeError): - msg = self.tr("There was a server problem with authentication.") - elif failure.check(srpauth.SRPAuthConnectionError): - msg = self.tr("Could not establish a connection.") - else: - # this shouldn't happen, but just in case. - msg = self.tr("Unknown error: {0!r}".format(failure.value)) + Handle the authentication errors. + :param msg: the message to show to the user. + :type msg: unicode + """ self._login_widget.set_status(msg) self._login_widget.set_enabled(True) + self.ui.action_create_new_account.setEnabled(True) + @QtCore.Slot() def _cancel_login(self): """ - SLOT TRIGGERS: - self._login_widget.cancel_login + self._login_widget.cancel_login Stops the login sequence. """ @@ -1097,21 +1249,18 @@ class MainWindow(QtGui.QMainWindow): """ Cancel the running defers to avoid app blocking. """ + # XXX: Should we stop all the backend defers? self._backend.cancel_setup_provider() - - if self._login_defer is not None: - logger.debug("Cancelling login defer.") - self._login_defer.cancel() - self._login_defer = None + self._backend.cancel_login() if self._soledad_defer is not None: logger.debug("Cancelling soledad defer.") self._soledad_defer.cancel() self._soledad_defer = None + @QtCore.Slot() def _set_login_cancelled(self): """ - SLOT TRIGGERS: Signaler.prov_cancelled_setup fired by self._backend.cancel_setup_provider() @@ -1122,10 +1271,11 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.set_status(self.tr("Log in cancelled by the user.")) self._login_widget.set_enabled(True) + @QtCore.Slot(dict) def _provider_config_loaded(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_check_api_certificate + TRIGGERS: + self._backend.signaler.prov_check_api_certificate Once the provider configuration is loaded, this starts the SRP authentication @@ -1136,27 +1286,19 @@ class MainWindow(QtGui.QMainWindow): username = self._login_widget.get_user() password = self._login_widget.get_password() - self._hide_unsupported_services() + self._show_hide_unsupported_services() - if self._srp_auth is None: - self._srp_auth = SRPAuth(self._provider_config) - self._srp_auth.authentication_finished.connect( - self._authentication_finished) - self._srp_auth.logout_ok.connect(self._logout_ok) - self._srp_auth.logout_error.connect(self._logout_error) - - self._login_defer = self._srp_auth.authenticate(username, password) - self._login_defer.addErrback(self._login_errback) + domain = self._provider_config.get_domain() + self._backend.login(domain, username, password) else: - self._login_widget.set_status( - "Unable to login: Problem with provider") logger.error(data[self._backend.ERROR_KEY]) - self._login_widget.set_enabled(True) + self._login_problem_provider() + @QtCore.Slot() def _authentication_finished(self): """ - SLOT - TRIGGER: self._srp_auth.authentication_finished + TRIGGERS: + self._srp_auth.authentication_finished Once the user is properly authenticated, try starting the EIP service @@ -1168,8 +1310,8 @@ class MainWindow(QtGui.QMainWindow): domain = self._provider_config.get_domain() full_user_id = make_address(user, domain) self._mail_conductor.userid = full_user_id - self._login_defer = None self._start_eip_bootstrap() + self.ui.action_create_new_account.setEnabled(True) # if soledad/mail is enabled: if MX_SERVICE in self._enabled_services: @@ -1179,6 +1321,9 @@ class MainWindow(QtGui.QMainWindow): self._soledad_bootstrapper.soledad_failed.connect( lambda: btn_enabled(True)) + if not self._get_best_provider_config().provides_mx(): + self._set_mx_visible(False) + def _start_eip_bootstrap(self): """ Changes the stackedWidget index to the EIP status one and @@ -1267,12 +1412,12 @@ class MainWindow(QtGui.QMainWindow): ################################################################### # Service control methods: soledad + @QtCore.Slot(dict) def _soledad_intermediate_stage(self, data): # TODO missing param docstring """ - SLOT TRIGGERS: - self._soledad_bootstrapper.download_config + self._soledad_bootstrapper.download_config If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case @@ -1285,29 +1430,13 @@ class MainWindow(QtGui.QMainWindow): # that sets the global status logger.error("Soledad failed to start: %s" % (data[self._soledad_bootstrapper.ERROR_KEY],)) - self._retry_soledad_connection() - - def _retry_soledad_connection(self): - """ - Retries soledad connection. - """ - # XXX should move logic to soledad boostrapper itself - logger.debug("Retrying soledad connection.") - if self._soledad_bootstrapper.should_retry_initialization(): - self._soledad_bootstrapper.increment_retries_count() - # XXX should cancel the existing socket --- this - # is avoiding a clean termination. - self._maybe_run_soledad_setup_checks() - else: - logger.warning("Max number of soledad initialization " - "retries reached.") + @QtCore.Slot(dict) def _soledad_bootstrapped_stage(self, data): """ - SLOT TRIGGERS: - self._soledad_bootstrapper.gen_key - self._soledad_bootstrapper.local_only_ready + self._soledad_bootstrapper.gen_key + self._soledad_bootstrapper.local_only_ready If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case @@ -1346,7 +1475,6 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _start_smtp_bootstrapping(self): """ - SLOT TRIGGERS: self.soledad_ready """ @@ -1354,20 +1482,15 @@ class MainWindow(QtGui.QMainWindow): logger.debug("not starting smtp in offline mode") return - # TODO for simmetry, this should be called start_smtp_service - # (and delegate all the checks to the conductor) if self._provides_mx_and_enabled(): - self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks( - self._provider_config, - self._mail_conductor.smtp_config, - download_if_needed=True) + self._mail_conductor.start_smtp_service(self._provider_config, + download_if_needed=True) # XXX --- should remove from here, and connecte directly to the state # machine. @QtCore.Slot() def _stop_smtp_service(self): """ - SLOT TRIGGERS: self.logout """ @@ -1380,7 +1503,6 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _start_imap_service(self): """ - SLOT TRIGGERS: self.soledad_ready """ @@ -1410,7 +1532,6 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _fetch_incoming_mail(self): """ - SLOT TRIGGERS: self.mail_client_logged_in """ @@ -1420,7 +1541,6 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _stop_imap_service(self): """ - SLOT TRIGGERS: self.logout """ @@ -1467,11 +1587,11 @@ class MainWindow(QtGui.QMainWindow): self._action_eip_startstop.setEnabled(True) @QtCore.Slot() - def _on_eip_connected(self): + def _on_eip_connection_connected(self): """ - SLOT TRIGGERS: self._eip_status.eip_connection_connected + Emits the EIPConnection.qtsigs.connected_signal This is a little workaround for connecting the vpn-connected @@ -1480,9 +1600,14 @@ class MainWindow(QtGui.QMainWindow): """ self._eip_connection.qtsigs.connected_signal.emit() - # check for connectivity provider_config = self._get_best_provider_config() domain = provider_config.get_domain() + + self._eip_status.set_provider(domain) + self._settings.set_defaultprovider(domain) + self._already_started_eip = True + + # check for connectivity self._check_name_resolution(domain) def _check_name_resolution(self, domain): @@ -1534,137 +1659,112 @@ class MainWindow(QtGui.QMainWindow): Tries to autostart EIP """ settings = self._settings - - if not self._update_eip_enabled_status(): - return - default_provider = settings.get_defaultprovider() self._enabled_services = settings.get_enabled_services( default_provider) loaded = self._provisional_provider_config.load( provider.get_provider_path(default_provider)) - if loaded: + if loaded and settings.get_autostart_eip(): # XXX I think we should not try to re-download config every time, # it adds some delay. # Maybe if it's the first run in a session, # or we can try only if it fails. self._maybe_start_eip() - else: + elif settings.get_autostart_eip(): # XXX: Display a proper message to the user self.eip_needs_login.emit() logger.error("Unable to load %s config, cannot autostart." % (default_provider,)) @QtCore.Slot() - def _start_eip(self): + def _start_EIP(self): """ - SLOT - TRIGGERS: - self._eip_connection.qtsigs.do_connect_signal - (via state machine) - or called from _finish_eip_bootstrap - Starts EIP """ - provider_config = self._get_best_provider_config() - provider = provider_config.get_domain() self._eip_status.eip_pre_up() self.user_stopped_eip = False - # until we set an option in the preferences window, - # we'll assume that by default we try to autostart. - # If we switch it off manually, it won't try the next - # time. + # Until we set an option in the preferences window, we'll assume that + # by default we try to autostart. If we switch it off manually, it + # won't try the next time. self._settings.set_autostart_eip(True) - loaded = eipconfig.load_eipconfig_if_needed( - provider_config, self._eip_config, provider) + self._backend.start_eip() - if not loaded: - eip_status_label = self.tr("Could not load {0} configuration.") - eip_status_label = eip_status_label.format(self._eip_name) - self._eip_status.set_eip_status(eip_status_label, error=True) - # signal connection aborted to state machine - qtsigs = self._eip_connection.qtsigs - qtsigs.connection_aborted_signal.emit() - logger.error("Tried to start EIP but cannot find any " - "available provider!") - return + @QtCore.Slot() + def _on_eip_connection_aborted(self): + """ + TRIGGERS: + Signaler.eip_connection_aborted + """ + logger.error("Tried to start EIP but cannot find any " + "available provider!") - try: - # XXX move this to EIPConductor - host, port = get_openvpn_management() - self._vpn.start(eipconfig=self._eip_config, - providerconfig=provider_config, - socket_host=host, - socket_port=port) - self._settings.set_defaultprovider(provider) - - # XXX move to the state machine too - self._eip_status.set_provider(provider) - - # TODO refactor exceptions so they provide translatable - # usef-facing messages. - except EIPNoPolkitAuthAgentAvailable: - self._eip_status.set_eip_status( - # XXX this should change to polkit-kde where - # applicable. - self.tr("We could not find any " - "authentication " - "agent in your system.<br/>" - "Make sure you have " - "<b>polkit-gnome-authentication-" - "agent-1</b> " - "running and try again."), - error=True) - self._set_eipstatus_off() - except EIPNoTunKextLoaded: - self._eip_status.set_eip_status( - self.tr("{0} cannot be started because " - "the tuntap extension is not installed properly " - "in your system.").format(self._eip_name)) - self._set_eipstatus_off() - except EIPNoPkexecAvailable: - self._eip_status.set_eip_status( - self.tr("We could not find <b>pkexec</b> " - "in your system."), - error=True) - self._set_eipstatus_off() - except OpenVPNNotFoundException: - self._eip_status.set_eip_status( - self.tr("We could not find openvpn binary."), - error=True) - self._set_eipstatus_off() - except OpenVPNAlreadyRunning as e: - self._eip_status.set_eip_status( - self.tr("Another openvpn instance is already running, and " - "could not be stopped."), - error=True) - self._set_eipstatus_off() - except AlienOpenVPNAlreadyRunning as e: - self._eip_status.set_eip_status( - self.tr("Another openvpn instance is already running, and " - "could not be stopped because it was not launched by " - "Bitmask. Please stop it and try again."), - error=True) - self._set_eipstatus_off() - except VPNLauncherException as e: - # XXX We should implement again translatable exceptions so - # we can pass a translatable string to the panel (usermessage attr) - self._eip_status.set_eip_status("%s" % (e,), error=True) - self._set_eipstatus_off() - else: - self._already_started_eip = True + eip_status_label = self.tr("Could not load {0} configuration.") + eip_status_label = eip_status_label.format(self._eip_name) + self._eip_status.set_eip_status(eip_status_label, error=True) + + # signal connection_aborted to state machine: + qtsigs = self._eip_connection.qtsigs + qtsigs.connection_aborted_signal.emit() + + def _on_eip_openvpn_already_running(self): + self._eip_status.set_eip_status( + self.tr("Another openvpn instance is already running, and " + "could not be stopped."), + error=True) + self._set_eipstatus_off() + + def _on_eip_alien_openvpn_already_running(self): + self._eip_status.set_eip_status( + self.tr("Another openvpn instance is already running, and " + "could not be stopped because it was not launched by " + "Bitmask. Please stop it and try again."), + error=True) + self._set_eipstatus_off() + + def _on_eip_openvpn_not_found_error(self): + self._eip_status.set_eip_status( + self.tr("We could not find openvpn binary."), + error=True) + self._set_eipstatus_off() + + def _on_eip_vpn_launcher_exception(self): + # XXX We should implement again translatable exceptions so + # we can pass a translatable string to the panel (usermessage attr) + self._eip_status.set_eip_status("VPN Launcher error.", error=True) + self._set_eipstatus_off() + + def _on_eip_no_polkit_agent_error(self): + self._eip_status.set_eip_status( + # XXX this should change to polkit-kde where + # applicable. + self.tr("We could not find any authentication agent in your " + "system.<br/>Make sure you have" + "<b>polkit-gnome-authentication-agent-1</b> running and" + "try again."), + error=True) + self._set_eipstatus_off() + + def _on_eip_no_pkexec_error(self): + self._eip_status.set_eip_status( + self.tr("We could not find <b>pkexec</b> in your system."), + error=True) + self._set_eipstatus_off() + + def _on_eip_no_tun_kext_error(self): + self._eip_status.set_eip_status( + self.tr("{0} cannot be started because the tuntap extension is " + "not installed properly in your " + "system.").format(self._eip_name)) + self._set_eipstatus_off() @QtCore.Slot() def _stop_eip(self): """ - SLOT TRIGGERS: - self._eip_connection.qtsigs.do_disconnect_signal - (via state machine) - or called from _eip_finished + self._eip_connection.qtsigs.do_disconnect_signal (via state machine) Stops vpn process and makes gui adjustments to reflect the change of state. @@ -1673,7 +1773,7 @@ class MainWindow(QtGui.QMainWindow): :type abnormal: bool """ self.user_stopped_eip = True - self._vpn.terminate() + self._backend.stop_eip() self._set_eipstatus_off(False) self._already_started_eip = False @@ -1692,7 +1792,6 @@ class MainWindow(QtGui.QMainWindow): def _on_eip_network_unreachable(self): # XXX Should move to EIP Conductor """ - SLOT TRIGGERS: self._eip_connection.qtsigs.network_unreachable @@ -1706,7 +1805,7 @@ class MainWindow(QtGui.QMainWindow): def _do_eip_restart(self): # XXX Should move to EIP Conductor """ - SLOT + TRIGGERS: self._eip_connection.qtsigs.process_restart Restart the connection. @@ -1714,7 +1813,7 @@ class MainWindow(QtGui.QMainWindow): # for some reason, emitting the do_disconnect/do_connect # signals hangs the UI. self._stop_eip() - QtCore.QTimer.singleShot(2000, self._start_eip) + QtCore.QTimer.singleShot(2000, self._start_EIP) def _set_eipstatus_off(self, error=True): """ @@ -1724,11 +1823,11 @@ class MainWindow(QtGui.QMainWindow): self._eip_status.set_eip_status("", error=error) self._eip_status.set_eip_status_icon("error") + @QtCore.Slot(int) def _eip_finished(self, exitCode): """ - SLOT TRIGGERS: - self._vpn.process_finished + Signaler.eip_process_finished Triggered when the EIP/VPN process finishes to set the UI accordingly. @@ -1764,12 +1863,14 @@ class MainWindow(QtGui.QMainWindow): "because you did not authenticate properly.") eip_status_label = eip_status_label.format(self._eip_name) self._eip_status.set_eip_status(eip_status_label, error=True) - self._vpn.killit() signal = qtsigs.connection_aborted_signal + self._backend.terminate_eip() elif exitCode != 0 or not self.user_stopped_eip: eip_status_label = self.tr("{0} finished in an unexpected manner!") eip_status_label = eip_status_label.format(self._eip_name) + self._eip_status.eip_stopped() + self._eip_status.set_eip_status_icon("error") self._eip_status.set_eip_status(eip_status_label, error=True) signal = qtsigs.connection_died_signal @@ -1787,17 +1888,14 @@ class MainWindow(QtGui.QMainWindow): Start the EIP bootstrapping sequence if the client is configured to do so. """ - leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") - - provider_config = self._get_best_provider_config() - if self._provides_eip_and_enabled() and not self._already_started_eip: # XXX this should be handled by the state machine. self._eip_status.set_eip_status( self.tr("Starting...")) - self._eip_bootstrapper.run_eip_setup_checks( - provider_config, - download_if_needed=True) + + domain = self._login_widget.get_selected_provider() + self._backend.setup_eip(domain) + self._already_started_eip = True # we want to start soledad anyway after a certain timeout if eip # fails to come up @@ -1816,45 +1914,33 @@ class MainWindow(QtGui.QMainWindow): # eip will not start, so we start soledad anyway self._maybe_run_soledad_setup_checks() + @QtCore.Slot(dict) def _finish_eip_bootstrap(self, data): """ - SLOT - TRIGGER: self._eip_bootstrapper.download_client_certificate + TRIGGERS: + self._backend.signaler.eip_client_certificate_ready Starts the VPN thread if the eip configuration is properly loaded """ - leap_assert(self._eip_config, "We need an eip config!") - passed = data[self._eip_bootstrapper.PASSED_KEY] + passed = data[self._backend.PASSED_KEY] if not passed: error_msg = self.tr("There was a problem with the provider") self._eip_status.set_eip_status(error_msg, error=True) - logger.error(data[self._eip_bootstrapper.ERROR_KEY]) + logger.error(data[self._backend.ERROR_KEY]) self._already_started_eip = False return - provider_config = self._get_best_provider_config() - domain = provider_config.get_domain() - - # XXX move check to _start_eip ? - loaded = eipconfig.load_eipconfig_if_needed( - provider_config, self._eip_config, domain) - - if loaded: - # DO START EIP Connection! - self._eip_connection.qtsigs.do_connect_signal.emit() - else: - eip_status_label = self.tr("Could not load {0} configuration.") - eip_status_label = eip_status_label.format(self._eip_name) - self._eip_status.set_eip_status(eip_status_label, error=True) + # DO START EIP Connection! + self._eip_connection.qtsigs.do_connect_signal.emit() + @QtCore.Slot(dict) def _eip_intermediate_stage(self, data): # TODO missing param """ - SLOT TRIGGERS: - self._eip_bootstrapper.download_config + self._backend.signaler.eip_config_ready If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case @@ -1896,12 +1982,11 @@ class MainWindow(QtGui.QMainWindow): @QtCore.Slot() def _logout(self): """ - SLOT - TRIGGER: self._login_widget.logout + TRIGGERS: + self._login_widget.logout Starts the logout sequence """ - self._soledad_bootstrapper.cancel_bootstrap() setProxiedObject(self._soledad, None) self._cancel_ongoing_defers() @@ -1911,13 +1996,14 @@ class MainWindow(QtGui.QMainWindow): # XXX: If other defers are doing authenticated stuff, this # might conflict with those. CHECK! - threads.deferToThread(self._srp_auth.logout) + self._backend.logout() self.logout.emit() + @QtCore.Slot() def _logout_error(self): """ - SLOT - TRIGGER: self._srp_auth.logout_error + TRIGGER: + self._srp_auth.logout_error Inform the user about a logout error. """ @@ -1926,10 +2012,11 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.set_status( self.tr("Something went wrong with the logout.")) + @QtCore.Slot() def _logout_ok(self): """ - SLOT - TRIGGER: self._srp_auth.logout_ok + TRIGGER: + self._srp_auth.logout_ok Switches the stackedWidget back to the login stage after logging out @@ -1941,15 +2028,16 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.logged_out() self._mail_status.mail_state_disabled() + self._show_hide_unsupported_services() + + @QtCore.Slot(dict) def _intermediate_stage(self, data): # TODO this method name is confusing as hell. """ - SLOT TRIGGERS: - self._backend.signaler.prov_name_resolution - self._backend.signaler.prov_https_connection - self._backend.signaler.prov_download_ca_cert - self._eip_bootstrapper.download_config + self._backend.signaler.prov_name_resolution + self._backend.signaler.prov_https_connection + self._backend.signaler.prov_download_ca_cert If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case @@ -1957,10 +2045,8 @@ class MainWindow(QtGui.QMainWindow): """ passed = data[self._backend.PASSED_KEY] if not passed: - msg = self.tr("Unable to connect: Problem with provider") - self._login_widget.set_status(msg) - self._login_widget.set_enabled(True) logger.error(data[self._backend.ERROR_KEY]) + self._login_problem_provider() # # window handling methods @@ -1974,9 +2060,9 @@ class MainWindow(QtGui.QMainWindow): raise_window_ack() self.raise_window.emit() + @QtCore.Slot() def _do_raise_mainwindow(self): """ - SLOT TRIGGERS: self._on_raise_window_event @@ -2012,11 +2098,8 @@ class MainWindow(QtGui.QMainWindow): self._stop_imap_service() - if self._srp_auth is not None: - if self._srp_auth.get_session_id() is not None or \ - self._srp_auth.get_token() is not None: - # XXX this can timeout after loong time: See #3368 - self._srp_auth.logout() + if self._logged_user is not None: + self._backend.logout() if self._soledad_bootstrapper.soledad is not None: logger.debug("Closing soledad...") @@ -2025,14 +2108,27 @@ class MainWindow(QtGui.QMainWindow): logger.error("No instance of soledad was found.") logger.debug('Terminating vpn') - self._vpn.terminate(shutdown=True) + self._backend.stop_eip(shutdown=True) + + # We need to give some time to the ongoing signals for shutdown + # to come into action. This needs to be solved using + # back-communication from backend. + QtCore.QTimer.singleShot(3000, self._shutdown) + def _shutdown(self): + """ + Actually shutdown. + """ self._cancel_ongoing_defers() # TODO missing any more cancels? logger.debug('Cleaning pidfiles') self._cleanup_pidfiles() + if self._quit_callback: + self._quit_callback() + + logger.debug('Bye.') def quit(self): """ @@ -2041,11 +2137,24 @@ class MainWindow(QtGui.QMainWindow): # TODO separate the shutting down of services from the # UI stuff. + # first thing to do quitting, hide the mainwindow and show tooltip. + self.hide() + if self._systray is not None: + self._systray.showMessage( + self.tr('Quitting...'), + self.tr('The app is quitting, please wait.')) + + # explicitly process events to display tooltip immediately + QtCore.QCoreApplication.processEvents() + # Set this in case that the app is hidden QtGui.QApplication.setQuitOnLastWindowClosed(True) - self._backend.stop() self._cleanup_and_quit() + + # We queue the call to stop since we need to wait until EIP is stopped. + # Otherwise we may exit leaving an unmanaged openvpn process. + reactor.callLater(0, self._backend.stop) self._really_quit = True if self._wizard: @@ -2055,8 +2164,3 @@ class MainWindow(QtGui.QMainWindow): self._logger_window.close() self.close() - - if self._quit_callback: - self._quit_callback() - - logger.debug('Bye.') diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index b2cc2236..2947c5db 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -29,7 +29,6 @@ from leap.bitmask.provider import get_provider_path from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.gui.ui_preferences import Ui_Preferences from leap.soledad.client import NoStorageSecret -from leap.bitmask.crypto.srpauth import SRPAuthBadUserOrPassword from leap.bitmask.util.password import basic_password_checks from leap.bitmask.services import get_supported from leap.bitmask.config.providerconfig import ProviderConfig @@ -44,25 +43,33 @@ class PreferencesWindow(QtGui.QDialog): """ preferences_saved = QtCore.Signal() - def __init__(self, parent, srp_auth, provider_config, soledad, domain): + def __init__(self, parent, backend, provider_config, + soledad, username, domain): """ :param parent: parent object of the PreferencesWindow. :parent type: QWidget - :param srp_auth: SRPAuth object configured in the main app. - :type srp_auth: SRPAuth + :param backend: Backend being used + :type backend: Backend :param provider_config: ProviderConfig object. :type provider_config: ProviderConfig :param soledad: Soledad instance :type soledad: Soledad + :param username: the user set in the login widget + :type username: unicode :param domain: the selected domain in the login widget :type domain: unicode """ QtGui.QDialog.__init__(self, parent) self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") - self._srp_auth = srp_auth + self._backend = backend self._settings = LeapSettings() self._soledad = soledad + self._provider_config = provider_config + self._username = username + self._domain = domain + + self._backend_connect() # Load UI self.ui = Ui_Preferences() @@ -82,43 +89,60 @@ class PreferencesWindow(QtGui.QDialog): else: self._add_configured_providers() - pw_enabled = False - - # check if the user is logged in - if srp_auth is not None and srp_auth.get_token() is not None: - # check if provider has 'mx' ... - if provider_config.provides_mx(): - enabled_services = self._settings.get_enabled_services(domain) - mx_name = get_service_display_name(MX_SERVICE) - - # ... and if the user have it enabled - if MX_SERVICE not in enabled_services: - msg = self.tr("You need to enable {0} in order to change " - "the password.".format(mx_name)) - self._set_password_change_status(msg, error=True) - else: - if sameProxiedObjects(self._soledad, None): - msg = self.tr( - "You need to wait until {0} is ready in " - "order to change the password.".format(mx_name)) - self._set_password_change_status(msg) - else: - # Soledad is bootstrapped - pw_enabled = True - else: - pw_enabled = True - else: - msg = self.tr( - "In order to change your password you need to be logged in.") - self._set_password_change_status(msg) + self._backend.get_logged_in_status() self._select_provider_by_name(domain) + @QtCore.Slot() + def _is_logged_in(self): + """ + TRIGGERS: + Signaler.srp_status_logged_in + + Actions to perform is the user is logged in. + """ + settings = self._settings + pw_enabled = True + + # check if provider has 'mx' ... + # TODO: we should move this to the backend. + if self._provider_config.provides_mx(): + enabled_services = settings.get_enabled_services(self._domain) + mx_name = get_service_display_name(MX_SERVICE) + + # ... and if the user have it enabled + if MX_SERVICE not in enabled_services: + msg = self.tr("You need to enable {0} in order to change " + "the password.".format(mx_name)) + self._set_password_change_status(msg, error=True) + pw_enabled = False + else: + # check if Soledad is bootstrapped + if sameProxiedObjects(self._soledad, None): + msg = self.tr( + "You need to wait until {0} is ready in " + "order to change the password.".format(mx_name)) + self._set_password_change_status(msg) + pw_enabled = False + self.ui.gbPasswordChange.setEnabled(pw_enabled) + @QtCore.Slot() + def _not_logged_in(self): + """ + TRIGGERS: + Signaler.srp_status_not_logged_in + + Actions to perform if the user is not logged in. + """ + msg = self.tr( + "In order to change your password you need to be logged in.") + self._set_password_change_status(msg) + self.ui.gbPasswordChange.setEnabled(False) + + @QtCore.Slot() def set_soledad_ready(self): """ - SLOT TRIGGERS: parent.soledad_ready @@ -163,15 +187,15 @@ class PreferencesWindow(QtGui.QDialog): self.ui.leNewPassword2.setEnabled(not disable) self.ui.pbChangePassword.setEnabled(not disable) + @QtCore.Slot() def _change_password(self): """ - SLOT TRIGGERS: self.ui.pbChangePassword.clicked Changes the user's password if the inputboxes are correctly filled. """ - username = self._srp_auth.get_username() + username = self._username current_password = self.ui.leCurrentPassword.text() new_password = self.ui.leNewPassword.text() new_password2 = self.ui.leNewPassword2.text() @@ -185,19 +209,17 @@ class PreferencesWindow(QtGui.QDialog): return self._set_changing_password(True) - d = self._srp_auth.change_password(current_password, new_password) - d.addCallback(partial(self._change_password_success, new_password)) - d.addErrback(self._change_password_problem) + self._backend.change_password(current_password, new_password) - def _change_password_success(self, new_password, _): + @QtCore.Slot() + def _change_password_ok(self): """ - Callback used to display a successfully performed action. + TRIGGERS: + self._backend.signaler.srp_password_change_ok - :param new_password: the new password for the user. - :type new_password: str. - :param _: the returned data from self._srp_auth.change_password - Ignored + Callback used to display a successfully changed password. """ + new_password = self.ui.leNewPassword.text() logger.debug("SRP password changed successfully.") try: self._soledad.change_passphrase(new_password) @@ -211,24 +233,21 @@ class PreferencesWindow(QtGui.QDialog): self._clear_password_inputs() self._set_changing_password(False) - def _change_password_problem(self, failure): - """ - Errback called if there was a problem with the deferred. - Also is used to display an error message. - - :param failure: the cause of the method failed. - :type failure: twisted.python.Failure + @QtCore.Slot(unicode) + def _change_password_problem(self, msg): """ - logger.error("Error changing password: %s", (failure, )) - problem = self.tr("There was a problem changing the password.") - - if failure.check(SRPAuthBadUserOrPassword): - problem = self.tr("You did not enter a correct current password.") + TRIGGERS: + self._backend.signaler.srp_password_change_error + self._backend.signaler.srp_password_change_badpw - self._set_password_change_status(problem, error=True) + Callback used to display an error on changing password. + :param msg: the message to show to the user. + :type msg: unicode + """ + logger.error("Error changing password") + self._set_password_change_status(msg, error=True) self._set_changing_password(False) - failure.trap(Exception) def _clear_password_inputs(self): """ @@ -272,10 +291,12 @@ class PreferencesWindow(QtGui.QDialog): provider_index = self.ui.cbProvidersServices.findText(name) self.ui.cbProvidersServices.setCurrentIndex(provider_index) + @QtCore.Slot(str, int) def _service_selection_changed(self, service, state): """ - SLOT - TRIGGER: service_checkbox.stateChanged + TRIGGERS: + service_checkbox.stateChanged + Adds the service to the state if the state is checked, removes it otherwise @@ -294,9 +315,9 @@ class PreferencesWindow(QtGui.QDialog): # We hide the maybe-visible status label after a change self.ui.lblProvidersServicesStatus.setVisible(False) + @QtCore.Slot(str) def _populate_services(self, domain): """ - SLOT TRIGGERS: self.ui.cbProvidersServices.currentIndexChanged[unicode] @@ -353,9 +374,9 @@ class PreferencesWindow(QtGui.QDialog): logger.error("Something went wrong while trying to " "load service %s" % (service,)) + @QtCore.Slot(str) def _save_enabled_services(self, provider): """ - SLOT TRIGGERS: self.ui.pbSaveServices.clicked @@ -387,3 +408,22 @@ class PreferencesWindow(QtGui.QDialog): provider_config = None return provider_config + + def _backend_connect(self): + """ + Helper to connect to backend signals + """ + sig = self._backend.signaler + + sig.srp_status_logged_in.connect(self._is_logged_in) + sig.srp_status_not_logged_in.connect(self._not_logged_in) + + sig.srp_password_change_ok.connect(self._change_password_ok) + + pwd_change_error = lambda: self._change_password_problem( + self.tr("There was a problem changing the password.")) + sig.srp_password_change_error.connect(pwd_change_error) + + pwd_change_badpw = lambda: self._change_password_problem( + self.tr("You did not enter a correct current password.")) + sig.srp_password_change_badpw.connect(pwd_change_badpw) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 93731ce0..31938a70 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -562,6 +562,8 @@ class ConnectionMachineBuilder(object): if action: off.assignProperty( action, 'text', off_label) + off.assignProperty( + action, 'enabled', True) off.setObjectName(_OFF) states[_OFF] = off diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py index 1e876c57..dfd69033 100644 --- a/src/leap/bitmask/gui/twisted_main.py +++ b/src/leap/bitmask/gui/twisted_main.py @@ -19,14 +19,23 @@ Main functions for integration of twisted reactor """ import logging -from twisted.internet import error - -# Resist the temptation of putting the import reactor here, -# it will raise an "reactor already imported" error. +from twisted.internet import error, reactor +from PySide import QtCore logger = logging.getLogger(__name__) +def stop(): + QtCore.QCoreApplication.sendPostedEvents() + QtCore.QCoreApplication.flush() + try: + reactor.stop() + logger.debug('Twisted reactor stopped') + except error.ReactorNotRunning: + logger.debug('Twisted reactor not running') + logger.debug("Done stopping all the things.") + + def quit(app): """ Stop the mainloop. @@ -34,9 +43,4 @@ def quit(app): :param app: the main qt QApplication instance. :type app: QtCore.QApplication """ - from twisted.internet import reactor - logger.debug('Stopping twisted reactor') - try: - reactor.callLater(0, reactor.stop) - except error.ReactorNotRunning: - logger.debug('Reactor not running') + reactor.callLater(0, stop) diff --git a/src/leap/bitmask/gui/ui/advanced_key_management.ui b/src/leap/bitmask/gui/ui/advanced_key_management.ui index 1112670f..3b567347 100644 --- a/src/leap/bitmask/gui/ui/advanced_key_management.ui +++ b/src/leap/bitmask/gui/ui/advanced_key_management.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>AdvancedKeyManagement</class> - <widget class="QWidget" name="AdvancedKeyManagement"> + <widget class="QDialog" name="AdvancedKeyManagement"> <property name="geometry"> <rect> <x>0</x> diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui index f5725d5a..216eca9e 100644 --- a/src/leap/bitmask/gui/ui/login.ui +++ b/src/leap/bitmask/gui/ui/login.ui @@ -215,7 +215,7 @@ <number>0</number> </property> <property name="bottomMargin"> - <number>0</number> + <number>12</number> </property> <item row="0" column="0" colspan="2"> <widget class="QLabel" name="lblUser"> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index e2c1a16e..020a58e2 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -24,6 +24,7 @@ from functools import partial from PySide import QtCore, QtGui +from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.provider import get_provider_path @@ -145,8 +146,7 @@ class Wizard(QtGui.QWizard): @QtCore.Slot() def _wizard_finished(self): """ - SLOT - TRIGGER: + TRIGGERS: self.finished This method is called when the wizard is accepted or rejected. @@ -210,15 +210,19 @@ class Wizard(QtGui.QWizard): def get_services(self): return self._selected_services - @QtCore.Slot() + @QtCore.Slot(unicode) def _enable_check(self, reset=True): """ - SLOT - TRIGGER: + TRIGGERS: self.ui.lnProvider.textChanged Enables/disables the 'check' button in the SELECT_PROVIDER_PAGE depending on the lnProvider content. + + :param reset: this contains the text of the line edit, and when is + called directly defines whether we want to reset the + checks. + :type reset: unicode or bool """ enabled = len(self.ui.lnProvider.text()) != 0 enabled = enabled or self.ui.rbExistingProvider.isChecked() @@ -282,11 +286,11 @@ class Wizard(QtGui.QWizard): # register button self.ui.btnRegister.setVisible(visible) + @QtCore.Slot() def _registration_finished(self): """ - SLOT TRIGGERS: - self._backend.signaler.srp_registration_finished + self._backend.signaler.srp_registration_finished The registration has finished successfully, so we do some final steps. """ @@ -308,11 +312,11 @@ class Wizard(QtGui.QWizard): self.page(self.REGISTER_USER_PAGE).set_completed() self.button(QtGui.QWizard.BackButton).setEnabled(False) + @QtCore.Slot() def _registration_failed(self): """ - SLOT TRIGGERS: - self._backend.signaler.srp_registration_failed + self._backend.signaler.srp_registration_failed The registration has failed, so we report the problem. """ @@ -322,11 +326,11 @@ class Wizard(QtGui.QWizard): self._set_register_status(error_msg, error=True) self.ui.btnRegister.setEnabled(True) + @QtCore.Slot() def _registration_taken(self): """ - SLOT TRIGGERS: - self._backend.signaler.srp_registration_taken + self._backend.signaler.srp_registration_taken The requested username is taken, warn the user about that. """ @@ -358,7 +362,8 @@ class Wizard(QtGui.QWizard): self.ui.lblProviderSelectStatus.setText("") self._domain = None self.button(QtGui.QWizard.NextButton).setEnabled(False) - self.page(self.SELECT_PROVIDER_PAGE).set_completed(False) + self.page(self.SELECT_PROVIDER_PAGE).set_completed( + flags.SKIP_WIZARD_CHECKS) def _reset_provider_setup(self): """ @@ -368,12 +373,12 @@ class Wizard(QtGui.QWizard): self.ui.lblCheckCaFpr.setPixmap(None) self.ui.lblCheckApiCert.setPixmap(None) + @QtCore.Slot() def _check_provider(self): """ - SLOT TRIGGERS: - self.ui.btnCheck.clicked - self.ui.lnProvider.returnPressed + self.ui.btnCheck.clicked + self.ui.lnProvider.returnPressed Starts the checks for a given provider """ @@ -390,17 +395,23 @@ class Wizard(QtGui.QWizard): self.ui.grpCheckProvider.setVisible(True) self.ui.btnCheck.setEnabled(False) - self.ui.lnProvider.setEnabled(False) + + # Disable provider widget + if self.ui.rbNewProvider.isChecked(): + self.ui.lnProvider.setEnabled(False) + else: + self.ui.cbProviders.setEnabled(False) + self.button(QtGui.QWizard.BackButton).clearFocus() self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) self._provider_select_defer = self._backend.\ setup_provider(self._domain) + @QtCore.Slot(bool) def _skip_provider_checks(self, skip): """ - SLOT - Triggered: + TRIGGERS: self.ui.rbExistingProvider.toggled Allows the user to move to the next page without make any checks, @@ -441,10 +452,11 @@ class Wizard(QtGui.QWizard): label.setPixmap(self.ERROR_ICON) logger.error(error) + @QtCore.Slot(dict) def _name_resolution(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_name_resolution + TRIGGERS: + self._backend.signaler.prov_name_resolution Sets the status for the name resolution check """ @@ -460,10 +472,11 @@ class Wizard(QtGui.QWizard): self.ui.btnCheck.setEnabled(not passed) self.ui.lnProvider.setEnabled(not passed) + @QtCore.Slot(dict) def _https_connection(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_https_connection + TRIGGERS: + self._backend.signaler.prov_https_connection Sets the status for the https connection check """ @@ -479,10 +492,11 @@ class Wizard(QtGui.QWizard): self.ui.btnCheck.setEnabled(not passed) self.ui.lnProvider.setEnabled(not passed) + @QtCore.Slot(dict) def _download_provider_info(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_download_provider_info + TRIGGERS: + self._backend.signaler.prov_download_provider_info Sets the status for the provider information download check. Since this check is the last of this set, it also @@ -506,12 +520,18 @@ class Wizard(QtGui.QWizard): "</b></font>") self.ui.lblProviderSelectStatus.setText(status) self.ui.btnCheck.setEnabled(True) - self.ui.lnProvider.setEnabled(True) + # Enable provider widget + if self.ui.rbNewProvider.isChecked(): + self.ui.lnProvider.setEnabled(True) + else: + self.ui.cbProviders.setEnabled(True) + + @QtCore.Slot(dict) def _download_ca_cert(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_download_ca_cert + TRIGGERS: + self._backend.signaler.prov_download_ca_cert Sets the status for the download of the CA certificate check """ @@ -520,10 +540,11 @@ class Wizard(QtGui.QWizard): if passed: self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) + @QtCore.Slot(dict) def _check_ca_fingerprint(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_check_ca_fingerprint + TRIGGERS: + self._backend.signaler.prov_check_ca_fingerprint Sets the status for the CA fingerprint check """ @@ -532,10 +553,11 @@ class Wizard(QtGui.QWizard): if passed: self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) + @QtCore.Slot(dict) def _check_api_certificate(self, data): """ - SLOT - TRIGGER: self._backend.signaler.prov_check_api_certificate + TRIGGERS: + self._backend.signaler.prov_check_api_certificate Sets the status for the API certificate check. Also finishes the provider bootstrapper thread since it's not needed anymore @@ -545,10 +567,12 @@ class Wizard(QtGui.QWizard): True, self.SETUP_PROVIDER_PAGE) self._provider_setup_ok = True + @QtCore.Slot(str, int) def _service_selection_changed(self, service, state): """ - SLOT - TRIGGER: service_checkbox.stateChanged + TRIGGERS: + service_checkbox.stateChanged + Adds the service to the state if the state is checked, removes it otherwise @@ -593,12 +617,16 @@ class Wizard(QtGui.QWizard): self.tr("Something went wrong while trying to " "load service %s" % (service,))) + @QtCore.Slot(int) def _current_id_changed(self, pageId): """ - SLOT - TRIGGER: self.currentIdChanged + TRIGGERS: + self.currentIdChanged Prepares the pages when they appear + + :param pageId: the new current page id. + :type pageId: int """ if pageId == self.SELECT_PROVIDER_PAGE: self._clear_register_widgets() diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index d93efbc6..f2710c58 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -366,15 +366,8 @@ def _linux_install_missing_scripts(badexec, notfound): fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") polfd, pol_tempfile = tempfile.mkstemp(prefix="leap_installer-") try: - path = launcher.OPENVPN_BIN_PATH - policy_contents = privilege_policies.get_policy_contents(path) - - with os.fdopen(polfd, 'w') as f: - f.write(policy_contents) - pkexec = first(launcher.maybe_pkexec()) - scriptlines = launcher.cmd_for_missing_scripts(installer_path, - pol_tempfile) + scriptlines = launcher.cmd_for_missing_scripts(installer_path) with os.fdopen(fd, 'w') as f: f.write(scriptlines) diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 2a519206..6cdfe4f4 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -88,6 +88,8 @@ class ProviderBootstrapper(AbstractBootstrapper): self._domain = None self._provider_config = None self._download_if_needed = False + if signaler is not None: + self._cancel_signal = signaler.PROV_CANCELLED_SETUP @property def verify(self): diff --git a/src/leap/bitmask/services/abstractbootstrapper.py b/src/leap/bitmask/services/abstractbootstrapper.py index fc6bd3e9..77929b75 100644 --- a/src/leap/bitmask/services/abstractbootstrapper.py +++ b/src/leap/bitmask/services/abstractbootstrapper.py @@ -78,6 +78,7 @@ class AbstractBootstrapper(QtCore.QObject): self._signal_to_emit = None self._err_msg = None self._signaler = signaler + self._cancel_signal = None def _gui_errback(self, failure): """ @@ -95,7 +96,8 @@ class AbstractBootstrapper(QtCore.QObject): if failure.check(CancelledError): logger.debug("Defer cancelled.") failure.trap(Exception) - self._signaler.signal(self._signaler.PROV_CANCELLED_SETUP) + if self._signaler is not None and self._cancel_signal is not None: + self._signaler.signal(self._cancel_signal) return if self._signal_to_emit: diff --git a/src/leap/bitmask/services/eip/eipbootstrapper.py b/src/leap/bitmask/services/eip/eipbootstrapper.py index 5a238a1c..c77977ce 100644 --- a/src/leap/bitmask/services/eip/eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/eipbootstrapper.py @@ -20,8 +20,6 @@ EIP bootstrapping import logging import os -from PySide import QtCore - from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.certs import download_client_cert from leap.bitmask.services import download_service_config @@ -41,17 +39,21 @@ class EIPBootstrapper(AbstractBootstrapper): If a check fails, the subsequent checks are not executed """ - # All dicts returned are of the form - # {"passed": bool, "error": str} - download_config = QtCore.Signal(dict) - download_client_certificate = QtCore.Signal(dict) + def __init__(self, signaler=None): + """ + Constructor for the EIP bootstrapper object - def __init__(self): - AbstractBootstrapper.__init__(self) + :param signaler: Signaler object used to receive notifications + from the backend + :type signaler: Signaler + """ + AbstractBootstrapper.__init__(self, signaler) self._provider_config = None self._eip_config = None self._download_if_needed = False + if signaler is not None: + self._cancel_signal = signaler.EIP_CANCELLED_SETUP def _download_config(self, *args): """ @@ -114,9 +116,9 @@ class EIPBootstrapper(AbstractBootstrapper): self._download_if_needed = download_if_needed cb_chain = [ - (self._download_config, self.download_config), + (self._download_config, self._signaler.EIP_CONFIG_READY), (self._download_client_certificates, - self.download_client_certificate) + self._signaler.EIP_CLIENT_CERTIFICATE_READY) ] return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py index d24e7ae7..1f0813e0 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -25,7 +25,6 @@ import sys import time from leap.bitmask.config import flags -from leap.bitmask.util import privilege_policies from leap.bitmask.util.privilege_policies import LinuxPolicyChecker from leap.common.files import which from leap.bitmask.services.eip.vpnlauncher import VPNLauncher @@ -36,6 +35,8 @@ from leap.bitmask.util import first logger = logging.getLogger(__name__) +COM = commands + class EIPNoPolkitAuthAgentAvailable(VPNLauncherException): pass @@ -64,9 +65,10 @@ def _is_auth_agent_running(): """ # the [x] thing is to avoid grep match itself polkit_options = [ - 'ps aux | grep polkit-[g]nome-authentication-agent-1', - 'ps aux | grep polkit-[k]de-authentication-agent-1', - 'ps aux | grep [l]xpolkit' + 'ps aux | grep "polkit-[g]nome-authentication-agent-1"', + 'ps aux | grep "polkit-[k]de-authentication-agent-1"', + 'ps aux | grep "polkit-[m]ate-authentication-agent-1"', + 'ps aux | grep "[l]xpolkit"' ] is_running = [commands.getoutput(cmd) for cmd in polkit_options] return any(is_running) @@ -84,35 +86,39 @@ def _try_to_launch_agent(): # will do "sh -c 'foo'", so if we do not quoute it we'll end # up with a invocation to the python interpreter. And that # is bad. + logger.debug("Trying to launch polkit agent") subprocess.call(["python -m leap.bitmask.util.polkit_agent"], shell=True, env=env) except Exception as exc: logger.exception(exc) +SYSTEM_CONFIG = "/etc/leap" +leapfile = lambda f: "%s/%s" % (SYSTEM_CONFIG, f) + + class LinuxVPNLauncher(VPNLauncher): PKEXEC_BIN = 'pkexec' - OPENVPN_BIN = 'openvpn' - OPENVPN_BIN_PATH = os.path.join( - get_path_prefix(), "..", "apps", "eip", OPENVPN_BIN) - - SYSTEM_CONFIG = "/etc/leap" - UP_DOWN_FILE = "resolv-update" - UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) + BITMASK_ROOT = "/usr/sbin/bitmask-root" # We assume this is there by our openvpn dependency, and # we will put it there on the bundle too. - # TODO adapt to the bundle path. - OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" - OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" - OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( - OPENVPN_DOWN_ROOT_BASE, - OPENVPN_DOWN_ROOT_FILE) - - UP_SCRIPT = DOWN_SCRIPT = UP_DOWN_PATH - UPDOWN_FILES = (UP_DOWN_PATH,) + if flags.STANDALONE: + OPENVPN_BIN_PATH = "/usr/sbin/leap-openvpn" + else: + OPENVPN_BIN_PATH = "/usr/sbin/openvpn" + POLKIT_PATH = LinuxPolicyChecker.get_polkit_path() - OTHER_FILES = (POLKIT_PATH, ) + + if flags.STANDALONE: + RESOLVCONF_BIN_PATH = "/usr/local/sbin/leap-resolvconf" + else: + # this only will work with debian/ubuntu distros. + RESOLVCONF_BIN_PATH = "/sbin/resolvconf" + + # XXX openvpn binary TOO + OTHER_FILES = (POLKIT_PATH, BITMASK_ROOT, OPENVPN_BIN_PATH, + RESOLVCONF_BIN_PATH) @classmethod def maybe_pkexec(kls): @@ -130,7 +136,7 @@ class LinuxVPNLauncher(VPNLauncher): if _is_pkexec_in_system(): if not _is_auth_agent_running(): _try_to_launch_agent() - time.sleep(0.5) + time.sleep(2) if _is_auth_agent_running(): pkexec_possibilities = which(kls.PKEXEC_BIN) leap_assert(len(pkexec_possibilities) > 0, @@ -145,28 +151,6 @@ class LinuxVPNLauncher(VPNLauncher): raise EIPNoPkexecAvailable() @classmethod - def missing_other_files(kls): - """ - 'Extend' the VPNLauncher's missing_other_files to check if the polkit - files is outdated, in the case of an standalone bundle. - If the polkit file that is in OTHER_FILES exists but is not up to date, - it is added to the missing list. - - :returns: a list of missing files - :rtype: list of str - """ - # we use `super` in order to send the class to use - missing = super(LinuxVPNLauncher, kls).missing_other_files() - - if flags.STANDALONE: - polkit_file = LinuxPolicyChecker.get_polkit_path() - if polkit_file not in missing: - if privilege_policies.is_policy_outdated(kls.OPENVPN_BIN_PATH): - missing.append(polkit_file) - - return missing - - @classmethod def get_vpn_command(kls, eipconfig, providerconfig, socket_host, socket_port="unix", openvpn_verb=1): """ @@ -197,6 +181,10 @@ class LinuxVPNLauncher(VPNLauncher): command = super(LinuxVPNLauncher, kls).get_vpn_command( eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) + command.insert(0, kls.BITMASK_ROOT) + command.insert(1, "openvpn") + command.insert(2, "start") + pkexec = kls.maybe_pkexec() if pkexec: command.insert(0, first(pkexec)) @@ -204,26 +192,44 @@ class LinuxVPNLauncher(VPNLauncher): return command @classmethod - def cmd_for_missing_scripts(kls, frompath, pol_file): + def cmd_for_missing_scripts(kls, frompath): """ Returns a sh script that can copy the missing files. - :param frompath: The path where the up/down scripts live + :param frompath: The path where the helper files live :type frompath: str - :param pol_file: The path where the dynamically generated - policy file lives - :type pol_file: str :rtype: str """ - to = kls.SYSTEM_CONFIG + # no system config for now + # sys_config = kls.SYSTEM_CONFIG + (polkit_file, openvpn_bin_file, + bitmask_root_file, resolvconf_bin_file) = map( + lambda p: os.path.split(p)[-1], + (kls.POLKIT_PATH, kls.OPENVPN_BIN_PATH, + kls.BITMASK_ROOT, kls.RESOLVCONF_BIN_PATH)) cmd = '#!/bin/sh\n' - cmd += 'mkdir -p "%s"\n' % (to, ) - cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to) - cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH) + cmd += 'mkdir -p /usr/local/sbin\n' + + cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, polkit_file), + kls.POLKIT_PATH) cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, ) + cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, bitmask_root_file), + kls.BITMASK_ROOT) + cmd += 'chmod 744 "%s"\n' % (kls.BITMASK_ROOT, ) + + if flags.STANDALONE: + cmd += 'cp "%s" "%s"\n' % ( + os.path.join(frompath, openvpn_bin_file), + kls.OPENVPN_BIN_PATH) + cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, ) + + cmd += 'cp "%s" "%s"\n' % ( + os.path.join(frompath, resolvconf_bin_file), + kls.RESOLVCONF_BIN_PATH) + cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, ) return cmd @classmethod diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index 99cae7f9..dcb48e8a 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -25,14 +25,12 @@ import stat from abc import ABCMeta, abstractmethod from functools import partial -from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector -from leap.bitmask.util import first -from leap.bitmask.util import get_path_prefix from leap.common.check import leap_assert, leap_assert_type -from leap.common.files import which + logger = logging.getLogger(__name__) @@ -107,10 +105,43 @@ class VPNLauncher(object): @classmethod @abstractmethod + def get_gateways(kls, eipconfig, providerconfig): + """ + Return the selected gateways for a given provider, looking at the EIP + config file. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :rtype: list + """ + gateways = [] + leap_settings = LeapSettings() + domain = providerconfig.get_domain() + gateway_conf = leap_settings.get_selected_gateway(domain) + + if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: + gateway_selector = VPNGatewaySelector(eipconfig) + gateways = gateway_selector.get_gateways() + else: + gateways = [gateway_conf] + + if not gateways: + logger.error('No gateway was found!') + raise VPNLauncherException('No gateway was found!') + + logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) + return gateways + + @classmethod + @abstractmethod def get_vpn_command(kls, eipconfig, providerconfig, socket_host, socket_port, openvpn_verb=1): """ - Returns the platform dependant vpn launching command + Return the platform-dependant vpn command for launching openvpn. Might raise: OpenVPNNotFoundException, @@ -134,16 +165,19 @@ class VPNLauncher(object): leap_assert_type(eipconfig, EIPConfig) leap_assert_type(providerconfig, ProviderConfig) - kwargs = {} - if flags.STANDALONE: - kwargs['path_extension'] = os.path.join( - get_path_prefix(), "..", "apps", "eip") - - openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) - if len(openvpn_possibilities) == 0: + # XXX this still has to be changed on osx and windows accordingly + #kwargs = {} + #openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) + #if not openvpn_possibilities: + #raise OpenVPNNotFoundException() + #openvpn = first(openvpn_possibilities) + # ----------------------------------------- + if not os.path.isfile(kls.OPENVPN_BIN_PATH): + logger.warning("Could not find openvpn bin in path %s" % ( + kls.OPENVPN_BIN_PATH)) raise OpenVPNNotFoundException() - openvpn = first(openvpn_possibilities) + openvpn = kls.OPENVPN_BIN_PATH args = [] args += [ @@ -154,22 +188,7 @@ class VPNLauncher(object): if openvpn_verb is not None: args += ['--verb', '%d' % (openvpn_verb,)] - gateways = [] - leap_settings = LeapSettings() - domain = providerconfig.get_domain() - gateway_conf = leap_settings.get_selected_gateway(domain) - - if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() - else: - gateways = [gateway_conf] - - if not gateways: - logger.error('No gateway was found!') - raise VPNLauncherException('No gateway was found!') - - logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) + gateways = kls.get_gateways(eipconfig, providerconfig) for gw in gateways: args += ['--remote', gw, '1194', 'udp'] @@ -177,11 +196,6 @@ class VPNLauncher(object): args += [ '--client', '--dev', 'tun', - ############################################################## - # persist-tun makes ping-restart fail because it leaves a - # broken routing table - ############################################################## - # '--persist-tun', '--persist-key', '--tls-client', '--remote-cert-tls', @@ -194,15 +208,6 @@ class VPNLauncher(object): user = getpass.getuser() - ############################################################## - # The down-root plugin fails in some situations, so we don't - # drop privs for the time being - ############################################################## - # args += [ - # '--user', user, - # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name - # ] - if socket_port == "unix": # that's always the case for linux args += [ '--management-client-user', user @@ -226,20 +231,6 @@ class VPNLauncher(object): '--down', '\"%s\"' % (kls.DOWN_SCRIPT,) ] - ########################################################### - # For the time being we are disabling the usage of the - # down-root plugin, because it doesn't quite work as - # expected (i.e. it doesn't run route -del as root - # when finishing, so it fails to properly - # restart/quit) - ########################################################### - # if _has_updown_scripts(kls.OPENVPN_DOWN_PLUGIN): - # args += [ - # '--plugin', kls.OPENVPN_DOWN_ROOT, - # '\'%s\'' % kls.DOWN_SCRIPT # for OSX - # '\'script_type=down %s\'' % kls.DOWN_SCRIPT # for Linux - # ] - args += [ '--cert', eipconfig.get_client_cert_path(providerconfig), '--key', eipconfig.get_client_cert_path(providerconfig), @@ -271,13 +262,18 @@ class VPNLauncher(object): :rtype: list """ - leap_assert(kls.UPDOWN_FILES is not None, - "Need to define UPDOWN_FILES for this particular " - "launcher before calling this method") - file_exist = partial(_has_updown_scripts, warn=False) - zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) - missing = filter(lambda (path, exists): exists is False, zipped) - return [path for path, exists in missing] + # FIXME + # XXX remove method when we ditch UPDOWN in osx and win too + if IS_LINUX: + return [] + else: + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define UPDOWN_FILES for this particular " + "launcher before calling this method") + file_exist = partial(_has_updown_scripts, warn=False) + zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] @classmethod def missing_other_files(kls): diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 5c100036..1559ea8b 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -21,6 +21,7 @@ import logging import os import shutil import socket +import subprocess import sys from itertools import chain, repeat @@ -33,14 +34,14 @@ except ImportError: # psutil >= 2.0.0 from psutil import AccessDenied as psutil_AccessDenied -from PySide import QtCore - +from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.services.eip import get_vpn_launcher +from leap.bitmask.services.eip import linuxvpnlauncher from leap.bitmask.services.eip.eipconfig import EIPConfig from leap.bitmask.services.eip.udstelnet import UDSTelnet from leap.bitmask.util import first -from leap.bitmask.platform_init import IS_MAC +from leap.bitmask.platform_init import IS_MAC, IS_LINUX from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -52,28 +53,6 @@ from twisted.internet import error as internet_error from twisted.internet.task import LoopingCall -class VPNSignals(QtCore.QObject): - """ - These are the signals that we use to let the UI know - about the events we are polling. - They are instantiated in the VPN object and passed along - till the VPNProcess. - """ - # signals for the process - state_changed = QtCore.Signal(dict) - status_changed = QtCore.Signal(dict) - process_finished = QtCore.Signal(int) - - # signals that come from parsing - # openvpn output - network_unreachable = QtCore.Signal() - process_restart_tls = QtCore.Signal() - process_restart_ping = QtCore.Signal() - - def __init__(self): - QtCore.QObject.__init__(self) - - class VPNObserver(object): """ A class containing different patterns in the openvpn output that @@ -90,19 +69,13 @@ class VPNObserver(object): 'PROCESS_RESTART_TLS': ( "SIGUSR1[soft,tls-error]",), 'PROCESS_RESTART_PING': ( - "SIGUSR1[soft,ping-restart]",), + "SIGTERM[soft,ping-restart]",), 'INITIALIZATION_COMPLETED': ( "Initialization Sequence Completed",), } - def __init__(self, qtsigs): - """ - Initializer. Keeps a reference to the passed qtsigs object - :param qtsigs: an object containing the different qt signals to - be used to communicate with different parts of - the application (the EIP state machine, for instance). - """ - self._qtsigs = qtsigs + def __init__(self, signaler=None): + self._signaler = signaler def watch(self, line): """ @@ -123,27 +96,29 @@ class VPNObserver(object): return sig = self._get_signal(event) - if sig: - sig.emit() + if sig is not None: + self._signaler.signal(sig) return else: - logger.debug( - 'We got %s event from openvpn output but we ' - 'could not find a matching signal for it.' - % event) + logger.debug('We got %s event from openvpn output but we could ' + 'not find a matching signal for it.' % event) def _get_signal(self, event): """ Tries to get the matching signal from the eip signals objects based on the name of the passed event (in lowercase) - :param event: the name of the event that we want to get a signal - for + :param event: the name of the event that we want to get a signal for :type event: str - :returns: a QtSignal, or None - :rtype: QtSignal or None + :returns: a Signaler signal or None + :rtype: str or None """ - return getattr(self._qtsigs, event.lower(), None) + signals = { + "network_unreachable": self._signaler.EIP_NETWORK_UNREACHABLE, + "process_restart_tls": self._signaler.EIP_PROCESS_RESTART_TLS, + "process_restart_ping": self._signaler.EIP_PROCESS_RESTART_PING, + } + return signals.get(event.lower()) class OpenVPNAlreadyRunning(Exception): @@ -181,14 +156,11 @@ class VPN(object): self._vpnproc = None self._pollers = [] self._reactor = reactor - self._qtsigs = VPNSignals() - # XXX should get it from config.flags - self._openvpn_verb = kwargs.get(self.OPENVPN_VERB, None) + self._signaler = kwargs['signaler'] + self._openvpn_verb = flags.OPENVPN_VERBOSITY - @property - def qtsigs(self): - return self._qtsigs + self._user_stopped = False def start(self, *args, **kwargs): """ @@ -200,19 +172,28 @@ class VPN(object): :param kwargs: kwargs to be passed to the VPNProcess :type kwargs: dict """ + logger.debug('VPN: start') + self._user_stopped = False self._stop_pollers() - kwargs['qtsigs'] = self.qtsigs kwargs['openvpn_verb'] = self._openvpn_verb + kwargs['signaler'] = self._signaler # start the main vpn subprocess vpnproc = VPNProcess(*args, **kwargs) - #qtsigs=self.qtsigs, - #openvpn_verb=self._openvpn_verb) if vpnproc.get_openvpn_process(): logger.info("Another vpn process is running. Will try to stop it.") vpnproc.stop_if_already_running() + # we try to bring the firewall up + if IS_LINUX: + gateways = vpnproc.getGateways() + firewall_up = self._launch_firewall(gateways) + if not firewall_up: + logger.error("Could not bring firewall up, " + "aborting openvpn launch.") + return + cmd = vpnproc.getCommand() env = os.environ for key, val in vpnproc.vpn_env.items(): @@ -230,9 +211,37 @@ class VPN(object): self._pollers.extend(poll_list) self._start_pollers() + def _launch_firewall(self, gateways): + """ + Launch the firewall using the privileged wrapper. + + :param gateways: + :type gateways: list + + :returns: True if the exitcode of calling the root helper in a + subprocess is 0. + :rtype: bool + """ + # XXX could check for wrapper existence, check it's root owned etc. + # XXX could check that the iptables rules are in place. + + BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT + exitCode = subprocess.call(["pkexec", + BM_ROOT, "firewall", "start"] + gateways) + return True if exitCode is 0 else False + + def _tear_down_firewall(self): + """ + Tear the firewall down using the privileged wrapper. + """ + BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT + exitCode = subprocess.call(["pkexec", + BM_ROOT, "firewall", "stop"]) + return True if exitCode is 0 else False + def _kill_if_left_alive(self, tries=0): """ - Check if the process is still alive, and sends a + Check if the process is still alive, and send a SIGKILL after a timeout period. :param tries: counter of tries, used in recursion @@ -242,6 +251,15 @@ class VPN(object): while tries < self.TERMINATE_MAXTRIES: if self._vpnproc.transport.pid is None: logger.debug("Process has been happily terminated.") + + # we try to tear the firewall down + if IS_LINUX and self._user_stopped: + firewall_down = self._tear_down_firewall() + if firewall_down: + logger.debug("Firewall down") + else: + logger.warning("Could not tear firewall down") + return else: logger.debug("Process did not die, waiting...") @@ -262,8 +280,11 @@ class VPN(object): Sends a kill signal to the process. """ self._stop_pollers() - self._vpnproc.aborted = True - self._vpnproc.killProcess() + if self._vpnproc is None: + logger.debug("There's no vpn process running to kill.") + else: + self._vpnproc.aborted = True + self._vpnproc.killProcess() def terminate(self, shutdown=False): """ @@ -275,6 +296,10 @@ class VPN(object): from twisted.internet import reactor self._stop_pollers() + # We assume that the only valid shutodowns are initiated + # by an user action. + self._user_stopped = shutdown + # First we try to be polite and send a SIGTERM... if self._vpnproc: self._sentterm = True @@ -282,12 +307,17 @@ class VPN(object): # ...but we also trigger a countdown to be unpolite # if strictly needed. - - # XXX Watch out! This will fail NOW since we are running - # openvpn as root as a workaround for some connection issues. reactor.callLater( self.TERMINATE_WAIT, self._kill_if_left_alive) + if shutdown: + if IS_LINUX and self._user_stopped: + firewall_down = self._tear_down_firewall() + if firewall_down: + logger.debug("Firewall down") + else: + logger.warning("Could not tear firewall down") + def _start_pollers(self): """ Iterate through the registered observers @@ -328,37 +358,21 @@ class VPNManager(object): POLL_TIME = 2.5 if IS_MAC else 1.0 CONNECTION_RETRY_TIME = 1 - TS_KEY = "ts" - STATUS_STEP_KEY = "status_step" - OK_KEY = "ok" - IP_KEY = "ip" - REMOTE_KEY = "remote" - - TUNTAP_READ_KEY = "tun_tap_read" - TUNTAP_WRITE_KEY = "tun_tap_write" - TCPUDP_READ_KEY = "tcp_udp_read" - TCPUDP_WRITE_KEY = "tcp_udp_write" - AUTH_READ_KEY = "auth_read" - - def __init__(self, qtsigs=None): + def __init__(self, signaler=None): """ Initializes the VPNManager. - :param qtsigs: a QObject containing the Qt signals used by the UI - to give feedback about state changes. - :type qtsigs: QObject + :param signaler: Signaler object used to send notifications to the + backend + :type signaler: backend.Signaler """ from twisted.internet import reactor self._reactor = reactor self._tn = None - self._qtsigs = qtsigs + self._signaler = signaler self._aborted = False @property - def qtsigs(self): - return self._qtsigs - - @property def aborted(self): return self._aborted @@ -552,17 +566,10 @@ class VPNManager(object): continue ts, status_step, ok, ip, remote = parts - state_dict = { - self.TS_KEY: ts, - self.STATUS_STEP_KEY: status_step, - self.OK_KEY: ok, - self.IP_KEY: ip, - self.REMOTE_KEY: remote - } - - if state_dict != self._last_state: - self.qtsigs.state_changed.emit(state_dict) - self._last_state = state_dict + state = status_step + if state != self._last_state: + self._signaler.signal(self._signaler.EIP_STATE_CHANGED, state) + self._last_state = state def _parse_status_and_notify(self, output): """ @@ -575,9 +582,7 @@ class VPNManager(object): """ tun_tap_read = "" tun_tap_write = "" - tcp_udp_read = "" - tcp_udp_write = "" - auth_read = "" + for line in output: stripped = line.strip() if stripped.endswith("STATISTICS") or stripped == "END": @@ -585,28 +590,24 @@ class VPNManager(object): parts = stripped.split(",") if len(parts) < 2: continue - if parts[0].strip() == "TUN/TAP read bytes": - tun_tap_read = parts[1] - elif parts[0].strip() == "TUN/TAP write bytes": - tun_tap_write = parts[1] - elif parts[0].strip() == "TCP/UDP read bytes": - tcp_udp_read = parts[1] - elif parts[0].strip() == "TCP/UDP write bytes": - tcp_udp_write = parts[1] - elif parts[0].strip() == "Auth read bytes": - auth_read = parts[1] - - status_dict = { - self.TUNTAP_READ_KEY: tun_tap_read, - self.TUNTAP_WRITE_KEY: tun_tap_write, - self.TCPUDP_READ_KEY: tcp_udp_read, - self.TCPUDP_WRITE_KEY: tcp_udp_write, - self.AUTH_READ_KEY: auth_read - } - if status_dict != self._last_status: - self.qtsigs.status_changed.emit(status_dict) - self._last_status = status_dict + text, value = parts + # text can be: + # "TUN/TAP read bytes" + # "TUN/TAP write bytes" + # "TCP/UDP read bytes" + # "TCP/UDP write bytes" + # "Auth read bytes" + + if text == "TUN/TAP read bytes": + tun_tap_read = value # download + elif text == "TUN/TAP write bytes": + tun_tap_write = value # upload + + status = (tun_tap_read, tun_tap_write) + if status != self._last_status: + self._signaler.signal(self._signaler.EIP_STATUS_CHANGED, status) + self._last_status = status def get_state(self): """ @@ -754,7 +755,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): """ def __init__(self, eipconfig, providerconfig, socket_host, socket_port, - qtsigs, openvpn_verb): + signaler, openvpn_verb): """ :param eipconfig: eip configuration object :type eipconfig: EIPConfig @@ -769,18 +770,17 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): socket, or port otherwise :type socket_port: str - :param qtsigs: a QObject containing the Qt signals used to notify the - UI. - :type qtsigs: QObject + :param signaler: Signaler object used to receive notifications to the + backend + :type signaler: backend.Signaler :param openvpn_verb: the desired level of verbosity in the openvpn invocation :type openvpn_verb: int """ - VPNManager.__init__(self, qtsigs=qtsigs) + VPNManager.__init__(self, signaler=signaler) leap_assert_type(eipconfig, EIPConfig) leap_assert_type(providerconfig, ProviderConfig) - leap_assert_type(qtsigs, QtCore.QObject) #leap_assert(not self.isRunning(), "Starting process more than once!") @@ -799,7 +799,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): # the parameter around. self._openvpn_verb = openvpn_verb - self._vpn_observer = VPNObserver(qtsigs) + self._vpn_observer = VPNObserver(signaler) # processProtocol methods @@ -835,7 +835,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): exit_code = reason.value.exitCode if isinstance(exit_code, int): logger.debug("processExited, status %d" % (exit_code,)) - self.qtsigs.process_finished.emit(exit_code) + self._signaler.signal(self._signaler.EIP_PROCESS_FINISHED, exit_code) self._alive = False def processEnded(self, reason): @@ -889,9 +889,20 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): if not isinstance(c, str): command[i] = c.encode(encoding) - logger.debug("Running VPN with command: {0}".format(command)) + logger.debug("Running VPN with command: ") + logger.debug("{0}".format(" ".join(command))) return command + def getGateways(self): + """ + Get the gateways from the appropiate launcher. + + :rtype: list + """ + gateways = self._launcher.get_gateways( + self._eipconfig, self._providerconfig) + return gateways + # shutdown def killProcess(self): diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index 79f324dc..1766a39d 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -18,22 +18,18 @@ Mail Services Conductor """ import logging -import os -from PySide import QtCore from zope.proxy import sameProxiedObjects from leap.bitmask.gui import statemachines -from leap.bitmask.services.mail import imap from leap.bitmask.services.mail import connection as mail_connection +from leap.bitmask.services.mail import imap from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper from leap.bitmask.services.mail.smtpconfig import SMTPConfig -from leap.bitmask.util import is_file from leap.common.check import leap_assert - -from leap.common.events import register as leap_register from leap.common.events import events_pb2 as leap_events +from leap.common.events import register as leap_register logger = logging.getLogger(__name__) @@ -167,10 +163,6 @@ class IMAPControl(object): class SMTPControl(object): - - PORT_KEY = "port" - IP_KEY = "ip_address" - def __init__(self): """ Initializes smtp variables. @@ -178,12 +170,8 @@ class SMTPControl(object): self.smtp_config = SMTPConfig() self.smtp_connection = None self.smtp_machine = None - self._smtp_service = None - self._smtp_port = None self.smtp_bootstrapper = SMTPBootstrapper() - self.smtp_bootstrapper.download_config.connect( - self.smtp_bootstrapped_stage) leap_register(signal=leap_events.SMTP_SERVICE_STARTED, callback=self._handle_smtp_events, @@ -200,101 +188,27 @@ class SMTPControl(object): """ self.smtp_connection = smtp_connection - def start_smtp_service(self, host, port, cert): + def start_smtp_service(self, provider_config, download_if_needed=False): """ - Starts the smtp service. + Starts the SMTP service. - :param host: the hostname of the remove SMTP server. - :type host: str - :param port: the port of the remote SMTP server - :type port: str - :param cert: the client certificate for authentication - :type cert: str + :param provider_config: Provider configuration + :type provider_config: ProviderConfig + :param download_if_needed: True if it should check for mtime + for the file + :type download_if_needed: bool """ - # TODO Make the encrypted_only configurable - # TODO pick local smtp port in a better way - # TODO remove hard-coded port and let leap.mail set - # the specific default. self.smtp_connection.qtsigs.connecting_signal.emit() - from leap.mail.smtp import setup_smtp_gateway - self._smtp_service, self._smtp_port = setup_smtp_gateway( - port=2013, - userid=self.userid, - keymanager=self._keymanager, - smtp_host=host, - smtp_port=port, - smtp_cert=cert, - smtp_key=cert, - encrypted_only=False) + self.smtp_bootstrapper.start_smtp_service( + provider_config, self.smtp_config, self._keymanager, + self.userid, download_if_needed) def stop_smtp_service(self): """ - Stops the smtp service (port and factory). + Stops the SMTP service. """ self.smtp_connection.qtsigs.disconnecting_signal.emit() - # TODO We should homogenize both services. - if self._smtp_service is not None: - logger.debug('Stopping smtp service.') - self._smtp_port.stopListening() - self._smtp_service.doStop() - - @QtCore.Slot() - def smtp_bootstrapped_stage(self, data): - """ - SLOT - TRIGGERS: - self.smtp_bootstrapper.download_config - - If there was a problem, displays it, otherwise it does nothing. - This is used for intermediate bootstrapping stages, in case - they fail. - - :param data: result from the bootstrapping stage for Soledad - :type data: dict - """ - passed = data[self.smtp_bootstrapper.PASSED_KEY] - if not passed: - logger.error(data[self.smtp_bootstrapper.ERROR_KEY]) - return - logger.debug("Done bootstrapping SMTP") - self.check_smtp_config() - - def check_smtp_config(self): - """ - Checks smtp config and tries to download smtp client cert if needed. - Currently called when smtp_bootstrapped_stage has successfuly finished. - """ - logger.debug("Checking SMTP config...") - leap_assert(self.smtp_bootstrapper._provider_config, - "smtp bootstrapper does not have a provider_config") - - provider_config = self.smtp_bootstrapper._provider_config - smtp_config = self.smtp_config - hosts = smtp_config.get_hosts() - # TODO handle more than one host and define how to choose - if len(hosts) > 0: - hostname = hosts.keys()[0] - logger.debug("Using hostname %s for SMTP" % (hostname,)) - host = hosts[hostname][self.IP_KEY].encode("utf-8") - port = hosts[hostname][self.PORT_KEY] - - client_cert = smtp_config.get_client_cert_path( - provider_config, - about_to_download=True) - - # XXX change this logic! - # check_config should be called from within start_service, - # and not the other way around. - if not is_file(client_cert): - self.smtp_bootstrapper._download_client_certificates() - if os.path.isfile(client_cert): - self.start_smtp_service(host, port, client_cert) - else: - logger.warning("Tried to download email client " - "certificate, but could not find any") - - else: - logger.warning("No smtp hosts configured") + self.smtp_bootstrapper.stop_smtp_service() # handle smtp events @@ -349,7 +263,7 @@ class MailConductor(IMAPControl, SMTPControl): :param keymanager: a transparent proxy that eventually will point to a Keymanager Instance. - :type soledad: zope.proxy.ProxyBase + :type keymanager: zope.proxy.ProxyBase """ IMAPControl.__init__(self) SMTPControl.__init__(self) @@ -407,4 +321,5 @@ class MailConductor(IMAPControl, SMTPControl): qtsigs.connecting_signal.connect(widget.mail_state_connecting) qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting) qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) - qtsigs.soledad_invalid_auth_token.connect(widget.soledad_invalid_auth_token) + qtsigs.soledad_invalid_auth_token.connect( + widget.soledad_invalid_auth_token) diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py index 032d6357..7ecf8134 100644 --- a/src/leap/bitmask/services/mail/smtpbootstrapper.py +++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py @@ -20,12 +20,13 @@ SMTP bootstrapping import logging import os -from PySide import QtCore - from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.certs import download_client_cert from leap.bitmask.services import download_service_config from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper +from leap.bitmask.services.mail.smtpconfig import SMTPConfig +from leap.bitmask.util import is_file + from leap.common import certs as leap_certs from leap.common.check import leap_assert, leap_assert_type from leap.common.files import check_and_fix_urw_only @@ -33,27 +34,33 @@ from leap.common.files import check_and_fix_urw_only logger = logging.getLogger(__name__) +class NoSMTPHosts(Exception): + """This is raised when there is no SMTP host to use.""" + + class SMTPBootstrapper(AbstractBootstrapper): """ SMTP init procedure """ - # All dicts returned are of the form - # {"passed": bool, "error": str} - download_config = QtCore.Signal(dict) + PORT_KEY = "port" + IP_KEY = "ip_address" def __init__(self): AbstractBootstrapper.__init__(self) self._provider_config = None self._smtp_config = None + self._userid = None self._download_if_needed = False - def _download_config(self, *args): + self._smtp_service = None + self._smtp_port = None + + def _download_config_and_cert(self): """ - Downloads the SMTP config for the given provider + Downloads the SMTP config and cert for the given provider. """ - leap_assert(self._provider_config, "We need a provider configuration!") @@ -66,63 +73,101 @@ class SMTPBootstrapper(AbstractBootstrapper): self._session, self._download_if_needed) - def _download_client_certificates(self, *args): - """ - Downloads the SMTP client certificate for the given provider + hosts = self._smtp_config.get_hosts() - We actually are downloading the certificate for the same uri as - for the EIP config, but we duplicate these bits to allow mail - service to be working in a provider that does not offer EIP. - """ - # TODO factor out with eipboostrapper.download_client_certificates - # TODO this shouldn't be a private method, it's called from - # mainwindow. - leap_assert(self._provider_config, "We need a provider configuration!") - leap_assert(self._smtp_config, "We need an smtp configuration!") + if len(hosts) == 0: + raise NoSMTPHosts() - logger.debug("Downloading SMTP client certificate for %s" % - (self._provider_config.get_domain(),)) + # TODO handle more than one host and define how to choose + hostname = hosts.keys()[0] + logger.debug("Using hostname %s for SMTP" % (hostname,)) - client_cert_path = self._smtp_config.\ - get_client_cert_path(self._provider_config, - about_to_download=True) + client_cert_path = self._smtp_config.get_client_cert_path( + self._provider_config, about_to_download=True) - # For re-download if something is wrong with the cert - self._download_if_needed = self._download_if_needed and \ - not leap_certs.should_redownload(client_cert_path) + if not is_file(client_cert_path): + # For re-download if something is wrong with the cert + self._download_if_needed = ( + self._download_if_needed and + not leap_certs.should_redownload(client_cert_path)) - if self._download_if_needed and \ - os.path.isfile(client_cert_path): - check_and_fix_urw_only(client_cert_path) - return + if self._download_if_needed and os.path.isfile(client_cert_path): + check_and_fix_urw_only(client_cert_path) + return - download_client_cert(self._provider_config, - client_cert_path, - self._session) + download_client_cert(self._provider_config, + client_cert_path, + self._session) - def run_smtp_setup_checks(self, - provider_config, - smtp_config, - download_if_needed=False): + def _start_smtp_service(self): + """ + Start the smtp service using the downloaded configurations. + """ + # TODO Make the encrypted_only configurable + # TODO pick local smtp port in a better way + # TODO remove hard-coded port and let leap.mail set + # the specific default. + # TODO handle more than one host and define how to choose + hosts = self._smtp_config.get_hosts() + hostname = hosts.keys()[0] + host = hosts[hostname][self.IP_KEY].encode("utf-8") + port = hosts[hostname][self.PORT_KEY] + client_cert_path = self._smtp_config.get_client_cert_path( + self._provider_config, about_to_download=True) + + from leap.mail.smtp import setup_smtp_gateway + self._smtp_service, self._smtp_port = setup_smtp_gateway( + port=2013, + userid=self._userid, + keymanager=self._keymanager, + smtp_host=host, + smtp_port=port, + smtp_cert=client_cert_path, + smtp_key=client_cert_path, + encrypted_only=False) + + def start_smtp_service(self, provider_config, smtp_config, keymanager, + userid, download_if_needed=False): """ - Starts the checks needed for a new smtp setup + Starts the SMTP service. :param provider_config: Provider configuration :type provider_config: ProviderConfig :param smtp_config: SMTP configuration to populate :type smtp_config: SMTPConfig + :param keymanager: a transparent proxy that eventually will point to a + Keymanager Instance. + :type keymanager: zope.proxy.ProxyBase + :param userid: the user id, in the form "user@provider" + :type userid: str :param download_if_needed: True if it should check for mtime for the file :type download_if_needed: bool """ leap_assert_type(provider_config, ProviderConfig) + leap_assert_type(smtp_config, SMTPConfig) self._provider_config = provider_config + self._keymanager = keymanager self._smtp_config = smtp_config + self._useid = userid self._download_if_needed = download_if_needed - cb_chain = [ - (self._download_config, self.download_config), - ] - - self.addCallbackChain(cb_chain) + try: + self._download_config_and_cert() + logger.debug("Starting SMTP service.") + self._start_smtp_service() + except NoSMTPHosts: + logger.warning("There is no SMTP host to use.") + except Exception as e: + # TODO: we should handle more specific exceptions in here + logger.exception("Error while bootstrapping SMTP: %r" % (e, )) + + def stop_smtp_service(self): + """ + Stops the smtp service (port and factory). + """ + if self._smtp_service is not None: + logger.debug('Stopping SMTP service.') + self._smtp_port.stopListening() + self._smtp_service.doStop() diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index ad5ee4d0..6bb7c036 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -139,7 +139,6 @@ class SoledadBootstrapper(AbstractBootstrapper): download_config = QtCore.Signal(dict) gen_key = QtCore.Signal(dict) local_only_ready = QtCore.Signal(dict) - soledad_timeout = QtCore.Signal() soledad_invalid_auth_token = QtCore.Signal() soledad_failed = QtCore.Signal() @@ -159,8 +158,6 @@ class SoledadBootstrapper(AbstractBootstrapper): self._srpauth = None self._soledad = None - self._soledad_retries = 0 - @property def keymanager(self): return self._keymanager @@ -177,26 +174,6 @@ class SoledadBootstrapper(AbstractBootstrapper): "We need a provider config") return SRPAuth(self._provider_config) - # retries - - def cancel_bootstrap(self): - self._soledad_retries = self.MAX_INIT_RETRIES - - def should_retry_initialization(self): - """ - Return True if we should retry the initialization. - """ - logger.debug("current retries: %s, max retries: %s" % ( - self._soledad_retries, - self.MAX_INIT_RETRIES)) - return self._soledad_retries < self.MAX_INIT_RETRIES - - def increment_retries_count(self): - """ - Increment the count of initialization retries. - """ - self._soledad_retries += 1 - # initialization def load_offline_soledad(self, username, password, uuid): @@ -265,6 +242,41 @@ class SoledadBootstrapper(AbstractBootstrapper): # in the case of an invalid token we have already turned off mail and # warned the user in _do_soledad_sync() + def _do_soledad_init(self, uuid, secrets_path, local_db_path, + server_url, cert_file, token): + """ + Initialize soledad, retry if necessary and emit soledad_failed if we + can't succeed. + + :param uuid: user identifier + :type uuid: str + :param secrets_path: path to secrets file + :type secrets_path: str + :param local_db_path: path to local db file + :type local_db_path: str + :param server_url: soledad server uri + :type server_url: str + :param cert_file: path to the certificate of the ca used + to validate the SSL certificate used by the remote + soledad server. + :type cert_file: str + :param auth token: auth token + :type auth_token: str + """ + init_tries = self.MAX_INIT_RETRIES + while init_tries > 0: + try: + self._try_soledad_init( + uuid, secrets_path, local_db_path, + server_url, cert_file, token) + logger.debug("Soledad has been initialized.") + return + except Exception: + init_tries -= 1 + continue + + self.soledad_failed.emit() + raise SoledadInitError() def load_and_sync_soledad(self, uuid=None, offline=False): """ @@ -283,10 +295,9 @@ class SoledadBootstrapper(AbstractBootstrapper): server_url, cert_file = remote_param try: - self._try_soledad_init( - uuid, secrets_path, local_db_path, - server_url, cert_file, token) - except Exception: + self._do_soledad_init(uuid, secrets_path, local_db_path, + server_url, cert_file, token) + except SoledadInitError: # re-raise the exceptions from try_init, # we're currently handling the retries from the # soledad-launcher in the gui. @@ -378,9 +389,13 @@ class SoledadBootstrapper(AbstractBootstrapper): Try to initialize soledad. :param uuid: user identifier + :type uuid: str :param secrets_path: path to secrets file + :type secrets_path: str :param local_db_path: path to local db file + :type local_db_path: str :param server_url: soledad server uri + :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. @@ -409,34 +424,17 @@ class SoledadBootstrapper(AbstractBootstrapper): # and return a subclass of SoledadInitializationFailed # recoverable, will guarantee retries - except socket.timeout: - logger.debug("SOLEDAD initialization TIMED OUT...") - self.soledad_timeout.emit() - raise - except socket.error as exc: - logger.warning("Socket error while initializing soledad") - self.soledad_timeout.emit() - raise - except BootstrapSequenceError as exc: - logger.warning("Error while initializing soledad") - self.soledad_timeout.emit() + except (socket.timeout, socket.error, BootstrapSequenceError): + logger.warning("Error while initializing Soledad") raise # unrecoverable - except u1db_errors.Unauthorized: - logger.error("Error while initializing soledad " - "(unauthorized).") - self.soledad_failed.emit() - raise - except u1db_errors.HTTPError as exc: - logger.exception("Error while initializing soledad " - "(HTTPError)") - self.soledad_failed.emit() + except (u1db_errors.Unauthorized, u1db_errors.HTTPError): + logger.error("Error while initializing Soledad (u1db error).") raise except Exception as exc: logger.exception("Unhandled error while initializating " - "soledad: %r" % (exc,)) - self.soledad_failed.emit() + "Soledad: %r" % (exc,)) raise def _try_soledad_sync(self): diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index 88267ff8..84af4e8d 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -59,6 +59,13 @@ def build_parser(): action="store_false", dest="api_version_check", help='Skip the api version compatibility check with ' 'the provider.') + parser.add_argument('-H', '--start-hidden', default=False, + action="store_true", dest="start_hidden", + help='Starts the application just in the taskbar.') + parser.add_argument('-S', '--skip-wizard-checks', default=False, + action="store_true", dest="skip_wizard_checks", + help='Skips the provider checks in the wizard (use ' + 'for testing purposes only).') # openvpn options parser.add_argument('--openvpn-verbosity', nargs='?', diff --git a/src/leap/bitmask/util/pastebin.py b/src/leap/bitmask/util/pastebin.py index 21b8a0b7..a3bdba02 100755 --- a/src/leap/bitmask/util/pastebin.py +++ b/src/leap/bitmask/util/pastebin.py @@ -27,8 +27,8 @@ __ALL__ = ['delete_paste', 'user_details', 'trending', 'pastes_by_user',
- 'generate_user_key', 'legacy_paste', 'paste', 'Pastebin',
- 'PastebinError']
+ 'generate_user_key', 'paste', 'Pastebin', 'PastebinError',
+ 'PostLimitError']
import urllib
@@ -40,6 +40,12 @@ class PastebinError(RuntimeError): exception message."""
+class PostLimitError(PastebinError):
+ """The user reached the limit of posts that can do in a day.
+ For more information look at: http://pastebin.com/faq#11a
+ """
+
+
class PastebinAPI(object):
"""Pastebin API interaction object.
@@ -67,6 +73,9 @@ class PastebinAPI(object): # String to determine bad API requests
_bad_request = 'Bad API request'
+ # String to determine if we reached the max post limit per day
+ _post_limit = 'Post limit, maximum pastes per 24h reached'
+
# Base domain name
_base_domain = 'pastebin.com'
@@ -708,95 +717,8 @@ class PastebinAPI(object): # errors we are likely to encounter
if response.startswith(self._bad_request):
raise PastebinError(response)
- elif not response.startswith(self._prefix_url):
- raise PastebinError(response)
-
- return response
-
- def legacy_paste(self, paste_code,
- paste_name=None, paste_private=None,
- paste_expire_date=None, paste_format=None):
- """Unofficial python interface to the Pastebin legacy API.
-
- Unlike the official API, this one doesn't require an API key, so it's
- virtually anonymous.
-
-
- Usage Example::
- from pastebin import PastebinAPI
- x = PastebinAPI()
- url = x.legacy_paste('Snippet of code to paste goes here',
- paste_name = 'title of paste',
- paste_private = 'unlisted',
- paste_expire_date = '10M',
- paste_format = 'python')
- print url
- http://pastebin.com/tawPUgqY
-
-
- @type paste_code: string
- @param paste_code: The file or string to paste to body of the
- U{http://pastebin.com} paste.
-
- @type paste_name: string
- @param paste_name: (Optional) Title of the paste.
- Default is to paste with no title.
-
- @type paste_private: string
- @param paste_private: (Optional) C{'public'} if the paste is public
- (visible by everyone), C{'unlisted'} if it's public but not
- searchable. C{'private'} if the paste is private and not
- searchable or indexed.
- The Pastebin FAQ (U{http://pastebin.com/faq}) claims
- private pastes are not indexed by search engines (aka Google).
-
- @type paste_expire_date: string
- @param paste_expire_date: (Optional) Expiration date for the paste.
- Once past this date the paste is deleted automatically. Valid
- values are found in the L{PastebinAPI.paste_expire_date} class
- member.
- If not provided, the paste never expires.
-
- @type paste_format: string
- @param paste_format: (Optional) Programming language of the code being
- pasted. This enables syntax highlighting when reading the code in
- U{http://pastebin.com}. Default is no syntax highlighting (text is
- just text and not source code).
-
- @rtype: string
- @return: Returns the URL to the newly created paste.
- """
-
- # Code snippet to submit
- argv = {'paste_code': str(paste_code)}
-
- # Name of the poster
- if paste_name is not None:
- argv['paste_name'] = str(paste_name)
-
- # Is the snippet private?
- if paste_private is not None:
- argv['paste_private'] = int(bool(int(paste_private)))
-
- # Expiration for the snippet
- if paste_expire_date is not None:
- paste_expire_date = str(paste_expire_date).strip().upper()
- argv['paste_expire_date'] = paste_expire_date
-
- # Syntax highlighting
- if paste_format is not None:
- paste_format = str(paste_format).strip().lower()
- argv['paste_format'] = paste_format
-
- # lets try to read the URL that we've just built.
- data = urllib.urlencode(argv)
- request_string = urllib.urlopen(self._legacy_api_url, data)
- response = request_string.read()
-
- # do some basic error checking here so we can gracefully handle any
- # errors we are likely to encounter
- if response.startswith(self._bad_request):
- raise PastebinError(response)
+ elif response.startswith(self._post_limit):
+ raise PostLimitError(response)
elif not response.startswith(self._prefix_url):
raise PastebinError(response)
@@ -810,5 +732,4 @@ user_details = PastebinAPI.user_details trending = PastebinAPI.trending
pastes_by_user = PastebinAPI.pastes_by_user
generate_user_key = PastebinAPI.generate_user_key
-legacy_paste = PastebinAPI.legacy_paste
paste = PastebinAPI.paste
diff --git a/src/leap/bitmask/util/privilege_policies.py b/src/leap/bitmask/util/privilege_policies.py index 72442553..9d1e2c9a 100644 --- a/src/leap/bitmask/util/privilege_policies.py +++ b/src/leap/bitmask/util/privilege_policies.py @@ -27,35 +27,6 @@ from abc import ABCMeta, abstractmethod logger = logging.getLogger(__name__) -POLICY_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE policyconfig PUBLIC - "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" - "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> -<policyconfig> - - <vendor>LEAP Project</vendor> - <vendor_url>https://leap.se/</vendor_url> - - <action id="net.openvpn.gui.leap.run-openvpn"> - <description>Runs the openvpn binary</description> - <description xml:lang="es">Ejecuta el binario openvpn</description> - <message>OpenVPN needs that you authenticate to start</message> - <message xml:lang="es"> - OpenVPN necesita autorizacion para comenzar - </message> - <icon_name>package-x-generic</icon_name> - <defaults> - <allow_any>yes</allow_any> - <allow_inactive>yes</allow_inactive> - <allow_active>yes</allow_active> - </defaults> - <annotate key="org.freedesktop.policykit.exec.path">{path}</annotate> - <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> - </action> -</policyconfig> -""" - - def is_missing_policy_permissions(): """ Returns True if we do not have implemented a policy checker for this @@ -76,36 +47,6 @@ def is_missing_policy_permissions(): return policy_checker().is_missing_policy_permissions() -def get_policy_contents(openvpn_path): - """ - Returns the contents that the policy file should have. - - :param openvpn_path: the openvpn path to use in the polkit file - :type openvpn_path: str - :rtype: str - """ - return POLICY_TEMPLATE.format(path=openvpn_path) - - -def is_policy_outdated(path): - """ - Returns if the existing polkit file is outdated, comparing if the path - is correct. - - :param path: the path that should have the polkit file. - :type path: str. - :rtype: bool - """ - _system = platform.system() - platform_checker = _system + "PolicyChecker" - policy_checker = globals().get(platform_checker, None) - if policy_checker is None: - logger.debug("we could not find a policy checker implementation " - "for %s" % (_system,)) - return False - return policy_checker().is_outdated(path) - - class PolicyChecker: """ Abstract PolicyChecker class @@ -129,7 +70,7 @@ class LinuxPolicyChecker(PolicyChecker): PolicyChecker for Linux """ LINUX_POLKIT_FILE = ("/usr/share/polkit-1/actions/" - "net.openvpn.gui.leap.policy") + "se.leap.bitmask.policy") @classmethod def get_polkit_path(self): @@ -141,6 +82,8 @@ class LinuxPolicyChecker(PolicyChecker): return self.LINUX_POLKIT_FILE def is_missing_policy_permissions(self): + # FIXME this name is quite confusing, it does not have anything to do with + # file permissions. """ Returns True if we could not find the appropriate policykit file in place @@ -148,22 +91,3 @@ class LinuxPolicyChecker(PolicyChecker): :rtype: bool """ return not os.path.isfile(self.LINUX_POLKIT_FILE) - - def is_outdated(self, path): - """ - Returns if the existing polkit file is outdated, comparing if the path - is correct. - - :param path: the path that should have the polkit file. - :type path: str. - :rtype: bool - """ - polkit = None - try: - with open(self.LINUX_POLKIT_FILE) as f: - polkit = f.read() - except IOError, e: - logger.error("Error reading polkit file(%s): %r" % ( - self.LINUX_POLKIT_FILE, e)) - - return get_policy_contents(path) != polkit |