# 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)
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.eip.eipconnection import EIPConnection
from leap.gui import mainwindow_rc
class LeapWindow(QMainWindow):
#XXX tbd: refactor into model / view / controller
#and put in its own modules...
newLogLine = pyqtSignal([str])
statusChange = pyqtSignal([object])
def __init__(self, opts):
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()
# create timer
self.timer = QTimer()
# bind signals
self.trayIcon.activated.connect(self.iconActivated)
self.newLogLine.connect(self.onLoggerNewLine)
self.statusChange.connect(self.onStatusChange)
self.timer.timeout.connect(self.onTimerTick)
widget = QWidget()
self.setCentralWidget(widget)
# 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)
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 = EIPConnection(
watcher_cb=self.newLogLine.emit,
config_file=config_file,
status_signals=(self.statusChange.emit, ),
debug=self.debugmode)
#
# 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.
'
'Make sure you have '
'polkit-gnome-authentication-agent-1 '
'running and try again.',
'error')
if self.conductor.missing_pkexec is True:
dialog = ErrorDialog()
dialog.warningMessage(
'We could not find pkexec in your '
'system.
Do you want to try '
'setuid workaround? '
'(DOES NOTHING YET)',
'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 "
"Quit in the "
"context menu of the system tray entry.")
self.hide()
event.ignore()
if self.debugmode:
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):
"""
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("Encryption \
Internet Proxy")
self.headerLabelSub = QLabel("trust your \
technolust")
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.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 eip_exceptions.EIPNoCommandError:
dialog = ErrorDialog()
dialog.warningMessage(
'No suitable openvpn command found. '
'
(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.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.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 cleanupAndQuit(self):
"""
cleans state before shutting down app.
"""
# TODO:make sure to shutdown all child process / threads
# in conductor
self.conductor.cleanup()
qApp.quit()