[prev in list] [next in list] [prev in thread] [next in thread] 

List:       calendarserver-changes
Subject:    [CalendarServer-changes] [4297] CalendarServer/trunk
From:       source_changes () macosforge ! org
Date:       2009-05-19 21:28:47
Message-ID: 20090519212847.BA5691B24A6C () beta ! macosforge ! org
[Download RAW message or body]

[Attachment #2 (multipart/alternative)]


Revision: 4297
          http://trac.macosforge.org/projects/calendarserver/changeset/4297
Author:   sagen@apple.com
Date:     2009-05-19 14:28:46 -0700 (Tue, 19 May 2009)
Log Message:
-----------
Mail gateway's /inbox now requires authentication

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/twistedcaldav/mail.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip.py
    CalendarServer/trunk/twistedcaldav/util.py

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2009-05-19 21:02:35 UTC (rev \
                4296)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2009-05-19 21:28:46 UTC (rev \
4297) @@ -75,7 +75,7 @@
 from twistedcaldav.static import CalendarHomeProvisioningFile
 from twistedcaldav.static import IScheduleInboxFile
 from twistedcaldav.static import TimezoneServiceFile
-from twistedcaldav.mail import IMIPInboxResource
+from twistedcaldav.mail import IMIPReplyInboxResource
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav.upgrade import upgradeData
 from twistedcaldav.pdmonster import PDClientAddressWrapper
@@ -339,7 +339,7 @@
     principalResourceClass       = DirectoryPrincipalProvisioningResource
     calendarResourceClass        = CalendarHomeProvisioningFile
     iScheduleResourceClass       = IScheduleInboxFile
-    imipResourceClass            = IMIPInboxResource
+    imipResourceClass            = IMIPReplyInboxResource
     timezoneServiceResourceClass = TimezoneServiceFile
     webCalendarResourceClass     = WebCalendarResource
     webAdminResourceClass        = WebAdminResource

Modified: CalendarServer/trunk/twistedcaldav/mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/mail.py	2009-05-19 21:02:35 UTC (rev 4296)
+++ CalendarServer/trunk/twistedcaldav/mail.py	2009-05-19 21:28:46 UTC (rev 4297)
@@ -20,36 +20,46 @@
 """
 from __future__ import with_statement
 
+from calendarserver.provision.root import RootResource
+
 from email.mime.image import MIMEImage
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 
 from twisted.application import internet, service
+from twisted.cred.portal import Portal
 from twisted.internet import protocol, defer, ssl, reactor
+from twisted.internet.address import IPv4Address
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twisted.mail import pop3client, imap4
 from twisted.mail.smtp import messageid, rfc822date, ESMTPSenderFactory
 from twisted.plugin import IPlugin
-from twisted.python.usage import Options, UsageError
 from twisted.python import failure
-from twisted.web import resource, server, client
-from twisted.web2 import responsecode
+from twisted.python.reflect import namedClass
+from twisted.python.usage import Options, UsageError
+from twisted.web import client
+from twisted.web2 import resource, server, responsecode
+from twisted.web2.channel.http import HTTPFactory
+from twisted.web2.dav import auth
 from twisted.web2.dav import davxml
 from twisted.web2.dav.noneprops import NonePropertyStore
 from twisted.web2.http import Response, HTTPError
 from twisted.web2.http_headers import MimeType
 
-from twistedcaldav.directory.digest import QopDigestCredentialFactory
 from twistedcaldav import ical, caldavxml
+from twistedcaldav import memcachepool
 from twistedcaldav.config import config, defaultConfig, defaultConfigFile
+from twistedcaldav.directory.digest import QopDigestCredentialFactory
+from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
+from twistedcaldav.directory.util import NotFilePath
 from twistedcaldav.ical import Property
+from twistedcaldav.localization import translationTo
 from twistedcaldav.log import Logger, LoggingMixIn
-from twistedcaldav.directory.util import NotFilePath
-from twistedcaldav.scheduling.scheduler import IMIPScheduler
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
-from twistedcaldav.static import CalDAVFile, deliverSchedulePrivilegeSet
+from twistedcaldav.scheduling.scheduler import IMIPScheduler
 from twistedcaldav.sql import AbstractSQLDatabase
-from twistedcaldav.localization import translationTo
+from twistedcaldav.static import CalDAVFile, deliverSchedulePrivilegeSet
+from twistedcaldav.util import AuthorizedHTTPGetter
 
 from zope.interface import implements
 
@@ -57,8 +67,6 @@
 import email.utils
 import os
 import uuid
-from hashlib import md5, sha1
-import base64
 
 try:
     from cStringIO import StringIO
@@ -251,22 +259,6 @@
         response.headers.setHeader("content-type", MimeType("text", "html"))
         return response
 
-    @inlineCallbacks
-    def http_POST(self, request):
-        """
-        The IMIP delivery POST method.
-        """
-
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
-
-        # Inject using the IMIPScheduler.
-        scheduler = IMIPScheduler(request, self)
-
-        # Do the POST processing treating this as a non-local schedule
-        result = (yield scheduler.doSchedulingViaPOST(use_request_headers=True))
-        returnValue(result.response())
-
     ##
     # File
     ##
@@ -299,230 +291,54 @@
         return succeed(deliverSchedulePrivilegeSet)
 
 
+class IMIPReplyInboxResource(IMIPInboxResource):
 
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The IMIP reply POST method (inbound)
+        """
 
-algorithms = {
-    'md5': md5,
-    'md5-sess': md5,
-    'sha': sha1,
-}
+        # Check authentication and access controls
+        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
 
-# DigestCalcHA1
-def calcHA1(
-    pszAlg,
-    pszUserName,
-    pszRealm,
-    pszPassword,
-    pszNonce,
-    pszCNonce,
-    preHA1=None
-):
-    """
-    @param pszAlg: The name of the algorithm to use to calculate the digest.
-        Currently supported are md5 md5-sess and sha.
+        # Inject using the IMIPScheduler.
+        scheduler = IMIPScheduler(request, self)
 
-    @param pszUserName: The username
-    @param pszRealm: The realm
-    @param pszPassword: The password
-    @param pszNonce: The nonce
-    @param pszCNonce: The cnonce
+        # Do the POST processing treating this as a non-local schedule
+        result = (yield scheduler.doSchedulingViaPOST(use_request_headers=True))
+        returnValue(result.response())
 
-    @param preHA1: If available this is a str containing a previously
-       calculated HA1 as a hex string. If this is given then the values for
-       pszUserName, pszRealm, and pszPassword are ignored.
-    """
 
-    if (preHA1 and (pszUserName or pszRealm or pszPassword)):
-        raise TypeError(("preHA1 is incompatible with the pszUserName, "
-                         "pszRealm, and pszPassword arguments"))
+class IMIPInvitationInboxResource(IMIPInboxResource):
 
-    if preHA1 is None:
-        # We need to calculate the HA1 from the username:realm:password
-        m = algorithms[pszAlg]()
-        m.update(pszUserName)
-        m.update(":")
-        m.update(pszRealm)
-        m.update(":")
-        m.update(pszPassword)
-        HA1 = m.digest()
-    else:
-        # We were given a username:realm:password
-        HA1 = preHA1.decode('hex')
+    def __init__(self, parent, mailer):
+        super(IMIPInvitationInboxResource, self).__init__(parent)
+        self.mailer = mailer
 
-    if pszAlg == "md5-sess":
-        m = algorithms[pszAlg]()
-        m.update(HA1)
-        m.update(":")
-        m.update(pszNonce)
-        m.update(":")
-        m.update(pszCNonce)
-        HA1 = m.digest()
+    @inlineCallbacks
+    def http_POST(self, request):
+        """
+        The IMIP invitation POST method (outbound)
+        """
 
-    return HA1.encode('hex')
+        # Check authentication and access controls
+        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
 
-# DigestCalcResponse
-def calcResponse(
-    HA1,
-    algo,
-    pszNonce,
-    pszNonceCount,
-    pszCNonce,
-    pszQop,
-    pszMethod,
-    pszDigestUri,
-    pszHEntity,
-):
-    m = algorithms[algo]()
-    m.update(pszMethod)
-    m.update(":")
-    m.update(pszDigestUri)
-    if pszQop == "auth-int":
-        m.update(":")
-        m.update(pszHEntity)
-    HA2 = m.digest().encode('hex')
+        # Compute token, add to db, generate email and send it
+        calendar = (yield ical.Component.fromIStream(request.stream))
+        originator = request.headers.getRawHeaders("originator")[0]
+        recipient = request.headers.getRawHeaders("recipient")[0]
+        language = config.Localization.Language
 
-    m = algorithms[algo]()
-    m.update(HA1)
-    m.update(":")
-    m.update(pszNonce)
-    m.update(":")
-    if pszNonceCount and pszCNonce and pszQop:
-        m.update(pszNonceCount)
-        m.update(":")
-        m.update(pszCNonce)
-        m.update(":")
-        m.update(pszQop)
-        m.update(":")
-    m.update(HA2)
-    respHash = m.digest().encode('hex')
-    return respHash
+        if not (yield self.mailer.outbound(originator,
+            recipient, calendar, language=language)):
+            returnValue(Response(code=responsecode.BAD_REQUEST))
 
-class Unauthorized(Exception):
-    pass
+        returnValue(Response(code=responsecode.OK))
 
-class AuthorizedHTTPGetter(client.HTTPPageGetter, LoggingMixIn):
 
-    def handleStatus_401(self):
 
-        self.quietLoss = 1
-        self.transport.loseConnection()
-
-        if not hasattr(self.factory, "username"):
-            self.factory.deferred.errback(failure.Failure(Unauthorized("Mail gateway \
                not able to process reply; authentication required for calendar \
                server")))
-            return self.factory.deferred
-
-        if hasattr(self.factory, "retried"):
-            self.factory.deferred.errback(failure.Failure(Unauthorized("Mail gateway \
not able to process reply; could not authenticate user %s with calendar server" % \
                (self.factory.username,))))
-            return self.factory.deferred
-
-        self.factory.retried = True
-
-        # self.log_debug("Got a 401 trying to inject [%s]" % (self.headers,))
-        details = {}
-        basicAvailable = digestAvailable = False
-        wwwauth = self.headers.get("www-authenticate")
-        for item in wwwauth:
-            if item.startswith("basic "):
-                basicAvailable = True
-            if item.startswith("digest "):
-                digestAvailable = True
-                wwwauth = item[7:]
-                def unq(s):
-                    if s[0] == s[-1] == '"':
-                        return s[1:-1]
-                    return s
-                parts = wwwauth.split(',')
-                for (k, v) in [p.split('=', 1) for p in parts]:
-                    details[k.strip()] = unq(v.strip())
-
-        user = self.factory.username
-        pswd = self.factory.password
-
-        if digestAvailable and details:
-            digest = calcResponse(
-                calcHA1(
-                    details.get('algorithm'),
-                    user,
-                    details.get('realm'),
-                    pswd,
-                    details.get('nonce'),
-                    details.get('cnonce')
-                ),
-                details.get('algorithm'),
-                details.get('nonce'),
-                details.get('nc'),
-                details.get('cnonce'),
-                details.get('qop'),
-                self.factory.method,
-                self.factory.url,
-                None
-            )
-
-            if details.get('qop'):
-                response = (
-                    'Digest username="%s", realm="%s", nonce="%s", uri="%s", '
-                    'response=%s, algorithm=%s, cnonce="%s", qop=%s, nc=%s' %
-                    (
-                        user,
-                        details.get('realm'),
-                        details.get('nonce'),
-                        self.factory.url,
-                        digest,
-                        details.get('algorithm'),
-                        details.get('cnonce'),
-                        details.get('qop'),
-                        details.get('nc'),
-                    )
-                )
-            else:
-                response = (
-                    'Digest username="%s", realm="%s", nonce="%s", uri="%s", '
-                    'response=%s, algorithm=%s' %
-                    (
-                        user,
-                        details.get('realm'),
-                        details.get('nonce'),
-                        self.factory.url,
-                        digest,
-                        details.get('algorithm'),
-                    )
-                )
-
-            self.factory.headers['Authorization'] = response
-
-            if self.factory.scheme == 'https':
-                reactor.connectSSL(self.factory.host, self.factory.port,
-                    self.factory, ssl.ClientContextFactory())
-            else:
-                reactor.connectTCP(self.factory.host, self.factory.port,
-                    self.factory)
-            # self.log_debug("Retrying with digest after 401")
-
-            return self.factory.deferred
-
-        elif basicAvailable:
-            basicauth = "%s:%s" % (user, pswd)
-            basicauth = "Basic " + base64.encodestring( basicauth )
-            basicauth = basicauth.replace( "\n", "" )
-
-            self.factory.headers['Authorization'] = basicauth
-
-            if self.factory.scheme == 'https':
-                reactor.connectSSL(self.factory.host, self.factory.port,
-                    self.factory, ssl.ClientContextFactory())
-            else:
-                reactor.connectTCP(self.factory.host, self.factory.port,
-                    self.factory)
-            # self.log_debug("Retrying with basic after 401")
-
-            return self.factory.deferred
-
-
-        else:
-            self.factory.deferred.errback(failure.Failure(Unauthorized("Mail gateway \
not able to process reply; calendar server returned 401 and doesn't support basic or \
                digest")))
-            return self.factory.deferred
-
-
 def injectMessage(organizer, attendee, calendar, msgId, reactor=None):
 
     if reactor is None:
@@ -726,6 +542,16 @@
 
     def makeService(self, options):
 
+        if config.Memcached.ClientEnabled:
+            memcachepool.installPool(
+                IPv4Address(
+                    "TCP",
+                    config.Memcached.BindAddress,
+                    config.Memcached.Port,
+                ),
+                config.Memcached.MaxClients,
+            )
+
         multiService = service.MultiService()
 
         settings = config.Scheduling['iMIP']
@@ -747,7 +573,10 @@
 
             client.setServiceParent(multiService)
 
+
+            # Set up /inbox -- server POSTs to it to send out iMIP invites
             IScheduleService(settings, mailer).setServiceParent(multiService)
+
         else:
             self.log_info("Mail Gateway Service not enabled")
 
@@ -762,12 +591,43 @@
     def __init__(self, settings, mailer):
         self.settings = settings
         self.mailer = mailer
-        root = resource.Resource()
-        root.putChild('', self.HomePage())
-        root.putChild('inbox', self.IScheduleInbox(mailer))
-        self.site = server.Site(root)
-        self.server = internet.TCPServer(settings['MailGatewayPort'], self.site)
 
+        directoryClass = namedClass(config.DirectoryService.type)
+        directory = directoryClass(config.DirectoryService.params)
+
+        principalCollection = DirectoryPrincipalProvisioningResource(
+            "/principals/",
+            directory,
+        )
+
+        root = RootResource(
+            config.DocumentRoot,
+            principalCollections=(principalCollection,),
+        )
+
+        # Authenticated /inbox
+        credentialFactories = []
+        portal = Portal(auth.DavRealm())
+        portal.registerChecker(directory)
+        realm = directory.realmName or ""
+        schemeConfig = config.Authentication.Digest
+        digestCredentialFactory = QopDigestCredentialFactory(
+            schemeConfig["Algorithm"],
+            schemeConfig["Qop"],
+            realm,
+        )
+        root.putChild('inbox',
+            auth.AuthenticationWrapper(
+                IMIPInvitationInboxResource(root, mailer),
+                portal,
+                (digestCredentialFactory,),
+                (auth.IPrincipal,),
+            )
+        )
+        self.factory = HTTPFactory(server.Site(root))
+        self.server = internet.TCPServer(settings['MailGatewayPort'],
+            self.factory)
+
     def startService(self):
         self.server.startService()
 
@@ -775,45 +635,8 @@
         self.server.stopService()
 
 
-    class HomePage(resource.Resource):
-        def render(self, request):
-            return """
-            <html>
-            <head><title>ISchedule - IMIP Gateway</title></head>
-            <body>ISchedule - IMIP Gateway</body>
-            </html>
-            """
 
-    class IScheduleInbox(resource.Resource):
 
-        def __init__(self, mailer):
-            resource.Resource.__init__(self)
-            self.mailer = mailer
-
-        def render_GET(self, request):
-            return """
-            <html>
-            <head><title>ISchedule Inbox</title></head>
-            <body>ISchedule Inbox</body>
-            </html>
-            """
-
-        def render_POST(self, request):
-            # Compute token, add to db, generate email and send it
-            calendar = ical.Component.fromString(request.content.read())
-            headers = request.getAllHeaders()
-            language = config.Localization.Language
-            self.mailer.outbound(headers['originator'], headers['recipient'],
-                calendar, language=language)
-
-            # TODO: what to return?
-            return """
-            <html>
-            <head><title>ISchedule Inbox</title></head>
-            <body>ISchedule Inbox</body>
-            </html>
-            """
-
 class MailHandler(LoggingMixIn):
 
     def __init__(self, dataRoot=None):
@@ -1059,10 +882,12 @@
         def _success(result, msgId, fromAddr, toAddr):
             self.log_info("Mail gateway sent message %s from %s to %s" %
                 (msgId, fromAddr, toAddr))
+            return True
 
         def _failure(failure, msgId, fromAddr, toAddr):
             self.log_error("Mail gateway failed to send message %s from %s to %s \
(Reason: %s)" %  (msgId, fromAddr, toAddr, failure.getErrorMessage()))
+            return False
 
         deferred = defer.Deferred()
 
@@ -1080,6 +905,7 @@
         reactor.connectTCP(settings['Server'], settings['Port'], factory)
         deferred.addCallback(_success, msgId, fromAddr, toAddr)
         deferred.addErrback(_failure, msgId, fromAddr, toAddr)
+        return deferred
 
 
     def getIconPath(self, details, canceled, language='en'):
@@ -1507,7 +1333,7 @@
         self.log_error("IMAP Error: %s" % (error,))
 
     def ebAuthenticateFailed(self, reason):
-        self.log_info("IMAP authenticate failed for %s, trying login" %
+        self.log_debug("IMAP authenticate failed for %s, trying login" %
             (self.factory.settings["Username"],))
         return self.login(self.factory.settings["Username"],
             self.factory.settings["Password"]

Modified: CalendarServer/trunk/twistedcaldav/scheduling/imip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip.py	2009-05-19 21:02:35 UTC \
                (rev 4296)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip.py	2009-05-19 21:28:46 UTC \
(rev 4297) @@ -27,6 +27,7 @@
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.log import Logger
+from twistedcaldav.util import AuthorizedHTTPGetter
 from twistedcaldav.scheduling.delivery import DeliveryService
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
 
@@ -98,6 +99,13 @@
         }
         factory = client.HTTPClientFactory(url, method='POST', headers=headers,
             postdata=caldata, agent="CalDAV server")
+
+        if config.Scheduling.iMIP.Username:
+            factory.username = config.Scheduling.iMIP.Username
+            factory.password = config.Scheduling.iMIP.Password
+
+        factory.noisy = False
+        factory.protocol = AuthorizedHTTPGetter
         reactor.connectTCP(mailGatewayServer, mailGatewayPort, factory)
         return factory.deferred
 

Modified: CalendarServer/trunk/twistedcaldav/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/util.py	2009-05-19 21:02:35 UTC (rev 4296)
+++ CalendarServer/trunk/twistedcaldav/util.py	2009-05-19 21:28:46 UTC (rev 4297)
@@ -19,6 +19,13 @@
 import sys
 from subprocess import Popen, PIPE, STDOUT
 
+from twisted.internet import ssl, reactor
+from twisted.web import client
+from twistedcaldav.log import Logger, LoggingMixIn
+from twisted.python import failure
+from hashlib import md5, sha1
+import base64
+
 ##
 # getNCPU
 ##
@@ -161,3 +168,232 @@
     else:
         error = "Keychain access utility ('security') not found"
         raise KeychainAccessError(error)
+
+
+
+
+##
+# Digest/Basic-capable HTTP GET factory
+##
+
+algorithms = {
+    'md5': md5,
+    'md5-sess': md5,
+    'sha': sha1,
+}
+
+# DigestCalcHA1
+def calcHA1(
+    pszAlg,
+    pszUserName,
+    pszRealm,
+    pszPassword,
+    pszNonce,
+    pszCNonce,
+    preHA1=None
+):
+    """
+    @param pszAlg: The name of the algorithm to use to calculate the digest.
+        Currently supported are md5 md5-sess and sha.
+
+    @param pszUserName: The username
+    @param pszRealm: The realm
+    @param pszPassword: The password
+    @param pszNonce: The nonce
+    @param pszCNonce: The cnonce
+
+    @param preHA1: If available this is a str containing a previously
+       calculated HA1 as a hex string. If this is given then the values for
+       pszUserName, pszRealm, and pszPassword are ignored.
+    """
+
+    if (preHA1 and (pszUserName or pszRealm or pszPassword)):
+        raise TypeError(("preHA1 is incompatible with the pszUserName, "
+                         "pszRealm, and pszPassword arguments"))
+
+    if preHA1 is None:
+        # We need to calculate the HA1 from the username:realm:password
+        m = algorithms[pszAlg]()
+        m.update(pszUserName)
+        m.update(":")
+        m.update(pszRealm)
+        m.update(":")
+        m.update(pszPassword)
+        HA1 = m.digest()
+    else:
+        # We were given a username:realm:password
+        HA1 = preHA1.decode('hex')
+
+    if pszAlg == "md5-sess":
+        m = algorithms[pszAlg]()
+        m.update(HA1)
+        m.update(":")
+        m.update(pszNonce)
+        m.update(":")
+        m.update(pszCNonce)
+        HA1 = m.digest()
+
+    return HA1.encode('hex')
+
+# DigestCalcResponse
+def calcResponse(
+    HA1,
+    algo,
+    pszNonce,
+    pszNonceCount,
+    pszCNonce,
+    pszQop,
+    pszMethod,
+    pszDigestUri,
+    pszHEntity,
+):
+    m = algorithms[algo]()
+    m.update(pszMethod)
+    m.update(":")
+    m.update(pszDigestUri)
+    if pszQop == "auth-int":
+        m.update(":")
+        m.update(pszHEntity)
+    HA2 = m.digest().encode('hex')
+
+    m = algorithms[algo]()
+    m.update(HA1)
+    m.update(":")
+    m.update(pszNonce)
+    m.update(":")
+    if pszNonceCount and pszCNonce and pszQop:
+        m.update(pszNonceCount)
+        m.update(":")
+        m.update(pszCNonce)
+        m.update(":")
+        m.update(pszQop)
+        m.update(":")
+    m.update(HA2)
+    respHash = m.digest().encode('hex')
+    return respHash
+
+class Unauthorized(Exception):
+    pass
+
+class AuthorizedHTTPGetter(client.HTTPPageGetter, LoggingMixIn):
+
+    def handleStatus_401(self):
+
+        self.quietLoss = 1
+        self.transport.loseConnection()
+
+        if not hasattr(self.factory, "username"):
+            self.factory.deferred.errback(failure.Failure(Unauthorized("Mail gateway \
not able to process reply; authentication required for calendar server"))) +          \
return self.factory.deferred +
+        if hasattr(self.factory, "retried"):
+            self.factory.deferred.errback(failure.Failure(Unauthorized("Mail gateway \
not able to process reply; could not authenticate user %s with calendar server" % \
(self.factory.username,)))) +            return self.factory.deferred
+
+        self.factory.retried = True
+
+        # self.log_debug("Got a 401 trying to inject [%s]" % (self.headers,))
+        details = {}
+        basicAvailable = digestAvailable = False
+        wwwauth = self.headers.get("www-authenticate")
+        for item in wwwauth:
+            if item.startswith("basic "):
+                basicAvailable = True
+            if item.startswith("digest "):
+                digestAvailable = True
+                wwwauth = item[7:]
+                def unq(s):
+                    if s[0] == s[-1] == '"':
+                        return s[1:-1]
+                    return s
+                parts = wwwauth.split(',')
+                for (k, v) in [p.split('=', 1) for p in parts]:
+                    details[k.strip()] = unq(v.strip())
+
+        user = self.factory.username
+        pswd = self.factory.password
+
+        if digestAvailable and details:
+            digest = calcResponse(
+                calcHA1(
+                    details.get('algorithm'),
+                    user,
+                    details.get('realm'),
+                    pswd,
+                    details.get('nonce'),
+                    details.get('cnonce')
+                ),
+                details.get('algorithm'),
+                details.get('nonce'),
+                details.get('nc'),
+                details.get('cnonce'),
+                details.get('qop'),
+                self.factory.method,
+                self.factory.url,
+                None
+            )
+
+            if details.get('qop'):
+                response = (
+                    'Digest username="%s", realm="%s", nonce="%s", uri="%s", '
+                    'response=%s, algorithm=%s, cnonce="%s", qop=%s, nc=%s' %
+                    (
+                        user,
+                        details.get('realm'),
+                        details.get('nonce'),
+                        self.factory.url,
+                        digest,
+                        details.get('algorithm'),
+                        details.get('cnonce'),
+                        details.get('qop'),
+                        details.get('nc'),
+                    )
+                )
+            else:
+                response = (
+                    'Digest username="%s", realm="%s", nonce="%s", uri="%s", '
+                    'response=%s, algorithm=%s' %
+                    (
+                        user,
+                        details.get('realm'),
+                        details.get('nonce'),
+                        self.factory.url,
+                        digest,
+                        details.get('algorithm'),
+                    )
+                )
+
+            self.factory.headers['Authorization'] = response
+
+            if self.factory.scheme == 'https':
+                reactor.connectSSL(self.factory.host, self.factory.port,
+                    self.factory, ssl.ClientContextFactory())
+            else:
+                reactor.connectTCP(self.factory.host, self.factory.port,
+                    self.factory)
+            # self.log_debug("Retrying with digest after 401")
+
+            return self.factory.deferred
+
+        elif basicAvailable:
+            basicauth = "%s:%s" % (user, pswd)
+            basicauth = "Basic " + base64.encodestring( basicauth )
+            basicauth = basicauth.replace( "\n", "" )
+
+            self.factory.headers['Authorization'] = basicauth
+
+            if self.factory.scheme == 'https':
+                reactor.connectSSL(self.factory.host, self.factory.port,
+                    self.factory, ssl.ClientContextFactory())
+            else:
+                reactor.connectTCP(self.factory.host, self.factory.port,
+                    self.factory)
+            # self.log_debug("Retrying with basic after 401")
+
+            return self.factory.deferred
+
+
+        else:
+            self.factory.deferred.errback(failure.Failure(Unauthorized("Mail gateway \
not able to process reply; calendar server returned 401 and doesn't support basic or \
digest"))) +            return self.factory.deferred
+


[Attachment #5 (text/html)]

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[4297] CalendarServer/trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: \
verdana,arial,helvetica,sans-serif; font-size: 10pt;  } #msg dl a { font-weight: \
bold} #msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: \
bold; } #msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: \
6px; } #logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em \
0; } #logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg \
h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; } \
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; \
} #logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: \
-1.5em; padding-left: 1.5em; } #logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em \
1em 0 1em; background: white;} #logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid \
#fa0; border-bottom: 1px solid #fa0; background: #fff; } #logmsg table th { \
text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted \
#fa0; } #logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: \
0.2em 0.5em; } #logmsg table thead th { text-align: center; border-bottom: 1px solid \
#fa0; } #logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: \
6px; } #patch { width: 100%; }
#patch h4 {font-family: \
verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
 #patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, \
#patch .copfile {border:1px solid #ccc;margin:10px 0;} #patch ins \
{background:#dfd;text-decoration:none;display:block;padding:0 10px;} #patch del \
{background:#fdd;text-decoration:none;display:block;padding:0 10px;} #patch .lines, \
                .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a \
href="http://trac.macosforge.org/projects/calendarserver/changeset/4297">4297</a></dd>
 <dt>Author</dt> <dd>sagen@apple.com</dd>
<dt>Date</dt> <dd>2009-05-19 14:28:46 -0700 (Tue, 19 May 2009)</dd>
</dl>

<h3>Log Message</h3>
<pre>Mail gateway's /inbox now requires authentication</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertapcaldavpy">CalendarServer/trunk/calendarserver/tap/caldav.py</a></li>
 <li><a href="#CalendarServertrunktwistedcaldavmailpy">CalendarServer/trunk/twistedcaldav/mail.py</a></li>
 <li><a href="#CalendarServertrunktwistedcaldavschedulingimippy">CalendarServer/trunk/twistedcaldav/scheduling/imip.py</a></li>
 <li><a href="#CalendarServertrunktwistedcaldavutilpy">CalendarServer/trunk/twistedcaldav/util.py</a></li>
 </ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertapcaldavpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tap/caldav.py \
(4296 => 4297)</h4> <pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tap/caldav.py	2009-05-19 \
                21:02:35 UTC (rev 4296)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2009-05-19 21:28:46 UTC (rev \
4297) </span><span class="lines">@@ -75,7 +75,7 @@
</span><span class="cx"> from twistedcaldav.static import \
CalendarHomeProvisioningFile </span><span class="cx"> from twistedcaldav.static \
import IScheduleInboxFile </span><span class="cx"> from twistedcaldav.static import \
TimezoneServiceFile </span><del>-from twistedcaldav.mail import IMIPInboxResource
</del><ins>+from twistedcaldav.mail import IMIPReplyInboxResource
</ins><span class="cx"> from twistedcaldav.timezones import TimezoneCache
</span><span class="cx"> from twistedcaldav.upgrade import upgradeData
</span><span class="cx"> from twistedcaldav.pdmonster import PDClientAddressWrapper
</span><span class="lines">@@ -339,7 +339,7 @@
</span><span class="cx">     principalResourceClass       = \
DirectoryPrincipalProvisioningResource </span><span class="cx">     \
calendarResourceClass        = CalendarHomeProvisioningFile </span><span class="cx">  \
iScheduleResourceClass       = IScheduleInboxFile </span><del>-    imipResourceClass  \
= IMIPInboxResource </del><ins>+    imipResourceClass            = \
IMIPReplyInboxResource </ins><span class="cx">     timezoneServiceResourceClass = \
TimezoneServiceFile </span><span class="cx">     webCalendarResourceClass     = \
WebCalendarResource </span><span class="cx">     webAdminResourceClass        = \
WebAdminResource </span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavmailpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/mail.py (4296 \
=> 4297)</h4> <pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/mail.py	2009-05-19 21:02:35 \
                UTC (rev 4296)
+++ CalendarServer/trunk/twistedcaldav/mail.py	2009-05-19 21:28:46 UTC (rev 4297)
</span><span class="lines">@@ -20,36 +20,46 @@
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> from __future__ import with_statement
</span><span class="cx"> 
</span><ins>+from calendarserver.provision.root import RootResource
+
</ins><span class="cx"> from email.mime.image import MIMEImage
</span><span class="cx"> from email.mime.multipart import MIMEMultipart
</span><span class="cx"> from email.mime.text import MIMEText
</span><span class="cx"> 
</span><span class="cx"> from twisted.application import internet, service
</span><ins>+from twisted.cred.portal import Portal
</ins><span class="cx"> from twisted.internet import protocol, defer, ssl, reactor
</span><ins>+from twisted.internet.address import IPv4Address
</ins><span class="cx"> from twisted.internet.defer import inlineCallbacks, \
returnValue, succeed </span><span class="cx"> from twisted.mail import pop3client, \
imap4 </span><span class="cx"> from twisted.mail.smtp import messageid, rfc822date, \
ESMTPSenderFactory </span><span class="cx"> from twisted.plugin import IPlugin
</span><del>-from twisted.python.usage import Options, UsageError
</del><span class="cx"> from twisted.python import failure
</span><del>-from twisted.web import resource, server, client
-from twisted.web2 import responsecode
</del><ins>+from twisted.python.reflect import namedClass
+from twisted.python.usage import Options, UsageError
+from twisted.web import client
+from twisted.web2 import resource, server, responsecode
+from twisted.web2.channel.http import HTTPFactory
+from twisted.web2.dav import auth
</ins><span class="cx"> from twisted.web2.dav import davxml
</span><span class="cx"> from twisted.web2.dav.noneprops import NonePropertyStore
</span><span class="cx"> from twisted.web2.http import Response, HTTPError
</span><span class="cx"> from twisted.web2.http_headers import MimeType
</span><span class="cx"> 
</span><del>-from twistedcaldav.directory.digest import QopDigestCredentialFactory
</del><span class="cx"> from twistedcaldav import ical, caldavxml
</span><ins>+from twistedcaldav import memcachepool
</ins><span class="cx"> from twistedcaldav.config import config, defaultConfig, \
defaultConfigFile </span><ins>+from twistedcaldav.directory.digest import \
QopDigestCredentialFactory +from twistedcaldav.directory.principal import \
DirectoryPrincipalProvisioningResource +from twistedcaldav.directory.util import \
NotFilePath </ins><span class="cx"> from twistedcaldav.ical import Property
</span><ins>+from twistedcaldav.localization import translationTo
</ins><span class="cx"> from twistedcaldav.log import Logger, LoggingMixIn
</span><del>-from twistedcaldav.directory.util import NotFilePath
-from twistedcaldav.scheduling.scheduler import IMIPScheduler
</del><span class="cx"> from twistedcaldav.scheduling.cuaddress import \
normalizeCUAddr </span><del>-from twistedcaldav.static import CalDAVFile, \
deliverSchedulePrivilegeSet </del><ins>+from twistedcaldav.scheduling.scheduler \
import IMIPScheduler </ins><span class="cx"> from twistedcaldav.sql import \
AbstractSQLDatabase </span><del>-from twistedcaldav.localization import translationTo
</del><ins>+from twistedcaldav.static import CalDAVFile, deliverSchedulePrivilegeSet
+from twistedcaldav.util import AuthorizedHTTPGetter
</ins><span class="cx"> 
</span><span class="cx"> from zope.interface import implements
</span><span class="cx"> 
</span><span class="lines">@@ -57,8 +67,6 @@
</span><span class="cx"> import email.utils
</span><span class="cx"> import os
</span><span class="cx"> import uuid
</span><del>-from hashlib import md5, sha1
-import base64
</del><span class="cx"> 
</span><span class="cx"> try:
</span><span class="cx">     from cStringIO import StringIO
</span><span class="lines">@@ -251,22 +259,6 @@
</span><span class="cx">         response.headers.setHeader(&quot;content-type&quot;, \
MimeType(&quot;text&quot;, &quot;html&quot;)) </span><span class="cx">         return \
response </span><span class="cx"> 
</span><del>-    @inlineCallbacks
-    def http_POST(self, request):
-        &quot;&quot;&quot;
-        The IMIP delivery POST method.
-        &quot;&quot;&quot;
-
-        # Check authentication and access controls
-        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
-
-        # Inject using the IMIPScheduler.
-        scheduler = IMIPScheduler(request, self)
-
-        # Do the POST processing treating this as a non-local schedule
-        result = (yield scheduler.doSchedulingViaPOST(use_request_headers=True))
-        returnValue(result.response())
-
</del><span class="cx">     ##
</span><span class="cx">     # File
</span><span class="cx">     ##
</span><span class="lines">@@ -299,230 +291,54 @@
</span><span class="cx">         return succeed(deliverSchedulePrivilegeSet)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+class IMIPReplyInboxResource(IMIPInboxResource):
</ins><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def http_POST(self, request):
+        &quot;&quot;&quot;
+        The IMIP reply POST method (inbound)
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-algorithms = {
-    'md5': md5,
-    'md5-sess': md5,
-    'sha': sha1,
-}
</del><ins>+        # Check authentication and access controls
+        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
</ins><span class="cx"> 
</span><del>-# DigestCalcHA1
-def calcHA1(
-    pszAlg,
-    pszUserName,
-    pszRealm,
-    pszPassword,
-    pszNonce,
-    pszCNonce,
-    preHA1=None
-):
-    &quot;&quot;&quot;
-    @param pszAlg: The name of the algorithm to use to calculate the digest.
-        Currently supported are md5 md5-sess and sha.
</del><ins>+        # Inject using the IMIPScheduler.
+        scheduler = IMIPScheduler(request, self)
</ins><span class="cx"> 
</span><del>-    @param pszUserName: The username
-    @param pszRealm: The realm
-    @param pszPassword: The password
-    @param pszNonce: The nonce
-    @param pszCNonce: The cnonce
</del><ins>+        # Do the POST processing treating this as a non-local schedule
+        result = (yield scheduler.doSchedulingViaPOST(use_request_headers=True))
+        returnValue(result.response())
</ins><span class="cx"> 
</span><del>-    @param preHA1: If available this is a str containing a previously
-       calculated HA1 as a hex string. If this is given then the values for
-       pszUserName, pszRealm, and pszPassword are ignored.
-    &quot;&quot;&quot;
</del><span class="cx"> 
</span><del>-    if (preHA1 and (pszUserName or pszRealm or pszPassword)):
-        raise TypeError((&quot;preHA1 is incompatible with the pszUserName, &quot;
-                         &quot;pszRealm, and pszPassword arguments&quot;))
</del><ins>+class IMIPInvitationInboxResource(IMIPInboxResource):
</ins><span class="cx"> 
</span><del>-    if preHA1 is None:
-        # We need to calculate the HA1 from the username:realm:password
-        m = algorithms[pszAlg]()
-        m.update(pszUserName)
-        m.update(&quot;:&quot;)
-        m.update(pszRealm)
-        m.update(&quot;:&quot;)
-        m.update(pszPassword)
-        HA1 = m.digest()
-    else:
-        # We were given a username:realm:password
-        HA1 = preHA1.decode('hex')
</del><ins>+    def __init__(self, parent, mailer):
+        super(IMIPInvitationInboxResource, self).__init__(parent)
+        self.mailer = mailer
</ins><span class="cx"> 
</span><del>-    if pszAlg == &quot;md5-sess&quot;:
-        m = algorithms[pszAlg]()
-        m.update(HA1)
-        m.update(&quot;:&quot;)
-        m.update(pszNonce)
-        m.update(&quot;:&quot;)
-        m.update(pszCNonce)
-        HA1 = m.digest()
</del><ins>+    @inlineCallbacks
+    def http_POST(self, request):
+        &quot;&quot;&quot;
+        The IMIP invitation POST method (outbound)
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-    return HA1.encode('hex')
</del><ins>+        # Check authentication and access controls
+        yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
</ins><span class="cx"> 
</span><del>-# DigestCalcResponse
-def calcResponse(
-    HA1,
-    algo,
-    pszNonce,
-    pszNonceCount,
-    pszCNonce,
-    pszQop,
-    pszMethod,
-    pszDigestUri,
-    pszHEntity,
-):
-    m = algorithms[algo]()
-    m.update(pszMethod)
-    m.update(&quot;:&quot;)
-    m.update(pszDigestUri)
-    if pszQop == &quot;auth-int&quot;:
-        m.update(&quot;:&quot;)
-        m.update(pszHEntity)
-    HA2 = m.digest().encode('hex')
</del><ins>+        # Compute token, add to db, generate email and send it
+        calendar = (yield ical.Component.fromIStream(request.stream))
+        originator = request.headers.getRawHeaders(&quot;originator&quot;)[0]
+        recipient = request.headers.getRawHeaders(&quot;recipient&quot;)[0]
+        language = config.Localization.Language
</ins><span class="cx"> 
</span><del>-    m = algorithms[algo]()
-    m.update(HA1)
-    m.update(&quot;:&quot;)
-    m.update(pszNonce)
-    m.update(&quot;:&quot;)
-    if pszNonceCount and pszCNonce and pszQop:
-        m.update(pszNonceCount)
-        m.update(&quot;:&quot;)
-        m.update(pszCNonce)
-        m.update(&quot;:&quot;)
-        m.update(pszQop)
-        m.update(&quot;:&quot;)
-    m.update(HA2)
-    respHash = m.digest().encode('hex')
-    return respHash
</del><ins>+        if not (yield self.mailer.outbound(originator,
+            recipient, calendar, language=language)):
+            returnValue(Response(code=responsecode.BAD_REQUEST))
</ins><span class="cx"> 
</span><del>-class Unauthorized(Exception):
-    pass
</del><ins>+        returnValue(Response(code=responsecode.OK))
</ins><span class="cx"> 
</span><del>-class AuthorizedHTTPGetter(client.HTTPPageGetter, LoggingMixIn):
</del><span class="cx"> 
</span><del>-    def handleStatus_401(self):
</del><span class="cx"> 
</span><del>-        self.quietLoss = 1
-        self.transport.loseConnection()
-
-        if not hasattr(self.factory, &quot;username&quot;):
-            self.factory.deferred.errback(failure.Failure(Unauthorized(&quot;Mail \
gateway not able to process reply; authentication required for calendar \
                server&quot;)))
-            return self.factory.deferred
-
-        if hasattr(self.factory, &quot;retried&quot;):
-            self.factory.deferred.errback(failure.Failure(Unauthorized(&quot;Mail \
gateway not able to process reply; could not authenticate user %s with calendar \
                server&quot; % (self.factory.username,))))
-            return self.factory.deferred
-
-        self.factory.retried = True
-
-        # self.log_debug(&quot;Got a 401 trying to inject [%s]&quot; % \
                (self.headers,))
-        details = {}
-        basicAvailable = digestAvailable = False
-        wwwauth = self.headers.get(&quot;www-authenticate&quot;)
-        for item in wwwauth:
-            if item.startswith(&quot;basic &quot;):
-                basicAvailable = True
-            if item.startswith(&quot;digest &quot;):
-                digestAvailable = True
-                wwwauth = item[7:]
-                def unq(s):
-                    if s[0] == s[-1] == '&quot;':
-                        return s[1:-1]
-                    return s
-                parts = wwwauth.split(',')
-                for (k, v) in [p.split('=', 1) for p in parts]:
-                    details[k.strip()] = unq(v.strip())
-
-        user = self.factory.username
-        pswd = self.factory.password
-
-        if digestAvailable and details:
-            digest = calcResponse(
-                calcHA1(
-                    details.get('algorithm'),
-                    user,
-                    details.get('realm'),
-                    pswd,
-                    details.get('nonce'),
-                    details.get('cnonce')
-                ),
-                details.get('algorithm'),
-                details.get('nonce'),
-                details.get('nc'),
-                details.get('cnonce'),
-                details.get('qop'),
-                self.factory.method,
-                self.factory.url,
-                None
-            )
-
-            if details.get('qop'):
-                response = (
-                    'Digest username=&quot;%s&quot;, realm=&quot;%s&quot;, \
                nonce=&quot;%s&quot;, uri=&quot;%s&quot;, '
-                    'response=%s, algorithm=%s, cnonce=&quot;%s&quot;, qop=%s, \
                nc=%s' %
-                    (
-                        user,
-                        details.get('realm'),
-                        details.get('nonce'),
-                        self.factory.url,
-                        digest,
-                        details.get('algorithm'),
-                        details.get('cnonce'),
-                        details.get('qop'),
-                        details.get('nc'),
-                    )
-                )
-            else:
-                response = (
-                    'Digest username=&quot;%s&quot;, realm=&quot;%s&quot;, \
                nonce=&quot;%s&quot;, uri=&quot;%s&quot;, '
-                    'response=%s, algorithm=%s' %
-                    (
-                        user,
-                        details.get('realm'),
-                        details.get('nonce'),
-                        self.factory.url,
-                        digest,
-                        details.get('algorithm'),
-                    )
-                )
-
-            self.factory.headers['Authorization'] = response
-
-            if self.factory.scheme == 'https':
-                reactor.connectSSL(self.factory.host, self.factory.port,
-                    self.factory, ssl.ClientContextFactory())
-            else:
-                reactor.connectTCP(self.factory.host, self.factory.port,
-                    self.factory)
-            # self.log_debug(&quot;Retrying with digest after 401&quot;)
-
-            return self.factory.deferred
-
-        elif basicAvailable:
-            basicauth = &quot;%s:%s&quot; % (user, pswd)
-            basicauth = &quot;Basic &quot; + base64.encodestring( basicauth )
-            basicauth = basicauth.replace( &quot;\n&quot;, &quot;&quot; )
-
-            self.factory.headers['Authorization'] = basicauth
-
-            if self.factory.scheme == 'https':
-                reactor.connectSSL(self.factory.host, self.factory.port,
-                    self.factory, ssl.ClientContextFactory())
-            else:
-                reactor.connectTCP(self.factory.host, self.factory.port,
-                    self.factory)
-            # self.log_debug(&quot;Retrying with basic after 401&quot;)
-
-            return self.factory.deferred
-
-
-        else:
-            self.factory.deferred.errback(failure.Failure(Unauthorized(&quot;Mail \
gateway not able to process reply; calendar server returned 401 and doesn't support \
                basic or digest&quot;)))
-            return self.factory.deferred
-
-
</del><span class="cx"> def injectMessage(organizer, attendee, calendar, msgId, \
reactor=None): </span><span class="cx"> 
</span><span class="cx">     if reactor is None:
</span><span class="lines">@@ -726,6 +542,16 @@
</span><span class="cx"> 
</span><span class="cx">     def makeService(self, options):
</span><span class="cx"> 
</span><ins>+        if config.Memcached.ClientEnabled:
+            memcachepool.installPool(
+                IPv4Address(
+                    &quot;TCP&quot;,
+                    config.Memcached.BindAddress,
+                    config.Memcached.Port,
+                ),
+                config.Memcached.MaxClients,
+            )
+
</ins><span class="cx">         multiService = service.MultiService()
</span><span class="cx"> 
</span><span class="cx">         settings = config.Scheduling['iMIP']
</span><span class="lines">@@ -747,7 +573,10 @@
</span><span class="cx"> 
</span><span class="cx">             client.setServiceParent(multiService)
</span><span class="cx"> 
</span><ins>+
+            # Set up /inbox -- server POSTs to it to send out iMIP invites
</ins><span class="cx">             IScheduleService(settings, \
mailer).setServiceParent(multiService) </span><ins>+
</ins><span class="cx">         else:
</span><span class="cx">             self.log_info(&quot;Mail Gateway Service not \
enabled&quot;) </span><span class="cx"> 
</span><span class="lines">@@ -762,12 +591,43 @@
</span><span class="cx">     def __init__(self, settings, mailer):
</span><span class="cx">         self.settings = settings
</span><span class="cx">         self.mailer = mailer
</span><del>-        root = resource.Resource()
-        root.putChild('', self.HomePage())
-        root.putChild('inbox', self.IScheduleInbox(mailer))
-        self.site = server.Site(root)
-        self.server = internet.TCPServer(settings['MailGatewayPort'], self.site)
</del><span class="cx"> 
</span><ins>+        directoryClass = namedClass(config.DirectoryService.type)
+        directory = directoryClass(config.DirectoryService.params)
+
+        principalCollection = DirectoryPrincipalProvisioningResource(
+            &quot;/principals/&quot;,
+            directory,
+        )
+
+        root = RootResource(
+            config.DocumentRoot,
+            principalCollections=(principalCollection,),
+        )
+
+        # Authenticated /inbox
+        credentialFactories = []
+        portal = Portal(auth.DavRealm())
+        portal.registerChecker(directory)
+        realm = directory.realmName or &quot;&quot;
+        schemeConfig = config.Authentication.Digest
+        digestCredentialFactory = QopDigestCredentialFactory(
+            schemeConfig[&quot;Algorithm&quot;],
+            schemeConfig[&quot;Qop&quot;],
+            realm,
+        )
+        root.putChild('inbox',
+            auth.AuthenticationWrapper(
+                IMIPInvitationInboxResource(root, mailer),
+                portal,
+                (digestCredentialFactory,),
+                (auth.IPrincipal,),
+            )
+        )
+        self.factory = HTTPFactory(server.Site(root))
+        self.server = internet.TCPServer(settings['MailGatewayPort'],
+            self.factory)
+
</ins><span class="cx">     def startService(self):
</span><span class="cx">         self.server.startService()
</span><span class="cx"> 
</span><span class="lines">@@ -775,45 +635,8 @@
</span><span class="cx">         self.server.stopService()
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    class HomePage(resource.Resource):
-        def render(self, request):
-            return &quot;&quot;&quot;
-            &lt;html&gt;
-            &lt;head&gt;&lt;title&gt;ISchedule - IMIP \
                Gateway&lt;/title&gt;&lt;/head&gt;
-            &lt;body&gt;ISchedule - IMIP Gateway&lt;/body&gt;
-            &lt;/html&gt;
-            &quot;&quot;&quot;
</del><span class="cx"> 
</span><del>-    class IScheduleInbox(resource.Resource):
</del><span class="cx"> 
</span><del>-        def __init__(self, mailer):
-            resource.Resource.__init__(self)
-            self.mailer = mailer
-
-        def render_GET(self, request):
-            return &quot;&quot;&quot;
-            &lt;html&gt;
-            &lt;head&gt;&lt;title&gt;ISchedule Inbox&lt;/title&gt;&lt;/head&gt;
-            &lt;body&gt;ISchedule Inbox&lt;/body&gt;
-            &lt;/html&gt;
-            &quot;&quot;&quot;
-
-        def render_POST(self, request):
-            # Compute token, add to db, generate email and send it
-            calendar = ical.Component.fromString(request.content.read())
-            headers = request.getAllHeaders()
-            language = config.Localization.Language
-            self.mailer.outbound(headers['originator'], headers['recipient'],
-                calendar, language=language)
-
-            # TODO: what to return?
-            return &quot;&quot;&quot;
-            &lt;html&gt;
-            &lt;head&gt;&lt;title&gt;ISchedule Inbox&lt;/title&gt;&lt;/head&gt;
-            &lt;body&gt;ISchedule Inbox&lt;/body&gt;
-            &lt;/html&gt;
-            &quot;&quot;&quot;
-
</del><span class="cx"> class MailHandler(LoggingMixIn):
</span><span class="cx"> 
</span><span class="cx">     def __init__(self, dataRoot=None):
</span><span class="lines">@@ -1059,10 +882,12 @@
</span><span class="cx">         def _success(result, msgId, fromAddr, toAddr):
</span><span class="cx">             self.log_info(&quot;Mail gateway sent message %s \
from %s to %s&quot; % </span><span class="cx">                 (msgId, fromAddr, \
toAddr)) </span><ins>+            return True
</ins><span class="cx"> 
</span><span class="cx">         def _failure(failure, msgId, fromAddr, toAddr):
</span><span class="cx">             self.log_error(&quot;Mail gateway failed to send \
message %s from %s to %s (Reason: %s)&quot; % </span><span class="cx">                \
(msgId, fromAddr, toAddr, failure.getErrorMessage())) </span><ins>+            return \
False </ins><span class="cx"> 
</span><span class="cx">         deferred = defer.Deferred()
</span><span class="cx"> 
</span><span class="lines">@@ -1080,6 +905,7 @@
</span><span class="cx">         reactor.connectTCP(settings['Server'], \
settings['Port'], factory) </span><span class="cx">         \
deferred.addCallback(_success, msgId, fromAddr, toAddr) </span><span class="cx">      \
deferred.addErrback(_failure, msgId, fromAddr, toAddr) </span><ins>+        return \
deferred </ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def getIconPath(self, details, canceled, language='en'):
</span><span class="lines">@@ -1507,7 +1333,7 @@
</span><span class="cx">         self.log_error(&quot;IMAP Error: %s&quot; % \
(error,)) </span><span class="cx"> 
</span><span class="cx">     def ebAuthenticateFailed(self, reason):
</span><del>-        self.log_info(&quot;IMAP authenticate failed for %s, trying \
login&quot; % </del><ins>+        self.log_debug(&quot;IMAP authenticate failed for \
%s, trying login&quot; % </ins><span class="cx">             \
(self.factory.settings[&quot;Username&quot;],)) </span><span class="cx">         \
return self.login(self.factory.settings[&quot;Username&quot;], </span><span \
class="cx">             self.factory.settings[&quot;Password&quot;] \
</span></span></pre></div> <a \
id="CalendarServertrunktwistedcaldavschedulingimippy"></a> <div \
class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/scheduling/imip.py \
(4296 => 4297)</h4> <pre class="diff"><span>
<span class="info">--- \
CalendarServer/trunk/twistedcaldav/scheduling/imip.py	2009-05-19 21:02:35 UTC (rev \
                4296)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip.py	2009-05-19 21:28:46 UTC \
(rev 4297) </span><span class="lines">@@ -27,6 +27,7 @@
</span><span class="cx"> from twistedcaldav.caldavxml import caldav_namespace
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.log import Logger
</span><ins>+from twistedcaldav.util import AuthorizedHTTPGetter
</ins><span class="cx"> from twistedcaldav.scheduling.delivery import DeliveryService
</span><span class="cx"> from twistedcaldav.scheduling.itip import iTIPRequestStatus
</span><span class="cx"> 
</span><span class="lines">@@ -98,6 +99,13 @@
</span><span class="cx">         }
</span><span class="cx">         factory = client.HTTPClientFactory(url, \
method='POST', headers=headers, </span><span class="cx">             \
postdata=caldata, agent=&quot;CalDAV server&quot;) </span><ins>+
+        if config.Scheduling.iMIP.Username:
+            factory.username = config.Scheduling.iMIP.Username
+            factory.password = config.Scheduling.iMIP.Password
+
+        factory.noisy = False
+        factory.protocol = AuthorizedHTTPGetter
</ins><span class="cx">         reactor.connectTCP(mailGatewayServer, \
mailGatewayPort, factory) </span><span class="cx">         return factory.deferred
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/util.py (4296 \
=> 4297)</h4> <pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/util.py	2009-05-19 21:02:35 \
                UTC (rev 4296)
+++ CalendarServer/trunk/twistedcaldav/util.py	2009-05-19 21:28:46 UTC (rev 4297)
</span><span class="lines">@@ -19,6 +19,13 @@
</span><span class="cx"> import sys
</span><span class="cx"> from subprocess import Popen, PIPE, STDOUT
</span><span class="cx"> 
</span><ins>+from twisted.internet import ssl, reactor
+from twisted.web import client
+from twistedcaldav.log import Logger, LoggingMixIn
+from twisted.python import failure
+from hashlib import md5, sha1
+import base64
+
</ins><span class="cx"> ##
</span><span class="cx"> # getNCPU
</span><span class="cx"> ##
</span><span class="lines">@@ -161,3 +168,232 @@
</span><span class="cx">     else:
</span><span class="cx">         error = &quot;Keychain access utility ('security') \
not found&quot; </span><span class="cx">         raise KeychainAccessError(error)
</span><ins>+
+
+
+
+##
+# Digest/Basic-capable HTTP GET factory
+##
+
+algorithms = {
+    'md5': md5,
+    'md5-sess': md5,
+    'sha': sha1,
+}
+
+# DigestCalcHA1
+def calcHA1(
+    pszAlg,
+    pszUserName,
+    pszRealm,
+    pszPassword,
+    pszNonce,
+    pszCNonce,
+    preHA1=None
+):
+    &quot;&quot;&quot;
+    @param pszAlg: The name of the algorithm to use to calculate the digest.
+        Currently supported are md5 md5-sess and sha.
+
+    @param pszUserName: The username
+    @param pszRealm: The realm
+    @param pszPassword: The password
+    @param pszNonce: The nonce
+    @param pszCNonce: The cnonce
+
+    @param preHA1: If available this is a str containing a previously
+       calculated HA1 as a hex string. If this is given then the values for
+       pszUserName, pszRealm, and pszPassword are ignored.
+    &quot;&quot;&quot;
+
+    if (preHA1 and (pszUserName or pszRealm or pszPassword)):
+        raise TypeError((&quot;preHA1 is incompatible with the pszUserName, &quot;
+                         &quot;pszRealm, and pszPassword arguments&quot;))
+
+    if preHA1 is None:
+        # We need to calculate the HA1 from the username:realm:password
+        m = algorithms[pszAlg]()
+        m.update(pszUserName)
+        m.update(&quot;:&quot;)
+        m.update(pszRealm)
+        m.update(&quot;:&quot;)
+        m.update(pszPassword)
+        HA1 = m.digest()
+    else:
+        # We were given a username:realm:password
+        HA1 = preHA1.decode('hex')
+
+    if pszAlg == &quot;md5-sess&quot;:
+        m = algorithms[pszAlg]()
+        m.update(HA1)
+        m.update(&quot;:&quot;)
+        m.update(pszNonce)
+        m.update(&quot;:&quot;)
+        m.update(pszCNonce)
+        HA1 = m.digest()
+
+    return HA1.encode('hex')
+
+# DigestCalcResponse
+def calcResponse(
+    HA1,
+    algo,
+    pszNonce,
+    pszNonceCount,
+    pszCNonce,
+    pszQop,
+    pszMethod,
+    pszDigestUri,
+    pszHEntity,
+):
+    m = algorithms[algo]()
+    m.update(pszMethod)
+    m.update(&quot;:&quot;)
+    m.update(pszDigestUri)
+    if pszQop == &quot;auth-int&quot;:
+        m.update(&quot;:&quot;)
+        m.update(pszHEntity)
+    HA2 = m.digest().encode('hex')
+
+    m = algorithms[algo]()
+    m.update(HA1)
+    m.update(&quot;:&quot;)
+    m.update(pszNonce)
+    m.update(&quot;:&quot;)
+    if pszNonceCount and pszCNonce and pszQop:
+        m.update(pszNonceCount)
+        m.update(&quot;:&quot;)
+        m.update(pszCNonce)
+        m.update(&quot;:&quot;)
+        m.update(pszQop)
+        m.update(&quot;:&quot;)
+    m.update(HA2)
+    respHash = m.digest().encode('hex')
+    return respHash
+
+class Unauthorized(Exception):
+    pass
+
+class AuthorizedHTTPGetter(client.HTTPPageGetter, LoggingMixIn):
+
+    def handleStatus_401(self):
+
+        self.quietLoss = 1
+        self.transport.loseConnection()
+
+        if not hasattr(self.factory, &quot;username&quot;):
+            self.factory.deferred.errback(failure.Failure(Unauthorized(&quot;Mail \
gateway not able to process reply; authentication required for calendar \
server&quot;))) +            return self.factory.deferred
+
+        if hasattr(self.factory, &quot;retried&quot;):
+            self.factory.deferred.errback(failure.Failure(Unauthorized(&quot;Mail \
gateway not able to process reply; could not authenticate user %s with calendar \
server&quot; % (self.factory.username,)))) +            return self.factory.deferred
+
+        self.factory.retried = True
+
+        # self.log_debug(&quot;Got a 401 trying to inject [%s]&quot; % \
(self.headers,)) +        details = {}
+        basicAvailable = digestAvailable = False
+        wwwauth = self.headers.get(&quot;www-authenticate&quot;)
+        for item in wwwauth:
+            if item.startswith(&quot;basic &quot;):
+                basicAvailable = True
+            if item.startswith(&quot;digest &quot;):
+                digestAvailable = True
+                wwwauth = item[7:]
+                def unq(s):
+                    if s[0] == s[-1] == '&quot;':
+                        return s[1:-1]
+                    return s
+                parts = wwwauth.split(',')
+                for (k, v) in [p.split('=', 1) for p in parts]:
+                    details[k.strip()] = unq(v.strip())
+
+        user = self.factory.username
+        pswd = self.factory.password
+
+        if digestAvailable and details:
+            digest = calcResponse(
+                calcHA1(
+                    details.get('algorithm'),
+                    user,
+                    details.get('realm'),
+                    pswd,
+                    details.get('nonce'),
+                    details.get('cnonce')
+                ),
+                details.get('algorithm'),
+                details.get('nonce'),
+                details.get('nc'),
+                details.get('cnonce'),
+                details.get('qop'),
+                self.factory.method,
+                self.factory.url,
+                None
+            )
+
+            if details.get('qop'):
+                response = (
+                    'Digest username=&quot;%s&quot;, realm=&quot;%s&quot;, \
nonce=&quot;%s&quot;, uri=&quot;%s&quot;, ' +                    'response=%s, \
algorithm=%s, cnonce=&quot;%s&quot;, qop=%s, nc=%s' % +                    (
+                        user,
+                        details.get('realm'),
+                        details.get('nonce'),
+                        self.factory.url,
+                        digest,
+                        details.get('algorithm'),
+                        details.get('cnonce'),
+                        details.get('qop'),
+                        details.get('nc'),
+                    )
+                )
+            else:
+                response = (
+                    'Digest username=&quot;%s&quot;, realm=&quot;%s&quot;, \
nonce=&quot;%s&quot;, uri=&quot;%s&quot;, ' +                    'response=%s, \
algorithm=%s' % +                    (
+                        user,
+                        details.get('realm'),
+                        details.get('nonce'),
+                        self.factory.url,
+                        digest,
+                        details.get('algorithm'),
+                    )
+                )
+
+            self.factory.headers['Authorization'] = response
+
+            if self.factory.scheme == 'https':
+                reactor.connectSSL(self.factory.host, self.factory.port,
+                    self.factory, ssl.ClientContextFactory())
+            else:
+                reactor.connectTCP(self.factory.host, self.factory.port,
+                    self.factory)
+            # self.log_debug(&quot;Retrying with digest after 401&quot;)
+
+            return self.factory.deferred
+
+        elif basicAvailable:
+            basicauth = &quot;%s:%s&quot; % (user, pswd)
+            basicauth = &quot;Basic &quot; + base64.encodestring( basicauth )
+            basicauth = basicauth.replace( &quot;\n&quot;, &quot;&quot; )
+
+            self.factory.headers['Authorization'] = basicauth
+
+            if self.factory.scheme == 'https':
+                reactor.connectSSL(self.factory.host, self.factory.port,
+                    self.factory, ssl.ClientContextFactory())
+            else:
+                reactor.connectTCP(self.factory.host, self.factory.port,
+                    self.factory)
+            # self.log_debug(&quot;Retrying with basic after 401&quot;)
+
+            return self.factory.deferred
+
+
+        else:
+            self.factory.deferred.errback(failure.Failure(Unauthorized(&quot;Mail \
gateway not able to process reply; calendar server returned 401 and doesn't support \
basic or digest&quot;))) +            return self.factory.deferred
+
</ins></span></pre>
</div>
</div>

</body>
</html>



_______________________________________________
calendarserver-changes mailing list
calendarserver-changes@lists.macosforge.org
http://lists.macosforge.org/mailman/listinfo.cgi/calendarserver-changes


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic