123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717 |
- /*
- Copyright (c) 2012-2019, Pierre-Olivier Latour
- All rights reserved.
-
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are met:
- * Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
- * The name of Pierre-Olivier Latour may not be used to endorse
- or promote products derived from this software without specific
- prior written permission.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
- #if !__has_feature(objc_arc)
- #error GCDWebDAVServer requires ARC
- #endif
- // WebDAV specifications: http://webdav.org/specs/rfc4918.html
- // Requires "HEADER_SEARCH_PATHS = $(SDKROOT)/usr/include/libxml2" in Xcode build settings
- #import <libxml/parser.h>
- #import "GCDWebDAVServer.h"
- #import "GCDWebServerFunctions.h"
- #import "GCDWebServerDataRequest.h"
- #import "GCDWebServerFileRequest.h"
- #import "GCDWebServerDataResponse.h"
- #import "GCDWebServerErrorResponse.h"
- #import "GCDWebServerFileResponse.h"
- #define kXMLParseOptions (XML_PARSE_NONET | XML_PARSE_RECOVER | XML_PARSE_NOBLANKS | XML_PARSE_COMPACT | XML_PARSE_NOWARNING | XML_PARSE_NOERROR)
- typedef NS_ENUM(NSInteger, DAVProperties) {
- kDAVProperty_ResourceType = (1 << 0),
- kDAVProperty_CreationDate = (1 << 1),
- kDAVProperty_LastModified = (1 << 2),
- kDAVProperty_ContentLength = (1 << 3),
- kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength
- };
- NS_ASSUME_NONNULL_BEGIN
- @interface GCDWebDAVServer (Methods)
- - (nullable GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request;
- - (nullable GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request;
- - (nullable GCDWebServerResponse*)performPUT:(GCDWebServerFileRequest*)request;
- - (nullable GCDWebServerResponse*)performDELETE:(GCDWebServerRequest*)request;
- - (nullable GCDWebServerResponse*)performMKCOL:(GCDWebServerDataRequest*)request;
- - (nullable GCDWebServerResponse*)performCOPY:(GCDWebServerRequest*)request isMove:(BOOL)isMove;
- - (nullable GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request;
- - (nullable GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request;
- - (nullable GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request;
- @end
- NS_ASSUME_NONNULL_END
- @implementation GCDWebDAVServer
- @dynamic delegate;
- - (instancetype)initWithUploadDirectory:(NSString*)path {
- if ((self = [super init])) {
- _uploadDirectory = [path copy];
- GCDWebDAVServer* __unsafe_unretained server = self;
- // 9.1 PROPFIND method
- [self addDefaultHandlerForMethod:@"PROPFIND"
- requestClass:[GCDWebServerDataRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performPROPFIND:(GCDWebServerDataRequest*)request];
- }];
- // 9.3 MKCOL Method
- [self addDefaultHandlerForMethod:@"MKCOL"
- requestClass:[GCDWebServerDataRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performMKCOL:(GCDWebServerDataRequest*)request];
- }];
- // 9.4 GET & HEAD methods
- [self addDefaultHandlerForMethod:@"GET"
- requestClass:[GCDWebServerRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performGET:request];
- }];
- // 9.6 DELETE method
- [self addDefaultHandlerForMethod:@"DELETE"
- requestClass:[GCDWebServerRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performDELETE:request];
- }];
- // 9.7 PUT method
- [self addDefaultHandlerForMethod:@"PUT"
- requestClass:[GCDWebServerFileRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performPUT:(GCDWebServerFileRequest*)request];
- }];
- // 9.8 COPY method
- [self addDefaultHandlerForMethod:@"COPY"
- requestClass:[GCDWebServerRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performCOPY:request isMove:NO];
- }];
- // 9.9 MOVE method
- [self addDefaultHandlerForMethod:@"MOVE"
- requestClass:[GCDWebServerRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performCOPY:request isMove:YES];
- }];
- // 9.10 LOCK method
- [self addDefaultHandlerForMethod:@"LOCK"
- requestClass:[GCDWebServerDataRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performLOCK:(GCDWebServerDataRequest*)request];
- }];
- // 9.11 UNLOCK method
- [self addDefaultHandlerForMethod:@"UNLOCK"
- requestClass:[GCDWebServerRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performUNLOCK:request];
- }];
- // 10.1 OPTIONS method / DAV Header
- [self addDefaultHandlerForMethod:@"OPTIONS"
- requestClass:[GCDWebServerRequest class]
- processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
- return [server performOPTIONS:request];
- }];
- }
- return self;
- }
- @end
- @implementation GCDWebDAVServer (Methods)
- - (BOOL)_checkFileExtension:(NSString*)fileName {
- if (_allowedFileExtensions && ![_allowedFileExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
- return NO;
- }
- return YES;
- }
- static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
- NSString* userAgentHeader = [request.headers objectForKey:@"User-Agent"];
- return ([userAgentHeader hasPrefix:@"WebDAVFS/"] || [userAgentHeader hasPrefix:@"WebDAVLib/"]); // OS X WebDAV client
- }
- - (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
- GCDWebServerResponse* response = [GCDWebServerResponse response];
- if (_IsMacFinder(request)) {
- [response setValue:@"1, 2" forAdditionalHeader:@"DAV"]; // Classes 1 and 2
- } else {
- [response setValue:@"1" forAdditionalHeader:@"DAV"]; // Class 1
- }
- return response;
- }
- - (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request {
- NSString* relativePath = request.path;
- NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
- BOOL isDirectory = NO;
- if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
- }
- NSString* itemName = [absolutePath lastPathComponent];
- if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading item name \"%@\" is not allowed", itemName];
- }
- // Because HEAD requests are mapped to GET ones, we need to handle directories but it's OK to return nothing per http://webdav.org/specs/rfc4918.html#rfc.section.9.4
- if (isDirectory) {
- return [GCDWebServerResponse response];
- }
- if ([self.delegate respondsToSelector:@selector(davServer:didDownloadFileAtPath:)]) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.delegate davServer:self didDownloadFileAtPath:absolutePath];
- });
- }
- if ([request hasByteRange]) {
- return [GCDWebServerFileResponse responseWithFile:absolutePath byteRange:request.byteRange];
- }
- return [GCDWebServerFileResponse responseWithFile:absolutePath];
- }
- - (GCDWebServerResponse*)performPUT:(GCDWebServerFileRequest*)request {
- if ([request hasByteRange]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Range uploads not supported"];
- }
- NSString* relativePath = request.path;
- NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
- BOOL isDirectory;
- if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
- }
- BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory];
- if (existing && isDirectory) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"PUT not allowed on existing collection \"%@\"", relativePath];
- }
- NSString* fileName = [absolutePath lastPathComponent];
- if (([fileName hasPrefix:@"."] && !_allowHiddenItems) || ![self _checkFileExtension:fileName]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file name \"%@\" is not allowed", fileName];
- }
- if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.temporaryPath]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not permitted", relativePath];
- }
- [[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL];
- NSError* error = nil;
- if (![[NSFileManager defaultManager] moveItemAtPath:request.temporaryPath toPath:absolutePath error:&error]) {
- return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
- }
- if ([self.delegate respondsToSelector:@selector(davServer:didUploadFileAtPath:)]) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.delegate davServer:self didUploadFileAtPath:absolutePath];
- });
- }
- return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
- }
- - (GCDWebServerResponse*)performDELETE:(GCDWebServerRequest*)request {
- NSString* depthHeader = [request.headers objectForKey:@"Depth"];
- if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
- }
- NSString* relativePath = request.path;
- NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
- BOOL isDirectory = NO;
- if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
- }
- NSString* itemName = [absolutePath lastPathComponent];
- if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
- }
- if (![self shouldDeleteItemAtPath:absolutePath]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
- }
- NSError* error = nil;
- if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
- return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
- }
- if ([self.delegate respondsToSelector:@selector(davServer:didDeleteItemAtPath:)]) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.delegate davServer:self didDeleteItemAtPath:absolutePath];
- });
- }
- return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
- }
- - (GCDWebServerResponse*)performMKCOL:(GCDWebServerDataRequest*)request {
- if ([request hasBody] && (request.contentLength > 0)) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_UnsupportedMediaType message:@"Unexpected request body for MKCOL method"];
- }
- NSString* relativePath = request.path;
- NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
- BOOL isDirectory;
- if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
- }
- NSString* directoryName = [absolutePath lastPathComponent];
- if (!_allowHiddenItems && [directoryName hasPrefix:@"."]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
- }
- if (![self shouldCreateDirectoryAtPath:absolutePath]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
- }
- NSError* error = nil;
- if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
- return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
- }
- #ifdef __GCDWEBSERVER_ENABLE_TESTING__
- NSString* creationDateHeader = [request.headers objectForKey:@"X-GCDWebServer-CreationDate"];
- if (creationDateHeader) {
- NSDate* date = GCDWebServerParseISO8601(creationDateHeader);
- if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate : date} ofItemAtPath:absolutePath error:&error]) {
- return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed setting creation date for directory \"%@\"", relativePath];
- }
- }
- #endif
- if ([self.delegate respondsToSelector:@selector(davServer:didCreateDirectoryAtPath:)]) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.delegate davServer:self didCreateDirectoryAtPath:absolutePath];
- });
- }
- return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Created];
- }
- - (GCDWebServerResponse*)performCOPY:(GCDWebServerRequest*)request isMove:(BOOL)isMove {
- if (!isMove) {
- NSString* depthHeader = [request.headers objectForKey:@"Depth"]; // TODO: Support "Depth: 0"
- if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
- }
- }
- NSString* srcRelativePath = request.path;
- NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(srcRelativePath)];
- NSString* dstRelativePath = [request.headers objectForKey:@"Destination"];
- NSRange range = [dstRelativePath rangeOfString:(NSString*)[request.headers objectForKey:@"Host"]];
- if ((dstRelativePath == nil) || (range.location == NSNotFound)) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Malformed 'Destination' header: %@", dstRelativePath];
- }
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
- #pragma clang diagnostic pop
- NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(dstRelativePath)];
- if (!dstAbsolutePath) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
- }
- BOOL isDirectory;
- if (![[NSFileManager defaultManager] fileExistsAtPath:[dstAbsolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Invalid destination \"%@\"", dstRelativePath];
- }
- NSString* srcName = [srcAbsolutePath lastPathComponent];
- if ((!_allowHiddenItems && [srcName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:srcName])) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ from item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", srcName];
- }
- NSString* dstName = [dstAbsolutePath lastPathComponent];
- if ((!_allowHiddenItems && [dstName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:dstName])) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", dstName];
- }
- NSString* overwriteHeader = [request.headers objectForKey:@"Overwrite"];
- BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:dstAbsolutePath];
- if (existing && ((isMove && ![overwriteHeader isEqualToString:@"T"]) || (!isMove && [overwriteHeader isEqualToString:@"F"]))) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_PreconditionFailed message:@"Destination \"%@\" already exists", dstRelativePath];
- }
- if (isMove) {
- if (![self shouldMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
- }
- } else {
- if (![self shouldCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
- }
- }
- NSError* error = nil;
- if (isMove) {
- [[NSFileManager defaultManager] removeItemAtPath:dstAbsolutePath error:NULL];
- if (![[NSFileManager defaultManager] moveItemAtPath:srcAbsolutePath toPath:dstAbsolutePath error:&error]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
- }
- } else {
- if (![[NSFileManager defaultManager] copyItemAtPath:srcAbsolutePath toPath:dstAbsolutePath error:&error]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
- }
- }
- if (isMove) {
- if ([self.delegate respondsToSelector:@selector(davServer:didMoveItemFromPath:toPath:)]) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.delegate davServer:self didMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
- });
- }
- } else {
- if ([self.delegate respondsToSelector:@selector(davServer:didCopyItemFromPath:toPath:)]) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.delegate davServer:self didCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
- });
- }
- }
- return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
- }
- static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name) {
- while (child) {
- if ((child->type == XML_ELEMENT_NODE) && !xmlStrcmp(child->name, name)) {
- return child;
- }
- child = child->next;
- }
- return NULL;
- }
- - (void)_addPropertyResponseForItem:(NSString*)itemPath resource:(NSString*)resourcePath properties:(DAVProperties)properties xmlString:(NSMutableString*)xmlString {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL, CFSTR("<&>?+"), kCFStringEncodingUTF8);
- #pragma clang diagnostic pop
- if (escapedPath) {
- NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL];
- NSString* type = [attributes objectForKey:NSFileType];
- BOOL isFile = [type isEqualToString:NSFileTypeRegular];
- BOOL isDirectory = [type isEqualToString:NSFileTypeDirectory];
- if ((isFile && [self _checkFileExtension:itemPath]) || isDirectory) {
- [xmlString appendString:@"<D:response>"];
- [xmlString appendFormat:@"<D:href>%@</D:href>", escapedPath];
- [xmlString appendString:@"<D:propstat>"];
- [xmlString appendString:@"<D:prop>"];
- if (properties & kDAVProperty_ResourceType) {
- if (isDirectory) {
- [xmlString appendString:@"<D:resourcetype><D:collection/></D:resourcetype>"];
- } else {
- [xmlString appendString:@"<D:resourcetype/>"];
- }
- }
- if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) {
- [xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", GCDWebServerFormatISO8601((NSDate*)[attributes fileCreationDate])];
- }
- if ((properties & kDAVProperty_LastModified) && isFile && [attributes objectForKey:NSFileModificationDate]) { // Last modification date is not useful for directories as it changes implicitely and 'Last-Modified' header is not provided for directories anyway
- [xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", GCDWebServerFormatRFC822((NSDate*)[attributes fileModificationDate])];
- }
- if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) {
- [xmlString appendFormat:@"<D:getcontentlength>%llu</D:getcontentlength>", [attributes fileSize]];
- }
- [xmlString appendString:@"</D:prop>"];
- [xmlString appendString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
- [xmlString appendString:@"</D:propstat>"];
- [xmlString appendString:@"</D:response>\n"];
- }
- CFRelease(escapedPath);
- } else {
- [self logError:@"Failed escaping path: %@", itemPath];
- }
- }
- - (GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request {
- NSInteger depth;
- NSString* depthHeader = [request.headers objectForKey:@"Depth"];
- if ([depthHeader isEqualToString:@"0"]) {
- depth = 0;
- } else if ([depthHeader isEqualToString:@"1"]) {
- depth = 1;
- } else {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader]; // TODO: Return 403 / propfind-finite-depth for "infinity" depth
- }
- DAVProperties properties = 0;
- if (request.data.length) {
- BOOL success = YES;
- xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
- if (document) {
- xmlNodePtr rootNode = _XMLChildWithName(document->children, (const xmlChar*)"propfind");
- xmlNodePtr allNode = rootNode ? _XMLChildWithName(rootNode->children, (const xmlChar*)"allprop") : NULL;
- xmlNodePtr propNode = rootNode ? _XMLChildWithName(rootNode->children, (const xmlChar*)"prop") : NULL;
- if (allNode) {
- properties = kDAVAllProperties;
- } else if (propNode) {
- xmlNodePtr node = propNode->children;
- while (node) {
- if (!xmlStrcmp(node->name, (const xmlChar*)"resourcetype")) {
- properties |= kDAVProperty_ResourceType;
- } else if (!xmlStrcmp(node->name, (const xmlChar*)"creationdate")) {
- properties |= kDAVProperty_CreationDate;
- } else if (!xmlStrcmp(node->name, (const xmlChar*)"getlastmodified")) {
- properties |= kDAVProperty_LastModified;
- } else if (!xmlStrcmp(node->name, (const xmlChar*)"getcontentlength")) {
- properties |= kDAVProperty_ContentLength;
- } else {
- [self logWarning:@"Unknown DAV property requested \"%s\"", node->name];
- }
- node = node->next;
- }
- } else {
- success = NO;
- }
- xmlFreeDoc(document);
- } else {
- success = NO;
- }
- if (!success) {
- NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
- }
- } else {
- properties = kDAVAllProperties;
- }
- NSString* relativePath = request.path;
- NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
- BOOL isDirectory = NO;
- if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
- }
- NSString* itemName = [absolutePath lastPathComponent];
- if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Retrieving properties for item name \"%@\" is not allowed", itemName];
- }
- NSArray* items = nil;
- if (isDirectory) {
- NSError* error = nil;
- items = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error] sortedArrayUsingSelector:@selector(localizedStandardCompare:)];
- if (items == nil) {
- return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
- }
- }
- NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
- [xmlString appendString:@"<D:multistatus xmlns:D=\"DAV:\">\n"];
- if (![relativePath hasPrefix:@"/"]) {
- relativePath = [@"/" stringByAppendingString:relativePath];
- }
- [self _addPropertyResponseForItem:absolutePath resource:relativePath properties:properties xmlString:xmlString];
- if (depth == 1) {
- if (![relativePath hasSuffix:@"/"]) {
- relativePath = [relativePath stringByAppendingString:@"/"];
- }
- for (NSString* item in items) {
- if (_allowHiddenItems || ![item hasPrefix:@"."]) {
- [self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString];
- }
- }
- }
- [xmlString appendString:@"</D:multistatus>"];
- GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:(NSData*)[xmlString dataUsingEncoding:NSUTF8StringEncoding]
- contentType:@"application/xml; charset=\"utf-8\""];
- response.statusCode = kGCDWebServerHTTPStatusCode_MultiStatus;
- return response;
- }
- - (GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request {
- if (!_IsMacFinder(request)) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"];
- }
- NSString* relativePath = request.path;
- NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
- BOOL isDirectory = NO;
- if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
- }
- NSString* depthHeader = [request.headers objectForKey:@"Depth"];
- NSString* timeoutHeader = [request.headers objectForKey:@"Timeout"];
- NSString* scope = nil;
- NSString* type = nil;
- NSString* owner = nil;
- NSString* token = nil;
- BOOL success = YES;
- xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
- if (document) {
- xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"lockinfo");
- if (node) {
- xmlNodePtr scopeNode = _XMLChildWithName(node->children, (const xmlChar*)"lockscope");
- if (scopeNode && scopeNode->children && scopeNode->children->name) {
- scope = [NSString stringWithUTF8String:(const char*)scopeNode->children->name];
- }
- xmlNodePtr typeNode = _XMLChildWithName(node->children, (const xmlChar*)"locktype");
- if (typeNode && typeNode->children && typeNode->children->name) {
- type = [NSString stringWithUTF8String:(const char*)typeNode->children->name];
- }
- xmlNodePtr ownerNode = _XMLChildWithName(node->children, (const xmlChar*)"owner");
- if (ownerNode) {
- ownerNode = _XMLChildWithName(ownerNode->children, (const xmlChar*)"href");
- if (ownerNode && ownerNode->children && ownerNode->children->content) {
- owner = [NSString stringWithUTF8String:(const char*)ownerNode->children->content];
- }
- }
- } else {
- success = NO;
- }
- xmlFreeDoc(document);
- } else {
- success = NO;
- }
- if (!success) {
- NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
- }
- if (![scope isEqualToString:@"exclusive"] || ![type isEqualToString:@"write"] || ![depthHeader isEqualToString:@"0"]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depthHeader, relativePath];
- }
- NSString* itemName = [absolutePath lastPathComponent];
- if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking item name \"%@\" is not allowed", itemName];
- }
- #ifdef __GCDWEBSERVER_ENABLE_TESTING__
- NSString* lockTokenHeader = [request.headers objectForKey:@"X-GCDWebServer-LockToken"];
- if (lockTokenHeader) {
- token = lockTokenHeader;
- }
- #endif
- if (!token) {
- CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
- CFStringRef string = CFUUIDCreateString(kCFAllocatorDefault, uuid);
- token = [NSString stringWithFormat:@"urn:uuid:%@", (__bridge NSString*)string];
- CFRelease(string);
- CFRelease(uuid);
- }
- NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
- [xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
- [xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
- [xmlString appendFormat:@"<D:locktype><D:%@/></D:locktype>\n", type];
- [xmlString appendFormat:@"<D:lockscope><D:%@/></D:lockscope>\n", scope];
- [xmlString appendFormat:@"<D:depth>%@</D:depth>\n", depthHeader];
- if (owner) {
- [xmlString appendFormat:@"<D:owner><D:href>%@</D:href></D:owner>\n", owner];
- }
- if (timeoutHeader) {
- [xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeoutHeader];
- }
- [xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
- NSString* lockroot = [@"http://" stringByAppendingString:[(NSString*)[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]];
- [xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
- [xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
- [xmlString appendString:@"</D:prop>"];
- [self logVerbose:@"WebDAV pretending to lock \"%@\"", relativePath];
- GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:(NSData*)[xmlString dataUsingEncoding:NSUTF8StringEncoding]
- contentType:@"application/xml; charset=\"utf-8\""];
- return response;
- }
- - (GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request {
- if (!_IsMacFinder(request)) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"];
- }
- NSString* relativePath = request.path;
- NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
- BOOL isDirectory = NO;
- if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
- }
- NSString* tokenHeader = [request.headers objectForKey:@"Lock-Token"];
- if (!tokenHeader.length) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Missing 'Lock-Token' header"];
- }
- NSString* itemName = [absolutePath lastPathComponent];
- if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
- return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Unlocking item name \"%@\" is not allowed", itemName];
- }
- [self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath];
- return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
- }
- @end
- @implementation GCDWebDAVServer (Subclassing)
- - (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath {
- return YES;
- }
- - (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
- return YES;
- }
- - (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
- return YES;
- }
- - (BOOL)shouldDeleteItemAtPath:(NSString*)path {
- return YES;
- }
- - (BOOL)shouldCreateDirectoryAtPath:(NSString*)path {
- return YES;
- }
- @end
|