Пример #1
0
    def __init__(self, userid, passwd, mdir=None):
        """
        Initialize the plumber with all that's needed to authenticate
        against the provider.

        :param userid: user identifier, foo@bar
        :type userid: basestring
        :param passwd: the soledad passphrase
        :type passwd: basestring
        :param mdir: a path to a maildir to import
        :type mdir: str or None
        """
        self.userid = userid
        self.passwd = passwd
        user, provider = userid.split('@')
        self.user = user
        self.mdir = mdir
        self.sol = None
        self._settings = Settings()

        provider_config_path = os.path.join(get_path_prefix(),
                                            get_provider_path(provider))
        provider_config = ProviderConfig()
        loaded = provider_config.load(provider_config_path)
        if not loaded:
            print "could not load provider config!"
            return self.exit()
Пример #2
0
    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 = []
        settings = Settings()
        domain = providerconfig.get_domain()
        gateway_conf = settings.get_selected_gateway(domain)
        gateway_selector = VPNGatewaySelector(eipconfig)

        if gateway_conf == GATEWAY_AUTOMATIC:
            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
Пример #3
0
    def __init__(self, bypass_checks=False, frontend_pid=None):
        """
        Constructor for the backend.
        """
        Backend.__init__(self, frontend_pid)

        self._settings = Settings()

        # 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 instances creation
        self._provider = components.Provider(self._signaler, bypass_checks)
        self._register = components.Register(self._signaler)
        self._authenticate = components.Authenticate(self._signaler)
        self._eip = components.EIP(self._signaler)
        self._soledad = components.Soledad(self._soledad_proxy,
                                           self._keymanager_proxy,
                                           self._signaler)
        self._keymanager = components.Keymanager(self._keymanager_proxy,
                                                 self._signaler)
        self._mail = components.Mail(self._soledad_proxy,
                                     self._keymanager_proxy, self._signaler)
Пример #4
0
    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 = []
        settings = Settings()
        domain = providerconfig.get_domain()
        gateway_conf = settings.get_selected_gateway(domain)
        gateway_selector = VPNGatewaySelector(eipconfig)

        if gateway_conf == GATEWAY_AUTOMATIC:
            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
Пример #5
0
    def __init__(self, userid, passwd, mdir=None):
        """
        Initialize the plumber with all that's needed to authenticate
        against the provider.

        :param userid: user identifier, foo@bar
        :type userid: basestring
        :param passwd: the soledad passphrase
        :type passwd: basestring
        :param mdir: a path to a maildir to import
        :type mdir: str or None
        """
        self.userid = userid
        self.passwd = passwd
        user, provider = userid.split('@')
        self.user = user
        self.mdir = mdir
        self.sol = None
        self._settings = Settings()

        provider_config_path = os.path.join(get_path_prefix(),
                                            get_provider_path(provider))
        provider_config = ProviderConfig()
        loaded = provider_config.load(provider_config_path)
        if not loaded:
            print "could not load provider config!"
            return self.exit()
Пример #6
0
    def __init__(self, bypass_checks=False, frontend_pid=None):
        """
        Constructor for the backend.
        """
        Backend.__init__(self, frontend_pid)

        self._settings = Settings()

        # 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 instances creation
        self._provider = components.Provider(self._signaler, bypass_checks)
        self._register = components.Register(self._signaler)
        self._authenticate = components.Authenticate(self._signaler)
        self._eip = components.EIP(self._signaler)
        self._soledad = components.Soledad(self._soledad_proxy,
                                           self._keymanager_proxy,
                                           self._signaler)
        self._keymanager = components.Keymanager(self._keymanager_proxy,
                                                 self._signaler)
        self._mail = components.Mail(self._soledad_proxy,
                                     self._keymanager_proxy,
                                     self._signaler)
Пример #7
0
    def get_gateways(kls, eipconfig, providerconfig):
        """
        Return a list with the selected gateways for a given provider, looking
        at the EIP config file.
        Each item of the list is a tuple containing (gateway, port).

        :param eipconfig: eip configuration object
        :type eipconfig: EIPConfig

        :param providerconfig: provider specific configuration
        :type providerconfig: ProviderConfig

        :rtype: list
        """
        gateways = []

        settings = Settings()
        domain = providerconfig.get_domain()
        gateway_conf = settings.get_selected_gateway(domain)
        gateway_selector = VPNGatewaySelector(eipconfig)

        if gateway_conf == GATEWAY_AUTOMATIC:
            gws = gateway_selector.get_gateways()
        else:
            gws = [gateway_conf]

        if not gws:
            logger.error('No gateway was found!')
            raise VPNLauncherException('No gateway was found!')

        for idx, gw in enumerate(gws):
            ports = eipconfig.get_gateway_ports(idx)

            the_port = "1194"  # default port

            # pick the port preferring this order:
            for port in kls.PREFERRED_PORTS:
                if port in ports:
                    the_port = port
                    break
                else:
                    continue

            gateways.append((gw, the_port))

        logger.debug("Using gateways (ip, port): {0!r}".format(gateways))
        return gateways
Пример #8
0
    def get_gateways(kls, eipconfig, providerconfig):
        """
        Return a list with the selected gateways for a given provider, looking
        at the EIP config file.
        Each item of the list is a tuple containing (gateway, port).

        :param eipconfig: eip configuration object
        :type eipconfig: EIPConfig

        :param providerconfig: provider specific configuration
        :type providerconfig: ProviderConfig

        :rtype: list
        """
        gateways = []

        settings = Settings()
        domain = providerconfig.get_domain()
        gateway_conf = settings.get_selected_gateway(domain)
        gateway_selector = VPNGatewaySelector(eipconfig)

        if gateway_conf == GATEWAY_AUTOMATIC:
            gws = gateway_selector.get_gateways()
        else:
            gws = [gateway_conf]

        if not gws:
            logger.error('No gateway was found!')
            raise VPNLauncherException('No gateway was found!')

        for idx, gw in enumerate(gws):
            ports = eipconfig.get_gateway_ports(idx)

            the_port = "1194"  # default port

            # pick the port preferring this order:
            for port in kls.PREFERRED_PORTS:
                if port in ports:
                    the_port = port
                    break
                else:
                    continue

            gateways.append((gw, the_port))

        logger.debug("Using gateways (ip, port): {0!r}".format(gateways))
        return gateways
Пример #9
0
    def get_gateway_country_code(self, domain):
        """
        Signal the country code for the currently used gateway for the given
        provider.

        :param domain: the domain to get country code.
        :type domain: str

        Signals:
            eip_get_gateway_country_code -> str
            eip_no_gateway
        """
        settings = Settings()

        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_config.load(eipconfig.get_eipconfig_path(domain))

        gateway_selector = eipconfig.VPNGatewaySelector(eip_config)
        gateway_conf = settings.get_selected_gateway(domain)

        if gateway_conf == GATEWAY_AUTOMATIC:
            gateways = gateway_selector.get_gateways()
        else:
            gateways = [gateway_conf]

        if not gateways:
            self._signaler.signal(self._signaler.eip_no_gateway)
            return

        # this only works for selecting the first gateway, as we're
        # currently doing.
        ccodes = gateway_selector.get_gateways_country_code()
        gateway_ccode = ''  # '' instead of None due to needed signal argument
        if ccodes is not None:
            gateway_ccode = ccodes[gateways[0]]

        self._signaler.signal(self._signaler.eip_get_gateway_country_code,
                              gateway_ccode)
Пример #10
0
    def get_gateway_country_code(self, domain):
        """
        Signal the country code for the currently used gateway for the given
        provider.

        :param domain: the domain to get country code.
        :type domain: str

        Signals:
            eip_get_gateway_country_code -> str
            eip_no_gateway
        """
        settings = Settings()

        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_config.load(eipconfig.get_eipconfig_path(domain))

        gateway_selector = eipconfig.VPNGatewaySelector(eip_config)
        gateway_conf = settings.get_selected_gateway(domain)

        if gateway_conf == GATEWAY_AUTOMATIC:
            gateways = gateway_selector.get_gateways()
        else:
            gateways = [gateway_conf]

        if not gateways:
            self._signaler.signal(self._signaler.eip_no_gateway)
            return

        # this only works for selecting the first gateway, as we're
        # currently doing.
        ccodes = gateway_selector.get_gateways_country_code()
        gateway_ccode = ''  # '' instead of None due to needed signal argument
        if ccodes is not None:
            gateway_ccode = ccodes[gateways[0]]

        self._signaler.signal(self._signaler.eip_get_gateway_country_code,
                              gateway_ccode)
Пример #11
0
        def __init__(self, provider_config, signaler=None):
            """
            Constructor for SRPAuth implementation

            :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
            """
            leap_assert(provider_config,
                        "We need a provider config to authenticate")

            self._provider_config = provider_config
            self._signaler = signaler
            self._settings = Settings()

            # **************************************************** #
            # Dependency injection helpers, override this for more
            # granular testing
            self._fetcher = requests
            self._srp = srp
            self._hashfun = self._srp.SHA256
            self._ng = self._srp.NG_1024
            # **************************************************** #

            self._reset_session()

            self._session_id = None
            self._session_id_lock = threading.Lock()
            self._uuid = None
            self._uuid_lock = threading.Lock()
            self._token = None
            self._token_lock = threading.Lock()

            self._srp_user = None
            self._srp_a = None

            # User credentials stored for password changing checks
            self._username = None
            self._password = None
Пример #12
0
        def __init__(self, provider_config, signaler=None):
            """
            Constructor for SRPAuth implementation

            :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
            """
            leap_assert(provider_config,
                        "We need a provider config to authenticate")

            self._provider_config = provider_config
            self._signaler = signaler
            self._settings = Settings()

            # **************************************************** #
            # Dependency injection helpers, override this for more
            # granular testing
            self._fetcher = requests
            self._srp = srp
            self._hashfun = self._srp.SHA256
            self._ng = self._srp.NG_1024
            # **************************************************** #

            self._reset_session()

            self._session_id = None
            self._session_id_lock = threading.Lock()
            self._uuid = None
            self._uuid_lock = threading.Lock()
            self._token = None
            self._token_lock = threading.Lock()

            self._srp_user = None
            self._srp_a = None

            # User credentials stored for password changing checks
            self._username = None
            self._password = None
Пример #13
0
class MBOXPlumber(object):
    """
    An class that can fix things inside a soledadbacked account.
    The idea is to gather in this helper different fixes for mailboxes
    that can be invoked when data migration in the client is needed.
    """

    def __init__(self, userid, passwd, mdir=None):
        """
        Initialize the plumber with all that's needed to authenticate
        against the provider.

        :param userid: user identifier, foo@bar
        :type userid: basestring
        :param passwd: the soledad passphrase
        :type passwd: basestring
        :param mdir: a path to a maildir to import
        :type mdir: str or None
        """
        self.userid = userid
        self.passwd = passwd
        user, provider = userid.split('@')
        self.user = user
        self.mdir = mdir
        self.sol = None
        self._settings = Settings()

        provider_config_path = os.path.join(get_path_prefix(),
                                            get_provider_path(provider))
        provider_config = ProviderConfig()
        loaded = provider_config.load(provider_config_path)
        if not loaded:
            print "could not load provider config!"
            return self.exit()

    def _init_local_soledad(self):
        """
        Initialize local Soledad instance.
        """
        self.uuid = self._settings.get_uuid(self.userid)
        if not self.uuid:
            print "Cannot get UUID from settings. Log in at least once."
            return False

        print "UUID: %s" % (self.uuid)

        secrets, localdb = get_db_paths(self.uuid)

        self.sol = initialize_soledad(
            self.uuid, self.userid, self.passwd,
            secrets, localdb, "/tmp", "/tmp")
        memstore = MemoryStore(
            permanent_store=SoledadStore(self.sol),
            write_period=5)
        self.acct = SoledadBackedAccount(self.userid, self.sol,
                                         memstore=memstore)
        return True

    #
    # Account repairing
    #

    def repair_account(self, *args):
        """
        Repair mbox uids for all mboxes in this account.
        """
        init = self._init_local_soledad()
        if not init:
            return self.exit()

        for mbox_name in self.acct.mailboxes:
            self.repair_mbox_uids(mbox_name)
        print "done."
        self.exit()

    def repair_mbox_uids(self, mbox_name):
        """
        Repair indexes for a given mbox.

        :param mbox_name: mailbox to repair
        :type mbox_name: basestring
        """
        print
        print "REPAIRING INDEXES FOR MAILBOX %s" % (mbox_name,)
        print "----------------------------------------------"
        mbox = self.acct.getMailbox(mbox_name)
        len_mbox = mbox.getMessageCount()
        print "There are %s messages" % (len_mbox,)

        last_ok = True if mbox.last_uid == len_mbox else False
        uids_iter = mbox.messages.all_msg_iter()
        dupes = self._has_dupes(uids_iter)
        if last_ok and not dupes:
            print "Mbox does not need repair."
            return

        # XXX CHANGE? ----
        msgs = mbox.messages.get_all()
        for zindex, doc in enumerate(msgs):
            mindex = zindex + 1
            old_uid = doc.content['uid']
            doc.content['uid'] = mindex
            self.sol.put_doc(doc)
            if mindex != old_uid:
                print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid)

        old_last_uid = mbox.last_uid
        mbox.last_uid = len_mbox
        print "LAST UID: %s (%s)" % (mbox.last_uid, old_last_uid)

    def _has_dupes(self, sequence):
        """
        Return True if the given sequence of ints has duplicates.

        :param sequence: a sequence of ints
        :type sequence: sequence
        :rtype: bool
        """
        d = defaultdict(lambda: 0)
        for uid in sequence:
            d[uid] += 1
            if d[uid] != 1:
                return True
        return False

    #
    # Maildir import
    #
    def import_mail(self, mail_filename):
        """
        Import a single mail into a mailbox.

        :param mbox: the Mailbox instance to save in.
        :type mbox: SoledadMailbox
        :param mail_filename: the filename to the mail file to save
        :type mail_filename: basestring
        :return: a deferred
        """
        def saved(_):
            print "message added"

        with open(mail_filename) as f:
            mail_string = f.read()
            # uid = self._mbox.getUIDNext()
            # print "saving with UID: %s" % uid
            d = self._mbox.messages.add_msg(
                mail_string, notify_on_disk=True)
        return d

    def import_maildir(self, mbox_name="INBOX"):
        """
        Import all mails in a maildir.

        We will process all subfolders as beloging
        to the same mailbox (cur, new, tmp).
        """
        # TODO parse hierarchical subfolders into
        # inferior mailboxes.

        if not os.path.isdir(self.mdir):
            print "ERROR: maildir path does not exist."
            return

        init = self._init_local_soledad()
        if not init:
            return self.exit()

        mbox = self.acct.getMailbox(mbox_name)
        self._mbox = mbox
        len_mbox = mbox.getMessageCount()

        mail_files_g = flatten(
            map(partial(os.path.join, f), files)
            for f, _, files in os.walk(self.mdir))

        # we only coerce the generator to give the
        # len, but we could skip than and inform at the end.
        mail_files = list(mail_files_g)
        print "Got %s mails to import into %s (%s)" % (
            len(mail_files), mbox_name, len_mbox)

        def all_saved(_):
            print "all messages imported"

        deferreds = []
        for f_name in mail_files:
            deferreds.append(self.import_mail(f_name))
        print "deferreds: ", deferreds

        d1 = defer.gatherResults(deferreds, consumeErrors=False)
        d1.addCallback(all_saved)
        d1.addCallback(self._cbExit)

    def _cbExit(self, ignored):
        return self.exit()

    def exit(self):
        from twisted.internet import reactor
        try:
            if self.sol:
                self.sol.close()
            reactor.stop()
        except Exception:
            pass
        return
Пример #14
0
class LeapBackend(Backend):
    """
    Backend server subclass, used to implement the API methods.
    """
    def __init__(self, bypass_checks=False, frontend_pid=None):
        """
        Constructor for the backend.
        """
        Backend.__init__(self, frontend_pid)

        self._settings = Settings()

        # 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 instances creation
        self._provider = components.Provider(self._signaler, bypass_checks)
        self._register = components.Register(self._signaler)
        self._authenticate = components.Authenticate(self._signaler)
        self._eip = components.EIP(self._signaler)
        self._soledad = components.Soledad(self._soledad_proxy,
                                           self._keymanager_proxy,
                                           self._signaler)
        self._keymanager = components.Keymanager(self._keymanager_proxy,
                                                 self._signaler)
        self._mail = components.Mail(self._soledad_proxy,
                                     self._keymanager_proxy,
                                     self._signaler)

    def _check_type(self, obj, expected_type):
        """
        Check the type of a parameter.

        :param obj: object to check its type.
        :type obj: any type
        :param expected_type: the expected type of the object.
        :type expected_type: type
        """
        if not isinstance(obj, expected_type):
            raise TypeError("The parameter type is incorrect.")

    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._provider.setup_provider(provider)

    def provider_cancel_setup(self):
        """
        Cancel the ongoing setup provider (if any).
        """
        self._provider.cancel_setup_provider()

    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._provider.bootstrap(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._provider.get_supported_services(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._provider.get_all_services(providers)

    def provider_get_details(self, domain, lang):
        """
        Signal a dict 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 -> dict
        """
        self._provider.get_details(domain, lang)

    def provider_get_pinned_providers(self):
        """
        Signal the pinned providers.

        Signals:
            prov_get_pinned_providers -> list of provider domains
        """
        self._provider.get_pinned_providers()

    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._register.register_user(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._eip.setup_eip(provider, skip_network)

    def eip_cancel_setup(self):
        """
        Cancel the ongoing setup EIP (if any).
        """
        self._eip.cancel_setup_eip()

    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._eip.start(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._eip.stop(shutdown, restart)

    def eip_terminate(self):
        """
        Terminate the EIP service, not necessarily in a nice way.
        """
        self._eip.terminate()

    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

        Signals:
            eip_get_gateways_list -> list of unicode
            eip_get_gateways_list_error
            eip_uninitialized_provider
        """
        self._eip.get_gateways_list(domain)

    def eip_get_gateway_country_code(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 -> str
            eip_no_gateway
        """
        self._eip.get_gateway_country_code(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._eip.get_initialized_providers(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._eip.can_start(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._eip.check_dns(domain)

    def tear_fw_down(self):
        """
        Signal the need to tear the fw down.
        """
        self._eip.tear_fw_down()

    def bitmask_root_vpn_down(self):
        """
        Signal the need to bring vpn down.
        """
        self._eip.bitmask_root_vpn_down()

    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._authenticate.login(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._authenticate.logout()

    def user_cancel_login(self):
        """
        Cancel the ongoing login (if any).
        """
        self._authenticate.cancel_login()

    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._authenticate.change_password(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._soledad.change_password(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._authenticate.get_logged_in_status()

    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._check_type(username, unicode)
        self._check_type(domain, unicode)
        self._check_type(password, unicode)
        self._soledad.bootstrap(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._soledad.load_offline(username, password, uuid)

    def soledad_cancel_bootstrap(self):
        """
        Cancel the ongoing soledad bootstrapping process (if any).
        """
        self._soledad.cancel_bootstrap()

    def soledad_close(self):
        """
        Close soledad database.
        """
        self._soledad.close()

    def keymanager_list_keys(self):
        """
        Signal a list of public keys locally stored.

        Signals:
            keymanager_keys_list -> list
        """
        self._keymanager.list_keys()

    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._keymanager.export_keys(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._keymanager.get_key_details(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._mail.start_smtp_service(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._mail.start_imap_service(full_user_id, offline)

    def smtp_stop_service(self):
        """
        Stop the SMTP service.
        """
        self._mail.stop_smtp_service()

    def imap_stop_service(self):
        """
        Stop imap service.

        Signals:
            imap_stopped
        """
        self._mail.stop_imap_service()

    def settings_set_selected_gateway(self, provider, gateway):
        """
        Set the selected gateway for a given provider.

        :param provider: provider domain
        :type provider: str
        :param gateway: gateway to use as default
        :type gateway: str
        """
        self._settings.set_selected_gateway(provider, gateway)
Пример #15
0
class LeapBackend(Backend):
    """
    Backend server subclass, used to implement the API methods.
    """
    def __init__(self, bypass_checks=False, frontend_pid=None):
        """
        Constructor for the backend.
        """
        Backend.__init__(self, frontend_pid)

        self._settings = Settings()

        # 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 instances creation
        self._provider = components.Provider(self._signaler, bypass_checks)
        self._register = components.Register(self._signaler)
        self._authenticate = components.Authenticate(self._signaler)
        self._eip = components.EIP(self._signaler)
        self._soledad = components.Soledad(self._soledad_proxy,
                                           self._keymanager_proxy,
                                           self._signaler)
        self._keymanager = components.Keymanager(self._keymanager_proxy,
                                                 self._signaler)
        self._mail = components.Mail(self._soledad_proxy,
                                     self._keymanager_proxy, self._signaler)

    def _check_type(self, obj, expected_type):
        """
        Check the type of a parameter.

        :param obj: object to check its type.
        :type obj: any type
        :param expected_type: the expected type of the object.
        :type expected_type: type
        """
        if not isinstance(obj, expected_type):
            raise TypeError("The parameter type is incorrect.")

    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._provider.setup_provider(provider)

    def provider_cancel_setup(self):
        """
        Cancel the ongoing setup provider (if any).
        """
        self._provider.cancel_setup_provider()

    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._provider.bootstrap(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._provider.get_supported_services(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._provider.get_all_services(providers)

    def provider_get_details(self, domain, lang):
        """
        Signal a dict 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 -> dict
        """
        self._provider.get_details(domain, lang)

    def provider_get_pinned_providers(self):
        """
        Signal the pinned providers.

        Signals:
            prov_get_pinned_providers -> list of provider domains
        """
        self._provider.get_pinned_providers()

    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._register.register_user(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._eip.setup_eip(provider, skip_network)

    def eip_cancel_setup(self):
        """
        Cancel the ongoing setup EIP (if any).
        """
        self._eip.cancel_setup_eip()

    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._eip.start(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._eip.stop(shutdown, restart)

    def eip_terminate(self):
        """
        Terminate the EIP service, not necessarily in a nice way.
        """
        self._eip.terminate()

    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

        Signals:
            eip_get_gateways_list -> list of unicode
            eip_get_gateways_list_error
            eip_uninitialized_provider
        """
        self._eip.get_gateways_list(domain)

    def eip_get_gateway_country_code(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 -> str
            eip_no_gateway
        """
        self._eip.get_gateway_country_code(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._eip.get_initialized_providers(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._eip.can_start(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._eip.check_dns(domain)

    def tear_fw_down(self):
        """
        Signal the need to tear the fw down.
        """
        self._eip.tear_fw_down()

    def bitmask_root_vpn_down(self):
        """
        Signal the need to bring vpn down.
        """
        self._eip.bitmask_root_vpn_down()

    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._authenticate.login(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._authenticate.logout()

    def user_cancel_login(self):
        """
        Cancel the ongoing login (if any).
        """
        self._authenticate.cancel_login()

    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._authenticate.change_password(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._soledad.change_password(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._authenticate.get_logged_in_status()

    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._check_type(username, unicode)
        self._check_type(domain, unicode)
        self._check_type(password, unicode)
        self._soledad.bootstrap(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._soledad.load_offline(username, password, uuid)

    def soledad_cancel_bootstrap(self):
        """
        Cancel the ongoing soledad bootstrapping process (if any).
        """
        self._soledad.cancel_bootstrap()

    def soledad_close(self):
        """
        Close soledad database.
        """
        self._soledad.close()

    def keymanager_list_keys(self):
        """
        Signal a list of public keys locally stored.

        Signals:
            keymanager_keys_list -> list
        """
        self._keymanager.list_keys()

    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._keymanager.export_keys(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._keymanager.get_key_details(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._mail.start_smtp_service(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._mail.start_imap_service(full_user_id, offline)

    def smtp_stop_service(self):
        """
        Stop the SMTP service.
        """
        self._mail.stop_smtp_service()

    def imap_stop_service(self):
        """
        Stop imap service.

        Signals:
            imap_stopped
        """
        self._mail.stop_imap_service()

    def settings_set_selected_gateway(self, provider, gateway):
        """
        Set the selected gateway for a given provider.

        :param provider: provider domain
        :type provider: str
        :param gateway: gateway to use as default
        :type gateway: str
        """
        self._settings.set_selected_gateway(provider, gateway)
Пример #16
0
class SRPAuthImpl(object):
    """
    Implementation of the SRPAuth interface
    """

    LOGIN_KEY = "login"
    A_KEY = "A"
    CLIENT_AUTH_KEY = "client_auth"
    SESSION_ID_KEY = "_session_id"
    USER_VERIFIER_KEY = 'user[password_verifier]'
    USER_SALT_KEY = 'user[password_salt]'
    AUTHORIZATION_KEY = "Authorization"

    def __init__(self, provider_config):
        """
        Constructor for SRPAuth implementation

        :param provider_config: ProviderConfig needed to authenticate.
        :type provider_config: ProviderConfig
        """
        leap_assert(provider_config,
                    "We need a provider config to authenticate")

        self._provider_config = provider_config
        self._settings = Settings()

        # **************************************************** #
        # Dependency injection helpers, override this for more
        # granular testing
        self._fetcher = requests
        self._srp = srp
        self._hashfun = self._srp.SHA256
        self._ng = self._srp.NG_1024
        # **************************************************** #

        self._reset_session()

        self._session_id = None
        self._session_id_lock = threading.Lock()
        self._uuid = None
        self._uuid_lock = threading.Lock()
        self._token = None
        self._token_lock = threading.Lock()

        self._srp_user = None
        self._srp_a = None

        # User credentials stored for password changing checks
        self._username = None
        self._password = None

    def _reset_session(self):
        """
        Resets the current session and sets max retries to 30.
        """
        self._session = self._fetcher.session()
        # We need to bump the default retries, otherwise logout
        # fails most of the times
        # NOTE: This is a workaround for the moment, the server
        # side seems to return correctly every time, but it fails
        # on the client end.
        if requests_has_max_retries:
            adapter = HTTPAdapter(max_retries=30)
        else:
            adapter = HTTPAdapter()
        self._session.mount('https://', adapter)

    def _safe_unhexlify(self, val):
        """
        Rounds the val to a multiple of 2 and returns the
        unhexlified value

        :param val: hexlified value
        :type val: str

        :rtype: binary hex data
        :return: unhexlified val
        """
        return binascii.unhexlify(val) \
            if (len(val) % 2 == 0) else binascii.unhexlify('0' + val)

    def _authentication_preprocessing(self, username, password):
        """
        Generates the SRP.User to get the A SRP parameter

        :param username: username to login
        :type username: str
        :param password: password for the username
        :type password: str
        """
        logger.debug("Authentication preprocessing...")

        self._srp_user = self._srp.User(username.encode('utf-8'),
                                        password.encode('utf-8'),
                                        self._hashfun, self._ng)
        _, A = self._srp_user.start_authentication()

        self._srp_a = A

    def _start_authentication(self, _, username):
        """
        Sends the first request for authentication to retrieve the
        salt and B parameter

        Might raise all SRPAuthenticationError based:
          SRPAuthenticationError
          SRPAuthConnectionError
          SRPAuthBadStatusCode
          SRPAuthNoSalt
          SRPAuthNoB

        :param _: IGNORED, output from the previous callback (None)
        :type _: IGNORED
        :param username: username to login
        :type username: str

        :return: salt and B parameters
        :rtype: tuple
        """
        logger.debug("Starting authentication process...")
        try:
            auth_data = {
                self.LOGIN_KEY: username,
                self.A_KEY: binascii.hexlify(self._srp_a)
            }
            sessions_url = "%s/%s/%s/" % \
                (self._provider_config.get_api_uri(),
                 self._provider_config.get_api_version(),
                 "sessions")

            ca_cert_path = self._provider_config.get_ca_cert_path()
            ca_cert_path = ca_cert_path.encode(sys.getfilesystemencoding())

            init_session = self._session.post(sessions_url,
                                              data=auth_data,
                                              verify=ca_cert_path,
                                              timeout=REQUEST_TIMEOUT)
            # Clean up A value, we don't need it anymore
            self._srp_a = None
        except requests.exceptions.ConnectionError as e:
            logger.error("No connection made (salt): {0!r}".format(e))
            raise SRPAuthConnectionError()
        except Exception as e:
            logger.error("Unknown error: %r" % (e,))
            raise SRPAuthenticationError()

        content, mtime = reqhelper.get_content(init_session)

        if init_session.status_code not in (200,):
            logger.error("No valid response (salt): "
                         "Status code = %r. Content: %r" %
                         (init_session.status_code, content))
            if init_session.status_code == 422:
                logger.error("Invalid username or password.")
                raise SRPAuthBadUserOrPassword()

            logger.error("There was a problem with authentication.")
            raise SRPAuthBadStatusCode()

        json_content = json.loads(content)
        salt = json_content.get("salt", None)
        B = json_content.get("B", None)

        if salt is None:
            logger.error("The server didn't send the salt parameter.")
            raise SRPAuthNoSalt()
        if B is None:
            logger.error("The server didn't send the B parameter.")
            raise SRPAuthNoB()

        return salt, B

    def _process_challenge(self, salt_B, username):
        """
        Given the salt and B processes the auth challenge and
        generates the M2 parameter

        Might raise SRPAuthenticationError based:
          SRPAuthenticationError
          SRPAuthBadDataFromServer
          SRPAuthConnectionError
          SRPAuthJSONDecodeError
          SRPAuthBadUserOrPassword

        :param salt_B: salt and B parameters for the username
        :type salt_B: tuple
        :param username: username for this session
        :type username: str

        :return: the M2 SRP parameter
        :rtype: str
        """
        logger.debug("Processing challenge...")
        try:
            salt, B = salt_B
            unhex_salt = self._safe_unhexlify(salt)
            unhex_B = self._safe_unhexlify(B)
        except (TypeError, ValueError) as e:
            logger.error("Bad data from server: %r" % (e,))
            raise SRPAuthBadDataFromServer()
        M = self._srp_user.process_challenge(unhex_salt, unhex_B)

        auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(),
                                    self._provider_config.
                                    get_api_version(),
                                    "sessions",
                                    username)

        auth_data = {
            self.CLIENT_AUTH_KEY: binascii.hexlify(M)
        }

        try:
            auth_result = self._session.put(auth_url,
                                            data=auth_data,
                                            verify=self._provider_config.
                                            get_ca_cert_path(),
                                            timeout=REQUEST_TIMEOUT)
        except requests.exceptions.ConnectionError as e:
            logger.error("No connection made (HAMK): %r" % (e,))
            raise SRPAuthConnectionError()

        try:
            content, mtime = reqhelper.get_content(auth_result)
        except JSONDecodeError:
            logger.error("Bad JSON content in auth result.")
            raise SRPAuthJSONDecodeError()

        if auth_result.status_code == 422:
            error = ""
            try:
                error = json.loads(content).get("errors", "")
            except ValueError:
                logger.error("Problem parsing the received response: %s"
                             % (content,))
            except AttributeError:
                logger.error("Expecting a dict but something else was "
                             "received: %s", (content,))
            logger.error("[%s] Wrong password (HAMK): [%s]" %
                         (auth_result.status_code, error))
            raise SRPAuthBadUserOrPassword()

        if auth_result.status_code not in (200,):
            logger.error("No valid response (HAMK): "
                         "Status code = %s. Content = %r" %
                         (auth_result.status_code, content))
            raise SRPAuthBadStatusCode()

        return json.loads(content)

    def _extract_data(self, json_content):
        """
        Extracts the necessary parameters from json_content (M2,
        id, token)

        Might raise SRPAuthenticationError based:
          SRPBadDataFromServer

        :param json_content: Data received from the server
        :type json_content: dict
        """
        try:
            M2 = json_content.get("M2", None)
            uuid = json_content.get("id", None)
            token = json_content.get("token", None)
        except Exception as e:
            logger.error(e)
            raise SRPAuthBadDataFromServer()

        self.set_uuid(uuid)
        self.set_token(token)

        if M2 is None or self.get_uuid() is None:
            logger.error("Something went wrong. Content = %r" %
                         (json_content,))
            raise SRPAuthBadDataFromServer()

        emit(catalog.CLIENT_UID, uuid)  # make the rpc call async

        return M2

    def _verify_session(self, M2):
        """
        Verifies the session based on the M2 parameter. If the
        verification succeeds, it sets the session_id for this
        session

        Might raise SRPAuthenticationError based:
          SRPAuthBadDataFromServer
          SRPAuthVerificationFailed

        :param M2: M2 SRP parameter
        :type M2: str
        """
        logger.debug("Verifying session...")
        try:
            unhex_M2 = self._safe_unhexlify(M2)
        except TypeError:
            logger.error("Bad data from server (HAWK)")
            raise SRPAuthBadDataFromServer()

        self._srp_user.verify_session(unhex_M2)

        if not self._srp_user.authenticated():
            logger.error("Auth verification failed.")
            raise SRPAuthVerificationFailed()
        logger.debug("Session verified.")

        session_id = self._session.cookies.get(self.SESSION_ID_KEY, None)
        if not session_id:
            logger.error("Bad cookie from server (missing _session_id)")
            raise SRPAuthNoSessionId()

        # make the rpc call async
        emit(catalog.CLIENT_SESSION_ID, session_id)

        self.set_session_id(session_id)
        logger.debug("SUCCESS LOGIN")
        return True

    def _threader(self, cb, res, *args, **kwargs):
        return threads.deferToThread(cb, res, *args, **kwargs)

    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.

        Might raise:
            SRPAuthBadUserOrPassword
            requests.exceptions.HTTPError

        :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
        """
        leap_assert(self.get_uuid() is not None)

        if current_password != self._password:
            raise SRPAuthBadUserOrPassword

        url = "%s/%s/users/%s.json" % (
            self._provider_config.get_api_uri(),
            self._provider_config.get_api_version(),
            self.get_uuid())

        salt, verifier = self._srp.create_salted_verification_key(
            self._username.encode('utf-8'), new_password.encode('utf-8'),
            self._hashfun, self._ng)

        cookies = {self.SESSION_ID_KEY: self.get_session_id()}
        headers = {
            self.AUTHORIZATION_KEY:
            "Token token={0}".format(self.get_token())
        }
        user_data = {
            self.USER_VERIFIER_KEY: binascii.hexlify(verifier),
            self.USER_SALT_KEY: binascii.hexlify(salt)
        }

        change_password = self._session.put(
            url, data=user_data,
            verify=self._provider_config.get_ca_cert_path(),
            cookies=cookies,
            timeout=REQUEST_TIMEOUT,
            headers=headers)

        # In case of non 2xx it raises HTTPError
        change_password.raise_for_status()

        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)
        return d

    def authenticate(self, username, password):
        """
        Executes the whole authentication process for a user

        Might raise SRPAuthenticationError

        :param username: username for this session
        :type username: unicode
        :param password: password for this user
        :type password: unicode

        :returns: A defer on a different thread
        :rtype: twisted.internet.defer.Deferred
        """
        leap_assert(self.get_session_id() is None, "Already logged in")

        # User credentials stored for password changing checks
        self._username = username
        self._password = password

        self._reset_session()

        d = threads.deferToThread(self._authentication_preprocessing,
                                  username=username,
                                  password=password)
        d.addCallback(partial(self._start_authentication, username=username))

        d.addCallback(partial(self._process_challenge, username=username))
        d.addCallback(self._extract_data)
        d.addCallback(self._verify_session)
        return d

    def logout(self):
        """
        Logs out the current session.
        Expects a session_id to exists, might raise AssertionError
        """
        logger.debug("Starting logout...")

        if self.get_session_id() is None:
            logger.debug("Already logged out")
            return

        logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(),
                                    self._provider_config.
                                    get_api_version(),
                                    "logout")
        try:
            self._session.delete(logout_url,
                                 data=self.get_session_id(),
                                 verify=self._provider_config.
                                 get_ca_cert_path(),
                                 timeout=REQUEST_TIMEOUT)
        except Exception as e:
            logger.warning("Something went wrong with the logout: %r" %
                           (e,))
            raise
        else:
            self.set_session_id(None)
            self.set_uuid(None)
            self.set_token(None)
            # Also reset the session
            self._session = self._fetcher.session()
            logger.debug("Successfully logged out.")

    def set_session_id(self, session_id):
        with self._session_id_lock:
            self._session_id = session_id

    def get_session_id(self):
        with self._session_id_lock:
            return self._session_id

    def set_uuid(self, 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):
        with self._uuid_lock:
            return self._uuid

    def set_token(self, token):
        with self._token_lock:
            self._token = token

    def get_token(self):
        with self._token_lock:
            return self._token

    def is_authenticated(self):
        """
        Return whether the user is authenticated or not.

        :rtype: bool
        """
        user = self._srp_user
        if user is not None:
            return user.authenticated()

        return False
Пример #17
0
class SRPAuthImpl(object):
    """
    Implementation of the SRPAuth interface
    """

    LOGIN_KEY = "login"
    A_KEY = "A"
    CLIENT_AUTH_KEY = "client_auth"
    SESSION_ID_KEY = "_session_id"
    USER_VERIFIER_KEY = 'user[password_verifier]'
    USER_SALT_KEY = 'user[password_salt]'
    AUTHORIZATION_KEY = "Authorization"

    def __init__(self, provider_config):
        """
        Constructor for SRPAuth implementation

        :param provider_config: ProviderConfig needed to authenticate.
        :type provider_config: ProviderConfig
        """
        leap_assert(provider_config,
                    "We need a provider config to authenticate")

        self._provider_config = provider_config
        self._settings = Settings()

        # **************************************************** #
        # Dependency injection helpers, override this for more
        # granular testing
        self._fetcher = requests
        self._srp = srp
        self._hashfun = self._srp.SHA256
        self._ng = self._srp.NG_1024
        # **************************************************** #

        self._reset_session()

        self._session_id = None
        self._session_id_lock = threading.Lock()
        self._uuid = None
        self._uuid_lock = threading.Lock()
        self._token = None
        self._token_lock = threading.Lock()

        self._srp_user = None
        self._srp_a = None

        # User credentials stored for password changing checks
        self._username = None
        self._password = None

    def _reset_session(self):
        """
        Resets the current session and sets max retries to 30.
        """
        self._session = self._fetcher.session()
        # We need to bump the default retries, otherwise logout
        # fails most of the times
        # NOTE: This is a workaround for the moment, the server
        # side seems to return correctly every time, but it fails
        # on the client end.
        if requests_has_max_retries:
            adapter = HTTPAdapter(max_retries=30)
        else:
            adapter = HTTPAdapter()
        self._session.mount('https://', adapter)

    def _safe_unhexlify(self, val):
        """
        Rounds the val to a multiple of 2 and returns the
        unhexlified value

        :param val: hexlified value
        :type val: str

        :rtype: binary hex data
        :return: unhexlified val
        """
        return binascii.unhexlify(val) \
            if (len(val) % 2 == 0) else binascii.unhexlify('0' + val)

    def _authentication_preprocessing(self, username, password):
        """
        Generates the SRP.User to get the A SRP parameter

        :param username: username to login
        :type username: str
        :param password: password for the username
        :type password: str
        """
        logger.debug("Authentication preprocessing...")

        self._srp_user = self._srp.User(username.encode('utf-8'),
                                        password.encode('utf-8'),
                                        self._hashfun, self._ng)
        _, A = self._srp_user.start_authentication()

        self._srp_a = A

    def _start_authentication(self, _, username):
        """
        Sends the first request for authentication to retrieve the
        salt and B parameter

        Might raise all SRPAuthenticationError based:
          SRPAuthenticationError
          SRPAuthConnectionError
          SRPAuthBadStatusCode
          SRPAuthNoSalt
          SRPAuthNoB

        :param _: IGNORED, output from the previous callback (None)
        :type _: IGNORED
        :param username: username to login
        :type username: str

        :return: salt and B parameters
        :rtype: tuple
        """
        logger.debug("Starting authentication process...")
        try:
            auth_data = {
                self.LOGIN_KEY: username,
                self.A_KEY: binascii.hexlify(self._srp_a)
            }
            sessions_url = "%s/%s/%s/" % \
                (self._provider_config.get_api_uri(),
                 self._provider_config.get_api_version(),
                 "sessions")

            ca_cert_path = self._provider_config.get_ca_cert_path()
            ca_cert_path = ca_cert_path.encode(sys.getfilesystemencoding())

            init_session = self._session.post(sessions_url,
                                              data=auth_data,
                                              verify=ca_cert_path,
                                              timeout=REQUEST_TIMEOUT)
            # Clean up A value, we don't need it anymore
            self._srp_a = None
        except requests.exceptions.ConnectionError as e:
            logger.error("No connection made (salt): {0!r}".format(e))
            raise SRPAuthConnectionError()
        except Exception as e:
            logger.error("Unknown error: %r" % (e, ))
            raise SRPAuthenticationError()

        content, mtime = reqhelper.get_content(init_session)

        if init_session.status_code not in (200, ):
            logger.error("No valid response (salt): "
                         "Status code = %r. Content: %r" %
                         (init_session.status_code, content))
            if init_session.status_code == 422:
                logger.error("Invalid username or password.")
                raise SRPAuthBadUserOrPassword()

            logger.error("There was a problem with authentication.")
            raise SRPAuthBadStatusCode()

        json_content = json.loads(content)
        salt = json_content.get("salt", None)
        B = json_content.get("B", None)

        if salt is None:
            logger.error("The server didn't send the salt parameter.")
            raise SRPAuthNoSalt()
        if B is None:
            logger.error("The server didn't send the B parameter.")
            raise SRPAuthNoB()

        return salt, B

    def _process_challenge(self, salt_B, username):
        """
        Given the salt and B processes the auth challenge and
        generates the M2 parameter

        Might raise SRPAuthenticationError based:
          SRPAuthenticationError
          SRPAuthBadDataFromServer
          SRPAuthConnectionError
          SRPAuthJSONDecodeError
          SRPAuthBadUserOrPassword

        :param salt_B: salt and B parameters for the username
        :type salt_B: tuple
        :param username: username for this session
        :type username: str

        :return: the M2 SRP parameter
        :rtype: str
        """
        logger.debug("Processing challenge...")
        try:
            salt, B = salt_B
            unhex_salt = self._safe_unhexlify(salt)
            unhex_B = self._safe_unhexlify(B)
        except (TypeError, ValueError) as e:
            logger.error("Bad data from server: %r" % (e, ))
            raise SRPAuthBadDataFromServer()
        M = self._srp_user.process_challenge(unhex_salt, unhex_B)

        auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(),
                                    self._provider_config.get_api_version(),
                                    "sessions", username)

        auth_data = {self.CLIENT_AUTH_KEY: binascii.hexlify(M)}

        try:
            auth_result = self._session.put(
                auth_url,
                data=auth_data,
                verify=self._provider_config.get_ca_cert_path(),
                timeout=REQUEST_TIMEOUT)
        except requests.exceptions.ConnectionError as e:
            logger.error("No connection made (HAMK): %r" % (e, ))
            raise SRPAuthConnectionError()

        try:
            content, mtime = reqhelper.get_content(auth_result)
        except JSONDecodeError:
            logger.error("Bad JSON content in auth result.")
            raise SRPAuthJSONDecodeError()

        if auth_result.status_code == 422:
            error = ""
            try:
                error = json.loads(content).get("errors", "")
            except ValueError:
                logger.error("Problem parsing the received response: %s" %
                             (content, ))
            except AttributeError:
                logger.error(
                    "Expecting a dict but something else was "
                    "received: %s", (content, ))
            logger.error("[%s] Wrong password (HAMK): [%s]" %
                         (auth_result.status_code, error))
            raise SRPAuthBadUserOrPassword()

        if auth_result.status_code not in (200, ):
            logger.error("No valid response (HAMK): "
                         "Status code = %s. Content = %r" %
                         (auth_result.status_code, content))
            raise SRPAuthBadStatusCode()

        return json.loads(content)

    def _extract_data(self, json_content):
        """
        Extracts the necessary parameters from json_content (M2,
        id, token)

        Might raise SRPAuthenticationError based:
          SRPBadDataFromServer

        :param json_content: Data received from the server
        :type json_content: dict
        """
        try:
            M2 = json_content.get("M2", None)
            uuid = json_content.get("id", None)
            token = json_content.get("token", None)
        except Exception as e:
            logger.error(e)
            raise SRPAuthBadDataFromServer()

        self.set_uuid(uuid)
        self.set_token(token)

        if M2 is None or self.get_uuid() is None:
            logger.error("Something went wrong. Content = %r" %
                         (json_content, ))
            raise SRPAuthBadDataFromServer()

        emit(catalog.CLIENT_UID, uuid)  # make the rpc call async

        return M2

    def _verify_session(self, M2):
        """
        Verifies the session based on the M2 parameter. If the
        verification succeeds, it sets the session_id for this
        session

        Might raise SRPAuthenticationError based:
          SRPAuthBadDataFromServer
          SRPAuthVerificationFailed

        :param M2: M2 SRP parameter
        :type M2: str
        """
        logger.debug("Verifying session...")
        try:
            unhex_M2 = self._safe_unhexlify(M2)
        except TypeError:
            logger.error("Bad data from server (HAWK)")
            raise SRPAuthBadDataFromServer()

        self._srp_user.verify_session(unhex_M2)

        if not self._srp_user.authenticated():
            logger.error("Auth verification failed.")
            raise SRPAuthVerificationFailed()
        logger.debug("Session verified.")

        session_id = self._session.cookies.get(self.SESSION_ID_KEY, None)
        if not session_id:
            logger.error("Bad cookie from server (missing _session_id)")
            raise SRPAuthNoSessionId()

        # make the rpc call async
        emit(catalog.CLIENT_SESSION_ID, session_id)

        self.set_session_id(session_id)
        logger.debug("SUCCESS LOGIN")
        return True

    def _threader(self, cb, res, *args, **kwargs):
        return threads.deferToThread(cb, res, *args, **kwargs)

    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.

        Might raise:
            SRPAuthBadUserOrPassword
            requests.exceptions.HTTPError

        :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
        """
        leap_assert(self.get_uuid() is not None)

        if current_password != self._password:
            raise SRPAuthBadUserOrPassword

        url = "%s/%s/users/%s.json" % (self._provider_config.get_api_uri(),
                                       self._provider_config.get_api_version(),
                                       self.get_uuid())

        salt, verifier = self._srp.create_salted_verification_key(
            self._username.encode('utf-8'), new_password.encode('utf-8'),
            self._hashfun, self._ng)

        cookies = {self.SESSION_ID_KEY: self.get_session_id()}
        headers = {
            self.AUTHORIZATION_KEY: "Token token={0}".format(self.get_token())
        }
        user_data = {
            self.USER_VERIFIER_KEY: binascii.hexlify(verifier),
            self.USER_SALT_KEY: binascii.hexlify(salt)
        }

        change_password = self._session.put(
            url,
            data=user_data,
            verify=self._provider_config.get_ca_cert_path(),
            cookies=cookies,
            timeout=REQUEST_TIMEOUT,
            headers=headers)

        # In case of non 2xx it raises HTTPError
        change_password.raise_for_status()

        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)
        return d

    def authenticate(self, username, password):
        """
        Executes the whole authentication process for a user

        Might raise SRPAuthenticationError

        :param username: username for this session
        :type username: unicode
        :param password: password for this user
        :type password: unicode

        :returns: A defer on a different thread
        :rtype: twisted.internet.defer.Deferred
        """
        leap_assert(self.get_session_id() is None, "Already logged in")

        # User credentials stored for password changing checks
        self._username = username
        self._password = password

        self._reset_session()

        d = threads.deferToThread(self._authentication_preprocessing,
                                  username=username,
                                  password=password)
        d.addCallback(partial(self._start_authentication, username=username))

        d.addCallback(partial(self._process_challenge, username=username))
        d.addCallback(self._extract_data)
        d.addCallback(self._verify_session)
        return d

    def logout(self):
        """
        Logs out the current session.
        Expects a session_id to exists, might raise AssertionError
        """
        logger.debug("Starting logout...")

        if self.get_session_id() is None:
            logger.debug("Already logged out")
            return

        logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(),
                                    self._provider_config.get_api_version(),
                                    "logout")
        cookies = {self.SESSION_ID_KEY: self.get_session_id()}
        headers = {
            self.AUTHORIZATION_KEY: "Token token={0}".format(self.get_token())
        }
        try:
            res = self._session.delete(
                logout_url,
                cookies=cookies,
                headers=headers,
                verify=self._provider_config.get_ca_cert_path(),
                timeout=REQUEST_TIMEOUT)
        except Exception as e:
            logger.warning("Something went wrong with the logout: %r" % (e, ))
            raise
        else:
            self.set_session_id(None)
            self.set_uuid(None)
            self.set_token(None)
            # Also reset the session
            self._session = self._fetcher.session()
            if res.status_code == 204:
                logger.debug("Successfully logged out.")
            else:
                logger.debug("Logout status code: %s" % res.status_code)

    def set_session_id(self, session_id):
        with self._session_id_lock:
            self._session_id = session_id

    def get_session_id(self):
        with self._session_id_lock:
            return self._session_id

    def set_uuid(self, 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):
        with self._uuid_lock:
            return self._uuid

    def set_token(self, token):
        with self._token_lock:
            self._token = token

    def get_token(self):
        with self._token_lock:
            return self._token

    def is_authenticated(self):
        """
        Return whether the user is authenticated or not.

        :rtype: bool
        """
        user = self._srp_user
        if user is not None:
            return user.authenticated()

        return False
Пример #18
0
class MBOXPlumber(object):
    """
    An class that can fix things inside a soledadbacked account.
    The idea is to gather in this helper different fixes for mailboxes
    that can be invoked when data migration in the client is needed.
    """
    def __init__(self, userid, passwd, mdir=None):
        """
        Initialize the plumber with all that's needed to authenticate
        against the provider.

        :param userid: user identifier, foo@bar
        :type userid: basestring
        :param passwd: the soledad passphrase
        :type passwd: basestring
        :param mdir: a path to a maildir to import
        :type mdir: str or None
        """
        self.userid = userid
        self.passwd = passwd
        user, provider = userid.split('@')
        self.user = user
        self.mdir = mdir
        self.sol = None
        self._settings = Settings()

        provider_config_path = os.path.join(get_path_prefix(),
                                            get_provider_path(provider))
        provider_config = ProviderConfig()
        loaded = provider_config.load(provider_config_path)
        if not loaded:
            print "could not load provider config!"
            return self.exit()

    def _init_local_soledad(self):
        """
        Initialize local Soledad instance.
        """
        self.uuid = self._settings.get_uuid(self.userid)
        if not self.uuid:
            print "Cannot get UUID from settings. Log in at least once."
            return False

        print "UUID: %s" % (self.uuid)

        secrets, localdb = get_db_paths(self.uuid)

        self.sol = initialize_soledad(self.uuid, self.userid, self.passwd,
                                      secrets, localdb, "/tmp", "/tmp")
        self.acct = IMAPAccount(self.userid, self.sol)
        return True

    #
    # Account repairing
    #

    def repair_account(self, *args):
        """
        Repair mbox uids for all mboxes in this account.
        """
        init = self._init_local_soledad()
        if not init:
            return self.exit()

        for mbox_name in self.acct.mailboxes:
            self.repair_mbox_uids(mbox_name)
        print "done."
        self.exit()

    def repair_mbox_uids(self, mbox_name):
        """
        Repair indexes for a given mbox.

        :param mbox_name: mailbox to repair
        :type mbox_name: basestring
        """
        print
        print "REPAIRING INDEXES FOR MAILBOX %s" % (mbox_name, )
        print "----------------------------------------------"
        mbox = self.acct.getMailbox(mbox_name)
        len_mbox = mbox.getMessageCount()
        print "There are %s messages" % (len_mbox, )

        last_ok = True if mbox.last_uid == len_mbox else False
        uids_iter = mbox.messages.all_msg_iter()
        dupes = self._has_dupes(uids_iter)
        if last_ok and not dupes:
            print "Mbox does not need repair."
            return

        # XXX CHANGE? ----
        msgs = mbox.messages.get_all()
        for zindex, doc in enumerate(msgs):
            mindex = zindex + 1
            old_uid = doc.content['uid']
            doc.content['uid'] = mindex
            self.sol.put_doc(doc)
            if mindex != old_uid:
                print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid)

        old_last_uid = mbox.last_uid
        mbox.last_uid = len_mbox
        print "LAST UID: %s (%s)" % (mbox.last_uid, old_last_uid)

    def _has_dupes(self, sequence):
        """
        Return True if the given sequence of ints has duplicates.

        :param sequence: a sequence of ints
        :type sequence: sequence
        :rtype: bool
        """
        d = defaultdict(lambda: 0)
        for uid in sequence:
            d[uid] += 1
            if d[uid] != 1:
                return True
        return False

    #
    # Maildir import
    #
    def import_mail(self, mail_filename):
        """
        Import a single mail into a mailbox.

        :param mbox: the Mailbox instance to save in.
        :type mbox: SoledadMailbox
        :param mail_filename: the filename to the mail file to save
        :type mail_filename: basestring
        :return: a deferred
        """
        def saved(_):
            print "message added"

        with open(mail_filename) as f:
            mail_string = f.read()
            # uid = self._mbox.getUIDNext()
            # print "saving with UID: %s" % uid
            d = self._mbox.messages.add_msg(mail_string, notify_on_disk=True)
        return d

    def import_maildir(self, mbox_name="INBOX"):
        """
        Import all mails in a maildir.

        We will process all subfolders as beloging
        to the same mailbox (cur, new, tmp).
        """
        # TODO parse hierarchical subfolders into
        # inferior mailboxes.

        if not os.path.isdir(self.mdir):
            print "ERROR: maildir path does not exist."
            return

        init = self._init_local_soledad()
        if not init:
            return self.exit()

        mbox = self.acct.getMailbox(mbox_name)
        self._mbox = mbox
        len_mbox = mbox.getMessageCount()

        mail_files_g = flatten(
            map(partial(os.path.join, f), files)
            for f, _, files in os.walk(self.mdir))

        # we only coerce the generator to give the
        # len, but we could skip than and inform at the end.
        mail_files = list(mail_files_g)
        print "Got %s mails to import into %s (%s)" % (len(mail_files),
                                                       mbox_name, len_mbox)

        def all_saved(_):
            print "all messages imported"

        deferreds = []
        for f_name in mail_files:
            deferreds.append(self.import_mail(f_name))
        print "deferreds: ", deferreds

        d1 = defer.gatherResults(deferreds, consumeErrors=False)
        d1.addCallback(all_saved)
        d1.addCallback(self._cbExit)

    def _cbExit(self, ignored):
        return self.exit()

    def exit(self):
        from twisted.internet import reactor
        try:
            if self.sol:
                self.sol.close()
            reactor.stop()
        except Exception:
            pass
        return