diff options
| -rw-r--r-- | src/leap/Authentication.py | 11 | ||||
| -rw-r--r-- | src/leap/Configuration.py | 11 | ||||
| -rw-r--r-- | src/leap/Connection.py | 129 | ||||
| -rw-r--r-- | src/leap/EIPConnection.py | 270 | ||||
| -rw-r--r-- | src/leap/OpenVPNConnection.py | 408 | 
5 files changed, 829 insertions, 0 deletions
diff --git a/src/leap/Authentication.py b/src/leap/Authentication.py new file mode 100644 index 00000000..0bd54fd6 --- /dev/null +++ b/src/leap/Authentication.py @@ -0,0 +1,11 @@ +""" +Authentication Base Class +""" + +class Authentication(object): +    """ +    I have no idea how Authentication (certs,?) will be done, but stub it here. +    """ +    pass + + diff --git a/src/leap/Configuration.py b/src/leap/Configuration.py new file mode 100644 index 00000000..b0ab2bf2 --- /dev/null +++ b/src/leap/Configuration.py @@ -0,0 +1,11 @@ +""" +Configuration Base Class +""" + +class Configuration(object): +    """ +    I have no idea how configuration (txt vs. sqlite) will be done, but let's stub it now. +    """ +    pass +     + diff --git a/src/leap/Connection.py b/src/leap/Connection.py new file mode 100644 index 00000000..6534560b --- /dev/null +++ b/src/leap/Connection.py @@ -0,0 +1,129 @@ +""" +Base Connection Classs +""" +from __future__ import (division, unicode_literals, print_function) +#import threading +from functools import partial +import logging + +from leap.utils.coroutines import spawn_and_watch_process +from leap.baseapp.config import get_config, get_vpn_stdout_mockup +from leap.eip.vpnwatcher import EIPConnectionStatus, status_watcher +from leap.eip.vpnmanager import OpenVPNManager, ConnectionRefusedError + +from leap.Configuration  import Configuration +from leap.Authentication import Authentication + +logger = logging.getLogger(name=__name__) + +class Connection(Configuration, Authentication): +    def __init__(self, *args, **kwargs): +        self.connection_state = None +        self.desired_connection_state = None +        super(Connection, self).__init__(*args, **kwargs) + +    def connect(self): +        """ +        entry point for connection process +        """ +        pass + +    def disconnect(self): +        """ +        disconnects client +        """ +        pass + +    def shutdown(self): +        """ +        shutdown and quit +        """ +        self.desired_con_state = self.status.DISCONNECTED + +    def connection_state(self): +        """ +        returns the current connection state +        """ +        return self.status.current + +    def desired_connection_state(self): +        """ +        returns the desired_connection state +        """ +        return self.desired_connection_state + +    def poll_connection_state(self): +        """ +        """ +        try: +            state = self.get_connection_state() +        except ConnectionRefusedError: +            # connection refused. might be not ready yet. +            return +        if not state: +            return +        (ts, status_step, +         ok, ip, remote) = state +        self.status.set_vpn_state(status_step) +        status_step = self.status.get_readable_status() +        return (ts, status_step, ok, ip, remote) + +    def get_icon_name(self): +        """ +        get icon name from status object +        """ +        return self.status.get_state_icon() + +    # +    # private methods +    # + +    def _disconnect(self): +        """ +        private method for disconnecting +        """ +        if self.subp is not None: +            self.subp.terminate() +            self.subp = None +        # XXX signal state changes! :) + +    def _is_alive(self): +        """ +        don't know yet +        """ +        pass + +    def _connect(self): +        """ +        entry point for connection cascade methods. +        """ +        #conn_result = ConState.DISCONNECTED +        try: +            conn_result = self._try_connection() +        except UnrecoverableError as except_msg: +            logger.error("FATAL: %s" % unicode(except_msg)) +            conn_result = self.status.UNRECOVERABLE +        except Exception as except_msg: +            self.error_queue.append(except_msg) +            logger.error("Failed Connection: %s" % +                         unicode(except_msg)) +        return conn_result +	 + + +class ConnectionError(Exception): +    """ +    generic connection error +    """ +    def __str__(self): +        if len(self.args) >= 1: +            return repr(self.args[0]) +        else: +            raise self() + + +class UnrecoverableError(ConnectionError): +    """ +    we cannot do anything about it, sorry +    """ +    pass diff --git a/src/leap/EIPConnection.py b/src/leap/EIPConnection.py new file mode 100644 index 00000000..f16f01f5 --- /dev/null +++ b/src/leap/EIPConnection.py @@ -0,0 +1,270 @@ +""" +EIP Connection Class +""" + +from leap.OpenVPNConnection import OpenVPNConnection, MissingSocketError, ConnectionRefusedError +from leap.Connection import ConnectionError + +class EIPConnection(OpenVPNConnection): +    """ +    Manages the execution of the OpenVPN process, auto starts, monitors the +    network connection, handles configuration, fixes leaky hosts, handles +    errors, etc. +    Preferences will be stored via the Storage API. (TBD) +    Status updates (connected, bandwidth, etc) are signaled to the GUI. +    """ + +    def __init__(self, *args, **kwargs): +        self.settingsfile = kwargs.get('settingsfile', None) +        self.logfile = kwargs.get('logfile', None) +        self.error_queue = [] +        self.desired_con_state = None  # ??? + +        status_signals = kwargs.pop('status_signals', None) +        self.status = EIPConnectionStatus(callbacks=status_signals) + +        super(EIPConnection, self).__init__(*args, **kwargs) + +    def connect(self): +        """ +        entry point for connection process +        """ +        self.forget_errors() +        self._try_connection() +        # XXX should capture errors? + +    def disconnect(self): +        """ +        disconnects client +        """ +        self._disconnect() +        self.status.change_to(self.status.DISCONNECTED) +        pass + +    def shutdown(self): +        """ +        shutdown and quit +        """ +        self.desired_con_state = self.status.DISCONNECTED + +    def connection_state(self): +        """ +        returns the current connection state +        """ +        return self.status.current + +    def desired_connection_state(self): +        """ +        returns the desired_connection state +        """ +        return self.desired_con_state + +    def poll_connection_state(self): +        """ +        """ +        try: +            state = self.get_connection_state() +        except ConnectionRefusedError: +            # connection refused. might be not ready yet. +            return +        if not state: +            return +        (ts, status_step, +         ok, ip, remote) = state +        self.status.set_vpn_state(status_step) +        status_step = self.status.get_readable_status() +        return (ts, status_step, ok, ip, remote) + +    def get_icon_name(self): +        """ +        get icon name from status object +        """ +        return self.status.get_state_icon() + +    # +    # private methods +    # + +    def _disconnect(self): +        """ +        private method for disconnecting +        """ +        if self.subp is not None: +            self.subp.terminate() +            self.subp = None +        # XXX signal state changes! :) + +    def _is_alive(self): +        """ +        don't know yet +        """ +        pass + +    def _connect(self): +        """ +        entry point for connection cascade methods. +        """ +        #conn_result = ConState.DISCONNECTED +        try: +            conn_result = self._try_connection() +        except UnrecoverableError as except_msg: +            logger.error("FATAL: %s" % unicode(except_msg)) +            conn_result = self.status.UNRECOVERABLE +        except Exception as except_msg: +            self.error_queue.append(except_msg) +            logger.error("Failed Connection: %s" % +                         unicode(except_msg)) +        return conn_result + +"""generic watcher object that keeps track of connection status""" +# This should be deprecated in favor of daemon mode + management +# interface. But we can leave it here for debug purposes. + + +class EIPConnectionStatus(object): +    """ +    Keep track of client (gui) and openvpn +    states. + +    These are the OpenVPN states: +    CONNECTING    -- OpenVPN's initial state. +    WAIT          -- (Client only) Waiting for initial response +                     from server. +    AUTH          -- (Client only) Authenticating with server. +    GET_CONFIG    -- (Client only) Downloading configuration options +                     from server. +    ASSIGN_IP     -- Assigning IP address to virtual network +                     interface. +    ADD_ROUTES    -- Adding routes to system. +    CONNECTED     -- Initialization Sequence Completed. +    RECONNECTING  -- A restart has occurred. +    EXITING       -- A graceful exit is in progress. + +    We add some extra states: + +    DISCONNECTED  -- GUI initial state. +    UNRECOVERABLE -- An unrecoverable error has been raised +                     while invoking openvpn service. +    """ +    CONNECTING = 1 +    WAIT = 2 +    AUTH = 3 +    GET_CONFIG = 4 +    ASSIGN_IP = 5 +    ADD_ROUTES = 6 +    CONNECTED = 7 +    RECONNECTING = 8 +    EXITING = 9 + +    # gui specific states: +    UNRECOVERABLE = 11 +    DISCONNECTED = 0 + +    def __init__(self, callbacks=None): +        """ +        EIPConnectionStatus is initialized with a tuple +        of signals to be triggered. +        :param callbacks: a tuple of (callable) observers +        :type callbacks: tuple +        """ +        # (callbacks to connect to signals in Qt-land) +        self.current = self.DISCONNECTED +        self.previous = None +        self.callbacks = callbacks + +    def get_readable_status(self): +        # XXX DRY status / labels a little bit. +        # think we'll want to i18n this. +        human_status = { +            0: 'disconnected', +            1: 'connecting', +            2: 'waiting', +            3: 'authenticating', +            4: 'getting config', +            5: 'assigning ip', +            6: 'adding routes', +            7: 'connected', +            8: 'reconnecting', +            9: 'exiting', +            11: 'unrecoverable error', +        } +        return human_status[self.current] + +    def get_state_icon(self): +        """ +        returns the high level icon +        for each fine-grain openvpn state +        """ +        connecting = (self.CONNECTING, +                      self.WAIT, +                      self.AUTH, +                      self.GET_CONFIG, +                      self.ASSIGN_IP, +                      self.ADD_ROUTES) +        connected = (self.CONNECTED,) +        disconnected = (self.DISCONNECTED, +                        self.UNRECOVERABLE) + +        # this can be made smarter, +        # but it's like it'll change, +        # so +readability. + +        if self.current in connecting: +            return "connecting" +        if self.current in connected: +            return "connected" +        if self.current in disconnected: +            return "disconnected" + +    def set_vpn_state(self, status): +        """ +        accepts a state string from the management +        interface, and sets the internal state. +        :param status: openvpn STATE (uppercase). +        :type status: str +        """ +        if hasattr(self, status): +            self.change_to(getattr(self, status)) + +    def set_current(self, to): +        """ +        setter for the 'current' property +        :param to: destination state +        :type to: int +        """ +        self.current = to + +    def change_to(self, to): +        """ +        :param to: destination state +        :type to: int +        """ +        if to == self.current: +            return +        changed = False +        from_ = self.current +        self.current = to + +        # We can add transition restrictions +        # here to ensure no transitions are +        # allowed outside the fsm. + +        self.set_current(to) +        changed = True + +        #trigger signals (as callbacks) +        #print('current state: %s' % self.current) +        if changed: +            self.previous = from_ +            if self.callbacks: +                for cb in self.callbacks: +                    if callable(cb): +                        cb(self) + + + +class EIPClientError(ConnectionError): +    """ +    base EIPClient Exception +    """ +    pass diff --git a/src/leap/OpenVPNConnection.py b/src/leap/OpenVPNConnection.py new file mode 100644 index 00000000..a26059a7 --- /dev/null +++ b/src/leap/OpenVPNConnection.py @@ -0,0 +1,408 @@ +""" +OpenVPN Connection +""" + +from __future__ import (print_function) +import logging +import os +import socket +import telnetlib +import time +from functools import partial + +logger = logging.getLogger(name=__name__) + +from leap.utils.coroutines import spawn_and_watch_process +from leap.baseapp.config import get_config +from leap.Connection import Connection + +class OpenVPNConnection(Connection): +    """ +    All related to invocation +    of the openvpn binary +    """ +    # Connection Methods + +    def __init__(self, config_file=None, watcher_cb=None,host="/tmp/.eip.sock", port="unix", password=None): +        #XXX FIXME +        #change watcher_cb to line_observer +        """ +        :param config_file: configuration file to read from +        :param watcher_cb: callback to be \ +called for each line in watched stdout +        :param signal_map: dictionary of signal names and callables \ +to be triggered for each one of them. +        :type config_file: str +        :type watcher_cb: function +        :type signal_map: dict +        """ + +        self.config_file = config_file +        self.watcher_cb = watcher_cb +        #self.signal_maps = signal_maps + +        self.subp = None +        self.watcher = None + +        self.server = None +        self.port = None +        self.proto = None + +        self.autostart = True + +        self._get_config() + +        #Get this info from the Configuration Class +        #XXX hardcoded host here. change. +        self.host = host +        if isinstance(port, str) and port.isdigit(): +            port = int(port) +        self.port = port +        self.password = password +        self.tn = None + +        #XXX workaround for signaling +        #the ui that we don't know how to +        #manage a connection error +        self.with_errors = False + + +    def _set_command_mockup(self): +        """ +        sets command and args for a command mockup +        that just mimics the output from the real thing +        """ +        command, args = get_vpn_stdout_mockup() +        self.command, self.args = command, args + +    def _get_config(self): +        """ +        retrieves the config options from defaults or +        home file, or config file passed in command line. +        """ +        config = get_config(config_file=self.config_file) +        self.config = config + +        if config.has_option('openvpn', 'command'): +            commandline = config.get('openvpn', 'command') +            if commandline == "mockup": +                self._set_command_mockup() +                return +            command_split = commandline.split(' ') +            command = command_split[0] +            if len(command_split) > 1: +                args = command_split[1:] +            else: +                args = [] +            self.command = command +            #print("debug: command = %s" % command) +            self.args = args +        else: +            self._set_command_mockup() + +        if config.has_option('openvpn', 'autostart'): +            autostart = config.get('openvpn', 'autostart') +            self.autostart = autostart + +    def _launch_openvpn(self): +        """ +        invocation of openvpn binaries in a subprocess. +        """ +        #XXX TODO: +        #deprecate watcher_cb, +        #use _only_ signal_maps instead + +        if self.watcher_cb is not None: +            linewrite_callback = self.watcher_cb +        else: +            #XXX get logger instead +            linewrite_callback = lambda line: print('watcher: %s' % line) + +        observers = (linewrite_callback, +                     partial(self.status_watcher, self.status)) +        subp, watcher = spawn_and_watch_process( +            self.command, +            self.args, +            observers=observers) +        self.subp = subp +        self.watcher = watcher + +        conn_result = self.status.CONNECTED +        return conn_result + +    def _try_connection(self): +        """ +        attempts to connect +        """ +        if self.subp is not None: +            print('cowardly refusing to launch subprocess again') +            return +        self._launch_openvpn() + +    def cleanup(self): +        """ +        terminates child subprocess +        """ +        if self.subp: +            self.subp.terminate() + + +    #Here are the actual code to manage OpenVPN Connection +    #TODO: Look into abstraction them and moving them up into base class  +    # this code based on code from cube-routed project + +    """ +    Run commands over OpenVPN management interface +    and parses the output. +    """ +    # XXX might need a lock to avoid +    # race conditions here... + +    def forget_errors(self): +        print('forgetting errors') +        self.with_errors = False + +    def connect(self): +        """Connect to openvpn management interface""" +        try: +            self.close() +        except: +            #XXX don't like this general +            #catch here. +            pass +        if self.connected(): +            return True +        self.tn = UDSTelnet(self.host, self.port) + +        # XXX make password optional +        # specially for win plat. we should generate +        # the pass on the fly when invoking manager +        # from conductor + +        #self.tn.read_until('ENTER PASSWORD:', 2) +        #self.tn.write(self.password + '\n') +        #self.tn.read_until('SUCCESS:', 2) + +        self._seek_to_eof() +        self.forget_errors() +        return True + +    def _seek_to_eof(self): +        """ +        Read as much as available. Position seek pointer to end of stream +        """ +        b = self.tn.read_eager() +        while b: +            b = self.tn.read_eager() + +    def connected(self): +        """ +        Returns True if connected +        rtype: bool +        """ +        #return bool(getattr(self, 'tn', None)) +        try: +            assert self.tn +            return True +        except: +            #XXX get rid of +            #this pokemon exception!!! +            return False + +    def close(self, announce=True): +        """ +        Close connection to openvpn management interface +        """ +        if announce: +            self.tn.write("quit\n") +            self.tn.read_all() +        self.tn.get_socket().close() +        del self.tn + +    def _send_command(self, cmd, tries=0): +        """ +        Send a command to openvpn and return response as list +        """ +        if tries > 3: +            return [] +        if not self.connected(): +            try: +                self.connect() +            except MissingSocketError: +                #XXX capture more helpful error +                #messages +                #pass +                return self.make_error() +        try: +            self.tn.write(cmd + "\n") +        except socket.error: +            logger.error('socket error') +            print('socket error!') +            self.close(announce=False) +            self._send_command(cmd, tries=tries + 1) +            return [] +        buf = self.tn.read_until(b"END", 2) +        self._seek_to_eof() +        blist = buf.split('\r\n') +        if blist[-1].startswith('END'): +            del blist[-1] +            return blist +        else: +            return [] + +    def _send_short_command(self, cmd): +        """ +        parse output from commands that are +        delimited by "success" instead +        """ +        if not self.connected(): +            self.connect() +        self.tn.write(cmd + "\n") +        # XXX not working? +        buf = self.tn.read_until(b"SUCCESS", 2) +        self._seek_to_eof() +        blist = buf.split('\r\n') +        return blist + +    # +    # useful vpn commands +    # + +    def pid(self): +        #XXX broken +        return self._send_short_command("pid") + +    def make_error(self): +        """ +        capture error and wrap it in an +        understandable format +        """ +        #XXX get helpful error codes +        self.with_errors = True +        now = int(time.time()) +        return '%s,LAUNCHER ERROR,ERROR,-,-' % now + +    def state(self): +        """ +        OpenVPN command: state +        """ +        state = self._send_command("state") +        if not state: +            return None +        if isinstance(state, str): +            return state +        if isinstance(state, list): +            if len(state) == 1: +                return state[0] +            else: +                return state[-1] + +    def status(self): +        """ +        OpenVPN command: status +        """ +        status = self._send_command("status") +        return status + +    def status2(self): +        """ +        OpenVPN command: last 2 statuses +        """ +        return self._send_command("status 2") + +    # +    # parse  info +    # + +    def get_status_io(self): +        status = self.status() +        if isinstance(status, str): +            lines = status.split('\n') +        if isinstance(status, list): +            lines = status +        try: +            (header, when, tun_read, tun_write, +             tcp_read, tcp_write, auth_read) = tuple(lines) +        except ValueError: +            return None + +        when_ts = time.strptime(when.split(',')[1], "%a %b %d %H:%M:%S %Y") +        sep = ',' +        # XXX cleanup! +        tun_read = tun_read.split(sep)[1] +        tun_write = tun_write.split(sep)[1] +        tcp_read = tcp_read.split(sep)[1] +        tcp_write = tcp_write.split(sep)[1] +        auth_read = auth_read.split(sep)[1] + +        # XXX this could be a named tuple. prettier. +        return when_ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read) + +    def get_connection_state(self): +        state = self.state() +        if state is not None: +            ts, status_step, ok, ip, remote = state.split(',') +            ts = time.gmtime(float(ts)) +            # XXX this could be a named tuple. prettier. +            return ts, status_step, ok, ip, remote + +    def status_watcher(self, cs, line): +        """ +        a wrapper that calls to ConnectionStatus object +        :param cs: a EIPConnectionStatus instance +        :type cs: EIPConnectionStatus object +        :param line: a single line of the watched output +        :type line: str +        """ +        #print('status watcher watching') + +        # from the mullvad code, should watch for +        # things like: +        # "Initialization Sequence Completed" +        # "With Errors" +        # "Tap-Win32" + +        if "Completed" in line: +            cs.change_to(cs.CONNECTED) +            return + +        if "Initial packet from" in line: +            cs.change_to(cs.CONNECTING) +            return + + + +class MissingSocketError(Exception): +    pass + + +class ConnectionRefusedError(Exception): +    pass + +class UDSTelnet(telnetlib.Telnet): + +    def open(self, host, port=23, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): +        """Connect to a host. If port is 'unix', it +        will open a connection over unix docmain sockets. + +        The optional second argument is the port number, which +        defaults to the standard telnet port (23). + +        Don't try to reopen an already connected instance. +        """ +        self.eof = 0 +        self.host = host +        self.port = port +        self.timeout = timeout + +        if self.port == "unix": +            # unix sockets spoken +            if not os.path.exists(self.host): +                raise MissingSocketError +            self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +            try: +                self.sock.connect(self.host) +            except socket.error: +                raise ConnectionRefusedError +        else: +            self.sock = socket.create_connection((host, port), timeout)  | 
