From e1103904fbdd9b54b53075956c279271c17e9a8f Mon Sep 17 00:00:00 2001
From: antialias <antialias@leap.se>
Date: Mon, 13 Aug 2012 22:53:40 -0700
Subject: First (non-working) pass at abstracting exisiting functionality into
 OO framework.

---
 src/leap/Authentication.py    |  11 ++
 src/leap/Configuration.py     |  11 ++
 src/leap/Connection.py        | 129 +++++++++++++
 src/leap/EIPConnection.py     | 270 ++++++++++++++++++++++++++++
 src/leap/OpenVPNConnection.py | 408 ++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 829 insertions(+)
 create mode 100644 src/leap/Authentication.py
 create mode 100644 src/leap/Configuration.py
 create mode 100644 src/leap/Connection.py
 create mode 100644 src/leap/EIPConnection.py
 create mode 100644 src/leap/OpenVPNConnection.py

(limited to 'src')

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)
-- 
cgit v1.2.3