[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