def test_PROPPATCH_basic(self): """ PROPPATCH """ # FIXME: # Do PROPFIND to make sure it's still there # Test nonexistant resource # Test None namespace in property def check_patch_response(response): response = IResponse(response) if response.code != responsecode.MULTI_STATUS: self.fail("Incorrect response code for PROPFIND (%s != %s)" % (response.code, responsecode.MULTI_STATUS)) content_type = response.headers.getHeader("content-type") if content_type not in (http_headers.MimeType("text", "xml"), http_headers.MimeType( "application", "xml")): self.fail( "Incorrect content-type for PROPPATCH response (%r not in %r)" % (content_type, (http_headers.MimeType("text", "xml"), http_headers.MimeType("application", "xml")))) return davXMLFromStream( response.stream).addCallback(check_patch_xml) def check_patch_xml(doc): multistatus = doc.root_element if not isinstance(multistatus, davxml.MultiStatus): self.fail( "PROPFIND response XML root element is not multistatus: %r" % (multistatus, )) # Requested a property change one resource, so there should be exactly one response response = multistatus.childOfType(davxml.Response) # Should have a response description (its contents are arbitrary) response.childOfType(davxml.ResponseDescription) # Requested property change was on / self.failUnless( response.childOfType(davxml.HRef) == "/", "Incorrect response URI: %s != /" % (response.childOfType(davxml.HRef), )) # Requested one property change, so there should be exactly one property status propstat = response.childOfType(davxml.PropertyStatus) # And the contained property should be a SpiffyProperty self.failIf( propstat.childOfType( davxml.PropertyContainer).childOfType(SpiffyProperty) is None, "Not a SpiffyProperty in PROPPATCH property status: %s" % (propstat.toxml())) # And the status should be 200 self.failUnless( propstat.childOfType(davxml.Status).code == responsecode.OK, "Incorrect status code for PROPPATCH of property %s: %s != %s" % (propstat.childOfType(davxml.PropertyContainer).toxml(), propstat.childOfType(davxml.Status).code, responsecode.OK)) patch = davxml.PropertyUpdate( davxml.Set( davxml.PropertyContainer( SpiffyProperty.fromString("This is a spiffy resource.")))) request = SimpleRequest(self.site, "PROPPATCH", "/") request.stream = MemoryStream(patch.toxml()) return self.send(request, check_patch_response)
def test_sign(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:[email protected]") headers.addRawHeader("Recipient", "mailto:[email protected]") 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, 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(DKIMUtils.canonicalizeBody(data)).digest()) sign_this = """originator:mailto:[email protected] recipient:mailto:[email protected] content-type:%s ischedule-version:1.0 ischedule-message-id:%s dkim-signature:v=1; d=example.com; s=dkim; t=%s; x=%s; a=%s; q=private-exchange:http/well-known:dns/txt; c=ischedule-relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; bh=%s; b=""".replace( "\n", "\r\n") % (headers.getRawHeaders("Content-Type")[0], request.message_id, request.time, request.expire, algorithm, bodyhash) with open(self.private_keyfile) as f: key = f.read() key = RSA.importKey(key) signature = DKIMUtils.sign(sign_this, key, DKIMUtils.hash_func(algorithm)) 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=private-exchange:http/well-known:dns/txt; c=ischedule-relaxed/simple; h=Originator:Recipient:Content-Type:iSchedule-Version:iSchedule-Message-ID; 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 with open(self.public_keyfile) as f: pubkey = f.read() pubkey = RSA.importKey(pubkey) self.assertEqual( DKIMUtils.verify(sign_this, result, pubkey, DKIMUtils.hash_func(algorithm)), None)
fbresult = yield FreebusyQuery( organizer=organizer, recipient=recipient, timerange=timerange).generateAttendeeFreeBusyResponse(fbset=fbset, method=None) except NumberOfMatchesWithinLimits: log.error("Too many matching components in free-busy report") raise HTTPError( ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components")) except TimeRangeLowerLimit, e: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit), ))) except TimeRangeUpperLimit, e: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, caldavxml.MaxDateTime(), "Time-range value too far in the future. Must be on or before %s." % (str(e.limit), ))) response = Response() response.stream = MemoryStream(fbresult.getText(accepted_type)) response.headers.setHeader( "content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type, ))) returnValue(response)
def test_get_attachment_data(self): """ Test that action=get-all-attachments works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) remote_id = attachment.id() yield self.commitTransaction(0) home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name="puser01") shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") attachment = yield ManagedAttachment._create(self.theTransactionUnderTest(1), None, home1.id()) attachment._contentType = MimeType.fromString("text/plain") attachment._name = "test.txt" yield shared_object.ownerHome().readAttachmentData(remote_id, attachment) yield self.commitTransaction(1)
def test_update_attachment(self): """ Test that action=update-attachment works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") resourceID = object1.id() attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) managedID = attachment.managedID() yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") data = "Here is some more text." attachment, location = yield shared_object.updateAttachment(managedID, MimeType.fromString("text/plain"), "test.txt", MemoryStream(data)) managedID = attachment.managedID() from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal self.assertTrue(isinstance(attachment, ManagedAttachmentExternal)) self.assertEqual(attachment.size(), len(data)) self.assertTrue("user01/dropbox/" in location) yield self.commitTransaction(1) cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID) self.assertEqual(cobjs, set((resourceID,))) attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID) self.assertEqual(attachment.name(), "test.txt") data = yield self.attachmentToString(attachment) self.assertEqual(data, "Here is some more text.") yield self.commitTransaction(0)
def _defer(data): response = Response() response.stream = MemoryStream(str(data)) response.headers.setHeader( "content-type", MimeType.fromString("text/calendar")) return response
def addressbook_query(self, addressbook_uri, query, got_xml, data, no_init): if not no_init: ''' FIXME: clear address book, possibly by removing mkcol = """<?xml version="1.0" encoding="utf-8" ?> <D:mkcol xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav"> <D:set> <D:prop> <D:resourcetype><D:collection/><C:addressbook/></D:resourcetype> </D:prop> </D:set> </D:mkcol> """ response = yield self.send(SimpleStoreRequest(self, "MKCOL", addressbook_uri, content=mkcol, authPrincipal=self.authPrincipal)) response = IResponse(response) if response.code != responsecode.CREATED: self.fail("MKCOL failed: %s" % (response.code,)) ''' if data: for filename, icaldata in data.iteritems(): request = SimpleStoreRequest( self, "PUT", joinURL(addressbook_uri, filename + ".vcf"), headers=Headers({ "content-type": MimeType.fromString("text/vcard") }), authPrincipal=self.authPrincipal) request.stream = MemoryStream(icaldata) yield self.send(request) else: # Add vcards to addressbook for child in FilePath(self.vcards_dir).children(): if os.path.splitext(child.basename())[1] != ".vcf": continue request = SimpleStoreRequest( self, "PUT", joinURL(addressbook_uri, child.basename()), headers=Headers({ "content-type": MimeType.fromString("text/vcard") }), authPrincipal=self.authPrincipal) request.stream = MemoryStream(child.getContent()) yield self.send(request) request = SimpleStoreRequest(self, "REPORT", addressbook_uri, authPrincipal=self.authPrincipal) request.stream = MemoryStream(query.toxml()) response = yield self.send(request) response = IResponse(response) if response.code != responsecode.MULTI_STATUS: self.fail("REPORT failed: %s" % (response.code, )) returnValue((yield davXMLFromStream(response.stream).addCallback(got_xml)))
def __init__(self, code, headers, body): self.code = code self.headers = Headers(headers) self.body = body self.stream = MemoryStream(body)
def initialState(self): """ Setup the server with an initial set of data user01 - migrating user user02 - has a calendar shared with user01 user03 - shared to by user01 puser01 - user on other pod puser02 - has a calendar shared with user01 puser03 - shared to by user01 """ # Data for user01 home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.stash["user01_pod0_home_id"] = home.id() calendar = yield home.childWithName("calendar") yield calendar.createCalendarObjectWithName( "01_1.ics", Component.fromString(self.data01_1)) yield calendar.createCalendarObjectWithName( "01_2.ics", Component.fromString(self.data01_2)) obj3 = yield calendar.createCalendarObjectWithName( "01_3.ics", Component.fromString(self.data01_3)) attachment, _ignore_location = yield obj3.addAttachment( None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text #1.")) self.stash["user01_attachment_id"] = attachment.id() self.stash["user01_attachment_md5"] = attachment.md5() self.stash["user01_attachment_mid"] = attachment.managedID() yield self.commitTransaction(0) # Data for user02 home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user02", create=True) calendar = yield home.childWithName("calendar") yield calendar.createCalendarObjectWithName( "02_1.ics", Component.fromString(self.data02_1)) yield calendar.createCalendarObjectWithName( "02_2.ics", Component.fromString(self.data02_2)) yield calendar.createCalendarObjectWithName( "02_3.ics", Component.fromString(self.data02_3)) yield self.commitTransaction(0) # Data for puser02 home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name="puser02", create=True) calendar = yield home.childWithName("calendar") yield calendar.createCalendarObjectWithName( "p02_1.ics", Component.fromString(self.datap02_1)) yield calendar.createCalendarObjectWithName( "p02_2.ics", Component.fromString(self.datap02_2)) yield calendar.createCalendarObjectWithName( "p02_3.ics", Component.fromString(self.datap02_3)) yield self.commitTransaction(1) # Share calendars self.stash["sharename_user01_to_user03"] = yield self._createShare( "user01", "user03") self.stash["sharename_user01_to_puser03"] = yield self._createShare( "user01", "puser03") self.stash["sharename_user02_to_user01"] = yield self._createShare( "user02", "user01") self.stash["sharename_puser02_to_user01"] = yield self._createShare( "puser02", "user01") # Add some delegates txn = self.theTransactionUnderTest(0) record01 = yield txn.directoryService().recordWithUID(u"user01") record02 = yield txn.directoryService().recordWithUID(u"user02") record03 = yield txn.directoryService().recordWithUID(u"user03") precord01 = yield txn.directoryService().recordWithUID(u"puser01") group02 = yield txn.directoryService().recordWithUID(u"group02") group03 = yield txn.directoryService().recordWithUID(u"group03") # Add user02 and user03 as individual delegates yield Delegates.addDelegate(txn, record01, record02, True) yield Delegates.addDelegate(txn, record01, record03, False) yield Delegates.addDelegate(txn, record01, precord01, False) # Add group delegates yield Delegates.addDelegate(txn, record01, group02, True) yield Delegates.addDelegate(txn, record01, group03, False) # Add external delegates yield txn.assignExternalDelegates(u"user01", None, None, u"external1", u"external2") yield self.commitTransaction(0) yield self.waitAllEmpty()
def _processFBURL(self, request): # # Check authentication and access controls # yield self.authorize(request, (davxml.Read(), )) # Extract query parameters from the URL args = ( 'start', 'end', 'duration', 'token', 'format', 'user', ) for arg in args: setattr(self, arg, request.args.get(arg, [None])[0]) # Some things we do not handle if self.token or self.user: raise HTTPError( ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-query-parameter"), "Invalid query parameter", )) # Check format if self.format: self.format = self.format.split(";")[0] if self.format not in ("text/calendar", "text/plain"): raise HTTPError( ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-format"), "Invalid return format requested", )) else: self.format = "text/calendar" # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values try: if self.start: self.start = DateTime.parseText(self.start) if not self.start.utc(): raise ValueError() if self.end: self.end = DateTime.parseText(self.end) if not self.end.utc(): raise ValueError() if self.duration: self.duration = Duration.parseText(self.duration) except ValueError: raise HTTPError( ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Sanity check start/end/duration # End and duration cannot both be present if self.end and self.duration: raise HTTPError( ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Duration must be positive if self.duration and self.duration.getTotalSeconds() < 0: raise HTTPError( ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Now fill in the missing pieces if self.start is None: self.start = DateTime.getNowUTC() self.start.setHHMMSS(0, 0, 0) if self.duration: self.end = self.start + self.duration if self.end is None: self.end = self.start + Duration( days=config.FreeBusyURL.TimePeriod) # End > start if self.end <= self.start: raise HTTPError( ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long) # Now lookup the principal details for the targeted user principal = (yield self.parent.principalForRecord()) # Pick the first mailto cu address or the first other type cuaddr = None for item in principal.calendarUserAddresses(): if cuaddr is None: cuaddr = item if item.startswith("mailto:"): cuaddr = item break # Get inbox details inboxURL = principal.scheduleInboxURL() if inboxURL is None: raise HTTPError( StatusResponse( responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox URL for principal: %s" % (principal, ))) try: inbox = (yield request.locateResource(inboxURL)) except: log.error("No schedule inbox for principal: {p}", p=principal) inbox = None if inbox is None: raise HTTPError( StatusResponse( responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal, ))) organizer = recipient = LocalCalendarUser(cuaddr, principal.record) recipient.inbox = inbox._newStoreObject attendeeProp = Property("ATTENDEE", recipient.cuaddr) timerange = Period(self.start, self.end) fbresult = yield FreebusyQuery( organizer=organizer, recipient=recipient, attendeeProp=attendeeProp, timerange=timerange, ).generateAttendeeFreeBusyResponse() response = Response() response.stream = MemoryStream(str(fbresult)) response.headers.setHeader( "content-type", MimeType.fromString("%s; charset=utf-8" % (self.format, ))) returnValue(response)
class WebCalendarResource(ReadOnlyResourceMixIn, DAVFile): def defaultAccessControlList(self): return succeed( davxml.ACL( davxml.ACE( davxml.Principal(davxml.Authenticated()), davxml.Grant(davxml.Privilege(davxml.Read()), ), davxml.Protected(), TwistedACLInheritable(), ), )) def etag(self): # Can't be calculated here return succeed(None) def contentLength(self): # Can't be calculated here return None def lastModified(self): return None def exists(self): return True def displayName(self): return "Web Calendar" def contentType(self): return MimeType.fromString("text/html; charset=utf-8") def contentEncoding(self): return None def createSimilarFile(self, path): return DAVFile(path, principalCollections=self.principalCollections()) _htmlContent_lastCheck = 0 _htmlContent_statInfo = 0 _htmlContentDebug_lastCheck = 0 _htmlContentDebug_statInfo = 0 def htmlContent(self, debug=False): if debug: cacheAttr = "_htmlContentDebug" templateFileName = "debug_standalone.html" else: cacheAttr = "_htmlContent" templateFileName = "standalone.html" templateFileName = os.path.join(config.WebCalendarRoot, templateFileName) # # See if the file changed, and dump the cached template if so. # Don't bother to check if we've checked in the past minute. # We don't cache if debug is true. # if not debug and hasattr(self, cacheAttr): currentTime = time() if currentTime - getattr(self, cacheAttr + "_lastCheck") > 60: statInfo = os.stat(templateFileName) statInfo = (statInfo.st_mtime, statInfo.st_size) if statInfo != getattr(self, cacheAttr + "_statInfo"): delattr(self, cacheAttr) setattr(self, cacheAttr + "_statInfo", statInfo) setattr(self, cacheAttr + "_lastCheck", currentTime) # # If we don't have a cached template, load it up. # if not hasattr(self, cacheAttr): templateFile = open(templateFileName) try: htmlContent = templateFile.read() finally: templateFile.close() if debug: # Don't cache return htmlContent setattr(self, cacheAttr, htmlContent) return getattr(self, cacheAttr) def render(self, request): if not self.fp.isdir(): return responsecode.NOT_FOUND # # Get URL of authenticated principal. # Don't need to authenticate here because the ACL will have already # required it. # authenticatedPrincipalURL = request.authnUser.principalURL() def queryValue(arg): query = parse_qs(urlparse(request.uri).query, True) return query.get(arg, [""])[0] # # Parse debug query arg # debug = queryValue("debug") debug = debug is not None and debug.lower() in ("1", "true", "yes") # # Parse TimeZone query arg # tzid = queryValue("tzid") if not tzid: tzid = getLocalTimezone() self.log.debug("Determined timezone to be %s" % (tzid, )) # # Make some HTML # try: htmlContent = self.htmlContent(debug) % { "tzid": tzid, "principalURL": authenticatedPrincipalURL, } except IOError, e: self.log.error("Unable to obtain WebCalendar template: %s" % (e, )) return responsecode.NOT_FOUND response = Response() response.stream = MemoryStream(htmlContent) for (header, value) in ( ("content-type", self.contentType()), ("content-encoding", self.contentEncoding()), ): if value is not None: response.headers.setHeader(header, value) return response
def test_make_calendar_with_props(self): """ Make calendar with properties (CalDAV-access-09, section 5.3.1.2) """ uri = "/calendars/users/user01/calendar_prop/" path = os.path.join(self.docroot, uri[1:]) if os.path.exists(path): rmdir(path) @inlineCallbacks def do_test(response): response = IResponse(response) if response.code != responsecode.CREATED: self.fail("MKCALENDAR failed: %s" % (response.code, )) resource = (yield request.locateResource(uri)) if not resource.isCalendarCollection(): self.fail("MKCALENDAR made non-calendar collection") for qname, value in ( (davxml.DisplayName.qname(), "Lisa's Events"), (caldavxml.CalendarDescription.qname(), "Calendar restricted to events."), ): stored = yield resource.readProperty(qname, None) stored = str(stored) if stored != value: self.fail( "MKCALENDAR failed to set property %s: %s != %s" % (qname, stored, value)) supported_components = yield resource.readProperty( caldavxml.SupportedCalendarComponentSet, None) supported_components = supported_components.children if len(supported_components) != 1: self.fail( "MKCALENDAR failed to set property %s: len(%s) != 1" % (caldavxml.SupportedCalendarComponentSet.qname(), supported_components)) if supported_components[0] != caldavxml.CalendarComponent( name="VEVENT"): self.fail("MKCALENDAR failed to set property %s: %s != %s" % (caldavxml.SupportedCalendarComponentSet.qname(), supported_components[0].toxml(), caldavxml.CalendarComponent(name="VEVENT").toxml())) tz = (yield resource.readProperty(caldavxml.CalendarTimeZone, None)) tz = tz.calendar() self.failUnless(tz.resourceType() == "VTIMEZONE") self.failUnless( tuple(tz.subcomponents())[0].propertyValue("TZID") == "US-Eastern") mk = caldavxml.MakeCalendar( davxml.Set( davxml.PropertyContainer( davxml.DisplayName("Lisa's Events"), caldavxml.CalendarDescription( "Calendar restricted to events."), # FIXME: lang=en caldavxml.SupportedCalendarComponentSet( caldavxml.CalendarComponent(name="VEVENT")), caldavxml.CalendarTimeZone("""BEGIN:VCALENDAR PRODID:-//Example Corp.//CalDAV Client//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:US-Eastern LAST-MODIFIED:19870101T000000Z BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZOFFSETFROM:-0400 TZOFFSETTO:-0500 TZNAME:Eastern Standard Time (US & Canada) END:STANDARD BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 TZNAME:Eastern Daylight Time (US & Canada) END:DAYLIGHT END:VTIMEZONE END:VCALENDAR """)))) request = SimpleStoreRequest(self, "MKCALENDAR", uri, authPrincipal=self.authPrincipal) request.stream = MemoryStream(mk.toxml()) return self.send(request, do_test)
def finalState(self): """ Setup the server with data changes appearing before the final sync """ txn = self.theTransactionUnderTest(1) obj = yield self.calendarObjectUnderTest(txn, name="p02_2.ics", calendar_name="calendar", home="puser02") attachment, _ignore_location = yield obj.addAttachment(None, MimeType.fromString("text/plain"), "test_p02.txt", MemoryStream("Here is some text #p02.")) self.stash["puser02_attachment_id"] = attachment.id() self.stash["puser02_attachment_mid"] = attachment.managedID() self.stash["puser02_attachment_md5"] = attachment.md5() yield self.commitTransaction(1) yield self.waitAllEmpty()
def secondState(self): """ Setup the server with data changes appearing after the first sync """ txn = self.theTransactionUnderTest(0) obj = yield self.calendarObjectUnderTest(txn, name="01_1.ics", calendar_name="calendar", home="user01") yield obj.setComponent(self.data01_1_changed) obj = yield self.calendarObjectUnderTest(txn, name="02_2.ics", calendar_name="calendar", home="user02") attachment, _ignore_location = yield obj.addAttachment(None, MimeType.fromString("text/plain"), "test_02.txt", MemoryStream("Here is some text #02.")) self.stash["user02_attachment_id"] = attachment.id() self.stash["user02_attachment_md5"] = attachment.md5() self.stash["user02_attachment_mid"] = attachment.managedID() yield self.commitTransaction(0) yield self.waitAllEmpty()
def test_PROPFIND_basic(self): """ PROPFIND request """ def check_result(response): response = IResponse(response) if response.code != responsecode.MULTI_STATUS: self.fail("Incorrect response code for PROPFIND (%s != %s)" % (response.code, responsecode.MULTI_STATUS)) content_type = response.headers.getHeader("content-type") if content_type not in (http_headers.MimeType("text", "xml"), http_headers.MimeType( "application", "xml")): self.fail( "Incorrect content-type for PROPFIND response (%r not in %r)" % (content_type, (http_headers.MimeType("text", "xml"), http_headers.MimeType("application", "xml")))) return davXMLFromStream(response.stream).addCallback(check_xml) def check_xml(doc): multistatus = doc.root_element if not isinstance(multistatus, davxml.MultiStatus): self.fail( "PROPFIND response XML root element is not multistatus: %r" % (multistatus, )) for response in multistatus.childrenOfType( davxml.PropertyStatusResponse): if response.childOfType(davxml.HRef) == "/": for propstat in response.childrenOfType( davxml.PropertyStatus): status = propstat.childOfType(davxml.Status) properties = propstat.childOfType( davxml.PropertyContainer).children if status.code != responsecode.OK: self.fail( "PROPFIND failed (status %s) to locate live properties: %s" % (status.code, properties)) properties_to_find = [ p.qname() for p in self.liveProperties() ] for property in properties: qname = property.qname() if qname in properties_to_find: properties_to_find.remove(qname) else: self.fail( "PROPFIND found property we didn't ask for: %r" % (property, )) if properties_to_find: self.fail( "PROPFIND failed to find properties: %r" % (properties_to_find, )) break else: self.fail("No response for URI /") query = davxml.PropertyFind( davxml.PropertyContainer(*self.liveProperties())) request = SimpleRequest(self.site, "PROPFIND", "/") depth = "1" if depth is not None: request.headers.setHeader("depth", depth) request.stream = MemoryStream(query.toxml()) return self.send(request, check_result)
def mkcalendar_cb(response): response = IResponse(response) if response.code != responsecode.CREATED: self.fail("MKCALENDAR failed: %s" % (response.code, )) def propfind_cb(response): response = IResponse(response) if response.code != responsecode.MULTI_STATUS: self.fail("Incorrect response to PROPFIND: %s" % (response.code, )) def got_xml(doc): if not isinstance(doc.root_element, davxml.MultiStatus): self.fail( "PROPFIND response XML root element is not multistatus: %r" % (doc.root_element, )) response = doc.root_element.childOfType(davxml.Response) href = response.childOfType(davxml.HRef) self.failUnless(str(href) == calendar_uri) container = response.childOfType( davxml.PropertyStatus).childOfType( davxml.PropertyContainer) # # Check CalDAV:supported-calendar-component-set # supported_components = container.childOfType( caldavxml.SupportedCalendarComponentSet) if supported_components: self.fail( "CalDAV:supported-calendar-component-set element was returned; but should be hidden." ) # # Check CalDAV:supported-calendar-data # supported_calendar = container.childOfType( caldavxml.SupportedCalendarData) if supported_calendar: self.fail( "CalDAV:supported-calendar-data elementwas returned; but should be hidden." ) # # Check DAV:supported-report-set # supported_reports = container.childOfType( davxml.SupportedReportSet) if supported_reports: self.fail( "DAV:supported-report-set element was returned; but should be hidden.." ) return davXMLFromStream(response.stream).addCallback(got_xml) query = davxml.PropertyFind(davxml.AllProperties(), ) request = SimpleStoreRequest( self, "PROPFIND", calendar_uri, headers=http_headers.Headers({"Depth": "0"}), authPrincipal=self.authPrincipal, ) request.stream = MemoryStream(query.toxml()) return self.send(request, propfind_cb)
def http_GET(self, request): if self.exists(): # Special sharing request on a calendar or address book if self.isCalendarCollection() or self.isAddressBookCollection(): # Check for action=share if request.args: action = request.args.get("action", ("",)) if len(action) != 1: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-action"), "Invalid action parameter: %s" % (action,), )) action = action[0] dispatch = { "share": self.directShare, }.get(action, None) if dispatch is None: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "supported-action"), "Action not supported: %s" % (action,), )) response = (yield dispatch(request)) returnValue(response) else: # FIXME: this should be implemented in storebridge.CalendarObject.render # Look for calendar access restriction on existing resource. parentURL = parentForURL(request.uri) parent = (yield request.locateResource(parentURL)) if isPseudoCalendarCollectionResource(parent): # Check authorization first yield self.authorize(request, (davxml.Read(),)) # Accept header handling accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes()) if accepted_type is None: raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type")) caldata = (yield self.componentForUser()) # Filter any attendee hidden instances caldata = HiddenInstanceFilter().filter(caldata) if self.accessMode: # Non DAV:owner's have limited access to the data isowner = (yield self.isOwner(request)) # Now "filter" the resource calendar data caldata = PrivateEventFilter(self.accessMode, isowner).filter(caldata) response = Response() response.stream = MemoryStream(caldata.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference, format=accepted_type)) response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,))) # Add Schedule-Tag header if property is present if self.scheduleTag: response.headers.setHeader("Schedule-Tag", self.scheduleTag) returnValue(response) # Do normal GET behavior response = (yield super(CalDAVResource, self).http_GET(request)) returnValue(response)
def mkcalendar_cb(response): response = IResponse(response) if response.code != responsecode.CREATED: self.fail("MKCALENDAR failed: %s" % (response.code, )) def propfind_cb(response): response = IResponse(response) if response.code != responsecode.MULTI_STATUS: self.fail("Incorrect response to PROPFIND: %s" % (response.code, )) def got_xml(doc): if not isinstance(doc.root_element, davxml.MultiStatus): self.fail( "PROPFIND response XML root element is not multistatus: %r" % (doc.root_element, )) response = doc.root_element.childOfType(davxml.Response) href = response.childOfType(davxml.HRef) self.failUnless(str(href) == calendar_uri) for propstat in response.childrenOfType( davxml.PropertyStatus): status = propstat.childOfType(davxml.Status) if status.code != responsecode.OK: self.fail( "Unable to read requested properties (%s): %r" % (status, propstat.childOfType( davxml.PropertyContainer).toxml())) container = propstat.childOfType(davxml.PropertyContainer) # # Check CalDAV:supported-calendar-component-set # supported_components = container.childOfType( caldavxml.SupportedCalendarComponentSet) if not supported_components: self.fail( "Expected CalDAV:supported-calendar-component-set element; but got none." ) supported = set(("VEVENT", )) for component in supported_components.children: if component.type in supported: supported.remove(component.type) if supported: self.fail( "Expected supported calendar component types: %s" % (tuple(supported), )) # # Check CalDAV:supported-calendar-data # supported_calendar = container.childOfType( caldavxml.SupportedCalendarData) if not supported_calendar: self.fail( "Expected CalDAV:supported-calendar-data element; but got none." ) for calendar in supported_calendar.children: if calendar.content_type not in ( "text/calendar", "application/calendar+json"): self.fail( "Expected a text/calendar calendar-data type restriction" ) if calendar.version != "2.0": self.fail( "Expected a version 2.0 calendar-data restriction" ) # # Check DAV:supported-report-set # supported_reports = container.childOfType( davxml.SupportedReportSet) if not supported_reports: self.fail( "Expected DAV:supported-report-set element; but got none." ) cal_query = False cal_multiget = False cal_freebusy = False for supported in supported_reports.childrenOfType( davxml.SupportedReport): report = supported.childOfType(davxml.Report) if report.childOfType( caldavxml.CalendarQuery) is not None: cal_query = True if report.childOfType( caldavxml.CalendarMultiGet) is not None: cal_multiget = True if report.childOfType( caldavxml.FreeBusyQuery) is not None: cal_freebusy = True if not cal_query: self.fail( "Expected CalDAV:CalendarQuery element; but got none." ) if not cal_multiget: self.fail( "Expected CalDAV:CalendarMultiGet element; but got none." ) if not cal_freebusy: self.fail( "Expected CalDAV:FreeBusyQuery element; but got none." ) return davXMLFromStream(response.stream).addCallback(got_xml) query = davxml.PropertyFind( davxml.PropertyContainer( caldavxml.SupportedCalendarData(), caldavxml.SupportedCalendarComponentSet(), davxml.SupportedReportSet(), ), ) request = SimpleStoreRequest( self, "PROPFIND", calendar_uri, headers=http_headers.Headers({"Depth": "0"}), authPrincipal=self.authPrincipal, ) request.stream = MemoryStream(query.toxml()) return self.send(request, propfind_cb)
def storeResource( request, source=None, source_uri=None, data=None, destination=None, destination_uri=None, deletesource=False, depth="0" ): """ Function that does common PUT/COPY/MOVE behaviour. @param request: the L{txweb2.server.Request} for the current HTTP request. @param source: the L{DAVFile} for the source resource to copy from, or None if source data is to be read from the request. @param source_uri: the URI for the source resource. @param data: a C{str} to copy data from instead of the request stream. @param destination: the L{DAVFile} for the destination resource to copy into. @param destination_uri: the URI for the destination resource. @param deletesource: True if the source resource is to be deleted on successful completion, False otherwise. @param depth: a C{str} containing the COPY/MOVE Depth header value. @return: status response. """ try: assert request is not None and destination is not None and destination_uri is not None assert (source is None) or (source is not None and source_uri is not None) assert not deletesource or (deletesource and source is not None) except AssertionError: log.error("Invalid arguments to storeResource():") log.error("request=%s\n" % (request,)) log.error("source=%s\n" % (source,)) log.error("source_uri=%s\n" % (source_uri,)) log.error("data=%s\n" % (data,)) log.error("destination=%s\n" % (destination,)) log.error("destination_uri=%s\n" % (destination_uri,)) log.error("deletesource=%s\n" % (deletesource,)) log.error("depth=%s\n" % (depth,)) raise class RollbackState(object): """ This class encapsulates the state needed to rollback the entire PUT/COPY/MOVE transaction, leaving the server state the same as it was before the request was processed. The DoRollback method will actually execute the rollback operations. """ def __init__(self): self.active = True self.source_copy = None self.destination_copy = None self.destination_created = False self.source_deleted = False def Rollback(self): """ Rollback the server state. Do not allow this to raise another exception. If rollback fails then we are going to be left in an awkward state that will need to be cleaned up eventually. """ if self.active: self.active = False log.error("Rollback: rollback") try: if self.source_copy and self.source_deleted: self.source_copy.moveTo(source.fp) log.error("Rollback: source restored %s to %s" % (self.source_copy.path, source.fp.path)) self.source_copy = None self.source_deleted = False if self.destination_copy: destination.fp.remove() log.error("Rollback: destination restored %s to %s" % (self.destination_copy.path, destination.fp.path)) self.destination_copy.moveTo(destination.fp) self.destination_copy = None elif self.destination_created: destination.fp.remove() log.error("Rollback: destination removed %s" % (destination.fp.path,)) self.destination_created = False except: log.error("Rollback: exception caught and not handled: %s" % Failure()) def Commit(self): """ Commit the resource changes by wiping the rollback state. """ if self.active: log.error("Rollback: commit") self.active = False if self.source_copy: self.source_copy.remove() log.error("Rollback: removed source backup %s" % (self.source_copy.path,)) self.source_copy = None if self.destination_copy: self.destination_copy.remove() log.error("Rollback: removed destination backup %s" % (self.destination_copy.path,)) self.destination_copy = None self.destination_created = False self.source_deleted = False rollback = RollbackState() try: """ Handle validation operations here. """ """ Handle rollback setup here. """ # Do quota checks on destination and source before we start messing with adding other files destquota = waitForDeferred(destination.quota(request)) yield destquota destquota = destquota.getResult() if destquota is not None and destination.exists(): old_dest_size = waitForDeferred(destination.quotaSize(request)) yield old_dest_size old_dest_size = old_dest_size.getResult() else: old_dest_size = 0 if source is not None: sourcequota = waitForDeferred(source.quota(request)) yield sourcequota sourcequota = sourcequota.getResult() if sourcequota is not None and source.exists(): old_source_size = waitForDeferred(source.quotaSize(request)) yield old_source_size old_source_size = old_source_size.getResult() else: old_source_size = 0 else: sourcequota = None old_source_size = 0 # We may need to restore the original resource data if the PUT/COPY/MOVE fails, # so rename the original file in case we need to rollback. overwrite = destination.exists() if overwrite: rollback.destination_copy = FilePath(destination.fp.path) rollback.destination_copy.path += ".rollback" destination.fp.copyTo(rollback.destination_copy) else: rollback.destination_created = True if deletesource: rollback.source_copy = FilePath(source.fp.path) rollback.source_copy.path += ".rollback" source.fp.copyTo(rollback.source_copy) """ Handle actual store operations here. """ # Do put or copy based on whether source exists if source is not None: response = maybeDeferred(copy, source.fp, destination.fp, destination_uri, depth) else: datastream = request.stream if data is not None: datastream = MemoryStream(data) md5 = MD5Stream(datastream) response = maybeDeferred(put, md5, destination.fp) response = waitForDeferred(response) yield response response = response.getResult() # Update the MD5 value on the resource if source is not None: # Copy MD5 value from source to destination if source.hasDeadProperty(TwistedGETContentMD5): md5 = source.readDeadProperty(TwistedGETContentMD5) destination.writeDeadProperty(md5) else: # Finish MD5 calc and write dead property md5.close() md5 = md5.getMD5() destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5)) # Update the content-type value on the resource if it is not been copied or moved if source is None: content_type = request.headers.getHeader("content-type") if content_type is not None: destination.writeDeadProperty(davxml.GETContentType.fromString(generateContentType(content_type))) response = IResponse(response) # Do quota check on destination if destquota is not None: # Get size of new/old resources new_dest_size = waitForDeferred(destination.quotaSize(request)) yield new_dest_size new_dest_size = new_dest_size.getResult() diff_size = new_dest_size - old_dest_size if diff_size >= destquota[0]: log.error("Over quota: available %d, need %d" % (destquota[0], diff_size)) raise HTTPError(ErrorResponse( responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded") )) d = waitForDeferred(destination.quotaSizeAdjust(request, diff_size)) yield d d.getResult() if deletesource: # Delete the source resource if sourcequota is not None: delete_size = 0 - old_source_size d = waitForDeferred(source.quotaSizeAdjust(request, delete_size)) yield d d.getResult() delete(source_uri, source.fp, depth) rollback.source_deleted = True # Can now commit changes and forget the rollback details rollback.Commit() yield response return except: # Roll back changes to original server state. Note this may do nothing # if the rollback has already ocurred or changes already committed. rollback.Rollback() raise
def getResponseForRequest(self, request): """ Try to match a request and a response cache entry. We first get the request key and match that, then pull the cache entry and decompose it into tokens and response. We then compare the cached tokens with their current values. If all match, we can return the cached response data. """ try: key = (yield self._hashedRequestKey(request)) self.log.debug("Checking cache for: {key!r}", key=key) _ignore_flags, value = (yield self.getCachePool().get(key)) if value is None: self.log.debug("Not in cache: {key!r}", key=key) returnValue(None) (principalToken, directoryToken, uriToken, childTokens, (code, headers, body)) = cPickle.loads(value) self.log.debug( "Found in cache: {key!r} = {value!r}", key=key, value=( principalToken, directoryToken, uriToken, childTokens, ) ) currentTokens = (yield self._getTokens(request)) if currentTokens[0] != principalToken: self.log.debug( "Principal token doesn't match for {key!r}: {currentToken!r} != {principalToken!r}", key=request.cacheKey, currentToken=currentTokens[0], principalToken=principalToken, ) returnValue(None) if currentTokens[1] != directoryToken: self.log.debug( "Directory Record Token doesn't match for {key!r}: {currentToken!r} != {directoryToken!r}", key=request.cacheKey, currentToken=currentTokens[1], directoryToken=directoryToken, ) returnValue(None) if currentTokens[2] != uriToken: self.log.debug( "URI token doesn't match for {key!r}: {currentToken!r} != {uriToken!r}", key=request.cacheKey, currentToken=currentTokens[2], uriToken=uriToken, ) returnValue(None) for childuri, token in childTokens.items(): currentToken = (yield self._tokenForURI(childuri)) if currentToken != token: self.log.debug( "Child {uri} token doesn't match for {key!r}: {currentToken!r} != {token!r}", uri=childuri, key=request.cacheKey, currentToken=currentToken, token=token, ) returnValue(None) self.log.debug("Response cache matched") r = Response(code, stream=MemoryStream(body)) for key, value in headers.iteritems(): r.headers.setRawHeaders(key, value) returnValue(r) except URINotFoundException, e: self.log.debug("Could not locate URI: {e!r}", e=e) returnValue(None)
def test_get_all_attachments(self): """ Test that action=get-all-attachments works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") attachments = yield shared_object.ownerHome().getAllAttachments() self.assertEqual(len(attachments), 1) self.assertTrue(isinstance(attachments[0], ManagedAttachment)) self.assertEqual(attachments[0].contentType(), MimeType.fromString("text/plain")) self.assertEqual(attachments[0].name(), "test.txt") yield self.commitTransaction(1)
def render(self, request): response = Response() response.stream = MemoryStream(self.renderOutput) return response
def test_get_attachment_links(self): """ Test that action=get-attachment-links works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") cobj1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) calobjID = cobj1.id() yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) attID = attachment.id() managedID = attachment.managedID() yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") links = yield shared_object.ownerHome().getAttachmentLinks() self.assertEqual(len(links), 1) self.assertTrue(isinstance(links[0], AttachmentLink)) self.assertEqual(links[0]._attachmentID, attID) self.assertEqual(links[0]._managedID, managedID) self.assertEqual(links[0]._calendarObjectID, calobjID) yield self.commitTransaction(1)
class TestDKIMVerifier (TestDKIMBase): """ L{DKIMVerifier} support tests. """ class StubRequest(object): def __init__(self, method, uri, headers, body): self.method = method self.uri = uri self.headers = Headers() for name, value in headers: self.headers.addRawHeader(name, value) self.stream = MemoryStream(body) def _makeHeaders(self, headers_pairs): headers = Headers() for name, value in headers_pairs: headers.addRawHeader(name, value) return headers 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=ischedule-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=ischedule-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=ischedule-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=ischedule-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=ischedule-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=ischedule-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=ischedule-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=ischedule-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=ischedule-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=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,), ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,), ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,), ((("DKIM-Signature", "v=1; d=example.com; s=dkim; t=1234; x=%d; a=rsa-sha256; q=dns/txt; c=ischedule-relaxed; h=Originator:Recipient; bh=abc; b=def" % (int(time.time() - 30),)),), False,), ) for headers, result in data: verifier = DKIMVerifier(self._makeHeaders(headers), "") 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:[email protected] ", "originator:mailto:[email protected]\r\n"), ("Recipient", " mailto:[email protected] ,\t mailto:[email protected]\t\t ", "recipient:mailto:[email protected],mailto:[email protected]\r\n"), ("iSchedule-Version", " 1.0 ", "ischedule-version:1.0\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=ischedule-relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=a b c; b=d ef", "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; c=ischedule-relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=a b c; b=", ), ( "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=ischedule-relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=a\t bc", "dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; b= ; c=ischedule-relaxed/simple; h=Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=a bc", ), ) for name, value, result in data: verifier = DKIMVerifier(self._makeHeaders(((name, value,),)), "") if name == "DKIM-Signature": verifier.processDKIMHeader() canonicalized = DKIMUtils.canonicalizeHeader(name, value, verifier.dkim_tags if name == "DKIM-Signature" else None) self.assertEqual(canonicalized, result) def test_extract_headers(self): """ L{DKIMVerifier.extractSignedHeaders} correctly extracts canonicalizes headers. """ data = ( # Count on Recipient ( """Host:example.com Content-Type: text/calendar ; charset = "utf-8" Originator: mailto:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\t\t iSchedule-Version: 1.0 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=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def Cache-Control:no-cache Connection:close """, """content-type:text/calendar ; charset = "utf-8" originator:mailto:[email protected] recipient:mailto:[email protected],mailto:[email protected] ischedule-version:1.0 dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=""" ), # Exact count on Recipient ( """Host:example.com Content-Type: text/calendar ; charset = "utf-8" Originator: mailto:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\t\t Recipient:\t\t mailto:[email protected] iSchedule-Version: 1.0 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=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=def Cache-Control:no-cache Connection:close """, """content-type:text/calendar ; charset = "utf-8" originator:mailto:[email protected] recipient:mailto:[email protected],mailto:[email protected],mailto:[email protected] ischedule-version:1.0 dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=""" ), # Re-ordered Content-Type ( """Host:example.com iSchedule-Version: 1.0 Originator: mailto:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; 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:[email protected] recipient:mailto:[email protected],mailto:[email protected] ischedule-version:1.0 dkim-signature:v=1; d=example.com; s = dkim; t = 1234; a=rsa-sha1; q=dns/txt:http/well-known ; c=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:iSchedule-Version:iSchedule-Message-ID; bh=abc; b=""" ), ) for hdrs, result in data: headers = [hdr.split(":", 1) for hdr in hdrs.splitlines()] verifier = DKIMVerifier(self._makeHeaders(headers), "") verifier.processDKIMHeader() extracted = verifier.extractSignedHeaders() self.assertEqual(extracted, result.replace("\n", "\r\n")) def test_canonicalize_body(self): """ L{DKIMUtils.canonicalizeBody} correctly canonicalizes bodies. """ data = ( ( """Simple""", """Simple\n""", ), ( """Simple\n""", """Simple\n""", ), ( """Simple\n\n""", """Simple\n""", ), ) for text, result in data: self.assertEqual( DKIMUtils.canonicalizeBody(text.replace("\n", "\r\n")), result.replace("\n", "\r\n"), ) @inlineCallbacks 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:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; 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:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; 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:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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=ischedule-relaxed/simple; h=Content-Type:Originator:Recipient:Recipient:iSchedule-Version:iSchedule-Message-ID; 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()] TestPublicKeyLookup.PublicKeyLookup_Testing.keys = keys TestPublicKeyLookup.PublicKeyLookup_Testing.flushCache() verifier = DKIMVerifier(self._makeHeaders(headers), "", key_lookup=(TestPublicKeyLookup.PublicKeyLookup_Testing,)) verifier.processDKIMHeader() pkey = (yield verifier.locatePublicKey()) if result: self.assertTrue(pkey is not None) else: self.assertTrue(pkey is 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, 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 data = (yield allDataFromStream(request.stream)) verifier = DKIMVerifier(request.headers, data, 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:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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:[email protected]",)) ) # Invalid - changed body yield _verify( """Host:example.com Content-Type: text/calendar ; charset = "utf-8" Originator: mailto:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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")), ) # Invalid - extra header yield _verify( """Host:example.com Content-Type: text/calendar ; charset = "utf-8" Originator: mailto:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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.getRawHeaders("Recipient").insert(0, "mailto:[email protected]"), ) # Valid - header yield _verify( """Host:example.com Content-Type: text/calendar ; charset = "utf-8" Originator: mailto:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\t\t Cache-Control:no-cache Connection:close """, """BEGIN:DATA END:DATA """, [DKIMUtils.extractTags("v=DKIM1; p=%s" % (self.public_key_data,))], True, sign_headers=("Originator", "Recipient", "Content-Type",), ) # Invalid - over sign header extra header yield _verify( """Host:example.com Content-Type: text/calendar ; charset = "utf-8" Originator: mailto:[email protected] Recipient: mailto:[email protected] ,\t mailto:[email protected]\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", "Content-Type",), manipulate_request=lambda request: request.headers.addRawHeader("Recipient", ("mailto:[email protected]",)) )
def test_remove_attachment(self): """ Test that action=remove-attachment works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") resourceID = object1.id() attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) managedID = attachment.managedID() yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") yield shared_object.removeAttachment(None, managedID) yield self.commitTransaction(1) cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID) self.assertEqual(cobjs, set()) attachment = yield ManagedAttachment.load(self.theTransactionUnderTest(0), resourceID, managedID) self.assertTrue(attachment is None) yield self.commitTransaction(0)
(caldavxml.ReadFreeBusy(), )) except NumberOfMatchesWithinLimits: log.error("Too many matching components in free-busy report") raise HTTPError( ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits(), "Too many components")) except TimeRangeLowerLimit, e: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, caldavxml.MinDateTime(), "Time-range value too far in the past. Must be on or after %s." % (str(e.limit), ))) except TimeRangeUpperLimit, e: raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, caldavxml.MaxDateTime(), "Time-range value too far in the future. Must be on or before %s." % (str(e.limit), ))) # Now build a new calendar object with the free busy info we have fbcalendar = report_common.buildFreeBusyResult(fbinfo, timerange) response = Response() response.stream = MemoryStream(fbcalendar.getText(accepted_type)) response.headers.setHeader( "content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type, ))) returnValue(response)