diff options
| -rw-r--r-- | mail/src/leap/mail/imap/account.py | 5 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/server.py | 47 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/service/imap-server.tac | 9 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/service/imap.py | 200 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/tests/utils.py | 5 | ||||
| -rw-r--r-- | mail/src/leap/mail/outgoing/service.py | 2 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/gateway.py | 3 | 
7 files changed, 150 insertions, 121 deletions
| diff --git a/mail/src/leap/mail/imap/account.py b/mail/src/leap/mail/imap/account.py index cc56fff..2f9ed1d 100644 --- a/mail/src/leap/mail/imap/account.py +++ b/mail/src/leap/mail/imap/account.py @@ -49,6 +49,7 @@ if PROFILE_CMD:  # Soledad IMAP Account  ####################################### +  class IMAPAccount(object):      """      An implementation of an imap4 Account @@ -68,8 +69,8 @@ class IMAPAccount(object):          You can either pass a deferred to this constructor, or use          `callWhenReady` method. -        :param user_id: The name of the account (user id, in the form -                        user@provider). +        :param user_id: The identifier of the user this account belongs to +                        (user id, in the form user@provider).          :type user_id: str          :param store: a Soledad instance. diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py index 0e5d011..2682db7 100644 --- a/mail/src/leap/mail/imap/server.py +++ b/mail/src/leap/mail/imap/server.py @@ -20,16 +20,10 @@ LEAP IMAP4 Server Implementation.  import StringIO  from copy import copy -from twisted import cred -from twisted.internet import reactor  from twisted.internet.defer import maybeDeferred  from twisted.mail import imap4  from twisted.python import log -from leap.common.check import leap_assert, leap_assert_type -from leap.common.events import emit_async, catalog -from leap.soledad.client import Soledad -  # imports for LITERAL+ patch  from twisted.internet import defer, interfaces  from twisted.mail.imap4 import IllegalClientResponse @@ -72,25 +66,6 @@ class LEAPIMAPServer(imap4.IMAP4Server):      """      An IMAP4 Server with a LEAP Storage Backend.      """ -    def __init__(self, *args, **kwargs): -        # pop extraneous arguments -        soledad = kwargs.pop('soledad', None) -        uuid = kwargs.pop('uuid', None) -        userid = kwargs.pop('userid', None) - -        leap_assert(soledad, "need a soledad instance") -        leap_assert_type(soledad, Soledad) -        leap_assert(uuid, "need a user in the initialization") - -        self._userid = userid - -        # initialize imap server! -        imap4.IMAP4Server.__init__(self, *args, **kwargs) - -        # we should initialize the account here, -        # but we move it to the factory so we can -        # populate the test account properly (and only once -        # per session)      #############################################################      # @@ -181,10 +156,6 @@ class LEAPIMAPServer(imap4.IMAP4Server):          :param line: the line from the server, without the line delimiter.          :type line: str          """ -        if self.theAccount.session_ended is True and self.state != "unauth": -            log.msg("Closing the session. State: unauth") -            self.state = "unauth" -          if "login" in line.lower():              # avoid to log the pass, even though we are using a dummy auth              # by now. @@ -208,24 +179,6 @@ class LEAPIMAPServer(imap4.IMAP4Server):              self.mbox = None          self.state = 'unauth' -    def authenticateLogin(self, username, password): -        """ -        Lookup the account with the given parameters, and deny -        the improper combinations. - -        :param username: the username that is attempting authentication. -        :type username: str -        :param password: the password to authenticate with. -        :type password: str -        """ -        # XXX this should use portal: -        # return portal.login(cred.credentials.UsernamePassword(user, pass) -        if username != self._userid: -            # bad username, reject. -            raise cred.error.UnauthorizedLogin() -        # any dummy password is allowed so far. use realm instead! -        emit_async(catalog.IMAP_CLIENT_LOGIN, username, "1") -        return imap4.IAccount, self.theAccount, lambda: None      def do_FETCH(self, tag, messages, query, uid=0):          """ diff --git a/mail/src/leap/mail/imap/service/imap-server.tac b/mail/src/leap/mail/imap/service/imap-server.tac index 2045757..c4d602d 100644 --- a/mail/src/leap/mail/imap/service/imap-server.tac +++ b/mail/src/leap/mail/imap/service/imap-server.tac @@ -1,3 +1,4 @@ +#!/usr/bin/env python  # -*- coding: utf-8 -*-  # imap-server.tac  # Copyright (C) 2013,2014 LEAP @@ -27,6 +28,9 @@ userid = 'user@provider'  uuid = 'deadbeefdeadabad'  passwd = 'supersecret' # optional, will get prompted if not found.  """ + +# TODO -- this .tac file should be deprecated in favor of bitmask.core.bitmaskd +  import ConfigParser  import getpass  import os @@ -112,7 +116,7 @@ tempdir = "/tmp/"  print "[~] user:", userid  soledad = initialize_soledad(uuid, userid, passwd, secrets, -                             localdb, gnupg_home, tempdir) +                             localdb, gnupg_home, tempdir, userid=userid)  km_args = (userid, "https://localhost", soledad)  km_kwargs = {      "token": "", @@ -131,7 +135,8 @@ keymanager = KeyManager(*km_args, **km_kwargs)  def getIMAPService(): -    factory = imap.LeapIMAPFactory(uuid, userid, soledad) +    soledad_sessions = {userid: soledad} +    factory = imap.LeapIMAPFactory(soledad_sessions)      return internet.TCPServer(port, factory, interface="localhost") diff --git a/mail/src/leap/mail/imap/service/imap.py b/mail/src/leap/mail/imap/service/imap.py index a50611b..24fa865 100644 --- a/mail/src/leap/mail/imap/service/imap.py +++ b/mail/src/leap/mail/imap/service/imap.py @@ -15,21 +15,26 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  """ -IMAP service initialization +IMAP Service Initialization.  """  import logging  import os  from collections import defaultdict +from twisted.cred.portal import Portal, IRealm +from twisted.cred.credentials import IUsernamePassword +from twisted.cred.checkers import ICredentialsChecker +from twisted.cred.error import UnauthorizedLogin +from twisted.mail.imap4 import IAccount +from twisted.internet import defer  from twisted.internet import reactor  from twisted.internet.error import CannotListenError  from twisted.internet.protocol import ServerFactory -from twisted.mail import imap4  from twisted.python import log +from zope.interface import implementer  from leap.common.events import emit_async, catalog -from leap.common.check import leap_check  from leap.mail.imap.account import IMAPAccount  from leap.mail.imap.server import LEAPIMAPServer @@ -41,57 +46,145 @@ DO_MANHOLE = os.environ.get("LEAP_MAIL_MANHOLE", None)  if DO_MANHOLE:      from leap.mail.imap.service import manhole -DO_PROFILE = os.environ.get("LEAP_PROFILE", None) -if DO_PROFILE: -    import cProfile -    log.msg("Starting PROFILING...") - -    PROFILE_DAT = "/tmp/leap_mail_profile.pstats" -    pr = cProfile.Profile() -    pr.enable() -  # The default port in which imap service will run +  IMAP_PORT = 1984 +# +# Credentials Handling +# + + +@implementer(IRealm) +class LocalSoledadIMAPRealm(object): + +    _encoding = 'utf-8' + +    def __init__(self, soledad_sessions): +        """ +        :param soledad_sessions: a dict-like object, containing instances +                                 of a Store (soledad instances), indexed by +                                 userid. +        """ +        self._soledad_sessions = soledad_sessions + +    def requestAvatar(self, avatarId, mind, *interfaces): +        if isinstance(avatarId, str): +            avatarId = avatarId.decode(self._encoding) + +        def gotSoledad(soledad): +            for iface in interfaces: +                if iface is IAccount: +                    avatar = IMAPAccount(avatarId, soledad) +                    return (IAccount, avatar, +                            getattr(avatar, 'logout', lambda: None)) +            raise NotImplementedError(self, interfaces) + +        return self.lookupSoledadInstance(avatarId).addCallback(gotSoledad) + +    def lookupSoledadInstance(self, userid): +        soledad = self._soledad_sessions[userid] +        # XXX this should return the instance after whenReady callback +        return defer.succeed(soledad) + + +@implementer(ICredentialsChecker) +class LocalSoledadTokenChecker(object): -class IMAPAuthRealm(object):      """ -    Dummy authentication realm. Do not use in production! +    A Credentials Checker for a LocalSoledad store. + +    It checks that: + +    1) The Local SoledadStorage has been correctly unlocked for the given +       user. This currently means that the right passphrase has been passed +       to the Local SoledadStorage. + +    2) The password passed in the credentials matches whatever token has +       been stored in the local encrypted SoledadStorage, associated to the +       Protocol that is requesting the authentication.      """ -    theAccount = None -    def requestAvatar(self, avatarId, mind, *interfaces): -        return imap4.IAccount, self.theAccount, lambda: None +    credentialInterfaces = (IUsernamePassword,) +    service = None + +    def __init__(self, soledad_sessions): +        """ +        :param soledad_sessions: a dict-like object, containing instances +                                 of a Store (soledad instances), indexed by +                                 userid. +        """ +        self._soledad_sessions = soledad_sessions + +    def requestAvatarId(self, credentials): +        if self.service is None: +            raise NotImplementedError( +                "this checker has not defined its service name") +        username, password = credentials.username, credentials.password +        d = self.checkSoledadToken(username, password, self.service) +        d.addErrback(lambda f: defer.fail(UnauthorizedLogin())) +        return d + +    def checkSoledadToken(self, username, password, service): +        soledad = self._soledad_sessions.get(username) +        if not soledad: +            return defer.fail(Exception("No soledad")) + +        def match_token(token): +            if token is None: +                raise RuntimeError('no token') +            if token == password: +                return username +            else: +                raise RuntimeError('bad token') + +        d = soledad.get_or_create_service_token(service) +        d.addCallback(match_token) +        return d + + +class IMAPTokenChecker(LocalSoledadTokenChecker): +    """A credentials checker that will lookup a token for the IMAP service.""" +    service = 'imap' + + +class LocalSoledadIMAPServer(LEAPIMAPServer): + +    """ +    An IMAP Server that authenticates against a LocalSoledad store. +    """ + +    def __init__(self, soledad_sessions, *args, **kw): + +        LEAPIMAPServer.__init__(self, *args, **kw) + +        realm = LocalSoledadIMAPRealm(soledad_sessions) +        portal = Portal(realm) +        checker = IMAPTokenChecker(soledad_sessions) +        self.checker = checker +        self.portal = portal +        portal.registerChecker(checker)  class LeapIMAPFactory(ServerFactory): +      """      Factory for a IMAP4 server with soledad remote sync and gpg-decryption      capabilities.      """ -    protocol = LEAPIMAPServer -    def __init__(self, uuid, userid, soledad): +    protocol = LocalSoledadIMAPServer + +    def __init__(self, soledad_sessions):          """          Initializes the server factory. -        :param uuid: user uuid -        :type uuid: str - -        :param userid: user id (user@provider.org) -        :type userid: str - -        :param soledad: soledad instance -        :type soledad: Soledad +        :param soledad_sessions: a dict-like object, containing instances +                                 of a Store (soledad instances), indexed by +                                 userid.          """ -        self._uuid = uuid -        self._userid = userid -        self._soledad = soledad - -        theAccount = IMAPAccount(uuid, soledad) -        self.theAccount = theAccount +        self._soledad_sessions = soledad_sessions          self._connections = defaultdict() -        # XXX how to pass the store along?      def buildProtocol(self, addr):          """ @@ -103,13 +196,7 @@ class LeapIMAPFactory(ServerFactory):          # TODO should reject anything from addr != localhost,          # just in case.          log.msg("Building protocol for connection %s" % addr) -        imapProtocol = self.protocol( -            uuid=self._uuid, -            userid=self._userid, -            soledad=self._soledad) -        imapProtocol.theAccount = self.theAccount -        imapProtocol.factory = self - +        imapProtocol = self.protocol(self._soledad_sessions)          self._connections[addr] = imapProtocol          return imapProtocol @@ -123,39 +210,21 @@ class LeapIMAPFactory(ServerFactory):          """          Stops imap service (fetcher, factory and port).          """ -        # mark account as unusable, so any imap command will fail -        # with unauth state. -        self.theAccount.end_session() - -        # TODO should wait for all the pending deferreds, -        # the twisted way! -        if DO_PROFILE: -            log.msg("Stopping PROFILING") -            pr.disable() -            pr.dump_stats(PROFILE_DAT) -          return ServerFactory.doStop(self) -def run_service(store, **kwargs): +def run_service(soledad_sessions, port=IMAP_PORT):      """      Main entry point to run the service from the client. -    :param store: a soledad instance +    :param soledad_sessions: a dict-like object, containing instances +                             of a Store (soledad instances), indexed by userid.      :returns: the port as returned by the reactor when starts listening, and                the factory for the protocol. +    :rtype: tuple      """ -    leap_check(store, "store cannot be None") -    # XXX this can also be a ProxiedObject, FIXME -    # leap_assert_type(store, Soledad) - -    port = kwargs.get('port', IMAP_PORT) -    userid = kwargs.get('userid', None) -    leap_check(userid is not None, "need an user id") - -    uuid = store.uuid -    factory = LeapIMAPFactory(uuid, userid, store) +    factory = LeapIMAPFactory(soledad_sessions)      try:          interface = "localhost" @@ -164,6 +233,7 @@ def run_service(store, **kwargs):          if os.environ.get("LEAP_DOCKERIZED"):              interface = '' +        # TODO use Endpoints !!!          tport = reactor.listenTCP(port, factory,                                    interface=interface)      except CannotListenError: @@ -178,9 +248,9 @@ def run_service(store, **kwargs):              # TODO get pass from env var.too.              manhole_factory = manhole.getManholeFactory(                  {'f': factory, -                 'a': factory.theAccount,                   'gm': factory.theAccount.getMailbox},                  "boss", "leap") +            # TODO  use Endpoints !!!              reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory,                                interface="127.0.0.1")          logger.debug("IMAP4 Server is RUNNING in port  %s" % (port,)) diff --git a/mail/src/leap/mail/imap/tests/utils.py b/mail/src/leap/mail/imap/tests/utils.py index a34538b..b1f8563 100644 --- a/mail/src/leap/mail/imap/tests/utils.py +++ b/mail/src/leap/mail/imap/tests/utils.py @@ -77,14 +77,11 @@ class IMAP4HelperMixin(SoledadTestMixin):          soledad_adaptor.cleanup_deferred_locks() -        UUID = 'deadbeef',          USERID = TEST_USER          def setup_server(account):              self.server = LEAPIMAPServer( -                uuid=UUID, userid=USERID, -                contextFactory=self.serverCTX, -                soledad=self._soledad) +                contextFactory=self.serverCTX)              self.server.theAccount = account              d_server_ready = defer.Deferred() diff --git a/mail/src/leap/mail/outgoing/service.py b/mail/src/leap/mail/outgoing/service.py index 3bd0ea2..7943c12 100644 --- a/mail/src/leap/mail/outgoing/service.py +++ b/mail/src/leap/mail/outgoing/service.py @@ -71,7 +71,7 @@ class OutgoingMail:      def __init__(self, from_address, keymanager, cert, key, host, port):          """ -        Initialize the mail service. +        Initialize the outgoing mail service.          :param from_address: The sender address.          :type from_address: str diff --git a/mail/src/leap/mail/smtp/gateway.py b/mail/src/leap/mail/smtp/gateway.py index 3657250..3c86d7e 100644 --- a/mail/src/leap/mail/smtp/gateway.py +++ b/mail/src/leap/mail/smtp/gateway.py @@ -49,6 +49,9 @@ from email import generator  generator.Generator = RFC3156CompliantGenerator +# TODO -- implement Queue using twisted.mail.mail.MailService + +  #  # Helper utilities  # | 
