diff options
| -rw-r--r-- | docs/changelog.rst | 3 | ||||
| -rw-r--r-- | src/leap/bitmask/bonafide/_protocol.py | 39 | ||||
| -rw-r--r-- | src/leap/bitmask/bonafide/config.py | 41 | ||||
| -rw-r--r-- | src/leap/bitmask/bonafide/service.py | 4 | ||||
| -rw-r--r-- | src/leap/bitmask/bonafide/session.py | 26 | ||||
| -rw-r--r-- | src/leap/bitmask/vpn/_checks.py | 11 | ||||
| -rw-r--r-- | src/leap/bitmask/vpn/service.py | 97 | 
7 files changed, 150 insertions, 71 deletions
| diff --git a/docs/changelog.rst b/docs/changelog.rst index 6a6a8125..69b314ee 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,10 +9,11 @@ Features  ~~~~~~~~  - `#8217 <https://0xacab.org/leap/bitmask-dev/issues/8217>`_: renew OpenPGP keys before they expire.  - `#9074 <https://0xacab.org/leap/bitmask-dev/issues/9074>`_: pin provider ca certs. -- `#6914 <https://0xacab.org/leap/bitmask-dev/issues/6914>`_: expose an API to retrive message status. +- `#6914 <https://0xacab.org/leap/bitmask-dev/issues/6914>`_: expose an API to retreive message status.  - `#9188 <https://0xacab.org/leap/bitmask-dev/issues/9188>`_: try other gateways if the main one fails.  - `#9125 <https://0xacab.org/leap/bitmask-dev/issues/9125>`_: port to use qtwebengine for rendering UI.  - Set a windows title, so that Bitmask windows can be programmatically manipulated. +- Support anonymous vpn.  Bugfixes  ~~~~~~~~ diff --git a/src/leap/bitmask/bonafide/_protocol.py b/src/leap/bitmask/bonafide/_protocol.py index e044875f..ceb29efd 100644 --- a/src/leap/bitmask/bonafide/_protocol.py +++ b/src/leap/bitmask/bonafide/_protocol.py @@ -25,7 +25,8 @@ from leap.bitmask.bonafide.provider import Api  from leap.bitmask.bonafide.session import Session, OK  from leap.common.config import get_path_prefix -from twisted.cred.credentials import UsernamePassword +from twisted.cred.credentials import UsernamePassword, Anonymous +from twisted.cred.checkers import ANONYMOUS  from twisted.internet.defer import fail  from twisted.logger import Logger @@ -52,13 +53,16 @@ class BonafideProtocol(object):          self._apis[provider.domain] = api          return api -    def _get_session(self, provider, full_id, password=""): +    def _get_or_create_session(self, provider, full_id, password=""):          if full_id in self._sessions:              return self._sessions[full_id] - -        # TODO if password/username null, then pass AnonymousCreds -        username, provider_id = config.get_username_and_provider(full_id) -        credentials = UsernamePassword(username, password) +        if full_id == ANONYMOUS: +            credentials = Anonymous() +            provider_id = provider.domain +        else: +            username, provider_id = config.get_username_and_provider( +                full_id) +            credentials = UsernamePassword(username, password)          api = self._get_api(provider)          provider_pem = config.get_ca_cert_path(_preffix, provider_id)          session = Session(credentials, api, provider_pem) @@ -90,7 +94,7 @@ class BonafideProtocol(object):                  return user          username, _ = config.get_username_and_provider(full_id) -        session = self._get_session(provider, full_id, password) +        session = self._get_or_create_session(provider, full_id, password)          d = session.signup(username, password, invite)          d.addCallback(return_user)          d.addErrback(self._del_session_errback, full_id) @@ -102,7 +106,7 @@ class BonafideProtocol(object):          provider = config.Provider.get(provider_id, autoconf=autoconf)          def maybe_finish_provider_bootstrap(result): -            session = self._get_session(provider, full_id, password) +            session = self._get_or_create_session(provider, full_id, password)              d = provider.download_services_config_with_auth(session)              d.addCallback(lambda _: result)              return d @@ -121,7 +125,7 @@ class BonafideProtocol(object):          self.log.debug('AUTH for %s' % full_id) -        session = self._get_session(provider, full_id, password) +        session = self._get_or_create_session(provider, full_id, password)          d = session.authenticate()          d.addCallback(return_token_and_uuid, session)          d.addErrback(self._del_session_errback, full_id) @@ -171,7 +175,6 @@ class BonafideProtocol(object):          return config.delete_provider(provider_id)      def do_provider_list(self, seeded=False): -        # TODO: seeded, we don't have pinned providers yet          providers = config.list_providers()          return [{"domain": p} for p in providers] @@ -182,11 +185,17 @@ class BonafideProtocol(object):          d = self._sessions[full_id].get_smtp_cert()          return d -    def do_get_vpn_cert(self, full_id): -        if (full_id not in self._sessions or -                not self._sessions[full_id].is_authenticated): -            return fail(RuntimeError("There is no session for such user")) -        d = self._sessions[full_id].get_vpn_cert() +    def do_get_vpn_cert(self, full_id, anonymous=False): +        if anonymous: +            _, provider_id = full_id.split('@') +            provider = config.Provider.get(provider_id, autoconf=True) +            d = self._get_or_create_session( +                provider, ANONYMOUS).get_vpn_cert() +        else: +            if (full_id not in self._sessions or +                    not self._sessions[full_id].is_authenticated): +                return fail(RuntimeError("There is no session for such user")) +            d = self._sessions[full_id].get_vpn_cert()          return d      def do_update_user(self): diff --git a/src/leap/bitmask/bonafide/config.py b/src/leap/bitmask/bonafide/config.py index 222726b7..fe40f277 100644 --- a/src/leap/bitmask/bonafide/config.py +++ b/src/leap/bitmask/bonafide/config.py @@ -31,13 +31,15 @@ from cryptography.hazmat.primitives import hashes  from cryptography.x509 import load_pem_x509_certificate  from urlparse import urlparse +from twisted.cred.credentials import Anonymous  from twisted.internet import defer  from twisted.logger import Logger  from twisted.web.client import downloadPage  from leap.bitmask.bonafide._http import httpRequest -from leap.bitmask.bonafide.provider import Discovery  from leap.bitmask.bonafide.errors import NotConfiguredError, NetworkError +from leap.bitmask.bonafide.provider import Discovery +from leap.bitmask.bonafide.session import Session  from leap.bitmask.util import here, STANDALONE  from leap.common.check import leap_assert @@ -266,6 +268,10 @@ class Provider(object):          self.log.debug('Bootstrapping provider %s' % domain)          def first_bootstrap_done(ignored): +            if self._allows_anonymous: +                # we continue bootstrapping, we do not +                # need to wait for authentication. +                return              try:                  self.first_bootstrap.callback('got config')              except defer.AlreadyCalledError: @@ -282,6 +288,14 @@ class Provider(object):          d.addCallback(self.maybe_download_services_config)          self.ongoing_bootstrap = d +    def _allows_anonymous(self): +        try: +            anon = self._provider_config.get( +                'service').get('allows_anonymous') +        except ValueError: +            anon = False +        return anon +      def callWhenMainConfigReady(self, cb, *args, **kw):          d = self.first_bootstrap          d.addCallback(lambda _: cb(*args, **kw)) @@ -388,17 +402,23 @@ class Provider(object):          return os.path.isfile(self._get_configs_path())      def maybe_download_services_config(self, ignored): -          # TODO --- currently, some providers (mail.bitmask.net) raise 401          # UNAUTHENTICATED if we try to get the services          # See: # https://leap.se/code/issues/7906 +        def first_bootstrap_done(ignored): +            try: +                self.first_bootstrap.callback('got config') +            except defer.AlreadyCalledError: +                pass +          uri, met, path = self._get_configs_download_params()          d = httpRequest(              self._http._agent, uri, method=met, saveto=path)          d.addCallback(lambda _: self._load_provider_json())          d.addCallback(              lambda _: self._get_config_for_all_services(session=None)) +        d.addCallback(first_bootstrap_done)          d.addErrback(lambda _: 'ok for now')          return d @@ -499,6 +519,10 @@ class Provider(object):              self._disco.netloc = parsed.netloc      def _get_config_for_all_services(self, session): +        if session is None: +            provider_cert = self._get_ca_cert_path() +            session = Session(Anonymous(), self.api_uri, provider_cert) +          services_dict = self._load_provider_configs()          configs_path = self._get_configs_path()          with open(configs_path) as jsonf: @@ -510,12 +534,8 @@ class Provider(object):                  for subservice in self.SERVICES_MAP[service]:                      uri = base + str(services_dict[subservice])                      path = self._get_service_config_path(subservice) -                    if session: -                        d = session.fetch_provider_configs( -                            uri, path, method='GET') -                    else: -                        d = self._fetch_provider_configs_unauthenticated( -                            uri, path, method='GET') +                    d = session.fetch_provider_configs( +                        uri, path, method='GET')                      pending.append(d)          return defer.gatherResults(pending) @@ -525,11 +545,6 @@ class Provider(object):              services_dict = Record(**json.load(jsonf)).services          return services_dict -    def _fetch_provider_configs_unauthenticated(self, uri, path): -        self.log.info('Downloading config for %s...' % uri) -        return httpRequest( -            self._http._agent, uri, saveto=path) -  class Record(object):      def __init__(self, **kw): diff --git a/src/leap/bitmask/bonafide/service.py b/src/leap/bitmask/bonafide/service.py index 43f51768..5856a263 100644 --- a/src/leap/bitmask/bonafide/service.py +++ b/src/leap/bitmask/bonafide/service.py @@ -131,12 +131,12 @@ class BonafideService(HookableService):      def do_provider_list(self, seeded=False):          return self._bonafide.do_provider_list(seeded) -    def do_get_vpn_cert(self, username): +    def do_get_vpn_cert(self, username, anonymous=False):          if not username:              return defer.fail(                  RuntimeError('No username, cannot get VPN cert.')) -        d = self._bonafide.do_get_vpn_cert(username) +        d = self._bonafide.do_get_vpn_cert(username, anonymous=anonymous)          d.addCallback(lambda response: (username, response))          return d diff --git a/src/leap/bitmask/bonafide/session.py b/src/leap/bitmask/bonafide/session.py index 988cbb99..d6a39447 100644 --- a/src/leap/bitmask/bonafide/session.py +++ b/src/leap/bitmask/bonafide/session.py @@ -17,6 +17,7 @@  """  LEAP Session management.  """ +from twisted.cred.credentials import IAnonymous, IUsernamePassword  from twisted.internet import defer, reactor  from twisted.logger import Logger @@ -47,7 +48,6 @@ class Session(object):      log = Logger()      def __init__(self, credentials, api, provider_cert): -        # TODO check if an anonymous credentials is passed.          # TODO move provider_cert to api object.          # On creation, it should be able to retrieve all the info it needs          # (calling bootstrap). @@ -56,9 +56,12 @@ class Session(object):          # and a "autoconfig" attribute passed on initialization.          # TODO get a file-descriptor for password if not in credentials          # TODO merge self._request with config.Provider._http_request ? - -        self.username = credentials.username -        self.password = credentials.password +        if IAnonymous.providedBy(credentials): +            self.username = None +            self.password = None +        elif IUsernamePassword.providedBy(credentials): +            self.username = credentials.username +            self.password = credentials.password          self._provider_cert = provider_cert          self._api = api          self._initialize_session() @@ -86,7 +89,10 @@ class Session(object):      @property      def is_authenticated(self): -        return self._srp_auth.srp_user.authenticated() +        if self.username is None: +            return False +        else: +            return self._srp_auth.srp_user.authenticated()      @defer.inlineCallbacks      def authenticate(self): @@ -133,8 +139,8 @@ class Session(object):          met = self._api.get_update_user_method()          params = self._srp_password.get_password_params(              self.username, password) -        update = yield self._request(self._agent, uri, values=params, -                                     method=met) +        yield self._request(self._agent, uri, values=params, +                            method=met)          self.password = password          self._srp_auth = _srp.SRPAuthMechanism(self.username, password)          defer.returnValue(OK) @@ -153,16 +159,12 @@ class Session(object):      # User certificates      def get_vpn_cert(self): -        # TODO pass it to the provider object so that it can save it in the -        # right path.          uri = self._api.get_vpn_cert_uri()          met = self._api.get_vpn_cert_method()          return self._request(self._agent, uri, method=met)      @_auth_required      def get_smtp_cert(self): -        # TODO pass it to the provider object so that it can save it in the -        # right path.          uri = self._api.get_smtp_cert_uri()          met = self._api.get_smtp_cert_method()          return self._request(self._agent, uri, method=met) @@ -198,7 +200,7 @@ class Session(object):      @defer.inlineCallbacks      def fetch_provider_configs(self, uri, path, method='GET'): -        config = yield self._request( +        yield self._request(              self._agent, uri, method=method, saveto=path)          defer.returnValue('ok') diff --git a/src/leap/bitmask/vpn/_checks.py b/src/leap/bitmask/vpn/_checks.py index c8f7dd36..9586d096 100644 --- a/src/leap/bitmask/vpn/_checks.py +++ b/src/leap/bitmask/vpn/_checks.py @@ -17,14 +17,19 @@ class ImproperlyConfigured(Exception):      pass -def is_service_ready(provider): +def get_failure_for(provider):      if not _has_valid_cert(provider):          raise ImproperlyConfigured('Missing VPN certificate') -      if IS_LINUX and not is_pkexec_in_system(): -        log.warn('System has no pkexec')          raise NoPkexecAvailable() + +def is_service_ready(provider): +    if not _has_valid_cert(provider): +        return False +    if IS_LINUX and not is_pkexec_in_system(): +        log.warn('System has no pkexec') +        return False      return True diff --git a/src/leap/bitmask/vpn/service.py b/src/leap/bitmask/vpn/service.py index 922cfaea..609d70ee 100644 --- a/src/leap/bitmask/vpn/service.py +++ b/src/leap/bitmask/vpn/service.py @@ -1,7 +1,7 @@  #!/usr/bin/env python  # -*- coding: utf-8 -*-  # service.py -# Copyright (C) 2015-2017 LEAP +# Copyright (C) 2015-2018 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 @@ -35,6 +35,7 @@ from leap.bitmask.vpn._checks import (      get_vpn_cert_path,      cert_expires  ) +  from leap.bitmask.vpn import privilege, helpers  from leap.common.config import get_path_prefix  from leap.common.files import check_and_fix_urw_only @@ -79,13 +80,22 @@ class VPNService(HookableService):          except ValueError:              self._loc = [] +        _autostart = self._cfg.get('autostart', False) +        self._autostart = _autostart + +        _anonymous = self._cfg.get('anonymous', True) +        self._anonymous_enabled = _anonymous +          if helpers.check() and self._firewall.is_up():              self._firewall.stop()      def startService(self): -        # TODO this could trigger a check for validity of the certificates, -        # etc. +        # TODO trigger a check for validity of the certificates, +        # and schedule a re-download if needed. +        # TODO start a watchDog service (to push status events)          super(VPNService, self).startService() +        if self._autostart: +            self.start_vpn()      def stopService(self):          try: @@ -100,18 +110,21 @@ class VPNService(HookableService):              exc = Exception('VPN already started')              exc.expected = True              raise exc -        if not domain: +        if domain is None:              domain = self._read_last() -            if not domain: +            if domain is None:                  exc = Exception("VPN can't start, a provider is needed")                  exc.expected = True                  raise exc + +        yield self._setup(domain)          if not is_service_ready(domain): -            exc = Exception("VPN is not ready") +            exc = Exception('VPN is not ready')              exc.expected = True              raise exc -        yield self._setup(domain) +        # XXX we can signal status to frontend, use +        # get_failure_for(provider) -- no polkit, etc.          fw_ok = self._firewall.start()          if not fw_ok: @@ -177,7 +190,7 @@ class VPNService(HookableService):          return ret      @defer.inlineCallbacks -    def do_get_cert(self, username): +    def do_get_cert(self, username, anonymous=False):          try:              _, provider = username.split('@')          except ValueError: @@ -188,7 +201,8 @@ class VPNService(HookableService):          # fetch vpn cert and store          bonafide = self.parent.getServiceNamed("bonafide") -        _, cert_str = yield bonafide.do_get_vpn_cert(username) +        _, cert_str = yield bonafide.do_get_vpn_cert( +             username, anonymous=anonymous)          cert_path = get_vpn_cert_path(provider)          cert_dir = os.path.dirname(cert_path) @@ -242,32 +256,29 @@ class VPNService(HookableService):          return self._cco      @defer.inlineCallbacks -    def _setup(self, provider): +    def _setup(self, provider_id):          """Set up ConfiguredTunnel for a specified provider.          :param provider: the provider to use, e.g. 'demo.bitmask.net'          :type provider: str"""          bonafide = self.parent.getServiceNamed('bonafide') -        config = yield bonafide.do_provider_read(provider, 'eip') -        sorted_gateways = GatewaySelector( -            config.gateways, config.locations, -            preferred={'cc': self._cco, 'loc': self._loc} -        ).select_gateways() +        # bootstrap if not yet done +        if provider_id not in bonafide.do_provider_list(seeded=False): +            yield bonafide.do_provider_create(provider_id) -        extra_flags = config.openvpn_configuration +        provider = yield bonafide.do_provider_read(provider_id) +        config = yield bonafide.do_provider_read(provider_id, 'eip') -        prefix = os.path.join( -            self._basepath, "leap", "providers", provider, "keys") -        cert_path = key_path = os.path.join(prefix, "client", "openvpn.pem") -        ca_path = os.path.join(prefix, "ca", "cacert.pem") +        sorted_gateways = self._get_gateways(config) +        extra_flags = config.openvpn_configuration +        cert_path, ca_path = self._get_cert_paths(provider_id) +        key_path = cert_path +        anonvpn = self._has_anonvpn(provider)          if not os.path.isfile(cert_path): -            gotcert = yield self.do_get_cert('ignored@%s' % provider) -            if gotcert['get_cert'] != 'ok': -                raise ImproperlyConfigured( -                    'Cannot find client certificate. Please get one') +            yield self._maybe_get_anon_cert(anonvpn, provider_id)          if not os.path.isfile(ca_path):              raise ImproperlyConfigured( @@ -277,9 +288,45 @@ class VPNService(HookableService):          # TODO add remote ports, according to preferred sequence          remotes = tuple([(ip, '443') for ip in sorted_gateways])          self._tunnel = ConfiguredTunnel( -            provider, remotes, cert_path, key_path, ca_path, extra_flags) +            provider_id, remotes, cert_path, key_path, ca_path, extra_flags)          self._firewall = FirewallManager(remotes) +    def _get_gateways(self, config): +        return GatewaySelector( +            config.gateways, config.locations, +            preferred={'cc': self._cco, 'loc': self._loc} +        ).select_gateways() + +    def _get_cert_paths(self, provider_id): +        prefix = os.path.join( +            self._basepath, "leap", "providers", provider_id, "keys") +        cert_path = os.path.join(prefix, "client", "openvpn.pem") +        ca_path = os.path.join(prefix, "ca", "cacert.pem") +        return cert_path, ca_path + +    def _has_anonvpn(self, provider): +        try: +            allows_anonymous = provider.get('service').get('allow_anonymous') +        except (ValueError,): +            allows_anonymous = False +        return self._anonymous_enabled and allows_anonymous + +    @defer.inlineCallbacks +    def _maybe_get_anon_cert(self, anonvpn, provider_id): +        if anonvpn: +            self.log.debug('Getting anon VPN cert') +            gotcert = yield self.do_get_cert( +                'anonymous@%s' % provider_id, anonymous=True) +            if gotcert['get_cert'] != 'ok': +                raise ImproperlyConfigured( +                    '(anon) Could not get client certificate. ' +                    'Please get one') +        else: +            # this should instruct get_cert to get a session from bonafide +            # if we are authenticated. +            raise ImproperlyConfigured( +                'Cannot find client certificate. Please get one') +      def _write_last(self, domain):          path = os.path.join(self._basepath, self._last_vpn_path)          with open(path, 'w') as f: | 
