diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | changes/feature_always_show_context_menu | 3 | ||||
-rw-r--r-- | changes/feature_display_provider_in_tray | 2 | ||||
-rw-r--r-- | changes/feature_osx-eip-scripts | 3 | ||||
-rw-r--r-- | changes/feature_refactor_login | 2 | ||||
-rwxr-xr-x | pkg/osx/install/client.down.sh | 32 | ||||
-rwxr-xr-x | pkg/osx/install/client.up.sh | 29 | ||||
-rw-r--r-- | pkg/requirements-testing.pip | 2 | ||||
-rw-r--r-- | src/leap/config/leapsettings.py | 38 | ||||
-rw-r--r-- | src/leap/gui/login.py | 212 | ||||
-rw-r--r-- | src/leap/gui/mainwindow.py | 251 | ||||
-rw-r--r-- | src/leap/gui/ui/login.ui | 129 | ||||
-rw-r--r-- | src/leap/gui/ui/mainwindow.ui | 216 | ||||
-rw-r--r-- | src/leap/platform_init/initializers.py | 121 | ||||
-rw-r--r-- | src/leap/services/abstractbootstrapper.py | 18 | ||||
-rw-r--r-- | src/leap/services/eip/eipbootstrapper.py | 4 | ||||
-rw-r--r-- | src/leap/services/eip/vpnlaunchers.py | 96 | ||||
-rw-r--r-- | src/leap/services/mail/smtpbootstrapper.py | 9 | ||||
-rw-r--r-- | src/leap/util/leap_log_handler.py | 81 |
19 files changed, 890 insertions, 360 deletions
@@ -20,7 +20,7 @@ TRANSLAT_DIR = data/translations PROJFILE = data/leap_client.pro #UI files to compile -UI_FILES = loggerwindow.ui mainwindow.ui wizard.ui +UI_FILES = loggerwindow.ui mainwindow.ui wizard.ui login.ui #Qt resource files to compile RESOURCES = locale.qrc loggerwindow.qrc mainwindow.qrc diff --git a/changes/feature_always_show_context_menu b/changes/feature_always_show_context_menu new file mode 100644 index 00000000..9e94726e --- /dev/null +++ b/changes/feature_always_show_context_menu @@ -0,0 +1,3 @@ + o Clicking in the tray icon will always show the context menu + instead of activating the window under certain + circumstances. Closes #2788
\ No newline at end of file diff --git a/changes/feature_display_provider_in_tray b/changes/feature_display_provider_in_tray new file mode 100644 index 00000000..b37e90a2 --- /dev/null +++ b/changes/feature_display_provider_in_tray @@ -0,0 +1,2 @@ + o Display the default provider configured in the systray menu. Close + #2813
\ No newline at end of file diff --git a/changes/feature_osx-eip-scripts b/changes/feature_osx-eip-scripts new file mode 100644 index 00000000..2b418291 --- /dev/null +++ b/changes/feature_osx-eip-scripts @@ -0,0 +1,3 @@ + o OSX: Add dialog with suggestion to install up/down scripts if these not found. + Closes: #1264, #2759, #2249 + o Workaround for pyside breaking with multiple inheritance. Closes #2827 diff --git a/changes/feature_refactor_login b/changes/feature_refactor_login new file mode 100644 index 00000000..41e1dc50 --- /dev/null +++ b/changes/feature_refactor_login @@ -0,0 +1,2 @@ + o Refactor login to its own widget and remove Utils menu. Closes + #2789
\ No newline at end of file diff --git a/pkg/osx/install/client.down.sh b/pkg/osx/install/client.down.sh index 66467c08..52ba4de6 100755 --- a/pkg/osx/install/client.down.sh +++ b/pkg/osx/install/client.down.sh @@ -2,7 +2,8 @@ # Note: must be bash; uses bash-specific tricks # # ****************************************************************************************************************** -# This Tunnelblick script does everything! It handles TUN and TAP interfaces, +# Based on the Tunnelblick script that just "does everything!" +# It handles TUN and TAP interfaces, # pushed configurations and DHCP leases. :) # # This is the "Down" version of the script, executed after the connection is @@ -11,6 +12,7 @@ # Created by: Nick Williams (using original code and parts of old Tblk scripts) # # ****************************************************************************************************************** +# TODO: review and adapt version 3 of the clientX.down.sh trap "" TSTP trap "" HUP @@ -26,30 +28,30 @@ if ! scutil -w State:/Network/OpenVPN &>/dev/null -t 1 ; then exit 0 fi -# NOTE: This script does not use any arguments passed to it by OpenVPN, so it doesn't shift Tunnelblick options out of the argument list +# NOTE: This script does not use any arguments passed to it by OpenVPN, so it doesn't shift LEAPClient options out of the argument list # Get info saved by the up script -TUNNELBLICK_CONFIG="$(/usr/sbin/scutil <<-EOF +LEAPCLIENT_CONFIG="$(/usr/sbin/scutil <<-EOF open show State:/Network/OpenVPN quit EOF)" -ARG_MONITOR_NETWORK_CONFIGURATION="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*MonitorNetwork :' | sed -e 's/^.*: //g')" -LEASEWATCHER_PLIST_PATH="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*LeaseWatcherPlistPath :' | sed -e 's/^.*: //g')" -PSID="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*Service :' | sed -e 's/^.*: //g')" -SCRIPT_LOG_FILE="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*ScriptLogFile :' | sed -e 's/^.*: //g')" -# Don't need: ARG_RESTORE_ON_DNS_RESET="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RestoreOnDNSReset :' | sed -e 's/^.*: //g')" -# Don't need: ARG_RESTORE_ON_WINS_RESET="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RestoreOnWINSReset :' | sed -e 's/^.*: //g')" -# Don't need: PROCESS="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*PID :' | sed -e 's/^.*: //g')" -# Don't need: ARG_IGNORE_OPTION_FLAGS="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*IgnoreOptionFlags :' | sed -e 's/^.*: //g')" -ARG_TAP="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*IsTapInterface :' | sed -e 's/^.*: //g')" -bRouteGatewayIsDhcp="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RouteGatewayIsDhcp :' | sed -e 's/^.*: //g')" +ARG_MONITOR_NETWORK_CONFIGURATION="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*MonitorNetwork :' | sed -e 's/^.*: //g')" +LEASEWATCHER_PLIST_PATH="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*LeaseWatcherPlistPath :' | sed -e 's/^.*: //g')" +PSID="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*Service :' | sed -e 's/^.*: //g')" +SCRIPT_LOG_FILE="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*ScriptLogFile :' | sed -e 's/^.*: //g')" +# Don't need: ARG_RESTORE_ON_DNS_RESET="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*RestoreOnDNSReset :' | sed -e 's/^.*: //g')" +# Don't need: ARG_RESTORE_ON_WINS_RESET="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*RestoreOnWINSReset :' | sed -e 's/^.*: //g')" +# Don't need: PROCESS="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*PID :' | sed -e 's/^.*: //g')" +# Don't need: ARG_IGNORE_OPTION_FLAGS="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*IgnoreOptionFlags :' | sed -e 's/^.*: //g')" +ARG_TAP="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*IsTapInterface :' | sed -e 's/^.*: //g')" +bRouteGatewayIsDhcp="$(echo "${LEAPCLIENT_CONFIG}" | grep -i '^[[:space:]]*RouteGatewayIsDhcp :' | sed -e 's/^.*: //g')" # @param String message - The message to log logMessage() { - echo "$(date '+%a %b %e %T %Y') *Tunnelblick $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}" + echo "$(date '+%a %b %e %T %Y') *LEAP CLient $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}" } trim() @@ -97,7 +99,7 @@ WINS_OLD="$(/usr/sbin/scutil <<-EOF quit EOF)" TB_NO_SUCH_KEY="<dictionary> { - TunnelblickNoSuchKey : true + LEAPClientNoSuchKey : true }" if [ "${DNS_OLD}" = "${TB_NO_SUCH_KEY}" ] ; then diff --git a/pkg/osx/install/client.up.sh b/pkg/osx/install/client.up.sh index fc7e341a..be9814c2 100755 --- a/pkg/osx/install/client.up.sh +++ b/pkg/osx/install/client.up.sh @@ -2,7 +2,8 @@ # Note: must be bash; uses bash-specific tricks # # ****************************************************************************************************************** -# This Tunnelblick script does everything! It handles TUN and TAP interfaces, +# Taken from the Tunnelblick script that "just does everything!" +# It handles TUN and TAP interfaces, # pushed configurations, DHCP with DNS and WINS, and renewed DHCP leases. :) # # This is the "Up" version of the script, executed after the interface is @@ -11,6 +12,7 @@ # Created by: Nick Williams (using original code and parts of old Tblk scripts) # # ****************************************************************************************************************** +# TODO: review and adapt revision 3 of the clientX-up.sh instead trap "" TSTP trap "" HUP @@ -19,7 +21,7 @@ export PATH="/bin:/sbin:/usr/sbin:/usr/bin" # Process optional arguments (if any) for the script # Each one begins with a "-" -# They come from Tunnelblick, and come first, before the OpenVPN arguments +# They come from the leap-client invocation, and come first, before the OpenVPN arguments # So we set ARG_ script variables to their values and shift them out of the argument list # When we're done, only the OpenVPN arguments remain for the rest of the script to use ARG_MONITOR_NETWORK_CONFIGURATION="false" @@ -63,24 +65,25 @@ readonly ARG_MONITOR_NETWORK_CONFIGURATION ARG_RESTORE_ON_DNS_RESET ARG_RESTORE_ # then convert to regular config /Users/Jonathan/Library/Application Support/Tunnelblick/Configurations/Folder/Subfolder/config.ovpn # to get the script log path # Note: "/Users/..." works even if the home directory has a different path; it is used in the name of the log file, and is not used as a path to get to anything. -readonly TBALTPREFIX="/Library/Application Support/Tunnelblick/Users/" +readonly TBALTPREFIX="/Library/Application Support/LEAP Client/Users/" readonly TBALTPREFIXLEN="${#TBALTPREFIX}" readonly TBCONFIGSTART="${config:0:$TBALTPREFIXLEN}" if [ "$TBCONFIGSTART" = "$TBALTPREFIX" ] ; then readonly TBBASE="${config:$TBALTPREFIXLEN}" readonly TBSUFFIX="${TBBASE#*/}" readonly TBUSERNAME="${TBBASE%%/*}" - readonly TBCONFIG="/Users/$TBUSERNAME/Library/Application Support/Tunnelblick/Configurations/$TBSUFFIX" + readonly TBCONFIG="/Users/$TBUSERNAME/Library/Application Support/LEAP Client/Configurations/$TBSUFFIX" else readonly TBCONFIG="${config}" fi readonly CONFIG_PATH_DASHES_SLASHES="$(echo "${TBCONFIG}" | sed -e 's/-/--/g' | sed -e 's/\//-S/g')" -readonly SCRIPT_LOG_FILE="/Library/Application Support/Tunnelblick/Logs/${CONFIG_PATH_DASHES_SLASHES}.script.log" +# XXX PUT LOGS SOMEWHERE BETTER +readonly SCRIPT_LOG_FILE="/Users/$LEAPUSER/.config/leap/logs/${CONFIG_PATH_DASHES_SLASHES}.script.log" readonly TB_RESOURCE_PATH=$(dirname "${0}") -LEASEWATCHER_PLIST_PATH="/Library/Application Support/Tunnelblick/LeaseWatch.plist" +LEASEWATCHER_PLIST_PATH="/Users/$LEAPUSER/.config/leap/logs/LeaseWatch.plist" readonly OSVER="$(sw_vers | grep 'ProductVersion:' | grep -o '10\.[0-9]*')" @@ -92,7 +95,7 @@ bRouteGatewayIsDhcp="false" readonly LOG_MESSAGE_COMMAND=$(basename "${0}") logMessage() { - echo "$(date '+%a %b %e %T %Y') *Tunnelblick $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}" + echo "$(date '+%a %b %e %T %Y') *LEAP Client $LOG_MESSAGE_COMMAND: "${@} >> "${SCRIPT_LOG_FILE}" } # @param String string - Content to trim @@ -270,7 +273,7 @@ EOF )" fi # Now, do the aggregation - # Save the openvpn process ID and the Network Primary Service ID, leasewather.plist path, logfile path, and optional arguments from Tunnelblick, + # Save the openvpn process ID and the Network Primary Service ID, leasewather.plist path, logfile path, and optional arguments from LEAP Client, # then save old and new DNS and WINS settings # PPID is a bash-script variable that contains the process ID of the parent of the process running the script (i.e., OpenVPN's process ID) # config is an environmental variable set to the configuration path by OpenVPN prior to running this up script @@ -290,7 +293,7 @@ EOF )" CORRECT_OLD_WINS_KEY="State:" fi - # If we are not expecting any WINS value, add <TunnelblickNoSuchKey : true> to the expected WINS setup + # If we are not expecting any WINS value, add <LEAPClientNoSuchKey : true> to the expected WINS setup NO_NOSUCH_KEY_WINS="#" if [ "${NO_NB}" = "#" -a "${AGG_WINS}" = "#" -a "${NO_WG}" = "#" ] ; then NO_NOSUCH_KEY_WINS="" @@ -315,14 +318,14 @@ EOF )" set State:/Network/OpenVPN # First, back up the device's current DNS and WINS configurations - # Indicate 'no such key' by a dictionary with a single entry: "TunnelblickNoSuchKey : true" + # Indicate 'no such key' by a dictionary with a single entry: "LEAPClientNoSuchKey : true" d.init - d.add TunnelblickNoSuchKey true + d.add LEAPClientNoSuchKey true get ${CORRECT_OLD_DNS_KEY}/Network/Service/${PSID}/DNS set State:/Network/OpenVPN/OldDNS d.init - d.add TunnelblickNoSuchKey true + d.add LEAPClientNoSuchKey true get ${CORRECT_OLD_WINS_KEY}/Network/Service/${PSID}/SMB set State:/Network/OpenVPN/OldSMB @@ -353,7 +356,7 @@ EOF )" ${NO_NB}d.add NetBIOSName ${STATIC_NETBIOSNAME} ${AGG_WINS}d.add WINSAddresses * ${ALL_WINS_SERVERS} ${NO_WG}d.add Workgroup ${STATIC_WORKGROUP} - ${NO_NOSUCH_KEY_WINS}d.add TunnelblickNoSuchKey true + ${NO_NOSUCH_KEY_WINS}d.add LEAPClientNoSuchKey true set State:/Network/OpenVPN/SMB # We are done diff --git a/pkg/requirements-testing.pip b/pkg/requirements-testing.pip index 5405a75b..2df5fe56 100644 --- a/pkg/requirements-testing.pip +++ b/pkg/requirements-testing.pip @@ -5,7 +5,7 @@ nose-progressive unittest2 # TODO we should include this dep only for python2.6 coverage -pep8==1.1 +pep8>=1.1 tox #sphinx>=1.1.2 diff --git a/src/leap/config/leapsettings.py b/src/leap/config/leapsettings.py index 006be851..88b7d8c9 100644 --- a/src/leap/config/leapsettings.py +++ b/src/leap/config/leapsettings.py @@ -62,10 +62,10 @@ class LeapSettings(object): GEOMETRY_KEY = "Geometry" WINDOWSTATE_KEY = "WindowState" USER_KEY = "User" - AUTOLOGIN_KEY = "AutoLogin" PROPERPROVIDER_KEY = "ProperProvider" REMEMBER_KEY = "RememberUserAndPass" DEFAULTPROVIDER_KEY = "DefaultProvider" + ALERTMISSING_KEY = "AlertMissingScripts" def __init__(self, standalone=False): """ @@ -191,24 +191,6 @@ class LeapSettings(object): leap_assert_type(remember, bool) self._settings.setValue(self.REMEMBER_KEY, remember) - def get_autologin(self): - """ - Returns True if the app should automatically login, False otherwise - - :rtype: bool - """ - return to_bool(self._settings.value(self.AUTOLOGIN_KEY, False)) - - def set_autologin(self, autologin): - """ - Sets whether the app should automatically login - - :param autologin: True if the app should autologin, False otherwise - :type autologin: bool - """ - leap_assert_type(autologin, bool) - self._settings.setValue(self.AUTOLOGIN_KEY, autologin) - # TODO: make this scale with multiple providers, we are assuming # just one for now def get_properprovider(self): @@ -249,3 +231,21 @@ class LeapSettings(object): """ leap_assert(len(provider) > 0, "We cannot save an empty provider") self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider) + + def get_alert_missing_scripts(self): + """ + Returns the setting for alerting of missing up/down scripts. + + :rtype: bool + """ + return to_bool(self._settings.value(self.ALERTMISSING_KEY, True)) + + def set_alert_missing_scripts(self, value): + """ + Sets the setting for alerting of missing up/down scripts. + + :param value: the value to set + :type value: bool + """ + leap_assert_type(value, bool) + self._settings.setValue(self.ALERTMISSING_KEY, value) diff --git a/src/leap/gui/login.py b/src/leap/gui/login.py new file mode 100644 index 00000000..c367b2fb --- /dev/null +++ b/src/leap/gui/login.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +# login.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Login widget implementation +""" +import logging +import keyring + +from PySide import QtCore, QtGui +from ui_login import Ui_LoginWidget + +logger = logging.getLogger(__name__) + + +class LoginWidget(QtGui.QWidget): + """ + Login widget that emits signals to display the wizard or to + perform login. + """ + + # Emitted when the login button is clicked + login = QtCore.Signal() + # Emitted when the user selects "Other..." in the provider + # combobox or click "Create Account" + show_wizard = QtCore.Signal() + + def __init__(self, settings, parent=None): + """ + Constructs the LoginWidget. + + :param settings: client wide settings + :type settings: LeapSettings + :param parent: The parent widget for this widget + :type parent: QWidget or None + """ + QtGui.QWidget.__init__(self, parent) + + self._settings = settings + self._selected_provider_index = -1 + + self.ui = Ui_LoginWidget() + self.ui.setupUi(self) + + self.ui.chkRemember.stateChanged.connect( + self._remember_state_changed) + self.ui.chkRemember.setEnabled(keyring.get_keyring() is not None) + + self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) + + self.ui.btnLogin.clicked.connect(self.login) + self.ui.lnPassword.returnPressed.connect(self.login) + + self.ui.lnUser.returnPressed.connect(self._focus_password) + + self.ui.cmbProviders.currentIndexChanged.connect( + self._current_provider_changed) + self.ui.btnCreateAccount.clicked.connect( + self.show_wizard) + + def _remember_state_changed(self, state): + """ + Saves the remember state in the LeapSettings + + :param state: possible stats can be Checked, Unchecked and + PartiallyChecked + :type state: QtCore.Qt.CheckState + """ + enable = True if state == QtCore.Qt.Checked else False + self._settings.set_remember(enable) + + def set_providers(self, provider_list): + """ + Set the provider list to provider_list plus an "Other..." item + that triggers the wizard + + :param provider_list: list of providers + :type provider_list: list of str + """ + self.ui.cmbProviders.blockSignals(True) + self.ui.cmbProviders.clear() + self.ui.cmbProviders.addItems(provider_list + ["Other..."]) + self.ui.cmbProviders.blockSignals(False) + + def select_provider_by_name(self, name): + """ + Given a provider name/domain, it selects it in the combobox + + :param name: name or domain for the provider + :type name: str + """ + provider_index = self.ui.cmbProviders.findText(name) + self.ui.cmbProviders.setCurrentIndex(provider_index) + + def get_selected_provider(self): + """ + Returns the selected provider in the combobox + """ + return self.ui.cmbProviders.currentText() + + def set_remember(self, value): + """ + Checks the remember user and password checkbox + + :param value: True to mark it checked, False otherwise + :type value: bool + """ + self.ui.chkRemember.setChecked(value) + + def get_remember(self): + """ + Returns the remember checkbox state + + :rtype: bool + """ + return self.ui.chkRemember.isChecked() + + def set_user(self, user): + """ + Sets the user and focuses on the next field, password. + + :param user: user to set the field to + :type user: str + """ + self.ui.lnUser.setText(user) + self._focus_password() + + def get_user(self): + """ + Returns the user that appears in the widget + + :rtype: str + """ + return self.ui.lnUser.text() + + def set_password(self, password): + """ + Sets the password for the widget + + :param password: password to set + :type password: str + """ + self.ui.lnPassword.setText(password) + + def get_password(self): + """ + Returns the password that appears in the widget + + :rtype: str + """ + return self.ui.lnPassword.text() + + def set_status(self, status, error=True): + """ + Sets the status label at the login stage to status + + :param status: status message + :type status: str + """ + if error: + status = "<font color='red'><b>%s</b></font>" % (status,) + self.ui.lblStatus.setText(status) + + def set_enabled(self, enabled=False): + """ + Enables or disables all the login widgets + + :param enabled: wether they should be enabled or not + :type enabled: bool + """ + self.ui.lnUser.setEnabled(enabled) + self.ui.lnPassword.setEnabled(enabled) + self.ui.btnLogin.setEnabled(enabled) + self.ui.chkRemember.setEnabled(enabled) + self.ui.cmbProviders.setEnabled(enabled) + + def _focus_password(self): + """ + Focuses in the password lineedit + """ + self.ui.lnPassword.setFocus() + + def _current_provider_changed(self, param): + """ + SLOT + TRIGGERS: self.ui.cmbProviders.currentIndexChanged + """ + if param == (self.ui.cmbProviders.count() - 1): + self.show_wizard.emit() + # Leave the previously selected provider in the combobox + prev_provider = 0 + if self._selected_provider_index != -1: + prev_provider = self._selected_provider_index + self.ui.cmbProviders.blockSignals(True) + self.ui.cmbProviders.setCurrentIndex(prev_provider) + self.ui.cmbProviders.blockSignals(False) + else: + self._selected_provider_index = param diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index 752f5ef1..e135c798 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -37,6 +37,7 @@ from leap.config.providerconfig import ProviderConfig from leap.crypto.srpauth import SRPAuth from leap.gui.loggerwindow import LoggerWindow from leap.gui.wizard import Wizard +from leap.gui.login import LoginWidget from leap.services.eip.eipbootstrapper import EIPBootstrapper from leap.services.eip.eipconfig import EIPConfig from leap.services.eip.providerbootstrapper import ProviderBootstrapper @@ -135,11 +136,18 @@ class MainWindow(QtGui.QMainWindow): self.ui = Ui_MainWindow() self.ui.setupUi(self) - self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) + self._settings = LeapSettings(standalone) + + self._login_widget = LoginWidget( + self._settings, + self.ui.stackedWidget.widget(self.LOGIN_INDEX)) + self.ui.loginLayout.addWidget(self._login_widget) - self.ui.btnLogin.clicked.connect(self._login) - self.ui.lnUser.returnPressed.connect(self._focus_password) - self.ui.lnPassword.returnPressed.connect(self._login) + self._login_widget.login.connect(self._login) + self._login_widget.show_wizard.connect( + self._launch_wizard) + + self.ui.btnShowLog.clicked.connect(self._show_logger_window) self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) @@ -206,10 +214,6 @@ class MainWindow(QtGui.QMainWindow): self._vpn.qtsigs.process_finished.connect( self._eip_finished) - self.ui.chkRemember.stateChanged.connect( - self._remember_state_changed) - self.ui.chkRemember.setEnabled(keyring.get_keyring() is not None) - self.ui.action_sign_out.setEnabled(False) self.ui.action_sign_out.triggered.connect(self._logout) self.ui.action_about_leap.triggered.connect(self._about) @@ -223,8 +227,12 @@ class MainWindow(QtGui.QMainWindow): self._systray = None + self._action_eip_provider = QtGui.QAction( + self.tr("No default provider"), self) + self._action_eip_provider.setEnabled(False) self._action_eip_status = QtGui.QAction( - self.tr("Encrypted internet is OFF"), self) + self.tr("Encrypted internet is OFF"), + self) self._action_eip_status.setEnabled(False) self._action_eip_startstop = QtGui.QAction( self.tr("Turn encryption ON"), self) @@ -243,7 +251,6 @@ class MainWindow(QtGui.QMainWindow): self._action_visible.triggered.connect(self._toggle_visible) self._enabled_services = [] - self._settings = LeapSettings(standalone) self._center_window() @@ -280,6 +287,13 @@ class MainWindow(QtGui.QMainWindow): self._finish_init() def _rejected_wizard(self): + """ + SLOT + TRIGGERS: self._wizard.rejected + + Called if the wizard has been cancelled or closed before + finishing. + """ if self._wizard_firstrun: self._settings.set_properprovider(False) self.quit() @@ -287,11 +301,24 @@ class MainWindow(QtGui.QMainWindow): self._finish_init() def _launch_wizard(self): + """ + SLOT + TRIGGERS: + self._login_widget.show_wizard + self.ui.action_wizard.triggered + + Also called in first run. + + Launches the wizard, creating the object itself if not already + there. + """ if self._wizard is None: self._wizard = Wizard(bypass_checks=self._bypass_checks) self._wizard.accepted.connect(self._finish_init) + self.setVisible(False) self._wizard.exec_() self._wizard = None + self.setVisible(True) def _get_leap_logging_handler(self): """ @@ -309,6 +336,11 @@ class MainWindow(QtGui.QMainWindow): def _show_logger_window(self): """ + SLOT + TRIGGERS: + self.ui.action_show_logs.triggered + self.ui.btnShowLog.clicked + Displays the window with the history of messages logged until now and displays the new ones on arrival. """ @@ -318,14 +350,12 @@ class MainWindow(QtGui.QMainWindow): logger.error('Leap logger handler not found') else: self._logger_window = LoggerWindow(handler=leap_log_handler) - self._logger_window.show() + self._logger_window.setVisible( + not self._logger_window.isVisible()) + self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) else: - self._logger_window.show() - - def _remember_state_changed(self, state): - enable = True if state == QtCore.Qt.Checked else False - self.ui.chkAutoLogin.setEnabled(enable) - self._settings.set_remember(enable) + self._logger_window.setVisible(not self._logger_window.isVisible()) + self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) def _new_updates_available(self, req): """ @@ -376,8 +406,21 @@ class MainWindow(QtGui.QMainWindow): msg) def _finish_init(self): - self.ui.cmbProviders.clear() - self.ui.cmbProviders.addItems(self._configured_providers()) + """ + SLOT + TRIGGERS: + self._wizard.accepted + + Also called at the end of the constructor if not first run, + and after _rejected_wizard if not first run. + + Implements the behavior after either constructing the + mainwindow object, loading the saved user/password, or after + the wizard has been executed. + """ + # XXX: May be this can be divided into two methods? + + self._login_widget.set_providers(self._configured_providers()) self._show_systray() self.show() @@ -387,20 +430,18 @@ class MainWindow(QtGui.QMainWindow): # select the configured provider in the combo box domain = self._wizard.get_domain() - provider_index = self.ui.cmbProviders.findText(domain) - self.ui.cmbProviders.setCurrentIndex(provider_index) + self._login_widget.select_provider_by_name(domain) - self.ui.chkRemember.setChecked(self._wizard.get_remember()) + self._login_widget.set_remember(self._wizard.get_remember()) self._enabled_services = list(self._wizard.get_services()) self._settings.set_enabled_services( - self.ui.cmbProviders.currentText(), + self._login_widget.get_selected_provider(), self._enabled_services) if possible_username is not None: - self.ui.lnUser.setText(possible_username) - self._focus_password() + self._login_widget.set_user(possible_username) if possible_password is not None: - self.ui.lnPassword.setText(possible_password) - self.ui.chkRemember.setChecked(True) + self._login_widget.set_password(possible_password) + self._login_widget.set_remember(True) self._login() self._wizard = None self._settings.set_properprovider(True) @@ -411,7 +452,6 @@ class MainWindow(QtGui.QMainWindow): return saved_user = self._settings.get_user() - auto_login = self._settings.get_autologin() try: username, domain = saved_user.split('@') @@ -422,15 +462,12 @@ class MainWindow(QtGui.QMainWindow): if saved_user is not None: # fill the username - self.ui.lnUser.setText(username) + self._login_widget.set_user(username) # select the configured provider in the combo box - provider_index = self.ui.cmbProviders.findText(domain) - self.ui.cmbProviders.setCurrentIndex(provider_index) + self._login_widget.select_provider_by_name(domain) - self.ui.chkRemember.setChecked(True) - self.ui.chkAutoLogin.setEnabled(self.ui.chkRemember - .isEnabled()) + self._login_widget.set_remember(True) saved_password = None try: @@ -441,12 +478,8 @@ class MainWindow(QtGui.QMainWindow): logger.debug("Incorrect Password. %r." % (e,)) if saved_password is not None: - self.ui.lnPassword.setText(saved_password.decode("utf8")) - - # Only automatically login if there is a saved user - # and the password was retrieved right - self.ui.chkAutoLogin.setChecked(auto_login) - if auto_login and saved_password: + self._login_widget.set_password( + saved_password.decode("utf8")) self._login() def _try_autostart_eip(self): @@ -460,6 +493,8 @@ class MainWindow(QtGui.QMainWindow): "provider configured") return + self._action_eip_provider.setText(default_provider) + self._enabled_services = self._settings.get_enabled_services( default_provider) @@ -487,37 +522,48 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addSeparator() systrayMenu.addAction(self.ui.action_quit) systrayMenu.addSeparator() + systrayMenu.addAction(self._action_eip_provider) systrayMenu.addAction(self._action_eip_status) systrayMenu.addAction(self._action_eip_startstop) self._systray = QtGui.QSystemTrayIcon(self) self._systray.setContextMenu(systrayMenu) self._systray.setIcon(QtGui.QIcon(self.ERROR_ICON)) self._systray.setVisible(True) - self._systray.activated.connect(self._toggle_visible) + self._systray.activated.connect(self._tray_activated) - def _toggle_visible(self, reason=None): + def _tray_activated(self, reason=None): """ SLOT TRIGGER: self._systray.activated - Toggles the window visibility + Displays the context menu from the tray icon """ get_action = lambda visible: ( - self.tr("Show"), - self.tr("Hide"))[int(visible)] - - minimized = self.isMinimized() + self.tr("Show Main Window"), + self.tr("Hide Main Window"))[int(visible)] if reason != QtGui.QSystemTrayIcon.Context: - # do show - if minimized: - self.showNormal() - self.setVisible(not self.isVisible()) - # set labels visible = self.isVisible() self._action_visible.setText(get_action(visible)) + context_menu = self._systray.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._systray.geometry().center()) + + def _toggle_visible(self): + """ + SLOT + TRIGGER: self._action_visible.triggered + + Toggles the window visibility + """ + self.setVisible(not self.isVisible()) + def _center_window(self): """ Centers the mainwindow based on the desktop geometry @@ -542,6 +588,9 @@ class MainWindow(QtGui.QMainWindow): def _about(self): """ + SLOT + TRIGGERS: self.ui.action_about_leap.triggered + Display the About LEAP dialog """ QtGui.QMessageBox.about( @@ -579,7 +628,6 @@ class MainWindow(QtGui.QMainWindow): self._settings.set_geometry(self.saveGeometry()) self._settings.set_windowstate(self.saveState()) - self._settings.set_autologin(self.ui.chkAutoLogin.isChecked()) QtGui.QMainWindow.closeEvent(self, e) @@ -614,23 +662,6 @@ class MainWindow(QtGui.QMainWindow): is_proper_provider = self._settings.get_properprovider() return not (has_provider_on_disk and is_proper_provider) - def _focus_password(self): - """ - Focuses in the password lineedit - """ - self.ui.lnPassword.setFocus() - - def _set_status(self, status, error=True): - """ - Sets the status label at the login stage to status - - :param status: status message - :type status: str - """ - if error: - status = "<font color='red'><b>%s</b></font>" % (status,) - self.ui.lblStatus.setText(status) - def _set_eip_status(self, status, error=False): """ Sets the status label at the VPN stage to status @@ -643,28 +674,13 @@ class MainWindow(QtGui.QMainWindow): status = "<font color='red'><b>%s</b></font>" % (status,) self.ui.lblEIPStatus.setText(status) - def _login_set_enabled(self, enabled=False): - """ - Enables or disables all the login widgets - - :param enabled: wether they should be enabled or not - :type enabled: bool - """ - self.ui.lnUser.setEnabled(enabled) - self.ui.lnPassword.setEnabled(enabled) - self.ui.btnLogin.setEnabled(enabled) - self.ui.chkRemember.setEnabled(enabled) - if not enabled: - self.ui.chkAutoLogin.setEnabled(False) - self.ui.cmbProviders.setEnabled(enabled) - def _download_provider_config(self): """ Starts the bootstrapping sequence. It will download the provider configuration if it's not present, otherwise will emit the corresponding signals inmediately """ - provider = self.ui.cmbProviders.currentText() + provider = self._login_widget.get_selected_provider() self._provider_bootstrapper.run_provider_select_checks( provider, @@ -684,7 +700,7 @@ class MainWindow(QtGui.QMainWindow): :type data: dict """ if data[self._provider_bootstrapper.PASSED_KEY]: - provider = self.ui.cmbProviders.currentText() + provider = self._login_widget.get_selected_provider() if self._provider_config.loaded() or \ self._provider_config.load(os.path.join("leap", "providers", @@ -694,12 +710,13 @@ class MainWindow(QtGui.QMainWindow): self._provider_config, download_if_needed=True) else: - self._set_status( + self._login_widget.set_status( self.tr("Could not load provider configuration")) - self._login_set_enabled(True) + self._login_widget.set_enabled(True) else: - self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) - self._login_set_enabled(True) + self._login_widget.set_status( + data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_enabled(True) def _login(self): """ @@ -715,29 +732,32 @@ class MainWindow(QtGui.QMainWindow): """ leap_assert(self._provider_config, "We need a provider config") - username = self.ui.lnUser.text() - password = self.ui.lnPassword.text() - provider = self.ui.cmbProviders.currentText() + username = self._login_widget.get_user() + password = self._login_widget.get_password() + provider = self._login_widget.get_selected_provider() self._enabled_services = self._settings.get_enabled_services( - self.ui.cmbProviders.currentText()) + self._login_widget.get_selected_provider()) if len(provider) == 0: - self._set_status(self.tr("Please select a valid provider")) + self._login_widget.set_status( + self.tr("Please select a valid provider")) return if len(username) == 0: - self._set_status(self.tr("Please provide a valid username")) + self._login_widget.set_status( + self.tr("Please provide a valid username")) return if len(password) == 0: - self._set_status(self.tr("Please provide a valid Password")) + self._login_widget.set_status( + self.tr("Please provide a valid Password")) return - self._set_status(self.tr("Logging in..."), error=False) - self._login_set_enabled(False) + self._login_widget.set_status(self.tr("Logging in..."), error=False) + self._login_widget.set_enabled(False) - if self.ui.chkRemember.isChecked(): + if self._login_widget.get_remember(): # in the keyring and in the settings # we store the value 'usename@provider' username_domain = (username + '@' + provider).encode("utf8") @@ -765,8 +785,8 @@ class MainWindow(QtGui.QMainWindow): leap_assert(self._provider_config, "We need a provider config!") if data[self._provider_bootstrapper.PASSED_KEY]: - username = self.ui.lnUser.text().encode("utf8") - password = self.ui.lnPassword.text().encode("utf8") + username = self._login_widget.get_user().encode("utf8") + password = self._login_widget.get_password().encode("utf8") if self._srp_auth is None: self._srp_auth = SRPAuth(self._provider_config) @@ -778,8 +798,9 @@ class MainWindow(QtGui.QMainWindow): # TODO: Add errback! self._login_defer = self._srp_auth.authenticate(username, password) else: - self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) - self._login_set_enabled(True) + self._login_widget.set_status( + data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_enabled(True) def _authentication_finished(self, ok, message): """ @@ -789,7 +810,7 @@ class MainWindow(QtGui.QMainWindow): Once the user is properly authenticated, try starting the EIP service """ - self._set_status(message, error=not ok) + self._login_widget.set_status(message, error=not ok) if ok: self.ui.action_sign_out.setEnabled(True) # We leave a bit of room for the user to see the @@ -798,7 +819,7 @@ class MainWindow(QtGui.QMainWindow): QtCore.QTimer.singleShot(1000, self._switch_to_status) self._login_defer = None else: - self._login_set_enabled(True) + self._login_widget.set_enabled(True) def _switch_to_status(self): """ @@ -809,8 +830,8 @@ class MainWindow(QtGui.QMainWindow): self._soledad_bootstrapper.run_soledad_setup_checks( self._provider_config, - self.ui.lnUser.text(), - self.ui.lnPassword.text(), + self._login_widget.get_user(), + self._login_widget.get_password(), download_if_needed=True) self._download_eip_config() @@ -948,6 +969,7 @@ class MainWindow(QtGui.QMainWindow): self._settings.set_defaultprovider( provider_config.get_domain()) + self._action_eip_provider.setText(provider_config.get_domain()) self.ui.btnEipStartStop.setText(self.tr("Turn Encryption OFF")) self.ui.btnEipStartStop.disconnect(self) self.ui.btnEipStartStop.clicked.connect( @@ -1163,9 +1185,9 @@ class MainWindow(QtGui.QMainWindow): """ self.ui.action_sign_out.setEnabled(False) self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) - self.ui.lnPassword.setText("") - self._login_set_enabled(True) - self._set_status("") + self._login_widget.set_password("") + self._login_widget.set_enabled(True) + self._login_widget.set_status("") def _intermediate_stage(self, data): """ @@ -1182,8 +1204,9 @@ class MainWindow(QtGui.QMainWindow): """ passed = data[self._provider_bootstrapper.PASSED_KEY] if not passed: - self._login_set_enabled(True) - self._set_status(data[self._provider_bootstrapper.ERROR_KEY]) + self._login_widget.set_enabled(True) + self._login_widget.set_status( + data[self._provider_bootstrapper.ERROR_KEY]) def _eip_intermediate_stage(self, data): """ diff --git a/src/leap/gui/ui/login.ui b/src/leap/gui/ui/login.ui new file mode 100644 index 00000000..88c9ef44 --- /dev/null +++ b/src/leap/gui/ui/login.ui @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LoginWidget</class> + <widget class="QWidget" name="LoginWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>356</width> + <height>219</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="5" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="1" colspan="2"> + <widget class="QComboBox" name="cmbProviders"/> + </item> + <item row="5" column="0"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="6" column="1"> + <widget class="QPushButton" name="btnCreateAccount"> + <property name="text"> + <string>Create a new account</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string><b>Provider:</b></string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2"> + <widget class="QLineEdit" name="lnPassword"> + <property name="inputMask"> + <string/> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2"> + <widget class="QLineEdit" name="lnUser"/> + </item> + <item row="4" column="1" colspan="2"> + <widget class="QCheckBox" name="chkRemember"> + <property name="text"> + <string>Remember username and password</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string><b>Username:</b></string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string><b>Password:</b></string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QPushButton" name="btnLogin"> + <property name="text"> + <string>Log In</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="3"> + <widget class="QLabel" name="lblStatus"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>cmbProviders</tabstop> + <tabstop>lnUser</tabstop> + <tabstop>lnPassword</tabstop> + <tabstop>chkRemember</tabstop> + <tabstop>btnLogin</tabstop> + <tabstop>btnCreateAccount</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/src/leap/gui/ui/mainwindow.ui b/src/leap/gui/ui/mainwindow.ui index fdf5c167..4874a324 100644 --- a/src/leap/gui/ui/mainwindow.ui +++ b/src/leap/gui/ui/mainwindow.ui @@ -6,7 +6,7 @@ <rect> <x>0</x> <y>0</y> - <width>415</width> + <width>429</width> <height>579</height> </rect> </property> @@ -28,19 +28,6 @@ </property> <widget class="QWidget" name="centralwidget"> <layout class="QGridLayout" name="gridLayout"> - <item row="15" column="2"> - <spacer name="verticalSpacer_2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> <item row="6" column="2"> <spacer name="verticalSpacer"> <property name="orientation"> @@ -54,18 +41,21 @@ </property> </spacer> </item> - <item row="7" column="3" colspan="2"> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> + <item row="7" column="2"> + <widget class="QLabel" name="label"> + <property name="autoFillBackground"> + <bool>false</bool> </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> + <property name="text"> + <string/> </property> - </spacer> + <property name="pixmap"> + <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/leap-color-big.png</pixmap> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> </item> <item row="7" column="0" colspan="2"> <spacer name="horizontalSpacer"> @@ -80,25 +70,18 @@ </property> </spacer> </item> - <item row="8" column="0" colspan="5"> + <item row="10" column="0" colspan="5"> <widget class="QStackedWidget" name="stackedWidget"> <property name="currentIndex"> - <number>1</number> + <number>0</number> </property> - <widget class="QWidget" name="page"> + <widget class="QWidget" name="loginPage"> <layout class="QGridLayout" name="gridLayout_2"> - <item row="4" column="2"> - <widget class="QCheckBox" name="chkRemember"> - <property name="text"> - <string>Remember</string> - </property> - </widget> - </item> - <item row="2" column="2" colspan="2"> - <widget class="QLineEdit" name="lnUser"/> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="loginLayout"/> </item> - <item row="2" column="0"> - <spacer name="horizontalSpacer_3"> + <item row="0" column="2"> + <spacer name="horizontalSpacer_4"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> @@ -110,15 +93,8 @@ </property> </spacer> </item> - <item row="6" column="2"> - <widget class="QPushButton" name="btnLogin"> - <property name="text"> - <string>Login</string> - </property> - </widget> - </item> - <item row="2" column="4"> - <spacer name="horizontalSpacer_4"> + <item row="0" column="0"> + <spacer name="horizontalSpacer_3"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> @@ -130,66 +106,6 @@ </property> </spacer> </item> - <item row="1" column="1"> - <widget class="QLabel" name="label_4"> - <property name="text"> - <string><b>Provider:</b></string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - <item row="3" column="1"> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string><b>Password:</b></string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - <item row="1" column="2" colspan="2"> - <widget class="QComboBox" name="cmbProviders"/> - </item> - <item row="3" column="2" colspan="2"> - <widget class="QLineEdit" name="lnPassword"> - <property name="inputMask"> - <string/> - </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string><b>User:</b></string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - <item row="0" column="0" colspan="5"> - <widget class="QLabel" name="lblStatus"> - <property name="text"> - <string/> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item row="5" column="2" colspan="2"> - <widget class="QCheckBox" name="chkAutoLogin"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>Automatically login</string> - </property> - </widget> - </item> </layout> </widget> <widget class="QWidget" name="page_2"> @@ -295,7 +211,7 @@ <item row="2" column="1" colspan="4"> <widget class="QPushButton" name="btnEipStartStop"> <property name="text"> - <string>Start EIP</string> + <string>Turn Encryption ON</string> </property> </widget> </item> @@ -303,21 +219,31 @@ </widget> </widget> </item> - <item row="7" column="2"> - <widget class="QLabel" name="label"> - <property name="autoFillBackground"> - <bool>false</bool> + <item row="7" column="3" colspan="2"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> </property> - <property name="text"> - <string/> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> </property> - <property name="pixmap"> - <pixmap resource="../../../../data/resources/mainwindow.qrc">:/images/leap-color-big.png</pixmap> + </spacer> + </item> + <item row="17" column="2"> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> </property> - </widget> + </spacer> </item> <item row="0" column="0" colspan="5"> <layout class="QGridLayout" name="gridLayout_4"> @@ -388,6 +314,39 @@ </item> </layout> </item> + <item row="9" column="2" colspan="3"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer_10"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="btnShowLog"> + <property name="text"> + <string>Show Log</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> </layout> </widget> <widget class="QMenuBar" name="menubar"> @@ -395,8 +354,8 @@ <rect> <x>0</x> <y>0</y> - <width>415</width> - <height>21</height> + <width>429</width> + <height>25</height> </rect> </property> <widget class="QMenu" name="menuSession"> @@ -415,15 +374,7 @@ <addaction name="separator"/> <addaction name="action_about_leap"/> </widget> - <widget class="QMenu" name="menuSettings"> - <property name="title"> - <string>&Utils</string> - </property> - <addaction name="action_wizard"/> - <addaction name="action_show_logs"/> - </widget> <addaction name="menuSession"/> - <addaction name="menuSettings"/> <addaction name="menuHelp"/> </widget> <widget class="QStatusBar" name="statusbar"/> @@ -458,13 +409,6 @@ </property> </action> </widget> - <tabstops> - <tabstop>lnUser</tabstop> - <tabstop>lnPassword</tabstop> - <tabstop>chkRemember</tabstop> - <tabstop>btnLogin</tabstop> - <tabstop>cmbProviders</tabstop> - </tabstops> <resources> <include location="../../../../data/resources/mainwindow.qrc"/> <include location="../../../../data/resources/locale.qrc"/> diff --git a/src/leap/platform_init/initializers.py b/src/leap/platform_init/initializers.py index 2e8cbe95..d72dc61f 100644 --- a/src/leap/platform_init/initializers.py +++ b/src/leap/platform_init/initializers.py @@ -22,10 +22,15 @@ Platform dependant initializing code import logging import os import platform +import stat import subprocess +import tempfile from PySide import QtGui +from leap.config.leapsettings import LeapSettings +from leap.services.eip import vpnlaunchers + logger = logging.getLogger(__name__) # NOTE we could use a deferToThread here, but should @@ -75,6 +80,30 @@ def _windows_has_tap_device(): return False +def _get_missing_updown_dialog(): + """ + Creates a dialog for notifying of missing updown scripts. + Returns that dialog. + + :rtype: QtGui.QMessageBox instance + """ + msg = QtGui.QMessageBox() + msg.setWindowTitle(msg.tr("Missing up/down scripts")) + msg.setText(msg.tr( + "LEAPClient needs to install up/down scripts " + "for Encrypted Internet to work properly. " + "Would you like to proceed?")) + msg.setInformativeText(msg.tr( + "It looks like either you have not installed " + "LEAP Client in a permanent location or you have an " + "incomplete installation. This will ask for " + "administrative privileges.")) + msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) + msg.addButton("No, don't ask again", QtGui.QMessageBox.RejectRole) + msg.setDefaultButton(QtGui.QMessageBox.Yes) + return msg + + def WindowsInitializer(): """ Raises a dialog in case that the windows tap driver has not been found @@ -128,15 +157,73 @@ def _darwin_has_tun_kext(): return has_tun_and_startup +def _darwin_install_missing_scripts(badexec, notfound): + """ + Tries to install the missing up/down scripts. + + :param badexec: error for notifying execution error during command. + :type badexec: str + :param notfound: error for notifying missing path. + :type notfound: str + """ + # We expect to execute this from some way of bundle, since + # the up/down scripts should be put in place by the installer. + installer_path = os.path.join( + os.getcwd(), + "..", + "Resources", + "openvpn") + launcher = vpnlaunchers.DarwinVPNLauncher + if os.path.isdir(installer_path): + tempscript = tempfile.mktemp() + try: + cmd = launcher.OSASCRIPT_BIN + scriptlines = launcher.cmd_for_missing_scripts(installer_path) + with open(tempscript, 'w') as f: + f.write(scriptlines) + st = os.stat(tempscript) + os.chmod(tempscript, st.st_mode | stat.S_IEXEC | stat.S_IXUSR | + stat.S_IXGRP | stat.S_IXOTH) + + osascript = launcher.OSX_ASADMIN % ("/bin/sh %s" % (tempscript,),) + cmdline = ["%s -e '%s'" % (cmd, osascript)] + ret = subprocess.call( + cmdline, stdout=subprocess.PIPE, + shell=True) + assert(ret) + except Exception as exc: + logger.error(badexec) + logger.error("Error was: %r" % (exc,)) + f.close() + finally: + # XXX remove file + pass + else: + logger.error(notfound) + logger.debug('path searched: %s' % (installer_path,)) + + def DarwinInitializer(): """ Raises a dialog in case that the osx tuntap driver has not been found in the registry, asking the user for permission to install the driver """ - NOTFOUND_MSG = ("Tried to install tuntaposx kext, but the installer " - "is not found inside this bundle.") - BADEXEC_MSG = ("Tried to install tuntaposx kext, but the installer " - "failed to be launched.") + # XXX split this function into several + + NOTFOUND_MSG = ("Tried to install %s, but %s " + "not found inside this bundle.") + BADEXEC_MSG = ("Tried to install %s, but %s " + "failed to %s.") + + TUNTAP_NOTFOUND_MSG = NOTFOUND_MSG % ( + "tuntaposx kext", "the installer") + TUNTAP_BADEXEC_MSG = BADEXEC_MSG % ( + "tuntaposx kext", "the installer", "be launched") + + UPDOWN_NOTFOUND_MSG = NOTFOUND_MSG % ( + "updown scripts", "those were") + UPDOWN_BADEXEC_MSG = BADEXEC_MSG % ( + "updown scripts", "they", "be copied") # TODO DRY this with other cases, and # factor out to _should_install() function. @@ -170,6 +257,28 @@ def DarwinInitializer(): cmd, stdout=subprocess.PIPE, shell=True) except: - logger.error(BADEXEC_MSG) + logger.error(TUNTAP_BADEXEC_MSG) else: - logger.error(NOTFOUND_MSG) + logger.error(TUNTAP_NOTFOUND_MSG) + + config = LeapSettings() + alert_missing = config.get_alert_missing_scripts() + missing_scripts = vpnlaunchers.DarwinVPNLauncher.missing_updown_scripts + if alert_missing and missing_scripts(): + msg = _get_missing_updown_dialog() + ret = msg.exec_() + + if ret == QtGui.QMessageBox.Yes: + _darwin_install_missing_scripts( + UPDOWN_BADEXEC_MSG, + UPDOWN_NOTFOUND_MSG) + + elif ret == QtGui.QMessageBox.No: + logger.debug("Not installing missing scripts, " + "user decided to ignore our warning.") + + elif ret == QtGui.QMessageBox.Rejected: + logger.debug( + "Setting alert_missing_scripts to False, we will not " + "ask again") + config.set_alert_missing_scripts(False) diff --git a/src/leap/services/abstractbootstrapper.py b/src/leap/services/abstractbootstrapper.py index 7bebdc15..2cbd56bc 100644 --- a/src/leap/services/abstractbootstrapper.py +++ b/src/leap/services/abstractbootstrapper.py @@ -48,14 +48,14 @@ class AbstractBootstrapper(QtCore.QObject): """ QtCore.QObject.__init__(self) - leap_assert(self._gui_errback.im_func == \ - AbstractBootstrapper._gui_errback.im_func, + leap_assert(self._gui_errback.im_func == + AbstractBootstrapper._gui_errback.im_func, "Cannot redefine _gui_errback") - leap_assert(self._errback.im_func == \ - AbstractBootstrapper._errback.im_func, + leap_assert(self._errback.im_func == + AbstractBootstrapper._errback.im_func, "Cannot redefine _errback") - leap_assert(self._gui_notify.im_func == \ - AbstractBootstrapper._gui_notify.im_func, + leap_assert(self._gui_notify.im_func == + AbstractBootstrapper._gui_notify.im_func, "Cannot redefine _gui_notify") # **************************************************** # @@ -87,9 +87,9 @@ class AbstractBootstrapper(QtCore.QObject): if self._err_msg is not None \ else str(failure.value) self._signal_to_emit.emit({ - self.PASSED_KEY: False, - self.ERROR_KEY: err_msg - }) + self.PASSED_KEY: False, + self.ERROR_KEY: err_msg + }) failure.trap(Exception) def _errback(self, failure, signal=None): diff --git a/src/leap/services/eip/eipbootstrapper.py b/src/leap/services/eip/eipbootstrapper.py index 51c3dab4..4da8f90f 100644 --- a/src/leap/services/eip/eipbootstrapper.py +++ b/src/leap/services/eip/eipbootstrapper.py @@ -173,8 +173,8 @@ class EIPBootstrapper(AbstractBootstrapper): cb_chain = [ (self._download_config, self.download_config), - (self._download_client_certificates, \ - self.download_client_certificate) + (self._download_client_certificates, + self.download_client_certificate) ] self.addCallbackChain(cb_chain) diff --git a/src/leap/services/eip/vpnlaunchers.py b/src/leap/services/eip/vpnlaunchers.py index 952d3618..6c2ff006 100644 --- a/src/leap/services/eip/vpnlaunchers.py +++ b/src/leap/services/eip/vpnlaunchers.py @@ -29,6 +29,7 @@ except ImportError: pass # ignore, probably windows from abc import ABCMeta, abstractmethod +from functools import partial from leap.common.check import leap_assert, leap_assert_type from leap.common.files import which @@ -105,22 +106,37 @@ def get_platform_launcher(): def _is_pkexec_in_system(): + """ + Checks the existence of the pkexec binary in system. + """ pkexec_path = which('pkexec') if len(pkexec_path) == 0: return False return True -def _has_updown_scripts(path): +def _has_updown_scripts(path, warn=True): """ - Checks the existence of the up/down scripts + Checks the existence of the up/down scripts. + + :param path: the path to be checked + :type path: str + + :param warn: whether we should log the absence + :type warn: bool + + :rtype: bool """ - # XXX should check permissions too is_file = os.path.isfile(path) - if not is_file: - logger.error("Could not find up/down scripts. " + - "Might produce DNS leaks.") - return is_file + if warn and not is_file: + logger.error("Could not find up/down script %s. " + "Might produce DNS leaks." % (path,)) + + is_exe = os.access(path, os.X_OK) + if warn and not is_exe: + logger.error("Up/down script %s is not executable. " + "Might produce DNS leaks." % (path,)) + return is_file and is_exe def _is_auth_agent_running(): @@ -229,7 +245,6 @@ class LinuxVPNLauncher(VPNLauncher): openvpn_configuration = eipconfig.get_openvpn_configuration() - # FIXME: sanitize this! -- for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] @@ -294,15 +309,40 @@ class DarwinVPNLauncher(VPNLauncher): OSASCRIPT_BIN = '/usr/bin/osascript' OSX_ASADMIN = "do shell script \"%s\" with administrator privileges" - OPENVPN_BIN = 'openvpn.leap' - INSTALL_PATH = "/Applications/LEAPClient.app/" + + INSTALL_PATH = "/Applications/LEAP\ Client.app" # OPENVPN_BIN = "/%s/Contents/Resources/openvpn.leap" % ( # self.INSTALL_PATH,) - UP_SCRIPT = "/%s/client.up.sh" % (INSTALL_PATH,) - DOWN_SCRIPT = "/%s/client.down.sh" % (INSTALL_PATH,) + OPENVPN_BIN = 'openvpn.leap' + OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) + + UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,) + DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,) + OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,) + + UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN) + + @classmethod + def missing_updown_scripts(kls): + """ + Returns what updown scripts are missing. + :rtype: list + """ + file_exist = partial(_has_updown_scripts, warn=False) + zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] - # TODO: Add - # OPENVPN_DOWN_ROOT = "/usr/lib/openvpn/openvpn-down-root.so" + @classmethod + def cmd_for_missing_scripts(kls, frompath): + """ + Returns a command that can copy the missing scripts. + :rtype: str + """ + to = kls.OPENVPN_PATH + cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s" % (to, frompath, to) + #return kls.OSX_ASADMIN % cmd + return cmd def get_vpn_command(self, eipconfig=None, providerconfig=None, socket_host=None, socket_port="unix"): @@ -365,20 +405,19 @@ class DarwinVPNLauncher(VPNLauncher): 'server' ] - # FIXME: sanitize this! -- - openvpn_configuration = eipconfig.get_openvpn_configuration() for key, value in openvpn_configuration.items(): args += ['--%s' % (key,), value] + user = getpass.getuser() args += [ - '--user', getpass.getuser(), + '--user', user, '--group', grp.getgrgid(os.getgroups()[-1]).gr_name ] if socket_port == "unix": args += [ - '--management-client-user', getpass.getuser() + '--management-client-user', user ] args += [ @@ -391,13 +430,21 @@ class DarwinVPNLauncher(VPNLauncher): args += [ '--up', self.UP_SCRIPT, ] + if _has_updown_scripts(self.DOWN_SCRIPT): args += [ - '--down', self.DOWN_SCRIPT, - # FIXME add down-plugin - # '--plugin', self.OPENVPN_DOWN_ROOT, - # '\'script_type=down %s\'' % self.DOWN_SCRIPT - ] + '--down', self.DOWN_SCRIPT] + + # should have the down script too + if _has_updown_scripts(self.OPENVPN_DOWN_PLUGIN): + args += [ + '--plugin', self.OPENVPN_DOWN_PLUGIN, + '\'%s\'' % self.DOWN_SCRIPT + ] + + # we set user to be passed to the up/down scripts + args += [ + '--setenv', "LEAPUSER", "%s" % (user,)] args += [ '--cert', eipconfig.get_client_cert_path(providerconfig), @@ -405,6 +452,9 @@ class DarwinVPNLauncher(VPNLauncher): '--ca', providerconfig.get_ca_cert_path() ] + # We are using osascript until we can write a proper wrapper + # for privilege escalation. + command = self.OSASCRIPT_BIN cmd_args = ["-e", self.OSX_ASADMIN % (' '.join(args),)] diff --git a/src/leap/services/mail/smtpbootstrapper.py b/src/leap/services/mail/smtpbootstrapper.py index ea480c6d..e8af5349 100644 --- a/src/leap/services/mail/smtpbootstrapper.py +++ b/src/leap/services/mail/smtpbootstrapper.py @@ -94,11 +94,10 @@ class SMTPBootstrapper(AbstractBootstrapper): # Not modified if res.status_code == 304: logger.debug("SMTP definition has not been modified") - self._smtp_config.load(os.path.join("leap", - "providers", - self._provider_config.\ - get_domain(), - "smtp-service.json")) + self._smtp_config.load(os.path.join( + "leap", "providers", + self._provider_config.get_domain(), + "smtp-service.json")) else: smtp_definition, mtime = get_content(res) diff --git a/src/leap/util/leap_log_handler.py b/src/leap/util/leap_log_handler.py index 0e598032..5b8ae789 100644 --- a/src/leap/util/leap_log_handler.py +++ b/src/leap/util/leap_log_handler.py @@ -14,7 +14,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - """ Custom handler for the logger window. """ @@ -23,23 +22,27 @@ import logging from PySide import QtCore -class LeapLogHandler(logging.Handler, QtCore.QObject): +class LogHandler(logging.Handler): """ - Custom logging handler. It emits Qt signals so it can be plugged to a gui. - Also stores an history of logs that can be fetched after connect to a gui. + This is the custom handler that implements our desired formatting + and also keeps a history of all the logged events. """ - # All dicts returned are of the form - # {'record': LogRecord, 'message': str} - new_log = QtCore.Signal(dict) MESSAGE_KEY = 'message' RECORD_KEY = 'record' - def __init__(self): - logging.Handler.__init__(self) - QtCore.QObject.__init__(self) + # TODO This is going to eat lots of memory after some time. + # Should be pruned at some moment. + _log_history = [] - self._log_history = [] + def __init__(self, qtsignal): + """ + LogHander initialization. + Calls parent method and keeps a reference to the qtsignal + that will be used to fire the gui update. + """ + logging.Handler.__init__(self) + self._qtsignal = qtsignal def _set_format(self, logging_level): """ @@ -66,6 +69,7 @@ class LeapLogHandler(logging.Handler, QtCore.QObject): format_attrs = [time, name, level, message] log_format = ' - '.join(format_attrs) formatter = logging.Formatter(log_format) + self.setFormatter(formatter) def emit(self, logRecord): @@ -74,8 +78,6 @@ class LeapLogHandler(logging.Handler, QtCore.QObject): logging module. This method reimplements logging.Handler.emit that is fired in every logged message. - QObject.emit gets in the way on the PySide signal model but we - workarouded that issue. :param logRecord: the record emitted by the logging module. :type logRecord: logging.LogRecord. @@ -83,17 +85,64 @@ class LeapLogHandler(logging.Handler, QtCore.QObject): self._set_format(logRecord.levelname) log = self.format(logRecord) log_item = {self.RECORD_KEY: logRecord, self.MESSAGE_KEY: log} - self._log_history.append(log_item) + self._qtsignal(log_item) + + +class HandlerAdapter(object): + """ + New style class that accesses all attributes from the LogHandler. + + Used as a workaround for a problem with multiple inheritance with Pyside + that surfaced under OSX with pyside 1.1.0. + """ + MESSAGE_KEY = 'message' + RECORD_KEY = 'record' + + def __init__(self, qtsignal): + self._handler = LogHandler(qtsignal=qtsignal) + + def setLevel(self, *args, **kwargs): + return self._handler.setLevel(*args, **kwargs) + + def handle(self, *args, **kwargs): + return self._handler.handle(*args, **kwargs) + + @property + def level(self): + return self._handler.level + + +class LeapLogHandler(QtCore.QObject, HandlerAdapter): + """ + Custom logging handler. It emits Qt signals so it can be plugged to a gui. + + Its inner handler also stores an history of logs that can be fetched after + having been connected to a gui. + """ + # All dicts returned are of the form + # {'record': LogRecord, 'message': str} + new_log = QtCore.Signal(dict) + + def __init__(self): + """ + LeapLogHandler initialization. + Initializes parent classes. + """ + QtCore.QObject.__init__(self) + HandlerAdapter.__init__(self, qtsignal=self.qtsignal) + def qtsignal(self, log_item): # WARNING: the new-style connection does NOT work because PySide # translates the emit method to self.emit, and that collides with # the emit method for logging.Handler # self.new_log.emit(log_item) - QtCore.QObject.emit(self, QtCore.SIGNAL('new_log(PyObject)'), log_item) + QtCore.QObject.emit( + self, + QtCore.SIGNAL('new_log(PyObject)'), log_item) @property def log_history(self): """ Returns the history of the logged messages. """ - return self._log_history + return self._handler._log_history |