def test_statusForFailure_HTTPError(self): """ statusForFailure() for HTTPErrors """ for code in responsecode.RESPONSES: self._check_exception(HTTPError(code), code) self._check_exception(HTTPError(ErrorResponse(code, ("http://twistedmatrix.com/", "bar"))), code)
def _prepareForCopy(destination, destination_uri, request, depth): # # Destination must be a DAV resource # try: destination = IDAVResource(destination) except TypeError: log.err("Attempt to %s to a non-DAV resource: (%s) %s" % (request.method, destination.__class__, destination_uri)) raise HTTPError( StatusResponse( responsecode.FORBIDDEN, "Destination %s is not a WebDAV resource." % (destination_uri, ))) # # FIXME: Right now we don't know how to copy to a non-DAVFile resource. # We may need some more API in IDAVResource. # So far, we need: .exists(), .fp.parent() # import OPSI.web2.dav.static # Lazy import to avoid circular dependency if not isinstance(destination, OPSI.web2.dav.static.DAVFile): log.err("DAV copy between non-DAVFile DAV resources isn't implemented") raise HTTPError( StatusResponse( responsecode.NOT_IMPLEMENTED, "Destination %s is not a DAVFile resource." % (destination_uri, ))) # # Check for existing destination resource # overwrite = request.headers.getHeader("overwrite", True) if destination.exists() and not overwrite: log.err( "Attempt to %s onto existing file without overwrite flag enabled: %s" % (request.method, destination.fp.path)) raise HTTPError( StatusResponse( responsecode.PRECONDITION_FAILED, "Destination %s already exists." % (destination_uri, ))) # # Make sure destination's parent exists # if not destination.fp.parent().isdir(): log.err("Attempt to %s to a resource with no parent: %s" % (request.method, destination.fp.path)) raise HTTPError( StatusResponse(responsecode.CONFLICT, "No parent collection.")) return destination, destination_uri, depth
def defer(): if property.protected: raise HTTPError(StatusResponse( responsecode.FORBIDDEN, "Protected property %s may not be set." % (property.sname(),) )) if property.namespace == twisted_private_namespace: raise HTTPError(StatusResponse( responsecode.FORBIDDEN, "Properties in the %s namespace are private to the server." % (property.sname(),) )) return self.deadProperties().set(property)
def doMove(r): destination, destination_uri, depth = r # # RFC 2518, section 8.9 says that we must act as if the Depth header is set # to infinity, and that the client must omit the Depth header or set it to # infinity. # # This seems somewhat at odds with the notion that a bad request should be # rejected outright; if the client sends a bad depth header, the client is # broken, and section 8 suggests that a bad request should be rejected... # # Let's play it safe for now and ignore broken clients. # if self.fp.isdir() and depth != "infinity": msg = "Client sent illegal depth header value for MOVE: %s" % ( depth, ) log.err(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # May need to add a location header addLocation(request, destination_uri) return move(self.fp, request.uri, destination.fp, destination_uri, depth)
def do(action, property): """ Perform action(property, request) while maintaining an undo queue. """ has = waitForDeferred(self.hasProperty(property, request)) yield has has = has.getResult() if has: oldProperty = waitForDeferred( self.readProperty(property, request)) yield oldProperty oldProperty.getResult() def undo(): return self.writeProperty(oldProperty, request) else: def undo(): return self.removeProperty(property, request) try: x = waitForDeferred(action(property, request)) yield x x.getResult() except ValueError, e: # Convert ValueError exception into HTTPError responses.add( Failure(exc_value=HTTPError( StatusResponse(responsecode.FORBIDDEN, str(e)))), property) yield False return
def http_REPORT(self, request): """ Respond to a REPORT request. (RFC 3253, section 3.6) """ if not self.fp.exists(): log.err("File not found: %s" % (self.fp.path,)) raise HTTPError(responsecode.NOT_FOUND) # # Read request body # try: doc = waitForDeferred(davXMLFromStream(request.stream)) yield doc doc = doc.getResult() except ValueError, e: log.err("Error while handling REPORT body: %s" % (e,)) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
def http_DELETE(self, request): """ Respond to a DELETE request. (RFC 2518, section 8.6) """ if not self.fp.exists(): log.err("File not found: %s" % (self.fp.path, )) raise HTTPError(responsecode.NOT_FOUND) depth = request.headers.getHeader("depth", "infinity") return delete(request.uri, self.fp, depth)
def preconditions_PUT(self, request): if self.fp.exists(): if not self.fp.isfile(): log.err("Unable to PUT to non-file: %s" % (self.fp.path, )) raise HTTPError( StatusResponse( responsecode.FORBIDDEN, "The requested resource exists but is not backed by a regular file." )) resource_is_new = False else: if not self.fp.parent().isdir(): log.err("No such directory: %s" % (self.fp.path, )) raise HTTPError( StatusResponse(responsecode.CONFLICT, "Parent collection resource does not exist.")) resource_is_new = True # # HTTP/1.1 (RFC 2068, section 9.6) requires that we respond with a Not # Implemented error if we get a Content-* header which we don't # recognize and handle properly. # for header, value in request.headers.getAllRawHeaders(): if header.startswith("Content-") and header not in ( #"Content-Base", # Doesn't make sense in PUT? #"Content-Encoding", # Requires that we decode it? "Content-Language", "Content-Length", #"Content-Location", # Doesn't make sense in PUT? "Content-MD5", #"Content-Range", # FIXME: Need to implement this "Content-Type", ): log.err( "Client sent unrecognized content header in PUT request: %s" % (header, )) raise HTTPError( StatusResponse( responsecode.NOT_IMPLEMENTED, "Unrecognized content header %r in request." % (header, )))
def get(self, qname): try: value = self.attrs[self._encode(qname)] except KeyError: raise HTTPError(StatusResponse( responsecode.NOT_FOUND, "No such property: {%s}%s" % qname )) doc = davxml.WebDAVDocument.fromString(value) return doc.root_element
def defer(): if type(property) is tuple: qname = property sname = "{%s}%s" % property else: qname = property.qname() sname = property.sname() if qname in self.liveProperties: raise HTTPError(StatusResponse( responsecode.FORBIDDEN, "Live property %s cannot be deleted." % (sname,) )) if qname[0] == twisted_private_namespace: raise HTTPError(StatusResponse( responsecode.FORBIDDEN, "Properties in the %s namespace are private to the server." % (sname,) )) return self.deadProperties().delete(qname)
def http_MKCOL(self, request): """ Respond to a MKCOL request. (RFC 2518, section 8.3) """ parent = waitForDeferred(request.locateResource(parentForURL(request.uri))) yield parent parent = parent.getResult() if self.fp.exists(): log.err("Attempt to create collection where file exists: %s" % (self.fp.path,)) raise HTTPError(responsecode.NOT_ALLOWED) if not parent.isCollection(): log.err("Attempt to create collection with non-collection parent: %s" % (self.fp.path,)) raise HTTPError(StatusResponse( responsecode.CONFLICT, "Parent resource is not a collection." )) if not self.fp.parent().isdir(): log.err("Attempt to create collection with no parent directory: %s" % (self.fp.path,)) raise HTTPError(StatusResponse( responsecode.INTERNAL_SERVER_ERROR, "The requested resource is not backed by a parent directory." )) # # Read request body # x = waitForDeferred(noDataFromStream(request.stream)) yield x try: x.getResult() except ValueError, e: log.err("Error while handling MKCOL body: %s" % (e,)) raise HTTPError(responsecode.UNSUPPORTED_MEDIA_TYPE)
def prepareForCopy(self, request): # # Get the depth # depth = request.headers.getHeader("depth", "infinity") if depth not in ("0", "infinity"): msg = ("Client sent illegal depth header value: %s" % (depth, )) log.err(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # # Verify this resource exists # if not self.exists(): log.err("File not found: %s" % (self.fp.path, )) raise HTTPError( StatusResponse(responsecode.NOT_FOUND, "Source resource %s not found." % (request.uri, ))) # # Get the destination # destination_uri = request.headers.getHeader("destination") if not destination_uri: msg = "No destination header in %s request." % (request.method, ) log.err(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) d = request.locateResource(destination_uri) d.addCallback(_prepareForCopy, destination_uri, request, depth) return d
def mkcollection(filepath): """ Perform a X{MKCOL} on the given filepath. @param filepath: the L{FilePath} of the collection resource to create. @raise HTTPError: (containing an appropriate response) if the operation fails. @return: a deferred response with a status code of L{responsecode.CREATED} if the destination already exists, or L{responsecode.NO_CONTENT} if the destination was created by the X{MKCOL} operation. """ try: os.mkdir(filepath.path) # Restat filepath because we modified it filepath.restat(False) except: raise HTTPError( statusForFailure( Failure(), "creating directory in MKCOL: %s" % (filepath.path, ))) return succeed(responsecode.CREATED)
def defer(): if type(property) is tuple: qname = property sname = "{%s}%s" % property else: qname = property.qname() sname = property.sname() namespace, name = qname if namespace == dav_namespace: if name == "resourcetype": # Allow live property to be overriden by dead property if self.deadProperties().contains(qname): return self.deadProperties().get(qname) if self.isCollection(): return davxml.ResourceType.collection return davxml.ResourceType.empty if name == "getetag": return davxml.GETETag(self.etag().generate()) if name == "getcontenttype": mimeType = self.contentType() mimeType.params = None # WebDAV getcontenttype property does not include parameters return davxml.GETContentType(generateContentType(mimeType)) if name == "getcontentlength": return davxml.GETContentLength(str(self.contentLength())) if name == "getlastmodified": return davxml.GETLastModified.fromDate(self.lastModified()) if name == "creationdate": return davxml.CreationDate.fromDate(self.creationDate()) if name == "displayname": return davxml.DisplayName(self.displayName()) if name == "supportedlock": return davxml.SupportedLock( davxml.LockEntry(davxml.LockScope.exclusive, davxml.LockType.write), davxml.LockEntry(davxml.LockScope.shared , davxml.LockType.write), ) if name == "acl-restrictions": return davxml.ACLRestrictions() if namespace == twisted_dav_namespace: if name == "resource-class": class ResourceClass (davxml.WebDAVTextElement): namespace = twisted_dav_namespace name = "resource-class" hidden = False return ResourceClass(self.__class__.__name__) if namespace == twisted_private_namespace: raise HTTPError(StatusResponse( responsecode.FORBIDDEN, "Properties in the %s namespace are private to the server." % (sname,) )) return self.deadProperties().get(qname)
def copy(source_filepath, destination_filepath, destination_uri, depth): """ Perform a X{COPY} from the given source and destination filepaths. This will perform a X{DELETE} on the destination if necessary; the caller should check and handle the X{overwrite} header before calling L{copy} (as in L{COPYMOVE.prepareForCopy}). @param source_filepath: a L{FilePath} for the file to copy from. @param destination_filepath: a L{FilePath} for the file to copy to. @param destination_uri: the URI of the destination resource. @param depth: the recursion X{Depth} for the X{COPY} operation, which must be one of "0", "1", or "infinity". @raise HTTPError: (containing a response with a status code of L{responsecode.BAD_REQUEST}) if C{depth} is not "0", "1" or "infinity". @raise HTTPError: (containing an appropriate response) if the operation fails. If C{source_filepath} is a directory, the response will be a L{MultiStatusResponse}. @return: a deferred response with a status code of L{responsecode.CREATED} if the destination already exists, or L{responsecode.NO_CONTENT} if the destination was created by the X{COPY} operation. """ if source_filepath.isfile(): # # Copy the file # log.msg("Copying file %s to %s" % (source_filepath.path, destination_filepath.path)) try: source_file = source_filepath.open() except: raise HTTPError( statusForFailure( Failure(), "opening file for reading: %s" % (source_filepath.path, ))) source_stream = FileStream(source_file) response = waitForDeferred( put(source_stream, destination_filepath, destination_uri)) yield response try: response = response.getResult() finally: source_stream.close() source_file.close() checkResponse(response, "put", responsecode.NO_CONTENT, responsecode.CREATED) yield response return elif source_filepath.isdir(): if destination_filepath.exists(): # # Delete the destination # response = waitForDeferred( delete(destination_uri, destination_filepath)) yield response response = response.getResult() checkResponse(response, "delete", responsecode.NO_CONTENT) success_code = responsecode.NO_CONTENT else: success_code = responsecode.CREATED # # Copy the directory # log.msg("Copying directory %s to %s" % (source_filepath.path, destination_filepath.path)) source_basename = source_filepath.path destination_basename = destination_filepath.path errors = ResponseQueue(source_basename, "COPY", success_code) if destination_filepath.parent().isdir(): if os.path.islink(source_basename): link_destination = os.readlink(source_basename) if link_destination[0] != os.path.sep: link_destination = os.path.join(source_basename, link_destination) try: os.symlink(destination_basename, link_destination) except: errors.add(source_basename, Failure()) else: try: os.mkdir(destination_basename) except: raise HTTPError( statusForFailure( Failure(), "creating directory %s" % (destination_basename, ))) if depth == "0": yield success_code return else: raise HTTPError( StatusResponse( responsecode.CONFLICT, "Parent collection for destination %s does not exist" % (destination_uri, ))) # # Recursive copy # # FIXME: When we report errors, do we report them on the source URI # or on the destination URI? We're using the source URI here. # # FIXME: defer the walk? source_basename_len = len(source_basename) def paths(basepath, subpath): source_path = os.path.join(basepath, subpath) assert source_path.startswith(source_basename) destination_path = os.path.join( destination_basename, source_path[source_basename_len + 1:]) return source_path, destination_path for dir, subdirs, files in os.walk(source_filepath.path, topdown=True): for filename in files: source_path, destination_path = paths(dir, filename) if not os.path.isdir(os.path.dirname(destination_path)): errors.add(source_path, responsecode.NOT_FOUND) else: response = waitForDeferred( copy(FilePath(source_path), FilePath(destination_path), destination_uri, depth)) yield response response = response.getResult() checkResponse(response, "copy", responsecode.CREATED, responsecode.NO_CONTENT) for subdir in subdirs: source_path, destination_path = paths(dir, subdir) log.msg("Copying directory %s to %s" % (source_path, destination_path)) if not os.path.isdir(os.path.dirname(destination_path)): errors.add(source_path, responsecode.CONFLICT) else: if os.path.islink(source_path): link_destination = os.readlink(source_path) if link_destination[0] != os.path.sep: link_destination = os.path.join( source_path, link_destination) try: os.symlink(destination_path, link_destination) except: errors.add(source_path, Failure()) else: try: os.mkdir(destination_path) except: errors.add(source_path, Failure()) yield errors.response() return else: log.err("Unable to COPY to non-file: %s" % (source_filepath.path, )) raise HTTPError( StatusResponse( responsecode.FORBIDDEN, "The requested resource exists but is not backed by a regular file." ))
def put(stream, filepath, uri=None): """ Perform a PUT of the given data stream into the given filepath. @param stream: the stream to write to the destination. @param filepath: the L{FilePath} of the destination file. @param uri: the URI of the destination resource. If the destination exists, if C{uri} is not C{None}, perform a X{DELETE} operation on the destination, but if C{uri} is C{None}, delete the destination directly. Note that whether a L{put} deletes the destination directly vs. performing a X{DELETE} on the destination affects the response returned in the event of an error during deletion. Specifically, X{DELETE} on collections must return a L{MultiStatusResponse} under certain circumstances, whereas X{PUT} isn't required to do so. Therefore, if the caller expects X{DELETE} semantics, it must provide a valid C{uri}. @raise HTTPError: (containing an appropriate response) if the operation fails. @return: a deferred response with a status code of L{responsecode.CREATED} if the destination already exists, or L{responsecode.NO_CONTENT} if the destination was created by the X{PUT} operation. """ log.msg("Writing to file %s" % (filepath.path, )) if filepath.exists(): if uri is None: try: if filepath.isdir(): rmdir(filepath.path) else: os.remove(filepath.path) except: raise HTTPError( statusForFailure(Failure(), "writing to file: %s" % (filepath.path, ))) else: response = waitForDeferred(delete(uri, filepath)) yield response response = response.getResult() checkResponse(response, "delete", responsecode.NO_CONTENT) success_code = responsecode.NO_CONTENT else: success_code = responsecode.CREATED # # Write the contents of the request stream to resource's file # try: resource_file = filepath.open("w") except: raise HTTPError( statusForFailure( Failure(), "opening file for writing: %s" % (filepath.path, ))) try: x = waitForDeferred(readIntoFile(stream, resource_file)) yield x x.getResult() except: raise HTTPError( statusForFailure(Failure(), "writing to file: %s" % (filepath.path, ))) # Restat filepath since we modified the backing file filepath.restat(False) yield success_code
def delete(uri, filepath, depth="infinity"): """ Perform a X{DELETE} operation on the given URI, which is backed by the given filepath. @param filepath: the L{FilePath} to delete. @param depth: the recursion X{Depth} for the X{DELETE} operation, which must be "infinity". @raise HTTPError: (containing a response with a status code of L{responsecode.BAD_REQUEST}) if C{depth} is not "infinity". @raise HTTPError: (containing an appropriate response) if the delete operation fails. If C{filepath} is a directory, the response will be a L{MultiStatusResponse}. @return: a deferred response with a status code of L{responsecode.NO_CONTENT} if the X{DELETE} operation succeeds. """ # # Remove the file(s) # # FIXME: defer if filepath.isdir(): # # RFC 2518, section 8.6 says that we must act as if the Depth header is # set to infinity, and that the client must omit the Depth header or set # it to infinity, meaning that for collections, we will delete all # members. # # This seems somewhat at odds with the notion that a bad request should # be rejected outright; if the client sends a bad depth header, the # client is broken, and RFC 2518, section 8 suggests that a bad request # should be rejected... # # Let's play it safe for now and ignore broken clients. # if depth != "infinity": msg = ("Client sent illegal depth header value for DELETE: %s" % (depth, )) log.err(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # # Recursive delete # # RFC 2518, section 8.6 says that if we get an error deleting a resource # other than the collection in the request-URI, that we must respond # with a multi-status response containing error statuses for each # resource that we fail to delete. It also says we should not return # no-content (success) status, which means that we should continue after # errors, rather than aborting right away. This is interesting in that # it's different from how most operating system tools act (eg. rm) when # recursive filsystem deletes fail. # uri_path = urllib.unquote(urlsplit(uri)[2]) if uri_path[-1] == "/": uri_path = uri_path[:-1] log.msg("Deleting directory %s" % (filepath.path, )) # NOTE: len(uri_path) is wrong if os.sep is not one byte long... meh. request_basename = filepath.path[:-len(uri_path)] request_basename_len = len(request_basename) errors = ResponseQueue(request_basename, "DELETE", responsecode.NO_CONTENT) # FIXME: defer this for dir, subdirs, files in os.walk(filepath.path, topdown=False): for filename in files: path = os.path.join(dir, filename) try: os.remove(path) except: errors.add(path, Failure()) for subdir in subdirs: path = os.path.join(dir, subdir) if os.path.islink(path): try: os.remove(path) except: errors.add(path, Failure()) else: try: os.rmdir(path) except: errors.add(path, Failure()) try: os.rmdir(filepath.path) except: raise HTTPError( statusForFailure(Failure(), "deleting directory: %s" % (filepath.path, ))) response = errors.response() else: # # Delete a file; much simpler, eh? # log.msg("Deleting file %s" % (filepath.path, )) try: os.remove(filepath.path) except: raise HTTPError( statusForFailure(Failure(), "deleting file: %s" % (filepath.path, ))) response = responsecode.NO_CONTENT # Restat filepath since we deleted the backing file filepath.restat(False) return succeed(response)
raise HTTPError(responsecode.NOT_FOUND) # # Read request body # try: doc = waitForDeferred(davXMLFromStream(request.stream)) yield doc doc = doc.getResult() except ValueError, e: log.err("Error while handling REPORT body: %s" % (e,)) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e))) if doc is None: raise HTTPError(StatusResponse( responsecode.BAD_REQUEST, "REPORT request body may not be empty" )) # # Parse request # namespace = doc.root_element.namespace name = doc.root_element.name def to_method(s): ok = string.ascii_letters + string.digits + "_" out = [] for c in s: if c in ok: out.append(c) else:
log.err("Error while handling PROPFIND body: %s" % (e,)) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e))) if doc is None: # No request body means get all properties. search_properties = "all" else: # # Parse request # find = doc.root_element if not isinstance(find, davxml.PropertyFind): error = ("Non-%s element in PROPFIND request body: %s" % (davxml.PropertyFind.sname(), find)) log.err(error) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error)) container = find.children[0] if isinstance(container, davxml.AllProperties): # Get all properties search_properties = "all" elif isinstance(container, davxml.PropertyName): # Get names only search_properties = "names" elif isinstance(container, davxml.PropertyContainer): properties = container.children search_properties = [(p.namespace, p.name) for p in properties] else: raise AssertionError("Unexpected element type in %s: %s" % (davxml.PropertyFind.sname(), container))