diff options
| -rw-r--r-- | src/leap/bitmask/vpn/_state.py | 98 | ||||
| -rw-r--r-- | src/leap/bitmask/vpn/_status.py | 98 | ||||
| -rw-r--r-- | src/leap/bitmask/vpn/management.py | 97 | ||||
| -rw-r--r-- | src/leap/bitmask/vpn/process.py | 95 | ||||
| -rw-r--r-- | src/leap/bitmask/vpn/tunnel.py | 5 | 
5 files changed, 164 insertions, 229 deletions
| diff --git a/src/leap/bitmask/vpn/_state.py b/src/leap/bitmask/vpn/_state.py new file mode 100644 index 00000000..53877735 --- /dev/null +++ b/src/leap/bitmask/vpn/_state.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# _state.py +# Copyright (C) 2017 LEAP Encryption Access Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +""" +VPN State. +""" + +import time + + +class State(object): + +    """ +    Possible States in an OpenVPN connection, according to the +    OpenVPN Management documentation. +    """ + +    CONNECTING = 'CONNECTING' +    WAIT = 'WAIT' +    AUTH = 'AUTH' +    GET_CONFIG = 'GET_CONFIG' +    ASSIGN_IP = 'ASSIGN_IP' +    ADD_ROUTES = 'ADD_ROUTES' +    CONNECTED = 'CONNECTED' +    RECONNECTING = 'RECONNECTING' +    EXITING = 'EXITING' + +    OFF = 'OFF' +    ON = 'ON' +    STARTING = 'STARTING' +    STOPPING = 'STOPPING' +    FAILED = 'FAILED' + +    _legend = { +        'CONNECTING': 'Connecting to remote server', +        'WAIT': 'Waiting from initial response from server', +        'AUTH': 'Authenticating with server', +        'GET_CONFIG': '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', +        'OFF': 'Disconnected', +        'FAILED': 'A failure has occurred', +    } + +    _simple = { +        'CONNECTING': STARTING, +        'WAIT': STARTING, +        'AUTH': STARTING, +        'GET_CONFIG': STARTING, +        'ASSIGN_IP': STARTING, +        'ADD_ROUTES': STARTING, +        'CONNECTED': ON, +        'RECONNECTING': STARTING, +        'EXITING': STOPPING, +        'OFF': OFF, +        'FAILED': OFF +    } + +    def __init__(self, state, timestamp): +        self.state = state +        self.timestamp = timestamp + +    @classmethod +    def get_legend(cls, state): +        return cls._legend.get(state) + +    @classmethod +    def get_simple(cls, state): +        return cls._simple.get(state) + +    @property +    def simple(self): +        return self.get_simple(self.state) + +    @property +    def legend(self): +        return self.get_legend(self.state) + +    def __repr__(self): +        return '<State: %s [%s]>' % ( +            self.state, time.ctime(int(self.timestamp))) diff --git a/src/leap/bitmask/vpn/_status.py b/src/leap/bitmask/vpn/_status.py deleted file mode 100644 index 4bf8d004..00000000 --- a/src/leap/bitmask/vpn/_status.py +++ /dev/null @@ -1,98 +0,0 @@ -from itertools import chain, repeat - -from leap.common.events import catalog, emit_async - -from leap.bitmask.vpn._human import bytes2human - -# TODO implement a more explicit state machine -# TODO check good transitions -# TODO check valid states - - -class VPNStatus(object): -    """ -    A class containing different patterns in the openvpn output that -    we can react upon. The patterns drive an internal state machine that can be -    queried through the ``status`` property. -    """ - -    _events = { -        'NETWORK_UNREACHABLE': ( -            'Network is unreachable (code=101)',), -        'PROCESS_RESTART_TLS': ( -            "SIGTERM[soft,tls-error]",), -        'INITIALIZATION_COMPLETED': ( -            "Initialization Sequence Completed",), -    } -    _STARTING = ('AUTH', 'WAIT', 'CONNECTING', 'GET_CONFIG', -                 'ASSIGN_IP', 'ADD_ROUTES', 'RECONNECTING') -    _STOPPING = ("EXITING",) -    _CONNECTED = ("CONNECTED",) - -    def __init__(self): -        self._status = 'off' -        self.errcode = None -        self._traffic_down = None -        self._traffic_up = None -        self._chained_iter = chain(*[ -            zip(repeat(key, len(l)), l) -            for key, l in self._events.iteritems()]) - -    def _status_codes(self, event): - -        _table = { -            "network_unreachable": ('off', 'network unreachable'), -            "process_restart_tls": ('starting', 'restart tls'), -            "initialization_completed": ('on', None) -        } -        return _table.get(event.lower()) - -    def watch(self, line): -        for event, pattern in self._chained_iter: -            if pattern in line: -                break -        else: -            return - -        status, errcode = self._status_codes(event) -        self.set_status(status, errcode) - -    @property -    def status(self): -        status = self.get_traffic_status() -        status.update({ -            'status': self._status, -            'error': self.errcode -        }) -        return status - -    def set_status(self, status, errcode): -        if not status: -            return - -        if status in self._STARTING: -            status = "starting" -        elif status in self._STOPPING: -            status = "stopping" -        elif status in self._CONNECTED: -            status = "on" - -        if self._status != status: -            self._status = status -            self.errcode = errcode -            emit_async(catalog.VPN_STATUS_CHANGED, status.upper()) - -    def get_traffic_status(self): -        down = None -        up = None -        if self._traffic_down: -            down = bytes2human(self._traffic_down) -        if self._traffic_up: -            up = bytes2human(self._traffic_up) -        return {'down': down, 'up': up} - -    def set_traffic_status(self, up, down): -        if up != self._traffic_up or down != self._traffic_down: -            self._traffic_up = up -            self._traffic_down = down -            emit_async(catalog.VPN_STATUS_CHANGED) diff --git a/src/leap/bitmask/vpn/management.py b/src/leap/bitmask/vpn/management.py index 9b081114..3ab230a6 100644 --- a/src/leap/bitmask/vpn/management.py +++ b/src/leap/bitmask/vpn/management.py @@ -27,15 +27,9 @@ from twisted.protocols.basic import LineReceiver  from twisted.internet.defer import Deferred  from twisted.logger import Logger -from zope.interface import Interface -  from _human import bytes2human - -class IStateListener(Interface): - -    def change_state(self, state): -        pass +from ._state import State  class ManagementProtocol(LineReceiver): @@ -53,23 +47,16 @@ class ManagementProtocol(LineReceiver):          self.pid = None          self._defs = [] -        self._statelog = OrderedDict()          self._linebuf = []          self._state_listeners = set([])      def addStateListener(self, listener):          """ -        A Listener must implement change_state method, +        A Listener must implement changeState method,          and it will be called with a State object.          """          self._state_listeners.add(listener) -    # TODO -- this needs to be exposed by the API -    # The UI needs this feature. - -    def getStateHistory(self): -        return self._statelog -      def lineReceived(self, line):          if self.verbose:              if int(self.verbose) > 1: @@ -169,11 +156,9 @@ class ManagementProtocol(LineReceiver):              return          if state != self.state: -            now = time.time()              stateobj = State(state, ts) -            self._statelog[now] = stateobj              for listener in self._state_listeners: -                listener.change_state(stateobj) +                listener.changeState(stateobj)          self.state = stateobj          self.remote = remote          self.rport = rport @@ -233,91 +218,15 @@ class ManagementProtocol(LineReceiver):          return d      def getInfo(self): -        state = self._statelog.values()[-1]          return {              'remote': self.remote,              'rport': self.rport, -            'state': state.state, -            'state_simple': state.simple, -            'state_legend': state.legend,              'openvpn_version': self.openvpn_version,              'pid': self.pid,              'traffic_down_total': self.traffic.down,              'traffic_up_total': self.traffic.up} -class State(object): - -    """ -    Possible States in an OpenVPN connection, according to the -    OpenVPN Management documentation. -    """ - -    CONNECTING = 'CONNECTING' -    WAIT = 'WAIT' -    AUTH = 'AUTH' -    GET_CONFIG = 'GET_CONFIG' -    ASSIGN_IP = 'ASSIGN_IP' -    ADD_ROUTES = 'ADD_ROUTES' -    CONNECTED = 'CONNECTED' -    RECONNECTING = 'RECONNECTING' -    EXITING = 'EXITING' - -    OFF = 'OFF' -    ON = 'ON' -    STARTING = 'STARTING' -    STOPPING = 'STOPPING' -    FAILED = 'FAILED' - -    _legend = { -        'CONNECTING': 'Connecting to remote server', -        'WAIT': 'Waiting from initial response from server', -        'AUTH': 'Authenticating with server', -        'GET_CONFIG': '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' -    } - -    _simple = { -        'CONNECTING': STARTING, -        'WAIT': STARTING, -        'AUTH': STARTING, -        'GET_CONFIG': STARTING, -        'ASSIGN_IP': STARTING, -        'ADD_ROUTES': STARTING, -        'CONNECTED': ON, -        'RECONNECTING': STARTING, -        'EXITING': STOPPING -    } - -    def __init__(self, state, timestamp): -        self.state = state -        self.timestamp = timestamp - -    @classmethod -    def get_legend(cls, state): -        return cls._legend.get(state) - -    @classmethod -    def get_simple(cls, state): -        return cls._simple.get(state) - -    @property -    def simple(self): -        return self.get_simple(self.state) - -    @property -    def legend(self): -        return self.get_legend(self.state) - -    def __repr__(self): -        return '<State: %s [%s]>' % ( -            self.state, time.ctime(int(self.timestamp))) - -  class TrafficCounter(object):      CAPACITY = 60 diff --git a/src/leap/bitmask/vpn/process.py b/src/leap/bitmask/vpn/process.py index 862215f8..92a2eedc 100644 --- a/src/leap/bitmask/vpn/process.py +++ b/src/leap/bitmask/vpn/process.py @@ -25,6 +25,8 @@ interface.  import os  import shutil  import sys +import time +from collections import OrderedDict  from twisted.internet import protocol, reactor, defer  from twisted.internet import error as internet_error @@ -33,26 +35,35 @@ from twisted.logger import Logger  from zope.interface import implementer  from leap.bitmask.vpn.utils import get_vpn_launcher -from leap.bitmask.vpn.management import ManagementProtocol, IStateListener +from leap.bitmask.vpn.management import ManagementProtocol  from leap.bitmask.vpn.launchers import darwin  from leap.bitmask.vpn.constants import IS_MAC, IS_LINUX -  from leap.common.events import catalog, emit_async +from zope.interface import Interface + +from ._state import State +  OPENVPN_VERBOSITY = 4 -@implementer(IStateListener) -class VPNStateListener(object): +class IStateListener(Interface): -    # TODO we should move the state history to this class -    # and make VPNProcess the implementer itself - or the service. +    def changeState(self, state): +        pass -    def change_state(self, state): -        emit_async(catalog.VPN_STATUS_CHANGED, state.simple) +    def appendToStateLog(self, state): +        pass + +    def getState(sef): +        pass + +    def getStateHistory(self): +        pass +@implementer(IStateListener)  class _VPNProcess(protocol.ProcessProtocol):      """ @@ -102,14 +113,39 @@ class _VPNProcess(protocol.ProcessProtocol):          self._providerconfig = providerconfig          self._launcher = get_vpn_launcher()          self._restartfun = restartfun -        self._status = 'off'          self.restarting = True          self.failed = False          self.errmsg = None          self.proto = None          self._remotes = remotes -        self._listener = VPNStateListener() +        self._statelog = OrderedDict() +        self._turn_state_off() + +    def _turn_state_off(self): +        ts = time.time() +        off = State('OFF', ts) +        self.changeState(off) + +    # IStateListener methods + +    def changeState(self, state): +        self.appendToStateLog(state) +        emit_async(catalog.VPN_STATUS_CHANGED, state.simple) + +    def appendToStateLog(self, state): +        ts = state.timestamp +        self._statelog[ts] = state + +    def getState(self): +        if self._statelog.values(): +            return self._statelog.values()[-1] +        else: +            return None + +    # TODO -- expose on the API, wanted by UI +    def getStateHistory(self): +        return self._statelog      # processProtocol methods @@ -121,7 +157,7 @@ class _VPNProcess(protocol.ProcessProtocol):      @defer.inlineCallbacks      def _got_management_protocol(self, proto):          self.proto = proto -        proto.addStateListener(self._listener) +        proto.addStateListener(self)          try:              yield proto.logOn() @@ -175,6 +211,7 @@ class _VPNProcess(protocol.ProcessProtocol):              # TODO: need to exit properly!              self.errmsg = None          self.proto = None +        self._turn_state_off()      def processEnded(self, reason):          """ @@ -213,32 +250,20 @@ class _VPNProcess(protocol.ProcessProtocol):      def status(self):          if self.failed:              return {'status': 'failed', 'error': self.errmsg} - -        # FIXME - hack, doesn't belong here ---------------------------------- -        # needs to go with implementing the history within the VPNProcess. -        # this only will work before the UI is pulling the state and therefore -        # checking this condition as a side-effect of reading the property. - -        # TODO trigger off transition when proto dissapears (at processExited) -        if not self.proto: -            OFF = 'off' -            if self._status != OFF: -                self._status = OFF  -                class _state(object): -                    simple = 'OFF' -                off = _state() -                self._listener.change_state(off) -            return {'status': OFF, 'error': None} -        # -------------------------------------------------------------------- -          try: -            self._status = self.proto.state.simple.lower() -            status = {'status': self._status, 'error': None} +            state = self.getState() +            if state: +                _status = state.simple.lower() +            status = {'status': _status, 'error': None}          except AttributeError: -            # glitch due to proto.state transition? -            status = {'status': self._status, 'error': None} - -        if self.proto.traffic: +            raise +            # BUG -- glitch due to proto.state transition? +            #state = self.getState() +            #if state: +            #    _status = state.simple.lower() +            #status = {'status': _status, 'error': None} + +        if self.proto and self.proto.traffic:              remote = self.proto.remote              rport = self.proto.rport              status['remote'] = '%s:%s' % (remote, rport) diff --git a/src/leap/bitmask/vpn/tunnel.py b/src/leap/bitmask/vpn/tunnel.py index f6080b85..b003a33c 100644 --- a/src/leap/bitmask/vpn/tunnel.py +++ b/src/leap/bitmask/vpn/tunnel.py @@ -118,8 +118,9 @@ class ConfiguredTunnel(object):              vpnproc.pid = running.pid              defer.returnValue(True)          except Exception as exc: -            self._vpnproc.failed = True -            self._vpnproc.errmsg = exc.message +            if self._vpnproc: +                self._vpnproc.failed = True +                self._vpnproc.errmsg = exc.message              raise      def __start_pre_up(self, proc): | 
