diff options
author | kali <kali@leap.se> | 2014-08-08 19:10:39 -0500 |
---|---|---|
committer | kali <kali@leap.se> | 2014-08-08 19:10:39 -0500 |
commit | cef0be0b4f866c9283eaf696fbc23b8120e40443 (patch) | |
tree | ff54448f7354e56235d22dabaf0fb03597790c98 /src/leap/bitmask/backend | |
parent | a9173e8e2816b767028787735e4a84b2576f258d (diff) | |
parent | 465c0b89ca61279eb036afec2de3b4d24b257311 (diff) |
Merge branch '0.6.1-rc' into deb-0.6.1-rc
Conflicts:
pkg/linux/bitmask-root
src/leap/bitmask/gui/login.py
Diffstat (limited to 'src/leap/bitmask/backend')
-rw-r--r-- | src/leap/bitmask/backend/api.py | 154 | ||||
-rw-r--r-- | src/leap/bitmask/backend/backend.py | 246 | ||||
-rw-r--r-- | src/leap/bitmask/backend/backend_proxy.py | 209 | ||||
-rw-r--r-- | src/leap/bitmask/backend/components.py | 144 | ||||
-rw-r--r-- | src/leap/bitmask/backend/leapbackend.py | 305 | ||||
-rw-r--r-- | src/leap/bitmask/backend/leapsignaler.py | 435 | ||||
-rw-r--r-- | src/leap/bitmask/backend/settings.py | 158 | ||||
-rw-r--r-- | src/leap/bitmask/backend/signaler.py | 177 | ||||
-rw-r--r-- | src/leap/bitmask/backend/signaler_qt.py | 136 | ||||
-rw-r--r-- | src/leap/bitmask/backend/utils.py | 64 |
10 files changed, 1426 insertions, 602 deletions
diff --git a/src/leap/bitmask/backend/api.py b/src/leap/bitmask/backend/api.py new file mode 100644 index 00000000..3f6c0ad1 --- /dev/null +++ b/src/leap/bitmask/backend/api.py @@ -0,0 +1,154 @@ +# -*- 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" +PING_REQUEST = "PING" + +# XXX this needs documentation. What is it used for? + +API = ( + STOP_REQUEST, # this method needs to be defined in order to support the + # backend stop action + PING_REQUEST, + + "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", + "bitmask_root_vpn_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_bitmask_root_vpn_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..37535f37 --- /dev/null +++ b/src/leap/bitmask/backend/backend.py @@ -0,0 +1,246 @@ +# -*- 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/>. + +# FIXME this is missing module documentation. It would be fine to say a couple +# of lines about the whole backend architecture. +# TODO use txzmq bindings instead. + +import json +import threading +import time + +import psutil + +from twisted.internet import defer, reactor, threads + +import zmq +from zmq.auth.thread import ThreadAuthenticator + +from leap.bitmask.backend.api import API, PING_REQUEST +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. + """ + # XXX this should not be hardcoded. Make it configurable. + PORT = '5556' + + # XXX we might want to make this configurable per-platform, + # and use the most performant socket type on each one. + BIND_ADDR = "tcp://127.0.0.1:%s" % PORT + + PING_INTERVAL = 2 # secs + + def __init__(self, frontend_pid=None): + """ + Backend constructor, create needed instances. + """ + self._signaler = Signaler() + + self._frontend_pid = frontend_pid + + 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() + # XXX do not hardcode this here. + 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. + """ + pid = self._frontend_pid + check_wait = 0 + 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) + + check_wait += 0.01 + if pid is not None and check_wait > self.PING_INTERVAL: + check_wait = 0 + self._check_frontend_alive() + + def _check_frontend_alive(self): + """ + Check if the frontend is alive and stop the backend if it is not. + """ + pid = self._frontend_pid + if pid is not None and not psutil.pid_exists(pid): + logger.critical("The frontend is down!") + self.stop() + + 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 + """ + if request_json == PING_REQUEST: + # do not process request if it's just a ping + return + + 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..e2611251 --- /dev/null +++ b/src/leap/bitmask/backend/backend_proxy.py @@ -0,0 +1,209 @@ +# -*- 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. +""" +# XXX should document the relationship to the API here. + +import functools +import Queue +import threading +import time + +import zmq + +from leap.bitmask.backend.api import API, STOP_REQUEST, PING_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 + + POLL_TIMEOUT = 4000 # ms + POLL_TRIES = 3 + + PING_INTERVAL = 2 # secs + + 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._ping_at = 0 + self.online = False + + 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) + self._ping() + + logger.debug("BackendProxy worker stopped.") + + def _reset_ping(self): + """ + Reset the ping timeout counter. + This is called for every ping and request. + """ + self._ping_at = time.time() + self.PING_INTERVAL + + def _ping(self): + """ + Heartbeat helper. + Sends a PING request just to know that the server is alive. + """ + if time.time() > self._ping_at: + self._send_request(PING_REQUEST) + self._reset_ping() + + 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) + + 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) + self.online = False + else: + # msg = "Received reply for '{0}' -> '{1}'".format(request, reply) + # logger.debug(msg) + self.online = True + # request received, no ping needed for other interval. + self._reset_ping() + + 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..f721086b 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 @@ -68,7 +69,6 @@ class ILEAPComponent(zope.interface.Interface): """ Interface that every component for the backend should comply to """ - key = zope.interface.Attribute("Key id for this component") @@ -197,7 +197,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 +234,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 +253,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 +268,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 +279,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 +324,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 +401,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 +425,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 +482,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 +542,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): @@ -551,6 +551,12 @@ class EIP(object): """ self._vpn.tear_down_firewall() + def bitmask_root_vpn_down(self): + """ + Bring openvpn down, using bitmask-root helper. + """ + self._vpn.bitmask_root_vpn_down() + def get_gateways_list(self, domain): """ Signal a list of gateways for the given provider. @@ -566,7 +572,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 +586,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 +654,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 +691,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 +713,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 +725,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 +785,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 +841,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 +856,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 +914,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 +924,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 +947,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 +971,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 +990,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 +1075,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 +1128,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 +1153,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 +1165,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 +1188,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..3b023563 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): + def __init__(self, bypass_checks=False, frontend_pid=None): """ 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, frontend_pid) - # 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) + # 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 _stop(self): + def _check_type(self, obj, expected_type): """ - 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.") + Check the type of a parameter. - def _register(self, component): + :param obj: object to check its type. + :type obj: any type + :param expected_type: the expected type of the object. + :type expected_type: type """ - Registers a component in this backend - - :param component: Component to register - :type component: any object that implements ILEAPComponent - """ - # TODO: assert that the component implements the interfaces - # expected - try: - self._components[component.key] = component - except Exception: - logger.error("There was a problem registering %s" % (component,)) - - def _signal_back(self, _, signal): - """ - Helper method to signal back (callback like behavior) to the - UI that an operation finished. - - :param signal: signal name - :type signal: str - """ - self._signaler.signal(signal) - - def _worker(self): - """ - Worker method, called from a different thread and as a part of - a looping call - """ - try: - # this'll become recv_multipart - cmd = self._call_queue.get(block=False) - - # cmd is: component, method, signalback, *args - func = getattr(self._components[cmd[0]], cmd[1]) - d = func(*cmd[3:]) - if d is not None: # d may be None if a defer chain is cancelled. - # A call might not have a callback signal, but if it does, - # we add it to the chain - if cmd[2] is not None: - d.addCallbacks(self._signal_back, logger.error, cmd[2]) - d.addCallbacks(self._done_action, logger.error, - callbackKeywords={"d": d}) - d.addErrback(logger.error) - self._ongoing_defers.append(d) - except Empty: - # If it's just empty we don't have anything to do. - pass - except defer.CancelledError: - logger.debug("defer cancelled somewhere (CancelledError).") - except Exception as e: - # But we log the rest - logger.exception("Unexpected exception: {0!r}".format(e)) - - def _done_action(self, _, d): - """ - Remover of the defer once it's done - - :param d: defer to remove - :type d: twisted.internet.defer.Deferred - """ - if d in self._ongoing_defers: - self._ongoing_defers.remove(d) - - # XXX: Temporal interface until we migrate to zmq - # We simulate the calls to zmq.send_multipart. Once we separate - # this in two processes, the methods bellow can be changed to - # send_multipart and this backend class will be really simple. + 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,19 @@ 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 bitmask_root_vpn_down(self): + """ + Signal the need to bring vpn down. + """ + self._eip.bitmask_root_vpn_down() def user_login(self, provider, username, password): """ @@ -447,8 +342,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 +353,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 +376,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 +391,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 +401,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 +419,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 +437,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 +458,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 +473,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 +485,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 +497,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 +508,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 +523,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..c0fdffdc 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,102 @@ # 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_bitmask_root_vpn_down = 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..65bf6753 --- /dev/null +++ b/src/leap/bitmask/backend/utils.py @@ -0,0 +1,64 @@ +# -*- 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 stat + +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) + # set permissions to: 0700 (U:rwx G:--- O:---) + os.chmod(KEYS_DIR, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + + # 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 |