[CalendarServer-changes] [9751] CalendarServer/branches/users/cdaboo/ischedule-dkim
source_changes at macosforge.org
source_changes at macosforge.org
Wed Aug 29 13:24:22 PDT 2012
Revision: 9751
http://trac.macosforge.org/projects/calendarserver/changeset/9751
Author: cdaboo at apple.com
Date: 2012-08-29 13:24:20 -0700 (Wed, 29 Aug 2012)
Log Message:
-----------
Initial iSchedule-DKIM support.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/ischedule-dkim/calendarserver/tap/util.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/__init__.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/schedule.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/addressmapping.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/scheduler.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/stdconfig.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/timezonexml.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/servers.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py
Removed Paths:
-------------
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule.py
CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py
Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/calendarserver/tap/util.py 2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/calendarserver/tap/util.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -58,7 +58,7 @@
from calendarserver.push.applepush import APNSubscriptionResource
from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
from twistedcaldav.resource import AuthenticationWrapper
-from twistedcaldav.schedule import IScheduleInboxResource
+from twistedcaldav.scheduling.ischedule.resource import IScheduleInboxResource
from twistedcaldav.simpleresource import SimpleResource, SimpleRedirectResource
from twistedcaldav.timezones import TimezoneCache
from twistedcaldav.timezoneservice import TimezoneServiceResource
@@ -533,6 +533,7 @@
(config.EnableCalDAV, "caldav", "/",),
(config.EnableCardDAV, "carddav", "/",),
(config.TimezoneService.Enabled, "timezone", "/stdtimezones",),
+ (config.Scheduling.iSchedule.Enabled, "ischedule", "/ischedule"),
):
if enabled:
if config.EnableSSL:
Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/__init__.py 2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/__init__.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -41,6 +41,8 @@
import twistedcaldav.carddavxml
import twistedcaldav.mkcolxml
import twistedcaldav.customxml
+import twistedcaldav.timezonexml
+import twistedcaldav.scheduling.ischedule.xml
twistedcaldav # Shhh.. pyflakes
Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/schedule.py 2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/schedule.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -30,12 +30,9 @@
from txdav.xml import element as davxml
from txdav.xml.rfc2518 import HRef
from twext.web2.dav.http import ErrorResponse, MultiStatusResponse
-from twext.web2.dav.noneprops import NonePropertyStore
from twext.web2.dav.resource import davPrivilegeSet
from twext.web2.dav.util import joinURL, normalizeURL
from twext.web2.http import HTTPError
-from twext.web2.http import Response
-from twext.web2.http_headers import MimeType
from twisted.internet.defer import inlineCallbacks, returnValue, succeed
@@ -45,10 +42,9 @@
from twistedcaldav.config import config
from twistedcaldav.customxml import calendarserver_namespace
from twistedcaldav.ical import allowedComponents
-from twistedcaldav.extensions import DAVResource
-from twistedcaldav.resource import CalDAVResource, ReadOnlyNoCopyResourceMixIn
+from twistedcaldav.resource import CalDAVResource
from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.scheduling.scheduler import CalDAVScheduler, IScheduleScheduler
+from twistedcaldav.scheduling.scheduler import CalDAVScheduler
from txdav.base.propertystore.base import PropertyName
@@ -488,117 +484,3 @@
responses = [davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)) for href in multiget.resources]
return succeed(MultiStatusResponse((responses)))
-class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResource):
- """
- iSchedule Inbox resource.
-
- Extends L{DAVResource} to provide iSchedule inbox functionality.
- """
-
- def __init__(self, parent, store):
- """
- @param parent: the parent resource of this one.
- """
- assert parent is not None
-
- DAVResource.__init__(self, principalCollections=parent.principalCollections())
-
- self.parent = parent
- self._newStore = store
-
- def deadProperties(self):
- if not hasattr(self, "_dead_properties"):
- self._dead_properties = NonePropertyStore(self)
- return self._dead_properties
-
- def etag(self):
- return succeed(None)
-
- def checkPreconditions(self, request):
- return None
-
- def resourceType(self):
- return davxml.ResourceType.ischeduleinbox
-
- def contentType(self):
- return MimeType.fromString("text/html; charset=utf-8");
-
- def isCollection(self):
- return False
-
- def isCalendarCollection(self):
- return False
-
- def isPseudoCalendarCollection(self):
- return False
-
- def principalForCalendarUserAddress(self, address):
- for principalCollection in self.principalCollections():
- principal = principalCollection.principalForCalendarUserAddress(address)
- if principal is not None:
- return principal
- return None
-
- def render(self, request):
- output = """<html>
-<head>
-<title>Server To Server Inbox Resource</title>
-</head>
-<body>
-<h1>Server To Server Inbox Resource.</h1>
-</body
-</html>"""
-
- response = Response(200, {}, output)
- response.headers.setHeader("content-type", MimeType("text", "html"))
- return response
-
- @inlineCallbacks
- def http_POST(self, request):
- """
- The server-to-server POST method.
- """
-
- # Check authentication and access controls
- yield self.authorize(request, (caldavxml.ScheduleDeliver(),))
-
- # This is a server-to-server scheduling operation.
- scheduler = IScheduleScheduler(request, self)
-
- # Need a transaction to work with
- txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
- request._newStoreTransaction = txn
-
- # Do the POST processing treating this as a non-local schedule
- try:
- result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
- except Exception, e:
- yield txn.abort()
- raise e
- else:
- yield txn.commit()
- returnValue(result.response())
-
- ##
- # ACL
- ##
-
- def supportedPrivileges(self, request):
- return succeed(deliverSchedulePrivilegeSet)
-
- def defaultAccessControlList(self):
- privs = (
- davxml.Privilege(davxml.Read()),
- davxml.Privilege(caldavxml.ScheduleDeliver()),
- )
- if config.Scheduling.CalDAV.OldDraftCompatibility:
- privs += (davxml.Privilege(caldavxml.Schedule()),)
-
- return davxml.ACL(
- # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
- davxml.ACE(
- davxml.Principal(davxml.All()),
- davxml.Grant(*privs),
- davxml.Protected(),
- ),
- )
Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/addressmapping.py 2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/addressmapping.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -23,7 +23,7 @@
from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
from twistedcaldav.scheduling.delivery import DeliveryService
from twistedcaldav.scheduling.imip import ScheduleViaIMip
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
+from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser, EmailCalendarUser, InvalidCalendarUser,\
calendarUserFromPrincipal
Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/__init__.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2012 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.
+##
+
+"""
+iSchedule scheduling.
+"""
Copied: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py (from rev 9747, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/delivery.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,344 @@
+##
+# Copyright (c) 2005-2012 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 StringIO import StringIO
+
+from twisted.internet.defer import inlineCallbacks, DeferredList, succeed
+from twisted.internet.protocol import Factory
+
+from twisted.python.failure import Failure
+
+from twext.web2 import responsecode
+from twext.web2.client.http import ClientRequest
+from twext.web2.client.http import HTTPClientProtocol
+from twext.web2.dav.util import davXMLFromStream, joinURL, allDataFromStream
+from twext.web2.http import HTTPError
+from twext.web2.http_headers import Headers
+from twext.web2.http_headers import MimeType
+from twext.web2.stream import MemoryStream
+
+from twext.python.log import Logger, logLevels
+from twext.web2.dav.http import ErrorResponse
+
+from twistedcaldav.client.pool import _configuredClientContextFactory
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.ischedule.servers import IScheduleServers
+from twistedcaldav.scheduling.ischedule.servers import IScheduleServerRecord
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twistedcaldav.util import utf8String, normalizationLookup
+from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser,\
+ OtherServerCalendarUser
+from twext.internet.gaiendpoint import GAIEndpoint
+
+"""
+Handles the sending of iSchedule scheduling messages. Used for both cross-domain scheduling,
+as well as internal partitioning or podding.
+"""
+
+__all__ = [
+ "ScheduleViaISchedule",
+]
+
+log = Logger()
+
+class ScheduleViaISchedule(DeliveryService):
+
+ @classmethod
+ def serviceType(cls):
+ return DeliveryService.serviceType_ischedule
+
+ @classmethod
+ def matchCalendarUserAddress(cls, cuaddr):
+
+ # TODO: here is where we would attempt service discovery based on the cuaddr.
+
+ # Do default match
+ return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
+
+ def generateSchedulingResponses(self, refreshOnly=False):
+ """
+ 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 = IScheduleServers()
+ for recipient in self.recipients:
+ if isinstance(recipient, RemoteCalendarUser):
+ # Map the recipient's domain to a server
+ server = servermgr.mapDomain(recipient.domain)
+ elif isinstance(recipient, PartitionedCalendarUser):
+ server = self._getServerForPartitionedUser(recipient)
+ elif isinstance(recipient, OtherServerCalendarUser):
+ server = self._getServerForOtherServerUser(recipient)
+ else:
+ assert False, "Incorrect calendar user address class"
+ if not server:
+ # Cannot do server-to-server for this recipient.
+ err = HTTPError(ErrorResponse(
+ responsecode.NOT_FOUND,
+ (caldav_namespace, "recipient-allowed"),
+ "No server for recipient",
+ ))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
+
+ # Process next recipient
+ continue
+
+ if not server.allow_to:
+ # Cannot do server-to-server outgoing requests for this server.
+ err = HTTPError(ErrorResponse(
+ responsecode.NOT_FOUND,
+ (caldav_namespace, "recipient-allowed"),
+ "Cannot send to recipient's server",
+ ))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+ # Process next recipient
+ continue
+
+ groups.setdefault(server, []).append(recipient)
+
+ if len(groups) == 0:
+ 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 = IScheduleRequest(self.scheduler, server, recipients, self.responses, refreshOnly)
+ deferreds.append(requestor.doRequest())
+
+ return DeferredList(deferreds)
+
+ def _getServerForPartitionedUser(self, recipient):
+
+ if not hasattr(self, "partitionedServers"):
+ self.partitionedServers = {}
+
+ partition = recipient.principal.partitionURI()
+ if partition not in self.partitionedServers:
+ self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
+ self.partitionedServers[partition].unNormalizeAddresses = False
+ self.partitionedServers[partition].moreHeaders.append(recipient.principal.server().secretHeader())
+
+ return self.partitionedServers[partition]
+
+ def _getServerForOtherServerUser(self, recipient):
+
+ if not hasattr(self, "otherServers"):
+ self.otherServers = {}
+
+ serverURI = recipient.principal.serverURI()
+ 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]
+
+class IScheduleRequest(object):
+
+ def __init__(self, scheduler, server, recipients, responses, refreshOnly=False):
+
+ self.scheduler = scheduler
+ self.server = server
+ self.recipients = recipients
+ self.responses = responses
+ self.refreshOnly = refreshOnly
+
+ self._generateHeaders()
+ self._prepareData()
+
+ @inlineCallbacks
+ def doRequest(self):
+
+ # Generate an HTTP client request
+ try:
+ if not hasattr(self.scheduler.request, "extendedLogItems"):
+ self.scheduler.request.extendedLogItems = {}
+ if "itip.ischedule" not in self.scheduler.request.extendedLogItems:
+ self.scheduler.request.extendedLogItems["itip.ischedule"] = 0
+ self.scheduler.request.extendedLogItems["itip.ischedule"] += 1
+
+ from twisted.internet import reactor
+ f = Factory()
+ f.protocol = HTTPClientProtocol
+ if self.server.ssl:
+ ep = GAIEndpoint(reactor, self.server.host, self.server.port,
+ _configuredClientContextFactory())
+ else:
+ ep = GAIEndpoint(reactor, self.server.host, self.server.port)
+ proto = (yield ep.connect(f))
+
+ request = ClientRequest("POST", self.server.path, self.headers, self.data)
+ yield self.logRequest("debug", "Sending server-to-server POST request:", request)
+ response = (yield proto.submitRequest(request))
+
+ yield self.logResponse("debug", "Received server-to-server POST response:", response)
+ xml = (yield davXMLFromStream(response.stream))
+
+ 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"),
+ "Server-to-server request failed",
+ ))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+ def logRequest(self, level, message, request, **kwargs):
+ """
+ Log an HTTP request.
+ """
+
+ assert level in logLevels
+
+ if log.willLogAtLevel(level):
+ iostr = StringIO()
+ iostr.write("%s\n" % (message,))
+ if hasattr(request, "clientproto"):
+ protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
+ else:
+ protocol = "HTTP/1.1"
+ iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
+ for name, valuelist in request.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("Authorization",):
+ iostr.write("%s: %s\n" % (name, value))
+ else:
+ iostr.write("%s: xxxxxxxxx\n" % (name,))
+ iostr.write("\n")
+
+ # We need to play a trick with the request stream as we can only read it once. So we
+ # read it, store the value in a MemoryStream, and replace the request's stream with that,
+ # so the data can be read again.
+ def _gotData(data):
+ iostr.write(data)
+
+ request.stream = MemoryStream(data if data is not None else "")
+ request.stream.doStartReading = None
+
+ log.emit(level, iostr.getvalue(), **kwargs)
+
+ d = allDataFromStream(request.stream)
+ d.addCallback(_gotData)
+ return d
+
+ else:
+ return succeed(None)
+
+ def logResponse(self, level, message, response, **kwargs):
+ """
+ Log an HTTP request.
+ """
+ assert level in logLevels
+
+ if log.willLogAtLevel(level):
+ iostr = StringIO()
+ iostr.write("%s\n" % (message,))
+ code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+ iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
+ for name, valuelist in response.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("WWW-Authenticate",):
+ iostr.write("%s: %s\n" % (name, value))
+ else:
+ iostr.write("%s: xxxxxxxxx\n" % (name,))
+ iostr.write("\n")
+
+ # We need to play a trick with the response stream to ensure we don't mess it up. So we
+ # read it, store the value in a MemoryStream, and replace the response's stream with that,
+ # so the data can be read again.
+ def _gotData(data):
+ iostr.write(data)
+
+ response.stream = MemoryStream(data if data is not None else "")
+ response.stream.doStartReading = None
+
+ log.emit(level, iostr.getvalue(), **kwargs)
+
+ d = allDataFromStream(response.stream)
+ d.addCallback(_gotData)
+ return d
+
+ def _generateHeaders(self):
+ self.headers = Headers()
+ self.headers.setHeader('Host', utf8String(self.server.host + ":%s" % (self.server.port,)))
+
+ # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
+ self.headers.addRawHeader('Originator', utf8String(self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee))
+ self._doAuthentication()
+ for recipient in self.recipients:
+ 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")
+
+ def _doAuthentication(self):
+ if self.server.authentication and self.server.authentication[0] == "basic":
+ self.headers.setHeader(
+ 'Authorization',
+ ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
+ )
+
+ def _prepareData(self):
+ if self.server.unNormalizeAddresses and self.scheduler.method == "PUT":
+ normalizedCalendar = self.scheduler.calendar.duplicate()
+ normalizedCalendar.normalizeCalendarUserAddresses(
+ normalizationLookup,
+ self.scheduler.resource.principalForCalendarUserAddress,
+ toUUID=False)
+ else:
+ normalizedCalendar = self.scheduler.calendar
+ self.data = str(normalizedCalendar)
+
+ 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/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/dkim.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,545 @@
+##
+# Copyright (c) 2012 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 twext.python.log import Logger
+from twext.web2.client.http import ClientRequest
+from twext.web2.dav.util import allDataFromStream
+from twext.web2.stream import MemoryStream
+from twisted.internet.defer import inlineCallbacks, returnValue
+import base64
+import collections
+import hashlib
+import rsa
+import textwrap
+import time
+
+"""
+DKIM HTTP message generation and validation,
+"""
+
+log = Logger()
+
+
+class DKIMUtils(object):
+ """
+ Some useful functions.
+ """
+
+ @staticmethod
+ def hashlib_method(algorithm):
+ """
+ Return hashlib function for DKIM algorithm.
+ """
+ return {
+ "rsa-sha1" : hashlib.sha1,
+ "rsa-sha256": hashlib.sha256,
+ }[algorithm]
+
+ @staticmethod
+ def hash_name(algorithm):
+ """
+ Return RSA hash name for DKIM algorithm.
+ """
+ return {
+ "rsa-sha1" : "SHA-1",
+ "rsa-sha256": "SHA-256"
+ }[algorithm]
+
+
+ @staticmethod
+ def extractTags(data):
+ """
+ Split a DKIM tag list into a dict, removing unneeded whitespace.
+ """
+ # Extract tags from the data
+ splits = [item.strip() for item in data.split(";")]
+ dkim_tags = {}
+ for item in splits:
+ name, value = item.split("=", 1)
+ dkim_tags[name.strip()] = value.strip()
+ return dkim_tags
+
+ @staticmethod
+ def canonicalizeHeader(name, value, remove_b=None):
+ """
+ Canonicalize the header using "relaxed" method. Optionally remove the b= value from
+ any DKIM-Signature present.
+
+ FIXME: this needs to be smarter about where valid WSP can occur in a header. Right now it will
+ blindly collapse all runs of SP/HTAB into a single SP. That could be wrong if a legitimate sequence of
+ SP/HTAB occurs in a header value.
+
+ @param name: header name
+ @type name: C{str}
+ @param value: header value
+ @type value: C{str}
+ @param remove_b: the b= value to remove, or C{None} if no removal needed
+ @type remove_b: C{str} or C{None}
+ """
+
+ # Basic relaxed behavior
+ name = name.lower()
+ value = " ".join(value.split())
+
+ # Special case DKIM-Signature: remove the b= value for signature
+ if remove_b is not None and name == "dkim-signature":
+ pos = value.find(remove_b)
+ value = value[:pos] + value[pos + len(remove_b):]
+ value = " ".join(value.split())
+
+ return "%s:%s\r\n" % (name, value,)
+
+
+
+class DKIMRequest(ClientRequest):
+ """
+ A ClientRequest that optionally creates a DKIM signature.
+ """
+
+ keys = {}
+
+ def __init__(
+ self,
+ method,
+ uri,
+ headers,
+ stream,
+ domain,
+ selector,
+ key_file,
+ algorithm,
+ sign_headers,
+ useDNSKey,
+ useHTTPKey,
+ expire,
+ ):
+ super(DKIMRequest, self).__init__(method, uri, headers, stream)
+ self.domain = domain
+ self.selector = selector
+ self.algorithm = algorithm
+ self.key_file = key_file
+ self.sign_headers = sign_headers
+ self.time = str(int(time.time()))
+ self.expire = str(int(time.time() + expire))
+
+ assert self.domain
+ assert self.selector
+ assert self.algorithm in ("rsa-sha1", "rsa-sha256",)
+ assert useDNSKey or useHTTPKey
+
+ self.hash_method = DKIMUtils.hashlib_method(self.algorithm)
+ self.hash_name = DKIMUtils.hash_name(self.algorithm)
+
+ self.keyMethods = []
+ if useDNSKey: self.keyMethods.append("dns/txt")
+ if useHTTPKey: self.keyMethods.append("http-well-known")
+
+
+ @inlineCallbacks
+ def sign(self):
+ """
+ Generate the DKIM headers by signing the request. This should only be called once on the request and there must
+ be no changes to the request (no headers, no body change) after it is called.
+ """
+
+ # Get the headers and the DKIM-Signature tags
+ headers, dkim_tags = (yield self.signatureHeaders())
+
+ # Sign the hash
+ signature = self.generateSignature(headers)
+
+ # Complete the header
+ dkim_tags[-1] = ("b", signature,)
+ dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
+ self.headers.addRawHeader("DKIM-Signature", dkim_header)
+
+ log.debug("DKIM: Generated header: DKIM-Signature:%s" % (dkim_header,))
+ log.debug("DKIM: Signed headers:\n%s" % (headers,))
+
+ returnValue(signature)
+
+
+ @inlineCallbacks
+ def bodyHash(self):
+ """
+ Generate the hash of the request body data.
+ """
+
+ # We need to play a trick with the request stream as we can only read it once. So we
+ # read it, store the value in a MemoryStream, and replace the request's stream with that,
+ # so the data can be read again.
+ data = (yield allDataFromStream(self.stream))
+ self.stream = MemoryStream(data if data is not None else "")
+ self.stream.doStartReading = None
+
+ returnValue(base64.b64encode(self.hash_method(data).digest()))
+
+
+ @inlineCallbacks
+ def signatureHeaders(self):
+ """
+ Generate the headers that are going to be signed as well as the DKIM-Signature tags.
+ """
+
+ # Figure out all the existing headers to sign
+ headers = []
+ sign_headers = []
+ raw = dict([(name, values) for name, values in self.headers.getAllRawHeaders()])
+ for name in self.sign_headers:
+ for value in raw[name]:
+ headers.append(DKIMUtils.canonicalizeHeader(name, value))
+ sign_headers.append(name)
+
+ # Generate the DKIM header tags we care about
+ dkim_tags = []
+ dkim_tags.append(("v", "1",))
+ dkim_tags.append(("d", self.domain,))
+ dkim_tags.append(("s", self.selector,))
+ dkim_tags.append(("t", self.time,))
+ dkim_tags.append(("x", self.expire,))
+ dkim_tags.append(("a", self.algorithm,))
+ dkim_tags.append(("q", ":".join(self.keyMethods),))
+ dkim_tags.append(("c", "relaxed/simple",))
+ dkim_tags.append(("h", ":".join(sign_headers),))
+ dkim_tags.append(("bh", (yield self.bodyHash()),))
+ dkim_tags.append(("b", "",))
+ dkim_header = "; ".join(["%s=%s" % item for item in dkim_tags])
+
+ headers.append(DKIMUtils.canonicalizeHeader("DKIM-Signature", dkim_header))
+ headers = "".join(headers)
+
+ returnValue((headers, dkim_tags,))
+
+
+ def generateSignature(self, headers):
+ # Sign the hash
+ if self.key_file not in self.keys:
+ self.keys[self.key_file] = rsa.PrivateKey.load_pkcs1(open(self.key_file).read())
+ return base64.b64encode(rsa.sign(headers, self.keys[self.key_file], self.hash_name))
+
+
+
+class DKIMVerificationError(Exception):
+ """
+ Used to indicate a DKIM verification error.
+ """
+ pass
+
+
+class DKIMVerifier(object):
+ """
+ Class used to verify an DKIM-signed HTTP request.
+ """
+
+ def __init__(self, request, key_lookup=None):
+ """
+ @param request: The HTTP request to process
+ @type request: L{twext.server.Request}
+ """
+ self.request = request
+ self.dkim_tags = {}
+
+ # Prefer HTTP over DNS when both are present
+ self.key_lookup_methods = (
+ PublicKeyLookup_HTTP_WellKnown,
+ PublicKeyLookup_DNSTXT,
+ ) if key_lookup is None else key_lookup
+
+
+
+ @inlineCallbacks
+ def verify(self):
+ """
+ @raise: DKIMVerificationError
+ """
+
+ # Check presence of DKIM header
+ self.processDKIMHeader()
+
+ # Extract the set of canonicalized headers being signed
+ headers = self.extractSignedHeaders()
+ log.debug("DKIM: Signed headers:\n%s" % (headers,))
+
+ # Locate the public key
+ pubkey = (yield self.locatePublicKey())
+ if pubkey is None:
+ raise DKIMVerificationError("No public key to verify the DKIM signature")
+
+ # Do header verification
+ try:
+ rsa.verify(headers, base64.b64decode(self.dkim_tags["b"]), pubkey)
+ except rsa.VerificationError:
+ log.debug("DKIM: Could not verify signature: DKIM-Signature:%s" % (self.request.headers.getRawHeaders("DKIM-Signature"),))
+ raise DKIMVerificationError("Could not verify the DKIM signature")
+
+ # Do body validation
+ data = (yield allDataFromStream(self.request.stream))
+ self.request.stream = MemoryStream(data if data is not None else "")
+ self.request.stream.doStartReading = None
+ bh = base64.b64encode(self.hash_method(data).digest())
+ if bh != self.dkim_tags["bh"]:
+ log.debug("DKIM: Could not verify body hash: DKIM-Signature:%s" % (self.request.headers.getRawHeaders("DKIM-Signature"),))
+ raise DKIMVerificationError("Could not verify the DKIM body hash")
+
+
+ def processDKIMHeader(self):
+ """
+ Extract the DKIM-Signature header and process the tags.
+
+ @raise: DKIMVerificationError
+ """
+
+ # Check presence of header
+ dkim = self.request.headers.getRawHeaders("DKIM-Signature")
+ if dkim is None:
+ log.debug("DKIM: No DKIM-Signature header present in the request")
+ raise DKIMVerificationError("No DKIM-Signature header present in the request")
+ if len(dkim) != 1:
+ # TODO: This might need to be changed if we ever support forwarding of iSchedule messages - the forwarder
+ # might also sign the message and add its own header
+ log.debug("DKIM: Only one DKIM-Signature allowed in the request")
+ raise DKIMVerificationError("Only one DKIM-Signature allowed in the request")
+ dkim = dkim[0]
+ log.debug("DKIM: Found header: DKIM-Signature:%s" % (dkim,))
+
+ # Extract tags from the header
+ self.dkim_tags = DKIMUtils.extractTags(dkim)
+
+ # Verify validity of tags
+ required_tags = ("v", "a", "b", "bh", "c", "d", "h", "s",)
+ for tag in required_tags:
+ if tag not in self.dkim_tags:
+ log.debug("DKIM: Missing DKIM-Signature tag: %s" % (tag,))
+ raise DKIMVerificationError("Missing DKIM-Signature tag: %s" % (tag,))
+
+ check_values = {
+ "v": ("1",),
+ "a": ("rsa-sha1", "rsa-sha256",),
+ "c": ("relaxed", "relaxed/simple",),
+ "q": ("dns/txt", "http-well-known",),
+ }
+ for tag, values in check_values.items():
+ if tag not in required_tags and tag not in self.dkim_tags:
+ pass
+
+ # Handle some structured values
+ if tag == "q":
+ test = self.dkim_tags[tag].split(":")
+ else:
+ test = (self.dkim_tags[tag],)
+ for item in test:
+ if item not in values:
+ log.debug("DKIM: Tag: %s has incorrect value: %s" % (tag, self.dkim_tags[tag],))
+ raise DKIMVerificationError("DKIM-Signature tag: %s has incorrect value: %s" % (tag, self.dkim_tags[tag],))
+
+ # Check expiration
+ if "x" in self.dkim_tags:
+ diff_time = int(time.time()) - int(self.dkim_tags["x"])
+ if diff_time > 0:
+ log.debug("DKIM: Signature expired: %d seconds" % (diff_time,))
+ raise DKIMVerificationError("DKIM-Signature expired: %d seconds" % (diff_time,))
+
+ # Some useful bits
+ self.hash_method = DKIMUtils.hashlib_method(self.dkim_tags["a"])
+ self.key_methods = self.dkim_tags["q"].split(":")
+
+
+ def extractSignedHeaders(self):
+ """
+ Extract the set of headers from the request that are supposed to be signed. Canonicalize them
+ and return the expected signed data.
+ """
+
+ # Extract all the expected signed headers taking into account the possibility of "over_counting"
+ # headers - a technique used to ensure headers cannot be added in transit
+ header_list = [hdr.strip() for hdr in self.dkim_tags["h"].split(":")]
+ header_counter = collections.defaultdict(int)
+
+ headers = []
+ for header in header_list:
+ actual_headers = self.request.headers.getRawHeaders(header)
+ try:
+ headers.append((header, actual_headers[header_counter[header]],))
+ except IndexError:
+ pass
+ header_counter[header] += 1
+
+ # DKIM-Signature is always included at the end
+ headers.append(("DKIM-Signature", self.request.headers.getRawHeaders("DKIM-Signature")[0],))
+
+ # Now canonicalize the values
+ return "".join([DKIMUtils.canonicalizeHeader(name, value, remove_b=self.dkim_tags["b"]) for name, value in headers])
+
+
+ @inlineCallbacks
+ def locatePublicKey(self):
+ """
+ Try to lookup the public key matching the signature.
+ """
+
+ for lookup in self.key_lookup_methods:
+ if lookup.method in self.key_methods:
+ pubkey = (yield lookup(self.dkim_tags).getPublicKey())
+ if pubkey is not None:
+ returnValue(pubkey)
+ else:
+ returnValue(None)
+
+
+class PublicKeyLookup(object):
+ """
+ Abstract base class for public key lookup methods
+ """
+
+ keyCache = {}
+ method = None
+
+ def __init__(self, dkim_tags):
+ self.dkim_tags = dkim_tags
+
+
+ @inlineCallbacks
+ def getPublicKey(self, useCache=True):
+ """
+ Get key from cache or directly do query.
+
+ @param useCache: whether or not to use the cache
+ @type useCache: C{bool}
+ """
+ key = self.getSelectorKey()
+ if key not in PublicKeyLookup.keyCache or not useCache:
+ pubkeys = (yield self.lookupKeys())
+ PublicKeyLookup.keyCache[key] = pubkeys
+
+ returnValue(self.selectKey())
+
+
+ def getSelectorKey(self):
+ """
+ Get a token used to uniquely identify the key being looked up. Token format will
+ depend on the lookup method.
+ """
+ raise NotImplementedError
+
+
+ def lookupKeys(self):
+ """
+ Do the key lookup using the actual lookup method. Return a C{list} of C{dict}
+ that contains the key tag-list. Return a L{Deferred}.
+ """
+ raise NotImplementedError
+
+
+ def selectKey(self):
+ """
+ Select a specific key from the list that best matches the DKIM-Signature tags
+ """
+
+ pubkeys = PublicKeyLookup.keyCache.get(self.getSelectorKey(), [])
+ for pkey in pubkeys:
+ # Check validity
+ if pkey.get("v", "DKIM1") != "DKIM1":
+ continue
+
+ # Check key type
+ if pkey.get("k", "rsa") != "rsa":
+ continue
+
+ # Check valid hash algorithms
+ hashes = set([hash.strip() for hash in pkey.get("h", "sha1:sha256").split(":")])
+ if self.dkim_tags["a"][4:] not in hashes:
+ continue
+
+ # Service type
+ if pkey.get("s", "ischedule") not in ("*", "ischedule",):
+ continue
+
+ # Non-revoked key
+ if len(pkey.get("p", "")) == 0:
+ continue
+
+ return self.makeKey(pkey)
+
+ log.debug("DKIM: No valid public key: %s %s" % (self.getSelectorKey(), pubkeys,))
+ return None
+
+
+ def makeKey(self, pkey):
+ """
+ Turn the key tag list into an actual RSA public key object
+
+ @param pkey: key tag list
+ @type pkey: C{list}
+ """
+ key_data = """-----BEGIN PUBLIC KEY-----
+%s
+-----END PUBLIC KEY-----
+""" % (textwrap.wrap(pkey["p"], 64),)
+
+ try:
+ return rsa.PublicKey.load_pkcs1(key_data)
+ except:
+ log.debug("DKIM: Unable to make public key:\n%s" % (key_data,))
+ return None
+
+
+ def flushCache(self):
+ PublicKeyLookup.keyCache = {}
+
+
+
+class PublicKeyLookup_DNSTXT(PublicKeyLookup):
+
+
+ method = "dns/txt"
+
+
+ def getSelectorKey(self):
+ """
+ Get a token used to uniquely identify the key being looked up. Token format will
+ depend on the lookup method.
+ """
+ return "%s._domainkey.%s" % (self.dkim_tags["s"], self.dkim_tags["d"],)
+
+
+ def lookupKeys(self):
+ """
+ Do the key lookup using the actual lookup method.
+ """
+ raise NotImplementedError
+
+
+
+class PublicKeyLookup_HTTP_WellKnown(PublicKeyLookup):
+
+
+ method = "http-well-known"
+
+
+ def getSelectorKey(self):
+ """
+ Get a token used to uniquely identify the key being looked up. Token format will
+ depend on the lookup method.
+ """
+ return "https://%s/.well-known/domainkey/%s" % (self.dkim_tags["d"], self.dkim_tags["s"],)
+
+
+ def lookupKeys(self):
+ """
+ Do the key lookup using the actual lookup method.
+ """
+ raise NotImplementedError
+
Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/resource.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,228 @@
+##
+# Copyright (c) 2005-2012 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 pycalendar.datetime import PyCalendarDateTime
+from pycalendar.timezone import PyCalendarTimezone
+from twext.web2 import responsecode
+from twext.web2.dav.noneprops import NonePropertyStore
+from twext.web2.http import Response, HTTPError, StatusResponse, XMLResponse
+from twext.web2.http_headers import MimeType
+from twisted.internet.defer import succeed, returnValue, inlineCallbacks
+from twistedcaldav import caldavxml
+from twistedcaldav.config import config
+from twistedcaldav.extensions import DAVResource
+from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
+from twistedcaldav.schedule import deliverSchedulePrivilegeSet
+from twistedcaldav.scheduling.scheduler import IScheduleScheduler
+from txdav.xml import element as davxml
+import twistedcaldav.scheduling.ischedule.xml as ischedulexml
+
+class IScheduleInboxResource (ReadOnlyNoCopyResourceMixIn, DAVResource):
+ """
+ iSchedule Inbox resource.
+
+ Extends L{DAVResource} to provide iSchedule inbox functionality.
+ """
+
+ def __init__(self, parent, store):
+ """
+ @param parent: the parent resource of this one.
+ """
+ assert parent is not None
+
+ DAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+ self.parent = parent
+ self._newStore = store
+
+ def deadProperties(self):
+ if not hasattr(self, "_dead_properties"):
+ self._dead_properties = NonePropertyStore(self)
+ return self._dead_properties
+
+ def etag(self):
+ return succeed(None)
+
+ def checkPreconditions(self, request):
+ return None
+
+ def resourceType(self):
+ return davxml.ResourceType.ischeduleinbox
+
+ def contentType(self):
+ return MimeType.fromString("text/html; charset=utf-8");
+
+ def isCollection(self):
+ return False
+
+ def isCalendarCollection(self):
+ return False
+
+ def isPseudoCalendarCollection(self):
+ return False
+
+ def principalForCalendarUserAddress(self, address):
+ for principalCollection in self.principalCollections():
+ principal = principalCollection.principalForCalendarUserAddress(address)
+ if principal is not None:
+ return principal
+ return None
+
+ def render(self, request):
+ output = """<html>
+<head>
+<title>Server To Server Inbox Resource</title>
+</head>
+<body>
+<h1>Server To Server Inbox Resource.</h1>
+</body
+</html>"""
+
+ response = Response(200, {}, output)
+ response.headers.setHeader("content-type", MimeType("text", "html"))
+ return response
+
+ def http_GET(self, request):
+ """
+ The iSchedule GET method.
+ """
+
+ if not request.args:
+ # Do normal GET behavior
+ return self.render(request)
+
+ query = request.args.get("query", ("",))
+ if len(query) != 1:
+ raise HTTPError(StatusResponse(
+ responsecode.BAD_REQUEST,
+ "Invalid query parameter",
+ ))
+ query = query[0]
+
+ query = {
+ "capabilities" : self.doCapabilities,
+ }.get(query, None)
+
+ if query is None:
+ raise HTTPError(StatusResponse(
+ responsecode.BAD_REQUEST,
+ "Unknown query query parameter",
+ ))
+
+ return query(request)
+
+ def doCapabilities(self, request):
+ """
+ Return a list of all timezones known to the server.
+ """
+
+ # Determine min/max date-time for iSchedule
+ now = PyCalendarDateTime.getNowUTC()
+ minDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
+ minDateTime.offsetYear(-1)
+ maxDateTime = PyCalendarDateTime(now.getYear(), 1, 1, 0, 0, 0, PyCalendarTimezone(utc=True))
+ maxDateTime.offsetYear(10)
+
+ result = ischedulexml.QueryResult(
+
+ ischedulexml.CapabilitySet(
+ ischedulexml.SupportedVersionSet(
+ ischedulexml.Version.fromString("1.0"),
+ ),
+ ischedulexml.SupportedSchedulingMessageSet(
+ ischedulexml.Comp(
+ ischedulexml.Method(name="REQUEST"),
+ ischedulexml.Method(name="CANCEL"),
+ ischedulexml.Method(name="REPLY"),
+ name="VEVENT"
+ ),
+ ischedulexml.Comp(
+ ischedulexml.Method(name="REQUEST"),
+ ischedulexml.Method(name="CANCEL"),
+ ischedulexml.Method(name="REPLY"),
+ name="VTODO"
+ ),
+ ischedulexml.Comp(
+ ischedulexml.Method(name="REQUEST"),
+ name="VFREEBUSY"
+ ),
+ ),
+ ischedulexml.SupportedCalendarDataType(
+ ischedulexml.CalendarDataType(**{
+ "content-type":"text/calendar",
+ "version":"2.0",
+ }),
+ ),
+ ischedulexml.SupportedAttachmentValues(
+ ischedulexml.ExternalAttachment(),
+ ),
+ ischedulexml.SupportedRecipientURISchemeSet(
+ ischedulexml.Scheme.fromString("mailto")
+ ),
+ ischedulexml.MaxContentLength.fromString(config.MaxResourceSize),
+ ischedulexml.MinDateTime.fromString(minDateTime.getText()),
+ ischedulexml.MaxDateTime.fromString(maxDateTime.getText()),
+ ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances),
+ ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance),
+ ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")),
+ ),
+ )
+ return XMLResponse(responsecode.OK, result)
+
+ @inlineCallbacks
+ def http_POST(self, request):
+ """
+ The server-to-server POST method.
+ """
+
+ # This is a server-to-server scheduling operation.
+ scheduler = IScheduleScheduler(request, self)
+
+ # Need a transaction to work with
+ txn = self._newStore.newTransaction("new transaction for Server To Server Inbox Resource")
+ request._newStoreTransaction = txn
+
+ # Do the POST processing treating this as a non-local schedule
+ try:
+ result = (yield scheduler.doSchedulingViaPOST(txn, use_request_headers=True))
+ except Exception, e:
+ yield txn.abort()
+ raise e
+ else:
+ yield txn.commit()
+ returnValue(result.response())
+
+ ##
+ # ACL
+ ##
+
+ def supportedPrivileges(self, request):
+ return succeed(deliverSchedulePrivilegeSet)
+
+ def defaultAccessControlList(self):
+ privs = (
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(caldavxml.ScheduleDeliver()),
+ )
+
+ return davxml.ACL(
+ # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
+ davxml.ACE(
+ davxml.Principal(davxml.All()),
+ davxml.Grant(*privs),
+ davxml.Protected(),
+ ),
+ )
Copied: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/servers.py (from rev 9747, CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/servers.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/servers.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,191 @@
+##
+# Copyright (c) 2006-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 twext.python.filepath import CachingFilePath as FilePath
+
+from twext.python.log import Logger
+
+from twistedcaldav.config import config, fullServerPath
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav import xmlutil
+
+"""
+XML based iSchedule configuration file handling.
+"""
+
+__all__ = [
+ "IScheduleServers",
+]
+
+log = Logger()
+
+class IScheduleServers(object):
+
+ _fileInfo = None
+ _xmlFile = None
+ _servers = None
+ _domainMap = None
+
+ def __init__(self):
+
+ self._loadConfig()
+
+ def _loadConfig(self):
+ if IScheduleServers._servers is None:
+ IScheduleServers._xmlFile = FilePath(
+ fullServerPath(
+ config.ConfigRoot,
+ config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"]
+ )
+ )
+ IScheduleServers._xmlFile.restat()
+ fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
+ if fileInfo != IScheduleServers._fileInfo:
+ parser = IScheduleServersParser(IScheduleServers._xmlFile)
+ IScheduleServers._servers = parser.servers
+ self._mapDomains()
+ IScheduleServers._fileInfo = fileInfo
+
+ def _mapDomains(self):
+ IScheduleServers._domainMap = {}
+ for server in IScheduleServers._servers:
+ for domain in server.domains:
+ IScheduleServers._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 IScheduleServers._domainMap.get(domain)
+
+ELEMENT_SERVERS = "servers"
+ELEMENT_SERVER = "server"
+ELEMENT_URI = "uri"
+ELEMENT_AUTHENTICATION = "authentication"
+ATTRIBUTE_TYPE = "type"
+ATTRIBUTE_BASICAUTH = "basic"
+ELEMENT_USER = "user"
+ELEMENT_PASSWORD = "password"
+ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
+ELEMENT_DOMAINS = "domains"
+ELEMENT_DOMAIN = "domain"
+ELEMENT_CLIENT_HOSTS = "hosts"
+ELEMENT_HOST = "host"
+
+class IScheduleServersParser(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
+ _ignore_etree, servers_node = xmlutil.readXML(xmlFile.path, ELEMENT_SERVERS)
+ 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.getchildren():
+ if child.tag == ELEMENT_SERVER:
+ self.servers.append(IScheduleServerRecord())
+ self.servers[-1].parseXML(child)
+
+class IScheduleServerRecord (object):
+ """
+ Contains server-to-server details.
+ """
+ def __init__(self, uri=None):
+ """
+ @param recordType: record type for directory entry.
+ """
+ self.uri = ""
+ self.authentication = None
+ self.allow_from = False
+ self.allow_to = True
+ self.domains = []
+ self.client_hosts = []
+ self.unNormalizeAddresses = True
+ self.moreHeaders = []
+
+ if uri:
+ self.uri = uri
+ self._parseDetails()
+
+ def parseXML(self, node):
+ for child in node.getchildren():
+ if child.tag == ELEMENT_URI:
+ self.uri = child.text
+ elif child.tag == ELEMENT_AUTHENTICATION:
+ self._parseAuthentication(child)
+ elif child.tag == ELEMENT_ALLOW_REQUESTS_FROM:
+ self.allow_from = True
+ elif child.tag == ELEMENT_ALLOW_REQUESTS_TO:
+ self.allow_to = True
+ elif child.tag == ELEMENT_DOMAINS:
+ self._parseList(child, ELEMENT_DOMAIN, self.domains)
+ elif child.tag == ELEMENT_CLIENT_HOSTS:
+ self._parseList(child, ELEMENT_HOST, self.client_hosts)
+ else:
+ raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child.tag,))
+
+ self._parseDetails()
+
+ def _parseList(self, node, element_name, appendto):
+ for child in node.getchildren():
+ if child.tag == element_name:
+ appendto.append(child.text)
+
+ def _parseAuthentication(self, node):
+ if node.get(ATTRIBUTE_TYPE) != ATTRIBUTE_BASICAUTH:
+ return
+
+ for child in node.getchildren():
+ if child.tag == ELEMENT_USER:
+ user = child.text
+ elif child.tag == ELEMENT_PASSWORD:
+ password = child.text
+
+ self.authentication = ("basic", user, password,)
+
+ 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]
Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/__init__.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2012 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.
+##
Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/test/test_dkim.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,741 @@
+##
+# Copyright (c) 2012 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 twext.web2.http_headers import Headers, MimeType
+from twext.web2.stream import MemoryStream
+from twisted.internet.defer import inlineCallbacks, succeed
+from twistedcaldav.scheduling.ischedule.dkim import DKIMRequest, DKIMVerifier,\
+ DKIMVerificationError, DKIMUtils, PublicKeyLookup_DNSTXT,\
+ PublicKeyLookup_HTTP_WellKnown
+import base64
+import hashlib
+import rsa
+import time
+import twistedcaldav.test.util
+
+class TestDKIMBase (twistedcaldav.test.util.TestCase):
+ """
+ DKIM support tests
+ """
+
+ class PublicKeyLookup_Testing(PublicKeyLookup_HTTP_WellKnown):
+
+ keys = []
+
+ def lookupKeys(self):
+ """
+ Do the key lookup using the actual lookup method.
+ """
+ return succeed(self.keys)
+
+
+ def setUp(self):
+ super(TestDKIMBase, self).setUp()
+
+ self.private_keyfile = self.mktemp()
+ f = open(self.private_keyfile, "w")
+ f.write("""-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAw7bJxD1k5VSA5AqdfmJ7vj99oKQ4qYtSeJ5HiK6W40dzC++k
+LweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yPdVqpWUo6s2jnUsSWOfhpre22yc4B
+K0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6f0OUiKi17jEPasKcxf1qZrWU0+Ik
+D2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1hE/Eso0gS79FSti1fkDeoPZ296Gu
+5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y9xYGLWubic0dUeWuhUipZdmQf8Bd
+t7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtujQIDAQABAoIBADfFuzHFHR+NOT3D
+GKaPghvxE+fXZJ5MKbBdypzUxAL4tXxNSkhsrIWtLN1MuSvbYYxEfmZNzYhrB3w1
+Oy1ieq9CqsfbM2c1GdaoVvcmJ1d9Sn2vyv19ZmcdBRKulIycKcgL0t+bEEDXTtjX
+beOmm8XwiD95dH7wVChkVTDGyq+BxtSY6wav9y15zWnBH7+BAeq3OnKaNIQB0iTI
+UA41jWocKYI18/6D5gQTDSoYvKB7saFVGw9IgmmHA/3rYztcHCxUoE15x7wWuwtF
+vzQanEt/QwEEFMibNTjvfIUPoeIeQH7MzcD56AL9u/cs8LNeSbappWE7BneQ0ll3
+CfTsAQECgYEA/eoDkpPMWxuoSrZ1oXLxeImEAB2EHNs4UV9dmcUkhNeYZP0rv7pL
+4jpkNHTRvFeeovy5khXhykb9BUYDuZy6rcELlFxpCKYRw3d+PPWM+wfqmJp+fIN7
+Z4F1Kpznt0F2e+9LXF1Qi5bM2dHy1maxEjaBUIOIoczbjJJDmNN8zR0CgYEAxVJg
+2VCpjaRoJtaZYeserkVgB8SFffBnm/8XQv8uTbKrz104t9nFyezbINmKrQs3cxT3
+1+PiVbLJpPRcik129x4xIlz3zapsMqwXL97Lz92vXm/nELRnV8d+F9SxVzlijRDL
+rvl3X3Vayq2zKb6euBOwOu8UnQO3xJkTtLPtHDECgYAptxuVJkEJqtaQR7+1oZu4
+UOdl2XOOBhoPjFplW/Uu+fiohst8OVAkP7GcyKB4j/CZGGoobP3mbJk/F4yfHvew
+eim72x7Kc/YxJd2QiEr8JwXMwn0LWdKZY7RrJtIO0mtz2xGHgDEubb0EADEkNkTb
+GCdQoft9kZl0U8dVQVGcpQKBgHsvjIre0ps8slDc1HDO6h597Q+sXnJbLTO0Mv9+
+c5fKHXydhBUy/UmsdrixVuPlBr7vrjK3b8t0jHJQo50r80MfNClxxLo+1MFlsiwO
+eUrR6POaBLTnC0U/o7aY8AW2K5JJk/8uepm7l+zEN/+to0Tj9bc1HrdPZOB1eFnt
+oe9hAoGAEwwDhNrmSlZjmZMT8WehCdyS7zQgI8OCLAlu9KTiwFzoSDcnhVbS4nd4
+iblllHCLZ2Q/rHSH3cQor94kxePm+b3KH9ZwAgInMModuSPcScrR5/vsORZCtJEO
+CAXnxZHhrExMGIIa7KV33W5v7Hstl7SnPWKFgCvlBH2QoMTjoUE=
+-----END RSA PRIVATE KEY-----
+""")
+ f.close()
+
+ pkey_data = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw7bJxD1k5VSA5AqdfmJ7
+vj99oKQ4qYtSeJ5HiK6W40dzC++kLweUWLzeUErgXwcJlyOC6rqVVPBfSJq4l7yP
+dVqpWUo6s2jnUsSWOfhpre22yc4BK0QY2Euc3R+gT59eM0mtJPtWaQw5BmQ2GrV6
+f0OUiKi17jEPasKcxf1qZrWU0+IkD2DhUCuRrNb/baUkuIkxoit6M7k7s5X9swT1
+hE/Eso0gS79FSti1fkDeoPZ296Gu5uYWdpaLl03Nr0w65Gbw+2v79AcwOyvbZD6y
+9xYGLWubic0dUeWuhUipZdmQf8Bdt7cZVgjQX/giQQqqLDFhfNFwapUZDhS7TCtu
+jQIDAQAB
+"""
+ self.public_keyfile = self.mktemp()
+ f = open(self.public_keyfile, "w")
+ f.write("""-----BEGIN PUBLIC KEY-----
+%s-----END PUBLIC KEY-----
+""" % (pkey_data,))
+ f.close()
+ self.public_key_data = pkey_data.replace("\n", "")
+
+class TestDKIMRequest (TestDKIMBase):
+ """
+ L{DKIMRequest} support tests.
+ """
+
+ def test_valid_request(self):
+
+ stream = MemoryStream("")
+ request = DKIMRequest("POST", "/", {}, stream, "example.com", "dkim", "/tmp/key", "rsa-sha1", ("Originator", "Recipient",), True, True, 3600)
+
+
+ @inlineCallbacks
+ def test_body_hash(self):
+
+ data = "Hello World!"
+ for algorithm, hash_method in (
+ ("rsa-sha1", hashlib.sha1,),
+ ("rsa-sha256", hashlib.sha256,),
+ ):
+ stream = MemoryStream(data)
+ headers = Headers()
+ headers.addRawHeader("Originator", "mailto:user01 at example.com")
+ headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+ headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component":"VEVENT", "charset":"utf-8"}))
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", "/tmp/key", algorithm, ("Originator", "Recipient", "Content-Type",), True, True, 3600)
+ hash = base64.b64encode(hash_method(data).digest())
+ result = (yield request.bodyHash())
+ self.assertEqual(result, hash)
+
+
+ def test_generateSignature(self):
+
+ data = "Hello World!"
+
+ for algorithm, hash_method, hash_name in (
+ ("rsa-sha1", hashlib.sha1, "SHA-1",),
+ ("rsa-sha256", hashlib.sha256, "SHA-256"),
+ ):
+ stream = MemoryStream(data)
+ headers = Headers()
+ headers.addRawHeader("Originator", "mailto:user01 at example.com")
+ headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+ headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component":"VEVENT", "charset":"utf-8"}))
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, 3600)
+
+ # Manually create what should be the correct thing to sign
+ bodyhash = base64.b64encode(hash_method(data).digest())
+ sign_this = """originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com
+content-type:%s
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient; bh=%s; b=
+""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], str(int(time.time())), str(int(time.time() + 3600)), algorithm, bodyhash)
+
+ result = request.generateSignature(sign_this)
+
+ key = rsa.PrivateKey.load_pkcs1(open(self.private_keyfile).read())
+ signature = base64.b64encode(rsa.sign(sign_this, key, hash_name))
+
+ self.assertEqual(result, signature)
+
+
+ @inlineCallbacks
+ def test_signatureHeaders(self):
+
+ data = "Hello World!"
+
+ for algorithm, hash_method in (
+ ("rsa-sha1", hashlib.sha1,),
+ ("rsa-sha256", hashlib.sha256,),
+ ):
+ stream = MemoryStream(data)
+ headers = Headers()
+ headers.addRawHeader("Originator", "mailto:user01 at example.com")
+ headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+ headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component":"VEVENT", "charset":"utf-8"}))
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, 3600)
+ result, _ignore_tags = (yield request.signatureHeaders())
+
+ # Manually create what should be the correct thing to sign
+ bodyhash = base64.b64encode(hash_method(data).digest())
+ sign_this = """originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com
+content-type:%s
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient:Content-Type; bh=%s; b=
+""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.time, request.expire, algorithm, bodyhash)
+
+ self.assertEqual(result, sign_this)
+
+
+ @inlineCallbacks
+ def test_sign(self):
+
+ data = "Hello World!"
+ for algorithm, hash_method, hash_name in (
+ ("rsa-sha1", hashlib.sha1, "SHA-1",),
+ ("rsa-sha256", hashlib.sha256, "SHA-256"),
+ ):
+ stream = MemoryStream(data)
+ headers = Headers()
+ headers.addRawHeader("Originator", "mailto:user01 at example.com")
+ headers.addRawHeader("Recipient", "mailto:user02 at example.com")
+ headers.setHeader("Content-Type", MimeType("text", "calendar", **{"component":"VEVENT", "charset":"utf-8"}))
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, ("Originator", "Recipient", "Content-Type",), True, True, 3600)
+ result = (yield request.sign())
+
+ # Manually create what should be the correct thing to sign and make sure signatures match
+ bodyhash = base64.b64encode(hash_method(data).digest())
+ sign_this = """originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com
+content-type:%s
+dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient:Content-Type; bh=%s; b=
+""".replace("\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.time, request.expire, algorithm, bodyhash)
+ key = rsa.PrivateKey.load_pkcs1(open(self.private_keyfile).read())
+ signature = base64.b64encode(rsa.sign(sign_this, key, hash_name))
+
+ self.assertEqual(result, signature)
+
+ # Make sure header is updated in the request
+ updated_header = "v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient:Content-Type; bh=%s; b=%s" % (request.time, request.expire, algorithm, bodyhash, signature,)
+ self.assertEqual(request.headers.getRawHeaders("DKIM-Signature")[0], updated_header)
+
+ # Try to verify result using public key
+ pubkey = rsa.PublicKey.load_pkcs1(open(self.public_keyfile).read())
+ self.assertEqual(rsa.verify(sign_this, base64.b64decode(result), pubkey), None)
+
+
+class TestDKIMVerifier (TestDKIMBase):
+ """
+ L{DKIMVerifier} support tests.
+ """
+
+ class StubRequest(object):
+
+ def __init__(self, headers, body):
+ self.headers = Headers()
+ for name, value in headers:
+ self.headers.addRawHeader(name, value)
+ self.stream = MemoryStream(body)
+
+
+ def test_valid_dkim_headers(self):
+ """
+ L{DKIMVerifier.processDKIMHeader} correctly validates DKIM-Signature headers.
+ """
+
+ data = (
+ # Bogus
+ ((("DKIM-Signature", "v=1"),), False,),
+
+ # More than one
+ ((
+ ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
+ ("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),
+ ), False,),
+
+ # Valid
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), True,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha256; q=dns/txt; c=relaxed; h=Originator:Recipient; bh=abc; b=def"),), True,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() + 30),)),), True,),
+
+ # Invalid
+ ((("DKIM-Signature", "v=2; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha512; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; a=rsa-sha1; q=dns/txt:http-well-known; c=relaxed/relaxed; h=Originator:Recipient; bh=abc; b=def"),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; t=1234; a=rsa-sha1; q=dns/txt:http-well-known; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def"),), False,),
+ ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,),
+ )
+
+ for headers, result in data:
+ request = self.StubRequest(headers, "")
+ verifier = DKIMVerifier(request)
+ if result:
+ verifier.processDKIMHeader()
+ else:
+ self.assertRaises(DKIMVerificationError, verifier.processDKIMHeader)
+
+
+ def test_canonicalize_header(self):
+ """
+ L{DKIMVerifier.canonicalizeHeader} correctly canonicalizes headers.
+ """
+
+ data = (
+ ("Content-Type", " text/calendar ; charset = \"utf-8\" ", "content-type:text/calendar ; charset = \"utf-8\"\r\n"),
+ ("Originator", " mailto:user01 at example.com ", "originator:mailto:user01 at example.com\r\n"),
+ ("Recipient", " mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t ", "recipient:mailto:user02 at example.com , mailto:user03 at example.com\r\n"),
+ (
+ "DKIM-Signature",
+ " v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http-well-known\t\t; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=def",
+ "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Originator:Recipient; bh=abc; b=\r\n",
+ ),
+ (
+ "DKIM-Signature",
+ " v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http-well-known\t\t; b= def ; c=relaxed/simple; h=Originator:Recipient; bh=abc",
+ "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; b= ; c=relaxed/simple; h=Originator:Recipient; bh=abc\r\n",
+ ),
+ )
+
+ for name, value, result in data:
+ request = self.StubRequest(((name, value,),), "")
+ verifier = DKIMVerifier(request)
+ if name == "DKIM-Signature":
+ verifier.processDKIMHeader()
+ canonicalized = DKIMUtils.canonicalizeHeader(name, value, remove_b=verifier.dkim_tags["b"] if name == "DKIM-Signature" else None)
+ self.assertEqual(canonicalized, result)
+
+
+ def test_extract_headers(self):
+ """
+ L{DKIMVerifier.extractSignedHeaders} correctly extracts canonicalizes headers.
+ """
+
+ data = (
+ # Over count on Recipient
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http-well-known\t\t; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ """content-type:text/calendar ; charset = "utf-8"
+originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com , mailto:user03 at example.com
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=
+"""
+ ),
+ # Exact count on Recipient
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Recipient:\t\t mailto:user04 at example.com
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http-well-known\t\t; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ """content-type:text/calendar ; charset = "utf-8"
+originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com , mailto:user03 at example.com
+recipient:mailto:user04 at example.com
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=
+"""
+ ),
+ # Under count on Recipient
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Recipient:\t\t mailto:user04 at example.com
+Recipient:\t\t mailto:user05 at example.com
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http-well-known\t\t; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ """content-type:text/calendar ; charset = "utf-8"
+originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com , mailto:user03 at example.com
+recipient:mailto:user04 at example.com
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=
+"""
+ ),
+ # Re-ordered Content-Type
+ ("""Host:example.com
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http-well-known\t\t; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=def
+Content-Type: text/calendar ; charset = "utf-8"
+Cache-Control:no-cache
+Connection:close
+""",
+ """content-type:text/calendar ; charset = "utf-8"
+originator:mailto:user01 at example.com
+recipient:mailto:user02 at example.com , mailto:user03 at example.com
+dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=
+"""
+ ),
+ )
+
+ for hdrs, result in data:
+ headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
+ request = self.StubRequest(headers, "")
+ verifier = DKIMVerifier(request)
+ verifier.processDKIMHeader()
+ extracted = verifier.extractSignedHeaders()
+ self.assertEqual(extracted, result.replace("\n", "\r\n"))
+
+
+ def test_locate_public_key(self):
+ """
+ L{DKIMVerifier.locatePublicKey} correctly finds key matching headers.
+ """
+
+ data = (
+ # Valid
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http-well-known\t\t; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ True,
+ ),
+ # Invalid - no method
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt\t\t; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ ),
+ # Invalid - wrong algorithm
+ ("""Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+DKIM-Signature: v=1;\t\t d=example.com; s = dkim; t\t=\t1234; a=rsa-sha1; \t\tq=dns/txt:http-well-known\t\t; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b=def
+Cache-Control:no-cache
+Connection:close
+""",
+ [DKIMUtils.extractTags("v=DKIM1; h=sha-1; p=%s" % (self.public_key_data,))],
+ False,
+ ),
+ )
+
+ for hdrs, keys, result in data:
+ headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()]
+ request = self.StubRequest(headers, "")
+ TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
+ verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
+ verifier.processDKIMHeader()
+ pkey = (yield verifier.locatePublicKey())
+ if result:
+ self.assertNotEqual(pkey, None)
+ else:
+ self.assertEqual(pkey, None)
+
+
+ @inlineCallbacks
+ def test_verify(self):
+ """
+ L{DKIMVerifier.verify} correctly finds key matching headers.
+ """
+
+ @inlineCallbacks
+ def _verify(hdrs, body, keys, result, sign_headers=("Originator", "Recipient", "Content-Type",), manipulate_request=None):
+ for algorithm in ("rsa-sha1", "rsa-sha256",):
+ # Create signature
+ stream = MemoryStream(body)
+ headers = Headers()
+ for name, value in [hdr.split(":", 1) for hdr in hdrs.splitlines()]:
+ headers.addRawHeader(name, value)
+ request = DKIMRequest("POST", "/", headers, stream, "example.com", "dkim", self.private_keyfile, algorithm, sign_headers, True, True, 3600)
+ yield request.sign()
+
+ # Possibly munge the request after the signature is done
+ if manipulate_request is not None:
+ manipulate_request(request)
+
+ # Verify signature
+ TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys
+ verifier = DKIMVerifier(request, key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,))
+ TestPublicKeyLookup.PublicKeyLookup_Testing({}).flushCache()
+ try:
+ yield verifier.verify()
+ except Exception, e:
+ if result:
+ self.fail("DKIMVerifier:verify failed: %s" % (e,))
+ else:
+ if not result:
+ self.fail("DKIMVerifier:verify did not fail")
+
+ # Valid
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ True,
+ )
+
+ # Invalid - key revoked
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=")],
+ False,
+ )
+
+ # Invalid - missing header
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ manipulate_request=lambda request:request.headers.removeHeader("Originator")
+ )
+
+ # Invalid - changed header
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ manipulate_request=lambda request:request.headers.setRawHeaders("Originator", ("mailto:user04 at example.com",))
+ )
+
+ # Invalid - changed body
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ manipulate_request=lambda request:setattr(request, "stream", MemoryStream("BEGIN:DATA\n")),
+ )
+
+ # Valid - extra header no over sign
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ True,
+ manipulate_request=lambda request:request.headers.addRawHeader("Recipient", ("mailto:user04 at example.com",))
+ )
+
+ # Valid - over sign header
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
+ )
+
+ # Invalid - over sign header extra header
+ yield _verify(
+ """Host:example.com
+Content-Type: text/calendar ; charset = "utf-8"
+Originator: mailto:user01 at example.com
+Recipient: mailto:user02 at example.com ,\t mailto:user03 at example.com\t\t
+Cache-Control:no-cache
+Connection:close
+""",
+ """BEGIN:DATA
+END:DATA
+""",
+ [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))],
+ False,
+ sign_headers=("Originator", "Recipient", "Recipient", "Content-Type",),
+ manipulate_request=lambda request:request.headers.addRawHeader("Recipient", ("mailto:user04 at example.com",))
+ )
+
+
+
+class TestPublicKeyLookup (TestDKIMBase):
+ """
+ L{PublicKeyLookup} support tests.
+ """
+
+ def test_selector_key(self):
+
+ for lookup, result in (
+ (PublicKeyLookup_DNSTXT, "dkim._domainkey.example.com"),
+ (PublicKeyLookup_HTTP_WellKnown, "https://example.com/.well-known/domainkey/dkim")
+ ):
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ tester = lookup(DKIMUtils.extractTags(dkim))
+ self.assertEqual(tester.getSelectorKey(), result)
+
+
+ @inlineCallbacks
+ def test_get_key(self):
+
+ # Valid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertNotEqual(pubkey, None)
+
+ # Valid with more tags
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=ischedule ; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertNotEqual(pubkey, None)
+
+ # Invalid - key type
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=dsa ; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertEqual(pubkey, None)
+
+ # Invalid - hash
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; h=sha512 ; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertEqual(pubkey, None)
+
+ # Invalid - service
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertEqual(pubkey, None)
+
+ # Invalid - revoked
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p=")]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertEqual(pubkey, None)
+
+ # Multiple valid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [
+ DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
+ DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=ischedule ; p=%s" % (self.public_key_data,)),
+ DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=* ; p=%s" % (self.public_key_data,)),
+ ]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertNotEqual(pubkey, None)
+
+ # Multiple - some valid, some invalid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [
+ DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
+ DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,)),
+ DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=ischedule ; p=%s" % (self.public_key_data,)),
+ DKIMUtils.extractTags("v=DKIM1; k = rsa ; h= sha1 : sha256 ; s=* ; p=%s" % (self.public_key_data,)),
+ ]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertNotEqual(pubkey, None)
+
+ # Multiple - invalid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [
+ DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
+ DKIMUtils.extractTags("v=DKIM1; k=rsa ; s=email ; p="),
+ ]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertEqual(pubkey, None)
+
+
+ @inlineCallbacks
+ def test_cached_key(self):
+
+ # Create cache entry
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))]
+ pubkey = (yield lookup.getPublicKey())
+ self.assertNotEqual(pubkey, None)
+
+ # Cache valid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.keys = []
+ pubkey = (yield lookup.getPublicKey())
+ self.assertNotEqual(pubkey, None)
+
+ # Cache invalid
+ dkim = "v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http-well-known ; c=relaxed/simple; h=Content-Type:Originator:Recipient:Recipient; bh=abc; b="
+ lookup = TestPublicKeyLookup.PublicKeyLookup_Testing(DKIMUtils.extractTags(dkim))
+ lookup.flushCache()
+ lookup.keys = []
+ pubkey = (yield lookup.getPublicKey())
+ self.assertEqual(pubkey, None)
Added: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule/xml.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -0,0 +1,191 @@
+##
+# Copyright (c) 2011-2012 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.
+##
+
+"""
+This module provides XML definitions for use with Timezone Standard Service.
+"""
+
+from txdav.xml.element import registerElement
+from txdav.xml.element import WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
+
+
+##
+# iSchedule XML Definitions
+##
+
+ischedule_namespace = "urn:ietf:params:xml:ns:ischedule"
+
+
+ at registerElement
+class QueryResult (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "query-result"
+ allowed_children = {
+ (ischedule_namespace, "capability-set"): (0, None),
+ }
+
+
+ at registerElement
+class CapabilitySet (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "capability-set"
+ allowed_children = {
+ (ischedule_namespace, "supported-version-set"): (1, 1),
+ (ischedule_namespace, "supported-scheduling-message-set"): (1, 1),
+ (ischedule_namespace, "supported-calendar-data-type"): (1, 1),
+ (ischedule_namespace, "supported-attachment-values"): (1, 1),
+ (ischedule_namespace, "supported-recipient-uri-scheme-set"): (1, 1),
+ (ischedule_namespace, "max-content-length"): (1, 1),
+ (ischedule_namespace, "min-date-time"): (1, 1),
+ (ischedule_namespace, "max-date-time"): (1, 1),
+ (ischedule_namespace, "max-instances"): (1, 1),
+ (ischedule_namespace, "max-recipients"): (1, 1),
+ (ischedule_namespace, "administrator"): (1, 1),
+ }
+
+
+ at registerElement
+class SupportedVersionSet (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "supported-version-set"
+ allowed_children = {
+ (ischedule_namespace, "version"): (1, None),
+ }
+
+
+ at registerElement
+class Version (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "version"
+
+
+ at registerElement
+class SupportedSchedulingMessageSet (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "supported-scheduling-message-set"
+ allowed_children = {
+ (ischedule_namespace, "comp"): (1, None),
+ }
+
+
+ at registerElement
+class Comp (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "comp"
+ allowed_children = {
+ (ischedule_namespace, "method"): (0, None),
+ }
+ allowed_attributes = { "name": True }
+
+
+ at registerElement
+class Method (WebDAVEmptyElement):
+ namespace = ischedule_namespace
+ name = "method"
+ allowed_attributes = { "name": True }
+
+
+ at registerElement
+class SupportedCalendarDataType (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "supported-calendar-data-type"
+ allowed_children = {
+ (ischedule_namespace, "calendar-data-type"): (1, None),
+ }
+
+
+ at registerElement
+class CalendarDataType (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "calendar-data-type"
+ allowed_attributes = {
+ "content-type": True,
+ "version": True,
+ }
+
+
+ at registerElement
+class SupportedAttachmentValues (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "supported-attachment-values"
+ allowed_children = {
+ (ischedule_namespace, "inline-attachment"): (0, 1),
+ (ischedule_namespace, "external-attachment"): (0, 1),
+ }
+
+
+ at registerElement
+class InlineAttachment (WebDAVEmptyElement):
+ namespace = ischedule_namespace
+ name = "inline-attachment"
+
+
+ at registerElement
+class ExternalAttachment (WebDAVEmptyElement):
+ namespace = ischedule_namespace
+ name = "external-attachment"
+
+
+ at registerElement
+class SupportedRecipientURISchemeSet (WebDAVElement):
+ namespace = ischedule_namespace
+ name = "supported-recipient-uri-scheme-set"
+ allowed_children = {
+ (ischedule_namespace, "scheme"): (1, None),
+ }
+
+
+ at registerElement
+class Scheme (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "scheme"
+
+
+ at registerElement
+class MaxContentLength (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "max-content-length"
+
+
+ at registerElement
+class MinDateTime (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "min-date-time"
+
+
+ at registerElement
+class MaxDateTime (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "max-date-time"
+
+
+ at registerElement
+class MaxInstances (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "max-instances"
+
+
+ at registerElement
+class MaxRecipients (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "max-recipients"
+
+
+ at registerElement
+class Administrator (WebDAVTextElement):
+ namespace = ischedule_namespace
+ name = "administrator"
+
Deleted: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule.py 2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischedule.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -1,343 +0,0 @@
-##
-# Copyright (c) 2005-2012 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 StringIO import StringIO
-
-from twisted.internet.defer import inlineCallbacks, DeferredList, succeed
-from twisted.internet.protocol import Factory
-
-from twisted.python.failure import Failure
-
-from twext.web2 import responsecode
-from twext.web2.client.http import ClientRequest
-from twext.web2.client.http import HTTPClientProtocol
-from twext.web2.dav.util import davXMLFromStream, joinURL, allDataFromStream
-from twext.web2.http import HTTPError
-from twext.web2.http_headers import Headers
-from twext.web2.http_headers import MimeType
-from twext.web2.stream import MemoryStream
-
-from twext.python.log import Logger, logLevels
-from twext.web2.dav.http import ErrorResponse
-
-from twistedcaldav.client.pool import _configuredClientContextFactory
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServerRecord
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.util import utf8String, normalizationLookup
-from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser, RemoteCalendarUser,\
- OtherServerCalendarUser
-from twext.internet.gaiendpoint import GAIEndpoint
-
-"""
-Handles the sending of iSchedule scheduling messages. Used for both cross-domain scheduling,
-as well as internal partitioning or podding.
-"""
-
-__all__ = [
- "ScheduleViaISchedule",
-]
-
-log = Logger()
-
-class ScheduleViaISchedule(DeliveryService):
-
- @classmethod
- def serviceType(cls):
- return DeliveryService.serviceType_ischedule
-
- @classmethod
- def matchCalendarUserAddress(cls, cuaddr):
-
- # TODO: here is where we would attempt service discovery based on the cuaddr.
-
- # Do default match
- return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
-
- def generateSchedulingResponses(self, refreshOnly=False):
- """
- 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 = IScheduleServers()
- for recipient in self.recipients:
- if isinstance(recipient, RemoteCalendarUser):
- # Map the recipient's domain to a server
- server = servermgr.mapDomain(recipient.domain)
- elif isinstance(recipient, PartitionedCalendarUser):
- server = self._getServerForPartitionedUser(recipient)
- elif isinstance(recipient, OtherServerCalendarUser):
- server = self._getServerForOtherServerUser(recipient)
- else:
- assert False, "Incorrect calendar user address class"
- if not server:
- # Cannot do server-to-server for this recipient.
- err = HTTPError(ErrorResponse(
- responsecode.NOT_FOUND,
- (caldav_namespace, "recipient-allowed"),
- "No server for recipient",
- ))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
-
- # Process next recipient
- continue
-
- if not server.allow_to:
- # Cannot do server-to-server outgoing requests for this server.
- err = HTTPError(ErrorResponse(
- responsecode.NOT_FOUND,
- (caldav_namespace, "recipient-allowed"),
- "Cannot send to recipient's server",
- ))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
- # Process next recipient
- continue
-
- groups.setdefault(server, []).append(recipient)
-
- if len(groups) == 0:
- 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 = IScheduleRequest(self.scheduler, server, recipients, self.responses, refreshOnly)
- deferreds.append(requestor.doRequest())
-
- return DeferredList(deferreds)
-
- def _getServerForPartitionedUser(self, recipient):
-
- if not hasattr(self, "partitionedServers"):
- self.partitionedServers = {}
-
- partition = recipient.principal.partitionURI()
- if partition not in self.partitionedServers:
- self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
- self.partitionedServers[partition].unNormalizeAddresses = False
- self.partitionedServers[partition].moreHeaders.append(recipient.principal.server().secretHeader())
-
- return self.partitionedServers[partition]
-
- def _getServerForOtherServerUser(self, recipient):
-
- if not hasattr(self, "otherServers"):
- self.otherServers = {}
-
- serverURI = recipient.principal.serverURI()
- 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]
-
-class IScheduleRequest(object):
-
- def __init__(self, scheduler, server, recipients, responses, refreshOnly=False):
-
- self.scheduler = scheduler
- self.server = server
- self.recipients = recipients
- self.responses = responses
- self.refreshOnly = refreshOnly
-
- self._generateHeaders()
- self._prepareData()
-
- @inlineCallbacks
- def doRequest(self):
-
- # Generate an HTTP client request
- try:
- if not hasattr(self.scheduler.request, "extendedLogItems"):
- self.scheduler.request.extendedLogItems = {}
- if "itip.ischedule" not in self.scheduler.request.extendedLogItems:
- self.scheduler.request.extendedLogItems["itip.ischedule"] = 0
- self.scheduler.request.extendedLogItems["itip.ischedule"] += 1
-
- from twisted.internet import reactor
- f = Factory()
- f.protocol = HTTPClientProtocol
- if self.server.ssl:
- ep = GAIEndpoint(reactor, self.server.host, self.server.port,
- _configuredClientContextFactory())
- else:
- ep = GAIEndpoint(reactor, self.server.host, self.server.port)
- proto = (yield ep.connect(f))
-
- request = ClientRequest("POST", self.server.path, self.headers, self.data)
- yield self.logRequest("debug", "Sending server-to-server POST request:", request)
- response = (yield proto.submitRequest(request))
-
- yield self.logResponse("debug", "Received server-to-server POST response:", response)
- xml = (yield davXMLFromStream(response.stream))
-
- 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"),
- "Server-to-server request failed",
- ))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
- def logRequest(self, level, message, request, **kwargs):
- """
- Log an HTTP request.
- """
-
- assert level in logLevels
-
- if log.willLogAtLevel(level):
- iostr = StringIO()
- iostr.write("%s\n" % (message,))
- if hasattr(request, "clientproto"):
- protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
- else:
- protocol = "HTTP/1.1"
- iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
- for name, valuelist in request.headers.getAllRawHeaders():
- for value in valuelist:
- # Do not log authorization details
- if name not in ("Authorization",):
- iostr.write("%s: %s\n" % (name, value))
- else:
- iostr.write("%s: xxxxxxxxx\n" % (name,))
- iostr.write("\n")
-
- # We need to play a trick with the request stream as we can only read it once. So we
- # read it, store the value in a MemoryStream, and replace the request's stream with that,
- # so the data can be read again.
- def _gotData(data):
- iostr.write(data)
-
- request.stream = MemoryStream(data if data is not None else "")
- request.stream.doStartReading = None
-
- log.emit(level, iostr.getvalue(), **kwargs)
-
- d = allDataFromStream(request.stream)
- d.addCallback(_gotData)
- return d
-
- else:
- return succeed(None)
-
- def logResponse(self, level, message, response, **kwargs):
- """
- Log an HTTP request.
- """
- assert level in logLevels
-
- if log.willLogAtLevel(level):
- iostr = StringIO()
- iostr.write("%s\n" % (message,))
- code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
- iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
- for name, valuelist in response.headers.getAllRawHeaders():
- for value in valuelist:
- # Do not log authorization details
- if name not in ("WWW-Authenticate",):
- iostr.write("%s: %s\n" % (name, value))
- else:
- iostr.write("%s: xxxxxxxxx\n" % (name,))
- iostr.write("\n")
-
- # We need to play a trick with the response stream to ensure we don't mess it up. So we
- # read it, store the value in a MemoryStream, and replace the response's stream with that,
- # so the data can be read again.
- def _gotData(data):
- iostr.write(data)
-
- response.stream = MemoryStream(data if data is not None else "")
- response.stream.doStartReading = None
-
- log.emit(level, iostr.getvalue(), **kwargs)
-
- d = allDataFromStream(response.stream)
- d.addCallback(_gotData)
- return d
-
- def _generateHeaders(self):
- self.headers = Headers()
- self.headers.setHeader('Host', utf8String(self.server.host + ":%s" % (self.server.port,)))
-
- # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
- self.headers.addRawHeader('Originator', utf8String(self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee))
- self._doAuthentication()
- for recipient in self.recipients:
- 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")
-
- def _doAuthentication(self):
- if self.server.authentication and self.server.authentication[0] == "basic":
- self.headers.setHeader(
- 'Authorization',
- ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
- )
-
- def _prepareData(self):
- if self.server.unNormalizeAddresses and self.scheduler.method == "PUT":
- normalizedCalendar = self.scheduler.calendar.duplicate()
- normalizedCalendar.normalizeCalendarUserAddresses(
- normalizationLookup,
- self.scheduler.resource.principalForCalendarUserAddress,
- toUUID=False)
- else:
- normalizedCalendar = self.scheduler.calendar
- self.data = str(normalizedCalendar)
-
- 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)
Deleted: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py 2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/ischeduleservers.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -1,214 +0,0 @@
-##
-# Copyright (c) 2006-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 twext.python.filepath import CachingFilePath as FilePath
-
-from twext.python.log import Logger
-
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.scheduling.delivery import DeliveryService
-
-import xml.dom.minidom
-
-"""
-XML based iSchedule configuration file handling.
-"""
-
-__all__ = [
- "IScheduleServers",
-]
-
-log = Logger()
-
-class IScheduleServers(object):
-
- _fileInfo = None
- _xmlFile = None
- _servers = None
- _domainMap = None
-
- def __init__(self):
-
- self._loadConfig()
-
- def _loadConfig(self):
- if IScheduleServers._servers is None:
- IScheduleServers._xmlFile = FilePath(
- fullServerPath(
- config.ConfigRoot,
- config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"]
- )
- )
- IScheduleServers._xmlFile.restat()
- fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
- if fileInfo != IScheduleServers._fileInfo:
- parser = IScheduleServersParser(IScheduleServers._xmlFile)
- IScheduleServers._servers = parser.servers
- self._mapDomains()
- IScheduleServers._fileInfo = fileInfo
-
- def _mapDomains(self):
- IScheduleServers._domainMap = {}
- for server in IScheduleServers._servers:
- for domain in server.domains:
- IScheduleServers._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 IScheduleServers._domainMap.get(domain)
-
-ELEMENT_SERVERS = "servers"
-ELEMENT_SERVER = "server"
-ELEMENT_URI = "uri"
-ELEMENT_AUTHENTICATION = "authentication"
-ATTRIBUTE_TYPE = "type"
-ATTRIBUTE_BASICAUTH = "basic"
-ELEMENT_USER = "user"
-ELEMENT_PASSWORD = "password"
-ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
-ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
-ELEMENT_DOMAINS = "domains"
-ELEMENT_DOMAIN = "domain"
-ELEMENT_CLIENT_HOSTS = "hosts"
-ELEMENT_HOST = "host"
-
-class IScheduleServersParser(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:
- log.error("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(IScheduleServerRecord())
- self.servers[-1].parseXML(child)
-
-class IScheduleServerRecord (object):
- """
- Contains server-to-server details.
- """
- def __init__(self, uri=None):
- """
- @param recordType: record type for directory entry.
- """
- self.uri = ""
- self.authentication = None
- self.allow_from = False
- self.allow_to = True
- self.domains = []
- self.client_hosts = []
- self.unNormalizeAddresses = True
- self.moreHeaders = []
-
- if uri:
- self.uri = uri
- self._parseDetails()
-
- 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_AUTHENTICATION:
- self._parseAuthentication(child)
- 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._parseList(child, ELEMENT_DOMAIN, self.domains)
- elif child_name == ELEMENT_CLIENT_HOSTS:
- self._parseList(child, ELEMENT_HOST, self.client_hosts)
- else:
- raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
-
- self._parseDetails()
-
- def _parseList(self, node, element_name, appendto):
- for child in node._get_childNodes():
- if child._get_localName() == element_name:
- if child.firstChild is not None:
- appendto.append(child.firstChild.data.encode("utf-8"))
-
- def _parseAuthentication(self, node):
- if node.hasAttribute(ATTRIBUTE_TYPE):
- atype = node.getAttribute(ATTRIBUTE_TYPE).encode("utf-8")
- if atype != ATTRIBUTE_BASICAUTH:
- return
- else:
- return
-
- for child in node._get_childNodes():
- if child._get_localName() == ELEMENT_USER:
- if child.firstChild is not None:
- user = child.firstChild.data.encode("utf-8")
- elif child._get_localName() == ELEMENT_PASSWORD:
- if child.firstChild is not None:
- password = child.firstChild.data.encode("utf-8")
-
- self.authentication = ("basic", user, password,)
-
- 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]
Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/scheduler.py 2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/scheduling/scheduler.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -47,8 +47,8 @@
from twistedcaldav.scheduling.cuaddress import EmailCalendarUser
from twistedcaldav.scheduling.cuaddress import PartitionedCalendarUser
from twistedcaldav.scheduling.imip import ScheduleViaIMip
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
+from twistedcaldav.scheduling.ischedule.delivery import ScheduleViaISchedule
+from twistedcaldav.scheduling.ischedule.servers import IScheduleServers
from twistedcaldav.scheduling.itip import iTIPRequestStatus
from twistedcaldav.servers import Servers
from twistedcaldav.util import normalizationLookup
Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/stdconfig.py 2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/stdconfig.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -606,6 +606,17 @@
"Enabled" : False, # iSchedule protocol
"AddressPatterns" : [], # Reg-ex patterns to match iSchedule-able calendar user addresses
"Servers" : "servertoserver.xml", # iSchedule server configurations
+ "DKIM" : { # DKIM options
+ "Enabled" : True, # DKIM signing/verification enabled
+ "Domain" : "", # Domain for DKIM (defaults to ServerHostName)
+ "SignatureAlgorithm" : "rsa-sha256", # Signature algorithm (one of rsa-sha1 or rsa-sha256)
+ "UseDNSKey" : True, # Public key stored in DNS
+ "UseHTTPKey" : True, # Public key stored in HTTP /.well-known
+ "KeySelector" : "ischedule", # Selector for public key
+ "ExpireSeconds" : 3600, # Expiration time for signature verification
+ "PrivateKeyFile" : "", # File where private key is stored
+ "PublicKeyFile" : "", # File where public key is stored
+ },
},
"iMIP": {
Modified: CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/timezonexml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/timezonexml.py 2012-08-29 14:56:30 UTC (rev 9750)
+++ CalendarServer/branches/users/cdaboo/ischedule-dkim/twistedcaldav/timezonexml.py 2012-08-29 20:24:20 UTC (rev 9751)
@@ -18,8 +18,8 @@
This module provides XML definitions for use with Timezone Standard Service.
"""
-from txdav.xml import element as davxml
from txdav.xml.element import registerElement
+from txdav.xml.element import WebDAVElement, WebDAVEmptyElement, WebDAVTextElement
##
@@ -30,7 +30,7 @@
@registerElement
-class Capabilities (davxml.WebDAVElement):
+class Capabilities (WebDAVElement):
namespace = timezone_namespace
name = "capabilities"
allowed_children = {
@@ -39,7 +39,7 @@
@registerElement
-class Operation (davxml.WebDAVElement):
+class Operation (WebDAVElement):
namespace = timezone_namespace
name = "operation"
allowed_children = {
@@ -50,19 +50,19 @@
@registerElement
-class Action (davxml.WebDAVTextElement):
+class Action (WebDAVTextElement):
namespace = timezone_namespace
name = "action"
@registerElement
-class Description (davxml.WebDAVTextElement):
+class Description (WebDAVTextElement):
namespace = timezone_namespace
name = "description"
@registerElement
-class AcceptParameter (davxml.WebDAVElement):
+class AcceptParameter (WebDAVElement):
namespace = timezone_namespace
name = "accept-parameter"
allowed_children = {
@@ -75,31 +75,31 @@
@registerElement
-class Name (davxml.WebDAVTextElement):
+class Name (WebDAVTextElement):
namespace = timezone_namespace
name = "name"
@registerElement
-class Required (davxml.WebDAVTextElement):
+class Required (WebDAVTextElement):
namespace = timezone_namespace
name = "required"
@registerElement
-class Multi (davxml.WebDAVTextElement):
+class Multi (WebDAVTextElement):
namespace = timezone_namespace
name = "multi"
@registerElement
-class Value (davxml.WebDAVTextElement):
+class Value (WebDAVTextElement):
namespace = timezone_namespace
name = "value"
@registerElement
-class TimezoneList (davxml.WebDAVElement):
+class TimezoneList (WebDAVElement):
namespace = timezone_namespace
name = "timezone-list"
allowed_children = {
@@ -109,13 +109,13 @@
@registerElement
-class Dtstamp (davxml.WebDAVTextElement):
+class Dtstamp (WebDAVTextElement):
namespace = timezone_namespace
name = "dtstamp"
@registerElement
-class Summary (davxml.WebDAVElement):
+class Summary (WebDAVElement):
namespace = timezone_namespace
name = "summary"
allowed_children = {
@@ -128,37 +128,37 @@
@registerElement
-class Tzid (davxml.WebDAVTextElement):
+class Tzid (WebDAVTextElement):
namespace = timezone_namespace
name = "tzid"
@registerElement
-class LastModified (davxml.WebDAVTextElement):
+class LastModified (WebDAVTextElement):
namespace = timezone_namespace
name = "last-modified"
@registerElement
-class LocalName (davxml.WebDAVTextElement):
+class LocalName (WebDAVTextElement):
namespace = timezone_namespace
name = "local-name"
@registerElement
-class Alias (davxml.WebDAVTextElement):
+class Alias (WebDAVTextElement):
namespace = timezone_namespace
name = "alias"
@registerElement
-class Inactive (davxml.WebDAVEmptyElement):
+class Inactive (WebDAVEmptyElement):
namespace = timezone_namespace
name = "inactive"
@registerElement
-class Timezones (davxml.WebDAVElement):
+class Timezones (WebDAVElement):
namespace = timezone_namespace
name = "timezones"
allowed_children = {
@@ -168,7 +168,7 @@
@registerElement
-class Tzdata (davxml.WebDAVElement):
+class Tzdata (WebDAVElement):
namespace = timezone_namespace
name = "tzdata"
allowed_children = {
@@ -179,13 +179,13 @@
@registerElement
-class Calscale (davxml.WebDAVTextElement):
+class Calscale (WebDAVTextElement):
namespace = timezone_namespace
name = "calscale"
@registerElement
-class Observance (davxml.WebDAVElement):
+class Observance (WebDAVElement):
namespace = timezone_namespace
name = "observance"
allowed_children = {
@@ -198,18 +198,18 @@
@registerElement
-class Onset (davxml.WebDAVTextElement):
+class Onset (WebDAVTextElement):
namespace = timezone_namespace
name = "onset"
@registerElement
-class UTCOffsetFrom (davxml.WebDAVTextElement):
+class UTCOffsetFrom (WebDAVTextElement):
namespace = timezone_namespace
name = "utc-offset-from"
@registerElement
-class UTCOffsetTo (davxml.WebDAVTextElement):
+class UTCOffsetTo (WebDAVTextElement):
namespace = timezone_namespace
name = "utc-offset-to"
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120829/a159ed2d/attachment-0001.html>
More information about the calendarserver-changes
mailing list