[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