diff options
Diffstat (limited to 'src/leap/baseapp/mainwindow.py')
-rw-r--r-- | src/leap/baseapp/mainwindow.py | 596 |
1 files changed, 436 insertions, 160 deletions
diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py index 91b0dc61..85129a9b 100644 --- a/src/leap/baseapp/mainwindow.py +++ b/src/leap/baseapp/mainwindow.py @@ -1,191 +1,467 @@ # vim: set fileencoding=utf-8 : #!/usr/bin/env python import logging +import time +logger = logging.getLogger(name=__name__) -import sip -sip.setapi('QString', 2) -sip.setapi('QVariant', 2) +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) -from PyQt4 import QtCore -from PyQt4 import QtGui +from leap.baseapp.dialogs import ErrorDialog +from leap.eip.conductor import (EIPConductor, + EIPNoCommandError) -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 +from leap.eip.config import (EIPInitBadKeyFilePermError) +# from leap.eip import exceptions as eip_exceptions + +from leap.gui import mainwindow_rc -logger = logging.getLogger(name=__name__) +class LeapWindow(QMainWindow): + #XXX tbd: refactor into model / view / controller + #and put in its own modules... -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]) + newLogLine = pyqtSignal([str]) + statusChange = pyqtSignal([object]) def __init__(self, opts): - logger.debug('init leap window') - self.debugmode = getattr(opts, 'debug', False) super(LeapWindow, self).__init__() + self.debugmode = getattr(opts, 'debug', False) + + self.vpn_service_started = False + + self.createWindowHeader() + self.createIconGroupBox() + + self.createActions() + self.createTrayIcon() if self.debugmode: self.createLogBrowser() - settings = QtCore.QSettings() - self.provider_domain = settings.value("provider_domain", None) - self.username = settings.value("username", None) + # create timer + self.timer = QTimer() - logger.debug('provider: %s', self.provider_domain) - logger.debug('username: %s', self.username) + # bind signals - provider = self.provider_domain - EIPConductorAppMixin.__init__( - self, opts=opts, provider=provider) - StatusAwareTrayIconMixin.__init__(self) + self.trayIcon.activated.connect(self.iconActivated) + self.newLogLine.connect(self.onLoggerNewLine) + self.statusChange.connect(self.onStatusChange) + self.timer.timeout.connect(self.onTimerTick) - # 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) + widget = QWidget() + self.setCentralWidget(widget) - geom_key = "DebugGeometry" if self.debugmode else "Geometry" - geom = settings.value(geom_key) - if geom: - self.restoreGeometry(geom) + # 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) - # XXX check for wizard - self.wizard_done = settings.value("FirstRunWizardDone") + self.trayIcon.show() + config_file = getattr(opts, 'config_file', None) - self.initchecks = FunThread(self.run_eip_checks) + # + # conductor is in charge of all + # vpn-related configuration / monitoring. + # we pass a tuple of signals that will be + # triggered when status changes. + # + self.conductor = EIPConductor( + watcher_cb=self.newLogLine.emit, + config_file=config_file, + status_signals=(self.statusChange.emit, ), + debug=self.debugmode) - # 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)) + # + # bunch of self checks. + # XXX move somewhere else alltogether. + # + + 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') + + if self.conductor.autostart: + self.start_or_stopVPN() + 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 runchecks_and_eipconnect(self): + 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): + """ + handles left click, left double click + showing the trayicon menu + """ + #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) """ - shows icon and run init checks + 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): """ - self.show_systray_icon() - self.initchecks.begin() + 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.setIcon('disconnected') + 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: + 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 + + # 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) - def do_start_eipconnection(self): + def cleanupAndQuit(self): """ - shows icon and init eip connection - called from the end of wizard + cleans state before shutting down app. """ - self.show_systray_icon() - # this will setup the command - self.conductor.run_openvpn_checks() - self.start_or_stopVPN() + # TODO:make sure to shutdown all child process / threads + # in conductor + self.conductor.cleanup() + qApp.quit() |