[CalendarServer-changes] [7739] CalendarServer/branches/users/wsanchez/deployment

source_changes at macosforge.org source_changes at macosforge.org
Thu Jul 7 12:54:13 PDT 2011


Revision: 7739
          http://trac.macosforge.org/projects/calendarserver/changeset/7739
Author:   cdaboo at apple.com
Date:     2011-07-07 12:54:13 -0700 (Thu, 07 Jul 2011)
Log Message:
-----------
Improve iSchedule connection verification by adding options to specify allowed client IPs and a shared secret
communicated via an HTTP header.

Modified Paths:
--------------
    CalendarServer/branches/users/wsanchez/deployment/conf/servers.dtd
    CalendarServer/branches/users/wsanchez/deployment/conf/servers.xml
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/schedule.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/servers.py

Added Paths:
-----------
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_servers.py

Modified: CalendarServer/branches/users/wsanchez/deployment/conf/servers.dtd
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/servers.dtd	2011-07-07 19:48:55 UTC (rev 7738)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/servers.dtd	2011-07-07 19:54:13 UTC (rev 7739)
@@ -16,11 +16,13 @@
 
 <!ELEMENT servers (server*) >
 
-	<!ELEMENT server (id, uri, partitions?) >
+	<!ELEMENT server (id, uri, allowed-from*, shared-secret?, partitions?) >
 		<!ATTLIST server implicit (yes|no) "yes">
 
 		<!ELEMENT id  (#PCDATA) >
 		<!ELEMENT uri (#PCDATA) >
+		<!ELEMENT allowed-from (#PCDATA) >
+		<!ELEMENT shared-secret (#PCDATA) >
 
 		<!ELEMENT partitions (partition*) >
 			<!ELEMENT partition (id, uri) >

Modified: CalendarServer/branches/users/wsanchez/deployment/conf/servers.xml
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/conf/servers.xml	2011-07-07 19:48:55 UTC (rev 7738)
+++ CalendarServer/branches/users/wsanchez/deployment/conf/servers.xml	2011-07-07 19:54:13 UTC (rev 7739)
@@ -27,6 +27,9 @@
   <server>
     <id>B</id>
     <uri>https://caldav2.example.com:8843</uri>
+    <allowed-from>127.0.0.1</allowed-from>
+    <allowed-from>example.local</allowed-from>
+    <shared-secret>ABC</shared-secret>
     <partitions>
     	<partition>
     		<id>00001</id>

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/directory.py	2011-07-07 19:48:55 UTC (rev 7738)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/directory.py	2011-07-07 19:54:13 UTC (rev 7739)
@@ -276,6 +276,15 @@
         else:
             return None
     
+    def server(self):
+        """
+        Server hosting this record. Return None if hosted on this server.
+        """
+        if config.Servers.Enabled and self.serverID:
+            return servers.Servers.getServerById(self.serverID)
+        else:
+            return None
+    
     def partitionURI(self):
         """
         URL of the server hosting this record. Return None if hosted on this server.

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py	2011-07-07 19:48:55 UTC (rev 7738)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/directory/principal.py	2011-07-07 19:54:13 UTC (rev 7739)
@@ -607,6 +607,9 @@
     def serverURI(self):
         return self.record.serverURI()
 
+    def server(self):
+        return self.record.server()
+
     def partitionURI(self):
         return self.record.partitionURI()
 

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/schedule.py	2011-07-07 19:48:55 UTC (rev 7738)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/schedule.py	2011-07-07 19:54:13 UTC (rev 7739)
@@ -395,6 +395,9 @@
             ),
         )
 
+    def exists(self):
+        return True
+
     def resourceType(self):
         return davxml.ResourceType.ischeduleinbox
 

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py	2011-07-07 19:48:55 UTC (rev 7738)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischedule.py	2011-07-07 19:54:13 UTC (rev 7739)
@@ -139,6 +139,7 @@
         if server not in self.otherServers:
             self.otherServers[server] = IScheduleServerRecord(uri=joinURL(server, "/ischedule"))
             self.otherServers[server].unNormalizeAddresses = False
+            self.otherServers[server].moreHeaders.append(recipient.principal.server().secretHeader())
         
         return self.otherServers[server]
 
@@ -193,6 +194,10 @@
             self.headers.addRawHeader('Recipient', utf8String(recipient.cuaddr))
         self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
 
+        # Add any additional headers
+        for name, value in self.server.moreHeaders:
+            self.headers.addRawHeader(name, value)
+
     def _doAuthentication(self):
         if self.server.authentication and self.server.authentication[0] == "basic":
             self.headers.setHeader(

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py	2011-07-07 19:48:55 UTC (rev 7738)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/ischeduleservers.py	2011-07-07 19:54:13 UTC (rev 7739)
@@ -134,6 +134,7 @@
         self.domains = []
         self.client_hosts = []
         self.unNormalizeAddresses = True
+        self.moreHeaders = []
         
         if uri:
             self.uri = uri

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py	2011-07-07 19:48:55 UTC (rev 7738)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py	2011-07-07 19:54:13 UTC (rev 7739)
@@ -782,7 +782,7 @@
         if expected_uri is None:
             expected_uri = principal.serverURI()
         expected_uri = urlparse.urlparse(expected_uri)
-    
+
         # Get the request IP and map to hostname.
         clientip = self.request.remoteAddr.host
         
@@ -794,6 +794,13 @@
         if Servers.getThisServer().checkThisIP(clientip):
             matched = True
     
+        # Checked allowed IPs - if any were defined we only check against them, we do not
+        # go on to check the expected server host ip
+        elif Servers.getThisServer().hasAllowedFromIP():
+            matched = Servers.getThisServer().checkAllowedFromIP(clientip)
+            if not matched:
+                log.error("Invalid iSchedule connection from client: %s" % (clientip,))
+
         # Next compare as dotted IP
         elif isIPAddress(expected_uri.hostname):
             if clientip == expected_uri.hostname:
@@ -810,6 +817,11 @@
             except socket.herror, e:
                 log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
         
+        # Check possible shared secret
+        if matched and not Servers.getThisServer().checkSharedSecret(self.request):
+            log.err("Invalid iSchedule shared secret")
+            matched = False
+
         if not matched:
             log.err("Originator not on allowed server: %s" % (self.originator,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/servers.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/servers.py	2011-07-07 19:48:55 UTC (rev 7738)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/servers.py	2011-07-07 19:54:13 UTC (rev 7739)
@@ -14,13 +14,13 @@
 # limitations under the License.
 ##
 
-
+from twisted.internet.abstract import isIPAddress
 from twistedcaldav.client.pool import installPool
 from twistedcaldav.config import config
 from twistedcaldav.log import Logger
 from twistedcaldav.xmlutil import readXML
+import socket
 import urlparse
-import socket
 
 """
 XML based server configuration file handling.
@@ -40,6 +40,8 @@
 
 log = Logger()
 
+SERVER_SECRET_HEADER = "X-CALENDARSERVER-ISCHEDULE"
+
 class ServersDB(object):
     """
     Represents the set of servers within the same domain.
@@ -51,14 +53,14 @@
         self._xmlFile = None
         self._thisServer = None
 
-    def load(self, xmlFile=None):
+    def load(self, xmlFile=None, ignoreIPLookupFailures=False):
         if self._xmlFile is None or xmlFile is not None:
             self._servers = {}
             if xmlFile:
                 self._xmlFile = xmlFile
             else:
                 self._xmlFile = config.Servers.ConfigFile
-        self._servers = ServersParser.parse(self._xmlFile)
+        self._servers = ServersParser.parse(self._xmlFile, ignoreIPLookupFailures=ignoreIPLookupFailures)
         for server in self._servers.values():
             if server.thisServer:
                 self._thisServer = server
@@ -93,12 +95,14 @@
     def __init__(self):
         self.id = None
         self.uri = None
+        self.thisServer = False
         self.ips = set()
-        self.thisServer = False
+        self.allowed_from_ips = set()
+        self.shared_secret = None
         self.partitions = {}
         self.partitions_ips = set()
     
-    def check(self):
+    def check(self, ignoreIPLookupFailures=False):
         # Check whether this matches the current server
         parsed_uri = urlparse.urlparse(self.uri)
         if parsed_uri.hostname == config.ServerHostName:
@@ -110,12 +114,44 @@
                     self.thisServer = parsed_uri.port in (config.SSLPort,) + tuple(config.BindSSLPorts)
         
         # Need to cache IP addresses
-        _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
+        try:
+            _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
+        except socket.gaierror, e:
+            msg = "Unable to lookup ip-addr for server '%s': %s" % (parsed_uri.hostname, str(e))
+            log.error(msg)
+            if ignoreIPLookupFailures:
+                ips = ()
+            else:
+                raise ValueError(msg)
         self.ips = set(ips)
 
+        actual_ips = set()
+        for item in self.allowed_from_ips:
+            if not isIPAddress(item):
+                try:
+                    _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(item)
+                except socket.gaierror, e:
+                    msg = "Unable to lookup ip-addr for allowed-from '%s': %s" % (item, str(e))
+                    log.error(msg)
+                    if not ignoreIPLookupFailures:
+                        raise ValueError(msg)
+                else:
+                    actual_ips.update(ips)
+            else:
+                actual_ips.add(item)
+        self.allowed_from_ips = actual_ips
+            
         for uri in self.partitions.values():
             parsed_uri = urlparse.urlparse(uri)
-            _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
+            try:
+                _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
+            except socket.gaierror, e:
+                msg = "Unable to lookup ip-addr for partition '%s': %s" % (parsed_uri.hostname, str(e))
+                log.error(msg)
+                if ignoreIPLookupFailures:
+                    ips = ()
+                else:
+                    raise ValueError(msg)
             self.partitions_ips.update(ips)
     
     def checkThisIP(self, ip):
@@ -124,6 +160,35 @@
         """
         return (ip in self.ips) or (ip in self.partitions_ips)
 
+    def hasAllowedFromIP(self):
+        return len(self.allowed_from_ips) > 0
+
+    def checkAllowedFromIP(self, ip):
+        return ip in self.allowed_from_ips
+
+    def checkSharedSecret(self, request):
+        
+        # Get header from the request
+        request_secret = request.headers.getRawHeaders(SERVER_SECRET_HEADER)
+        
+        if request_secret is not None and self.shared_secret is None:
+            log.error("iSchedule request included unexpected %s header" % (SERVER_SECRET_HEADER,))
+            return False
+        elif request_secret is None and self.shared_secret is not None:
+            log.error("iSchedule request did not include required %s header" % (SERVER_SECRET_HEADER,))
+            return False
+        elif (request_secret[0] if request_secret else None) != self.shared_secret:
+            log.error("iSchedule request %s header did not match" % (SERVER_SECRET_HEADER,))
+            return False
+        else:
+            return True
+
+    def secretHeader(self):
+        """
+        Return a tuple of header name, header value
+        """
+        return (SERVER_SECRET_HEADER, self.shared_secret,)
+
     def addPartition(self, id, uri):
         self.partitions[id] = uri
     
@@ -146,6 +211,8 @@
 ELEMENT_SERVER                  = "server"
 ELEMENT_ID                      = "id"
 ELEMENT_URI                     = "uri"
+ELEMENT_ALLOWED_FROM            = "allowed-from"
+ELEMENT_SHARED_SECRET           = "shared-secret"
 ELEMENT_PARTITIONS              = "partitions"
 ELEMENT_PARTITION               = "partition"
 
@@ -154,7 +221,7 @@
     Servers configuration file parser.
     """
     @staticmethod
-    def parse(xmlFile):
+    def parse(xmlFile, ignoreIPLookupFailures=False):
 
         results = {}
 
@@ -175,6 +242,10 @@
                     server.id = node.text
                 elif node.tag == ELEMENT_URI:
                     server.uri = node.text
+                elif node.tag == ELEMENT_ALLOWED_FROM:
+                    server.allowed_from_ips.add(node.text)
+                elif node.tag == ELEMENT_SHARED_SECRET:
+                    server.shared_secret = node.text
                 elif node.tag == ELEMENT_PARTITIONS:
                     ServersParser._parsePartition(xmlFile, node, server)
                 else:
@@ -183,7 +254,7 @@
             if server.id is None or server.uri is None:
                 log.error("Invalid partition '%s' in servers file: '%s'" % (child.tag, xmlFile,), raiseException=RuntimeError)
 
-            server.check()
+            server.check(ignoreIPLookupFailures=ignoreIPLookupFailures)
             results[server.id] = server
 
         return results

Added: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_servers.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_servers.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_servers.py	2011-07-07 19:54:13 UTC (rev 7739)
@@ -0,0 +1,193 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from twisted.web2.test.test_server import SimpleRequest
+from twistedcaldav.config import config
+from twistedcaldav.servers import Servers, SERVER_SECRET_HEADER
+from twistedcaldav.test.test_tap import CleanupHelper
+from twistedcaldav.test.util import TestCase
+import StringIO as StringIO
+
+class ServerTests(CleanupHelper, TestCase):
+
+    data1 = """<?xml version="1.0" encoding="utf-8"?>
+<servers>
+  <server>
+    <id>00001</id>
+    <uri>http://caldav1.example.com:8008</uri>
+    <allowed-from>127.0.0.1</allowed-from>
+    <shared-secret>foobar</shared-secret>
+  </server>
+  <server>
+    <id>00002</id>
+    <uri>https://caldav2.example.com:8843</uri>
+    <partitions>
+        <partition>
+            <id>A</id>
+            <uri>https://machine1.example.com:8443</uri>
+        </partition>
+        <partition>
+            <id>B</id>
+            <uri>https://machine2.example.com:8443</uri>
+        </partition>
+    </partitions>
+  </server>
+</servers>
+"""
+
+    data2 = """<?xml version="1.0" encoding="utf-8"?>
+<servers>
+  <server>
+    <id>00001</id>
+    <uri>http://caldav1.example.com:8008</uri>
+    <allowed-from>localhost</allowed-from>
+    <shared-secret>foobar</shared-secret>
+  </server>
+  <server>
+    <id>00002</id>
+    <uri>https://caldav2.example.com:8843</uri>
+    <partitions>
+        <partition>
+            <id>A</id>
+            <uri>https://machine1.example.com:8443</uri>
+        </partition>
+        <partition>
+            <id>B</id>
+            <uri>https://machine2.example.com:8443</uri>
+        </partition>
+    </partitions>
+  </server>
+</servers>
+"""
+
+    def patch(self, obj, attribute, value):
+        """
+        Monkey patch an object for the duration of the test.
+
+        The monkey patch will be reverted at the end of the test using the
+        L{addCleanup} mechanism.
+
+        The L{MonkeyPatcher} is returned so that users can restore and
+        re-apply the monkey patch within their tests.
+
+        @param obj: The object to monkey patch.
+        @param attribute: The name of the attribute to change.
+        @param value: The value to set the attribute to.
+        @return: A L{monkey.MonkeyPatcher} object.
+        """
+        old_value = getattr(obj, attribute)
+        setattr(obj, attribute, value)
+        self.addCleanup(lambda:setattr(obj, attribute, old_value))
+
+
+    def _setupServers(self, data=data1):
+        self.patch(config, "ServerHostName", "caldav1.example.com")
+        self.patch(config, "HTTPPort", 8008)
+
+        xmlFile = StringIO.StringIO(data)
+        servers = Servers
+        servers.load(xmlFile, ignoreIPLookupFailures=True)
+
+        return servers
+
+    def test_read_ok(self):
+        
+        servers = self._setupServers()
+
+        self.assertTrue(servers.getServerById("00001") is not None)
+        self.assertTrue(servers.getServerById("00002") is not None)
+
+        self.assertEqual(servers.getServerById("00001").uri, "http://caldav1.example.com:8008")
+        self.assertEqual(servers.getServerById("00002").uri, "https://caldav2.example.com:8843")
+
+        self.assertEqual(servers.getServerById("00001").allowed_from_ips, set(("127.0.0.1",)))
+        self.assertEqual(servers.getServerById("00002").allowed_from_ips, set())
+
+        self.assertEqual(servers.getServerById("00001").shared_secret, "foobar")
+        self.assertEqual(servers.getServerById("00002").shared_secret, None)
+
+        self.assertEqual(len(servers.getServerById("00001").partitions), 0)
+        self.assertEqual(len(servers.getServerById("00002").partitions), 2)
+
+        self.assertEqual(servers.getServerById("00002").getPartitionURIForId("A"), "https://machine1.example.com:8443")
+        self.assertEqual(servers.getServerById("00002").getPartitionURIForId("B"), "https://machine2.example.com:8443")
+
+    def test_this_server(self):
+        
+        servers = self._setupServers()
+
+        self.assertTrue(servers.getServerById("00001").thisServer)
+        self.assertFalse(servers.getServerById("00002").thisServer)
+        
+        self.patch(config, "ServerHostName", "caldav2.example.com")
+        self.patch(config, "SSLPort", 8443)
+        self.patch(config, "BindSSLPorts", [8843])
+        
+        xmlFile = StringIO.StringIO(ServerTests.data1)
+        servers = Servers
+        servers.load(xmlFile, ignoreIPLookupFailures=True)
+
+        self.assertFalse(servers.getServerById("00001").thisServer)
+        self.assertTrue(servers.getServerById("00002").thisServer)
+
+    def test_check_this_ip(self):
+
+        servers = self._setupServers()
+        servers.getServerById("00001").ips = set(("127.0.0.2",))
+        servers.getServerById("00002").ips = set(("127.0.0.3",))
+        
+        self.assertTrue(servers.getServerById("00001").checkThisIP("127.0.0.2"))
+        self.assertFalse(servers.getServerById("00001").checkThisIP("127.0.0.3"))
+
+    def test_check_allowed_from(self):
+
+        for servers in (self._setupServers(), self._setupServers(data=self.data2),):
+            self.assertTrue(servers.getServerById("00001").hasAllowedFromIP())
+            self.assertFalse(servers.getServerById("00002").hasAllowedFromIP())
+
+            self.assertTrue(servers.getServerById("00001").checkAllowedFromIP("127.0.0.1"))
+            self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.2"))
+            self.assertFalse(servers.getServerById("00001").checkAllowedFromIP("127.0.0.3"))
+            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.1"))
+            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.2"))
+            self.assertFalse(servers.getServerById("00002").checkAllowedFromIP("127.0.0.3"))
+
+    def test_check_shared_secret(self):
+
+        servers = self._setupServers()
+        
+        request = SimpleRequest(None, "POST", "/ischedule")
+        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
+        self.assertTrue(servers.getServerById("00001").checkSharedSecret(request))
+        
+        request = SimpleRequest(None, "POST", "/ischedule")
+        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
+        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
+        
+        request = SimpleRequest(None, "POST", "/ischedule")
+        self.assertFalse(servers.getServerById("00001").checkSharedSecret(request))
+        
+        request = SimpleRequest(None, "POST", "/ischedule")
+        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar")
+        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
+        
+        request = SimpleRequest(None, "POST", "/ischedule")
+        request.headers.addRawHeader(SERVER_SECRET_HEADER, "foobar1")
+        self.assertFalse(servers.getServerById("00002").checkSharedSecret(request))
+        
+        request = SimpleRequest(None, "POST", "/ischedule")
+        self.assertTrue(servers.getServerById("00002").checkSharedSecret(request))
+        
\ No newline at end of file
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110707/425d7962/attachment-0001.html>


More information about the calendarserver-changes mailing list