diff options
Diffstat (limited to 'src')
46 files changed, 1929 insertions, 1012 deletions
| diff --git a/src/leap/bitmask/__init__.py b/src/leap/bitmask/__init__.py index 0f733f26..9ec5aae7 100644 --- a/src/leap/bitmask/__init__.py +++ b/src/leap/bitmask/__init__.py @@ -25,6 +25,12 @@ from pkg_resources import parse_version  from leap.bitmask.util import first +# HACK: This is a hack so that py2app copies _scrypt.so to the right +# place, it can't be technically imported, but that doesn't matter +# because the import is never executed +if False: +    import _scrypt  # noqa - skip 'not used' warning +  def _is_release_version(version):      """ @@ -60,16 +66,16 @@ try:      IS_RELEASE_VERSION = _is_release_version(__version__)      del get_versions  except ImportError: -    #running on a tree that has not run -    #the setup.py setver +    # running on a tree that has not run +    # the setup.py setver      pass  __appname__ = "unknown"  try:      from leap.bitmask._appname import __appname__  except ImportError: -    #running on a tree that has not run -    #the setup.py setver +    # running on a tree that has not run +    # the setup.py setver      pass  __short_version__ = first(re.findall('\d+\.\d+\.\d+', __version__)) diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 6a7d6ff1..88f6bc15 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -39,56 +39,49 @@  # M:::::::::::~NMMM7???7MMMM:::::::::::::::::::::::NMMMI??I7MMMM:::::::::::::M  # M::::::::::::::7MMMMMMM+:::::::::::::::::::::::::::?MMMMMMMZ:::::::::::::::M  #                (thanks to: http://www.glassgiant.com/ascii/) -import signal -import sys +import multiprocessing  import os +import sys -from functools import partial -from PySide import QtCore, QtGui +from leap.bitmask.backend.utils import generate_certificates  from leap.bitmask import __version__ as VERSION  from leap.bitmask.config import flags -from leap.bitmask.gui import locale_rc  # noqa - silence pylint -from leap.bitmask.gui.mainwindow import MainWindow +from leap.bitmask.frontend_app import run_frontend +from leap.bitmask.backend_app import run_backend  from leap.bitmask.logs.utils import create_logger  from leap.bitmask.platform_init.locks import we_are_the_one_and_only  from leap.bitmask.services.mail import plumber -from leap.bitmask.util import leap_argparse +from leap.bitmask.util import leap_argparse, flags_to_dict  from leap.bitmask.util.requirement_checker import check_requirements  from leap.common.events import server as event_server  from leap.mail import __version__ as MAIL_VERSION -from twisted.internet import reactor -from twisted.internet.task import LoopingCall -  import codecs  codecs.register(lambda name: codecs.lookup('utf-8')                  if name == 'cp65001' else None) - -def sigint_handler(*args, **kwargs): -    """ -    Signal handler for SIGINT -    """ -    logger = kwargs.get('logger', None) -    if logger: -        logger.debug("SIGINT catched. shutting down...") -    mainwindow = args[0] -    mainwindow.quit() +import psutil -def sigterm_handler(*args, **kwargs): +def kill_the_children():      """ -    Signal handler for SIGTERM. -    This handler is actually passed to twisted reactor +    Make sure no lingering subprocesses are left in case of a bad termination.      """ -    logger = kwargs.get('logger', None) -    if logger: -        logger.debug("SIGTERM catched. shutting down...") -    mainwindow = args[0] -    mainwindow.quit() +    me = os.getpid() +    parent = psutil.Process(me) +    print "Killing all the children processes..." +    for child in parent.get_children(recursive=True): +        try: +            child.terminate() +        except Exception as exc: +            print exc + +# XXX This is currently broken, but we need to fix it to avoid +# orphaned processes in case of a crash. +# atexit.register(kill_the_children)  def do_display_version(opts): @@ -116,16 +109,22 @@ def do_mail_plumbing(opts):      # XXX catch when import is used w/o acct -def main(): +def start_app():      """      Starts the main event loop and launches the main window.      """ +    # Ignore the signals since we handle them in the subprocesses +    # signal.signal(signal.SIGINT, signal.SIG_IGN) +      # Parse arguments and store them      opts = leap_argparse.get_options()      do_display_version(opts) -    bypass_checks = opts.danger -    start_hidden = opts.start_hidden +    options = { +        'start_hidden': opts.start_hidden, +        'debug': opts.debug, +        'log_file': opts.log_file, +    }      flags.STANDALONE = opts.standalone      flags.OFFLINE = opts.offline @@ -177,59 +176,17 @@ def main():      logger.info('Starting app') -    # We force the style if on KDE so that it doesn't load all the kde -    # libs, which causes a compatibility issue in some systems. -    # For more info, see issue #3194 -    if flags.STANDALONE and os.environ.get("KDE_SESSION_UID") is not None: -        sys.argv.append("-style") -        sys.argv.append("Cleanlooks") - -    app = QtGui.QApplication(sys.argv) - -    # To test: -    # $ LANG=es ./app.py -    locale = QtCore.QLocale.system().name() -    qtTranslator = QtCore.QTranslator() -    if qtTranslator.load("qt_%s" % locale, ":/translations"): -        app.installTranslator(qtTranslator) -    appTranslator = QtCore.QTranslator() -    if appTranslator.load("%s.qm" % locale[:2], ":/translations"): -        app.installTranslator(appTranslator) - -    # Needed for initializing qsettings it will write -    # .config/leap/leap.conf top level app settings in a platform -    # independent way -    app.setOrganizationName("leap") -    app.setApplicationName("leap") -    app.setOrganizationDomain("leap.se") - -    # XXX --------------------------------------------------------- -    # In quarantine, looks like we don't need it anymore. -    # This dummy timer ensures that control is given to the outside -    # loop, so we can hook our sigint handler. -    #timer = QtCore.QTimer() -    #timer.start(500) -    #timer.timeout.connect(lambda: None) -    # XXX --------------------------------------------------------- - -    window = MainWindow(bypass_checks=bypass_checks, -                        start_hidden=start_hidden) - -    sigint_window = partial(sigint_handler, window, logger=logger) -    signal.signal(signal.SIGINT, sigint_window) - -    # callable used in addSystemEventTrigger to handle SIGTERM -    sigterm_window = partial(sigterm_handler, window, logger=logger) - -    l = LoopingCall(QtCore.QCoreApplication.processEvents, 0, 10) -    l.start(0.01) - -    # SIGTERM can't be handled the same way SIGINT is, since it's -    # caught by twisted. See _handleSignals method in -    # twisted/internet/base.py#L1150. So, addSystemEventTrigger -    # reactor's method is used. -    reactor.addSystemEventTrigger('before', 'shutdown', sigterm_window) -    reactor.run() +    generate_certificates() + +    flags_dict = flags_to_dict() + +    backend = lambda: run_backend(opts.danger, flags_dict) +    backend_process = multiprocessing.Process(target=backend, name='Backend') +    backend_process.daemon = True +    backend_process.start() + +    run_frontend(options, flags_dict) +  if __name__ == "__main__": -    main() +    start_app() diff --git a/src/leap/bitmask/backend/api.py b/src/leap/bitmask/backend/api.py new file mode 100644 index 00000000..b8533f36 --- /dev/null +++ b/src/leap/bitmask/backend/api.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# api.py +# Copyright (C) 2013, 2014 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 available API and SIGNALS definition. +""" +STOP_REQUEST = "stop" + +API = ( +    STOP_REQUEST,  # this method needs to be defined in order to support the +                   # backend stop action + +    "eip_can_start", +    "eip_cancel_setup", +    "eip_check_dns", +    "eip_get_gateway_country_code", +    "eip_get_gateways_list", +    "eip_get_initialized_providers", +    "eip_setup", +    "eip_start", +    "eip_stop", +    "eip_terminate", +    "imap_start_service", +    "imap_stop_service", +    "keymanager_export_keys", +    "keymanager_get_key_details", +    "keymanager_list_keys", +    "provider_bootstrap", +    "provider_cancel_setup", +    "provider_get_all_services", +    "provider_get_details", +    "provider_get_pinned_providers", +    "provider_get_supported_services", +    "provider_setup", +    "settings_set_selected_gateway", +    "smtp_start_service", +    "smtp_stop_service", +    "soledad_bootstrap", +    "soledad_cancel_bootstrap", +    "soledad_change_password", +    "soledad_close", +    "soledad_load_offline", +    "tear_fw_down", +    "user_cancel_login", +    "user_change_password", +    "user_get_logged_in_status", +    "user_login", +    "user_logout", +    "user_register", +) + + +SIGNALS = ( +    "backend_bad_call", +    "eip_alien_openvpn_already_running", +    "eip_can_start", +    "eip_cancelled_setup", +    "eip_cannot_start", +    "eip_client_certificate_ready", +    "eip_config_ready", +    "eip_connected", +    "eip_connection_aborted", +    "eip_connection_died", +    "eip_disconnected", +    "eip_dns_error", +    "eip_dns_ok", +    "eip_get_gateway_country_code", +    "eip_get_gateways_list", +    "eip_get_gateways_list_error", +    "eip_get_initialized_providers", +    "eip_network_unreachable", +    "eip_no_gateway", +    "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", +    "eip_status_changed", +    "eip_stopped", +    "eip_tear_fw_down", +    "eip_uninitialized_provider", +    "eip_vpn_launcher_exception", +    "imap_stopped", +    "keymanager_export_error", +    "keymanager_export_ok", +    "keymanager_import_addressmismatch", +    "keymanager_import_datamismatch", +    "keymanager_import_ioerror", +    "keymanager_import_missingkey", +    "keymanager_import_ok", +    "keymanager_key_details", +    "keymanager_keys_list", +    "prov_cancelled_setup", +    "prov_check_api_certificate", +    "prov_check_ca_fingerprint", +    "prov_download_ca_cert", +    "prov_download_provider_info", +    "prov_get_all_services", +    "prov_get_details", +    "prov_get_pinned_providers", +    "prov_get_supported_services", +    "prov_https_connection", +    "prov_name_resolution", +    "prov_problem_with_provider", +    "prov_unsupported_api", +    "prov_unsupported_client", +    "soledad_bootstrap_failed", +    "soledad_bootstrap_finished", +    "soledad_cancelled_bootstrap", +    "soledad_invalid_auth_token", +    "soledad_offline_failed", +    "soledad_offline_finished", +    "soledad_password_change_error", +    "soledad_password_change_ok", +    "srp_auth_bad_user_or_password", +    "srp_auth_connection_error", +    "srp_auth_error", +    "srp_auth_ok", +    "srp_auth_server_error", +    "srp_logout_error", +    "srp_logout_ok", +    "srp_not_logged_in_error", +    "srp_password_change_badpw", +    "srp_password_change_error", +    "srp_password_change_ok", +    "srp_registration_failed", +    "srp_registration_finished", +    "srp_registration_taken", +    "srp_status_logged_in", +    "srp_status_not_logged_in", +) diff --git a/src/leap/bitmask/backend/backend.py b/src/leap/bitmask/backend/backend.py new file mode 100644 index 00000000..833f4368 --- /dev/null +++ b/src/leap/bitmask/backend/backend.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# backend.py +# Copyright (C) 2013, 2014 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/>. +import json +import threading +import time + +from twisted.internet import defer, reactor, threads + +import zmq +from zmq.auth.thread import ThreadAuthenticator + +from leap.bitmask.backend.api import API +from leap.bitmask.backend.utils import get_backend_certificates +from leap.bitmask.backend.signaler import Signaler + +import logging +logger = logging.getLogger(__name__) + + +class Backend(object): +    """ +    Backend server. +    Receives signals from backend_proxy and emit signals if needed. +    """ +    PORT = '5556' +    BIND_ADDR = "tcp://127.0.0.1:%s" % PORT + +    def __init__(self): +        """ +        Backend constructor, create needed instances. +        """ +        self._signaler = Signaler() + +        self._do_work = threading.Event()  # used to stop the worker thread. +        self._zmq_socket = None + +        self._ongoing_defers = [] +        self._init_zmq() + +    def _init_zmq(self): +        """ +        Configure the zmq components and connection. +        """ +        context = zmq.Context() +        socket = context.socket(zmq.REP) + +        # Start an authenticator for this context. +        auth = ThreadAuthenticator(context) +        auth.start() +        auth.allow('127.0.0.1') + +        # Tell authenticator to use the certificate in a directory +        auth.configure_curve(domain='*', location=zmq.auth.CURVE_ALLOW_ANY) +        public, secret = get_backend_certificates() +        socket.curve_publickey = public +        socket.curve_secretkey = secret +        socket.curve_server = True  # must come before bind + +        socket.bind(self.BIND_ADDR) + +        self._zmq_socket = socket + +    def _worker(self): +        """ +        Receive requests and send it to process. + +        Note: we use a simple while since is less resource consuming than a +        Twisted's LoopingCall. +        """ +        while self._do_work.is_set(): +            # Wait for next request from client +            try: +                request = self._zmq_socket.recv(zmq.NOBLOCK) +                self._zmq_socket.send("OK") +                # logger.debug("Received request: '{0}'".format(request)) +                self._process_request(request) +            except zmq.ZMQError as e: +                if e.errno != zmq.EAGAIN: +                    raise +            time.sleep(0.01) + +    def _stop_reactor(self): +        """ +        Stop the Twisted reactor, but first wait a little for some threads to +        complete their work. + +        Note: this method needs to be run in a different thread so the +        time.sleep() does not block and other threads can finish. +        i.e.: +            use threads.deferToThread(this_method) instead of this_method() +        """ +        wait_max = 5  # seconds +        wait_step = 0.5 +        wait = 0 +        while self._ongoing_defers and wait < wait_max: +            time.sleep(wait_step) +            wait += wait_step +            msg = "Waiting for running threads to finish... {0}/{1}" +            msg = msg.format(wait, wait_max) +            logger.debug(msg) + +        # after a timeout we shut down the existing threads. +        for d in self._ongoing_defers: +            d.cancel() + +        reactor.stop() +        logger.debug("Twisted reactor stopped.") + +    def run(self): +        """ +        Start the ZMQ server and run the loop to handle requests. +        """ +        self._signaler.start() +        self._do_work.set() +        threads.deferToThread(self._worker) +        reactor.run() + +    def stop(self): +        """ +        Stop the server and the zmq request parse loop. +        """ +        logger.debug("STOP received.") +        self._signaler.stop() +        self._do_work.clear() +        threads.deferToThread(self._stop_reactor) + +    def _process_request(self, request_json): +        """ +        Process a request and call the according method with the given +        parameters. + +        :param request_json: a json specification of a request. +        :type request_json: str +        """ +        try: +            # request = zmq.utils.jsonapi.loads(request_json) +            # We use stdlib's json to ensure that we get unicode strings +            request = json.loads(request_json) +            api_method = request['api_method'] +            kwargs = request['arguments'] or None +        except Exception as e: +            msg = "Malformed JSON data in Backend request '{0}'. Exc: {1!r}" +            msg = msg.format(request_json, e) +            msg = msg.format(request_json) +            logger.critical(msg) +            raise + +        if api_method not in API: +            logger.error("Invalid API call '{0}'".format(api_method)) +            return + +        self._run_in_thread(api_method, kwargs) + +    def _run_in_thread(self, api_method, kwargs): +        """ +        Run the method name in a thread with the given arguments. + +        :param api_method: the callable name to run in a thread. +        :type api_method: str +        :param kwargs: the arguments dict that will be sent to the callable. +        :type kwargs: tuple +        """ +        func = getattr(self, api_method) + +        method = func +        if kwargs is not None: +            method = lambda: func(**kwargs) + +        # logger.debug("Running method: '{0}' " +        #            "with args: '{1}' in a thread".format(api_method, kwargs)) + +        # run the action in a thread and keep track of it +        d = threads.deferToThread(method) +        d.addCallback(self._done_action, d) +        d.addErrback(self._done_action, d) +        self._ongoing_defers.append(d) + +    def _done_action(self, failure, d): +        """ +        Remove the defer from the ongoing list. + +        :param failure: the failure that triggered the errback. +                        None if no error. +        :type failure: twisted.python.failure.Failure +        :param d: defer to remove +        :type d: twisted.internet.defer.Deferred +        """ +        if failure is not None: +            if failure.check(defer.CancelledError): +                logger.debug("A defer was cancelled.") +            else: +                logger.error("There was a failure - {0!r}".format(failure)) +                logger.error(failure.getTraceback()) + +        if d in self._ongoing_defers: +            self._ongoing_defers.remove(d) diff --git a/src/leap/bitmask/backend/backend_proxy.py b/src/leap/bitmask/backend/backend_proxy.py new file mode 100644 index 00000000..f683e465 --- /dev/null +++ b/src/leap/bitmask/backend/backend_proxy.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# backend_proxy.py +# Copyright (C) 2013, 2014 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/>. +""" +The BackendProxy handles calls from the GUI and forwards (through ZMQ) +to the backend. +""" +import functools +import Queue +import threading +import time + +import zmq + +from leap.bitmask.backend.api import API, STOP_REQUEST +from leap.bitmask.backend.utils import get_backend_certificates + +import logging +logger = logging.getLogger(__name__) + + +class BackendProxy(object): +    """ +    The BackendProxy handles calls from the GUI and forwards (through ZMQ) +    to the backend. +    """ +    PORT = '5556' +    SERVER = "tcp://localhost:%s" % PORT + +    def __init__(self): +        self._socket = None + +        # initialize ZMQ stuff: +        context = zmq.Context() +        logger.debug("Connecting to server...") +        socket = context.socket(zmq.REQ) + +        # public, secret = zmq.curve_keypair() +        client_keys = zmq.curve_keypair() +        socket.curve_publickey = client_keys[0] +        socket.curve_secretkey = client_keys[1] + +        # The client must know the server's public key to make a CURVE +        # connection. +        public, _ = get_backend_certificates() +        socket.curve_serverkey = public + +        socket.setsockopt(zmq.RCVTIMEO, 1000) +        socket.connect(self.SERVER) +        self._socket = socket + +        self._call_queue = Queue.Queue() +        self._worker_caller = threading.Thread(target=self._worker) +        self._worker_caller.start() + +    def _worker(self): +        """ +        Worker loop that processes the Queue of pending requests to do. +        """ +        while True: +            try: +                request = self._call_queue.get(block=False) +                # break the loop after sending the 'stop' action to the +                # backend. +                if request == STOP_REQUEST: +                    break + +                self._send_request(request) +            except Queue.Empty: +                pass +            time.sleep(0.01) + +        logger.debug("BackendProxy worker stopped.") + +    def _api_call(self, *args, **kwargs): +        """ +        Call the `api_method` method in backend (through zmq). + +        :param kwargs: named arguments to forward to the backend api method. +        :type kwargs: dict + +        Note: is mandatory to have the kwarg 'api_method' defined. +        """ +        if args: +            # Use a custom message to be more clear about using kwargs *only* +            raise Exception("All arguments need to be kwargs!") + +        api_method = kwargs.pop('api_method', None) +        if api_method is None: +            raise Exception("Missing argument, no method name specified.") + +        request = { +            'api_method': api_method, +            'arguments': kwargs, +        } + +        try: +            request_json = zmq.utils.jsonapi.dumps(request) +        except Exception as e: +            msg = ("Error serializing request into JSON.\n" +                   "Exception: {0} Data: {1}") +            msg = msg.format(e, request) +            logger.critical(msg) +            raise + +        # queue the call in order to handle the request in a thread safe way. +        self._call_queue.put(request_json) + +        if api_method == STOP_REQUEST: +            self._call_queue.put(STOP_REQUEST) + +    def _send_request(self, request): +        """ +        Send the given request to the server. +        This is used from a thread safe loop in order to avoid sending a +        request without receiving a response from a previous one. + +        :param request: the request to send. +        :type request: str +        """ +        # logger.debug("Sending request to backend: {0}".format(request)) +        self._socket.send(request) + +        try: +            # Get the reply. +            self._socket.recv() +            # response = self._socket.recv() +            # msg = "Received reply for '{0}' -> '{1}'" +            # msg = msg.format(request, response) +            # logger.debug(msg) +        except zmq.error.Again as e: +            msg = "Timeout error contacting backend. {0!r}".format(e) +            logger.critical(msg) + +    def __getattribute__(self, name): +        """ +        This allows the user to do: +            bp = BackendProxy() +            bp.some_method() + +        Just by having defined 'some_method' in the API + +        :param name: the attribute name that is requested. +        :type name: str +        """ +        if name in API: +            return functools.partial(self._api_call, api_method=name) +        else: +            return object.__getattribute__(self, name) diff --git a/src/leap/bitmask/backend/components.py b/src/leap/bitmask/backend/components.py index 19fcf283..b372db89 100644 --- a/src/leap/bitmask/backend/components.py +++ b/src/leap/bitmask/backend/components.py @@ -31,6 +31,7 @@ from twisted.python import log  import zope.interface  import zope.proxy +from leap.bitmask.backend.settings import Settings, GATEWAY_AUTOMATIC  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.srpauth import SRPAuth  from leap.bitmask.crypto.srpregister import SRPRegister @@ -197,7 +198,7 @@ class Provider(object):          else:              if self._signaler is not None:                  self._signaler.signal( -                    self._signaler.PROV_PROBLEM_WITH_PROVIDER_KEY) +                    self._signaler.prov_problem_with_provider)              logger.error("Could not load provider configuration.")              self._login_widget.set_enabled(True) @@ -234,7 +235,7 @@ class Provider(object):          services = get_supported(self._get_services(domain))          self._signaler.signal( -            self._signaler.PROV_GET_SUPPORTED_SERVICES, services) +            self._signaler.prov_get_supported_services, services)      def get_all_services(self, providers):          """ @@ -253,7 +254,7 @@ class Provider(object):              services_all = services_all.union(set(services))          self._signaler.signal( -            self._signaler.PROV_GET_ALL_SERVICES, services_all) +            self._signaler.prov_get_all_services, list(services_all))      def get_details(self, domain, lang=None):          """ @@ -268,7 +269,7 @@ class Provider(object):              prov_get_details -> dict          """          self._signaler.signal( -            self._signaler.PROV_GET_DETAILS, +            self._signaler.prov_get_details,              self._provider_config.get_light_config(domain, lang))      def get_pinned_providers(self): @@ -279,7 +280,7 @@ class Provider(object):              prov_get_pinned_providers -> list of provider domains          """          self._signaler.signal( -            self._signaler.PROV_GET_PINNED_PROVIDERS, +            self._signaler.prov_get_pinned_providers,              PinnedProviders.domains()) @@ -324,7 +325,7 @@ class Register(object):                  partial(srpregister.register_user, username, password))          else:              if self._signaler is not None: -                self._signaler.signal(self._signaler.SRP_REGISTRATION_FAILED) +                self._signaler.signal(self._signaler.srp_registration_failed)              logger.error("Could not load provider configuration.") @@ -401,12 +402,12 @@ class EIP(object):          if not self._can_start(domain):              if self._signaler is not None: -                self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED) +                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) +                self._signaler.signal(self._signaler.eip_connection_aborted)              logger.error("Tried to start EIP but cannot find any "                           "available provider!")              return @@ -425,28 +426,28 @@ class EIP(object):          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(), " +            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) +            signaler.signal(signaler.eip_openvpn_already_running)          except vpnprocess.AlienOpenVPNAlreadyRunning: -            signaler.signal(signaler.EIP_ALIEN_OPENVPN_ALREADY_RUNNING) +            signaler.signal(signaler.eip_alien_openvpn_already_running)          except vpnlauncher.OpenVPNNotFoundException: -            signaler.signal(signaler.EIP_OPENVPN_NOT_FOUND_ERROR) +            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) +            signaler.signal(signaler.eip_vpn_launcher_exception)          except linuxvpnlauncher.EIPNoPolkitAuthAgentAvailable: -            signaler.signal(signaler.EIP_NO_POLKIT_AGENT_ERROR) +            signaler.signal(signaler.eip_no_polkit_agent_error)          except linuxvpnlauncher.EIPNoPkexecAvailable: -            signaler.signal(signaler.EIP_NO_PKEXEC_ERROR) +            signaler.signal(signaler.eip_no_pkexec_error)          except darwinvpnlauncher.EIPNoTunKextLoaded: -            signaler.signal(signaler.EIP_NO_TUN_KEXT_ERROR) +            signaler.signal(signaler.eip_no_tun_kext_error)          except Exception as e:              logger.error("Unexpected problem: {0!r}".format(e))          else: @@ -482,12 +483,12 @@ class EIP(object):          while retry <= MAX_FW_WAIT_RETRIES:              if self._vpn.is_fw_down(): -                self._signaler.signal(self._signaler.EIP_STOPPED) +                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) +                # 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... " @@ -542,7 +543,7 @@ class EIP(object):              filtered_domains.append((domain, is_initialized))          if self._signaler is not None: -            self._signaler.signal(self._signaler.EIP_GET_INITIALIZED_PROVIDERS, +            self._signaler.signal(self._signaler.eip_get_initialized_providers,                                    filtered_domains)      def tear_fw_down(self): @@ -566,7 +567,7 @@ class EIP(object):          if not self._provider_is_initialized(domain):              if self._signaler is not None:                  self._signaler.signal( -                    self._signaler.EIP_UNINITIALIZED_PROVIDER) +                    self._signaler.eip_uninitialized_provider)              return          eip_config = eipconfig.EIPConfig() @@ -580,14 +581,55 @@ class EIP(object):          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) +                    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) +                self._signaler.eip_get_gateways_list, gateways) + +    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 = ccodes[gateways[0]] + +        self._signaler.signal(self._signaler.eip_get_gateway_country_code, +                              gateway_ccode)      def _can_start(self, domain):          """ @@ -607,7 +649,8 @@ class EIP(object):          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") +            logger.error("Cannot start OpenVPN, binary not found: %s" % +                         (ovpn_path,))              return False          # check for other problems @@ -643,10 +686,10 @@ class EIP(object):          """          if self._can_start(domain):              if self._signaler is not None: -                self._signaler.signal(self._signaler.EIP_CAN_START) +                self._signaler.signal(self._signaler.eip_can_start)          else:              if self._signaler is not None: -                self._signaler.signal(self._signaler.EIP_CANNOT_START) +                self._signaler.signal(self._signaler.eip_cannot_start)      def check_dns(self, domain):          """ @@ -665,7 +708,7 @@ class EIP(object):              """              Callback handler for `do_check`.              """ -            self._signaler.signal(self._signaler.EIP_DNS_OK) +            self._signaler.signal(self._signaler.eip_dns_ok)              logger.debug("DNS check OK")          def check_err(failure): @@ -677,7 +720,7 @@ class EIP(object):              """              logger.debug("Can't resolve hostname. {0!r}".format(failure)) -            self._signaler.signal(self._signaler.EIP_DNS_ERROR) +            self._signaler.signal(self._signaler.eip_dns_error)              # python 2.7.4 raises socket.error              # python 2.7.5 raises socket.gaierror @@ -737,7 +780,7 @@ class Soledad(object):              self._soledad_defer.addCallback(self._set_proxies_cb)          else:              if self._signaler is not None: -                self._signaler.signal(self._signaler.SOLEDAD_BOOTSTRAP_FAILED) +                self._signaler.signal(self._signaler.soledad_bootstrap_failed)              logger.error("Could not load provider configuration.")          return self._soledad_defer @@ -793,7 +836,7 @@ class Soledad(object):          Password change callback.          """          if self._signaler is not None: -            self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_OK) +            self._signaler.signal(self._signaler.soledad_password_change_ok)      def _change_password_error(self, failure):          """ @@ -808,7 +851,7 @@ class Soledad(object):              logger.error("Passphrase too short.")          if self._signaler is not None: -            self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_ERROR) +            self._signaler.signal(self._signaler.soledad_password_change_error)      def change_password(self, new_password):          """ @@ -866,7 +909,7 @@ class Keymanager(object):                  new_key = keys_file.read()          except IOError as e:              logger.error("IOError importing key. {0!r}".format(e)) -            signal = self._signaler.KEYMANAGER_IMPORT_IOERROR +            signal = self._signaler.keymanager_import_ioerror              self._signaler.signal(signal)              return @@ -876,19 +919,19 @@ class Keymanager(object):                  new_key)          except (KeyAddressMismatch, KeyFingerprintMismatch) as e:              logger.error(repr(e)) -            signal = self._signaler.KEYMANAGER_IMPORT_DATAMISMATCH +            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 +            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 +            signal = self._signaler.keymanager_import_addressmismatch              self._signaler.signal(signal)              return @@ -899,7 +942,7 @@ class Keymanager(object):          keymanager.send_key(openpgp.OpenPGPKey)          logger.debug('Import ok') -        signal = self._signaler.KEYMANAGER_IMPORT_OK +        signal = self._signaler.keymanager_import_ok          self._signaler.signal(signal) @@ -923,17 +966,17 @@ class Keymanager(object):                  keys_file.write(private_key.key_data)              logger.debug('Export ok') -            self._signaler.signal(self._signaler.KEYMANAGER_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) +            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) +        self._signaler.signal(self._signaler.keymanager_keys_list, keys)      def get_key_details(self, username):          """ @@ -942,7 +985,7 @@ class Keymanager(object):          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) +        self._signaler.signal(self._signaler.keymanager_key_details, details)  class Mail(object): @@ -1027,7 +1070,7 @@ class Mail(object):          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) +        self._signaler.signal(self._signaler.imap_stopped)      def stop_imap_service(self):          """ @@ -1080,7 +1123,7 @@ class Authenticate(object):              return self._login_defer          else:              if self._signaler is not None: -                self._signaler.signal(self._signaler.SRP_AUTH_ERROR) +                self._signaler.signal(self._signaler.srp_auth_error)              logger.error("Could not load provider configuration.")      def cancel_login(self): @@ -1105,7 +1148,7 @@ class Authenticate(object):          """          if not self._is_logged_in():              if self._signaler is not None: -                self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) +                self._signaler.signal(self._signaler.srp_not_logged_in_error)              return          return self._srp_auth.change_password(current_password, new_password) @@ -1117,7 +1160,7 @@ class Authenticate(object):          """          if not self._is_logged_in():              if self._signaler is not None: -                self._signaler.signal(self._signaler.SRP_NOT_LOGGED_IN_ERROR) +                self._signaler.signal(self._signaler.srp_not_logged_in_error)              return          self._srp_auth.logout() @@ -1140,8 +1183,8 @@ class Authenticate(object):          signal = None          if self._is_logged_in(): -            signal = self._signaler.SRP_STATUS_LOGGED_IN +            signal = self._signaler.srp_status_logged_in          else: -            signal = self._signaler.SRP_STATUS_NOT_LOGGED_IN +            signal = self._signaler.srp_status_not_logged_in          self._signaler.signal(signal) diff --git a/src/leap/bitmask/backend/leapbackend.py b/src/leap/bitmask/backend/leapbackend.py index 3c5222f4..d3c4fcda 100644 --- a/src/leap/bitmask/backend/leapbackend.py +++ b/src/leap/bitmask/backend/leapbackend.py @@ -1,6 +1,6 @@  # -*- coding: utf-8 -*-  # leapbackend.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -15,178 +15,65 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  """ -Backend for GUI/Logic communication. +Backend for everything  """  import logging -from Queue import Queue, Empty - -from twisted.internet import reactor -from twisted.internet import threads, defer -from twisted.internet.task import LoopingCall -  import zope.interface  import zope.proxy -from leap.bitmask.backend.leapsignaler import Signaler  from leap.bitmask.backend import components +from leap.bitmask.backend.backend import Backend +from leap.bitmask.backend.settings import Settings  logger = logging.getLogger(__name__) +ERROR_KEY = "error" +PASSED_KEY = "passed" + -class Backend(object): +class LeapBackend(Backend):      """ -    Backend for everything, the UI should only use this class. +    Backend server subclass, used to implement the API methods.      """ - -    PASSED_KEY = "passed" -    ERROR_KEY = "error" -      def __init__(self, bypass_checks=False):          """          Constructor for the backend.          """ -        # Components map for the commands received -        self._components = {} - -        # Ongoing defers that will be cancelled at stop time -        self._ongoing_defers = [] +        Backend.__init__(self) -        # Signaler object to translate commands into Qt signals -        self._signaler = Signaler() +        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 registration -        self._register(components.Provider(self._signaler, bypass_checks)) -        self._register(components.Register(self._signaler)) -        self._register(components.Authenticate(self._signaler)) -        self._register(components.EIP(self._signaler)) -        self._register(components.Soledad(self._soledad_proxy, -                                          self._keymanager_proxy, -                                          self._signaler)) -        self._register(components.Keymanager(self._keymanager_proxy, -                                             self._signaler)) -        self._register(components.Mail(self._soledad_proxy, -                                       self._keymanager_proxy, -                                       self._signaler)) - -        # We have a looping call on a thread executing all the -        # commands in queue. Right now this queue is an actual Queue -        # object, but it'll become the zmq recv_multipart queue -        self._lc = LoopingCall(threads.deferToThread, self._worker) - -        # Temporal call_queue for worker, will be replaced with -        # recv_multipart os something equivalent in the loopingcall -        self._call_queue = Queue() - -    @property -    def signaler(self): -        """ -        Public signaler access to let the UI connect to its signals. -        """ -        return self._signaler - -    def start(self): -        """ -        Starts the looping call -        """ -        logger.debug("Starting worker...") -        self._lc.start(0.01) - -    def stop(self): -        """ -        Stops the looping call and tries to cancel all the defers. -        """ -        reactor.callLater(2, self._stop) - -    def _stop(self): -        """ -        Delayed stopping of worker. Called from `stop`. -        """ -        logger.debug("Stopping worker...") -        if self._lc.running: -            self._lc.stop() -        else: -            logger.warning("Looping call is not running, cannot stop") - -        logger.debug("Cancelling ongoing defers...") -        while len(self._ongoing_defers) > 0: -            d = self._ongoing_defers.pop() -            d.cancel() -        logger.debug("Defers cancelled.") - -    def _register(self, component): -        """ -        Registers a component in this backend - -        :param component: Component to register -        :type component: any object that implements ILEAPComponent -        """ -        # TODO: assert that the component implements the interfaces -        # expected -        try: -            self._components[component.key] = component -        except Exception: -            logger.error("There was a problem registering %s" % (component,)) - -    def _signal_back(self, _, signal): -        """ -        Helper method to signal back (callback like behavior) to the -        UI that an operation finished. - -        :param signal: signal name -        :type signal: str -        """ -        self._signaler.signal(signal) - -    def _worker(self): -        """ -        Worker method, called from a different thread and as a part of -        a looping call -        """ -        try: -            # this'll become recv_multipart -            cmd = self._call_queue.get(block=False) - -            # cmd is: component, method, signalback, *args -            func = getattr(self._components[cmd[0]], cmd[1]) -            d = func(*cmd[3:]) -            if d is not None:  # d may be None if a defer chain is cancelled. -                # A call might not have a callback signal, but if it does, -                # we add it to the chain -                if cmd[2] is not None: -                    d.addCallbacks(self._signal_back, logger.error, cmd[2]) -                d.addCallbacks(self._done_action, logger.error, -                               callbackKeywords={"d": d}) -                d.addErrback(logger.error) -                self._ongoing_defers.append(d) -        except Empty: -            # If it's just empty we don't have anything to do. -            pass -        except defer.CancelledError: -            logger.debug("defer cancelled somewhere (CancelledError).") -        except Exception as e: -            # But we log the rest -            logger.exception("Unexpected exception: {0!r}".format(e)) +        # 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 _done_action(self, _, d): +    def _check_type(self, obj, expected_type):          """ -        Remover of the defer once it's done +        Check the type of a parameter. -        :param d: defer to remove -        :type d: twisted.internet.defer.Deferred +        :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 d in self._ongoing_defers: -            self._ongoing_defers.remove(d) - -    # XXX: Temporal interface until we migrate to zmq -    # We simulate the calls to zmq.send_multipart. Once we separate -    # this in two processes, the methods bellow can be changed to -    # send_multipart and this backend class will be really simple. +        if not isinstance(obj, expected_type): +            raise TypeError("The parameter type is incorrect.")      def provider_setup(self, provider):          """ @@ -202,13 +89,13 @@ class Backend(object):              prov_https_connection       -> { PASSED_KEY: bool, ERROR_KEY: str }              prov_download_provider_info -> { PASSED_KEY: bool, ERROR_KEY: str }          """ -        self._call_queue.put(("provider", "setup_provider", None, provider)) +        self._provider.setup_provider(provider)      def provider_cancel_setup(self):          """          Cancel the ongoing setup provider (if any).          """ -        self._call_queue.put(("provider", "cancel_setup_provider", None)) +        self._provider.cancel_setup_provider()      def provider_bootstrap(self, provider):          """ @@ -223,7 +110,7 @@ class Backend(object):              prov_check_ca_fingerprint  -> {PASSED_KEY: bool, ERROR_KEY: str}              prov_check_api_certificate -> {PASSED_KEY: bool, ERROR_KEY: str}          """ -        self._call_queue.put(("provider", "bootstrap", None, provider)) +        self._provider.bootstrap(provider)      def provider_get_supported_services(self, domain):          """ @@ -235,8 +122,7 @@ class Backend(object):          Signals:              prov_get_supported_services -> list of unicode          """ -        self._call_queue.put(("provider", "get_supported_services", None, -                              domain)) +        self._provider.get_supported_services(domain)      def provider_get_all_services(self, providers):          """ @@ -248,13 +134,11 @@ class Backend(object):          Signals:              prov_get_all_services -> list of unicode          """ -        self._call_queue.put(("provider", "get_all_services", None, -                              providers)) +        self._provider.get_all_services(providers)      def provider_get_details(self, domain, lang):          """ -        Signal a ProviderConfigLight object with the current ProviderConfig -        settings. +        Signal a dict with the current ProviderConfig settings.          :param domain: the domain name of the provider.          :type domain: str @@ -262,9 +146,9 @@ class Backend(object):          :type lang: str          Signals: -            prov_get_details -> ProviderConfigLight +            prov_get_details -> dict          """ -        self._call_queue.put(("provider", "get_details", None, domain, lang)) +        self._provider.get_details(domain, lang)      def provider_get_pinned_providers(self):          """ @@ -273,7 +157,7 @@ class Backend(object):          Signals:              prov_get_pinned_providers -> list of provider domains          """ -        self._call_queue.put(("provider", "get_pinned_providers", None)) +        self._provider.get_pinned_providers()      def user_register(self, provider, username, password):          """ @@ -291,8 +175,7 @@ class Backend(object):              srp_registration_taken              srp_registration_failed          """ -        self._call_queue.put(("register", "register_user", None, provider, -                              username, password)) +        self._register.register_user(provider, username, password)      def eip_setup(self, provider, skip_network=False):          """ @@ -309,14 +192,13 @@ class Backend(object):              eip_client_certificate_ready -> {PASSED_KEY: bool, ERROR_KEY: str}              eip_cancelled_setup          """ -        self._call_queue.put(("eip", "setup_eip", None, provider, -                              skip_network)) +        self._eip.setup_eip(provider, skip_network)      def eip_cancel_setup(self):          """          Cancel the ongoing setup EIP (if any).          """ -        self._call_queue.put(("eip", "cancel_setup_eip", None)) +        self._eip.cancel_setup_eip()      def eip_start(self, restart=False):          """ @@ -343,7 +225,7 @@ class Backend(object):          :param restart: whether is is a restart.          :type restart: bool          """ -        self._call_queue.put(("eip", "start", None, restart)) +        self._eip.start(restart)      def eip_stop(self, shutdown=False, restart=False, failed=False):          """ @@ -355,13 +237,13 @@ class Backend(object):          :param restart: whether this is part of a restart.          :type restart: bool          """ -        self._call_queue.put(("eip", "stop", None, shutdown, restart)) +        self._eip.stop(shutdown, restart)      def eip_terminate(self):          """          Terminate the EIP service, not necessarily in a nice way.          """ -        self._call_queue.put(("eip", "terminate", None)) +        self._eip.terminate()      def eip_get_gateways_list(self, domain):          """ @@ -370,16 +252,25 @@ class Backend(object):          :param domain: the domain to get the gateways.          :type domain: str -        # TODO discuss how to document the expected result object received of -        # the signal -        :signal type: list of str -          Signals:              eip_get_gateways_list -> list of unicode              eip_get_gateways_list_error              eip_uninitialized_provider          """ -        self._call_queue.put(("eip", "get_gateways_list", None, domain)) +        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):          """ @@ -392,8 +283,7 @@ class Backend(object):              eip_get_initialized_providers -> list of tuple(unicode, bool)          """ -        self._call_queue.put(("eip", "get_initialized_providers", -                              None, domains)) +        self._eip.get_initialized_providers(domains)      def eip_can_start(self, domain):          """ @@ -406,8 +296,7 @@ class Backend(object):              eip_can_start              eip_cannot_start          """ -        self._call_queue.put(("eip", "can_start", -                              None, domain)) +        self._eip.can_start(domain)      def eip_check_dns(self, domain):          """ @@ -420,13 +309,13 @@ class Backend(object):              eip_dns_ok              eip_dns_error          """ -        self._call_queue.put(("eip", "check_dns", None, domain)) +        self._eip.check_dns(domain)      def tear_fw_down(self):          """          Signal the need to tear the fw down.          """ -        self._call_queue.put(("eip", "tear_fw_down", None)) +        self._eip.tear_fw_down()      def user_login(self, provider, username, password):          """ @@ -447,8 +336,7 @@ class Backend(object):              srp_auth_connection_error              srp_auth_error          """ -        self._call_queue.put(("authenticate", "login", None, provider, -                              username, password)) +        self._authenticate.login(provider, username, password)      def user_logout(self):          """ @@ -459,13 +347,13 @@ class Backend(object):              srp_logout_error              srp_not_logged_in_error          """ -        self._call_queue.put(("authenticate", "logout", None)) +        self._authenticate.logout()      def user_cancel_login(self):          """          Cancel the ongoing login (if any).          """ -        self._call_queue.put(("authenticate", "cancel_login", None)) +        self._authenticate.cancel_login()      def user_change_password(self, current_password, new_password):          """ @@ -482,8 +370,7 @@ class Backend(object):              srp_password_change_badpw              srp_password_change_error          """ -        self._call_queue.put(("authenticate", "change_password", None, -                              current_password, new_password)) +        self._authenticate.change_password(current_password, new_password)      def soledad_change_password(self, new_password):          """ @@ -498,8 +385,7 @@ class Backend(object):              srp_password_change_badpw              srp_password_change_error          """ -        self._call_queue.put(("soledad", "change_password", None, -                              new_password)) +        self._soledad.change_password(new_password)      def user_get_logged_in_status(self):          """ @@ -509,7 +395,7 @@ class Backend(object):              srp_status_logged_in              srp_status_not_logged_in          """ -        self._call_queue.put(("authenticate", "get_logged_in_status", None)) +        self._authenticate.get_logged_in_status()      def soledad_bootstrap(self, username, domain, password):          """ @@ -527,8 +413,10 @@ class Backend(object):              soledad_bootstrap_failed              soledad_invalid_auth_token          """ -        self._call_queue.put(("soledad", "bootstrap", None, -                              username, domain, password)) +        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):          """ @@ -543,20 +431,19 @@ class Backend(object):          Signals:          """ -        self._call_queue.put(("soledad", "load_offline", None, -                              username, password, uuid)) +        self._soledad.load_offline(username, password, uuid)      def soledad_cancel_bootstrap(self):          """          Cancel the ongoing soledad bootstrapping process (if any).          """ -        self._call_queue.put(("soledad", "cancel_bootstrap", None)) +        self._soledad.cancel_bootstrap()      def soledad_close(self):          """          Close soledad database.          """ -        self._call_queue.put(("soledad", "close", None)) +        self._soledad.close()      def keymanager_list_keys(self):          """ @@ -565,7 +452,7 @@ class Backend(object):          Signals:              keymanager_keys_list -> list          """ -        self._call_queue.put(("keymanager", "list_keys", None)) +        self._keymanager.list_keys()      def keymanager_export_keys(self, username, filename):          """ @@ -580,8 +467,7 @@ class Backend(object):              keymanager_export_ok              keymanager_export_error          """ -        self._call_queue.put(("keymanager", "export_keys", None, -                              username, filename)) +        self._keymanager.export_keys(username, filename)      def keymanager_get_key_details(self, username):          """ @@ -593,7 +479,7 @@ class Backend(object):          Signals:              keymanager_key_details          """ -        self._call_queue.put(("keymanager", "get_key_details", None, username)) +        self._keymanager.get_key_details(username)      def smtp_start_service(self, full_user_id, download_if_needed=False):          """ @@ -605,8 +491,7 @@ class Backend(object):                                     for the file          :type download_if_needed: bool          """ -        self._call_queue.put(("mail", "start_smtp_service", None, -                              full_user_id, download_if_needed)) +        self._mail.start_smtp_service(full_user_id, download_if_needed)      def imap_start_service(self, full_user_id, offline=False):          """ @@ -617,14 +502,13 @@ class Backend(object):          :param offline: whether imap should start in offline mode or not.          :type offline: bool          """ -        self._call_queue.put(("mail", "start_imap_service", None, -                              full_user_id, offline)) +        self._mail.start_imap_service(full_user_id, offline)      def smtp_stop_service(self):          """          Stop the SMTP service.          """ -        self._call_queue.put(("mail", "stop_smtp_service", None)) +        self._mail.stop_smtp_service()      def imap_stop_service(self):          """ @@ -633,4 +517,15 @@ class Backend(object):          Signals:              imap_stopped          """ -        self._call_queue.put(("mail", "stop_imap_service", None)) +        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) diff --git a/src/leap/bitmask/backend/leapsignaler.py b/src/leap/bitmask/backend/leapsignaler.py index da8908fd..a36e6fdc 100644 --- a/src/leap/bitmask/backend/leapsignaler.py +++ b/src/leap/bitmask/backend/leapsignaler.py @@ -1,5 +1,5 @@  # -*- coding: utf-8 -*- -# components.py +# leapsignaler.py  # Copyright (C) 2013 LEAP  #  # This program is free software: you can redistribute it and/or modify @@ -15,371 +15,101 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  """ -Signaler for Backend/Frontend communication. +Signaling server, used to define the API signals.  """ -import logging -  from PySide import QtCore -logger = logging.getLogger(__name__) +from leap.bitmask.backend.signaler_qt import SignalerQt -class Signaler(QtCore.QObject): +class LeapSignaler(SignalerQt):      """ -    Signaler object, handles converting string commands to Qt signals. - -    This is intended for the separation in frontend/backend, this will -    live in the frontend. +    Signaling server subclass, used to define the API signals.      """ +    backend_bad_call = QtCore.Signal(object) -    #################### -    # These will only exist in the frontend -    # Signals for the ProviderBootstrapper -    prov_name_resolution = QtCore.Signal(object) -    prov_https_connection = QtCore.Signal(object) -    prov_download_provider_info = QtCore.Signal(object) - -    prov_download_ca_cert = QtCore.Signal(object) -    prov_check_ca_fingerprint = QtCore.Signal(object) -    prov_check_api_certificate = QtCore.Signal(object) - -    prov_problem_with_provider = QtCore.Signal(object) - -    prov_unsupported_client = QtCore.Signal(object) -    prov_unsupported_api = QtCore.Signal(object) - -    prov_get_all_services = QtCore.Signal(object) -    prov_get_supported_services = QtCore.Signal(object) -    prov_get_details = QtCore.Signal(object) -    prov_get_pinned_providers = QtCore.Signal(object) - -    prov_cancelled_setup = QtCore.Signal(object) - -    # Signals for SRPRegister -    srp_registration_finished = QtCore.Signal(object) -    srp_registration_failed = QtCore.Signal(object) -    srp_registration_taken = QtCore.Signal(object) - -    # Signals for EIP bootstrapping -    eip_config_ready = QtCore.Signal(object) +    eip_alien_openvpn_already_running = QtCore.Signal() +    eip_can_start = QtCore.Signal() +    eip_cancelled_setup = QtCore.Signal() +    eip_cannot_start = QtCore.Signal()      eip_client_certificate_ready = QtCore.Signal(object) - -    eip_cancelled_setup = QtCore.Signal(object) - -    # Signals for SRPAuth -    srp_auth_ok = QtCore.Signal(object) -    srp_auth_error = QtCore.Signal(object) -    srp_auth_server_error = QtCore.Signal(object) -    srp_auth_connection_error = QtCore.Signal(object) -    srp_auth_bad_user_or_password = QtCore.Signal(object) -    srp_logout_ok = QtCore.Signal(object) -    srp_logout_error = QtCore.Signal(object) -    srp_password_change_ok = QtCore.Signal(object) -    srp_password_change_error = QtCore.Signal(object) -    srp_password_change_badpw = QtCore.Signal(object) -    srp_not_logged_in_error = QtCore.Signal(object) -    srp_status_logged_in = QtCore.Signal(object) -    srp_status_not_logged_in = QtCore.Signal(object) - -    # Signals for EIP -    eip_connected = QtCore.Signal(object) -    eip_disconnected = QtCore.Signal(object) +    eip_config_ready = QtCore.Signal(object) +    eip_connected = QtCore.Signal() +    eip_connection_aborted = QtCore.Signal()      eip_connection_died = QtCore.Signal(object) -    eip_connection_aborted = QtCore.Signal(object) -    eip_stopped = QtCore.Signal(object) - -    eip_dns_ok = QtCore.Signal(object) -    eip_dns_error = QtCore.Signal(object) - -    # EIP problems -    eip_no_polkit_agent_error = QtCore.Signal(object) -    eip_no_tun_kext_error = QtCore.Signal(object) -    eip_no_pkexec_error = QtCore.Signal(object) -    eip_openvpn_not_found_error = QtCore.Signal(object) -    eip_openvpn_already_running = QtCore.Signal(object) -    eip_alien_openvpn_already_running = QtCore.Signal(object) -    eip_vpn_launcher_exception = QtCore.Signal(object) - +    eip_disconnected = QtCore.Signal(object) +    eip_dns_error = QtCore.Signal() +    eip_dns_ok = QtCore.Signal() +    eip_get_gateway_country_code = QtCore.Signal(object)      eip_get_gateways_list = QtCore.Signal(object) -    eip_get_gateways_list_error = QtCore.Signal(object) -    eip_uninitialized_provider = QtCore.Signal(object) +    eip_get_gateways_list_error = QtCore.Signal()      eip_get_initialized_providers = QtCore.Signal(object) - -    # signals from parsing openvpn output -    eip_network_unreachable = QtCore.Signal(object) -    eip_process_restart_tls = QtCore.Signal(object) -    eip_process_restart_ping = QtCore.Signal(object) - -    # signals from vpnprocess.py +    eip_network_unreachable = QtCore.Signal() +    eip_no_gateway = QtCore.Signal() +    eip_no_pkexec_error = QtCore.Signal() +    eip_no_polkit_agent_error = QtCore.Signal() +    eip_no_tun_kext_error = QtCore.Signal() +    eip_openvpn_already_running = QtCore.Signal() +    eip_openvpn_not_found_error = QtCore.Signal() +    eip_process_finished = QtCore.Signal(int) +    eip_process_restart_ping = QtCore.Signal() +    eip_process_restart_tls = QtCore.Signal()      eip_state_changed = QtCore.Signal(dict)      eip_status_changed = QtCore.Signal(dict) -    eip_process_finished = QtCore.Signal(int) +    eip_stopped = QtCore.Signal()      eip_tear_fw_down = QtCore.Signal(object) - -    # signals whether the needed files to start EIP exist or not -    eip_can_start = QtCore.Signal(object) -    eip_cannot_start = QtCore.Signal(object) - -    # Signals for Soledad -    soledad_bootstrap_failed = QtCore.Signal(object) -    soledad_bootstrap_finished = QtCore.Signal(object) -    soledad_offline_failed = QtCore.Signal(object) -    soledad_offline_finished = QtCore.Signal(object) -    soledad_invalid_auth_token = QtCore.Signal(object) -    soledad_cancelled_bootstrap = QtCore.Signal(object) -    soledad_password_change_ok = QtCore.Signal(object) -    soledad_password_change_error = QtCore.Signal(object) - -    # Keymanager signals -    keymanager_export_ok = QtCore.Signal(object) -    keymanager_export_error = QtCore.Signal(object) -    keymanager_keys_list = QtCore.Signal(object) - -    keymanager_import_ioerror = QtCore.Signal(object) -    keymanager_import_datamismatch = QtCore.Signal(object) -    keymanager_import_missingkey = QtCore.Signal(object) -    keymanager_import_addressmismatch = QtCore.Signal(object) -    keymanager_import_ok = QtCore.Signal(object) - +    eip_uninitialized_provider = QtCore.Signal() +    eip_vpn_launcher_exception = QtCore.Signal() + +    imap_stopped = QtCore.Signal() + +    keymanager_export_error = QtCore.Signal() +    keymanager_export_ok = QtCore.Signal() +    keymanager_import_addressmismatch = QtCore.Signal() +    keymanager_import_datamismatch = QtCore.Signal() +    keymanager_import_ioerror = QtCore.Signal() +    keymanager_import_missingkey = QtCore.Signal() +    keymanager_import_ok = QtCore.Signal()      keymanager_key_details = QtCore.Signal(object) +    keymanager_keys_list = QtCore.Signal(object) -    # mail related signals -    imap_stopped = QtCore.Signal(object) - -    # This signal is used to warn the backend user that is doing something -    # wrong -    backend_bad_call = QtCore.Signal(object) - -    #################### -    # These will exist both in the backend AND the front end. -    # The frontend might choose to not "interpret" all the signals -    # from the backend, but the backend needs to have all the signals -    # it's going to emit defined here -    PROV_NAME_RESOLUTION_KEY = "prov_name_resolution" -    PROV_HTTPS_CONNECTION_KEY = "prov_https_connection" -    PROV_DOWNLOAD_PROVIDER_INFO_KEY = "prov_download_provider_info" -    PROV_DOWNLOAD_CA_CERT_KEY = "prov_download_ca_cert" -    PROV_CHECK_CA_FINGERPRINT_KEY = "prov_check_ca_fingerprint" -    PROV_CHECK_API_CERTIFICATE_KEY = "prov_check_api_certificate" -    PROV_PROBLEM_WITH_PROVIDER_KEY = "prov_problem_with_provider" -    PROV_UNSUPPORTED_CLIENT = "prov_unsupported_client" -    PROV_UNSUPPORTED_API = "prov_unsupported_api" -    PROV_CANCELLED_SETUP = "prov_cancelled_setup" -    PROV_GET_ALL_SERVICES = "prov_get_all_services" -    PROV_GET_SUPPORTED_SERVICES = "prov_get_supported_services" -    PROV_GET_DETAILS = "prov_get_details" -    PROV_GET_PINNED_PROVIDERS = "prov_get_pinned_providers" - -    SRP_REGISTRATION_FINISHED = "srp_registration_finished" -    SRP_REGISTRATION_FAILED = "srp_registration_failed" -    SRP_REGISTRATION_TAKEN = "srp_registration_taken" -    SRP_AUTH_OK = "srp_auth_ok" -    SRP_AUTH_ERROR = "srp_auth_error" -    SRP_AUTH_SERVER_ERROR = "srp_auth_server_error" -    SRP_AUTH_CONNECTION_ERROR = "srp_auth_connection_error" -    SRP_AUTH_BAD_USER_OR_PASSWORD = "srp_auth_bad_user_or_password" -    SRP_LOGOUT_OK = "srp_logout_ok" -    SRP_LOGOUT_ERROR = "srp_logout_error" -    SRP_PASSWORD_CHANGE_OK = "srp_password_change_ok" -    SRP_PASSWORD_CHANGE_ERROR = "srp_password_change_error" -    SRP_PASSWORD_CHANGE_BADPW = "srp_password_change_badpw" -    SRP_NOT_LOGGED_IN_ERROR = "srp_not_logged_in_error" -    SRP_STATUS_LOGGED_IN = "srp_status_logged_in" -    SRP_STATUS_NOT_LOGGED_IN = "srp_status_not_logged_in" - -    EIP_CONFIG_READY = "eip_config_ready" -    EIP_CLIENT_CERTIFICATE_READY = "eip_client_certificate_ready" -    EIP_CANCELLED_SETUP = "eip_cancelled_setup" - -    EIP_CONNECTED = "eip_connected" -    EIP_DISCONNECTED = "eip_disconnected" -    EIP_CONNECTION_DIED = "eip_connection_died" -    EIP_CONNECTION_ABORTED = "eip_connection_aborted" -    EIP_STOPPED = "eip_stopped" - -    EIP_NO_POLKIT_AGENT_ERROR = "eip_no_polkit_agent_error" -    EIP_NO_TUN_KEXT_ERROR = "eip_no_tun_kext_error" -    EIP_NO_PKEXEC_ERROR = "eip_no_pkexec_error" -    EIP_OPENVPN_NOT_FOUND_ERROR = "eip_openvpn_not_found_error" -    EIP_OPENVPN_ALREADY_RUNNING = "eip_openvpn_already_running" -    EIP_ALIEN_OPENVPN_ALREADY_RUNNING = "eip_alien_openvpn_already_running" -    EIP_VPN_LAUNCHER_EXCEPTION = "eip_vpn_launcher_exception" - -    EIP_GET_GATEWAYS_LIST = "eip_get_gateways_list" -    EIP_GET_GATEWAYS_LIST_ERROR = "eip_get_gateways_list_error" -    EIP_UNINITIALIZED_PROVIDER = "eip_uninitialized_provider" -    EIP_GET_INITIALIZED_PROVIDERS = "eip_get_initialized_providers" - -    EIP_NETWORK_UNREACHABLE = "eip_network_unreachable" -    EIP_PROCESS_RESTART_TLS = "eip_process_restart_tls" -    EIP_PROCESS_RESTART_PING = "eip_process_restart_ping" - -    EIP_STATE_CHANGED = "eip_state_changed" -    EIP_STATUS_CHANGED = "eip_status_changed" -    EIP_PROCESS_FINISHED = "eip_process_finished" -    EIP_TEAR_FW_DOWN = "eip_tear_fw_down" - -    EIP_CAN_START = "eip_can_start" -    EIP_CANNOT_START = "eip_cannot_start" - -    EIP_DNS_OK = "eip_dns_ok" -    EIP_DNS_ERROR = "eip_dns_error" - -    SOLEDAD_BOOTSTRAP_FAILED = "soledad_bootstrap_failed" -    SOLEDAD_BOOTSTRAP_FINISHED = "soledad_bootstrap_finished" -    SOLEDAD_OFFLINE_FAILED = "soledad_offline_failed" -    SOLEDAD_OFFLINE_FINISHED = "soledad_offline_finished" -    SOLEDAD_INVALID_AUTH_TOKEN = "soledad_invalid_auth_token" - -    SOLEDAD_PASSWORD_CHANGE_OK = "soledad_password_change_ok" -    SOLEDAD_PASSWORD_CHANGE_ERROR = "soledad_password_change_error" - -    SOLEDAD_CANCELLED_BOOTSTRAP = "soledad_cancelled_bootstrap" - -    KEYMANAGER_EXPORT_OK = "keymanager_export_ok" -    KEYMANAGER_EXPORT_ERROR = "keymanager_export_error" -    KEYMANAGER_KEYS_LIST = "keymanager_keys_list" - -    KEYMANAGER_IMPORT_IOERROR = "keymanager_import_ioerror" -    KEYMANAGER_IMPORT_DATAMISMATCH = "keymanager_import_datamismatch" -    KEYMANAGER_IMPORT_MISSINGKEY = "keymanager_import_missingkey" -    KEYMANAGER_IMPORT_ADDRESSMISMATCH = "keymanager_import_addressmismatch" -    KEYMANAGER_IMPORT_OK = "keymanager_import_ok" -    KEYMANAGER_KEY_DETAILS = "keymanager_key_details" - -    IMAP_STOPPED = "imap_stopped" - -    BACKEND_BAD_CALL = "backend_bad_call" - -    def __init__(self): -        """ -        Constructor for the Signaler -        """ -        QtCore.QObject.__init__(self) -        self._signals = {} - -        signals = [ -            self.PROV_NAME_RESOLUTION_KEY, -            self.PROV_HTTPS_CONNECTION_KEY, -            self.PROV_DOWNLOAD_PROVIDER_INFO_KEY, -            self.PROV_DOWNLOAD_CA_CERT_KEY, -            self.PROV_CHECK_CA_FINGERPRINT_KEY, -            self.PROV_CHECK_API_CERTIFICATE_KEY, -            self.PROV_PROBLEM_WITH_PROVIDER_KEY, -            self.PROV_UNSUPPORTED_CLIENT, -            self.PROV_UNSUPPORTED_API, -            self.PROV_CANCELLED_SETUP, -            self.PROV_GET_ALL_SERVICES, -            self.PROV_GET_SUPPORTED_SERVICES, -            self.PROV_GET_DETAILS, -            self.PROV_GET_PINNED_PROVIDERS, - -            self.SRP_REGISTRATION_FINISHED, -            self.SRP_REGISTRATION_FAILED, -            self.SRP_REGISTRATION_TAKEN, - -            self.EIP_CONFIG_READY, -            self.EIP_CLIENT_CERTIFICATE_READY, -            self.EIP_CANCELLED_SETUP, - -            self.EIP_CONNECTED, -            self.EIP_DISCONNECTED, -            self.EIP_CONNECTION_DIED, -            self.EIP_CONNECTION_ABORTED, -            self.EIP_STOPPED, - -            self.EIP_NO_POLKIT_AGENT_ERROR, -            self.EIP_NO_TUN_KEXT_ERROR, -            self.EIP_NO_PKEXEC_ERROR, -            self.EIP_OPENVPN_NOT_FOUND_ERROR, -            self.EIP_OPENVPN_ALREADY_RUNNING, -            self.EIP_ALIEN_OPENVPN_ALREADY_RUNNING, -            self.EIP_VPN_LAUNCHER_EXCEPTION, - -            self.EIP_GET_GATEWAYS_LIST, -            self.EIP_GET_GATEWAYS_LIST_ERROR, -            self.EIP_UNINITIALIZED_PROVIDER, -            self.EIP_GET_INITIALIZED_PROVIDERS, - -            self.EIP_NETWORK_UNREACHABLE, -            self.EIP_PROCESS_RESTART_TLS, -            self.EIP_PROCESS_RESTART_PING, - -            self.EIP_STATE_CHANGED, -            self.EIP_STATUS_CHANGED, -            self.EIP_PROCESS_FINISHED, - -            self.EIP_CAN_START, -            self.EIP_CANNOT_START, - -            self.EIP_DNS_OK, -            self.EIP_DNS_ERROR, - -            self.SRP_AUTH_OK, -            self.SRP_AUTH_ERROR, -            self.SRP_AUTH_SERVER_ERROR, -            self.SRP_AUTH_CONNECTION_ERROR, -            self.SRP_AUTH_BAD_USER_OR_PASSWORD, -            self.SRP_LOGOUT_OK, -            self.SRP_LOGOUT_ERROR, -            self.SRP_PASSWORD_CHANGE_OK, -            self.SRP_PASSWORD_CHANGE_ERROR, -            self.SRP_PASSWORD_CHANGE_BADPW, -            self.SRP_NOT_LOGGED_IN_ERROR, -            self.SRP_STATUS_LOGGED_IN, -            self.SRP_STATUS_NOT_LOGGED_IN, - -            self.SOLEDAD_BOOTSTRAP_FAILED, -            self.SOLEDAD_BOOTSTRAP_FINISHED, -            self.SOLEDAD_OFFLINE_FAILED, -            self.SOLEDAD_OFFLINE_FINISHED, -            self.SOLEDAD_INVALID_AUTH_TOKEN, -            self.SOLEDAD_CANCELLED_BOOTSTRAP, - -            self.SOLEDAD_PASSWORD_CHANGE_OK, -            self.SOLEDAD_PASSWORD_CHANGE_ERROR, - -            self.KEYMANAGER_EXPORT_OK, -            self.KEYMANAGER_EXPORT_ERROR, -            self.KEYMANAGER_KEYS_LIST, - -            self.KEYMANAGER_IMPORT_IOERROR, -            self.KEYMANAGER_IMPORT_DATAMISMATCH, -            self.KEYMANAGER_IMPORT_MISSINGKEY, -            self.KEYMANAGER_IMPORT_ADDRESSMISMATCH, -            self.KEYMANAGER_IMPORT_OK, -            self.KEYMANAGER_KEY_DETAILS, - -            self.IMAP_STOPPED, - -            self.BACKEND_BAD_CALL, -        ] - -        for sig in signals: -            self._signals[sig] = getattr(self, sig) - -    def signal(self, key, data=None): -        """ -        Emits a Qt signal based on the key provided, with the data if provided. - -        :param key: string identifying the signal to emit -        :type key: str -        :param data: object to send with the data -        :type data: object - -        NOTE: The data object will be a serialized str in the backend, -        and an unserialized object in the frontend, but for now we -        just care about objects. -        """ -        # Right now it emits Qt signals. The backend version of this -        # will do zmq.send_multipart, and the frontend version will be -        # similar to this - -        # for some reason emitting 'None' gives a segmentation fault. -        if data is None: -            data = '' - -        try: -            self._signals[key].emit(data) -        except KeyError: -            logger.error("Unknown key for signal %s!" % (key,)) +    prov_cancelled_setup = QtCore.Signal() +    prov_check_api_certificate = QtCore.Signal(object) +    prov_check_ca_fingerprint = QtCore.Signal(object) +    prov_download_ca_cert = QtCore.Signal(object) +    prov_download_provider_info = QtCore.Signal(object) +    prov_get_all_services = QtCore.Signal(object) +    prov_get_details = QtCore.Signal(object) +    prov_get_pinned_providers = QtCore.Signal(object) +    prov_get_supported_services = QtCore.Signal(object) +    prov_https_connection = QtCore.Signal(object) +    prov_name_resolution = QtCore.Signal(object) +    prov_problem_with_provider = QtCore.Signal() +    prov_unsupported_api = QtCore.Signal() +    prov_unsupported_client = QtCore.Signal() + +    soledad_bootstrap_failed = QtCore.Signal() +    soledad_bootstrap_finished = QtCore.Signal() +    soledad_cancelled_bootstrap = QtCore.Signal() +    soledad_invalid_auth_token = QtCore.Signal() +    soledad_offline_failed = QtCore.Signal() +    soledad_offline_finished = QtCore.Signal() +    soledad_password_change_error = QtCore.Signal() +    soledad_password_change_ok = QtCore.Signal() + +    srp_auth_bad_user_or_password = QtCore.Signal() +    srp_auth_connection_error = QtCore.Signal() +    srp_auth_error = QtCore.Signal() +    srp_auth_ok = QtCore.Signal() +    srp_auth_server_error = QtCore.Signal() +    srp_logout_error = QtCore.Signal() +    srp_logout_ok = QtCore.Signal() +    srp_not_logged_in_error = QtCore.Signal() +    srp_password_change_badpw = QtCore.Signal() +    srp_password_change_error = QtCore.Signal() +    srp_password_change_ok = QtCore.Signal() +    srp_registration_failed = QtCore.Signal() +    srp_registration_finished = QtCore.Signal() +    srp_registration_taken = QtCore.Signal() +    srp_status_logged_in = QtCore.Signal() +    srp_status_not_logged_in = QtCore.Signal() diff --git a/src/leap/bitmask/backend/settings.py b/src/leap/bitmask/backend/settings.py new file mode 100644 index 00000000..5cb4c616 --- /dev/null +++ b/src/leap/bitmask/backend/settings.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# settings.py +# Copyright (C) 2013, 2014 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 settings +""" +import ConfigParser +import logging +import os + +from leap.bitmask.util import get_path_prefix +from leap.common.check import leap_assert, leap_assert_type + +logger = logging.getLogger(__name__) + +# We need this one available for the default decorator +GATEWAY_AUTOMATIC = "Automatic" +GENERAL_SECTION = "General" + + +class Settings(object): +    """ +    Leap backend settings hanler. +    """ +    CONFIG_NAME = "leap-backend.conf" + +    # keys +    GATEWAY_KEY = "Gateway" + +    def __init__(self): +        """ +        Create the ConfigParser object and read it. +        """ +        self._settings_path = os.path.join(get_path_prefix(), +                                           "leap", self.CONFIG_NAME) + +        self._settings = ConfigParser.ConfigParser() +        self._settings.read(self._settings_path) + +        self._add_section(GENERAL_SECTION) + +    def _add_section(self, section): +        """ +        Add `section` to the config file and don't fail if already exists. + +        :param section: the section to add. +        :type section: str +        """ +        self._settings.read(self._settings_path) +        try: +            self._settings.add_section(section) +        except ConfigParser.DuplicateSectionError: +            pass + +    def _save(self): +        """ +        Save the current state to the config file. +        """ +        with open(self._settings_path, 'wb') as f: +            self._settings.write(f) + +    def _get_value(self, section, key, default): +        """ +        Return the value for the fiven `key` in `section`. +        If there's no such section/key, `default` is returned. + +        :param section: the section to get the value from. +        :type section: str +        :param key: the key which value we want to get. +        :type key: str +        :param default: the value to return if there is no section/key. +        :type default: object + +        :rtype: object +        """ +        try: +            return self._settings.get(section, key) +        except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): +            return default + +    def get_selected_gateway(self, provider): +        """ +        Return the configured gateway for the given `provider`. + +        :param provider: provider domain +        :type provider: str + +        :rtype: str +        """ +        leap_assert(len(provider) > 0, "We need a nonempty provider") +        return self._get_value(provider, self.GATEWAY_KEY, GATEWAY_AUTOMATIC) + +    def set_selected_gateway(self, provider, gateway): +        """ +        Saves the configured gateway for the given provider + +        :param provider: provider domain +        :type provider: str + +        :param gateway: gateway to use as default +        :type gateway: str +        """ + +        leap_assert(len(provider) > 0, "We need a nonempty provider") +        leap_assert_type(gateway, (str, unicode)) + +        self._add_section(provider) + +        self._settings.set(provider, self.GATEWAY_KEY, gateway) +        self._save() + +    def get_uuid(self, username): +        """ +        Gets the uuid for a given username. + +        :param username: the full user identifier in the form user@provider +        :type username: basestring +        """ +        leap_assert("@" in username, +                    "Expected username in the form user@provider") +        user, provider = username.split('@') + +        return self._get_value(provider, username, "") + +    def set_uuid(self, username, value): +        """ +        Sets the uuid for a given username. + +        :param username: the full user identifier in the form user@provider +        :type username: str or unicode +        :param value: the uuid to save or None to remove it +        :type value: str or unicode or None +        """ +        leap_assert("@" in username, +                    "Expected username in the form user@provider") +        user, provider = username.split('@') + +        if value is None: +            self._settings.remove_option(provider, username) +        else: +            leap_assert(len(value) > 0, "We cannot save an empty uuid") +            self._add_section(provider) +            self._settings.set(provider, username, value) + +        self._save() diff --git a/src/leap/bitmask/backend/signaler.py b/src/leap/bitmask/backend/signaler.py new file mode 100644 index 00000000..574bfa71 --- /dev/null +++ b/src/leap/bitmask/backend/signaler.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# signaler.py +# Copyright (C) 2013, 2014 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/>. +""" +Signaler client. +Receives signals from the backend and sends to the signaling server. +""" +import Queue +import threading +import time + +import zmq + +from leap.bitmask.backend.api import SIGNALS +from leap.bitmask.backend.utils import get_frontend_certificates + +import logging +logger = logging.getLogger(__name__) + + +class Signaler(object): +    """ +    Signaler client. +    Receives signals from the backend and sends to the signaling server. +    """ +    PORT = "5667" +    SERVER = "tcp://localhost:%s" % PORT +    POLL_TIMEOUT = 2000  # ms +    POLL_TRIES = 500 + +    def __init__(self): +        """ +        Initialize the ZMQ socket to talk to the signaling server. +        """ +        context = zmq.Context() +        logger.debug("Connecting to signaling server...") +        socket = context.socket(zmq.REQ) + +        # public, secret = zmq.curve_keypair() +        client_keys = zmq.curve_keypair() +        socket.curve_publickey = client_keys[0] +        socket.curve_secretkey = client_keys[1] + +        # The client must know the server's public key to make a CURVE +        # connection. +        public, _ = get_frontend_certificates() +        socket.curve_serverkey = public + +        socket.setsockopt(zmq.RCVTIMEO, 1000) +        socket.connect(self.SERVER) +        self._socket = socket + +        self._signal_queue = Queue.Queue() + +        self._do_work = threading.Event()  # used to stop the worker thread. +        self._worker_signaler = threading.Thread(target=self._worker) + +    def __getattribute__(self, name): +        """ +        This allows the user to do: +            S = Signaler() +            S.SOME_SIGNAL + +        Just by having defined 'some_signal' in _SIGNALS + +        :param name: the attribute name that is requested. +        :type name: str +        """ +        if name in SIGNALS: +            return name +        else: +            return object.__getattribute__(self, name) + +    def signal(self, signal, data=None): +        """ +        Sends a signal to the signaling server. + +        :param signal: the signal to send. +        :type signal: str +        """ +        if signal not in SIGNALS: +            raise Exception("Unknown signal: '{0}'".format(signal)) + +        request = { +            'signal': signal, +            'data': data, +        } + +        try: +            request_json = zmq.utils.jsonapi.dumps(request) +        except Exception as e: +            msg = ("Error serializing request into JSON.\n" +                   "Exception: {0} Data: {1}") +            msg = msg.format(e, request) +            logger.critical(msg) +            raise + +        # queue the call in order to handle the request in a thread safe way. +        self._signal_queue.put(request_json) + +    def _worker(self): +        """ +        Worker loop that processes the Queue of pending requests to do. +        """ +        while self._do_work.is_set(): +            try: +                request = self._signal_queue.get(block=False) +                self._send_request(request) +            except Queue.Empty: +                pass +            time.sleep(0.01) + +        logger.debug("Signaler thread stopped.") + +    def start(self): +        """ +        Start the Signaler worker. +        """ +        self._do_work.set() +        self._worker_signaler.start() + +    def stop(self): +        """ +        Stop the Signaler worker. +        """ +        self._do_work.clear() + +    def _send_request(self, request): +        """ +        Send the given request to the server. +        This is used from a thread safe loop in order to avoid sending a +        request without receiving a response from a previous one. + +        :param request: the request to send. +        :type request: str +        """ +        # logger.debug("Signaling '{0}'".format(request)) +        self._socket.send(request) + +        poll = zmq.Poller() +        poll.register(self._socket, zmq.POLLIN) + +        reply = None +        tries = 0 + +        while True: +            socks = dict(poll.poll(self.POLL_TIMEOUT)) +            if socks.get(self._socket) == zmq.POLLIN: +                reply = self._socket.recv() +                break + +            tries += 1 +            if tries < self.POLL_TRIES: +                logger.warning('Retrying receive... {0}/{1}'.format( +                    tries, self.POLL_TRIES)) +            else: +                break + +        if reply is None: +            msg = "Timeout error contacting backend." +            logger.critical(msg) +        # else: +        #     msg = "Received reply for '{0}' -> '{1}'".format(request, reply) +        #     logger.debug(msg) diff --git a/src/leap/bitmask/backend/signaler_qt.py b/src/leap/bitmask/backend/signaler_qt.py new file mode 100644 index 00000000..433f18ed --- /dev/null +++ b/src/leap/bitmask/backend/signaler_qt.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# signaler_qt.py +# Copyright (C) 2013, 2014 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/>. +""" +Signaling server. +Receives signals from the signaling client and emit Qt signals for the GUI. +""" +import threading +import time + +from PySide import QtCore + +import zmq +from zmq.auth.thread import ThreadAuthenticator + +from leap.bitmask.backend.api import SIGNALS +from leap.bitmask.backend.utils import get_frontend_certificates + +import logging +logger = logging.getLogger(__name__) + + +class SignalerQt(QtCore.QObject): +    """ +    Signaling server. +    Receives signals from the signaling client and emit Qt signals for the GUI. +    """ +    PORT = "5667" +    BIND_ADDR = "tcp://127.0.0.1:%s" % PORT + +    def __init__(self): +        QtCore.QObject.__init__(self) + +        # Note: we use a plain thread instead of a QThread since works better. +        # The signaler was not responding on OSX if the worker loop was run in +        # a QThread. +        # Possibly, ZMQ was not getting cycles to do work because Qt not +        # receiving focus or something. +        self._worker_thread = threading.Thread(target=self._run) +        self._do_work = threading.Event() + +    def start(self): +        """ +        Start the worker thread for the signaler server. +        """ +        self._do_work.set() +        self._worker_thread.start() + +    def _run(self): +        """ +        Start a loop to process the ZMQ requests from the signaler client. +        """ +        logger.debug("Running SignalerQt loop") +        context = zmq.Context() +        socket = context.socket(zmq.REP) + +        # Start an authenticator for this context. +        auth = ThreadAuthenticator(context) +        auth.start() +        auth.allow('127.0.0.1') + +        # Tell authenticator to use the certificate in a directory +        auth.configure_curve(domain='*', location=zmq.auth.CURVE_ALLOW_ANY) +        public, secret = get_frontend_certificates() +        socket.curve_publickey = public +        socket.curve_secretkey = secret +        socket.curve_server = True  # must come before bind + +        socket.bind(self.BIND_ADDR) + +        while self._do_work.is_set(): +            # Wait for next request from client +            try: +                request = socket.recv(zmq.NOBLOCK) +                # logger.debug("Received request: '{0}'".format(request)) +                socket.send("OK") +                self._process_request(request) +            except zmq.ZMQError as e: +                if e.errno != zmq.EAGAIN: +                    raise +            time.sleep(0.01) + +        logger.debug("SignalerQt thread stopped.") + +    def stop(self): +        """ +        Stop the SignalerQt blocking loop. +        """ +        self._do_work.clear() + +    def _process_request(self, request_json): +        """ +        Process a request and call the according method with the given +        parameters. + +        :param request_json: a json specification of a request. +        :type request_json: str +        """ +        try: +            request = zmq.utils.jsonapi.loads(request_json) +            signal = request['signal'] +            data = request['data'] +        except Exception as e: +            msg = "Malformed JSON data in Signaler request '{0}'. Exc: {1!r}" +            msg = msg.format(request_json, e) +            logger.critical(msg) +            raise + +        if signal not in SIGNALS: +            logger.error("Unknown signal received, '{0}'".format(signal)) +            return + +        try: +            qt_signal = getattr(self, signal) +        except Exception: +            logger.warning("Signal not implemented, '{0}'".format(signal)) +            return + +        # logger.debug("Emitting '{0}'".format(signal)) +        if data is None: +            qt_signal.emit() +        else: +            qt_signal.emit(data) diff --git a/src/leap/bitmask/backend/utils.py b/src/leap/bitmask/backend/utils.py new file mode 100644 index 00000000..54a16fd7 --- /dev/null +++ b/src/leap/bitmask/backend/utils.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# utils.py +# Copyright (C) 2013, 2014 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 utilities to handle ZMQ certificates. +""" +import os +import shutil + +import zmq.auth + +from leap.bitmask.util import get_path_prefix +from leap.common.files import mkdir_p + +KEYS_DIR = os.path.join(get_path_prefix(), 'leap', 'zmq_certificates') + + +def generate_certificates(): +    """ +    Generate client and server CURVE certificate files. +    """ +    # Create directory for certificates, remove old content if necessary +    if os.path.exists(KEYS_DIR): +        shutil.rmtree(KEYS_DIR) +    mkdir_p(KEYS_DIR) + +    # create new keys in certificates dir +    # public_file, secret_file = create_certificates(...) +    zmq.auth.create_certificates(KEYS_DIR, "frontend") +    zmq.auth.create_certificates(KEYS_DIR, "backend") + + +def get_frontend_certificates(): +    """ +    Return the frontend's public and secret certificates. +    """ +    frontend_secret_file = os.path.join(KEYS_DIR, "frontend.key_secret") +    public, secret = zmq.auth.load_certificate(frontend_secret_file) +    return public, secret + + +def get_backend_certificates(base_dir='.'): +    """ +    Return the backend's public and secret certificates. +    """ +    backend_secret_file = os.path.join(KEYS_DIR, "backend.key_secret") +    public, secret = zmq.auth.load_certificate(backend_secret_file) +    return public, secret diff --git a/src/leap/bitmask/backend_app.py b/src/leap/bitmask/backend_app.py index e69de29b..5c0e4803 100644 --- a/src/leap/bitmask/backend_app.py +++ b/src/leap/bitmask/backend_app.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# backend_app.py +# Copyright (C) 2013, 2014 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/>. +""" +Start point for the Backend. +""" +import logging +import multiprocessing +import signal + +from leap.bitmask.backend.leapbackend import LeapBackend +from leap.bitmask.util import dict_to_flags + +logger = logging.getLogger(__name__) + + +def signal_handler(signum, frame): +    """ +    Signal handler that quits the running app cleanly. + +    :param signum: number of the signal received (e.g. SIGINT -> 2) +    :type signum: int +    :param frame: current stack frame +    :type frame: frame or None +    """ +    # Note: we don't stop the backend in here since the frontend signal handler +    # will take care of that. +    # In the future we may need to do the stop in here when the frontend and +    # the backend are run separately (without multiprocessing) +    pname = multiprocessing.current_process().name +    logger.debug("{0}: SIGNAL #{1} catched.".format(pname, signum)) + + +def run_backend(bypass_checks, flags_dict): +    """ +    Run the backend for the application. + +    :param bypass_checks: whether we should bypass the checks or not +    :type bypass_checks: bool +    :param flags_dict: a dict containing the flag values set on app start. +    :type flags_dict: dict +    """ +    # ignore SIGINT since app.py takes care of signaling SIGTERM to us. +    signal.signal(signal.SIGINT, signal.SIG_IGN) +    signal.signal(signal.SIGTERM, signal_handler) + +    dict_to_flags(flags_dict) + +    backend = LeapBackend(bypass_checks=bypass_checks) +    backend.run() diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py index 2f3fdde4..6b70659d 100644 --- a/src/leap/bitmask/config/flags.py +++ b/src/leap/bitmask/config/flags.py @@ -55,5 +55,3 @@ OPENVPN_VERBOSITY = 1  # Skip the checks in the wizard, use for testing purposes only!  SKIP_WIZARD_CHECKS = False - -CURRENT_VPN_COUNTRY = None diff --git a/src/leap/bitmask/config/provider_spec.py b/src/leap/bitmask/config/provider_spec.py index cf942c7b..a1d91b90 100644 --- a/src/leap/bitmask/config/provider_spec.py +++ b/src/leap/bitmask/config/provider_spec.py @@ -37,7 +37,7 @@ leap_provider_spec = {              'default': {u'en': u'Test Provider'}          },          'description': { -            #'type': LEAPTranslatable, +            # 'type': LEAPTranslatable,              'type': dict,              'format': 'translatable',              'default': {u'en': u'Test provider'} diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 192a9d5c..d59b3c31 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -24,7 +24,7 @@ import requests  import srp  import json -#this error is raised from requests +# this error is raised from requests  from simplejson.decoder import JSONDecodeError  from functools import partial  from requests.adapters import HTTPAdapter @@ -32,7 +32,7 @@ from requests.adapters import HTTPAdapter  from twisted.internet import threads  from twisted.internet.defer import CancelledError -from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.backend.settings import Settings  from leap.bitmask.util import request_helpers as reqhelper  from leap.bitmask.util.compat import requests_has_max_retries  from leap.bitmask.util.constants import REQUEST_TIMEOUT @@ -151,7 +151,7 @@ class SRPAuth(object):              self._provider_config = provider_config              self._signaler = signaler -            self._settings = LeapSettings() +            self._settings = Settings()              # **************************************************** #              # Dependency injection helpers, override this for more @@ -523,7 +523,7 @@ class SRPAuth(object):              Password change callback.              """              if self._signaler is not None: -                self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_OK) +                self._signaler.signal(self._signaler.srp_password_change_ok)          def _change_password_error(self, failure):              """ @@ -535,9 +535,9 @@ class SRPAuth(object):                  return              if failure.check(SRPAuthBadUserOrPassword): -                self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_BADPW) +                self._signaler.signal(self._signaler.srp_password_change_badpw)              else: -                self._signaler.signal(self._signaler.SRP_PASSWORD_CHANGE_ERROR) +                self._signaler.signal(self._signaler.srp_password_change_error)          def authenticate(self, username, password):              """ @@ -591,7 +591,7 @@ class SRPAuth(object):              :type _: IGNORED              """              logger.debug("Successful login!") -            self._signaler.signal(self._signaler.SRP_AUTH_OK) +            self._signaler.signal(self._signaler.srp_auth_ok)          def _authenticate_error(self, failure):              """ @@ -612,13 +612,13 @@ class SRPAuth(object):                  return              if failure.check(SRPAuthBadUserOrPassword): -                signal = self._signaler.SRP_AUTH_BAD_USER_OR_PASSWORD +                signal = self._signaler.srp_auth_bad_user_or_password              elif failure.check(SRPAuthConnectionError): -                signal = self._signaler.SRP_AUTH_CONNECTION_ERROR +                signal = self._signaler.srp_auth_connection_error              elif failure.check(SRPAuthenticationError): -                signal = self._signaler.SRP_AUTH_SERVER_ERROR +                signal = self._signaler.srp_auth_server_error              else: -                signal = self._signaler.SRP_AUTH_ERROR +                signal = self._signaler.srp_auth_error              self._signaler.signal(signal) @@ -647,7 +647,7 @@ class SRPAuth(object):                  logger.warning("Something went wrong with the logout: %r" %                                 (e,))                  if self._signaler is not None: -                    self._signaler.signal(self._signaler.SRP_LOGOUT_ERROR) +                    self._signaler.signal(self._signaler.srp_logout_error)                  raise              else:                  self.set_session_id(None) @@ -657,7 +657,7 @@ class SRPAuth(object):                  self._session = self._fetcher.session()                  logger.debug("Successfully logged out.")                  if self._signaler is not None: -                    self._signaler.signal(self._signaler.SRP_LOGOUT_OK) +                    self._signaler.signal(self._signaler.srp_logout_ok)          def set_session_id(self, session_id):              with self._session_id_lock: diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py index f03dc469..86510de1 100644 --- a/src/leap/bitmask/crypto/srpregister.py +++ b/src/leap/bitmask/crypto/srpregister.py @@ -179,11 +179,11 @@ class SRPRegister(QtCore.QObject):              return          if status_code in self.STATUS_OK: -            self._signaler.signal(self._signaler.SRP_REGISTRATION_FINISHED) +            self._signaler.signal(self._signaler.srp_registration_finished)          elif status_code == self.STATUS_TAKEN: -            self._signaler.signal(self._signaler.SRP_REGISTRATION_TAKEN) +            self._signaler.signal(self._signaler.srp_registration_taken)          else: -            self._signaler.signal(self._signaler.SRP_REGISTRATION_FAILED) +            self._signaler.signal(self._signaler.srp_registration_failed)  if __name__ == "__main__": diff --git a/src/leap/bitmask/crypto/tests/fake_provider.py b/src/leap/bitmask/crypto/tests/fake_provider.py index b8cdbb12..60a3ef0a 100755 --- a/src/leap/bitmask/crypto/tests/fake_provider.py +++ b/src/leap/bitmask/crypto/tests/fake_provider.py @@ -156,7 +156,7 @@ def getSession(self, sessionInterface=None):      put the right cookie name in place      """      if not self.session: -        #cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath) +        # cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath)          cookiename = b"_".join([b'_session_id'] + self.sitepath)          sessionCookie = self.getCookie(cookiename)          if sessionCookie: @@ -321,7 +321,7 @@ class OpenSSLServerContextFactory(object):          Create an SSL context.          """          ctx = SSL.Context(SSL.SSLv23_METHOD) -        #ctx = SSL.Context(SSL.TLSv1_METHOD) +        # ctx = SSL.Context(SSL.TLSv1_METHOD)          ctx.use_certificate_file(where('leaptestscert.pem'))          ctx.use_privatekey_file(where('leaptestskey.pem')) diff --git a/src/leap/bitmask/frontend_app.py b/src/leap/bitmask/frontend_app.py index e69de29b..51607d0b 100644 --- a/src/leap/bitmask/frontend_app.py +++ b/src/leap/bitmask/frontend_app.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# frontend_app.py +# Copyright (C) 2013, 2014 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/>. +""" +Start point for the Frontend. +""" +import multiprocessing +import signal +import sys +import os + +from functools import partial + +from PySide import QtCore, QtGui + +from leap.bitmask.config import flags +from leap.bitmask.gui.mainwindow import MainWindow +from leap.bitmask.util import dict_to_flags + +import logging +logger = logging.getLogger(__name__) + + +def signal_handler(window, pid, signum, frame): +    """ +    Signal handler that quits the running app cleanly. + +    :param window: a window with a `quit` callable +    :type window: MainWindow +    :param pid: process id of the main process. +    :type pid: int +    :param signum: number of the signal received (e.g. SIGINT -> 2) +    :type signum: int +    :param frame: current stack frame +    :type frame: frame or None +    """ +    my_pid = os.getpid() +    if pid == my_pid: +        pname = multiprocessing.current_process().name +        logger.debug("{0}: SIGNAL #{1} catched.".format(pname, signum)) +        window.quit() + + +def run_frontend(options, flags_dict): +    """ +    Run the GUI for the application. + +    :param options: a dict of options parsed from the command line. +    :type options: dict +    :param flags_dict: a dict containing the flag values set on app start. +    :type flags_dict: dict +    """ +    dict_to_flags(flags_dict) + +    start_hidden = options["start_hidden"] + +    # We force the style if on KDE so that it doesn't load all the kde +    # libs, which causes a compatibility issue in some systems. +    # For more info, see issue #3194 +    if flags.STANDALONE and os.environ.get("KDE_SESSION_UID") is not None: +        sys.argv.append("-style") +        sys.argv.append("Cleanlooks") + +    qApp = QtGui.QApplication(sys.argv) + +    # To test: +    # $ LANG=es ./app.py +    locale = QtCore.QLocale.system().name() +    qtTranslator = QtCore.QTranslator() +    if qtTranslator.load("qt_%s" % locale, ":/translations"): +        qApp.installTranslator(qtTranslator) +    appTranslator = QtCore.QTranslator() +    if appTranslator.load("%s.qm" % locale[:2], ":/translations"): +        qApp.installTranslator(appTranslator) + +    # Needed for initializing qsettings it will write +    # .config/leap/leap.conf top level app settings in a platform +    # independent way +    qApp.setOrganizationName("leap") +    qApp.setApplicationName("leap") +    qApp.setOrganizationDomain("leap.se") + +    # HACK: +    # We need to do some 'python work' once in a while, otherwise, no python +    # code will be called and the Qt event loop will prevent the signal +    # handlers for SIGINT/SIGTERM to be called. +    # see: http://stackoverflow.com/a/4939113/687989 +    timer = QtCore.QTimer() +    timer.start(500)  # You may change this if you wish. +    timer.timeout.connect(lambda: None)  # Let the interpreter run each 500 ms. + +    window = MainWindow(start_hidden=start_hidden) + +    my_pid = os.getpid() +    sig_handler = partial(signal_handler, window, my_pid) +    signal.signal(signal.SIGINT, sig_handler) +    signal.signal(signal.SIGTERM, sig_handler) + +    sys.exit(qApp.exec_()) + + +if __name__ == '__main__': +    run_frontend() diff --git a/src/leap/bitmask/gui/__init__.py b/src/leap/bitmask/gui/__init__.py index 4b289442..bba02061 100644 --- a/src/leap/bitmask/gui/__init__.py +++ b/src/leap/bitmask/gui/__init__.py @@ -17,5 +17,8 @@  """  init file for leap.gui  """ -app = __import__("app", globals(), locals(), [], 2) -__all__ = [app] +# This was added for coverage and testing, but when doing the osx +# bundle with py2app it fails because of this, so commenting for now +# Also creates conflicts with the new frontend/backend separation. +# app = __import__("app", globals(), locals(), [], 2) +# __all__ = [app] diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index 530cd38d..0f63972f 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -33,7 +33,7 @@ class EIPPreferencesWindow(QtGui.QDialog):      """      Window that displays the EIP preferences.      """ -    def __init__(self, parent, domain, backend): +    def __init__(self, parent, domain, backend, leap_signaler):          """          :param parent: parent object of the EIPPreferencesWindow.          :type parent: QWidget @@ -46,6 +46,7 @@ class EIPPreferencesWindow(QtGui.QDialog):          self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic")          self._settings = LeapSettings() +        self._leap_signaler = leap_signaler          self._backend = backend          # Load UI @@ -96,7 +97,7 @@ class EIPPreferencesWindow(QtGui.QDialog):          if not providers:              return -        self._backend.eip_get_initialized_providers(providers) +        self._backend.eip_get_initialized_providers(domains=providers)      @QtCore.Slot(list)      def _load_providers_in_combo(self, providers): @@ -148,6 +149,8 @@ class EIPPreferencesWindow(QtGui.QDialog):              gateway = self.ui.cbGateways.itemData(idx)          self._settings.set_selected_gateway(provider, gateway) +        self._backend.settings_set_selected_gateway(provider=provider, +                                                    gateway=gateway)          msg = self.tr(              "Gateway settings for provider '{0}' saved.").format(provider) @@ -174,7 +177,7 @@ class EIPPreferencesWindow(QtGui.QDialog):          domain = self.ui.cbProvidersGateway.itemData(domain_idx)          self._selected_domain = domain -        self._backend.eip_get_gateways_list(domain) +        self._backend.eip_get_gateways_list(domain=domain)      @QtCore.Slot(list)      def _update_gateways_list(self, gateways): @@ -248,7 +251,7 @@ class EIPPreferencesWindow(QtGui.QDialog):          self.ui.cbGateways.setEnabled(False)      def _backend_connect(self): -        sig = self._backend.signaler +        sig = self._leap_signaler          sig.eip_get_gateways_list.connect(self._update_gateways_list)          sig.eip_get_gateways_list_error.connect(self._gateways_list_error)          sig.eip_uninitialized_provider.connect( diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index bd569343..a707050a 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -24,7 +24,6 @@ from functools import partial  from PySide import QtCore, QtGui -from leap.bitmask.config import flags  from leap.bitmask.services import get_service_display_name, EIP_SERVICE  from leap.bitmask.platform_init import IS_LINUX  from leap.bitmask.util.averages import RateMovingAverage @@ -44,7 +43,7 @@ class EIPStatusWidget(QtGui.QWidget):      RATE_STR = "%1.2f KB/s"      TOTAL_STR = "%1.2f Kb" -    def __init__(self, parent=None, eip_conductor=None): +    def __init__(self, parent, eip_conductor, leap_signaler):          """          :param parent: the parent of the widget.          :type parent: QObject @@ -60,6 +59,8 @@ class EIPStatusWidget(QtGui.QWidget):          self.ui = Ui_EIPStatus()          self.ui.setupUi(self) +        self._leap_signaler = leap_signaler +          self.eip_conductor = eip_conductor          self.eipconnection = eip_conductor.eip_connection @@ -98,7 +99,7 @@ class EIPStatusWidget(QtGui.QWidget):          """          Connect backend signals.          """ -        signaler = self.eip_conductor._backend.signaler +        signaler = self._leap_signaler          signaler.eip_openvpn_already_running.connect(              self._on_eip_openvpn_already_running) @@ -121,8 +122,8 @@ class EIPStatusWidget(QtGui.QWidget):          # XXX we cannot connect this signal now because          # it interferes with the proper notifications during restarts          # without available network. -        #signaler.eip_network_unreachable.connect( -            #self._on_eip_network_unreachable) +        # signaler.eip_network_unreachable.connect( +        #     self._on_eip_network_unreachable)      def _make_status_clickable(self):          """ @@ -324,7 +325,7 @@ class EIPStatusWidget(QtGui.QWidget):          Triggered after a successful login.          Enables the start button.          """ -        #logger.debug('Showing EIP start button') +        # logger.debug('Showing EIP start button')          self.eip_button.show()          # Restore the eip action menu @@ -543,7 +544,7 @@ class EIPStatusWidget(QtGui.QWidget):          elif vpn_state == "ALREADYRUNNING":              # Put the following calls in Qt's event queue, otherwise              # the UI won't update properly -            #self.send_disconnect_signal() +            # self.send_disconnect_signal()              QtDelayedCall(                  0, self.eipconnection.qtsigns.do_disconnect_signal.emit)              msg = self.tr("Unable to start VPN, it's already running.") @@ -587,16 +588,23 @@ class EIPStatusWidget(QtGui.QWidget):          self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray))          self._eip_status_menu.setTitle(tray_message) -    def set_provider(self, provider): +    def set_provider(self, provider, country_code): +        """ +        Set the provider used right now, name and flag (if available). + +        :param provider: the provider in use. +        :type provider: str +        :param country_code: the country code of the gateway in use. +        :type country_code: str +        """          self._provider = provider          self.ui.lblEIPMessage.setText(              self.tr("Routing traffic through: <b>{0}</b>").format(                  provider)) -        ccode = flags.CURRENT_VPN_COUNTRY -        if ccode is not None: -            self.set_country_code(ccode) +        if country_code is not None: +            self.set_country_code(country_code)      def set_country_code(self, code):          """ @@ -729,7 +737,7 @@ class EIPStatusWidget(QtGui.QWidget):          self.set_eip_status_icon("error")      def set_eipstatus_off(self, error=True): -    # XXX this should be handled by the state machine. +        # XXX this should be handled by the state machine.          """          Sets eip status to off          """ diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 5caef745..bb755b5c 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -304,7 +304,7 @@ class MailStatusWidget(QtGui.QWidget):          ext_status = ""          if req.event == proto.KEYMANAGER_LOOKING_FOR_KEY: -            ext_status = self.tr("Looking for key for this user") +            ext_status = self.tr("Initial sync in progress, please wait...")          elif req.event == proto.KEYMANAGER_KEY_FOUND:              ext_status = self.tr("Found key! Starting mail...")          # elif req.event == proto.KEYMANAGER_KEY_NOT_FOUND: diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 53a7d95a..6959650b 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -18,6 +18,7 @@  Main window for Bitmask.  """  import logging +import time  from datetime import datetime @@ -25,6 +26,11 @@ from PySide import QtCore, QtGui  from leap.bitmask import __version__ as VERSION  from leap.bitmask import __version_hash__ as VERSION_HASH + +# TODO: we should use a more granular signaling instead of passing error/ok as +# a result. +from leap.bitmask.backend.leapbackend import ERROR_KEY, PASSED_KEY +  from leap.bitmask.config import flags  from leap.bitmask.config.leapsettings import LeapSettings @@ -37,13 +43,13 @@ from leap.bitmask.gui.mail_status import MailStatusWidget  from leap.bitmask.gui.preferenceswindow import PreferencesWindow  from leap.bitmask.gui.systray import SysTray  from leap.bitmask.gui.wizard import Wizard -from leap.bitmask.gui import twisted_main  from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX  from leap.bitmask.platform_init.initializers import init_platform  from leap.bitmask.platform_init.initializers import init_signals -from leap.bitmask.backend import leapbackend +from leap.bitmask.backend.backend_proxy import BackendProxy +from leap.bitmask.backend.leapsignaler import LeapSignaler  from leap.bitmask.services.eip import conductor as eip_conductor  from leap.bitmask.services.mail import conductor as mail_conductor @@ -91,13 +97,10 @@ class MainWindow(QtGui.QMainWindow):      # We give the services some time to a halt before forcing quit.      SERVICES_STOP_TIMEOUT = 20000  # in milliseconds -    def __init__(self, bypass_checks=False, start_hidden=False): +    def __init__(self, start_hidden=False):          """          Constructor for the client main window -        :param bypass_checks: Set to true if the app should bypass first round -                              of checks for CA certificates at bootstrap -        :type bypass_checks: bool          :param start_hidden: Set to true if the app should not show the window                               but just the tray.          :type start_hidden: bool @@ -119,15 +122,18 @@ class MainWindow(QtGui.QMainWindow):          self.ui = Ui_MainWindow()          self.ui.setupUi(self)          self.menuBar().setNativeMenuBar(not IS_LINUX) -        self._backend = leapbackend.Backend(bypass_checks) -        self._backend.start() + +        self._backend = BackendProxy() + +        self._leap_signaler = LeapSignaler() +        self._leap_signaler.start()          self._settings = LeapSettings() +        # gateway = self._settings.get_selected_gateway(provider) +        # self._backend.settings_set_selected_gateway(provider, gateway)          # Login Widget -        self._login_widget = LoginWidget( -            self._settings, -            self) +        self._login_widget = LoginWidget(self._settings, self)          self.ui.loginLayout.addWidget(self._login_widget)          # Mail Widget @@ -144,8 +150,9 @@ class MainWindow(QtGui.QMainWindow):          # EIP Control redux #########################################          self._eip_conductor = eip_conductor.EIPConductor( -            self._settings, self._backend) -        self._eip_status = EIPStatusWidget(self, self._eip_conductor) +            self._settings, self._backend, self._leap_signaler) +        self._eip_status = EIPStatusWidget(self, self._eip_conductor, +                                           self._leap_signaler)          init_signals.eip_missing_helpers.connect(              self._disable_eip_missing_helpers) @@ -180,6 +187,7 @@ class MainWindow(QtGui.QMainWindow):          self._services_being_stopped = {}          # used to know if we are in the final steps of quitting +        self._quitting = False          self._finally_quitting = False          self._backend_connected_signals = [] @@ -249,7 +257,7 @@ class MainWindow(QtGui.QMainWindow):          # XXX should connect to mail_conductor.start_mail_service instead          self.soledad_ready.connect(self._start_smtp_bootstrapping)          self.soledad_ready.connect(self._start_imap_service) -        ################################# end Qt Signals connection ######## +        # ################################ end Qt Signals connection ########          init_platform() @@ -258,7 +266,6 @@ class MainWindow(QtGui.QMainWindow):          self._logger_window = None -        self._bypass_checks = bypass_checks          self._start_hidden = start_hidden          self._mail_conductor = mail_conductor.MailConductor(self._backend) @@ -283,7 +290,7 @@ class MainWindow(QtGui.QMainWindow):              self._wizard_firstrun = True              self._disconnect_and_untrack()              self._wizard = Wizard(backend=self._backend, -                                  bypass_checks=bypass_checks) +                                  leap_signaler=self._leap_signaler)              # Give this window time to finish init and then show the wizard              QtDelayedCall(1, self._launch_wizard)              self._wizard.accepted.connect(self._finish_init) @@ -342,7 +349,7 @@ class MainWindow(QtGui.QMainWindow):                               that we are tracking to disconnect later.          :type only_tracked: bool          """ -        sig = self._backend.signaler +        sig = self._leap_signaler          conntrack = self._connect_and_track          auth_err = self._authentication_error @@ -407,6 +414,9 @@ class MainWindow(QtGui.QMainWindow):          sig.eip_dns_error.connect(self._eip_dns_error) +        sig.eip_get_gateway_country_code.connect(self._set_eip_provider) +        sig.eip_no_gateway.connect(self._set_eip_provider) +          # ==================================================================          # Soledad signals @@ -480,7 +490,7 @@ class MainWindow(QtGui.QMainWindow):          if self._wizard is None:              self._disconnect_and_untrack()              self._wizard = Wizard(backend=self._backend, -                                  bypass_checks=self._bypass_checks) +                                  leap_signaler=self._leap_signaler)              self._wizard.accepted.connect(self._finish_init)              self._wizard.rejected.connect(self._rejected_wizard) @@ -575,7 +585,8 @@ class MainWindow(QtGui.QMainWindow):          if self._provider_details is not None:              mx_provided = MX_SERVICE in self._provider_details['services']          preferences = PreferencesWindow(self, user, domain, self._backend, -                                        self._soledad_started, mx_provided) +                                        self._soledad_started, mx_provided, +                                        self._leap_signaler)          self.soledad_ready.connect(preferences.set_soledad_ready)          preferences.show() @@ -604,12 +615,13 @@ class MainWindow(QtGui.QMainWindow):              return          self._trying_to_start_eip = settings.get_autostart_eip() -        self._backend.eip_can_start(default_provider) +        self._backend.eip_can_start(domain=default_provider)          # If we don't want to start eip, we leave everything          # initialized to quickly start it          if not self._trying_to_start_eip: -            self._backend.eip_setup(default_provider, skip_network=True) +            self._backend.eip_setup(provider=default_provider, +                                    skip_network=True)      def _backend_can_start_eip(self):          """ @@ -686,7 +698,9 @@ class MainWindow(QtGui.QMainWindow):          Displays the EIP preferences window.          """          domain = self._login_widget.get_selected_provider() -        EIPPreferencesWindow(self, domain, self._backend).show() +        pref = EIPPreferencesWindow(self, domain, +                                    self._backend, self._leap_signaler) +        pref.show()      #      # updates @@ -821,7 +835,7 @@ class MainWindow(QtGui.QMainWindow):          """          providers = self._settings.get_configured_providers() -        self._backend.provider_get_all_services(providers) +        self._backend.provider_get_all_services(providers=providers)      def _provider_get_all_services(self, services):          self._set_eip_visible(EIP_SERVICE in services) @@ -1121,7 +1135,7 @@ class MainWindow(QtGui.QMainWindow):          emit the corresponding signals inmediately          """          domain = self._login_widget.get_selected_provider() -        self._backend.provider_setup(domain) +        self._backend.provider_setup(provider=domain)      @QtCore.Slot(dict)      def _load_provider_config(self, data): @@ -1136,11 +1150,11 @@ class MainWindow(QtGui.QMainWindow):                       backend.provider_setup()          :type data: dict          """ -        if data[self._backend.PASSED_KEY]: +        if data[PASSED_KEY]:              selected_provider = self._login_widget.get_selected_provider() -            self._backend.provider_bootstrap(selected_provider) +            self._backend.provider_bootstrap(provider=selected_provider)          else: -            logger.error(data[self._backend.ERROR_KEY]) +            logger.error(data[ERROR_KEY])              self._login_problem_provider()      @QtCore.Slot() @@ -1242,16 +1256,17 @@ class MainWindow(QtGui.QMainWindow):          Once the provider configuration is loaded, this starts the SRP          authentication          """ -        if data[self._backend.PASSED_KEY]: +        if data[PASSED_KEY]:              username = self._login_widget.get_user()              password = self._login_widget.get_password()              self._show_hide_unsupported_services()              domain = self._login_widget.get_selected_provider() -            self._backend.user_login(domain, username, password) +            self._backend.user_login(provider=domain, +                                     username=username, password=password)          else: -            logger.error(data[self._backend.ERROR_KEY]) +            logger.error(data[ERROR_KEY])              self._login_problem_provider()      @QtCore.Slot() @@ -1277,11 +1292,11 @@ class MainWindow(QtGui.QMainWindow):          if MX_SERVICE in self._enabled_services:              btn_enabled = self._login_widget.set_logout_btn_enabled              btn_enabled(False) -            sig = self._backend.signaler +            sig = self._leap_signaler              sig.soledad_bootstrap_failed.connect(lambda: btn_enabled(True))              sig.soledad_bootstrap_finished.connect(lambda: btn_enabled(True)) -        if not MX_SERVICE in self._provider_details['services']: +        if MX_SERVICE not in self._provider_details['services']:              self._set_mx_visible(False)      def _start_eip_bootstrap(self): @@ -1316,7 +1331,7 @@ class MainWindow(QtGui.QMainWindow):          """          domain = self._login_widget.get_selected_provider()          lang = QtCore.QLocale.system().name() -        self._backend.provider_get_details(domain, lang) +        self._backend.provider_get_details(domain=domain, lang=lang)      @QtCore.Slot()      def _provider_get_details(self, details): @@ -1385,11 +1400,14 @@ class MainWindow(QtGui.QMainWindow):                  # this is mostly for internal use/debug for now.                  logger.warning("Sorry! Log-in at least one time.")                  return -            self._backend.soledad_load_offline(full_user_id, password, uuid) +            self._backend.soledad_load_offline(username=full_user_id, +                                               password=password, uuid=uuid)          else:              if self._logged_user is not None:                  domain = self._login_widget.get_selected_provider() -                self._backend.soledad_bootstrap(username, domain, password) +                self._backend.soledad_bootstrap(username=username, +                                                domain=domain, +                                                password=password)      ###################################################################      # Service control methods: soledad @@ -1468,14 +1486,27 @@ class MainWindow(QtGui.QMainWindow):          signal that currently is beeing processed under status_panel.          After the refactor to EIPConductor this should not be necessary.          """ -        domain = self._login_widget.get_selected_provider() +        self._already_started_eip = True -        self._eip_status.set_provider(domain) +        domain = self._login_widget.get_selected_provider()          self._settings.set_defaultprovider(domain) -        self._already_started_eip = True + +        self._backend.eip_get_gateway_country_code(domain=domain)          # check for connectivity -        self._backend.eip_check_dns(domain) +        self._backend.eip_check_dns(domain=domain) + +    @QtCore.Slot() +    def _set_eip_provider(self, country_code=None): +        """ +        TRIGGERS: +            Signaler.eip_get_gateway_country_code +            Signaler.eip_no_gateway + +        Set the current provider and country code in the eip status widget. +        """ +        domain = self._login_widget.get_selected_provider() +        self._eip_status.set_provider(domain, country_code)      @QtCore.Slot()      def _eip_dns_error(self): @@ -1539,7 +1570,7 @@ class MainWindow(QtGui.QMainWindow):              self._eip_status.eip_button.setEnabled(False)              domain = self._login_widget.get_selected_provider() -            self._backend.eip_setup(domain) +            self._backend.eip_setup(provider=domain)              self._already_started_eip = True              # we want to start soledad anyway after a certain timeout if eip @@ -1571,12 +1602,12 @@ class MainWindow(QtGui.QMainWindow):          Start the VPN thread if the eip configuration is properly          loaded.          """ -        passed = data[self._backend.PASSED_KEY] +        passed = data[PASSED_KEY]          if not passed:              error_msg = self.tr("There was a problem with the provider")              self._eip_status.set_eip_status(error_msg, error=True) -            logger.error(data[self._backend.ERROR_KEY]) +            logger.error(data[ERROR_KEY])              self._already_started_eip = False              return @@ -1594,11 +1625,11 @@ class MainWindow(QtGui.QMainWindow):          This is used for intermediate bootstrapping stages, in case          they fail.          """ -        passed = data[self._backend.PASSED_KEY] +        passed = data[PASSED_KEY]          if not passed:              self._login_widget.set_status(                  self.tr("Unable to connect: Problem with provider")) -            logger.error(data[self._backend.ERROR_KEY]) +            logger.error(data[ERROR_KEY])              self._already_started_eip = False              self._eip_status.aborted() @@ -1663,9 +1694,9 @@ class MainWindow(QtGui.QMainWindow):          This is used for intermediate bootstrapping stages, in case          they fail.          """ -        passed = data[self._backend.PASSED_KEY] +        passed = data[PASSED_KEY]          if not passed: -            logger.error(data[self._backend.ERROR_KEY]) +            logger.error(data[ERROR_KEY])              self._login_problem_provider()      # @@ -1708,13 +1739,13 @@ class MainWindow(QtGui.QMainWindow):          self._cancel_ongoing_defers() -        self._services_being_stopped = {'imap', 'eip'} +        self._services_being_stopped = set(('imap', 'eip'))          imap_stopped = lambda: self._remove_service('imap') -        self._backend.signaler.imap_stopped.connect(imap_stopped) +        self._leap_signaler.imap_stopped.connect(imap_stopped)          eip_stopped = lambda: self._remove_service('eip') -        self._backend.signaler.eip_stopped.connect(eip_stopped) +        self._leap_signaler.eip_stopped.connect(eip_stopped)          logger.debug('Stopping mail services')          self._backend.imap_stop_service() @@ -1732,8 +1763,10 @@ class MainWindow(QtGui.QMainWindow):          Start the quit sequence and wait for services to finish.          Cleanup and close the main window before quitting.          """ -        # TODO separate the shutting down of services from the -        # UI stuff. +        if self._quitting: +            return + +        self._quitting = True          # first thing to do quitting, hide the mainwindow and show tooltip.          self.hide() @@ -1786,8 +1819,6 @@ class MainWindow(QtGui.QMainWindow):          Final steps to quit the app, starting from here we don't care about          running services or user interaction, just quitting.          """ -        logger.debug('Final quit...') -          # We can reach here because all the services are stopped or because a          # timeout was triggered. Since we want to run this only once, we exit          # if this is called twice. @@ -1796,12 +1827,17 @@ class MainWindow(QtGui.QMainWindow):          self._finally_quitting = True +        logger.debug('Closing soledad...') +        self._backend.soledad_close() +        logger.debug('Final quit...') + +        self._leap_signaler.stop() +        self._backend.stop() +        time.sleep(0.05)  # give the thread a little time to finish. +          # Remove lockfiles on a clean shutdown.          logger.debug('Cleaning pidfiles')          if IS_WIN:              WindowsLock.release_all_locks() -        self._backend.stop()          self.close() - -        QtDelayedCall(100, twisted_main.quit) diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index a3b81d38..3c9cd5d0 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -38,7 +38,8 @@ class PreferencesWindow(QtGui.QDialog):      """      preferences_saved = QtCore.Signal() -    def __init__(self, parent, username, domain, backend, soledad_started, mx): +    def __init__(self, parent, username, domain, backend, soledad_started, mx, +                 leap_signaler):          """          :param parent: parent object of the PreferencesWindow.          :parent type: QWidget @@ -58,6 +59,7 @@ class PreferencesWindow(QtGui.QDialog):          self._username = username          self._domain = domain +        self._leap_signaler = leap_signaler          self._backend = backend          self._soledad_started = soledad_started          self._mx_provided = mx @@ -194,7 +196,8 @@ class PreferencesWindow(QtGui.QDialog):              return          self._set_changing_password(True) -        self._backend.user_change_password(current_password, new_password) +        self._backend.user_change_password(current_password=current_password, +                                           new_password=new_password)      @QtCore.Slot()      def _srp_change_password_ok(self): @@ -208,7 +211,7 @@ class PreferencesWindow(QtGui.QDialog):          logger.debug("SRP password changed successfully.")          if self._mx_provided: -            self._backend.soledad_change_password(new_password) +            self._backend.soledad_change_password(new_password=new_password)          else:              self._change_password_success() @@ -357,7 +360,7 @@ class PreferencesWindow(QtGui.QDialog):          save_services = partial(self._save_enabled_services, domain)          self.ui.pbSaveServices.clicked.connect(save_services) -        self._backend.provider_get_supported_services(domain) +        self._backend.provider_get_supported_services(domain=domain)      @QtCore.Slot(str)      def _load_services(self, services): @@ -423,7 +426,7 @@ class PreferencesWindow(QtGui.QDialog):          """          Helper to connect to backend signals          """ -        sig = self._backend.signaler +        sig = self._leap_signaler          sig.prov_get_supported_services.connect(self._load_services) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 00a1387e..91f1f605 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -240,9 +240,9 @@ class CompositeMachine(QStateMachine):          c2.qtsigs.disconnected_signal.connect(self.off_ev2_slot)      # XXX why is this getting deletec in c++? -    #Traceback (most recent call last): -    #self.postEvent(self.events.on_ev2) -    #RuntimeError: Internal C++ object (ConnectedEvent2) already deleted. +    # Traceback (most recent call last): +    # self.postEvent(self.events.on_ev2) +    # RuntimeError: Internal C++ object (ConnectedEvent2) already deleted.      # XXX trying the following workaround, since      # I cannot find why in the world this is getting deleted :(      # XXX refactor! @@ -318,9 +318,8 @@ class ConnectionMachineBuilder(object):          components = self._conn.components          if components is None: -        # simple case: connection definition inherits directly from -        # the abstract connection. - +            # simple case: connection definition inherits directly from +            # the abstract connection.              leap_assert_type(self._conn, connections.AbstractLEAPConnection)              return self._make_simple_machine(self._conn, **kwargs) diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py deleted file mode 100644 index b1ce0ead..00000000 --- a/src/leap/bitmask/gui/twisted_main.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# twisted_main.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/>. -""" -Main functions for integration of twisted reactor -""" -import logging - -from twisted.internet import error, reactor -from PySide import QtCore - -logger = logging.getLogger(__name__) - - -def stop(): -    QtCore.QCoreApplication.sendPostedEvents() -    QtCore.QCoreApplication.flush() -    try: -        reactor.stop() -        logger.debug('Twisted reactor stopped') -    except error.ReactorNotRunning: -        logger.debug('Twisted reactor not running') -    logger.debug("Done stopping all the things.") - - -def quit(): -    """ -    Stop the mainloop. -    """ -    reactor.callLater(0, stop) diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index f66c553d..be5bde52 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -24,6 +24,10 @@ from functools import partial  from PySide import QtCore, QtGui +# TODO: we should use a more granular signaling instead of passing error/ok as +# a result. +from leap.bitmask.backend.leapbackend import ERROR_KEY, PASSED_KEY +  from leap.bitmask.config import flags  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.services import get_service_display_name, get_supported @@ -49,16 +53,12 @@ class Wizard(QtGui.QWizard):      REGISTER_USER_PAGE = 4      SERVICES_PAGE = 5 -    def __init__(self, backend, bypass_checks=False): +    def __init__(self, backend, leap_signaler):          """          Constructor for the main Wizard.          :param backend: Backend being used          :type backend: Backend -        :param bypass_checks: Set to true if the app should bypass -                              first round of checks for CA -                              certificates at bootstrap -        :type bypass_checks: bool          """          QtGui.QWizard.__init__(self) @@ -83,7 +83,10 @@ class Wizard(QtGui.QWizard):          self.ui.grpCheckProvider.setVisible(False)          self._connect_and_track(self.ui.btnCheck.clicked, self._check_provider) -        self._connect_and_track(self.ui.lnProvider.returnPressed, self._check_provider) +        self._connect_and_track(self.ui.lnProvider.returnPressed, +                                self._check_provider) + +        self._leap_signaler = leap_signaler          self._backend = backend          self._backend_connect() @@ -98,22 +101,24 @@ class Wizard(QtGui.QWizard):          self._provider_select_defer = None          self._provider_setup_defer = None -        self._connect_and_track(self.currentIdChanged, self._current_id_changed) +        self._connect_and_track(self.currentIdChanged, +                                self._current_id_changed) -        self._connect_and_track(self.ui.lnProvider.textChanged, self._enable_check) +        self._connect_and_track(self.ui.lnProvider.textChanged, +                                self._enable_check)          self._connect_and_track(self.ui.rbNewProvider.toggled, -            lambda x: self._enable_check()) +                                lambda x: self._enable_check())          self._connect_and_track(self.ui.cbProviders.currentIndexChanged[int], -            self._reset_provider_check) +                                self._reset_provider_check)          self._connect_and_track(self.ui.lblUser.returnPressed, -            self._focus_password) +                                self._focus_password)          self._connect_and_track(self.ui.lblPassword.returnPressed, -            self._focus_second_password) +                                self._focus_second_password)          self._connect_and_track(self.ui.lblPassword2.returnPressed, -            self._register) +                                self._register)          self._connect_and_track(self.ui.btnRegister.clicked, -            self._register) +                                self._register)          self._connect_and_track(self.ui.rbExistingProvider.toggled,                                  self._skip_provider_checks) @@ -184,21 +189,55 @@ class Wizard(QtGui.QWizard):          :param pinned: list of pinned providers          :type pinned: list of str + + +        How the combobox items are arranged: +        ----------------------------------- + +        First run: + +            demo.bitmask.net +            -- +            pinned2.org +            pinned1.org +            pinned3.org + +        After some usage: + +            added-by-user.org +            pinned-but-then-used.org +            --- +            demo.bitmask.net +            pinned1.org +            pinned3.org +            pinned2.org + +        In other words: +            * There are two sections. +            * Section one consists of all the providers that the user has used. +              If this is empty, than use demo.bitmask.net for this section. +              This list is sorted alphabetically. +            * Section two consists of all the pinned or 'pre seeded' providers, +              minus any providers that are now in section one. This last list +              is in random order.          """          ls = LeapSettings() -        providers = ls.get_configured_providers() -        if not providers and not pinned: +        user_added = ls.get_configured_providers() +        if not user_added and not pinned:              self.ui.rbExistingProvider.setEnabled(False)              self.ui.label_8.setEnabled(False)  # 'https://' label              self.ui.cbProviders.setEnabled(False)              return -        user_added = [] +        user_added.sort() + +        if not user_added: +            user_added = [pinned.pop(0)] -        # separate pinned providers from user added ones -        for p in providers: -            if p not in pinned: -                user_added.append(p) +        # separate unused pinned providers from user added ones +        for p in user_added: +            if p in pinned: +                pinned.remove(p)          if user_added:              self.ui.cbProviders.addItems(user_added) @@ -288,7 +327,8 @@ class Wizard(QtGui.QWizard):          if user_ok and pass_ok:              self._set_register_status(self.tr("Starting registration...")) -            self._backend.user_register(self._domain, username, password) +            self._backend.user_register(provider=self._domain, +                                        username=username, password=password)              self._username = username              self._password = password          else: @@ -440,7 +480,7 @@ class Wizard(QtGui.QWizard):          self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON)          self._provider_select_defer = self._backend.\ -            provider_setup(self._domain) +            provider_setup(provider=self._domain)      @QtCore.Slot(bool)      def _skip_provider_checks(self, skip): @@ -475,8 +515,8 @@ class Wizard(QtGui.QWizard):          :param complete_page: page id to complete          :type complete_page: int          """ -        passed = data[self._backend.PASSED_KEY] -        error = data[self._backend.ERROR_KEY] +        passed = data[PASSED_KEY] +        error = data[ERROR_KEY]          if passed:              label.setPixmap(self.OK_ICON)              if complete: @@ -496,7 +536,7 @@ class Wizard(QtGui.QWizard):          """          self._complete_task(data, self.ui.lblNameResolution)          status = "" -        passed = data[self._backend.PASSED_KEY] +        passed = data[PASSED_KEY]          if not passed:              status = self.tr("<font color='red'><b>Non-existent "                               "provider</b></font>") @@ -516,10 +556,10 @@ class Wizard(QtGui.QWizard):          """          self._complete_task(data, self.ui.lblHTTPS)          status = "" -        passed = data[self._backend.PASSED_KEY] +        passed = data[PASSED_KEY]          if not passed:              status = self.tr("<font color='red'><b>%s</b></font>") \ -                % (data[self._backend.ERROR_KEY]) +                % (data[ERROR_KEY])              self.ui.lblProviderSelectStatus.setText(status)          else:              self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON) @@ -536,22 +576,21 @@ class Wizard(QtGui.QWizard):          check. Since this check is the last of this set, it also          completes the page if passed          """ -        if data[self._backend.PASSED_KEY]: +        if data[PASSED_KEY]:              self._complete_task(data, self.ui.lblProviderInfo,                                  True, self.SELECT_PROVIDER_PAGE)              self._provider_checks_ok = True              lang = QtCore.QLocale.system().name() -            self._backend.provider_get_details(self._domain, lang) +            self._backend.provider_get_details(domain=self._domain, lang=lang)          else:              new_data = { -                self._backend.PASSED_KEY: False, -                self._backend.ERROR_KEY: -                self.tr("Unable to load provider configuration") +                PASSED_KEY: False, +                ERROR_KEY: self.tr("Unable to load provider configuration")              }              self._complete_task(new_data, self.ui.lblProviderInfo)          status = "" -        if not data[self._backend.PASSED_KEY]: +        if not data[PASSED_KEY]:              status = self.tr("<font color='red'><b>Not a valid provider"                               "</b></font>")              self.ui.lblProviderSelectStatus.setText(status) @@ -582,7 +621,7 @@ class Wizard(QtGui.QWizard):          Sets the status for the download of the CA certificate check          """          self._complete_task(data, self.ui.lblDownloadCaCert) -        passed = data[self._backend.PASSED_KEY] +        passed = data[PASSED_KEY]          if passed:              self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON) @@ -595,7 +634,7 @@ class Wizard(QtGui.QWizard):          Sets the status for the CA fingerprint check          """          self._complete_task(data, self.ui.lblCheckCaFpr) -        passed = data[self._backend.PASSED_KEY] +        passed = data[PASSED_KEY]          if passed:              self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON) @@ -689,7 +728,7 @@ class Wizard(QtGui.QWizard):                  self.page(pageId).setSubTitle(sub_title)                  self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON)                  self._provider_setup_defer = self._backend.\ -                    provider_bootstrap(self._domain) +                    provider_bootstrap(provider=self._domain)          if pageId == self.PRESENT_PROVIDER_PAGE:              sub_title = self.tr("Description of services offered by {0}") @@ -752,7 +791,7 @@ class Wizard(QtGui.QWizard):          """          Connects all the backend signals with the wizard.          """ -        sig = self._backend.signaler +        sig = self._leap_signaler          conntrack = self._connect_and_track          conntrack(sig.prov_name_resolution, self._name_resolution)          conntrack(sig.prov_https_connection, self._https_connection) diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index 384e1ec1..e4d6f9b3 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -21,7 +21,6 @@ import logging  import os  import platform  import stat -import sys  import subprocess  import tempfile @@ -103,7 +102,7 @@ def get_missing_helpers_dialog():      msg.setWindowTitle(msg.tr("Missing helper files"))      msg.setText(msg.tr(WE_NEED_POWERS))      # but maybe the user really deserve to know more -    #msg.setInformativeText(msg.tr(BECAUSE)) +    # msg.setInformativeText(msg.tr(BECAUSE))      msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)      msg.addButton("No, don't ask again", QtGui.QMessageBox.RejectRole)      msg.setDefaultButton(QtGui.QMessageBox.Yes) @@ -373,30 +372,6 @@ def DarwinInitializer():  # Linux initializers  # -def _get_missing_resolvconf_dialog(): -    """ -    Create a dialog for notifying about missing openresolv. - -    :rtype: QtGui.QMessageBox instance -    """ -    msgstr = QtCore.QObject() -    msgstr.NO_RESOLVCONF = msgstr.tr( -        "Could not find <b>resolvconf</b> installed in your system.\n" -        "Do you want to quit Bitmask now?") - -    msgstr.EXPLAIN = msgstr.tr( -        "Encrypted Internet needs resolvconf installed to work properly.\n" -        "Please use your package manager to install it.\n") - -    msg = QtGui.QMessageBox() -    msg.setWindowTitle(msg.tr("Missing resolvconf framework")) -    msg.setText(msgstr.NO_RESOLVCONF) -    # but maybe the user really deserve to know more -    msg.setInformativeText(msgstr.EXPLAIN) -    msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) -    msg.setDefaultButton(QtGui.QMessageBox.Yes) -    return msg -  def _get_missing_complain_dialog(stuff):      """ @@ -445,21 +420,6 @@ def _get_missing_complain_dialog(stuff):      return msg -def _linux_check_resolvconf(): -    """ -    Raise a dialog warning about the lack of the resolvconf framework. -    """ -    RESOLVCONF_PATH = "/sbin/resolvconf" -    missing = not os.path.isfile(RESOLVCONF_PATH) - -    if missing: -        msg = _get_missing_resolvconf_dialog() -        ret = msg.exec_() - -        if ret == QtGui.QMessageBox.Yes: -            sys.exit() - -  def _linux_install_missing_scripts(badexec, notfound):      """      Try to install the missing helper files. @@ -509,9 +469,8 @@ def LinuxInitializer():      """      Raise a dialog if needed files are missing. -    Missing files can be either system-wide resolvconf, bitmask-root, or -    policykit file. The dialog will also be raised if some of those files are +    Missing files can be either bitmask-root policykit file. +    The dialog will also be raised if some of those files are      found to have incorrect permissions.      """ -    _linux_check_resolvconf()      check_missing() diff --git a/src/leap/bitmask/provider/pinned.py b/src/leap/bitmask/provider/pinned.py index 38851621..6fd2fa70 100644 --- a/src/leap/bitmask/provider/pinned.py +++ b/src/leap/bitmask/provider/pinned.py @@ -32,6 +32,7 @@ class PinnedProviders(object):      CONFIG_KEY = "config"      CACERT_KEY = "cacert" +    PREFERRED_PROVIDER = pinned_demobitmask.DOMAIN      PROVIDERS = {          pinned_demobitmask.DOMAIN: { @@ -50,11 +51,16 @@ class PinnedProviders(object):      @classmethod      def domains(self):          """ -        Return the domains that are pinned in here +        Return the domains that are pinned in here. +        The first domain in the list is the preferred one.          :rtype: list of str          """ -        return self.PROVIDERS.keys() +        domains = self.PROVIDERS.keys() +        domains.remove(self.PREFERRED_PROVIDER) +        domains.insert(0, self.PREFERRED_PROVIDER) + +        return domains      @classmethod      def save_hardcoded(self, domain, provider_path, cacert_path): diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 8c96a8b5..71edbb87 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -90,7 +90,7 @@ class ProviderBootstrapper(AbstractBootstrapper):          self._provider_config = None          self._download_if_needed = False          if signaler is not None: -            self._cancel_signal = signaler.PROV_CANCELLED_SETUP +            self._cancel_signal = signaler.prov_cancelled_setup      @property      def verify(self): @@ -194,8 +194,8 @@ class ProviderBootstrapper(AbstractBootstrapper):          verify = self.verify          if mtime:  # the provider.json exists -        # So, we're getting it from the api.* and checking against -        # the provider ca. +            # So, we're getting it from the api.* and checking against +            # the provider ca.              try:                  provider_config = ProviderConfig()                  provider_config.load(provider_json) @@ -228,7 +228,7 @@ class ProviderBootstrapper(AbstractBootstrapper):                  # TODO split                  if not provider.supports_client(min_client_version):                      self._signaler.signal( -                        self._signaler.PROV_UNSUPPORTED_CLIENT) +                        self._signaler.prov_unsupported_client)                      raise UnsupportedClientVersionError()              provider_definition, mtime = get_content(res) @@ -250,7 +250,7 @@ class ProviderBootstrapper(AbstractBootstrapper):                               'Found: {1}.').format(api_supported, api_version)                      logger.error(error) -                    self._signaler.signal(self._signaler.PROV_UNSUPPORTED_API) +                    self._signaler.signal(self._signaler.prov_unsupported_api)                      raise UnsupportedProviderAPI(error)      def run_provider_select_checks(self, domain, download_if_needed=False): @@ -271,10 +271,10 @@ class ProviderBootstrapper(AbstractBootstrapper):          cb_chain = [              (self._check_name_resolution, -             self._signaler.PROV_NAME_RESOLUTION_KEY), -            (self._check_https, self._signaler.PROV_HTTPS_CONNECTION_KEY), +             self._signaler.prov_name_resolution), +            (self._check_https, self._signaler.prov_https_connection),              (self._download_provider_info, -             self._signaler.PROV_DOWNLOAD_PROVIDER_INFO_KEY) +             self._signaler.prov_download_provider_info)          ]          return self.addCallbackChain(cb_chain) @@ -401,11 +401,11 @@ class ProviderBootstrapper(AbstractBootstrapper):          self._download_if_needed = download_if_needed          cb_chain = [ -            (self._download_ca_cert, self._signaler.PROV_DOWNLOAD_CA_CERT_KEY), +            (self._download_ca_cert, self._signaler.prov_download_ca_cert),              (self._check_ca_fingerprint, -             self._signaler.PROV_CHECK_CA_FINGERPRINT_KEY), +             self._signaler.prov_check_ca_fingerprint),              (self._check_api_certificate, -             self._signaler.PROV_CHECK_API_CERTIFICATE_KEY) +             self._signaler.prov_check_api_certificate)          ]          return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/eip/conductor.py b/src/leap/bitmask/services/eip/conductor.py index a8821160..bb07809a 100644 --- a/src/leap/bitmask/services/eip/conductor.py +++ b/src/leap/bitmask/services/eip/conductor.py @@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)  class EIPConductor(object): -    def __init__(self, settings, backend, **kwargs): +    def __init__(self, settings, backend, leap_signaler, **kwargs):          """          Initializes EIP Conductor. @@ -46,6 +46,7 @@ class EIPConductor(object):          self.eip_connection = EIPConnection()          self.eip_name = get_service_display_name(EIP_SERVICE)          self._settings = settings +        self._leap_signaler = leap_signaler          self._backend = backend          self._eip_status = None @@ -76,7 +77,7 @@ class EIPConductor(object):          """          Connect to backend signals.          """ -        signaler = self._backend.signaler +        signaler = self._leap_signaler          # for conductor          signaler.eip_process_restart_tls.connect(self._do_eip_restart) @@ -201,7 +202,7 @@ class EIPConductor(object):              # we bypass the on_eip_disconnected here              plug_restart_on_disconnected()              self.qtsigs.disconnected_signal.emit() -            #QtDelayedCall(0, self.qtsigs.disconnected_signal.emit) +            # QtDelayedCall(0, self.qtsigs.disconnected_signal.emit)              # ...and reconnect the original signal again, after having used the              # diversion              QtDelayedCall(500, reconnect_disconnected_signal) @@ -300,7 +301,7 @@ class EIPConductor(object):          # XXX FIXME --- check exitcode is != 0 really.          # bitmask-root is masking the exitcode, so we might need          # to fix it on that side. -        #if exitCode != 0 and not self.user_stopped_eip: +        # if exitCode != 0 and not self.user_stopped_eip:          if not self.user_stopped_eip:              eip_status_label = self._eip_status.tr(                  "{0} finished in an unexpected manner!") diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py index 41d75052..f83e0170 100644 --- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py +++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py @@ -46,7 +46,9 @@ class DarwinVPNLauncher(VPNLauncher):      INSTALL_MSG = ("\"Bitmask needs administrative privileges to install "                     "missing scripts and fix permissions.\"") -    INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../") +    # Hardcode the installation path for OSX for security, openvpn is +    # run as root +    INSTALL_PATH = "/Applications/Bitmask.app/"      INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../")      OPENVPN_BIN = 'openvpn.leap'      OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) diff --git a/src/leap/bitmask/services/eip/eipbootstrapper.py b/src/leap/bitmask/services/eip/eipbootstrapper.py index c77977ce..264eac2e 100644 --- a/src/leap/bitmask/services/eip/eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/eipbootstrapper.py @@ -53,7 +53,7 @@ class EIPBootstrapper(AbstractBootstrapper):          self._eip_config = None          self._download_if_needed = False          if signaler is not None: -            self._cancel_signal = signaler.EIP_CANCELLED_SETUP +            self._cancel_signal = signaler.eip_cancelled_setup      def _download_config(self, *args):          """ @@ -116,9 +116,9 @@ class EIPBootstrapper(AbstractBootstrapper):          self._download_if_needed = download_if_needed          cb_chain = [ -            (self._download_config, self._signaler.EIP_CONFIG_READY), +            (self._download_config, self._signaler.eip_config_ready),              (self._download_client_certificates, -             self._signaler.EIP_CLIENT_CERTIFICATE_READY) +             self._signaler.eip_client_certificate_ready)          ]          return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py index 8ec0c050..b6e47f25 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -74,6 +74,7 @@ def _is_auth_agent_running():          'ps aux | grep "polkit-[m]ate-authentication-agent-1"',          'ps aux | grep "[l]xpolkit"',          'ps aux | grep "[g]nome-shell"', +        'ps aux | grep "[f]ingerprint-polkit-agent"',      ]      is_running = [commands.getoutput(cmd) for cmd in polkit_options] @@ -126,12 +127,6 @@ class LinuxVPNLauncher(VPNLauncher):              # LinuxPolicyChecker will give us the right path if standalone.              return LinuxPolicyChecker.get_polkit_path() -    class RESOLVCONF_BIN_PATH(object): -        def __call__(self): -            return ("/usr/local/sbin/leap-resolvconf" if flags.STANDALONE else -                    "/sbin/resolvconf") -        # this only will work with debian/ubuntu distros. -      OTHER_FILES = (POLKIT_PATH, BITMASK_ROOT, OPENVPN_BIN_PATH)      @classmethod diff --git a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py index 6640a860..1888f2c9 100644 --- a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py @@ -30,7 +30,7 @@ import time  try:      import unittest2 as unittest  except ImportError: -    import unittest +    import unittest  # noqa - skip 'unused import' warning  from nose.twistedtools import deferred, reactor  from twisted.internet import threads diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index 0731bee3..72e19413 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -27,7 +27,7 @@ from abc import ABCMeta, abstractmethod  from functools import partial  from leap.bitmask.config import flags -from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.backend.settings import Settings, GATEWAY_AUTOMATIC  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.platform_init import IS_LINUX  from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector @@ -122,12 +122,12 @@ class VPNLauncher(object):          :rtype: list          """          gateways = [] -        leap_settings = LeapSettings() +        settings = Settings()          domain = providerconfig.get_domain() -        gateway_conf = leap_settings.get_selected_gateway(domain) +        gateway_conf = settings.get_selected_gateway(domain)          gateway_selector = VPNGatewaySelector(eipconfig) -        if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: +        if gateway_conf == GATEWAY_AUTOMATIC:              gateways = gateway_selector.get_gateways()          else:              gateways = [gateway_conf] @@ -136,12 +136,6 @@ class VPNLauncher(object):              logger.error('No gateway was found!')              raise VPNLauncherException('No gateway was found!') -        # this only works for selecting the first gateway, as we're -        # currently doing. -        ccodes = gateway_selector.get_gateways_country_code() -        gateway_ccode = ccodes[gateways[0]] -        flags.CURRENT_VPN_COUNTRY = gateway_ccode -          logger.debug("Using gateways ips: {0}".format(', '.join(gateways)))          return gateways @@ -175,11 +169,11 @@ class VPNLauncher(object):          leap_assert_type(providerconfig, ProviderConfig)          # XXX this still has to be changed on osx and windows accordingly -        #kwargs = {} -        #openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) -        #if not openvpn_possibilities: -            #raise OpenVPNNotFoundException() -        #openvpn = first(openvpn_possibilities) +        # kwargs = {} +        # openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) +        # if not openvpn_possibilities: +        #     raise OpenVPNNotFoundException() +        # openvpn = first(openvpn_possibilities)          # -----------------------------------------          openvpn_path = force_eval(kls.OPENVPN_BIN_PATH) diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index b54f2925..d1a3fdaa 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -118,10 +118,10 @@ class VPNObserver(object):          """          sig = self._signaler          signals = { -            "network_unreachable": sig.EIP_NETWORK_UNREACHABLE, -            "process_restart_tls": sig.EIP_PROCESS_RESTART_TLS, -            "process_restart_ping": sig.EIP_PROCESS_RESTART_PING, -            "initialization_completed": sig.EIP_CONNECTED +            "network_unreachable": sig.eip_network_unreachable, +            "process_restart_tls": sig.eip_process_restart_tls, +            "process_restart_ping": sig.eip_process_restart_ping, +            "initialization_completed": sig.eip_connected          }          return signals.get(event.lower()) @@ -255,6 +255,9 @@ class VPN(object):          """          Tear the firewall down using the privileged wrapper.          """ +        if IS_MAC: +            # We don't support Mac so far +            return True          BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)          exitCode = subprocess.call(["pkexec",                                      BM_ROOT, "firewall", "stop"]) @@ -594,7 +597,7 @@ class VPNManager(object):              state = status_step              if state != self._last_state: -                self._signaler.signal(self._signaler.EIP_STATE_CHANGED, state) +                self._signaler.signal(self._signaler.eip_state_changed, state)                  self._last_state = state      def _parse_status_and_notify(self, output): @@ -632,7 +635,7 @@ class VPNManager(object):          status = (tun_tap_read, tun_tap_write)          if status != self._last_status: -            self._signaler.signal(self._signaler.EIP_STATUS_CHANGED, status) +            self._signaler.signal(self._signaler.eip_status_changed, status)              self._last_status = status      def get_state(self): @@ -814,7 +817,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):          leap_assert_type(eipconfig, EIPConfig)          leap_assert_type(providerconfig, ProviderConfig) -        #leap_assert(not self.isRunning(), "Starting process more than once!") +        # leap_assert(not self.isRunning(), "Starting process more than once!")          self._eipconfig = eipconfig          self._providerconfig = providerconfig @@ -869,7 +872,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):          if isinstance(exit_code, int):              logger.debug("processExited, status %d" % (exit_code,))          self._signaler.signal( -            self._signaler.EIP_PROCESS_FINISHED, exit_code) +            self._signaler.eip_process_finished, exit_code)          self._alive = False      def processEnded(self, reason): diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index 98b40929..5e85368f 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -64,7 +64,8 @@ class IMAPControl(object):          """          Start imap service.          """ -        self._backend.imap_start_service(self.userid, flags.OFFLINE) +        self._backend.imap_start_service(full_user_id=self.userid, +                                         offline=flags.OFFLINE)      def stop_imap_service(self):          """ @@ -146,7 +147,8 @@ class SMTPControl(object):          :type download_if_needed: bool          """          self.smtp_connection.qtsigs.connecting_signal.emit() -        self._backend.smtp_start_service(self.userid, download_if_needed) +        self._backend.smtp_start_service(full_user_id=self.userid, +                                         download_if_needed=download_if_needed)      def stop_smtp_service(self):          """ diff --git a/src/leap/bitmask/services/mail/plumber.py b/src/leap/bitmask/services/mail/plumber.py index c16a1fed..1af65c5d 100644 --- a/src/leap/bitmask/services/mail/plumber.py +++ b/src/leap/bitmask/services/mail/plumber.py @@ -26,7 +26,7 @@ from functools import partial  from twisted.internet import defer -from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.backend.settings import Settings  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.provider import get_provider_path  from leap.bitmask.services.soledad.soledadbootstrapper import get_db_paths @@ -83,7 +83,8 @@ def initialize_soledad(uuid, email, passwd,          secrets,          localdb,          server_url, -        cert_file) +        cert_file, +        defer_encryption=True)      return soledad @@ -113,7 +114,7 @@ class MBOXPlumber(object):          self.user = user          self.mdir = mdir          self.sol = None -        self._settings = LeapSettings() +        self._settings = Settings()          provider_config_path = os.path.join(get_path_prefix(),                                              get_provider_path(provider)) @@ -231,8 +232,8 @@ class MBOXPlumber(object):          with open(mail_filename) as f:              mail_string = f.read() -            #uid = self._mbox.getUIDNext() -            #print "saving with UID: %s" % uid +            # uid = self._mbox.getUIDNext() +            # print "saving with UID: %s" % uid              d = self._mbox.messages.add_msg(                  mail_string, notify_on_disk=True)          return d diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index db12fd80..c4e43bfe 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -21,6 +21,7 @@ import logging  import os  import socket  import sys +import time  from ssl import SSLError  from sqlite3 import ProgrammingError as sqlite_ProgrammingError @@ -132,12 +133,15 @@ class SoledadBootstrapper(AbstractBootstrapper):      MAX_INIT_RETRIES = 10      MAX_SYNC_RETRIES = 10 +    WAIT_MAX_SECONDS = 600 +    # WAIT_STEP_SECONDS = 1 +    WAIT_STEP_SECONDS = 5      def __init__(self, signaler=None):          AbstractBootstrapper.__init__(self, signaler)          if signaler is not None: -            self._cancel_signal = signaler.SOLEDAD_CANCELLED_BOOTSTRAP +            self._cancel_signal = signaler.soledad_cancelled_bootstrap          self._provider_config = None          self._soledad_config = None @@ -181,17 +185,16 @@ class SoledadBootstrapper(AbstractBootstrapper):          :param uuid: the user uuid          :type uuid: str or unicode          """ -        print "UUID ", uuid          self._address = username          self._password = password          self._uuid = uuid          try:              self.load_and_sync_soledad(uuid, offline=True) -            self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FINISHED) +            self._signaler.signal(self._signaler.soledad_offline_finished)          except Exception as e:              # TODO: we should handle more specific exceptions in here              logger.exception(e) -            self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FAILED) +            self._signaler.signal(self._signaler.soledad_offline_failed)      def _get_soledad_local_params(self, uuid, offline=False):          """ @@ -356,12 +359,20 @@ class SoledadBootstrapper(AbstractBootstrapper):          Do several retries to get an initial soledad sync.          """          # and now, let's sync -        sync_tries = 1 -        while sync_tries <= self.MAX_SYNC_RETRIES: +        sync_tries = self.MAX_SYNC_RETRIES +        step = self.WAIT_STEP_SECONDS +        max_wait = self.WAIT_MAX_SECONDS +        while sync_tries > 0: +            wait = 0              try:                  logger.debug("Trying to sync soledad....")                  self._try_soledad_sync() -                logger.debug("Soledad has been synced.") +                while self.soledad.syncing: +                    time.sleep(step) +                    wait += step +                    if wait >= max_wait: +                        raise SoledadSyncError("timeout!") +                logger.debug("Soledad has been synced!")                  # so long, and thanks for all the fish                  return              except SoledadSyncError: @@ -379,9 +390,10 @@ class SoledadBootstrapper(AbstractBootstrapper):                  continue              except InvalidAuthTokenError:                  self._signaler.signal( -                    self._signaler.SOLEDAD_INVALID_AUTH_TOKEN) +                    self._signaler.soledad_invalid_auth_token)                  raise              except Exception as e: +                # XXX release syncing lock                  logger.exception("Unhandled error while syncing "                                   "soledad: %r" % (e,))                  break @@ -423,7 +435,8 @@ class SoledadBootstrapper(AbstractBootstrapper):                  local_db_path=local_db_path.encode(encoding),                  server_url=server_url,                  cert_file=cert_file.encode(encoding), -                auth_token=auth_token) +                auth_token=auth_token, +                defer_encryption=True)          # XXX All these errors should be handled by soledad itself,          # and return a subclass of SoledadInitializationFailed @@ -448,7 +461,10 @@ class SoledadBootstrapper(AbstractBootstrapper):          Raises SoledadSyncError if not successful.          """          try: -            self._soledad.sync() +            logger.debug("BOOTSTRAPPER: trying to sync Soledad....") +            # pass defer_decryption=False to get inline decryption +            # for debugging. +            self._soledad.sync(defer_decryption=True)          except SSLError as exc:              logger.error("%r" % (exc,))              raise SoledadSyncError("Failed to sync soledad") @@ -633,11 +649,11 @@ class SoledadBootstrapper(AbstractBootstrapper):          self._password = password          if flags.OFFLINE: -            signal_finished = self._signaler.SOLEDAD_OFFLINE_FINISHED -            signal_failed = self._signaler.SOLEDAD_OFFLINE_FAILED +            signal_finished = self._signaler.soledad_offline_finished +            signal_failed = self._signaler.soledad_offline_failed          else: -            signal_finished = self._signaler.SOLEDAD_BOOTSTRAP_FINISHED -            signal_failed = self._signaler.SOLEDAD_BOOTSTRAP_FAILED +            signal_finished = self._signaler.soledad_bootstrap_finished +            signal_failed = self._signaler.soledad_bootstrap_failed          try:              self._download_config() diff --git a/src/leap/bitmask/services/tests/test_abstractbootstrapper.py b/src/leap/bitmask/services/tests/test_abstractbootstrapper.py index 3ac126ac..c3fda9e1 100644 --- a/src/leap/bitmask/services/tests/test_abstractbootstrapper.py +++ b/src/leap/bitmask/services/tests/test_abstractbootstrapper.py @@ -1,4 +1,4 @@ -## -*- coding: utf-8 -*- +# -*- coding: utf-8 -*-  # test_abstrctbootstrapper.py  # Copyright (C) 2013 LEAP  # diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index 25b86874..caa94ec7 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -129,3 +129,28 @@ def force_eval(items):          return map(do_eval, items)      else:          return do_eval(items) + + +def dict_to_flags(values): +    """ +    Set the flags values given in the values dict. +    If a value isn't provided then use the already existing one. + +    :param values: the values to set. +    :type values: dict. +    """ +    for k, v in values.items(): +        setattr(flags, k, v) + + +def flags_to_dict(): +    """ +    Get the flags values in a dict. + +    :return: the values of flags into a dict. +    :rtype: dict. +    """ +    items = [i for i in dir(flags) if i[0] != '_'] +    values = {i: getattr(flags, i) for i in items} + +    return values diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index 0717aea5..cbd6d8a5 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -107,19 +107,19 @@ def build_parser():                               'against domains.')      # Not in use, we might want to reintroduce them. -    #parser.add_argument('-i', '--no-provider-checks', -                        #action="store_true", default=False, -                        #help="skips download of provider config files. gets " -                        #"config from local files only. Will fail if cannot " -                        #"find any") -    #parser.add_argument('-k', '--no-ca-verify', -                        #action="store_true", default=False, -                        #help="(insecure). Skips verification of the server " -                        #"certificate used in TLS handshake.") -    #parser.add_argument('-c', '--config', metavar="CONFIG FILE", nargs='?', -                        #action="store", dest="config_file", -                        #type=argparse.FileType('r'), -                        #help='optional config file') +    # parser.add_argument('-i', '--no-provider-checks', +    #                     action="store_true", default=False, +    #                     help="skips download of provider config files. gets " +    #                     "config from local files only. Will fail if cannot " +    #                     "find any") +    # parser.add_argument('-k', '--no-ca-verify', +    #                     action="store_true", default=False, +    #                     help="(insecure). Skips verification of the server " +    #                     "certificate used in TLS handshake.") +    # parser.add_argument('-c', '--config', metavar="CONFIG FILE", nargs='?', +    #                     action="store", dest="config_file", +    #                     type=argparse.FileType('r'), +    #                     help='optional config file')      return parser @@ -132,4 +132,9 @@ def get_options():      """      parser = build_parser()      opts, unknown = parser.parse_known_args() + +    # we add this option manually since it's not defined for 'release version' +    if IS_RELEASE_VERSION: +        opts.danger = False +      return opts diff --git a/src/leap/bitmask/util/polkit_agent.py b/src/leap/bitmask/util/polkit_agent.py index 6fda2f88..7764f571 100644 --- a/src/leap/bitmask/util/polkit_agent.py +++ b/src/leap/bitmask/util/polkit_agent.py @@ -39,9 +39,9 @@ def _launch_agent():          logger.error('Exception while running polkit authentication agent '                       '%s' % (exc,))          # XXX fix KDE launch. See: #3755 -        #try: -            #subprocess.call(KDE_PATH) -        #except Exception as exc: +        # try: +        #     subprocess.call(KDE_PATH) +        # except Exception as exc:  def launch(): diff --git a/src/leap/bitmask/util/privilege_policies.py b/src/leap/bitmask/util/privilege_policies.py index adc3503f..f894d73b 100644 --- a/src/leap/bitmask/util/privilege_policies.py +++ b/src/leap/bitmask/util/privilege_policies.py @@ -87,8 +87,8 @@ class LinuxPolicyChecker(PolicyChecker):                  else self.LINUX_POLKIT_FILE)      def is_missing_policy_permissions(self): -    # FIXME this name is quite confusing, it does not have anything to do with -    # file permissions. +        # FIXME this name is quite confusing, it does not have anything to do +        # with file permissions.          """          Returns True if we could not find the appropriate policykit file          in place | 
