summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/leap/bitmask/__init__.py1
-rw-r--r--src/leap/bitmask/_appname.py1
-rw-r--r--src/leap/bitmask/app.py106
-rw-r--r--src/leap/bitmask/backend.py489
-rw-r--r--src/leap/bitmask/config/flags.py20
-rw-r--r--src/leap/bitmask/config/leapsettings.py38
-rw-r--r--src/leap/bitmask/config/providerconfig.py10
-rw-r--r--src/leap/bitmask/crypto/srpauth.py128
-rw-r--r--src/leap/bitmask/crypto/srpregister.py63
-rwxr-xr-xsrc/leap/bitmask/crypto/tests/fake_provider.py1
-rw-r--r--src/leap/bitmask/crypto/tests/test_srpauth.py8
-rw-r--r--src/leap/bitmask/gui/advanced_key_management.py38
-rw-r--r--src/leap/bitmask/gui/eip_preferenceswindow.py21
-rw-r--r--src/leap/bitmask/gui/eip_status.py10
-rw-r--r--src/leap/bitmask/gui/loggerwindow.py82
-rw-r--r--src/leap/bitmask/gui/login.py52
-rw-r--r--src/leap/bitmask/gui/mail_status.py55
-rw-r--r--src/leap/bitmask/gui/mainwindow.py725
-rw-r--r--src/leap/bitmask/gui/preferenceswindow.py10
-rw-r--r--src/leap/bitmask/gui/twisted_main.py18
-rw-r--r--src/leap/bitmask/gui/ui/advanced_key_management.ui79
-rw-r--r--src/leap/bitmask/gui/ui/eip_status.ui66
-rw-r--r--src/leap/bitmask/gui/ui/loggerwindow.ui15
-rw-r--r--src/leap/bitmask/gui/ui/login.ui45
-rw-r--r--src/leap/bitmask/gui/ui/mainwindow.ui195
-rw-r--r--src/leap/bitmask/gui/ui/wizard.ui90
-rw-r--r--src/leap/bitmask/gui/wizard.py244
-rw-r--r--src/leap/bitmask/platform_init/locks.py2
-rw-r--r--src/leap/bitmask/provider/__init__.py33
-rw-r--r--src/leap/bitmask/provider/providerbootstrapper.py103
-rw-r--r--src/leap/bitmask/provider/supportedapis.py38
-rw-r--r--src/leap/bitmask/provider/tests/test_providerbootstrapper.py11
-rw-r--r--src/leap/bitmask/services/abstractbootstrapper.py40
-rw-r--r--src/leap/bitmask/services/eip/darwinvpnlauncher.py2
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py12
-rw-r--r--src/leap/bitmask/services/mail/conductor.py30
-rw-r--r--src/leap/bitmask/services/mail/connection.py2
-rw-r--r--src/leap/bitmask/services/mail/imap.py12
-rw-r--r--src/leap/bitmask/services/mail/plumber.py341
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py343
-rw-r--r--src/leap/bitmask/util/__init__.py43
-rw-r--r--src/leap/bitmask/util/constants.py1
-rw-r--r--src/leap/bitmask/util/keyring_helpers.py65
-rw-r--r--src/leap/bitmask/util/leap_argparse.py81
-rw-r--r--src/leap/bitmask/util/leap_log_handler.py7
-rw-r--r--src/leap/bitmask/util/log_silencer.py1
-rwxr-xr-xsrc/leap/bitmask/util/pastebin.py814
47 files changed, 3608 insertions, 983 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 3bb9c8c3..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,11 +50,16 @@ 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 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')
@@ -72,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.
@@ -98,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:
@@ -115,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)
@@ -137,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
@@ -162,28 +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)
+
+
+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.
@@ -191,11 +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
@@ -206,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 "
@@ -217,6 +264,7 @@ def main():
logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
logger.info('Bitmask version %s', VERSION)
+ logger.info('leap.mail version %s', MAIL_VERSION)
logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
logger.info('Starting app')
@@ -230,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()
@@ -275,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
new file mode 100644
index 00000000..45ea451c
--- /dev/null
+++ b/src/leap/bitmask/backend.py
@@ -0,0 +1,489 @@
+# -*- coding: utf-8 -*-
+# backend.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/>.
+"""
+Backend for everything
+"""
+import logging
+
+from functools import partial
+from Queue import Queue, Empty
+
+from twisted.internet import threads, defer
+from twisted.internet.task import LoopingCall
+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
+from PySide import QtCore
+
+logger = logging.getLogger(__name__)
+
+
+class ILEAPComponent(zope.interface.Interface):
+ """
+ Interface that every component for the backend should comply to
+ """
+
+ key = zope.interface.Attribute("Key id for this component")
+
+
+class ILEAPService(ILEAPComponent):
+ """
+ Interface that every Service needs to implement
+ """
+
+ def start(self):
+ """
+ Starts the service.
+ """
+ pass
+
+ def stop(self):
+ """
+ Stops the service.
+ """
+ pass
+
+ def terminate(self):
+ """
+ Terminates the service, not necessarily in a nice way.
+ """
+ pass
+
+ def status(self):
+ """
+ Returns a json object with the current status for the service.
+
+ :rtype: object (list, str, dict)
+ """
+ # XXX: Use a namedtuple or a specific object instead of a json
+ # object, since parsing it will be problematic otherwise.
+ # It has to be something easily serializable though.
+ pass
+
+ def set_configs(self, keyval):
+ """
+ Sets the config parameters for this Service.
+
+ :param keyval: values to configure
+ :type keyval: dict, {str: str}
+ """
+ pass
+
+ def get_configs(self, keys):
+ """
+ Returns the configuration values for the list of keys.
+
+ :param keys: keys to retrieve
+ :type keys: list of str
+
+ :rtype: dict, {str: str}
+ """
+ pass
+
+
+class Provider(object):
+ """
+ Interfaces with setup and bootstrapping operations for a provider
+ """
+
+ zope.interface.implements(ILEAPComponent)
+
+ PROBLEM_SIGNAL = "prov_problem_with_provider"
+
+ def __init__(self, signaler=None, bypass_checks=False):
+ """
+ Constructor for the Provider component
+
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ :param bypass_checks: Set to true if the app should bypass
+ first round of checks for CA
+ certificates at bootstrap
+ :type bypass_checks: bool
+ """
+ object.__init__(self)
+ self.key = "provider"
+ self._provider_bootstrapper = ProviderBootstrapper(signaler,
+ bypass_checks)
+ self._download_provider_defer = None
+ self._provider_config = ProviderConfig()
+
+ def setup_provider(self, provider):
+ """
+ Initiates the setup 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
+ """
+ log.msg("Setting up provider %s..." % (provider.encode("idna"),))
+ pb = self._provider_bootstrapper
+ d = pb.run_provider_select_checks(provider, download_if_needed=True)
+ 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(get_provider_path(provider))
+
+ if self._provider_config.loaded():
+ d = self._provider_bootstrapper.run_provider_setup_checks(
+ self._provider_config,
+ download_if_needed=True)
+ else:
+ if self._signaler is not None:
+ self._signaler.signal(self.PROBLEM_SIGNAL)
+ logger.error("Could not load provider configuration.")
+ self._login_widget.set_enabled(True)
+
+ if d is None:
+ d = defer.Deferred()
+ 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.
+
+ This is intended for the separation in frontend/backend, this will
+ live in the frontend.
+ """
+
+ ####################
+ # 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)
+
+ prov_download_ca_cert = QtCore.Signal(object)
+ prov_check_ca_fingerprint = QtCore.Signal(object)
+ prov_check_api_certificate = QtCore.Signal(object)
+
+ prov_problem_with_provider = QtCore.Signal(object)
+
+ 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
+ PROV_NAME_RESOLUTION_KEY = "prov_name_resolution"
+ PROV_HTTPS_CONNECTION_KEY = "prov_https_connection"
+ PROV_DOWNLOAD_PROVIDER_INFO_KEY = "prov_download_provider_info"
+ 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_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):
+ """
+ Constructor for the Signaler
+ """
+ QtCore.QObject.__init__(self)
+ self._signals = {}
+
+ signals = [
+ self.PROV_NAME_RESOLUTION_KEY,
+ self.PROV_HTTPS_CONNECTION_KEY,
+ self.PROV_DOWNLOAD_PROVIDER_INFO_KEY,
+ self.PROV_DOWNLOAD_CA_CERT_KEY,
+ self.PROV_CHECK_CA_FINGERPRINT_KEY,
+ self.PROV_CHECK_API_CERTIFICATE_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:
+ self._signals[sig] = getattr(self, sig)
+
+ def signal(self, key, data=None):
+ """
+ Emits a Qt signal based on the key provided, with the data if provided.
+
+ :param key: string identifying the signal to emit
+ :type key: str
+ :param data: object to send with the data
+ :type data: object
+
+ NOTE: The data object will be a serialized str in the backend,
+ and an unserialized object in the frontend, but for now we
+ just care about objects.
+ """
+ # Right now it emits Qt signals. The backend version of this
+ # 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:
+ log.msg("Unknown key for signal %s!" % (key,))
+
+
+class Backend(object):
+ """
+ Backend for everything, the UI should only use this class.
+ """
+
+ PASSED_KEY = "passed"
+ ERROR_KEY = "error"
+
+ def __init__(self, bypass_checks=False):
+ """
+ Constructor for the backend.
+ """
+ object.__init__(self)
+
+ # Components map for the commands received
+ self._components = {}
+
+ # Ongoing defers that will be cancelled at stop time
+ self._ongoing_defers = []
+
+ # Signaler object to translate commands into Qt signals
+ self._signaler = Signaler()
+
+ # 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
+ # object, but it'll become the zmq recv_multipart queue
+ self._lc = LoopingCall(threads.deferToThread, self._worker)
+
+ # Temporal call_queue for worker, will be replaced with
+ # recv_multipart os something equivalent in the loopingcall
+ self._call_queue = Queue()
+
+ @property
+ def signaler(self):
+ """
+ Public signaler access to let the UI connect to its signals.
+ """
+ return self._signaler
+
+ def start(self):
+ """
+ Starts the looping call
+ """
+ log.msg("Starting worker...")
+ self._lc.start(0.01)
+
+ def stop(self):
+ """
+ Stops the looping call and tries to cancel all the defers.
+ """
+ log.msg("Stopping worker...")
+ if self._lc.running:
+ self._lc.stop()
+ while len(self._ongoing_defers) > 0:
+ d = self._ongoing_defers.pop()
+ d.cancel()
+
+ def _register(self, component):
+ """
+ Registers a component in this backend
+
+ :param component: Component to register
+ :type component: any object that implements ILEAPComponent
+ """
+ # TODO: assert that the component implements the interfaces
+ # expected
+ try:
+ self._components[component.key] = component
+ except Exception:
+ log.msg("There was a problem registering %s" % (component,))
+ log.err()
+
+ def _signal_back(self, _, signal):
+ """
+ Helper method to signal back (callback like behavior) to the
+ UI that an operation finished.
+
+ :param signal: signal name
+ :type signal: str
+ """
+ self._signaler.signal(signal)
+
+ def _worker(self):
+ """
+ Worker method, called from a different thread and as a part of
+ a looping call
+ """
+ try:
+ # this'll become recv_multipart
+ cmd = self._call_queue.get(block=False)
+
+ # cmd is: component, method, signalback, *args
+ func = getattr(self._components[cmd[0]], cmd[1])
+ d = func(*cmd[3:])
+ 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()
+
+ def _done_action(self, _, d):
+ """
+ Remover of the defer once it's done
+
+ :param d: defer to remove
+ :type d: twisted.internet.defer.Deferred
+ """
+ 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
+ # this in two processes, the methods bellow can be changed to
+ # send_multipart and this backend class will be really simple.
+
+ 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 98395def..5d8bc9b3 100644
--- a/src/leap/bitmask/config/flags.py
+++ b/src/leap/bitmask/config/flags.py
@@ -30,3 +30,23 @@ WARNING: You should NOT use this kind of flags unless you're sure of what
# - search for binaries inside the bundled app instead of the system ones.
# e.g.: openvpn, gpg
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 2c0fa034..cbc8c3e3 100644
--- a/src/leap/bitmask/gui/advanced_key_management.py
+++ b/src/leap/bitmask/gui/advanced_key_management.py
@@ -48,20 +48,25 @@ 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.container.setEnabled(False)
+ self.ui.gbMyKeyPair.setEnabled(False)
+ self.ui.gbStoredPublicKeys.setEnabled(False)
msg = self.tr("<span style='color:#0000FF;'>NOTE</span>: "
"To use this, you need to enable/start {0}.")
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
@@ -79,6 +84,12 @@ class AdvancedKeyManagement(QtGui.QWidget):
self.ui.pbImportKeys.clicked.connect(self._import_keys)
self.ui.pbExportKeys.clicked.connect(self._export_keys)
+ # Stretch columns to content
+ self.ui.twPublicKeys.horizontalHeader().setResizeMode(
+ 0, QtGui.QHeaderView.Stretch)
+
+ self._list_keys()
+
def _import_keys(self):
"""
Imports the user's key pair.
@@ -183,3 +194,16 @@ class AdvancedKeyManagement(QtGui.QWidget):
return
else:
logger.debug('Export canceled by the user.')
+
+ def _list_keys(self):
+ """
+ Loads all the public keys stored in the local db to the keys table.
+ """
+ keys = self._keymanager.get_all_keys_in_local_db()
+
+ keys_table = self.ui.twPublicKeys
+ for key in keys:
+ row = keys_table.rowCount()
+ keys_table.insertRow(row)
+ keys_table.setItem(row, 0, QtGui.QTableWidgetItem(key.address))
+ keys_table.setItem(row, 1, QtGui.QTableWidgetItem(key.key_id))
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 4b4d360f..19942d9d 100644
--- a/src/leap/bitmask/gui/eip_status.py
+++ b/src/leap/bitmask/gui/eip_status.py
@@ -41,8 +41,8 @@ class EIPStatusWidget(QtGui.QWidget):
EIP Status widget that displays the current state of the EIP service
"""
DISPLAY_TRAFFIC_RATES = True
- RATE_STR = "%14.2f KB/s"
- TOTAL_STR = "%14.2f Kb"
+ RATE_STR = "%1.2f KB/s"
+ TOTAL_STR = "%1.2f Kb"
eip_connection_connected = QtCore.Signal()
@@ -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 929919ac..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 os
+import socket
+
+from threading import Condition
+from datetime import datetime
from PySide import QtCore, QtGui
-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,11 +49,12 @@ 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.provider.providerbootstrapper import ProviderBootstrapper
-from leap.bitmask.services import get_service_display_name, EIP_SERVICE
+from leap.bitmask import backend
+
+from leap.bitmask.services import get_service_display_name
from leap.bitmask.services.mail import conductor as mail_conductor
@@ -66,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
@@ -77,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__)
@@ -93,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([])
@@ -102,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):
@@ -132,11 +152,14 @@ 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()
self._settings = LeapSettings()
@@ -173,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(
@@ -180,36 +208,23 @@ class MainWindow(QtGui.QMainWindow):
# This is loaded only once, there's a bug when doing that more
# than once
- self._provider_config = ProviderConfig()
+ # XXX HACK!! But we need it as long as we are using
+ # provider_config in here
+ self._provider_config = (
+ self._backend._components["provider"]._provider_config)
# Used for automatic start of EIP
self._provisional_provider_config = ProviderConfig()
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
- # This thread is always running, although it's quite
- # lightweight when it's done setting up provider
- # configuration and certificate.
- self._provider_bootstrapper = ProviderBootstrapper(bypass_checks)
-
- # Intermediate stages, only do something if there was an error
- self._provider_bootstrapper.name_resolution.connect(
- self._intermediate_stage)
- self._provider_bootstrapper.https_connection.connect(
- self._intermediate_stage)
- self._provider_bootstrapper.download_ca_cert.connect(
- self._intermediate_stage)
-
- # Important stages, loads the provider config and checks
- # certificates
- self._provider_bootstrapper.download_provider_info.connect(
- self._load_provider_config)
- self._provider_bootstrapper.check_api_certificate.connect(
- self._provider_config_loaded)
+ self._backend_connect()
# This thread is similar to the provider bootstrapper
self._eip_bootstrapper = EIPBootstrapper()
@@ -243,15 +258,23 @@ 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)
+ self.ui.action_preferences.triggered.connect(self._show_preferences)
+ self.ui.action_eip_preferences.triggered.connect(
+ self._show_eip_preferences)
self.ui.action_about_leap.triggered.connect(self._about)
self.ui.action_quit.triggered.connect(self.quit)
self.ui.action_wizard.triggered.connect(self._launch_wizard)
self.ui.action_show_logs.triggered.connect(self._show_logger_window)
+ self.ui.action_help.triggered.connect(self._help)
self.ui.action_create_new_account.triggered.connect(
self._launch_wizard)
@@ -279,13 +302,15 @@ class MainWindow(QtGui.QMainWindow):
self._action_visible = QtGui.QAction(self.tr("Hide Main Window"), self)
self._action_visible.triggered.connect(self._toggle_visible)
- self.ui.btnPreferences.clicked.connect(self._show_preferences)
- self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences)
+ # disable buttons for now, may come back later.
+ # self.ui.btnPreferences.clicked.connect(self._show_preferences)
+ # self.ui.btnEIPPreferences.clicked.connect(self._show_eip_preferences)
self._enabled_services = []
- self._center_window()
+ # last minute UI manipulations
+ self._center_window()
self.ui.lblNewUpdates.setVisible(False)
self.ui.btnMore.setVisible(False)
#########################################
@@ -294,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)
@@ -323,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)
@@ -345,7 +372,9 @@ class MainWindow(QtGui.QMainWindow):
if self._first_run():
self._wizard_firstrun = True
- self._wizard = Wizard(bypass_checks=bypass_checks)
+ self._backend_disconnect()
+ self._wizard = Wizard(backend=self._backend,
+ bypass_checks=bypass_checks)
# Give this window time to finish init and then show the wizard
QtCore.QTimer.singleShot(1, self._launch_wizard)
self._wizard.accepted.connect(self._finish_init)
@@ -355,6 +384,42 @@ class MainWindow(QtGui.QMainWindow):
# so this has to be done after eip_machine is started
self._finish_init()
+ def _backend_connect(self):
+ """
+ Helper to connect to backend signals
+ """
+ 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)
+
+ 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
+ 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):
+ """
+ Helper to disconnect from backend signals.
+
+ Some signals are emitted from the wizard, and we want to
+ ignore those.
+ """
+ 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)
+
+ sig.prov_download_provider_info.disconnect(self._load_provider_config)
+ sig.prov_check_api_certificate.disconnect(self._provider_config_loaded)
+
def _rejected_wizard(self):
"""
SLOT
@@ -375,7 +440,9 @@ class MainWindow(QtGui.QMainWindow):
# This happens if the user finishes the provider
# setup but does not register
self._wizard = None
- self._finish_init()
+ self._backend_connect()
+ if self._wizard_firstrun:
+ self._finish_init()
def _launch_wizard(self):
"""
@@ -390,9 +457,11 @@ class MainWindow(QtGui.QMainWindow):
there.
"""
if self._wizard is None:
- self._wizard = Wizard(bypass_checks=self._bypass_checks)
+ self._backend_disconnect()
+ 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!
@@ -467,26 +536,66 @@ class MainWindow(QtGui.QMainWindow):
"""
SLOT
TRIGGERS:
- self.ui.btnPreferences.clicked
+ self.ui.btnPreferences.clicked (disabled for now)
+ self.ui.action_preferences
Displays the preferences window.
"""
- preferences_window = PreferencesWindow(
+ preferences = PreferencesWindow(
self, self._srp_auth, self._provider_config, self._soledad,
self._login_widget.get_selected_provider())
- self.soledad_ready.connect(preferences_window.set_soledad_ready)
- preferences_window.show()
+ self.soledad_ready.connect(preferences.set_soledad_ready)
+ preferences.show()
+ preferences.preferences_saved.connect(self._update_eip_enabled_status)
+
+ def _update_eip_enabled_status(self):
+ """
+ SLOT
+ TRIGGER:
+ PreferencesWindow.preferences_saved
+
+ Enable or disable the EIP start/stop actions and stop EIP if the user
+ disabled that service.
+
+ :returns: if the eip actions were enabled or disabled
+ :rtype: bool
+ """
+ settings = self._settings
+ default_provider = settings.get_defaultprovider()
+ enabled_services = []
+ if default_provider is not None:
+ enabled_services = settings.get_enabled_services(default_provider)
+
+ eip_enabled = False
+ if EIP_SERVICE in enabled_services:
+ should_autostart = settings.get_autostart_eip()
+ if should_autostart and default_provider is not None:
+ self._eip_status.enable_eip_start()
+ self._eip_status.set_eip_status("")
+ eip_enabled = True
+ else:
+ # we don't have an usable provider
+ # so the user needs to log in first
+ self._eip_status.disable_eip_start()
+ else:
+ self._stop_eip()
+ self._eip_status.disable_eip_start()
+ self._eip_status.set_eip_status(self.tr("Disabled"))
+
+ return eip_enabled
def _show_eip_preferences(self):
"""
SLOT
TRIGGERS:
self.ui.btnEIPPreferences.clicked
+ self.ui.action_eip_preferences (disabled for now)
Displays the EIP preferences window.
"""
- EIPPreferencesWindow(self).show()
+ domain = self._login_widget.get_selected_provider()
+ EIPPreferencesWindow(self, domain).show()
#
# updates
@@ -585,6 +694,7 @@ class MainWindow(QtGui.QMainWindow):
self.eip_needs_login.emit()
self._wizard = None
+ self._backend_connect()
else:
self._try_autostart_eip()
@@ -626,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
#
@@ -748,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>"
@@ -763,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):
"""
@@ -812,51 +989,36 @@ class MainWindow(QtGui.QMainWindow):
"""
# XXX should rename this provider, name clash.
provider = self._login_widget.get_selected_provider()
-
- pb = self._provider_bootstrapper
- d = pb.run_provider_select_checks(provider, download_if_needed=True)
- self._download_provider_defer = d
+ self._backend.setup_provider(provider)
def _load_provider_config(self, data):
"""
SLOT
- TRIGGER: self._provider_bootstrapper.download_provider_info
+ TRIGGER: self._backend.signaler.prov_download_provider_info
Once the provider config has been downloaded, this loads the
self._provider_config instance with it and starts the second
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._provider_bootstrapper.PASSED_KEY]:
- # XXX should rename this provider, name clash.
- provider = self._login_widget.get_selected_provider()
-
- # 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"))
-
- if self._provider_config.loaded():
- self._provider_bootstrapper.run_provider_setup_checks(
- self._provider_config,
- download_if_needed=True)
- else:
- self._login_widget.set_status(
- self.tr("Unable to login: Problem with provider"))
- logger.error("Could not load provider configuration.")
- self._login_widget.set_enabled(True)
+ 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._provider_bootstrapper.ERROR_KEY])
+ 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
@@ -868,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()
- 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))
+
+ self._login_widget.set_status(msg)
+ self._login_widget.set_enabled(True)
def _cancel_login(self):
"""
@@ -882,28 +1091,48 @@ 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):
"""
SLOT
- TRIGGER: self._provider_bootstrapper.check_api_certificate
+ TRIGGER: self._backend.signaler.prov_check_api_certificate
Once the provider configuration is loaded, this starts the SRP
authentication
"""
leap_assert(self._provider_config, "We need a provider config!")
- if data[self._provider_bootstrapper.PASSED_KEY]:
+ if data[self._backend.PASSED_KEY]:
username = self._login_widget.get_user()
password = self._login_widget.get_password()
@@ -913,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._provider_bootstrapper.ERROR_KEY])
+ 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
@@ -932,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):
"""
@@ -956,28 +1186,83 @@ class MainWindow(QtGui.QMainWindow):
"""
self._login_widget.logged_in()
- self.ui.lblLoginProvider.setText(self._provider_config.get_name())
+ 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
@@ -1010,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.")
@@ -1021,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
@@ -1047,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
@@ -1062,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,
@@ -1093,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):
"""
@@ -1120,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)
@@ -1171,27 +1480,65 @@ 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
"""
settings = self._settings
- should_autostart = settings.get_autostart_eip()
- if not should_autostart:
- logger.debug('Will not autostart EIP since it is setup '
- 'to not to do it')
- self.eip_needs_login.emit()
+ if not self._update_eip_enabled_status():
return
default_provider = settings.get_defaultprovider()
-
- if default_provider is None:
- logger.info("Cannot autostart Encrypted Internet because there is "
- "no default provider configured")
- self.eip_needs_login.emit()
- return
-
self._enabled_services = settings.get_enabled_services(
default_provider)
@@ -1202,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()
@@ -1336,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()
@@ -1434,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..."))
@@ -1453,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):
"""
@@ -1506,11 +1860,11 @@ class MainWindow(QtGui.QMainWindow):
This is used for intermediate bootstrapping stages, in case
they fail.
"""
- passed = data[self._provider_bootstrapper.PASSED_KEY]
+ passed = data[self._backend.PASSED_KEY]
if not passed:
self._login_widget.set_status(
self.tr("Unable to connect: Problem with provider"))
- logger.error(data[self._provider_bootstrapper.ERROR_KEY])
+ logger.error(data[self._backend.ERROR_KEY])
self._already_started_eip = False
# end of EIP methods ---------------------------------------------
@@ -1547,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_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_finished
+ TRIGGER: self._srp_auth.logout_ok
Switches the stackedWidget back to the login stage after
logging out
@@ -1568,36 +1937,30 @@ 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.
"""
SLOT
TRIGGERS:
- self._provider_bootstrapper.name_resolution
- self._provider_bootstrapper.https_connection
- self._provider_bootstrapper.download_ca_cert
+ self._backend.signaler.prov_name_resolution
+ self._backend.signaler.prov_https_connection
+ self._backend.signaler.prov_download_ca_cert
self._eip_bootstrapper.download_config
If there was a problem, displays it, otherwise it does nothing.
This is used for intermediate bootstrapping stages, in case
they fail.
"""
- passed = data[self._provider_bootstrapper.PASSED_KEY]
+ 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._provider_bootstrapper.ERROR_KEY])
+ logger.error(data[self._backend.ERROR_KEY])
#
# window handling methods
@@ -1647,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 \
@@ -1664,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?
@@ -1687,8 +2044,8 @@ class MainWindow(QtGui.QMainWindow):
# Set this in case that the app is hidden
QtGui.QApplication.setQuitOnLastWindowClosed(True)
+ 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 b4bddef2..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
@@ -42,6 +42,8 @@ class PreferencesWindow(QtGui.QDialog):
"""
Window that displays the preferences.
"""
+ preferences_saved = QtCore.Signal()
+
def __init__(self, parent, srp_auth, provider_config, soledad, domain):
"""
:param parent: parent object of the PreferencesWindow.
@@ -369,6 +371,7 @@ class PreferencesWindow(QtGui.QDialog):
"Services settings for provider '{0}' saved.".format(provider))
logger.debug(msg)
self._set_providers_services_status(msg, success=True)
+ self.preferences_saved.emit()
def _get_provider_config(self, domain):
"""
@@ -380,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/advanced_key_management.ui b/src/leap/bitmask/gui/ui/advanced_key_management.ui
index d61aa87e..1112670f 100644
--- a/src/leap/bitmask/gui/ui/advanced_key_management.ui
+++ b/src/leap/bitmask/gui/ui/advanced_key_management.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>431</width>
- <height>188</height>
+ <width>504</width>
+ <height>546</height>
</rect>
</property>
<property name="windowTitle">
@@ -17,10 +17,13 @@
<iconset resource="../../../../../data/resources/mainwindow.qrc">
<normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset>
</property>
- <layout class="QGridLayout" name="gridLayout_3">
- <item row="0" column="0">
- <widget class="QWidget" name="container" native="true">
- <layout class="QGridLayout" name="gridLayout_2">
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="1" column="0" colspan="2">
+ <widget class="QGroupBox" name="gbMyKeyPair">
+ <property name="title">
+ <string>My key pair</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
@@ -90,20 +93,7 @@
</property>
</widget>
</item>
- <item row="3" column="1">
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- <item row="4" column="0" colspan="2">
+ <item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QPushButton" name="pbExportKeys">
@@ -135,9 +125,56 @@
</layout>
</item>
</layout>
+ <zorder>leKeyID</zorder>
+ <zorder>leUser</zorder>
+ <zorder>leFingerprint</zorder>
+ <zorder>label_3</zorder>
+ <zorder>label_5</zorder>
+ <zorder>label</zorder>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <widget class="QGroupBox" name="gbStoredPublicKeys">
+ <property name="title">
+ <string>Stored public keys</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0">
+ <widget class="QTableWidget" name="twPublicKeys">
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideRight</enum>
+ </property>
+ <attribute name="horizontalHeaderStretchLastSection">
+ <bool>true</bool>
+ </attribute>
+ <column>
+ <property name="text">
+ <string>Email</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Key ID</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
</widget>
</item>
- <item row="1" column="0">
+ <item row="3" column="0" colspan="2">
<widget class="QLabel" name="lblStatus">
<property name="text">
<string/>
diff --git a/src/leap/bitmask/gui/ui/eip_status.ui b/src/leap/bitmask/gui/ui/eip_status.ui
index d078ca0c..64821ad6 100644
--- a/src/leap/bitmask/gui/ui/eip_status.ui
+++ b/src/leap/bitmask/gui/ui/eip_status.ui
@@ -25,6 +25,9 @@
</property>
<item>
<layout class="QGridLayout" name="gridLayout">
+ <property name="verticalSpacing">
+ <number>0</number>
+ </property>
<item row="0" column="2">
<widget class="QPushButton" name="btnEipStartStop">
<property name="text">
@@ -75,13 +78,6 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
- <property name="font">
- <font>
- <pointsize>14</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
<property name="text">
<string>Traffic is being routed in the clear</string>
</property>
@@ -124,12 +120,6 @@
</item>
<item row="2" column="1" colspan="3">
<widget class="QWidget" name="eip_bandwidth" native="true">
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>32</height>
- </size>
- </property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
@@ -146,16 +136,6 @@
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
- <widget class="QLabel" name="label_5">
- <property name="text">
- <string/>
- </property>
- <property name="pixmap">
- <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap>
- </property>
- </widget>
- </item>
- <item>
<widget class="QPushButton" name="btnDownload">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
@@ -175,25 +155,18 @@
<height>16777215</height>
</size>
</property>
- <property name="font">
- <font>
- <pointsize>11</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
- <property name="styleSheet">
- <string notr="true">text-align: left;</string>
- </property>
<property name="text">
<string>0.0 KB/s</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
+ <property name="icon">
+ <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap>
+ </property>
</widget>
</item>
<item>
@@ -206,22 +179,12 @@
</property>
<property name="sizeHint" stdset="0">
<size>
- <width>10</width>
+ <width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
- <item>
- <widget class="QLabel" name="label_7">
- <property name="text">
- <string/>
- </property>
- <property name="pixmap">
- <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap>
- </property>
- </widget>
- </item>
<item alignment="Qt::AlignLeft">
<widget class="QPushButton" name="btnUpload">
<property name="sizePolicy">
@@ -242,25 +205,18 @@
<height>16777215</height>
</size>
</property>
- <property name="font">
- <font>
- <pointsize>11</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
- <property name="styleSheet">
- <string notr="true">text-align: left;</string>
- </property>
<property name="text">
<string>0.0 KB/s</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
+ <property name="icon">
+ <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap>
+ </property>
</widget>
</item>
<item>
@@ -271,7 +227,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
- <height>20</height>
+ <height>0</height>
</size>
</property>
</spacer>
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 e7ca1652..f5725d5a 100644
--- a/src/leap/bitmask/gui/ui/login.ui
+++ b/src/leap/bitmask/gui/ui/login.ui
@@ -215,28 +215,8 @@
<number>0</number>
</property>
<property name="bottomMargin">
- <number>24</number>
+ <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 &quot;Lucida Grande&quot;;</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 3b83788e..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">
@@ -86,108 +86,6 @@
<number>0</number>
</property>
<item>
- <widget class="QWidget" name="eipWidget" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="margin">
- <number>0</number>
- </property>
- <item>
- <widget class="QFrame" name="frame_2">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="styleSheet">
- <string notr="true">QFrame{background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(160, 160, 160, 128), stop:1 rgba(255, 255, 255, 0));}</string>
- </property>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
- <property name="leftMargin">
- <number>24</number>
- </property>
- <property name="rightMargin">
- <number>24</number>
- </property>
- <item>
- <widget class="QLabel" name="label_2">
- <property name="font">
- <font>
- <pointsize>16</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="styleSheet">
- <string notr="true">background-color: rgba(255, 255, 255, 0);</string>
- </property>
- <property name="text">
- <string>Encrypted Internet</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="btnEIPPreferences">
- <property name="maximumSize">
- <size>
- <width>48</width>
- <height>20</height>
- </size>
- </property>
- <property name="styleSheet">
- <string notr="true"/>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="../../../../../data/resources/mainwindow.qrc">
- <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset>
- </property>
- <property name="autoDefault">
- <bool>false</bool>
- </property>
- <property name="default">
- <bool>false</bool>
- </property>
- <property name="flat">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <layout class="QVBoxLayout" name="eipLayout">
- <property name="leftMargin">
- <number>12</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>12</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- </layout>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="Line" name="line">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- </widget>
- </item>
- <item>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
@@ -199,9 +97,7 @@
<bool>false</bool>
</property>
<property name="styleSheet">
- <string notr="true">QFrame{
-background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(160, 160, 160, 128), stop:1 rgba(255, 255, 255, 0));
-}</string>
+ <string notr="true">background-color: rgba(0,0,0,20); border-bottom: 1px solid rgba(0,0,0,30);</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
@@ -214,36 +110,15 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb
<widget class="QLabel" name="lblLoginProvider">
<property name="font">
<font>
- <pointsize>16</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
- <string notr="true">background-color: rgba(255, 255, 255, 0);</string>
- </property>
- <property name="text">
- <string>Login</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="btnPreferences">
- <property name="maximumSize">
- <size>
- <width>48</width>
- <height>20</height>
- </size>
- </property>
- <property name="styleSheet">
- <string notr="true"/>
+ <string notr="true">background-color: rgba(255, 255, 255, 0); border: none;</string>
</property>
<property name="text">
- <string/>
- </property>
- <property name="icon">
- <iconset resource="../../../../../data/resources/mainwindow.qrc">
- <normaloff>:/images/black/32/gear.png</normaloff>:/images/black/32/gear.png</iconset>
+ <string>Please Log In</string>
</property>
</widget>
</item>
@@ -258,7 +133,42 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb
</layout>
</item>
<item>
- <widget class="Line" name="line_2">
+ <widget class="Line" name="lineUnderLogin">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="eipWidget" native="true">
+ <layout class="QVBoxLayout" name="eipVerticalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="eipLayout">
+ <property name="leftMargin">
+ <number>12</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>12</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="lineUnderEIP">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
@@ -287,6 +197,13 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb
</widget>
</item>
<item>
+ <widget class="Line" name="lineUnderEmail">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -380,7 +297,7 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb
<x>0</x>
<y>0</y>
<width>524</width>
- <height>21</height>
+ <height>23</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@@ -390,11 +307,14 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb
<addaction name="action_create_new_account"/>
<addaction name="action_advanced_key_management"/>
<addaction name="separator"/>
+ <addaction name="action_preferences"/>
+ <addaction name="action_eip_preferences"/>
+ <addaction name="separator"/>
<addaction name="action_quit"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
- <string>Help</string>
+ <string>&amp;Help</string>
</property>
<addaction name="action_help"/>
<addaction name="action_show_logs"/>
@@ -404,10 +324,17 @@ background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgb
<addaction name="menuFile"/>
<addaction name="menuHelp"/>
</widget>
- <widget class="QStatusBar" name="statusbar"/>
<action name="action_preferences">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Account Preferences...</string>
+ </property>
+ </action>
+ <action name="action_eip_preferences">
<property name="text">
- <string>Preferences...</string>
+ <string>Internet Preferences...</string>
</property>
</action>
<action name="action_quit">
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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Now we will guide you through some configuration that is needed before you can connect for the first time.&lt;/p&gt;&lt;p&gt;If you ever need to modify these options again, you can find the wizard in the &lt;span style=&quot; font-style:italic;&quot;&gt;'Settings'&lt;/span&gt; menu from the main window.&lt;/p&gt;&lt;p&gt;Do you want to &lt;span style=&quot; font-weight:600;&quot;&gt;sign up&lt;/span&gt; for a new account, or &lt;span style=&quot; font-weight:600;&quot;&gt;log in&lt;/span&gt; with an already existing username?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Now we will guide you through some configuration that is needed before you can connect for the first time.&lt;/p&gt;&lt;p&gt;If you ever need to modify these options again, you can find the wizard in the &lt;span style=&quot; font-style:italic;&quot;&gt;'Bitmask -&amp;gt; Create new account...'&lt;/span&gt; menu from the main window.&lt;/p&gt;&lt;p&gt;Do you want to &lt;span style=&quot; font-weight:600;&quot;&gt;sign up&lt;/span&gt; for a new account, or &lt;span style=&quot; font-weight:600;&quot;&gt;log in&lt;/span&gt; with an already existing username?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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 5f5224ae..e2c1a16e 100644
--- a/src/leap/bitmask/gui/wizard.py
+++ b/src/leap/bitmask/gui/wizard.py
@@ -17,22 +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.providerbootstrapper import ProviderBootstrapper
+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
@@ -55,12 +50,15 @@ class Wizard(QtGui.QWizard):
BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$"
- def __init__(self, bypass_checks=False):
+ def __init__(self, backend, bypass_checks=False):
"""
Constructor for the main Wizard.
+ :param backend: Backend being used
+ :type backend: Backend
:param bypass_checks: Set to true if the app should bypass
- first round of checks for CA certificates at bootstrap
+ first round of checks for CA
+ certificates at bootstrap
:type bypass_checks: bool
"""
QtGui.QWizard.__init__(self)
@@ -86,23 +84,14 @@ class Wizard(QtGui.QWizard):
self.ui.btnCheck.clicked.connect(self._check_provider)
self.ui.lnProvider.returnPressed.connect(self._check_provider)
- self._provider_bootstrapper = ProviderBootstrapper(bypass_checks)
- self._provider_bootstrapper.name_resolution.connect(
- self._name_resolution)
- self._provider_bootstrapper.https_connection.connect(
- self._https_connection)
- self._provider_bootstrapper.download_provider_info.connect(
- self._download_provider_info)
-
- self._provider_bootstrapper.download_ca_cert.connect(
- self._download_ca_cert)
- self._provider_bootstrapper.check_ca_fingerprint.connect(
- self._check_ca_fingerprint)
- self._provider_bootstrapper.check_api_certificate.connect(
- self._check_api_certificate)
+ self._backend = backend
+ self._backend_connect()
self._domain = None
- self._provider_config = ProviderConfig()
+ # HACK!! We need provider_config for the time being, it'll be
+ # removed
+ self._provider_config = (
+ self._backend._components["provider"]._provider_config)
# We will store a reference to the defers for eventual use
# (eg, to cancel them) but not doing anything with them right now.
@@ -114,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)
@@ -166,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):
"""
@@ -199,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
@@ -225,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:
@@ -255,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()
@@ -291,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):
"""
@@ -369,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
@@ -382,11 +392,10 @@ 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._provider_bootstrapper.\
- run_provider_select_checks(self._domain)
+ self._provider_select_defer = self._backend.\
+ setup_provider(self._domain)
def _skip_provider_checks(self, skip):
"""
@@ -403,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):
@@ -423,8 +430,8 @@ class Wizard(QtGui.QWizard):
:param complete_page: page id to complete
:type complete_page: int
"""
- passed = data[self._provider_bootstrapper.PASSED_KEY]
- error = data[self._provider_bootstrapper.ERROR_KEY]
+ passed = data[self._backend.PASSED_KEY]
+ error = data[self._backend.ERROR_KEY]
if passed:
label.setPixmap(self.OK_ICON)
if complete:
@@ -437,13 +444,13 @@ class Wizard(QtGui.QWizard):
def _name_resolution(self, data):
"""
SLOT
- TRIGGER: self._provider_bootstrapper.name_resolution
+ TRIGGER: self._backend.signaler.prov_name_resolution
Sets the status for the name resolution check
"""
self._complete_task(data, self.ui.lblNameResolution)
status = ""
- passed = data[self._provider_bootstrapper.PASSED_KEY]
+ passed = data[self._backend.PASSED_KEY]
if not passed:
status = self.tr("<font color='red'><b>Non-existent "
"provider</b></font>")
@@ -456,16 +463,16 @@ class Wizard(QtGui.QWizard):
def _https_connection(self, data):
"""
SLOT
- TRIGGER: self._provider_bootstrapper.https_connection
+ TRIGGER: self._backend.signaler.prov_https_connection
Sets the status for the https connection check
"""
self._complete_task(data, self.ui.lblHTTPS)
status = ""
- passed = data[self._provider_bootstrapper.PASSED_KEY]
+ passed = data[self._backend.PASSED_KEY]
if not passed:
status = self.tr("<font color='red'><b>%s</b></font>") \
- % (data[self._provider_bootstrapper.ERROR_KEY])
+ % (data[self._backend.ERROR_KEY])
self.ui.lblProviderSelectStatus.setText(status)
else:
self.ui.lblProviderInfo.setPixmap(self.QUESTION_ICON)
@@ -475,29 +482,26 @@ class Wizard(QtGui.QWizard):
def _download_provider_info(self, data):
"""
SLOT
- TRIGGER: self._provider_bootstrapper.download_provider_info
+ TRIGGER: self._backend.signaler.prov_download_provider_info
Sets the status for the provider information download
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
else:
new_data = {
- self._provider_bootstrapper.PASSED_KEY: False,
- self._provider_bootstrapper.ERROR_KEY:
+ self._backend.PASSED_KEY: False,
+ self._backend.ERROR_KEY:
self.tr("Unable to load provider configuration")
}
self._complete_task(new_data, self.ui.lblProviderInfo)
status = ""
- if not data[self._provider_bootstrapper.PASSED_KEY]:
+ if not data[self._backend.PASSED_KEY]:
status = self.tr("<font color='red'><b>Not a valid provider"
"</b></font>")
self.ui.lblProviderSelectStatus.setText(status)
@@ -507,31 +511,31 @@ class Wizard(QtGui.QWizard):
def _download_ca_cert(self, data):
"""
SLOT
- TRIGGER: self._provider_bootstrapper.download_ca_cert
+ TRIGGER: self._backend.signaler.prov_download_ca_cert
Sets the status for the download of the CA certificate check
"""
self._complete_task(data, self.ui.lblDownloadCaCert)
- passed = data[self._provider_bootstrapper.PASSED_KEY]
+ passed = data[self._backend.PASSED_KEY]
if passed:
self.ui.lblCheckCaFpr.setPixmap(self.QUESTION_ICON)
def _check_ca_fingerprint(self, data):
"""
SLOT
- TRIGGER: self._provider_bootstrapper.check_ca_fingerprint
+ TRIGGER: self._backend.signaler.prov_check_ca_fingerprint
Sets the status for the CA fingerprint check
"""
self._complete_task(data, self.ui.lblCheckCaFpr)
- passed = data[self._provider_bootstrapper.PASSED_KEY]
+ passed = data[self._backend.PASSED_KEY]
if passed:
self.ui.lblCheckApiCert.setPixmap(self.QUESTION_ICON)
def _check_api_certificate(self, data):
"""
SLOT
- TRIGGER: self._provider_bootstrapper.check_api_certificate
+ TRIGGER: self._backend.signaler.prov_check_api_certificate
Sets the status for the API certificate check. Also finishes
the provider bootstrapper thread since it's not needed anymore
@@ -597,6 +601,7 @@ class Wizard(QtGui.QWizard):
Prepares the pages when they appear
"""
if pageId == self.SELECT_PROVIDER_PAGE:
+ self._clear_register_widgets()
skip = self.ui.rbExistingProvider.isChecked()
if not self._provider_checks_ok:
self._enable_check()
@@ -611,8 +616,8 @@ class Wizard(QtGui.QWizard):
sub_title = sub_title.format(self._provider_config.get_name())
self.page(pageId).setSubTitle(sub_title)
self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON)
- self._provider_setup_defer = self._provider_bootstrapper.\
- run_provider_setup_checks(self._provider_config)
+ self._provider_setup_defer = self._backend.\
+ provider_bootstrap(self._domain)
if pageId == self.PRESENT_PROVIDER_PAGE:
self.page(pageId).setSubTitle(self.tr("Description of services "
@@ -670,3 +675,50 @@ class Wizard(QtGui.QWizard):
return self.SERVICES_PAGE
return QtGui.QWizard.nextId(self)
+
+ def _clear_register_widgets(self):
+ """
+ Clears the widgets that my be filled and a possible error message.
+ """
+ self._set_register_status("")
+ 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 f5a2003f..2a519206 100644
--- a/src/leap/bitmask/provider/providerbootstrapper.py
+++ b/src/leap/bitmask/provider/providerbootstrapper.py
@@ -24,18 +24,18 @@ import sys
import requests
-from PySide import QtCore
-
-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__)
@@ -47,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.
@@ -61,25 +69,21 @@ class ProviderBootstrapper(AbstractBootstrapper):
If a check fails, the subsequent checks are not executed
"""
- # All dicts returned are of the form
- # {"passed": bool, "error": str}
- name_resolution = QtCore.Signal(dict)
- https_connection = QtCore.Signal(dict)
- download_provider_info = QtCore.Signal(dict)
+ MIN_CLIENT_VERSION = 'x-minimum-client-version'
- download_ca_cert = QtCore.Signal(dict)
- check_ca_fingerprint = QtCore.Signal(dict)
- check_api_certificate = QtCore.Signal(dict)
-
- def __init__(self, bypass_checks=False):
+ def __init__(self, signaler=None, bypass_checks=False):
"""
Constructor for provider bootstrapper object
+ :param signaler: Signaler object used to receive notifications
+ from the backend
+ :type signaler: Signaler
:param bypass_checks: Set to true if the app should bypass
- first round of checks for CA certificates at bootstrap
+ first round of checks for CA
+ certificates at bootstrap
:type bypass_checks: bool
"""
- AbstractBootstrapper.__init__(self, bypass_checks)
+ AbstractBootstrapper.__init__(self, signaler, bypass_checks)
self._domain = None
self._provider_config = None
@@ -95,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):
@@ -163,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:
@@ -195,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")
@@ -202,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()
@@ -209,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):
"""
@@ -238,9 +259,11 @@ class ProviderBootstrapper(AbstractBootstrapper):
self._download_if_needed = download_if_needed
cb_chain = [
- (self._check_name_resolution, self.name_resolution),
- (self._check_https, self.https_connection),
- (self._download_provider_info, self.download_provider_info)
+ (self._check_name_resolution,
+ self._signaler.PROV_NAME_RESOLUTION_KEY),
+ (self._check_https, self._signaler.PROV_HTTPS_CONNECTION_KEY),
+ (self._download_provider_info,
+ self._signaler.PROV_DOWNLOAD_PROVIDER_INFO_KEY)
]
return self.addCallbackChain(cb_chain)
@@ -367,9 +390,11 @@ class ProviderBootstrapper(AbstractBootstrapper):
self._download_if_needed = download_if_needed
cb_chain = [
- (self._download_ca_cert, self.download_ca_cert),
- (self._check_ca_fingerprint, self.check_ca_fingerprint),
- (self._check_api_certificate, self.check_api_certificate)
+ (self._download_ca_cert, self._signaler.PROV_DOWNLOAD_CA_CERT_KEY),
+ (self._check_ca_fingerprint,
+ self._signaler.PROV_CHECK_CA_FINGERPRINT_KEY),
+ (self._check_api_certificate,
+ self._signaler.PROV_CHECK_API_CERTIFICATE_KEY)
]
return self.addCallbackChain(cb_chain)
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 88a4ff0b..6cf3e469 100644
--- a/src/leap/bitmask/provider/tests/test_providerbootstrapper.py
+++ b/src/leap/bitmask/provider/tests/test_providerbootstrapper.py
@@ -36,21 +36,22 @@ 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 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):
def setUp(self):
- self.pb = ProviderBootstrapper()
+ self.pb = ProviderBootstrapper(Signaler())
def tearDown(self):
pass
@@ -488,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 6d4d319b..fc6bd3e9 100644
--- a/src/leap/bitmask/services/abstractbootstrapper.py
+++ b/src/leap/bitmask/services/abstractbootstrapper.py
@@ -25,7 +25,10 @@ import requests
from functools import partial
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
@@ -40,10 +43,13 @@ class AbstractBootstrapper(QtCore.QObject):
PASSED_KEY = "passed"
ERROR_KEY = "error"
- def __init__(self, bypass_checks=False):
+ def __init__(self, signaler=None, bypass_checks=False):
"""
Constructor for the abstract bootstrapper
+ :param signaler: Signaler object used to receive notifications
+ from the backend
+ :type signaler: Signaler
:param bypass_checks: Set to true if the app should bypass
first round of checks for CA
certificates at bootstrap
@@ -71,6 +77,7 @@ class AbstractBootstrapper(QtCore.QObject):
self._bypass_checks = bypass_checks
self._signal_to_emit = None
self._err_msg = None
+ self._signaler = signaler
def _gui_errback(self, failure):
"""
@@ -85,14 +92,30 @@ 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 \
else str(failure.value)
- self._signal_to_emit.emit({
+ data = {
self.PASSED_KEY: False,
self.ERROR_KEY: err_msg
- })
+ }
+ # TODO: Remove this check when all the bootstrappers are
+ # in the backend form
+ if isinstance(self._signal_to_emit, basestring):
+ if self._signaler is not None:
+ self._signaler.signal(self._signal_to_emit, data)
+ else:
+ logger.warning("Tried to notify but no signaler found")
+ else:
+ self._signal_to_emit.emit(data)
+ log.err(failure)
failure.trap(Exception)
def _errback(self, failure, signal=None):
@@ -127,8 +150,15 @@ class AbstractBootstrapper(QtCore.QObject):
:param signal: Signal to emit if it fails here first
:type signal: QtCore.SignalInstance
"""
- if signal:
- signal.emit({self.PASSED_KEY: True, self.ERROR_KEY: ""})
+ if signal is not None:
+ data = {self.PASSED_KEY: True, self.ERROR_KEY: ""}
+ if isinstance(signal, basestring):
+ if self._signaler is not None:
+ self._signaler.signal(signal, data)
+ else:
+ logger.warning("Tried to notify but no signaler found")
+ else:
+ signal.emit(data)
def _callback_threader(self, cb, res, *args, **kwargs):
return threads.deferToThread(cb, res, *args, **kwargs)
diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
index fe3fe4c1..a03bfc44 100644
--- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
@@ -95,7 +95,7 @@ class DarwinVPNLauncher(VPNLauncher):
resources_path = os.path.abspath(
os.path.join(os.getcwd(), "../../Contents/Resources"))
- return os.path.join(resources_path, "leap-client.tiff")
+ return os.path.join(resources_path, "bitmask.tiff")
@classmethod
def get_cocoasudo_ovpn_cmd(kls):
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/imap.py b/src/leap/bitmask/services/mail/imap.py
index 2667f156..5db18cb9 100644
--- a/src/leap/bitmask/services/mail/imap.py
+++ b/src/leap/bitmask/services/mail/imap.py
@@ -19,10 +19,10 @@ Initialization of imap service
"""
import logging
import os
-#import sys
+import sys
from leap.mail.imap.service import imap
-#from twisted.python import log
+from twisted.python import log
logger = logging.getLogger(__name__)
@@ -58,15 +58,15 @@ def start_imap_service(*args, **kwargs):
:returns: twisted.internet.task.LoopingCall instance
"""
+ from leap.bitmask.config import flags
logger.debug('Launching imap service')
override_period = get_mail_check_period()
if override_period:
kwargs['check_period'] = override_period
- # Uncomment the next two lines to get a separate debugging log
- # TODO handle this by a separate flag.
- #log.startLogging(open('/tmp/leap-imap.log', 'w'))
- #log.startLogging(sys.stdout)
+ if flags.MAIL_LOGFILE:
+ log.startLogging(open(flags.MAIL_LOGFILE, 'w'))
+ log.startLogging(sys.stdout)
return imap.run_service(*args, **kwargs)
diff --git a/src/leap/bitmask/services/mail/plumber.py b/src/leap/bitmask/services/mail/plumber.py
new file mode 100644
index 00000000..c16a1fed
--- /dev/null
+++ b/src/leap/bitmask/services/mail/plumber.py
@@ -0,0 +1,341 @@
+# -*- coding: utf-8 -*-
+# 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
+# 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/>.
+"""
+Utils for manipulating local mailboxes.
+"""
+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.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.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__)
+logger.setLevel(logging.DEBUG)
+
+
+def initialize_soledad(uuid, email, passwd,
+ secrets, localdb,
+ gnupg_home, tempdir):
+ """
+ Initializes soledad by hand
+
+ :param email: ID for the user
+ :param gnupg_home: path to home used by gnupg
+ :param tempdir: path to temporal dir
+ :rtype: Soledad instance
+ """
+ # XXX TODO unify with an authoritative source of mocks
+ # for soledad (or partial initializations).
+ # This is copied from the imap tests.
+
+ server_url = "http://provider"
+ cert_file = ""
+
+ class Mock(object):
+ def __init__(self, return_value=None):
+ self._return = return_value
+
+ def __call__(self, *args, **kwargs):
+ return self._return
+
+ class MockSharedDB(object):
+
+ get_doc = Mock()
+ put_doc = Mock()
+ lock = Mock(return_value=('atoken', 300))
+ unlock = Mock(return_value=True)
+
+ def __call__(self):
+ return self
+
+ Soledad._shared_db = MockSharedDB()
+ soledad = Soledad(
+ uuid,
+ passwd,
+ secrets,
+ localdb,
+ server_url,
+ cert_file)
+
+ return soledad
+
+
+class MBOXPlumber(object):
+ """
+ An class that can fix things inside a soledadbacked account.
+ The idea is to gather in this helper different fixes for mailboxes
+ that can be invoked when data migration in the client is needed.
+ """
+
+ def __init__(self, userid, passwd, mdir=None):
+ """
+ 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
+ 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()
+
+ def _init_local_soledad(self):
+ """
+ Initialize local Soledad instance.
+ """
+ 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
+
+ print "UUID: %s" % (self.uuid)
+
+ secrets, localdb = get_db_paths(self.uuid)
+
+ self.sol = initialize_soledad(
+ 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()
+
+ for mbox_name in self.acct.mailboxes:
+ self.repair_mbox_uids(mbox_name)
+ print "done."
+ self.exit()
+
+ def repair_mbox_uids(self, mbox_name):
+ """
+ Repair indexes for a given mbox.
+
+ :param mbox_name: mailbox to repair
+ :type mbox_name: basestring
+ """
+ print
+ print "REPAIRING INDEXES FOR MAILBOX %s" % (mbox_name,)
+ print "----------------------------------------------"
+ mbox = self.acct.getMailbox(mbox_name)
+ len_mbox = mbox.getMessageCount()
+ print "There are %s messages" % (len_mbox,)
+
+ last_ok = True if mbox.last_uid == len_mbox else False
+ 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)
+ 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
+ print "LAST UID: %s (%s)" % (mbox.last_uid, old_last_uid)
+
+ def _has_dupes(self, sequence):
+ """
+ Return True if the given sequence of ints has duplicates.
+
+ :param sequence: a sequence of ints
+ :type sequence: sequence
+ :rtype: bool
+ """
+ d = defaultdict(lambda: 0)
+ for uid in sequence:
+ d[uid] += 1
+ if d[uid] != 1:
+ 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
+ try:
+ if self.sol:
+ self.sol.close()
+ reactor.stop()
+ except Exception:
+ pass
+ return
+
+
+def repair_account(userid):
+ """
+ Start repair 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)
+ 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()
+
+
+if __name__ == "__main__":
+ import sys
+
+ logging.basicConfig()
+
+ if len(sys.argv) != 3:
+ print "Usage: plumber [repair|import] <username>"
+ sys.exit(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 d078ae96..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"
@@ -59,9 +95,36 @@ class SoledadInitError(Exception):
message = "Error while initializing Soledad"
+def get_db_paths(uuid):
+ """
+ Return the secrets and local db paths needed for soledad
+ initialization
+
+ :param uuid: uuid for user
+ :type uuid: str
+
+ :return: a tuple with secrets, local_db paths
+ :rtype: tuple
+ """
+ prefix = os.path.join(get_path_prefix(), "leap", "soledad")
+ secrets = "%s/%s.secret" % (prefix, uuid)
+ local_db = "%s/%s.db" % (prefix, uuid)
+
+ # We remove an empty file if found to avoid complains
+ # about the db not being properly initialized
+ if is_file(local_db) and is_empty_file(local_db):
+ try:
+ os.remove(local_db)
+ except OSError:
+ logger.warning(
+ "Could not remove empty file %s"
+ % local_db)
+ return secrets, local_db
+
+
class SoledadBootstrapper(AbstractBootstrapper):
"""
- Soledad init procedure
+ Soledad init procedure.
"""
SOLEDAD_KEY = "soledad"
KEYMANAGER_KEY = "keymanager"
@@ -75,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):
@@ -88,6 +153,9 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._user = ""
self._password = ""
+ self._address = ""
+ self._uuid = ""
+
self._srpauth = None
self._soledad = None
@@ -103,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)
@@ -114,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,
@@ -123,72 +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
- def _get_db_paths(self, uuid):
+ # initialization
+
+ def load_offline_soledad(self, username, password, uuid):
+ """
+ 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
"""
- Returns the secrets and local db paths needed for soledad
- initialization
+ print "UUID ", uuid
+ self._address = username
+ self._uuid = uuid
+ return self.load_and_sync_soledad(uuid, offline=True)
- :param uuid: uuid for user
- :type uuid: str
+ def _get_soledad_local_params(self, uuid, offline=False):
+ """
+ Return the locals parameters needed for the soledad initialization.
- :return: a tuple with secrets, local_db paths
+ :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
"""
- prefix = os.path.join(get_path_prefix(), "leap", "soledad")
- secrets = "%s/%s.secret" % (prefix, uuid)
- local_db = "%s/%s.db" % (prefix, uuid)
+ # 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.
- # We remove an empty file if found to avoid complains
- # about the db not being properly initialized
- if is_file(local_db) and is_empty_file(local_db):
- try:
- os.remove(local_db)
- except OSError:
- logger.warning("Could not remove empty file %s"
- % local_db)
- return secrets, local_db
+ if offline is False:
+ token = self.srpauth.get_token()
+ else:
+ token = ""
- # initialization
+ secrets_path, local_db_path = get_db_paths(uuid)
+
+ logger.debug('secrets_path:%s' % (secrets_path,))
+ logger.debug('local_db:%s' % (local_db_path,))
+ return (secrets_path, local_db_path, token)
- def load_and_sync_soledad(self):
+ def _get_soledad_server_params(self, uuid, offline):
"""
- Once everthing is in the right place, we instantiate and sync
- Soledad
+ Return the remote parameters needed for the soledad initialization.
+
+ :param uuid: the uuid of the user, used in offline mode.
+ :type uuid: unicode, or None.
+ :return: server_url, cert_file
+ :rtype: tuple
"""
- # TODO this method is still too large
- uuid = self.srpauth.get_uid()
- token = self.srpauth.get_token()
+ if uuid is None:
+ uuid = self.srpauth.get_uuid()
- secrets_path, local_db_path = self._get_db_paths(uuid)
+ 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()
- # TODO: Select server based on timezone (issue #3308)
- server_dict = self._soledad_config.get_hosts()
+ return server_url, cert_file
- if not server_dict.keys():
- # XXX raise more specific exception, and catch it properly!
- raise Exception("No soledad server found")
+ 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()
- 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,))
- cert_file = self._provider_config.get_ca_cert_path()
+ 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)
- logger.debug('local_db:%s' % (local_db_path,))
- logger.debug('secrets_path:%s' % (secrets_path,))
+ 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.
@@ -196,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
@@ -220,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...
@@ -229,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
@@ -245,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,
@@ -279,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
@@ -291,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:
@@ -300,6 +450,16 @@ class SoledadBootstrapper(AbstractBootstrapper):
except SSLError as exc:
logger.error("%r" % (exc,))
raise SoledadSyncError("Failed to sync soledad")
+ 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,))
@@ -307,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,
@@ -326,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
"""
@@ -356,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, _):
"""
@@ -401,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:
@@ -462,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 e8a9fda9..88267ff8 100644
--- a/src/leap/bitmask/util/leap_argparse.py
+++ b/src/leap/bitmask/util/leap_argparse.py
@@ -27,30 +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",
+ 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')
+ 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",
+ 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. 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