From 451189d369f6661a67a1692945e68b5128cb9a65 Mon Sep 17 00:00:00 2001 From: antialias Date: Thu, 16 Aug 2012 16:06:53 -0700 Subject: Cleaned up files and file names using the PEP 8 style guide. --- 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 -------------------------------------- src/leap/base/authentication.py | 11 + src/leap/base/configuration.py | 11 + src/leap/base/connection.py | 129 ++++++++++++ src/leap/eip/eipconnection.py | 270 +++++++++++++++++++++++++ src/leap/eip/openvpnconnection.py | 408 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 829 insertions(+), 829 deletions(-) delete mode 100644 src/leap/Authentication.py delete mode 100644 src/leap/Configuration.py delete mode 100644 src/leap/Connection.py delete mode 100644 src/leap/EIPConnection.py delete mode 100644 src/leap/OpenVPNConnection.py create mode 100644 src/leap/base/authentication.py create mode 100644 src/leap/base/configuration.py create mode 100644 src/leap/base/connection.py create mode 100644 src/leap/eip/eipconnection.py create mode 100644 src/leap/eip/openvpnconnection.py diff --git a/src/leap/Authentication.py b/src/leap/Authentication.py deleted file mode 100644 index 0bd54fd6..00000000 --- a/src/leap/Authentication.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -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 deleted file mode 100644 index b0ab2bf2..00000000 --- a/src/leap/Configuration.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -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 deleted file mode 100644 index 6534560b..00000000 --- a/src/leap/Connection.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -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 deleted file mode 100644 index f16f01f5..00000000 --- a/src/leap/EIPConnection.py +++ /dev/null @@ -1,270 +0,0 @@ -""" -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 deleted file mode 100644 index a26059a7..00000000 --- a/src/leap/OpenVPNConnection.py +++ /dev/null @@ -1,408 +0,0 @@ -""" -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) diff --git a/src/leap/base/authentication.py b/src/leap/base/authentication.py new file mode 100644 index 00000000..0bd54fd6 --- /dev/null +++ b/src/leap/base/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/base/configuration.py b/src/leap/base/configuration.py new file mode 100644 index 00000000..b0ab2bf2 --- /dev/null +++ b/src/leap/base/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/base/connection.py b/src/leap/base/connection.py new file mode 100644 index 00000000..6534560b --- /dev/null +++ b/src/leap/base/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/eip/eipconnection.py b/src/leap/eip/eipconnection.py new file mode 100644 index 00000000..f16f01f5 --- /dev/null +++ b/src/leap/eip/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/eip/openvpnconnection.py b/src/leap/eip/openvpnconnection.py new file mode 100644 index 00000000..a26059a7 --- /dev/null +++ b/src/leap/eip/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