diff options
Diffstat (limited to 'src/leap')
-rw-r--r-- | src/leap/__init__.py | 5 | ||||
-rw-r--r-- | src/leap/app.py | 6 | ||||
-rw-r--r-- | src/leap/baseapp/config.py | 40 | ||||
-rw-r--r-- | src/leap/baseapp/dialogs.py | 33 | ||||
-rw-r--r-- | src/leap/baseapp/mainwindow.py | 87 | ||||
-rw-r--r-- | src/leap/baseapp/permcheck.py | 17 | ||||
-rw-r--r-- | src/leap/eip/conductor.py | 166 | ||||
-rw-r--r-- | src/leap/eip/config.py | 426 | ||||
-rw-r--r-- | src/leap/eip/vpnmanager.py | 3 | ||||
-rw-r--r-- | src/leap/gui/mainwindow_rc.py | 2 | ||||
-rw-r--r-- | src/leap/gui/test_mainwindow_rc.py | 26 | ||||
-rw-r--r-- | src/leap/tests/fakeclient.py | 63 | ||||
-rw-r--r-- | src/leap/tests/mocks/__init__.py | 1 | ||||
-rw-r--r-- | src/leap/tests/mocks/manager.py | 20 | ||||
-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.py | 113 | ||||
-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.py | 99 | ||||
-rw-r--r-- | src/leap/util/test_leap_argparse.py | 27 |
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)) |