[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"> """
</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("content-type", \
MimeType("text", "html")) </span><span class="cx"> return \
response </span><span class="cx">
</span><del>- @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())
-
</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):
+ """
+ The IMIP reply POST method (inbound)
+ """
</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
-):
- """
- @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.
- """
</del><span class="cx">
</span><del>- if (preHA1 and (pszUserName or pszRealm or pszPassword)):
- raise TypeError(("preHA1 is incompatible with the pszUserName, "
- "pszRealm, and pszPassword arguments"))
</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(":")
- m.update(pszRealm)
- m.update(":")
- 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 == "md5-sess":
- m = algorithms[pszAlg]()
- m.update(HA1)
- m.update(":")
- m.update(pszNonce)
- m.update(":")
- m.update(pszCNonce)
- HA1 = m.digest()
</del><ins>+ @inlineCallbacks
+ def http_POST(self, request):
+ """
+ The IMIP invitation POST method (outbound)
+ """
</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(":")
- m.update(pszDigestUri)
- if pszQop == "auth-int":
- m.update(":")
- 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("originator")[0]
+ recipient = request.headers.getRawHeaders("recipient")[0]
+ language = config.Localization.Language
</ins><span class="cx">
</span><del>- 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
</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, "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
-
-
</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(
+ "TCP",
+ 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("Mail Gateway Service not \
enabled") </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(
+ "/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)
+
</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 """
- <html>
- <head><title>ISchedule - IMIP \
Gateway</title></head>
- <body>ISchedule - IMIP Gateway</body>
- </html>
- """
</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 """
- <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>
- """
-
</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("Mail gateway sent message %s \
from %s to %s" % </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("Mail gateway failed to send \
message %s from %s to %s (Reason: %s)" % </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("IMAP Error: %s" % \
(error,)) </span><span class="cx">
</span><span class="cx"> def ebAuthenticateFailed(self, reason):
</span><del>- self.log_info("IMAP authenticate failed for %s, trying \
login" % </del><ins>+ self.log_debug("IMAP authenticate failed for \
%s, trying login" % </ins><span class="cx"> \
(self.factory.settings["Username"],)) </span><span class="cx"> \
return self.login(self.factory.settings["Username"], </span><span \
class="cx"> self.factory.settings["Password"] \
</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="CalDAV server") </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 = "Keychain access utility ('security') \
not found" </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
+):
+ """
+ @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
+
</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