[CalendarServer-changes] [1851] CalendarServer/branches/users/cdaboo/server2server-1842/ twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Sun Sep 9 20:23:07 PDT 2007


Revision: 1851
          http://trac.macosforge.org/projects/calendarserver/changeset/1851
Author:   cdaboo at apple.com
Date:     2007-09-09 20:23:06 -0700 (Sun, 09 Sep 2007)

Log Message:
-----------
Now does requests to remote server using HTTPClient capability from Twisted. It will aggregate the results from
multple remote servers into the response to the request initiated on the local server. Calendar user address to
server mapping is controlled via an xml config file and can be edited whilst the server is running.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule_common.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/servertoserver.py
    CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/servertoserverparser.py

Modified: CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule_common.py	2007-09-09 21:07:43 UTC (rev 1850)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/schedule_common.py	2007-09-10 03:23:06 UTC (rev 1851)
@@ -25,6 +25,7 @@
 ]
 
 from twisted.internet import reactor
+from twisted.internet.defer import DeferredList
 from twisted.internet.defer import deferredGenerator, maybeDeferred, waitForDeferred
 from twisted.python import log
 from twisted.python.failure import Failure
@@ -45,6 +46,8 @@
 from twistedcaldav.method import report_common
 from twistedcaldav.method.put_common import storeCalendarObjectResource
 from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.servertoserver import ServerToServer
+from twistedcaldav.servertoserver import ServerToServerRequest
 
 import md5
 import time
@@ -276,6 +279,7 @@
         uid = self.calendar.resourceUID()
     
         # Loop over each recipient and do appropriate action.
+        remote_recipients = []
         autoresponses = []
         for recipient in self.recipients:
     
@@ -286,9 +290,8 @@
                 # Process next recipient
                 continue
             elif isinstance(recipient, Scheduler.RemoteCalendarUser):
-                # TODO: support remote recipients when server-to-server is enabled
-                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
-                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
+                # Pool remote recipients into a seperate list for processing after the local ones.
+                remote_recipients.append(recipient)
             
                 # Process next recipient
                 continue
@@ -323,6 +326,12 @@
                 yield d
                 d.getResult()
     
+        # Now process remote recipients
+        if remote_recipients:
+            d = waitForDeferred(self.generateRemoteSchedulingResponses(remote_recipients, responses))
+            yield d
+            d.getResult()
+
         # Now we have to do auto-respond
         if len(autoresponses) != 0:
             # First check that we have a method that we can auto-respond to
@@ -338,6 +347,45 @@
         yield responses.response()
     
     @deferredGenerator
+    def generateRemoteSchedulingResponses(self, recipients, responses):
+        """
+        Generate scheduling responses for remote recipients.
+        """
+        
+        # Group recipients by server so that we can do a single request with multiple recipients
+        # to each different server.
+        groups = {}
+        servermgr = ServerToServer()
+        for recipient in recipients:
+            # Map the recipient's domain to a server
+            server = servermgr.mapDomain(recipient.domain)
+            if not server:
+                # Cannot do server-to-server for this recipient.
+                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
+                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
+            
+                # Process next recipient
+                continue
+            
+            groups.setdefault(server, []).append(recipient)
+        
+        if len(groups) == 0:
+            yield None
+            return
+
+        # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
+        # we will generate for each request. That way we can have parallel requests in progress
+        # rather than serialize them.
+        deferreds = []
+        for server, recipients in groups.iteritems():
+            requestor = ServerToServerRequest(self, server, recipients, responses)
+            deferreds.append(requestor.doRequest())
+
+        d = waitForDeferred(DeferredList(deferreds))
+        yield d
+        d.getResult()
+
+    @deferredGenerator
     def generateLocalResponse(self, recipient, responses, autoresponses):
         # Hash the iCalendar data for use as the last path element of the URI path
         calendar_str = str(self.calendar)
@@ -817,6 +865,31 @@
             children.append(davxml.ResponseDescription(message))
         self.responses.append(caldavxml.Response(*children))
 
+    def clone(self, clone):
+        """
+        Add a response cloned from an existing caldavxml.Response element.
+        @param clone: the response to clone.
+        """
+        if not isinstance(clone, caldavxml.Response):
+            raise AssertionError("Incorrect element type: %r" % (clone,))
+
+        recipient = clone.childOfType(caldavxml.Recipient)
+        request_status = clone.childOfType(caldavxml.RequestStatus)
+        calendar_data = clone.childOfType(caldavxml.CalendarData)
+        error = clone.childOfType(davxml.Error)
+        desc = clone.childOfType(davxml.ResponseDescription)
+
+        children = []
+        children.append(recipient)
+        children.append(request_status)
+        if calendar_data is not None:
+            children.append(calendar_data)
+        if error is not None:
+            children.append(error)
+        if desc is not None:
+            children.append(desc)
+        self.responses.append(caldavxml.Response(*children))
+
     def response(self):
         """
         Generate a L{ScheduleResponseResponse} with the responses contained in the

Added: CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/servertoserver.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/servertoserver.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/servertoserver.py	2007-09-10 03:23:06 UTC (rev 1851)
@@ -0,0 +1,149 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# DRI: Cyrus Daboo, cdaboo at apple.com
+##
+
+from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import waitForDeferred
+from twisted.internet.protocol import ClientCreator
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.python.filepath import FilePath
+from twisted.web2 import responsecode
+from twisted.web2.client.http import ClientRequest
+from twisted.web2.client.http import HTTPClientProtocol
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.dav.util import davXMLFromStream
+from twisted.web2.http import HTTPError
+from twisted.web2.http_headers import MimeType
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.servertoserverparser import ServerToServerParser
+from twisted.web2.http_headers import Headers
+from twistedcaldav import caldavxml
+
+"""
+Server to server utility functions and client requests.
+"""
+
+class ServerToServer(object):
+    
+    _fileInfo = None
+    _xmlFile = None
+    _servers = None
+    _domainMap = None
+    
+    def __init__(self):
+        
+        self._loadConfig()
+
+    def _loadConfig(self):
+        if ServerToServer._servers is None:
+            ServerToServer._xmlFile = FilePath(config.ServerToServer["Servers"])
+        ServerToServer._xmlFile.restat()
+        fileInfo = (ServerToServer._xmlFile.getmtime(), ServerToServer._xmlFile.getsize())
+        if fileInfo != ServerToServer._fileInfo:
+            parser = ServerToServerParser(ServerToServer._xmlFile)
+            ServerToServer._servers = parser.servers
+            self._mapDomains()
+            ServerToServer._fileInfo = fileInfo
+        
+    def _mapDomains(self):
+        ServerToServer._domainMap = {}
+        for server in ServerToServer._servers:
+            for domain in server.domains:
+                ServerToServer._domainMap[domain] = server
+    
+    def mapDomain(self, domain):
+        """
+        Map a calendar user address domain to a suitable server that can
+        handle server-to-server requests for that user.
+        """
+        return ServerToServer._domainMap.get(domain)
+
+class ServerToServerRequest(object):
+    
+    def __init__(self, scheduler, server, recipients, responses):
+
+        self.scheduler = scheduler
+        self.server = server
+        self.recipients = recipients
+        self.responses = responses
+        
+        self._generateHeaders()
+        self._prepareData()
+        
+    @deferredGenerator
+    def doRequest(self):
+        
+        # Generate an HTTP client request
+        try:
+            from twisted.internet import reactor
+            if self.server.ssl:
+                from tap import ChainingOpenSSLContextFactory
+                context = ChainingOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, certificateChainFile=config.SSLAuthorityChain)
+                d = waitForDeferred(ClientCreator(reactor, HTTPClientProtocol).connectSSL(self.server.host, self.server.port, context))
+            else:
+                d = waitForDeferred(ClientCreator(reactor, HTTPClientProtocol).connectTCP(self.server.host, self.server.port))
+            yield d
+            proto = d.getResult()
+            
+            d = waitForDeferred(proto.submitRequest(ClientRequest("POST", self.server.path, self.headers, self.data)))
+            yield d
+            response = d.getResult()
+    
+            d = waitForDeferred(davXMLFromStream(response.stream))
+            yield d
+            xml = d.getResult()
+    
+            self._parseResponse(xml)
+        except Exception, e:
+            # Generated failed responses for each recipient
+            log.err("Could not do server-to-server request : %s %s" % (self, e))
+            for recipient in self.recipients:
+                err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
+                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
+
+    def _generateHeaders(self):
+        self.headers = Headers()
+        self.headers.setHeader('Host', self.server.host + ":%s" % (self.server.port,))
+        self.headers.addRawHeader('Originator', self.scheduler.originator.cuaddr)
+        for recipient in self.recipients:
+            self.headers.addRawHeader('Recipient', recipient.cuaddr)
+        self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
+
+    def _prepareData(self):
+        self.data = str(self.scheduler.calendar)
+
+    def _parseResponse(self, xml):
+
+        # Check for correct root element
+        schedule_response = xml.root_element
+        if not isinstance(schedule_response, caldavxml.ScheduleResponse) or not schedule_response.children:
+            raise HTTPError(responsecode.BAD_REQUEST)
+        
+        # Parse each response - do this twice: once looking for errors that will
+        # result in all recipients shown as failures; the second loop adds all the
+        # valid responses to the actual result.
+        for response in schedule_response.children:
+            if not isinstance(response, caldavxml.Response) or not response.children:
+                raise HTTPError(responsecode.BAD_REQUEST)
+            recipient = response.childOfType(caldavxml.Recipient)
+            request_status = response.childOfType(caldavxml.RequestStatus)
+            if not recipient or not request_status:
+                raise HTTPError(responsecode.BAD_REQUEST)
+        for response in schedule_response.children:
+            self.responses.clone(response)

Added: CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/servertoserverparser.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/servertoserverparser.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-1842/twistedcaldav/servertoserverparser.py	2007-09-10 03:23:06 UTC (rev 1851)
@@ -0,0 +1,132 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# DRI: Cyrus Daboo, cdaboo at apple.com
+##
+
+
+"""
+XML based server-to-server configuration file handling.
+"""
+
+__all__ = [
+    "ServerToServerParser",
+    "ServerToServerRecord",
+]
+
+import xml.dom.minidom
+
+ELEMENT_SERVERS                 = "servers"
+ELEMENT_SERVER                  = "server"
+ELEMENT_URI                     = "uri"
+ELEMENT_ALLOW_REQUESTS_FROM     = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO       = "allow-requests-to"
+ELEMENT_DOMAINS                 = "domains"
+ELEMENT_DOMAIN                  = "domain"
+
+class ServerToServerParser(object):
+    """
+    Server-to-server configuration file parser.
+    """
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+    def __init__(self, xmlFile):
+
+        self.servers = []
+        
+        # Read in XML
+        fd = open(xmlFile.path, "r")
+        doc = xml.dom.minidom.parse(fd)
+        fd.close()
+
+        # Verify that top-level element is correct
+        servers_node = doc._get_documentElement()
+        if servers_node._get_localName() != ELEMENT_SERVERS:
+            self.log("Ignoring file %r because it is not a server-to-server config file" % (self.xmlFile,))
+            return
+        self._parseXML(servers_node)
+        
+    def _parseXML(self, node):
+        """
+        Parse the XML root node from the server-to-server configuration document.
+        @param node: the L{Node} to parse.
+        """
+
+        for child in node._get_childNodes():
+            child_name = child._get_localName()
+            if child_name is None:
+                continue
+            elif child_name == ELEMENT_SERVER:
+                self.servers.append(ServerToServerRecord())
+                self.servers[-1].parseXML(child)
+                
+class ServerToServerRecord (object):
+    """
+    Contains server-to-server details.
+    """
+    def __init__(self):
+        """
+        @param recordType: record type for directory entry.
+        """
+        self.uri = ""
+        self.allow_from = False
+        self.allow_to = True
+        self.domains = []
+
+    def parseXML(self, node):
+        for child in node._get_childNodes():
+            child_name = child._get_localName()
+            if child_name is None:
+                continue
+            elif child_name == ELEMENT_URI:
+                if child.firstChild is not None:
+                    self.uri = child.firstChild.data.encode("utf-8")
+            elif child_name == ELEMENT_ALLOW_REQUESTS_FROM:
+                self.allow_from = True
+            elif child_name == ELEMENT_ALLOW_REQUESTS_TO:
+                self.allow_to = True
+            elif child_name == ELEMENT_DOMAINS:
+                self._parseDomains(child)
+            else:
+                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
+        
+        self._parseDetails()
+
+    def _parseDomains(self, node):
+        for child in node._get_childNodes():
+            if child._get_localName() == ELEMENT_DOMAIN:
+                if child.firstChild is not None:
+                    self.domains.append(child.firstChild.data.encode("utf-8"))
+
+    def _parseDetails(self):
+        # Extract scheme, host, port and path
+        if self.uri.startswith("http://"):
+            self.ssl = False
+            rest = self.uri[7:]
+        elif self.uri.startswith("https://"):
+            self.ssl = True
+            rest = self.uri[8:]
+        
+        splits = rest.split("/", 1)
+        hostport = splits[0].split(":")
+        self.host = hostport[0]
+        if len(hostport) > 1:
+            self.port = int(hostport[1])
+        else:
+            self.port = {False:80, True:443}[self.ssl]
+        self.path = "/"
+        if len(splits) > 1:
+            self.path += splits[1]

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20070909/7a66d7b6/attachment.html


More information about the calendarserver-changes mailing list