diff --git a/GCDWebDAVServer/GCDWebDAVServer.h b/GCDWebDAVServer/GCDWebDAVServer.h new file mode 100644 index 0000000..9f96f4d --- /dev/null +++ b/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 +@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 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 diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m new file mode 100644 index 0000000..bf57bbd --- /dev/null +++ b/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 + +#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 __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:@""]; + [xmlString appendFormat:@"%@", escapedPath]; + [xmlString appendString:@""]; + [xmlString appendString:@""]; + + if (properties & kDAVProperty_ResourceType) { + if (isDirectory) { + [xmlString appendString:@""]; + } else { + [xmlString appendString:@""]; + } + } + + 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:@"%@", [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:@"%@", [formatter stringFromDate:[attributes fileModificationDate]]]; + } + + if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) { + [xmlString appendFormat:@"%llu", [attributes fileSize]]; + } + + [xmlString appendString:@""]; + [xmlString appendString:@"HTTP/1.1 200 OK"]; + [xmlString appendString:@""]; + [xmlString appendString:@"\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:@""]; + [xmlString appendString:@"\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:@""]; + + 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 diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index 79dd7ca..2cb44a8 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/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 = ""; }; E2A0E80318F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerURLEncodedFormRequest.h; sourceTree = ""; }; E2A0E80418F1D4A700C580B1 /* GCDWebServerURLEncodedFormRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerURLEncodedFormRequest.m; sourceTree = ""; }; + E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebDAVServer.h; sourceTree = ""; }; + E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebDAVServer.m; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; + E2A0E80718F3432600C580B1 /* GCDWebDAVServer */ = { + isa = PBXGroup; + children = ( + E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */, + E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */, + ); + path = GCDWebDAVServer; + sourceTree = ""; + }; 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; diff --git a/Mac/main.m b/Mac/main.m index b3862c5..40a583b 100644 --- a/Mac/main.m +++ b/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; } diff --git a/README.md b/README.md index b752461..cfc3b28 100644 --- a/README.md +++ b/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)