diff options
author | Kali Kaneko <kali@leap.se> | 2014-04-09 16:24:12 -0500 |
---|---|---|
committer | Kali Kaneko <kali@leap.se> | 2014-04-09 16:24:12 -0500 |
commit | cf9fb29fd1098f694974ee60e5673f22e286d859 (patch) | |
tree | 5cdc4210b8c944ee4f2669fd8096998d507beda3 /src | |
parent | f0232265a2725d8129b472479dd380b9ec3ca6b2 (diff) | |
parent | 81715dc47d77934c4f67d2527a56c28f58f0345d (diff) |
Merge tag '0.5.0' into deb-0.5.0
Tag leap.bitmask version 0.5.0
Conflicts:
pkg/requirements.pip
Diffstat (limited to 'src')
43 files changed, 2674 insertions, 731 deletions
diff --git a/src/leap/bitmask/__init__.py b/src/leap/bitmask/__init__.py index a4642e27..c844beb1 100644 --- a/src/leap/bitmask/__init__.py +++ b/src/leap/bitmask/__init__.py @@ -56,6 +56,7 @@ __short_version__ = "unknown" try: from leap.bitmask._version import get_versions __version__ = get_versions()['version'] + __version_hash__ = get_versions()['full'] IS_RELEASE_VERSION = _is_release_version(__version__) del get_versions except ImportError: diff --git a/src/leap/bitmask/_appname.py b/src/leap/bitmask/_appname.py new file mode 100644 index 00000000..82e8bd43 --- /dev/null +++ b/src/leap/bitmask/_appname.py @@ -0,0 +1 @@ +__appname__ = "bitmask" diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index b16a51aa..02e27123 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # app.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -50,14 +50,17 @@ from PySide import QtCore, QtGui from leap.bitmask import __version__ as VERSION from leap.bitmask.util import leap_argparse -from leap.bitmask.util import log_silencer +from leap.bitmask.util import log_silencer, LOG_FORMAT from leap.bitmask.util.leap_log_handler import LeapLogHandler from leap.bitmask.util.streamtologger import StreamToLogger from leap.bitmask.platform_init import IS_WIN -from leap.bitmask.services.mail.repair import repair_account +from leap.bitmask.services.mail import plumber from leap.common.events import server as event_server from leap.mail import __version__ as MAIL_VERSION +from twisted.internet import reactor +from twisted.internet.task import LoopingCall + import codecs codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None) @@ -74,13 +77,7 @@ def sigint_handler(*args, **kwargs): mainwindow.quit() -def install_qtreactor(logger): - import qt4reactor - qt4reactor.install() - logger.debug("Qt4 reactor installed") - - -def add_logger_handlers(debug=False, logfile=None): +def add_logger_handlers(debug=False, logfile=None, replace_stdout=True): """ Create the logger and attach the handlers. @@ -100,10 +97,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:%(funcName)s:L#%(lineno)s ' - '- %(levelname)s - %(message)s') - formatter = logging.Formatter(log_format) + formatter = logging.Formatter(LOG_FORMAT) # Console handler try: @@ -117,6 +111,9 @@ def add_logger_handlers(debug=False, logfile=None): else: using_coloredlog = True + if using_coloredlog: + replace_stdout = False + silencer = log_silencer.SelectiveSilencerFilter() console.addFilter(silencer) logger.addHandler(console) @@ -139,7 +136,7 @@ def add_logger_handlers(debug=False, logfile=None): logger.addHandler(fileh) logger.debug('File handler plugged!') - if not using_coloredlog: + if replace_stdout: replace_stdout_stderr_with_logging(logger) return logger @@ -164,34 +161,47 @@ def replace_stdout_stderr_with_logging(logger): log.startLogging(sys.stdout) -def main(): +def do_display_version(opts): """ - Starts the main event loop and launches the main window. + Display version and exit. """ - _, opts = leap_argparse.init_leapc_args() - + # TODO move to a different module: commands? if opts.version: print "Bitmask version: %s" % (VERSION,) print "leap.mail version: %s" % (MAIL_VERSION,) sys.exit(0) - if opts.acct_to_repair: - repair_account(opts.acct_to_repair) + +def do_mail_plumbing(opts): + """ + Analize options and do mailbox plumbing if requested. + """ + # TODO move to a different module: commands? + if opts.repair: + plumber.repair_account(opts.acct) + sys.exit(0) + if opts.import_maildir and opts.acct: + plumber.import_maildir(opts.acct, opts.import_maildir) sys.exit(0) + # XXX catch when import is used w/o acct + + +def main(): + """ + Starts the main event loop and launches the main window. + """ + # TODO move boilerplate outa here! + _, opts = leap_argparse.init_leapc_args() + do_display_version(opts) standalone = opts.standalone + offline = opts.offline bypass_checks = getattr(opts, 'danger', False) debug = opts.debug logfile = opts.log_file mail_logfile = opts.mail_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. @@ -199,12 +209,39 @@ def main(): from leap.bitmask.config import flags from leap.common.config.baseconfig import BaseConfig flags.STANDALONE = standalone + flags.OFFLINE = offline flags.MAIL_LOGFILE = mail_logfile + flags.APP_VERSION_CHECK = opts.app_version_check + flags.API_VERSION_CHECK = opts.api_version_check + + flags.CA_CERT_FILE = opts.ca_cert_file + BaseConfig.standalone = standalone - logger = add_logger_handlers(debug, logfile) + replace_stdout = True + if opts.repair or opts.import_maildir: + # We don't want too much clutter on the comand mode + # this could be more generic with a Command class. + replace_stdout = False + logger = add_logger_handlers(debug, logfile, replace_stdout) + + # ok, we got logging in place, we can satisfy mail plumbing requests + # and show logs there. it normally will exit there if we got that path. + do_mail_plumbing(opts) + + 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,) + + PLAY_NICE = os.environ.get("LEAP_NICE") + if PLAY_NICE and PLAY_NICE.isdigit(): + nice = os.nice(int(PLAY_NICE)) + logger.info("Setting NICE: %s" % nice) # And then we import all the other stuff + # I think it's safe to import at the top by now -- kali from leap.bitmask.gui import locale_rc from leap.bitmask.gui import twisted_main from leap.bitmask.gui.mainwindow import MainWindow @@ -215,6 +252,7 @@ def main(): # pylint: avoid unused import assert(locale_rc) + # TODO move to a different module: commands? if not we_are_the_one_and_only(): # Bitmask is already running logger.warning("Tried to launch more than one instance " @@ -240,9 +278,6 @@ def main(): app = QtGui.QApplication(sys.argv) - # install the qt4reactor. - install_qtreactor(logger) - # To test: # $ LANG=es ./app.py locale = QtCore.QLocale.system().name() @@ -285,8 +320,9 @@ def main(): #tx_app = leap_services() #assert(tx_app) - # Run main loop - twisted_main.start(app) + l = LoopingCall(QtCore.QCoreApplication.processEvents, 0, 10) + l.start(0.01) + reactor.run() if __name__ == "__main__": main() diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index 8a289a79..45ea451c 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -18,8 +18,8 @@ Backend for everything """ import logging -import os +from functools import partial from Queue import Queue, Empty from twisted.internet import threads, defer @@ -29,6 +29,8 @@ from twisted.python import log import zope.interface from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.crypto.srpregister import SRPRegister +from leap.bitmask.provider import get_provider_path from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper # Frontend side @@ -134,6 +136,9 @@ class Provider(object): :param provider: URL for the provider :type provider: unicode + + :returns: the defer for the operation running in a thread. + :rtype: twisted.internet.defer.Deferred """ log.msg("Setting up provider %s..." % (provider.encode("idna"),)) pb = self._provider_bootstrapper @@ -141,23 +146,31 @@ class Provider(object): self._download_provider_defer = d return d + def cancel_setup_provider(self): + """ + Cancel the ongoing setup provider defer (if any). + """ + d = self._download_provider_defer + if d is not None: + d.cancel() + def bootstrap(self, provider): """ Second stage of bootstrapping for a provider. :param provider: URL for the provider :type provider: unicode - """ + :returns: the defer for the operation running in a thread. + :rtype: twisted.internet.defer.Deferred + """ d = None # If there's no loaded provider or # we want to connect to other provider... if (not self._provider_config.loaded() or self._provider_config.get_domain() != provider): - self._provider_config.load( - os.path.join("leap", "providers", - provider, "provider.json")) + self._provider_config.load(get_provider_path(provider)) if self._provider_config.loaded(): d = self._provider_bootstrapper.run_provider_setup_checks( @@ -174,6 +187,57 @@ class Provider(object): return d +class Register(object): + """ + Interfaces with setup and bootstrapping operations for a provider + """ + + zope.interface.implements(ILEAPComponent) + + def __init__(self, signaler=None): + """ + Constructor for the Register component + + :param signaler: Object in charge of handling communication + back to the frontend + :type signaler: Signaler + """ + object.__init__(self) + self.key = "register" + self._signaler = signaler + self._provider_config = ProviderConfig() + + def register_user(self, domain, username, password): + """ + Register a user using the domain and password given as parameters. + + :param domain: the domain we need to register the user. + :type domain: unicode + :param username: the user name + :type username: unicode + :param password: the password for the username + :type password: unicode + + :returns: the defer for the operation running in a thread. + :rtype: twisted.internet.defer.Deferred + """ + # If there's no loaded provider or + # we want to connect to other provider... + if (not self._provider_config.loaded() or + self._provider_config.get_domain() != domain): + self._provider_config.load(get_provider_path(domain)) + + if self._provider_config.loaded(): + srpregister = SRPRegister(signaler=self._signaler, + provider_config=self._provider_config) + return threads.deferToThread( + partial(srpregister.register_user, username, password)) + else: + if self._signaler is not None: + self._signaler.signal(self._signaler.srp_registration_failed) + logger.error("Could not load provider configuration.") + + class Signaler(QtCore.QObject): """ Signaler object, handles converting string commands to Qt signals. @@ -182,8 +246,9 @@ class Signaler(QtCore.QObject): live in the frontend. """ - # Signals for the ProviderBootstrapper + #################### # These will only exist in the frontend + # Signals for the ProviderBootstrapper prov_name_resolution = QtCore.Signal(object) prov_https_connection = QtCore.Signal(object) prov_download_provider_info = QtCore.Signal(object) @@ -194,7 +259,18 @@ class Signaler(QtCore.QObject): prov_problem_with_provider = QtCore.Signal(object) - # These will exist both in the backend and the front end. + prov_unsupported_client = QtCore.Signal(object) + prov_unsupported_api = QtCore.Signal(object) + + prov_cancelled_setup = QtCore.Signal(object) + + # Signals for SRPRegister + srp_registration_finished = QtCore.Signal(object) + srp_registration_failed = QtCore.Signal(object) + srp_registration_taken = QtCore.Signal(object) + + #################### + # These will exist both in the backend AND the front end. # The frontend might choose to not "interpret" all the signals # from the backend, but the backend needs to have all the signals # it's going to emit defined here @@ -204,7 +280,14 @@ class Signaler(QtCore.QObject): PROV_DOWNLOAD_CA_CERT_KEY = "prov_download_ca_cert" PROV_CHECK_CA_FINGERPRINT_KEY = "prov_check_ca_fingerprint" PROV_CHECK_API_CERTIFICATE_KEY = "prov_check_api_certificate" - PROV_PROV_PROBLEM_WITH_PROVIER_KEY = "prov_problem_with_provider" + PROV_PROBLEM_WITH_PROVIDER_KEY = "prov_problem_with_provider" + PROV_UNSUPPORTED_CLIENT = "prov_unsupported_client" + PROV_UNSUPPORTED_API = "prov_unsupported_api" + PROV_CANCELLED_SETUP = "prov_cancelled_setup" + + SRP_REGISTRATION_FINISHED = "srp_registration_finished" + SRP_REGISTRATION_FAILED = "srp_registration_failed" + SRP_REGISTRATION_TAKEN = "srp_registration_taken" def __init__(self): """ @@ -220,7 +303,14 @@ class Signaler(QtCore.QObject): self.PROV_DOWNLOAD_CA_CERT_KEY, self.PROV_CHECK_CA_FINGERPRINT_KEY, self.PROV_CHECK_API_CERTIFICATE_KEY, - self.PROV_PROV_PROBLEM_WITH_PROVIER_KEY + self.PROV_PROBLEM_WITH_PROVIDER_KEY, + self.PROV_UNSUPPORTED_CLIENT, + self.PROV_UNSUPPORTED_API, + self.PROV_CANCELLED_SETUP, + + self.SRP_REGISTRATION_FINISHED, + self.SRP_REGISTRATION_FAILED, + self.SRP_REGISTRATION_TAKEN, ] for sig in signals: @@ -243,6 +333,11 @@ class Signaler(QtCore.QObject): # will do zmq.send_multipart, and the frontend version will be # similar to this log.msg("Signaling %s :: %s" % (key, data)) + + # for some reason emitting 'None' gives a segmentation fault. + if data is None: + data = '' + try: self._signals[key].emit(data) except KeyError: @@ -274,6 +369,7 @@ class Backend(object): # Component registration self._register(Provider(self._signaler, bypass_checks)) + self._register(Register(self._signaler)) # We have a looping call on a thread executing all the # commands in queue. Right now this queue is an actual Queue @@ -303,7 +399,8 @@ class Backend(object): Stops the looping call and tries to cancel all the defers. """ log.msg("Stopping worker...") - self._lc.stop() + if self._lc.running: + self._lc.stop() while len(self._ongoing_defers) > 0: d = self._ongoing_defers.pop() d.cancel() @@ -345,17 +442,20 @@ class Backend(object): # cmd is: component, method, signalback, *args func = getattr(self._components[cmd[0]], cmd[1]) d = func(*cmd[3:]) - # A call might not have a callback signal, but if it does, - # we add it to the chain - if cmd[2] is not None: - d.addCallbacks(self._signal_back, log.err, cmd[2]) - d.addCallbacks(self._done_action, log.err, - callbackKeywords={"d": d}) - d.addErrback(log.err) - self._ongoing_defers.append(d) + if d is not None: # d may be None if a defer chain is cancelled. + # A call might not have a callback signal, but if it does, + # we add it to the chain + if cmd[2] is not None: + d.addCallbacks(self._signal_back, log.err, cmd[2]) + d.addCallbacks(self._done_action, log.err, + callbackKeywords={"d": d}) + d.addErrback(log.err) + self._ongoing_defers.append(d) except Empty: # If it's just empty we don't have anything to do. pass + except defer.CancelledError: + logger.debug("defer cancelled somewhere (CancelledError).") except Exception: # But we log the rest log.err() @@ -367,7 +467,8 @@ class Backend(object): :param d: defer to remove :type d: twisted.internet.defer.Deferred """ - self._ongoing_defers.remove(d) + if d in self._ongoing_defers: + self._ongoing_defers.remove(d) # XXX: Temporal interface until we migrate to zmq # We simulate the calls to zmq.send_multipart. Once we separate @@ -377,5 +478,12 @@ class Backend(object): def setup_provider(self, provider): self._call_queue.put(("provider", "setup_provider", None, provider)) + def cancel_setup_provider(self): + self._call_queue.put(("provider", "cancel_setup_provider", None)) + def provider_bootstrap(self, provider): self._call_queue.put(("provider", "bootstrap", None, provider)) + + def register_user(self, provider, username, password): + self._call_queue.put(("register", "register_user", None, provider, + username, password)) diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py index ba1b65b9..5d8bc9b3 100644 --- a/src/leap/bitmask/config/flags.py +++ b/src/leap/bitmask/config/flags.py @@ -32,3 +32,21 @@ WARNING: You should NOT use this kind of flags unless you're sure of what STANDALONE = False MAIL_LOGFILE = None + +# The APP/API version check flags are used to provide a way to skip +# that checks. +# This can be used for: +# - allow the use of a client that is not compatible with a provider. +# - use a development version of the client with an older version number +# since it's not released yet, and it is compatible with a newer provider. +APP_VERSION_CHECK = True +API_VERSION_CHECK = True + +# Offline mode? +# Used for skipping soledad bootstrapping/syncs. +OFFLINE = False + + +# CA cert path +# used to allow self signed certs in requests that needs SSL +CA_CERT_FILE = None diff --git a/src/leap/bitmask/config/leapsettings.py b/src/leap/bitmask/config/leapsettings.py index c524425e..13a1e99e 100644 --- a/src/leap/bitmask/config/leapsettings.py +++ b/src/leap/bitmask/config/leapsettings.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # leapsettings.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -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/>. - """ -QSettings abstraction +QSettings abstraction. """ import os import logging @@ -70,6 +69,7 @@ class LeapSettings(object): GATEWAY_KEY = "Gateway" PINNED_KEY = "Pinned" SKIPFIRSTRUN_KEY = "SkipFirstRun" + UUIDFORUSER_KEY = "%s/%s_uuid" # values GATEWAY_AUTOMATIC = "Automatic" @@ -353,3 +353,35 @@ class LeapSettings(object): """ leap_assert_type(skip, bool) self._settings.setValue(self.SKIPFIRSTRUN_KEY, skip) + + def get_uuid(self, username): + """ + Gets the uuid for a given username. + + :param username: the full user identifier in the form user@provider + :type username: basestring + """ + leap_assert("@" in username, + "Expected username in the form user@provider") + user, provider = username.split('@') + return self._settings.value( + self.UUIDFORUSER_KEY % (provider, user), "") + + def set_uuid(self, username, value): + """ + Sets the uuid for a given username. + + :param username: the full user identifier in the form user@provider + :type username: str or unicode + :param value: the uuid to save or None to remove it + :type value: str or unicode or None + """ + leap_assert("@" in username, + "Expected username in the form user@provider") + user, provider = username.split('@') + key = self.UUIDFORUSER_KEY % (provider, user) + if value is None: + self._settings.remove(key) + else: + leap_assert(len(value) > 0, "We cannot save an empty uuid") + self._settings.setValue(key, value) diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py index e80b2337..2ebe05ce 100644 --- a/src/leap/bitmask/config/providerconfig.py +++ b/src/leap/bitmask/config/providerconfig.py @@ -21,11 +21,12 @@ Provider configuration import logging import os -from leap.common.check import leap_check -from leap.common.config.baseconfig import BaseConfig, LocalizedKey +from leap.bitmask import provider from leap.bitmask.config.provider_spec import leap_provider_spec from leap.bitmask.services import get_service_display_name from leap.bitmask.util import get_path_prefix +from leap.common.check import leap_check +from leap.common.config.baseconfig import BaseConfig, LocalizedKey logger = logging.getLogger(__name__) @@ -55,10 +56,7 @@ class ProviderConfig(BaseConfig): :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): + if not provider_config.load(provider.get_provider_path(domain)): provider_config = None return provider_config diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 85b9b003..7cf7e55a 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -31,6 +31,7 @@ from requests.adapters import HTTPAdapter from PySide import QtCore from twisted.internet import threads +from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.util import request_helpers as reqhelper from leap.bitmask.util.compat import requests_has_max_retries from leap.bitmask.util.constants import REQUEST_TIMEOUT @@ -147,6 +148,7 @@ class SRPAuth(QtCore.QObject): "We need a provider config to authenticate") self._provider_config = provider_config + self._settings = LeapSettings() # **************************************************** # # Dependency injection helpers, override this for more @@ -161,17 +163,14 @@ class SRPAuth(QtCore.QObject): self._session_id = None self._session_id_lock = QtCore.QMutex() - self._uid = None - self._uid_lock = QtCore.QMutex() + self._uuid = None + self._uuid_lock = QtCore.QMutex() self._token = None self._token_lock = QtCore.QMutex() self._srp_user = None self._srp_a = None - # Error msg displayed if the username or the password is invalid - self._WRONG_USER_PASS = self.tr("Invalid username or password.") - # User credentials stored for password changing checks self._username = None self._password = None @@ -265,14 +264,11 @@ class SRPAuth(QtCore.QObject): # Clean up A value, we don't need it anymore self._srp_a = None except requests.exceptions.ConnectionError as e: - logger.error("No connection made (salt): %r" % - (e,)) - raise SRPAuthConnectionError("Could not establish a " - "connection") + logger.error("No connection made (salt): {0!r}".format(e)) + raise SRPAuthConnectionError() except Exception as e: logger.error("Unknown error: %r" % (e,)) - raise SRPAuthenticationError("Unknown error: %r" % - (e,)) + raise SRPAuthenticationError() content, mtime = reqhelper.get_content(init_session) @@ -281,23 +277,22 @@ class SRPAuth(QtCore.QObject): "Status code = %r. Content: %r" % (init_session.status_code, content)) if init_session.status_code == 422: - raise SRPAuthBadUserOrPassword(self._WRONG_USER_PASS) + logger.error("Invalid username or password.") + raise SRPAuthBadUserOrPassword() - raise SRPAuthBadStatusCode(self.tr("There was a problem with" - " authentication")) + logger.error("There was a problem with authentication.") + raise SRPAuthBadStatusCode() json_content = json.loads(content) salt = json_content.get("salt", None) B = json_content.get("B", None) if salt is None: - logger.error("No salt parameter sent") - raise SRPAuthNoSalt(self.tr("The server did not send " - "the salt parameter")) + logger.error("The server didn't send the salt parameter.") + raise SRPAuthNoSalt() if B is None: - logger.error("No B parameter sent") - raise SRPAuthNoB(self.tr("The server did not send " - "the B parameter")) + logger.error("The server didn't send the B parameter.") + raise SRPAuthNoB() return salt, B @@ -328,8 +323,7 @@ class SRPAuth(QtCore.QObject): unhex_B = self._safe_unhexlify(B) except (TypeError, ValueError) as e: logger.error("Bad data from server: %r" % (e,)) - raise SRPAuthBadDataFromServer( - self.tr("The data sent from the server had errors")) + raise SRPAuthBadDataFromServer() M = self._srp_user.process_challenge(unhex_salt, unhex_B) auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(), @@ -350,13 +344,13 @@ class SRPAuth(QtCore.QObject): timeout=REQUEST_TIMEOUT) except requests.exceptions.ConnectionError as e: logger.error("No connection made (HAMK): %r" % (e,)) - raise SRPAuthConnectionError(self.tr("Could not connect to " - "the server")) + raise SRPAuthConnectionError() try: content, mtime = reqhelper.get_content(auth_result) except JSONDecodeError: - raise SRPAuthJSONDecodeError("Bad JSON content in auth result") + logger.error("Bad JSON content in auth result.") + raise SRPAuthJSONDecodeError() if auth_result.status_code == 422: error = "" @@ -370,14 +364,13 @@ class SRPAuth(QtCore.QObject): "received: %s", (content,)) logger.error("[%s] Wrong password (HAMK): [%s]" % (auth_result.status_code, error)) - raise SRPAuthBadUserOrPassword(self._WRONG_USER_PASS) + raise SRPAuthBadUserOrPassword() if auth_result.status_code not in (200,): logger.error("No valid response (HAMK): " "Status code = %s. Content = %r" % (auth_result.status_code, content)) - raise SRPAuthBadStatusCode(self.tr("Unknown error (%s)") % - (auth_result.status_code,)) + raise SRPAuthBadStatusCode() return json.loads(content) @@ -394,24 +387,22 @@ class SRPAuth(QtCore.QObject): """ try: M2 = json_content.get("M2", None) - uid = json_content.get("id", None) + uuid = json_content.get("id", None) token = json_content.get("token", None) except Exception as e: logger.error(e) - raise SRPAuthBadDataFromServer("Something went wrong with the " - "login") + raise SRPAuthBadDataFromServer() - self.set_uid(uid) + self.set_uuid(uuid) self.set_token(token) - if M2 is None or self.get_uid() is None: + if M2 is None or self.get_uuid() is None: logger.error("Something went wrong. Content = %r" % (json_content,)) - raise SRPAuthBadDataFromServer(self.tr("Problem getting data " - "from server")) + raise SRPAuthBadDataFromServer() events_signal( - proto.CLIENT_UID, content=uid, + proto.CLIENT_UID, content=uuid, reqcbk=lambda req, res: None) # make the rpc call async return M2 @@ -434,22 +425,19 @@ class SRPAuth(QtCore.QObject): unhex_M2 = self._safe_unhexlify(M2) except TypeError: logger.error("Bad data from server (HAWK)") - raise SRPAuthBadDataFromServer(self.tr("Bad data from server")) + raise SRPAuthBadDataFromServer() self._srp_user.verify_session(unhex_M2) if not self._srp_user.authenticated(): - logger.error("Auth verification failed") - raise SRPAuthVerificationFailed(self.tr("Auth verification " - "failed")) + logger.error("Auth verification failed.") + raise SRPAuthVerificationFailed() logger.debug("Session verified.") session_id = self._session.cookies.get(self.SESSION_ID_KEY, None) if not session_id: logger.error("Bad cookie from server (missing _session_id)") - raise SRPAuthNoSessionId(self.tr("Session cookie " - "verification " - "failed")) + raise SRPAuthNoSessionId() events_signal( proto.CLIENT_SESSION_ID, content=session_id, @@ -475,7 +463,7 @@ class SRPAuth(QtCore.QObject): :param new_password: the new password for the user :type new_password: str """ - leap_assert(self.get_uid() is not None) + leap_assert(self.get_uuid() is not None) if current_password != self._password: raise SRPAuthBadUserOrPassword @@ -483,7 +471,7 @@ class SRPAuth(QtCore.QObject): url = "%s/%s/users/%s.json" % ( self._provider_config.get_api_uri(), self._provider_config.get_api_version(), - self.get_uid()) + self.get_uuid()) salt, verifier = self._srp.create_salted_verification_key( self._username.encode('utf-8'), new_password.encode('utf-8'), @@ -580,7 +568,7 @@ class SRPAuth(QtCore.QObject): raise else: self.set_session_id(None) - self.set_uid(None) + self.set_uuid(None) self.set_token(None) # Also reset the session self._session = self._fetcher.session() @@ -594,13 +582,17 @@ class SRPAuth(QtCore.QObject): QtCore.QMutexLocker(self._session_id_lock) return self._session_id - def set_uid(self, uid): - QtCore.QMutexLocker(self._uid_lock) - self._uid = uid + def set_uuid(self, uuid): + QtCore.QMutexLocker(self._uuid_lock) + full_uid = "%s@%s" % ( + self._username, self._provider_config.get_domain()) + if uuid is not None: # avoid removing the uuid from settings + self._settings.set_uuid(full_uid, uuid) + self._uuid = uuid - def get_uid(self): - QtCore.QMutexLocker(self._uid_lock) - return self._uid + def get_uuid(self): + QtCore.QMutexLocker(self._uuid_lock) + return self._uuid def set_token(self, token): QtCore.QMutexLocker(self._token_lock) @@ -612,8 +604,9 @@ class SRPAuth(QtCore.QObject): __instance = None - authentication_finished = QtCore.Signal(bool, str) - logout_finished = QtCore.Signal(bool, str) + authentication_finished = QtCore.Signal() + logout_ok = QtCore.Signal() + logout_error = QtCore.Signal() def __init__(self, provider_config): """ @@ -650,7 +643,6 @@ class SRPAuth(QtCore.QObject): username = username.lower() d = self.__instance.authenticate(username, password) d.addCallback(self._gui_notify) - d.addErrback(self._errback) return d def change_password(self, current_password, new_password): @@ -676,7 +668,7 @@ class SRPAuth(QtCore.QObject): :rtype: str or None """ - if self.get_uid() is None: + if self.get_uuid() is None: return None return self.__instance._username @@ -688,25 +680,13 @@ class SRPAuth(QtCore.QObject): :type _: IGNORED """ logger.debug("Successful login!") - self.authentication_finished.emit(True, self.tr("Succeeded")) - - def _errback(self, failure): - """ - General errback for the whole login process. Will notify the - UI with the proper signal. - - :param failure: Failure object captured from a callback. - :type failure: twisted.python.failure.Failure - """ - logger.error("Error logging in %s" % (failure,)) - self.authentication_finished.emit(False, "%s" % (failure.value,)) - failure.trap(Exception) + self.authentication_finished.emit() def get_session_id(self): return self.__instance.get_session_id() - def get_uid(self): - return self.__instance.get_uid() + def get_uuid(self): + return self.__instance.get_uuid() def get_token(self): return self.__instance.get_token() @@ -718,8 +698,10 @@ class SRPAuth(QtCore.QObject): """ try: self.__instance.logout() - self.logout_finished.emit(True, self.tr("Succeeded")) + logger.debug("Logout success") + self.logout_ok.emit() return True except Exception as e: - self.logout_finished.emit(False, "%s" % (e,)) + logger.debug("Logout error: {0!r}".format(e)) + self.logout_error.emit() return False diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py index 02a1ea63..4c52db42 100644 --- a/src/leap/bitmask/crypto/srpregister.py +++ b/src/leap/bitmask/crypto/srpregister.py @@ -16,6 +16,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import binascii +import json import logging import requests @@ -26,6 +27,7 @@ from urlparse import urlparse from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.util.constants import SIGNUP_TIMEOUT +from leap.bitmask.util.request_helpers import get_content from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -40,16 +42,22 @@ class SRPRegister(QtCore.QObject): USER_VERIFIER_KEY = 'user[password_verifier]' USER_SALT_KEY = 'user[password_salt]' + STATUS_OK = (200, 201) + STATUS_TAKEN = 422 + STATUS_ERROR = -999 # Custom error status + registration_finished = QtCore.Signal(bool, object) - def __init__(self, - provider_config=None, - register_path="users"): + def __init__(self, signaler=None, + provider_config=None, register_path="users"): """ Constructor + :param signaler: Signaler object used to receive notifications + from the backend + :type signaler: Signaler :param provider_config: provider configuration instance, - properly loaded + properly loaded :type privider_config: ProviderConfig :param register_path: webapp path for registering users :type register_path; str @@ -59,6 +67,7 @@ class SRPRegister(QtCore.QObject): leap_assert_type(provider_config, ProviderConfig) self._provider_config = provider_config + self._signaler = signaler # **************************************************** # # Dependency injection helpers, override this for more @@ -104,8 +113,8 @@ class SRPRegister(QtCore.QObject): :param password: password for this username :type password: str - :rtype: tuple - :rparam: (ok, request) + :returns: if the registration went ok or not. + :rtype: bool """ username = username.lower().encode('utf-8') @@ -129,11 +138,7 @@ class SRPRegister(QtCore.QObject): logger.debug("Will try to register user = %s" % (username,)) ok = False - # This should be None, but we don't like when PySide segfaults, - # so it something else. - # To reproduce it, just do: - # self.registration_finished.emit(False, None) - req = [] + req = None try: req = self._session.post(uri, data=user_data, @@ -143,13 +148,45 @@ class SRPRegister(QtCore.QObject): except requests.exceptions.RequestException as exc: logger.error(exc.message) - ok = False else: ok = req.ok - self.registration_finished.emit(ok, req) + status_code = self.STATUS_ERROR + if req is not None: + status_code = req.status_code + self._emit_result(status_code) + + if not ok: + try: + content, _ = get_content(req) + json_content = json.loads(content) + error_msg = json_content.get("errors").get("login")[0] + if not error_msg.istitle(): + error_msg = "%s %s" % (username, error_msg) + logger.error(error_msg) + except Exception as e: + logger.error("Unknown error: %r" % (e, )) + return ok + def _emit_result(self, status_code): + """ + Emit the corresponding signal depending on the status code. + + :param status_code: the status code received. + :type status_code: int or str + """ + logger.debug("Status code is: {0}".format(status_code)) + if self._signaler is None: + return + + if status_code in self.STATUS_OK: + self._signaler.signal(self._signaler.SRP_REGISTRATION_FINISHED) + elif status_code == self.STATUS_TAKEN: + self._signaler.signal(self._signaler.SRP_REGISTRATION_TAKEN) + else: + self._signaler.signal(self._signaler.SRP_REGISTRATION_FAILED) + if __name__ == "__main__": logger = logging.getLogger(name='leap') diff --git a/src/leap/bitmask/crypto/tests/fake_provider.py b/src/leap/bitmask/crypto/tests/fake_provider.py index 54af485d..b8cdbb12 100755 --- a/src/leap/bitmask/crypto/tests/fake_provider.py +++ b/src/leap/bitmask/crypto/tests/fake_provider.py @@ -280,7 +280,6 @@ class FakeSession(Resource): if HAMK is None: print '[server] verification failed!!!' raise Exception("Authentication failed!") - #import ipdb;ipdb.set_trace() assert svr.authenticated() print "***" diff --git a/src/leap/bitmask/crypto/tests/test_srpauth.py b/src/leap/bitmask/crypto/tests/test_srpauth.py index e63c1385..511a12ed 100644 --- a/src/leap/bitmask/crypto/tests/test_srpauth.py +++ b/src/leap/bitmask/crypto/tests/test_srpauth.py @@ -520,9 +520,9 @@ class SRPAuthTestCase(unittest.TestCase): m2 = self.auth_backend._extract_data(test_data) self.assertEqual(m2, test_m2) - self.assertEqual(self.auth_backend.get_uid(), test_uid) - self.assertEqual(self.auth_backend.get_uid(), - self.auth.get_uid()) + self.assertEqual(self.auth_backend.get_uuid(), test_uid) + self.assertEqual(self.auth_backend.get_uuid(), + self.auth.get_uuid()) self.assertEqual(self.auth_backend.get_token(), test_token) self.assertEqual(self.auth_backend.get_token(), self.auth.get_token()) @@ -691,7 +691,7 @@ class SRPAuthTestCase(unittest.TestCase): old_session = self.auth_backend._session self.auth_backend.logout() self.assertIsNone(self.auth_backend.get_session_id()) - self.assertIsNone(self.auth_backend.get_uid()) + self.assertIsNone(self.auth_backend.get_uuid()) self.assertNotEqual(old_session, self.auth_backend._session) d = threads.deferToThread(wrapper) diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py index 8f15719d..cbc8c3e3 100644 --- a/src/leap/bitmask/gui/advanced_key_management.py +++ b/src/leap/bitmask/gui/advanced_key_management.py @@ -48,6 +48,9 @@ class AdvancedKeyManagement(QtGui.QWidget): self.ui = Ui_AdvancedKeyManagement() self.ui.setupUi(self) + # XXX: Temporarily disable the key import. + self.ui.pbImportKeys.setVisible(False) + # if Soledad is not started yet if sameProxiedObjects(soledad, None): self.ui.gbMyKeyPair.setEnabled(False) @@ -57,12 +60,13 @@ class AdvancedKeyManagement(QtGui.QWidget): msg = msg.format(get_service_display_name(MX_SERVICE)) self.ui.lblStatus.setText(msg) return - else: - msg = self.tr( - "<span style='color:#ff0000;'>WARNING</span>:<br>" - "This is an experimental feature, you can lose access to " - "existing e-mails.") - self.ui.lblStatus.setText(msg) + # XXX: since import is disabled this is no longer a dangerous feature. + # else: + # msg = self.tr( + # "<span style='color:#ff0000;'>WARNING</span>:<br>" + # "This is an experimental feature, you can lose access to " + # "existing e-mails.") + # self.ui.lblStatus.setText(msg) self._keymanager = keymanager self._soledad = soledad diff --git a/src/leap/bitmask/gui/eip_preferenceswindow.py b/src/leap/bitmask/gui/eip_preferenceswindow.py index 504d1cf1..dcaa8b1e 100644 --- a/src/leap/bitmask/gui/eip_preferenceswindow.py +++ b/src/leap/bitmask/gui/eip_preferenceswindow.py @@ -22,7 +22,7 @@ import os import logging from functools import partial -from PySide import QtGui +from PySide import QtCore, QtGui from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig @@ -37,10 +37,12 @@ class EIPPreferencesWindow(QtGui.QDialog): """ Window that displays the EIP preferences. """ - def __init__(self, parent): + def __init__(self, parent, domain): """ :param parent: parent object of the EIPPreferencesWindow. - :parent type: QWidget + :type parent: QWidget + :param domain: the selected by default domain. + :type domain: unicode """ QtGui.QDialog.__init__(self, parent) self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic") @@ -59,7 +61,7 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.cbGateways.currentIndexChanged[unicode].connect( lambda x: self.ui.lblProvidersGatewayStatus.setVisible(False)) - self._add_configured_providers() + self._add_configured_providers(domain) def _set_providers_gateway_status(self, status, success=False, error=False): @@ -83,9 +85,12 @@ class EIPPreferencesWindow(QtGui.QDialog): self.ui.lblProvidersGatewayStatus.setVisible(True) self.ui.lblProvidersGatewayStatus.setText(status) - def _add_configured_providers(self): + def _add_configured_providers(self, domain=None): """ Add the client's configured providers to the providers combo boxes. + + :param domain: the domain to be selected by default. + :type domain: unicode """ self.ui.cbProvidersGateway.clear() providers = self._settings.get_configured_providers() @@ -100,6 +105,12 @@ class EIPPreferencesWindow(QtGui.QDialog): label = provider + self.tr(" (uninitialized)") self.ui.cbProvidersGateway.addItem(label, userData=provider) + # Select provider by name + if domain is not None: + provider_index = self.ui.cbProvidersGateway.findText( + domain, QtCore.Qt.MatchStartsWith) + self.ui.cbProvidersGateway.setCurrentIndex(provider_index) + def _save_selected_gateway(self, provider): """ SLOT diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py index 92bb623e..19942d9d 100644 --- a/src/leap/bitmask/gui/eip_status.py +++ b/src/leap/bitmask/gui/eip_status.py @@ -248,10 +248,10 @@ class EIPStatusWidget(QtGui.QWidget): 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') + #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 + # probably the best thing would be to make a conditional # transition there, but that's more involved. self.eip_button.hide() msg = self.tr("You must login to use {0}".format(self._service_name)) @@ -272,7 +272,7 @@ class EIPStatusWidget(QtGui.QWidget): Triggered after a successful login. Enables the start button. """ - logger.debug('Showing EIP start button') + #logger.debug('Showing EIP start button') self.eip_button.show() # Restore the eip action menu diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py index 6ef58558..9f396574 100644 --- a/src/leap/bitmask/gui/loggerwindow.py +++ b/src/leap/bitmask/gui/loggerwindow.py @@ -22,10 +22,13 @@ import logging import cgi from PySide import QtGui +from twisted.internet import threads from ui_loggerwindow import Ui_LoggerWindow +from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY from leap.bitmask.util.leap_log_handler import LeapLogHandler +from leap.bitmask.util.pastebin import PastebinAPI, PastebinError from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -42,6 +45,9 @@ class LoggerWindow(QtGui.QDialog): :param handler: Custom handler that supports history and signal. :type handler: LeapLogHandler. """ + from twisted.internet import reactor + self.reactor = reactor + QtGui.QDialog.__init__(self) leap_assert(handler, "We need a handler for the logger window") leap_assert_type(handler, LeapLogHandler) @@ -59,8 +65,10 @@ class LoggerWindow(QtGui.QDialog): self.ui.btnCritical.toggled.connect(self._load_history) self.ui.leFilterBy.textEdited.connect(self._filter_by) self.ui.cbCaseInsensitive.stateChanged.connect(self._load_history) + self.ui.btnPastebin.clicked.connect(self._pastebin_this) self._current_filter = "" + self._current_history = "" # Load logging history and connect logger with the widget self._logging_handler = handler @@ -116,8 +124,13 @@ class LoggerWindow(QtGui.QDialog): self._set_logs_to_display() self.ui.txtLogHistory.clear() history = self._logging_handler.log_history + current_history = [] for line in history: self._add_log_line(line) + message = line[LeapLogHandler.MESSAGE_KEY] + current_history.append(message) + + self._current_history = "\n".join(current_history) def _set_logs_to_display(self): """ @@ -164,3 +177,72 @@ class LoggerWindow(QtGui.QDialog): logger.error("Error saving log file: %r" % (e, )) else: logger.debug('Log not saved!') + + def _set_pastebin_sending(self, sending): + """ + Define the status of the pastebin button. + Change the text and enable/disable according to the current action. + + :param sending: if we are sending to pastebin or not. + :type sending: bool + """ + if sending: + self.ui.btnPastebin.setText(self.tr("Sending to pastebin...")) + self.ui.btnPastebin.setEnabled(False) + else: + self.ui.btnPastebin.setText(self.tr("Send to Pastebin.com")) + self.ui.btnPastebin.setEnabled(True) + + def _pastebin_this(self): + """ + Send the current log history to pastebin.com and gives the user a link + to see it. + """ + def do_pastebin(): + """ + Send content to pastebin and return the link. + """ + content = self._current_history + pb = PastebinAPI() + link = pb.paste(PASTEBIN_API_DEV_KEY, content, + paste_name="Bitmask log", + paste_expire_date='1W') + + # convert to 'raw' link + link = "http://pastebin.com/raw.php?i=" + link.split('/')[-1] + + return link + + def pastebin_ok(link): + """ + Callback handler for `do_pastebin`. + + :param link: the recently created pastebin link. + :type link: str + """ + msg = self.tr("Your pastebin link <a href='{0}'>{0}</a>") + msg = msg.format(link) + show_info = lambda: QtGui.QMessageBox.information( + self, self.tr("Pastebin OK"), msg) + self._set_pastebin_sending(False) + self.reactor.callLater(0, show_info) + + def pastebin_err(failure): + """ + Errback handler for `do_pastebin`. + + :param failure: the failure that triggered the errback. + :type failure: twisted.python.failure.Failure + """ + logger.error(repr(failure)) + msg = self.tr("Sending logs to Pastebin failed!") + show_err = lambda: QtGui.QMessageBox.critical( + self, self.tr("Pastebin Error"), msg) + self._set_pastebin_sending(False) + self.reactor.callLater(0, show_err) + failure.trap(PastebinError) + + self._set_pastebin_sending(True) + d = threads.deferToThread(do_pastebin) + d.addCallback(pastebin_ok) + d.addErrback(pastebin_err) diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index b21057f0..4a483c32 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -19,12 +19,13 @@ Login widget implementation """ import logging -import keyring - from PySide import QtCore, QtGui from ui_login import Ui_LoginWidget +from leap.bitmask.config import flags +from leap.bitmask.util import make_address from leap.bitmask.util.keyring_helpers import has_keyring +from leap.bitmask.util.keyring_helpers import get_keyring from leap.common.check import leap_assert_type logger = logging.getLogger(__name__) @@ -221,6 +222,15 @@ class LoginWidget(QtGui.QWidget): self._set_cancel(not enabled) + def set_logout_btn_enabled(self, enabled): + """ + Enables or disables the logout button. + + :param enabled: wether they should be enabled or not + :type enabled: bool + """ + self.ui.btnLogout.setEnabled(enabled) + def _set_cancel(self, enabled=False): """ Enables or disables the cancel action in the "log in" process. @@ -304,14 +314,15 @@ class LoginWidget(QtGui.QWidget): 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") + full_user_id = make_address(username, provider).encode("utf8") try: + keyring = get_keyring() keyring.set_password(self.KEYRING_KEY, - username_domain, + full_user_id, password.encode("utf8")) # Only save the username if it was saved correctly in # the keyring - self._settings.set_user(username_domain) + self._settings.set_user(full_user_id) except Exception as e: logger.exception("Problem saving data to keyring. %r" % (e,)) @@ -323,15 +334,19 @@ class LoginWidget(QtGui.QWidget): """ 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() + self.ui.lblUser.setText(make_address( + self.get_user(), self.get_selected_provider())) + + if flags.OFFLINE is False: + self.logged_in_signal.emit() def logged_out(self): """ Sets the widgets to the logged out state """ + # TODO consider "logging out offline" too... + # how that would be ??? + self.ui.login_widget.show() self.ui.logged_widget.hide() @@ -339,27 +354,11 @@ class LoginWidget(QtGui.QWidget): 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.setText(self.tr("Logging out...")) self.ui.btnLogout.setEnabled(False) def done_logout(self): @@ -396,6 +395,7 @@ class LoginWidget(QtGui.QWidget): saved_password = None try: + keyring = get_keyring() saved_password = keyring.get_password(self.KEYRING_KEY, saved_user .encode("utf8")) diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 3c933c9a..44a138e2 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -112,6 +112,10 @@ class MailStatusWidget(QtGui.QWidget): callback=self._mail_handle_imap_events, reqcbk=lambda req, resp: None) + register(signal=proto.SOLEDAD_INVALID_AUTH_TOKEN, + callback=self.set_soledad_invalid_auth_token, + reqcbk=lambda req, resp: None) + self._soledad_event.connect( self._mail_handle_soledad_events_slot) self._imap_event.connect( @@ -191,6 +195,17 @@ class MailStatusWidget(QtGui.QWidget): msg = self.tr("There was an unexpected problem with Soledad.") self._set_mail_status(msg, ready=-1) + def set_soledad_invalid_auth_token(self): + """ + SLOT + TRIGGER: + SoledadBootstrapper.soledad_invalid_token + + This method is called when the auth token is invalid + """ + msg = self.tr("Invalid auth token, try logging in again.") + self._set_mail_status(msg, ready=-1) + def _set_mail_status(self, status, ready=0): """ Sets the Mail status in the label and in the tray icon. @@ -213,7 +228,7 @@ class MailStatusWidget(QtGui.QWidget): self._service_name)) elif ready == 1: icon = self.CONNECTING_ICON - self._mx_status = self.tr('Starting..') + self._mx_status = self.tr('Starting…') tray_status = self.tr('Mail is starting') elif ready >= 2: icon = self.CONNECTED_ICON @@ -362,10 +377,19 @@ class MailStatusWidget(QtGui.QWidget): ext_status = None if req.event == proto.IMAP_UNREAD_MAIL: + # By now, the semantics of the UNREAD_MAIL event are + # limited to mails with the Unread flag *in the Inbox". + # We could make this configurable to include all unread mail + # or all unread mail in subscribed folders. if self._started: - if req.content != "0": - self._set_mail_status(self.tr("%s Unread Emails") % - (req.content,), ready=2) + count = req.content + if count != "0": + status = self.tr("{0} Unread Emails " + "in your Inbox").format(count) + if count == "1": + status = self.tr("1 Unread Email in your Inbox") + + self._set_mail_status(status, ready=2) else: self._set_mail_status("", ready=2) elif req.event == proto.IMAP_SERVICE_STARTED: @@ -375,7 +399,7 @@ class MailStatusWidget(QtGui.QWidget): def about_to_start(self): """ - Displays the correct UI for the point where mail components + Display 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..."), @@ -383,7 +407,7 @@ class MailStatusWidget(QtGui.QWidget): def set_disabled(self): """ - Displays the correct UI for disabled mail. + Display the correct UI for disabled mail. """ self._set_mail_status(self.tr("Disabled"), -1) @@ -394,7 +418,7 @@ class MailStatusWidget(QtGui.QWidget): @QtCore.Slot() def mail_state_disconnected(self): """ - Displays the correct UI for the disconnected state. + Display the correct UI for the disconnected state. """ # XXX this should handle the disabled state better. self._started = False @@ -406,7 +430,7 @@ class MailStatusWidget(QtGui.QWidget): @QtCore.Slot() def mail_state_connecting(self): """ - Displays the correct UI for the connecting state. + Display the correct UI for the connecting state. """ self._disabled = False self._started = True @@ -415,23 +439,32 @@ class MailStatusWidget(QtGui.QWidget): @QtCore.Slot() def mail_state_disconnecting(self): """ - Displays the correct UI for the connecting state. + Display the correct UI for the connecting state. """ self._set_mail_status(self.tr("Disconnecting..."), 1) @QtCore.Slot() def mail_state_connected(self): """ - Displays the correct UI for the connected state. + Display the correct UI for the connected state. """ self._set_mail_status(self.tr("ON"), 2) @QtCore.Slot() def mail_state_disabled(self): """ - Displays the correct UI for the disabled state. + Display the correct UI for the disabled state. """ self._disabled = True status = self.tr("You must be logged in to use {0}.").format( self._service_name) self._set_mail_status(status, -1) + + @QtCore.Slot() + def soledad_invalid_auth_token(self): + """ + Display the correct UI for the invalid token state + """ + self._disabled = True + status = self.tr("Invalid auth token, try logging in again.") + self._set_mail_status(status, -1) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 96aa8074..5abfaa67 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # mainwindow.py -# Copyright (C) 2013 LEAP +# Copyright (C) 2013, 2014 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 @@ -18,16 +18,25 @@ Main window for Bitmask. """ import logging +import socket + +from threading import Condition +from datetime import datetime from PySide import QtCore, QtGui -from functools import partial -from twisted.internet import threads from zope.proxy import ProxyBase, setProxiedObject +from twisted.internet import reactor, threads +from twisted.internet.defer import CancelledError from leap.bitmask import __version__ as VERSION +from leap.bitmask import __version_hash__ as VERSION_HASH +from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig + +from leap.bitmask.crypto import srpauth from leap.bitmask.crypto.srpauth import SRPAuth + from leap.bitmask.gui.loggerwindow import LoggerWindow from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement from leap.bitmask.gui.login import LoginWidget @@ -40,7 +49,7 @@ from leap.bitmask.gui.wizard import Wizard from leap.bitmask.gui.systray import SysTray from leap.bitmask import provider -from leap.bitmask.platform_init import IS_WIN, IS_MAC +from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX from leap.bitmask.platform_init.initializers import init_platform from leap.bitmask import backend @@ -67,6 +76,7 @@ from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded from leap.bitmask.services.soledad.soledadbootstrapper import \ SoledadBootstrapper +from leap.bitmask.util import make_address from leap.bitmask.util.keyring_helpers import has_keyring from leap.bitmask.util.leap_log_handler import LeapLogHandler @@ -78,6 +88,8 @@ from leap.common.check import leap_assert from leap.common.events import register from leap.common.events import events_pb2 as proto +from leap.mail.imap.service.imap import IMAP_PORT + from ui_mainwindow import Ui_MainWindow logger = logging.getLogger(__name__) @@ -94,6 +106,7 @@ class MainWindow(QtGui.QMainWindow): # Signals eip_needs_login = QtCore.Signal([]) + offline_mode_bypass_login = QtCore.Signal([]) new_updates = QtCore.Signal(object) raise_window = QtCore.Signal([]) soledad_ready = QtCore.Signal([]) @@ -103,6 +116,12 @@ class MainWindow(QtGui.QMainWindow): # We use this flag to detect abnormal terminations user_stopped_eip = False + # We give EIP some time to come up before starting soledad anyway + EIP_TIMEOUT = 60000 # in milliseconds + + # We give each service some time to come to a halt before forcing quit + SERVICE_STOP_TIMEOUT = 20 + def __init__(self, quit_callback, openvpn_verb=1, bypass_checks=False): @@ -133,12 +152,12 @@ class MainWindow(QtGui.QMainWindow): # end register leap events #################################### self._quit_callback = quit_callback - self._updates_content = "" + # setup UI self.ui = Ui_MainWindow() self.ui.setupUi(self) - + self.menuBar().setNativeMenuBar(not IS_LINUX) self._backend = backend.Backend(bypass_checks) self._backend.start() @@ -177,6 +196,11 @@ class MainWindow(QtGui.QMainWindow): self._eip_status.eip_connection_connected.connect( self._on_eip_connected) + self._eip_status.eip_connection_connected.connect( + self._maybe_run_soledad_setup_checks) + self.offline_mode_bypass_login.connect( + self._maybe_run_soledad_setup_checks) + self.eip_needs_login.connect( self._eip_status.disable_eip_start) self.eip_needs_login.connect( @@ -193,10 +217,12 @@ class MainWindow(QtGui.QMainWindow): self._eip_config = eipconfig.EIPConfig() self._already_started_eip = False + self._already_started_soledad = False # This is created once we have a valid provider config self._srp_auth = None self._logged_user = None + self._logged_in_offline = False self._backend_connect() @@ -232,8 +258,12 @@ class MainWindow(QtGui.QMainWindow): self._soledad_intermediate_stage) self._soledad_bootstrapper.gen_key.connect( self._soledad_bootstrapped_stage) + self._soledad_bootstrapper.local_only_ready.connect( + self._soledad_bootstrapped_stage) self._soledad_bootstrapper.soledad_timeout.connect( self._retry_soledad_connection) + self._soledad_bootstrapper.soledad_invalid_auth_token.connect( + self._mail_status.set_soledad_invalid_auth_token) self._soledad_bootstrapper.soledad_failed.connect( self._mail_status.set_soledad_failed) @@ -244,6 +274,7 @@ class MainWindow(QtGui.QMainWindow): 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_help.triggered.connect(self._help) self.ui.action_create_new_account.triggered.connect( self._launch_wizard) @@ -277,8 +308,9 @@ class MainWindow(QtGui.QMainWindow): self._enabled_services = [] - self._center_window() + # last minute UI manipulations + self._center_window() self.ui.lblNewUpdates.setVisible(False) self.ui.btnMore.setVisible(False) ######################################### @@ -287,6 +319,8 @@ class MainWindow(QtGui.QMainWindow): self.ui.btnMore.resize(0, 0) ######################################### self.ui.btnMore.clicked.connect(self._updates_details) + if flags.OFFLINE is True: + self._set_label_offline() # Services signals/slots connection self.new_updates.connect(self._react_to_new_updates) @@ -316,7 +350,7 @@ class MainWindow(QtGui.QMainWindow): self._keymanager = ProxyBase(None) self._login_defer = None - self._download_provider_defer = None + self._soledad_defer = None self._mail_conductor = mail_conductor.MailConductor( self._soledad, self._keymanager) @@ -354,23 +388,22 @@ class MainWindow(QtGui.QMainWindow): """ Helper to connect to backend signals """ - self._backend.signaler.prov_name_resolution.connect( - self._intermediate_stage) - self._backend.signaler.prov_https_connection.connect( - self._intermediate_stage) - self._backend.signaler.prov_download_ca_cert.connect( - self._intermediate_stage) + sig = self._backend.signaler + sig.prov_name_resolution.connect(self._intermediate_stage) + sig.prov_https_connection.connect(self._intermediate_stage) + sig.prov_download_ca_cert.connect(self._intermediate_stage) - self._backend.signaler.prov_download_provider_info.connect( - self._load_provider_config) - self._backend.signaler.prov_check_api_certificate.connect( - self._provider_config_loaded) + sig.prov_download_provider_info.connect(self._load_provider_config) + sig.prov_check_api_certificate.connect(self._provider_config_loaded) # Only used at login, no need to disconnect this like we do # with the other - self._backend.signaler.prov_problem_with_provider.connect( - partial(self._login_widget.set_status, - self.tr("Unable to login: Problem with provider"))) + sig.prov_problem_with_provider.connect(self._login_problem_provider) + + sig.prov_unsupported_client.connect(self._needs_update) + sig.prov_unsupported_api.connect(self._incompatible_api) + + sig.prov_cancelled_setup.connect(self._set_login_cancelled) def _backend_disconnect(self): """ @@ -379,17 +412,13 @@ class MainWindow(QtGui.QMainWindow): Some signals are emitted from the wizard, and we want to ignore those. """ - self._backend.signaler.prov_name_resolution.disconnect( - self._intermediate_stage) - self._backend.signaler.prov_https_connection.disconnect( - self._intermediate_stage) - self._backend.signaler.prov_download_ca_cert.disconnect( - self._intermediate_stage) + sig = self._backend.signaler + sig.prov_name_resolution.disconnect(self._intermediate_stage) + sig.prov_https_connection.disconnect(self._intermediate_stage) + sig.prov_download_ca_cert.disconnect(self._intermediate_stage) - self._backend.signaler.prov_download_provider_info.disconnect( - self._load_provider_config) - self._backend.signaler.prov_check_api_certificate.disconnect( - self._provider_config_loaded) + sig.prov_download_provider_info.disconnect(self._load_provider_config) + sig.prov_check_api_certificate.disconnect(self._provider_config_loaded) def _rejected_wizard(self): """ @@ -412,7 +441,8 @@ class MainWindow(QtGui.QMainWindow): # setup but does not register self._wizard = None self._backend_connect() - self._finish_init() + if self._wizard_firstrun: + self._finish_init() def _launch_wizard(self): """ @@ -431,7 +461,7 @@ class MainWindow(QtGui.QMainWindow): self._wizard = Wizard(backend=self._backend, bypass_checks=self._bypass_checks) self._wizard.accepted.connect(self._finish_init) - self._wizard.rejected.connect(self._wizard.close) + self._wizard.rejected.connect(self._rejected_wizard) self.setVisible(False) # Do NOT use exec_, it will use a child event loop! @@ -564,7 +594,8 @@ class MainWindow(QtGui.QMainWindow): Displays the EIP preferences window. """ - EIPPreferencesWindow(self).show() + domain = self._login_widget.get_selected_provider() + EIPPreferencesWindow(self, domain).show() # # updates @@ -705,6 +736,19 @@ class MainWindow(QtGui.QMainWindow): self.ui.eipWidget.setVisible(EIP_SERVICE in services) self.ui.mailWidget.setVisible(MX_SERVICE in services) + def _set_label_offline(self): + """ + Set the login label to reflect offline status. + """ + if self._logged_in_offline: + provider = "" + else: + provider = self.ui.lblLoginProvider.text() + + self.ui.lblLoginProvider.setText( + provider + + self.tr(" (offline mode)")) + # # systray # @@ -827,10 +871,13 @@ class MainWindow(QtGui.QMainWindow): Display the About Bitmask dialog """ + today = datetime.now().date() + greet = ("Happy New 1984!... or not ;)<br><br>" + if today.month == 1 and today.day < 15 else "") QtGui.QMessageBox.about( self, self.tr("About Bitmask - %s") % (VERSION,), - self.tr("Version: <b>%s</b><br>" - "<br>" + self.tr("Version: <b>%s</b> (%s)<br>" + "<br>%s" "Bitmask is the Desktop client application for " "the LEAP platform, supporting encrypted internet " "proxy, secure email, and secure chat (coming soon).<br>" @@ -842,7 +889,58 @@ class MainWindow(QtGui.QMainWindow): "and widely available. <br>" "<br>" "<a href='https://leap.se'>More about LEAP" - "</a>") % (VERSION,)) + "</a>") % (VERSION, VERSION_HASH[:10], greet)) + + def _help(self): + """ + SLOT + TRIGGERS: self.ui.action_help.triggered + + Display the Bitmask help dialog. + """ + # TODO: don't hardcode! + smtp_port = 2013 + + url = ("<a href='https://addons.mozilla.org/es/thunderbird/" + "addon/bitmask/'>bitmask addon</a>") + + msg = self.tr( + "<strong>Instructions to use mail:</strong><br>" + "If you use Thunderbird you can use the Bitmask extension helper. " + "Search for 'Bitmask' in the add-on manager or download it " + "from: {0}.<br><br>" + "You can configure Bitmask manually with these options:<br>" + "<em>" + " Incoming -> IMAP, port: {1}<br>" + " Outgoing -> SMTP, port: {2}<br>" + " Username -> your bitmask username.<br>" + " Password -> does not matter, use any text. " + " Just don't leave it empty and don't use your account's password." + "</em>").format(url, IMAP_PORT, smtp_port) + QtGui.QMessageBox.about(self, self.tr("Bitmask Help"), msg) + + def _needs_update(self): + """ + Display a warning dialog to inform the user that the app needs update. + """ + url = "https://dl.bitmask.net/" + msg = self.tr( + "The current client version is not supported " + "by this provider.<br>" + "Please update to latest version.<br><br>" + "You can get the latest version from " + "<a href='{0}'>{1}</a>").format(url, url) + QtGui.QMessageBox.warning(self, self.tr("Update Needed"), msg) + + def _incompatible_api(self): + """ + Display a warning dialog to inform the user that the provider has an + incompatible API. + """ + msg = self.tr( + "This provider is not compatible with the client.<br><br>" + "Error: API version incompatible.") + QtGui.QMessageBox.warning(self, self.tr("Incompatible Provider"), msg) def changeEvent(self, e): """ @@ -891,7 +989,6 @@ class MainWindow(QtGui.QMainWindow): """ # XXX should rename this provider, name clash. provider = self._login_widget.get_selected_provider() - self._backend.setup_provider(provider) def _load_provider_config(self, data): @@ -904,18 +1001,24 @@ class MainWindow(QtGui.QMainWindow): part of the bootstrapping sequence :param data: result from the last stage of the - run_provider_select_checks + run_provider_select_checks :type data: dict """ if data[self._backend.PASSED_KEY]: selected_provider = self._login_widget.get_selected_provider() self._backend.provider_bootstrap(selected_provider) else: - self._login_widget.set_status( - self.tr("Unable to login: Problem with provider")) logger.error(data[self._backend.ERROR_KEY]) self._login_widget.set_enabled(True) + def _login_problem_provider(self): + """ + Warns the user about a problem with the provider during login. + """ + self._login_widget.set_status( + self.tr("Unable to login: Problem with provider")) + self._login_widget.set_enabled(True) + def _login(self): """ SLOT @@ -927,10 +1030,57 @@ class MainWindow(QtGui.QMainWindow): start the SRP authentication, and as the last step bootstrapping the EIP service """ - leap_assert(self._provider_config, "We need a provider config") + # TODO most of this could ve handled by the login widget, + # but we'd have to move lblLoginProvider into the widget itself, + # instead of having it as a top-level attribute. + if flags.OFFLINE is True: + logger.debug("OFFLINE mode! bypassing remote login") + # TODO reminder, we're not handling logout for offline + # mode. + self._login_widget.logged_in() + self._logged_in_offline = True + self._set_label_offline() + self.offline_mode_bypass_login.emit() + else: + leap_assert(self._provider_config, "We need a provider config") + if self._login_widget.start_login(): + self._download_provider_config() + + def _login_errback(self, failure): + """ + Error handler for the srpauth.authenticate method. + + :param failure: failure object that Twisted generates + :type failure: twisted.python.failure.Failure + """ + # NOTE: this behavior needs to be managed through the signaler, + # as we are doing with the prov_cancelled_setup signal. + # After we move srpauth to the backend, we need to update this. + logger.error("Error logging in, {0!r}".format(failure)) + + if failure.check(CancelledError): + logger.debug("Defer cancelled.") + failure.trap(Exception) + self._set_login_cancelled() + return + elif failure.check(srpauth.SRPAuthBadUserOrPassword): + msg = self.tr("Invalid username or password.") + elif failure.check(srpauth.SRPAuthBadStatusCode, + srpauth.SRPAuthenticationError, + srpauth.SRPAuthVerificationFailed, + srpauth.SRPAuthNoSessionId, + srpauth.SRPAuthNoSalt, srpauth.SRPAuthNoB, + srpauth.SRPAuthBadDataFromServer, + srpauth.SRPAuthJSONDecodeError): + msg = self.tr("There was a server problem with authentication.") + elif failure.check(srpauth.SRPAuthConnectionError): + msg = self.tr("Could not establish a connection.") + else: + # this shouldn't happen, but just in case. + msg = self.tr("Unknown error: {0!r}".format(failure.value)) - if self._login_widget.start_login(): - self._download_provider_config() + self._login_widget.set_status(msg) + self._login_widget.set_enabled(True) def _cancel_login(self): """ @@ -941,16 +1091,36 @@ class MainWindow(QtGui.QMainWindow): Stops the login sequence. """ logger.debug("Cancelling log in.") + self._cancel_ongoing_defers() - if self._download_provider_defer: - logger.debug("Cancelling download provider defer.") - self._download_provider_defer.cancel() + def _cancel_ongoing_defers(self): + """ + Cancel the running defers to avoid app blocking. + """ + self._backend.cancel_setup_provider() - if self._login_defer: + if self._login_defer is not None: logger.debug("Cancelling login defer.") self._login_defer.cancel() + self._login_defer = None + + if self._soledad_defer is not None: + logger.debug("Cancelling soledad defer.") + self._soledad_defer.cancel() + self._soledad_defer = None + + def _set_login_cancelled(self): + """ + SLOT + TRIGGERS: + Signaler.prov_cancelled_setup fired by + self._backend.cancel_setup_provider() + This method re-enables the login widget and display a message for + the cancelled operation. + """ self._login_widget.set_status(self.tr("Log in cancelled by the user.")) + self._login_widget.set_enabled(True) def _provider_config_loaded(self, data): """ @@ -972,18 +1142,18 @@ class MainWindow(QtGui.QMainWindow): self._srp_auth = SRPAuth(self._provider_config) self._srp_auth.authentication_finished.connect( self._authentication_finished) - self._srp_auth.logout_finished.connect( - self._done_logging_out) + self._srp_auth.logout_ok.connect(self._logout_ok) + self._srp_auth.logout_error.connect(self._logout_error) - # TODO Add errback! self._login_defer = self._srp_auth.authenticate(username, password) + self._login_defer.addErrback(self._login_errback) else: self._login_widget.set_status( "Unable to login: Problem with provider") logger.error(data[self._backend.ERROR_KEY]) self._login_widget.set_enabled(True) - def _authentication_finished(self, ok, message): + def _authentication_finished(self): """ SLOT TRIGGER: self._srp_auth.authentication_finished @@ -991,22 +1161,23 @@ class MainWindow(QtGui.QMainWindow): Once the user is properly authenticated, try starting the EIP service """ - # In general we want to "filter" likely complicated error - # messages, but in this case, the messages make more sense as - # they come. Since they are "Unknown user" or "Unknown - # password" - self._login_widget.set_status(message, error=not ok) - - if ok: - self._logged_user = self._login_widget.get_user() - user = self._logged_user - domain = self._provider_config.get_domain() - userid = "%s@%s" % (user, domain) - self._mail_conductor.userid = userid - self._login_defer = None - self._start_eip_bootstrap() - else: - self._login_widget.set_enabled(True) + self._login_widget.set_status(self.tr("Succeeded"), error=False) + + self._logged_user = self._login_widget.get_user() + user = self._logged_user + domain = self._provider_config.get_domain() + full_user_id = make_address(user, domain) + self._mail_conductor.userid = full_user_id + self._login_defer = None + self._start_eip_bootstrap() + + # if soledad/mail is enabled: + if MX_SERVICE in self._enabled_services: + btn_enabled = self._login_widget.set_logout_btn_enabled + btn_enabled(False) + self.soledad_ready.connect(lambda: btn_enabled(True)) + self._soledad_bootstrapper.soledad_failed.connect( + lambda: btn_enabled(True)) def _start_eip_bootstrap(self): """ @@ -1015,28 +1186,83 @@ class MainWindow(QtGui.QMainWindow): """ self._login_widget.logged_in() - self.ui.lblLoginProvider.setText(self._provider_config.get_domain()) + provider = self._provider_config.get_domain() + self.ui.lblLoginProvider.setText(provider) 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. - if self._provider_config.provides_mx() and \ - self._enabled_services.count(MX_SERVICE) > 0: + if self._provides_mx_and_enabled(): 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() - # XXX the config should be downloaded from the start_eip - # method. - self._download_eip_config() + self._maybe_start_eip() + + def _provides_mx_and_enabled(self): + """ + Defines if the current provider provides mx and if we have it enabled. + + :returns: True if provides and is enabled, False otherwise + :rtype: bool + """ + provider_config = self._get_best_provider_config() + return (provider_config.provides_mx() and + MX_SERVICE in self._enabled_services) + + def _provides_eip_and_enabled(self): + """ + Defines if the current provider provides eip and if we have it enabled. + + :returns: True if provides and is enabled, False otherwise + :rtype: bool + """ + provider_config = self._get_best_provider_config() + return (provider_config.provides_eip() and + EIP_SERVICE in self._enabled_services) + + def _maybe_run_soledad_setup_checks(self): + """ + Conditionally start Soledad. + """ + # TODO split. + if self._already_started_soledad is True: + return + + if not self._provides_mx_and_enabled(): + return + + username = self._login_widget.get_user() + password = unicode(self._login_widget.get_password()) + provider_domain = self._login_widget.get_selected_provider() + + sb = self._soledad_bootstrapper + if flags.OFFLINE is True: + provider_domain = self._login_widget.get_selected_provider() + sb._password = password + + self._provisional_provider_config.load( + provider.get_provider_path(provider_domain)) + + full_user_id = make_address(username, provider_domain) + uuid = self._settings.get_uuid(full_user_id) + self._mail_conductor.userid = full_user_id + + if uuid is None: + # We don't need more visibility at the moment, + # this is mostly for internal use/debug for now. + logger.warning("Sorry! Log-in at least one time.") + return + fun = sb.load_offline_soledad + fun(full_user_id, password, uuid) + else: + provider_config = self._provider_config + + if self._logged_user is not None: + self._soledad_defer = sb.run_soledad_setup_checks( + provider_config, username, password, + download_if_needed=True) ################################################################### # Service control methods: soledad @@ -1069,8 +1295,9 @@ class MainWindow(QtGui.QMainWindow): logger.debug("Retrying soledad connection.") if self._soledad_bootstrapper.should_retry_initialization(): self._soledad_bootstrapper.increment_retries_count() - threads.deferToThread( - self._soledad_bootstrapper.load_and_sync_soledad) + # XXX should cancel the existing socket --- this + # is avoiding a clean termination. + self._maybe_run_soledad_setup_checks() else: logger.warning("Max number of soledad initialization " "retries reached.") @@ -1080,6 +1307,7 @@ class MainWindow(QtGui.QMainWindow): SLOT TRIGGERS: self._soledad_bootstrapper.gen_key + self._soledad_bootstrapper.local_only_ready If there was a problem, displays it, otherwise it does nothing. This is used for intermediate bootstrapping stages, in case @@ -1106,6 +1334,7 @@ class MainWindow(QtGui.QMainWindow): # Ok, now soledad is ready, so we can allow other things that # depend on soledad to start. + self._soledad_defer = None # this will trigger start_imap_service # and start_smtp_boostrapping @@ -1121,10 +1350,13 @@ class MainWindow(QtGui.QMainWindow): TRIGGERS: self.soledad_ready """ + if flags.OFFLINE is True: + logger.debug("not starting smtp in offline mode") + return + # TODO for simmetry, this should be called start_smtp_service # (and delegate all the checks to the conductor) - if self._provider_config.provides_mx() and \ - self._enabled_services.count(MX_SERVICE) > 0: + if self._provides_mx_and_enabled(): self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks( self._provider_config, self._mail_conductor.smtp_config, @@ -1152,9 +1384,22 @@ class MainWindow(QtGui.QMainWindow): TRIGGERS: self.soledad_ready """ - if self._provider_config.provides_mx() and \ - self._enabled_services.count(MX_SERVICE) > 0: - self._mail_conductor.start_imap_service() + # TODO in the OFFLINE mode we should also modify the rules + # in the mail state machine so it shows that imap is active + # (but not smtp since it's not yet ready for offline use) + start_fun = self._mail_conductor.start_imap_service + if flags.OFFLINE is True: + provider_domain = self._login_widget.get_selected_provider() + self._provider_config.load( + provider.get_provider_path(provider_domain)) + provides_mx = self._provider_config.provides_mx() + + if flags.OFFLINE is True and provides_mx: + start_fun() + return + + if self._provides_mx_and_enabled(): + start_fun() def _on_mail_client_logged_in(self, req): """ @@ -1179,8 +1424,13 @@ class MainWindow(QtGui.QMainWindow): TRIGGERS: self.logout """ + cv = Condition() + cv.acquire() # TODO call stop_mail_service - self._mail_conductor.stop_imap_service() + threads.deferToThread(self._mail_conductor.stop_imap_service, cv) + # and wait for it to be stopped + logger.debug('Waiting for imap service to stop.') + cv.wait(self.SERVICE_STOP_TIMEOUT) # end service control methods (imap) @@ -1230,6 +1480,55 @@ class MainWindow(QtGui.QMainWindow): """ self._eip_connection.qtsigs.connected_signal.emit() + # check for connectivity + provider_config = self._get_best_provider_config() + domain = provider_config.get_domain() + self._check_name_resolution(domain) + + def _check_name_resolution(self, domain): + """ + Check if we can resolve the given domain name. + + :param domain: the domain to check. + :type domain: str + """ + def do_check(): + """ + Try to resolve the domain name. + """ + socket.gethostbyname(domain.encode('idna')) + + def check_err(failure): + """ + Errback handler for `do_check`. + + :param failure: the failure that triggered the errback. + :type failure: twisted.python.failure.Failure + """ + logger.error(repr(failure)) + logger.error("Can't resolve hostname.") + + msg = self.tr( + "The server at {0} can't be found, because the DNS lookup " + "failed. DNS is the network service that translates a " + "website's name to its Internet address. Either your computer " + "is having trouble connecting to the network, or you are " + "missing some helper files that are needed to securely use " + "DNS while {1} is active. To install these helper files, quit " + "this application and start it again." + ).format(domain, self._eip_name) + + show_err = lambda: QtGui.QMessageBox.critical( + self, self.tr("Connection Error"), msg) + reactor.callLater(0, show_err) + + # python 2.7.4 raises socket.error + # python 2.7.5 raises socket.gaierror + failure.trap(socket.gaierror, socket.error) + + d = threads.deferToThread(do_check) + d.addErrback(check_err) + def _try_autostart_eip(self): """ Tries to autostart EIP @@ -1250,7 +1549,7 @@ class MainWindow(QtGui.QMainWindow): # it adds some delay. # Maybe if it's the first run in a session, # or we can try only if it fails. - self._download_eip_config() + self._maybe_start_eip() else: # XXX: Display a proper message to the user self.eip_needs_login.emit() @@ -1384,8 +1683,9 @@ class MainWindow(QtGui.QMainWindow): if self._logged_user: self._eip_status.set_provider( - "%s@%s" % (self._logged_user, - self._get_best_provider_config().get_domain())) + make_address( + self._logged_user, + self._get_best_provider_config().get_domain())) self._eip_status.eip_stopped() @QtCore.Slot() @@ -1482,18 +1782,16 @@ class MainWindow(QtGui.QMainWindow): # eip boostrapping, config etc... - def _download_eip_config(self): + def _maybe_start_eip(self): """ - Starts the EIP bootstrapping sequence + Start the EIP bootstrapping sequence if the client is configured to + do so. """ leap_assert(self._eip_bootstrapper, "We need an eip bootstrapper!") provider_config = self._get_best_provider_config() - if provider_config.provides_eip() and \ - self._enabled_services.count(EIP_SERVICE) > 0 and \ - not self._already_started_eip: - + if self._provides_eip_and_enabled() and not self._already_started_eip: # XXX this should be handled by the state machine. self._eip_status.set_eip_status( self.tr("Starting...")) @@ -1501,14 +1799,22 @@ class MainWindow(QtGui.QMainWindow): provider_config, download_if_needed=True) self._already_started_eip = True - elif not self._already_started_eip: - if self._enabled_services.count(EIP_SERVICE) > 0: - self._eip_status.set_eip_status( - self.tr("Not supported"), - error=True) - else: - self._eip_status.disable_eip_start() - self._eip_status.set_eip_status(self.tr("Disabled")) + # we want to start soledad anyway after a certain timeout if eip + # fails to come up + QtCore.QTimer.singleShot( + self.EIP_TIMEOUT, + self._maybe_run_soledad_setup_checks) + else: + if not self._already_started_eip: + if EIP_SERVICE in self._enabled_services: + self._eip_status.set_eip_status( + self.tr("Not supported"), + error=True) + else: + self._eip_status.disable_eip_start() + self._eip_status.set_eip_status(self.tr("Disabled")) + # eip will not start, so we start soledad anyway + self._maybe_run_soledad_setup_checks() def _finish_eip_bootstrap(self, data): """ @@ -1595,20 +1901,35 @@ class MainWindow(QtGui.QMainWindow): Starts the logout sequence """ - self._soledad_bootstrapper.cancel_bootstrap() setProxiedObject(self._soledad, None) + self._cancel_ongoing_defers() + + # reset soledad status flag + self._already_started_soledad = False + # 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): - # TODO missing params in docstring + def _logout_error(self): """ SLOT - TRIGGER: self._srp_auth.logout_finished + TRIGGER: self._srp_auth.logout_error + + Inform the user about a logout error. + """ + self._login_widget.done_logout() + self.ui.lblLoginProvider.setText(self.tr("Login")) + self._login_widget.set_status( + self.tr("Something went wrong with the logout.")) + + def _logout_ok(self): + """ + SLOT + TRIGGER: self._srp_auth.logout_ok Switches the stackedWidget back to the login stage after logging out @@ -1616,15 +1937,9 @@ class MainWindow(QtGui.QMainWindow): self._login_widget.done_logout() self.ui.lblLoginProvider.setText(self.tr("Login")) - if ok: - self._logged_user = None - self._login_widget.logged_out() - self._mail_status.mail_state_disabled() - - else: - self._login_widget.set_login_status( - self.tr("Something went wrong with the logout."), - error=True) + self._logged_user = None + self._login_widget.logged_out() + self._mail_status.mail_state_disabled() def _intermediate_stage(self, data): # TODO this method name is confusing as hell. @@ -1642,9 +1957,9 @@ class MainWindow(QtGui.QMainWindow): """ passed = data[self._backend.PASSED_KEY] if not passed: + msg = self.tr("Unable to connect: Problem with provider") + self._login_widget.set_status(msg) self._login_widget.set_enabled(True) - self._login_widget.set_status( - self.tr("Unable to connect: Problem with provider")) logger.error(data[self._backend.ERROR_KEY]) # @@ -1695,7 +2010,7 @@ class MainWindow(QtGui.QMainWindow): """ logger.debug('About to quit, doing cleanup...') - self._mail_conductor.stop_imap_service() + self._stop_imap_service() if self._srp_auth is not None: if self._srp_auth.get_session_id() is not None or \ @@ -1712,13 +2027,7 @@ class MainWindow(QtGui.QMainWindow): logger.debug('Terminating vpn') self._vpn.terminate(shutdown=True) - if self._login_defer: - logger.debug("Cancelling login defer.") - self._login_defer.cancel() - - if self._download_provider_defer: - logger.debug("Cancelling download provider defer.") - self._download_provider_defer.cancel() + self._cancel_ongoing_defers() # TODO missing any more cancels? @@ -1737,7 +2046,6 @@ class MainWindow(QtGui.QMainWindow): self._backend.stop() self._cleanup_and_quit() - self._really_quit = True if self._wizard: diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py index 517a90c4..b2cc2236 100644 --- a/src/leap/bitmask/gui/preferenceswindow.py +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -18,7 +18,6 @@ """ Preferences window """ -import os import logging from functools import partial @@ -26,6 +25,7 @@ from functools import partial from PySide import QtCore, QtGui from zope.proxy import sameProxiedObjects +from leap.bitmask.provider import get_provider_path from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.gui.ui_preferences import Ui_Preferences from leap.soledad.client import NoStorageSecret @@ -383,10 +383,7 @@ class PreferencesWindow(QtGui.QDialog): :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): + if not provider_config.load(get_provider_path(domain)): provider_config = None return provider_config diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py index e11af7bd..1e876c57 100644 --- a/src/leap/bitmask/gui/twisted_main.py +++ b/src/leap/bitmask/gui/twisted_main.py @@ -27,24 +27,6 @@ from twisted.internet import error logger = logging.getLogger(__name__) -def start(app): - """ - Start the mainloop. - - :param app: the main qt QApplication instance. - :type app: QtCore.QApplication - """ - from twisted.internet import reactor - logger.debug('starting twisted reactor') - - # this seems to be troublesome under some - # unidentified settings. - #reactor.run() - - reactor.runReturn() - app.exec_() - - def quit(app): """ Stop the mainloop. diff --git a/src/leap/bitmask/gui/ui/loggerwindow.ui b/src/leap/bitmask/gui/ui/loggerwindow.ui index 3de786f7..b19ed91a 100644 --- a/src/leap/bitmask/gui/ui/loggerwindow.ui +++ b/src/leap/bitmask/gui/ui/loggerwindow.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>648</width> - <height>469</height> + <width>769</width> + <height>464</height> </rect> </property> <property name="windowTitle"> @@ -154,6 +154,17 @@ </property> </widget> </item> + <item> + <widget class="QPushButton" name="btnPastebin"> + <property name="text"> + <string>Send to Pastebin.com</string> + </property> + <property name="icon"> + <iconset resource="../../../../../data/resources/loggerwindow.qrc"> + <normaloff>:/images/pastebin.png</normaloff>:/images/pastebin.png</iconset> + </property> + </widget> + </item> </layout> </item> </layout> diff --git a/src/leap/bitmask/gui/ui/login.ui b/src/leap/bitmask/gui/ui/login.ui index 7e8f9daf..f5725d5a 100644 --- a/src/leap/bitmask/gui/ui/login.ui +++ b/src/leap/bitmask/gui/ui/login.ui @@ -217,26 +217,6 @@ <property name="bottomMargin"> <number>0</number> </property> - <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="1" column="0"> - <widget class="QPushButton" name="btnLogout"> - <property name="text"> - <string>Logout</string> - </property> - </widget> - </item> <item row="0" column="0" colspan="2"> <widget class="QLabel" name="lblUser"> <property name="font"> @@ -251,17 +231,26 @@ </property> </widget> </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;</string> - </property> + <item row="1" column="0"> + <widget class="QPushButton" name="btnLogout"> <property name="text"> - <string/> + <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> </layout> </widget> </item> diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index ce05f8f3..d755115a 100644 --- a/src/leap/bitmask/gui/ui/mainwindow.ui +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -75,7 +75,7 @@ <x>0</x> <y>0</y> <width>524</width> - <height>651</height> + <height>667</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout"> @@ -85,8 +85,6 @@ <property name="margin"> <number>0</number> </property> - - <!-- LOGIN --> <item> <widget class="QFrame" name="frame"> <property name="sizePolicy"> @@ -134,7 +132,6 @@ </property> </layout> </item> - <item> <widget class="Line" name="lineUnderLogin"> <property name="orientation"> @@ -142,8 +139,6 @@ </property> </widget> </item> - - <!-- EIP --> <item> <widget class="QWidget" name="eipWidget" native="true"> <layout class="QVBoxLayout" name="eipVerticalLayout"> @@ -172,7 +167,6 @@ </layout> </widget> </item> - <item> <widget class="Line" name="lineUnderEIP"> <property name="orientation"> @@ -180,8 +174,6 @@ </property> </widget> </item> - - <!-- EMAIL --> <item> <widget class="QWidget" name="mailWidget" native="true"> <layout class="QVBoxLayout" name="verticalLayout_3"> @@ -204,7 +196,6 @@ </layout> </widget> </item> - <item> <widget class="Line" name="lineUnderEmail"> <property name="orientation"> @@ -212,7 +203,6 @@ </property> </widget> </item> - <item> <spacer name="verticalSpacer"> <property name="orientation"> @@ -307,7 +297,7 @@ <x>0</x> <y>0</y> <width>524</width> - <height>21</height> + <height>23</height> </rect> </property> <widget class="QMenu" name="menuFile"> @@ -324,7 +314,7 @@ </widget> <widget class="QMenu" name="menuHelp"> <property name="title"> - <string>Help</string> + <string>&Help</string> </property> <addaction name="action_help"/> <addaction name="action_show_logs"/> diff --git a/src/leap/bitmask/gui/ui/wizard.ui b/src/leap/bitmask/gui/ui/wizard.ui index cf591470..6c592522 100644 --- a/src/leap/bitmask/gui/ui/wizard.ui +++ b/src/leap/bitmask/gui/ui/wizard.ui @@ -59,7 +59,7 @@ <item row="0" column="0"> <widget class="QLabel" name="label_3"> <property name="text"> - <string><html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Settings'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html></string> + <string><html><head/><body><p>Now we will guide you through some configuration that is needed before you can connect for the first time.</p><p>If you ever need to modify these options again, you can find the wizard in the <span style=" font-style:italic;">'Bitmask -&gt; Create new account...'</span> menu from the main window.</p><p>Do you want to <span style=" font-weight:600;">sign up</span> for a new account, or <span style=" font-weight:600;">log in</span> with an already existing username?</p></body></html></string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> @@ -269,7 +269,7 @@ <string>Configure or select a provider</string> </property> <layout class="QGridLayout" name="gridLayout_5"> - <item row="0" column="0"> + <item row="1" column="0"> <widget class="QRadioButton" name="rbNewProvider"> <property name="text"> <string>Configure new provider:</string> @@ -279,14 +279,14 @@ </property> </widget> </item> - <item row="2" column="0"> - <widget class="QRadioButton" name="rbExistingProvider"> - <property name="text"> - <string>Use existing one:</string> + <item row="0" column="2"> + <widget class="QComboBox" name="cbProviders"> + <property name="enabled"> + <bool>false</bool> </property> </widget> </item> - <item row="1" column="0"> + <item row="1" column="1"> <widget class="QLabel" name="label"> <property name="text"> <string>https://</string> @@ -296,17 +296,20 @@ </property> </widget> </item> - <item row="1" column="1"> + <item row="1" column="2"> <widget class="QLineEdit" name="lnProvider"/> </item> - <item row="1" column="2"> - <widget class="QPushButton" name="btnCheck"> + <item row="0" column="0"> + <widget class="QRadioButton" name="rbExistingProvider"> <property name="text"> - <string>Check</string> + <string>Use existing one:</string> + </property> + <property name="checked"> + <bool>false</bool> </property> </widget> </item> - <item row="3" column="0"> + <item row="0" column="1"> <widget class="QLabel" name="label_8"> <property name="text"> <string>https://</string> @@ -316,12 +319,29 @@ </property> </widget> </item> - <item row="3" column="1"> - <widget class="QComboBox" name="cbProviders"> - <property name="enabled"> - <bool>false</bool> - </property> - </widget> + <item row="2" column="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <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> + <item> + <widget class="QPushButton" name="btnCheck"> + <property name="text"> + <string>Check</string> + </property> + </widget> + </item> + </layout> </item> </layout> </widget> @@ -820,8 +840,8 @@ <slot>setFocus()</slot> <hints> <hint type="sourcelabel"> - <x>167</x> - <y>192</y> + <x>174</x> + <y>174</y> </hint> <hint type="destinationlabel"> <x>265</x> @@ -836,12 +856,12 @@ <slot>setFocus()</slot> <hints> <hint type="sourcelabel"> - <x>171</x> - <y>164</y> + <x>174</x> + <y>227</y> </hint> <hint type="destinationlabel"> - <x>246</x> - <y>164</y> + <x>425</x> + <y>254</y> </hint> </hints> </connection> @@ -852,12 +872,12 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>169</x> - <y>196</y> + <x>174</x> + <y>174</y> </hint> <hint type="destinationlabel"> - <x>327</x> - <y>163</y> + <x>450</x> + <y>266</y> </hint> </hints> </connection> @@ -868,8 +888,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>169</x> - <y>162</y> + <x>174</x> + <y>227</y> </hint> <hint type="destinationlabel"> <x>269</x> @@ -881,15 +901,15 @@ <sender>rbExistingProvider</sender> <signal>toggled(bool)</signal> <receiver>btnCheck</receiver> - <slot>setDisabled(bool)</slot> + <slot>setEnabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>154</x> - <y>193</y> + <x>169</x> + <y>174</y> </hint> <hint type="destinationlabel"> - <x>498</x> - <y>170</y> + <x>520</x> + <y>255</y> </hint> </hints> </connection> diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py index ec007110..e2c1a16e 100644 --- a/src/leap/bitmask/gui/wizard.py +++ b/src/leap/bitmask/gui/wizard.py @@ -17,21 +17,17 @@ """ 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.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.crypto.srpregister import SRPRegister +from leap.bitmask.provider import get_provider_path 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 @@ -70,8 +66,6 @@ class Wizard(QtGui.QWizard): self.ui = Ui_Wizard() self.ui.setupUi(self) - self._backend = backend - self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/images/mask-icon.png")) @@ -90,19 +84,8 @@ class Wizard(QtGui.QWizard): self.ui.btnCheck.clicked.connect(self._check_provider) self.ui.lnProvider.returnPressed.connect(self._check_provider) - self._backend.signaler.prov_name_resolution.connect( - self._name_resolution) - self._backend.signaler.prov_https_connection.connect( - self._https_connection) - self._backend.signaler.prov_download_provider_info.connect( - self._download_provider_info) - - self._backend.signaler.prov_download_ca_cert.connect( - self._download_ca_cert) - self._backend.signaler.prov_check_ca_fingerprint.connect( - self._check_ca_fingerprint) - self._backend.signaler.prov_check_api_certificate.connect( - self._check_api_certificate) + self._backend = backend + self._backend_connect() self._domain = None # HACK!! We need provider_config for the time being, it'll be @@ -120,6 +103,8 @@ class Wizard(QtGui.QWizard): self.ui.lnProvider.textChanged.connect(self._enable_check) self.ui.rbNewProvider.toggled.connect( lambda x: self._enable_check()) + self.ui.cbProviders.currentIndexChanged[int].connect( + self._reset_provider_check) self.ui.lblUser.returnPressed.connect( self._focus_password) @@ -172,6 +157,7 @@ class Wizard(QtGui.QWizard): self._provider_setup_ok = False self.ui.lnProvider.setText('') self.ui.grpCheckProvider.setVisible(False) + self._backend_disconnect() def _load_configured_providers(self): """ @@ -205,6 +191,10 @@ class Wizard(QtGui.QWizard): random.shuffle(pinned) # don't prioritize alphabetically self.ui.cbProviders.addItems(pinned) + # We have configured providers, so by default we select the + # 'Use existing provider' option. + self.ui.rbExistingProvider.setChecked(True) + def get_domain(self): return self._domain @@ -231,7 +221,7 @@ class Wizard(QtGui.QWizard): depending on the lnProvider content. """ enabled = len(self.ui.lnProvider.text()) != 0 - enabled = enabled and self.ui.rbNewProvider.isChecked() + enabled = enabled or self.ui.rbExistingProvider.isChecked() self.ui.btnCheck.setEnabled(enabled) if reset: @@ -261,16 +251,11 @@ class Wizard(QtGui.QWizard): ok, msg = basic_password_checks(username, password, password2) if ok: - register = SRPRegister(provider_config=self._provider_config) - register.registration_finished.connect( - self._registration_finished) - - threads.deferToThread( - partial(register.register_user, username, password)) + self._set_register_status(self.tr("Starting registration...")) + self._backend.register_user(self._domain, username, password) self._username = username self._password = password - self._set_register_status(self.tr("Starting registration...")) else: self._set_register_status(msg, error=True) self._focus_password() @@ -297,42 +282,59 @@ class Wizard(QtGui.QWizard): # register button self.ui.btnRegister.setVisible(visible) - def _registration_finished(self, ok, req): - if ok: - user_domain = self._username + "@" + self._domain - message = "<font color='green'><h3>" - message += self.tr("User %s successfully registered.") % ( - user_domain, ) - message += "</h3></font>" - self._set_register_status(message) - - self.ui.lblPassword2.clearFocus() - self._set_registration_fields_visibility(False) - - # Allow the user to remember his password - if has_keyring(): - self.ui.chkRemember.setVisible(True) - self.ui.chkRemember.setEnabled(True) - - self.page(self.REGISTER_USER_PAGE).set_completed() - self.button(QtGui.QWizard.BackButton).setEnabled(False) - else: - old_username = self._username - self._username = None - self._password = None - error_msg = self.tr("Something has gone wrong. " - "Please try again.") - try: - content, _ = get_content(req) - json_content = json.loads(content) - error_msg = json_content.get("errors").get("login")[0] - if not error_msg.istitle(): - error_msg = "%s %s" % (old_username, error_msg) - except Exception as e: - logger.error("Unknown error: %r" % (e,)) - - self._set_register_status(error_msg, error=True) - self.ui.btnRegister.setEnabled(True) + def _registration_finished(self): + """ + SLOT + TRIGGERS: + self._backend.signaler.srp_registration_finished + + The registration has finished successfully, so we do some final steps. + """ + user_domain = self._username + "@" + self._domain + message = "<font color='green'><h3>" + message += self.tr("User %s successfully registered.") % ( + user_domain, ) + message += "</h3></font>" + self._set_register_status(message) + + self.ui.lblPassword2.clearFocus() + self._set_registration_fields_visibility(False) + + # Allow the user to remember his password + if has_keyring(): + self.ui.chkRemember.setVisible(True) + self.ui.chkRemember.setEnabled(True) + + self.page(self.REGISTER_USER_PAGE).set_completed() + self.button(QtGui.QWizard.BackButton).setEnabled(False) + + def _registration_failed(self): + """ + SLOT + TRIGGERS: + self._backend.signaler.srp_registration_failed + + The registration has failed, so we report the problem. + """ + self._username = self._password = None + + error_msg = self.tr("Something has gone wrong. Please try again.") + self._set_register_status(error_msg, error=True) + self.ui.btnRegister.setEnabled(True) + + def _registration_taken(self): + """ + SLOT + TRIGGERS: + self._backend.signaler.srp_registration_taken + + The requested username is taken, warn the user about that. + """ + self._username = self._password = None + + error_msg = self.tr("The requested username is taken, choose another.") + self._set_register_status(error_msg, error=True) + self.ui.btnRegister.setEnabled(True) def _set_register_status(self, status, error=False): """ @@ -375,8 +377,10 @@ class Wizard(QtGui.QWizard): Starts the checks for a given provider """ - if len(self.ui.lnProvider.text()) == 0: - return + if self.ui.rbNewProvider.isChecked(): + self._domain = self.ui.lnProvider.text() + else: + self._domain = self.ui.cbProviders.currentText() self._provider_checks_ok = False @@ -388,7 +392,6 @@ class Wizard(QtGui.QWizard): self.ui.btnCheck.setEnabled(False) self.ui.lnProvider.setEnabled(False) self.button(QtGui.QWizard.BackButton).clearFocus() - self._domain = self.ui.lnProvider.text() self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON) self._provider_select_defer = self._backend.\ @@ -409,8 +412,6 @@ class Wizard(QtGui.QWizard): 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): @@ -487,10 +488,7 @@ class Wizard(QtGui.QWizard): check. Since this check is the last of this set, it also completes the page if passed """ - if self._provider_config.load(os.path.join("leap", - "providers", - self._domain, - "provider.json")): + if self._provider_config.load(get_provider_path(self._domain)): self._complete_task(data, self.ui.lblProviderInfo, True, self.SELECT_PROVIDER_PAGE) self._provider_checks_ok = True @@ -686,3 +684,41 @@ class Wizard(QtGui.QWizard): self.ui.lblUser.setText("") self.ui.lblPassword.setText("") self.ui.lblPassword2.setText("") + + def _backend_connect(self): + """ + Connects all the backend signals with the wizard. + """ + sig = self._backend.signaler + sig.prov_name_resolution.connect(self._name_resolution) + sig.prov_https_connection.connect(self._https_connection) + sig.prov_download_provider_info.connect(self._download_provider_info) + + sig.prov_download_ca_cert.connect(self._download_ca_cert) + sig.prov_check_ca_fingerprint.connect(self._check_ca_fingerprint) + sig.prov_check_api_certificate.connect(self._check_api_certificate) + + sig.srp_registration_finished.connect(self._registration_finished) + sig.srp_registration_failed.connect(self._registration_failed) + sig.srp_registration_taken.connect(self._registration_taken) + + def _backend_disconnect(self): + """ + This method is called when the wizard dialog is closed. + We disconnect all the backend signals in here. + """ + sig = self._backend.signaler + try: + # disconnect backend signals + sig.prov_name_resolution.disconnect(self._name_resolution) + sig.prov_https_connection.disconnect(self._https_connection) + sig.prov_download_provider_info.disconnect( + self._download_provider_info) + + sig.prov_download_ca_cert.disconnect(self._download_ca_cert) + sig.prov_check_ca_fingerprint.disconnect( + self._check_ca_fingerprint) + sig.prov_check_api_certificate.disconnect( + self._check_api_certificate) + except RuntimeError: + pass # Signal was not connected diff --git a/src/leap/bitmask/platform_init/locks.py b/src/leap/bitmask/platform_init/locks.py index 34f884dc..78ebf4cd 100644 --- a/src/leap/bitmask/platform_init/locks.py +++ b/src/leap/bitmask/platform_init/locks.py @@ -83,8 +83,6 @@ if platform_init.IS_UNIX: flock(self._fd, LOCK_EX | LOCK_NB) except IOError as exc: # could not get the lock - #import ipdb; ipdb.set_trace() - if exc.args[0] in (errno.EDEADLK, errno.EAGAIN): # errno 11 or 35 # Resource temporarily unavailable diff --git a/src/leap/bitmask/provider/__init__.py b/src/leap/bitmask/provider/__init__.py index 53587d65..89ff5d95 100644 --- a/src/leap/bitmask/provider/__init__.py +++ b/src/leap/bitmask/provider/__init__.py @@ -15,12 +15,20 @@ # 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 +Provider utilities. """ import os + +from pkg_resources import parse_version + +from leap.bitmask import __short_version__ as BITMASK_VERSION from leap.common.check import leap_assert +# The currently supported API versions by the client. +SUPPORTED_APIS = ["1"] + + def get_provider_path(domain): """ Returns relative path for provider config. @@ -32,3 +40,26 @@ def get_provider_path(domain): """ leap_assert(domain is not None, "get_provider_path: We need a domain") return os.path.join("leap", "providers", domain, "provider.json") + + +def supports_api(api_version): + """ + :param api_version: the version number of the api that we need to check + :type api_version: str + + :returns: if that version is supported or not. + :return type: bool + """ + return api_version in SUPPORTED_APIS + + +def supports_client(minimum_version): + """ + :param minimum_version: the version number of the client that + we need to check. + :type minimum_version: str + + :returns: True if that version is supported or False otherwise. + :return type: bool + """ + return parse_version(minimum_version) <= parse_version(BITMASK_VERSION) diff --git a/src/leap/bitmask/provider/providerbootstrapper.py b/src/leap/bitmask/provider/providerbootstrapper.py index 947ba0c9..2a519206 100644 --- a/src/leap/bitmask/provider/providerbootstrapper.py +++ b/src/leap/bitmask/provider/providerbootstrapper.py @@ -24,16 +24,18 @@ import sys import requests -from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert -from leap.bitmask.util.request_helpers import get_content +from leap.bitmask import provider from leap.bitmask import util -from leap.bitmask.util.constants import REQUEST_TIMEOUT +from leap.bitmask.config import flags +from leap.bitmask.config.providerconfig import ProviderConfig, MissingCACert +from leap.bitmask.provider import get_provider_path from leap.bitmask.services.abstractbootstrapper import AbstractBootstrapper -from leap.bitmask.provider.supportedapis import SupportedAPIs +from leap.bitmask.util.constants import REQUEST_TIMEOUT +from leap.bitmask.util.request_helpers import get_content 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 +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p logger = logging.getLogger(__name__) @@ -45,6 +47,14 @@ class UnsupportedProviderAPI(Exception): pass +class UnsupportedClientVersionError(Exception): + """ + Raised when attempting to use a provider with an older + client than supported. + """ + pass + + class WrongFingerprint(Exception): """ Raised when a fingerprint comparison does not match. @@ -59,6 +69,8 @@ class ProviderBootstrapper(AbstractBootstrapper): If a check fails, the subsequent checks are not executed """ + MIN_CLIENT_VERSION = 'x-minimum-client-version' + def __init__(self, signaler=None, bypass_checks=False): """ Constructor for provider bootstrapper object @@ -87,9 +99,14 @@ class ProviderBootstrapper(AbstractBootstrapper): :rtype: bool or str """ if self._bypass_checks: - verify = False + return False + + cert = flags.CA_CERT_FILE + if cert is not None: + verify = cert else: verify = ca_bundle.where() + return verify def _check_name_resolution(self): @@ -155,8 +172,8 @@ class ProviderBootstrapper(AbstractBootstrapper): headers = {} domain = self._domain.encode(sys.getfilesystemencoding()) provider_json = os.path.join(util.get_path_prefix(), - "leap", "providers", domain, - "provider.json") + get_provider_path(domain)) + mtime = get_mtime(provider_json) if self._download_if_needed and mtime: @@ -187,6 +204,8 @@ class ProviderBootstrapper(AbstractBootstrapper): res.raise_for_status() logger.debug("Request status code: {0}".format(res.status_code)) + min_client_version = res.headers.get(self.MIN_CLIENT_VERSION, '0') + # Not modified if res.status_code == 304: logger.debug("Provider definition has not been modified") @@ -194,6 +213,13 @@ class ProviderBootstrapper(AbstractBootstrapper): # end refactor, more or less... # XXX Watch out, have to check the supported api yet. else: + if flags.APP_VERSION_CHECK: + # TODO split + if not provider.supports_client(min_client_version): + self._signaler.signal( + self._signaler.PROV_UNSUPPORTED_CLIENT) + raise UnsupportedClientVersionError() + provider_definition, mtime = get_content(res) provider_config = ProviderConfig() @@ -201,17 +227,20 @@ class ProviderBootstrapper(AbstractBootstrapper): provider_config.save(["leap", "providers", domain, "provider.json"]) - api_version = provider_config.get_api_version() - if SupportedAPIs.supports(api_version): - logger.debug("Provider definition has been modified") - else: - api_supported = ', '.join(SupportedAPIs.SUPPORTED_APIS) - error = ('Unsupported provider API version. ' - 'Supported versions are: {0}. ' - 'Found: {1}.').format(api_supported, api_version) - - logger.error(error) - raise UnsupportedProviderAPI(error) + if flags.API_VERSION_CHECK: + # TODO split + api_version = provider_config.get_api_version() + if provider.supports_api(api_version): + logger.debug("Provider definition has been modified") + else: + api_supported = ', '.join(provider.SUPPORTED_APIS) + error = ('Unsupported provider API version. ' + 'Supported versions are: {0}. ' + 'Found: {1}.').format(api_supported, api_version) + + logger.error(error) + self._signaler.signal(self._signaler.PROV_UNSUPPORTED_API) + raise UnsupportedProviderAPI(error) def run_provider_select_checks(self, domain, download_if_needed=False): """ diff --git a/src/leap/bitmask/provider/supportedapis.py b/src/leap/bitmask/provider/supportedapis.py deleted file mode 100644 index 3e650ba2..00000000 --- a/src/leap/bitmask/provider/supportedapis.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# supportedapis.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/>. - -""" -API Support check. -""" - - -class SupportedAPIs(object): - """ - Class responsible of checking for API compatibility. - """ - SUPPORTED_APIS = ["1"] - - @classmethod - def supports(self, api_version): - """ - :param api_version: the version number of the api that we need to check - :type api_version: str - - :returns: if that version is supported or not. - :return type: bool - """ - return api_version in self.SUPPORTED_APIS diff --git a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py index d8336fec..6cf3e469 100644 --- a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py +++ b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py @@ -36,17 +36,17 @@ from nose.twistedtools import deferred, reactor from twisted.internet import threads from requests.models import Response +from leap.bitmask import provider +from leap.bitmask import util +from leap.bitmask.backend import Signaler 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.backend import Signaler -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 +from leap.common.testing.https_server import where class ProviderBootstrapperTest(BaseLeapTest): @@ -489,7 +489,7 @@ class ProviderBootstrapperActiveTest(unittest.TestCase): 'leap.bitmask.config.providerconfig.ProviderConfig.get_ca_cert_path', lambda x: where('cacert.pem')) def test_download_provider_info_unsupported_api(self): - self._setup_provider_config_with(SupportedAPIs.SUPPORTED_APIS[0], + self._setup_provider_config_with(provider.SUPPORTED_APIS[0], tempfile.mkdtemp()) self._setup_providerbootstrapper(False) self._produce_dummy_provider_json() diff --git a/src/leap/bitmask/services/abstractbootstrapper.py b/src/leap/bitmask/services/abstractbootstrapper.py index 3bee8e01..fc6bd3e9 100644 --- a/src/leap/bitmask/services/abstractbootstrapper.py +++ b/src/leap/bitmask/services/abstractbootstrapper.py @@ -28,6 +28,7 @@ from PySide import QtCore from twisted.python import log from twisted.internet import threads +from twisted.internet.defer import CancelledError from leap.common.check import leap_assert, leap_assert_type @@ -91,6 +92,12 @@ class AbstractBootstrapper(QtCore.QObject): :param failure: failure object that Twisted generates :type failure: twisted.python.failure.Failure """ + if failure.check(CancelledError): + logger.debug("Defer cancelled.") + failure.trap(Exception) + self._signaler.signal(self._signaler.PROV_CANCELLED_SETUP) + return + if self._signal_to_emit: err_msg = self._err_msg \ if self._err_msg is not None \ diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index 51f0f738..5c100036 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -19,14 +19,20 @@ VPN Manager, spawned in a custom processProtocol. """ import logging import os -import psutil -import psutil.error import shutil import socket import sys from itertools import chain, repeat +import psutil +try: + # psutil < 2.0.0 + from psutil.error import AccessDenied as psutil_AccessDenied +except ImportError: + # psutil >= 2.0.0 + from psutil import AccessDenied as psutil_AccessDenied + from PySide import QtCore from leap.bitmask.config.providerconfig import ProviderConfig @@ -672,7 +678,7 @@ class VPNManager(object): if any(map(lambda s: s.find("LEAPOPENVPN") != -1, p.cmdline)): openvpn_process = p break - except psutil.error.AccessDenied: + except psutil_AccessDenied: pass return openvpn_process diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index addf9bef..79f324dc 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -35,6 +35,7 @@ from leap.common.check import leap_assert from leap.common.events import register as leap_register from leap.common.events import events_pb2 as leap_events + logger = logging.getLogger(__name__) @@ -72,6 +73,8 @@ class IMAPControl(object): """ Starts imap service. """ + from leap.bitmask.config import flags + logger.debug('Starting imap service') leap_assert(sameProxiedObjects(self._soledad, None) is not True, @@ -81,16 +84,25 @@ class IMAPControl(object): "We need a non-null keymanager for initializing imap " "service") + offline = flags.OFFLINE self.imap_service, self.imap_port, \ self.imap_factory = imap.start_imap_service( self._soledad, self._keymanager, - userid=self.userid) - self.imap_service.start_loop() + userid=self.userid, + offline=offline) + + if offline is False: + logger.debug("Starting loop") + self.imap_service.start_loop() - def stop_imap_service(self): + def stop_imap_service(self, cv): """ Stops imap service (fetcher, factory and port). + + :param cv: A condition variable to which we can signal when imap + indeed stops. + :type cv: threading.Condition """ self.imap_connection.qtsigs.disconnecting_signal.emit() # TODO We should homogenize both services. @@ -102,7 +114,14 @@ class IMAPControl(object): # Stop listening on the IMAP port self.imap_port.stopListening() # Stop the protocol - self.imap_factory.doStop() + self.imap_factory.theAccount.closed = True + self.imap_factory.doStop(cv) + else: + # main window does not have to wait because there's no service to + # be stopped, so we release the condition variable + cv.acquire() + cv.notify() + cv.release() def fetch_incoming_mail(self): """ @@ -339,7 +358,7 @@ class MailConductor(IMAPControl, SMTPControl): self._mail_machine = None self._mail_connection = mail_connection.MailConnection() - self.userid = None + self._userid = None @property def userid(self): @@ -388,3 +407,4 @@ class MailConductor(IMAPControl, SMTPControl): qtsigs.connecting_signal.connect(widget.mail_state_connecting) qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting) qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) + qtsigs.soledad_invalid_auth_token.connect(widget.soledad_invalid_auth_token) diff --git a/src/leap/bitmask/services/mail/connection.py b/src/leap/bitmask/services/mail/connection.py index 29378f62..fdc28fe4 100644 --- a/src/leap/bitmask/services/mail/connection.py +++ b/src/leap/bitmask/services/mail/connection.py @@ -93,6 +93,8 @@ class MailConnectionSignals(QtCore.QObject): connection_died_signal = QtCore.Signal() connection_aborted_signal = QtCore.Signal() + soledad_invalid_auth_token = QtCore.Signal() + class MailConnection(AbstractLEAPConnection): diff --git a/src/leap/bitmask/services/mail/repair.py b/src/leap/bitmask/services/mail/plumber.py index 767df1ef..c16a1fed 100644 --- a/src/leap/bitmask/services/mail/repair.py +++ b/src/leap/bitmask/services/mail/plumber.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# repair.py -# Copyright (C) 2013 LEAP +# plumber.py +# Copyright (C) 2013, 2014 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 @@ -15,20 +15,26 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """ -Utils for repairing mailbox indexes. +Utils for manipulating local mailboxes. """ -import logging import getpass +import logging import os from collections import defaultdict +from functools import partial +from twisted.internet import defer + +from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.crypto.srpauth import SRPAuth -from leap.bitmask.util import get_path_prefix +from leap.bitmask.provider import get_provider_path from leap.bitmask.services.soledad.soledadbootstrapper import get_db_paths +from leap.bitmask.util import flatten, get_path_prefix -from leap.mail.imap.server import SoledadBackedAccount +from leap.mail.imap.account import SoledadBackedAccount +from leap.mail.imap.memorystore import MemoryStore +from leap.mail.imap.soledadstore import SoledadStore from leap.soledad.client import Soledad logger = logging.getLogger(__name__) @@ -89,69 +95,77 @@ class MBOXPlumber(object): that can be invoked when data migration in the client is needed. """ - def __init__(self, userid, passwd): + def __init__(self, userid, passwd, mdir=None): """ - Initializes the plumber with all that's needed to authenticate + Initialize the plumber with all that's needed to authenticate against the provider. :param userid: user identifier, foo@bar :type userid: basestring :param passwd: the soledad passphrase :type passwd: basestring + :param mdir: a path to a maildir to import + :type mdir: str or None """ self.userid = userid self.passwd = passwd user, provider = userid.split('@') self.user = user + self.mdir = mdir self.sol = None - provider_config_path = os.path.join( - get_path_prefix(), - "leap", "providers", - provider, "provider.json") + self._settings = LeapSettings() + + provider_config_path = os.path.join(get_path_prefix(), + get_provider_path(provider)) provider_config = ProviderConfig() loaded = provider_config.load(provider_config_path) if not loaded: print "could not load provider config!" return self.exit() - self.srp = SRPAuth(provider_config) - self.srp.authentication_finished.connect(self.repair_account) - - def start_auth(self): + def _init_local_soledad(self): """ - returns the user identifier for a given provider. - - :param provider: the provider to which we authenticate against. + Initialize local Soledad instance. """ - print "Authenticating with provider..." - self.d = self.srp.authenticate(self.user, self.passwd) + self.uuid = self._settings.get_uuid(self.userid) + if not self.uuid: + print "Cannot get UUID from settings. Log in at least once." + return False - def repair_account(self, *args): - """ - Gets the user id for this account. - """ - print "Got authenticated." - self.uid = self.srp.get_uid() - if not self.uid: - print "Got BAD UID from provider!" - return self.exit() - print "UID: %s" % (self.uid) + print "UUID: %s" % (self.uuid) - secrets, localdb = get_db_paths(self.uid) + secrets, localdb = get_db_paths(self.uuid) self.sol = initialize_soledad( - self.uid, self.userid, self.passwd, + self.uuid, self.userid, self.passwd, secrets, localdb, "/tmp", "/tmp") + memstore = MemoryStore( + permanent_store=SoledadStore(self.sol), + write_period=5) + self.acct = SoledadBackedAccount(self.userid, self.sol, + memstore=memstore) + return True + + # + # Account repairing + # + + def repair_account(self, *args): + """ + Repair mbox uids for all mboxes in this account. + """ + init = self._init_local_soledad() + if not init: + return self.exit() - self.acct = SoledadBackedAccount(self.userid, self.sol) for mbox_name in self.acct.mailboxes: - self.repair_mbox(mbox_name) + self.repair_mbox_uids(mbox_name) print "done." self.exit() - def repair_mbox(self, mbox_name): + def repair_mbox_uids(self, mbox_name): """ - Repairs indexes for a given mbox + Repair indexes for a given mbox. :param mbox_name: mailbox to repair :type mbox_name: basestring @@ -164,19 +178,21 @@ class MBOXPlumber(object): print "There are %s messages" % (len_mbox,) last_ok = True if mbox.last_uid == len_mbox else False - uids_iter = (doc.content['uid'] for doc in mbox.messages.get_all()) + uids_iter = mbox.messages.all_msg_iter() dupes = self._has_dupes(uids_iter) if last_ok and not dupes: print "Mbox does not need repair." return + # XXX CHANGE? ---- msgs = mbox.messages.get_all() for zindex, doc in enumerate(msgs): mindex = zindex + 1 old_uid = doc.content['uid'] doc.content['uid'] = mindex self.sol.put_doc(doc) - print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid) + if mindex != old_uid: + print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid) old_last_uid = mbox.last_uid mbox.last_uid = len_mbox @@ -184,7 +200,7 @@ class MBOXPlumber(object): def _has_dupes(self, sequence): """ - Returns True if the given sequence of ints has duplicates. + Return True if the given sequence of ints has duplicates. :param sequence: a sequence of ints :type sequence: sequence @@ -197,12 +213,82 @@ class MBOXPlumber(object): return True return False + # + # Maildir import + # + def import_mail(self, mail_filename): + """ + Import a single mail into a mailbox. + + :param mbox: the Mailbox instance to save in. + :type mbox: SoledadMailbox + :param mail_filename: the filename to the mail file to save + :type mail_filename: basestring + :return: a deferred + """ + def saved(_): + print "message added" + + with open(mail_filename) as f: + mail_string = f.read() + #uid = self._mbox.getUIDNext() + #print "saving with UID: %s" % uid + d = self._mbox.messages.add_msg( + mail_string, notify_on_disk=True) + return d + + def import_maildir(self, mbox_name="INBOX"): + """ + Import all mails in a maildir. + + We will process all subfolders as beloging + to the same mailbox (cur, new, tmp). + """ + # TODO parse hierarchical subfolders into + # inferior mailboxes. + + if not os.path.isdir(self.mdir): + print "ERROR: maildir path does not exist." + return + + init = self._init_local_soledad() + if not init: + return self.exit() + + mbox = self.acct.getMailbox(mbox_name) + self._mbox = mbox + len_mbox = mbox.getMessageCount() + + mail_files_g = flatten( + map(partial(os.path.join, f), files) + for f, _, files in os.walk(self.mdir)) + + # we only coerce the generator to give the + # len, but we could skip than and inform at the end. + mail_files = list(mail_files_g) + print "Got %s mails to import into %s (%s)" % ( + len(mail_files), mbox_name, len_mbox) + + def all_saved(_): + print "all messages imported" + + deferreds = [] + for f_name in mail_files: + deferreds.append(self.import_mail(f_name)) + print "deferreds: ", deferreds + + d1 = defer.gatherResults(deferreds, consumeErrors=False) + d1.addCallback(all_saved) + d1.addCallback(self._cbExit) + + def _cbExit(self, ignored): + return self.exit() + def exit(self): from twisted.internet import reactor - self.d.cancel() - if self.sol: - self.sol.close() try: + if self.sol: + self.sol.close() reactor.stop() except Exception: pass @@ -211,7 +297,8 @@ class MBOXPlumber(object): def repair_account(userid): """ - Starts repair process for a given account. + Start repair process for a given account. + :param userid: the user id (email-like) """ from twisted.internet import reactor @@ -219,7 +306,22 @@ def repair_account(userid): # go mario! plumber = MBOXPlumber(userid, passwd) - reactor.callLater(1, plumber.start_auth) + reactor.callLater(1, plumber.repair_account) + reactor.run() + + +def import_maildir(userid, maildir_path): + """ + Start import-maildir process for a given account. + + :param userid: the user id (email-like) + """ + from twisted.internet import reactor + passwd = unicode(getpass.getpass("Passphrase: ")) + + # go mario! + plumber = MBOXPlumber(userid, passwd, mdir=maildir_path) + reactor.callLater(1, plumber.import_maildir) reactor.run() @@ -228,7 +330,12 @@ if __name__ == "__main__": logging.basicConfig() - if len(sys.argv) != 2: - print "Usage: repair <username>" + if len(sys.argv) != 3: + print "Usage: plumber [repair|import] <username>" sys.exit(1) - repair_account(sys.argv[1]) + + # this would be better with a dict if it grows + if sys.argv[1] == "repair": + repair_account(sys.argv[2]) + if sys.argv[1] == "import": + print "Not implemented yet." diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 3ab62b2e..ad5ee4d0 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -23,12 +23,13 @@ import socket import sys from ssl import SSLError +from sqlite3 import ProgrammingError as sqlite_ProgrammingError from PySide import QtCore from u1db import errors as u1db_errors +from twisted.internet import threads from zope.proxy import sameProxiedObjects - -from twisted.internet.threads import deferToThread +from pysqlcipher.dbapi2 import ProgrammingError as sqlcipher_ProgrammingError from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig @@ -36,21 +37,56 @@ 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 import is_file, is_empty_file +from leap.bitmask.util import first, is_file, is_empty_file, make_address from leap.bitmask.util import get_path_prefix from leap.bitmask.platform_init import IS_WIN 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.common.errors import InvalidAuthTokenError from leap.soledad.client import Soledad, BootstrapSequenceError logger = logging.getLogger(__name__) +""" +These mocks are replicated from imap tests and the repair utility. +They are needed for the moment to knock out the remote capabilities of soledad +during the use of the offline mode. + +They should not be needed after we allow a null remote initialization in the +soledad client, and a switch to remote sync-able mode during runtime. +""" + + +class Mock(object): + """ + A generic simple mock class + """ + def __init__(self, return_value=None): + self._return = return_value + + def __call__(self, *args, **kwargs): + return self._return + + +class MockSharedDB(object): + """ + Mocked SharedDB object to replace in soledad before + instantiating it in offline mode. + """ + get_doc = Mock() + put_doc = Mock() + lock = Mock(return_value=('atoken', 300)) + unlock = Mock(return_value=True) + + def __call__(self): + return self # TODO these exceptions could be moved to soledad itself # after settling this down. + class SoledadSyncError(Exception): message = "Error while syncing Soledad" @@ -61,7 +97,7 @@ class SoledadInitError(Exception): def get_db_paths(uuid): """ - Returns the secrets and local db paths needed for soledad + Return the secrets and local db paths needed for soledad initialization :param uuid: uuid for user @@ -88,7 +124,7 @@ def get_db_paths(uuid): class SoledadBootstrapper(AbstractBootstrapper): """ - Soledad init procedure + Soledad init procedure. """ SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" @@ -102,7 +138,9 @@ class SoledadBootstrapper(AbstractBootstrapper): # {"passed": bool, "error": str} download_config = QtCore.Signal(dict) gen_key = QtCore.Signal(dict) + local_only_ready = QtCore.Signal(dict) soledad_timeout = QtCore.Signal() + soledad_invalid_auth_token = QtCore.Signal() soledad_failed = QtCore.Signal() def __init__(self): @@ -115,6 +153,9 @@ class SoledadBootstrapper(AbstractBootstrapper): self._user = "" self._password = "" + self._address = "" + self._uuid = "" + self._srpauth = None self._soledad = None @@ -130,6 +171,8 @@ class SoledadBootstrapper(AbstractBootstrapper): @property def srpauth(self): + if flags.OFFLINE is True: + return None leap_assert(self._provider_config is not None, "We need a provider config") return SRPAuth(self._provider_config) @@ -141,7 +184,7 @@ class SoledadBootstrapper(AbstractBootstrapper): def should_retry_initialization(self): """ - Returns True if we should retry the initialization. + Return True if we should retry the initialization. """ logger.debug("current retries: %s, max retries: %s" % ( self._soledad_retries, @@ -150,47 +193,100 @@ class SoledadBootstrapper(AbstractBootstrapper): def increment_retries_count(self): """ - Increments the count of initialization retries. + Increment the count of initialization retries. """ self._soledad_retries += 1 # initialization - def load_and_sync_soledad(self): + def load_offline_soledad(self, username, password, uuid): """ - Once everthing is in the right place, we instantiate and sync - Soledad + Instantiate Soledad for offline use. + + :param username: full user id (user@provider) + :type username: basestring + :param password: the soledad passphrase + :type password: unicode + :param uuid: the user uuid + :type uuid: basestring + """ + print "UUID ", uuid + self._address = username + self._uuid = uuid + return self.load_and_sync_soledad(uuid, offline=True) + + def _get_soledad_local_params(self, uuid, offline=False): """ - # TODO this method is still too large - uuid = self.srpauth.get_uid() - token = self.srpauth.get_token() + Return the locals parameters needed for the soledad initialization. + + :param uuid: the uuid of the user, used in offline mode. + :type uuid: unicode, or None. + :return: secrets_path, local_db_path, token + :rtype: tuple + """ + # in the future, when we want to be able to switch to + # online mode, this should be a proxy object too. + # Same for server_url below. + + if offline is False: + token = self.srpauth.get_token() + else: + token = "" secrets_path, local_db_path = get_db_paths(uuid) - # TODO: Select server based on timezone (issue #3308) - server_dict = self._soledad_config.get_hosts() + logger.debug('secrets_path:%s' % (secrets_path,)) + logger.debug('local_db:%s' % (local_db_path,)) + return (secrets_path, local_db_path, token) - if not server_dict.keys(): - # XXX raise more specific exception, and catch it properly! - raise Exception("No soledad server found") + def _get_soledad_server_params(self, uuid, offline): + """ + Return the remote parameters needed for the soledad initialization. - 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,)) + :param uuid: the uuid of the user, used in offline mode. + :type uuid: unicode, or None. + :return: server_url, cert_file + :rtype: tuple + """ + if uuid is None: + uuid = self.srpauth.get_uuid() - cert_file = self._provider_config.get_ca_cert_path() + if offline is True: + server_url = "http://localhost:9999/" + cert_file = "" + else: + server_url = self._pick_server(uuid) + cert_file = self._provider_config.get_ca_cert_path() - logger.debug('local_db:%s' % (local_db_path,)) - logger.debug('secrets_path:%s' % (secrets_path,)) + return server_url, cert_file + + def _soledad_sync_errback(self, failure): + failure.trap(InvalidAuthTokenError) + # in the case of an invalid token we have already turned off mail and + # warned the user in _do_soledad_sync() + + + def load_and_sync_soledad(self, uuid=None, offline=False): + """ + Once everthing is in the right place, we instantiate and sync + Soledad + + :param uuid: the uuid of the user, used in offline mode. + :type uuid: unicode, or None. + :param offline: whether to instantiate soledad for offline use. + :type offline: bool + """ + local_param = self._get_soledad_local_params(uuid, offline) + remote_param = self._get_soledad_server_params(uuid, offline) + + secrets_path, local_db_path, token = local_param + server_url, cert_file = remote_param try: self._try_soledad_init( uuid, secrets_path, local_db_path, server_url, cert_file, token) - except: + except Exception: # re-raise the exceptions from try_init, # we're currently handling the retries from the # soledad-launcher in the gui. @@ -198,11 +294,52 @@ class SoledadBootstrapper(AbstractBootstrapper): leap_assert(not sameProxiedObjects(self._soledad, None), "Null soledad, error while initializing") - self._do_soledad_sync() + + if flags.OFFLINE is True: + self._init_keymanager(self._address, token) + self.local_only_ready.emit({self.PASSED_KEY: True}) + else: + try: + address = make_address( + self._user, self._provider_config.get_domain()) + self._init_keymanager(address, token) + self._keymanager.get_key( + address, openpgp.OpenPGPKey, + private=True, fetch_remote=False) + d = threads.deferToThread(self._do_soledad_sync) + d.addErrback(self._soledad_sync_errback) + except KeyNotFound: + logger.debug("Key not found. Generating key for %s" % + (address,)) + self._do_soledad_sync() + + def _pick_server(self, uuid): + """ + Choose a soledad server to sync against. + + :param uuid: the uuid for the user. + :type uuid: unicode + :returns: the server url + :rtype: unicode + """ + # TODO: Select server based on timezone (issue #3308) + server_dict = self._soledad_config.get_hosts() + + if not server_dict.keys(): + # XXX raise more specific exception, and catch it properly! + raise Exception("No soledad server found") + + selected_server = server_dict[first(server_dict.keys())] + server_url = "https://%s:%s/user-%s" % ( + selected_server["hostname"], + selected_server["port"], + uuid) + logger.debug("Using soledad server url: %s" % (server_url,)) + return server_url def _do_soledad_sync(self): """ - Does several retries to get an initial soledad sync. + Do several retries to get an initial soledad sync. """ # and now, let's sync sync_tries = self.MAX_SYNC_RETRIES @@ -222,6 +359,13 @@ class SoledadBootstrapper(AbstractBootstrapper): # ubuntu folks. sync_tries -= 1 continue + except InvalidAuthTokenError: + self.soledad_invalid_auth_token.emit() + raise + except Exception as e: + logger.exception("Unhandled error while syncing " + "soledad: %r" % (e,)) + break # reached bottom, failed to sync # and there's nothing we can do... @@ -231,7 +375,7 @@ class SoledadBootstrapper(AbstractBootstrapper): def _try_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, auth_token): """ - Tries to initialize soledad. + Try to initialize soledad. :param uuid: user identifier :param secrets_path: path to secrets file @@ -247,6 +391,10 @@ class SoledadBootstrapper(AbstractBootstrapper): # TODO: If selected server fails, retry with another host # (issue #3309) encoding = sys.getfilesystemencoding() + + # XXX We should get a flag in soledad itself + if flags.OFFLINE is True: + Soledad._shared_db = MockSharedDB() try: self._soledad = Soledad( uuid, @@ -281,7 +429,7 @@ class SoledadBootstrapper(AbstractBootstrapper): self.soledad_failed.emit() raise except u1db_errors.HTTPError as exc: - logger.exception("Error whie initializing soledad " + logger.exception("Error while initializing soledad " "(HTTPError)") self.soledad_failed.emit() raise @@ -293,7 +441,7 @@ class SoledadBootstrapper(AbstractBootstrapper): def _try_soledad_sync(self): """ - Tries to sync soledad. + Try to sync soledad. Raises SoledadSyncError if not successful. """ try: @@ -305,7 +453,13 @@ class SoledadBootstrapper(AbstractBootstrapper): except u1db_errors.InvalidGeneration as exc: logger.error("%r" % (exc,)) raise SoledadSyncError("u1db: InvalidGeneration") - + except (sqlite_ProgrammingError, sqlcipher_ProgrammingError) as e: + logger.exception("%r" % (e,)) + raise + except InvalidAuthTokenError: + # token is invalid, probably expired + logger.error('Invalid auth token while trying to sync Soledad') + raise except Exception as exc: logger.exception("Unhandled error while syncing " "soledad: %r" % (exc,)) @@ -313,7 +467,7 @@ class SoledadBootstrapper(AbstractBootstrapper): def _download_config(self): """ - Downloads the Soledad config for the given provider + Download the Soledad config for the given provider """ leap_assert(self._provider_config, @@ -332,11 +486,14 @@ class SoledadBootstrapper(AbstractBootstrapper): # 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() + + uuid = self.srpauth.get_uuid() + self.load_and_sync_soledad(uuid) def _get_gpg_bin_path(self): """ - Returns the path to gpg binary. + Return the path to gpg binary. + :returns: the gpg binary path :rtype: str """ @@ -362,40 +519,62 @@ class SoledadBootstrapper(AbstractBootstrapper): leap_check(gpgbin is not None, "Could not find gpg binary") return gpgbin - def _init_keymanager(self, address): + def _init_keymanager(self, address, token): """ - Initializes the keymanager. + Initialize the keymanager. + :param address: the address to initialize the keymanager with. :type address: str + :param token: the auth token for accessing webapp. + :type token: str """ srp_auth = self.srpauth logger.debug('initializing keymanager...') - try: - self._keymanager = KeyManager( + + if flags.OFFLINE is True: + args = (address, "https://localhost", self._soledad) + kwargs = { + "ca_cert_path": "", + "api_uri": "", + "api_version": "", + "uid": self._uuid, + "gpgbinary": self._get_gpg_bin_path() + } + else: + args = ( address, "https://nicknym.%s:6425" % ( self._provider_config.get_domain(),), - self._soledad, - #token=srp_auth.get_token(), # TODO: enable token usage - session_id=srp_auth.get_session_id(), - ca_cert_path=self._provider_config.get_ca_cert_path(), - api_uri=self._provider_config.get_api_uri(), - api_version=self._provider_config.get_api_version(), - uid=srp_auth.get_uid(), - gpgbinary=self._get_gpg_bin_path()) + self._soledad + ) + kwargs = { + "token": token, + "ca_cert_path": self._provider_config.get_ca_cert_path(), + "api_uri": self._provider_config.get_api_uri(), + "api_version": self._provider_config.get_api_version(), + "uid": srp_auth.get_uuid(), + "gpgbinary": self._get_gpg_bin_path() + } + try: + self._keymanager = KeyManager(*args, **kwargs) + except KeyNotFound: + logger.debug('key for %s not found.' % address) except Exception as exc: logger.exception(exc) raise - logger.debug('sending key to server...') - - # make sure key is in server - try: - self._keymanager.send_key(openpgp.OpenPGPKey) - except Exception as exc: - logger.error("Error sending key to server.") - logger.exception(exc) - # but we do not raise + if flags.OFFLINE is False: + # make sure key is in server + logger.debug('Trying to send key to server...') + try: + self._keymanager.send_key(openpgp.OpenPGPKey) + except KeyNotFound: + logger.debug('No key found for %s, will generate soon.' + % address) + except Exception as exc: + logger.error("Error sending key to server.") + logger.exception(exc) + # but we do not raise def _gen_key(self, _): """ @@ -407,8 +586,8 @@ class SoledadBootstrapper(AbstractBootstrapper): 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) + address = make_address( + self._user, self._provider_config.get_domain()) logger.debug("Retrieving key for %s" % (address,)) try: @@ -468,4 +647,4 @@ class SoledadBootstrapper(AbstractBootstrapper): (self._gen_key, self.gen_key) ] - self.addCallbackChain(cb_chain) + return self.addCallbackChain(cb_chain) diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index b58e6e3b..2b2cd874 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -18,19 +18,28 @@ Some small and handy functions. """ import datetime +import itertools import os from leap.bitmask.config import flags from leap.common.config import get_path_prefix as common_get_path_prefix +# functional goodies for a healthier life: +# We'll give your money back if it does not alleviate the eye strain, at least. -def get_path_prefix(): - return common_get_path_prefix(flags.STANDALONE) + +# levelname length == 8, since 'CRITICAL' is the longest +LOG_FORMAT = ('%(asctime)s - %(levelname)-8s - ' + 'L#%(lineno)-4s : %(name)s:%(funcName)s() - %(message)s') def first(things): """ Return the head of a collection. + + :param things: a sequence to extract the head from. + :type things: sequence + :return: object, or None """ try: return things[0] @@ -38,6 +47,23 @@ def first(things): return None +def flatten(things): + """ + Return a generator iterating through a flattened sequence. + + :param things: a nested sequence, eg, a list of lists. + :type things: sequence + :rtype: generator + """ + return itertools.chain(*things) + + +# leap repetitive chores + +def get_path_prefix(): + return common_get_path_prefix(flags.STANDALONE) + + def get_modification_ts(path): """ Gets modification time of a file. @@ -76,3 +102,16 @@ def is_empty_file(path): Returns True if the file at path is empty. """ return os.stat(path).st_size is 0 + + +def make_address(user, provider): + """ + Return a full identifier for an user, as a email-like + identifier. + + :param user: the username + :type user: basestring + :param provider: the provider domain + :type provider: basestring + """ + return "%s@%s" % (user, provider) diff --git a/src/leap/bitmask/util/constants.py b/src/leap/bitmask/util/constants.py index e6a6bdce..e7e72cc4 100644 --- a/src/leap/bitmask/util/constants.py +++ b/src/leap/bitmask/util/constants.py @@ -17,3 +17,4 @@ SIGNUP_TIMEOUT = 5 REQUEST_TIMEOUT = 15 +PASTEBIN_API_DEV_KEY = "09563100642af6085d641f749a1922b4" diff --git a/src/leap/bitmask/util/keyring_helpers.py b/src/leap/bitmask/util/keyring_helpers.py index 4b3eb57f..ee2d7a1c 100644 --- a/src/leap/bitmask/util/keyring_helpers.py +++ b/src/leap/bitmask/util/keyring_helpers.py @@ -19,30 +19,67 @@ Keyring helpers. """ import logging -import keyring +try: + import keyring + from keyring.backends.file import EncryptedKeyring, PlaintextKeyring + OBSOLETE_KEYRINGS = [ + EncryptedKeyring, + PlaintextKeyring + ] + canuse = lambda kr: (kr is not None + and kr.__class__ not in OBSOLETE_KEYRINGS) + +except Exception: + # Problems when importing keyring! It might be a problem binding to the + # dbus socket, or stuff like that. + keyring = None -from keyring.backends.file import EncryptedKeyring, PlaintextKeyring logger = logging.getLogger(__name__) -OBSOLETE_KEYRINGS = [ - EncryptedKeyring, - PlaintextKeyring -] +def _get_keyring_with_fallback(): + """ + Get the default keyring, and if obsolete try to pick SecretService keyring + if available. + + This is a workaround for the cases in which the keyring module chooses + an insecure keyring by default (ie, inside a virtualenv). + """ + if not keyring: + return None + kr = keyring.get_keyring() + if not canuse(kr): + try: + kr_klass = keyring.backends.SecretService + kr = kr_klass.Keyring() + except AttributeError: + logger.warning("Keyring cannot find SecretService Backend") + logger.debug("Selected keyring: %s" % (kr.__class__,)) + if not canuse(kr): + logger.debug("Not using default keyring since it is obsolete") + return kr def has_keyring(): """ - Returns whether we have an useful keyring to use. + Return whether we have an useful keyring to use. :rtype: bool """ - kr = keyring.get_keyring() - klass = kr.__class__ - logger.debug("Selected keyring: %s" % (klass,)) + if not keyring: + return False + kr = _get_keyring_with_fallback() + return canuse(kr) + - canuse = kr is not None and klass not in OBSOLETE_KEYRINGS - if not canuse: - logger.debug("Not using this keyring since it is obsolete") - return canuse +def get_keyring(): + """ + Return an usable keyring. + + :rtype: keyringBackend or None + """ + if not keyring: + return False + kr = _get_keyring_with_fallback() + return kr if canuse(kr) else None diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index 6703b600..88267ff8 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -27,41 +27,77 @@ def build_parser(): All the options for the leap arg parser Some of these could be switched on only if debug flag is present! """ - epilog = "Copyright 2012-2013 The LEAP Encryption Access Project" - parser = argparse.ArgumentParser(description=""" -Launches Bitmask""", epilog=epilog) + parser = argparse.ArgumentParser( + description="Launches the Bitmask client.", + epilog="Copyright 2012-2014 The LEAP Encryption Access Project") + parser.add_argument('-d', '--debug', action="store_true", - help=("Launches Bitmask in debug mode, writing debug" - "info to stdout")) - if not IS_RELEASE_VERSION: - help_text = "Bypasses the certificate check for bootstrap" - parser.add_argument('--danger', action="store_true", help=help_text) + help=("Launches Bitmask in debug mode, writing debug " + "info to stdout.")) + parser.add_argument('-V', '--version', action="store_true", + help='Displays Bitmask version and exits.') + # files parser.add_argument('-l', '--logfile', metavar="LOG FILE", nargs='?', action="store", dest="log_file", - #type=argparse.FileType('w'), - help='optional log file') + help='Optional log file.') parser.add_argument('-m', '--mail-logfile', metavar="MAIL LOG FILE", nargs='?', action="store", dest="mail_log_file", - #type=argparse.FileType('w'), - help='optional log file for email') + help='Optional log file for email.') + + # flags + parser.add_argument('-s', '--standalone', action="store_true", + help='Makes Bitmask use standalone ' + 'directories for configuration and binary ' + 'searching.') + parser.add_argument('-N', '--no-app-version-check', default=True, + action="store_false", dest="app_version_check", + help='Skip the app version compatibility check with ' + 'the provider.') + parser.add_argument('-M', '--no-api-version-check', default=True, + action="store_false", dest="api_version_check", + help='Skip the api version compatibility check with ' + 'the provider.') + + # openvpn options parser.add_argument('--openvpn-verbosity', nargs='?', type=int, action="store", dest="openvpn_verb", - help='verbosity level for openvpn logs [1-6]') - parser.add_argument('-s', '--standalone', action="store_true", - help='Makes Bitmask use standalone' - 'directories for configuration and binary' - 'searching') - parser.add_argument('-V', '--version', action="store_true", - help='Displays Bitmask version and exits') - parser.add_argument('-r', '--repair-mailboxes', metavar="user@provider", + help='Verbosity level for openvpn logs [1-6]') + + # mail stuff + parser.add_argument('-o', '--offline', action="store_true", + help='Starts Bitmask in offline mode: will not ' + 'try to sync with remote replicas for email.') + + parser.add_argument('--acct', metavar="user@provider", nargs='?', - action="store", dest="acct_to_repair", + action="store", dest="acct", + help='Manipulate mailboxes for this account') + parser.add_argument('-r', '--repair-mailboxes', default=False, + action="store_true", dest="repair", help='Repair mailboxes for a given account. ' 'Use when upgrading versions after a schema ' - 'change.') + 'change. Use with --acct') + parser.add_argument('--import-maildir', metavar="/path/to/Maildir", + nargs='?', + action="store", dest="import_maildir", + help='Import the given maildir. Use with the ' + '--to-mbox flag to import to folders other ' + 'than INBOX. Use with --acct') + + if not IS_RELEASE_VERSION: + help_text = ("Bypasses the certificate check during provider " + "bootstraping, for debugging development servers. " + "Use at your own risk!") + parser.add_argument('--danger', action="store_true", help=help_text) + + # optional cert file used to check domains with self signed certs. + parser.add_argument('--ca-cert-file', metavar="/path/to/cacert.pem", + nargs='?', action="store", dest="ca_cert_file", + help='Uses the given cert file to verify ' + 'against domains.') # Not in use, we might want to reintroduce them. #parser.add_argument('-i', '--no-provider-checks', diff --git a/src/leap/bitmask/util/leap_log_handler.py b/src/leap/bitmask/util/leap_log_handler.py index 1ab62331..807e53d4 100644 --- a/src/leap/bitmask/util/leap_log_handler.py +++ b/src/leap/bitmask/util/leap_log_handler.py @@ -21,6 +21,8 @@ import logging from PySide import QtCore +from leap.bitmask.util import LOG_FORMAT + class LogHandler(logging.Handler): """ @@ -52,10 +54,7 @@ class LogHandler(logging.Handler): :param logging_level: the debug level to define the color. :type logging_level: str. """ - log_format = ('%(asctime)s - %(name)s:%(funcName)s:L#%(lineno)s ' - '- %(levelname)s - %(message)s') - formatter = logging.Formatter(log_format) - + formatter = logging.Formatter(LOG_FORMAT) return formatter def emit(self, logRecord): diff --git a/src/leap/bitmask/util/log_silencer.py b/src/leap/bitmask/util/log_silencer.py index b9f69ad2..56b290e4 100644 --- a/src/leap/bitmask/util/log_silencer.py +++ b/src/leap/bitmask/util/log_silencer.py @@ -46,6 +46,7 @@ class SelectiveSilencerFilter(logging.Filter): # to us. SILENCER_RULES = ( 'leap.common.events', + 'leap.common.decorators', ) def __init__(self): diff --git a/src/leap/bitmask/util/pastebin.py b/src/leap/bitmask/util/pastebin.py new file mode 100755 index 00000000..21b8a0b7 --- /dev/null +++ b/src/leap/bitmask/util/pastebin.py @@ -0,0 +1,814 @@ +#!/usr/bin/env python
+
+#############################################################################
+# Pastebin.py - Python 3.2 Pastebin API.
+# Copyright (C) 2012 Ian Havelock
+#
+# 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/>.
+#
+
+#############################################################################
+
+# This software is a derivative work of:
+# http://winappdbg.sourceforge.net/blog/pastebin.py
+
+#############################################################################
+
+
+__ALL__ = ['delete_paste', 'user_details', 'trending', 'pastes_by_user',
+ 'generate_user_key', 'legacy_paste', 'paste', 'Pastebin',
+ 'PastebinError']
+
+import urllib
+
+
+class PastebinError(RuntimeError):
+ """Pastebin API error.
+
+ The error message returned by the web application is stored as the Python
+ exception message."""
+
+
+class PastebinAPI(object):
+ """Pastebin API interaction object.
+
+ Public functions:
+
+ paste -- Pastes a user-specified file or string using the new API-key POST
+ method.
+
+ legacy_paste -- Pastes a user-specified file or string using the old
+ anonymous POST method.
+
+ generate_user_key -- Generates a session-key that is required for other
+ functions.
+
+ pastes_by_user -- Returns all public pastes submitted by the specified
+ login credentials.
+
+ trending -- Returns the top trending paste.
+
+ user_details -- Returns details about the user for the specified API user
+ key.
+
+ delete_paste -- Adds two numbers together and returns the result."""
+
+ # String to determine bad API requests
+ _bad_request = 'Bad API request'
+
+ # Base domain name
+ _base_domain = 'pastebin.com'
+
+ # Valid Pastebin URLs begin with this string (kinda obvious)
+ _prefix_url = 'http://%s/' % _base_domain
+
+ # Valid Pastebin URLs with a custom subdomain begin with this string
+ _subdomain_url = 'http://%%s.%s/' % _base_domain
+
+ # URL to the LEGACY POST API
+ _legacy_api_url = 'http://%s/api_public.php' % _base_domain
+
+ # URL to the POST API
+ _api_url = 'http://%s/api/api_post.php' % _base_domain
+
+ # URL to the login POST API
+ _api_login_url = 'http://%s/api/api_login.php' % _base_domain
+
+ # Valid paste_expire_date values: Never, 10 minutes, 1 Hour, 1 Day, 1 Month
+ paste_expire_date = ('N', '10M', '1H', '1D', '1M')
+
+ # Valid paste_expire_date values (0 = public, 1 = unlisted, 2 = private)
+ paste_private = ('public', 'unlisted', 'private')
+
+ # Valid parse_format values
+ paste_format = (
+ '4cs', # 4CS
+ '6502acme', # 6502 ACME Cross Assembler
+ '6502kickass', # 6502 Kick Assembler
+ '6502tasm', # 6502 TASM/64TASS
+ 'abap', # ABAP
+ 'actionscript', # ActionScript
+ 'actionscript3', # ActionScript 3
+ 'ada', # Ada
+ 'algol68', # ALGOL 68
+ 'apache', # Apache Log
+ 'applescript', # AppleScript
+ 'apt_sources', # APT Sources
+ 'asm', # ASM (NASM)
+ 'asp', # ASP
+ 'autoconf', # autoconf
+ 'autohotkey', # Autohotkey
+ 'autoit', # AutoIt
+ 'avisynth', # Avisynth
+ 'awk', # Awk
+ 'bascomavr', # BASCOM AVR
+ 'bash', # Bash
+ 'basic4gl', # Basic4GL
+ 'bibtex', # BibTeX
+ 'blitzbasic', # Blitz Basic
+ 'bnf', # BNF
+ 'boo', # BOO
+ 'bf', # BrainFuck
+ 'c', # C
+ 'c_mac', # C for Macs
+ 'cil', # C Intermediate Language
+ 'csharp', # C#
+ 'cpp', # C++
+ 'cpp-qt', # C++ (with QT extensions)
+ 'c_loadrunner', # C: Loadrunner
+ 'caddcl', # CAD DCL
+ 'cadlisp', # CAD Lisp
+ 'cfdg', # CFDG
+ 'chaiscript', # ChaiScript
+ 'clojure', # Clojure
+ 'klonec', # Clone C
+ 'klonecpp', # Clone C++
+ 'cmake', # CMake
+ 'cobol', # COBOL
+ 'coffeescript', # CoffeeScript
+ 'cfm', # ColdFusion
+ 'css', # CSS
+ 'cuesheet', # Cuesheet
+ 'd', # D
+ 'dcs', # DCS
+ 'delphi', # Delphi
+ 'oxygene', # Delphi Prism (Oxygene)
+ 'diff', # Diff
+ 'div', # DIV
+ 'dos', # DOS
+ 'dot', # DOT
+ 'e', # E
+ 'ecmascript', # ECMAScript
+ 'eiffel', # Eiffel
+ 'email', # Email
+ 'epc', # EPC
+ 'erlang', # Erlang
+ 'fsharp', # F#
+ 'falcon', # Falcon
+ 'fo', # FO Language
+ 'f1', # Formula One
+ 'fortran', # Fortran
+ 'freebasic', # FreeBasic
+ 'freeswitch', # FreeSWITCH
+ 'gambas', # GAMBAS
+ 'gml', # Game Maker
+ 'gdb', # GDB
+ 'genero', # Genero
+ 'genie', # Genie
+ 'gettext', # GetText
+ 'go', # Go
+ 'groovy', # Groovy
+ 'gwbasic', # GwBasic
+ 'haskell', # Haskell
+ 'hicest', # HicEst
+ 'hq9plus', # HQ9 Plus
+ 'html4strict', # HTML
+ 'html5', # HTML 5
+ 'icon', # Icon
+ 'idl', # IDL
+ 'ini', # INI file
+ 'inno', # Inno Script
+ 'intercal', # INTERCAL
+ 'io', # IO
+ 'j', # J
+ 'java', # Java
+ 'java5', # Java 5
+ 'javascript', # JavaScript
+ 'jquery', # jQuery
+ 'kixtart', # KiXtart
+ 'latex', # Latex
+ 'lb', # Liberty BASIC
+ 'lsl2', # Linden Scripting
+ 'lisp', # Lisp
+ 'llvm', # LLVM
+ 'locobasic', # Loco Basic
+ 'logtalk', # Logtalk
+ 'lolcode', # LOL Code
+ 'lotusformulas', # Lotus Formulas
+ 'lotusscript', # Lotus Script
+ 'lscript', # LScript
+ 'lua', # Lua
+ 'm68k', # M68000 Assembler
+ 'magiksf', # MagikSF
+ 'make', # Make
+ 'mapbasic', # MapBasic
+ 'matlab', # MatLab
+ 'mirc', # mIRC
+ 'mmix', # MIX Assembler
+ 'modula2', # Modula 2
+ 'modula3', # Modula 3
+ '68000devpac', # Motorola 68000 HiSoft Dev
+ 'mpasm', # MPASM
+ 'mxml', # MXML
+ 'mysql', # MySQL
+ 'newlisp', # newLISP
+ 'text', # None
+ 'nsis', # NullSoft Installer
+ 'oberon2', # Oberon 2
+ 'objeck', # Objeck Programming Langua
+ 'objc', # Objective C
+ 'ocaml-brief', # OCalm Brief
+ 'ocaml', # OCaml
+ 'pf', # OpenBSD PACKET FILTER
+ 'glsl', # OpenGL Shading
+ 'oobas', # Openoffice BASIC
+ 'oracle11', # Oracle 11
+ 'oracle8', # Oracle 8
+ 'oz', # Oz
+ 'pascal', # Pascal
+ 'pawn', # PAWN
+ 'pcre', # PCRE
+ 'per', # Per
+ 'perl', # Perl
+ 'perl6', # Perl 6
+ 'php', # PHP
+ 'php-brief', # PHP Brief
+ 'pic16', # Pic 16
+ 'pike', # Pike
+ 'pixelbender', # Pixel Bender
+ 'plsql', # PL/SQL
+ 'postgresql', # PostgreSQL
+ 'povray', # POV-Ray
+ 'powershell', # Power Shell
+ 'powerbuilder', # PowerBuilder
+ 'proftpd', # ProFTPd
+ 'progress', # Progress
+ 'prolog', # Prolog
+ 'properties', # Properties
+ 'providex', # ProvideX
+ 'purebasic', # PureBasic
+ 'pycon', # PyCon
+ 'python', # Python
+ 'q', # q/kdb+
+ 'qbasic', # QBasic
+ 'rsplus', # R
+ 'rails', # Rails
+ 'rebol', # REBOL
+ 'reg', # REG
+ 'robots', # Robots
+ 'rpmspec', # RPM Spec
+ 'ruby', # Ruby
+ 'gnuplot', # Ruby Gnuplot
+ 'sas', # SAS
+ 'scala', # Scala
+ 'scheme', # Scheme
+ 'scilab', # Scilab
+ 'sdlbasic', # SdlBasic
+ 'smalltalk', # Smalltalk
+ 'smarty', # Smarty
+ 'sql', # SQL
+ 'systemverilog', # SystemVerilog
+ 'tsql', # T-SQL
+ 'tcl', # TCL
+ 'teraterm', # Tera Term
+ 'thinbasic', # thinBasic
+ 'typoscript', # TypoScript
+ 'unicon', # Unicon
+ 'uscript', # UnrealScript
+ 'vala', # Vala
+ 'vbnet', # VB.NET
+ 'verilog', # VeriLog
+ 'vhdl', # VHDL
+ 'vim', # VIM
+ 'visualprolog', # Visual Pro Log
+ 'vb', # VisualBasic
+ 'visualfoxpro', # VisualFoxPro
+ 'whitespace', # WhiteSpace
+ 'whois', # WHOIS
+ 'winbatch', # Winbatch
+ 'xbasic', # XBasic
+ 'xml', # XML
+ 'xorg_conf', # Xorg Config
+ 'xpp', # XPP
+ 'yaml', # YAML
+ 'z80', # Z80 Assembler
+ 'zxbasic', # ZXBasic
+ )
+
+ def __init__(self):
+ pass
+
+ def delete_paste(self, api_dev_key, api_user_key, api_paste_key):
+ """Delete the paste specified by the api_paste_key.
+
+ Usage Example::
+ from pastebin import PastebinAPI
+ x = PastebinAPI()
+ paste_to_delete = x.delete_paste(
+ '453a994e0e2f1efae07f8759e59e075b',
+ 'c57a18e6c0ae228cd4bd16fe36da381a',
+ 'WkgcTFtv')
+ print paste_to_delete
+ Paste Removed
+
+
+ @type api_dev_key: string
+ @param api_dev_key: The API Developer Key of a registered
+ U{http://pastebin.com} account.
+
+ @type api_user_key: string
+ @param api_user_key: The API User Key of a U{http://pastebin.com}
+ registered user.
+
+ @type api_paste_key: string
+ @param api_paste_key: The Paste Key of the paste to be deleted
+ (string after final / in
+ U{http://pastebin.com} URL).
+
+ @rtype: string
+ @returns: A successful deletion returns 'Paste Removed'.
+ """
+
+ # Valid api developer key
+ argv = {'api_dev_key': str(api_dev_key)}
+
+ # Requires pre-registered account
+ if api_user_key is not None:
+ argv['api_user_key'] = str(api_user_key)
+
+ # Key of the paste to be deleted.
+ if api_paste_key is not None:
+ argv['api_paste_key'] = str(api_paste_key)
+
+ # Valid API option - 'user_details' in this instance
+ argv['api_option'] = str('delete')
+
+ # lets try to read the URL that we've just built.
+ request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
+ response = request_string.read()
+
+ return response
+
+ def user_details(self, api_dev_key, api_user_key):
+ """Return user details of the user specified by the api_user_key.
+
+
+ Usage Example::
+ from pastebin import PastebinAPI
+ x = PastebinAPI()
+ details = x.user_details('453a994e0e2f1efae07f8759e59e075b', 'c57a18e6c0ae228cd4bd16fe36da381a')
+ print details
+ <user>
+ <user_name>MonkeyPuzzle</user_name>
+ <user_format_short>python</user_format_short>
+ <user_expiration>N</user_expiration>
+ <user_avatar_url>http://pastebin.com/i/guest.gif</user_avatar_url>
+ <user_private>0</user_private>
+ <user_website></user_website>
+ <user_email>user@email.com</user_email>
+ <user_location></user_location>
+ <user_account_type>0</user_account_type>
+ </user>
+
+
+ @type api_dev_key: string
+ @param api_dev_key: The API Developer Key of a registered
+ U{http://pastebin.com} account.
+
+ @type api_user_key: string
+ @param api_user_key: The API User Key of a U{http://pastebin.com}
+ registered user.
+
+ @rtype: string
+ @returns: Returns an XML string containing user information.
+ """
+
+ # Valid api developer key
+ argv = {'api_dev_key': str(api_dev_key)}
+
+ # Requires pre-registered account to generate an api_user_key
+ # (see generate_user_key)
+ if api_user_key is not None:
+ argv['api_user_key'] = str(api_user_key)
+
+ # Valid API option - 'user_details' in this instance
+ argv['api_option'] = str('userdetails')
+
+ # lets try to read the URL that we've just built.
+ request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
+ response = request_string.read()
+
+ # do some basic error checking here so we can gracefully handle any
+ # errors we are likely to encounter
+ if response.startswith(self._bad_request):
+ raise PastebinError(response)
+
+ elif not response.startswith('<user>'):
+ raise PastebinError(response)
+
+ return response
+
+ def trending(self, api_dev_key):
+ """Returns the top trending paste details.
+
+
+ Usage Example::
+ from pastebin import PastebinAPI
+ x = PastebinAPI()
+ details = x.trending('453a994e0e2f1efae07f8759e59e075b')
+ print details
+ <paste>
+ <paste_key>jjMRFDH6</paste_key>
+ <paste_date>1333230838</paste_date>
+ <paste_title></paste_title>
+ <paste_size>6416</paste_size>
+ <paste_expire_date>0</paste_expire_date>
+ <paste_private>0</paste_private>
+ <paste_format_long>None</paste_format_long>
+ <paste_format_short>text</paste_format_short>
+ <paste_url>http://pastebin.com/jjMRFDH6</paste_url>
+ <paste_hits>6384</paste_hits>
+ </paste>
+
+ Note: Returns multiple trending pastes, not just 1.
+
+
+ @type api_dev_key: string
+ @param api_dev_key: The API Developer Key of a registered
+ U{http://pastebin.com} account.
+
+
+ @rtype: string
+ @return: Returns the string (XML formatted) containing the top
+ trending pastes.
+ """
+
+ # Valid api developer key
+ argv = {'api_dev_key': str(api_dev_key), 'api_option': str('trends')}
+
+ # Valid API option - 'trends' is returns trending pastes
+
+ # lets try to read the URL that we've just built.
+ request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
+ response = request_string.read()
+
+ # do some basic error checking here so we can gracefully handle any
+ # errors we are likely to encounter
+ if response.startswith(self._bad_request):
+ raise PastebinError(response)
+
+ elif not response.startswith('<paste>'):
+ raise PastebinError(response)
+
+ return response
+
+ def pastes_by_user(self, api_dev_key, api_user_key, results_limit=None):
+ """Returns all pastes for the provided api_user_key.
+
+
+ Usage Example::
+ from pastebin import PastebinAPI
+ x = PastebinAPI()
+ details = x.user_details('453a994e0e2f1efae07f8759e59e075b',
+ 'c57a18e6c0ae228cd4bd16fe36da381a',
+ 100)
+ print details
+ <paste>
+ <paste_key>DLiSspYT</paste_key>
+ <paste_date>1332714730</paste_date>
+ <paste_title>Pastebin.py - Python 3.2 Pastebin.com API</paste_title>
+ <paste_size>25300</paste_size>
+ <paste_expire_date>0</paste_expire_date>
+ <paste_private>0</paste_private>
+ <paste_format_long>Python</paste_format_long>
+ <paste_format_short>python</paste_format_short>
+ <paste_url>http://pastebin.com/DLiSspYT</paste_url>
+ <paste_hits>70</paste_hits>
+ </paste>
+
+ Note: Returns multiple pastes, not just 1.
+
+
+ @type api_dev_key: string
+ @param api_dev_key: The API Developer Key of a registered
+ U{http://pastebin.com} account.
+
+ @type api_user_key: string
+ @param api_user_key: The API User Key of a U{http://pastebin.com}
+ registered user.
+
+ @type results_limit: number
+ @param results_limit: The number of pastes to return between 1 - 1000.
+
+ @rtype: string
+ @returns: Returns an XML string containing number of specified pastes
+ by user.
+ """
+
+ # Valid api developer key
+ argv = {'api_dev_key': str(api_dev_key)}
+
+ # Requires pre-registered account
+ if api_user_key is not None:
+ argv['api_user_key'] = str(api_user_key)
+
+ # Number of results to return - between 1 & 1000, default = 50
+ if results_limit is None:
+ argv['api_results_limit'] = 50
+
+ if results_limit is not None:
+ if results_limit < 1:
+ argv['api_results_limit'] = 50
+ elif results_limit > 1000:
+ argv['api_results_limit'] = 1000
+ else:
+ argv['api_results_limit'] = int(results_limit)
+
+ # Valid API option - 'paste' is default for new paste
+ argv['api_option'] = str('list')
+
+ # lets try to read the URL that we've just built.
+ request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
+ response = request_string.read()
+
+ # do some basic error checking here so we can gracefully handle any
+ # errors we are likely to encounter
+ if response.startswith(self._bad_request):
+ raise PastebinError(response)
+
+ elif not response.startswith('<paste>'):
+ raise PastebinError(response)
+
+ return response
+
+ def generate_user_key(self, api_dev_key, username, password):
+ """Generate a user session key - needed for other functions.
+
+
+ Usage Example::
+ from pastebin import PastebinAPI
+ x = PastebinAPI()
+ my_key = x.generate_user_key(
+ '453a994e0e2f1efae07f8759e59e075b',
+ 'MonkeyPuzzle',
+ '12345678')
+ print my_key
+ c57a18e6c0ae228cd4bd16fe36da381a
+
+
+ @type api_dev_key: string
+ @param api_dev_key: The API Developer Key of a registered
+ U{http://pastebin.com} account.
+
+ @type username: string
+ @param username: The username of a registered U{http://pastebin.com}
+ account.
+
+ @type password: string
+ @param password: The password of a registered U{http://pastebin.com}
+ account.
+
+ @rtype: string
+ @returns: Session key (api_user_key) to allow authenticated
+ interaction to the API.
+
+ """
+ # Valid api developer key
+ argv = {'api_dev_key': str(api_dev_key)}
+
+ # Requires pre-registered pastebin account
+ if username is not None:
+ argv['api_user_name'] = str(username)
+
+ # Requires pre-registered pastebin account
+ if password is not None:
+ argv['api_user_password'] = str(password)
+
+ # lets try to read the URL that we've just built.
+ data = urllib.urlencode(argv)
+ request_string = urllib.urlopen(self._api_login_url, data)
+ response = request_string.read()
+
+ # do some basic error checking here so we can gracefully handle
+ # any errors we are likely to encounter
+ if response.startswith(self._bad_request):
+ raise PastebinError(response)
+
+ return response
+
+ def paste(self, api_dev_key, api_paste_code,
+ api_user_key=None, paste_name=None, paste_format=None,
+ paste_private=None, paste_expire_date=None):
+
+ """Submit a code snippet to Pastebin using the new API.
+
+
+ Usage Example::
+ from pastebin import PastebinAPI
+ x = PastebinAPI()
+ url = x.paste(
+ '453a994e0e2f1efae07f8759e59e075b' ,
+ 'Snippet of code to paste goes here',
+ paste_name = 'title of paste',
+ api_user_key = 'c57a18e6c0ae228cd4bd16fe36da381a',
+ paste_format = 'python',
+ paste_private = 'unlisted',
+ paste_expire_date = '10M')
+ print url
+ http://pastebin.com/tawPUgqY
+
+
+ @type api_dev_key: string
+ @param api_dev_key: The API Developer Key of a registered
+ U{http://pastebin.com} account.
+
+ @type api_paste_code: string
+ @param api_paste_code: The file or string to paste to body of the
+ U{http://pastebin.com} paste.
+
+ @type api_user_key: string
+ @param api_user_key: The API User Key of a U{http://pastebin.com}
+ registered user.
+ If none specified, paste is made as a guest.
+
+ @type paste_name: string
+ @param paste_name: (Optional) Title of the paste.
+ Default is to paste anonymously.
+
+ @type paste_format: string
+ @param paste_format: (Optional) Programming language of the code being
+ pasted. This enables syntax highlighting when reading the code in
+ U{http://pastebin.com}. Default is no syntax highlighting (text is
+ just text and not source code).
+
+ @type paste_private: string
+ @param paste_private: (Optional) C{'public'} if the paste is public
+ (visible by everyone), C{'unlisted'} if it's public but not
+ searchable. C{'private'} if the paste is private and not
+ searchable or indexed.
+ The Pastebin FAQ (U{http://pastebin.com/faq}) claims
+ private pastes are not indexed by search engines (aka Google).
+
+ @type paste_expire_date: str
+ @param paste_expire_date: (Optional) Expiration date for the paste.
+ Once past this date the paste is deleted automatically. Valid
+ values are found in the L{PastebinAPI.paste_expire_date} class
+ member.
+ If not provided, the paste never expires.
+
+ @rtype: string
+ @return: Returns the URL to the newly created paste.
+ """
+
+ # Valid api developer key
+ argv = {'api_dev_key': str(api_dev_key)}
+
+ # Code snippet to submit
+ if api_paste_code is not None:
+ argv['api_paste_code'] = str(api_paste_code)
+
+ # Valid API option - 'paste' is default for new paste
+ argv['api_option'] = str('paste')
+
+ # API User Key
+ if api_user_key is not None:
+ argv['api_user_key'] = str(api_user_key)
+ elif api_user_key is None:
+ argv['api_user_key'] = str('')
+
+ # Name of the poster
+ if paste_name is not None:
+ argv['api_paste_name'] = str(paste_name)
+
+ # Syntax highlighting
+ if paste_format is not None:
+ paste_format = str(paste_format).strip().lower()
+ argv['api_paste_format'] = paste_format
+
+ # Is the snippet private?
+ if paste_private is not None:
+ if paste_private == 'public':
+ argv['api_paste_private'] = int(0)
+ elif paste_private == 'unlisted':
+ argv['api_paste_private'] = int(1)
+ elif paste_private == 'private':
+ argv['api_paste_private'] = int(2)
+
+ # Expiration for the snippet
+ if paste_expire_date is not None:
+ paste_expire_date = str(paste_expire_date).strip().upper()
+ argv['api_paste_expire_date'] = paste_expire_date
+
+ # lets try to read the URL that we've just built.
+ request_string = urllib.urlopen(self._api_url, urllib.urlencode(argv))
+ response = request_string.read()
+
+ # do some basic error checking here so we can gracefully handle any
+ # errors we are likely to encounter
+ if response.startswith(self._bad_request):
+ raise PastebinError(response)
+ elif not response.startswith(self._prefix_url):
+ raise PastebinError(response)
+
+ return response
+
+ def legacy_paste(self, paste_code,
+ paste_name=None, paste_private=None,
+ paste_expire_date=None, paste_format=None):
+ """Unofficial python interface to the Pastebin legacy API.
+
+ Unlike the official API, this one doesn't require an API key, so it's
+ virtually anonymous.
+
+
+ Usage Example::
+ from pastebin import PastebinAPI
+ x = PastebinAPI()
+ url = x.legacy_paste('Snippet of code to paste goes here',
+ paste_name = 'title of paste',
+ paste_private = 'unlisted',
+ paste_expire_date = '10M',
+ paste_format = 'python')
+ print url
+ http://pastebin.com/tawPUgqY
+
+
+ @type paste_code: string
+ @param paste_code: The file or string to paste to body of the
+ U{http://pastebin.com} paste.
+
+ @type paste_name: string
+ @param paste_name: (Optional) Title of the paste.
+ Default is to paste with no title.
+
+ @type paste_private: string
+ @param paste_private: (Optional) C{'public'} if the paste is public
+ (visible by everyone), C{'unlisted'} if it's public but not
+ searchable. C{'private'} if the paste is private and not
+ searchable or indexed.
+ The Pastebin FAQ (U{http://pastebin.com/faq}) claims
+ private pastes are not indexed by search engines (aka Google).
+
+ @type paste_expire_date: string
+ @param paste_expire_date: (Optional) Expiration date for the paste.
+ Once past this date the paste is deleted automatically. Valid
+ values are found in the L{PastebinAPI.paste_expire_date} class
+ member.
+ If not provided, the paste never expires.
+
+ @type paste_format: string
+ @param paste_format: (Optional) Programming language of the code being
+ pasted. This enables syntax highlighting when reading the code in
+ U{http://pastebin.com}. Default is no syntax highlighting (text is
+ just text and not source code).
+
+ @rtype: string
+ @return: Returns the URL to the newly created paste.
+ """
+
+ # Code snippet to submit
+ argv = {'paste_code': str(paste_code)}
+
+ # Name of the poster
+ if paste_name is not None:
+ argv['paste_name'] = str(paste_name)
+
+ # Is the snippet private?
+ if paste_private is not None:
+ argv['paste_private'] = int(bool(int(paste_private)))
+
+ # Expiration for the snippet
+ if paste_expire_date is not None:
+ paste_expire_date = str(paste_expire_date).strip().upper()
+ argv['paste_expire_date'] = paste_expire_date
+
+ # Syntax highlighting
+ if paste_format is not None:
+ paste_format = str(paste_format).strip().lower()
+ argv['paste_format'] = paste_format
+
+ # lets try to read the URL that we've just built.
+ data = urllib.urlencode(argv)
+ request_string = urllib.urlopen(self._legacy_api_url, data)
+ response = request_string.read()
+
+ # do some basic error checking here so we can gracefully handle any
+ # errors we are likely to encounter
+ if response.startswith(self._bad_request):
+ raise PastebinError(response)
+ elif not response.startswith(self._prefix_url):
+ raise PastebinError(response)
+
+ return response
+
+
+######################################################
+
+delete_paste = PastebinAPI.delete_paste
+user_details = PastebinAPI.user_details
+trending = PastebinAPI.trending
+pastes_by_user = PastebinAPI.pastes_by_user
+generate_user_key = PastebinAPI.generate_user_key
+legacy_paste = PastebinAPI.legacy_paste
+paste = PastebinAPI.paste
|