diff options
-rw-r--r-- | changes/bug_openvpn-process | 1 | ||||
-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 |
7 files changed, 616 insertions, 484 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/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 89f06a1c..2cad6df3 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( @@ -816,8 +817,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 @@ -852,8 +854,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, @@ -919,7 +919,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("Start EIP")) @@ -983,7 +983,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...")) @@ -1014,12 +1014,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) @@ -1079,7 +1079,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): """ @@ -1147,8 +1147,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, |