def error(self): """ Convert any 2xx codes in the propstat responses to 424 Failed Dependency. """ for index, propstat in enumerate(self.propstats): # Check the status changed_status = False newchildren = [] for child in propstat.children: if isinstance(child, element.Status) and (child.code / 100 == 2): # Change the code newchildren.append( element.Status.fromResponseCode( responsecode.FAILED_DEPENDENCY)) changed_status = True elif changed_status and isinstance( child, element.ResponseDescription): newchildren.append( element.ResponseDescription(responsecode.RESPONSES[ responsecode.FAILED_DEPENDENCY])) else: newchildren.append(child) self.propstats[index] = element.PropertyStatus(*newchildren)
def add(self, path, what): """ Add a response. @param path: a path, which must be a subpath of C{path_basename} as provided to L{__init__}. @param what: a status code or a L{Failure} for the given path. """ assert path.startswith( self.path_basename), "%s does not start with %s" % ( path, self.path_basename) if type(what) is int: code = what error = None message = responsecode.RESPONSES[code] elif isinstance(what, Failure): code = statusForFailure(what) error = errorForFailure(what) message = messageForFailure(what) else: raise AssertionError("Unknown data type: %r" % (what, )) if code > 400: # Error codes only log.error("Error during {method} for {path}: {msg}", method=self.method, path=path, msg=message) uri = path[self.path_basename_len:] children = [] children.append(element.HRef(uri)) children.append(element.Status.fromResponseCode(code)) if error is not None: children.append(error) if message is not None: children.append(element.ResponseDescription(message)) self.responses.append(element.StatusResponse(*children))
def add(self, what, property): """ Add a response. @param what: a status code or a L{Failure} for the given path. @param property: the property whose status is being reported. """ if type(what) is int: code = what error = None message = responsecode.RESPONSES[code] elif isinstance(what, Failure): code = statusForFailure(what) error = errorForFailure(what) message = messageForFailure(what) else: raise AssertionError("Unknown data type: %r" % (what, )) if len(property.children) > 0: # Re-instantiate as empty element. property = element.WebDAVUnknownElement.withName( property.namespace, property.name) if code > 400: # Error codes only log.error("Error during {method} for {property}: {msg}", method=self.method, property=property, msg=message) children = [] children.append(element.PropertyContainer(property)) children.append(element.Status.fromResponseCode(code)) if error is not None: children.append(error) if message is not None: children.append(element.ResponseDescription(message)) self.propstats.append(element.PropertyStatus(*children))
def propertyValue(value): return davxml.ResponseDescription(value)
class DirectoryPrincipalPropertySearchMixIn(object): @inlineCallbacks def report_DAV__principal_property_search(self, request, principal_property_search): """ Generate a principal-property-search REPORT. (RFC 3744, section 9.4) Overrides twisted implementation, targeting only directory-enabled searching. """ # Verify root element if not isinstance(principal_property_search, element.PrincipalPropertySearch): msg = "%s expected as root element, not %s." % ( element.PrincipalPropertySearch.sname(), principal_property_search.sname()) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Should we AND (the default) or OR (if test="anyof")? testMode = principal_property_search.attributes.get("test", "allof") if testMode not in ("allof", "anyof"): msg = "Bad XML: unknown value for test attribute: %s" % ( testMode, ) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) operand = Operand.AND if testMode == "allof" else Operand.OR # Are we narrowing results down to a single CUTYPE? cuType = principal_property_search.attributes.get("type", None) if cuType not in ("INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", None): msg = "Bad XML: unknown value for type attribute: %s" % (cuType, ) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error( "Error in principal-property-search REPORT, Depth set to %s" % (depth, )) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth, ))) # Get any limit value from xml clientLimit = None # Get a single DAV:prop element from the REPORT request body propertiesForResource = None propElement = None propertySearches = [] applyTo = False for child in principal_property_search.children: if child.qname() == (dav_namespace, "prop"): propertiesForResource = prop_common.propertyListForResource propElement = child elif child.qname() == (dav_namespace, "apply-to-principal-collection-set"): applyTo = True elif child.qname() == (dav_namespace, "property-search"): props = child.childOfType(element.PropertyContainer) props.removeWhitespaceNodes() match = child.childOfType(element.Match) caseless = match.attributes.get("caseless", "yes") if caseless not in ("yes", "no"): msg = "Bad XML: unknown value for caseless attribute: %s" % ( caseless, ) log.warn(msg) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, msg)) caseless = (caseless == "yes") matchType = match.attributes.get("match-type", u"contains").encode("utf-8") if matchType not in ("starts-with", "contains", "equals"): msg = "Bad XML: unknown value for match-type attribute: %s" % ( matchType, ) log.warn(msg) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, msg)) # Convert to twext.who.expression form matchType = { "starts-with": MatchType.startsWith, "contains": MatchType.contains, "equals": MatchType.equals }.get(matchType) matchFlags = MatchFlags.caseInsensitive if caseless else MatchFlags.none # Ignore any query strings under three letters matchText = match.toString() # gives us unicode if len(matchText) >= 3: propertySearches.append( (props.children, matchText, matchFlags, matchType)) elif child.qname() == (calendarserver_namespace, "limit"): try: nresults = child.childOfType(customxml.NResults) clientLimit = int(str(nresults)) except ( TypeError, ValueError, ): msg = "Bad XML: unknown value for <limit> element" log.warn(msg) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, msg)) # Run report resultsWereLimited = None resources = [] if applyTo or not hasattr(self, "directory"): for principalCollection in self.principalCollections(): uri = principalCollection.principalCollectionURL() resource = (yield request.locateResource(uri)) if resource: resources.append((resource, uri)) else: resources.append((self, request.uri)) # We need to access a directory service principalCollection = resources[0][0] if not hasattr(principalCollection, "directory"): # Use Twisted's implementation instead in this case result = (yield super(DirectoryPrincipalPropertySearchMixIn, self).report_DAV__principal_property_search( request, principal_property_search)) returnValue(result) dir = principalCollection.directory # See if we can take advantage of the directory fields = [] nonDirectorySearches = [] for props, match, matchFlags, matchType in propertySearches: nonDirectoryProps = [] for prop in props: try: fieldName, match = principalCollection.propertyToField( prop, match) except ValueError, e: raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, str(e))) if fieldName: fields.append((fieldName, match, matchFlags, matchType)) else: nonDirectoryProps.append(prop) if nonDirectoryProps: nonDirectorySearches.append( (nonDirectoryProps, match, matchFlags, matchType)) matchingResources = [] matchcount = 0 # nonDirectorySearches are ignored if fields: records = (yield dir.recordsMatchingFieldsWithCUType(fields, operand=operand, cuType=cuType)) for record in records: resource = yield principalCollection.principalForRecord(record) if resource: matchingResources.append(resource) # We've determined this is a matching resource matchcount += 1 if clientLimit is not None and matchcount >= clientLimit: resultsWereLimited = ("client", matchcount) break if matchcount >= config.MaxPrincipalSearchReportResults: resultsWereLimited = ("server", matchcount) break # Generate the response responses = [] for resource in matchingResources: url = resource.url() yield prop_common.responseForHref(request, responses, element.HRef.fromString(url), resource, propertiesForResource, propElement) if resultsWereLimited is not None: if resultsWereLimited[0] == "server": log.error( "Too many matching resources in principal-property-search report" ) responses.append( element.StatusResponse( element.HRef.fromString(request.uri), element.Status.fromResponseCode( responsecode.INSUFFICIENT_STORAGE_SPACE), element.Error(element.NumberOfMatchesWithinLimits()), element.ResponseDescription("Results limited by %s at %d" % resultsWereLimited), )) returnValue(MultiStatusResponse(responses))
def report_http___calendarserver_org_ns__calendarserver_principal_search( self, request, calendarserver_principal_search): """ Generate a calendarserver-principal-search REPORT. @param request: Request object @param calendarserver_principal_search: CalendarServerPrincipalSearch object """ # Verify root element if not isinstance(calendarserver_principal_search, customxml.CalendarServerPrincipalSearch): msg = "%s expected as root element, not %s." % ( customxml.CalendarServerPrincipalSearch.sname(), calendarserver_principal_search.sname()) log.warn(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error( "Error in calendarserver-principal-search REPORT, Depth set to %s" % (depth, )) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth, ))) tokens, context, applyTo, clientLimit, propElement = extractCalendarServerPrincipalSearchData( calendarserver_principal_search) if not validateTokens(tokens): raise HTTPError( StatusResponse(responsecode.FORBIDDEN, "Insufficient search token length")) # Run report resultsWereLimited = None resources = [] if applyTo or not hasattr(self, "directory"): for principalCollection in self.principalCollections(): uri = principalCollection.principalCollectionURL() resource = (yield request.locateResource(uri)) if resource: resources.append((resource, uri)) else: resources.append((self, request.uri)) # We need to access a directory service principalCollection = resources[0][0] dir = principalCollection.directory matchingResources = [] matchcount = 0 records = (yield dir.recordsMatchingTokens(tokens, context=context)) for record in records: resource = yield principalCollection.principalForRecord(record) if resource: matchingResources.append(resource) # We've determined this is a matching resource matchcount += 1 if clientLimit is not None and matchcount >= clientLimit: resultsWereLimited = ("client", matchcount) break if matchcount >= config.MaxPrincipalSearchReportResults: resultsWereLimited = ("server", matchcount) break # Generate the response responses = [] for resource in matchingResources: url = resource.url() yield prop_common.responseForHref( request, responses, element.HRef.fromString(url), resource, prop_common.propertyListForResource, propElement) if resultsWereLimited is not None: if resultsWereLimited[0] == "server": log.error( "Too many matching resources in calendarserver-principal-search report" ) responses.append( element.StatusResponse( element.HRef.fromString(request.uri), element.Status.fromResponseCode( responsecode.INSUFFICIENT_STORAGE_SPACE), element.Error(element.NumberOfMatchesWithinLimits()), element.ResponseDescription("Results limited by %s at %d" % resultsWereLimited), )) returnValue(MultiStatusResponse(responses))
def report_urn_ietf_params_xml_ns_carddav_addressbook_query( self, request, addressbook_query): """ Generate an addressbook-query REPORT. (CardDAV, section 8.6) """ # Verify root element if addressbook_query.qname() != (carddav_namespace, "addressbook-query"): raise ValueError( "{CardDAV:}addressbook-query expected as root element, not {elementName}." .format(elementName=addressbook_query.sname())) if not self.isCollection(): parent = (yield self.locateParent(request, request.uri)) if not parent.isAddressBookCollection(): log.error( "addressbook-query report is not allowed on a resource outside of an address book collection {parent}", parent=self) raise HTTPError( StatusResponse( responsecode.FORBIDDEN, "Must be address book collection or address book resource") ) responses = [] xmlfilter = addressbook_query.filter filter = Filter(xmlfilter) query = addressbook_query.props limit = addressbook_query.limit assert query is not None if query.qname() == ("DAV:", "allprop"): propertiesForResource = report_common.allPropertiesForResource generate_address_data = False elif query.qname() == ("DAV:", "propname"): propertiesForResource = report_common.propertyNamesForResource generate_address_data = False elif query.qname() == ("DAV:", "prop"): propertiesForResource = report_common.propertyListForResource # Verify that any address-data element matches what we can handle result, message, generate_address_data = report_common.validPropertyListAddressDataTypeVersion( query) if not result: log.error(message) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (carddav_namespace, "supported-address-data"), "Invalid address-data", )) else: raise AssertionError("We shouldn't be here") # Verify that the filter element is valid if (filter is None) or not filter.valid(): log.error("Invalid filter element: %r" % (filter, )) raise HTTPError( ErrorResponse( responsecode.FORBIDDEN, (carddav_namespace, "valid-filter"), "Invalid filter element", )) matchcount = [ 0, ] max_number_of_results = [ config.MaxQueryWithDataResults if generate_address_data else None, ] limited = [ False, ] if limit: clientLimit = int(str(limit.childOfType(NResults))) if max_number_of_results[ 0] is None or clientLimit < max_number_of_results[0]: max_number_of_results[0] = clientLimit @inlineCallbacks def doQuery(addrresource, uri): """ Run a query on the specified address book collection accumulating the query responses. @param addrresource: the L{CalDAVResource} for an address book collection. @param uri: the uri for the address book collecton resource. """ def checkMaxResults(): matchcount[0] += 1 if max_number_of_results[0] is not None and matchcount[ 0] > max_number_of_results[0]: raise NumberOfMatchesWithinLimits(max_number_of_results[0]) @inlineCallbacks def queryAddressBookObjectResource(resource, uri, name, vcard, query_ok=False): """ Run a query on the specified vcard. @param resource: the L{CalDAVResource} for the vcard. @param uri: the uri of the resource. @param name: the name of the resource. @param vcard: the L{Component} vcard read from the resource. """ if query_ok or filter.match(vcard): # Check size of results is within limit checkMaxResults() if name: href = davxml.HRef.fromString(joinURL(uri, name)) else: href = davxml.HRef.fromString(uri) try: yield report_common.responseForHref(request, responses, href, resource, propertiesForResource, query, vcard=vcard) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, we ignore the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during sync: {href}", href=href) @inlineCallbacks def queryDirectoryBackedAddressBook(directoryBackedAddressBook, addressBookFilter): """ """ results, limited[ 0] = yield directoryBackedAddressBook.doAddressBookDirectoryQuery( addressBookFilter, query, max_number_of_results[0]) for vCardResult in results: # match against original filter if different from addressBookFilter if addressBookFilter is filter or filter.match( (yield vCardResult.vCard())): # Check size of results is within limit checkMaxResults() try: yield report_common.responseForHref( request, responses, vCardResult.hRef(), vCardResult, propertiesForResource, query, vcard=(yield vCardResult.vCard())) except ConcurrentModification: # This can happen because of a race-condition between the # time we determine which resources exist and the deletion # of one of these resources in another request. In this # case, we ignore the now missing resource rather # than raise an error for the entire report. log.error("Missing resource during sync: {href}", href=vCardResult.hRef()) if not addrresource.isAddressBookCollection(): #do UID lookup on last part of uri resource_name = urllib.unquote(uri[uri.rfind("/") + 1:]) if resource_name.endswith(".vcf") and len(resource_name) > 4: # see if parent is directory backed address book parent = (yield addrresource.locateParent(request, uri)) # Check whether supplied resource is an address book or an address book object resource if addrresource.isAddressBookCollection(): if addrresource.isDirectoryBackedAddressBookCollection(): yield maybeDeferred(queryDirectoryBackedAddressBook, addrresource, filter) else: # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = (yield addrresource.inheritedACEsforChildren(request)) # Check for disabled access if filteredaces is not None: index_query_ok = True try: # Get list of children that match the search and have read access names = [ name for name, ignore_uid in ( yield addrresource.search(filter)) ] #@UnusedVariable except IndexedSearchException: names = yield addrresource.listChildren() index_query_ok = False if not names: return # Now determine which valid resources are readable and which are not ok_resources = [] yield addrresource.findChildrenFaster( "1", request, lambda x, y: ok_resources.append((x, y)), None, None, None, names, (davxml.Read(), ), inherited_aces=filteredaces) for child, child_uri in ok_resources: child_uri_name = child_uri[child_uri.rfind("/") + 1:] if generate_address_data or not index_query_ok: vcard = yield child.vCard() assert vcard is not None, "vCard {name} is missing from address book collection {collection!r}".format( name=child_uri_name, collection=self) else: vcard = None yield queryAddressBookObjectResource( child, uri, child_uri_name, vcard, query_ok=index_query_ok) else: handled = False resource_name = urllib.unquote(uri[uri.rfind("/") + 1:]) if resource_name.endswith(".vcf") and len(resource_name) > 4: # see if parent is directory backed address book parent = (yield addrresource.locateParent(request, uri)) if parent.isDirectoryBackedAddressBookCollection(): vCardFilter = carddavxml.Filter(*[ carddavxml.PropertyFilter( carddavxml.TextMatch.fromString( resource_name[:-4]), name="UID", # attributes ), ]) vCardFilter = Filter(vCardFilter) yield maybeDeferred(queryDirectoryBackedAddressBook, parent, vCardFilter) handled = True if not handled: vcard = yield addrresource.vCard() yield queryAddressBookObjectResource(addrresource, uri, None, vcard) if limited[0]: raise NumberOfMatchesWithinLimits(matchcount[0]) # Run report taking depth into account try: depth = request.headers.getHeader("depth", "0") yield report_common.applyToAddressBookCollections( self, request, request.uri, depth, doQuery, (davxml.Read(), )) except NumberOfMatchesWithinLimits, e: self.log.info( "Too many matching components in addressbook-query report. Limited to {limit} items", limit=e.maxLimit()) responses.append( davxml.StatusResponse( davxml.HRef.fromString(request.uri), davxml.Status.fromResponseCode( responsecode.INSUFFICIENT_STORAGE_SPACE), davxml.Error(davxml.NumberOfMatchesWithinLimits()), davxml.ResponseDescription( "Results limited to {limit} items".format( limit=e.maxLimit())), ))