From c46d8da153ac658c8bd145376e22b1218db1090a Mon Sep 17 00:00:00 2001 From: kali Date: Sun, 22 Jul 2012 21:10:15 -0700 Subject: initial import --- src/leap/baseapp/__init__.py | 0 src/leap/baseapp/config.py | 40 +++++ src/leap/baseapp/mainwindow.py | 398 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 438 insertions(+) create mode 100644 src/leap/baseapp/__init__.py create mode 100644 src/leap/baseapp/config.py create mode 100644 src/leap/baseapp/mainwindow.py (limited to 'src/leap/baseapp') diff --git a/src/leap/baseapp/__init__.py b/src/leap/baseapp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/baseapp/config.py b/src/leap/baseapp/config.py new file mode 100644 index 00000000..efdb4726 --- /dev/null +++ b/src/leap/baseapp/config.py @@ -0,0 +1,40 @@ +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/mainwindow.py b/src/leap/baseapp/mainwindow.py new file mode 100644 index 00000000..68b6de8f --- /dev/null +++ b/src/leap/baseapp/mainwindow.py @@ -0,0 +1,398 @@ +# 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.gui import mainwindow_rc +from leap.eip.conductor import EIPConductor + + +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) + + # + # 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, )) + + self.trayIcon.show() + + self.setWindowTitle("Leap") + 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.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) + + 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() -- cgit v1.2.3