summaryrefslogtreecommitdiff
path: root/src/leap/baseapp/mainwindow.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/baseapp/mainwindow.py')
-rw-r--r--src/leap/baseapp/mainwindow.py596
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()