diff options
author | Tomás Touceda <chiiph@leap.se> | 2013-10-04 12:09:58 -0300 |
---|---|---|
committer | Tomás Touceda <chiiph@leap.se> | 2013-10-04 12:09:58 -0300 |
commit | 584866689560bd2ebea01ecc5e6ae5e79ce7fc81 (patch) | |
tree | 4fe300c3c105e17721f79eeece9bbd3745b43d78 /src/leap/bitmask | |
parent | 1f0f8efc4cb985c082b3b8fe7b3dc45aed047a47 (diff) | |
parent | 759d73ae0728f074e0fb0740269249d0e5066574 (diff) |
Merge branch 'release-0.3.4'
Diffstat (limited to 'src/leap/bitmask')
47 files changed, 4106 insertions, 3051 deletions
diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 02b1693d..c1859478 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -152,12 +152,6 @@ def main(): """ Starts the main event loop and launches the main window. """ - try: - event_server.ensure_server(event_server.SERVER_PORT) - except Exception as e: - # We don't even have logger configured in here - print "Could not ensure server: %r" % (e,) - _, opts = leap_argparse.init_leapc_args() if opts.version: @@ -170,6 +164,12 @@ def main(): logfile = opts.log_file openvpn_verb = opts.openvpn_verb + try: + event_server.ensure_server(event_server.SERVER_PORT) + except Exception as e: + # We don't even have logger configured in here + print "Could not ensure server: %r" % (e,) + ############################################################# # Given how paths and bundling works, we need to delay the imports # of certain parts that depend on this path settings. @@ -179,6 +179,9 @@ def main(): flags.STANDALONE = standalone BaseConfig.standalone = standalone + logger = add_logger_handlers(debug, logfile) + replace_stdout_stderr_with_logging(logger) + # And then we import all the other stuff from leap.bitmask.gui import locale_rc from leap.bitmask.gui import twisted_main @@ -190,9 +193,6 @@ def main(): # pylint: avoid unused import assert(locale_rc) - logger = add_logger_handlers(debug, logfile) - replace_stdout_stderr_with_logging(logger) - if not we_are_the_one_and_only(): # Bitmask is already running logger.warning("Tried to launch more than one instance " @@ -211,7 +211,7 @@ def main(): # We force the style if on KDE so that it doesn't load all the kde # libs, which causes a compatibility issue in some systems. # For more info, see issue #3194 - if os.environ.get("KDE_SESSION_UID") is not None: + if flags.STANDALONE and os.environ.get("KDE_SESSION_UID") is not None: sys.argv.append("-style") sys.argv.append("Cleanlooks") diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py index 338fa475..4660535a 100644 --- a/src/leap/bitmask/config/leapsettings.py +++ b/src/leap/bitmask/config/leapsettings.py @@ -65,8 +65,10 @@ class LeapSettings(object): PROPERPROVIDER_KEY = "ProperProvider" REMEMBER_KEY = "RememberUserAndPass" DEFAULTPROVIDER_KEY = "DefaultProvider" + AUTOSTARTEIP_KEY = "AutoStartEIP" ALERTMISSING_KEY = "AlertMissingScripts" GATEWAY_KEY = "Gateway" + PINNED_KEY = "Pinned" # values GATEWAY_AUTOMATIC = "Automatic" @@ -134,6 +136,22 @@ class LeapSettings(object): return providers + def is_pinned_provider(self, domain): + """ + Returns True if the domain 'domain' is pinned with the application. + False otherwise. + + :param provider: provider domain + :type provider: str + + :rtype: bool + """ + leap_assert(len(domain) > 0, "We need a nonempty domain.") + pinned_key = "{0}/{1}".format(domain, self.PINNED_KEY) + result = to_bool(self._settings.value(pinned_key, False)) + + return result + def get_selected_gateway(self, provider): """ Returns the configured gateway for the given provider. @@ -285,6 +303,24 @@ class LeapSettings(object): else: self._settings.setValue(self.DEFAULTPROVIDER_KEY, provider) + def get_autostart_eip(self): + """ + Gets whether the app should autostart EIP. + + :rtype: bool + """ + return to_bool(self._settings.value(self.AUTOSTARTEIP_KEY, False)) + + def set_autostart_eip(self, autostart): + """ + Sets whether the app should autostart EIP. + + :param autostart: True if we should try to autostart EIP. + :type autostart: bool + """ + leap_assert_type(autostart, bool) + self._settings.setValue(self.AUTOSTARTEIP_KEY, autostart) + def get_alert_missing_scripts(self): """ Returns the setting for alerting of missing up/down scripts. diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py index c8c8a59e..44698d83 100644 --- a/src/leap/bitmask/config/providerconfig.py +++ b/src/leap/bitmask/config/providerconfig.py @@ -44,6 +44,25 @@ class ProviderConfig(BaseConfig): def __init__(self): BaseConfig.__init__(self) + @classmethod + def get_provider_config(self, domain): + """ + Helper to return a valid Provider Config from the domain name. + + :param domain: the domain name of the provider. + :type domain: str + + :rtype: ProviderConfig or None if there is a problem loading the config + """ + provider_config = ProviderConfig() + provider_config_path = os.path.join( + "leap", "providers", domain, "provider.json") + + if not provider_config.load(provider_config_path): + provider_config = None + + return provider_config + def _get_schema(self): """ Returns the schema corresponding to the version given. diff --git a/src/leap/bitmask/config/tests/test_leapsettings.py b/src/leap/bitmask/config/tests/test_leapsettings.py index 18166923..b45abfdd 100644 --- a/src/leap/bitmask/config/tests/test_leapsettings.py +++ b/src/leap/bitmask/config/tests/test_leapsettings.py @@ -29,6 +29,7 @@ import mock from leap.common.testing.basetest import BaseLeapTest from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.config import flags class LeapSettingsTest(BaseLeapTest): @@ -44,6 +45,7 @@ class LeapSettingsTest(BaseLeapTest): """ Test that the config file IS NOT stored under the CWD. """ + flags.STANDALONE = False self._leapsettings = LeapSettings() with mock.patch('os.listdir') as os_listdir: # use this method only to spy where LeapSettings is looking for @@ -57,7 +59,8 @@ class LeapSettingsTest(BaseLeapTest): """ Test that the config file IS stored under the CWD. """ - self._leapsettings = LeapSettings(standalone=True) + flags.STANDALONE = True + self._leapsettings = LeapSettings() with mock.patch('os.listdir') as os_listdir: # use this method only to spy where LeapSettings is looking for self._leapsettings.get_configured_providers() diff --git a/src/leap/bitmask/config/tests/test_providerconfig.py b/src/leap/bitmask/config/tests/test_providerconfig.py index 7661a1ce..fe27e683 100644 --- a/src/leap/bitmask/config/tests/test_providerconfig.py +++ b/src/leap/bitmask/config/tests/test_providerconfig.py @@ -175,10 +175,9 @@ class ProviderConfigTest(BaseLeapTest): def test_get_ca_cert_path_as_expected(self): pc = self._provider_config - pc.get_path_prefix = Mock(return_value='test') provider_domain = sample_config['domain'] - expected_path = os.path.join('test', 'leap', 'providers', + expected_path = os.path.join('leap', 'providers', provider_domain, 'keys', 'ca', 'cacert.pem') @@ -186,24 +185,21 @@ class ProviderConfigTest(BaseLeapTest): os.path.exists = Mock(return_value=True) cert_path = pc.get_ca_cert_path() - self.assertEqual(cert_path, expected_path) + self.assertTrue(cert_path.endswith(expected_path)) def test_get_ca_cert_path_about_to_download(self): pc = self._provider_config - pc.get_path_prefix = Mock(return_value='test') provider_domain = sample_config['domain'] - expected_path = os.path.join('test', 'leap', 'providers', + expected_path = os.path.join('leap', 'providers', provider_domain, 'keys', 'ca', 'cacert.pem') cert_path = pc.get_ca_cert_path(about_to_download=True) - - self.assertEqual(cert_path, expected_path) + self.assertTrue(cert_path.endswith(expected_path)) def test_get_ca_cert_path_fails(self): pc = self._provider_config - pc.get_path_prefix = Mock(return_value='test') # mock 'get_domain' so we don't need to load a config provider_domain = 'test.provider.com' diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index bf85f75c..cbff4b49 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -52,13 +52,6 @@ class SRPAuthConnectionError(SRPAuthenticationError): pass -class SRPAuthUnknownUser(SRPAuthenticationError): - """ - Exception raised when trying to authenticate an unknown user - """ - pass - - class SRPAuthBadStatusCode(SRPAuthenticationError): """ Exception raised when we received an unknown bad status code @@ -97,7 +90,7 @@ class SRPAuthJSONDecodeError(SRPAuthenticationError): pass -class SRPAuthBadPassword(SRPAuthenticationError): +class SRPAuthBadUserOrPassword(SRPAuthenticationError): """ Exception raised when the user provided a bad password to auth. """ @@ -136,6 +129,7 @@ class SRPAuth(QtCore.QObject): SESSION_ID_KEY = "_session_id" USER_VERIFIER_KEY = 'user[password_verifier]' USER_SALT_KEY = 'user[password_salt]' + AUTHORIZATION_KEY = "Authorization" def __init__(self, provider_config): """ @@ -219,7 +213,6 @@ class SRPAuth(QtCore.QObject): Might raise all SRPAuthenticationError based: SRPAuthenticationError SRPAuthConnectionError - SRPAuthUnknownUser SRPAuthBadStatusCode SRPAuthNoSalt SRPAuthNoB @@ -266,7 +259,7 @@ class SRPAuth(QtCore.QObject): "Status code = %r. Content: %r" % (init_session.status_code, content)) if init_session.status_code == 422: - raise SRPAuthUnknownUser(self._WRONG_USER_PASS) + raise SRPAuthBadUserOrPassword(self._WRONG_USER_PASS) raise SRPAuthBadStatusCode(self.tr("There was a problem with" " authentication")) @@ -296,7 +289,7 @@ class SRPAuth(QtCore.QObject): SRPAuthBadDataFromServer SRPAuthConnectionError SRPAuthJSONDecodeError - SRPAuthBadPassword + SRPAuthBadUserOrPassword :param salt_B: salt and B parameters for the username :type salt_B: tuple @@ -355,7 +348,7 @@ class SRPAuth(QtCore.QObject): "received: %s", (content,)) logger.error("[%s] Wrong password (HAMK): [%s]" % (auth_result.status_code, error)) - raise SRPAuthBadPassword(self._WRONG_USER_PASS) + raise SRPAuthBadUserOrPassword(self._WRONG_USER_PASS) if auth_result.status_code not in (200,): logger.error("No valid response (HAMK): " @@ -452,7 +445,7 @@ class SRPAuth(QtCore.QObject): It requires to be authenticated. Might raise: - SRPAuthBadPassword + SRPAuthBadUserOrPassword requests.exceptions.HTTPError :param current_password: the current password for the logged user. @@ -463,7 +456,7 @@ class SRPAuth(QtCore.QObject): leap_assert(self.get_uid() is not None) if current_password != self._password: - raise SRPAuthBadPassword + raise SRPAuthBadUserOrPassword url = "%s/%s/users/%s.json" % ( self._provider_config.get_api_uri(), @@ -474,6 +467,10 @@ class SRPAuth(QtCore.QObject): self._username, new_password, self._hashfun, self._ng) cookies = {self.SESSION_ID_KEY: self.get_session_id()} + headers = { + self.AUTHORIZATION_KEY: + "Token token={0}".format(self.get_token()) + } user_data = { self.USER_VERIFIER_KEY: binascii.hexlify(verifier), self.USER_SALT_KEY: binascii.hexlify(salt) @@ -483,7 +480,8 @@ class SRPAuth(QtCore.QObject): url, data=user_data, verify=self._provider_config.get_ca_cert_path(), cookies=cookies, - timeout=REQUEST_TIMEOUT) + timeout=REQUEST_TIMEOUT, + headers=headers) # In case of non 2xx it raises HTTPError change_password.raise_for_status() @@ -510,6 +508,8 @@ class SRPAuth(QtCore.QObject): self._username = username self._password = password + self._session = self._fetcher.session() + d = threads.deferToThread(self._authentication_preprocessing, username=username, password=password) @@ -605,6 +605,13 @@ class SRPAuth(QtCore.QObject): # Store instance reference as the only member in the handle self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance + # Generally, we initialize this with a provider_config once, + # and after that initialize it without one and use the one + # that was assigned before. But we need to update it if we + # want to be able to logout and login into another provider. + if provider_config is not None: + SRPAuth.__instance._provider_config = provider_config + def authenticate(self, username, password): """ Executes the whole authentication process for a user diff --git a/src/leap/bitmask/crypto/tests/test_srpauth.py b/src/leap/bitmask/crypto/tests/test_srpauth.py index 6fb2b739..e63c1385 100644 --- a/src/leap/bitmask/crypto/tests/test_srpauth.py +++ b/src/leap/bitmask/crypto/tests/test_srpauth.py @@ -246,7 +246,7 @@ class SRPAuthTestCase(unittest.TestCase): d = self._prepare_auth_test(422) def wrapper(_): - with self.assertRaises(srpauth.SRPAuthUnknownUser): + with self.assertRaises(srpauth.SRPAuthBadUserOrPassword): with mock.patch( 'leap.bitmask.util.request_helpers.get_content', new=mock.create_autospec(get_content)) as content: @@ -425,7 +425,7 @@ class SRPAuthTestCase(unittest.TestCase): new=mock.create_autospec(get_content)) as \ content: content.return_value = ("", 0) - with self.assertRaises(srpauth.SRPAuthBadPassword): + with self.assertRaises(srpauth.SRPAuthBadUserOrPassword): self.auth_backend._process_challenge( salt_B, username=self.TEST_USER) @@ -449,7 +449,7 @@ class SRPAuthTestCase(unittest.TestCase): new=mock.create_autospec(get_content)) as \ content: content.return_value = ("[]", 0) - with self.assertRaises(srpauth.SRPAuthBadPassword): + with self.assertRaises(srpauth.SRPAuthBadUserOrPassword): self.auth_backend._process_challenge( salt_B, username=self.TEST_USER) @@ -680,10 +680,7 @@ class SRPAuthTestCase(unittest.TestCase): self.auth_backend._session.delete, side_effect=Exception()) - def wrapper(*args): - self.auth_backend.logout() - - d = threads.deferToThread(wrapper) + d = threads.deferToThread(self.auth.logout) return d @deferred() diff --git a/src/leap/bitmask/gui/clickablelabel.py b/src/leap/bitmask/gui/clickablelabel.py new file mode 100644 index 00000000..2808a601 --- /dev/null +++ b/src/leap/bitmask/gui/clickablelabel.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# clickablelabel.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/>. + +""" +Clickable label +""" +from PySide import QtCore, QtGui + + +class ClickableLabel(QtGui.QLabel): + clicked = QtCore.Signal() + + def mousePressEvent(self, event): + self.clicked.emit() diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py new file mode 100644 index 00000000..9f8c4ff4 --- /dev/null +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# eip_preferenceswindow.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/>. + +""" +EIP Preferences window +""" +import os +import logging + +from functools import partial +from PySide import QtGui + +from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.gui.ui_eippreferences import Ui_EIPPreferences +from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector + +logger = logging.getLogger(__name__) + + +class EIPPreferencesWindow(QtGui.QDialog): + """ + Window that displays the EIP preferences. + """ + def __init__(self, parent): + """ + :param parent: parent object of the EIPPreferencesWindow. + :parent type: QWidget + """ + QtGui.QDialog.__init__(self, parent) + self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") + + self._settings = LeapSettings() + + # Load UI + self.ui = Ui_EIPPreferences() + self.ui.setupUi(self) + self.ui.lblProvidersGatewayStatus.setVisible(False) + self.ui.lblAutoStartEIPStatus.setVisible(False) + + # Connections + self.ui.cbProvidersGateway.currentIndexChanged[unicode].connect( + self._populate_gateways) + + self.ui.cbGateways.currentIndexChanged[unicode].connect( + lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) + + self.ui.cbProvidersEIP.currentIndexChanged[unicode].connect( + lambda x: self.ui.lblAutoStartEIPStatus.setVisible(False)) + + self.ui.cbAutoStartEIP.toggled.connect( + lambda x: self.ui.lblAutoStartEIPStatus.setVisible(False)) + + self.ui.pbSaveAutoStartEIP.clicked.connect(self._save_auto_start_eip) + + self._add_configured_providers() + + # Load auto start EIP settings + self.ui.cbAutoStartEIP.setChecked(self._settings.get_autostart_eip()) + default_provider = self._settings.get_defaultprovider() + idx = self.ui.cbProvidersEIP.findText(default_provider) + self.ui.cbProvidersEIP.setCurrentIndex(idx) + + def _save_auto_start_eip(self): + """ + SLOT + TRIGGER: + self.ui.cbAutoStartEIP.toggled + + Saves the automatic start of EIP user preference. + """ + default_provider = self.ui.cbProvidersEIP.currentText() + enabled = self.ui.cbAutoStartEIP.isChecked() + + self._settings.set_autostart_eip(enabled) + self._settings.set_defaultprovider(default_provider) + + self.ui.lblAutoStartEIPStatus.show() + logger.debug('Auto start EIP saved: {0} {1}.'.format( + default_provider, enabled)) + + def _set_providers_gateway_status(self, status, success=False, + error=False): + """ + Sets the status label for the gateway change. + + :param status: status message to display, can be HTML + :type status: str + :param success: is set to True if we should display the + message as green + :type success: bool + :param error: is set to True if we should display the + message as red + :type error: bool + """ + if success: + status = "<font color='green'><b>%s</b></font>" % (status,) + elif error: + status = "<font color='red'><b>%s</b></font>" % (status,) + + self.ui.lblProvidersGatewayStatus.setVisible(True) + self.ui.lblProvidersGatewayStatus.setText(status) + + def _add_configured_providers(self): + """ + Add the client's configured providers to the providers combo boxes. + """ + self.ui.cbProvidersGateway.clear() + self.ui.cbProvidersEIP.clear() + providers = self._settings.get_configured_providers() + if not providers: + self.ui.gbAutomaticEIP.setEnabled(False) + self.ui.gbGatewaySelector.setEnabled(False) + return + + for provider in providers: + self.ui.cbProvidersGateway.addItem(provider) + self.ui.cbProvidersEIP.addItem(provider) + + def _save_selected_gateway(self, provider): + """ + SLOT + TRIGGERS: + self.ui.pbSaveGateway.clicked + + Saves the new gateway setting to the configuration file. + + :param provider: the provider config that we need to save. + :type provider: str + """ + gateway = self.ui.cbGateways.currentText() + + if gateway == self.AUTOMATIC_GATEWAY_LABEL: + gateway = self._settings.GATEWAY_AUTOMATIC + else: + idx = self.ui.cbGateways.currentIndex() + gateway = self.ui.cbGateways.itemData(idx) + + self._settings.set_selected_gateway(provider, gateway) + + msg = self.tr( + "Gateway settings for provider '{0}' saved.").format(provider) + self._set_providers_gateway_status(msg, success=True) + + def _populate_gateways(self, domain): + """ + SLOT + TRIGGERS: + self.ui.cbProvidersGateway.currentIndexChanged[unicode] + + Loads the gateways that the provider provides into the UI for + the user to select. + + :param domain: the domain of the provider to load gateways from. + :type domain: str + """ + # We hide the maybe-visible status label after a change + self.ui.lblProvidersGatewayStatus.setVisible(False) + + if not domain: + return + + try: + # disconnect previously connected save method + self.ui.pbSaveGateway.clicked.disconnect() + except RuntimeError: + pass # Signal was not connected + + # set the proper connection for the 'save' button + save_gateway = partial(self._save_selected_gateway, domain) + self.ui.pbSaveGateway.clicked.connect(save_gateway) + + eip_config = EIPConfig() + provider_config = ProviderConfig.get_provider_config(domain) + + eip_config_path = os.path.join("leap", "providers", + domain, "eip-service.json") + api_version = provider_config.get_api_version() + eip_config.set_api_version(api_version) + eip_loaded = eip_config.load(eip_config_path) + + if not eip_loaded or provider_config is None: + self._set_providers_gateway_status( + self.tr("There was a problem with configuration files."), + error=True) + return + + gateways = VPNGatewaySelector(eip_config).get_gateways_list() + logger.debug(gateways) + + self.ui.cbGateways.clear() + self.ui.cbGateways.addItem(self.AUTOMATIC_GATEWAY_LABEL) + + # Add the available gateways and + # select the one stored in configuration file. + selected_gateway = self._settings.get_selected_gateway(domain) + index = 0 + for idx, (gw_name, gw_ip) in enumerate(gateways): + gateway = "{0} ({1})".format(gw_name, gw_ip) + self.ui.cbGateways.addItem(gateway, gw_ip) + if gw_ip == selected_gateway: + index = idx + 1 + + self.ui.cbGateways.setCurrentIndex(index) diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py new file mode 100644 index 00000000..946eaa4e --- /dev/null +++ b/src/leap/bitmask/gui/eip_status.py @@ -0,0 +1,435 @@ +# -*- coding: utf-8 -*- +# eip_status.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/>. +""" +EIP Status Panel widget implementation +""" +import logging + +from datetime import datetime +from functools import partial + +from PySide import QtCore, QtGui + +from leap.bitmask.services.eip.connection import EIPConnection +from leap.bitmask.services.eip.vpnprocess import VPNManager +from leap.bitmask.platform_init import IS_LINUX +from leap.bitmask.util.averages import RateMovingAverage +from leap.common.check import leap_assert_type + +from ui_eip_status import Ui_EIPStatus + +logger = logging.getLogger(__name__) + + +class EIPStatusWidget(QtGui.QWidget): + """ + EIP Status widget that displays the current state of the EIP service + """ + DISPLAY_TRAFFIC_RATES = True + RATE_STR = "%14.2f KB/s" + TOTAL_STR = "%14.2f Kb" + + eip_connection_connected = QtCore.Signal() + + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + + self._systray = None + self._eip_status_menu = None + + self.ui = Ui_EIPStatus() + self.ui.setupUi(self) + + self.eipconnection = EIPConnection() + + # set systray tooltip status + self._eip_status = "" + + self.ui.eip_bandwidth.hide() + + # Set the EIP status icons + self.CONNECTING_ICON = None + self.CONNECTED_ICON = None + self.ERROR_ICON = None + self.CONNECTING_ICON_TRAY = None + self.CONNECTED_ICON_TRAY = None + self.ERROR_ICON_TRAY = None + self._set_eip_icons() + + self._set_traffic_rates() + self._make_status_clickable() + + self._provider = "" + + def _make_status_clickable(self): + """ + Makes upload and download figures clickable. + """ + onclicked = self._on_VPN_status_clicked + self.ui.btnUpload.clicked.connect(onclicked) + self.ui.btnDownload.clicked.connect(onclicked) + + def _on_VPN_status_clicked(self): + """ + SLOT + TRIGGER: self.ui.btnUpload.clicked + self.ui.btnDownload.clicked + + Toggles between rate and total throughput display for vpn + status figures. + """ + self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES + self.update_vpn_status(None) # refresh + + def _set_traffic_rates(self): + """ + Initializes up and download rates. + """ + self._up_rate = RateMovingAverage() + self._down_rate = RateMovingAverage() + + self.ui.btnUpload.setText(self.RATE_STR % (0,)) + self.ui.btnDownload.setText(self.RATE_STR % (0,)) + + def _reset_traffic_rates(self): + """ + Resets up and download rates, and cleans up the labels. + """ + self._up_rate.reset() + self._down_rate.reset() + self.update_vpn_status(None) + + def _update_traffic_rates(self, up, down): + """ + Updates up and download rates. + + :param up: upload total. + :type up: int + :param down: download total. + :type down: int + """ + ts = datetime.now() + self._up_rate.append((ts, up)) + self._down_rate.append((ts, down)) + + def _get_traffic_rates(self): + """ + Gets the traffic rates (in KB/s). + + :returns: a tuple with the (up, down) rates + :rtype: tuple + """ + up = self._up_rate + down = self._down_rate + + return (up.get_average(), down.get_average()) + + def _get_traffic_totals(self): + """ + Gets the traffic total throughput (in Kb). + + :returns: a tuple with the (up, down) totals + :rtype: tuple + """ + up = self._up_rate + down = self._down_rate + + return (up.get_total(), down.get_total()) + + def _set_eip_icons(self): + """ + Sets the EIP status icons for the main window and for the tray + + MAC : dark icons + LINUX : dark icons in window, light icons in tray + WIN : light icons + """ + EIP_ICONS = EIP_ICONS_TRAY = ( + ":/images/black/32/wait.png", + ":/images/black/32/on.png", + ":/images/black/32/off.png") + + if IS_LINUX: + EIP_ICONS_TRAY = ( + ":/images/white/32/wait.png", + ":/images/white/32/on.png", + ":/images/white/32/off.png") + + self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) + self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) + self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) + + self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) + self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) + self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) + + # Systray and actions + + def set_systray(self, systray): + """ + Sets the systray object to use. + + :param systray: Systray object + :type systray: QtGui.QSystemTrayIcon + """ + leap_assert_type(systray, QtGui.QSystemTrayIcon) + self._systray = systray + self._systray.setToolTip(self.tr("All services are OFF")) + + def _update_systray_tooltip(self): + """ + Updates the system tray icon tooltip using the eip and mx status. + """ + status = self.tr("Encrypted Internet: {0}").format(self._eip_status) + self._systray.setToolTip(status) + + def set_action_eip_startstop(self, action_eip_startstop): + """ + Sets the action_eip_startstop to use. + + :param action_eip_startstop: action_eip_status to be used + :type action_eip_startstop: QtGui.QAction + """ + self._action_eip_startstop = action_eip_startstop + + def set_eip_status_menu(self, eip_status_menu): + """ + Sets the eip_status_menu to use. + + :param eip_status_menu: eip_status_menu to be used + :type eip_status_menu: QtGui.QMenu + """ + leap_assert_type(eip_status_menu, QtGui.QMenu) + self._eip_status_menu = eip_status_menu + + # EIP status --- + + @property + def eip_button(self): + return self.ui.btnEipStartStop + + @property + def eip_label(self): + return self.ui.lblEIPStatus + + def eip_pre_up(self): + """ + Triggered when the app activates eip. + Hides the status box and disables the start/stop button. + """ + self.set_startstop_enabled(False) + + @QtCore.Slot() + def disable_eip_start(self): + """ + Triggered when a default provider_config has not been found. + Disables the start button and adds instructions to the user. + """ + logger.debug('Hiding EIP start button') + # you might be tempted to change this for a .setEnabled(False). + # it won't work. it's under the claws of the state machine. + # probably the best thing would be to make a transitional + # transition there, but that's more involved. + self.eip_button.hide() + msg = self.tr("You must login to use Encrypted Internet") + self.eip_label.setText(msg) + + @QtCore.Slot() + def enable_eip_start(self): + """ + Triggered after a successful login. + Enables the start button. + """ + logger.debug('Showing EIP start button') + self.eip_button.show() + + # XXX disable (later) -------------------------- + def set_eip_status(self, status, error=False): + """ + Sets the status label at the VPN stage to status + + :param status: status message + :type status: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + self._eip_status = status + if error: + status = "<font color='red'>%s</font>" % (status,) + self.ui.lblEIPStatus.setText(status) + self.ui.lblEIPStatus.show() + self._update_systray_tooltip() + + # XXX disable --------------------------------- + def set_startstop_enabled(self, value): + """ + Enable or disable btnEipStartStop and _action_eip_startstop + based on value + + :param value: True for enabled, False otherwise + :type value: bool + """ + # TODO use disable_eip_start instead + # this should be handled by the state machine + leap_assert_type(value, bool) + self.ui.btnEipStartStop.setEnabled(value) + self._action_eip_startstop.setEnabled(value) + + # XXX disable ----------------------------- + def eip_started(self): + """ + Sets the state of the widget to how it should look after EIP + has started + """ + self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) + self.ui.btnEipStartStop.disconnect(self) + self.ui.btnEipStartStop.clicked.connect( + self.eipconnection.qtsigs.do_connect_signal) + + # XXX disable ----------------------------- + def eip_stopped(self): + """ + Sets the state of the widget to how it should look after EIP + has stopped + """ + # XXX should connect this to EIPConnection.disconnected_signal + self._reset_traffic_rates() + # XXX disable ----------------------------- + self.ui.btnEipStartStop.setText(self.tr("Turn ON")) + self.ui.btnEipStartStop.disconnect(self) + self.ui.btnEipStartStop.clicked.connect( + self.eipconnection.qtsigs.do_disconnect_signal) + + self.ui.eip_bandwidth.hide() + self.ui.lblEIPMessage.setText( + self.tr("Traffic is being routed in the clear")) + self.ui.lblEIPStatus.show() + + def update_vpn_status(self, data): + """ + SLOT + TRIGGER: VPN.status_changed + + Updates the download/upload labels based on the data provided + by the VPN thread. + + :param data: a dictionary with the tcp/udp write and read totals. + If data is None, we just will refresh the display based + on the previous data. + :type data: dict + """ + if data: + upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0") + download = float(data[VPNManager.TCPUDP_READ_KEY] or "0") + self._update_traffic_rates(upload, download) + + if self.DISPLAY_TRAFFIC_RATES: + uprate, downrate = self._get_traffic_rates() + upload_str = self.RATE_STR % (uprate,) + download_str = self.RATE_STR % (downrate,) + + else: # display total throughput + uptotal, downtotal = self._get_traffic_totals() + upload_str = self.TOTAL_STR % (uptotal,) + download_str = self.TOTAL_STR % (downtotal,) + + self.ui.btnUpload.setText(upload_str) + self.ui.btnDownload.setText(download_str) + + def update_vpn_state(self, data): + """ + SLOT + TRIGGER: VPN.state_changed + + Updates the displayed VPN state based on the data provided by + the VPN thread. + + Emits: + If the status is connected, we emit EIPConnection.qtsigs. + connected_signal + """ + status = data[VPNManager.STATUS_STEP_KEY] + self.set_eip_status_icon(status) + if status == "CONNECTED": + self.ui.eip_bandwidth.show() + self.ui.lblEIPStatus.hide() + + # XXX should be handled by the state machine too. + self.eip_connection_connected.emit() + + # XXX should lookup status map in EIPConnection + elif status == "AUTH": + self.set_eip_status(self.tr("Authenticating...")) + elif status == "GET_CONFIG": + self.set_eip_status(self.tr("Retrieving configuration...")) + elif status == "WAIT": + self.set_eip_status(self.tr("Waiting to start...")) + elif status == "ASSIGN_IP": + self.set_eip_status(self.tr("Assigning IP")) + elif status == "RECONNECTING": + self.set_eip_status(self.tr("Reconnecting...")) + elif status == "ALREADYRUNNING": + # Put the following calls in Qt's event queue, otherwise + # the UI won't update properly + QtCore.QTimer.singleShot( + 0, self.eipconnection.qtsigs.do_disconnect_signal) + QtCore.QTimer.singleShot(0, partial(self.set_eip_status, + self.tr("Unable to start VPN, " + "it's already " + "running."))) + else: + self.set_eip_status(status) + + def set_eip_icon(self, icon): + """ + Sets the icon to display for EIP + + :param icon: icon to display + :type icon: QPixmap + """ + self.ui.lblVPNStatusIcon.setPixmap(icon) + + def set_eip_status_icon(self, status): + """ + Given a status step from the VPN thread, set the icon properly + + :param status: status step + :type status: str + """ + selected_pixmap = self.ERROR_ICON + selected_pixmap_tray = self.ERROR_ICON_TRAY + tray_message = self.tr("Encrypted Internet: OFF") + if status in ("WAIT", "AUTH", "GET_CONFIG", + "RECONNECTING", "ASSIGN_IP"): + selected_pixmap = self.CONNECTING_ICON + selected_pixmap_tray = self.CONNECTING_ICON_TRAY + tray_message = self.tr("Encrypted Internet: Starting...") + elif status in ("CONNECTED"): + tray_message = self.tr("Encrypted Internet: ON") + selected_pixmap = self.CONNECTED_ICON + selected_pixmap_tray = self.CONNECTED_ICON_TRAY + + self.set_eip_icon(selected_pixmap) + self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) + self._eip_status_menu.setTitle(tray_message) + + def set_provider(self, provider): + self._provider = provider + self.ui.lblEIPMessage.setText( + self.tr("Route traffic through: {0}").format(self._provider)) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index db7b8e2a..582f26be 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -14,16 +14,18 @@ # # 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 from leap.bitmask.util.keyring_helpers import has_keyring +from leap.common.check import leap_assert_type logger = logging.getLogger(__name__) @@ -33,10 +35,11 @@ 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() + logged_in_signal = QtCore.Signal() cancel_login = QtCore.Signal() + logout = QtCore.Signal() # Emitted when the user selects "Other..." in the provider # combobox or click "Create Account" @@ -76,13 +79,21 @@ class LoginWidget(QtGui.QWidget): self.ui.cmbProviders.currentIndexChanged.connect( self._current_provider_changed) - self.ui.btnCreateAccount.clicked.connect( - self.show_wizard) + + self.ui.btnLogout.clicked.connect( + self.logout) username_re = QtCore.QRegExp(self.BARE_USERNAME_REGEX) self.ui.lnUser.setValidator( QtGui.QRegExpValidator(username_re, self)) + self.logged_out() + + self.ui.btnLogout.clicked.connect(self.start_logout) + + self.ui.clblErrorMsg.hide() + self.ui.clblErrorMsg.clicked.connect(self.ui.clblErrorMsg.hide) + def _remember_state_changed(self, state): """ Saves the remember state in the LeapSettings @@ -185,8 +196,10 @@ class LoginWidget(QtGui.QWidget): if len(status) > self.MAX_STATUS_WIDTH: status = status[:self.MAX_STATUS_WIDTH] + "..." if error: - status = "<font color='red'><b>%s</b></font>" % (status,) - self.ui.lblStatus.setText(status) + self.ui.clblErrorMsg.show() + self.ui.clblErrorMsg.setText(status) + else: + self.ui.lblStatus.setText(status) def set_enabled(self, enabled=False): """ @@ -211,6 +224,7 @@ class LoginWidget(QtGui.QWidget): """ text = self.tr("Cancel") login_or_cancel = self.cancel_login + hide_remember = enabled if not enabled: text = self.tr("Log In") @@ -220,6 +234,8 @@ class LoginWidget(QtGui.QWidget): self.ui.btnLogin.clicked.disconnect() self.ui.btnLogin.clicked.connect(login_or_cancel) + self.ui.chkRemember.setVisible(not hide_remember) + self.ui.lblStatus.setVisible(hide_remember) def _focus_password(self): """ @@ -243,3 +259,106 @@ class LoginWidget(QtGui.QWidget): self.ui.cmbProviders.blockSignals(False) else: self._selected_provider_index = param + + def start_login(self): + """ + Setups the login widgets for actually performing the login and + performs some basic checks. + + :returns: True if everything's good to go, False otherwise + :rtype: bool + """ + username = self.get_user() + password = self.get_password() + provider = self.get_selected_provider() + + self._enabled_services = self._settings.get_enabled_services( + self.get_selected_provider()) + + if len(provider) == 0: + self.set_status( + self.tr("Please select a valid provider")) + return False + + if len(username) == 0: + self.set_status( + self.tr("Please provide a valid username")) + return False + + if len(password) == 0: + self.set_status( + self.tr("Please provide a valid password")) + return False + + self.set_status(self.tr("Logging in..."), error=False) + self.set_enabled(False) + self.ui.clblErrorMsg.hide() + + if self.get_remember() and has_keyring(): + # in the keyring and in the settings + # we store the value 'usename@provider' + username_domain = (username + '@' + provider).encode("utf8") + try: + keyring.set_password(self.KEYRING_KEY, + username_domain, + password.encode("utf8")) + # Only save the username if it was saved correctly in + # the keyring + self._settings.set_user(username_domain) + except Exception as e: + logger.exception("Problem saving data to keyring. %r" + % (e,)) + return True + + def logged_in(self): + """ + Sets the widgets to the logged in state + """ + self.ui.login_widget.hide() + self.ui.logged_widget.show() + self.ui.lblUser.setText("%s@%s" % (self.get_user(), + self.get_selected_provider())) + self.set_login_status("") + self.logged_in_signal.emit() + + def logged_out(self): + """ + Sets the widgets to the logged out state + """ + self.ui.login_widget.show() + self.ui.logged_widget.hide() + + self.set_password("") + self.set_enabled(True) + self.set_status("", error=False) + + def set_login_status(self, msg, error=False): + """ + Sets the status label for the logged in state. + + :param msg: status message + :type msg: str or unicode + :param error: if the status is an erroneous one, then set this + to True + :type error: bool + """ + leap_assert_type(error, bool) + if error: + msg = "<font color='red'><b>%s</b></font>" % (msg,) + self.ui.lblLoginStatus.setText(msg) + self.ui.lblLoginStatus.show() + + def start_logout(self): + """ + Sets the widgets to the logging out state + """ + self.ui.btnLogout.setText(self.tr("Loggin out...")) + self.ui.btnLogout.setEnabled(False) + + def done_logout(self): + """ + Sets the widgets to the logged out state + """ + self.ui.btnLogout.setText(self.tr("Logout")) + self.ui.btnLogout.setEnabled(True) + self.ui.clblErrorMsg.hide() diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py new file mode 100644 index 00000000..ab9052d7 --- /dev/null +++ b/src/leap/bitmask/gui/mail_status.py @@ -0,0 +1,408 @@ +# -*- coding: utf-8 -*- +# mail_status.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/>. +""" +Mail Status Panel widget implementation +""" +import logging + +from PySide import QtCore, QtGui + +from leap.bitmask.platform_init import IS_LINUX +from leap.common.check import leap_assert, leap_assert_type +from leap.common.events import register +from leap.common.events import events_pb2 as proto + +from ui_mail_status import Ui_MailStatusWidget + +logger = logging.getLogger(__name__) + + +class MailStatusWidget(QtGui.QWidget): + """ + Status widget that displays the state of the LEAP Mail service + """ + eip_connection_connected = QtCore.Signal() + _soledad_event = QtCore.Signal(object) + _smtp_event = QtCore.Signal(object) + _imap_event = QtCore.Signal(object) + _keymanager_event = QtCore.Signal(object) + + def __init__(self, parent=None): + """ + Constructor for MailStatusWidget + + :param parent: parent widget for this one. + :type parent: QtGui.QWidget + """ + QtGui.QWidget.__init__(self, parent) + + self._systray = None + + self.ui = Ui_MailStatusWidget() + self.ui.setupUi(self) + + # set systray tooltip status + self._mx_status = "" + + # Set the Mail status icons + self.CONNECTING_ICON = None + self.CONNECTED_ICON = None + self.ERROR_ICON = None + self.CONNECTING_ICON_TRAY = None + self.CONNECTED_ICON_TRAY = None + self.ERROR_ICON_TRAY = None + self._set_mail_icons() + + register(signal=proto.KEYMANAGER_LOOKING_FOR_KEY, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.KEYMANAGER_KEY_FOUND, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + # register(signal=proto.KEYMANAGER_KEY_NOT_FOUND, + # callback=self._mail_handle_keymanager_events, + # reqcbk=lambda req, resp: None) + + register(signal=proto.KEYMANAGER_STARTED_KEY_GENERATION, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.KEYMANAGER_FINISHED_KEY_GENERATION, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.KEYMANAGER_DONE_UPLOADING_KEYS, + callback=self._mail_handle_keymanager_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.SOLEDAD_DONE_DOWNLOADING_KEYS, + callback=self._mail_handle_soledad_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.SOLEDAD_DONE_UPLOADING_KEYS, + callback=self._mail_handle_soledad_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.SMTP_SERVICE_STARTED, + callback=self._mail_handle_smtp_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.SMTP_SERVICE_FAILED_TO_START, + callback=self._mail_handle_smtp_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.IMAP_SERVICE_STARTED, + callback=self._mail_handle_imap_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.IMAP_SERVICE_FAILED_TO_START, + callback=self._mail_handle_imap_events, + reqcbk=lambda req, resp: None) + + register(signal=proto.IMAP_UNREAD_MAIL, + callback=self._mail_handle_imap_events, + reqcbk=lambda req, resp: None) + + self._smtp_started = False + self._imap_started = False + + self._soledad_event.connect( + self._mail_handle_soledad_events_slot) + self._imap_event.connect( + self._mail_handle_imap_events_slot) + self._smtp_event.connect( + self._mail_handle_smtp_events_slot) + self._keymanager_event.connect( + self._mail_handle_keymanager_events_slot) + + def _set_mail_icons(self): + """ + Sets the Mail status icons for the main window and for the tray + + MAC : dark icons + LINUX : dark icons in window, light icons in tray + WIN : light icons + """ + EIP_ICONS = EIP_ICONS_TRAY = ( + ":/images/black/32/wait.png", + ":/images/black/32/on.png", + ":/images/black/32/off.png") + + if IS_LINUX: + EIP_ICONS_TRAY = ( + ":/images/white/32/wait.png", + ":/images/white/32/on.png", + ":/images/white/32/off.png") + + self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) + self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) + self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) + + self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) + self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) + self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) + + # Systray and actions + + def set_systray(self, systray): + """ + Sets the systray object to use. + + :param systray: Systray object + :type systray: QtGui.QSystemTrayIcon + """ + leap_assert_type(systray, QtGui.QSystemTrayIcon) + self._systray = systray + self._systray.setToolTip(self.tr("All services are OFF")) + + def _update_systray_tooltip(self): + """ + Updates the system tray icon tooltip using the eip and mx status. + """ + # TODO: Figure out how to handle this with the two status in different + # classes + # status = self.tr("Encrypted Internet: {0}").format(self._eip_status) + # status += '\n' + # status += self.tr("Mail is {0}").format(self._mx_status) + # self._systray.setToolTip(status) + pass + + def set_action_mail_status(self, action_mail_status): + """ + Sets the action_mail_status to use. + + :param action_mail_status: action_mail_status to be used + :type action_mail_status: QtGui.QAction + """ + leap_assert_type(action_mail_status, QtGui.QAction) + self._action_mail_status = action_mail_status + + def _set_mail_status(self, status, ready=0): + """ + Sets the Mail status in the label and in the tray icon. + + :param status: the status text to display + :type status: unicode + :param ready: 2 or >2 if mx is ready, 0 if stopped, 1 if it's + starting, < 0 if disabled. + :type ready: int + """ + self.ui.lblMailStatus.setText(status) + + self._mx_status = self.tr('OFF') + tray_status = self.tr('Mail is OFF') + + icon = self.ERROR_ICON + if ready == 0: + self.ui.lblMailStatus.setText( + self.tr("You must be logged in to use encrypted email.")) + elif ready == 1: + icon = self.CONNECTING_ICON + self._mx_status = self.tr('Starting..') + tray_status = self.tr('Mail is starting') + elif ready >= 2: + icon = self.CONNECTED_ICON + self._mx_status = self.tr('ON') + tray_status = self.tr('Mail is ON') + elif ready < 0: + tray_status = self.tr("Mail is disabled") + + self.ui.lblMailStatusIcon.setPixmap(icon) + self._action_mail_status.setText(tray_status) + self._update_systray_tooltip() + + def _mail_handle_soledad_events(self, req): + """ + Callback for handling events that are emitted from Soledad + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._soledad_event.emit(req) + + def _mail_handle_soledad_events_slot(self, req): + """ + SLOT + TRIGGER: _mail_handle_soledad_events + + Reacts to an Soledad event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._set_mail_status(self.tr("Starting..."), ready=1) + + ext_status = "" + + if req.event == proto.SOLEDAD_DONE_UPLOADING_KEYS: + ext_status = self.tr("Soledad has started...") + elif req.event == proto.SOLEDAD_DONE_DOWNLOADING_KEYS: + ext_status = self.tr("Soledad is starting, please wait...") + else: + leap_assert(False, + "Don't know how to handle this state: %s" + % (req.event)) + + self._set_mail_status(ext_status, ready=1) + + def _mail_handle_keymanager_events(self, req): + """ + Callback for the KeyManager events + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._keymanager_event.emit(req) + + def _mail_handle_keymanager_events_slot(self, req): + """ + SLOT + TRIGGER: _mail_handle_keymanager_events + + Reacts to an KeyManager event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + # We want to ignore this kind of events once everything has + # started + if self._smtp_started and self._imap_started: + return + + self._set_mail_status(self.tr("Starting..."), ready=1) + + ext_status = "" + + if req.event == proto.KEYMANAGER_LOOKING_FOR_KEY: + ext_status = self.tr("Looking for key for this user") + elif req.event == proto.KEYMANAGER_KEY_FOUND: + ext_status = self.tr("Found key! Starting mail...") + # elif req.event == proto.KEYMANAGER_KEY_NOT_FOUND: + # ext_status = self.tr("Key not found!") + elif req.event == proto.KEYMANAGER_STARTED_KEY_GENERATION: + ext_status = self.tr("Generating new key, please wait...") + elif req.event == proto.KEYMANAGER_FINISHED_KEY_GENERATION: + ext_status = self.tr("Finished generating key!") + elif req.event == proto.KEYMANAGER_DONE_UPLOADING_KEYS: + ext_status = self.tr("Starting mail...") + else: + leap_assert(False, + "Don't know how to handle this state: %s" + % (req.event)) + + self._set_mail_status(ext_status, ready=1) + + def _mail_handle_smtp_events(self, req): + """ + Callback for the SMTP events + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._smtp_event.emit(req) + + def _mail_handle_smtp_events_slot(self, req): + """ + SLOT + TRIGGER: _mail_handle_smtp_events + + Reacts to an SMTP event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + ext_status = "" + + if req.event == proto.SMTP_SERVICE_STARTED: + ext_status = self.tr("SMTP has started...") + self._smtp_started = True + if self._smtp_started and self._imap_started: + self._set_mail_status(self.tr("ON"), ready=2) + ext_status = "" + elif req.event == proto.SMTP_SERVICE_FAILED_TO_START: + ext_status = self.tr("SMTP failed to start, check the logs.") + self._set_mail_status(self.tr("Failed")) + else: + leap_assert(False, + "Don't know how to handle this state: %s" + % (req.event)) + + self._set_mail_status(ext_status, ready=2) + + def _mail_handle_imap_events(self, req): + """ + Callback for the IMAP events + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + self._imap_event.emit(req) + + def _mail_handle_imap_events_slot(self, req): + """ + SLOT + TRIGGER: _mail_handle_imap_events + + Reacts to an IMAP event + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + ext_status = None + + if req.event == proto.IMAP_SERVICE_STARTED: + ext_status = self.tr("IMAP has started...") + self._imap_started = True + if self._smtp_started and self._imap_started: + self._set_mail_status(self.tr("ON"), ready=2) + ext_status = "" + elif req.event == proto.IMAP_SERVICE_FAILED_TO_START: + ext_status = self.tr("IMAP failed to start, check the logs.") + self._set_mail_status(self.tr("Failed")) + elif req.event == proto.IMAP_UNREAD_MAIL: + if self._smtp_started and self._imap_started: + self._set_mail_status(self.tr("%s Unread Emails") % + (req.content), ready=2) + else: + leap_assert(False, # XXX ??? + "Don't know how to handle this state: %s" + % (req.event)) + + if ext_status is not None: + self._set_mail_status(ext_status, ready=1) + + def about_to_start(self): + """ + Displays the correct UI for the point where mail components + haven't really started, but they are about to in a second. + """ + self._set_mail_status(self.tr("About to start, please wait..."), + ready=1) + + def set_disabled(self): + """ + Displays the correct UI for disabled mail. + """ + self._set_mail_status(self.tr("Disabled"), -1) + + def stopped_mail(self): + """ + Displayes the correct UI for the stopped state. + """ + self._set_mail_status(self.tr("OFF")) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 200d68aa..84f09fd9 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -32,13 +32,16 @@ from leap.bitmask.crypto.srpauth import SRPAuth from leap.bitmask.gui.loggerwindow import LoggerWindow from leap.bitmask.gui.login import LoginWidget from leap.bitmask.gui.preferenceswindow import PreferencesWindow +from leap.bitmask.gui.eip_preferenceswindow import EIPPreferencesWindow from leap.bitmask.gui import statemachines -from leap.bitmask.gui.statuspanel import StatusPanelWidget +from leap.bitmask.gui.eip_status import EIPStatusWidget +from leap.bitmask.gui.mail_status import MailStatusWidget from leap.bitmask.gui.wizard import Wizard +from leap.bitmask import provider +from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper -from leap.bitmask.services.eip.eipconfig import EIPConfig -from leap.bitmask.services.eip.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.services.eip import eipconfig # XXX: Soledad might not work out of the box in Windows, issue #2932 from leap.bitmask.services.soledad.soledadbootstrapper import \ SoledadBootstrapper @@ -53,12 +56,12 @@ from leap.bitmask.services.eip.vpnprocess import VPN from leap.bitmask.services.eip.vpnprocess import OpenVPNAlreadyRunning from leap.bitmask.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning -from leap.bitmask.services.eip.vpnlaunchers import VPNLauncherException -from leap.bitmask.services.eip.vpnlaunchers import OpenVPNNotFoundException -from leap.bitmask.services.eip.vpnlaunchers import EIPNoPkexecAvailable -from leap.bitmask.services.eip.vpnlaunchers import \ +from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException +from leap.bitmask.services.eip.vpnlauncher import OpenVPNNotFoundException +from leap.bitmask.services.eip.linuxvpnlauncher import EIPNoPkexecAvailable +from leap.bitmask.services.eip.linuxvpnlauncher import \ EIPNoPolkitAuthAgentAvailable -from leap.bitmask.services.eip.vpnlaunchers import EIPNoTunKextLoaded +from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded from leap.bitmask.util.keyring_helpers import has_keyring from leap.bitmask.util.leap_log_handler import LeapLogHandler @@ -98,6 +101,7 @@ class MainWindow(QtGui.QMainWindow): MX_SERVICE = "mx" # Signals + eip_needs_login = QtCore.Signal([]) new_updates = QtCore.Signal(object) raise_window = QtCore.Signal([]) soledad_ready = QtCore.Signal([]) @@ -147,7 +151,7 @@ class MainWindow(QtGui.QMainWindow): self._login_widget = LoginWidget( self._settings, - self.ui.stackedWidget.widget(self.LOGIN_INDEX)) + self) self.ui.loginLayout.addWidget(self._login_widget) # Qt Signal Connections ##################################### @@ -155,17 +159,18 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.login.connect(self._login) self._login_widget.cancel_login.connect(self._cancel_login) - self._login_widget.show_wizard.connect( - self._launch_wizard) - - self.ui.btnShowLog.clicked.connect(self._show_logger_window) - self.ui.btnPreferences.clicked.connect(self._show_preferences) + self._login_widget.show_wizard.connect(self._launch_wizard) + self._login_widget.logout.connect(self._logout) - self._status_panel = StatusPanelWidget( - self.ui.stackedWidget.widget(self.EIP_STATUS_INDEX)) - self.ui.statusLayout.addWidget(self._status_panel) + self._eip_status = EIPStatusWidget(self) + self.ui.eipLayout.addWidget(self._eip_status) + self._login_widget.logged_in_signal.connect( + self._eip_status.enable_eip_start) + self._login_widget.logged_in_signal.connect( + self._enable_eip_start_action) - self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) + self._mail_status = MailStatusWidget(self) + self.ui.mailLayout.addWidget(self._mail_status) self._eip_connection = EIPConnection() @@ -173,15 +178,19 @@ class MainWindow(QtGui.QMainWindow): self._start_eip) self._eip_connection.qtsigs.disconnecting_signal.connect( self._stop_eip) - self._status_panel.eip_connection_connected.connect( + self._eip_status.eip_connection_connected.connect( self._on_eip_connected) + self.eip_needs_login.connect( + self._eip_status.disable_eip_start) + self.eip_needs_login.connect( + self._disable_eip_start_action) # This is loaded only once, there's a bug when doing that more # than once self._provider_config = ProviderConfig() # Used for automatic start of EIP self._provisional_provider_config = ProviderConfig() - self._eip_config = EIPConfig() + self._eip_config = eipconfig.EIPConfig() self._already_started_eip = False @@ -221,9 +230,9 @@ class MainWindow(QtGui.QMainWindow): self._finish_eip_bootstrap) self._vpn = VPN(openvpn_verb=openvpn_verb) self._vpn.qtsigs.state_changed.connect( - self._status_panel.update_vpn_state) + self._eip_status.update_vpn_state) self._vpn.qtsigs.status_changed.connect( - self._status_panel.update_vpn_status) + self._eip_status.update_vpn_status) self._vpn.qtsigs.process_finished.connect( self._eip_finished) @@ -234,17 +243,23 @@ class MainWindow(QtGui.QMainWindow): self._soledad_bootstrapped_stage) self._soledad_bootstrapper.soledad_timeout.connect( self._retry_soledad_connection) + # XXX missing connect to soledad_failed (signal unrecoverable to user) + # TODO wait until chiiph ui refactor. self._smtp_bootstrapper = SMTPBootstrapper() self._smtp_bootstrapper.download_config.connect( self._smtp_bootstrapped_stage) - self.ui.action_log_out.setEnabled(False) - self.ui.action_log_out.triggered.connect(self._logout) self.ui.action_about_leap.triggered.connect(self._about) self.ui.action_quit.triggered.connect(self.quit) self.ui.action_wizard.triggered.connect(self._launch_wizard) self.ui.action_show_logs.triggered.connect(self._show_logger_window) + self.ui.action_create_new_account.triggered.connect( + self._launch_wizard) + + if IS_MAC: + self.ui.menuFile.menuAction().setText(self.tr("Util")) + self.raise_window.connect(self._do_raise_mainwindow) # Used to differentiate between real quits and close to tray @@ -254,17 +269,17 @@ class MainWindow(QtGui.QMainWindow): self._action_mail_status = QtGui.QAction(self.tr("Mail is OFF"), self) self._action_mail_status.setEnabled(False) - self._status_panel.set_action_mail_status(self._action_mail_status) + self._mail_status.set_action_mail_status(self._action_mail_status) self._action_eip_startstop = QtGui.QAction("", self) - self._status_panel.set_action_eip_startstop(self._action_eip_startstop) - - self._action_preferences = QtGui.QAction(self.tr("Preferences"), self) - self._action_preferences.triggered.connect(self._show_preferences) + self._eip_status.set_action_eip_startstop(self._action_eip_startstop) self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self) self._action_visible.triggered.connect(self._toggle_visible) + self.ui.btnPreferences.clicked.connect(self._show_preferences) + self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences) + self._enabled_services = [] self._center_window() @@ -280,6 +295,7 @@ class MainWindow(QtGui.QMainWindow): self.mail_client_logged_in.connect(self._fetch_incoming_mail) self.logout.connect(self._stop_imap_service) self.logout.connect(self._stop_smtp_service) + self.logout.connect(self._mail_status.stopped_mail) ################################# end Qt Signals connection ######## @@ -296,6 +312,7 @@ class MainWindow(QtGui.QMainWindow): self._soledad_ready = False self._keymanager = None self._smtp_service = None + self._smtp_port = None self._imap_service = None self._login_defer = None @@ -303,27 +320,29 @@ class MainWindow(QtGui.QMainWindow): self._smtp_config = SMTPConfig() - if self._first_run(): - self._wizard_firstrun = True - self._wizard = Wizard(bypass_checks=bypass_checks) - # Give this window time to finish init and then show the wizard - QtCore.QTimer.singleShot(1, self._launch_wizard) - self._wizard.accepted.connect(self._finish_init) - self._wizard.rejected.connect(self._rejected_wizard) - else: - self._finish_init() - # Eip machine is a public attribute where the state machine for # the eip connection will be available to the different components. # Remember that this will not live in the +1600LOC mainwindow for # all the eternity, so at some point we will be moving this to # the EIPConductor or some other clever component that we will # instantiate from here. - self.eip_machine = None + self.eip_machine = None # start event machines self.start_eip_machine() + if self._first_run(): + self._wizard_firstrun = True + self._wizard = Wizard(bypass_checks=bypass_checks) + # Give this window time to finish init and then show the wizard + QtCore.QTimer.singleShot(1, self._launch_wizard) + self._wizard.accepted.connect(self._finish_init) + self._wizard.rejected.connect(self._rejected_wizard) + else: + # during finish_init, we disable the eip start button + # so this has to be done after eip_machine is started + self._finish_init() + def _rejected_wizard(self): """ SLOT @@ -391,7 +410,6 @@ class MainWindow(QtGui.QMainWindow): 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. @@ -405,18 +423,13 @@ class MainWindow(QtGui.QMainWindow): self._logger_window = LoggerWindow(handler=leap_log_handler) self._logger_window.setVisible( not self._logger_window.isVisible()) - self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) else: self._logger_window.setVisible(not self._logger_window.isVisible()) - self.ui.btnShowLog.setChecked(self._logger_window.isVisible()) - - self._logger_window.finished.connect(self._uncheck_logger_button) def _show_preferences(self): """ SLOT TRIGGERS: - self.ui.action_show_preferences.triggered self.ui.btnPreferences.clicked Displays the preferences window. @@ -431,22 +444,25 @@ class MainWindow(QtGui.QMainWindow): preferences_window.show() - def _set_soledad_ready(self): + def _show_eip_preferences(self): """ SLOT TRIGGERS: - self.soledad_ready + self.ui.btnEIPPreferences.clicked - It sets the soledad object as ready to use. + Displays the EIP preferences window. """ - self._soledad_ready = True + EIPPreferencesWindow(self).show() - def _uncheck_logger_button(self): + def _set_soledad_ready(self): """ SLOT - Sets the checked state of the loggerwindow button to false. + TRIGGERS: + self.soledad_ready + + It sets the soledad object as ready to use. """ - self.ui.btnShowLog.setChecked(False) + self._soledad_ready = True def _new_updates_available(self, req): """ @@ -579,21 +595,29 @@ class MainWindow(QtGui.QMainWindow): """ Tries to autostart EIP """ - default_provider = self._settings.get_defaultprovider() + settings = self._settings + + should_autostart = settings.get_autostart_eip() + if not should_autostart: + logger.debug('Will not autostart EIP since it is setup ' + 'to not to do it') + self.eip_needs_login.emit() + return + + default_provider = settings.get_defaultprovider() if default_provider is None: logger.info("Cannot autostart Encrypted Internet because there is " "no default provider configured") + self.eip_needs_login.emit() return - self._enabled_services = self._settings.get_enabled_services( + self._enabled_services = settings.get_enabled_services( default_provider) - if self._provisional_provider_config.load( - os.path.join("leap", - "providers", - default_provider, - "provider.json")): + loaded = self._provisional_provider_config.load( + provider.get_provider_path(default_provider)) + if loaded: # XXX I think we should not try to re-download config every time, # it adds some delay. # Maybe if it's the first run in a session, @@ -601,6 +625,7 @@ class MainWindow(QtGui.QMainWindow): self._download_eip_config() else: # XXX: Display a proper message to the user + self.eip_needs_login.emit() logger.error("Unable to load %s config, cannot autostart." % (default_provider,)) @@ -621,24 +646,20 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addAction(self._action_visible) systrayMenu.addSeparator() - eip_menu = systrayMenu.addMenu(self.tr("Encrypted Internet is OFF")) + eip_menu = systrayMenu.addMenu(self.tr("Encrypted Internet: OFF")) eip_menu.addAction(self._action_eip_startstop) - self._status_panel.set_eip_status_menu(eip_menu) + self._eip_status.set_eip_status_menu(eip_menu) systrayMenu.addAction(self._action_mail_status) systrayMenu.addSeparator() - systrayMenu.addAction(self._action_preferences) - systrayMenu.addAction(help_action) - systrayMenu.addSeparator() - systrayMenu.addAction(self.ui.action_log_out) systrayMenu.addAction(self.ui.action_quit) self._systray = QtGui.QSystemTrayIcon(self) self._systray.setContextMenu(systrayMenu) - self._systray.setIcon(self._status_panel.ERROR_ICON_TRAY) + self._systray.setIcon(self._eip_status.ERROR_ICON_TRAY) self._systray.setVisible(True) self._systray.activated.connect(self._tray_activated) - self._status_panel.set_systray(self._systray) + self._eip_status.set_systray(self._systray) def _tray_activated(self, reason=None): """ @@ -746,9 +767,10 @@ class MainWindow(QtGui.QMainWindow): """ Reimplements the changeEvent method to minimize to tray """ - if QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ - e.type() == QtCore.QEvent.WindowStateChange and \ - self.isMinimized(): + if not IS_MAC and \ + QtGui.QSystemTrayIcon.isSystemTrayAvailable() and \ + e.type() == QtCore.QEvent.WindowStateChange and \ + self.isMinimized(): self._toggle_visible() e.accept() return @@ -844,47 +866,8 @@ class MainWindow(QtGui.QMainWindow): """ leap_assert(self._provider_config, "We need a provider config") - 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._login_widget.get_selected_provider()) - - if len(provider) == 0: - self._login_widget.set_status( - self.tr("Please select a valid provider")) - return - - if len(username) == 0: - self._login_widget.set_status( - self.tr("Please provide a valid username")) - return - - if len(password) == 0: - self._login_widget.set_status( - self.tr("Please provide a valid Password")) - return - - self._login_widget.set_status(self.tr("Logging in..."), error=False) - self._login_widget.set_enabled(False) - - if self._login_widget.get_remember() and has_keyring(): - # in the keyring and in the settings - # we store the value 'usename@provider' - username_domain = (username + '@' + provider).encode("utf8") - try: - keyring.set_password(self.KEYRING_KEY, - username_domain, - password.encode("utf8")) - # Only save the username if it was saved correctly in - # the keyring - self._settings.set_user(username_domain) - except Exception as e: - logger.error("Problem saving data to keyring. %r" - % (e,)) - - self._download_provider_config() + if self._login_widget.start_login(): + self._download_provider_config() def _cancel_login(self): """ @@ -952,7 +935,6 @@ class MainWindow(QtGui.QMainWindow): if ok: self._logged_user = self._login_widget.get_user() - self.ui.action_log_out.setEnabled(True) # We leave a bit of room for the user to see the # "Succeeded" message and then we switch to the EIP status # panel @@ -967,20 +949,25 @@ class MainWindow(QtGui.QMainWindow): Changes the stackedWidget index to the EIP status one and triggers the eip bootstrapping """ - if not self._already_started_eip: - self._status_panel.set_provider( - "%s@%s" % (self._login_widget.get_user(), - self._get_best_provider_config().get_domain())) - self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) + self._login_widget.logged_in() + + self._enabled_services = self._settings.get_enabled_services( + self._provider_config.get_domain()) # TODO separate UI from logic. # TODO soledad should check if we want to run only over EIP. - self._soledad_bootstrapper.run_soledad_setup_checks( - self._provider_config, - self._login_widget.get_user(), - self._login_widget.get_password(), - download_if_needed=True) + if self._provider_config.provides_mx() and \ + self._enabled_services.count(self.MX_SERVICE) > 0: + self._mail_status.about_to_start() + + self._soledad_bootstrapper.run_soledad_setup_checks( + self._provider_config, + self._login_widget.get_user(), + self._login_widget.get_password(), + download_if_needed=True) + else: + self._mail_status.set_disabled() self._download_eip_config() @@ -1007,6 +994,7 @@ class MainWindow(QtGui.QMainWindow): """ Retries soledad connection. """ + # XXX should move logic to soledad boostrapper itself logger.debug("Retrying soledad connection.") if self._soledad_bootstrapper.should_retry_initialization(): self._soledad_bootstrapper.increment_retries_count() @@ -1031,8 +1019,9 @@ class MainWindow(QtGui.QMainWindow): """ passed = data[self._soledad_bootstrapper.PASSED_KEY] if not passed: + # TODO should actually *display* on the panel. logger.debug("ERROR on soledad bootstrapping:") - logger.error(data[self._soledad_bootstrapper.ERROR_KEY]) + logger.error("%r" % data[self._soledad_bootstrapper.ERROR_KEY]) return else: logger.debug("Done bootstrapping Soledad") @@ -1116,7 +1105,7 @@ class MainWindow(QtGui.QMainWindow): # the specific default. from leap.mail.smtp import setup_smtp_relay - self._smtp_service = setup_smtp_relay( + self._smtp_service, self._smtp_port = setup_smtp_relay( port=2013, keymanager=self._keymanager, smtp_host=host, @@ -1136,6 +1125,7 @@ class MainWindow(QtGui.QMainWindow): # but in the imap case we are just stopping the fetcher. if self._smtp_service is not None: logger.debug('Stopping smtp service.') + self._smtp_port.stopListening() self._smtp_service.doStop() ################################################################### @@ -1195,22 +1185,37 @@ class MainWindow(QtGui.QMainWindow): """ Initializes and starts the EIP state machine """ - button = self._status_panel.eip_button + button = self._eip_status.eip_button action = self._action_eip_startstop - label = self._status_panel.eip_label + label = self._eip_status.eip_label builder = statemachines.ConnectionMachineBuilder(self._eip_connection) eip_machine = builder.make_machine(button=button, action=action, label=label) self.eip_machine = eip_machine self.eip_machine.start() + logger.debug('eip machine started') + + @QtCore.Slot() + def _disable_eip_start_action(self): + """ + Disables the EIP start action in the systray menu. + """ + self._action_eip_startstop.setEnabled(False) + + @QtCore.Slot() + def _enable_eip_start_action(self): + """ + Enables the EIP start action in the systray menu. + """ + self._action_eip_startstop.setEnabled(True) @QtCore.Slot() def _on_eip_connected(self): """ SLOT TRIGGERS: - self._status_panel.eip_connection_connected + self._eip_status.eip_connection_connected Emits the EIPConnection.qtsigs.connected_signal This is a little workaround for connecting the vpn-connected @@ -1225,7 +1230,7 @@ class MainWindow(QtGui.QMainWindow): """ SLOT TRIGGERS: - self._status_panel.start_eip + self._eip_status.start_eip self._action_eip_startstop.triggered or called from _finish_eip_bootstrap @@ -1233,9 +1238,30 @@ class MainWindow(QtGui.QMainWindow): """ provider_config = self._get_best_provider_config() provider = provider_config.get_domain() - self._status_panel.eip_pre_up() + self._eip_status.eip_pre_up() self.user_stopped_eip = False + # until we set an option in the preferences window, + # we'll assume that by default we try to autostart. + # If we switch it off manually, it won't try the next + # time. + self._settings.set_autostart_eip(True) + + loaded = eipconfig.load_eipconfig_if_needed( + provider_config, self._eip_config, provider) + + if not loaded: + self._eip_status.set_eip_status( + self.tr("Could not load Encrypted Internet " + "Configuration."), + error=True) + # signal connection aborted to state machine + qtsigs = self._eip_connection.qtsigs + qtsigs.connection_aborted_signal.emit() + logger.error("Tried to start EIP but cannot find any " + "available provider!") + return + try: # XXX move this to EIPConductor host, port = get_openvpn_management() @@ -1244,16 +1270,14 @@ class MainWindow(QtGui.QMainWindow): socket_host=host, socket_port=port) self._settings.set_defaultprovider(provider) - if self._logged_user is not None: - provider = "%s@%s" % (self._logged_user, provider) # XXX move to the state machine too - self._status_panel.set_provider(provider) + self._eip_status.set_provider(provider) # TODO refactor exceptions so they provide translatable # usef-facing messages. except EIPNoPolkitAuthAgentAvailable: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( # XXX this should change to polkit-kde where # applicable. self.tr("We could not find any " @@ -1266,30 +1290,30 @@ class MainWindow(QtGui.QMainWindow): error=True) self._set_eipstatus_off() except EIPNoTunKextLoaded: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Encrypted Internet cannot be started because " "the tuntap extension is not installed properly " "in your system.")) self._set_eipstatus_off() except EIPNoPkexecAvailable: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("We could not find <b>pkexec</b> " "in your system."), error=True) self._set_eipstatus_off() except OpenVPNNotFoundException: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("We could not find openvpn binary."), error=True) self._set_eipstatus_off() except OpenVPNAlreadyRunning as e: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Another openvpn instance is already running, and " "could not be stopped."), error=True) self._set_eipstatus_off() except AlienOpenVPNAlreadyRunning as e: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Another openvpn instance is already running, and " "could not be stopped because it was not launched by " "Bitmask. Please stop it and try again."), @@ -1298,17 +1322,17 @@ class MainWindow(QtGui.QMainWindow): except VPNLauncherException as e: # XXX We should implement again translatable exceptions so # we can pass a translatable string to the panel (usermessage attr) - self._status_panel.set_global_status("%s" % (e,), error=True) + self._eip_status.set_eip_status("%s" % (e,), error=True) self._set_eipstatus_off() else: self._already_started_eip = True @QtCore.Slot() - def _stop_eip(self, abnormal=False): + def _stop_eip(self): """ SLOT TRIGGERS: - self._status_panel.stop_eip + self._eip_status.stop_eip self._action_eip_startstop.triggered or called from _eip_finished @@ -1318,29 +1342,28 @@ class MainWindow(QtGui.QMainWindow): :param abnormal: whether this was an abnormal termination. :type abnormal: bool """ - if abnormal: - logger.warning("Abnormal EIP termination.") - self.user_stopped_eip = True self._vpn.terminate() - self._set_eipstatus_off() - + self._set_eipstatus_off(False) self._already_started_eip = False - # XXX do via signal - self._settings.set_defaultprovider(None) + logger.debug('Setting autostart to: False') + self._settings.set_autostart_eip(False) + if self._logged_user: - self._status_panel.set_provider( + self._eip_status.set_provider( "%s@%s" % (self._logged_user, self._get_best_provider_config().get_domain())) + self._eip_status.eip_stopped() - def _set_eipstatus_off(self): + def _set_eipstatus_off(self, error=True): """ Sets eip status to off """ - self._status_panel.set_eip_status(self.tr("OFF"), error=True) - self._status_panel.set_eip_status_icon("error") + self._eip_status.set_eip_status(self.tr("EIP has stopped"), + error=error) + self._eip_status.set_eip_status_icon("error") def _download_eip_config(self): """ @@ -1355,7 +1378,7 @@ class MainWindow(QtGui.QMainWindow): not self._already_started_eip: # XXX this should be handled by the state machine. - self._status_panel.set_eip_status( + self._eip_status.set_eip_status( self.tr("Starting...")) self._eip_bootstrapper.run_eip_setup_checks( provider_config, @@ -1363,11 +1386,11 @@ class MainWindow(QtGui.QMainWindow): self._already_started_eip = True elif not self._already_started_eip: if self._enabled_services.count(self.OPENVPN_SERVICE) > 0: - self._status_panel.set_eip_status( + self._eip_status.set_eip_status( self.tr("Not supported"), error=True) else: - self._status_panel.set_eip_status(self.tr("Disabled")) + self._eip_status.set_eip_status(self.tr("Disabled")) def _finish_eip_bootstrap(self, data): """ @@ -1382,7 +1405,7 @@ class MainWindow(QtGui.QMainWindow): if not passed: error_msg = self.tr("There was a problem with the provider") - self._status_panel.set_eip_status(error_msg, error=True) + self._eip_status.set_eip_status(error_msg, error=True) logger.error(data[self._eip_bootstrapper.ERROR_KEY]) self._already_started_eip = False return @@ -1390,19 +1413,15 @@ class MainWindow(QtGui.QMainWindow): provider_config = self._get_best_provider_config() domain = provider_config.get_domain() - loaded = self._eip_config.loaded() - if not loaded: - eip_config_path = os.path.join("leap", "providers", - domain, "eip-service.json") - api_version = provider_config.get_api_version() - self._eip_config.set_api_version(api_version) - loaded = self._eip_config.load(eip_config_path) + # XXX move check to _start_eip ? + loaded = eipconfig.load_eipconfig_if_needed( + provider_config, self._eip_config, domain) if loaded: # DO START EIP Connection! self._eip_connection.qtsigs.do_connect_signal.emit() else: - self._status_panel.set_eip_status( + self._eip_status.set_eip_status( self.tr("Could not load Encrypted Internet " "Configuration."), error=True) @@ -1437,7 +1456,7 @@ class MainWindow(QtGui.QMainWindow): def _logout(self): """ SLOT - TRIGGER: self.ui.action_log_out.triggered + TRIGGER: self._login_widget.logout Starts the logout sequence """ @@ -1457,16 +1476,17 @@ class MainWindow(QtGui.QMainWindow): Switches the stackedWidget back to the login stage after logging out """ + self._login_widget.done_logout() + if ok: self._logged_user = None - self.ui.action_log_out.setEnabled(False) - self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) - self._login_widget.set_password("") - self._login_widget.set_enabled(True) - self._login_widget.set_status("") + + self._login_widget.logged_out() + else: - status_text = self.tr("Something went wrong with the logout.") - self._status_panel.set_global_status(status_text, error=True) + self._login_widget.set_login_status( + self.tr("Something went wrong with the logout."), + error=True) def _intermediate_stage(self, data): """ @@ -1531,37 +1551,36 @@ class MainWindow(QtGui.QMainWindow): # TODO we should have a way of parsing the latest lines in the vpn # log buffer so we can have a more precise idea of which type # of error did we have (server side, local problem, etc) - abnormal = True + + qtsigs = self._eip_connection.qtsigs + signal = qtsigs.disconnected_signal # XXX check if these exitCodes are pkexec/cocoasudo specific if exitCode in (126, 127): - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Encrypted Internet could not be launched " "because you did not authenticate properly."), error=True) self._vpn.killit() + signal = qtsigs.connection_aborted_signal + elif exitCode != 0 or not self.user_stopped_eip: - self._status_panel.set_global_status( + self._eip_status.set_eip_status( self.tr("Encrypted Internet finished in an " "unexpected manner!"), error=True) - else: - abnormal = False + signal = qtsigs.connection_died_signal + if exitCode == 0 and IS_MAC: # XXX remove this warning after I fix cocoasudo. logger.warning("The above exit code MIGHT BE WRONG.") - # We emit signals to trigger transitions in the state machine: - qtsigs = self._eip_connection.qtsigs - if abnormal: - signal = qtsigs.connection_died_signal - else: - signal = qtsigs.disconnected_signal - # XXX verify that the logic kees the same w/o the abnormal flag # after the refactor to EIPConnection has been completed # (eipconductor taking the most of the logic under transitions # that right now are handled under status_panel) #self._stop_eip(abnormal) + + # We emit signals to trigger transitions in the state machine: signal.emit() def _on_raise_window_event(self, req): diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index 2d17f6c2..58cb05ba 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -27,11 +27,10 @@ from PySide import QtCore, QtGui from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.gui.ui_preferences import Ui_Preferences from leap.soledad.client import NoStorageSecret -from leap.bitmask.crypto.srpauth import SRPAuthBadPassword +from leap.bitmask.crypto.srpauth import SRPAuthBadUserOrPassword from leap.bitmask.util.password import basic_password_checks from leap.bitmask.services import get_supported from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector from leap.bitmask.services import get_service_display_name logger = logging.getLogger(__name__) @@ -60,7 +59,6 @@ class PreferencesWindow(QtGui.QDialog): self.ui.setupUi(self) self.ui.lblPasswordChangeStatus.setVisible(False) self.ui.lblProvidersServicesStatus.setVisible(False) - self.ui.lblProvidersGatewayStatus.setVisible(False) self._selected_services = set() @@ -68,11 +66,6 @@ class PreferencesWindow(QtGui.QDialog): self.ui.pbChangePassword.clicked.connect(self._change_password) self.ui.cbProvidersServices.currentIndexChanged[unicode].connect( self._populate_services) - self.ui.cbProvidersGateway.currentIndexChanged[unicode].connect( - self._populate_gateways) - - self.ui.cbGateways.currentIndexChanged[unicode].connect( - lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) if not self._settings.get_configured_providers(): self.ui.gbEnabledServices.setEnabled(False) @@ -185,7 +178,7 @@ class PreferencesWindow(QtGui.QDialog): logger.error("Error changing password: %s", (failure, )) problem = self.tr("There was a problem changing the password.") - if failure.check(SRPAuthBadPassword): + if failure.check(SRPAuthBadUserOrPassword): problem = self.tr("You did not enter a correct current password.") self._set_password_change_status(problem, error=True) @@ -217,37 +210,13 @@ class PreferencesWindow(QtGui.QDialog): self.ui.lblProvidersServicesStatus.setVisible(True) self.ui.lblProvidersServicesStatus.setText(status) - def _set_providers_gateway_status(self, status, success=False, - error=False): - """ - Sets the status label for the gateway change. - - :param status: status message to display, can be HTML - :type status: str - :param success: is set to True if we should display the - message as green - :type success: bool - :param error: is set to True if we should display the - message as red - :type error: bool - """ - if success: - status = "<font color='green'><b>%s</b></font>" % (status,) - elif error: - status = "<font color='red'><b>%s</b></font>" % (status,) - - self.ui.lblProvidersGatewayStatus.setVisible(True) - self.ui.lblProvidersGatewayStatus.setText(status) - def _add_configured_providers(self): """ Add the client's configured providers to the providers combo boxes. """ self.ui.cbProvidersServices.clear() - self.ui.cbProvidersGateway.clear() for provider in self._settings.get_configured_providers(): self.ui.cbProvidersServices.addItem(provider) - self.ui.cbProvidersGateway.addItem(provider) def _service_selection_changed(self, service, state): """ @@ -366,90 +335,3 @@ class PreferencesWindow(QtGui.QDialog): provider_config = None return provider_config - - def _save_selected_gateway(self, provider): - """ - SLOT - TRIGGERS: - self.ui.pbSaveGateway.clicked - - Saves the new gateway setting to the configuration file. - - :param provider: the provider config that we need to save. - :type provider: str - """ - gateway = self.ui.cbGateways.currentText() - - if gateway == self.AUTOMATIC_GATEWAY_LABEL: - gateway = self._settings.GATEWAY_AUTOMATIC - else: - idx = self.ui.cbGateways.currentIndex() - gateway = self.ui.cbGateways.itemData(idx) - - self._settings.set_selected_gateway(provider, gateway) - - msg = self.tr( - "Gateway settings for provider '{0}' saved.".format(provider)) - logger.debug(msg) - self._set_providers_gateway_status(msg, success=True) - - def _populate_gateways(self, domain): - """ - SLOT - TRIGGERS: - self.ui.cbProvidersGateway.currentIndexChanged[unicode] - - Loads the gateways that the provider provides into the UI for - the user to select. - - :param domain: the domain of the provider to load gateways from. - :type domain: str - """ - # We hide the maybe-visible status label after a change - self.ui.lblProvidersGatewayStatus.setVisible(False) - - if not domain: - return - - try: - # disconnect prevoiusly connected save method - self.ui.pbSaveGateway.clicked.disconnect() - except RuntimeError: - pass # Signal was not connected - - # set the proper connection for the 'save' button - save_gateway = partial(self._save_selected_gateway, domain) - self.ui.pbSaveGateway.clicked.connect(save_gateway) - - eip_config = EIPConfig() - provider_config = self._get_provider_config(domain) - - eip_config_path = os.path.join("leap", "providers", - domain, "eip-service.json") - api_version = provider_config.get_api_version() - eip_config.set_api_version(api_version) - eip_loaded = eip_config.load(eip_config_path) - - if not eip_loaded or provider_config is None: - self._set_providers_gateway_status( - self.tr("There was a problem with configuration files."), - error=True) - return - - gateways = VPNGatewaySelector(eip_config).get_gateways_list() - logger.debug(gateways) - - self.ui.cbGateways.clear() - self.ui.cbGateways.addItem(self.AUTOMATIC_GATEWAY_LABEL) - - # Add the available gateways and - # select the one stored in configuration file. - selected_gateway = self._settings.get_selected_gateway(domain) - index = 0 - for idx, (gw_name, gw_ip) in enumerate(gateways): - gateway = "{0} ({1})".format(gw_name, gw_ip) - self.ui.cbGateways.addItem(gateway, gw_ip) - if gw_ip == selected_gateway: - index = idx + 1 - - self.ui.cbGateways.setCurrentIndex(index) diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index c3dd5ed3..94726720 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -128,11 +128,25 @@ class ConnectionMachineBuilder(object): states[_OFF]) # * If we receive the connection_died, we transition - # to the off state + # from on directly to the off state states[_ON].addTransition( conn.qtsigs.connection_died_signal, states[_OFF]) + # * If we receive the connection_aborted, we transition + # from connecting to the off state + states[_CON].addTransition( + conn.qtsigs.connection_aborted_signal, + states[_OFF]) + # * Connection died can in some cases also be + # triggered while we are in CONNECTING + # state. I should be avoided, since connection_aborted + # is clearer (and reserve connection_died + # for transitions from on->off + states[_CON].addTransition( + conn.qtsigs.connection_died_signal, + states[_OFF]) + # adding states to the machine for state in states.itervalues(): machine.addState(state) diff --git a/src/leap/bitmask/gui/statuspanel.py b/src/leap/bitmask/gui/statuspanel.py deleted file mode 100644 index 679f00b1..00000000 --- a/src/leap/bitmask/gui/statuspanel.py +++ /dev/null @@ -1,710 +0,0 @@ -# -*- coding: utf-8 -*- -# statuspanel.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/>. -""" -Status Panel widget implementation -""" -import logging - -from datetime import datetime -from functools import partial - -from PySide import QtCore, QtGui - -from leap.bitmask.services.eip.connection import EIPConnection -from leap.bitmask.services.eip.vpnprocess import VPNManager -from leap.bitmask.platform_init import IS_WIN, IS_LINUX -from leap.bitmask.util.averages import RateMovingAverage -from leap.common.check import leap_assert, leap_assert_type -from leap.common.events import register -from leap.common.events import events_pb2 as proto - -from ui_statuspanel import Ui_StatusPanel - -logger = logging.getLogger(__name__) - - -class StatusPanelWidget(QtGui.QWidget): - """ - Status widget that displays the current state of the LEAP services - """ - DISPLAY_TRAFFIC_RATES = True - RATE_STR = "%14.2f KB/s" - TOTAL_STR = "%14.2f Kb" - - MAIL_OFF_ICON = ":/images/mail-unlocked.png" - MAIL_ON_ICON = ":/images/mail-locked.png" - - eip_connection_connected = QtCore.Signal() - _soledad_event = QtCore.Signal(object) - _smtp_event = QtCore.Signal(object) - _imap_event = QtCore.Signal(object) - _keymanager_event = QtCore.Signal(object) - - def __init__(self, parent=None): - QtGui.QWidget.__init__(self, parent) - - self._systray = None - self._eip_status_menu = None - - self.ui = Ui_StatusPanel() - self.ui.setupUi(self) - - self.eipconnection = EIPConnection() - - self.hide_status_box() - - # set systray tooltip statuses - self._eip_status = self._mx_status = "" - - # Set the EIP status icons - self.CONNECTING_ICON = None - self.CONNECTED_ICON = None - self.ERROR_ICON = None - self.CONNECTING_ICON_TRAY = None - self.CONNECTED_ICON_TRAY = None - self.ERROR_ICON_TRAY = None - self._set_eip_icons() - - self._set_traffic_rates() - self._make_status_clickable() - - register(signal=proto.KEYMANAGER_LOOKING_FOR_KEY, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.KEYMANAGER_KEY_FOUND, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - # register(signal=proto.KEYMANAGER_KEY_NOT_FOUND, - # callback=self._mail_handle_keymanager_events, - # reqcbk=lambda req, resp: None) - - register(signal=proto.KEYMANAGER_STARTED_KEY_GENERATION, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.KEYMANAGER_FINISHED_KEY_GENERATION, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.KEYMANAGER_DONE_UPLOADING_KEYS, - callback=self._mail_handle_keymanager_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.SOLEDAD_DONE_DOWNLOADING_KEYS, - callback=self._mail_handle_soledad_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.SOLEDAD_DONE_UPLOADING_KEYS, - callback=self._mail_handle_soledad_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.SMTP_SERVICE_STARTED, - callback=self._mail_handle_smtp_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.SMTP_SERVICE_FAILED_TO_START, - callback=self._mail_handle_smtp_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.IMAP_SERVICE_STARTED, - callback=self._mail_handle_imap_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.IMAP_SERVICE_FAILED_TO_START, - callback=self._mail_handle_imap_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.IMAP_UNREAD_MAIL, - callback=self._mail_handle_imap_events, - reqcbk=lambda req, resp: None) - - self._set_long_mail_status("") - self.ui.lblUnread.setVisible(False) - - self._smtp_started = False - self._imap_started = False - - self._soledad_event.connect( - self._mail_handle_soledad_events_slot) - self._imap_event.connect( - self._mail_handle_imap_events_slot) - self._smtp_event.connect( - self._mail_handle_smtp_events_slot) - self._keymanager_event.connect( - self._mail_handle_keymanager_events_slot) - - def _make_status_clickable(self): - """ - Makes upload and download figures clickable. - """ - onclicked = self._on_VPN_status_clicked - self.ui.btnUpload.clicked.connect(onclicked) - self.ui.btnDownload.clicked.connect(onclicked) - - def _on_VPN_status_clicked(self): - """ - SLOT - TRIGGER: self.ui.btnUpload.clicked - self.ui.btnDownload.clicked - - Toggles between rate and total throughput display for vpn - status figures. - """ - self.DISPLAY_TRAFFIC_RATES = not self.DISPLAY_TRAFFIC_RATES - self.update_vpn_status(None) # refresh - - def _set_traffic_rates(self): - """ - Initializes up and download rates. - """ - self._up_rate = RateMovingAverage() - self._down_rate = RateMovingAverage() - - self.ui.btnUpload.setText(self.RATE_STR % (0,)) - self.ui.btnDownload.setText(self.RATE_STR % (0,)) - - def _reset_traffic_rates(self): - """ - Resets up and download rates, and cleans up the labels. - """ - self._up_rate.reset() - self._down_rate.reset() - self.update_vpn_status(None) - - def _update_traffic_rates(self, up, down): - """ - Updates up and download rates. - - :param up: upload total. - :type up: int - :param down: download total. - :type down: int - """ - ts = datetime.now() - self._up_rate.append((ts, up)) - self._down_rate.append((ts, down)) - - def _get_traffic_rates(self): - """ - Gets the traffic rates (in KB/s). - - :returns: a tuple with the (up, down) rates - :rtype: tuple - """ - up = self._up_rate - down = self._down_rate - - return (up.get_average(), down.get_average()) - - def _get_traffic_totals(self): - """ - Gets the traffic total throughput (in Kb). - - :returns: a tuple with the (up, down) totals - :rtype: tuple - """ - up = self._up_rate - down = self._down_rate - - return (up.get_total(), down.get_total()) - - def _set_eip_icons(self): - """ - Sets the EIP status icons for the main window and for the tray - - MAC : dark icons - LINUX : dark icons in window, light icons in tray - WIN : light icons - """ - EIP_ICONS = EIP_ICONS_TRAY = ( - ":/images/conn_connecting-light.png", - ":/images/conn_connected-light.png", - ":/images/conn_error-light.png") - - if IS_LINUX: - EIP_ICONS_TRAY = ( - ":/images/conn_connecting.png", - ":/images/conn_connected.png", - ":/images/conn_error.png") - elif IS_WIN: - EIP_ICONS = EIP_ICONS_TRAY = ( - ":/images/conn_connecting.png", - ":/images/conn_connected.png", - ":/images/conn_error.png") - - self.CONNECTING_ICON = QtGui.QPixmap(EIP_ICONS[0]) - self.CONNECTED_ICON = QtGui.QPixmap(EIP_ICONS[1]) - self.ERROR_ICON = QtGui.QPixmap(EIP_ICONS[2]) - - self.CONNECTING_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[0]) - self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1]) - self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) - - # Systray and actions - - def set_systray(self, systray): - """ - Sets the systray object to use. - - :param systray: Systray object - :type systray: QtGui.QSystemTrayIcon - """ - leap_assert_type(systray, QtGui.QSystemTrayIcon) - self._systray = systray - self._systray.setToolTip(self.tr("All services are OFF")) - - def _update_systray_tooltip(self): - """ - Updates the system tray icon tooltip using the eip and mx statuses. - """ - status = self.tr("Encrypted Internet is {0}").format(self._eip_status) - status += '\n' - status += self.tr("Mail is {0}").format(self._mx_status) - self._systray.setToolTip(status) - - def set_action_eip_startstop(self, action_eip_startstop): - """ - Sets the action_eip_startstop to use. - - :param action_eip_startstop: action_eip_status to be used - :type action_eip_startstop: QtGui.QAction - """ - self._action_eip_startstop = action_eip_startstop - - def set_eip_status_menu(self, eip_status_menu): - """ - Sets the eip_status_menu to use. - - :param eip_status_menu: eip_status_menu to be used - :type eip_status_menu: QtGui.QMenu - """ - leap_assert_type(eip_status_menu, QtGui.QMenu) - self._eip_status_menu = eip_status_menu - - def set_action_mail_status(self, action_mail_status): - """ - Sets the action_mail_status to use. - - :param action_mail_status: action_mail_status to be used - :type action_mail_status: QtGui.QAction - """ - leap_assert_type(action_mail_status, QtGui.QAction) - self._action_mail_status = action_mail_status - - def set_global_status(self, status, error=False): - """ - Sets the global status label. - - :param status: status message - :type status: str or unicode - :param error: if the status is an erroneous one, then set this - to True - :type error: bool - """ - leap_assert_type(error, bool) - if error: - status = "<font color='red'><b>%s</b></font>" % (status,) - self.ui.lblGlobalStatus.setText(status) - self.ui.globalStatusBox.show() - - def hide_status_box(self): - """ - Hide global status box. - """ - self.ui.globalStatusBox.hide() - - # EIP status --- - - @property - def eip_button(self): - return self.ui.btnEipStartStop - - @property - def eip_label(self): - return self.ui.lblEIPStatus - - def eip_pre_up(self): - """ - Triggered when the app activates eip. - Hides the status box and disables the start/stop button. - """ - self.hide_status_box() - self.set_startstop_enabled(False) - - # XXX disable (later) -------------------------- - def set_eip_status(self, status, error=False): - """ - Sets the status label at the VPN stage to status - - :param status: status message - :type status: str or unicode - :param error: if the status is an erroneous one, then set this - to True - :type error: bool - """ - leap_assert_type(error, bool) - - self._eip_status = status - - if error: - status = "<font color='red'>%s</font>" % (status,) - self.ui.lblEIPStatus.setText(status) - self._update_systray_tooltip() - - # XXX disable --------------------------------- - def set_startstop_enabled(self, value): - """ - Enable or disable btnEipStartStop and _action_eip_startstop - based on value - - :param value: True for enabled, False otherwise - :type value: bool - """ - leap_assert_type(value, bool) - self.ui.btnEipStartStop.setEnabled(value) - self._action_eip_startstop.setEnabled(value) - - # XXX disable ----------------------------- - def eip_started(self): - """ - Sets the state of the widget to how it should look after EIP - has started - """ - self.ui.btnEipStartStop.setText(self.tr("Turn OFF")) - self.ui.btnEipStartStop.disconnect(self) - self.ui.btnEipStartStop.clicked.connect( - self.eipconnection.qtsigs.do_connect_signal) - - # XXX disable ----------------------------- - def eip_stopped(self): - """ - Sets the state of the widget to how it should look after EIP - has stopped - """ - # XXX should connect this to EIPConnection.disconnected_signal - self._reset_traffic_rates() - # XXX disable ----------------------------- - self.ui.btnEipStartStop.setText(self.tr("Turn ON")) - self.ui.btnEipStartStop.disconnect(self) - self.ui.btnEipStartStop.clicked.connect( - self.eipconnection.qtsigs.do_disconnect_signal) - - def update_vpn_status(self, data): - """ - SLOT - TRIGGER: VPN.status_changed - - Updates the download/upload labels based on the data provided - by the VPN thread. - - :param data: a dictionary with the tcp/udp write and read totals. - If data is None, we just will refresh the display based - on the previous data. - :type data: dict - """ - if data: - upload = float(data[VPNManager.TCPUDP_WRITE_KEY] or "0") - download = float(data[VPNManager.TCPUDP_READ_KEY] or "0") - self._update_traffic_rates(upload, download) - - if self.DISPLAY_TRAFFIC_RATES: - uprate, downrate = self._get_traffic_rates() - upload_str = self.RATE_STR % (uprate,) - download_str = self.RATE_STR % (downrate,) - - else: # display total throughput - uptotal, downtotal = self._get_traffic_totals() - upload_str = self.TOTAL_STR % (uptotal,) - download_str = self.TOTAL_STR % (downtotal,) - - self.ui.btnUpload.setText(upload_str) - self.ui.btnDownload.setText(download_str) - - def update_vpn_state(self, data): - """ - SLOT - TRIGGER: VPN.state_changed - - Updates the displayed VPN state based on the data provided by - the VPN thread. - - Emits: - If the status is connected, we emit EIPConnection.qtsigs. - connected_signal - """ - status = data[VPNManager.STATUS_STEP_KEY] - self.set_eip_status_icon(status) - if status == "CONNECTED": - # XXX should be handled by the state machine too. - self.set_eip_status(self.tr("ON")) - logger.debug("STATUS IS CONNECTED --- emitting signal") - self.eip_connection_connected.emit() - - # XXX should lookup status map in EIPConnection - elif status == "AUTH": - self.set_eip_status(self.tr("Authenticating...")) - elif status == "GET_CONFIG": - self.set_eip_status(self.tr("Retrieving configuration...")) - elif status == "WAIT": - self.set_eip_status(self.tr("Waiting to start...")) - elif status == "ASSIGN_IP": - self.set_eip_status(self.tr("Assigning IP")) - elif status == "RECONNECTING": - self.set_eip_status(self.tr("Reconnecting...")) - elif status == "ALREADYRUNNING": - # Put the following calls in Qt's event queue, otherwise - # the UI won't update properly - QtCore.QTimer.singleShot( - 0, self.eipconnection.qtsigs.do_disconnect_signal) - QtCore.QTimer.singleShot(0, partial(self.set_global_status, - self.tr("Unable to start VPN, " - "it's already " - "running."))) - else: - self.set_eip_status(status) - - def set_eip_icon(self, icon): - """ - Sets the icon to display for EIP - - :param icon: icon to display - :type icon: QPixmap - """ - self.ui.lblVPNStatusIcon.setPixmap(icon) - - def set_eip_status_icon(self, status): - """ - Given a status step from the VPN thread, set the icon properly - - :param status: status step - :type status: str - """ - selected_pixmap = self.ERROR_ICON - selected_pixmap_tray = self.ERROR_ICON_TRAY - tray_message = self.tr("Encrypted Internet is OFF") - if status in ("WAIT", "AUTH", "GET_CONFIG", - "RECONNECTING", "ASSIGN_IP"): - selected_pixmap = self.CONNECTING_ICON - selected_pixmap_tray = self.CONNECTING_ICON_TRAY - tray_message = self.tr("Encrypted Internet is STARTING") - elif status in ("CONNECTED"): - tray_message = self.tr("Encrypted Internet is ON") - selected_pixmap = self.CONNECTED_ICON - selected_pixmap_tray = self.CONNECTED_ICON_TRAY - - self.set_eip_icon(selected_pixmap) - self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray)) - self._eip_status_menu.setTitle(tray_message) - - def set_provider(self, provider): - self.ui.lblProvider.setText(provider) - - # - # mail methods - # - - def _set_mail_status(self, status, ready=False): - """ - Sets the Mail status in the label and in the tray icon. - - :param status: the status text to display - :type status: unicode - :param ready: if mx is ready or not. - :type ready: bool - """ - self.ui.lblMailStatus.setText(status) - - self._mx_status = self.tr('OFF') - tray_status = self.tr('Mail is OFF') - - icon = QtGui.QPixmap(self.MAIL_OFF_ICON) - if ready: - icon = QtGui.QPixmap(self.MAIL_ON_ICON) - self._mx_status = self.tr('ON') - tray_status = self.tr('Mail is ON') - - self.ui.lblMailIcon.setPixmap(icon) - self._action_mail_status.setText(tray_status) - self._update_systray_tooltip() - - def _mail_handle_soledad_events(self, req): - """ - Callback for ... - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._soledad_event.emit(req) - - def _mail_handle_soledad_events_slot(self, req): - """ - SLOT - TRIGGER: _mail_handle_soledad_events - - Reacts to an Soledad event - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._set_mail_status(self.tr("Starting...")) - - ext_status = "" - - if req.event == proto.SOLEDAD_DONE_UPLOADING_KEYS: - ext_status = self.tr("Soledad has started...") - elif req.event == proto.SOLEDAD_DONE_DOWNLOADING_KEYS: - ext_status = self.tr("Soledad is starting, please wait...") - else: - leap_assert(False, - "Don't know how to handle this state: %s" - % (req.event)) - - self._set_long_mail_status(ext_status) - - def _mail_handle_keymanager_events(self, req): - """ - Callback for the KeyManager events - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._keymanager_event.emit(req) - - def _mail_handle_keymanager_events_slot(self, req): - """ - SLOT - TRIGGER: _mail_handle_keymanager_events - - Reacts to an KeyManager event - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - # We want to ignore this kind of events once everything has - # started - if self._smtp_started and self._imap_started: - return - - self._set_mail_status(self.tr("Starting...")) - - ext_status = "" - - if req.event == proto.KEYMANAGER_LOOKING_FOR_KEY: - ext_status = self.tr("Looking for key for this user") - elif req.event == proto.KEYMANAGER_KEY_FOUND: - ext_status = self.tr("Found key! Starting mail...") - # elif req.event == proto.KEYMANAGER_KEY_NOT_FOUND: - # ext_status = self.tr("Key not found!") - elif req.event == proto.KEYMANAGER_STARTED_KEY_GENERATION: - ext_status = self.tr("Generating new key, please wait...") - elif req.event == proto.KEYMANAGER_FINISHED_KEY_GENERATION: - ext_status = self.tr("Finished generating key!") - elif req.event == proto.KEYMANAGER_DONE_UPLOADING_KEYS: - ext_status = self.tr("Starting mail...") - else: - leap_assert(False, - "Don't know how to handle this state: %s" - % (req.event)) - - self._set_long_mail_status(ext_status) - - def _mail_handle_smtp_events(self, req): - """ - Callback for the SMTP events - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._smtp_event.emit(req) - - def _mail_handle_smtp_events_slot(self, req): - """ - SLOT - TRIGGER: _mail_handle_smtp_events - - Reacts to an SMTP event - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - ext_status = "" - - if req.event == proto.SMTP_SERVICE_STARTED: - ext_status = self.tr("SMTP has started...") - self._smtp_started = True - if self._smtp_started and self._imap_started: - self._set_mail_status(self.tr("ON"), ready=True) - ext_status = "" - elif req.event == proto.SMTP_SERVICE_FAILED_TO_START: - ext_status = self.tr("SMTP failed to start, check the logs.") - self._set_mail_status(self.tr("Failed")) - else: - leap_assert(False, - "Don't know how to handle this state: %s" - % (req.event)) - - self._set_long_mail_status(ext_status) - - def _mail_handle_imap_events(self, req): - """ - Callback for the IMAP events - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - self._imap_event.emit(req) - - def _mail_handle_imap_events_slot(self, req): - """ - SLOT - TRIGGER: _mail_handle_imap_events - - Reacts to an IMAP event - - :param req: Request type - :type req: leap.common.events.events_pb2.SignalRequest - """ - ext_status = None - - if req.event == proto.IMAP_SERVICE_STARTED: - ext_status = self.tr("IMAP has started...") - self._imap_started = True - if self._smtp_started and self._imap_started: - self._set_mail_status(self.tr("ON"), ready=True) - ext_status = "" - elif req.event == proto.IMAP_SERVICE_FAILED_TO_START: - ext_status = self.tr("IMAP failed to start, check the logs.") - self._set_mail_status(self.tr("Failed")) - elif req.event == proto.IMAP_UNREAD_MAIL: - if self._smtp_started and self._imap_started: - self.ui.lblUnread.setText( - self.tr("%s Unread Emails") % (req.content)) - self.ui.lblUnread.setVisible(req.content != "0") - self._set_mail_status(self.tr("ON"), ready=True) - else: - leap_assert(False, # XXX ??? - "Don't know how to handle this state: %s" - % (req.event)) - - if ext_status is not None: - self._set_long_mail_status(ext_status) - - def _set_long_mail_status(self, ext_status): - self.ui.lblLongMailStatus.setText(ext_status) - self.ui.grpMailStatus.setVisible(len(ext_status) > 0) diff --git a/src/leap/bitmask/gui/ui/eip_status.ui b/src/leap/bitmask/gui/ui/eip_status.ui new file mode 100644 index 00000000..27df3f31 --- /dev/null +++ b/src/leap/bitmask/gui/ui/eip_status.ui @@ -0,0 +1,287 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EIPStatus</class> + <widget class="QWidget" name="EIPStatus"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>470</width> + <height>157</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="bottomMargin"> + <number>24</number> + </property> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="2"> + <widget class="QPushButton" name="btnEipStartStop"> + <property name="text"> + <string>Turn On</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="maximumSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../../../../../data/resources/mainwindow.qrc">:/images/black/32/earth.png</pixmap> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="lblEIPStatus"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>32</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true">color: rgb(80, 80, 80);</string> + </property> + <property name="text"> + <string>...</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="lblEIPMessage"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>14</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Traffic is being routed in the clear</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="lblVPNStatusIcon"> + <property name="maximumSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../../../../../data/resources/mainwindow.qrc">:/images/black/32/off.png</pixmap> + </property> + <property name="scaledContents"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="1" colspan="3"> + <widget class="QWidget" name="eip_bandwidth" native="true"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>32</height> + </size> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="margin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="eip_bandwidth_layout"> + <property name="spacing"> + <number>4</number> + </property> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="btnDownload"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>120</width> + <height>16777215</height> + </size> + </property> + <property name="font"> + <font> + <pointsize>11</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="cursor"> + <cursorShape>PointingHandCursor</cursorShape> + </property> + <property name="text"> + <string>0.0 KB/s</string> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>10</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap> + </property> + </widget> + </item> + <item alignment="Qt::AlignLeft"> + <widget class="QPushButton" name="btnUpload"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>120</width> + <height>16777215</height> + </size> + </property> + <property name="font"> + <font> + <pointsize>11</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="cursor"> + <cursorShape>PointingHandCursor</cursorShape> + </property> + <property name="text"> + <string>0.0 KB/s</string> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources> + <include location="../../../../../data/resources/mainwindow.qrc"/> + <include location="../../../../../data/resources/icons.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/leap/bitmask/gui/ui/eippreferences.ui b/src/leap/bitmask/gui/ui/eippreferences.ui new file mode 100644 index 00000000..9493d330 --- /dev/null +++ b/src/leap/bitmask/gui/ui/eippreferences.ui @@ -0,0 +1,183 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EIPPreferences</class> + <widget class="QDialog" name="EIPPreferences"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>398</width> + <height>262</height> + </rect> + </property> + <property name="windowTitle"> + <string>EIP Preferences</string> + </property> + <property name="windowIcon"> + <iconset resource="../../../../../data/resources/mainwindow.qrc"> + <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="0"> + <widget class="QGroupBox" name="gbGatewaySelector"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Select gateway for provider</string> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="lblSelectProvider"> + <property name="text"> + <string>&Select provider:</string> + </property> + <property name="buddy"> + <cstring>cbProvidersGateway</cstring> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QComboBox" name="cbProvidersGateway"> + <item> + <property name="text"> + <string><Select provider></string> + </property> + </item> + </widget> + </item> + <item row="7" column="2"> + <widget class="QPushButton" name="pbSaveGateway"> + <property name="text"> + <string>Save this provider settings</string> + </property> + </widget> + </item> + <item row="4" column="0" colspan="3"> + <widget class="QLabel" name="lblProvidersGatewayStatus"> + <property name="text"> + <string>< Providers Gateway Status ></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Select gateway:</string> + </property> + </widget> + </item> + <item row="1" column="1" colspan="2"> + <widget class="QComboBox" name="cbGateways"> + <item> + <property name="text"> + <string>Automatic</string> + </property> + </item> + </widget> + </item> + </layout> + </widget> + </item> + <item row="0" column="0"> + <widget class="QGroupBox" name="gbAutomaticEIP"> + <property name="title"> + <string>Automatic EIP start</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="3" column="0"> + <widget class="QLabel" name="lblAutoStartEIPStatus"> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="text"> + <string><font color='green'><b>Automatic EIP start saved!</b></font></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QPushButton" name="pbSaveAutoStartEIP"> + <property name="text"> + <string>Save auto start setting</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QCheckBox" name="cbAutoStartEIP"> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="text"> + <string>Enable Automatic start of EIP</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="cbProvidersEIP"> + <item> + <property name="text"> + <string><Select provider></string> + </property> + </item> + </widget> + </item> + </layout> + <zorder>cbAutoStartEIP</zorder> + <zorder>lblAutoStartEIPStatus</zorder> + <zorder>pbSaveAutoStartEIP</zorder> + <zorder>cbProvidersEIP</zorder> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>cbAutoStartEIP</tabstop> + <tabstop>cbProvidersEIP</tabstop> + <tabstop>pbSaveAutoStartEIP</tabstop> + <tabstop>cbProvidersGateway</tabstop> + <tabstop>cbGateways</tabstop> + <tabstop>pbSaveGateway</tabstop> + </tabstops> + <resources> + <include location="../../../../../data/resources/mainwindow.qrc"/> + </resources> + <connections> + <connection> + <sender>cbAutoStartEIP</sender> + <signal>toggled(bool)</signal> + <receiver>cbProvidersEIP</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>180</x> + <y>53</y> + </hint> + <hint type="destinationlabel"> + <x>238</x> + <y>53</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui index 42a6897a..a1842608 100644 --- a/src/leap/bitmask/gui/ui/login.ui +++ b/src/leap/bitmask/gui/ui/login.ui @@ -6,127 +6,273 @@ <rect> <x>0</x> <y>0</y> - <width>356</width> - <height>223</height> + <width>468</width> + <height>350</height> </rect> </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> <property name="windowTitle"> <string>Form</string> </property> + <property name="styleSheet"> + <string notr="true"/> + </property> <layout class="QGridLayout" name="gridLayout"> - <item row="5" column="2"> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> + <property name="margin"> + <number>0</number> + </property> + <item row="2" column="2" colspan="2"> + <widget class="QWidget" name="logged_widget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <property name="sizeHint" stdset="0"> + <property name="minimumSize"> <size> - <width>40</width> - <height>20</height> + <width>0</width> + <height>0</height> </size> </property> - </spacer> - </item> - <item row="1" column="1" colspan="2"> - <widget class="QComboBox" name="cmbProviders"/> + <layout class="QGridLayout" name="gridLayout_5"> + <property name="bottomMargin"> + <number>24</number> + </property> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="lblUser"> + <property name="font"> + <font> + <pointsize>15</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QPushButton" name="btnLogout"> + <property name="text"> + <string>Logout</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <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="2" column="0" colspan="2"> + <widget class="QLabel" name="lblLoginStatus"> + <property name="styleSheet"> + <string notr="true">color: rgb(132, 132, 132); +font: 75 12pt "Lucida Grande";</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> </item> - <item row="5" column="0"> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> + <item row="1" column="1" rowspan="2"> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <property name="sizeHint" stdset="0"> + <property name="maximumSize"> <size> - <width>40</width> - <height>20</height> + <width>16777215</width> + <height>800</height> </size> </property> - </spacer> - </item> - <item row="6" column="1"> - <widget class="QPushButton" name="btnCreateAccount"> <property name="text"> - <string>Create a new account</string> + <string/> </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_4"> - <property name="text"> - <string><b>Provider:</b></string> + <property name="pixmap"> + <pixmap resource="../../../../../data/resources/mainwindow.qrc">:/images/black/32/user.png</pixmap> </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + <property name="scaledContents"> + <bool>false</bool> </property> - </widget> - </item> - <item row="3" column="1" colspan="2"> - <widget class="QLineEdit" name="lnPassword"> - <property name="inputMask"> - <string/> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> </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 name="margin"> + <number>0</number> </property> </widget> </item> - <item row="2" column="0"> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string><b>Username:</b></string> + <item row="1" column="2" colspan="2"> + <widget class="QWidget" name="login_widget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> </property> + <layout class="QGridLayout" name="gridLayout_4"> + <property name="rightMargin"> + <number>24</number> + </property> + <item row="0" 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="0"> + <widget class="QPushButton" name="btnLogin"> + <property name="text"> + <string>Log In</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QCheckBox" name="chkRemember"> + <property name="text"> + <string>Remember username and password</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="lblStatus"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="cmbProviders"/> + </item> + <item row="1" 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="1" column="1"> + <widget class="QLineEdit" name="lnUser"/> + </item> + <item row="2" 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="2" column="1"> + <widget class="QLineEdit" name="lnPassword"> + <property name="inputMask"> + <string/> + </property> + </widget> + </item> + </layout> </widget> </item> - <item row="3" column="0"> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string><b>Password:</b></string> + <item row="1" column="0"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> </property> - </widget> - </item> - <item row="5" column="1"> - <widget class="QPushButton" name="btnLogin"> - <property name="text"> - <string>Log In</string> + <property name="sizeHint" stdset="0"> + <size> + <width>12</width> + <height>0</height> + </size> </property> - </widget> + </spacer> </item> - <item row="0" column="0" colspan="3"> - <widget class="QLabel" name="lblStatus"> + <item row="0" column="0" colspan="4"> + <widget class="ClickableLabel" name="clblErrorMsg"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>50</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true">background-color: rgb(255, 127, 114);</string> + </property> <property name="text"> <string/> </property> <property name="alignment"> <set>Qt::AlignCenter</set> </property> - <property name="wordWrap"> - <bool>true</bool> - </property> </widget> </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>ClickableLabel</class> + <extends>QLabel</extends> + <header>clickablelabel.h</header> + </customwidget> + </customwidgets> <tabstops> - <tabstop>cmbProviders</tabstop> - <tabstop>lnUser</tabstop> - <tabstop>lnPassword</tabstop> <tabstop>chkRemember</tabstop> - <tabstop>btnLogin</tabstop> - <tabstop>btnCreateAccount</tabstop> </tabstops> - <resources/> + <resources> + <include location="../../../../../data/resources/mainwindow.qrc"/> + </resources> <connections/> </ui> diff --git a/src/leap/bitmask/gui/ui/mail_status.ui b/src/leap/bitmask/gui/ui/mail_status.ui new file mode 100644 index 00000000..1327f9e7 --- /dev/null +++ b/src/leap/bitmask/gui/ui/mail_status.ui @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MailStatusWidget</class> + <widget class="QWidget" name="MailStatusWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>72</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="1" rowspan="2"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="0" colspan="3"> + <widget class="QLabel" name="lblMailStatus"> + <property name="styleSheet"> + <string notr="true">color: rgb(80, 80, 80);</string> + </property> + <property name="text"> + <string>You must login to use encrypted email.</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Email</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <spacer name="horizontalSpacer_4"> + <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="0" column="3"> + <widget class="QLabel" name="lblMailStatusIcon"> + <property name="maximumSize"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../../../../../data/resources/mainwindow.qrc">:/images/black/32/off.png</pixmap> + </property> + <property name="scaledContents"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../../../../../data/resources/mainwindow.qrc">:/images/black/32/email.png</pixmap> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../../../../../data/resources/mainwindow.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index 17837642..920160b8 100644 --- a/src/leap/bitmask/gui/ui/mainwindow.ui +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -6,10 +6,28 @@ <rect> <x>0</x> <y>0</y> - <width>429</width> - <height>579</height> + <width>524</width> + <height>722</height> </rect> </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>524</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>524</width> + <height>16777215</height> + </size> + </property> <property name="windowTitle"> <string>Bitmask</string> </property> @@ -28,9 +46,222 @@ </property> <widget class="QWidget" name="centralwidget"> <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0" colspan="5"> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="1" column="0" colspan="2"> + <widget class="QScrollArea" name="scrollArea"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="lineWidth"> + <number>0</number> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>524</width> + <height>635</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="margin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="widget_2" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="leftMargin"> + <number>24</number> + </property> + <property name="rightMargin"> + <number>24</number> + </property> + <item> + <widget class="QLabel" name="label_2"> + <property name="font"> + <font> + <pointsize>16</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Encrypted Internet</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="btnEIPPreferences"> + <property name="maximumSize"> + <size> + <width>48</width> + <height>20</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../../../../../data/resources/mainwindow.qrc"> + <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + <property name="default"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="eipLayout"> + <property name="leftMargin"> + <number>12</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>12</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + </layout> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="widget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>24</number> + </property> + <property name="rightMargin"> + <number>24</number> + </property> + <item> + <widget class="QLabel" name="lblLoginProvider"> + <property name="font"> + <font> + <pointsize>16</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Login</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="btnPreferences"> + <property name="maximumSize"> + <size> + <width>48</width> + <height>20</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../../../../../data/resources/mainwindow.qrc"> + <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="loginLayout"/> + </item> + <item> + <widget class="Line" name="line_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="mailLayout"> + <property name="margin"> + <number>12</number> + </property> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + <item row="0" column="0" colspan="2"> <layout class="QGridLayout" name="gridLayout_4"> - <item row="2" column="0"> + <item row="1" column="1"> + <widget class="QLabel" name="lblNewUpdates"> + <property name="text"> + <string>There are new updates available, please restart.</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="0"> <spacer name="horizontalSpacer_8"> <property name="orientation"> <enum>Qt::Horizontal</enum> @@ -43,7 +274,7 @@ </property> </spacer> </item> - <item row="1" column="1"> + <item row="0" column="1"> <spacer name="horizontalSpacer_7"> <property name="orientation"> <enum>Qt::Horizontal</enum> @@ -56,17 +287,7 @@ </property> </spacer> </item> - <item row="2" column="1"> - <widget class="QLabel" name="lblNewUpdates"> - <property name="text"> - <string>There are new updates available, please restart.</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - <item row="2" column="2"> + <item row="1" column="2"> <widget class="QPushButton" name="btnMore"> <property name="sizePolicy"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> @@ -82,7 +303,7 @@ </property> </widget> </item> - <item row="2" column="3"> + <item row="1" column="3"> <spacer name="horizontalSpacer_9"> <property name="orientation"> <enum>Qt::Horizontal</enum> @@ -97,164 +318,6 @@ </item> </layout> </item> - <item row="6" column="2"> - <spacer name="verticalSpacer"> - <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="10" column="0" colspan="5"> - <widget class="QStackedWidget" name="stackedWidget"> - <property name="currentIndex"> - <number>1</number> - </property> - <widget class="QWidget" name="loginPage"> - <layout class="QGridLayout" name="gridLayout_2"> - <item row="0" column="1"> - <layout class="QHBoxLayout" name="loginLayout"/> - </item> - <item row="0" column="2"> - <spacer name="horizontalSpacer_4"> - <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="0" column="0"> - <spacer name="horizontalSpacer_3"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - <widget class="QWidget" name="page_2"> - <layout class="QGridLayout" name="gridLayout_3"> - <item row="0" column="0"> - <layout class="QVBoxLayout" name="statusLayout"/> - </item> - </layout> - </widget> - </widget> - </item> - <item row="7" column="2"> - <widget class="QLabel" name="label"> - <property name="autoFillBackground"> - <bool>false</bool> - </property> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/mainwindow.qrc">:/images/mask-launcher.png</pixmap> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item row="7" column="3" colspan="2"> - <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="17" 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="7" column="0" colspan="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="18" column="2"> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QPushButton" name="btnPreferences"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="text"> - <string>Preferences</string> - </property> - </widget> - </item> - <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"> @@ -262,15 +325,15 @@ <rect> <x>0</x> <y>0</y> - <width>429</width> - <height>21</height> + <width>524</width> + <height>22</height> </rect> </property> - <widget class="QMenu" name="menuSession"> + <widget class="QMenu" name="menuFile"> <property name="title"> - <string>&Session</string> + <string>&Bitmask</string> </property> - <addaction name="action_log_out"/> + <addaction name="action_create_new_account"/> <addaction name="separator"/> <addaction name="action_quit"/> </widget> @@ -279,16 +342,17 @@ <string>Help</string> </property> <addaction name="action_help"/> + <addaction name="action_show_logs"/> <addaction name="separator"/> <addaction name="action_about_leap"/> </widget> - <addaction name="menuSession"/> + <addaction name="menuFile"/> <addaction name="menuHelp"/> </widget> <widget class="QStatusBar" name="statusbar"/> - <action name="action_log_out"> + <action name="action_preferences"> <property name="text"> - <string>Log &out</string> + <string>Preferences...</string> </property> </action> <action name="action_quit"> @@ -313,7 +377,12 @@ </action> <action name="action_show_logs"> <property name="text"> - <string>Show &logs</string> + <string>Show &Log</string> + </property> + </action> + <action name="action_create_new_account"> + <property name="text"> + <string>Create a new account...</string> </property> </action> </widget> diff --git a/src/leap/bitmask/gui/ui/preferences.ui b/src/leap/bitmask/gui/ui/preferences.ui index e66a2d68..e187c016 100644 --- a/src/leap/bitmask/gui/ui/preferences.ui +++ b/src/leap/bitmask/gui/ui/preferences.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>503</width> - <height>529</height> + <height>401</height> </rect> </property> <property name="windowTitle"> @@ -18,7 +18,7 @@ <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset> </property> <layout class="QGridLayout" name="gridLayout_3"> - <item row="5" column="0"> + <item row="4" column="0"> <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -31,64 +31,80 @@ </property> </spacer> </item> - <item row="1" column="0"> - <widget class="QGroupBox" name="gbGatewaySelector"> + <item row="0" column="0"> + <widget class="QGroupBox" name="gbPasswordChange"> <property name="enabled"> - <bool>true</bool> + <bool>false</bool> </property> <property name="title"> - <string>Select gateway for provider</string> - </property> - <property name="checkable"> - <bool>false</bool> + <string>Password Change</string> </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="1" colspan="2"> - <widget class="QComboBox" name="cbProvidersGateway"> - <item> - <property name="text"> - <string><Select provider></string> - </property> - </item> - </widget> - </item> + <layout class="QFormLayout" name="formLayout"> + <property name="fieldGrowthPolicy"> + <enum>QFormLayout::ExpandingFieldsGrow</enum> + </property> <item row="0" column="0"> - <widget class="QLabel" name="lblSelectProvider"> + <widget class="QLabel" name="lblCurrentPassword"> <property name="text"> - <string>&Select provider:</string> + <string>&Current password:</string> </property> <property name="buddy"> - <cstring>cbProvidersGateway</cstring> + <cstring>leCurrentPassword</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="leCurrentPassword"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> </property> </widget> </item> <item row="1" column="0"> - <widget class="QLabel" name="label"> + <widget class="QLabel" name="lblNewPassword"> <property name="text"> - <string>Select gateway:</string> + <string>&New password:</string> + </property> + <property name="buddy"> + <cstring>leNewPassword</cstring> </property> </widget> </item> - <item row="1" column="1" colspan="2"> - <widget class="QComboBox" name="cbGateways"> - <item> - <property name="text"> - <string>Automatic</string> - </property> - </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="leNewPassword"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> </widget> </item> - <item row="5" column="2"> - <widget class="QPushButton" name="pbSaveGateway"> + <item row="2" column="0"> + <widget class="QLabel" name="lblNewPassword2"> <property name="text"> - <string>Save this provider settings</string> + <string>&Re-enter new password:</string> + </property> + <property name="buddy"> + <cstring>leNewPassword2</cstring> </property> </widget> </item> - <item row="2" column="0" colspan="3"> - <widget class="QLabel" name="lblProvidersGatewayStatus"> + <item row="2" column="1"> + <widget class="QLineEdit" name="leNewPassword2"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QPushButton" name="pbChangePassword"> <property name="text"> - <string>< Providers Gateway Status ></string> + <string>Change</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <widget class="QLabel" name="lblPasswordChangeStatus"> + <property name="text"> + <string><Password change status></string> </property> <property name="alignment"> <set>Qt::AlignCenter</set> @@ -98,7 +114,7 @@ </layout> </widget> </item> - <item row="4" column="0"> + <item row="3" column="0"> <widget class="QGroupBox" name="gbEnabledServices"> <property name="title"> <string>Enabled services</string> @@ -155,89 +171,6 @@ </layout> </widget> </item> - <item row="0" column="0"> - <widget class="QGroupBox" name="gbPasswordChange"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="title"> - <string>Password Change</string> - </property> - <layout class="QFormLayout" name="formLayout"> - <property name="fieldGrowthPolicy"> - <enum>QFormLayout::ExpandingFieldsGrow</enum> - </property> - <item row="0" column="0"> - <widget class="QLabel" name="lblCurrentPassword"> - <property name="text"> - <string>&Current password:</string> - </property> - <property name="buddy"> - <cstring>leCurrentPassword</cstring> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="leCurrentPassword"> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="lblNewPassword"> - <property name="text"> - <string>&New password:</string> - </property> - <property name="buddy"> - <cstring>leNewPassword</cstring> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QLineEdit" name="leNewPassword"> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - </widget> - </item> - <item row="2" column="0"> - <widget class="QLabel" name="lblNewPassword2"> - <property name="text"> - <string>&Re-enter new password:</string> - </property> - <property name="buddy"> - <cstring>leNewPassword2</cstring> - </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QLineEdit" name="leNewPassword2"> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - </widget> - </item> - <item row="4" column="1"> - <widget class="QPushButton" name="pbChangePassword"> - <property name="text"> - <string>Change</string> - </property> - </widget> - </item> - <item row="3" column="0" colspan="2"> - <widget class="QLabel" name="lblPasswordChangeStatus"> - <property name="text"> - <string><Password change status></string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - </layout> - </widget> - </item> </layout> </widget> <resources> diff --git a/src/leap/bitmask/gui/ui/statuspanel.ui b/src/leap/bitmask/gui/ui/statuspanel.ui deleted file mode 100644 index d77af1da..00000000 --- a/src/leap/bitmask/gui/ui/statuspanel.ui +++ /dev/null @@ -1,393 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>StatusPanel</class> - <widget class="QWidget" name="StatusPanel"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>470</width> - <height>477</height> - </rect> - </property> - <property name="windowTitle"> - <string>Form</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QLabel" name="lblProvider"> - <property name="styleSheet"> - <string notr="true">font: bold;</string> - </property> - <property name="text"> - <string>user@domain.org</string> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QWidget" name="status_rows" native="true"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="styleSheet"> - <string notr="true"/> - </property> - <layout class="QGridLayout" name="gridLayout"> - <item row="5" column="1"> - <layout class="QGridLayout" name="gridLayout_3"> - <item row="1" column="0" colspan="3"> - <widget class="QLabel" name="lblUnread"> - <property name="text"> - <string>0 Unread Emails</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QLabel" name="lblMailStatus"> - <property name="styleSheet"> - <string notr="true">font: bold;</string> - </property> - <property name="text"> - <string>Disabled</string> - </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="label_4"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string>Encrypted Mail:</string> - </property> - </widget> - </item> - <item row="0" column="2"> - <spacer name="horizontalSpacer_4"> - <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="2" column="0" colspan="3"> - <spacer name="verticalSpacer_3"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>1</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item row="1" column="1"> - <layout class="QHBoxLayout" name="eip_bandwidth"> - <property name="spacing"> - <number>4</number> - </property> - <property name="sizeConstraint"> - <enum>QLayout::SetDefaultConstraint</enum> - </property> - <item> - <widget class="QLabel" name="label_5"> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="btnDownload"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>100</width> - <height>0</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>120</width> - <height>16777215</height> - </size> - </property> - <property name="cursor"> - <cursorShape>PointingHandCursor</cursorShape> - </property> - <property name="text"> - <string>0.0 KB/s</string> - </property> - <property name="flat"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_3"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLabel" name="label_7"> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap> - </property> - </widget> - </item> - <item alignment="Qt::AlignLeft"> - <widget class="QPushButton" name="btnUpload"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>100</width> - <height>0</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>120</width> - <height>16777215</height> - </size> - </property> - <property name="cursor"> - <cursorShape>PointingHandCursor</cursorShape> - </property> - <property name="text"> - <string>0.0 KB/s</string> - </property> - <property name="flat"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <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> - </layout> - </item> - <item row="2" column="0"> - <spacer name="verticalSpacer_2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Preferred</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>0</width> - <height>11</height> - </size> - </property> - </spacer> - </item> - <item row="0" column="0" rowspan="2"> - <widget class="QLabel" name="lblVPNStatusIcon"> - <property name="maximumSize"> - <size> - <width>64</width> - <height>64</height> - </size> - </property> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/64/network-eip-down.png</pixmap> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item row="4" column="0" rowspan="2"> - <widget class="QLabel" name="lblMailIcon"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>64</width> - <height>64</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>64</width> - <height>999</height> - </size> - </property> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/icons.qrc">:/images/mail-unlocked.png</pixmap> - </property> - </widget> - </item> - <item row="7" column="0" colspan="2"> - <widget class="QGroupBox" name="grpMailStatus"> - <property name="title"> - <string/> - </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QLabel" name="lblLongMailStatus"> - <property name="text"> - <string>...</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item row="3" column="0" colspan="2"> - <widget class="QGroupBox" name="globalStatusBox"> - <property name="enabled"> - <bool>false</bool> - </property> - <layout class="QGridLayout" name="gridLayout_2"> - <item row="0" column="0"> - <widget class="QLabel" name="lblGlobalStatus"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>...</string> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item row="0" column="1"> - <layout class="QHBoxLayout" name="eip_controls"> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Encrypted Internet: </string> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="lblEIPStatus"> - <property name="styleSheet"> - <string notr="true">font: bold;</string> - </property> - <property name="text"> - <string>Off</string> - </property> - <property name="textFormat"> - <enum>Qt::AutoText</enum> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - <property name="wordWrap"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <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> - <widget class="QPushButton" name="btnEipStartStop"> - <property name="text"> - <string>Turn On</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - <resources> - <include location="../../../../../data/resources/icons.qrc"/> - </resources> - <connections/> -</ui> diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui index 2a412784..0f6eef6e 100644 --- a/src/leap/bitmask/gui/ui/wizard.ui +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -134,17 +134,7 @@ </property> </spacer> </item> - <item row="1" column="1"> - <widget class="QLineEdit" name="lnProvider"/> - </item> - <item row="1" column="2"> - <widget class="QPushButton" name="btnCheck"> - <property name="text"> - <string>Check</string> - </property> - </widget> - </item> - <item row="3" column="1"> + <item row="5" column="1"> <spacer name="verticalSpacer_2"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -157,17 +147,7 @@ </property> </spacer> </item> - <item row="1" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>https://</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - <item row="4" column="0" colspan="3"> + <item row="6" column="0" colspan="3"> <widget class="QGroupBox" name="grpCheckProvider"> <property name="title"> <string>Checking for a valid provider</string> @@ -276,13 +256,76 @@ </layout> </widget> </item> - <item row="2" column="1" colspan="2"> + <item row="4" column="1" colspan="2"> <widget class="QLabel" name="lblProviderSelectStatus"> <property name="text"> <string/> </property> </widget> </item> + <item row="1" column="0" colspan="3"> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Configure or select a provider</string> + </property> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="0" column="0"> + <widget class="QRadioButton" name="rbNewProvider"> + <property name="text"> + <string>Configure new provider:</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QRadioButton" name="rbExistingProvider"> + <property name="text"> + <string>Use existing one:</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>https://</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="lnProvider"/> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="btnCheck"> + <property name="text"> + <string>Check</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>https://</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QComboBox" name="cbProviders"> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> </layout> </widget> <widget class="QWizardPage" name="provider_info_page"> @@ -626,10 +669,18 @@ </widget> </item> <item row="3" column="1" colspan="2"> - <widget class="QLineEdit" name="lblPassword"/> + <widget class="QLineEdit" name="lblPassword"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> </item> <item row="4" column="1" colspan="2"> - <widget class="QLineEdit" name="lblPassword2"/> + <widget class="QLineEdit" name="lblPassword2"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> </item> <item row="2" column="1" colspan="2"> <widget class="QLineEdit" name="lblUser"/> @@ -756,12 +807,91 @@ <tabstop>btnRegister</tabstop> <tabstop>rdoRegister</tabstop> <tabstop>rdoLogin</tabstop> - <tabstop>lnProvider</tabstop> - <tabstop>btnCheck</tabstop> </tabstops> <resources> <include location="../../../../../data/resources/mainwindow.qrc"/> <include location="../../../../../data/resources/locale.qrc"/> </resources> - <connections/> + <connections> + <connection> + <sender>rbExistingProvider</sender> + <signal>toggled(bool)</signal> + <receiver>cbProviders</receiver> + <slot>setFocus()</slot> + <hints> + <hint type="sourcelabel"> + <x>167</x> + <y>192</y> + </hint> + <hint type="destinationlabel"> + <x>265</x> + <y>191</y> + </hint> + </hints> + </connection> + <connection> + <sender>rbNewProvider</sender> + <signal>toggled(bool)</signal> + <receiver>lnProvider</receiver> + <slot>setFocus()</slot> + <hints> + <hint type="sourcelabel"> + <x>171</x> + <y>164</y> + </hint> + <hint type="destinationlabel"> + <x>246</x> + <y>164</y> + </hint> + </hints> + </connection> + <connection> + <sender>rbExistingProvider</sender> + <signal>toggled(bool)</signal> + <receiver>lnProvider</receiver> + <slot>setDisabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>169</x> + <y>196</y> + </hint> + <hint type="destinationlabel"> + <x>327</x> + <y>163</y> + </hint> + </hints> + </connection> + <connection> + <sender>rbNewProvider</sender> + <signal>toggled(bool)</signal> + <receiver>cbProviders</receiver> + <slot>setDisabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>169</x> + <y>162</y> + </hint> + <hint type="destinationlabel"> + <x>269</x> + <y>193</y> + </hint> + </hints> + </connection> + <connection> + <sender>rbExistingProvider</sender> + <signal>toggled(bool)</signal> + <receiver>btnCheck</receiver> + <slot>setDisabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>154</x> + <y>193</y> + </hint> + <hint type="destinationlabel"> + <x>498</x> + <y>170</y> + </hint> + </hints> + </connection> + </connections> </ui> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index 45734b81..e3f5904e 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -14,28 +14,27 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - """ First run wizard """ import os import logging import json +import random from functools import partial from PySide import QtCore, QtGui from twisted.internet import threads -from leap.bitmask.config import flags +from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.srpregister import SRPRegister -from leap.bitmask.util.privilege_policies import is_missing_policy_permissions +from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.services import get_service_display_name, get_supported from leap.bitmask.util.request_helpers import get_content from leap.bitmask.util.keyring_helpers import has_keyring from leap.bitmask.util.password import basic_password_checks -from leap.bitmask.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.bitmask.services import get_service_display_name, get_supported from ui_wizard import Ui_Wizard @@ -84,6 +83,8 @@ class Wizard(QtGui.QWizard): self._show_register = False + self._use_existing_provider = False + self.ui.grpCheckProvider.setVisible(False) self.ui.btnCheck.clicked.connect(self._check_provider) self.ui.lnProvider.returnPressed.connect(self._check_provider) @@ -113,9 +114,6 @@ class Wizard(QtGui.QWizard): self.currentIdChanged.connect(self._current_id_changed) - self.ui.lblPassword.setEchoMode(QtGui.QLineEdit.Password) - self.ui.lblPassword2.setEchoMode(QtGui.QLineEdit.Password) - self.ui.lnProvider.textChanged.connect( self._enable_check) @@ -128,6 +126,8 @@ class Wizard(QtGui.QWizard): self.ui.btnRegister.clicked.connect( self._register) + self.ui.rbExistingProvider.toggled.connect(self._skip_provider_checks) + usernameRe = QtCore.QRegExp(self.BARE_USERNAME_REGEX) self.ui.lblUser.setValidator( QtGui.QRegExpValidator(usernameRe, self)) @@ -147,6 +147,40 @@ class Wizard(QtGui.QWizard): self.ui.label_12.setVisible(False) self.ui.lblProviderPolicy.setVisible(False) + self._load_configured_providers() + + def _load_configured_providers(self): + """ + Loads the configured providers into the wizard providers combo box. + """ + ls = LeapSettings() + providers = ls.get_configured_providers() + if not providers: + self.ui.rbExistingProvider.setEnabled(False) + self.ui.label_8.setEnabled(False) # 'https://' label + self.ui.cbProviders.setEnabled(False) + return + + pinned = [] + user_added = [] + + # separate pinned providers from user added ones + for p in providers: + if ls.is_pinned_provider(p): + pinned.append(p) + else: + user_added.append(p) + + if user_added: + self.ui.cbProviders.addItems(user_added) + + if user_added and pinned: + self.ui.cbProviders.addItem('---') + + if pinned: + random.shuffle(pinned) # don't prioritize alphabetically + self.ui.cbProviders.addItems(pinned) + def get_domain(self): return self._domain @@ -318,6 +352,25 @@ class Wizard(QtGui.QWizard): self._provider_select_defer = self._provider_bootstrapper.\ run_provider_select_checks(self._domain) + def _skip_provider_checks(self, skip): + """ + SLOT + Triggered: + self.ui.rbExistingProvider.toggled + + Allows the user to move to the next page without make any checks, + used when we are selecting an already configured provider. + + :param skip: if we should skip checks or not + :type skip: bool + """ + if skip: + self._reset_provider_check() + + self.page(self.SELECT_PROVIDER_PAGE).set_completed(skip) + self.button(QtGui.QWizard.NextButton).setEnabled(skip) + self._use_existing_provider = skip + def _complete_task(self, data, label, complete=False, complete_page=-1): """ Checks a task and completes a page if specified @@ -564,4 +617,14 @@ class Wizard(QtGui.QWizard): else: return self.SERVICES_PAGE + if self.currentPage() == self.page(self.SELECT_PROVIDER_PAGE): + if self._use_existing_provider: + self._domain = self.ui.cbProviders.currentText() + self._provider_config = ProviderConfig.get_provider_config( + self._domain) + if self._show_register: + return self.REGISTER_USER_PAGE + else: + return self.SERVICES_PAGE + return QtGui.QWizard.nextId(self) diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index 831c6a1c..d93efbc6 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -29,7 +29,9 @@ import tempfile from PySide import QtGui from leap.bitmask.config.leapsettings import LeapSettings -from leap.bitmask.services.eip import vpnlaunchers +from leap.bitmask.services.eip import get_vpn_launcher +from leap.bitmask.services.eip.linuxvpnlauncher import LinuxVPNLauncher +from leap.bitmask.services.eip.darwinvpnlauncher import DarwinVPNLauncher from leap.bitmask.util import first from leap.bitmask.util import privilege_policies @@ -106,7 +108,7 @@ def check_missing(): config = LeapSettings() alert_missing = config.get_alert_missing_scripts() - launcher = vpnlaunchers.get_platform_launcher() + launcher = get_vpn_launcher() missing_scripts = launcher.missing_updown_scripts missing_other = launcher.missing_other_files @@ -251,7 +253,7 @@ def _darwin_install_missing_scripts(badexec, notfound): "..", "Resources", "openvpn") - launcher = vpnlaunchers.DarwinVPNLauncher + launcher = DarwinVPNLauncher if os.path.isdir(installer_path): fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") @@ -356,7 +358,7 @@ def _linux_install_missing_scripts(badexec, notfound): """ success = False installer_path = os.path.join(os.getcwd(), "apps", "eip", "files") - launcher = vpnlaunchers.LinuxVPNLauncher + launcher = LinuxVPNLauncher # XXX refactor with darwin, same block. diff --git a/src/leap/bitmask/provider/__init__.py b/src/leap/bitmask/provider/__init__.py index e69de29b..53587d65 100644 --- a/src/leap/bitmask/provider/__init__.py +++ b/src/leap/bitmask/provider/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# __init.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/>. +""" +Module initialization for leap.bitmask.provider +""" +import os +from leap.common.check import leap_assert + + +def get_provider_path(domain): + """ + Returns relative path for provider config. + + :param domain: the domain to which this providerconfig belongs to. + :type domain: str + :returns: the path + :rtype: str + """ + leap_assert(domain is not None, "get_provider_path: We need a domain") + return os.path.join("leap", "providers", domain, "provider.json") diff --git a/src/leap/bitmask/services/eip/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 3b7c9899..1b5947e1 100644 --- a/src/leap/bitmask/services/eip/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.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/>. - """ Provider bootstrapping """ @@ -28,15 +27,15 @@ from PySide import QtCore from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert from leap.bitmask.util.request_helpers import get_content -from leap.bitmask.util import get_path_prefix +from leap.bitmask import util from leap.bitmask.util.constants import REQUEST_TIMEOUT from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper from leap.bitmask.provider.supportedapis import SupportedAPIs +from leap.common import ca_bundle from leap.common.certs import get_digest from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p from leap.common.check import leap_assert, leap_assert_type, leap_check - logger = logging.getLogger(__name__) @@ -85,16 +84,32 @@ class ProviderBootstrapper(AbstractBootstrapper): self._provider_config = None self._download_if_needed = False + @property + def verify(self): + """ + Verify parameter for requests. + + :returns: either False, if checks are skipped, or the + path to the ca bundle. + :rtype: bool or str + """ + if self._bypass_checks: + verify = False + else: + verify = ca_bundle.where() + return verify + def _check_name_resolution(self): """ Checks that the name resolution for the provider name works """ leap_assert(self._domain, "Cannot check DNS without a domain") - logger.debug("Checking name resolution for %s" % (self._domain)) # We don't skip this check, since it's basic for the whole # system to work + # err --- but we can do it after a failure, to diagnose what went + # wrong. Right now we're just adding connection overhead. -- kali socket.gethostbyname(self._domain) def _check_https(self, *args): @@ -102,24 +117,29 @@ class ProviderBootstrapper(AbstractBootstrapper): Checks that https is working and that the provided certificate checks out """ - leap_assert(self._domain, "Cannot check HTTPS without a domain") - logger.debug("Checking https for %s" % (self._domain)) # We don't skip this check, since it's basic for the whole - # system to work + # system to work. + # err --- but we can do it after a failure, to diagnose what went + # wrong. Right now we're just adding connection overhead. -- kali try: res = self._session.get("https://%s" % (self._domain,), - verify=not self._bypass_checks, + verify=self.verify, timeout=REQUEST_TIMEOUT) res.raise_for_status() - except requests.exceptions.SSLError: + except requests.exceptions.SSLError as exc: + logger.exception(exc) self._err_msg = self.tr("Provider certificate could " "not be verified") raise - except Exception: + except Exception as exc: + # XXX careful!. The error might be also a SSL handshake + # timeout error, in which case we should retry a couple of times + # more, for cases where the ssl server gives high latencies. + logger.exception(exc) self._err_msg = self.tr("Provider does not support HTTPS") raise @@ -129,12 +149,16 @@ class ProviderBootstrapper(AbstractBootstrapper): """ leap_assert(self._domain, "Cannot download provider info without a domain") - logger.debug("Downloading provider info for %s" % (self._domain)) - headers = {} + # -------------------------------------------------------------- + # TODO factor out with the download routines in services. + # Watch out! We're handling the verify paramenter differently here. - provider_json = os.path.join(get_path_prefix(), "leap", "providers", + headers = {} + provider_json = os.path.join(util.get_path_prefix(), + "leap", + "providers", self._domain, "provider.json") mtime = get_mtime(provider_json) @@ -142,16 +166,18 @@ class ProviderBootstrapper(AbstractBootstrapper): headers['if-modified-since'] = mtime uri = "https://%s/%s" % (self._domain, "provider.json") - verify = not self._bypass_checks + verify = self.verify if mtime: # the provider.json exists - provider_config = ProviderConfig() - provider_config.load(provider_json) + # So, we're getting it from the api.* and checking against + # the provider ca. try: - verify = provider_config.get_ca_cert_path() + provider_config = ProviderConfig() + provider_config.load(provider_json) uri = provider_config.get_api_uri() + '/provider.json' + verify = provider_config.get_ca_cert_path() except MissingCACert: - # get_ca_cert_path fails if the certificate does not exists. + # no ca? then download from main domain again. pass logger.debug("Requesting for provider.json... " @@ -165,6 +191,9 @@ class ProviderBootstrapper(AbstractBootstrapper): # Not modified if res.status_code == 304: logger.debug("Provider definition has not been modified") + # -------------------------------------------------------------- + # end refactor, more or less... + # XXX Watch out, have to check the supported api yet. else: provider_definition, mtime = get_content(res) @@ -181,8 +210,8 @@ class ProviderBootstrapper(AbstractBootstrapper): else: api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS) error = ('Unsupported provider API version. ' - 'Supported versions are: {}. ' - 'Found: {}.').format(api_supported, api_version) + 'Supported versions are: {0}. ' + 'Found: {1}.').format(api_supported, api_version) logger.error(error) raise UnsupportedProviderAPI(error) @@ -230,7 +259,8 @@ class ProviderBootstrapper(AbstractBootstrapper): """ Downloads the CA cert that is going to be used for the api URL """ - + # XXX maybe we can skip this step if + # we have a fresh one. leap_assert(self._provider_config, "Cannot download the ca cert " "without a provider config!") @@ -244,7 +274,7 @@ class ProviderBootstrapper(AbstractBootstrapper): return res = self._session.get(self._provider_config.get_ca_cert_uri(), - verify=not self._bypass_checks, + verify=self.verify, timeout=REQUEST_TIMEOUT) res.raise_for_status() diff --git a/src/leap/bitmask/config/__init__.py b/src/leap/bitmask/provider/tests/__init__.py index e69de29b..e69de29b 100644 --- a/src/leap/bitmask/config/__init__.py +++ b/src/leap/bitmask/provider/tests/__init__.py diff --git a/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py index b0685676..88a4ff0b 100644 --- a/src/leap/bitmask/services/eip/tests/test_providerbootstrapper.py +++ b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py @@ -14,15 +14,12 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - - """ Tests for the Provider Boostrapper checks These will be whitebox tests since we want to make sure the private implementation is checking what we expect. """ - import os import mock import socket @@ -39,13 +36,13 @@ from nose.twistedtools import deferred, reactor from twisted.internet import threads from requests.models import Response -from leap.bitmask.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.bitmask.services.eip.providerbootstrapper import \ - UnsupportedProviderAPI -from leap.bitmask.services.eip.providerbootstrapper import WrongFingerprint -from leap.bitmask.provider.supportedapis import SupportedAPIs from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.tests import fake_provider +from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.provider.providerbootstrapper import UnsupportedProviderAPI +from leap.bitmask.provider.providerbootstrapper import WrongFingerprint +from leap.bitmask.provider.supportedapis import SupportedAPIs +from leap.bitmask import util from leap.common.files import mkdir_p from leap.common.testing.https_server import where from leap.common.testing.basetest import BaseLeapTest @@ -319,16 +316,19 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): # tearDown so we are sure everything is as expected for each # test. If we do it inside each specific test, a failure in # the test will leave the implementation with the mock. - self.old_gpp = ProviderConfig.get_path_prefix + self.old_gpp = util.get_path_prefix + self.old_load = ProviderConfig.load self.old_save = ProviderConfig.save self.old_api_version = ProviderConfig.get_api_version + self.old_api_uri = ProviderConfig.get_api_uri def tearDown(self): - ProviderConfig.get_path_prefix = self.old_gpp + util.get_path_prefix = self.old_gpp ProviderConfig.load = self.old_load ProviderConfig.save = self.old_save ProviderConfig.get_api_version = self.old_api_version + ProviderConfig.get_api_uri = self.old_api_uri def test_check_https_succeeds(self): # XXX: Need a proper CA signed cert to test this @@ -376,10 +376,11 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): paths :type path_prefix: str """ - ProviderConfig.get_path_prefix = mock.MagicMock( - return_value=path_prefix) + util.get_path_prefix = mock.MagicMock(return_value=path_prefix) ProviderConfig.get_api_version = mock.MagicMock( return_value=api) + ProviderConfig.get_api_uri = mock.MagicMock( + return_value="https://localhost:%s" % (self.https_port,)) ProviderConfig.load = mock.MagicMock() ProviderConfig.save = mock.MagicMock() @@ -404,10 +405,8 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): :returns: the provider.json path used :rtype: str """ - provider_dir = os.path.join(ProviderConfig() - .get_path_prefix(), - "leap", - "providers", + provider_dir = os.path.join(util.get_path_prefix(), + "leap", "providers", self.pb._domain) mkdir_p(provider_dir) provider_path = os.path.join(provider_dir, @@ -417,6 +416,9 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): p.write("A") return provider_path + @mock.patch( + 'leap.bitmask.config.providerconfig.ProviderConfig.get_domain', + lambda x: where('testdomain.com')) def test_download_provider_info_new_provider(self): self._setup_provider_config_with("1", tempfile.mkdtemp()) self._setup_providerbootstrapper(True) @@ -435,10 +437,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): # set mtime to something really new os.utime(provider_path, (-1, time.time())) - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() + self.pb._download_provider_info() # we check that it doesn't save the provider # config, because it's new enough self.assertFalse(ProviderConfig.save.called) @@ -454,10 +453,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): # set mtime to something really new os.utime(provider_path, (-1, time.time())) - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() + self.pb._download_provider_info() # we check that it doesn't save the provider # config, because it's new enough self.assertFalse(ProviderConfig.save.called) @@ -473,10 +469,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): # set mtime to something really old os.utime(provider_path, (-1, 100)) - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() + self.pb._download_provider_info() self.assertTrue(ProviderConfig.load.called) self.assertTrue(ProviderConfig.save.called) @@ -488,11 +481,8 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): self._setup_providerbootstrapper(False) self._produce_dummy_provider_json() - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - with self.assertRaises(UnsupportedProviderAPI): - self.pb._download_provider_info() + with self.assertRaises(UnsupportedProviderAPI): + self.pb._download_provider_info() @mock.patch( 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', @@ -503,10 +493,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): self._setup_providerbootstrapper(False) self._produce_dummy_provider_json() - with mock.patch.object( - ProviderConfig, 'get_api_uri', - return_value="https://localhost:%s" % (self.https_port,)): - self.pb._download_provider_info() + self.pb._download_provider_info() @mock.patch( 'leap.bitmask.config.providerconfig.ProviderConfig.get_api_uri', diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py index afce72f6..f9456159 100644 --- a/src/leap/bitmask/services/__init__.py +++ b/src/leap/bitmask/services/__init__.py @@ -27,7 +27,7 @@ from leap.bitmask.crypto.srpauth import SRPAuth from leap.bitmask.util.constants import REQUEST_TIMEOUT from leap.bitmask.util.privilege_policies import is_missing_policy_permissions from leap.bitmask.util.request_helpers import get_content -from leap.bitmask.util import get_path_prefix +from leap.bitmask import util from leap.common.check import leap_assert from leap.common.config.baseconfig import BaseConfig @@ -105,10 +105,11 @@ def download_service_config(provider_config, service_config, service_name = service_config.name service_json = "{0}-service.json".format(service_name) headers = {} - mtime = get_mtime(os.path.join(get_path_prefix(), + mtime = get_mtime(os.path.join(util.get_path_prefix(), "leap", "providers", provider_config.get_domain(), service_json)) + if download_if_needed and mtime: headers['if-modified-since'] = mtime @@ -125,10 +126,15 @@ def download_service_config(provider_config, service_config, # XXX make and use @with_srp_auth decorator srp_auth = SRPAuth(provider_config) session_id = srp_auth.get_session_id() + token = srp_auth.get_token() cookies = None - if session_id: + if session_id is not None: cookies = {"_session_id": session_id} + # API v2 will only support token auth, but in v1 we can send both + if token is not None: + headers["Authorization"] = 'Token token="{0}"'.format(token) + res = session.get(config_uri, verify=provider_config.get_ca_cert_path(), headers=headers, diff --git a/src/leap/bitmask/services/connections.py b/src/leap/bitmask/services/connections.py index f3ab9e8e..8aeb4e0c 100644 --- a/src/leap/bitmask/services/connections.py +++ b/src/leap/bitmask/services/connections.py @@ -103,6 +103,7 @@ class AbstractLEAPConnection(object): # Bypass stages connection_died_signal = None + connection_aborted_signal = None class Disconnected(State): """Disconnected state""" diff --git a/src/leap/bitmask/services/eip/__init__.py b/src/leap/bitmask/services/eip/__init__.py index dd010027..6030cac3 100644 --- a/src/leap/bitmask/services/eip/__init__.py +++ b/src/leap/bitmask/services/eip/__init__.py @@ -20,7 +20,11 @@ leap.bitmask.services.eip module initialization import os import tempfile -from leap.bitmask.platform_init import IS_WIN +from leap.bitmask.services.eip.darwinvpnlauncher import DarwinVPNLauncher +from leap.bitmask.services.eip.linuxvpnlauncher import LinuxVPNLauncher +from leap.bitmask.services.eip.windowsvpnlauncher import WindowsVPNLauncher +from leap.bitmask.platform_init import IS_LINUX, IS_MAC, IS_WIN +from leap.common.check import leap_assert def get_openvpn_management(): @@ -40,3 +44,24 @@ def get_openvpn_management(): port = "unix" return host, port + + +def get_vpn_launcher(): + """ + Return the VPN launcher for the current platform. + """ + if not (IS_LINUX or IS_MAC or IS_WIN): + error_msg = "VPN Launcher not implemented for this platform." + raise NotImplementedError(error_msg) + + launcher = None + if IS_LINUX: + launcher = LinuxVPNLauncher + elif IS_MAC: + launcher = DarwinVPNLauncher + elif IS_WIN: + launcher = WindowsVPNLauncher + + leap_assert(launcher is not None) + + return launcher() diff --git a/src/leap/bitmask/services/eip/connection.py b/src/leap/bitmask/services/eip/connection.py index 5f05ba07..08b29070 100644 --- a/src/leap/bitmask/services/eip/connection.py +++ b/src/leap/bitmask/services/eip/connection.py @@ -40,6 +40,7 @@ class EIPConnectionSignals(QtCore.QObject): disconnected_signal = QtCore.Signal() connection_died_signal = QtCore.Signal() + connection_aborted_signal = QtCore.Signal() class EIPConnection(AbstractLEAPConnection): diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py new file mode 100644 index 00000000..f3b6bfc8 --- /dev/null +++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# darwinvpnlauncher.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/>. +""" +Darwin VPN launcher implementation. +""" +import commands +import getpass +import logging +import os + +from leap.bitmask.services.eip.vpnlauncher import VPNLauncher +from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException +from leap.bitmask.util import get_path_prefix + +logger = logging.getLogger(__name__) + + +class EIPNoTunKextLoaded(VPNLauncherException): + pass + + +class DarwinVPNLauncher(VPNLauncher): + """ + VPN launcher for the Darwin Platform + """ + COCOASUDO = "cocoasudo" + # XXX need the good old magic translate for these strings + # (look for magic in 0.2.0 release) + SUDO_MSG = ("Bitmask needs administrative privileges to run " + "Encrypted Internet.") + INSTALL_MSG = ("\"Bitmask needs administrative privileges to install " + "missing scripts and fix permissions.\"") + + INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../") + INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../") + OPENVPN_BIN = 'openvpn.leap' + OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) + OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % ( + INSTALL_PATH_ESCAPED,) + + 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) + OTHER_FILES = [] + + @classmethod + def cmd_for_missing_scripts(kls, frompath): + """ + Returns a command that can copy the missing scripts. + :rtype: str + """ + to = kls.OPENVPN_PATH_ESCAPED + + cmd = "#!/bin/sh\n" + cmd += "mkdir -p {0}\n".format(to) + cmd += "cp '{0}'/* {1}\n".format(frompath, to) + cmd += "chmod 744 {0}/*".format(to) + + return cmd + + @classmethod + def is_kext_loaded(kls): + """ + Checks if the needed kext is loaded before launching openvpn. + + :returns: True if kext is loaded, False otherwise. + :rtype: bool + """ + return bool(commands.getoutput('kextstat | grep "leap.tun"')) + + @classmethod + def _get_icon_path(kls): + """ + Returns the absolute path to the app icon. + + :rtype: str + """ + resources_path = os.path.abspath( + os.path.join(os.getcwd(), "../../Contents/Resources")) + + return os.path.join(resources_path, "leap-client.tiff") + + @classmethod + def get_cocoasudo_ovpn_cmd(kls): + """ + Returns a string with the cocoasudo command needed to run openvpn + as admin with a nice password prompt. The actual command needs to be + appended. + + :rtype: (str, list) + """ + # TODO add translation support for this + sudo_msg = ("Bitmask needs administrative privileges to run " + "Encrypted Internet.") + iconpath = kls._get_icon_path() + has_icon = os.path.isfile(iconpath) + args = ["--icon=%s" % iconpath] if has_icon else [] + args.append("--prompt=%s" % (sudo_msg,)) + + return kls.COCOASUDO, args + + @classmethod + def get_cocoasudo_installmissing_cmd(kls): + """ + Returns a string with the cocoasudo command needed to install missing + files as admin with a nice password prompt. The actual command needs to + be appended. + + :rtype: (str, list) + """ + # TODO add translation support for this + install_msg = ('"Bitmask needs administrative privileges to install ' + 'missing scripts and fix permissions."') + iconpath = kls._get_icon_path() + has_icon = os.path.isfile(iconpath) + args = ["--icon=%s" % iconpath] if has_icon else [] + args.append("--prompt=%s" % (install_msg,)) + + return kls.COCOASUDO, args + + @classmethod + def get_vpn_command(kls, eipconfig, providerconfig, socket_host, + socket_port="unix", openvpn_verb=1): + """ + Returns the OSX implementation for the vpn launching command. + + Might raise: + EIPNoTunKextLoaded, + OpenVPNNotFoundException, + VPNLauncherException. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, + or port otherwise + :type socket_port: str + :param openvpn_verb: the openvpn verbosity wanted + :type openvpn_verb: int + + :return: A VPN command ready to be launched. + :rtype: list + """ + if not kls.is_kext_loaded(): + raise EIPNoTunKextLoaded + + # we use `super` in order to send the class to use + command = super(DarwinVPNLauncher, kls).get_vpn_command( + eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) + + cocoa, cargs = kls.get_cocoasudo_ovpn_cmd() + cargs.extend(command) + command = cargs + command.insert(0, cocoa) + + command.extend(['--setenv', "LEAPUSER", getpass.getuser()]) + + return command + + @classmethod + def get_vpn_env(kls): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + :rtype: dict + """ + return { + "DYLD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib") + } diff --git a/src/leap/bitmask/services/eip/eipbootstrapper.py b/src/leap/bitmask/services/eip/eipbootstrapper.py index 885c4420..5a238a1c 100644 --- a/src/leap/bitmask/services/eip/eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/eipbootstrapper.py @@ -28,7 +28,6 @@ from leap.bitmask.services import download_service_config from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper from leap.bitmask.services.eip.eipconfig import EIPConfig from leap.common import certs as leap_certs -from leap.bitmask.util import get_path_prefix from leap.common.check import leap_assert, leap_assert_type from leap.common.files import check_and_fix_urw_only diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py index 7d8995b4..16ed4cc0 100644 --- a/src/leap/bitmask/services/eip/eipconfig.py +++ b/src/leap/bitmask/services/eip/eipconfig.py @@ -33,6 +33,45 @@ from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) +def get_eipconfig_path(domain): + """ + Returns relative path for EIP config. + + :param domain: the domain to which this eipconfig belongs to. + :type domain: str + :returns: the path + :rtype: str + """ + leap_assert(domain is not None, "get_eipconfig_path: We need a domain") + return os.path.join("leap", "providers", domain, "eip-service.json") + + +def load_eipconfig_if_needed(provider_config, eip_config, domain): + """ + Utility function to prime a eip_config object from a loaded + provider_config and the chosen provider domain. + + :param provider_config: a loaded instance of ProviderConfig + :type provider_config: ProviderConfig + + :param eip_config: the eipconfig object to be primed. + :type eip_config: EIPConfig + + :param domain: the chosen provider domain + :type domain: str + + :returns: Whether the eip_config object has been succesfully loaded + :rtype: bool + """ + loaded = eip_config.loaded() + if not loaded: + eip_config_path = get_eipconfig_path(domain) + api_version = provider_config.get_api_version() + eip_config.set_api_version(api_version) + loaded = eip_config.load(eip_config_path) + return loaded + + class VPNGatewaySelector(object): """ VPN Gateway selector. @@ -59,7 +98,6 @@ class VPNGatewaySelector(object): tz_offset = self.equivalent_timezones[tz_offset] self._local_offset = tz_offset - self._eipconfig = eipconfig def get_gateways_list(self): diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py new file mode 100644 index 00000000..c2c28627 --- /dev/null +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +# linuxvpnlauncher.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/>. +""" +Linux VPN launcher implementation. +""" +import commands +import logging +import os +import subprocess +import time + +from leap.bitmask.config import flags +from leap.bitmask.util import privilege_policies +from leap.bitmask.util.privilege_policies import LinuxPolicyChecker +from leap.common.files import which +from leap.bitmask.services.eip.vpnlauncher import VPNLauncher +from leap.bitmask.services.eip.vpnlauncher import VPNLauncherException +from leap.bitmask.util import get_path_prefix +from leap.common.check import leap_assert +from leap.bitmask.util import first + +logger = logging.getLogger(__name__) + + +class EIPNoPolkitAuthAgentAvailable(VPNLauncherException): + pass + + +class EIPNoPkexecAvailable(VPNLauncherException): + pass + + +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 _is_auth_agent_running(): + """ + Checks if a polkit daemon is running. + + :return: True if it's running, False if it's not. + :rtype: boolean + """ + ps = 'ps aux | grep polkit-%s-authentication-agent-1' + opts = (ps % case for case in ['[g]nome', '[k]de']) + is_running = map(lambda l: commands.getoutput(l), opts) + return any(is_running) + + +def _try_to_launch_agent(): + """ + Tries to launch a polkit daemon. + """ + env = None + if flags.STANDALONE is True: + env = {"PYTHONPATH": os.path.abspath('../../../../lib/')} + try: + # We need to quote the command because subprocess call + # will do "sh -c 'foo'", so if we do not quoute it we'll end + # up with a invocation to the python interpreter. And that + # is bad. + subprocess.call(["python -m leap.bitmask.util.polkit_agent"], + shell=True, env=env) + except Exception as exc: + logger.exception(exc) + + +class LinuxVPNLauncher(VPNLauncher): + PKEXEC_BIN = 'pkexec' + OPENVPN_BIN = 'openvpn' + OPENVPN_BIN_PATH = os.path.join( + get_path_prefix(), "..", "apps", "eip", OPENVPN_BIN) + + SYSTEM_CONFIG = "/etc/leap" + UP_DOWN_FILE = "resolv-update" + UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) + + # We assume this is there by our openvpn dependency, and + # we will put it there on the bundle too. + # TODO adapt to the bundle path. + OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" + OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" + OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( + OPENVPN_DOWN_ROOT_BASE, + OPENVPN_DOWN_ROOT_FILE) + + UP_SCRIPT = DOWN_SCRIPT = UP_DOWN_PATH + UPDOWN_FILES = (UP_DOWN_PATH,) + POLKIT_PATH = LinuxPolicyChecker.get_polkit_path() + OTHER_FILES = (POLKIT_PATH, ) + + @classmethod + def maybe_pkexec(kls): + """ + Checks whether pkexec is available in the system, and + returns the path if found. + + Might raise: + EIPNoPkexecAvailable, + EIPNoPolkitAuthAgentAvailable. + + :returns: a list of the paths where pkexec is to be found + :rtype: list + """ + if _is_pkexec_in_system(): + if not _is_auth_agent_running(): + _try_to_launch_agent() + time.sleep(0.5) + if _is_auth_agent_running(): + pkexec_possibilities = which(kls.PKEXEC_BIN) + leap_assert(len(pkexec_possibilities) > 0, + "We couldn't find pkexec") + return pkexec_possibilities + else: + logger.warning("No polkit auth agent found. pkexec " + + "will use its own auth agent.") + raise EIPNoPolkitAuthAgentAvailable() + else: + logger.warning("System has no pkexec") + raise EIPNoPkexecAvailable() + + @classmethod + def missing_other_files(kls): + """ + 'Extend' the VPNLauncher's missing_other_files to check if the polkit + files is outdated. If the polkit file that is in OTHER_FILES exists but + is not up to date, it is added to the missing list. + + :returns: a list of missing files + :rtype: list of str + """ + # we use `super` in order to send the class to use + missing = super(LinuxVPNLauncher, kls).missing_other_files() + polkit_file = LinuxPolicyChecker.get_polkit_path() + if polkit_file not in missing: + if privilege_policies.is_policy_outdated(kls.OPENVPN_BIN_PATH): + missing.append(polkit_file) + + return missing + + @classmethod + def get_vpn_command(kls, eipconfig, providerconfig, socket_host, + socket_port="unix", openvpn_verb=1): + """ + Returns the Linux implementation for the vpn launching command. + + Might raise: + EIPNoPkexecAvailable, + EIPNoPolkitAuthAgentAvailable, + OpenVPNNotFoundException, + VPNLauncherException. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, + or port otherwise + :type socket_port: str + :param openvpn_verb: the openvpn verbosity wanted + :type openvpn_verb: int + + :return: A VPN command ready to be launched. + :rtype: list + """ + # we use `super` in order to send the class to use + command = super(LinuxVPNLauncher, kls).get_vpn_command( + eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) + + pkexec = kls.maybe_pkexec() + if pkexec: + command.insert(0, first(pkexec)) + + return command + + @classmethod + def cmd_for_missing_scripts(kls, frompath, pol_file): + """ + Returns a sh script that can copy the missing files. + + :param frompath: The path where the up/down scripts live + :type frompath: str + :param pol_file: The path where the dynamically generated + policy file lives + :type pol_file: str + + :rtype: str + """ + to = kls.SYSTEM_CONFIG + + cmd = '#!/bin/sh\n' + cmd += 'mkdir -p "%s"\n' % (to, ) + cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to) + cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH) + cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, ) + + return cmd + + @classmethod + def get_vpn_env(kls): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + :rtype: dict + """ + return { + "LD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib") + } diff --git a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py index d0d78eed..6640a860 100644 --- a/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py +++ b/src/leap/bitmask/services/eip/tests/test_eipbootstrapper.py @@ -41,6 +41,7 @@ from leap.bitmask.services.eip.eipconfig import EIPConfig from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.tests import fake_provider from leap.bitmask.crypto.srpauth import SRPAuth +from leap.bitmask import util from leap.common.testing.basetest import BaseLeapTest from leap.common.files import mkdir_p @@ -60,13 +61,13 @@ class EIPBootstrapperActiveTest(BaseLeapTest): def setUp(self): self.eb = EIPBootstrapper() - self.old_pp = EIPConfig.get_path_prefix + self.old_pp = util.get_path_prefix self.old_save = EIPConfig.save self.old_load = EIPConfig.load self.old_si = SRPAuth.get_session_id def tearDown(self): - EIPConfig.get_path_prefix = self.old_pp + util.get_path_prefix = self.old_pp EIPConfig.save = self.old_save EIPConfig.load = self.old_load SRPAuth.get_session_id = self.old_si @@ -97,13 +98,13 @@ class EIPBootstrapperActiveTest(BaseLeapTest): pc.get_ca_cert_path = mock.MagicMock(return_value=False) path_prefix = tempfile.mkdtemp() - EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + util.get_path_prefix = mock.MagicMock(return_value=path_prefix) EIPConfig.save = mock.MagicMock() EIPConfig.load = mock.MagicMock() self.eb._download_if_needed = ifneeded - provider_dir = os.path.join(EIPConfig.get_path_prefix(), + provider_dir = os.path.join(util.get_path_prefix(), "leap", "providers", pc.get_domain()) @@ -150,10 +151,10 @@ class EIPBootstrapperActiveTest(BaseLeapTest): def check(*args): self.eb._eip_config.save.assert_called_once_with( - ["leap", + ("leap", "providers", self.eb._provider_config.get_domain(), - "eip-service.json"]) + "eip-service.json")) d.addCallback(check) return d @@ -184,13 +185,13 @@ class EIPBootstrapperActiveTest(BaseLeapTest): pc.get_ca_cert_path = mock.MagicMock(return_value=False) path_prefix = tempfile.mkdtemp() - EIPConfig.get_path_prefix = mock.MagicMock(return_value=path_prefix) + util.get_path_prefix = mock.MagicMock(return_value=path_prefix) EIPConfig.save = mock.MagicMock() EIPConfig.load = mock.MagicMock() self.eb._download_if_needed = ifneeded - provider_dir = os.path.join(EIPConfig.get_path_prefix(), + provider_dir = os.path.join(util.get_path_prefix(), "leap", "providers", "somedomain") diff --git a/src/leap/bitmask/services/eip/tests/test_eipconfig.py b/src/leap/bitmask/services/eip/tests/test_eipconfig.py index 76305e83..4e340f4c 100644 --- a/src/leap/bitmask/services/eip/tests/test_eipconfig.py +++ b/src/leap/bitmask/services/eip/tests/test_eipconfig.py @@ -262,15 +262,13 @@ class EIPConfigTest(BaseLeapTest): def test_get_client_cert_path_as_expected(self): config = self._get_eipconfig() - config.get_path_prefix = Mock(return_value='test') - provider_config = ProviderConfig() # mock 'get_domain' so we don't need to load a config provider_domain = 'test.provider.com' provider_config.get_domain = Mock(return_value=provider_domain) - expected_path = os.path.join('test', 'leap', 'providers', + expected_path = os.path.join('leap', 'providers', provider_domain, 'keys', 'client', 'openvpn.pem') @@ -278,26 +276,24 @@ class EIPConfigTest(BaseLeapTest): os.path.exists = Mock(return_value=True) cert_path = config.get_client_cert_path(provider_config) - self.assertEqual(cert_path, expected_path) + self.assertTrue(cert_path.endswith(expected_path)) def test_get_client_cert_path_about_to_download(self): config = self._get_eipconfig() - config.get_path_prefix = Mock(return_value='test') - provider_config = ProviderConfig() # mock 'get_domain' so we don't need to load a config provider_domain = 'test.provider.com' provider_config.get_domain = Mock(return_value=provider_domain) - expected_path = os.path.join('test', 'leap', 'providers', + expected_path = os.path.join('leap', 'providers', provider_domain, 'keys', 'client', 'openvpn.pem') cert_path = config.get_client_cert_path( provider_config, about_to_download=True) - self.assertEqual(cert_path, expected_path) + self.assertTrue(cert_path.endswith(expected_path)) def test_get_client_cert_path_fails(self): config = self._get_eipconfig() diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py new file mode 100644 index 00000000..935d75f1 --- /dev/null +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# vpnlauncher.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/>. +""" +Platform independant VPN launcher interface. +""" +import getpass +import logging +import os +import stat + +from abc import ABCMeta, abstractmethod +from functools import partial + +from leap.bitmask.config import flags +from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector +from leap.bitmask.util import first +from leap.bitmask.util import get_path_prefix +from leap.common.check import leap_assert, leap_assert_type +from leap.common.files import which + +logger = logging.getLogger(__name__) + + +class VPNLauncherException(Exception): + pass + + +class OpenVPNNotFoundException(VPNLauncherException): + pass + + +def _has_updown_scripts(path, warn=True): + """ + Checks the existence of the up/down scripts and its + exec bit if applicable. + + :param path: the path to be checked + :type path: str + + :param warn: whether we should log the absence + :type warn: bool + + :rtype: bool + """ + is_file = os.path.isfile(path) + if warn and not is_file: + logger.error("Could not find up/down script %s. " + "Might produce DNS leaks." % (path,)) + + # XXX check if applies in win + is_exe = False + try: + is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) + except OSError as e: + logger.warn("%s" % (e,)) + 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 _has_other_files(path, warn=True): + """ + Checks the existence of other important files. + + :param path: the path to be checked + :type path: str + + :param warn: whether we should log the absence + :type warn: bool + + :rtype: bool + """ + is_file = os.path.isfile(path) + if warn and not is_file: + logger.warning("Could not find file during checks: %s. " % ( + path,)) + return is_file + + +class VPNLauncher(object): + """ + Abstract launcher class + """ + __metaclass__ = ABCMeta + + UPDOWN_FILES = None + OTHER_FILES = None + + @classmethod + @abstractmethod + def get_vpn_command(kls, eipconfig, providerconfig, + socket_host, socket_port, openvpn_verb=1): + """ + Returns the platform dependant vpn launching command + + Might raise: + OpenVPNNotFoundException, + VPNLauncherException. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, + or port otherwise + :type socket_port: str + :param openvpn_verb: the openvpn verbosity wanted + :type openvpn_verb: int + + :return: A VPN command ready to be launched. + :rtype: list + """ + leap_assert_type(eipconfig, EIPConfig) + leap_assert_type(providerconfig, ProviderConfig) + + kwargs = {} + if flags.STANDALONE: + kwargs['path_extension'] = os.path.join( + get_path_prefix(), "..", "apps", "eip") + + openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) + if len(openvpn_possibilities) == 0: + raise OpenVPNNotFoundException() + + openvpn = first(openvpn_possibilities) + args = [] + + args += [ + '--setenv', "LEAPOPENVPN", "1" + ] + + if openvpn_verb is not None: + args += ['--verb', '%d' % (openvpn_verb,)] + + gateways = [] + leap_settings = LeapSettings() + domain = providerconfig.get_domain() + gateway_conf = leap_settings.get_selected_gateway(domain) + + if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: + gateway_selector = VPNGatewaySelector(eipconfig) + gateways = gateway_selector.get_gateways() + else: + gateways = [gateway_conf] + + if not gateways: + logger.error('No gateway was found!') + raise VPNLauncherException(kls.tr('No gateway was found!')) + + logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) + + for gw in gateways: + args += ['--remote', gw, '1194', 'udp'] + + args += [ + '--client', + '--dev', 'tun', + ############################################################## + # persist-tun makes ping-restart fail because it leaves a + # broken routing table + ############################################################## + # '--persist-tun', + '--persist-key', + '--tls-client', + '--remote-cert-tls', + 'server' + ] + + openvpn_configuration = eipconfig.get_openvpn_configuration() + for key, value in openvpn_configuration.items(): + args += ['--%s' % (key,), value] + + user = getpass.getuser() + + ############################################################## + # The down-root plugin fails in some situations, so we don't + # drop privs for the time being + ############################################################## + # args += [ + # '--user', user, + # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name + # ] + + if socket_port == "unix": # that's always the case for linux + args += [ + '--management-client-user', user + ] + + args += [ + '--management-signal', + '--management', socket_host, socket_port, + '--script-security', '2' + ] + + if _has_updown_scripts(kls.UP_SCRIPT): + args += [ + '--up', '\"%s\"' % (kls.UP_SCRIPT,), + ] + + if _has_updown_scripts(kls.DOWN_SCRIPT): + args += [ + '--down', '\"%s\"' % (kls.DOWN_SCRIPT,) + ] + + ########################################################### + # For the time being we are disabling the usage of the + # down-root plugin, because it doesn't quite work as + # expected (i.e. it doesn't run route -del as root + # when finishing, so it fails to properly + # restart/quit) + ########################################################### + # if _has_updown_scripts(kls.OPENVPN_DOWN_PLUGIN): + # args += [ + # '--plugin', kls.OPENVPN_DOWN_ROOT, + # '\'%s\'' % kls.DOWN_SCRIPT # for OSX + # '\'script_type=down %s\'' % kls.DOWN_SCRIPT # for Linux + # ] + + args += [ + '--cert', eipconfig.get_client_cert_path(providerconfig), + '--key', eipconfig.get_client_cert_path(providerconfig), + '--ca', providerconfig.get_ca_cert_path() + ] + + command_and_args = [openvpn] + args + logger.debug("Running VPN with command:") + logger.debug(" ".join(command_and_args)) + + return command_and_args + + @classmethod + def get_vpn_env(kls): + """ + Returns a dictionary with the custom env for the platform. + This is mainly used for setting LD_LIBRARY_PATH to the correct + path when distributing a standalone client + + :rtype: dict + """ + return {} + + @classmethod + def missing_updown_scripts(kls): + """ + Returns what updown scripts are missing. + + :rtype: list + """ + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define UPDOWN_FILES for this particular " + "launcher before calling this method") + 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] + + @classmethod + def missing_other_files(kls): + """ + Returns what other important files are missing during startup. + Same as missing_updown_scripts but does not check for exec bit. + + :rtype: list + """ + leap_assert(kls.OTHER_FILES is not None, + "Need to define OTHER_FILES for this particular " + "auncher before calling this method") + file_exist = partial(_has_other_files, warn=False) + zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] diff --git a/src/leap/bitmask/services/eip/vpnlaunchers.py b/src/leap/bitmask/services/eip/vpnlaunchers.py deleted file mode 100644 index daa0d81f..00000000 --- a/src/leap/bitmask/services/eip/vpnlaunchers.py +++ /dev/null @@ -1,963 +0,0 @@ -# -*- coding: utf-8 -*- -# vpnlaunchers.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/>. - -""" -Platform dependant VPN launchers -""" -import commands -import logging -import getpass -import os -import platform -import stat -import subprocess -try: - import grp -except ImportError: - pass # ignore, probably windows - -from abc import ABCMeta, abstractmethod -from functools import partial -from time import sleep - -from leap.bitmask.config import flags -from leap.bitmask.config.leapsettings import LeapSettings - -from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector -from leap.bitmask.util import first -from leap.bitmask.util import get_path_prefix -from leap.bitmask.util.privilege_policies import LinuxPolicyChecker -from leap.bitmask.util import privilege_policies -from leap.common.check import leap_assert, leap_assert_type -from leap.common.files import which - - -logger = logging.getLogger(__name__) - - -class VPNLauncherException(Exception): - pass - - -class OpenVPNNotFoundException(VPNLauncherException): - pass - - -class EIPNoPolkitAuthAgentAvailable(VPNLauncherException): - pass - - -class EIPNoPkexecAvailable(VPNLauncherException): - pass - - -class EIPNoTunKextLoaded(VPNLauncherException): - pass - - -class VPNLauncher(object): - """ - Abstract launcher class - """ - __metaclass__ = ABCMeta - - UPDOWN_FILES = None - OTHER_FILES = None - - @abstractmethod - def get_vpn_command(self, eipconfig=None, providerconfig=None, - socket_host=None, socket_port=None): - """ - Returns the platform dependant vpn launching command - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - - :return: A VPN command ready to be launched - :rtype: list - """ - return [] - - @abstractmethod - def get_vpn_env(self): - """ - Returns a dictionary with the custom env for the platform. - This is mainly used for setting LD_LIBRARY_PATH to the correct - path when distributing a standalone client - - :rtype: dict - """ - return {} - - @classmethod - def missing_updown_scripts(kls): - """ - Returns what updown scripts are missing. - :rtype: list - """ - leap_assert(kls.UPDOWN_FILES is not None, - "Need to define UPDOWN_FILES for this particular " - "auncher before calling this method") - 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] - - @classmethod - def missing_other_files(kls): - """ - Returns what other important files are missing during startup. - Same as missing_updown_scripts but does not check for exec bit. - :rtype: list - """ - leap_assert(kls.UPDOWN_FILES is not None, - "Need to define OTHER_FILES for this particular " - "auncher before calling this method") - file_exist = partial(_has_other_files, warn=False) - zipped = zip(kls.OTHER_FILES, map(file_exist, kls.OTHER_FILES)) - missing = filter(lambda (path, exists): exists is False, zipped) - return [path for path, exists in missing] - - -def get_platform_launcher(): - launcher = globals()[platform.system() + "VPNLauncher"] - leap_assert(launcher, "Unimplemented platform launcher: %s" % - (platform.system(),)) - return 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, warn=True): - """ - Checks the existence of the up/down scripts and its - exec bit if applicable. - - :param path: the path to be checked - :type path: str - - :param warn: whether we should log the absence - :type warn: bool - - :rtype: bool - """ - is_file = os.path.isfile(path) - if warn and not is_file: - logger.error("Could not find up/down script %s. " - "Might produce DNS leaks." % (path,)) - - # XXX check if applies in win - is_exe = False - try: - is_exe = (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] != 0) - except OSError as e: - logger.warn("%s" % (e,)) - 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 _has_other_files(path, warn=True): - """ - Checks the existence of other important files. - - :param path: the path to be checked - :type path: str - - :param warn: whether we should log the absence - :type warn: bool - - :rtype: bool - """ - is_file = os.path.isfile(path) - if warn and not is_file: - logger.warning("Could not find file during checks: %s. " % ( - path,)) - return is_file - - -def _is_auth_agent_running(): - """ - Checks if a polkit daemon is running. - - :return: True if it's running, False if it's not. - :rtype: boolean - """ - ps = 'ps aux | grep polkit-%s-authentication-agent-1' - opts = (ps % case for case in ['[g]nome', '[k]de']) - is_running = map(lambda l: commands.getoutput(l), opts) - return any(is_running) - - -def _try_to_launch_agent(): - """ - Tries to launch a polkit daemon. - """ - env = None - if flags.STANDALONE is True: - env = {"PYTHONPATH": os.path.abspath('../../../../lib/')} - try: - # We need to quote the command because subprocess call - # will do "sh -c 'foo'", so if we do not quoute it we'll end - # up with a invocation to the python interpreter. And that - # is bad. - subprocess.call(["python -m leap.bitmask.util.polkit_agent"], - shell=True, env=env) - except Exception as exc: - logger.exception(exc) - - -class LinuxVPNLauncher(VPNLauncher): - """ - VPN launcher for the Linux platform - """ - - PKEXEC_BIN = 'pkexec' - OPENVPN_BIN = 'openvpn' - OPENVPN_BIN_PATH = os.path.join( - get_path_prefix(), "..", "apps", "eip", OPENVPN_BIN) - - SYSTEM_CONFIG = "/etc/leap" - UP_DOWN_FILE = "resolv-update" - UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) - - # We assume this is there by our openvpn dependency, and - # we will put it there on the bundle too. - # TODO adapt to the bundle path. - OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" - OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" - OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( - OPENVPN_DOWN_ROOT_BASE, - OPENVPN_DOWN_ROOT_FILE) - - UPDOWN_FILES = (UP_DOWN_PATH,) - POLKIT_PATH = LinuxPolicyChecker.get_polkit_path() - OTHER_FILES = (POLKIT_PATH, ) - - def missing_other_files(self): - """ - 'Extend' the VPNLauncher's missing_other_files to check if the polkit - files is outdated. If the polkit file that is in OTHER_FILES exists but - is not up to date, it is added to the missing list. - - :returns: a list of missing files - :rtype: list of str - """ - missing = VPNLauncher.missing_other_files.im_func(self) - polkit_file = LinuxPolicyChecker.get_polkit_path() - if polkit_file not in missing: - if privilege_policies.is_policy_outdated(self.OPENVPN_BIN_PATH): - missing.append(polkit_file) - - return missing - - @classmethod - def cmd_for_missing_scripts(kls, frompath, pol_file): - """ - Returns a sh script that can copy the missing files. - - :param frompath: The path where the up/down scripts live - :type frompath: str - :param pol_file: The path where the dynamically generated - policy file lives - :type pol_file: str - - :rtype: str - """ - to = kls.SYSTEM_CONFIG - - cmd = '#!/bin/sh\n' - cmd += 'mkdir -p "%s"\n' % (to, ) - cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to) - cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH) - cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, ) - - return cmd - - @classmethod - def maybe_pkexec(kls): - """ - Checks whether pkexec is available in the system, and - returns the path if found. - - Might raise EIPNoPkexecAvailable or EIPNoPolkitAuthAgentAvailable - - :returns: a list of the paths where pkexec is to be found - :rtype: list - """ - if _is_pkexec_in_system(): - if not _is_auth_agent_running(): - _try_to_launch_agent() - sleep(0.5) - if _is_auth_agent_running(): - pkexec_possibilities = which(kls.PKEXEC_BIN) - leap_assert(len(pkexec_possibilities) > 0, - "We couldn't find pkexec") - return pkexec_possibilities - else: - logger.warning("No polkit auth agent found. pkexec " + - "will use its own auth agent.") - raise EIPNoPolkitAuthAgentAvailable() - else: - logger.warning("System has no pkexec") - raise EIPNoPkexecAvailable() - - @classmethod - def maybe_down_plugin(kls): - """ - Returns the path of the openvpn down-root-plugin, searching first - in the relative path for the standalone bundle, and then in the system - path where the debian package puts it. - - :returns: the path where the plugin was found, or None - :rtype: str or None - """ - cwd = os.getcwd() - rel_path_in_bundle = os.path.join( - 'apps', 'eip', 'files', kls.OPENVPN_DOWN_ROOT_FILE) - abs_path_in_bundle = os.path.join(cwd, rel_path_in_bundle) - if os.path.isfile(abs_path_in_bundle): - return abs_path_in_bundle - abs_path_in_system = kls.OPENVPN_DOWN_ROOT_PATH - if os.path.isfile(abs_path_in_system): - return abs_path_in_system - - logger.warning("We could not find the down-root-plugin, so no updown " - "scripts will be run. DNS leaks are likely!") - return None - - def get_vpn_command(self, eipconfig=None, providerconfig=None, - socket_host=None, socket_port="unix", openvpn_verb=1): - """ - Returns the platform dependant vpn launching command. It will - look for openvpn in the regular paths and algo in - path_prefix/apps/eip/ (in case standalone is set) - - Might raise: - VPNLauncherException, - OpenVPNNotFoundException. - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - - :param openvpn_verb: openvpn verbosity wanted - :type openvpn_verb: int - - :return: A VPN command ready to be launched - :rtype: list - """ - leap_assert(eipconfig, "We need an eip config") - leap_assert_type(eipconfig, EIPConfig) - leap_assert(providerconfig, "We need a provider config") - leap_assert_type(providerconfig, ProviderConfig) - leap_assert(socket_host, "We need a socket host!") - leap_assert(socket_port, "We need a socket port!") - - kwargs = {} - if flags.STANDALONE: - kwargs['path_extension'] = os.path.join( - get_path_prefix(), "..", "apps", "eip") - - openvpn_possibilities = which(self.OPENVPN_BIN, **kwargs) - - if len(openvpn_possibilities) == 0: - raise OpenVPNNotFoundException() - - openvpn = first(openvpn_possibilities) - args = [] - - pkexec = self.maybe_pkexec() - if pkexec: - args.append(openvpn) - openvpn = first(pkexec) - - args += [ - '--setenv', "LEAPOPENVPN", "1" - ] - - if openvpn_verb is not None: - args += ['--verb', '%d' % (openvpn_verb,)] - - gateways = [] - leap_settings = LeapSettings() - domain = providerconfig.get_domain() - gateway_conf = leap_settings.get_selected_gateway(domain) - - if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() - else: - gateways = [gateway_conf] - - if not gateways: - logger.error('No gateway was found!') - raise VPNLauncherException(self.tr('No gateway was found!')) - - logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) - - for gw in gateways: - args += ['--remote', gw, '1194', 'udp'] - - args += [ - '--client', - '--dev', 'tun', - ############################################################## - # persist-tun makes ping-restart fail because it leaves a - # broken routing table - ############################################################## - # '--persist-tun', - '--persist-key', - '--tls-client', - '--remote-cert-tls', - 'server' - ] - - openvpn_configuration = eipconfig.get_openvpn_configuration() - - for key, value in openvpn_configuration.items(): - args += ['--%s' % (key,), value] - - ############################################################## - # The down-root plugin fails in some situations, so we don't - # drop privs for the time being - ############################################################## - # args += [ - # '--user', getpass.getuser(), - # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name - # ] - - if socket_port == "unix": # that's always the case for linux - args += [ - '--management-client-user', getpass.getuser() - ] - - args += [ - '--management-signal', - '--management', socket_host, socket_port, - '--script-security', '2' - ] - - plugin_path = self.maybe_down_plugin() - # If we do not have the down plugin neither in the bundle - # nor in the system, we do not do updown scripts. The alternative - # is leaving the user without the ability to restore dns and routes - # to its original state. - - if plugin_path and _has_updown_scripts(self.UP_DOWN_PATH): - args += [ - '--up', self.UP_DOWN_PATH, - '--down', self.UP_DOWN_PATH, - ############################################################## - # For the time being we are disabling the usage of the - # down-root plugin, because it doesn't quite work as - # expected (i.e. it doesn't run route -del as root - # when finishing, so it fails to properly - # restart/quit) - ############################################################## - # '--plugin', plugin_path, - # '\'script_type=down %s\'' % self.UP_DOWN_PATH - ] - - args += [ - '--cert', eipconfig.get_client_cert_path(providerconfig), - '--key', eipconfig.get_client_cert_path(providerconfig), - '--ca', providerconfig.get_ca_cert_path() - ] - - logger.debug("Running VPN with command:") - logger.debug("%s %s" % (openvpn, " ".join(args))) - - return [openvpn] + args - - def get_vpn_env(self): - """ - Returns a dictionary with the custom env for the platform. - This is mainly used for setting LD_LIBRARY_PATH to the correct - path when distributing a standalone client - - :rtype: dict - """ - return { - "LD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib") - } - - -class DarwinVPNLauncher(VPNLauncher): - """ - VPN launcher for the Darwin Platform - """ - - COCOASUDO = "cocoasudo" - # XXX need the good old magic translate for these strings - # (look for magic in 0.2.0 release) - SUDO_MSG = ("Bitmask needs administrative privileges to run " - "Encrypted Internet.") - INSTALL_MSG = ("\"Bitmask needs administrative privileges to install " - "missing scripts and fix permissions.\"") - - INSTALL_PATH = os.path.realpath(os.getcwd() + "/../../") - INSTALL_PATH_ESCAPED = os.path.realpath(os.getcwd() + "/../../") - OPENVPN_BIN = 'openvpn.leap' - OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,) - OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % ( - INSTALL_PATH_ESCAPED,) - - 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) - OTHER_FILES = [] - - @classmethod - def cmd_for_missing_scripts(kls, frompath): - """ - Returns a command that can copy the missing scripts. - :rtype: str - """ - to = kls.OPENVPN_PATH_ESCAPED - cmd = "#!/bin/sh\nmkdir -p %s\ncp \"%s/\"* %s\nchmod 744 %s/*" % ( - to, frompath, to, to) - return cmd - - @classmethod - def maybe_kextloaded(kls): - """ - Checks if the needed kext is loaded before launching openvpn. - """ - return bool(commands.getoutput('kextstat | grep "leap.tun"')) - - def _get_resource_path(self): - """ - Returns the absolute path to the app resources directory - - :rtype: str - """ - return os.path.abspath( - os.path.join( - os.getcwd(), - "../../Contents/Resources")) - - def _get_icon_path(self): - """ - Returns the absolute path to the app icon - - :rtype: str - """ - return os.path.join(self._get_resource_path(), - "leap-client.tiff") - - def get_cocoasudo_ovpn_cmd(self): - """ - Returns a string with the cocoasudo command needed to run openvpn - as admin with a nice password prompt. The actual command needs to be - appended. - - :rtype: (str, list) - """ - iconpath = self._get_icon_path() - has_icon = os.path.isfile(iconpath) - args = ["--icon=%s" % iconpath] if has_icon else [] - args.append("--prompt=%s" % (self.SUDO_MSG,)) - - return self.COCOASUDO, args - - def get_cocoasudo_installmissing_cmd(self): - """ - Returns a string with the cocoasudo command needed to install missing - files as admin with a nice password prompt. The actual command needs to - be appended. - - :rtype: (str, list) - """ - iconpath = self._get_icon_path() - has_icon = os.path.isfile(iconpath) - args = ["--icon=%s" % iconpath] if has_icon else [] - args.append("--prompt=%s" % (self.INSTALL_MSG,)) - - return self.COCOASUDO, args - - def get_vpn_command(self, eipconfig=None, providerconfig=None, - socket_host=None, socket_port="unix", openvpn_verb=1): - """ - Returns the platform dependant vpn launching command - - Might raise VPNException. - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - - :param openvpn_verb: openvpn verbosity wanted - :type openvpn_verb: int - - :return: A VPN command ready to be launched - :rtype: list - """ - leap_assert(eipconfig, "We need an eip config") - leap_assert_type(eipconfig, EIPConfig) - leap_assert(providerconfig, "We need a provider config") - leap_assert_type(providerconfig, ProviderConfig) - leap_assert(socket_host, "We need a socket host!") - leap_assert(socket_port, "We need a socket port!") - - if not self.maybe_kextloaded(): - raise EIPNoTunKextLoaded - - kwargs = {} - if flags.STANDALONE: - kwargs['path_extension'] = os.path.join( - get_path_prefix(), "..", "apps", "eip") - - openvpn_possibilities = which( - self.OPENVPN_BIN, - **kwargs) - if len(openvpn_possibilities) == 0: - raise OpenVPNNotFoundException() - - openvpn = first(openvpn_possibilities) - args = [openvpn] - - args += [ - '--setenv', "LEAPOPENVPN", "1" - ] - - if openvpn_verb is not None: - args += ['--verb', '%d' % (openvpn_verb,)] - - gateways = [] - leap_settings = LeapSettings() - domain = providerconfig.get_domain() - gateway_conf = leap_settings.get_selected_gateway(domain) - - if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() - else: - gateways = [gateway_conf] - - if not gateways: - logger.error('No gateway was found!') - raise VPNLauncherException(self.tr('No gateway was found!')) - - logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) - - for gw in gateways: - args += ['--remote', gw, '1194', 'udp'] - - args += [ - '--client', - '--dev', 'tun', - ############################################################## - # persist-tun makes ping-restart fail because it leaves a - # broken routing table - ############################################################## - # '--persist-tun', - '--persist-key', - '--tls-client', - '--remote-cert-tls', - 'server' - ] - - openvpn_configuration = eipconfig.get_openvpn_configuration() - for key, value in openvpn_configuration.items(): - args += ['--%s' % (key,), value] - - user = getpass.getuser() - - ############################################################## - # The down-root plugin fails in some situations, so we don't - # drop privs for the time being - ############################################################## - # args += [ - # '--user', user, - # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name - # ] - - if socket_port == "unix": - args += [ - '--management-client-user', user - ] - - args += [ - '--management-signal', - '--management', socket_host, socket_port, - '--script-security', '2' - ] - - if _has_updown_scripts(self.UP_SCRIPT): - args += [ - '--up', '\"%s\"' % (self.UP_SCRIPT,), - ] - - if _has_updown_scripts(self.DOWN_SCRIPT): - args += [ - '--down', '\"%s\"' % (self.DOWN_SCRIPT,) - ] - - # should have the down script too - if _has_updown_scripts(self.OPENVPN_DOWN_PLUGIN): - args += [ - ########################################################### - # For the time being we are disabling the usage of the - # down-root plugin, because it doesn't quite work as - # expected (i.e. it doesn't run route -del as root - # when finishing, so it fails to properly - # restart/quit) - ########################################################### - # '--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), - '--key', eipconfig.get_client_cert_path(providerconfig), - '--ca', providerconfig.get_ca_cert_path() - ] - - command, cargs = self.get_cocoasudo_ovpn_cmd() - cmd_args = cargs + args - - logger.debug("Running VPN with command:") - logger.debug("%s %s" % (command, " ".join(cmd_args))) - - return [command] + cmd_args - - def get_vpn_env(self): - """ - Returns a dictionary with the custom env for the platform. - This is mainly used for setting LD_LIBRARY_PATH to the correct - path when distributing a standalone client - - :rtype: dict - """ - return { - "DYLD_LIBRARY_PATH": os.path.join(get_path_prefix(), "..", "lib") - } - - -class WindowsVPNLauncher(VPNLauncher): - """ - VPN launcher for the Windows platform - """ - - OPENVPN_BIN = 'openvpn_leap.exe' - - # XXX UPDOWN_FILES ... we do not have updown files defined yet! - # (and maybe we won't) - - def get_vpn_command(self, eipconfig=None, providerconfig=None, - socket_host=None, socket_port="9876", openvpn_verb=1): - """ - Returns the platform dependant vpn launching command. It will - look for openvpn in the regular paths and algo in - path_prefix/apps/eip/ (in case standalone is set) - - Might raise VPNException. - - :param eipconfig: eip configuration object - :type eipconfig: EIPConfig - - :param providerconfig: provider specific configuration - :type providerconfig: ProviderConfig - - :param socket_host: either socket path (unix) or socket IP - :type socket_host: str - - :param socket_port: either string "unix" if it's a unix - socket, or port otherwise - :type socket_port: str - - :param openvpn_verb: the openvpn verbosity wanted - :type openvpn_verb: int - - :return: A VPN command ready to be launched - :rtype: list - """ - leap_assert(eipconfig, "We need an eip config") - leap_assert_type(eipconfig, EIPConfig) - leap_assert(providerconfig, "We need a provider config") - leap_assert_type(providerconfig, ProviderConfig) - leap_assert(socket_host, "We need a socket host!") - leap_assert(socket_port, "We need a socket port!") - leap_assert(socket_port != "unix", - "We cannot use unix sockets in windows!") - - openvpn_possibilities = which( - self.OPENVPN_BIN, - path_extension=os.path.join(get_path_prefix(), - "..", "apps", "eip")) - - if len(openvpn_possibilities) == 0: - raise OpenVPNNotFoundException() - - openvpn = first(openvpn_possibilities) - args = [] - - args += [ - '--setenv', "LEAPOPENVPN", "1" - ] - - if openvpn_verb is not None: - args += ['--verb', '%d' % (openvpn_verb,)] - - gateways = [] - leap_settings = LeapSettings() - domain = providerconfig.get_domain() - gateway_conf = leap_settings.get_selected_gateway(domain) - - if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() - else: - gateways = [gateway_conf] - - if not gateways: - logger.error('No gateway was found!') - raise VPNLauncherException(self.tr('No gateway was found!')) - - logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) - - for gw in gateways: - args += ['--remote', gw, '1194', 'udp'] - - args += [ - '--client', - '--dev', 'tun', - ############################################################## - # persist-tun makes ping-restart fail because it leaves a - # broken routing table - ############################################################## - # '--persist-tun', - '--persist-key', - '--tls-client', - # We make it log to a file because we cannot attach to the - # openvpn process' stdout since it's a process with more - # privileges than we are - '--log-append', 'eip.log', - '--remote-cert-tls', - 'server' - ] - - openvpn_configuration = eipconfig.get_openvpn_configuration() - for key, value in openvpn_configuration.items(): - args += ['--%s' % (key,), value] - - ############################################################## - # The down-root plugin fails in some situations, so we don't - # drop privs for the time being - ############################################################## - # args += [ - # '--user', getpass.getuser(), - # #'--group', grp.getgrgid(os.getgroups()[-1]).gr_name - # ] - - args += [ - '--management-signal', - '--management', socket_host, socket_port, - '--script-security', '2' - ] - - args += [ - '--cert', eipconfig.get_client_cert_path(providerconfig), - '--key', eipconfig.get_client_cert_path(providerconfig), - '--ca', providerconfig.get_ca_cert_path() - ] - - logger.debug("Running VPN with command:") - logger.debug("%s %s" % (openvpn, " ".join(args))) - - return [openvpn] + args - - def get_vpn_env(self): - """ - Returns a dictionary with the custom env for the platform. - This is mainly used for setting LD_LIBRARY_PATH to the correct - path when distributing a standalone client - - :rtype: dict - """ - return {} - - -if __name__ == "__main__": - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - try: - abs_launcher = VPNLauncher() - except Exception as e: - assert isinstance(e, TypeError), "Something went wrong" - print "Abstract Prefixer class is working as expected" - - vpnlauncher = get_platform_launcher() - - eipconfig = EIPConfig() - eipconfig.set_api_version('1') - if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): - provider = ProviderConfig() - if provider.load("leap/providers/bitmask.net/provider.json"): - vpnlauncher.get_vpn_command(eipconfig=eipconfig, - providerconfig=provider, - socket_host="/blah") diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 15ac812b..707967e0 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -27,7 +27,7 @@ import socket from PySide import QtCore from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.services.eip.vpnlaunchers import get_platform_launcher +from leap.bitmask.services.eip import get_vpn_launcher from leap.bitmask.services.eip.eipconfig import EIPConfig from leap.bitmask.services.eip.udstelnet import UDSTelnet from leap.bitmask.util import first @@ -697,7 +697,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): self._socket_host = socket_host self._socket_port = socket_port - self._launcher = get_platform_launcher() + self._launcher = get_vpn_launcher() self._last_state = None self._last_status = None diff --git a/src/leap/bitmask/services/eip/windowsvpnlauncher.py b/src/leap/bitmask/services/eip/windowsvpnlauncher.py new file mode 100644 index 00000000..3f1ed43b --- /dev/null +++ b/src/leap/bitmask/services/eip/windowsvpnlauncher.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# windowsvpnlauncher.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/>. +""" +Windows VPN launcher implementation. +""" +import logging + +from leap.bitmask.services.eip.vpnlauncher import VPNLauncher +from leap.common.check import leap_assert + +logger = logging.getLogger(__name__) + + +class WindowsVPNLauncher(VPNLauncher): + """ + VPN launcher for the Windows platform + """ + + OPENVPN_BIN = 'openvpn_leap.exe' + + # XXX UPDOWN_FILES ... we do not have updown files defined yet! + # (and maybe we won't) + @classmethod + def get_vpn_command(kls, eipconfig, providerconfig, socket_host, + socket_port="9876", openvpn_verb=1): + """ + Returns the Windows implementation for the vpn launching command. + + Might raise: + OpenVPNNotFoundException, + VPNLauncherException. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + :param socket_host: either socket path (unix) or socket IP + :type socket_host: str + :param socket_port: either string "unix" if it's a unix socket, + or port otherwise + :type socket_port: str + :param openvpn_verb: the openvpn verbosity wanted + :type openvpn_verb: int + + :return: A VPN command ready to be launched. + :rtype: list + """ + leap_assert(socket_port != "unix", + "We cannot use unix sockets in windows!") + + # we use `super` in order to send the class to use + command = super(WindowsVPNLauncher, kls).get_vpn_command( + eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) + + return command diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index cac91440..7968dd6a 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -14,27 +14,28 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - """ Soledad bootstrapping """ - import logging import os import socket +from ssl import SSLError + from PySide import QtCore from u1db import errors as u1db_errors from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.srpauth import SRPAuth +from leap.bitmask.services import download_service_config from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper from leap.bitmask.services.soledad.soledadconfig import SoledadConfig -from leap.bitmask.util.request_helpers import get_content +from leap.bitmask.util import is_file, is_empty_file from leap.bitmask.util import get_path_prefix -from leap.common.check import leap_assert, leap_assert_type -from leap.common.files import get_mtime +from leap.common.check import leap_assert, leap_assert_type, leap_check +from leap.common.files import which from leap.keymanager import KeyManager, openpgp from leap.keymanager.errors import KeyNotFound from leap.soledad.client import Soledad @@ -42,17 +43,28 @@ from leap.soledad.client import Soledad logger = logging.getLogger(__name__) +# TODO these exceptions could be moved to soledad itself +# after settling this down. + +class SoledadSyncError(Exception): + message = "Error while syncing Soledad" + + +class SoledadInitError(Exception): + message = "Error while initializing Soledad" + + class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure """ - SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" MAX_INIT_RETRIES = 10 + MAX_SYNC_RETRIES = 10 # All dicts returned are of the form # {"passed": bool, "error": str} @@ -68,6 +80,7 @@ class SoledadBootstrapper(AbstractBootstrapper): self._soledad_config = None self._keymanager = None self._download_if_needed = False + self._user = "" self._password = "" self._srpauth = None @@ -109,68 +122,169 @@ class SoledadBootstrapper(AbstractBootstrapper): """ self._soledad_retries += 1 + def _get_db_paths(self, uuid): + """ + Returns the secrets and local db paths needed for soledad + initialization + + :param uuid: uuid for user + :type uuid: str + + :return: a tuple with secrets, local_db paths + :rtype: tuple + """ + prefix = os.path.join(get_path_prefix(), "leap", "soledad") + secrets = "%s/%s.secret" % (prefix, uuid) + local_db = "%s/%s.db" % (prefix, uuid) + + # We remove an empty file if found to avoid complains + # about the db not being properly initialized + if is_file(local_db) and is_empty_file(local_db): + try: + os.remove(local_db) + except OSError: + logger.warning("Could not remove empty file %s" + % local_db) + return secrets, local_db + # initialization def load_and_sync_soledad(self): """ Once everthing is in the right place, we instantiate and sync Soledad - - :param srp_auth: SRPAuth object used - :type srp_auth: SRPAuth """ - srp_auth = self.srpauth - uuid = srp_auth.get_uid() + # TODO this method is still too large + uuid = self.srpauth.get_uid() + token = self.srpauth.get_token() - prefix = os.path.join(get_path_prefix(), "leap", "soledad") - secrets_path = "%s/%s.secret" % (prefix, uuid) - local_db_path = "%s/%s.db" % (prefix, uuid) + secrets_path, local_db_path = self._get_db_paths(uuid) # TODO: Select server based on timezone (issue #3308) server_dict = self._soledad_config.get_hosts() - if server_dict.keys(): - selected_server = server_dict[server_dict.keys()[0]] - server_url = "https://%s:%s/user-%s" % ( - selected_server["hostname"], - selected_server["port"], - uuid) + if not server_dict.keys(): + # XXX raise more specific exception, and catch it properly! + raise Exception("No soledad server found") + + selected_server = server_dict[server_dict.keys()[0]] + server_url = "https://%s:%s/user-%s" % ( + selected_server["hostname"], + selected_server["port"], + uuid) + logger.debug("Using soledad server url: %s" % (server_url,)) - logger.debug("Using soledad server url: %s" % (server_url,)) + cert_file = self._provider_config.get_ca_cert_path() - cert_file = self._provider_config.get_ca_cert_path() + logger.debug('local_db:%s' % (local_db_path,)) + logger.debug('secrets_path:%s' % (secrets_path,)) - # TODO: If selected server fails, retry with another host - # (issue #3309) + try: + self._try_soledad_init( + uuid, secrets_path, local_db_path, + server_url, cert_file, token) + except: + # re-raise the exceptions from try_init, + # we're currently handling the retries from the + # soledad-launcher in the gui. + raise + + leap_check(self._soledad is not None, + "Null soledad, error while initializing") + + # and now, let's sync + sync_tries = self.MAX_SYNC_RETRIES + while sync_tries > 0: try: - self._soledad = Soledad( - uuid, - self._password.encode("utf-8"), - secrets_path=secrets_path, - local_db_path=local_db_path, - server_url=server_url, - cert_file=cert_file, - auth_token=srp_auth.get_token()) - self._soledad.sync() - - # XXX All these errors should be handled by soledad itself, - # and return a subclass of SoledadInitializationFailed - except socket.timeout: - logger.debug("SOLEDAD TIMED OUT...") - self.soledad_timeout.emit() - except socket.error as exc: - logger.error("Socket error while initializing soledad") - self.soledad_failed.emit() - except u1db_errors.Unauthorized: - logger.error("Error while initializing soledad " - "(unauthorized).") - self.soledad_failed.emit() - except Exception as exc: - logger.error("Unhandled error while initializating " + self._try_soledad_sync() + + # at this point, sometimes the client + # gets stuck and does not progress to + # the _gen_key step. XXX investigate. + logger.debug("Soledad has been synced.") + # so long, and thanks for all the fish + return + except SoledadSyncError: + # maybe it's my connection, but I'm getting + # ssl handshake timeouts and read errors quite often. + # A particularly big sync is a disaster. + # This deserves further investigation, maybe the + # retry strategy can be pushed to u1db, or at least + # it's something worthy to talk about with the + # ubuntu folks. + sync_tries -= 1 + continue + + # reached bottom, failed to sync + # and there's nothing we can do... + self.soledad_failed.emit() + raise SoledadSyncError() + + def _try_soledad_init(self, uuid, secrets_path, local_db_path, + server_url, cert_file, auth_token): + """ + Tries to initialize soledad. + + :param uuid: user identifier + :param secrets_path: path to secrets file + :param local_db_path: path to local db file + :param server_url: soledad server uri + :param cert_file: path to the certificate of the ca used + to validate the SSL certificate used by the remote + soledad server. + :type cert_file: str + :param auth token: auth token + :type auth_token: str + """ + # TODO: If selected server fails, retry with another host + # (issue #3309) + try: + self._soledad = Soledad( + uuid, + self._password.encode("utf-8"), + secrets_path=secrets_path, + local_db_path=local_db_path, + server_url=server_url, + cert_file=cert_file, + auth_token=auth_token) + + # XXX All these errors should be handled by soledad itself, + # and return a subclass of SoledadInitializationFailed + + # recoverable, will guarantee retries + except socket.timeout: + logger.debug("SOLEDAD initialization TIMED OUT...") + self.soledad_timeout.emit() + except socket.error as exc: + logger.error("Socket error while initializing soledad") + self.soledad_timeout.emit() + + # unrecoverable + except u1db_errors.Unauthorized: + logger.error("Error while initializing soledad " + "(unauthorized).") + self.soledad_failed.emit() + except Exception as exc: + logger.exception("Unhandled error while initializating " "soledad: %r" % (exc,)) - raise - else: - raise Exception("No soledad server found") + self.soledad_failed.emit() + + def _try_soledad_sync(self): + """ + Tries to sync soledad. + Raises SoledadSyncError if not successful. + """ + try: + logger.error("trying to sync soledad....") + self._soledad.sync() + except SSLError as exc: + logger.error("%r" % (exc,)) + raise SoledadSyncError("Failed to sync soledad") + except Exception as exc: + logger.exception("Unhandled error while syncing" + "soledad: %r" % (exc,)) + self.soledad_failed.emit() + raise SoledadSyncError("Failed to sync soledad") def _download_config(self): """ @@ -179,86 +293,57 @@ class SoledadBootstrapper(AbstractBootstrapper): leap_assert(self._provider_config, "We need a provider configuration!") - logger.debug("Downloading Soledad config for %s" % (self._provider_config.get_domain(),)) self._soledad_config = SoledadConfig() - - headers = {} - mtime = get_mtime( - os.path.join(get_path_prefix(), "leap", "providers", - self._provider_config.get_domain(), - "soledad-service.json")) - - if self._download_if_needed and mtime: - headers['if-modified-since'] = mtime - - api_version = self._provider_config.get_api_version() - - # there is some confusion with this uri, - config_uri = "%s/%s/config/soledad-service.json" % ( - self._provider_config.get_api_uri(), - api_version) - logger.debug('Downloading soledad config from: %s' % config_uri) - - # TODO factor out this srpauth protected get (make decorator) - srp_auth = self.srpauth - session_id = srp_auth.get_session_id() - cookies = None - if session_id: - cookies = {"_session_id": session_id} - - res = self._session.get(config_uri, - verify=self._provider_config - .get_ca_cert_path(), - headers=headers, - cookies=cookies) - res.raise_for_status() - - self._soledad_config.set_api_version(api_version) - - # Not modified - if res.status_code == 304: - logger.debug("Soledad definition has not been modified") - self._soledad_config.load( - os.path.join( - "leap", "providers", - self._provider_config.get_domain(), - "soledad-service.json")) - else: - soledad_definition, mtime = get_content(res) - - self._soledad_config.load(data=soledad_definition, mtime=mtime) - self._soledad_config.save(["leap", - "providers", - self._provider_config.get_domain(), - "soledad-service.json"]) - + download_service_config( + self._provider_config, + self._soledad_config, + self._session, + self._download_if_needed) + + # soledad config is ok, let's proceed to load and sync soledad + # XXX but honestly, this is a pretty strange entry point for that. + # it feels like it should be the other way around: + # load_and_sync, and from there, if needed, call download_config self.load_and_sync_soledad() - def _gen_key(self, _): + def _get_gpg_bin_path(self): """ - Generates the key pair if needed, uploads it to the webapp and - nickserver + Returns the path to gpg binary. + :returns: the gpg binary path + :rtype: str """ - leap_assert(self._provider_config, - "We need a provider configuration!") - - address = "%s@%s" % (self._user, self._provider_config.get_domain()) - - logger.debug("Retrieving key for %s" % (address,)) - - srp_auth = self.srpauth - - # TODO: use which implementation with known paths # TODO: Fix for Windows - gpgbin = "/usr/bin/gpg" - + gpgbin = None if flags.STANDALONE: - gpgbin = os.path.join(get_path_prefix(), - "..", "apps", "mail", "gpg") - + gpgbin = os.path.join( + get_path_prefix(), "..", "apps", "mail", "gpg") + else: + try: + gpgbin_options = which("gpg") + # gnupg checks that the path to the binary is not a + # symlink, so we need to filter those and come up with + # just one option. + for opt in gpgbin_options: + if not os.path.islink(opt): + gpgbin = opt + break + except IndexError as e: + logger.debug("Couldn't find the gpg binary!") + logger.exception(e) + leap_check(gpgbin is not None, "Could not find gpg binary") + return gpgbin + + def _init_keymanager(self, address): + """ + Initializes the keymanager. + :param address: the address to initialize the keymanager with. + :type address: str + """ + srp_auth = self.srpauth + logger.debug('initializing keymanager...') self._keymanager = KeyManager( address, "https://nicknym.%s:6425" % (self._provider_config.get_domain(),), @@ -269,15 +354,46 @@ class SoledadBootstrapper(AbstractBootstrapper): api_uri=self._provider_config.get_api_uri(), api_version=self._provider_config.get_api_version(), uid=srp_auth.get_uid(), - gpgbinary=gpgbin) + gpgbinary=self._get_gpg_bin_path()) + + def _gen_key(self, _): + """ + Generates the key pair if needed, uploads it to the webapp and + nickserver + """ + leap_assert(self._provider_config is not None, + "We need a provider configuration!") + leap_assert(self._soledad is not None, + "We need a non-null soledad to generate keys") + + address = "%s@%s" % (self._user, self._provider_config.get_domain()) + self._init_keymanager(address) + logger.debug("Retrieving key for %s" % (address,)) + try: - self._keymanager.get_key(address, openpgp.OpenPGPKey, - private=True, fetch_remote=False) + self._keymanager.get_key( + address, openpgp.OpenPGPKey, private=True, fetch_remote=False) + return except KeyNotFound: logger.debug("Key not found. Generating key for %s" % (address,)) + + # generate key + try: self._keymanager.gen_key(openpgp.OpenPGPKey) + except Exception as exc: + logger.error("error while generating key!") + logger.exception(exc) + raise + + # send key + try: self._keymanager.send_key(openpgp.OpenPGPKey) - logger.debug("Key generated successfully.") + except Exception as exc: + logger.error("error while sending key!") + logger.exception(exc) + raise + + logger.debug("Key generated successfully.") def run_soledad_setup_checks(self, provider_config, diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index f762a350..b58e6e3b 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -62,3 +62,17 @@ def update_modification_ts(path): """ os.utime(path, None) return get_modification_ts(path) + + +def is_file(path): + """ + Returns True if the path exists and is a file. + """ + return os.path.isfile(path) + + +def is_empty_file(path): + """ + Returns True if the file at path is empty. + """ + return os.stat(path).st_size is 0 diff --git a/src/leap/bitmask/util/constants.py b/src/leap/bitmask/util/constants.py index 63f6b1f7..e6a6bdce 100644 --- a/src/leap/bitmask/util/constants.py +++ b/src/leap/bitmask/util/constants.py @@ -16,4 +16,4 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. SIGNUP_TIMEOUT = 5 -REQUEST_TIMEOUT = 10 +REQUEST_TIMEOUT = 15 |