summaryrefslogtreecommitdiff
path: root/src/leap
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap')
-rw-r--r--src/leap/__init__.py5
-rw-r--r--src/leap/app.py6
-rw-r--r--src/leap/baseapp/config.py40
-rw-r--r--src/leap/baseapp/dialogs.py33
-rw-r--r--src/leap/baseapp/mainwindow.py87
-rw-r--r--src/leap/baseapp/permcheck.py17
-rw-r--r--src/leap/eip/conductor.py166
-rw-r--r--src/leap/eip/config.py426
-rw-r--r--src/leap/eip/vpnmanager.py3
-rw-r--r--src/leap/gui/mainwindow_rc.py2
-rw-r--r--src/leap/gui/test_mainwindow_rc.py26
-rw-r--r--src/leap/tests/fakeclient.py63
-rw-r--r--src/leap/tests/mocks/__init__.py1
-rw-r--r--src/leap/tests/mocks/manager.py20
-rw-r--r--src/leap/util/__init__.py (renamed from src/leap/utils/__init__.py)0
-rw-r--r--src/leap/util/coroutines.py (renamed from src/leap/utils/coroutines.py)4
-rw-r--r--src/leap/util/fileutil.py113
-rw-r--r--src/leap/util/leap_argparse.py (renamed from src/leap/utils/leap_argparse.py)0
-rw-r--r--src/leap/util/test_fileutil.py99
-rw-r--r--src/leap/util/test_leap_argparse.py27
20 files changed, 950 insertions, 188 deletions
diff --git a/src/leap/__init__.py b/src/leap/__init__.py
index e69de29b..a7ae10e3 100644
--- a/src/leap/__init__.py
+++ b/src/leap/__init__.py
@@ -0,0 +1,5 @@
+from leap import eip
+from leap import baseapp
+from leap import util
+
+__all__ = [eip, baseapp, util]
diff --git a/src/leap/app.py b/src/leap/app.py
index 0a61fd4f..db48701b 100644
--- a/src/leap/app.py
+++ b/src/leap/app.py
@@ -6,6 +6,7 @@ from PyQt4.QtGui import (QApplication, QSystemTrayIcon, QMessageBox)
from leap.baseapp.mainwindow import LeapWindow
+logging.basicConfig()
logger = logging.getLogger(name=__name__)
@@ -15,13 +16,14 @@ def main():
long live to the (hidden) leap window!
"""
import sys
- from leap.utils import leap_argparse
+ from leap.util import leap_argparse
parser, opts = leap_argparse.init_leapc_args()
debug = getattr(opts, 'debug', False)
#XXX get debug level and set logger accordingly
if debug:
- logger.debug('args: ', opts)
+ logger.setLevel('DEBUG')
+ logger.debug('args: %s' % opts)
app = QApplication(sys.argv)
diff --git a/src/leap/baseapp/config.py b/src/leap/baseapp/config.py
deleted file mode 100644
index efdb4726..00000000
--- a/src/leap/baseapp/config.py
+++ /dev/null
@@ -1,40 +0,0 @@
-import ConfigParser
-import os
-
-
-def get_config(config_file=None):
- """
- temporary method for getting configs,
- mainly for early stage development process.
- in the future we will get preferences
- from the storage api
- """
- config = ConfigParser.ConfigParser()
- #config.readfp(open('defaults.cfg'))
- #XXX does this work on win / mac also???
- conf_path_list = ['eip.cfg', # XXX build a
- # proper path with platform-specific places
- # XXX make .config/foo
- os.path.expanduser('~/.eip.cfg')]
- if config_file:
- config.readfp(config_file)
- else:
- config.read(conf_path_list)
- return config
-
-
-# XXX wrapper around config? to get default values
-
-def get_with_defaults(config, section, option):
- if config.has_option(section, option):
- return config.get(section, option)
- else:
- # XXX lookup in defaults dict???
- pass
-
-
-def get_vpn_stdout_mockup():
- command = "python"
- args = ["-u", "-c", "from eip_client import fakeclient;\
-fakeclient.write_output()"]
- return command, args
diff --git a/src/leap/baseapp/dialogs.py b/src/leap/baseapp/dialogs.py
new file mode 100644
index 00000000..4b1b5b62
--- /dev/null
+++ b/src/leap/baseapp/dialogs.py
@@ -0,0 +1,33 @@
+from PyQt4.QtGui import (QDialog, QFrame, QPushButton, QLabel, QMessageBox)
+
+
+class ErrorDialog(QDialog):
+ def __init__(self, parent=None):
+ super(ErrorDialog, self).__init__(parent)
+
+ frameStyle = QFrame.Sunken | QFrame.Panel
+ self.warningLabel = QLabel()
+ self.warningLabel.setFrameStyle(frameStyle)
+ self.warningButton = QPushButton("QMessageBox.&warning()")
+
+ def warningMessage(self, msg, label):
+ msgBox = QMessageBox(QMessageBox.Warning,
+ "QMessageBox.warning()", msg,
+ QMessageBox.NoButton, self)
+ msgBox.addButton("&Ok", QMessageBox.AcceptRole)
+ msgBox.addButton("&Cancel", QMessageBox.RejectRole)
+ if msgBox.exec_() == QMessageBox.AcceptRole:
+ self.warningLabel.setText("Save Again")
+ else:
+ self.warningLabel.setText("Continue")
+
+ def criticalMessage(self, msg, label):
+ msgBox = QMessageBox(QMessageBox.Critical,
+ "QMessageBox.critical()", msg,
+ QMessageBox.NoButton, self)
+ msgBox.addButton("&Ok", QMessageBox.AcceptRole)
+ msgBox.addButton("&Cancel", QMessageBox.RejectRole)
+ if msgBox.exec_() == QMessageBox.AcceptRole:
+ self.warningLabel.setText("Save Again")
+ else:
+ self.warningLabel.setText("Continue")
diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py
index 68b6de8f..85129a9b 100644
--- a/src/leap/baseapp/mainwindow.py
+++ b/src/leap/baseapp/mainwindow.py
@@ -11,8 +11,14 @@ from PyQt4.QtGui import (QMainWindow, QWidget, QVBoxLayout, QMessageBox,
QTextBrowser, qApp)
from PyQt4.QtCore import (pyqtSlot, pyqtSignal, QTimer)
+from leap.baseapp.dialogs import ErrorDialog
+from leap.eip.conductor import (EIPConductor,
+ EIPNoCommandError)
+
+from leap.eip.config import (EIPInitBadKeyFilePermError)
+# from leap.eip import exceptions as eip_exceptions
+
from leap.gui import mainwindow_rc
-from leap.eip.conductor import EIPConductor
class LeapWindow(QMainWindow):
@@ -58,21 +64,76 @@ class LeapWindow(QMainWindow):
mainLayout.addWidget(self.loggerBox)
widget.setLayout(mainLayout)
+ self.trayIcon.show()
+ config_file = getattr(opts, 'config_file', None)
+
#
# conductor is in charge of all
# vpn-related configuration / monitoring.
# we pass a tuple of signals that will be
# triggered when status changes.
#
- config_file = getattr(opts, 'config_file', None)
self.conductor = EIPConductor(
watcher_cb=self.newLogLine.emit,
config_file=config_file,
- status_signals=(self.statusChange.emit, ))
+ status_signals=(self.statusChange.emit, ),
+ debug=self.debugmode)
- self.trayIcon.show()
+ #
+ # bunch of self checks.
+ # XXX move somewhere else alltogether.
+ #
- self.setWindowTitle("Leap")
+ if self.conductor.missing_provider is True:
+ dialog = ErrorDialog()
+ dialog.criticalMessage(
+ 'Missing provider. Add a remote_ip entry '
+ 'under section [provider] in eip.cfg',
+ 'error')
+
+ if self.conductor.missing_vpn_keyfile is True:
+ dialog = ErrorDialog()
+ dialog.criticalMessage(
+ 'Could not find the vpn keys file',
+ 'error')
+
+ # ... btw, review pending.
+ # os.kill of subprocess fails if we have
+ # some of this errors.
+
+ if self.conductor.bad_provider is True:
+ dialog = ErrorDialog()
+ dialog.criticalMessage(
+ 'Bad provider entry. Check that remote_ip entry '
+ 'has an IP under section [provider] in eip.cfg',
+ 'error')
+
+ if self.conductor.bad_keyfile_perms is True:
+ dialog = ErrorDialog()
+ dialog.criticalMessage(
+ 'The vpn keys file has bad permissions',
+ 'error')
+
+ if self.conductor.missing_auth_agent is True:
+ dialog = ErrorDialog()
+ dialog.warningMessage(
+ 'We could not find any authentication '
+ 'agent in your system.<br/>'
+ 'Make sure you have '
+ '<b>polkit-gnome-authentication-agent-1</b> '
+ 'running and try again.',
+ 'error')
+
+ if self.conductor.missing_pkexec is True:
+ dialog = ErrorDialog()
+ dialog.warningMessage(
+ 'We could not find <b>pkexec</b> in your '
+ 'system.<br/> Do you want to try '
+ '<b>setuid workaround</b>? '
+ '(<i>DOES NOTHING YET</i>)',
+ 'error')
+
+ self.setWindowTitle("LEAP Client")
self.resize(400, 300)
self.set_statusbarMessage('ready')
@@ -222,6 +283,7 @@ technolust</i>")
self.trayIconMenu.addAction(self.quitAction)
self.trayIcon = QSystemTrayIcon(self)
+ self.setIcon('disconnected')
self.trayIcon.setContextMenu(self.trayIconMenu)
def createLogBrowser(self):
@@ -285,11 +347,11 @@ technolust</i>")
updating icon, status bar, etc.
"""
- print('STATUS CHANGED! (on Qt-land)')
- print('%s -> %s' % (status.previous, status.current))
+ #print('STATUS CHANGED! (on Qt-land)')
+ #print('%s -> %s' % (status.previous, status.current))
icon_name = self.conductor.get_icon_name()
self.setIcon(icon_name)
- print 'icon = ', icon_name
+ #print 'icon = ', icon_name
# change connection pixmap widget
self.setConnWidget(icon_name)
@@ -315,7 +377,14 @@ technolust</i>")
stub for running child process with vpn
"""
if self.vpn_service_started is False:
- self.conductor.connect()
+ try:
+ self.conductor.connect()
+ except EIPNoCommandError:
+ dialog = ErrorDialog()
+ dialog.warningMessage(
+ 'No suitable openvpn command found. '
+ '<br/>(Might be a permissions problem)',
+ 'error')
if self.debugmode:
self.startStopButton.setText('&Disconnect')
self.vpn_service_started = True
diff --git a/src/leap/baseapp/permcheck.py b/src/leap/baseapp/permcheck.py
new file mode 100644
index 00000000..6b74cb6e
--- /dev/null
+++ b/src/leap/baseapp/permcheck.py
@@ -0,0 +1,17 @@
+import commands
+import os
+
+from leap.util.fileutil import which
+
+
+def is_pkexec_in_system():
+ pkexec_path = which('pkexec')
+ if not pkexec_path:
+ return False
+ return os.access(pkexec_path, os.X_OK)
+
+
+def is_auth_agent_running():
+ return bool(
+ commands.getoutput(
+ 'ps aux | grep polkit-[g]nome-authentication-agent-1'))
diff --git a/src/leap/eip/conductor.py b/src/leap/eip/conductor.py
index e3adadc4..8f9d6051 100644
--- a/src/leap/eip/conductor.py
+++ b/src/leap/eip/conductor.py
@@ -6,8 +6,20 @@ from __future__ import (division, unicode_literals, print_function)
from functools import partial
import logging
-from leap.utils.coroutines import spawn_and_watch_process
-from leap.baseapp.config import get_config, get_vpn_stdout_mockup
+from leap.util.coroutines import spawn_and_watch_process
+
+# XXX from leap.eip import config as eipconfig
+# from leap.eip import exceptions as eip_exceptions
+
+from leap.eip.config import (get_config, build_ovpn_command,
+ check_or_create_default_vpnconf,
+ check_vpn_keys,
+ EIPNoPkexecAvailable,
+ EIPNoPolkitAuthAgentAvailable,
+ EIPInitNoProviderError,
+ EIPInitBadProviderError,
+ EIPInitNoKeyFileError,
+ EIPInitBadKeyFilePermError)
from leap.eip.vpnwatcher import EIPConnectionStatus, status_watcher
from leap.eip.vpnmanager import OpenVPNManager, ConnectionRefusedError
@@ -15,6 +27,10 @@ logger = logging.getLogger(name=__name__)
# TODO Move exceptions to their own module
+# eip.exceptions
+
+class EIPNoCommandError(Exception):
+ pass
class ConnectionError(Exception):
@@ -39,8 +55,16 @@ class UnrecoverableError(EIPClientError):
"""
we cannot do anything about it, sorry
"""
+ # XXX we should catch this and raise
+ # to qtland, so we emit signal
+ # to translate whatever kind of error
+ # to user-friendly msg in dialog.
pass
+#
+# Openvpn related classes
+#
+
class OpenVPNConnection(object):
"""
@@ -49,7 +73,8 @@ class OpenVPNConnection(object):
"""
# Connection Methods
- def __init__(self, config_file=None, watcher_cb=None):
+ def __init__(self, config_file=None,
+ watcher_cb=None, debug=False):
#XXX FIXME
#change watcher_cb to line_observer
"""
@@ -64,6 +89,8 @@ to be triggered for each one of them.
"""
# XXX get host/port from config
self.manager = OpenVPNManager()
+ self.debug = debug
+ #print('conductor:%s' % debug)
self.config_file = config_file
self.watcher_cb = watcher_cb
@@ -76,46 +103,104 @@ to be triggered for each one of them.
self.port = None
self.proto = None
- self.autostart = True
+ self.missing_pkexec = False
+ self.missing_auth_agent = False
+ self.bad_keyfile_perms = False
+ self.missing_vpn_keyfile = False
+ self.missing_provider = False
+ self.bad_provider = False
- self._get_config()
+ self.command = None
+ self.args = None
- def _set_command_mockup(self):
- """
- sets command and args for a command mockup
- that just mimics the output from the real thing
- """
- command, args = get_vpn_stdout_mockup()
- self.command, self.args = command, args
+ self.autostart = True
+ self._get_or_create_config()
+ self._check_vpn_keys()
- def _get_config(self):
- """
- retrieves the config options from defaults or
- home file, or config file passed in command line.
- """
- config = get_config(config_file=self.config_file)
- self.config = config
+ def _set_autostart(self):
+ config = self.config
+ if config.has_option('openvpn', 'autostart'):
+ autostart = config.getboolean('openvpn',
+ 'autostart')
+ self.autostart = autostart
+ else:
+ if config.has_option('DEFAULT', 'autostart'):
+ autostart = config.getboolean('DEFAULT',
+ 'autostart')
+ self.autostart = autostart
+ def _set_ovpn_command(self):
+ config = self.config
if config.has_option('openvpn', 'command'):
commandline = config.get('openvpn', 'command')
- if commandline == "mockup":
- self._set_command_mockup()
- return
+
command_split = commandline.split(' ')
command = command_split[0]
if len(command_split) > 1:
args = command_split[1:]
else:
args = []
+
self.command = command
- #print("debug: command = %s" % command)
self.args = args
else:
- self._set_command_mockup()
+ # no command in config, we build it up.
+ # XXX check also for command-line --command flag
+ try:
+ command, args = build_ovpn_command(config,
+ debug=self.debug)
+ except EIPNoPolkitAuthAgentAvailable:
+ command = args = None
+ self.missing_auth_agent = True
+ except EIPNoPkexecAvailable:
+ command = args = None
+ self.missing_pkexec = True
+
+ # XXX if not command, signal error.
+ self.command = command
+ self.args = args
- if config.has_option('openvpn', 'autostart'):
- autostart = config.get('openvpn', 'autostart')
- self.autostart = autostart
+ def _check_ovpn_config(self):
+ """
+ checks if there is a default openvpn config.
+ if not, it writes one with info from the provider
+ definition file
+ """
+ # TODO
+ # - get --with-openvpn-config from opts
+ try:
+ check_or_create_default_vpnconf(self.config)
+ except EIPInitNoProviderError:
+ logger.error('missing default provider definition')
+ self.missing_provider = True
+ except EIPInitBadProviderError:
+ logger.error('bad provider definition')
+ self.bad_provider = True
+
+ def _get_or_create_config(self):
+ """
+ retrieves the config options from defaults or
+ home file, or config file passed in command line.
+ populates command and args to be passed to subprocess.
+ """
+ config = get_config(config_file=self.config_file)
+ self.config = config
+
+ self._set_autostart()
+ self._set_ovpn_command()
+ self._check_ovpn_config()
+
+ def _check_vpn_keys(self):
+ """
+ checks for correct permissions on vpn keys
+ """
+ try:
+ check_vpn_keys(self.config)
+ except EIPInitNoKeyFileError:
+ self.missing_vpn_keyfile = True
+ except EIPInitBadKeyFilePermError:
+ logger.error('error while checking vpn keys')
+ self.bad_keyfile_perms = True
def _launch_openvpn(self):
"""
@@ -140,13 +225,15 @@ to be triggered for each one of them.
self.subp = subp
self.watcher = watcher
- conn_result = self.status.CONNECTED
- return conn_result
+ #conn_result = self.status.CONNECTED
+ #return conn_result
def _try_connection(self):
"""
attempts to connect
"""
+ if self.command is None:
+ raise EIPNoCommandError
if self.subp is not None:
print('cowardly refusing to launch subprocess again')
return
@@ -186,7 +273,7 @@ class EIPConductor(OpenVPNConnection):
"""
self.manager.forget_errors()
self._try_connection()
- # XXX should capture errors?
+ # XXX should capture errors here?
def disconnect(self):
"""
@@ -194,25 +281,6 @@ class EIPConductor(OpenVPNConnection):
"""
self._disconnect()
self.status.change_to(self.status.DISCONNECTED)
- pass
-
- def shutdown(self):
- """
- shutdown and quit
- """
- self.desired_con_state = self.status.DISCONNECTED
-
- def connection_state(self):
- """
- returns the current connection state
- """
- return self.status.current
-
- def desired_connection_state(self):
- """
- returns the desired_connection state
- """
- return self.desired_con_state
def poll_connection_state(self):
"""
diff --git a/src/leap/eip/config.py b/src/leap/eip/config.py
new file mode 100644
index 00000000..6118c9de
--- /dev/null
+++ b/src/leap/eip/config.py
@@ -0,0 +1,426 @@
+import ConfigParser
+import grp
+import logging
+import os
+import platform
+import socket
+
+from leap.util.fileutil import (which, mkdir_p,
+ check_and_fix_urw_only)
+from leap.baseapp.permcheck import (is_pkexec_in_system,
+ is_auth_agent_running)
+
+logger = logging.getLogger(name=__name__)
+logger.setLevel('DEBUG')
+
+# XXX move exceptions to
+# from leap.eip import exceptions as eip_exceptions
+
+
+class EIPNoPkexecAvailable(Exception):
+ pass
+
+
+class EIPNoPolkitAuthAgentAvailable(Exception):
+ pass
+
+
+class EIPInitNoProviderError(Exception):
+ pass
+
+
+class EIPInitBadProviderError(Exception):
+ pass
+
+
+class EIPInitNoKeyFileError(Exception):
+ pass
+
+
+class EIPInitBadKeyFilePermError(Exception):
+ pass
+
+
+OPENVPN_CONFIG_TEMPLATE = """#Autogenerated by eip-client wizard
+remote {VPN_REMOTE_HOST} {VPN_REMOTE_PORT}
+
+client
+dev tun
+persist-tun
+persist-key
+proto udp
+tls-client
+remote-cert-tls server
+
+cert {LEAP_EIP_KEYS}
+key {LEAP_EIP_KEYS}
+ca {LEAP_EIP_KEYS}
+"""
+
+
+def get_config_dir():
+ """
+ get the base dir for all leap config
+ @rparam: config path
+ @rtype: string
+ """
+ # TODO
+ # check for $XDG_CONFIG_HOME var?
+ # get a more sensible path for win/mac
+ # kclair: opinion? ^^
+ return os.path.expanduser(
+ os.path.join('~',
+ '.config',
+ 'leap'))
+
+
+def get_config_file(filename, folder=None):
+ """
+ concatenates the given filename
+ with leap config dir.
+ @param filename: name of the file
+ @type filename: string
+ @rparam: full path to config file
+ """
+ path = []
+ path.append(get_config_dir())
+ if folder is not None:
+ path.append(folder)
+ path.append(filename)
+ return os.path.join(*path)
+
+
+def get_default_provider_path():
+ default_subpath = os.path.join("providers",
+ "default")
+ default_provider_path = get_config_file(
+ '',
+ folder=default_subpath)
+ return default_provider_path
+
+
+def validate_ip(ip_str):
+ """
+ raises exception if the ip_str is
+ not a valid representation of an ip
+ """
+ socket.inet_aton(ip_str)
+
+
+def check_or_create_default_vpnconf(config):
+ """
+ checks that a vpn config file
+ exists for a default provider,
+ or creates one if it does not.
+ ATM REQURES A [provider] section in
+ eip.cfg with _at least_ a remote_ip value
+ """
+ default_provider_path = get_default_provider_path()
+
+ if not os.path.isdir(default_provider_path):
+ mkdir_p(default_provider_path)
+
+ conf_file = get_config_file(
+ 'openvpn.conf',
+ folder=default_provider_path)
+
+ if os.path.isfile(conf_file):
+ return
+ else:
+ logger.debug(
+ 'missing default openvpn config\n'
+ 'creating one...')
+
+ # We're getting provider from eip.cfg
+ # by now. Get it from a list of gateways
+ # instead.
+
+ try:
+ remote_ip = config.get('provider',
+ 'remote_ip')
+ validate_ip(remote_ip)
+
+ except ConfigParser.NoOptionError:
+ raise EIPInitNoProviderError
+
+ except socket.error:
+ # this does not look like an ip, dave
+ raise EIPInitBadProviderError
+
+ if config.has_option('provider', 'remote_port'):
+ remote_port = config.get('provider',
+ 'remote_port')
+ else:
+ remote_port = 1194
+
+ default_subpath = os.path.join("providers",
+ "default")
+ default_provider_path = get_config_file(
+ '',
+ folder=default_subpath)
+
+ if not os.path.isdir(default_provider_path):
+ mkdir_p(default_provider_path)
+
+ conf_file = get_config_file(
+ 'openvpn.conf',
+ folder=default_provider_path)
+
+ # XXX keys have to be manually placed by now
+ keys_file = get_config_file(
+ 'openvpn.keys',
+ folder=default_provider_path)
+
+ ovpn_config = OPENVPN_CONFIG_TEMPLATE.format(
+ VPN_REMOTE_HOST=remote_ip,
+ VPN_REMOTE_PORT=remote_port,
+ LEAP_EIP_KEYS=keys_file)
+
+ with open(conf_file, 'wb') as f:
+ f.write(ovpn_config)
+
+
+def build_ovpn_options(daemon=False):
+ """
+ build a list of options
+ to be passed in the
+ openvpn invocation
+ @rtype: list
+ @rparam: options
+ """
+ # XXX review which of the
+ # options we don't need.
+
+ # TODO pass also the config file,
+ # since we will need to take some
+ # things from there if present.
+
+ # get user/group name
+ # also from config.
+ user = os.getlogin()
+ gid = os.getgroups()[-1]
+ group = grp.getgrgid(gid).gr_name
+
+ opts = []
+
+ #moved to config files
+ #opts.append('--persist-tun')
+ #opts.append('--persist-key')
+
+ # set user and group
+ opts.append('--user')
+ opts.append('%s' % user)
+ opts.append('--group')
+ opts.append('%s' % group)
+
+ opts.append('--management-client-user')
+ opts.append('%s' % user)
+ opts.append('--management-signal')
+
+ # set default options for management
+ # interface. unix sockets or telnet interface for win.
+ # XXX take them from the config object.
+
+ ourplatform = platform.system()
+ if ourplatform in ("Linux", "Mac"):
+ opts.append('--management')
+ opts.append('/tmp/.eip.sock')
+ opts.append('unix')
+ if ourplatform == "Windows":
+ opts.append('--management')
+ opts.append('localhost')
+ # XXX which is a good choice?
+ opts.append('7777')
+
+ # remaining config options will go in a file
+
+ # NOTE: we will build this file from
+ # the service definition file.
+ # XXX override from --with-openvpn-config
+
+ opts.append('--config')
+
+ default_provider_path = get_default_provider_path()
+ ovpncnf = get_config_file(
+ 'openvpn.conf',
+ folder=default_provider_path)
+ opts.append(ovpncnf)
+
+ # we cannot run in daemon mode
+ # with the current subp setting.
+ # see: https://leap.se/code/issues/383
+ #if daemon is True:
+ #opts.append('--daemon')
+
+ return opts
+
+
+def build_ovpn_command(config, debug=False):
+ """
+ build a string with the
+ complete openvpn invocation
+
+ @param config: config object
+ @type config: ConfigParser instance
+
+ @rtype [string, [list of strings]]
+ @rparam: a list containing the command string
+ and a list of options.
+ """
+ command = []
+ use_pkexec = True
+ ovpn = None
+
+ if config.has_option('openvpn', 'use_pkexec'):
+ use_pkexec = config.get('openvpn', 'use_pkexec')
+ if platform.system() == "Linux" and use_pkexec:
+
+ # XXX check for both pkexec (done)
+ # AND a suitable authentication
+ # agent running.
+ logger.info('use_pkexec set to True')
+
+ if not is_pkexec_in_system():
+ logger.error('no pkexec in system')
+ raise EIPNoPkexecAvailable
+
+ if not is_auth_agent_running():
+ logger.warning(
+ "no polkit auth agent found. "
+ "pkexec will use its own text "
+ "based authentication agent. "
+ "that's probably a bad idea")
+ raise EIPNoPolkitAuthAgentAvailable
+
+ command.append('pkexec')
+
+ if config.has_option('openvpn',
+ 'openvpn_binary'):
+ ovpn = config.get('openvpn',
+ 'openvpn_binary')
+ if not ovpn and config.has_option('DEFAULT',
+ 'openvpn_binary'):
+ ovpn = config.get('DEFAULT',
+ 'openvpn_binary')
+
+ if ovpn:
+ command.append(ovpn)
+
+ daemon_mode = not debug
+
+ for opt in build_ovpn_options(daemon=daemon_mode):
+ command.append(opt)
+
+ # XXX check len and raise proper error
+
+ return [command[0], command[1:]]
+
+
+def get_sensible_defaults():
+ """
+ gathers a dict of sensible defaults,
+ platform sensitive,
+ to be used to initialize the config parser
+ @rtype: dict
+ @rparam: default options.
+ """
+
+ # this way we're passing a simple dict
+ # that will initialize the configparser
+ # and will get written to "DEFAULTS" section,
+ # which is fine for now.
+ # if we want to write to a particular section
+ # we can better pass a tuple of triples
+ # (('section1', 'foo', '23'),)
+ # and config.set them
+
+ defaults = dict()
+ defaults['openvpn_binary'] = which('openvpn')
+ defaults['autostart'] = 'true'
+
+ # TODO
+ # - management.
+ return defaults
+
+
+def get_config(config_file=None):
+ """
+ temporary method for getting configs,
+ mainly for early stage development process.
+ in the future we will get preferences
+ from the storage api
+
+ @rtype: ConfigParser instance
+ @rparam: a config object
+ """
+ # TODO
+ # - refactor out common things and get
+ # them to util/ or baseapp/
+
+ defaults = get_sensible_defaults()
+ config = ConfigParser.ConfigParser(defaults)
+
+ if not config_file:
+ fpath = get_config_file('eip.cfg')
+ if not os.path.isfile(fpath):
+ dpath, cfile = os.path.split(fpath)
+ if not os.path.isdir(dpath):
+ mkdir_p(dpath)
+ with open(fpath, 'wb') as configfile:
+ config.write(configfile)
+ config_file = open(fpath)
+
+ #TODO
+ # - convert config_file to list;
+ # look in places like /etc/leap/eip.cfg
+ # for global settings.
+ # - raise warnings/error if bad options.
+
+ # at this point, the file should exist.
+ # errors would have been raised above.
+
+ config.readfp(config_file)
+
+ return config
+
+
+def check_vpn_keys(config):
+ """
+ performs an existance and permission check
+ over the openvpn keys file.
+ Currently we're expecting a single file
+ per provider, containing the CA cert,
+ the provider key, and our client certificate
+ """
+
+ keyopt = ('provider', 'keyfile')
+
+ # XXX at some point,
+ # should separate between CA, provider cert
+ # and our certificate.
+ # make changes in the default provider template
+ # accordingly.
+
+ # get vpn keys
+ if config.has_option(*keyopt):
+ keyfile = config.get(*keyopt)
+ else:
+ keyfile = get_config_file(
+ 'openvpn.keys',
+ folder=get_default_provider_path())
+ logger.debug('keyfile = %s', keyfile)
+
+ # if no keys, raise error.
+ # should be catched by the ui and signal user.
+
+ if not os.path.isfile(keyfile):
+ logger.error('key file %s not found. aborting.',
+ keyfile)
+ raise EIPInitNoKeyFileError
+
+ # check proper permission on keys
+ # bad perms? try to fix them
+ try:
+ check_and_fix_urw_only(keyfile)
+ except OSError:
+ raise EIPInitBadKeyFilePermError
diff --git a/src/leap/eip/vpnmanager.py b/src/leap/eip/vpnmanager.py
index 78777cfb..caf7ab76 100644
--- a/src/leap/eip/vpnmanager.py
+++ b/src/leap/eip/vpnmanager.py
@@ -6,6 +6,7 @@ import telnetlib
import time
logger = logging.getLogger(name=__name__)
+logger.setLevel('DEBUG')
TELNET_PORT = 23
@@ -74,7 +75,7 @@ class OpenVPNManager(object):
self.with_errors = False
def forget_errors(self):
- print('forgetting errors')
+ logger.debug('forgetting errors')
self.with_errors = False
def connect(self):
diff --git a/src/leap/gui/mainwindow_rc.py b/src/leap/gui/mainwindow_rc.py
index e5a671f3..59cd6948 100644
--- a/src/leap/gui/mainwindow_rc.py
+++ b/src/leap/gui/mainwindow_rc.py
@@ -2,7 +2,7 @@
# Resource object code
#
-# Created: Sun Jul 22 17:08:49 2012
+# Created: Thu Aug 9 23:13:20 2012
# by: The Resource Compiler for PyQt (Qt v4.8.2)
#
# WARNING! All changes made in this file will be lost!
diff --git a/src/leap/gui/test_mainwindow_rc.py b/src/leap/gui/test_mainwindow_rc.py
new file mode 100644
index 00000000..fd02704e
--- /dev/null
+++ b/src/leap/gui/test_mainwindow_rc.py
@@ -0,0 +1,26 @@
+import unittest
+import hashlib
+
+import sip
+sip.setapi('QVariant', 2)
+
+from leap.gui import mainwindow_rc
+
+# I have to admit that there's something
+# perverse in testing this.
+# But I thought that it could be a good idea
+# to put a check to avoid non-updated resources files.
+
+# so, if you came here because an updated resource
+# did break a test, what you have to do is getting
+# the md5 hash of your qt_resource_data and change it here.
+
+# annoying? yep. try making a script for that :P
+
+
+class MainWindowResourcesTest(unittest.TestCase):
+
+ def test_mainwindow_resources_hash(self):
+ self.assertEqual(
+ hashlib.md5(mainwindow_rc.qt_resource_data).hexdigest(),
+ '5cc26322f96fabaa05c404f22774c716')
diff --git a/src/leap/tests/fakeclient.py b/src/leap/tests/fakeclient.py
deleted file mode 100644
index 45de2cd6..00000000
--- a/src/leap/tests/fakeclient.py
+++ /dev/null
@@ -1,63 +0,0 @@
-fakeoutput = """
-mullvad Sun Jun 17 14:34:57 2012 OpenVPN 2.2.1 i486-linux-gnu [SSL] [LZO2] [EPOLL] [PKCS11] [eurephia] [MH] [PF_INET6] [IPv6 payload 20110424-2 (2.2RC2)] built
- on Mar 23 2012
-Sun Jun 17 14:34:57 2012 MANAGEMENT: TCP Socket listening on [AF_INET]127.0.0.1:7505
-Sun Jun 17 14:34:57 2012 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
-Sun Jun 17 14:34:57 2012 WARNING: file 'ssl/1021380964266.key' is group or others accessible
-Sun Jun 17 14:34:57 2012 LZO compression initialized
-Sun Jun 17 14:34:57 2012 Control Channel MTU parms [ L:1542 D:138 EF:38 EB:0 ET:0 EL:0 ]
-Sun Jun 17 14:34:57 2012 Socket Buffers: R=[163840->131072] S=[163840->131072]
-Sun Jun 17 14:34:57 2012 Data Channel MTU parms [ L:1542 D:1450 EF:42 EB:135 ET:0 EL:0 AF:3/1 ]
-Sun Jun 17 14:34:57 2012 Local Options hash (VER=V4): '41690919'
-Sun Jun 17 14:34:57 2012 Expected Remote Options hash (VER=V4): '530fdded'
-Sun Jun 17 14:34:57 2012 UDPv4 link local: [undef]
-Sun Jun 17 14:34:57 2012 UDPv4 link remote: [AF_INET]46.21.99.25:1197
-Sun Jun 17 14:34:57 2012 TLS: Initial packet from [AF_INET]46.21.99.25:1197, sid=63c29ace 1d3060d0
-Sun Jun 17 14:34:58 2012 VERIFY OK: depth=2, /C=NA/ST=None/L=None/O=Mullvad/CN=Mullvad_CA/emailAddress=info@mullvad.net
-Sun Jun 17 14:34:58 2012 VERIFY OK: depth=1, /C=NA/ST=None/L=None/O=Mullvad/CN=master.mullvad.net/emailAddress=info@mullvad.net
-Sun Jun 17 14:34:58 2012 Validating certificate key usage
-Sun Jun 17 14:34:58 2012 ++ Certificate has key usage 00a0, expects 00a0
-Sun Jun 17 14:34:58 2012 VERIFY KU OK
-Sun Jun 17 14:34:58 2012 Validating certificate extended key usage
-Sun Jun 17 14:34:58 2012 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication
-Sun Jun 17 14:34:58 2012 VERIFY EKU OK
-Sun Jun 17 14:34:58 2012 VERIFY OK: depth=0, /C=NA/ST=None/L=None/O=Mullvad/CN=se2.mullvad.net/emailAddress=info@mullvad.net
-Sun Jun 17 14:34:59 2012 Data Channel Encrypt: Cipher 'BF-CBC' initialized with 128 bit key
-Sun Jun 17 14:34:59 2012 Data Channel Encrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
-Sun Jun 17 14:34:59 2012 Data Channel Decrypt: Cipher 'BF-CBC' initialized with 128 bit key
-Sun Jun 17 14:34:59 2012 Data Channel Decrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
-Sun Jun 17 14:34:59 2012 Control Channel: TLSv1, cipher TLSv1/SSLv3 DHE-RSA-AES256-SHA, 2048 bit RSA
-Sun Jun 17 14:34:59 2012 [se2.mullvad.net] Peer Connection Initiated with [AF_INET]46.21.99.25:1197
-Sun Jun 17 14:35:01 2012 SENT CONTROL [se2.mullvad.net]: 'PUSH_REQUEST' (status=1)
-Sun Jun 17 14:35:02 2012 PUSH: Received control message: 'PUSH_REPLY,redirect-gateway def1 bypass-dhcp,dhcp-option DNS 10.11.0.1,route 10.11.0.1,topology net30,ifconfig 10.11.0.202 10.11.0.201'
-Sun Jun 17 14:35:02 2012 OPTIONS IMPORT: --ifconfig/up options modified
-Sun Jun 17 14:35:02 2012 OPTIONS IMPORT: route options modified
-Sun Jun 17 14:35:02 2012 OPTIONS IMPORT: --ip-win32 and/or --dhcp-option options modified
-Sun Jun 17 14:35:02 2012 ROUTE default_gateway=192.168.0.1
-Sun Jun 17 14:35:02 2012 TUN/TAP device tun0 opened
-Sun Jun 17 14:35:02 2012 TUN/TAP TX queue length set to 100
-Sun Jun 17 14:35:02 2012 do_ifconfig, tt->ipv6=0, tt->did_ifconfig_ipv6_setup=0
-Sun Jun 17 14:35:02 2012 /sbin/ifconfig tun0 10.11.0.202 pointopoint 10.11.0.201 mtu 1500
-Sun Jun 17 14:35:02 2012 /etc/openvpn/update-resolv-conf tun0 1500 1542 10.11.0.202 10.11.0.201 init
-dhcp-option DNS 10.11.0.1
-Sun Jun 17 14:35:05 2012 /sbin/route add -net 46.21.99.25 netmask 255.255.255.255 gw 192.168.0.1
-Sun Jun 17 14:35:05 2012 /sbin/route add -net 0.0.0.0 netmask 128.0.0.0 gw 10.11.0.201
-Sun Jun 17 14:35:05 2012 /sbin/route add -net 128.0.0.0 netmask 128.0.0.0 gw 10.11.0.201
-Sun Jun 17 14:35:05 2012 /sbin/route add -net 10.11.0.1 netmask 255.255.255.255 gw 10.11.0.201
-Sun Jun 17 14:35:05 2012 Initialization Sequence Completed
-Sun Jun 17 14:34:57 2012 MANAGEMENT: TCP Socket listening on [AF_INET]127.0.0.1:7505
-"""
-
-import time
-import sys
-
-
-def write_output():
- for line in fakeoutput.split('\n'):
- sys.stdout.write(line + '\n')
- sys.stdout.flush()
- #print(line)
- time.sleep(0.1)
-
-if __name__ == "__main__":
- write_output()
diff --git a/src/leap/tests/mocks/__init__.py b/src/leap/tests/mocks/__init__.py
deleted file mode 100644
index 06f96870..00000000
--- a/src/leap/tests/mocks/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-import manager
diff --git a/src/leap/tests/mocks/manager.py b/src/leap/tests/mocks/manager.py
deleted file mode 100644
index 564631cd..00000000
--- a/src/leap/tests/mocks/manager.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from mock import Mock
-
-from eip_client.vpnmanager import OpenVPNManager
-
-vpn_commands = {
- 'status': [
- 'OpenVPN STATISTICS', 'Updated,Mon Jun 25 11:51:21 2012',
- 'TUN/TAP read bytes,306170', 'TUN/TAP write bytes,872102',
- 'TCP/UDP read bytes,986177', 'TCP/UDP write bytes,439329',
- 'Auth read bytes,872102'],
- 'state': ['1340616463,CONNECTED,SUCCESS,172.28.0.2,198.252.153.38'],
- # XXX add more tests
- }
-
-
-def get_openvpn_manager_mocks():
- manager = OpenVPNManager()
- manager.status = Mock(return_value='\n'.join(vpn_commands['status']))
- manager.state = Mock(return_value=vpn_commands['state'][0])
- return manager
diff --git a/src/leap/utils/__init__.py b/src/leap/util/__init__.py
index e69de29b..e69de29b 100644
--- a/src/leap/utils/__init__.py
+++ b/src/leap/util/__init__.py
diff --git a/src/leap/utils/coroutines.py b/src/leap/util/coroutines.py
index 5e25eb63..e7ccfacf 100644
--- a/src/leap/utils/coroutines.py
+++ b/src/leap/util/coroutines.py
@@ -74,8 +74,8 @@ def watch_output(out, observers):
for each event
:type ovservers: tuple
"""
- observer_dict = {observer: process_events(observer)
- for observer in observers}
+ observer_dict = dict(((observer, process_events(observer))
+ for observer in observers))
for line in iter(out.readline, b''):
for obs in observer_dict:
observer_dict[obs].send(line)
diff --git a/src/leap/util/fileutil.py b/src/leap/util/fileutil.py
new file mode 100644
index 00000000..429e4b12
--- /dev/null
+++ b/src/leap/util/fileutil.py
@@ -0,0 +1,113 @@
+import errno
+from itertools import chain
+import logging
+import os
+import platform
+import stat
+
+
+logger = logging.getLogger()
+
+
+def is_user_executable(fpath):
+ st = os.stat(fpath)
+ return bool(st.st_mode & stat.S_IXUSR)
+
+
+def extend_path():
+ ourplatform = platform.system()
+ if ourplatform == "Linux":
+ return "/usr/local/sbin:/usr/sbin"
+ # XXX add mac / win extended search paths?
+
+
+def which(program):
+ """
+ an implementation of which
+ that extends the path with
+ other locations, like sbin
+ (f.i., openvpn binary is likely to be there)
+ @param program: a string representing the binary we're looking for.
+ """
+ def is_exe(fpath):
+ """
+ check that path exists,
+ it's a file,
+ and is executable by the owner
+ """
+ # we would check for access,
+ # but it's likely that we're
+ # using uid 0 + polkitd
+
+ return os.path.isfile(fpath)\
+ and is_user_executable(fpath)
+
+ def ext_candidates(fpath):
+ yield fpath
+ for ext in os.environ.get("PATHEXT", "").split(os.pathsep):
+ yield fpath + ext
+
+ def iter_path(pathset):
+ """
+ returns iterator with
+ full path for a given path list
+ and the current target bin.
+ """
+ for path in pathset.split(os.pathsep):
+ exe_file = os.path.join(path, program)
+ #print 'file=%s' % exe_file
+ for candidate in ext_candidates(exe_file):
+ if is_exe(candidate):
+ yield candidate
+
+ fpath, fname = os.path.split(program)
+ if fpath:
+ if is_exe(program):
+ return program
+ else:
+ # extended iterator
+ # with extra path
+ extended_path = chain(
+ iter_path(os.environ["PATH"]),
+ iter_path(extend_path()))
+ for candidate in extended_path:
+ if candidate is not None:
+ return candidate
+
+ # sorry bro.
+ return None
+
+
+def mkdir_p(path):
+ """
+ implements mkdir -p functionality
+ """
+ try:
+ os.makedirs(path)
+ except OSError as exc:
+ if exc.errno == errno.EEXIST:
+ pass
+ else:
+ raise
+
+
+def check_and_fix_urw_only(_file):
+ """
+ test for 600 mode and try
+ to set it if anything different found
+ """
+ mode = stat.S_IMODE(
+ os.stat(_file).st_mode)
+
+ if mode != int('600', 8):
+ try:
+ logger.warning(
+ 'bad permission on %s '
+ 'attempting to set 600',
+ _file)
+ os.chmod(_file, stat.S_IRUSR | stat.S_IWUSR)
+ except OSError:
+ logger.error(
+ 'error while trying to chmod 600 %s',
+ _file)
+ raise
diff --git a/src/leap/utils/leap_argparse.py b/src/leap/util/leap_argparse.py
index 9c355134..9c355134 100644
--- a/src/leap/utils/leap_argparse.py
+++ b/src/leap/util/leap_argparse.py
diff --git a/src/leap/util/test_fileutil.py b/src/leap/util/test_fileutil.py
new file mode 100644
index 00000000..849decaf
--- /dev/null
+++ b/src/leap/util/test_fileutil.py
@@ -0,0 +1,99 @@
+import os
+import platform
+import shutil
+import stat
+import tempfile
+import unittest
+
+from leap.util import fileutil
+
+
+class FileUtilTest(unittest.TestCase):
+ """
+ test our file utils
+ """
+
+ def setUp(self):
+ self.system = platform.system()
+ self.create_temp_dir()
+
+ def tearDown(self):
+ self.remove_temp_dir()
+
+ #
+ # helpers
+ #
+
+ def create_temp_dir(self):
+ self.tmpdir = tempfile.mkdtemp()
+
+ def remove_temp_dir(self):
+ shutil.rmtree(self.tmpdir)
+
+ def get_file_path(self, filename):
+ return os.path.join(
+ self.tmpdir,
+ filename)
+
+ def touch_exec_file(self):
+ fp = self.get_file_path('testexec')
+ open(fp, 'w').close()
+ os.chmod(
+ fp,
+ stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+ return fp
+
+ def get_mode(self, fp):
+ return stat.S_IMODE(os.stat(fp).st_mode)
+
+ #
+ # tests
+ #
+
+ def test_is_user_executable(self):
+ """
+ test that a 700 file
+ is an 700 file. kindda oximoronic, but...
+ """
+ # XXX could check access X_OK
+
+ fp = self.touch_exec_file()
+ mode = self.get_mode(fp)
+ self.assertEqual(mode, int('700', 8))
+
+ def test_which(self):
+ """
+ not a very reliable test,
+ but I cannot think of anything smarter now
+ I guess it's highly improbable that copy
+ command is somewhere else..?
+ """
+ # XXX yep, we can change the syspath
+ # for the test... !
+
+ if self.system == "Linux":
+ self.assertEqual(
+ fileutil.which('cp'),
+ '/bin/cp')
+
+ def test_mkdir_p(self):
+ """
+ test our mkdir -p implementation
+ """
+ testdir = self.get_file_path(
+ os.path.join('test', 'foo', 'bar'))
+ self.assertEqual(os.path.isdir(testdir), False)
+ fileutil.mkdir_p(testdir)
+ self.assertEqual(os.path.isdir(testdir), True)
+
+ def test_check_and_fix_urw_only(self):
+ """
+ test function that fixes perms on
+ files that should be rw only for owner
+ """
+ fp = self.touch_exec_file()
+ mode = self.get_mode(fp)
+ self.assertEqual(mode, int('700', 8))
+ fileutil.check_and_fix_urw_only(fp)
+ mode = self.get_mode(fp)
+ self.assertEqual(mode, int('600', 8))
diff --git a/src/leap/util/test_leap_argparse.py b/src/leap/util/test_leap_argparse.py
new file mode 100644
index 00000000..1442e827
--- /dev/null
+++ b/src/leap/util/test_leap_argparse.py
@@ -0,0 +1,27 @@
+from argparse import Namespace
+import unittest
+
+from leap.util import leap_argparse
+
+
+class LeapArgParseTest(unittest.TestCase):
+ """
+ Test argparse options for eip client
+ """
+
+ def setUp(self):
+ """
+ get the parser
+ """
+ self.parser = leap_argparse.build_parser()
+
+ def test_debug_mode(self):
+ """
+ test debug mode option
+ """
+ opts = self.parser.parse_args(
+ ['--debug'])
+ self.assertEqual(
+ opts,
+ Namespace(config_file=None,
+ debug=True))