Эх сурвалжийг харах

First pass at implementing class 1 WebDAV server

Pierre-Olivier Latour 11 жил өмнө
parent
commit
efb1f9f4eb

+ 58 - 0
GCDWebDAVServer/GCDWebDAVServer.h

@@ -0,0 +1,58 @@
+/*
+ Copyright (c) 2012-2014, 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.
+ */
+
+// Requires HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2" in Xcode build settings
+
+#import "GCDWebServer.h"
+
+@class GCDWebDAVServer;
+
+@protocol GCDWebDAVServerDelegate <NSObject>
+@optional
+- (void)davServer:(GCDWebDAVServer*)uploader didDownloadFileAtPath:(NSString*)path;
+- (void)davServer:(GCDWebDAVServer*)uploader didUploadFileAtPath:(NSString*)path;
+- (void)davServer:(GCDWebDAVServer*)uploader didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
+- (void)davServer:(GCDWebDAVServer*)uploader didCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
+- (void)davServer:(GCDWebDAVServer*)uploader didDeleteItemAtPath:(NSString*)path;
+- (void)davServer:(GCDWebDAVServer*)uploader didCreateDirectoryAtPath:(NSString*)path;
+@end
+
+@interface GCDWebDAVServer : GCDWebServer
+@property(nonatomic, readonly) NSString* uploadDirectory;
+@property(nonatomic, assign) id<GCDWebDAVServerDelegate> delegate;
+@property(nonatomic, copy) NSArray* allowedFileExtensions;  // Default is nil i.e. all file extensions are allowed
+@property(nonatomic) BOOL showHiddenFiles;  // Default is NO
+- (id)initWithUploadDirectory:(NSString*)path;
+@end
+
+@interface GCDWebDAVServer (Subclassing)
+- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath;  // Default implementation returns YES
+- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;  // Default implementation returns YES
+- (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;  // Default implementation returns YES
+- (BOOL)shouldDeleteItemAtPath:(NSString*)path;  // Default implementation returns YES
+- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path;  // Default implementation returns YES
+@end

+ 544 - 0
GCDWebDAVServer/GCDWebDAVServer.m

@@ -0,0 +1,544 @@
+/*
+ Copyright (c) 2012-2014, 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.
+ */
+
+// WebDAV specifications: http://webdav.org/specs/rfc4918.html
+
+#import <libxml/parser.h>
+
+#import "GCDWebDAVServer.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
+};
+
+@interface GCDWebDAVServer () {
+@private
+  NSString* _uploadDirectory;
+  id<GCDWebDAVServerDelegate> __unsafe_unretained _delegate;
+  NSArray* _allowedExtensions;
+  BOOL _showHidden;
+}
+@end
+
+@implementation GCDWebDAVServer (Methods)
+
+- (BOOL)_checkFileExtension:(NSString*)fileName {
+  if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
+    return NO;
+  }
+  return YES;
+}
+
+- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
+  GCDWebServerResponse* response = [GCDWebServerResponse response];
+  [response setValue:@"1" forAdditionalHeader:@"DAV"];  // Class 1
+  return response;
+}
+
+- (GCDWebServerResponse*)performHEAD:(GCDWebServerRequest*)request {
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  NSError* error = nil;
+  NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:absolutePath error:&error];
+  if (!attributes) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound underlyingError:error message:@"Failed retrieving attributes for \"%@\"", relativePath];
+  }
+  
+  GCDWebServerResponse* response = [GCDWebServerResponse response];
+  if ([[attributes fileType] isEqualToString:NSFileTypeRegular]) {
+    [response setValue:GCDWebServerGetMimeTypeForExtension([absolutePath pathExtension]) forAdditionalHeader:@"Content-Type"];
+    [response setValue:[NSString stringWithFormat:@"%llu", [attributes fileSize]] forAdditionalHeader:@"Content-Length"];
+  }
+  return response;
+}
+
+- (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request {
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  BOOL isDirectory = YES;
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  if (isDirectory) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"\"%@\" is not a file", relativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(davServer:didDownloadFileAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_delegate davServer:self didDownloadFileAtPath:absolutePath];
+    });
+  }
+  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:relativePath];
+  if (![absolutePath hasPrefix:_uploadDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", 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:@"."] && !_showHidden) || ![self _checkFileExtension:fileName]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", fileName];
+  }
+  
+  if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.filePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not allowed", relativePath];
+  }
+  
+  [[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL];
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] moveItemAtPath:request.filePath toPath:absolutePath error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(davServer:didUploadFileAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_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:relativePath];
+  if (![absolutePath hasPrefix:_uploadDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  if (![self shouldDeleteItemAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not allowed", relativePath];
+  }
+  
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(davServer:didDeleteItemAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_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:relativePath];
+  if (![absolutePath hasPrefix:_uploadDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", 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 (!_showHidden && [directoryName hasPrefix:@"."]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Directory name \"%@\" is not allowed", directoryName];
+  }
+  
+  if (![self shouldCreateDirectoryAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not allowed", 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];
+  }
+  
+  if ([_delegate respondsToSelector:@selector(davServer:didCreateDirectoryAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [_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:srcRelativePath];
+  if (![srcAbsolutePath hasPrefix:_uploadDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
+  }
+  
+  NSString* dstRelativePath = [request.headers objectForKey:@"Destination"];
+  NSRange range = [dstRelativePath rangeOfString:[request.headers objectForKey:@"Host"]];
+  if ((dstRelativePath == nil) || (range.location == NSNotFound)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Malformed 'Destination' header: %@", dstRelativePath];
+  }
+  dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+  NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath];
+  if (![dstAbsolutePath hasPrefix:_uploadDirectory]) {
+    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* fileName = [dstAbsolutePath lastPathComponent];
+  if ((!_showHidden && [fileName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:fileName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Destination name \"%@\" is not allowed", fileName];
+  }
+  
+  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 allowed", srcRelativePath, dstRelativePath];
+    }
+  } else {
+    if (![self shouldCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
+      return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not allowed", 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 ([_delegate respondsToSelector:@selector(davServer:didMoveItemFromPath:toPath:)]) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        [_delegate davServer:self didMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
+      });
+    }
+  } else {
+    if ([_delegate respondsToSelector:@selector(davServer:didCopyItemFromPath:toPath:)]) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        [_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 {
+  CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL, CFSTR("<&>?+"), kCFStringEncodingUTF8);
+  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]) {
+        NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
+        formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
+        formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
+        formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'";
+        [xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", [formatter stringFromDate:[attributes fileCreationDate]]];
+      }
+      
+      if ((properties & kDAVProperty_LastModified) && [attributes objectForKey:NSFileModificationDate]) {
+        NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
+        formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
+        formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
+        formatter.dateFormat = @"EEE', 'd' 'MMM' 'yyyy' 'HH:mm:ss' GMT'";
+        [xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", [formatter stringFromDate:[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);
+  }
+}
+
+- (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) {
+    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 {
+        NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
+        [self logError:@"Invalid DAV properties\n%@", string];
+#if !__has_feature(objc_arc)
+        [string release];
+#endif
+      }
+      xmlFreeDoc(document);
+    }
+  } else {
+    properties = kDAVAllProperties;
+  }
+  
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  NSError* error = nil;
+  NSArray* items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
+  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 (_showHidden || ![item hasPrefix:@"."]) {
+        [self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString];
+      }
+    }
+  }
+  [xmlString appendString:@"</D:multistatus>"];
+  
+  GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]
+                                                                      contentType:@"application/xml; charset=\"utf-8\""];
+  response.statusCode = kGCDWebServerHTTPStatusCode_MultiStatus;
+  return response;
+}
+
+@end
+
+@implementation GCDWebDAVServer
+
+@synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden;
+
+- (id)initWithUploadDirectory:(NSString*)path {
+  if ((self = [super init])) {
+    _uploadDirectory = [[path stringByStandardizingPath] 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 HEAD method
+    [self addDefaultHandlerForMethod:@"HEAD" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performHEAD:request];
+    }];
+    
+    // 9.4 GET method
+    [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];
+    }];
+    
+    // 10.1 OPTIONS method / DAV Header
+    [self addDefaultHandlerForMethod:@"OPTIONS" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performOPTIONS:request];
+    }];
+    
+  }
+  return self;
+}
+
+#if !__has_feature(objc_arc)
+
+- (void)dealloc {
+  [_uploadDirectory release];
+  [_allowedExtensions release];
+  
+  [super dealloc];
+}
+
+#endif
+
+@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

+ 29 - 0
GCDWebServer.xcodeproj/project.pbxproj

@@ -54,6 +54,10 @@
 		E2A0E80218F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */; };
 		E2A0E80518F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */; };
 		E2A0E80618F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */; };
+		E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
+		E2A0E80B18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
+		E2A0E80D18F35C9A00C580B1 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */; };
+		E2A0E80F18F35CA300C580B1 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A0E80E18F35CA300C580B1 /* libxml2.dylib */; };
 		E2B0D4A718F13495009A7927 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2B0D4A618F13495009A7927 /* libz.dylib */; };
 		E2B0D4A918F134A8009A7927 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2B0D4A818F134A8009A7927 /* libz.dylib */; };
 		E2BE850A18E77ECA0061360B /* GCDWebUploader.bundle in Resources */ = {isa = PBXBuildFile; fileRef = E2BE850718E77ECA0061360B /* GCDWebUploader.bundle */; };
@@ -131,6 +135,10 @@
 		E2A0E80018F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerMultiPartFormRequest.m; sourceTree = "<group>"; };
 		E2A0E80318F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerURLEncodedFormRequest.h; sourceTree = "<group>"; };
 		E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerURLEncodedFormRequest.m; sourceTree = "<group>"; };
+		E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebDAVServer.h; sourceTree = "<group>"; };
+		E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebDAVServer.m; sourceTree = "<group>"; };
+		E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.1.sdk/usr/lib/libxml2.dylib; sourceTree = DEVELOPER_DIR; };
+		E2A0E80E18F35CA300C580B1 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; };
 		E2A0E81018F3737B00C580B1 /* GCDWebServerHTTPStatusCodes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GCDWebServerHTTPStatusCodes.h; sourceTree = "<group>"; };
 		E2B0D4A618F13495009A7927 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
 		E2B0D4A818F134A8009A7927 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.1.sdk/usr/lib/libz.dylib; sourceTree = DEVELOPER_DIR; };
@@ -148,6 +156,7 @@
 				E2BE851118E79DAF0061360B /* SystemConfiguration.framework in Frameworks */,
 				E208D1B3167BB17E00500836 /* CoreServices.framework in Frameworks */,
 				E208D149167B76B700500836 /* CFNetwork.framework in Frameworks */,
+				E2A0E80F18F35CA300C580B1 /* libxml2.dylib in Frameworks */,
 				E2B0D4A718F13495009A7927 /* libz.dylib in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -159,6 +168,7 @@
 				E221129D1690B7BA0048D2B2 /* MobileCoreServices.framework in Frameworks */,
 				E221129B1690B7B10048D2B2 /* UIKit.framework in Frameworks */,
 				E22112991690B7AA0048D2B2 /* CFNetwork.framework in Frameworks */,
+				E2A0E80D18F35C9A00C580B1 /* libxml2.dylib in Frameworks */,
 				E2B0D4A918F134A8009A7927 /* libz.dylib in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -170,6 +180,7 @@
 			isa = PBXGroup;
 			children = (
 				E221127B1690B63A0048D2B2 /* CGDWebServer */,
+				E2A0E80718F3432600C580B1 /* GCDWebDAVServer */,
 				E2BE850618E77ECA0061360B /* GCDWebUploader */,
 				E221128D1690B6470048D2B2 /* Mac */,
 				E22112901690B64F0048D2B2 /* iOS */,
@@ -247,6 +258,7 @@
 				E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */,
 				E221129A1690B7B10048D2B2 /* UIKit.framework */,
 				E22112981690B7AA0048D2B2 /* CFNetwork.framework */,
+				E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */,
 				E2B0D4A818F134A8009A7927 /* libz.dylib */,
 			);
 			name = "iOS Frameworks and Libraries";
@@ -258,11 +270,21 @@
 				E2BE851018E79DAF0061360B /* SystemConfiguration.framework */,
 				E208D1B2167BB17E00500836 /* CoreServices.framework */,
 				E208D148167B76B700500836 /* CFNetwork.framework */,
+				E2A0E80E18F35CA300C580B1 /* libxml2.dylib */,
 				E2B0D4A618F13495009A7927 /* libz.dylib */,
 			);
 			name = "Mac Frameworks and Libraries";
 			sourceTree = "<group>";
 		};
+		E2A0E80718F3432600C580B1 /* GCDWebDAVServer */ = {
+			isa = PBXGroup;
+			children = (
+				E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */,
+				E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */,
+			);
+			path = GCDWebDAVServer;
+			sourceTree = "<group>";
+		};
 		E2BE850618E77ECA0061360B /* GCDWebUploader */ = {
 			isa = PBXGroup;
 			children = (
@@ -368,6 +390,7 @@
 				E2A0E7F518F1D1E500C580B1 /* GCDWebServerStreamingResponse.m in Sources */,
 				E2A0E80118F1D3DE00C580B1 /* GCDWebServerMultiPartFormRequest.m in Sources */,
 				E221128B1690B63A0048D2B2 /* GCDWebServerResponse.m in Sources */,
+				E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */,
 				E2BE850C18E785940061360B /* GCDWebUploader.m in Sources */,
 				E221128F1690B6470048D2B2 /* main.m in Sources */,
 				E2A0E7F118F1D12E00C580B1 /* GCDWebServerFileResponse.m in Sources */,
@@ -378,6 +401,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				E2A0E80B18F3432600C580B1 /* GCDWebDAVServer.m in Sources */,
 				E22112861690B63A0048D2B2 /* GCDWebServer.m in Sources */,
 				E276647D18F3BC2100A034BA /* GCDWebServerErrorResponse.m in Sources */,
 				E2A0E7EE18F1D03700C580B1 /* GCDWebServerDataResponse.m in Sources */,
@@ -437,6 +461,7 @@
 			buildSettings = {
 				CLANG_ENABLE_OBJC_ARC = YES;
 				GCC_OPTIMIZATION_LEVEL = 0;
+				HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
 				ONLY_ACTIVE_ARCH = YES;
 				WARNING_CFLAGS = (
 					"-Wall",
@@ -451,6 +476,9 @@
 					"-Wno-assign-enum",
 					"-Wno-format-nonliteral",
 					"-Wno-cast-align",
+					"-Wno-padded",
+					"-Wno-documentation",
+					"-Wno-documentation-unknown-command",
 				);
 			};
 			name = Debug;
@@ -461,6 +489,7 @@
 				CLANG_ENABLE_OBJC_ARC = YES;
 				GCC_PREPROCESSOR_DEFINITIONS = NDEBUG;
 				GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+				HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
 				WARNING_CFLAGS = "-Wall";
 			};
 			name = Release;

+ 11 - 2
Mac/main.m

@@ -25,14 +25,18 @@
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#import "GCDWebUploader.h"
+#import "GCDWebServer.h"
 #import "GCDWebServerDataRequest.h"
 #import "GCDWebServerURLEncodedFormRequest.h"
 #import "GCDWebServerDataResponse.h"
 
+#import "GCDWebDAVServer.h"
+
+#import "GCDWebUploader.h"
+
 int main(int argc, const char* argv[]) {
   BOOL success = NO;
-  int mode = (argc == 2 ? MIN(MAX(atoi(argv[1]), 0), 3) : 0);
+  int mode = (argc == 2 ? MIN(MAX(atoi(argv[1]), 0), 4) : 0);
   @autoreleasepool {
     GCDWebServer* webServer = nil;
     switch (mode) {
@@ -90,6 +94,11 @@ int main(int argc, const char* argv[]) {
       }
       
       case 3: {
+        webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:@"/tmp"];
+        break;
+      }
+      
+      case 4: {
         webServer = [[GCDWebUploader alloc] initWithUploadDirectory:@"/tmp"];
         break;
       }

+ 1 - 0
README.md

@@ -16,6 +16,7 @@ Extra built-in features:
 
 Included extensions:
 * [GCDWebUploader](GCDWebUploader/GCDWebUploader.h): subclass of GCDWebServer that implements an interface for uploading and downloading files from an iOS app's sandbox using a web browser
+* [GCDWebDAVServer](GCDWebDAVServer/GCDWebDAVServer.h): subclass of GCDWebServer that implements a class 1 [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server
 
 What's not available out of the box but can be implemented on top of the API:
 * Authentication like [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)