summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/backend/components.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/bitmask/backend/components.py')
-rw-r--r--src/leap/bitmask/backend/components.py1147
1 files changed, 1147 insertions, 0 deletions
diff --git a/src/leap/bitmask/backend/components.py b/src/leap/bitmask/backend/components.py
new file mode 100644
index 00000000..19fcf283
--- /dev/null
+++ b/src/leap/bitmask/backend/components.py
@@ -0,0 +1,1147 @@
+# -*- coding: utf-8 -*-
+# components.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Backend components
+"""
+import logging
+import os
+import socket
+import time
+
+from functools import partial
+from threading import Condition
+
+from twisted.internet import threads, defer
+from twisted.python import log
+
+import zope.interface
+import zope.proxy
+
+from leap.bitmask.config.providerconfig import ProviderConfig
+from leap.bitmask.crypto.srpauth import SRPAuth
+from leap.bitmask.crypto.srpregister import SRPRegister
+from leap.bitmask.platform_init import IS_LINUX
+from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper
+from leap.bitmask.provider.pinned import PinnedProviders
+from leap.bitmask.services import get_supported
+from leap.bitmask.services.eip import eipconfig
+from leap.bitmask.services.eip import get_openvpn_management
+from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper
+
+from leap.bitmask.services.eip import vpnlauncher, vpnprocess
+from leap.bitmask.services.eip import linuxvpnlauncher, darwinvpnlauncher
+from leap.bitmask.services.eip import get_vpn_launcher
+
+from leap.bitmask.services.mail.imapcontroller import IMAPController
+from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper
+from leap.bitmask.services.mail.smtpconfig import SMTPConfig
+
+from leap.bitmask.services.soledad.soledadbootstrapper import \
+ SoledadBootstrapper
+from leap.bitmask.util import force_eval
+
+from leap.common import certs as leap_certs
+
+from leap.keymanager import openpgp
+from leap.keymanager.errors import KeyAddressMismatch, KeyFingerprintMismatch
+
+from leap.soledad.client import NoStorageSecret, PassphraseTooShort
+
+logger = logging.getLogger(__name__)
+
+
+class ILEAPComponent(zope.interface.Interface):
+ """
+ Interface that every component for the backend should comply to
+ """
+
+ key = zope.interface.Attribute("Key id for this component")
+
+
+class ILEAPService(ILEAPComponent):
+ """
+ Interface that every Service needs to implement
+ """
+
+ def start(self, *args, **kwargs):
+ """
+ Start the service.
+ """
+ pass
+
+ def stop(self, *args, **kwargs):
+ """
+ Stops the service.
+ """
+ pass
+
+ def terminate(self):
+ """
+ Terminate the service, not necessarily in a nice way.
+ """
+ pass
+
+ def status(self):
+ """
+ Return a json object with the current status for the service.
+
+ :rtype: object (list, str, dict)
+ """
+ # XXX: Use a namedtuple or a specific object instead of a json
+ # object, since parsing it will be problematic otherwise.
+ # It has to be something easily serializable though.
+ pass
+
+ def set_configs(self, keyval):
+ """
+ Set the config parameters for this Service.
+
+ :param keyval: values to configure
+ :type keyval: dict, {str: str}
+ """
+ pass
+
+ def get_configs(self, keys):
+ """
+ Return the configuration values for the list of keys.
+
+ :param keys: keys to retrieve
+ :type keys: list of str
+
+ :rtype: dict, {str: str}
+ """
+ pass
+
+
+class Provider(object):
+ """
+ Interfaces with setup and bootstrapping operations for a provider
+ """
+
+ zope.interface.implements(ILEAPComponent)
+
+ def __init__(self, signaler=None, bypass_checks=False):
+ """
+ Constructor for the Provider component
+
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ :param bypass_checks: Set to true if the app should bypass
+ first round of checks for CA
+ certificates at bootstrap
+ :type bypass_checks: bool
+ """
+ self.key = "provider"
+ self._signaler = signaler
+ self._provider_bootstrapper = ProviderBootstrapper(signaler,
+ bypass_checks)
+ self._download_provider_defer = None
+ self._provider_config = ProviderConfig()
+
+ def setup_provider(self, provider):
+ """
+ Initiate the setup for a provider
+
+ :param provider: URL for the provider
+ :type provider: unicode
+
+ :returns: the defer for the operation running in a thread.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ log.msg("Setting up provider %s..." % (provider.encode("idna"),))
+ pb = self._provider_bootstrapper
+ d = pb.run_provider_select_checks(provider, download_if_needed=True)
+ self._download_provider_defer = d
+ return d
+
+ def cancel_setup_provider(self):
+ """
+ Cancel the ongoing setup provider defer (if any).
+ """
+ d = self._download_provider_defer
+ if d is not None:
+ d.cancel()
+
+ def bootstrap(self, provider):
+ """
+ Second stage of bootstrapping for a provider.
+
+ :param provider: URL for the provider
+ :type provider: unicode
+
+ :returns: the defer for the operation running in a thread.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ d = None
+
+ config = ProviderConfig.get_provider_config(provider)
+ self._provider_config = config
+ if config is not None:
+ d = self._provider_bootstrapper.run_provider_setup_checks(
+ config, download_if_needed=True)
+ else:
+ if self._signaler is not None:
+ self._signaler.signal(
+ self._signaler.PROV_PROBLEM_WITH_PROVIDER_KEY)
+ logger.error("Could not load provider configuration.")
+ self._login_widget.set_enabled(True)
+
+ if d is None:
+ d = defer.Deferred()
+ return d
+
+ def _get_services(self, domain):
+ """
+ Returns a list of services provided by the given provider.
+
+ :param domain: the provider to get the services from.
+ :type domain: str
+
+ :rtype: list of str
+ """
+ services = []
+ provider_config = ProviderConfig.get_provider_config(domain)
+ if provider_config is not None:
+ services = provider_config.get_services()
+
+ return services
+
+ def 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
+ """
+ services = get_supported(self._get_services(domain))
+
+ self._signaler.signal(
+ self._signaler.PROV_GET_SUPPORTED_SERVICES, services)
+
+ def 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
+ """
+ services_all = set()
+
+ for domain in providers:
+ services = self._get_services(domain)
+ services_all = services_all.union(set(services))
+
+ self._signaler.signal(
+ self._signaler.PROV_GET_ALL_SERVICES, services_all)
+
+ def get_details(self, domain, lang=None):
+ """
+ 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._signaler.signal(
+ self._signaler.PROV_GET_DETAILS,
+ self._provider_config.get_light_config(domain, lang))
+
+ def get_pinned_providers(self):
+ """
+ Signal the list of pinned provider domains.
+
+ Signals:
+ prov_get_pinned_providers -> list of provider domains
+ """
+ self._signaler.signal(
+ self._signaler.PROV_GET_PINNED_PROVIDERS,
+ PinnedProviders.domains())
+
+
+class Register(object):
+ """
+ Interfaces with setup and bootstrapping operations for a provider
+ """
+
+ zope.interface.implements(ILEAPComponent)
+
+ def __init__(self, signaler=None):
+ """
+ Constructor for the Register component
+
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ """
+ self.key = "register"
+ self._signaler = signaler
+
+ def register_user(self, domain, 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
+
+ :returns: the defer for the operation running in a thread.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ config = ProviderConfig.get_provider_config(domain)
+ self._provider_config = config
+ if config is not None:
+ srpregister = SRPRegister(signaler=self._signaler,
+ provider_config=config)
+ return threads.deferToThread(
+ partial(srpregister.register_user, username, password))
+ else:
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.SRP_REGISTRATION_FAILED)
+ logger.error("Could not load provider configuration.")
+
+
+class EIP(object):
+ """
+ Interfaces with setup and launch of EIP
+ """
+
+ zope.interface.implements(ILEAPService)
+
+ def __init__(self, signaler=None):
+ """
+ Constructor for the EIP component
+
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ """
+ self.key = "eip"
+ self._signaler = signaler
+ self._eip_bootstrapper = EIPBootstrapper(signaler)
+ self._eip_setup_defer = None
+ self._provider_config = ProviderConfig()
+
+ self._vpn = vpnprocess.VPN(signaler=signaler)
+
+ def setup_eip(self, domain, skip_network=False):
+ """
+ Initiate the setup for a provider
+
+ :param domain: URL for the provider
+ :type domain: unicode
+ :param skip_network: Whether checks that involve network should be done
+ or not
+ :type skip_network: bool
+
+ :returns: the defer for the operation running in a thread.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ config = ProviderConfig.get_provider_config(domain)
+ self._provider_config = config
+ if config is not None:
+ if skip_network:
+ return defer.Deferred()
+ eb = self._eip_bootstrapper
+ d = eb.run_eip_setup_checks(self._provider_config,
+ download_if_needed=True)
+ self._eip_setup_defer = d
+ return d
+ else:
+ raise Exception("No provider setup loaded")
+
+ def cancel_setup_eip(self):
+ """
+ Cancel the ongoing setup eip defer (if any).
+ """
+ d = self._eip_setup_defer
+ if d is not None:
+ d.cancel()
+
+ def _start_eip(self, restart=False):
+ """
+ Start EIP
+
+ :param restart: whether is is a restart.
+ :type restart: bool
+ """
+ provider_config = self._provider_config
+ eip_config = eipconfig.EIPConfig()
+ domain = provider_config.get_domain()
+
+ loaded = eipconfig.load_eipconfig_if_needed(
+ provider_config, eip_config, domain)
+
+ if not self._can_start(domain):
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED)
+ return
+
+ if not loaded:
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED)
+ logger.error("Tried to start EIP but cannot find any "
+ "available provider!")
+ return
+
+ host, port = get_openvpn_management()
+ self._vpn.start(eipconfig=eip_config,
+ providerconfig=provider_config,
+ socket_host=host, socket_port=port,
+ restart=restart)
+
+ def start(self, *args, **kwargs):
+ """
+ Start the service.
+ """
+ signaler = self._signaler
+
+ if not self._provider_config.loaded():
+ # This means that the user didn't call setup_eip first.
+ self._signaler.signal(signaler.BACKEND_BAD_CALL, "EIP.start(), "
+ "no provider loaded")
+ return
+
+ try:
+ self._start_eip(*args, **kwargs)
+ except vpnprocess.OpenVPNAlreadyRunning:
+ signaler.signal(signaler.EIP_OPENVPN_ALREADY_RUNNING)
+ except vpnprocess.AlienOpenVPNAlreadyRunning:
+ signaler.signal(signaler.EIP_ALIEN_OPENVPN_ALREADY_RUNNING)
+ except vpnlauncher.OpenVPNNotFoundException:
+ signaler.signal(signaler.EIP_OPENVPN_NOT_FOUND_ERROR)
+ except vpnlauncher.VPNLauncherException:
+ # TODO: this seems to be used for 'gateway not found' only.
+ # see vpnlauncher.py
+ signaler.signal(signaler.EIP_VPN_LAUNCHER_EXCEPTION)
+ except linuxvpnlauncher.EIPNoPolkitAuthAgentAvailable:
+ signaler.signal(signaler.EIP_NO_POLKIT_AGENT_ERROR)
+ except linuxvpnlauncher.EIPNoPkexecAvailable:
+ signaler.signal(signaler.EIP_NO_PKEXEC_ERROR)
+ except darwinvpnlauncher.EIPNoTunKextLoaded:
+ signaler.signal(signaler.EIP_NO_TUN_KEXT_ERROR)
+ except Exception as e:
+ logger.error("Unexpected problem: {0!r}".format(e))
+ else:
+ logger.debug('EIP: no errors')
+
+ def _do_stop(self, shutdown=False, restart=False):
+ """
+ Stop the service. This is run in a thread to avoid blocking.
+ """
+ self._vpn.terminate(shutdown, restart)
+ if IS_LINUX:
+ self._wait_for_firewall_down()
+
+ def stop(self, shutdown=False, restart=False):
+ """
+ Stop the service.
+ """
+ return threads.deferToThread(self._do_stop, shutdown, restart)
+
+ def _wait_for_firewall_down(self):
+ """
+ Wait for the firewall to come down.
+ """
+ # Due to how we delay the resolvconf action in linux.
+ # XXX this *has* to wait for a reasonable lapse, since we have some
+ # delay in vpn.terminate.
+ # For a better solution it should be signaled from backend that
+ # everything is clear to proceed, or a timeout happened.
+ MAX_FW_WAIT_RETRIES = 25
+ FW_WAIT_STEP = 0.5
+
+ retry = 1
+
+ while retry <= MAX_FW_WAIT_RETRIES:
+ if self._vpn.is_fw_down():
+ self._signaler.signal(self._signaler.EIP_STOPPED)
+ return
+ else:
+ #msg = "Firewall is not down yet, waiting... {0} of {1}"
+ #msg = msg.format(retry, MAX_FW_WAIT_RETRIES)
+ #logger.debug(msg)
+ time.sleep(FW_WAIT_STEP)
+ retry += 1
+ logger.warning("After waiting, firewall is not down... "
+ "You might experience lack of connectivity")
+
+ def terminate(self):
+ """
+ Terminate the service, not necessarily in a nice way.
+ """
+ self._vpn.killit()
+
+ def status(self):
+ """
+ Return a json object with the current status for the service.
+
+ :rtype: object (list, str, dict)
+ """
+ # XXX: Use a namedtuple or a specific object instead of a json
+ # object, since parsing it will be problematic otherwise.
+ # It has to be something easily serializable though.
+ pass
+
+ def _provider_is_initialized(self, domain):
+ """
+ Return whether the given domain is initialized or not.
+
+ :param domain: the domain to check
+ :type domain: str
+
+ :returns: True if is initialized, False otherwise.
+ :rtype: bool
+ """
+ eipconfig_path = eipconfig.get_eipconfig_path(domain, relative=False)
+ if os.path.isfile(eipconfig_path):
+ return True
+ else:
+ return False
+
+ def get_initialized_providers(self, domains):
+ """
+ Signal a list of the given domains and if they are initialized or not.
+
+ :param domains: the list of domains to check.
+ :type domain: list of str
+
+ Signals:
+ eip_get_initialized_providers -> list of tuple(unicode, bool)
+ """
+ filtered_domains = []
+ for domain in domains:
+ is_initialized = self._provider_is_initialized(domain)
+ filtered_domains.append((domain, is_initialized))
+
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_GET_INITIALIZED_PROVIDERS,
+ filtered_domains)
+
+ def tear_fw_down(self):
+ """
+ Tear the firewall down.
+ """
+ self._vpn.tear_down_firewall()
+
+ def get_gateways_list(self, domain):
+ """
+ Signal a list of gateways for the given provider.
+
+ :param domain: the domain to get the gateways.
+ :type domain: str
+
+ Signals:
+ eip_get_gateways_list -> list of unicode
+ eip_get_gateways_list_error
+ eip_uninitialized_provider
+ """
+ if not self._provider_is_initialized(domain):
+ if self._signaler is not None:
+ self._signaler.signal(
+ self._signaler.EIP_UNINITIALIZED_PROVIDER)
+ return
+
+ eip_config = eipconfig.EIPConfig()
+ provider_config = ProviderConfig.get_provider_config(domain)
+
+ api_version = provider_config.get_api_version()
+ eip_config.set_api_version(api_version)
+ eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain))
+
+ # check for other problems
+ if not eip_loaded or provider_config is None:
+ if self._signaler is not None:
+ self._signaler.signal(
+ self._signaler.EIP_GET_GATEWAYS_LIST_ERROR)
+ return
+
+ gateways = eipconfig.VPNGatewaySelector(eip_config).get_gateways_list()
+
+ if self._signaler is not None:
+ self._signaler.signal(
+ self._signaler.EIP_GET_GATEWAYS_LIST, gateways)
+
+ def _can_start(self, domain):
+ """
+ Returns True if it has everything that is needed to run EIP,
+ False otherwise
+
+ :param domain: the domain for the provider to check
+ :type domain: str
+ """
+ eip_config = eipconfig.EIPConfig()
+ provider_config = ProviderConfig.get_provider_config(domain)
+
+ api_version = provider_config.get_api_version()
+ eip_config.set_api_version(api_version)
+ eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain))
+
+ launcher = get_vpn_launcher()
+ ovpn_path = force_eval(launcher.OPENVPN_BIN_PATH)
+ if not os.path.isfile(ovpn_path):
+ logger.error("Cannot start OpenVPN, binary not found")
+ return False
+
+ # check for other problems
+ if not eip_loaded or provider_config is None:
+ logger.error("Cannot load provider and eip config, cannot "
+ "autostart")
+ return False
+
+ client_cert_path = eip_config.\
+ get_client_cert_path(provider_config, about_to_download=False)
+
+ if leap_certs.should_redownload(client_cert_path):
+ logger.error("The client should redownload the certificate,"
+ " cannot autostart")
+ return False
+
+ if not os.path.isfile(client_cert_path):
+ logger.error("Can't find the certificate, cannot autostart")
+ return False
+
+ return True
+
+ def can_start(self, domain):
+ """
+ Signal whether it has everything that is needed to run EIP or not
+
+ :param domain: the domain for the provider to check
+ :type domain: str
+
+ Signals:
+ eip_can_start
+ eip_cannot_start
+ """
+ if self._can_start(domain):
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_CAN_START)
+ else:
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_CANNOT_START)
+
+ def check_dns(self, domain):
+ """
+ Check if we can resolve the given domain name.
+
+ :param domain: the domain to check.
+ :type domain: str
+ """
+ def do_check():
+ """
+ Try to resolve the domain name.
+ """
+ socket.gethostbyname(domain.encode('idna'))
+
+ def check_ok(_):
+ """
+ Callback handler for `do_check`.
+ """
+ self._signaler.signal(self._signaler.EIP_DNS_OK)
+ logger.debug("DNS check OK")
+
+ def check_err(failure):
+ """
+ Errback handler for `do_check`.
+
+ :param failure: the failure that triggered the errback.
+ :type failure: twisted.python.failure.Failure
+ """
+ logger.debug("Can't resolve hostname. {0!r}".format(failure))
+
+ self._signaler.signal(self._signaler.EIP_DNS_ERROR)
+
+ # python 2.7.4 raises socket.error
+ # python 2.7.5 raises socket.gaierror
+ failure.trap(socket.gaierror, socket.error)
+
+ d = threads.deferToThread(do_check)
+ d.addCallback(check_ok)
+ d.addErrback(check_err)
+
+
+class Soledad(object):
+ """
+ Interfaces with setup of Soledad.
+ """
+ zope.interface.implements(ILEAPComponent)
+
+ def __init__(self, soledad_proxy, keymanager_proxy, signaler=None):
+ """
+ Constructor for the Soledad component.
+
+ :param soledad_proxy: proxy to pass around a Soledad object.
+ :type soledad_proxy: zope.ProxyBase
+ :param keymanager_proxy: proxy to pass around a Keymanager object.
+ :type keymanager_proxy: zope.ProxyBase
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ """
+ self.key = "soledad"
+ self._soledad_proxy = soledad_proxy
+ self._keymanager_proxy = keymanager_proxy
+ self._signaler = signaler
+ self._soledad_bootstrapper = SoledadBootstrapper(signaler)
+ self._soledad_defer = None
+
+ def bootstrap(self, username, domain, password):
+ """
+ Bootstrap Soledad with the user credentials.
+
+ Signals:
+ soledad_download_config
+ soledad_gen_key
+
+ :param user: user's login
+ :type user: unicode
+ :param domain: the domain that we are using.
+ :type domain: unicode
+ :param password: user's password
+ :type password: unicode
+ """
+ provider_config = ProviderConfig.get_provider_config(domain)
+ if provider_config is not None:
+ self._soledad_defer = threads.deferToThread(
+ self._soledad_bootstrapper.run_soledad_setup_checks,
+ provider_config, username, password,
+ download_if_needed=True)
+ self._soledad_defer.addCallback(self._set_proxies_cb)
+ else:
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.SOLEDAD_BOOTSTRAP_FAILED)
+ logger.error("Could not load provider configuration.")
+
+ return self._soledad_defer
+
+ def _set_proxies_cb(self, _):
+ """
+ Update the soledad and keymanager proxies to reference the ones created
+ in the bootstrapper.
+ """
+ zope.proxy.setProxiedObject(self._soledad_proxy,
+ self._soledad_bootstrapper.soledad)
+ zope.proxy.setProxiedObject(self._keymanager_proxy,
+ self._soledad_bootstrapper.keymanager)
+
+ def 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:
+ Signaler.soledad_offline_finished
+ Signaler.soledad_offline_failed
+ """
+ self._soledad_bootstrapper.load_offline_soledad(
+ username, password, uuid)
+
+ def cancel_bootstrap(self):
+ """
+ Cancel the ongoing soledad bootstrap (if any).
+ """
+ if self._soledad_defer is not None:
+ logger.debug("Cancelling soledad defer.")
+ self._soledad_defer.cancel()
+ self._soledad_defer = None
+ zope.proxy.setProxiedObject(self._soledad_proxy, None)
+
+ def close(self):
+ """
+ Close soledad database.
+ """
+ if not zope.proxy.sameProxiedObjects(self._soledad_proxy, None):
+ self._soledad_proxy.close()
+ zope.proxy.setProxiedObject(self._soledad_proxy, None)
+
+ def _change_password_ok(self, _):
+ """
+ Password change callback.
+ """
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_OK)
+
+ def _change_password_error(self, failure):
+ """
+ Password change errback.
+
+ :param failure: failure object containing problem.
+ :type failure: twisted.python.failure.Failure
+ """
+ if failure.check(NoStorageSecret):
+ logger.error("No storage secret for password change in Soledad.")
+ if failure.check(PassphraseTooShort):
+ logger.error("Passphrase too short.")
+
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_ERROR)
+
+ def change_password(self, new_password):
+ """
+ Change the database's password.
+
+ :param new_password: the new password.
+ :type new_password: unicode
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ d = threads.deferToThread(self._soledad_proxy.change_passphrase,
+ new_password)
+ d.addCallback(self._change_password_ok)
+ d.addErrback(self._change_password_error)
+
+
+class Keymanager(object):
+ """
+ Interfaces with KeyManager.
+ """
+ zope.interface.implements(ILEAPComponent)
+
+ def __init__(self, keymanager_proxy, signaler=None):
+ """
+ Constructor for the Keymanager component.
+
+ :param keymanager_proxy: proxy to pass around a Keymanager object.
+ :type keymanager_proxy: zope.ProxyBase
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ """
+ self.key = "keymanager"
+ self._keymanager_proxy = keymanager_proxy
+ self._signaler = signaler
+
+ def import_keys(self, username, filename):
+ """
+ Imports the username's key pair.
+ Those keys need to be ascii armored.
+
+ :param username: the user that will have the imported pair of keys.
+ :type username: str
+ :param filename: the name of the file where the key pair is stored.
+ :type filename: str
+ """
+ # NOTE: This feature is disabled right now since is dangerous
+ return
+
+ new_key = ''
+ signal = None
+ try:
+ with open(filename, 'r') as keys_file:
+ new_key = keys_file.read()
+ except IOError as e:
+ logger.error("IOError importing key. {0!r}".format(e))
+ signal = self._signaler.KEYMANAGER_IMPORT_IOERROR
+ self._signaler.signal(signal)
+ return
+
+ keymanager = self._keymanager_proxy
+ try:
+ public_key, private_key = keymanager.parse_openpgp_ascii_key(
+ new_key)
+ except (KeyAddressMismatch, KeyFingerprintMismatch) as e:
+ logger.error(repr(e))
+ signal = self._signaler.KEYMANAGER_IMPORT_DATAMISMATCH
+ self._signaler.signal(signal)
+ return
+
+ if public_key is None or private_key is None:
+ signal = self._signaler.KEYMANAGER_IMPORT_MISSINGKEY
+ self._signaler.signal(signal)
+ return
+
+ current_public_key = keymanager.get_key(username, openpgp.OpenPGPKey)
+ if public_key.address != current_public_key.address:
+ logger.error("The key does not match the ID")
+ signal = self._signaler.KEYMANAGER_IMPORT_ADDRESSMISMATCH
+ self._signaler.signal(signal)
+ return
+
+ keymanager.delete_key(self._key)
+ keymanager.delete_key(self._key_priv)
+ keymanager.put_key(public_key)
+ keymanager.put_key(private_key)
+ keymanager.send_key(openpgp.OpenPGPKey)
+
+ logger.debug('Import ok')
+ signal = self._signaler.KEYMANAGER_IMPORT_OK
+
+ self._signaler.signal(signal)
+
+ def 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
+ """
+ keymanager = self._keymanager_proxy
+
+ public_key = keymanager.get_key(username, openpgp.OpenPGPKey)
+ private_key = keymanager.get_key(username, openpgp.OpenPGPKey,
+ private=True)
+ try:
+ with open(filename, 'w') as keys_file:
+ keys_file.write(public_key.key_data)
+ keys_file.write(private_key.key_data)
+
+ logger.debug('Export ok')
+ self._signaler.signal(self._signaler.KEYMANAGER_EXPORT_OK)
+ except IOError as e:
+ logger.error("IOError exporting key. {0!r}".format(e))
+ self._signaler.signal(self._signaler.KEYMANAGER_EXPORT_ERROR)
+
+ def list_keys(self):
+ """
+ List all the keys stored in the local DB.
+ """
+ keys = self._keymanager_proxy.get_all_keys_in_local_db()
+ self._signaler.signal(self._signaler.KEYMANAGER_KEYS_LIST, keys)
+
+ def get_key_details(self, username):
+ """
+ List all the keys stored in the local DB.
+ """
+ public_key = self._keymanager_proxy.get_key(username,
+ openpgp.OpenPGPKey)
+ details = (public_key.key_id, public_key.fingerprint)
+ self._signaler.signal(self._signaler.KEYMANAGER_KEY_DETAILS, details)
+
+
+class Mail(object):
+ """
+ Interfaces with setup and launch of Mail.
+ """
+ # We give each service some time to come to a halt before forcing quit
+ SERVICE_STOP_TIMEOUT = 20
+
+ zope.interface.implements(ILEAPComponent)
+
+ def __init__(self, soledad_proxy, keymanager_proxy, signaler=None):
+ """
+ Constructor for the Mail component.
+
+ :param soledad_proxy: proxy to pass around a Soledad object.
+ :type soledad_proxy: zope.ProxyBase
+ :param keymanager_proxy: proxy to pass around a Keymanager object.
+ :type keymanager_proxy: zope.ProxyBase
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ """
+ self.key = "mail"
+ self._signaler = signaler
+ self._soledad_proxy = soledad_proxy
+ self._keymanager_proxy = keymanager_proxy
+ self._imap_controller = IMAPController(self._soledad_proxy,
+ self._keymanager_proxy)
+ self._smtp_bootstrapper = SMTPBootstrapper()
+ self._smtp_config = SMTPConfig()
+
+ def start_smtp_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
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ return threads.deferToThread(
+ self._smtp_bootstrapper.start_smtp_service,
+ self._keymanager_proxy, full_user_id, download_if_needed)
+
+ def start_imap_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
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ return threads.deferToThread(
+ self._imap_controller.start_imap_service,
+ full_user_id, offline)
+
+ def stop_smtp_service(self):
+ """
+ Stop the SMTP service.
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ return threads.deferToThread(self._smtp_bootstrapper.stop_smtp_service)
+
+ def _stop_imap_service(self):
+ """
+ Stop imap and wait until the service is stopped to signal that is done.
+ """
+ cv = Condition()
+ cv.acquire()
+ threads.deferToThread(self._imap_controller.stop_imap_service, cv)
+ logger.debug('Waiting for imap service to stop.')
+ cv.wait(self.SERVICE_STOP_TIMEOUT)
+ logger.debug('IMAP stopped')
+ self._signaler.signal(self._signaler.IMAP_STOPPED)
+
+ def stop_imap_service(self):
+ """
+ Stop imap service (fetcher, factory and port).
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ return threads.deferToThread(self._stop_imap_service)
+
+
+class Authenticate(object):
+ """
+ Interfaces with setup and bootstrapping operations for a provider
+ """
+
+ zope.interface.implements(ILEAPComponent)
+
+ def __init__(self, signaler=None):
+ """
+ Constructor for the Authenticate component
+
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ """
+ self.key = "authenticate"
+ self._signaler = signaler
+ self._login_defer = None
+ self._srp_auth = SRPAuth(ProviderConfig(), self._signaler)
+
+ def login(self, domain, username, password):
+ """
+ Execute the whole authentication process for a user
+
+ :param domain: the domain where we need to authenticate.
+ :type domain: unicode
+ :param username: username for this session
+ :type username: str
+ :param password: password for this user
+ :type password: str
+
+ :returns: the defer for the operation running in a thread.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ config = ProviderConfig.get_provider_config(domain)
+ if config is not None:
+ self._srp_auth = SRPAuth(config, self._signaler)
+ self._login_defer = self._srp_auth.authenticate(username, password)
+ return self._login_defer
+ else:
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.SRP_AUTH_ERROR)
+ logger.error("Could not load provider configuration.")
+
+ def cancel_login(self):
+ """
+ Cancel the ongoing login defer (if any).
+ """
+ d = self._login_defer
+ if d is not None:
+ d.cancel()
+
+ def change_password(self, current_password, new_password):
+ """
+ Change the user's password.
+
+ :param current_password: the current password of the user.
+ :type current_password: str
+ :param new_password: the new password for the user.
+ :type new_password: str
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ if not self._is_logged_in():
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR)
+ return
+
+ return self._srp_auth.change_password(current_password, new_password)
+
+ def logout(self):
+ """
+ Log out the current session.
+ Expects a session_id to exists, might raise AssertionError
+ """
+ if not self._is_logged_in():
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR)
+ return
+
+ self._srp_auth.logout()
+
+ def _is_logged_in(self):
+ """
+ Return whether the user is logged in or not.
+
+ :rtype: bool
+ """
+ return (self._srp_auth is not None and
+ self._srp_auth.is_authenticated())
+
+ def get_logged_in_status(self):
+ """
+ Signal if the user is currently logged in or not.
+ """
+ if self._signaler is None:
+ return
+
+ signal = None
+ if self._is_logged_in():
+ signal = self._signaler.SRP_STATUS_LOGGED_IN
+ else:
+ signal = self._signaler.SRP_STATUS_NOT_LOGGED_IN
+
+ self._signaler.signal(signal)