summaryrefslogtreecommitdiff
path: root/src/leap/mail/imap/service
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2015-11-26 21:27:23 -0400
committerKali Kaneko <kali@leap.se>2015-12-15 02:30:19 -0400
commit8ddddd77478984777be79e817458183a41f0a978 (patch)
treedb50d1332acd997df5bb732a2dcf0acde3d80207 /src/leap/mail/imap/service
parentcbfc7cd2acb1ab1d58030ab9a5205a295be1ca5d (diff)
[feat] credentials handling: use twisted.cred
Diffstat (limited to 'src/leap/mail/imap/service')
-rw-r--r--src/leap/mail/imap/service/imap-server.tac9
-rw-r--r--src/leap/mail/imap/service/imap.py200
2 files changed, 142 insertions, 67 deletions
diff --git a/src/leap/mail/imap/service/imap-server.tac b/src/leap/mail/imap/service/imap-server.tac
index 2045757..c4d602d 100644
--- a/src/leap/mail/imap/service/imap-server.tac
+++ b/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/src/leap/mail/imap/service/imap.py b/src/leap/mail/imap/service/imap.py
index a50611b..24fa865 100644
--- a/src/leap/mail/imap/service/imap.py
+++ b/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,))