diff options
Diffstat (limited to 'src/leap/baseapp')
-rw-r--r-- | src/leap/baseapp/config.py | 40 | ||||
-rw-r--r-- | src/leap/baseapp/constants.py | 6 | ||||
-rw-r--r-- | src/leap/baseapp/dialogs.py | 61 | ||||
-rw-r--r-- | src/leap/baseapp/eip.py | 243 | ||||
-rw-r--r-- | src/leap/baseapp/leap_app.py | 153 | ||||
-rw-r--r-- | src/leap/baseapp/log.py | 69 | ||||
-rw-r--r-- | src/leap/baseapp/mainwindow.py | 529 | ||||
-rw-r--r-- | src/leap/baseapp/network.py | 63 | ||||
-rw-r--r-- | src/leap/baseapp/permcheck.py | 17 | ||||
-rw-r--r-- | src/leap/baseapp/systray.py | 268 |
10 files changed, 1041 insertions, 408 deletions
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/constants.py b/src/leap/baseapp/constants.py new file mode 100644 index 00000000..e312be21 --- /dev/null +++ b/src/leap/baseapp/constants.py @@ -0,0 +1,6 @@ +# This timer used for polling vpn manager state. + +# XXX what is an optimum polling interval? +# too little will be overkill, too much will +# miss transition states. +TIMER_MILLISECONDS = 250.0 diff --git a/src/leap/baseapp/dialogs.py b/src/leap/baseapp/dialogs.py new file mode 100644 index 00000000..d256fc99 --- /dev/null +++ b/src/leap/baseapp/dialogs.py @@ -0,0 +1,61 @@ +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 +import logging + +from PyQt4.QtGui import (QDialog, QFrame, QPushButton, QLabel, QMessageBox) + +logger = logging.getLogger(name=__name__) + + +class ErrorDialog(QDialog): + def __init__(self, parent=None, errtype=None, msg=None, label=None): + super(ErrorDialog, self).__init__(parent) + frameStyle = QFrame.Sunken | QFrame.Panel + self.warningLabel = QLabel() + self.warningLabel.setFrameStyle(frameStyle) + self.warningButton = QPushButton("QMessageBox.&warning()") + + if msg is not None: + self.msg = msg + if label is not None: + self.label = label + if errtype == "critical": + self.criticalMessage(self.msg, self.label) + + def warningMessage(self, msg, label): + msgBox = QMessageBox(QMessageBox.Warning, + "LEAP Client Error", + msg, + QMessageBox.NoButton, self) + msgBox.addButton("&Ok", QMessageBox.AcceptRole) + if msgBox.exec_() == QMessageBox.AcceptRole: + pass + # do whatever we want to do after + # closing the dialog. we can pass that + # in the constructor + + def criticalMessage(self, msg, label): + msgBox = QMessageBox(QMessageBox.Critical, + "LEAP Client Error", + msg, + QMessageBox.NoButton, self) + msgBox.addButton("&Ok", QMessageBox.AcceptRole) + msgBox.exec_() + + # It's critical, so we exit. + # We should better emit a signal and connect it + # with the proper shutdownAndQuit method, but + # this suffices for now. + logger.info('Quitting') + import sys + sys.exit() + + def confirmMessage(self, msg, label, action): + msgBox = QMessageBox(QMessageBox.Critical, + self.tr("LEAP Client Error"), + msg, + QMessageBox.NoButton, self) + msgBox.addButton("&Ok", QMessageBox.AcceptRole) + msgBox.addButton("&Cancel", QMessageBox.RejectRole) + + if msgBox.exec_() == QMessageBox.AcceptRole: + action() diff --git a/src/leap/baseapp/eip.py b/src/leap/baseapp/eip.py new file mode 100644 index 00000000..b34cc82e --- /dev/null +++ b/src/leap/baseapp/eip.py @@ -0,0 +1,243 @@ +from __future__ import print_function +import logging +import time +#import sys + +from PyQt4 import QtCore + +from leap.baseapp.dialogs import ErrorDialog +from leap.baseapp import constants +from leap.eip import exceptions as eip_exceptions +from leap.eip.eipconnection import EIPConnection +from leap.base.checks import EVENT_CONNECT_REFUSED +from leap.util import geo + +logger = logging.getLogger(name=__name__) + + +class EIPConductorAppMixin(object): + """ + initializes an instance of EIPConnection, + gathers errors, and passes status-change signals + from Qt land along to the conductor. + Connects the eip connect/disconnect logic + to the switches in the app (buttons/menu items). + """ + ERR_DIALOG = False + + def __init__(self, *args, **kwargs): + opts = kwargs.pop('opts') + config_file = getattr(opts, 'config_file', None) + provider = kwargs.pop('provider') + + self.eip_service_started = False + + # conductor (eip connection) is in charge of all + # vpn-related configuration / monitoring. + # we pass a tuple of signals that will be + # triggered when status changes. + + self.conductor = EIPConnection( + watcher_cb=self.newLogLine.emit, + config_file=config_file, + checker_signals=(self.eipStatusChange.emit, ), + status_signals=(self.openvpnStatusChange.emit, ), + debug=self.debugmode, + ovpn_verbosity=opts.openvpn_verb, + provider=provider) + + # Do we want to enable the skip checks w/o being + # in debug mode?? + #self.skip_download = opts.no_provider_checks + #self.skip_verify = opts.no_ca_verify + self.skip_download = False + self.skip_verify = False + + def run_eip_checks(self): + """ + runs eip checks and + the error checking loop + """ + logger.debug('running EIP CHECKS') + self.conductor.run_checks( + skip_download=self.skip_download, + skip_verify=self.skip_verify) + self.error_check() + + self.start_eipconnection.emit() + + def error_check(self): + """ + consumes the conductor error queue. + pops errors, and acts accordingly (launching user dialogs). + """ + logger.debug('error check') + + errq = self.conductor.error_queue + while errq.qsize() != 0: + logger.debug('%s errors left in conductor queue', errq.qsize()) + # we get exception and original traceback from queue + error, tb = errq.get() + + # redundant log, debugging the loop. + logger.error('%s: %s', error.__class__.__name__, error.message) + + if issubclass(error.__class__, eip_exceptions.EIPClientError): + self.triggerEIPError.emit(error) + + else: + # deprecated form of raising exception. + raise error, None, tb + + if error.failfirst is True: + break + + @QtCore.pyqtSlot(object) + def onEIPError(self, error): + """ + check severity and launches + dialogs informing user about the errors. + in the future we plan to derive errors to + our log viewer. + """ + if self.ERR_DIALOG: + logger.warning('another error dialog suppressed') + return + + # XXX this is actually a one-shot. + # On the dialog there should be + # a reset signal binded to the ok button + # or something like that. + self.ERR_DIALOG = True + + if getattr(error, 'usermessage', None): + message = error.usermessage + else: + message = error.message + + # XXX + # check headless = False before + # launching dialog. + # (so Qt tests can assert stuff) + + if error.critical: + logger.critical(error.message) + #critical error (non recoverable), + #we give user some info and quit. + #(critical error dialog will exit app) + ErrorDialog(errtype="critical", + msg=message, + label="critical error") + + elif error.warning: + logger.warning(error.message) + + else: + dialog = ErrorDialog() + dialog.warningMessage(message, 'error') + + @QtCore.pyqtSlot() + def statusUpdate(self): + """ + polls status and updates ui with real time + info about transferred bytes / connection state. + right now is triggered by a timer tick + (timer controlled by StatusAwareTrayIcon class) + """ + # TODO I guess it's too expensive to poll + # continously. move to signal events instead. + # (i.e., subscribe to connection status changes + # from openvpn manager) + + if not self.eip_service_started: + # there is a race condition + # going on here. Depending on how long we take + # to init the qt app, the management socket + # is not ready yet. + return + + #if self.conductor.with_errors: + #XXX how to wait on pkexec??? + #something better that this workaround, plz!! + #I removed the pkexec pass authentication at all. + #time.sleep(5) + #logger.debug('timeout') + #logger.error('errors. disconnect') + #self.start_or_stopVPN() # is stop + + state = self.conductor.poll_connection_state() + if not state: + return + + ts, con_status, ok, ip, remote = state + self.set_statusbarMessage(con_status) + self.setIconToolTip() + + ts = time.strftime("%a %b %d %X", ts) + if self.debugmode: + self.updateTS.setText(ts) + self.status_label.setText(con_status) + self.ip_label.setText(ip) + self.remote_label.setText(remote) + self.remote_country.setText( + geo.get_country_name(remote)) + + # status i/o + + status = self.conductor.get_status_io() + if status and self.debugmode: + #XXX move this to systray menu indicators + ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read) = status + ts = time.strftime("%a %b %d %X", ts) + self.updateTS.setText(ts) + self.tun_read_bytes.setText(tun_read) + self.tun_write_bytes.setText(tun_write) + + # connection information via management interface + log = self.conductor.get_log() + error_matrix = [(EVENT_CONNECT_REFUSED, (self.start_or_stopVPN, ))] + if hasattr(self.network_checker, 'checker'): + self.network_checker.checker.parse_log_and_react(log, error_matrix) + + @QtCore.pyqtSlot() + def start_or_stopVPN(self, **kwargs): + """ + stub for running child process with vpn + """ + if self.conductor.has_errors(): + logger.debug('not starting vpn; conductor has errors') + return + + if self.eip_service_started is False: + try: + self.conductor.connect() + + except eip_exceptions.EIPNoCommandError as exc: + logger.error('tried to run openvpn but no command is set') + self.triggerEIPError.emit(exc) + + except Exception as err: + # raise generic exception (Bad Thing Happened?) + logger.exception(err) + else: + # no errors, so go on. + if self.debugmode: + self.startStopButton.setText(self.tr('&Disconnect')) + self.eip_service_started = True + self.toggleEIPAct() + + # XXX decouple! (timer is init by icons class). + # we could bring Timer Init to this Mixin + # or to its own Mixin. + self.timer.start(constants.TIMER_MILLISECONDS) + return + + if self.eip_service_started is True: + self.network_checker.stop() + self.conductor.disconnect() + if self.debugmode: + self.startStopButton.setText(self.tr('&Connect')) + self.eip_service_started = False + self.toggleEIPAct() + self.timer.stop() + return diff --git a/src/leap/baseapp/leap_app.py b/src/leap/baseapp/leap_app.py new file mode 100644 index 00000000..4d3aebd6 --- /dev/null +++ b/src/leap/baseapp/leap_app.py @@ -0,0 +1,153 @@ +import logging + +import sip +sip.setapi('QVariant', 2) + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.gui import mainwindow_rc + +logger = logging.getLogger(name=__name__) + + +APP_LOGO = ':/images/leap-color-small.png' + + +class MainWindowMixin(object): + """ + create the main window + for leap app + """ + + def __init__(self, *args, **kwargs): + # XXX set initial visibility + # debug = no visible + + widget = QtGui.QWidget() + self.setCentralWidget(widget) + + mainLayout = QtGui.QVBoxLayout() + # add widgets to layout + #self.createWindowHeader() + #mainLayout.addWidget(self.headerBox) + + # created in systray + mainLayout.addWidget(self.statusIconBox) + if self.debugmode: + mainLayout.addWidget(self.statusBox) + mainLayout.addWidget(self.loggerBox) + widget.setLayout(mainLayout) + + self.createMainActions() + self.createMainMenus() + + self.setWindowTitle("LEAP Client") + self.set_app_icon() + self.set_statusbarMessage('ready') + + def createMainActions(self): + #self.openAct = QtGui.QAction("&Open...", self, shortcut="Ctrl+O", + #triggered=self.open) + + self.firstRunWizardAct = QtGui.QAction( + "&First run wizard...", self, + triggered=self.stop_connection_and_launch_first_run_wizard) + self.aboutAct = QtGui.QAction("&About", self, triggered=self.about) + + #self.aboutQtAct = QtGui.QAction("About &Qt", self, + #triggered=QtGui.qApp.aboutQt) + + def createMainMenus(self): + self.connMenu = QtGui.QMenu("&Connections", self) + #self.viewMenu.addSeparator() + self.connMenu.addAction(self.quitAction) + + self.settingsMenu = QtGui.QMenu("&Settings", self) + self.settingsMenu.addAction(self.firstRunWizardAct) + + self.helpMenu = QtGui.QMenu("&Help", self) + self.helpMenu.addAction(self.aboutAct) + #self.helpMenu.addAction(self.aboutQtAct) + + self.menuBar().addMenu(self.connMenu) + self.menuBar().addMenu(self.settingsMenu) + self.menuBar().addMenu(self.helpMenu) + + def stop_connection_and_launch_first_run_wizard(self): + settings = QtCore.QSettings() + settings.setValue('FirstRunWizardDone', False) + logger.debug('should run first run wizard again...') + + status = self.conductor.get_icon_name() + if status != "disconnected": + self.start_or_stopVPN() + + self.launch_first_run_wizard() + #from leap.gui.firstrunwizard import FirstRunWizard + #wizard = FirstRunWizard( + #parent=self, + #success_cb=self.initReady.emit) + #wizard.show() + + def set_app_icon(self): + icon = QtGui.QIcon(APP_LOGO) + self.setWindowIcon(icon) + + #def createWindowHeader(self): + #""" + #description lines for main window + #""" + #self.headerBox = QtGui.QGroupBox() + #self.headerLabel = QtGui.QLabel( + #"<font size=40>LEAP Encryption Access Project</font>") + #self.headerLabelSub = QtGui.QLabel( + #"<br><i>your internet encryption toolkit</i>") +# + #pixmap = QtGui.QPixmap(APP_LOGO) + #leap_lbl = QtGui.QLabel() + #leap_lbl.setPixmap(pixmap) +# + #headerLayout = QtGui.QHBoxLayout() + #headerLayout.addWidget(leap_lbl) + #headerLayout.addWidget(self.headerLabel) + #headerLayout.addWidget(self.headerLabelSub) + #headerLayout.addStretch() + #self.headerBox.setLayout(headerLayout) + + def set_statusbarMessage(self, msg): + self.statusBar().showMessage(msg) + + def closeEvent(self, event): + """ + redefines close event (persistent window behaviour) + """ + if self.trayIcon.isVisible() and not self.debugmode: + QtGui.QMessageBox.information( + self, "Systray", + "The program will keep running " + "in the system tray. To " + "terminate the program, choose " + "<b>Quit</b> in the " + "context menu of the system tray entry.") + self.hide() + event.ignore() + return + self.cleanupAndQuit() + + def cleanupAndQuit(self): + """ + cleans state before shutting down app. + """ + # save geometry for restoring + settings = QtCore.QSettings() + geom_key = "DebugGeometry" if self.debugmode else "Geometry" + settings.setValue(geom_key, self.saveGeometry()) + + # TODO:make sure to shutdown all child process / threads + # in conductor + # XXX send signal instead? + logger.info('Shutting down') + self.conductor.disconnect(shutdown=True) + logger.info('Exiting. Bye.') + QtGui.qApp.quit() diff --git a/src/leap/baseapp/log.py b/src/leap/baseapp/log.py new file mode 100644 index 00000000..636e5bae --- /dev/null +++ b/src/leap/baseapp/log.py @@ -0,0 +1,69 @@ +import logging + +from PyQt4 import QtGui +from PyQt4 import QtCore + +vpnlogger = logging.getLogger('leap.openvpn') + + +class LogPaneMixin(object): + """ + a simple log pane + that writes new lines as they come + """ + EXCLUDES = ('MANAGEMENT',) + + def createLogBrowser(self): + """ + creates Browser widget for displaying logs + (in debug mode only). + """ + self.loggerBox = QtGui.QGroupBox() + logging_layout = QtGui.QVBoxLayout() + self.logbrowser = QtGui.QTextBrowser() + + startStopButton = QtGui.QPushButton(self.tr("&Connect")) + self.startStopButton = startStopButton + + logging_layout.addWidget(self.logbrowser) + logging_layout.addWidget(self.startStopButton) + self.loggerBox.setLayout(logging_layout) + + # status box + + self.statusBox = QtGui.QGroupBox() + grid = QtGui.QGridLayout() + + self.updateTS = QtGui.QLabel('') + self.status_label = QtGui.QLabel(self.tr('Disconnected')) + self.ip_label = QtGui.QLabel('') + self.remote_label = QtGui.QLabel('') + self.remote_country = QtGui.QLabel('') + + tun_read_label = QtGui.QLabel("tun read") + self.tun_read_bytes = QtGui.QLabel("0") + tun_write_label = QtGui.QLabel("tun write") + self.tun_write_bytes = QtGui.QLabel("0") + + grid.addWidget(self.updateTS, 0, 0) + grid.addWidget(self.status_label, 0, 1) + grid.addWidget(self.ip_label, 1, 0) + grid.addWidget(self.remote_label, 1, 1) + grid.addWidget(self.remote_country, 2, 1) + grid.addWidget(tun_read_label, 3, 0) + grid.addWidget(self.tun_read_bytes, 3, 1) + grid.addWidget(tun_write_label, 4, 0) + grid.addWidget(self.tun_write_bytes, 4, 1) + + self.statusBox.setLayout(grid) + + @QtCore.pyqtSlot(str) + def onLoggerNewLine(self, line): + """ + simple slot: writes new line to logger Pane. + """ + msg = line[:-1] + if self.debugmode and all(map(lambda w: w not in msg, + LogPaneMixin.EXCLUDES)): + self.logbrowser.append(msg) + vpnlogger.info(msg) diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py index 68b6de8f..91b0dc61 100644 --- a/src/leap/baseapp/mainwindow.py +++ b/src/leap/baseapp/mainwindow.py @@ -1,398 +1,191 @@ # vim: set fileencoding=utf-8 : #!/usr/bin/env python import logging -import time -logger = logging.getLogger(name=__name__) -from PyQt4.QtGui import (QMainWindow, QWidget, QVBoxLayout, QMessageBox, - QSystemTrayIcon, QGroupBox, QLabel, QPixmap, - QHBoxLayout, QIcon, - QPushButton, QGridLayout, QAction, QMenu, - QTextBrowser, qApp) -from PyQt4.QtCore import (pyqtSlot, pyqtSignal, QTimer) +import sip +sip.setapi('QString', 2) +sip.setapi('QVariant', 2) + +from PyQt4 import QtCore +from PyQt4 import QtGui -from leap.gui import mainwindow_rc -from leap.eip.conductor import EIPConductor +from leap.baseapp.eip import EIPConductorAppMixin +from leap.baseapp.log import LogPaneMixin +from leap.baseapp.systray import StatusAwareTrayIconMixin +from leap.baseapp.network import NetworkCheckerAppMixin +from leap.baseapp.leap_app import MainWindowMixin +from leap.eip.checks import ProviderCertChecker +from leap.gui.threads import FunThread +logger = logging.getLogger(name=__name__) -class LeapWindow(QMainWindow): - #XXX tbd: refactor into model / view / controller - #and put in its own modules... - newLogLine = pyqtSignal([str]) - statusChange = pyqtSignal([object]) +class LeapWindow(QtGui.QMainWindow, + MainWindowMixin, EIPConductorAppMixin, + StatusAwareTrayIconMixin, + NetworkCheckerAppMixin, + LogPaneMixin): + """ + main window for the leap app. + Initializes all of its base classes + We keep here some signal initialization + that gets tricky otherwise. + """ + + # signals + + newLogLine = QtCore.pyqtSignal([str]) + mainappReady = QtCore.pyqtSignal([]) + initReady = QtCore.pyqtSignal([]) + networkError = QtCore.pyqtSignal([object]) + triggerEIPError = QtCore.pyqtSignal([object]) + start_eipconnection = QtCore.pyqtSignal([]) + shutdownSignal = QtCore.pyqtSignal([]) + initNetworkChecker = QtCore.pyqtSignal([]) + + # this is status change got from openvpn management + openvpnStatusChange = QtCore.pyqtSignal([object]) + # this is global eip status + eipStatusChange = QtCore.pyqtSignal([str]) def __init__(self, opts): - super(LeapWindow, self).__init__() + logger.debug('init leap window') self.debugmode = getattr(opts, 'debug', False) - - self.vpn_service_started = False - - self.createWindowHeader() - self.createIconGroupBox() - - self.createActions() - self.createTrayIcon() + super(LeapWindow, self).__init__() if self.debugmode: self.createLogBrowser() - # create timer - self.timer = QTimer() + settings = QtCore.QSettings() + self.provider_domain = settings.value("provider_domain", None) + self.username = settings.value("username", None) - # bind signals + logger.debug('provider: %s', self.provider_domain) + logger.debug('username: %s', self.username) - self.trayIcon.activated.connect(self.iconActivated) - self.newLogLine.connect(self.onLoggerNewLine) - self.statusChange.connect(self.onStatusChange) - self.timer.timeout.connect(self.onTimerTick) + provider = self.provider_domain + EIPConductorAppMixin.__init__( + self, opts=opts, provider=provider) + StatusAwareTrayIconMixin.__init__(self) - widget = QWidget() - self.setCentralWidget(widget) + # XXX network checker should probably not + # trigger run_checks on init... but wait + # for ready signal instead... + NetworkCheckerAppMixin.__init__(self, provider=provider) + MainWindowMixin.__init__(self) - # add widgets to layout - mainLayout = QVBoxLayout() - mainLayout.addWidget(self.headerBox) - mainLayout.addWidget(self.statusIconBox) - if self.debugmode: - mainLayout.addWidget(self.statusBox) - mainLayout.addWidget(self.loggerBox) - widget.setLayout(mainLayout) + geom_key = "DebugGeometry" if self.debugmode else "Geometry" + geom = settings.value(geom_key) + if geom: + self.restoreGeometry(geom) - # - # 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, )) + # XXX check for wizard + self.wizard_done = settings.value("FirstRunWizardDone") - self.trayIcon.show() + self.initchecks = FunThread(self.run_eip_checks) - self.setWindowTitle("Leap") - self.resize(400, 300) - - self.set_statusbarMessage('ready') - - if self.conductor.autostart: - self.start_or_stopVPN() + # bind signals + self.initchecks.finished.connect( + lambda: logger.debug('Initial checks thread finished')) + self.trayIcon.activated.connect(self.iconActivated) + self.newLogLine.connect( + lambda line: self.onLoggerNewLine(line)) + self.timer.timeout.connect( + lambda: self.onTimerTick()) + self.networkError.connect( + lambda exc: self.onNetworkError(exc)) + self.triggerEIPError.connect( + lambda exc: self.onEIPError(exc)) - def closeEvent(self, event): - """ - redefines close event (persistent window behaviour) - """ - if self.trayIcon.isVisible() and not self.debugmode: - QMessageBox.information(self, "Systray", - "The program will keep running " - "in the system tray. To " - "terminate the program, choose " - "<b>Quit</b> in the " - "context menu of the system tray entry.") - self.hide() - event.ignore() if self.debugmode: + self.startStopButton.clicked.connect( + lambda: self.start_or_stopVPN()) + self.start_eipconnection.connect( + self.do_start_eipconnection) + self.shutdownSignal.connect( + self.cleanupAndQuit) + self.initNetworkChecker.connect( + lambda: self.init_network_checker(self.conductor.provider)) + + # status change. + # TODO unify + self.openvpnStatusChange.connect( + lambda status: self.onOpenVPNStatusChange(status)) + self.eipStatusChange.connect( + lambda newstatus: self.onEIPConnStatusChange(newstatus)) + self.eipStatusChange.connect( + lambda newstatus: self.toggleEIPAct()) + + # do first run wizard and init signals + self.mainappReady.connect(self.do_first_run_wizard_check) + self.initReady.connect(self.runchecks_and_eipconnect) + + # ... all ready. go! + # connected to do_first_run_wizard_check + self.mainappReady.emit() + + def do_first_run_wizard_check(self): + """ + checks whether first run wizard needs to be run + launches it if needed + and emits initReady signal if not. + """ + + logger.debug('first run wizard check...') + need_wizard = False + + # do checks (can overlap if wizard was interrupted) + if not self.wizard_done: + need_wizard = True + + if not self.provider_domain: + need_wizard = True + else: + pcertchecker = ProviderCertChecker(domain=self.provider_domain) + if not pcertchecker.is_cert_valid(do_raise=False): + logger.warning('missing valid client cert. need wizard') + need_wizard = True + + # launch wizard if needed + if need_wizard: + logger.debug('running first run wizard') + self.launch_first_run_wizard() + else: # no wizard needed + self.initReady.emit() + + def launch_first_run_wizard(self): + """ + launches wizard and blocks + """ + from leap.gui.firstrun.wizard import FirstRunWizard + wizard = FirstRunWizard( + self.conductor, + parent=self, + username=self.username, + start_eipconnection_signal=self.start_eipconnection, + eip_statuschange_signal=self.eipStatusChange, + quitcallback=self.onWizardCancel) + wizard.show() + + def onWizardCancel(self): + if not self.wizard_done: + logger.debug( + 'clicked on Cancel during first ' + 'run wizard. shutting down') self.cleanupAndQuit() - def setIcon(self, name): - icon = self.Icons.get(name) - self.trayIcon.setIcon(icon) - self.setWindowIcon(icon) - - def setToolTip(self): - """ - get readable status and place it on systray tooltip - """ - status = self.conductor.status.get_readable_status() - self.trayIcon.setToolTip(status) - - def iconActivated(self, reason): + def runchecks_and_eipconnect(self): """ - handles left click, left double click - showing the trayicon menu + shows icon and run init checks """ - #XXX there's a bug here! - #menu shows on (0,0) corner first time, - #until double clicked at least once. - if reason in (QSystemTrayIcon.Trigger, - QSystemTrayIcon.DoubleClick): - self.trayIconMenu.show() - - def createWindowHeader(self): - """ - description lines for main window - """ - #XXX good candidate to refactor out! :) - self.headerBox = QGroupBox() - self.headerLabel = QLabel("<font size=40><b>E</b>ncryption \ -<b>I</b>nternet <b>P</b>roxy</font>") - self.headerLabelSub = QLabel("<i>trust your \ -technolust</i>") - - pixmap = QPixmap(':/images/leapfrog.jpg') - frog_lbl = QLabel() - frog_lbl.setPixmap(pixmap) - - headerLayout = QHBoxLayout() - headerLayout.addWidget(frog_lbl) - headerLayout.addWidget(self.headerLabel) - headerLayout.addWidget(self.headerLabelSub) - headerLayout.addStretch() - self.headerBox.setLayout(headerLayout) - - def getIcon(self, icon_name): - # XXX get from connection dict - icons = {'disconnected': 0, - 'connecting': 1, - 'connected': 2} - return icons.get(icon_name, None) - - def createIconGroupBox(self): - """ - dummy icongroupbox - (to be removed from here -- reference only) - """ - icons = { - 'disconnected': ':/images/conn_error.png', - 'connecting': ':/images/conn_connecting.png', - 'connected': ':/images/conn_connected.png' - } - con_widgets = { - 'disconnected': QLabel(), - 'connecting': QLabel(), - 'connected': QLabel(), - } - con_widgets['disconnected'].setPixmap( - QPixmap(icons['disconnected'])) - con_widgets['connecting'].setPixmap( - QPixmap(icons['connecting'])) - con_widgets['connected'].setPixmap( - QPixmap(icons['connected'])), - self.ConnectionWidgets = con_widgets - - con_icons = { - 'disconnected': QIcon(icons['disconnected']), - 'connecting': QIcon(icons['connecting']), - 'connected': QIcon(icons['connected']) - } - self.Icons = con_icons - - self.statusIconBox = QGroupBox("Connection Status") - statusIconLayout = QHBoxLayout() - statusIconLayout.addWidget(self.ConnectionWidgets['disconnected']) - statusIconLayout.addWidget(self.ConnectionWidgets['connecting']) - statusIconLayout.addWidget(self.ConnectionWidgets['connected']) - statusIconLayout.itemAt(1).widget().hide() - statusIconLayout.itemAt(2).widget().hide() - self.statusIconBox.setLayout(statusIconLayout) - - def createActions(self): - """ - creates actions to be binded to tray icon - """ - self.connectVPNAction = QAction("Connect to &VPN", self, - triggered=self.hide) - # XXX change action name on (dis)connect - self.dis_connectAction = QAction("&(Dis)connect", self, - triggered=self.start_or_stopVPN) - self.minimizeAction = QAction("Mi&nimize", self, - triggered=self.hide) - self.maximizeAction = QAction("Ma&ximize", self, - triggered=self.showMaximized) - self.restoreAction = QAction("&Restore", self, - triggered=self.showNormal) - self.quitAction = QAction("&Quit", self, - triggered=self.cleanupAndQuit) - - def createTrayIcon(self): - """ - creates the tray icon - """ - self.trayIconMenu = QMenu(self) - - self.trayIconMenu.addAction(self.connectVPNAction) - self.trayIconMenu.addAction(self.dis_connectAction) - self.trayIconMenu.addSeparator() - self.trayIconMenu.addAction(self.minimizeAction) - self.trayIconMenu.addAction(self.maximizeAction) - self.trayIconMenu.addAction(self.restoreAction) - self.trayIconMenu.addSeparator() - self.trayIconMenu.addAction(self.quitAction) - - self.trayIcon = QSystemTrayIcon(self) - self.trayIcon.setContextMenu(self.trayIconMenu) - - def createLogBrowser(self): - """ - creates Browser widget for displaying logs - (in debug mode only). - """ - self.loggerBox = QGroupBox() - logging_layout = QVBoxLayout() - self.logbrowser = QTextBrowser() - - startStopButton = QPushButton("&Connect") - startStopButton.clicked.connect(self.start_or_stopVPN) - self.startStopButton = startStopButton - - logging_layout.addWidget(self.logbrowser) - logging_layout.addWidget(self.startStopButton) - self.loggerBox.setLayout(logging_layout) - - # status box - - self.statusBox = QGroupBox() - grid = QGridLayout() - - self.updateTS = QLabel('') - self.status_label = QLabel('Disconnected') - self.ip_label = QLabel('') - self.remote_label = QLabel('') - - tun_read_label = QLabel("tun read") - self.tun_read_bytes = QLabel("0") - tun_write_label = QLabel("tun write") - self.tun_write_bytes = QLabel("0") - - grid.addWidget(self.updateTS, 0, 0) - grid.addWidget(self.status_label, 0, 1) - grid.addWidget(self.ip_label, 1, 0) - grid.addWidget(self.remote_label, 1, 1) - grid.addWidget(tun_read_label, 2, 0) - grid.addWidget(self.tun_read_bytes, 2, 1) - grid.addWidget(tun_write_label, 3, 0) - grid.addWidget(self.tun_write_bytes, 3, 1) - - self.statusBox.setLayout(grid) - - @pyqtSlot(str) - def onLoggerNewLine(self, line): - """ - simple slot: writes new line to logger Pane. - """ - if self.debugmode: - self.logbrowser.append(line[:-1]) - - def set_statusbarMessage(self, msg): - self.statusBar().showMessage(msg) - - @pyqtSlot(object) - def onStatusChange(self, status): - """ - slot for status changes. triggers new signals for - updating icon, status bar, etc. - """ - - 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 - - # change connection pixmap widget - self.setConnWidget(icon_name) - - def setConnWidget(self, icon_name): - #print 'changing icon to %s' % icon_name - oldlayout = self.statusIconBox.layout() - - # XXX reuse with icons - # XXX move states to StateWidget - states = {"disconnected": 0, - "connecting": 1, - "connected": 2} - - for i in range(3): - oldlayout.itemAt(i).widget().hide() - new = states[icon_name] - oldlayout.itemAt(new).widget().show() - - @pyqtSlot() - def start_or_stopVPN(self): - """ - stub for running child process with vpn - """ - if self.vpn_service_started is False: - self.conductor.connect() - if self.debugmode: - self.startStopButton.setText('&Disconnect') - self.vpn_service_started = True - - # XXX what is optimum polling interval? - # too little is overkill, too much - # will miss transition states.. - - self.timer.start(250.0) - return - if self.vpn_service_started is True: - self.conductor.disconnect() - # FIXME this should trigger also - # statuschange event. why isn't working?? - if self.debugmode: - self.startStopButton.setText('&Connect') - self.vpn_service_started = False - self.timer.stop() - return - - @pyqtSlot() - def onTimerTick(self): - self.statusUpdate() - - @pyqtSlot() - def statusUpdate(self): - """ - called on timer tick - polls status and updates ui with real time - info about transferred bytes / connection state. - """ - # XXX it's too expensive to poll - # continously. move to signal events instead. - - if not self.vpn_service_started: - return - - # XXX remove all access to manager layer - # from here. - if self.conductor.manager.with_errors: - #XXX how to wait on pkexec??? - #something better that this workaround, plz!! - time.sleep(10) - print('errors. disconnect.') - self.start_or_stopVPN() # is stop - - state = self.conductor.poll_connection_state() - if not state: - return - - ts, con_status, ok, ip, remote = state - self.set_statusbarMessage(con_status) - self.setToolTip() - - ts = time.strftime("%a %b %d %X", ts) - if self.debugmode: - self.updateTS.setText(ts) - self.status_label.setText(con_status) - self.ip_label.setText(ip) - self.remote_label.setText(remote) - - # status i/o - - status = self.conductor.manager.get_status_io() - if status and self.debugmode: - #XXX move this to systray menu indicators - ts, (tun_read, tun_write, tcp_read, tcp_write, auth_read) = status - ts = time.strftime("%a %b %d %X", ts) - self.updateTS.setText(ts) - self.tun_read_bytes.setText(tun_read) - self.tun_write_bytes.setText(tun_write) + self.show_systray_icon() + self.initchecks.begin() - def cleanupAndQuit(self): + def do_start_eipconnection(self): """ - cleans state before shutting down app. + shows icon and init eip connection + called from the end of wizard """ - # TODO:make sure to shutdown all child process / threads - # in conductor - self.conductor.cleanup() - qApp.quit() + self.show_systray_icon() + # this will setup the command + self.conductor.run_openvpn_checks() + self.start_or_stopVPN() diff --git a/src/leap/baseapp/network.py b/src/leap/baseapp/network.py new file mode 100644 index 00000000..dc5182a4 --- /dev/null +++ b/src/leap/baseapp/network.py @@ -0,0 +1,63 @@ +from __future__ import print_function + +import logging + +logger = logging.getLogger(name=__name__) + +from PyQt4 import QtCore + +from leap.baseapp.dialogs import ErrorDialog +from leap.base.network import NetworkCheckerThread + +from leap.util.misc import null_check + + +class NetworkCheckerAppMixin(object): + """ + initialize an instance of the Network Checker, + which gathers error and passes them on. + """ + ERR_NETERR = False + + def __init__(self, *args, **kwargs): + provider = kwargs.pop('provider', None) + self.network_checker = None + if provider: + self.init_network_checker(provider) + + def init_network_checker(self, provider): + null_check(provider, "provider_domain") + if not self.network_checker: + self.network_checker = NetworkCheckerThread( + error_cb=self.networkError.emit, + debug=self.debugmode, + provider=provider) + self.network_checker.start() + + @QtCore.pyqtSlot(object) + def runNetworkChecks(self): + logger.debug('running checks (from NetworkChecker Mixin slot)') + self.network_checker.run_checks() + + @QtCore.pyqtSlot(object) + def onNetworkError(self, exc): + """ + slot that receives a network exceptions + and raises a user error message + """ + # FIXME this should not HANDLE anything after + # the network check thread has been stopped. + + logger.debug('handling network exception') + if not self.ERR_NETERR: + self.ERR_NETERR = True + + logger.error(exc.message) + dialog = ErrorDialog(parent=self) + if exc.critical: + dialog.criticalMessage(exc.usermessage, "network error") + else: + dialog.warningMessage(exc.usermessage, "network error") + + self.start_or_stopVPN() + self.network_checker.stop() 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/baseapp/systray.py b/src/leap/baseapp/systray.py new file mode 100644 index 00000000..77eb3fe9 --- /dev/null +++ b/src/leap/baseapp/systray.py @@ -0,0 +1,268 @@ +import logging +import sys + +import sip +sip.setapi('QString', 2) +sip.setapi('QVariant', 2) + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap import __branding as BRANDING +from leap import __version__ as VERSION + +from leap.gui import mainwindow_rc + +logger = logging.getLogger(__name__) + + +class StatusAwareTrayIconMixin(object): + """ + a mix of several functions needed + to create a systray and make it + get updated from conductor status + polling. + """ + states = { + "disconnected": 0, + "connecting": 1, + "connected": 2} + + iconpath = { + "disconnected": ':/images/conn_error.png', + "connecting": ':/images/conn_connecting.png', + "connected": ':/images/conn_connected.png'} + + Icons = { + 'disconnected': lambda self: QtGui.QIcon( + self.iconpath['disconnected']), + 'connecting': lambda self: QtGui.QIcon( + self.iconpath['connecting']), + 'connected': lambda self: QtGui.QIcon( + self.iconpath['connected']) + } + + def __init__(self, *args, **kwargs): + self.createIconGroupBox() + self.createActions() + self.createTrayIcon() + + # not sure if this really belongs here, but... + self.timer = QtCore.QTimer() + + def show_systray_icon(self): + #logger.debug('showing tray icon................') + self.trayIcon.show() + + def createIconGroupBox(self): + """ + dummy icongroupbox + (to be removed from here -- reference only) + """ + con_widgets = { + 'disconnected': QtGui.QLabel(), + 'connecting': QtGui.QLabel(), + 'connected': QtGui.QLabel(), + } + con_widgets['disconnected'].setPixmap( + QtGui.QPixmap( + self.iconpath['disconnected'])) + con_widgets['connecting'].setPixmap( + QtGui.QPixmap( + self.iconpath['connecting'])) + con_widgets['connected'].setPixmap( + QtGui.QPixmap( + self.iconpath['connected'])), + self.ConnectionWidgets = con_widgets + + self.statusIconBox = QtGui.QGroupBox( + self.tr("EIP Connection Status")) + statusIconLayout = QtGui.QHBoxLayout() + statusIconLayout.addWidget(self.ConnectionWidgets['disconnected']) + statusIconLayout.addWidget(self.ConnectionWidgets['connecting']) + statusIconLayout.addWidget(self.ConnectionWidgets['connected']) + statusIconLayout.itemAt(1).widget().hide() + statusIconLayout.itemAt(2).widget().hide() + + self.leapConnStatus = QtGui.QLabel( + self.tr("<b>disconnected</b>")) + statusIconLayout.addWidget(self.leapConnStatus) + + self.statusIconBox.setLayout(statusIconLayout) + + def createTrayIcon(self): + """ + creates the tray icon + """ + self.trayIconMenu = QtGui.QMenu(self) + + self.trayIconMenu.addAction(self.connAct) + self.trayIconMenu.addSeparator() + self.trayIconMenu.addAction(self.detailsAct) + self.trayIconMenu.addSeparator() + self.trayIconMenu.addAction(self.aboutAct) + # we should get this hidden inside the "about" dialog + # (as a little button maybe) + #self.trayIconMenu.addAction(self.aboutQtAct) + self.trayIconMenu.addSeparator() + self.trayIconMenu.addAction(self.quitAction) + + self.trayIcon = QtGui.QSystemTrayIcon(self) + self.setIcon('disconnected') + self.trayIcon.setContextMenu(self.trayIconMenu) + + #self.trayIconMenu.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + #self.trayIconMenu.customContextMenuRequested.connect( + #self.on_context_menu) + + #def bad(self): + #logger.error('this should not be called') + + def createActions(self): + """ + creates actions to be binded to tray icon + """ + # XXX change action name on (dis)connect + self.connAct = QtGui.QAction( + self.tr("Encryption ON turn &off"), + self, + triggered=lambda: self.start_or_stopVPN()) + + self.detailsAct = QtGui.QAction( + self.tr("&Details..."), + self, + triggered=self.detailsWin) + self.aboutAct = QtGui.QAction( + self.tr("&About"), self, + triggered=self.about) + self.aboutQtAct = QtGui.QAction( + self.tr("About Q&t"), self, + triggered=QtGui.qApp.aboutQt) + self.quitAction = QtGui.QAction( + self.tr("&Quit"), self, + triggered=self.cleanupAndQuit) + + def toggleEIPAct(self): + # this is too simple by now. + # XXX get STATUS CONSTANTS INSTEAD + + icon_status = self.conductor.get_icon_name() + if icon_status == "connected": + self.connAct.setEnabled(True) + self.connAct.setText( + self.tr('Encryption ON turn o&ff')) + return + if icon_status == "disconnected": + self.connAct.setEnabled(True) + self.connAct.setText( + self.tr('Encryption OFF turn &on')) + return + if icon_status == "connecting": + self.connAct.setDisabled(True) + self.connAct.setText(self.tr('connecting...')) + return + + def detailsWin(self): + visible = self.isVisible() + if visible: + self.hide() + else: + self.show() + if sys.platform == "darwin": + self.raise_() + + def about(self): + # move to widget + flavor = BRANDING.get('short_name', None) + content = self.tr( + ("LEAP client<br>" + "(version <b>%s</b>)<br>" % VERSION)) + if flavor: + content = content + ('<br>Flavor: <i>%s</i><br>' % flavor) + content = content + ( + "<br><a href='https://leap.se/'>" + "https://leap.se</a>") + QtGui.QMessageBox.about(self, self.tr("About"), content) + + def setConnWidget(self, icon_name): + oldlayout = self.statusIconBox.layout() + + for i in range(3): + oldlayout.itemAt(i).widget().hide() + new = self.states[icon_name] + oldlayout.itemAt(new).widget().show() + + def setIcon(self, name): + icon_fun = self.Icons.get(name) + if icon_fun and callable(icon_fun): + icon = icon_fun(self) + self.trayIcon.setIcon(icon) + + def getIcon(self, icon_name): + return self.states.get(icon_name, None) + + def setIconToolTip(self): + """ + get readable status and place it on systray tooltip + """ + status = self.conductor.status.get_readable_status() + self.trayIcon.setToolTip(status) + + def iconActivated(self, reason): + """ + handles left click, left double click + showing the trayicon menu + """ + if reason in (QtGui.QSystemTrayIcon.Trigger, + QtGui.QSystemTrayIcon.DoubleClick): + context_menu = self.trayIcon.contextMenu() + # for some reason, context_menu.show() + # is failing in a way beyond my understanding. + # (not working the first time it's clicked). + # this works however. + # XXX in osx it shows some glitches. + context_menu.exec_(self.trayIcon.geometry().center()) + + @QtCore.pyqtSlot() + def onTimerTick(self): + self.statusUpdate() + + @QtCore.pyqtSlot(object) + def onOpenVPNStatusChange(self, status): + """ + updates icon, according to the openvpn status change. + """ + icon_name = self.conductor.get_icon_name() + if not icon_name: + return + + # XXX refactor. Use QStateMachine + + if icon_name in ("disconnected", "connected"): + self.eipStatusChange.emit(icon_name) + + if icon_name in ("connecting"): + # let's see how it matches + leap_status_name = self.conductor.get_leap_status() + self.eipStatusChange.emit(leap_status_name) + + if icon_name == "connected": + # When we change to "connected', we launch + # the network checker. + self.initNetworkChecker.emit() + + self.setIcon(icon_name) + # change connection pixmap widget + self.setConnWidget(icon_name) + + @QtCore.pyqtSlot(str) + def onEIPConnStatusChange(self, newstatus): + """ + slot for EIP status changes + not to be confused with onOpenVPNStatusChange. + this only updates the non-debug LEAP Status line + next to the connection icon. + """ + # XXX move bold to style sheet + self.leapConnStatus.setText( + "<b>%s</b>" % newstatus) |