diff options
Diffstat (limited to 'src/leap/bitmask/backend/leapbackend.py')
| -rw-r--r-- | src/leap/bitmask/backend/leapbackend.py | 636 | 
1 files changed, 636 insertions, 0 deletions
| diff --git a/src/leap/bitmask/backend/leapbackend.py b/src/leap/bitmask/backend/leapbackend.py new file mode 100644 index 00000000..3c5222f4 --- /dev/null +++ b/src/leap/bitmask/backend/leapbackend.py @@ -0,0 +1,636 @@ +# -*- coding: utf-8 -*- +# leapbackend.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +""" +Backend for GUI/Logic communication. +""" +import logging + +from Queue import Queue, Empty + +from twisted.internet import reactor +from twisted.internet import threads, defer +from twisted.internet.task import LoopingCall + +import zope.interface +import zope.proxy + +from leap.bitmask.backend.leapsignaler import Signaler +from leap.bitmask.backend import components + +logger = logging.getLogger(__name__) + + +class Backend(object): +    """ +    Backend for everything, the UI should only use this class. +    """ + +    PASSED_KEY = "passed" +    ERROR_KEY = "error" + +    def __init__(self, bypass_checks=False): +        """ +        Constructor for the backend. +        """ +        # Components map for the commands received +        self._components = {} + +        # Ongoing defers that will be cancelled at stop time +        self._ongoing_defers = [] + +        # Signaler object to translate commands into Qt signals +        self._signaler = Signaler() + +        # Objects needed by several components, so we make a proxy and pass +        # them around +        self._soledad_proxy = zope.proxy.ProxyBase(None) +        self._keymanager_proxy = zope.proxy.ProxyBase(None) + +        # Component registration +        self._register(components.Provider(self._signaler, bypass_checks)) +        self._register(components.Register(self._signaler)) +        self._register(components.Authenticate(self._signaler)) +        self._register(components.EIP(self._signaler)) +        self._register(components.Soledad(self._soledad_proxy, +                                          self._keymanager_proxy, +                                          self._signaler)) +        self._register(components.Keymanager(self._keymanager_proxy, +                                             self._signaler)) +        self._register(components.Mail(self._soledad_proxy, +                                       self._keymanager_proxy, +                                       self._signaler)) + +        # We have a looping call on a thread executing all the +        # commands in queue. Right now this queue is an actual Queue +        # object, but it'll become the zmq recv_multipart queue +        self._lc = LoopingCall(threads.deferToThread, self._worker) + +        # Temporal call_queue for worker, will be replaced with +        # recv_multipart os something equivalent in the loopingcall +        self._call_queue = Queue() + +    @property +    def signaler(self): +        """ +        Public signaler access to let the UI connect to its signals. +        """ +        return self._signaler + +    def start(self): +        """ +        Starts the looping call +        """ +        logger.debug("Starting worker...") +        self._lc.start(0.01) + +    def stop(self): +        """ +        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`. +        """ +        logger.debug("Stopping worker...") +        if self._lc.running: +            self._lc.stop() +        else: +            logger.warning("Looping call is not running, cannot stop") + +        logger.debug("Cancelling ongoing defers...") +        while len(self._ongoing_defers) > 0: +            d = self._ongoing_defers.pop() +            d.cancel() +        logger.debug("Defers cancelled.") + +    def _register(self, component): +        """ +        Registers a component in this backend + +        :param component: Component to register +        :type component: any object that implements ILEAPComponent +        """ +        # TODO: assert that the component implements the interfaces +        # expected +        try: +            self._components[component.key] = component +        except Exception: +            logger.error("There was a problem registering %s" % (component,)) + +    def _signal_back(self, _, signal): +        """ +        Helper method to signal back (callback like behavior) to the +        UI that an operation finished. + +        :param signal: signal name +        :type signal: str +        """ +        self._signaler.signal(signal) + +    def _worker(self): +        """ +        Worker method, called from a different thread and as a part of +        a looping call +        """ +        try: +            # this'll become recv_multipart +            cmd = self._call_queue.get(block=False) + +            # cmd is: component, method, signalback, *args +            func = getattr(self._components[cmd[0]], cmd[1]) +            d = func(*cmd[3:]) +            if d is not None:  # d may be None if a defer chain is cancelled. +                # A call might not have a callback signal, but if it does, +                # we add it to the chain +                if cmd[2] is not None: +                    d.addCallbacks(self._signal_back, logger.error, cmd[2]) +                d.addCallbacks(self._done_action, logger.error, +                               callbackKeywords={"d": d}) +                d.addErrback(logger.error) +                self._ongoing_defers.append(d) +        except Empty: +            # If it's just empty we don't have anything to do. +            pass +        except defer.CancelledError: +            logger.debug("defer cancelled somewhere (CancelledError).") +        except Exception as e: +            # But we log the rest +            logger.exception("Unexpected exception: {0!r}".format(e)) + +    def _done_action(self, _, d): +        """ +        Remover of the defer once it's done + +        :param d: defer to remove +        :type d: twisted.internet.defer.Deferred +        """ +        if d in self._ongoing_defers: +            self._ongoing_defers.remove(d) + +    # XXX: Temporal interface until we migrate to zmq +    # We simulate the calls to zmq.send_multipart. Once we separate +    # this in two processes, the methods bellow can be changed to +    # send_multipart and this backend class will be really simple. + +    def provider_setup(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 provider_cancel_setup(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 provider_get_supported_services(self, domain): +        """ +        Signal a list of supported services provided by the given provider. + +        :param domain: the provider to get the services from. +        :type domain: str + +        Signals: +            prov_get_supported_services -> list of unicode +        """ +        self._call_queue.put(("provider", "get_supported_services", None, +                              domain)) + +    def provider_get_all_services(self, providers): +        """ +        Signal a list of services provided by all the configured providers. + +        :param providers: the list of providers to get the services. +        :type providers: list + +        Signals: +            prov_get_all_services -> list of unicode +        """ +        self._call_queue.put(("provider", "get_all_services", None, +                              providers)) + +    def provider_get_details(self, domain, lang): +        """ +        Signal a ProviderConfigLight object with the current ProviderConfig +        settings. + +        :param domain: the domain name of the provider. +        :type domain: str +        :param lang: the language to use for localized strings. +        :type lang: str + +        Signals: +            prov_get_details -> ProviderConfigLight +        """ +        self._call_queue.put(("provider", "get_details", None, domain, lang)) + +    def provider_get_pinned_providers(self): +        """ +        Signal the pinned providers. + +        Signals: +            prov_get_pinned_providers -> list of provider domains +        """ +        self._call_queue.put(("provider", "get_pinned_providers", None)) + +    def user_register(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 eip_setup(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 eip_cancel_setup(self): +        """ +        Cancel the ongoing setup EIP (if any). +        """ +        self._call_queue.put(("eip", "cancel_setup_eip", None)) + +    def eip_start(self, restart=False): +        """ +        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 + +        :param restart: whether is is a restart. +        :type restart: bool +        """ +        self._call_queue.put(("eip", "start", None, restart)) + +    def eip_stop(self, shutdown=False, restart=False, failed=False): +        """ +        Stop the EIP service. + +        :param shutdown: whether this is the final shutdown. +        :type shutdown: bool + +        :param restart: whether this is part of a restart. +        :type restart: bool +        """ +        self._call_queue.put(("eip", "stop", None, shutdown, restart)) + +    def eip_terminate(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 eip_check_dns(self, domain): +        """ +        Check if we can resolve the given domain name. + +        :param domain: the domain for the provider to check +        :type domain: str + +        Signals: +            eip_dns_ok +            eip_dns_error +        """ +        self._call_queue.put(("eip", "check_dns", None, domain)) + +    def tear_fw_down(self): +        """ +        Signal the need to tear the fw down. +        """ +        self._call_queue.put(("eip", "tear_fw_down", None)) + +    def user_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 user_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 user_cancel_login(self): +        """ +        Cancel the ongoing login (if any). +        """ +        self._call_queue.put(("authenticate", "cancel_login", None)) + +    def user_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 soledad_change_password(self, new_password): +        """ +        Change the database's password. + +        :param new_password: the new password for the user. +        :type new_password: unicode + +        Signals: +            srp_not_logged_in_error +            srp_password_change_ok +            srp_password_change_badpw +            srp_password_change_error +        """ +        self._call_queue.put(("soledad", "change_password", None, +                              new_password)) + +    def user_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)) + +    def soledad_bootstrap(self, username, domain, password): +        """ +        Bootstrap the soledad database. + +        :param username: the user name +        :type username: unicode +        :param domain: the domain that we are using. +        :type domain: unicode +        :param password: the password for the username +        :type password: unicode + +        Signals: +            soledad_bootstrap_finished +            soledad_bootstrap_failed +            soledad_invalid_auth_token +        """ +        self._call_queue.put(("soledad", "bootstrap", None, +                              username, domain, password)) + +    def soledad_load_offline(self, username, password, uuid): +        """ +        Load the soledad database in offline mode. + +        :param username: full user id (user@provider) +        :type username: str or unicode +        :param password: the soledad passphrase +        :type password: unicode +        :param uuid: the user uuid +        :type uuid: str or unicode + +        Signals: +        """ +        self._call_queue.put(("soledad", "load_offline", None, +                              username, password, uuid)) + +    def soledad_cancel_bootstrap(self): +        """ +        Cancel the ongoing soledad bootstrapping process (if any). +        """ +        self._call_queue.put(("soledad", "cancel_bootstrap", None)) + +    def soledad_close(self): +        """ +        Close soledad database. +        """ +        self._call_queue.put(("soledad", "close", None)) + +    def keymanager_list_keys(self): +        """ +        Signal a list of public keys locally stored. + +        Signals: +            keymanager_keys_list -> list +        """ +        self._call_queue.put(("keymanager", "list_keys", None)) + +    def keymanager_export_keys(self, username, filename): +        """ +        Export the given username's keys to a file. + +        :param username: the username whos keys we need to export. +        :type username: str +        :param filename: the name of the file where we want to save the keys. +        :type filename: str + +        Signals: +            keymanager_export_ok +            keymanager_export_error +        """ +        self._call_queue.put(("keymanager", "export_keys", None, +                              username, filename)) + +    def keymanager_get_key_details(self, username): +        """ +        Signal the given username's key details. + +        :param username: the username whos keys we need to get details. +        :type username: str + +        Signals: +            keymanager_key_details +        """ +        self._call_queue.put(("keymanager", "get_key_details", None, username)) + +    def smtp_start_service(self, full_user_id, download_if_needed=False): +        """ +        Start the SMTP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param download_if_needed: True if it should check for mtime +                                   for the file +        :type download_if_needed: bool +        """ +        self._call_queue.put(("mail", "start_smtp_service", None, +                              full_user_id, download_if_needed)) + +    def imap_start_service(self, full_user_id, offline=False): +        """ +        Start the IMAP service. + +        :param full_user_id: user id, in the form "user@provider" +        :type full_user_id: str +        :param offline: whether imap should start in offline mode or not. +        :type offline: bool +        """ +        self._call_queue.put(("mail", "start_imap_service", None, +                              full_user_id, offline)) + +    def smtp_stop_service(self): +        """ +        Stop the SMTP service. +        """ +        self._call_queue.put(("mail", "stop_smtp_service", None)) + +    def imap_stop_service(self): +        """ +        Stop imap service. + +        Signals: +            imap_stopped +        """ +        self._call_queue.put(("mail", "stop_imap_service", None)) | 
