[CalendarServer-changes] [7738] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Jul 7 12:48:56 PDT 2011


Revision: 7738
          http://trac.macosforge.org/projects/calendarserver/changeset/7738
Author:   cdaboo at apple.com
Date:     2011-07-07 12:48:55 -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/trunk/conf/servers.dtd
    CalendarServer/trunk/conf/servers.xml
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py
    CalendarServer/trunk/twistedcaldav/servers.py
    CalendarServer/trunk/twistedcaldav/test/test_servers.py

Modified: CalendarServer/trunk/conf/servers.dtd
===================================================================
--- CalendarServer/trunk/conf/servers.dtd	2011-07-07 19:34:44 UTC (rev 7737)
+++ CalendarServer/trunk/conf/servers.dtd	2011-07-07 19:48:55 UTC (rev 7738)
@@ -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/trunk/conf/servers.xml
===================================================================
--- CalendarServer/trunk/conf/servers.xml	2011-07-07 19:34:44 UTC (rev 7737)
+++ CalendarServer/trunk/conf/servers.xml	2011-07-07 19:48:55 UTC (rev 7738)
@@ -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/trunk/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py	2011-07-07 19:34:44 UTC (rev 7737)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py	2011-07-07 19:48:55 UTC (rev 7738)
@@ -150,6 +150,7 @@
         if serverURI not in self.otherServers:
             self.otherServers[serverURI] = IScheduleServerRecord(uri=joinURL(serverURI, "/ischedule"))
             self.otherServers[serverURI].unNormalizeAddresses = not recipient.principal.server().isImplicit
+            self.otherServers[serverURI].moreHeaders.append(recipient.principal.server().secretHeader())
         
         return self.otherServers[serverURI]
 
@@ -286,6 +287,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)
+            
         if self.refreshOnly:
             self.headers.addRawHeader("X-CALENDARSERVER-ITIP-REFRESHONLY", "T")
 

Modified: CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py	2011-07-07 19:34:44 UTC (rev 7737)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py	2011-07-07 19:48:55 UTC (rev 7738)
@@ -140,6 +140,7 @@
         self.domains = []
         self.client_hosts = []
         self.unNormalizeAddresses = True
+        self.moreHeaders = []
         
         if uri:
             self.uri = uri

Modified: CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py	2011-07-07 19:34:44 UTC (rev 7737)
+++ CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py	2011-07-07 19:48:55 UTC (rev 7738)
@@ -940,7 +940,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
         
@@ -952,6 +952,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:
@@ -967,7 +974,12 @@
                         break
             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(

Modified: CalendarServer/trunk/twistedcaldav/servers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/servers.py	2011-07-07 19:34:44 UTC (rev 7737)
+++ CalendarServer/trunk/twistedcaldav/servers.py	2011-07-07 19:48:55 UTC (rev 7738)
@@ -15,12 +15,12 @@
 ##
 
 from twext.python.log import Logger
-
+from twisted.internet.abstract import isIPAddress
 from twistedcaldav.client.pool import installPool
 from twistedcaldav.config import config, fullServerPath
 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,7 +53,7 @@
         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:
@@ -61,7 +63,7 @@
                     config.ConfigRoot,
                     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
@@ -98,11 +100,13 @@
         self.uri = None
         self.thisServer = False
         self.ips = set()
+        self.allowed_from_ips = set()
+        self.shared_secret = None
         self.partitions = {}
         self.partitions_ips = set()
         self.isImplicit = True
     
-    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:
@@ -117,17 +121,41 @@
         try:
             _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
         except socket.gaierror, e:
-            log.error("Unable to lookup ip-addr for server '%s': %s" % (parsed_uri.hostname, str(e)))
-            ips = ()
+            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)
             try:
                 _ignore_host, _ignore_aliases, ips = socket.gethostbyname_ex(parsed_uri.hostname)
             except socket.gaierror, e:
-                log.error("Unable to lookup ip-addr for partition '%s': %s" % (parsed_uri.hostname, str(e)))
-                ips = ()
+                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):
@@ -136,6 +164,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
     
@@ -158,6 +215,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"
 ATTR_IMPLICIT                   = "implicit"
@@ -169,7 +228,7 @@
     Servers configuration file parser.
     """
     @staticmethod
-    def parse(xmlFile):
+    def parse(xmlFile, ignoreIPLookupFailures=False):
 
         results = {}
 
@@ -192,6 +251,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:
@@ -200,7 +263,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

Modified: CalendarServer/trunk/twistedcaldav/test/test_servers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_servers.py	2011-07-07 19:34:44 UTC (rev 7737)
+++ CalendarServer/trunk/twistedcaldav/test/test_servers.py	2011-07-07 19:48:55 UTC (rev 7738)
@@ -14,10 +14,11 @@
 # limitations under the License.
 ##
 
-from twistedcaldav.servers import Servers
+from twext.web2.test.test_server import SimpleRequest
+from twistedcaldav.config import config
+from twistedcaldav.servers import Servers, SERVER_SECRET_HEADER
 from twistedcaldav.test.util import TestCase
 import StringIO as StringIO
-from twistedcaldav.config import config
 
 class ServerTests(TestCase):
 
@@ -26,6 +27,8 @@
   <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>
@@ -43,22 +46,58 @@
   </server>
 </servers>
 """
-        
-    def test_read_ok(self):
-        
+
+    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 _setupServers(self, data=data1):
         self.patch(config, "ServerHostName", "caldav1.example.com")
         self.patch(config, "HTTPPort", 8008)
 
-        xmlFile = StringIO.StringIO(ServerTests.data1)
+        xmlFile = StringIO.StringIO(data)
         servers = Servers
-        servers.load(xmlFile)
+        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)
 
@@ -67,12 +106,7 @@
 
     def test_this_server(self):
         
-        self.patch(config, "ServerHostName", "caldav1.example.com")
-        self.patch(config, "HTTPPort", 8008)
-        
-        xmlFile = StringIO.StringIO(ServerTests.data1)
-        servers = Servers
-        servers.load(xmlFile)
+        servers = self._setupServers()
 
         self.assertTrue(servers.getServerById("00001").thisServer)
         self.assertFalse(servers.getServerById("00002").thisServer)
@@ -83,7 +117,56 @@
         
         xmlFile = StringIO.StringIO(ServerTests.data1)
         servers = Servers
-        servers.load(xmlFile)
+        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/40868758/attachment-0001.html>


More information about the calendarserver-changes mailing list