diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/leap/base/checks.py | 107 | ||||
| -rw-r--r-- | src/leap/base/constants.py | 2 | ||||
| -rw-r--r-- | src/leap/base/exceptions.py | 23 | ||||
| -rw-r--r-- | src/leap/base/network.py | 73 | ||||
| -rw-r--r-- | src/leap/base/tests/test_checks.py | 116 | ||||
| -rw-r--r-- | src/leap/baseapp/eip.py | 2 | ||||
| -rw-r--r-- | src/leap/baseapp/mainwindow.py | 3 | ||||
| -rw-r--r-- | src/leap/baseapp/network.py | 57 | ||||
| -rw-r--r-- | src/leap/eip/checks.py | 77 | ||||
| -rw-r--r-- | src/leap/eip/exceptions.py | 16 | ||||
| -rw-r--r-- | src/leap/eip/tests/test_checks.py | 60 | ||||
| -rw-r--r-- | src/leap/util/coroutines.py | 2 | 
12 files changed, 384 insertions, 154 deletions
| diff --git a/src/leap/base/checks.py b/src/leap/base/checks.py new file mode 100644 index 00000000..a775e162 --- /dev/null +++ b/src/leap/base/checks.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +import logging +import platform + +import ping +import requests + +from leap.base import constants +from leap.base import exceptions + +logger = logging.getLogger(name=__name__) + +class LeapNetworkChecker(object): +    """ +    all network related checks +    """ +    # TODO eventually, use a more portable solution +    # like psutil + +    def run_all(self, checker=None): +        if not checker: +            checker = self +        self.error = None  # ? + +        # for MVS +        checker.check_tunnel_default_interface() +        checker.check_internet_connection() +        checker.is_internet_up() +        checker.ping_gateway() + +    def check_internet_connection(self): +        try: +            # XXX remove this hardcoded random ip +            requests.get('http://216.172.161.165') +        except (requests.HTTPError, requests.RequestException) as e: +            raise exceptions.NoInternetConnection(e.message) +        except requests.ConnectionError as e: +            error = "Unidentified Connection Error" +            if e.message == "[Errno 113] No route to host": +                if not self.is_internet_up(): +                    error = "No valid internet connection found." +                else: +                    error = "Provider server appears to be down." +            raise exceptions.NoInternetConnection(error) +        logger.debug('Network appears to be up.') + +    def is_internet_up(self): +        iface, gateway = self.get_default_interface_gateway() +        self.ping_gateway(self) + +    def check_tunnel_default_interface(self): +        """ +        Raises an TunnelNotDefaultRouteError  +        (including when no routes are present) +        """ +        if not platform.system() == "Linux": +            raise NotImplementedError + +        f = open("/proc/net/route") +        route_table = f.readlines() +        f.close() +        #toss out header +        route_table.pop(0) + +        if not route_table: +            raise exceptions.TunnelNotDefaultRouteError() + +        line = route_table.pop(0) +        iface, destination = line.split('\t')[0:2] +        if not destination == '00000000' or not iface == 'tun0': +            raise exceptions.TunnelNotDefaultRouteError() + + +    def get_default_interface_gateway(self): +        """only impletemented for linux so far.""" +        if not platform.system() == "Linux": +            raise NotImplementedError + +        f = open("/proc/net/route") +        route_table = f.readlines() +        f.close() +        #toss out header +        route_table.pop(0) + +        default_iface = None +        gateway = None +        while route_table: +            line = route_table.pop(0) +            iface, destination, gateway = line.split('\t')[0:3] +            if destination == '00000000': +                default_iface = iface +                break + +        if not default_iface: +            raise exceptions.NoDefaultInterfaceFoundError + +        if default_iface not in netifaces.interfaces(): +            raise exceptions.InterfaceNotFoundError + +        return default_iface, gateway + +    def ping_gateway(self, gateway): +        #TODO: Discuss how much packet loss (%) is acceptable. +        packet_loss = ping.quiet_ping(gateway)[0] +        if packet_loss > constants.MAX_ICMP_PACKET_LOSS: +            raise exceptions.NoConnectionToGateway diff --git a/src/leap/base/constants.py b/src/leap/base/constants.py index 7a1415fb..8a76b6b4 100644 --- a/src/leap/base/constants.py +++ b/src/leap/base/constants.py @@ -28,3 +28,5 @@ DEFAULT_PROVIDER_DEFINITION = {      u'version': u'0.1.0'}  MAX_ICMP_PACKET_LOSS = 10 + +ROUTE_CHECK_INTERVAL = 120 diff --git a/src/leap/base/exceptions.py b/src/leap/base/exceptions.py index 9c4aa77b..48d827f5 100644 --- a/src/leap/base/exceptions.py +++ b/src/leap/base/exceptions.py @@ -4,3 +4,26 @@ class MissingConfigFileError(Exception):  class ImproperlyConfigured(Exception):      pass + + +class NoDefaultInterfaceFoundError(Exception): +    message = "no default interface found" +    usermessage = "Looks like your computer is not connected to the internet" + + +class InterfaceNotFoundError(Exception): +    # XXX should take iface arg on init maybe? +    message = "interface not found" + + +class NoConnectionToGateway(Exception): +    message = "no connection to gateway" +    usermessage = "Looks like there are problems with your internet connection" + + +class NoInternetConnection(Exception): +    message = "No Internet connection found" + + +class TunnelNotDefaultRouteError(Exception): +    message = "VPN Maybe be down." diff --git a/src/leap/base/network.py b/src/leap/base/network.py new file mode 100644 index 00000000..a1e7c880 --- /dev/null +++ b/src/leap/base/network.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from __future__ import (print_function) +import logging +import threading + +from leap.base.checks import LeapNetworkChecker +from leap.base.constants import ROUTE_CHECK_INTERVAL +from leap.base.exceptions import TunnelNotDefaultRouteError +from leap.util.coroutines import (launch_thread_no_daemon, process_events) + +from time import sleep + +logger = logging.getLogger(name=__name__) + + +class NetworkChecker(object): +    """ +    Manages network checking thread that makes sure we have a working network +    connection. +    """ +    def __init__(self, *args, **kwargs): +        self.status_signals = kwargs.pop('status_signals', None) +        self.watcher_cb = kwargs.pop('status_signals', None) +        self.excp_logger = lambda exc: logger.error("%s", exc.message) +        self.checker = LeapNetworkChecker() + +    def start(self): +        self.process_handle = self._launch_recurrent_network_checks((self.excp_logger,)) + +    def stop(self): +        #TODO: Thread still not being stopped when openvpn is stopped. +        logger.debug("stopping network checker...") +        self.process_handle._Thread__stop() +        logger.debug("network checked stopped.") + +    def run_checks(self): +        pass + +    #private methods + +    #here all the observers in fail_callbacks expect one positional argument, +    #which is exception so we can try by passing a lambda with logger to +    #check it works. +    def _network_checks_thread(self, fail_callbacks): +        #TODO: replace this with waiting for a signal from openvpn +        while True: +            try: +                self.checker.check_tunnel_default_interface() +                break +            except TunnelNotDefaultRouteError: +                sleep(1) + +        observer_dict = dict((( +            observer, process_events(observer)) for observer in fail_callbacks)) +        while True: +            try: +                self.checker.check_tunnel_default_interface() +                self.checker.check_internet_connection() +                sleep(ROUTE_CHECK_INTERVAL) +            except Exception as exc: +                for obs in observer_dict: +                    observer_dict[obs].send(exc) +                sleep(ROUTE_CHECK_INTERVAL) + + +    def _launch_recurrent_network_checks(self, fail_callbacks): +        #we need to wrap the fail callback in a turple +        watcher = launch_thread_no_daemon( +            self._network_checks_thread, +            (fail_callbacks,)) +        return watcher + + diff --git a/src/leap/base/tests/test_checks.py b/src/leap/base/tests/test_checks.py new file mode 100644 index 00000000..30746991 --- /dev/null +++ b/src/leap/base/tests/test_checks.py @@ -0,0 +1,116 @@ +try: +    import unittest2 as unittest +except ImportError: +    import unittest +import os + +from mock import (patch, Mock) +from StringIO import StringIO + +import ping +import requests + +from leap.base import checks +from leap.base import exceptions +from leap.testing.basetest import BaseLeapTest + +_uid = os.getuid() + + +class LeapNetworkCheckTest(BaseLeapTest): +    __name__ = "leap_network_check_tests" + +    def setUp(self): +        pass + +    def tearDown(self): +        pass + +    def test_checker_should_implement_check_methods(self): +        checker = checks.LeapNetworkChecker() + +        self.assertTrue(hasattr(checker, "check_internet_connection"), +                        "missing meth") +        self.assertTrue(hasattr(checker, "check_tunnel_default_interface"), +                        "missing meth") +        self.assertTrue(hasattr(checker, "is_internet_up"), +                        "missing meth") +        self.assertTrue(hasattr(checker, "ping_gateway"), +                        "missing meth") + +    def test_checker_should_actually_call_all_tests(self): +        checker = checks.LeapNetworkChecker() + +        mc = Mock() +        checker.run_all(checker=mc) +        self.assertTrue(mc.check_internet_connection.called, "not called") +        self.assertTrue(mc.check_tunnel_default_interface.called, "not called") +        self.assertTrue(mc.ping_gateway.called, "not called") +        self.assertTrue(mc.is_internet_up.called, "not called") + +    def test_get_default_interface_no_interface(self): +        checker = checks.LeapNetworkChecker() +        with patch('leap.base.checks.open', create=True) as mock_open: +            with self.assertRaises(exceptions.NoDefaultInterfaceFoundError): +                mock_open.return_value = StringIO( +                    "Iface\tDestination Gateway\t" +                    "Flags\tRefCntd\tUse\tMetric\t" +                    "Mask\tMTU\tWindow\tIRTT") +                checker.get_default_interface_gateway() + +    def test_check_tunnel_default_interface(self): +        checker = checks.LeapNetworkChecker() +        with patch('leap.base.checks.open', create=True) as mock_open: +            with self.assertRaises(exceptions.TunnelNotDefaultRouteError): +                mock_open.return_value = StringIO( +                    "Iface\tDestination Gateway\t" +                    "Flags\tRefCntd\tUse\tMetric\t" +                    "Mask\tMTU\tWindow\tIRTT") +                checker.check_tunnel_default_interface() + +        with patch('leap.base.checks.open', create=True) as mock_open: +            with self.assertRaises(exceptions.TunnelNotDefaultRouteError): +                mock_open.return_value = StringIO( +                    "Iface\tDestination Gateway\t" +                    "Flags\tRefCntd\tUse\tMetric\t" +                    "Mask\tMTU\tWindow\tIRTT\n" +                    "wlan0\t00000000\t0102A8C0\t0003\t0\t0\t0\t00000000\t0\t0\t0") +                checker.check_tunnel_default_interface() + +        with patch('leap.base.checks.open', create=True) as mock_open: +            mock_open.return_value = StringIO( +                "Iface\tDestination Gateway\t" +                "Flags\tRefCntd\tUse\tMetric\t" +                "Mask\tMTU\tWindow\tIRTT\n" +                "tun0\t00000000\t01002A0A\t0003\t0\t0\t0\t00000080\t0\t0\t0") +            checker.check_tunnel_default_interface() + +    def test_ping_gateway_fail(self): +        checker = checks.LeapNetworkChecker() +        with patch.object(ping, "quiet_ping") as mocked_ping: +            with self.assertRaises(exceptions.NoConnectionToGateway): +                mocked_ping.return_value = [11, "", ""] +                checker.ping_gateway("4.2.2.2") + +    def test_check_internet_connection_failures(self): +        checker = checks.LeapNetworkChecker() +        with patch.object(requests, "get") as mocked_get: +            mocked_get.side_effect = requests.HTTPError +            with self.assertRaises(exceptions.NoInternetConnection): +                checker.check_internet_connection() + +        with patch.object(requests, "get") as mocked_get: +            mocked_get.side_effect = requests.RequestException +            with self.assertRaises(exceptions.NoInternetConnection): +                checker.check_internet_connection() + +        #TODO: Mock possible errors that can be raised by is_internet_up +        with patch.object(requests, "get") as mocked_get: +            mocked_get.side_effect = requests.ConnectionError +            with self.assertRaises(exceptions.NoInternetConnection): +                checker.check_internet_connection() + +    @unittest.skipUnless(_uid == 0, "root only") +    def test_ping_gateway(self): +        checker = checks.LeapNetworkChecker() +        checker.ping_gateway("4.2.2.2") diff --git a/src/leap/baseapp/eip.py b/src/leap/baseapp/eip.py index b0e14be7..ad074abc 100644 --- a/src/leap/baseapp/eip.py +++ b/src/leap/baseapp/eip.py @@ -224,9 +224,11 @@ class EIPConductorAppMixin(object):                  # we could bring Timer Init to this Mixin                  # or to its own Mixin.                  self.timer.start(constants.TIMER_MILLISECONDS) +                self.network_checker.start()              return          if self.eip_service_started is True: +            self.network_checker.stop()              self.conductor.disconnect()              if self.debugmode:                  self.startStopButton.setText('&Connect') diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py index 10b23d9a..7b2ecb1d 100644 --- a/src/leap/baseapp/mainwindow.py +++ b/src/leap/baseapp/mainwindow.py @@ -8,6 +8,7 @@ from PyQt4 import QtGui  from leap.baseapp.eip import EIPConductorAppMixin  from leap.baseapp.log import LogPaneMixin  from leap.baseapp.systray import StatusAwareTrayIconMixin +from leap.baseapp.network import NetworkCheckerAppMixin  from leap.baseapp.leap_app import MainWindowMixin  logger = logging.getLogger(name=__name__) @@ -16,6 +17,7 @@ logger = logging.getLogger(name=__name__)  class LeapWindow(QtGui.QMainWindow,                   MainWindowMixin, EIPConductorAppMixin,                   StatusAwareTrayIconMixin, +                 NetworkCheckerAppMixin,                   LogPaneMixin):      """      main window for the leap app. @@ -36,6 +38,7 @@ class LeapWindow(QtGui.QMainWindow,              self.createLogBrowser()          EIPConductorAppMixin.__init__(self, opts=opts)          StatusAwareTrayIconMixin.__init__(self) +        NetworkCheckerAppMixin.__init__(self)          MainWindowMixin.__init__(self)          # bind signals diff --git a/src/leap/baseapp/network.py b/src/leap/baseapp/network.py new file mode 100644 index 00000000..75690cc9 --- /dev/null +++ b/src/leap/baseapp/network.py @@ -0,0 +1,57 @@ +from __future__ import print_function +import logging +import time +logger = logging.getLogger(name=__name__) + +from leap.base.network import NetworkChecker +from leap.baseapp.dialogs import ErrorDialog + + +class NetworkCheckerAppMixin(object): +    """ +    initialize an instance of the Network Checker, +    which gathers error and passes them on. +    """ + +    def __init__(self, *args, **kwargs): +        opts = kwargs.pop('opts', None) +        config_file = getattr(opts, 'config_file', None) + +        self.network_checker_started = False + +        self.network_checker = NetworkChecker( +            watcher_cb=self.newLogLine.emit, +            status_signals=(self.statusChange.emit, ), +            debug=self.debugmode) + +        self.network_checker.run_checks() +        self.error_check() + +    def error_check(self): +        """ +        consumes the conductor error queue. +        pops errors, and acts accordingly (launching user dialogs). +        """ +        logger.debug('error check') + +        errq = self.conductor.error_queue +        while errq.qsize() != 0: +            logger.debug('%s errors left in network queue', errq.qsize()) +            # we get exception and original traceback from queue +            error, tb = errq.get() + +            # redundant log, debugging the loop. +            logger.error('%s: %s', error.__class__.__name__, error.message) + +            if issubclass(error.__class__, eip_exceptions.EIPClientError): +                self.handle_network_error(error) + +            else: +                # deprecated form of raising exception. +                raise error, None, tb + +            if error.failfirst is True: +                break + +    def handle_network_error(self, error): +        pass diff --git a/src/leap/eip/checks.py b/src/leap/eip/checks.py index 9b7b1cee..9872f8d8 100644 --- a/src/leap/eip/checks.py +++ b/src/leap/eip/checks.py @@ -39,10 +39,6 @@ into base.tests to be invoked by the base leap init routines.  However, I'm testing them alltogether for the sake of having the whole unit  reachable and testable as a whole. -LeapNetworkChecker ------------------- -Network checks. To be moved to base. -docs TBD  """ @@ -52,79 +48,6 @@ def get_ca_cert():          return certs.where(ca_file) -class LeapNetworkChecker(object): -    """ -    all network related checks -    """ -    # XXX to be moved to leap.base.checks -    # TODO eventually, use a more portable solution -    # like psutil - -    def run_all(self, checker=None): -        if not checker: -            checker = self -        self.error = None  # ? - -        # for MVS -        checker.test_internet_connection() -        checker.is_internet_up() -        checker.ping_gateway() - -    def test_internet_connection(self): -        # XXX we're not passing the error anywhere. -        # XXX we probably should raise an exception here? -        # unless we use this as smoke test -        try: -            # XXX remove this hardcoded random ip -            requests.get('http://216.172.161.165') -        except (requests.HTTPError, requests.RequestException) as e: -            self.error = e.message -        except requests.ConenctionError as e: -            if e.message == "[Errno 113] No route to host": -                if not self.is_internet_up(): -                    self.error = "No valid internet connection found." -                else: -                    self.error = "Provider server appears to be down." - -    def is_internet_up(self): -        iface, gateway = self.get_default_interface_gateway() -        self.ping_gateway(self) - -    def get_default_interface_gateway(self): -        """only impletemented for linux so far.""" -        if not platform.system() == "Linux": -            raise NotImplementedError - -        f = open("/proc/net/route") -        route_table = f.readlines() -        f.close() -        #toss out header -        route_table.pop(0) - -        default_iface = None -        gateway = None -        while route_table: -            line = route_table.pop(0) -            iface, destination, gateway = line.split('\t')[0:3] -            if destination == '00000000': -                default_iface = iface -                break - -        if not default_iface: -            raise eipexceptions.NoDefaultInterfaceFoundError - -        if default_iface not in netifaces.interfaces(): -            raise eipexceptions.InterfaceNotFoundError - -        return default_iface, gateway - -    def ping_gateway(self, gateway): -        #TODO: Discuss how much packet loss (%) is acceptable. -        packet_loss = ping.quiet_ping(gateway)[0] -        if packet_loss > baseconstants.MAX_ICMP_PACKET_LOSS: -            raise eipexceptions.NoConnectionToGateway - -  class ProviderCertChecker(object):      """      Several checks needed for getting diff --git a/src/leap/eip/exceptions.py b/src/leap/eip/exceptions.py index f048621f..6b4ee6aa 100644 --- a/src/leap/eip/exceptions.py +++ b/src/leap/eip/exceptions.py @@ -121,22 +121,6 @@ class EIPInitBadProviderError(EIPClientError):  class EIPConfigurationError(EIPClientError):      pass - -class NoDefaultInterfaceFoundError(EIPClientError): -    message = "no default interface found" -    usermessage = "Looks like your computer is not connected to the internet" - - -class InterfaceNotFoundError(EIPClientError): -    # XXX should take iface arg on init maybe? -    message = "interface not found" - - -class NoConnectionToGateway(EIPClientError): -    message = "no connection to gateway" -    usermessage = "Looks like there are problems with your internet connection" - -  #  # Errors that probably we don't need anymore  # chase down for them and check. diff --git a/src/leap/eip/tests/test_checks.py b/src/leap/eip/tests/test_checks.py index 19b54c04..06133825 100644 --- a/src/leap/eip/tests/test_checks.py +++ b/src/leap/eip/tests/test_checks.py @@ -9,10 +9,8 @@ import os  import time  import urlparse -from StringIO import StringIO  from mock import (patch, Mock) -import ping  import requests  from leap.base import config as baseconfig @@ -26,8 +24,6 @@ from leap.testing.basetest import BaseLeapTest  from leap.testing.https_server import BaseHTTPSServerTestCase  from leap.testing.https_server import where as where_cert -_uid = os.getuid() -  class NoLogRequestHandler:      def log_message(self, *args): @@ -38,60 +34,6 @@ class NoLogRequestHandler:          return '' -class LeapNetworkCheckTest(BaseLeapTest): -    # XXX to be moved to base.checks - -    __name__ = "leap_network_check_tests" - -    def setUp(self): -        pass - -    def tearDown(self): -        pass - -    def test_checker_should_implement_check_methods(self): -        checker = eipchecks.LeapNetworkChecker() - -        self.assertTrue(hasattr(checker, "test_internet_connection"), -                        "missing meth") -        self.assertTrue(hasattr(checker, "is_internet_up"), -                        "missing meth") -        self.assertTrue(hasattr(checker, "ping_gateway"), -                        "missing meth") - -    def test_checker_should_actually_call_all_tests(self): -        checker = eipchecks.LeapNetworkChecker() - -        mc = Mock() -        checker.run_all(checker=mc) -        self.assertTrue(mc.test_internet_connection.called, "not called") -        self.assertTrue(mc.ping_gateway.called, "not called") -        self.assertTrue(mc.is_internet_up.called, -                        "not called") - -    def test_get_default_interface_no_interface(self): -        checker = eipchecks.LeapNetworkChecker() -        with patch('leap.eip.checks.open', create=True) as mock_open: -            with self.assertRaises(eipexceptions.NoDefaultInterfaceFoundError): -                mock_open.return_value = StringIO( -                    "Iface\tDestination Gateway\t" -                    "Flags\tRefCntd\tUse\tMetric\t" -                    "Mask\tMTU\tWindow\tIRTT") -                checker.get_default_interface_gateway() - -    def test_ping_gateway_fail(self): -        checker = eipchecks.LeapNetworkChecker() -        with patch.object(ping, "quiet_ping") as mocked_ping: -            with self.assertRaises(eipexceptions.NoConnectionToGateway): -                mocked_ping.return_value = [11, "", ""] -                checker.ping_gateway("4.2.2.2") - -    @unittest.skipUnless(_uid == 0, "root only") -    def test_ping_gateway(self): -        checker = eipchecks.LeapNetworkChecker() -        checker.ping_gateway("4.2.2.2") - -  class EIPCheckTest(BaseLeapTest):      __name__ = "eip_check_tests" @@ -131,8 +73,6 @@ class EIPCheckTest(BaseLeapTest):                          "not called")          self.assertTrue(mc.check_complete_eip_config.called,                          "not called") -        #self.assertTrue(mc.ping_gateway.called, -                        #"not called")      # test individual check methods diff --git a/src/leap/util/coroutines.py b/src/leap/util/coroutines.py index e7ccfacf..b9d0a98b 100644 --- a/src/leap/util/coroutines.py +++ b/src/leap/util/coroutines.py @@ -72,7 +72,7 @@ def watch_output(out, observers):      :type out: fd      :param observers: tuple of coroutines to send data\  for each event -    :type ovservers: tuple +    :type observers: tuple      """      observer_dict = dict(((observer, process_events(observer))                           for observer in observers)) | 
