Revision: 6472 http://trac.macosforge.org/projects/calendarserver/changeset/6472 Author: cdaboo@apple.com Date: 2010-10-27 11:53:51 -0700 (Wed, 27 Oct 2010) Log Message: ----------- Handle link resources with missing target resources by returning a 404. Modified Paths: -------------- CalendarServer/trunk/twistedcaldav/linkresource.py CalendarServer/trunk/twistedcaldav/sharedcollection.py Added Paths: ----------- CalendarServer/trunk/twistedcaldav/test/test_link.py Modified: CalendarServer/trunk/twistedcaldav/linkresource.py =================================================================== --- CalendarServer/trunk/twistedcaldav/linkresource.py 2010-10-27 17:36:01 UTC (rev 6471) +++ CalendarServer/trunk/twistedcaldav/linkresource.py 2010-10-27 18:53:51 UTC (rev 6472) @@ -14,15 +14,16 @@ # limitations under the License. ## + from twext.python.log import LoggingMixIn +from twext.web2 import responsecode, server, http +from twext.web2.dav import davxml +from twext.web2.http import HTTPError, StatusResponse +from twext.web2.resource import WrapperResource from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred -from twext.web2.http import HTTPError -from twext.web2 import responsecode -from twext.web2.resource import WrapperResource from twistedcaldav.config import config -from twext.web2.dav import davxml __all__ = [ "LinkResource", @@ -42,27 +43,34 @@ class LinkResource(CalDAVComplianceMixIn, WrapperResource, LoggingMixIn): """ - This is similar to a WrapperResource except that we locate our resource dynamically. + This is similar to a WrapperResource except that we locate our resource dynamically. We need to deal with the + case of a missing underlying resource (broken link) as indicated by self._linkedResource being None. """ def __init__(self, parent, link_url): self.parent = parent self.linkURL = link_url + self.loopDetect = set() super(LinkResource, self).__init__(self.parent.principalCollections()) @inlineCallbacks def linkedResource(self, request): if not hasattr(self, "_linkedResource"): + if self.linkURL in self.loopDetect: + raise HTTPError(StatusResponse(responsecode.LOOP_DETECTED, "Recursive link target: %s" % (self.linkURL,))) + else: + self.loopDetect.add(self.linkURL) self._linkedResource = (yield request.locateResource(self.linkURL)) + self.loopDetect.remove(self.linkURL) if self._linkedResource is None: - raise HTTPError(responsecode.NOT_FOUND) + raise HTTPError(StatusResponse(responsecode.NOT_FOUND, "Missing link target: %s" % (self.linkURL,))) returnValue(self._linkedResource) def isCollection(self): - return True + return True if hasattr(self, "_linkedResource") else False def resourceType(self): return self._linkedResource.resourceType() if hasattr(self, "_linkedResource") else davxml.ResourceType.link @@ -70,33 +78,41 @@ def locateChild(self, request, segments): def _defer(result): - return (result, segments) + if result is None: + return (self, server.StopTraversal) + else: + return (result, segments) d = self.linkedResource(request) d.addCallback(_defer) return d + @inlineCallbacks def renderHTTP(self, request): - return self.linkedResource(request) + linked_to = (yield self.linkedResource(request)) + if linked_to: + returnValue(linked_to) + else: + returnValue(http.StatusResponse(responsecode.OK, "Link resource with missing target: %s" % (self.linkURL,))) def getChild(self, name): - return self._linkedResource.getChild(name) + return self._linkedResource.getChild(name) if hasattr(self, "_linkedResource") else None @inlineCallbacks def hasProperty(self, property, request): hosted = (yield self.linkedResource(request)) - result = (yield hosted.hasProperty(property, request)) + result = (yield hosted.hasProperty(property, request)) if hosted else False returnValue(result) @inlineCallbacks def readProperty(self, property, request): hosted = (yield self.linkedResource(request)) - result = (yield hosted.readProperty(property, request)) + result = (yield hosted.readProperty(property, request)) if hosted else None returnValue(result) @inlineCallbacks def writeProperty(self, property, request): hosted = (yield self.linkedResource(request)) - result = (yield hosted.writeProperty(property, request)) + result = (yield hosted.writeProperty(property, request)) if hosted else None returnValue(result) class LinkFollowerMixIn(object): @@ -104,17 +120,13 @@ @inlineCallbacks def locateChild(self, req, segments): + self._inside_locateChild = True resource, path = (yield maybeDeferred(super(LinkFollowerMixIn, self).locateChild, req, segments)) - MAX_LINK_DEPTH = 10 - ctr = 0 - seenResource = set() while isinstance(resource, LinkResource): - seenResource.add(resource) - ctr += 1 - resource = (yield resource.linkedResource(req)) - - if ctr > MAX_LINK_DEPTH or resource in seenResource: - raise HTTPError(responsecode.LOOP_DETECTED) + linked_to = (yield resource.linkedResource(req)) + if linked_to is None: + break + resource = linked_to returnValue((resource, path)) Modified: CalendarServer/trunk/twistedcaldav/sharedcollection.py =================================================================== --- CalendarServer/trunk/twistedcaldav/sharedcollection.py 2010-10-27 17:36:01 UTC (rev 6471) +++ CalendarServer/trunk/twistedcaldav/sharedcollection.py 2010-10-27 18:53:51 UTC (rev 6472) @@ -18,6 +18,9 @@ "SharedCollectionResource", ] +from twext.web2.http import HTTPError, StatusResponse +from twext.web2 import responsecode + from twisted.internet.defer import inlineCallbacks, returnValue from twistedcaldav.linkresource import LinkResource @@ -29,24 +32,31 @@ class SharedCollectionResource(LinkResource): """ - This is similar to a WrapperResource except that we locate our shared collection resource dynamically. + This is similar to a LinkResource except that we locate our shared collection resource dynamically. """ def __init__(self, parent, share): self.share = share - super(SharedCollectionResource, self).__init__(parent, None) + super(SharedCollectionResource, self).__init__(parent, self.share.hosturl) @inlineCallbacks def linkedResource(self, request): + """ + Resolve the share host url to the underlying resource (set to be a virtual share). + """ if not hasattr(self, "_linkedResource"): self._linkedResource = (yield request.locateResource(self.share.hosturl)) - # FIXME: this is awkward - because we are "mutation" this object into a virtual share - # we must not cache the resource at this URL, otherwise an access of the owner's resource - # will return the same virtually shared one which would be wrong. - request._forgetResource(self._linkedResource, self.share.hosturl) + if self._linkedResource is not None: + # FIXME: this is awkward - because we are "mutating" this object into a virtual share + # we must not cache the resource at this URL, otherwise an access of the owner's resource + # will return the same virtually shared one which would be wrong. + request._forgetResource(self._linkedResource, self.share.hosturl) + + ownerPrincipal = (yield self.parent.ownerPrincipal(request)) + self._linkedResource.setVirtualShare(ownerPrincipal, self.share) + else: + raise HTTPError(StatusResponse(responsecode.NOT_FOUND, "Missing link target: %s" % (self.linkURL,))) - ownerPrincipal = (yield self.parent.ownerPrincipal(request)) - self._linkedResource.setVirtualShare(ownerPrincipal, self.share) returnValue(self._linkedResource) Added: CalendarServer/trunk/twistedcaldav/test/test_link.py =================================================================== --- CalendarServer/trunk/twistedcaldav/test/test_link.py (rev 0) +++ CalendarServer/trunk/twistedcaldav/test/test_link.py 2010-10-27 18:53:51 UTC (rev 6472) @@ -0,0 +1,119 @@ +## +# Copyright (c) 2008 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 import responsecode +from twext.web2.http import HTTPError +from twext.web2.test.test_server import SimpleRequest + +from twisted.internet.defer import inlineCallbacks, succeed + +from twistedcaldav.linkresource import LinkResource +from twistedcaldav.resource import CalendarHomeResource +from twistedcaldav.sharedcollection import SharedCollectionResource +from twistedcaldav.test.util import TestCase + + +class StubProperty(object): + def qname(self): + return "StubQnamespace", "StubQname" + +class StubHome(object): + def properties(self): + return [] + + def calendarWithName(self, name): + return succeed(None) + +class StubCalendarHomeResource(CalendarHomeResource): + def principalForRecord(self): + return None + +class StubShare(object): + def __init__(self, link): + self.hosturl = link + +class LinkResourceTests(TestCase): + + @inlineCallbacks + def test_okLink(self): + resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome()) + self.site.resource.putChild("home", resource) + link = LinkResource(resource, "/home/outbox/") + resource.putChild("link", link) + + request = SimpleRequest(self.site, "GET", "/home/link/") + linked_to, _ignore = (yield resource.locateChild(request, ["link",])) + self.assertTrue(linked_to is resource.getChild("outbox")) + + @inlineCallbacks + def test_badLink(self): + resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome()) + self.site.resource.putChild("home", resource) + link = LinkResource(resource, "/home/outbox/abc") + resource.putChild("link", link) + + request = SimpleRequest(self.site, "GET", "/home/link/") + try: + yield resource.locateChild(request, ["link",]) + except HTTPError, e: + self.assertEqual(e.response.code, responsecode.NOT_FOUND) + else: + self.fail("HTTPError exception not raised") + + @inlineCallbacks + def test_recursiveLink(self): + resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome()) + self.site.resource.putChild("home", resource) + link1 = LinkResource(resource, "/home/link2/") + resource.putChild("link1", link1) + link2 = LinkResource(resource, "/home/link1/") + resource.putChild("link2", link2) + + request = SimpleRequest(self.site, "GET", "/home/link1/") + try: + yield resource.locateChild(request, ["link1",]) + except HTTPError, e: + self.assertEqual(e.response.code, responsecode.LOOP_DETECTED) + else: + self.fail("HTTPError exception not raised") + +class SharedCollectionResourceTests(TestCase): + + @inlineCallbacks + def test_okLink(self): + resource = StubCalendarHomeResource(self.site.resource, "home", object(), StubHome()) + self.site.resource.putChild("home", resource) + link = SharedCollectionResource(resource, StubShare("/home/outbox/")) + resource.putChild("link", link) + + request = SimpleRequest(self.site, "GET", "/home/link/") + linked_to, _ignore = (yield resource.locateChild(request, ["link",])) + self.assertTrue(linked_to is resource.getChild("outbox")) + + @inlineCallbacks + def test_badLink(self): + resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome()) + self.site.resource.putChild("home", resource) + link = SharedCollectionResource(resource, StubShare("/home/outbox/abc")) + resource.putChild("link", link) + + request = SimpleRequest(self.site, "GET", "/home/link/") + try: + yield resource.locateChild(request, ["link",]) + except HTTPError, e: + self.assertEqual(e.response.code, responsecode.NOT_FOUND) + else: + self.fail("HTTPError exception not raised")