diff options
| -rw-r--r-- | changes/bug_openvpn-process | 1 | ||||
| -rw-r--r-- | docs/api/leap.gui.rst | 40 | ||||
| -rw-r--r-- | docs/api/leap.keymanager.rst | 50 | ||||
| -rw-r--r-- | docs/api/leap.keymanager.tests.rst | 11 | ||||
| -rw-r--r-- | docs/api/leap.rst | 18 | ||||
| -rw-r--r-- | docs/api/leap.services.eip.rst | 19 | ||||
| -rw-r--r-- | docs/api/leap.services.eip.tests.rst | 11 | ||||
| -rw-r--r-- | docs/api/leap.services.mail.rst | 27 | ||||
| -rw-r--r-- | docs/api/leap.services.rst | 18 | ||||
| -rw-r--r-- | docs/api/leap.services.soledad.rst | 27 | ||||
| -rw-r--r-- | docs/api/leap.util.rst | 18 | ||||
| -rw-r--r-- | docs/conf.py | 22 | ||||
| -rw-r--r-- | src/leap/gui/mainwindow.py | 33 | ||||
| -rw-r--r-- | src/leap/services/eip/vpn.py | 465 | ||||
| -rw-r--r-- | src/leap/services/eip/vpnlaunchers.py | 1 | ||||
| -rw-r--r-- | src/leap/services/eip/vpnprocess.py | 592 | ||||
| -rw-r--r-- | src/leap/services/mail/smtpspec.py | 4 | ||||
| -rw-r--r-- | src/leap/services/soledad/soledadspec.py | 4 | 
18 files changed, 863 insertions, 498 deletions
| diff --git a/changes/bug_openvpn-process b/changes/bug_openvpn-process new file mode 100644 index 00000000..86443436 --- /dev/null +++ b/changes/bug_openvpn-process @@ -0,0 +1 @@ +  o Reimplement openvpn invocation to use twisted ProcessProtocol diff --git a/docs/api/leap.gui.rst b/docs/api/leap.gui.rst index 1559e079..d78de5d3 100644 --- a/docs/api/leap.gui.rst +++ b/docs/api/leap.gui.rst @@ -1,6 +1,14 @@  gui Package  =========== +:mod:`gui` Package +------------------ + +.. automodule:: leap.gui +    :members: +    :undoc-members: +    :show-inheritance: +  :mod:`locale_rc` Module  ----------------------- @@ -9,6 +17,22 @@ gui Package      :undoc-members:      :show-inheritance: +:mod:`loggerwindow` Module +-------------------------- + +.. automodule:: leap.gui.loggerwindow +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`loggerwindow_rc` Module +----------------------------- + +.. automodule:: leap.gui.loggerwindow_rc +    :members: +    :undoc-members: +    :show-inheritance: +  :mod:`mainwindow` Module  ------------------------ @@ -25,6 +49,22 @@ gui Package      :undoc-members:      :show-inheritance: +:mod:`twisted_main` Module +-------------------------- + +.. automodule:: leap.gui.twisted_main +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`ui_loggerwindow` Module +----------------------------- + +.. automodule:: leap.gui.ui_loggerwindow +    :members: +    :undoc-members: +    :show-inheritance: +  :mod:`ui_mainwindow` Module  --------------------------- diff --git a/docs/api/leap.keymanager.rst b/docs/api/leap.keymanager.rst new file mode 100644 index 00000000..0de8fc5f --- /dev/null +++ b/docs/api/leap.keymanager.rst @@ -0,0 +1,50 @@ +keymanager Package +================== + +:mod:`keymanager` Package +------------------------- + +.. automodule:: leap.keymanager +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`errors` Module +-------------------- + +.. automodule:: leap.keymanager.errors +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`gpg` Module +----------------- + +.. automodule:: leap.keymanager.gpg +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`keys` Module +------------------ + +.. automodule:: leap.keymanager.keys +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`openpgp` Module +--------------------- + +.. automodule:: leap.keymanager.openpgp +    :members: +    :undoc-members: +    :show-inheritance: + +Subpackages +----------- + +.. toctree:: + +    leap.keymanager.tests + diff --git a/docs/api/leap.keymanager.tests.rst b/docs/api/leap.keymanager.tests.rst new file mode 100644 index 00000000..9836bd01 --- /dev/null +++ b/docs/api/leap.keymanager.tests.rst @@ -0,0 +1,11 @@ +tests Package +============= + +:mod:`test_keymanager` Module +----------------------------- + +.. automodule:: leap.keymanager.tests.test_keymanager +    :members: +    :undoc-members: +    :show-inheritance: + diff --git a/docs/api/leap.rst b/docs/api/leap.rst index 0f03c9e4..102f3b61 100644 --- a/docs/api/leap.rst +++ b/docs/api/leap.rst @@ -4,11 +4,26 @@ leap Package  :mod:`leap` Package  ------------------- -.. automodule:: leap +.. automodule:: leap.__init__      :members:      :undoc-members:      :show-inheritance: +:mod:`_version` Module +---------------------- + +.. automodule:: leap._version +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`app` Module +----------------- + +.. automodule:: leap.app +    :members: +    :undoc-members: +    :show-inheritance:  Subpackages  ----------- @@ -18,6 +33,7 @@ Subpackages      leap.config      leap.crypto      leap.gui +    leap.keymanager      leap.platform_init      leap.services      leap.util diff --git a/docs/api/leap.services.eip.rst b/docs/api/leap.services.eip.rst index 0cf489cf..450c1a93 100644 --- a/docs/api/leap.services.eip.rst +++ b/docs/api/leap.services.eip.rst @@ -41,19 +41,26 @@ eip Package      :undoc-members:      :show-inheritance: -:mod:`vpn` Module ------------------ +:mod:`vpnlaunchers` Module +-------------------------- -.. automodule:: leap.services.eip.vpn +.. automodule:: leap.services.eip.vpnlaunchers      :members:      :undoc-members:      :show-inheritance: -:mod:`vpnlaunchers` Module --------------------------- +:mod:`vpnprocess` Module +------------------------ -.. automodule:: leap.services.eip.vpnlaunchers +.. automodule:: leap.services.eip.vpnprocess      :members:      :undoc-members:      :show-inheritance: +Subpackages +----------- + +.. toctree:: + +    leap.services.eip.tests + diff --git a/docs/api/leap.services.eip.tests.rst b/docs/api/leap.services.eip.tests.rst new file mode 100644 index 00000000..ea6e8ba5 --- /dev/null +++ b/docs/api/leap.services.eip.tests.rst @@ -0,0 +1,11 @@ +tests Package +============= + +:mod:`test_eipconfig` Module +---------------------------- + +.. automodule:: leap.services.eip.tests.test_eipconfig +    :members: +    :undoc-members: +    :show-inheritance: + diff --git a/docs/api/leap.services.mail.rst b/docs/api/leap.services.mail.rst new file mode 100644 index 00000000..bd479972 --- /dev/null +++ b/docs/api/leap.services.mail.rst @@ -0,0 +1,27 @@ +mail Package +============ + +:mod:`smtpbootstrapper` Module +------------------------------ + +.. automodule:: leap.services.mail.smtpbootstrapper +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`smtpconfig` Module +------------------------ + +.. automodule:: leap.services.mail.smtpconfig +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`smtpspec` Module +---------------------- + +.. automodule:: leap.services.mail.smtpspec +    :members: +    :undoc-members: +    :show-inheritance: + diff --git a/docs/api/leap.services.rst b/docs/api/leap.services.rst index 1a35ab5b..e5b22e64 100644 --- a/docs/api/leap.services.rst +++ b/docs/api/leap.services.rst @@ -1,10 +1,28 @@  services Package  ================ +:mod:`abstractbootstrapper` Module +---------------------------------- + +.. automodule:: leap.services.abstractbootstrapper +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`tx` Module +---------------- + +.. automodule:: leap.services.tx +    :members: +    :undoc-members: +    :show-inheritance: +  Subpackages  -----------  .. toctree::      leap.services.eip +    leap.services.mail +    leap.services.soledad diff --git a/docs/api/leap.services.soledad.rst b/docs/api/leap.services.soledad.rst new file mode 100644 index 00000000..efefeb50 --- /dev/null +++ b/docs/api/leap.services.soledad.rst @@ -0,0 +1,27 @@ +soledad Package +=============== + +:mod:`soledadbootstrapper` Module +--------------------------------- + +.. automodule:: leap.services.soledad.soledadbootstrapper +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`soledadconfig` Module +--------------------------- + +.. automodule:: leap.services.soledad.soledadconfig +    :members: +    :undoc-members: +    :show-inheritance: + +:mod:`soledadspec` Module +------------------------- + +.. automodule:: leap.services.soledad.soledadspec +    :members: +    :undoc-members: +    :show-inheritance: + diff --git a/docs/api/leap.util.rst b/docs/api/leap.util.rst index 85eb79da..601f6cba 100644 --- a/docs/api/leap.util.rst +++ b/docs/api/leap.util.rst @@ -9,18 +9,18 @@ util Package      :undoc-members:      :show-inheritance: -:mod:`checkerthread` Module +:mod:`leap_argparse` Module  --------------------------- -.. automodule:: leap.util.checkerthread +.. automodule:: leap.util.leap_argparse      :members:      :undoc-members:      :show-inheritance: -:mod:`leap_argparse` Module ---------------------------- +:mod:`leap_log_handler` Module +------------------------------ -.. automodule:: leap.util.leap_argparse +.. automodule:: leap.util.leap_log_handler      :members:      :undoc-members:      :show-inheritance: @@ -41,3 +41,11 @@ util Package      :undoc-members:      :show-inheritance: +:mod:`requirement_checker` Module +--------------------------------- + +.. automodule:: leap.util.requirement_checker +    :members: +    :undoc-members: +    :show-inheritance: + diff --git a/docs/conf.py b/docs/conf.py index 4a90d7d5..39f17d9b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,21 @@ import sys, os  # If extensions (or modules to document with autodoc) are in another directory,  # add these directories to sys.path here. If the directory is relative to the  # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('../src')) +sys.path.insert(0, os.path.abspath('../src/leap')) +sys.path.insert(0, os.path.abspath('../src/leap/crypto')) +sys.path.insert(0, os.path.abspath('../src/leap/keymanager')) +sys.path.insert(0, os.path.abspath('../src/leap/services')) +sys.path.insert(0, os.path.abspath('../src/leap/services/eip')) +sys.path.insert(0, os.path.abspath('../src/leap/util')) + +sys.path.insert(0, os.path.abspath( +    os.path.expanduser( +        '~/Virtualenvs/leap-client/local/lib/python2.7/' +        'site-packages/leap/common'))) + +# TODO: should add all the virtualenv site-packages to the path +# as a workaround, install all in your path.  # -- General configuration ----------------------------------------------------- @@ -25,7 +39,11 @@ import sys, os  # Add any Sphinx extension module names here, as strings. They can be extensions  # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] + +intersphinx_mapping = {'python': ('http://docs.python.org/2.7', None), +                       'twisted': ('http://twistedsphinx.funsize.net/', +                                   None)}  # Add any paths that contain templates here, relative to this directory.  templates_path = ['_templates'] diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index d9fe478c..a7d88aee 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -44,7 +44,8 @@ from leap.services.soledad.soledadbootstrapper import SoledadBootstrapper  from leap.services.mail.smtpbootstrapper import SMTPBootstrapper  from leap.platform_init import IS_MAC, IS_WIN  from leap.platform_init.initializers import init_platform -from leap.services.eip.vpn import VPN +from leap.services.eip.vpnprocess import VPN, VPNManager +  from leap.services.eip.vpnlaunchers import (VPNLauncherException,                                              OpenVPNNotFoundException,                                              EIPNoPkexecAvailable, @@ -196,9 +197,9 @@ class MainWindow(QtGui.QMainWindow):              self._smtp_bootstrapped_stage)          self._vpn = VPN() -        self._vpn.state_changed.connect(self._update_vpn_state) -        self._vpn.status_changed.connect(self._update_vpn_status) -        self._vpn.process_finished.connect( +        self._vpn.qtsigs.state_changed.connect(self._update_vpn_state) +        self._vpn.qtsigs.status_changed.connect(self._update_vpn_status) +        self._vpn.qtsigs.process_finished.connect(              self._eip_finished)          self.ui.chkRemember.stateChanged.connect( @@ -829,8 +830,9 @@ class MainWindow(QtGui.QMainWindow):              else:                  if self._enabled_services.count(self.MX_SERVICE) > 0:                      pass  # TODO: show MX status -                    #self._set_eip_status(self.tr("%s does not support MX") % -                    #                    (self._provider_config.get_domain(),), +                    #self._set_eip_status( +                    #    self.tr("%s does not support MX") % +                    #    (self._provider_config.get_domain(),),                      #                     error=True)                  else:                      pass  # TODO: show MX status @@ -865,8 +867,6 @@ class MainWindow(QtGui.QMainWindow):                  # TODO: pick local smtp port in a better way                  # TODO: Make the encrypted_only configurable -                # TODO: Remove mocking!!! -                self._keymanager.fetch_keys_from_server = Mock(return_value=[])                  from leap.mail.smtp import setup_smtp_relay                  setup_smtp_relay(port=1234,                                   keymanager=self._keymanager, @@ -944,7 +944,7 @@ class MainWindow(QtGui.QMainWindow):          self.ui.btnEipStartStop.setEnabled(True)      def _stop_eip(self): -        self._vpn.set_should_quit() +        self._vpn.terminate()          self._set_eip_status(self.tr("EIP has stopped"))          self._set_eip_status_icon("error")          self.ui.btnEipStartStop.setText(self.tr("Turn Encryption ON")) @@ -1008,7 +1008,7 @@ class MainWindow(QtGui.QMainWindow):          Updates the displayed VPN state based on the data provided by          the VPN thread          """ -        status = data[self._vpn.STATUS_STEP_KEY] +        status = data[VPNManager.STATUS_STEP_KEY]          self._set_eip_status_icon(status)          if status == "AUTH":              self._set_eip_status(self.tr("VPN: Authenticating...")) @@ -1039,12 +1039,12 @@ class MainWindow(QtGui.QMainWindow):          Updates the download/upload labels based on the data provided          by the VPN thread          """ -        upload = float(data[self._vpn.TUNTAP_WRITE_KEY]) +        upload = float(data[VPNManager.TUNTAP_WRITE_KEY])          upload = upload / 1000.0          upload_str = "%12.2f Kb" % (upload,)          self.ui.lblUpload.setText(upload_str)          self._action_eip_write.setText(upload_str) -        download = float(data[self._vpn.TUNTAP_READ_KEY]) +        download = float(data[VPNManager.TUNTAP_READ_KEY])          download = download / 1000.0          download_str = "%12.2f Kb" % (download,)          self.ui.lblDownload.setText(download_str) @@ -1103,7 +1103,7 @@ class MainWindow(QtGui.QMainWindow):          self.ui.lnPassword.setText("")          self._login_set_enabled(True)          self._set_status("") -        self._vpn.set_should_quit() +        self._vpn.terminate()      def _intermediate_stage(self, data):          """ @@ -1171,8 +1171,11 @@ class MainWindow(QtGui.QMainWindow):          Should be called from the quit function.          """          logger.debug('About to quit, doing cleanup...') -        self._vpn.set_should_quit() -        self._vpn.wait() + +        logger.debug('Killing vpn') +        self._vpn.terminate() + +        logger.debug('Cleaning pidfiles')          self._cleanup_pidfiles()      def quit(self): diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py deleted file mode 100644 index af1febe6..00000000 --- a/src/leap/services/eip/vpn.py +++ /dev/null @@ -1,465 +0,0 @@ -# -*- coding: utf-8 -*- -# vpn.py -# Copyright (C) 2013 LEAP -# -# 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 launcher and watcher thread -""" - -import logging -import sys -import psutil - -from PySide import QtCore, QtGui -from functools import partial - -from leap.common.check import leap_assert, leap_assert_type -from leap.config.providerconfig import ProviderConfig -from leap.services.eip.vpnlaunchers import get_platform_launcher -from leap.services.eip.eipconfig import EIPConfig -from leap.services.eip.udstelnet import UDSTelnet - -logger = logging.getLogger(__name__) - - -# TODO: abstract the thread that can be asked to quit to another -# generic class that Fetcher and VPN inherit from -class VPN(QtCore.QThread): -    """ -    VPN launcher and watcher thread. It will emit signals based on -    different events caught by the management interface -    """ - -    state_changed = QtCore.Signal(dict) -    status_changed = QtCore.Signal(dict) - -    process_finished = QtCore.Signal(int) - -    CONNECTION_RETRY_TIME = 1000 -    POLL_TIME = 100 - -    TS_KEY = "ts" -    STATUS_STEP_KEY = "status_step" -    OK_KEY = "ok" -    IP_KEY = "ip" -    REMOTE_KEY = "remote" - -    TUNTAP_READ_KEY = "tun_tap_read" -    TUNTAP_WRITE_KEY = "tun_tap_write" -    TCPUDP_READ_KEY = "tcp_udp_read" -    TCPUDP_WRITE_KEY = "tcp_udp_write" -    AUTH_READ_KEY = "auth_read" - -    ALREADY_RUNNING_STEP = "ALREADYRUNNING" - -    def __init__(self): -        QtCore.QThread.__init__(self) - -        self._should_quit = False -        self._should_quit_lock = QtCore.QMutex() - -        self._launcher = get_platform_launcher() -        self._subp = None - -        self._tn = None -        self._host = None -        self._port = None - -        self._last_state = None -        self._last_status = None - -    def get_should_quit(self): -        """ -        Returns wether this thread should quit - -        :rtype: bool -        :return: True if the thread should terminate itself, Flase otherwise -        """ -        QtCore.QMutexLocker(self._should_quit_lock) -        return self._should_quit - -    def set_should_quit(self): -        """ -        Sets the should_quit flag to True so that this thread -        terminates the first chance it gets. -        Also terminates the VPN process and the connection to it -        """ -        QtCore.QMutexLocker(self._should_quit_lock) -        self._should_quit = True -        if self._tn is None or self._subp is None: -            return - -        try: -            self._send_command("signal SIGTERM") -            self._tn.close() -            self._subp.terminate() -            self._subp.waitForFinished() -        except Exception as e: -            logger.debug("Could not terminate process, trying command " + -                         "signal SIGNINT: %r" % (e,)) -        finally: -            self._tn = None - -    def start(self, eipconfig, providerconfig, socket_host, socket_port): -        """ -        Launches OpenVPN and starts the thread to watch its output - -        :param eipconfig: eip configuration object -        :type eipconfig: EIPConfig -        :param providerconfig: provider specific configuration -        :type providerconfig: ProviderConfig -        :param socket_host: either socket path (unix) or socket IP -        :type socket_host: str -        :param socket_port: either string "unix" if it's a unix -        socket, or port otherwise -        :type socket_port: str -        """ -        leap_assert(eipconfig, "We need an eip config") -        leap_assert_type(eipconfig, EIPConfig) -        leap_assert(providerconfig, "We need a provider config") -        leap_assert_type(providerconfig, ProviderConfig) -        leap_assert(not self.isRunning(), "Starting process more than once!") - -        logger.debug("Starting VPN...") - -        with QtCore.QMutexLocker(self._should_quit_lock): -            self._should_quit = False - -        if not self._stop_if_already_running(): -            # We send a fake state -            state_dict = { -                self.TS_KEY: "", -                self.STATUS_STEP_KEY: self.ALREADY_RUNNING_STEP, -                self.OK_KEY: "", -                self.IP_KEY: "", -                self.REMOTE_KEY: "" -            } - -            self.state_changed.emit(state_dict) -            # And just return, don't start the process -            return - -        command = self._launcher.get_vpn_command(eipconfig=eipconfig, -                                                 providerconfig=providerconfig, -                                                 socket_host=socket_host, -                                                 socket_port=socket_port) -        try: -            env = QtCore.QProcessEnvironment.systemEnvironment() -            for key, val in self._launcher.get_vpn_env(providerconfig).items(): -                env.insert(key, val) - -            self._subp = QtCore.QProcess() - -            self._subp.setProcessEnvironment(env) - -            self._subp.finished.connect(self.process_finished) -            self._subp.finished.connect(self._dump_exitinfo) -            self._subp.start(command[:1][0], command[1:]) -            logger.debug("Waiting for started...") -            self._subp.waitForStarted() -            logger.debug("Started!") - -            self._host = socket_host -            self._port = socket_port - -            self._started = True - -            QtCore.QThread.start(self) -        except Exception as e: -            logger.warning("Something went wrong while starting OpenVPN: %r" % -                           (e,)) - -    def _dump_exitinfo(self): -        """ -        SLOT -        TRIGGER: self._subp.finished - -        Prints debug info when quitting the process -        """ -        logger.debug("stdout: %s", self._subp.readAllStandardOutput()) -        logger.debug("stderr: %s", self._subp.readAllStandardError()) - -    def _get_openvpn_process(self): -        """ -        Looks for openvpn instances running - -        :rtype: process -        """ -        openvpn_process = None -        for p in psutil.process_iter(): -            try: -                # XXX Not exact! -                # Will give false positives. -                # we should check that cmdline BEGINS -                # with openvpn or with our wrapper -                # (pkexec / osascript / whatever) -                if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): -                    openvpn_process = p -                    break -            except psutil.error.AccessDenied: -                pass -        return openvpn_process - -    def _stop_if_already_running(self): -        """ -        Checks if VPN is already running and tries to stop it - -        :return: True if stopped, False otherwise -        """ - -        process = self._get_openvpn_process() -        if process: -            logger.debug("OpenVPN is already running, trying to stop it") -            cmdline = process.cmdline - -            manag_flag = "--management" -            if isinstance(cmdline, list) and manag_flag in cmdline: -                try: -                    index = cmdline.index(manag_flag) -                    host = cmdline[index + 1] -                    port = cmdline[index + 2] -                    logger.debug("Trying to connect to %s:%s" -                                 % (host, port)) -                    self._connect(host, port) -                    self._send_command("signal SIGTERM") -                    self._tn.close() -                    self._tn = None -                except Exception as e: -                    logger.warning("Problem trying to terminate OpenVPN: %r" -                                   % (e,)) - -            process = self._get_openvpn_process() -            if process is None: -                logger.warning("Unabled to terminate OpenVPN") -                return True -            else: -                return False - -        return True - -    def _connect(self, socket_host, socket_port): -        """ -        Connects to the specified socket_host socket_port -        :param socket_host: either socket path (unix) or socket IP -        :type socket_host: str -        :param socket_port: either string "unix" if it's a unix -        socket, or port otherwise -        :type socket_port: str -        """ -        try: -            self._tn = UDSTelnet(socket_host, socket_port) - -            # XXX make password optional -            # specially for win. 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) -            if self._tn: -                self._tn.read_eager() -        except Exception as e: -            logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) -            self._tn = None - -    def _disconnect(self): -        """ -        Disconnects the telnet connection to the openvpn process -        """ -        logger.debug('Closing socket') -        self._tn.write("quit\n") -        self._tn.read_all() -        self._tn.close() -        self._tn = None - -    def _send_command(self, command, until=b"END"): -        """ -        Sends a command to the telnet connection and reads until END -        is reached - -        :param command: command to send -        :type command: str -        :param until: byte delimiter string for reading command output -        :type until: byte str -        :return: response read -        :rtype: list -        """ -        leap_assert(self._tn, "We need a tn connection!") -        try: -            self._tn.write("%s\n" % (command,)) -            buf = self._tn.read_until(until, 2) -            self._tn.read_eager() -            lines = buf.split("\n") -            return lines -        except Exception as e: -            logger.warning("Error sending command %s: %r" % -                           (command, e)) -        return [] - -    def _parse_state_and_notify(self, output): -        """ -        Parses the output of the state command and emits state_changed -        signal when the state changes - -        :param output: list of lines that the state command printed as -        its output -        :type output: list -        """ -        for line in output: -            stripped = line.strip() -            if stripped == "END": -                continue -            parts = stripped.split(",") -            if len(parts) < 5: -                continue -            ts, status_step, ok, ip, remote = parts - -            state_dict = { -                self.TS_KEY: ts, -                self.STATUS_STEP_KEY: status_step, -                self.OK_KEY: ok, -                self.IP_KEY: ip, -                self.REMOTE_KEY: remote -            } - -            if state_dict != self._last_state: -                self.state_changed.emit(state_dict) -                self._last_state = state_dict - -    def _parse_status_and_notify(self, output): -        """ -        Parses the output of the status command and emits -        status_changed signal when the status changes - -        :param output: list of lines that the status command printed -        as its output -        :type output: list -        """ -        tun_tap_read = "" -        tun_tap_write = "" -        tcp_udp_read = "" -        tcp_udp_write = "" -        auth_read = "" -        for line in output: -            stripped = line.strip() -            if stripped.endswith("STATISTICS") or stripped == "END": -                continue -            parts = stripped.split(",") -            if len(parts) < 2: -                continue -            if parts[0].strip() == "TUN/TAP read bytes": -                tun_tap_read = parts[1] -            elif parts[0].strip() == "TUN/TAP write bytes": -                tun_tap_write = parts[1] -            elif parts[0].strip() == "TCP/UDP read bytes": -                tcp_udp_read = parts[1] -            elif parts[0].strip() == "TCP/UDP write bytes": -                tcp_udp_write = parts[1] -            elif parts[0].strip() == "Auth read bytes": -                auth_read = parts[1] - -        status_dict = { -            self.TUNTAP_READ_KEY: tun_tap_read, -            self.TUNTAP_WRITE_KEY: tun_tap_write, -            self.TCPUDP_READ_KEY: tcp_udp_read, -            self.TCPUDP_WRITE_KEY: tcp_udp_write, -            self.AUTH_READ_KEY: auth_read -        } - -        if status_dict != self._last_status: -            self.status_changed.emit(status_dict) -            self._last_status = status_dict - -    def run(self): -        """ -        Main run loop for this thread -        """ -        while True: -            if self.get_should_quit(): -                logger.debug("Quitting VPN thread") -                return - -            if self._subp and self._subp.state() != QtCore.QProcess.Running: -                QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) - -            if self._tn is None: -                self._connect(self._host, self._port) -                QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME) -            else: -                self._parse_state_and_notify(self._send_command("state")) -                self._parse_status_and_notify(self._send_command("status")) -                output_sofar = self._subp.readAllStandardOutput() -                if len(output_sofar) > 0: -                    logger.debug(output_sofar) -                output_sofar = self._subp.readAllStandardError() -                if len(output_sofar) > 0: -                    logger.debug(output_sofar) -                QtCore.QThread.msleep(self.POLL_TIME) - - -if __name__ == "__main__": -    import os -    import signal - -    app = QtGui.QApplication(sys.argv) - -    def sigint_handler(*args, **kwargs): -        logger.debug('SIGINT catched. shutting down...') -        vpn_thread = args[0] -        vpn_thread.set_should_quit() -        QtGui.QApplication.quit() - -    def signal_tester(d): -        print d - -    logger = logging.getLogger(name='leap') -    logger.setLevel(logging.DEBUG) -    console = logging.StreamHandler() -    console.setLevel(logging.DEBUG) -    formatter = logging.Formatter( -        '%(asctime)s ' -        '- %(name)s - %(levelname)s - %(message)s') -    console.setFormatter(formatter) -    logger.addHandler(console) - -    vpn_thread = VPN() - -    sigint = partial(sigint_handler, vpn_thread) -    signal.signal(signal.SIGINT, sigint) - -    eipconfig = EIPConfig() -    if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): -        provider = ProviderConfig() -        if provider.load("leap/providers/bitmask.net/provider.json"): -            vpn_thread.start(eipconfig=eipconfig, -                             providerconfig=provider, -                             socket_host=os.path.expanduser("~/vpnsock"), -                             socket_port="unix") - -    timer = QtCore.QTimer() -    timer.start(500) -    timer.timeout.connect(lambda: None) -    app.connect(app, QtCore.SIGNAL("aboutToQuit()"), -                vpn_thread.set_should_quit) -    w = QtGui.QWidget() -    w.resize(100, 100) -    w.show() - -    vpn_thread.state_changed.connect(signal_tester) -    vpn_thread.status_changed.connect(signal_tester) - -    sys.exit(app.exec_()) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 0691e121..952d3618 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -132,6 +132,7 @@ def _is_auth_agent_running():      """      polkit_gnome = 'ps aux | grep polkit-[g]nome-authentication-agent-1'      polkit_kde = 'ps aux | grep polkit-[k]de-authentication-agent-1' +      return (len(commands.getoutput(polkit_gnome)) > 0 or              len(commands.getoutput(polkit_kde)) > 0) diff --git a/src/leap/services/eip/vpnprocess.py b/src/leap/services/eip/vpnprocess.py new file mode 100644 index 00000000..eae8aadd --- /dev/null +++ b/src/leap/services/eip/vpnprocess.py @@ -0,0 +1,592 @@ +# -*- coding: utf-8 -*- +# vpnprocess.py +# Copyright (C) 2013 LEAP +# +# 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 Manager, spawned in a custom processProtocol. +""" +import logging +import os +import psutil + +from PySide import QtCore + +from leap.common.check import leap_assert, leap_assert_type +from leap.config.providerconfig import ProviderConfig +from leap.services.eip.vpnlaunchers import get_platform_launcher +from leap.services.eip.eipconfig import EIPConfig +from leap.services.eip.udstelnet import UDSTelnet + +logger = logging.getLogger(__name__) +vpnlog = logging.getLogger('leap.openvpn') + +from twisted.internet import protocol +from twisted.internet import defer +from twisted.internet.task import LoopingCall +from twisted.internet import error as internet_error + + +class VPNSignals(QtCore.QObject): +    """ +    These are the signals that we use to let the UI know +    about the events we are polling. +    They are instantiated in the VPN object and passed along +    till the VPNProcess. +    """ +    state_changed = QtCore.Signal(dict) +    status_changed = QtCore.Signal(dict) +    process_finished = QtCore.Signal(int) + +    def __init__(self): +        QtCore.QObject.__init__(self) + + +class VPN(object): +    """ +    This is the high-level object that the GUI is dealing with. +    It exposes the start and terminate methods. + +    On start, it spawns a VPNProcess instance that will use a vpnlauncher +    suited for the running platform and connect to the management interface +    opened by the openvpn process, executing commands over that interface on +    demand. +    """ +    def __init__(self): +        """ +        Instantiate empty attributes and get a copy +        of a QObject containing the QSignals that we will pass along +        to the VPNManager. +        """ +        from twisted.internet import reactor +        self._vpnproc = None +        self._pollers = [] +        self._reactor = reactor +        self._qtsigs = VPNSignals() + +    @property +    def qtsigs(self): +        return self._qtsigs + +    def start(self, *args, **kwargs): +        """ +        Starts the openvpn subprocess. + +        :param args: args to be passed to the VPNProcess +        :type args: tuple + +        :param kwargs: kwargs to be passed to the VPNProcess +        :type kwargs: dict +        """ +        kwargs['qtsigs'] = self.qtsigs + +        # start the main vpn subprocess +        vpnproc = VPNProcess(*args, **kwargs) + +        cmd = vpnproc.getCommand() +        env = os.environ +        for key, val in vpnproc.vpn_env.items(): +            env[key] = val + +        self._reactor.spawnProcess(vpnproc, cmd[0], cmd, env) +        self._vpnproc = vpnproc + +        # add pollers for status and state +        # XXX this could be extended to a collection of +        # generic watchers + +        poll_list = [LoopingCall(vpnproc.pollStatus), +                     LoopingCall(vpnproc.pollState)] +        self._pollers.extend(poll_list) +        self._start_pollers() + +    def terminate(self): +        """ +        Stops the openvpn subprocess. +        """ +        self._stop_pollers() +        # XXX we should leave a KILL as a last resort. +        # First we should try to send a SIGTERM +        if self._vpnproc: +            self._vpnproc.killProcess() + +    def _start_pollers(self): +        """ +        Iterate through the registered observers +        and start the looping call for them. +        """ +        for poller in self._pollers: +            poller.start(VPNManager.POLL_TIME) + +    def _stop_pollers(self): +        """ +        Iterate through the registered observers +        and stop the looping calls if they are running. +        """ +        for poller in self._pollers: +            if poller.running: +                poller.stop() +        self._pollers = [] + + +class VPNManager(object): +    """ +    This is a mixin that we use in the VPNProcess class. +    Here we get together all methods related with the openvpn management +    interface. + +    A copy of a QObject containing signals as attributes is passed along +    upon initialization, and we use that object to emit signals to qt-land. +    """ + +    # Timers, in secs +    POLL_TIME = 0.5 +    CONNECTION_RETRY_TIME = 1 + +    TS_KEY = "ts" +    STATUS_STEP_KEY = "status_step" +    OK_KEY = "ok" +    IP_KEY = "ip" +    REMOTE_KEY = "remote" + +    TUNTAP_READ_KEY = "tun_tap_read" +    TUNTAP_WRITE_KEY = "tun_tap_write" +    TCPUDP_READ_KEY = "tcp_udp_read" +    TCPUDP_WRITE_KEY = "tcp_udp_write" +    AUTH_READ_KEY = "auth_read" + +    def __init__(self, qtsigs=None): +        """ +        Initializes the VPNManager. + +        :param qtsigs: a QObject containing the Qt signals used by the UI +                       to give feedback about state changes. +        :type qtsigs: QObject +        """ +        from twisted.internet import reactor +        self._reactor = reactor +        self._tn = None +        self._qtsigs = qtsigs + +    @property +    def qtsigs(self): +        return self._qtsigs + +    def _disconnect(self): +        """ +        Disconnects the telnet connection to the openvpn process. +        """ +        logger.debug('Closing socket') +        self._tn.write("quit\n") +        self._tn.read_all() +        self._tn.close() +        self._tn = None + +    def _send_command(self, command, until=b"END"): +        """ +        Sends a command to the telnet connection and reads until END +        is reached. + +        :param command: command to send +        :type command: str + +        :param until: byte delimiter string for reading command output +        :type until: byte str + +        :return: response read +        :rtype: list +        """ +        leap_assert(self._tn, "We need a tn connection!") +        try: +            self._tn.write("%s\n" % (command,)) +            buf = self._tn.read_until(until, 2) +            self._tn.read_eager() +            lines = buf.split("\n") +            return lines + +        # XXX should move this to a errBack! +        except Exception as e: +            logger.warning("Error sending command %s: %r" % +                           (command, e)) +        return [] + +    def _connect(self, socket_host, socket_port): +        """ +        Connects to the specified socket_host socket_port. + +        :param socket_host: either socket path (unix) or socket IP +        :type socket_host: str + +        :param socket_port: either string "unix" if it's a unix +                            socket, or port otherwise +        :type socket_port: str +        """ +        try: +            self._tn = UDSTelnet(socket_host, socket_port) + +            # XXX make password optional +            # specially for win. 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) +            if self._tn: +                self._tn.read_eager() + +        # XXX move this to the Errback +        except Exception as e: +            logger.warning("Could not connect to OpenVPN yet: %r" % (e,)) +            self._tn = None + +    def _connectCb(self, *args): +        """ +        Callback for connection. + +        :param args: not used +        """ +        if self._tn: +            logger.info('connected to management') + +    def _connectErr(self, failure): +        """ +        Errorback for connection. + +        :param failure: Failure +        """ +        logger.warning(failure) + +    def connect(self, host, port): +        """ +        Connect to a management interface. + +        :param host: the host of the management interface +        :type host: str + +        :param port: the port of the management interface +        :type port: str + +        :returns: a deferred +        """ +        self.connectd = defer.maybeDeferred(self._connect, host, port) +        self.connectd.addCallbacks(self._connectCb, self._connectErr) +        return self.connectd + +    def is_connected(self): +        """ +        Returns the status of the management interface. + +        :returns: True if connected, False otherwise +        :rtype: bool +        """ +        return True if self._tn else False + +    def try_to_connect(self, retry=0): +        """ +        Attempts to connect to a management interface, and retries +        after CONNECTION_RETRY_TIME if not successful. + +        :param retry: number of the retry +        :type retry: int +        """ +        # TODO decide about putting a max_lim to retries and signaling +        # an error. +        if not self.is_connected(): +            self.connect(self._socket_host, self._socket_port) +            self._reactor.callLater( +                self.CONNECTION_RETRY_TIME, self.try_to_connect, retry + 1) + +    def _parse_state_and_notify(self, output): +        """ +        Parses the output of the state command and emits state_changed +        signal when the state changes. + +        :param output: list of lines that the state command printed as +                       its output +        :type output: list +        """ +        for line in output: +            stripped = line.strip() +            if stripped == "END": +                continue +            parts = stripped.split(",") +            if len(parts) < 5: +                continue +            ts, status_step, ok, ip, remote = parts + +            state_dict = { +                self.TS_KEY: ts, +                self.STATUS_STEP_KEY: status_step, +                self.OK_KEY: ok, +                self.IP_KEY: ip, +                self.REMOTE_KEY: remote +            } + +            if state_dict != self._last_state: +                self.qtsigs.state_changed.emit(state_dict) +                self._last_state = state_dict + +    def _parse_status_and_notify(self, output): +        """ +        Parses the output of the status command and emits +        status_changed signal when the status changes. + +        :param output: list of lines that the status command printed +                       as its output +        :type output: list +        """ +        tun_tap_read = "" +        tun_tap_write = "" +        tcp_udp_read = "" +        tcp_udp_write = "" +        auth_read = "" +        for line in output: +            stripped = line.strip() +            if stripped.endswith("STATISTICS") or stripped == "END": +                continue +            parts = stripped.split(",") +            if len(parts) < 2: +                continue +            if parts[0].strip() == "TUN/TAP read bytes": +                tun_tap_read = parts[1] +            elif parts[0].strip() == "TUN/TAP write bytes": +                tun_tap_write = parts[1] +            elif parts[0].strip() == "TCP/UDP read bytes": +                tcp_udp_read = parts[1] +            elif parts[0].strip() == "TCP/UDP write bytes": +                tcp_udp_write = parts[1] +            elif parts[0].strip() == "Auth read bytes": +                auth_read = parts[1] + +        status_dict = { +            self.TUNTAP_READ_KEY: tun_tap_read, +            self.TUNTAP_WRITE_KEY: tun_tap_write, +            self.TCPUDP_READ_KEY: tcp_udp_read, +            self.TCPUDP_WRITE_KEY: tcp_udp_write, +            self.AUTH_READ_KEY: auth_read +        } + +        if status_dict != self._last_status: +            self.qtsigs.status_changed.emit(status_dict) +            self._last_status = status_dict + +    def get_state(self): +        """ +        Notifies the gui of the output of the state command over +        the openvpn management interface. +        """ +        if self.is_connected(): +            return self._parse_state_and_notify(self._send_command("state")) + +    def get_status(self): +        """ +        Notifies the gui of the output of the status command over +        the openvpn management interface. +        """ +        if self.is_connected(): +            return self._parse_status_and_notify(self._send_command("status")) + +    @property +    def vpn_env(self): +        """ +        Return a dict containing the vpn environment to be used. +        """ +        return self._launcher.get_vpn_env(self._providerconfig) + +    # XXX old methods, not adapted to twisted process yet + +    def _get_openvpn_process(self): +        """ +        Looks for openvpn instances running. + +        :rtype: process +        """ +        openvpn_process = None +        for p in psutil.process_iter(): +            try: +                # XXX Not exact! +                # Will give false positives. +                # we should check that cmdline BEGINS +                # with openvpn or with our wrapper +                # (pkexec / osascript / whatever) +                if self._launcher.OPENVPN_BIN in ' '.join(p.cmdline): +                    openvpn_process = p +                    break +            except psutil.error.AccessDenied: +                pass +        return openvpn_process + +    def _stop_if_already_running(self): +        """ +        Checks if VPN is already running and tries to stop it. + +        :return: True if stopped, False otherwise +        """ + +        process = self._get_openvpn_process() +        if process: +            logger.debug("OpenVPN is already running, trying to stop it") +            cmdline = process.cmdline + +            manag_flag = "--management" +            if isinstance(cmdline, list) and manag_flag in cmdline: +                try: +                    index = cmdline.index(manag_flag) +                    host = cmdline[index + 1] +                    port = cmdline[index + 2] +                    logger.debug("Trying to connect to %s:%s" +                                 % (host, port)) +                    self._connect(host, port) +                    self._send_command("signal SIGTERM") +                    self._tn.close() +                    self._tn = None +                    #self._disconnect() +                except Exception as e: +                    logger.warning("Problem trying to terminate OpenVPN: %r" +                                   % (e,)) + +            process = self._get_openvpn_process() +            if process is None: +                logger.warning("Unabled to terminate OpenVPN") +                return True +            else: +                return False +        return True + + +class VPNProcess(protocol.ProcessProtocol, VPNManager): +    """ +    A ProcessProtocol class that can be used to spawn a process that will +    launch openvpn and connect to its management interface to control it +    programmatically. +    """ + +    def __init__(self, eipconfig, providerconfig, socket_host, socket_port, +                 qtsigs): +        """ +        :param eipconfig: eip configuration object +        :type eipconfig: EIPConfig + +        :param providerconfig: provider specific configuration +        :type providerconfig: ProviderConfig + +        :param socket_host: either socket path (unix) or socket IP +        :type socket_host: str + +        :param socket_port: either string "unix" if it's a unix +                            socket, or port otherwise +        :type socket_port: str + +        :param qtsigs: a QObject containing the Qt signals used to notify the +                       UI. +        :type qtsigs: QObject +        """ +        VPNManager.__init__(self, qtsigs=qtsigs) +        leap_assert_type(eipconfig, EIPConfig) +        leap_assert_type(providerconfig, ProviderConfig) +        leap_assert_type(qtsigs, QtCore.QObject) + +        #leap_assert(not self.isRunning(), "Starting process more than once!") + +        self._eipconfig = eipconfig +        self._providerconfig = providerconfig +        self._socket_host = socket_host +        self._socket_port = socket_port + +        self._launcher = get_platform_launcher() + +        self._last_state = None +        self._last_status = None + +    # processProtocol methods + +    def connectionMade(self): +        """ +        Called when the connection is made. + +        .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa +        """ +        self.try_to_connect() + +    def outReceived(self, data): +        """ +        Called when new data is available on stdout. + +        :param data: the data read on stdout + +        .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa +        """ +        # truncate the newline +        # should send this to the logging window +        vpnlog.info(data[:-1]) + +    def processExited(self, reason): +        """ +        Called when the child process exits. + +        .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa +        """ +        exit_code = reason.value.exitCode +        if isinstance(exit_code, int): +            logger.debug("processExited, status %d" % (exit_code,)) + +    def processEnded(self, reason): +        """ +        Called when the child process exits and all file descriptors associated +        with it have been closed. + +        .. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa +        """ +        exit_code = reason.value.exitCode +        if isinstance(exit_code, int): +            logger.debug("processEnded, status %d" % (exit_code,)) + +    # polling + +    def pollStatus(self): +        """ +        Polls connection status. +        """ +        self.get_status() + +    def pollState(self): +        """ +        Polls connection state. +        """ +        self.get_state() + +    # launcher + +    def getCommand(self): +        """ +        Gets the vpn command from the aproppriate launcher. +        """ +        cmd = self._launcher.get_vpn_command( +            eipconfig=self._eipconfig, +            providerconfig=self._providerconfig, +            socket_host=self._socket_host, +            socket_port=self._socket_port) +        return map(str, cmd) + +    # shutdown + +    def killProcess(self): +        """ +        Sends the KILL signal to the running process. +        """ +        try: +            self.transport.signalProcess('KILL') +        except internet_error.ProcessExitedAlready: +            logger.debug('Process Exited Already') diff --git a/src/leap/services/mail/smtpspec.py b/src/leap/services/mail/smtpspec.py index b455b196..270dfb76 100644 --- a/src/leap/services/mail/smtpspec.py +++ b/src/leap/services/mail/smtpspec.py @@ -22,12 +22,12 @@ smtp_config_spec = {          'serial': {              'type': int,              'default': 1, -            'required': True +            'required': ["True"]          },          'version': {              'type': int,              'default': 1, -            'required': True +            'required': ["True"]          },          'hosts': {              'type': dict, diff --git a/src/leap/services/soledad/soledadspec.py b/src/leap/services/soledad/soledadspec.py index d5a437cc..8233d6a0 100644 --- a/src/leap/services/soledad/soledadspec.py +++ b/src/leap/services/soledad/soledadspec.py @@ -22,12 +22,12 @@ soledad_config_spec = {          'serial': {              'type': int,              'default': 1, -            'required': True +            'required': ["True"]          },          'version': {              'type': int,              'default': 1, -            'required': True +            'required': ["True"]          },          'hosts': {              'type': dict, | 
