diff options
Diffstat (limited to 'src/leap/bitmask')
25 files changed, 1125 insertions, 387 deletions
diff --git a/src/leap/bitmask/__init__.py b/src/leap/bitmask/__init__.py index e69de29b..ebdd53c4 100644 --- a/src/leap/bitmask/__init__.py +++ b/src/leap/bitmask/__init__.py @@ -0,0 +1,75 @@ +# -*- 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/>. +""" +Init file for leap.bitmask + +Initializes version and app info. +""" +import re + +from pkg_resources import parse_version + +from leap.bitmask.util import first + + +def _is_release_version(version): + """ + Helper to determine whether a version is a final release or not. + The release needs to be of the form: w.x.y.z containing only numbers + and dots. + + :param version: the version string + :type version: str + :returns: if the version is a release version or not. + :rtype: bool + """ + parsed_version = parse_version(version) + not_number = 0 + for x in parsed_version: + try: + int(x) + except: + not_number += 1 + + return not_number == 1 + + +__version__ = "unknown" +IS_RELEASE_VERSION = False + +__short_version__ = "unknown" + +try: + from leap.bitmask._version import get_versions + __version__ = get_versions()['version'] + IS_RELEASE_VERSION = _is_release_version(__version__) + del get_versions +except ImportError: + #running on a tree that has not run + #the setup.py setver + pass + +__appname__ = "unknown" +try: + from leap._appname import __appname__ +except ImportError: + #running on a tree that has not run + #the setup.py setver + pass + +__short_version__ = first(re.findall('\d\.\d\.\d', __version__)) +__full_version__ = __appname__ + '/' + str(__version__) diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 6ffa1d25..158f1afe 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -25,6 +25,7 @@ from functools import partial from PySide import QtCore, QtGui from leap.bitmask.util import leap_argparse +from leap.bitmask.util import log_silencer from leap.bitmask.util.leap_log_handler import LeapLogHandler from leap.bitmask.util.streamtologger import StreamToLogger from leap.common.events import server as event_server @@ -51,7 +52,7 @@ def install_qtreactor(logger): logger.debug("Qt4 reactor installed") -def add_logger_handlers(debug=False, logfile=None): +def add_logger_handlers(debug=False, logfile=None, standalone=False): """ Create the logger and attach the handlers. @@ -71,6 +72,7 @@ def add_logger_handlers(debug=False, logfile=None): # Create logger and formatter logger = logging.getLogger(name='leap') logger.setLevel(level) + log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' formatter = logging.Formatter(log_format) @@ -78,12 +80,16 @@ def add_logger_handlers(debug=False, logfile=None): console = logging.StreamHandler() console.setLevel(level) console.setFormatter(formatter) + + silencer = log_silencer.SelectiveSilencerFilter(standalone=standalone) + console.addFilter(silencer) logger.addHandler(console) logger.debug('Console handler plugged!') # LEAP custom handler leap_handler = LeapLogHandler() leap_handler.setLevel(level) + leap_handler.addFilter(silencer) logger.addHandler(leap_handler) logger.debug('Leap handler plugged!') @@ -93,6 +99,7 @@ def add_logger_handlers(debug=False, logfile=None): fileh = logging.FileHandler(logfile) fileh.setLevel(logging.DEBUG) fileh.setFormatter(formatter) + fileh.addFilter(silencer) logger.addHandler(fileh) logger.debug('File handler plugged!') @@ -149,13 +156,13 @@ def main(): from leap.bitmask.gui.mainwindow import MainWindow from leap.bitmask.platform_init import IS_MAC from leap.bitmask.platform_init.locks import we_are_the_one_and_only - from leap.bitmask.util import __version__ as VERSION + from leap.bitmask import __version__ as VERSION from leap.bitmask.util.requirement_checker import check_requirements # pylint: avoid unused import assert(locale_rc) - logger = add_logger_handlers(debug, logfile) + logger = add_logger_handlers(debug, logfile, standalone) replace_stdout_stderr_with_logging(logger) if not we_are_the_one_and_only(): diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py index 35010280..7d8b5977 100644 --- a/src/leap/bitmask/config/leapsettings.py +++ b/src/leap/bitmask/config/leapsettings.py @@ -24,7 +24,7 @@ import logging from PySide import QtCore from leap.common.check import leap_assert, leap_assert_type -from leap.common.config.prefixers import get_platform_prefixer +from leap.common.config import get_path_prefix logger = logging.getLogger(__name__) @@ -66,20 +66,22 @@ class LeapSettings(object): REMEMBER_KEY = "RememberUserAndPass" DEFAULTPROVIDER_KEY = "DefaultProvider" ALERTMISSING_KEY = "AlertMissingScripts" + GATEWAY_KEY = "Gateway" + + # values + GATEWAY_AUTOMATIC = "Automatic" def __init__(self, standalone=False): """ Constructor - :param standalone: parameter used to define the location of - the config + :param standalone: parameter used to define the location of the config. :type standalone: bool """ + self._path_prefix = get_path_prefix(standalone=standalone) + settings_path = os.path.join(self._path_prefix, + "leap", self.CONFIG_NAME) - settings_path = os.path.join(get_platform_prefixer() - .get_path_prefix(standalone=standalone), - "leap", - self.CONFIG_NAME) self._settings = QtCore.QSettings(settings_path, QtCore.QSettings.IniFormat) @@ -119,6 +121,58 @@ class LeapSettings(object): leap_assert(windowstate, "We need a window state") self._settings.setValue(self.WINDOWSTATE_KEY, windowstate) + def get_configured_providers(self): + """ + Returns the configured providers based on the file structure in the + settings directory. + + :rtype: list of str + """ + # TODO: check which providers have a valid certificate among + # other things, not just the directories + providers = [] + try: + providers_path = os.path.join(self._path_prefix, + "leap", "providers") + providers = os.listdir(providers_path) + except Exception as e: + logger.debug("Error listing providers, assume there are none. %r" + % (e,)) + + return providers + + def get_selected_gateway(self, provider): + """ + Returns the configured gateway for the given provider. + + :param provider: provider domain + :type provider: str + + :rtype: str + """ + leap_assert(len(provider) > 0, "We need a nonempty provider") + gateway_key = "{0}/{1}".format(provider, self.GATEWAY_KEY) + gateway = self._settings.value(gateway_key, self.GATEWAY_AUTOMATIC) + + return gateway + + def set_selected_gateway(self, provider, gateway): + """ + Saves the configured gateway for the given provider + + :param provider: provider domain + :type provider: str + + :param gateway: gateway to use as default + :type gateway: str + """ + + leap_assert(len(provider) > 0, "We need a nonempty provider") + leap_assert_type(gateway, (str, unicode)) + + gateway_key = "{0}/{1}".format(provider, self.GATEWAY_KEY) + self._settings.setValue(gateway_key, gateway) + def get_enabled_services(self, provider): """ Returns a list of enabled services for the given provider @@ -151,8 +205,12 @@ class LeapSettings(object): leap_assert(len(provider) > 0, "We need a nonempty provider") leap_assert_type(services, list) - self._settings.setValue("%s/Services" % (provider,), - services) + key = "{0}/Services".format(provider) + if not services: + # if there are no enabled services we don't need that key + self._settings.remove(key) + else: + self._settings.setValue(key, services) def get_user(self): """ diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py index c65932be..a7808399 100644 --- a/src/leap/bitmask/config/providerconfig.py +++ b/src/leap/bitmask/config/providerconfig.py @@ -24,6 +24,7 @@ import os from leap.bitmask.config.provider_spec import leap_provider_spec from leap.common.check import leap_check from leap.common.config.baseconfig import BaseConfig, LocalizedKey +from leap.bitmask.services import get_service_display_name logger = logging.getLogger(__name__) @@ -130,9 +131,11 @@ class ProviderConfig(BaseConfig): Returns a string with the available services in the current provider, ready to be shown to the user. """ - services_str = ", ".join(self.get_services()) - services_str = services_str.replace( - "openvpn", "Encrypted Internet") + services = [] + for service in self.get_services(): + services.append(get_service_display_name(service)) + + services_str = ", ".join(services) return services_str def get_ca_cert_path(self, about_to_download=False): @@ -216,3 +219,4 @@ if __name__ == "__main__": print provider.get_languages() print provider.get_name() print provider.get_services() + print provider.get_services_string() diff --git a/src/leap/bitmask/config/tests/test_leapsettings.py b/src/leap/bitmask/config/tests/test_leapsettings.py new file mode 100644 index 00000000..18166923 --- /dev/null +++ b/src/leap/bitmask/config/tests/test_leapsettings.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# test_leapsettings.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/>. + +""" +Tests for leapsettings module. +""" + +try: + import unittest2 as unittest +except ImportError: + import unittest + +import os +import mock + +from leap.common.testing.basetest import BaseLeapTest +from leap.bitmask.config.leapsettings import LeapSettings + + +class LeapSettingsTest(BaseLeapTest): + """Tests for LeapSettings""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_configured_providers(self): + """ + Test that the config file IS NOT stored under the CWD. + """ + 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() + args, kwargs = os_listdir.call_args + config_dir = args[0] + self.assertFalse(config_dir.startswith(os.getcwd())) + self.assertFalse(config_dir.endswith('config')) + + def test_get_configured_providers_in_bundle(self): + """ + Test that the config file IS stored under the CWD. + """ + self._leapsettings = LeapSettings(standalone=True) + with mock.patch('os.listdir') as os_listdir: + # use this method only to spy where LeapSettings is looking for + self._leapsettings.get_configured_providers() + args, kwargs = os_listdir.call_args + config_dir = args[0] + self.assertTrue(config_dir.startswith(os.getcwd())) + self.assertFalse(config_dir.endswith('config')) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index 9b4ba55d..ece4cad6 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -146,7 +146,8 @@ class LoggerWindow(QtGui.QDialog): Lets the user save the current log to a file """ fileName, filtr = QtGui.QFileDialog.getSaveFileName( - self, self.tr("Save As")) + self, self.tr("Save As"), + options=QtGui.QFileDialog.DontUseNativeDialog) if fileName: try: diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index c832887a..0950462b 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -14,9 +14,8 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - """ -Main window for the leap client +Main window for Bitmask. """ import logging import os @@ -59,7 +58,7 @@ from leap.bitmask.services.eip.vpnlaunchers import \ EIPNoPolkitAuthAgentAvailable from leap.bitmask.services.eip.vpnlaunchers import EIPNoTunKextLoaded -from leap.bitmask.util import __version__ as VERSION +from leap.bitmask import __version__ as VERSION from leap.bitmask.util.keyring_helpers import has_keyring from leap.bitmask.util.leap_log_handler import LeapLogHandler @@ -102,6 +101,7 @@ class MainWindow(QtGui.QMainWindow): raise_window = QtCore.Signal([]) soledad_ready = QtCore.Signal([]) mail_client_logged_in = QtCore.Signal([]) + logout = QtCore.Signal([]) # We use this flag to detect abnormal terminations user_stopped_eip = False @@ -254,14 +254,20 @@ class MainWindow(QtGui.QMainWindow): self._action_eip_provider = QtGui.QAction( self.tr("No default provider"), self) self._action_eip_provider.setEnabled(False) + self._action_eip_status = QtGui.QAction( self.tr("Encrypted internet is OFF"), self) self._action_eip_status.setEnabled(False) - self._status_panel.set_action_eip_status( self._action_eip_status) + self._action_mail_status = QtGui.QAction( + self.tr("Encrypted Mail is OFF"), self) + self._action_mail_status.setEnabled(False) + self._status_panel.set_action_mail_status( + self._action_mail_status) + self._action_eip_startstop = QtGui.QAction( self.tr("Turn OFF"), self) self._action_eip_startstop.triggered.connect( @@ -270,6 +276,9 @@ class MainWindow(QtGui.QMainWindow): 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._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self) self._action_visible.triggered.connect(self._toggle_visible) @@ -284,26 +293,26 @@ class MainWindow(QtGui.QMainWindow): # Services signals/slots connection self.new_updates.connect(self._react_to_new_updates) self.soledad_ready.connect(self._start_imap_service) + self.soledad_ready.connect(self._set_soledad_ready) self.mail_client_logged_in.connect(self._fetch_incoming_mail) + self.logout.connect(self._stop_imap_service) + self.logout.connect(self._stop_smtp_service) ################################# end Qt Signals connection ######## - # Enable the password change when soledad is ready - self.soledad_ready.connect( - partial(self.ui.btnPreferences.setEnabled, True)) - init_platform() self._wizard = None self._wizard_firstrun = False self._logger_window = None - self._preferences_window = None self._bypass_checks = bypass_checks self._soledad = None + self._soledad_ready = False self._keymanager = None + self._smtp_service = None self._imap_service = None self._login_defer = None @@ -419,7 +428,26 @@ class MainWindow(QtGui.QMainWindow): Displays the preferences window. """ - PreferencesWindow(self, self._srp_auth, self._soledad).show() + preferences_window = PreferencesWindow( + self, self._srp_auth, self._settings, self._standalone) + + if self._soledad_ready: + preferences_window.set_soledad_ready(self._soledad) + else: + self.soledad_ready.connect( + lambda: preferences_window.set_soledad_ready(self._soledad)) + + preferences_window.show() + + def _set_soledad_ready(self): + """ + SLOT + TRIGGERS: + self.soledad_ready + + It sets the soledad object as ready to use. + """ + self._soledad_ready = True def _uncheck_logger_button(self): """ @@ -491,7 +519,8 @@ class MainWindow(QtGui.QMainWindow): """ # XXX: May be this can be divided into two methods? - self._login_widget.set_providers(self._configured_providers()) + providers = self._settings.get_configured_providers() + self._login_widget.set_providers(providers) self._show_systray() self.show() if IS_MAC: @@ -589,10 +618,8 @@ class MainWindow(QtGui.QMainWindow): self._systray.setVisible(True) return - # Placeholder actions - # They are temporary to display the tray as designed - preferences_action = QtGui.QAction(self.tr("Preferences"), self) - preferences_action.setEnabled(False) + # Placeholder action + # It is temporary to display the tray as designed help_action = QtGui.QAction(self.tr("Help"), self) help_action.setEnabled(False) @@ -602,8 +629,9 @@ class MainWindow(QtGui.QMainWindow): systrayMenu.addAction(self._action_eip_provider) systrayMenu.addAction(self._action_eip_status) systrayMenu.addAction(self._action_eip_startstop) + systrayMenu.addAction(self._action_mail_status) systrayMenu.addSeparator() - systrayMenu.addAction(preferences_action) + systrayMenu.addAction(self._action_preferences) systrayMenu.addAction(help_action) systrayMenu.addSeparator() systrayMenu.addAction(self.ui.action_log_out) @@ -736,34 +764,14 @@ class MainWindow(QtGui.QMainWindow): QtGui.QMainWindow.closeEvent(self, e) - def _configured_providers(self): - """ - Returns the available providers based on the file structure - - :rtype: list - """ - - # TODO: check which providers have a valid certificate among - # other things, not just the directories - providers = [] - try: - providers = os.listdir( - os.path.join(self._provider_config.get_path_prefix(), - "leap", - "providers")) - except Exception as e: - logger.debug("Error listing providers, assume there are none. %r" - % (e,)) - - return providers - def _first_run(self): """ Returns True if there are no configured providers. False otherwise :rtype: bool """ - has_provider_on_disk = len(self._configured_providers()) != 0 + providers = self._settings.get_configured_providers() + has_provider_on_disk = len(providers) != 0 is_proper_provider = self._settings.get_properprovider() return not (has_provider_on_disk and is_proper_provider) @@ -891,6 +899,8 @@ class MainWindow(QtGui.QMainWindow): logger.debug("Cancelling login defer.") self._login_defer.cancel() + self._login_widget.set_status(self.tr("Log in cancelled by the user.")) + def _provider_config_loaded(self, data): """ SLOT @@ -912,7 +922,7 @@ class MainWindow(QtGui.QMainWindow): self._srp_auth.logout_finished.connect( self._done_logging_out) - # TODO: Add errback! + # TODO Add errback! self._login_defer = self._srp_auth.authenticate(username, password) else: self._login_widget.set_status( @@ -979,7 +989,7 @@ class MainWindow(QtGui.QMainWindow): """ passed = data[self._soledad_bootstrapper.PASSED_KEY] if not passed: - # TODO: display in the GUI: + # TODO display in the GUI: # should pass signal to a slot in status_panel # that sets the global status logger.error("Soledad failed to start: %s" % @@ -1040,13 +1050,13 @@ class MainWindow(QtGui.QMainWindow): True) else: if self._enabled_services.count(self.MX_SERVICE) > 0: - pass # TODO: show MX status + pass # TODO show MX status #self._status_panel.set_eip_status( # self.tr("%s does not support MX") % # (self._provider_config.get_domain(),), # error=True) else: - pass # TODO: show MX status + pass # TODO show MX status #self._status_panel.set_eip_status( # self.tr("MX is disabled")) @@ -1073,25 +1083,43 @@ class MainWindow(QtGui.QMainWindow): logger.debug("Done bootstrapping SMTP") hosts = self._smtp_config.get_hosts() - # TODO: handle more than one host and define how to choose + # TODO handle more than one host and define how to choose if len(hosts) > 0: hostname = hosts.keys()[0] logger.debug("Using hostname %s for SMTP" % (hostname,)) host = hosts[hostname][self.IP_KEY].encode("utf-8") port = hosts[hostname][self.PORT_KEY] - # TODO: pick local smtp port in a better way - # TODO: Make the encrypted_only configurable + # TODO move the start to _start_smtp_service + + # TODO Make the encrypted_only configurable + # TODO pick local smtp port in a better way + # TODO remove hard-coded port and let leap.mail set + # the specific default. from leap.mail.smtp import setup_smtp_relay client_cert = self._eip_config.get_client_cert_path( self._provider_config) - setup_smtp_relay(port=2013, - keymanager=self._keymanager, - smtp_host=host, - smtp_port=port, - smtp_cert=client_cert, - smtp_key=client_cert, - encrypted_only=False) + self._smtp_service = setup_smtp_relay( + port=2013, + keymanager=self._keymanager, + smtp_host=host, + smtp_port=port, + smtp_cert=client_cert, + smtp_key=client_cert, + encrypted_only=False) + + def _stop_smtp_service(self): + """ + SLOT + TRIGGERS: + self.logout + """ + # There is a subtle difference here: + # we are stopping the factory for the smtp service here, + # 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_service.doStop() ################################################################### # Service control methods: imap @@ -1100,7 +1128,7 @@ class MainWindow(QtGui.QMainWindow): """ SLOT TRIGGERS: - soledad_ready + self.soledad_ready """ if self._provider_config.provides_mx() and \ self._enabled_services.count(self.MX_SERVICE) > 0: @@ -1109,17 +1137,6 @@ class MainWindow(QtGui.QMainWindow): self._imap_service = imap.start_imap_service( self._soledad, self._keymanager) - else: - if self._enabled_services.count(self.MX_SERVICE) > 0: - pass # TODO: show MX status - #self._status_panel.set_eip_status( - # self.tr("%s does not support MX") % - # (self._provider_config.get_domain(),), - # error=True) - else: - pass # TODO: show MX status - #self._status_panel.set_eip_status( - # self.tr("MX is disabled")) def _on_mail_client_logged_in(self, req): """ @@ -1131,13 +1148,27 @@ class MainWindow(QtGui.QMainWindow): """ SLOT TRIGGERS: - mail_client_logged_in + self.mail_client_logged_in """ # TODO have a mutex over fetch operation. if self._imap_service: logger.debug('Client connected, fetching mail...') self._imap_service.fetch() + def _stop_imap_service(self): + """ + SLOT + TRIGGERS: + self.logout + """ + # There is a subtle difference here: + # we are just stopping the fetcher here, + # but in the smtp case we are stopping the factory. + # We should homogenize both services. + if self._imap_service is not None: + logger.debug('Stopping imap service.') + self._imap_service.stop() + # end service control methods (imap) ################################################################### @@ -1149,7 +1180,8 @@ class MainWindow(QtGui.QMainWindow): :rtype: tuple (str, str) (host, port) """ - # TODO: make this properly multiplatform + # TODO make this properly multiplatform + # TODO get this out of gui/ if platform.system() == "Windows": host = "localhost" @@ -1402,6 +1434,7 @@ class MainWindow(QtGui.QMainWindow): # XXX: If other defers are doing authenticated stuff, this # might conflict with those. CHECK! threads.deferToThread(self._srp_auth.logout) + self.logout.emit() def _done_logging_out(self, ok, message): """ @@ -1576,7 +1609,7 @@ class MainWindow(QtGui.QMainWindow): """ Cleanup and tidely close the main window before quitting. """ - # TODO: separate the shutting down of services from the + # TODO separate the shutting down of services from the # UI stuff. self._cleanup_and_quit() diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index a8220e86..1becfb18 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -18,15 +18,20 @@ """ Preferences window """ +import os import logging from functools import partial -from PySide import QtGui +from PySide import QtCore, QtGui 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.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__) @@ -38,27 +43,62 @@ class PreferencesWindow(QtGui.QDialog): WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", "password") - def __init__(self, parent, srp_auth, soledad): + def __init__(self, parent, srp_auth, leap_settings, standalone): """ :param parent: parent object of the PreferencesWindow. :parent type: QWidget :param srp_auth: SRPAuth object configured in the main app. :type srp_auth: SRPAuth - :param soledad: Soledad object configured in the main app. - :type soledad: Soledad + :param standalone: If True, the application is running as standalone + and the preferences dialog should display some + messages according to this. + :type standalone: bool """ QtGui.QDialog.__init__(self, parent) + self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") self._srp_auth = srp_auth - self._soledad = soledad + self._settings = leap_settings + self._standalone = standalone + self._soledad = None # Load UI self.ui = Ui_Preferences() self.ui.setupUi(self) self.ui.lblPasswordChangeStatus.setVisible(False) + self.ui.lblProvidersServicesStatus.setVisible(False) + self.ui.lblProvidersGatewayStatus.setVisible(False) + + self._selected_services = set() # Connections 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) + else: + self._add_configured_providers() + + def set_soledad_ready(self, soledad): + """ + SLOT + TRIGGERS: + parent.soledad_ready + + It sets the soledad object as ready to use. + + :param soledad: Soledad object configured in the main app. + :type soledad: Soledad + """ + self._soledad = soledad + self.ui.gbPasswordChange.setEnabled(True) def _set_password_change_status(self, status, error=False, success=False): """ @@ -86,7 +126,7 @@ class PreferencesWindow(QtGui.QDialog): :type disable: bool """ if disable: - self._set_password_change_disable(self.tr("Changing password...")) + self._set_password_change_status(self.tr("Changing password...")) self.ui.leCurrentPassword.setEnabled(not disable) self.ui.leNewPassword.setEnabled(not disable) @@ -95,6 +135,10 @@ class PreferencesWindow(QtGui.QDialog): def _change_password(self): """ + SLOT + TRIGGERS: + self.ui.pbChangePassword.clicked + Changes the user's password if the inputboxes are correctly filled. """ username = self._srp_auth.get_username() @@ -134,7 +178,7 @@ class PreferencesWindow(QtGui.QDialog): self._set_password_change_status( self.tr("Password changed successfully."), success=True) - self._clear_inputs() + self._clear_password_inputs() self._set_changing_password(False) def _change_password_problem(self, failure): @@ -156,10 +200,264 @@ class PreferencesWindow(QtGui.QDialog): self._set_changing_password(False) failure.trap(Exception) - def _clear_inputs(self): + def _clear_password_inputs(self): """ Clear the contents of the inputs. """ self.ui.leCurrentPassword.setText("") self.ui.leNewPassword.setText("") self.ui.leNewPassword2.setText("") + + def _set_providers_services_status(self, status, success=False): + """ + Sets the status label for the password 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 + """ + if success: + status = "<font color='green'><b>%s</b></font>" % (status,) + + 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): + """ + SLOT + TRIGGER: service_checkbox.stateChanged + Adds the service to the state if the state is checked, removes + it otherwise + + :param service: service to handle + :type service: str + :param state: state of the checkbox + :type state: int + """ + if state == QtCore.Qt.Checked: + self._selected_services = \ + self._selected_services.union(set([service])) + else: + self._selected_services = \ + self._selected_services.difference(set([service])) + + # We hide the maybe-visible status label after a change + self.ui.lblProvidersServicesStatus.setVisible(False) + + def _populate_services(self, domain): + """ + SLOT + TRIGGERS: + self.ui.cbProvidersServices.currentIndexChanged[unicode] + + Loads the services that the provider provides into the UI for + the user to enable or disable. + + :param domain: the domain of the provider to load services from. + :type domain: str + """ + # We hide the maybe-visible status label after a change + self.ui.lblProvidersServicesStatus.setVisible(False) + + if not domain: + return + + provider_config = self._get_provider_config(domain) + if provider_config is None: + return + + # set the proper connection for the 'save' button + try: + self.ui.pbSaveServices.clicked.disconnect() + except RuntimeError: + pass # Signal was not connected + + save_services = partial(self._save_enabled_services, domain) + self.ui.pbSaveServices.clicked.connect(save_services) + + services = get_supported(provider_config.get_services()) + services_conf = self._settings.get_enabled_services(domain) + + # discard changes if other provider is selected + self._selected_services = set() + + # from: http://stackoverflow.com/a/13103617/687989 + # remove existing checkboxes + layout = self.ui.vlServices + for i in reversed(range(layout.count())): + layout.itemAt(i).widget().setParent(None) + + # add one checkbox per service and set the current configured value + for service in services: + try: + checkbox = QtGui.QCheckBox(self) + service_label = get_service_display_name( + service, self._standalone) + checkbox.setText(service_label) + + self.ui.vlServices.addWidget(checkbox) + checkbox.stateChanged.connect( + partial(self._service_selection_changed, service)) + + checkbox.setChecked(service in services_conf) + except ValueError: + logger.error("Something went wrong while trying to " + "load service %s" % (service,)) + + def _save_enabled_services(self, provider): + """ + SLOT + TRIGGERS: + self.ui.pbSaveServices.clicked + + Saves the new enabled services settings to the configuration file. + + :param provider: the provider config that we need to save. + :type provider: str + """ + services = list(self._selected_services) + self._settings.set_enabled_services(provider, services) + + msg = self.tr( + "Services settings for provider '{0}' saved.".format(provider)) + logger.debug(msg) + self._set_providers_services_status(msg, success=True) + + 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 _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/statuspanel.py b/src/leap/bitmask/gui/statuspanel.py index 9352eb04..3a91f08e 100644 --- a/src/leap/bitmask/gui/statuspanel.py +++ b/src/leap/bitmask/gui/statuspanel.py @@ -356,6 +356,16 @@ class StatusPanelWidget(QtGui.QWidget): leap_assert_type(action_eip_status, QtGui.QAction) self._action_eip_status = action_eip_status + 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. @@ -538,6 +548,27 @@ class StatusPanelWidget(QtGui.QWidget): def set_provider(self, provider): self.ui.lblProvider.setText(provider) + def _set_mail_status(self, status, ready=False): + """ + Sets the Encrypted 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) + + tray_status = self.tr('Encrypted Mail is OFF') + + icon = QtGui.QPixmap(self.MAIL_OFF_ICON) + if ready: + icon = QtGui.QPixmap(self.MAIL_ON_ICON) + tray_status = self.tr('Encrypted Mail is ON') + + self.ui.lblMailIcon.setPixmap(icon) + self._action_mail_status.setText(tray_status) + def _mail_handle_soledad_events(self, req): """ Callback for ... @@ -557,7 +588,7 @@ class StatusPanelWidget(QtGui.QWidget): :param req: Request type :type req: leap.common.events.events_pb2.SignalRequest """ - self.ui.lblMailStatus.setText(self.tr("Starting...")) + self._set_mail_status(self.tr("Starting...")) ext_status = "" @@ -591,7 +622,12 @@ class StatusPanelWidget(QtGui.QWidget): :param req: Request type :type req: leap.common.events.events_pb2.SignalRequest """ - self.ui.lblMailStatus.setText(self.tr("Starting...")) + # 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 = "" @@ -639,14 +675,11 @@ class StatusPanelWidget(QtGui.QWidget): ext_status = self.tr("SMTP has started...") self._smtp_started = True if self._smtp_started and self._imap_started: - self.ui.lblMailStatus.setText(self.tr("ON")) - self.ui.lblMailIcon.setPixmap(QtGui.QPixmap(self.MAIL_ON_ICON)) - self.ui.lblMailIcon.setPixmap( - QtGui.QPixmap(":/images/mail-locked.png")) + 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.ui.lblMailStatus.setText(self.tr("Failed")) + self._set_mail_status(self.tr("Failed")) else: leap_assert(False, "Don't know how to handle this state: %s" @@ -679,19 +712,17 @@ class StatusPanelWidget(QtGui.QWidget): ext_status = self.tr("IMAP has started...") self._imap_started = True if self._smtp_started and self._imap_started: - self.ui.lblMailStatus.setText(self.tr("ON")) - self.ui.lblMailIcon.setPixmap(QtGui.QPixmap(self.MAIL_ON_ICON)) + 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.ui.lblMailStatus.setText(self.tr("Failed")) + 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.ui.lblMailStatus.setText(self.tr("ON")) - self.ui.lblMailIcon.setPixmap(QtGui.QPixmap(self.MAIL_ON_ICON)) + self._set_mail_status(self.tr("ON"), ready=True) else: leap_assert(False, "Don't know how to handle this state: %s" diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index 834a562e..17837642 100644 --- a/src/leap/bitmask/gui/ui/mainwindow.ui +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -217,7 +217,7 @@ <item> <widget class="QPushButton" name="btnPreferences"> <property name="enabled"> - <bool>false</bool> + <bool>true</bool> </property> <property name="text"> <string>Preferences</string> diff --git a/src/leap/bitmask/gui/ui/preferences.ui b/src/leap/bitmask/gui/ui/preferences.ui index 8c63ccad..e66a2d68 100644 --- a/src/leap/bitmask/gui/ui/preferences.ui +++ b/src/leap/bitmask/gui/ui/preferences.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>451</width> - <height>267</height> + <width>503</width> + <height>529</height> </rect> </property> <property name="windowTitle"> @@ -17,9 +17,149 @@ <iconset resource="../../../../../data/resources/mainwindow.qrc"> <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset> </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="5" column="0"> + <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="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="1" colspan="2"> + <widget class="QComboBox" name="cbProvidersGateway"> + <item> + <property name="text"> + <string><Select provider></string> + </property> + </item> + </widget> + </item> + <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="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> + <item row="5" column="2"> + <widget class="QPushButton" name="pbSaveGateway"> + <property name="text"> + <string>Save this provider settings</string> + </property> + </widget> + </item> + <item row="2" 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> + </layout> + </widget> + </item> + <item row="4" column="0"> + <widget class="QGroupBox" name="gbEnabledServices"> + <property name="title"> + <string>Enabled services</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="3" column="1"> + <widget class="QPushButton" name="pbSaveServices"> + <property name="text"> + <string>Save this provider settings</string> + </property> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QGroupBox" name="gbServices"> + <property name="title"> + <string>Services</string> + </property> + <property name="flat"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="0" column="0"> + <layout class="QVBoxLayout" name="vlServices"/> + </item> + </layout> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="cbProvidersServices"> + <item> + <property name="text"> + <string><Select provider></string> + </property> + </item> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Select provider:</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QLabel" name="lblProvidersServicesStatus"> + <property name="text"> + <string>< Providers Services Status ></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </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> @@ -98,69 +238,6 @@ </layout> </widget> </item> - <item> - <widget class="QGroupBox" name="gbGatewaySelector"> - <property name="enabled"> - <bool>false</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>cbProviders</cstring> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QComboBox" name="cbProviders"> - <item> - <property name="text"> - <string><Select provider></string> - </property> - </item> - </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"> - <widget class="QComboBox" name="comboBox"> - <item> - <property name="text"> - <string>Automatic</string> - </property> - </item> - </widget> - </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> diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui index 3b8f1215..2a412784 100644 --- a/src/leap/bitmask/gui/ui/wizard.ui +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -183,7 +183,7 @@ <item row="2" column="0"> <widget class="QLabel" name="label_4"> <property name="text"> - <string>Can we stablish a secure connection?</string> + <string>Can we establish a secure connection?</string> </property> </widget> </item> @@ -740,97 +740,6 @@ </item> </layout> </widget> - <widget class="QWizardPage" name="finish_page"> - <property name="title"> - <string>Congratulations!</string> - </property> - <property name="subTitle"> - <string>You have successfully configured Bitmask.</string> - </property> - <attribute name="pageId"> - <string notr="true">6</string> - </attribute> - <layout class="QGridLayout" name="gridLayout_10"> - <item row="1" column="0"> - <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="1"> - <spacer name="verticalSpacer_9"> - <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="1" column="1"> - <widget class="QLabel" name="label_23"> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/mainwindow.qrc">:/images/mask-icon.png</pixmap> - </property> - </widget> - </item> - <item row="1" column="2"> - <widget class="QLabel" name="label_25"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="../../../../../data/resources/mainwindow.qrc">:/images/Globe.png</pixmap> - </property> - </widget> - </item> - <item row="3" column="1"> - <spacer name="verticalSpacer_10"> - <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="1" column="3"> - <spacer name="horizontalSpacer_5"> - <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> <customwidgets> <customwidget> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index e3f0085b..e004e6cf 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -34,7 +34,7 @@ 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_supported +from leap.bitmask.services import get_service_display_name, get_supported from ui_wizard import Ui_Wizard @@ -52,7 +52,6 @@ class Wizard(QtGui.QWizard): SETUP_PROVIDER_PAGE = 3 REGISTER_USER_PAGE = 4 SERVICES_PAGE = 5 - FINISH_PAGE = 6 WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", "password") @@ -84,23 +83,6 @@ class Wizard(QtGui.QWizard): self.ERROR_ICON = QtGui.QPixmap(":/images/Dialog-error.png") self.OK_ICON = QtGui.QPixmap(":/images/Dialog-accept.png") - # Correspondence for services and their name to display - EIP_LABEL = self.tr("Encrypted Internet") - MX_LABEL = self.tr("Encrypted Mail") - - if self._is_need_eip_password_warning(): - EIP_LABEL += " " + self.tr( - "(will need admin password to start)") - - self.SERVICE_DISPLAY = [ - EIP_LABEL, - MX_LABEL - ] - self.SERVICE_CONFIG = [ - "openvpn", - "mx" - ] - self._selected_services = set() self._shown_services = set() @@ -161,7 +143,7 @@ class Wizard(QtGui.QWizard): self.page(self.REGISTER_USER_PAGE).setButtonText( QtGui.QWizard.CommitButton, self.tr("&Next >")) - self.page(self.FINISH_PAGE).setButtonText( + self.page(self.SERVICES_PAGE).setButtonText( QtGui.QWizard.FinishButton, self.tr("Connect")) # XXX: Temporary removal for enrollment policy @@ -507,8 +489,10 @@ class Wizard(QtGui.QWizard): try: if service not in self._shown_services: checkbox = QtGui.QCheckBox(self) - service_index = self.SERVICE_CONFIG.index(service) - checkbox.setText(self.SERVICE_DISPLAY[service_index]) + service_label = get_service_display_name( + service, self.standalone) + checkbox.setText(service_label) + self.ui.serviceListLayout.addWidget(checkbox) checkbox.stateChanged.connect( partial(self._service_selection_changed, service)) diff --git a/src/leap/bitmask/services/__init__.py b/src/leap/bitmask/services/__init__.py index 253359cd..339f9cc6 100644 --- a/src/leap/bitmask/services/__init__.py +++ b/src/leap/bitmask/services/__init__.py @@ -17,9 +17,48 @@ """ Services module. """ +from PySide import QtCore +from leap.bitmask.util.privilege_policies import is_missing_policy_permissions + DEPLOYED = ["openvpn", "mx"] +def get_service_display_name(service, standalone=False): + """ + Returns the name to display of the given service. + If there is no configured name for that service, then returns the same + parameter + + :param service: the 'machine' service name + :type service: str + :param standalone: True if the app is running in a standalone mode, used + to display messages according that. + :type standalone: bool + + :rtype: str + """ + # qt translator method helper + _tr = QtCore.QObject().tr + + # Correspondence for services and their name to display + EIP_LABEL = _tr("Encrypted Internet") + MX_LABEL = _tr("Encrypted Mail") + + service_display = { + "openvpn": EIP_LABEL, + "mx": MX_LABEL + } + + # If we need to add a warning about eip needing + # administrative permissions to start. That can be either + # because we are running in standalone mode, or because we could + # not find the needed privilege escalation mechanisms being operative. + if standalone or is_missing_policy_permissions(): + EIP_LABEL += " " + _tr("(will need admin password to start)") + + return service_display.get(service, service) + + def get_supported(services): """ Returns a list of the available services. diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py index 843e7397..1cb7419e 100644 --- a/src/leap/bitmask/services/eip/eipconfig.py +++ b/src/leap/bitmask/services/eip/eipconfig.py @@ -62,11 +62,12 @@ class VPNGatewaySelector(object): self._eipconfig = eipconfig - def get_gateways(self): + def get_gateways_list(self): """ - Returns the 4 best gateways, sorted by timezone proximity. + Returns the existing gateways, sorted by timezone proximity. - :rtype: list of IPv4Address or IPv6Address object. + :rtype: list of tuples (location, ip) + (str, IPv4Address or IPv6Address object) """ gateways_timezones = [] locations = self._eipconfig.get_locations() @@ -77,19 +78,35 @@ class VPNGatewaySelector(object): gateway_distance = 99 # if hasn't location -> should go last if gateway_location is not None: - gw_offset = int(locations[gateway['location']]['timezone']) + timezone = locations[gateway['location']]['timezone'] + gateway_name = locations[gateway['location']].get('name', None) + if gateway_name is not None: + gateway_location = gateway_name + + gw_offset = int(timezone) if gw_offset in self.equivalent_timezones: gw_offset = self.equivalent_timezones[gw_offset] gateway_distance = self._get_timezone_distance(gw_offset) ip = self._eipconfig.get_gateway_ip(idx) - gateways_timezones.append((ip, gateway_distance)) + gateways_timezones.append((ip, gateway_distance, gateway_location)) - gateways_timezones = sorted(gateways_timezones, - key=lambda gw: gw[1])[:4] + gateways_timezones = sorted(gateways_timezones, key=lambda gw: gw[1]) + + gateways = [] + for ip, distance, location in gateways_timezones: + gateways.append((location, ip)) + + return gateways - gateways = [ip for ip, dist in gateways_timezones] + def get_gateways(self): + """ + Returns the 4 best gateways, sorted by timezone proximity. + + :rtype: list of IPv4Address or IPv6Address object. + """ + gateways = [ip for location, ip in self.get_gateways_list()][:4] return gateways def _get_timezone_distance(self, offset): @@ -124,7 +141,7 @@ class VPNGatewaySelector(object): if time.daylight: local_offset = time.altzone - return local_offset / 3600 + return -local_offset / 3600 class EIPConfig(BaseConfig): @@ -246,7 +263,8 @@ if __name__ == "__main__": console.setFormatter(formatter) logger.addHandler(console) - eipconfig = EIPConfig('1') + eipconfig = EIPConfig() + eipconfig.set_api_version('1') try: eipconfig.get_clusters() @@ -255,9 +273,14 @@ if __name__ == "__main__": print "Safe value getting is working" if eipconfig.load("leap/providers/bitmask.net/eip-service.json"): + print "EIPConfig methods" print eipconfig.get_clusters() print eipconfig.get_gateways() print eipconfig.get_locations() print eipconfig.get_openvpn_configuration() print eipconfig.get_serial() print eipconfig.get_version() + print "VPNGatewaySelector methods" + gws = VPNGatewaySelector(eipconfig) + print gws.get_gateways() + print gws.get_gateways_list() diff --git a/src/leap/bitmask/services/eip/vpnlaunchers.py b/src/leap/bitmask/services/eip/vpnlaunchers.py index f8c51ad8..a50da8b9 100644 --- a/src/leap/bitmask/services/eip/vpnlaunchers.py +++ b/src/leap/bitmask/services/eip/vpnlaunchers.py @@ -23,8 +23,8 @@ import logging import getpass import os import platform -import subprocess import stat +import subprocess try: import grp except ImportError: @@ -32,6 +32,9 @@ except ImportError: from abc import ABCMeta, abstractmethod from functools import partial +from time import sleep + +from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector @@ -217,19 +220,23 @@ def _is_auth_agent_running(): return any(is_running) -def _try_to_launch_agent(): +def _try_to_launch_agent(standalone=False): """ Tries to launch a polkit daemon. """ - opts = [ - "/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1", - # XXX add kde thing here - ] - for cmd in opts: - try: - subprocess.Popen([cmd], shell=True) - except: - pass + env = None + if 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): @@ -313,7 +320,8 @@ class LinuxVPNLauncher(VPNLauncher): """ if _is_pkexec_in_system(): if not _is_auth_agent_running(): - _try_to_launch_agent() + _try_to_launch_agent(ProviderConfig.standalone) + sleep(0.5) if _is_auth_agent_running(): pkexec_possibilities = which(kls.PKEXEC_BIN) leap_assert(len(pkexec_possibilities) > 0, @@ -414,14 +422,22 @@ class LinuxVPNLauncher(VPNLauncher): if openvpn_verb is not None: args += ['--verb', '%d' % (openvpn_verb,)] - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() + gateways = [] + leap_settings = LeapSettings(ProviderConfig.standalone) + 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: {}".format(', '.join(gateways))) + logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) for gw in gateways: args += ['--remote', gw, '1194', 'udp'] @@ -669,11 +685,22 @@ class DarwinVPNLauncher(VPNLauncher): if openvpn_verb is not None: args += ['--verb', '%d' % (openvpn_verb,)] - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() + gateways = [] + leap_settings = LeapSettings(ProviderConfig.standalone) + 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: {gw}".format( - gw=', '.join(gateways))) + logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) for gw in gateways: args += ['--remote', gw, '1194', 'udp'] @@ -841,10 +868,22 @@ class WindowsVPNLauncher(VPNLauncher): if openvpn_verb is not None: args += ['--verb', '%d' % (openvpn_verb,)] - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() + gateways = [] + leap_settings = LeapSettings(ProviderConfig.standalone) + 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: {}".format(', '.join(gateways))) + logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) for gw in gateways: args += ['--remote', gw, '1194', 'udp'] diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 2419fc0d..3bbfea85 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -159,8 +159,7 @@ class SoledadBootstrapper(AbstractBootstrapper): self.soledad_timeout.emit() except socket.error as exc: logger.error("Socket error while initializing soledad") - if exc.errno in (111, ): - self.soledad_failed.emit() + self.soledad_failed.emit() except u1db_errors.Unauthorized: logger.error("Error while initializing soledad " "(unauthorized).") diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index ce8323cd..78efcb6e 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -15,59 +15,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """ -Initializes version and app info, plus some small and handy functions. +Some small and handy functions. """ import datetime import os -from pkg_resources import parse_version - - -def _is_release_version(version): - """ - Helper to determine whether a version is a final release or not. - The release needs to be of the form: w.x.y.z containing only numbers - and dots. - - :param version: the version string - :type version: str - :returns: if the version is a release version or not. - :rtype: bool - """ - parsed_version = parse_version(version) - not_number = 0 - for x in parsed_version: - try: - int(x) - except: - not_number += 1 - - return not_number == 1 - - -__version__ = "unknown" -IS_RELEASE_VERSION = False - -try: - from leap.bitmask._version import get_versions - __version__ = get_versions()['version'] - IS_RELEASE_VERSION = _is_release_version(__version__) - del get_versions -except ImportError: - #running on a tree that has not run - #the setup.py setver - pass - -__appname__ = "unknown" -try: - from leap._appname import __appname__ -except ImportError: - #running on a tree that has not run - #the setup.py setver - pass - -__full_version__ = __appname__ + '/' + str(__version__) - def first(things): """ @@ -75,7 +27,7 @@ def first(things): """ try: return things[0] - except TypeError: + except (IndexError, TypeError): return None diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index 71f5163d..bc21a9cf 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -14,10 +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/>. - +""" +Parses the command line arguments passed to the application. +""" import argparse -from leap.bitmask.util import IS_RELEASE_VERSION +from leap.bitmask import IS_RELEASE_VERSION def build_parser(): diff --git a/src/leap/bitmask/util/leap_log_handler.py b/src/leap/bitmask/util/leap_log_handler.py index 9adb21a5..98924c12 100644 --- a/src/leap/bitmask/util/leap_log_handler.py +++ b/src/leap/bitmask/util/leap_log_handler.py @@ -90,6 +90,9 @@ class HandlerAdapter(object): def setLevel(self, *args, **kwargs): return self._handler.setLevel(*args, **kwargs) + def addFilter(self, *args, **kwargs): + return self._handler.addFilter(*args, **kwargs) + def handle(self, *args, **kwargs): return self._handler.handle(*args, **kwargs) diff --git a/src/leap/bitmask/util/log_silencer.py b/src/leap/bitmask/util/log_silencer.py new file mode 100644 index 00000000..09aa2cff --- /dev/null +++ b/src/leap/bitmask/util/log_silencer.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# log_silencer.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/>. +""" +Filter for leap logs. +""" +import logging +import os +import re + +from leap.common.config import get_path_prefix + + +class SelectiveSilencerFilter(logging.Filter): + """ + Configurable filter for root leap logger. + + If you want to ignore components from the logging, just add them, + one by line, to ~/.config/leap/leap.dev.conf + """ + # TODO we can augment this by properly parsing the log-silencer file + # and having different sections: ignore, levels, ... + + # TODO use ConfigParser to unify sections [log-ignore] [log-debug] etc + + CONFIG_NAME = "leap.dev.conf" + + # Components to be completely silenced in the main bitmask logs. + # You probably should think twice before adding a component to + # the tuple below. Only very well tested components should go here, and + # only in those cases in which we gain more from silencing them than from + # having their logs into the main log file that the user will likely send + # to us. + SILENCER_RULES = ( + 'leap.common.events', + ) + + def __init__(self, standalone=False): + """ + Tries to load silencer rules from the default path, + or load from the SILENCER_RULES tuple if not found. + """ + self.standalone = standalone + self.rules = None + if os.path.isfile(self._rules_path): + self.rules = self._load_rules() + if not self.rules: + self.rules = self.SILENCER_RULES + + @property + def _rules_path(self): + """ + The configuration file for custom ignore rules. + """ + return os.path.join( + get_path_prefix(standalone=self.standalone), + "leap", self.CONFIG_NAME) + + def _load_rules(self): + """ + Loads a list of paths to be ignored from the logging. + """ + lines = open(self._rules_path).readlines() + return map(lambda line: re.sub('\s', '', line), + lines) + + def filter(self, record): + """ + Implements the filter functionality for this Filter + + :param record: the record to be examined + :type record: logging.LogRecord + :returns: a bool indicating whether the record should be logged or not. + :rtype: bool + """ + if not self.rules: + return True + logger_path = record.name + for path in self.rules: + if logger_path.startswith(path): + return False + return True diff --git a/src/leap/bitmask/util/polkit_agent.py b/src/leap/bitmask/util/polkit_agent.py new file mode 100644 index 00000000..6fda2f88 --- /dev/null +++ b/src/leap/bitmask/util/polkit_agent.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# polkit_agent.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/>. +""" +Daemonizes polkit authentication agent. +""" +import logging +import subprocess + +import daemon + +logger = logging.getLogger(__name__) + +AUTH_FILE = "polkit-%s-authentication-agent-1" +BASE_PATH_GNO = "/usr/lib/policykit-1-gnome/" +BASE_PATH_KDE = "/usr/lib/kde4/libexec/" +GNO_PATH = BASE_PATH_GNO + AUTH_FILE % ("gnome",) +KDE_PATH = BASE_PATH_KDE + AUTH_FILE % ("kde",) + + +def _launch_agent(): + logger.debug('Launching polkit auth agent') + try: + subprocess.call(GNO_PATH) + except Exception as exc: + logger.error('Exception while running polkit authentication agent ' + '%s' % (exc,)) + # XXX fix KDE launch. See: #3755 + #try: + #subprocess.call(KDE_PATH) + #except Exception as exc: + + +def launch(): + with daemon.DaemonContext(): + _launch_agent() + +if __name__ == "__main__": + launch() diff --git a/src/leap/bitmask/util/reqs.txt b/src/leap/bitmask/util/reqs.txt deleted file mode 100644 index 0bcf85dc..00000000 --- a/src/leap/bitmask/util/reqs.txt +++ /dev/null @@ -1,14 +0,0 @@ -requests -srp>=1.0.2 -pyopenssl -keyring -python-dateutil -psutil -ipaddr -twisted -qt4reactor -python-gnupg -leap.common>=0.2.5 -leap.soledad>=0.1.0 -mock -oauth
\ No newline at end of file diff --git a/src/leap/bitmask/util/requirement_checker.py b/src/leap/bitmask/util/requirement_checker.py index bd3c1412..37e8e693 100644 --- a/src/leap/bitmask/util/requirement_checker.py +++ b/src/leap/bitmask/util/requirement_checker.py @@ -53,7 +53,7 @@ def get_requirements(): if not develop: requires_file_name = os.path.join( 'leap', 'bitmask', 'util', 'reqs.txt') - dist_name = Requirement.parse('bitmask') + dist_name = Requirement.parse('leap.bitmask') try: with resource_stream(dist_name, requires_file_name) as stream: diff --git a/src/leap/bitmask/util/tests/test_is_release_version.py b/src/leap/bitmask/util/tests/test_is_release_version.py index 088ec66d..0a0093da 100644 --- a/src/leap/bitmask/util/tests/test_is_release_version.py +++ b/src/leap/bitmask/util/tests/test_is_release_version.py @@ -19,7 +19,7 @@ tests for _is_release_version function """ import unittest -from leap.bitmask.util import _is_release_version as is_release_version +from leap.bitmask import _is_release_version as is_release_version from leap.common.testing.basetest import BaseLeapTest |