diff --git a/GCDWebDAVServer/GCDWebDAVServer.h b/GCDWebDAVServer/GCDWebDAVServer.h index 84914db..4fc53a0 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.h +++ b/GCDWebDAVServer/GCDWebDAVServer.h @@ -27,6 +27,8 @@ #import "GCDWebServer.h" +NS_ASSUME_NONNULL_BEGIN + @class GCDWebDAVServer; /** @@ -86,7 +88,7 @@ /** * Sets the delegate for the server. */ -@property(nonatomic, assign) id delegate; +@property(nonatomic, weak, nullable) id delegate; /** * Sets which files are allowed to be operated on depending on their extension. @@ -154,3 +156,5 @@ - (BOOL)shouldCreateDirectoryAtPath:(NSString*)path; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebDAVServer/GCDWebDAVServer.m b/GCDWebDAVServer/GCDWebDAVServer.m index 521e8db..54e9f3c 100644 --- a/GCDWebDAVServer/GCDWebDAVServer.m +++ b/GCDWebDAVServer/GCDWebDAVServer.m @@ -55,564 +55,24 @@ typedef NS_ENUM(NSInteger, DAVProperties) { kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength }; -@interface GCDWebDAVServer () { -@private - NSString* _uploadDirectory; - NSArray* _allowedExtensions; - BOOL _allowHidden; -} +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 -@implementation GCDWebDAVServer (Methods) - -// Must match implementation in GCDWebUploader -- (BOOL)_checkSandboxedPath:(NSString*)path { - return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory]; -} - -- (BOOL)_checkFileExtension:(NSString*)fileName { - if (_allowedExtensions && ![_allowedExtensions 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:relativePath]; - BOOL isDirectory = NO; - if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - - NSString* itemName = [absolutePath lastPathComponent]; - if (([itemName hasPrefix:@"."] && !_allowHidden) || (!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:relativePath]; - if (![self _checkSandboxedPath:absolutePath]) { - 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:@"."] && !_allowHidden) || ![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:relativePath]; - BOOL isDirectory = NO; - if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - - NSString* itemName = [absolutePath lastPathComponent]; - if (([itemName hasPrefix:@"."] && !_allowHidden) || (!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:relativePath]; - if (![self _checkSandboxedPath:absolutePath]) { - 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 (!_allowHidden && [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:srcRelativePath]; - if (![self _checkSandboxedPath:srcAbsolutePath]) { - 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]; - } -#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:dstRelativePath]; - if (![self _checkSandboxedPath: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* itemName = [dstAbsolutePath lastPathComponent]; - if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", itemName]; - } - - 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:@""]; - [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]) { - [xmlString appendFormat:@"%@", GCDWebServerFormatISO8601([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:@"%@", GCDWebServerFormatRFC822([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); - } 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:relativePath]; - BOOL isDirectory = NO; - if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - - NSString* itemName = [absolutePath lastPathComponent]; - if (([itemName hasPrefix:@"."] && !_allowHidden) || (!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]; - 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 (_allowHidden || ![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; -} - -- (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:relativePath]; - BOOL isDirectory = NO; - if (![self _checkSandboxedPath:absolutePath] || ![[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 ((!_allowHidden && [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:@""]; - [xmlString appendString:@"\n"]; - [xmlString appendString:@"\n\n"]; - [xmlString appendFormat:@"\n", type]; - [xmlString appendFormat:@"\n", scope]; - [xmlString appendFormat:@"%@\n", depthHeader]; - if (owner) { - [xmlString appendFormat:@"%@\n", owner]; - } - if (timeoutHeader) { - [xmlString appendFormat:@"%@\n", timeoutHeader]; - } - [xmlString appendFormat:@"%@\n", token]; - NSString* lockroot = [@"http://" stringByAppendingString:[[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]]; - [xmlString appendFormat:@"%@\n", lockroot]; - [xmlString appendString:@"\n\n"]; - [xmlString appendString:@""]; - - [self logVerbose:@"WebDAV pretending to lock \"%@\"", relativePath]; - GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[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:relativePath]; - BOOL isDirectory = NO; - if (![self _checkSandboxedPath:absolutePath] || ![[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 ((!_allowHidden && [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 +NS_ASSUME_NONNULL_END @implementation GCDWebDAVServer -@synthesize uploadDirectory = _uploadDirectory, allowedFileExtensions = _allowedExtensions, allowHiddenItems = _allowHidden; - @dynamic delegate; - (instancetype)initWithUploadDirectory:(NSString*)path { @@ -695,6 +155,552 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name @end +@implementation GCDWebDAVServer (Methods) + +// Must match implementation in GCDWebUploader +- (BOOL)_checkSandboxedPath:(NSString*)path { + return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory]; +} + +- (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:relativePath]; + BOOL isDirectory = NO; + if (![self _checkSandboxedPath:absolutePath] || ![[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:relativePath]; + if (![self _checkSandboxedPath:absolutePath]) { + 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:@"."] && !_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:relativePath]; + BOOL isDirectory = NO; + if (![self _checkSandboxedPath:absolutePath] || ![[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:relativePath]; + if (![self _checkSandboxedPath:absolutePath]) { + 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 (!_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:srcRelativePath]; + if (![self _checkSandboxedPath:srcAbsolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", 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:dstRelativePath]; + if (![self _checkSandboxedPath: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* itemName = [dstAbsolutePath lastPathComponent]; + if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", itemName]; + } + + 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:@""]; + [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]) { + [xmlString appendFormat:@"%@", 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:@"%@", GCDWebServerFormatRFC822((NSDate*)[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); + } 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:relativePath]; + BOOL isDirectory = NO; + if (![self _checkSandboxedPath:absolutePath] || ![[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]; + 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 (_allowHiddenItems || ![item hasPrefix:@"."]) { + [self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString]; + } + } + } + [xmlString appendString:@""]; + + 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:relativePath]; + BOOL isDirectory = NO; + if (![self _checkSandboxedPath:absolutePath] || ![[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:@""]; + [xmlString appendString:@"\n"]; + [xmlString appendString:@"\n\n"]; + [xmlString appendFormat:@"\n", type]; + [xmlString appendFormat:@"\n", scope]; + [xmlString appendFormat:@"%@\n", depthHeader]; + if (owner) { + [xmlString appendFormat:@"%@\n", owner]; + } + if (timeoutHeader) { + [xmlString appendFormat:@"%@\n", timeoutHeader]; + } + [xmlString appendFormat:@"%@\n", token]; + NSString* lockroot = [@"http://" stringByAppendingString:[(NSString*)[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]]; + [xmlString appendFormat:@"%@\n", lockroot]; + [xmlString appendString:@"\n\n"]; + [xmlString appendString:@""]; + + [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:relativePath]; + BOOL isDirectory = NO; + if (![self _checkSandboxedPath:absolutePath] || ![[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 { diff --git a/GCDWebServer.xcodeproj/project.pbxproj b/GCDWebServer.xcodeproj/project.pbxproj index 1858186..9e380b1 100644 --- a/GCDWebServer.xcodeproj/project.pbxproj +++ b/GCDWebServer.xcodeproj/project.pbxproj @@ -1237,7 +1237,6 @@ "-Wno-cstring-format-directive", "-Wno-reserved-id-macro", "-Wno-cast-qual", - "-Wno-nullable-to-nonnull-conversion", "-Wno-partial-availability", ); }; diff --git a/GCDWebServer/Core/GCDWebServer.h b/GCDWebServer/Core/GCDWebServer.h index ca0f5a6..beec7b8 100644 --- a/GCDWebServer/Core/GCDWebServer.h +++ b/GCDWebServer/Core/GCDWebServer.h @@ -30,6 +30,8 @@ #import "GCDWebServerRequest.h" #import "GCDWebServerResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerMatchBlock is called for every handler added to the * GCDWebServer whenever a new HTTP request has started (i.e. HTTP headers have @@ -40,7 +42,7 @@ * GCDWebServerRequest instance created with the same basic info. * Otherwise, it simply returns nil. */ -typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery); +typedef GCDWebServerRequest* _Nullable (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery); /** * The GCDWebServerProcessBlock is called after the HTTP request has been fully @@ -52,7 +54,7 @@ typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod, * recommended to return a GCDWebServerErrorResponse on error so more useful * information can be returned to the client. */ -typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(__kindof GCDWebServerRequest* request); +typedef GCDWebServerResponse* _Nullable (^GCDWebServerProcessBlock)(__kindof GCDWebServerRequest* request); /** * The GCDWebServerAsynchronousProcessBlock works like the GCDWebServerProcessBlock @@ -64,7 +66,7 @@ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(__kindof GCDWebServerR * It's however recommended to return a GCDWebServerErrorResponse on error so more * useful information can be returned to the client. */ -typedef void (^GCDWebServerCompletionBlock)(GCDWebServerResponse* response); +typedef void (^GCDWebServerCompletionBlock)(GCDWebServerResponse* _Nullable response); typedef void (^GCDWebServerAsyncProcessBlock)(__kindof GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock); /** @@ -295,7 +297,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; /** * Sets the delegate for the server. */ -@property(nonatomic, assign) id delegate; +@property(nonatomic, weak, nullable) id delegate; /** * Returns YES if the server is currently running. @@ -315,7 +317,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * @warning This property is only valid if the server is running and Bonjour * registration has successfully completed, which can take up to a few seconds. */ -@property(nonatomic, readonly) NSString* bonjourName; +@property(nonatomic, readonly, nullable) NSString* bonjourName; /** * Returns the Bonjour service type used by the server. @@ -323,7 +325,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * @warning This property is only valid if the server is running and Bonjour * registration has successfully completed, which can take up to a few seconds. */ -@property(nonatomic, readonly) NSString* bonjourType; +@property(nonatomic, readonly, nullable) NSString* bonjourType; /** * This method is the designated initializer for the class. @@ -363,7 +365,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * * Returns NO if the server failed to start and sets "error" argument if not NULL. */ -- (BOOL)startWithOptions:(NSDictionary*)options error:(NSError**)error; +- (BOOL)startWithOptions:(nullable NSDictionary*)options error:(NSError** _Nullable)error; /** * Stops the server and prevents it to accepts new HTTP requests. @@ -383,7 +385,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * * @warning This property is only valid if the server is running. */ -@property(nonatomic, readonly) NSURL* serverURL; +@property(nonatomic, readonly, nullable) NSURL* serverURL; /** * Returns the server's Bonjour URL. @@ -393,7 +395,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * Also be aware this property will not automatically update if the Bonjour hostname * has been dynamically changed after the server started running (this should be rare). */ -@property(nonatomic, readonly) NSURL* bonjourServerURL; +@property(nonatomic, readonly, nullable) NSURL* bonjourServerURL; /** * Returns the server's public URL. @@ -401,7 +403,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * @warning This property is only valid if the server is running and NAT port * mapping is active. */ -@property(nonatomic, readonly) NSURL* publicServerURL; +@property(nonatomic, readonly, nullable) NSURL* publicServerURL; /** * Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS) @@ -418,7 +420,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * * Returns NO if the server failed to start. */ -- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name; +- (BOOL)startWithPort:(NSUInteger)port bonjourName:(nullable NSString*)name; #if !TARGET_OS_IPHONE @@ -431,7 +433,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * * @warning This method must be used from the main thread only. */ -- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name; +- (BOOL)runWithPort:(NSUInteger)port bonjourName:(nullable NSString*)name; /** * Runs the server synchronously using -startWithOptions: until a SIGTERM or @@ -442,7 +444,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * * @warning This method must be used from the main thread only. */ -- (BOOL)runWithOptions:(NSDictionary*)options error:(NSError**)error; +- (BOOL)runWithOptions:(nullable NSDictionary*)options error:(NSError** _Nullable)error; #endif @@ -498,7 +500,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * Adds a handler to the server to respond to incoming "GET" HTTP requests * with a specific case-insensitive path with in-memory data. */ -- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge; +- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(nullable NSString*)contentType cacheAge:(NSUInteger)cacheAge; /** * Adds a handler to the server to respond to incoming "GET" HTTP requests @@ -515,7 +517,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * The "indexFilename" argument allows to specify an "index" file name to use * when the request path corresponds to a directory. */ -- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; +- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(nullable NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; @end @@ -612,8 +614,10 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess; * * Returns the number of failed tests or -1 if server failed to start. */ -- (NSInteger)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path; +- (NSInteger)runTestsWithOptions:(nullable NSDictionary*)options inDirectory:(NSString*)path; @end #endif + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Core/GCDWebServer.m b/GCDWebServer/Core/GCDWebServer.m index 7e115d3..837c083 100644 --- a/GCDWebServer/Core/GCDWebServer.m +++ b/GCDWebServer/Core/GCDWebServer.m @@ -132,18 +132,9 @@ static void _ExecuteMainThreadRunLoopSources() { #endif -@interface GCDWebServerHandler () { -@private - GCDWebServerMatchBlock _matchBlock; - GCDWebServerAsyncProcessBlock _asyncProcessBlock; -} -@end - @implementation GCDWebServerHandler -@synthesize matchBlock = _matchBlock, asyncProcessBlock = _asyncProcessBlock; - -- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock { +- (instancetype)initWithMatchBlock:(GCDWebServerMatchBlock _Nonnull)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock _Nonnull)processBlock { if ((self = [super init])) { _matchBlock = [matchBlock copy]; _asyncProcessBlock = [processBlock copy]; @@ -153,9 +144,7 @@ static void _ExecuteMainThreadRunLoopSources() { @end -@interface GCDWebServer () { -@private - id __unsafe_unretained _delegate; +@implementation GCDWebServer { dispatch_queue_t _syncQueue; dispatch_group_t _sourceGroup; NSMutableArray* _handlers; @@ -164,15 +153,10 @@ static void _ExecuteMainThreadRunLoopSources() { CFRunLoopTimerRef _disconnectTimer; // Accessed on main thread only NSDictionary* _options; - NSString* _serverName; - NSString* _authenticationRealm; NSMutableDictionary* _authenticationBasicAccounts; NSMutableDictionary* _authenticationDigestAccounts; Class _connectionClass; - BOOL _mapHEADToGET; CFTimeInterval _disconnectDelay; - dispatch_queue_priority_t _dispatchQueuePriority; - NSUInteger _port; dispatch_source_t _source4; dispatch_source_t _source6; CFNetServiceRef _registrationService; @@ -191,13 +175,6 @@ static void _ExecuteMainThreadRunLoopSources() { BOOL _recording; #endif } -@end - -@implementation GCDWebServer - -@synthesize delegate = _delegate, handlers = _handlers, port = _port, serverName = _serverName, authenticationRealm = _authenticationRealm, - authenticationBasicAccounts = _authenticationBasicAccounts, authenticationDigestAccounts = _authenticationDigestAccounts, - shouldAutomaticallyMapHEADToGET = _mapHEADToGET, dispatchQueuePriority = _dispatchQueuePriority; + (void)initialize { GCDWebServerInitializeFunctions(); @@ -600,7 +577,7 @@ static inline NSString* _EncodeBase64(NSString* string) { }]; } _connectionClass = _GetOption(_options, GCDWebServerOption_ConnectionClass, [GCDWebServerConnection class]); - _mapHEADToGET = [_GetOption(_options, GCDWebServerOption_AutomaticallyMapHEADToGET, @YES) boolValue]; + _shouldAutomaticallyMapHEADToGET = [_GetOption(_options, GCDWebServerOption_AutomaticallyMapHEADToGET, @YES) boolValue]; _disconnectDelay = [_GetOption(_options, GCDWebServerOption_ConnectedStateCoalescingInterval, @1.0) doubleValue]; _dispatchQueuePriority = [_GetOption(_options, GCDWebServerOption_DispatchQueuePriority, @(DISPATCH_QUEUE_PRIORITY_DEFAULT)) longValue]; @@ -1294,9 +1271,9 @@ static void _LogResult(NSString* format, ...) { success = NO; #if !TARGET_OS_IPHONE #if DEBUG - if (GCDWebServerIsTextContentType([expectedHeaders objectForKey:@"Content-Type"])) { - NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; - NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; + if (GCDWebServerIsTextContentType((NSString*)[expectedHeaders objectForKey:@"Content-Type"])) { + NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:(NSString*)[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; + NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:(NSString*)[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; if ([expectedBody writeToFile:expectedPath atomically:YES] && [actualBody writeToFile:actualPath atomically:YES]) { NSTask* task = [[NSTask alloc] init]; [task setLaunchPath:@"/usr/bin/opendiff"]; diff --git a/GCDWebServer/Core/GCDWebServerConnection.h b/GCDWebServer/Core/GCDWebServerConnection.h index d353c8b..420d12a 100644 --- a/GCDWebServer/Core/GCDWebServerConnection.h +++ b/GCDWebServer/Core/GCDWebServerConnection.h @@ -27,6 +27,8 @@ #import "GCDWebServer.h" +NS_ASSUME_NONNULL_BEGIN + @class GCDWebServerHandler; /** @@ -139,7 +141,7 @@ * The default implementation checks for HTTP authentication if applicable * and returns a barebone 401 status code response if authentication failed. */ -- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request; +- (nullable GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request; /** * Assuming a valid HTTP request was received and -preflightRequest: returned nil, @@ -169,7 +171,7 @@ * @warning If the request was invalid (e.g. the HTTP headers were malformed), * the "request" argument will be nil. */ -- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; +- (void)abortRequest:(nullable GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; /** * Called when the connection is closed. @@ -177,3 +179,5 @@ - (void)close; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Core/GCDWebServerConnection.m b/GCDWebServer/Core/GCDWebServerConnection.m index e3e21f4..b59f3f4 100644 --- a/GCDWebServer/Core/GCDWebServerConnection.m +++ b/GCDWebServer/Core/GCDWebServerConnection.m @@ -57,14 +57,25 @@ static NSString* _digestAuthenticationNonce = nil; static int32_t _connectionCounter = 0; #endif -@interface GCDWebServerConnection () { -@private - GCDWebServer* _server; - NSData* _localAddress; - NSData* _remoteAddress; +NS_ASSUME_NONNULL_BEGIN + +@interface GCDWebServerConnection (Read) +- (void)readData:(NSMutableData*)data withLength:(NSUInteger)length completionBlock:(ReadDataCompletionBlock)block; +- (void)readHeaders:(NSMutableData*)headersData withCompletionBlock:(ReadHeadersCompletionBlock)block; +- (void)readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block; +- (void)readNextBodyChunk:(NSMutableData*)chunkData completionBlock:(ReadBodyCompletionBlock)block; +@end + +@interface GCDWebServerConnection (Write) +- (void)writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block; +- (void)writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block; +- (void)writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block; +@end + +NS_ASSUME_NONNULL_END + +@implementation GCDWebServerConnection { CFSocketNativeHandle _socket; - NSUInteger _bytesRead; - NSUInteger _bytesWritten; BOOL _virtualHEAD; CFHTTPMessageRef _requestMessage; @@ -83,266 +94,6 @@ static int32_t _connectionCounter = 0; int _responseFD; #endif } -@end - -@implementation GCDWebServerConnection (Read) - -- (void)_readData:(NSMutableData*)data withLength:(NSUInteger)length completionBlock:(ReadDataCompletionBlock)block { - dispatch_read(_socket, length, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^(dispatch_data_t buffer, int error) { - - @autoreleasepool { - if (error == 0) { - size_t size = dispatch_data_get_size(buffer); - if (size > 0) { - NSUInteger originalLength = data.length; - dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { - [data appendBytes:chunkBytes length:chunkSize]; - return true; - }); - [self didReadBytes:((char*)data.bytes + originalLength) length:(data.length - originalLength)]; - block(YES); - } else { - if (_bytesRead > 0) { - GWS_LOG_ERROR(@"No more data available on socket %i", _socket); - } else { - GWS_LOG_WARNING(@"No data received from socket %i", _socket); - } - block(NO); - } - } else { - GWS_LOG_ERROR(@"Error while reading from socket %i: %s (%i)", _socket, strerror(error), error); - block(NO); - } - } - - }); -} - -- (void)_readHeaders:(NSMutableData*)headersData withCompletionBlock:(ReadHeadersCompletionBlock)block { - GWS_DCHECK(_requestMessage); - [self _readData:headersData - withLength:NSUIntegerMax - completionBlock:^(BOOL success) { - - if (success) { - NSRange range = [headersData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(0, headersData.length)]; - if (range.location == NSNotFound) { - [self _readHeaders:headersData withCompletionBlock:block]; - } else { - NSUInteger length = range.location + range.length; - if (CFHTTPMessageAppendBytes(_requestMessage, headersData.bytes, length)) { - if (CFHTTPMessageIsHeaderComplete(_requestMessage)) { - block([headersData subdataWithRange:NSMakeRange(length, headersData.length - length)]); - } else { - GWS_LOG_ERROR(@"Failed parsing request headers from socket %i", _socket); - block(nil); - } - } else { - GWS_LOG_ERROR(@"Failed appending request headers data from socket %i", _socket); - block(nil); - } - } - } else { - block(nil); - } - - }]; -} - -- (void)_readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block { - GWS_DCHECK([_request hasBody] && ![_request usesChunkedTransferEncoding]); - NSMutableData* bodyData = [[NSMutableData alloc] initWithCapacity:kBodyReadCapacity]; - [self _readData:bodyData - withLength:length - completionBlock:^(BOOL success) { - - if (success) { - if (bodyData.length <= length) { - NSError* error = nil; - if ([_request performWriteData:bodyData error:&error]) { - NSUInteger remainingLength = length - bodyData.length; - if (remainingLength) { - [self _readBodyWithRemainingLength:remainingLength completionBlock:block]; - } else { - block(YES); - } - } else { - GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error); - block(NO); - } - } else { - GWS_LOG_ERROR(@"Unexpected extra content reading request body on socket %i", _socket); - block(NO); - GWS_DNOT_REACHED(); - } - } else { - block(NO); - } - - }]; -} - -static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { - char buffer[size + 1]; - bcopy(bytes, buffer, size); - buffer[size] = 0; - char* end = NULL; - long result = strtol(buffer, &end, 16); - return ((end != NULL) && (*end == 0) && (result >= 0) ? result : NSNotFound); -} - -- (void)_readNextBodyChunk:(NSMutableData*)chunkData completionBlock:(ReadBodyCompletionBlock)block { - GWS_DCHECK([_request hasBody] && [_request usesChunkedTransferEncoding]); - - while (1) { - NSRange range = [chunkData rangeOfData:_CRLFData options:0 range:NSMakeRange(0, chunkData.length)]; - if (range.location == NSNotFound) { - break; - } - NSRange extensionRange = [chunkData rangeOfData:[NSData dataWithBytes:";" length:1] options:0 range:NSMakeRange(0, range.location)]; // Ignore chunk extensions - NSUInteger length = _ScanHexNumber((char*)chunkData.bytes, extensionRange.location != NSNotFound ? extensionRange.location : range.location); - if (length != NSNotFound) { - if (length) { - if (chunkData.length < range.location + range.length + length + 2) { - break; - } - const char* ptr = (char*)chunkData.bytes + range.location + range.length + length; - if ((*ptr == '\r') && (*(ptr + 1) == '\n')) { - NSError* error = nil; - if ([_request performWriteData:[chunkData subdataWithRange:NSMakeRange(range.location + range.length, length)] error:&error]) { - [chunkData replaceBytesInRange:NSMakeRange(0, range.location + range.length + length + 2) withBytes:NULL length:0]; - } else { - GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error); - block(NO); - return; - } - } else { - GWS_LOG_ERROR(@"Missing terminating CRLF sequence for chunk reading request body on socket %i", _socket); - block(NO); - return; - } - } else { - NSRange trailerRange = [chunkData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(range.location, chunkData.length - range.location)]; // Ignore trailers - if (trailerRange.location != NSNotFound) { - block(YES); - return; - } - } - } else { - GWS_LOG_ERROR(@"Invalid chunk length reading request body on socket %i", _socket); - block(NO); - return; - } - } - - [self _readData:chunkData - withLength:NSUIntegerMax - completionBlock:^(BOOL success) { - - if (success) { - [self _readNextBodyChunk:chunkData completionBlock:block]; - } else { - block(NO); - } - - }]; -} - -@end - -@implementation GCDWebServerConnection (Write) - -- (void)_writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block { - dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^{ - [data self]; // Keeps ARC from releasing data too early - }); - dispatch_write(_socket, buffer, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^(dispatch_data_t remainingData, int error) { - - @autoreleasepool { - if (error == 0) { - GWS_DCHECK(remainingData == NULL); - [self didWriteBytes:data.bytes length:data.length]; - block(YES); - } else { - GWS_LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error); - block(NO); - } - } - - }); -#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE - dispatch_release(buffer); -#endif -} - -- (void)_writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block { - GWS_DCHECK(_responseMessage); - CFDataRef data = CFHTTPMessageCopySerializedMessage(_responseMessage); - [self _writeData:(__bridge NSData*)data withCompletionBlock:block]; - CFRelease(data); -} - -- (void)_writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block { - GWS_DCHECK([_response hasBody]); - [_response performReadDataWithCompletion:^(NSData* data, NSError* error) { - - if (data) { - if (data.length) { - if (_response.usesChunkedTransferEncoding) { - const char* hexString = [[NSString stringWithFormat:@"%lx", (unsigned long)data.length] UTF8String]; - size_t hexLength = strlen(hexString); - NSData* chunk = [NSMutableData dataWithLength:(hexLength + 2 + data.length + 2)]; - if (chunk == nil) { - GWS_LOG_ERROR(@"Failed allocating memory for response body chunk for socket %i: %@", _socket, error); - block(NO); - return; - } - char* ptr = (char*)[(NSMutableData*)chunk mutableBytes]; - bcopy(hexString, ptr, hexLength); - ptr += hexLength; - *ptr++ = '\r'; - *ptr++ = '\n'; - bcopy(data.bytes, ptr, data.length); - ptr += data.length; - *ptr++ = '\r'; - *ptr = '\n'; - data = chunk; - } - [self _writeData:data - withCompletionBlock:^(BOOL success) { - - if (success) { - [self _writeBodyWithCompletionBlock:block]; - } else { - block(NO); - } - - }]; - } else { - if (_response.usesChunkedTransferEncoding) { - [self _writeData:_lastChunkData - withCompletionBlock:^(BOOL success) { - - block(success); - - }]; - } else { - block(YES); - } - } - } else { - GWS_LOG_ERROR(@"Failed reading response body for socket %i: %@", _socket, error); - block(NO); - } - - }]; -} - -@end - -@implementation GCDWebServerConnection - -@synthesize server = _server, localAddressData = _localAddress, remoteAddressData = _remoteAddress, totalBytesRead = _bytesRead, totalBytesWritten = _bytesWritten; + (void)initialize { if (_CRLFData == nil) { @@ -370,7 +121,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } - (BOOL)isUsingIPv6 { - const struct sockaddr* localSockAddr = _localAddress.bytes; + const struct sockaddr* localSockAddr = _localAddressData.bytes; return (localSockAddr->sa_family == AF_INET6); } @@ -420,7 +171,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { if (_response) { [self _initializeResponseHeadersWithStatusCode:_response.statusCode]; if (_response.lastModifiedDate) { - CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Last-Modified"), (__bridge CFStringRef)GCDWebServerFormatRFC822(_response.lastModifiedDate)); + CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Last-Modified"), (__bridge CFStringRef)GCDWebServerFormatRFC822((NSDate*)_response.lastModifiedDate)); } if (_response.eTag) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("ETag"), (__bridge CFStringRef)_response.eTag); @@ -444,11 +195,11 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [_response.additionalHeaders enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) { CFHTTPMessageSetHeaderFieldValue(_responseMessage, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); }]; - [self _writeHeadersWithCompletionBlock:^(BOOL success) { + [self writeHeadersWithCompletionBlock:^(BOOL success) { if (success) { if (hasBody) { - [self _writeBodyWithCompletionBlock:^(BOOL successInner) { + [self writeBodyWithCompletionBlock:^(BOOL successInner) { [_response performClose]; // TODO: There's nothing we can do on failure as headers have already been sent @@ -485,18 +236,18 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } if (length) { - [self _readBodyWithRemainingLength:length - completionBlock:^(BOOL success) { + [self readBodyWithRemainingLength:length + completionBlock:^(BOOL success) { - NSError* localError = nil; - if ([_request performClose:&localError]) { - [self _startProcessingRequest]; - } else { - GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); - [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; - } + NSError* localError = nil; + if ([_request performClose:&localError]) { + [self _startProcessingRequest]; + } else { + GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + } - }]; + }]; } else { if ([_request performClose:&error]) { [self _startProcessingRequest]; @@ -516,24 +267,24 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } NSMutableData* chunkData = [[NSMutableData alloc] initWithData:initialData]; - [self _readNextBodyChunk:chunkData - completionBlock:^(BOOL success) { + [self readNextBodyChunk:chunkData + completionBlock:^(BOOL success) { - NSError* localError = nil; - if ([_request performClose:&localError]) { - [self _startProcessingRequest]; - } else { - GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); - [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; - } + NSError* localError = nil; + if ([_request performClose:&localError]) { + [self _startProcessingRequest]; + } else { + GWS_LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); + [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; + } - }]; + }]; } - (void)_readRequestHeaders { _requestMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true); NSMutableData* headersData = [[NSMutableData alloc] initWithCapacity:kHeadersReadCapacity]; - [self _readHeaders:headersData + [self readHeaders:headersData withCompletionBlock:^(NSData* extraData) { if (extraData) { @@ -548,7 +299,8 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { requestURL = [self rewriteRequestURL:requestURL withMethod:requestMethod headers:requestHeaders]; GWS_DCHECK(requestURL); } - NSString* requestPath = requestURL ? GCDWebServerUnescapeURLString(CFBridgingRelease(CFURLCopyPath((CFURLRef)requestURL))) : nil; // Don't use -[NSURL path] which strips the ending slash + NSString* urlPath = requestURL ? CFBridgingRelease(CFURLCopyPath((CFURLRef)requestURL)) : nil; // Don't use -[NSURL path] which strips the ending slash + NSString* requestPath = urlPath ? GCDWebServerUnescapeURLString(urlPath) : nil; NSString* queryString = requestURL ? CFBridgingRelease(CFURLCopyQueryString((CFURLRef)requestURL, NULL)) : nil; // Don't use -[NSURL query] to make sure query is not unescaped; NSDictionary* requestQuery = queryString ? GCDWebServerParseURLEncodedForm(queryString) : @{}; if (requestMethod && requestURL && requestHeaders && requestPath && requestQuery) { @@ -567,7 +319,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { NSString* expectHeader = [requestHeaders objectForKey:@"Expect"]; if (expectHeader) { if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) { // TODO: Actually validate request before continuing - [self _writeData:_continueData + [self writeData:_continueData withCompletionBlock:^(BOOL success) { if (success) { @@ -613,11 +365,11 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { }]; } -- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket { +- (instancetype)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket { if ((self = [super init])) { _server = server; - _localAddress = localAddress; - _remoteAddress = remoteAddress; + _localAddressData = localAddress; + _remoteAddressData = remoteAddress; _socket = socket; GWS_LOG_DEBUG(@"Did open connection on socket %i", _socket); @@ -635,11 +387,11 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } - (NSString*)localAddressString { - return GCDWebServerStringFromSockAddr(_localAddress.bytes, YES); + return GCDWebServerStringFromSockAddr(_localAddressData.bytes, YES); } - (NSString*)remoteAddressString { - return GCDWebServerStringFromSockAddr(_remoteAddress.bytes, YES); + return GCDWebServerStringFromSockAddr(_remoteAddressData.bytes, YES); } - (void)dealloc { @@ -667,6 +419,261 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { @end +@implementation GCDWebServerConnection (Read) + +- (void)readData:(NSMutableData*)data withLength:(NSUInteger)length completionBlock:(ReadDataCompletionBlock)block { + dispatch_read(_socket, length, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^(dispatch_data_t buffer, int error) { + + @autoreleasepool { + if (error == 0) { + size_t size = dispatch_data_get_size(buffer); + if (size > 0) { + NSUInteger originalLength = data.length; + dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { + [data appendBytes:chunkBytes length:chunkSize]; + return true; + }); + [self didReadBytes:((char*)data.bytes + originalLength) length:(data.length - originalLength)]; + block(YES); + } else { + if (_totalBytesRead > 0) { + GWS_LOG_ERROR(@"No more data available on socket %i", _socket); + } else { + GWS_LOG_WARNING(@"No data received from socket %i", _socket); + } + block(NO); + } + } else { + GWS_LOG_ERROR(@"Error while reading from socket %i: %s (%i)", _socket, strerror(error), error); + block(NO); + } + } + + }); +} + +- (void)readHeaders:(NSMutableData*)headersData withCompletionBlock:(ReadHeadersCompletionBlock)block { + GWS_DCHECK(_requestMessage); + [self readData:headersData + withLength:NSUIntegerMax + completionBlock:^(BOOL success) { + + if (success) { + NSRange range = [headersData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(0, headersData.length)]; + if (range.location == NSNotFound) { + [self readHeaders:headersData withCompletionBlock:block]; + } else { + NSUInteger length = range.location + range.length; + if (CFHTTPMessageAppendBytes(_requestMessage, headersData.bytes, length)) { + if (CFHTTPMessageIsHeaderComplete(_requestMessage)) { + block([headersData subdataWithRange:NSMakeRange(length, headersData.length - length)]); + } else { + GWS_LOG_ERROR(@"Failed parsing request headers from socket %i", _socket); + block(nil); + } + } else { + GWS_LOG_ERROR(@"Failed appending request headers data from socket %i", _socket); + block(nil); + } + } + } else { + block(nil); + } + + }]; +} + +- (void)readBodyWithRemainingLength:(NSUInteger)length completionBlock:(ReadBodyCompletionBlock)block { + GWS_DCHECK([_request hasBody] && ![_request usesChunkedTransferEncoding]); + NSMutableData* bodyData = [[NSMutableData alloc] initWithCapacity:kBodyReadCapacity]; + [self readData:bodyData + withLength:length + completionBlock:^(BOOL success) { + + if (success) { + if (bodyData.length <= length) { + NSError* error = nil; + if ([_request performWriteData:bodyData error:&error]) { + NSUInteger remainingLength = length - bodyData.length; + if (remainingLength) { + [self readBodyWithRemainingLength:remainingLength completionBlock:block]; + } else { + block(YES); + } + } else { + GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error); + block(NO); + } + } else { + GWS_LOG_ERROR(@"Unexpected extra content reading request body on socket %i", _socket); + block(NO); + GWS_DNOT_REACHED(); + } + } else { + block(NO); + } + + }]; +} + +static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { + char buffer[size + 1]; + bcopy(bytes, buffer, size); + buffer[size] = 0; + char* end = NULL; + long result = strtol(buffer, &end, 16); + return ((end != NULL) && (*end == 0) && (result >= 0) ? result : NSNotFound); +} + +- (void)readNextBodyChunk:(NSMutableData*)chunkData completionBlock:(ReadBodyCompletionBlock)block { + GWS_DCHECK([_request hasBody] && [_request usesChunkedTransferEncoding]); + + while (1) { + NSRange range = [chunkData rangeOfData:_CRLFData options:0 range:NSMakeRange(0, chunkData.length)]; + if (range.location == NSNotFound) { + break; + } + NSRange extensionRange = [chunkData rangeOfData:[NSData dataWithBytes:";" length:1] options:0 range:NSMakeRange(0, range.location)]; // Ignore chunk extensions + NSUInteger length = _ScanHexNumber((char*)chunkData.bytes, extensionRange.location != NSNotFound ? extensionRange.location : range.location); + if (length != NSNotFound) { + if (length) { + if (chunkData.length < range.location + range.length + length + 2) { + break; + } + const char* ptr = (char*)chunkData.bytes + range.location + range.length + length; + if ((*ptr == '\r') && (*(ptr + 1) == '\n')) { + NSError* error = nil; + if ([_request performWriteData:[chunkData subdataWithRange:NSMakeRange(range.location + range.length, length)] error:&error]) { + [chunkData replaceBytesInRange:NSMakeRange(0, range.location + range.length + length + 2) withBytes:NULL length:0]; + } else { + GWS_LOG_ERROR(@"Failed writing request body on socket %i: %@", _socket, error); + block(NO); + return; + } + } else { + GWS_LOG_ERROR(@"Missing terminating CRLF sequence for chunk reading request body on socket %i", _socket); + block(NO); + return; + } + } else { + NSRange trailerRange = [chunkData rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(range.location, chunkData.length - range.location)]; // Ignore trailers + if (trailerRange.location != NSNotFound) { + block(YES); + return; + } + } + } else { + GWS_LOG_ERROR(@"Invalid chunk length reading request body on socket %i", _socket); + block(NO); + return; + } + } + + [self readData:chunkData + withLength:NSUIntegerMax + completionBlock:^(BOOL success) { + + if (success) { + [self readNextBodyChunk:chunkData completionBlock:block]; + } else { + block(NO); + } + + }]; +} + +@end + +@implementation GCDWebServerConnection (Write) + +- (void)writeData:(NSData*)data withCompletionBlock:(WriteDataCompletionBlock)block { + dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^{ + [data self]; // Keeps ARC from releasing data too early + }); + dispatch_write(_socket, buffer, dispatch_get_global_queue(_server.dispatchQueuePriority, 0), ^(dispatch_data_t remainingData, int error) { + + @autoreleasepool { + if (error == 0) { + GWS_DCHECK(remainingData == NULL); + [self didWriteBytes:data.bytes length:data.length]; + block(YES); + } else { + GWS_LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error); + block(NO); + } + } + + }); +#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE + dispatch_release(buffer); +#endif +} + +- (void)writeHeadersWithCompletionBlock:(WriteHeadersCompletionBlock)block { + GWS_DCHECK(_responseMessage); + CFDataRef data = CFHTTPMessageCopySerializedMessage(_responseMessage); + [self writeData:(__bridge NSData*)data withCompletionBlock:block]; + CFRelease(data); +} + +- (void)writeBodyWithCompletionBlock:(WriteBodyCompletionBlock)block { + GWS_DCHECK([_response hasBody]); + [_response performReadDataWithCompletion:^(NSData* data, NSError* error) { + + if (data) { + if (data.length) { + if (_response.usesChunkedTransferEncoding) { + const char* hexString = [[NSString stringWithFormat:@"%lx", (unsigned long)data.length] UTF8String]; + size_t hexLength = strlen(hexString); + NSData* chunk = [NSMutableData dataWithLength:(hexLength + 2 + data.length + 2)]; + if (chunk == nil) { + GWS_LOG_ERROR(@"Failed allocating memory for response body chunk for socket %i: %@", _socket, error); + block(NO); + return; + } + char* ptr = (char*)[(NSMutableData*)chunk mutableBytes]; + bcopy(hexString, ptr, hexLength); + ptr += hexLength; + *ptr++ = '\r'; + *ptr++ = '\n'; + bcopy(data.bytes, ptr, data.length); + ptr += data.length; + *ptr++ = '\r'; + *ptr = '\n'; + data = chunk; + } + [self writeData:data + withCompletionBlock:^(BOOL success) { + + if (success) { + [self writeBodyWithCompletionBlock:block]; + } else { + block(NO); + } + + }]; + } else { + if (_response.usesChunkedTransferEncoding) { + [self writeData:_lastChunkData + withCompletionBlock:^(BOOL success) { + + block(success); + + }]; + } else { + block(YES); + } + } + } else { + GWS_LOG_ERROR(@"Failed reading response body for socket %i: %@", _socket, error); + block(NO); + } + + }]; +} + +@end + @implementation GCDWebServerConnection (Subclassing) - (BOOL)open { @@ -692,7 +699,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { - (void)didReadBytes:(const void*)bytes length:(NSUInteger)length { GWS_LOG_DEBUG(@"Connection received %lu bytes on socket %i", (unsigned long)length, _socket); - _bytesRead += length; + _totalBytesRead += length; #ifdef __GCDWEBSERVER_ENABLE_TESTING__ if ((_requestFD > 0) && (write(_requestFD, bytes, length) != (ssize_t)length)) { @@ -705,7 +712,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { - (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length { GWS_LOG_DEBUG(@"Connection sent %lu bytes on socket %i", (unsigned long)length, _socket); - _bytesWritten += length; + _totalBytesWritten += length; #ifdef __GCDWEBSERVER_ENABLE_TESTING__ if ((_responseFD > 0) && (write(_responseFD, bytes, length) != (ssize_t)length)) { @@ -722,7 +729,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { // https://tools.ietf.org/html/rfc2617 - (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request { - GWS_LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead); + GWS_LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_totalBytesRead); GCDWebServerResponse* response = nil; if (_server.authenticationBasicAccounts) { __block BOOL authenticated = NO; @@ -772,7 +779,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { } - (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion { - GWS_LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead); + GWS_LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_totalBytesRead); _handler.asyncProcessBlock(request, [completion copy]); } @@ -812,7 +819,7 @@ static inline BOOL _CompareResources(NSString* responseETag, NSString* requestET GWS_DCHECK(_responseMessage == NULL); GWS_DCHECK((statusCode >= 400) && (statusCode < 600)); [self _initializeResponseHeadersWithStatusCode:statusCode]; - [self _writeHeadersWithCompletionBlock:^(BOOL success) { + [self writeHeadersWithCompletionBlock:^(BOOL success) { ; // Nothing more to do }]; GWS_LOG_DEBUG(@"Connection aborted with status code %i on socket %i", (int)statusCode, _socket); @@ -852,9 +859,9 @@ static inline BOOL _CompareResources(NSString* responseETag, NSString* requestET #endif if (_request) { - GWS_LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten); + GWS_LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_totalBytesRead, (unsigned long)_totalBytesWritten); } else { - GWS_LOG_VERBOSE(@"[%@] %@ %i \"(invalid request)\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, (unsigned long)_bytesRead, (unsigned long)_bytesWritten); + GWS_LOG_VERBOSE(@"[%@] %@ %i \"(invalid request)\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, (unsigned long)_totalBytesRead, (unsigned long)_totalBytesWritten); } } diff --git a/GCDWebServer/Core/GCDWebServerFunctions.h b/GCDWebServer/Core/GCDWebServerFunctions.h index e5be05c..0752bce 100644 --- a/GCDWebServer/Core/GCDWebServerFunctions.h +++ b/GCDWebServer/Core/GCDWebServerFunctions.h @@ -27,6 +27,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + #ifdef __cplusplus extern "C" { #endif @@ -42,12 +44,12 @@ NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension); * The legal characters ":@/?&=+" are also escaped to ensure compatibility * with URL encoded forms and URL queries. */ -NSString* GCDWebServerEscapeURLString(NSString* string); +NSString* _Nullable GCDWebServerEscapeURLString(NSString* string); /** * Unescapes a URL percent-encoded string. */ -NSString* GCDWebServerUnescapeURLString(NSString* string); +NSString* _Nullable GCDWebServerUnescapeURLString(NSString* string); /** * Extracts the unescaped names and values from an @@ -63,7 +65,7 @@ NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form); * On iOS, returns the IPv4 or IPv6 address as a string of the WiFi * interface if connected or nil otherwise. */ -NSString* GCDWebServerGetPrimaryIPAddress(BOOL useIPv6); +NSString* _Nullable GCDWebServerGetPrimaryIPAddress(BOOL useIPv6); /** * Converts a date into a string using RFC822 formatting. @@ -79,7 +81,7 @@ NSString* GCDWebServerFormatRFC822(NSDate* date); * * @warning Timezones other than GMT are not supported by this function. */ -NSDate* GCDWebServerParseRFC822(NSString* string); +NSDate* _Nullable GCDWebServerParseRFC822(NSString* string); /** * Converts a date into a string using IOS 8601 formatting. @@ -94,8 +96,10 @@ NSString* GCDWebServerFormatISO8601(NSDate* date); * @warning Only "calendar" variant is supported at this time and timezones * other than GMT are not supported either. */ -NSDate* GCDWebServerParseISO8601(NSString* string); +NSDate* _Nullable GCDWebServerParseISO8601(NSString* string); #ifdef __cplusplus } #endif + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Core/GCDWebServerFunctions.m b/GCDWebServer/Core/GCDWebServerFunctions.m index bcfc102..f6e755f 100644 --- a/GCDWebServer/Core/GCDWebServerFunctions.m +++ b/GCDWebServer/Core/GCDWebServerFunctions.m @@ -83,21 +83,28 @@ NSString* GCDWebServerNormalizeHeaderValue(NSString* value) { } NSString* GCDWebServerTruncateHeaderValue(NSString* value) { - NSRange range = [value rangeOfString:@";"]; - return range.location != NSNotFound ? [value substringToIndex:range.location] : value; + if (value) { + NSRange range = [value rangeOfString:@";"]; + if (range.location != NSNotFound) { + return [value substringToIndex:range.location]; + } + } + return value; } NSString* GCDWebServerExtractHeaderValueParameter(NSString* value, NSString* name) { NSString* parameter = nil; - NSScanner* scanner = [[NSScanner alloc] initWithString:value]; - [scanner setCaseSensitive:NO]; // Assume parameter names are case-insensitive - NSString* string = [NSString stringWithFormat:@"%@=", name]; - if ([scanner scanUpToString:string intoString:NULL]) { - [scanner scanString:string intoString:NULL]; - if ([scanner scanString:@"\"" intoString:NULL]) { - [scanner scanUpToString:@"\"" intoString:¶meter]; - } else { - [scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:¶meter]; + if (value) { + NSScanner* scanner = [[NSScanner alloc] initWithString:value]; + [scanner setCaseSensitive:NO]; // Assume parameter names are case-insensitive + NSString* string = [NSString stringWithFormat:@"%@=", name]; + if ([scanner scanUpToString:string intoString:NULL]) { + [scanner scanString:string intoString:NULL]; + if ([scanner scanString:@"\"" intoString:NULL]) { + [scanner scanUpToString:@"\"" intoString:¶meter]; + } else { + [scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:¶meter]; + } } } return parameter; @@ -232,15 +239,16 @@ NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) { } NSString* GCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOOL includeService) { - NSString* string = nil; char hostBuffer[NI_MAXHOST]; char serviceBuffer[NI_MAXSERV]; - if (getnameinfo(addr, addr->sa_len, hostBuffer, sizeof(hostBuffer), serviceBuffer, sizeof(serviceBuffer), NI_NUMERICHOST | NI_NUMERICSERV | NI_NOFQDN) >= 0) { - string = includeService ? [NSString stringWithFormat:@"%s:%s", hostBuffer, serviceBuffer] : [NSString stringWithUTF8String:hostBuffer]; - } else { + if (getnameinfo(addr, addr->sa_len, hostBuffer, sizeof(hostBuffer), serviceBuffer, sizeof(serviceBuffer), NI_NUMERICHOST | NI_NUMERICSERV | NI_NOFQDN) != 0) { +#if DEBUG GWS_DNOT_REACHED(); +#else + return @""; +#endif } - return string; + return includeService ? [NSString stringWithFormat:@"%s:%s", hostBuffer, serviceBuffer] : (NSString*)[NSString stringWithUTF8String:hostBuffer]; } NSString* GCDWebServerGetPrimaryIPAddress(BOOL useIPv6) { @@ -255,7 +263,10 @@ NSString* GCDWebServerGetPrimaryIPAddress(BOOL useIPv6) { if (store) { CFPropertyListRef info = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/IPv4")); // There is no equivalent for IPv6 but the primary interface should be the same if (info) { - primaryInterface = [[NSString stringWithString:[(__bridge NSDictionary*)info objectForKey:@"PrimaryInterface"]] UTF8String]; + NSString* interface = [(__bridge NSDictionary*)info objectForKey:@"PrimaryInterface"]; + if (interface) { + primaryInterface = [[NSString stringWithString:interface] UTF8String]; // Copy string to auto-release pool + } CFRelease(info); } CFRelease(store); @@ -303,5 +314,5 @@ NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) { buffer[2 * i + 1] = byteLo >= 10 ? 'a' + byteLo - 10 : '0' + byteLo; } buffer[2 * CC_MD5_DIGEST_LENGTH] = 0; - return [NSString stringWithUTF8String:buffer]; + return (NSString*)[NSString stringWithUTF8String:buffer]; } diff --git a/GCDWebServer/Core/GCDWebServerPrivate.h b/GCDWebServer/Core/GCDWebServerPrivate.h index a7e5016..e1e6353 100644 --- a/GCDWebServer/Core/GCDWebServerPrivate.h +++ b/GCDWebServer/Core/GCDWebServerPrivate.h @@ -48,6 +48,8 @@ #import "GCDWebServerFileResponse.h" #import "GCDWebServerStreamedResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** * Check if a custom logging facility should be used instead. */ @@ -190,9 +192,9 @@ static inline NSError* GCDWebServerMakePosixError(int code) { } extern void GCDWebServerInitializeFunctions(); -extern NSString* GCDWebServerNormalizeHeaderValue(NSString* value); -extern NSString* GCDWebServerTruncateHeaderValue(NSString* value); -extern NSString* GCDWebServerExtractHeaderValueParameter(NSString* header, NSString* attribute); +extern NSString* _Nullable GCDWebServerNormalizeHeaderValue(NSString* _Nullable value); +extern NSString* _Nullable GCDWebServerTruncateHeaderValue(NSString* _Nullable value); +extern NSString* _Nullable GCDWebServerExtractHeaderValueParameter(NSString* _Nullable value, NSString* attribute); extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset); extern BOOL GCDWebServerIsTextContentType(NSString* type); extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType); @@ -200,15 +202,15 @@ extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_F extern NSString* GCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOOL includeService); @interface GCDWebServerConnection () -- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket; +- (instancetype)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket; @end @interface GCDWebServer () -@property(nonatomic, readonly) NSArray* handlers; +@property(nonatomic, readonly) NSMutableArray* handlers; @property(nonatomic, readonly) NSString* serverName; @property(nonatomic, readonly) NSString* authenticationRealm; -@property(nonatomic, readonly) NSDictionary* authenticationBasicAccounts; -@property(nonatomic, readonly) NSDictionary* authenticationDigestAccounts; +@property(nonatomic, readonly) NSMutableDictionary* authenticationBasicAccounts; +@property(nonatomic, readonly) NSMutableDictionary* authenticationDigestAccounts; @property(nonatomic, readonly) BOOL shouldAutomaticallyMapHEADToGET; @property(nonatomic, readonly) dispatch_queue_priority_t dispatchQueuePriority; - (void)willStartConnection:(GCDWebServerConnection*)connection; @@ -222,13 +224,13 @@ extern NSString* GCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOO @interface GCDWebServerRequest () @property(nonatomic, readonly) BOOL usesChunkedTransferEncoding; -@property(nonatomic, readwrite) NSData* localAddressData; -@property(nonatomic, readwrite) NSData* remoteAddressData; +@property(nonatomic) NSData* localAddressData; +@property(nonatomic) NSData* remoteAddressData; - (void)prepareForWriting; - (BOOL)performOpen:(NSError**)error; - (BOOL)performWriteData:(NSData*)data error:(NSError**)error; - (BOOL)performClose:(NSError**)error; -- (void)setAttribute:(id)attribute forKey:(NSString*)key; +- (void)setAttribute:(nullable id)attribute forKey:(NSString*)key; @end @interface GCDWebServerResponse () @@ -239,3 +241,5 @@ extern NSString* GCDWebServerStringFromSockAddr(const struct sockaddr* addr, BOO - (void)performReadDataWithCompletion:(GCDWebServerBodyReaderCompletionBlock)block; - (void)performClose; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Core/GCDWebServerRequest.h b/GCDWebServer/Core/GCDWebServerRequest.h index c7bc31b..3fe9029 100644 --- a/GCDWebServer/Core/GCDWebServerRequest.h +++ b/GCDWebServer/Core/GCDWebServerRequest.h @@ -27,6 +27,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** * Attribute key to retrieve an NSArray containing NSStrings from a GCDWebServerRequest * with the contents of any regular expression captures done on the request path. @@ -112,7 +114,7 @@ extern NSString* const GCDWebServerRequestAttribute_RegexCaptures; * * @warning This property will be nil if there is no query in the URL. */ -@property(nonatomic, readonly) NSDictionary* query; +@property(nonatomic, readonly, nullable) NSDictionary* query; /** * Returns the content type for the body of the request parsed from the @@ -122,7 +124,7 @@ extern NSString* const GCDWebServerRequestAttribute_RegexCaptures; * "application/octet-stream" if a body is present but there was no * "Content-Type" header. */ -@property(nonatomic, readonly) NSString* contentType; +@property(nonatomic, readonly, nullable) NSString* contentType; /** * Returns the content length for the body of the request parsed from the @@ -137,12 +139,12 @@ extern NSString* const GCDWebServerRequestAttribute_RegexCaptures; /** * Returns the parsed "If-Modified-Since" header or nil if absent or malformed. */ -@property(nonatomic, readonly) NSDate* ifModifiedSince; +@property(nonatomic, readonly, nullable) NSDate* ifModifiedSince; /** * Returns the parsed "If-None-Match" header or nil if absent or malformed. */ -@property(nonatomic, readonly) NSString* ifNoneMatch; +@property(nonatomic, readonly, nullable) NSString* ifNoneMatch; /** * Returns the parsed "Range" header or (NSUIntegerMax, 0) if absent or malformed. @@ -184,7 +186,7 @@ extern NSString* const GCDWebServerRequestAttribute_RegexCaptures; /** * This method is the designated initializer for the class. */ -- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query; +- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(nullable NSDictionary*)query; /** * Convenience method that checks if the contentType property is defined. @@ -201,6 +203,8 @@ extern NSString* const GCDWebServerRequestAttribute_RegexCaptures; * * @return The attribute value for the key. */ -- (id)attributeForKey:(NSString*)key; +- (nullable id)attributeForKey:(NSString*)key; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Core/GCDWebServerRequest.m b/GCDWebServer/Core/GCDWebServerRequest.m index 5365f59..05988cd 100644 --- a/GCDWebServer/Core/GCDWebServerRequest.m +++ b/GCDWebServer/Core/GCDWebServerRequest.m @@ -39,22 +39,17 @@ NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerReque #define kGZipInitialBufferSize (256 * 1024) @interface GCDWebServerBodyDecoder : NSObject -- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id)writer; @end @interface GCDWebServerGZipDecoder : GCDWebServerBodyDecoder @end -@interface GCDWebServerBodyDecoder () { -@private +@implementation GCDWebServerBodyDecoder { GCDWebServerRequest* __unsafe_unretained _request; id __unsafe_unretained _writer; } -@end -@implementation GCDWebServerBodyDecoder - -- (id)initWithRequest:(GCDWebServerRequest*)request writer:(id)writer { +- (instancetype)initWithRequest:(GCDWebServerRequest* _Nonnull)request writer:(id _Nonnull)writer { if ((self = [super init])) { _request = request; _writer = writer; @@ -76,14 +71,10 @@ NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerReque @end -@interface GCDWebServerGZipDecoder () { -@private +@implementation GCDWebServerGZipDecoder { z_stream _stream; BOOL _finished; } -@end - -@implementation GCDWebServerGZipDecoder - (BOOL)open:(NSError**)error { int result = inflateInit2(&_stream, 15 + 16); @@ -143,77 +134,55 @@ NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerReque @end -@interface GCDWebServerRequest () { -@private - NSString* _method; - NSURL* _url; - NSDictionary* _headers; - NSString* _path; - NSDictionary* _query; - NSString* _type; - BOOL _chunked; - NSUInteger _length; - NSDate* _modifiedSince; - NSString* _noneMatch; - NSRange _range; - BOOL _gzipAccepted; - NSData* _localAddress; - NSData* _remoteAddress; - +@implementation GCDWebServerRequest { BOOL _opened; NSMutableArray* _decoders; - NSMutableDictionary* _attributes; id __unsafe_unretained _writer; + NSMutableDictionary* _attributes; } -@end - -@implementation GCDWebServerRequest : NSObject - -@synthesize method = _method, URL = _url, headers = _headers, path = _path, query = _query, contentType = _type, contentLength = _length, ifModifiedSince = _modifiedSince, ifNoneMatch = _noneMatch, - byteRange = _range, acceptsGzipContentEncoding = _gzipAccepted, usesChunkedTransferEncoding = _chunked, localAddressData = _localAddress, remoteAddressData = _remoteAddress; - (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { if ((self = [super init])) { _method = [method copy]; - _url = url; + _URL = url; _headers = headers; _path = [path copy]; _query = query; - _type = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"]); - _chunked = [GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"]; + _contentType = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Content-Type"]); + _usesChunkedTransferEncoding = [GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Transfer-Encoding"]) isEqualToString:@"chunked"]; NSString* lengthHeader = [_headers objectForKey:@"Content-Length"]; if (lengthHeader) { NSInteger length = [lengthHeader integerValue]; - if (_chunked || (length < 0)) { - GWS_LOG_WARNING(@"Invalid 'Content-Length' header '%@' for '%@' request on \"%@\"", lengthHeader, _method, _url); + if (_usesChunkedTransferEncoding || (length < 0)) { + GWS_LOG_WARNING(@"Invalid 'Content-Length' header '%@' for '%@' request on \"%@\"", lengthHeader, _method, _URL); GWS_DNOT_REACHED(); return nil; } - _length = length; - if (_type == nil) { - _type = kGCDWebServerDefaultMimeType; + _contentLength = length; + if (_contentType == nil) { + _contentType = kGCDWebServerDefaultMimeType; } - } else if (_chunked) { - if (_type == nil) { - _type = kGCDWebServerDefaultMimeType; + } else if (_usesChunkedTransferEncoding) { + if (_contentType == nil) { + _contentType = kGCDWebServerDefaultMimeType; } - _length = NSUIntegerMax; + _contentLength = NSUIntegerMax; } else { - if (_type) { - GWS_LOG_WARNING(@"Ignoring 'Content-Type' header for '%@' request on \"%@\"", _method, _url); - _type = nil; // Content-Type without Content-Length or chunked-encoding doesn't make sense + if (_contentType) { + GWS_LOG_WARNING(@"Ignoring 'Content-Type' header for '%@' request on \"%@\"", _method, _URL); + _contentType = nil; // Content-Type without Content-Length or chunked-encoding doesn't make sense } - _length = NSUIntegerMax; + _contentLength = NSUIntegerMax; } NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"]; if (modifiedHeader) { - _modifiedSince = [GCDWebServerParseRFC822(modifiedHeader) copy]; + _ifModifiedSince = [GCDWebServerParseRFC822(modifiedHeader) copy]; } - _noneMatch = [_headers objectForKey:@"If-None-Match"]; + _ifNoneMatch = [_headers objectForKey:@"If-None-Match"]; - _range = NSMakeRange(NSUIntegerMax, 0); + _byteRange = NSMakeRange(NSUIntegerMax, 0); NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]); if (rangeHeader) { if ([rangeHeader hasPrefix:@"bytes="]) { @@ -226,25 +195,25 @@ NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerReque NSString* endString = [components objectAtIndex:1]; NSInteger endValue = [endString integerValue]; if (startString.length && (startValue >= 0) && endString.length && (endValue >= startValue)) { // The second 500 bytes: "500-999" - _range.location = startValue; - _range.length = endValue - startValue + 1; + _byteRange.location = startValue; + _byteRange.length = endValue - startValue + 1; } else if (startString.length && (startValue >= 0)) { // The bytes after 9500 bytes: "9500-" - _range.location = startValue; - _range.length = NSUIntegerMax; + _byteRange.location = startValue; + _byteRange.length = NSUIntegerMax; } else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500" - _range.location = NSUIntegerMax; - _range.length = endValue; + _byteRange.location = NSUIntegerMax; + _byteRange.length = endValue; } } } } - if ((_range.location == NSUIntegerMax) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid + if ((_byteRange.location == NSUIntegerMax) && (_byteRange.length == 0)) { // Ignore "Range" header if syntactically invalid GWS_LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url); } } if ([[_headers objectForKey:@"Accept-Encoding"] rangeOfString:@"gzip"].location != NSNotFound) { - _gzipAccepted = YES; + _acceptsGzipContentEncoding = YES; } _decoders = [[NSMutableArray alloc] init]; @@ -254,11 +223,11 @@ NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerReque } - (BOOL)hasBody { - return _type ? YES : NO; + return _contentType ? YES : NO; } - (BOOL)hasByteRange { - return GCDWebServerIsValidByteRange(_range); + return GCDWebServerIsValidByteRange(_byteRange); } - (id)attributeForKey:(NSString*)key { @@ -287,7 +256,7 @@ NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerReque } - (BOOL)performOpen:(NSError**)error { - GWS_DCHECK(_type); + GWS_DCHECK(_contentType); GWS_DCHECK(_writer); if (_opened) { GWS_DNOT_REACHED(); @@ -312,11 +281,11 @@ NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerReque } - (NSString*)localAddressString { - return GCDWebServerStringFromSockAddr(_localAddress.bytes, YES); + return GCDWebServerStringFromSockAddr(_localAddressData.bytes, YES); } - (NSString*)remoteAddressString { - return GCDWebServerStringFromSockAddr(_remoteAddress.bytes, YES); + return GCDWebServerStringFromSockAddr(_remoteAddressData.bytes, YES); } - (NSString*)description { diff --git a/GCDWebServer/Core/GCDWebServerResponse.h b/GCDWebServer/Core/GCDWebServerResponse.h index 2ec2dee..1e5e8c9 100644 --- a/GCDWebServer/Core/GCDWebServerResponse.h +++ b/GCDWebServer/Core/GCDWebServerResponse.h @@ -27,11 +27,13 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerBodyReaderCompletionBlock is passed by GCDWebServer to the * GCDWebServerBodyReader object when reading data from it asynchronously. */ -typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* error); +typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* _Nullable error); /** * This protocol is used by the GCDWebServerConnection to communicate with @@ -62,7 +64,7 @@ typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* err * or an empty NSData there is no more body data, or nil on error and set * the "error" argument which is guaranteed to be non-NULL. */ -- (NSData*)readData:(NSError**)error; +- (nullable NSData*)readData:(NSError**)error; /** * This method is called after all body data has been sent. @@ -102,7 +104,7 @@ typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* err * * @warning This property must be set if a body is present. */ -@property(nonatomic, copy) NSString* contentType; +@property(nonatomic, copy, nullable) NSString* contentType; /** * Sets the content length for the body of the response. If a body is present @@ -136,14 +138,14 @@ typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* err * * The default value is nil. */ -@property(nonatomic, retain) NSDate* lastModifiedDate; +@property(nonatomic, nullable) NSDate* lastModifiedDate; /** * Sets the ETag for the response using the "ETag" header. * * The default value is nil. */ -@property(nonatomic, copy) NSString* eTag; +@property(nonatomic, copy, nullable) NSString* eTag; /** * Enables gzip encoding for the response body. @@ -174,7 +176,7 @@ typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* err * @warning Do not attempt to override the primary headers used * by GCDWebServerResponse like "Content-Type", "ETag", etc... */ -- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header; +- (void)setValue:(nullable NSString*)value forAdditionalHeader:(NSString*)header; /** * Convenience method that checks if the contentType property is defined. @@ -206,3 +208,5 @@ typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* err - (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Core/GCDWebServerResponse.m b/GCDWebServer/Core/GCDWebServerResponse.m index b5eb57c..9153ff6 100644 --- a/GCDWebServer/Core/GCDWebServerResponse.m +++ b/GCDWebServer/Core/GCDWebServerResponse.m @@ -37,22 +37,17 @@ #define kGZipInitialBufferSize (256 * 1024) @interface GCDWebServerBodyEncoder : NSObject -- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id)reader; @end @interface GCDWebServerGZipEncoder : GCDWebServerBodyEncoder @end -@interface GCDWebServerBodyEncoder () { -@private +@implementation GCDWebServerBodyEncoder { GCDWebServerResponse* __unsafe_unretained _response; id __unsafe_unretained _reader; } -@end -@implementation GCDWebServerBodyEncoder - -- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id)reader { +- (instancetype)initWithResponse:(GCDWebServerResponse* _Nonnull)response reader:(id _Nonnull)reader { if ((self = [super init])) { _response = response; _reader = reader; @@ -74,16 +69,12 @@ @end -@interface GCDWebServerGZipEncoder () { -@private +@implementation GCDWebServerGZipEncoder { z_stream _stream; BOOL _finished; } -@end -@implementation GCDWebServerGZipEncoder - -- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id)reader { +- (instancetype)initWithResponse:(GCDWebServerResponse* _Nonnull)response reader:(id _Nonnull)reader { if ((self = [super initWithResponse:response reader:reader])) { response.contentLength = NSUIntegerMax; // Make sure "Content-Length" header is not set since we don't know it [response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"]; @@ -157,28 +148,11 @@ @end -@interface GCDWebServerResponse () { -@private - NSString* _type; - NSUInteger _length; - NSInteger _status; - NSUInteger _maxAge; - NSDate* _lastModified; - NSString* _eTag; - NSMutableDictionary* _headers; - BOOL _chunked; - BOOL _gzipped; - +@implementation GCDWebServerResponse { BOOL _opened; NSMutableArray* _encoders; id __unsafe_unretained _reader; } -@end - -@implementation GCDWebServerResponse - -@synthesize contentType = _type, contentLength = _length, statusCode = _status, cacheControlMaxAge = _maxAge, lastModifiedDate = _lastModified, eTag = _eTag, - gzipContentEncodingEnabled = _gzipped, additionalHeaders = _headers; + (instancetype)response { return [[[self class] alloc] init]; @@ -186,26 +160,26 @@ - (instancetype)init { if ((self = [super init])) { - _type = nil; - _length = NSUIntegerMax; - _status = kGCDWebServerHTTPStatusCode_OK; - _maxAge = 0; - _headers = [[NSMutableDictionary alloc] init]; + _contentType = nil; + _contentLength = NSUIntegerMax; + _statusCode = kGCDWebServerHTTPStatusCode_OK; + _cacheControlMaxAge = 0; + _additionalHeaders = [[NSMutableDictionary alloc] init]; _encoders = [[NSMutableArray alloc] init]; } return self; } - (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header { - [_headers setValue:value forKey:header]; + [_additionalHeaders setValue:value forKey:header]; } - (BOOL)hasBody { - return _type ? YES : NO; + return _contentType ? YES : NO; } - (BOOL)usesChunkedTransferEncoding { - return (_type != nil) && (_length == NSUIntegerMax); + return (_contentType != nil) && (_contentLength == NSUIntegerMax); } - (BOOL)open:(NSError**)error { @@ -222,7 +196,7 @@ - (void)prepareForReading { _reader = self; - if (_gzipped) { + if (_gzipContentEncodingEnabled) { GCDWebServerGZipEncoder* encoder = [[GCDWebServerGZipEncoder alloc] initWithResponse:self reader:_reader]; [_encoders addObject:encoder]; _reader = encoder; @@ -230,7 +204,7 @@ } - (BOOL)performOpen:(NSError**)error { - GWS_DCHECK(_type); + GWS_DCHECK(_contentType); GWS_DCHECK(_reader); if (_opened) { GWS_DNOT_REACHED(); @@ -257,24 +231,24 @@ } - (NSString*)description { - NSMutableString* description = [NSMutableString stringWithFormat:@"Status Code = %i", (int)_status]; - if (_type) { - [description appendFormat:@"\nContent Type = %@", _type]; + NSMutableString* description = [NSMutableString stringWithFormat:@"Status Code = %i", (int)_statusCode]; + if (_contentType) { + [description appendFormat:@"\nContent Type = %@", _contentType]; } - if (_length != NSUIntegerMax) { - [description appendFormat:@"\nContent Length = %lu", (unsigned long)_length]; + if (_contentLength != NSUIntegerMax) { + [description appendFormat:@"\nContent Length = %lu", (unsigned long)_contentLength]; } - [description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge]; - if (_lastModified) { - [description appendFormat:@"\nLast Modified Date = %@", _lastModified]; + [description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_cacheControlMaxAge]; + if (_lastModifiedDate) { + [description appendFormat:@"\nLast Modified Date = %@", _lastModifiedDate]; } if (_eTag) { [description appendFormat:@"\nETag = %@", _eTag]; } - if (_headers.count) { + if (_additionalHeaders.count) { [description appendString:@"\n"]; - for (NSString* header in [[_headers allKeys] sortedArrayUsingSelector:@selector(compare:)]) { - [description appendFormat:@"\n%@: %@", header, [_headers objectForKey:header]]; + for (NSString* header in [[_additionalHeaders allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + [description appendFormat:@"\n%@: %@", header, [_additionalHeaders objectForKey:header]]; } } return description; diff --git a/GCDWebServer/Requests/GCDWebServerDataRequest.h b/GCDWebServer/Requests/GCDWebServerDataRequest.h index 5048d08..f21a4b7 100644 --- a/GCDWebServer/Requests/GCDWebServerDataRequest.h +++ b/GCDWebServer/Requests/GCDWebServerDataRequest.h @@ -27,6 +27,8 @@ #import "GCDWebServerRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerDataRequest subclass of GCDWebServerRequest stores the body * of the HTTP request in memory. @@ -49,12 +51,14 @@ * The text encoding used to interpret the data is extracted from the * "Content-Type" header or defaults to UTF-8. */ -@property(nonatomic, readonly) NSString* text; +@property(nonatomic, readonly, nullable) NSString* text; /** * Returns the data for the request body interpreted as a JSON object. If the * content type of the body is not JSON, or if an error occurs, nil is returned. */ -@property(nonatomic, readonly) id jsonObject; +@property(nonatomic, readonly, nullable) id jsonObject; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Requests/GCDWebServerDataRequest.m b/GCDWebServer/Requests/GCDWebServerDataRequest.m index 861ab52..3ea9bba 100644 --- a/GCDWebServer/Requests/GCDWebServerDataRequest.m +++ b/GCDWebServer/Requests/GCDWebServerDataRequest.m @@ -31,18 +31,14 @@ #import "GCDWebServerPrivate.h" -@interface GCDWebServerDataRequest () { -@private - NSMutableData* _data; +@interface GCDWebServerDataRequest () +@property(nonatomic) NSMutableData* data; +@end +@implementation GCDWebServerDataRequest { NSString* _text; id _jsonObject; } -@end - -@implementation GCDWebServerDataRequest - -@synthesize data = _data; - (BOOL)open:(NSError**)error { if (self.contentLength != NSUIntegerMax) { @@ -72,7 +68,7 @@ NSMutableString* description = [NSMutableString stringWithString:[super description]]; if (_data) { [description appendString:@"\n\n"]; - [description appendString:GCDWebServerDescribeData(_data, self.contentType)]; + [description appendString:GCDWebServerDescribeData(_data, (NSString*)self.contentType)]; } return description; } diff --git a/GCDWebServer/Requests/GCDWebServerFileRequest.h b/GCDWebServer/Requests/GCDWebServerFileRequest.h index ad29eab..8aceae4 100644 --- a/GCDWebServer/Requests/GCDWebServerFileRequest.h +++ b/GCDWebServer/Requests/GCDWebServerFileRequest.h @@ -27,6 +27,8 @@ #import "GCDWebServerRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerFileRequest subclass of GCDWebServerRequest stores the body * of the HTTP request to a file on disk. @@ -43,3 +45,5 @@ @property(nonatomic, readonly) NSString* temporaryPath; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Requests/GCDWebServerFileRequest.m b/GCDWebServer/Requests/GCDWebServerFileRequest.m index 2d7a8db..8a47fcc 100644 --- a/GCDWebServer/Requests/GCDWebServerFileRequest.m +++ b/GCDWebServer/Requests/GCDWebServerFileRequest.m @@ -31,16 +31,9 @@ #import "GCDWebServerPrivate.h" -@interface GCDWebServerFileRequest () { -@private - NSString* _temporaryPath; +@implementation GCDWebServerFileRequest { int _file; } -@end - -@implementation GCDWebServerFileRequest - -@synthesize temporaryPath = _temporaryPath; - (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query { if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) { diff --git a/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.h b/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.h index 832c2e7..93ac179 100644 --- a/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.h +++ b/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.h @@ -27,6 +27,8 @@ #import "GCDWebServerRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerMultiPart class is an abstract class that wraps the content * of a part. @@ -69,7 +71,7 @@ * The text encoding used to interpret the data is extracted from the * "Content-Type" header or defaults to UTF-8. */ -@property(nonatomic, readonly) NSString* string; +@property(nonatomic, readonly, nullable) NSString* string; @end @@ -122,11 +124,13 @@ /** * Returns the first argument for a given control name or nil if not found. */ -- (GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name; +- (nullable GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name; /** * Returns the first file for a given control name or nil if not found. */ -- (GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name; +- (nullable GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.m b/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.m index c6cb485..4e6bf09 100644 --- a/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.m +++ b/GCDWebServer/Requests/GCDWebServerMultiPartFormRequest.m @@ -42,50 +42,28 @@ typedef enum { } ParserState; @interface GCDWebServerMIMEStreamParser : NSObject -- (id)initWithBoundary:(NSString*)boundary defaultControlName:(NSString*)name arguments:(NSMutableArray*)arguments files:(NSMutableArray*)files; -- (BOOL)appendBytes:(const void*)bytes length:(NSUInteger)length; -- (BOOL)isAtEnd; @end static NSData* _newlineData = nil; static NSData* _newlinesData = nil; static NSData* _dashNewlineData = nil; -@interface GCDWebServerMultiPart () { -@private - NSString* _controlName; - NSString* _contentType; - NSString* _mimeType; -} -@end - @implementation GCDWebServerMultiPart -@synthesize controlName = _controlName, contentType = _contentType, mimeType = _mimeType; - -- (id)initWithControlName:(NSString*)name contentType:(NSString*)type { +- (instancetype)initWithControlName:(NSString* _Nonnull)name contentType:(NSString* _Nonnull)type { if ((self = [super init])) { _controlName = [name copy]; _contentType = [type copy]; - _mimeType = GCDWebServerTruncateHeaderValue(_contentType); + _mimeType = (NSString*)GCDWebServerTruncateHeaderValue(_contentType); } return self; } @end -@interface GCDWebServerMultiPartArgument () { -@private - NSData* _data; - NSString* _string; -} -@end - @implementation GCDWebServerMultiPartArgument -@synthesize data = _data, string = _string; - -- (id)initWithControlName:(NSString*)name contentType:(NSString*)type data:(NSData*)data { +- (instancetype)initWithControlName:(NSString* _Nonnull)name contentType:(NSString* _Nonnull)type data:(NSData* _Nonnull)data { if ((self = [super initWithControlName:name contentType:type])) { _data = data; @@ -103,18 +81,9 @@ static NSData* _dashNewlineData = nil; @end -@interface GCDWebServerMultiPartFile () { -@private - NSString* _fileName; - NSString* _temporaryPath; -} -@end - @implementation GCDWebServerMultiPartFile -@synthesize fileName = _fileName, temporaryPath = _temporaryPath; - -- (id)initWithControlName:(NSString*)name contentType:(NSString*)type fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath { +- (instancetype)initWithControlName:(NSString* _Nonnull)name contentType:(NSString* _Nonnull)type fileName:(NSString* _Nonnull)fileName temporaryPath:(NSString* _Nonnull)temporaryPath { if ((self = [super initWithControlName:name contentType:type])) { _fileName = [fileName copy]; _temporaryPath = [temporaryPath copy]; @@ -132,8 +101,7 @@ static NSData* _dashNewlineData = nil; @end -@interface GCDWebServerMIMEStreamParser () { -@private +@implementation GCDWebServerMIMEStreamParser { NSData* _boundary; NSString* _defaultcontrolName; ParserState _state; @@ -148,9 +116,6 @@ static NSData* _dashNewlineData = nil; int _tmpFile; GCDWebServerMIMEStreamParser* _subParser; } -@end - -@implementation GCDWebServerMIMEStreamParser + (void)initialize { if (_newlineData == nil) { @@ -167,7 +132,7 @@ static NSData* _dashNewlineData = nil; } } -- (id)initWithBoundary:(NSString*)boundary defaultControlName:(NSString*)name arguments:(NSMutableArray*)arguments files:(NSMutableArray*)files { +- (instancetype)initWithBoundary:(NSString* _Nonnull)boundary defaultControlName:(NSString* _Nullable)name arguments:(NSMutableArray* _Nonnull)arguments files:(NSMutableArray* _Nonnull)files { NSData* data = boundary.length ? [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding] : nil; if (data == nil) { GWS_DNOT_REACHED(); @@ -346,17 +311,14 @@ static NSData* _dashNewlineData = nil; @end -@interface GCDWebServerMultiPartFormRequest () { -@private - GCDWebServerMIMEStreamParser* _parser; - NSMutableArray* _arguments; - NSMutableArray* _files; -} +@interface GCDWebServerMultiPartFormRequest () +@property(nonatomic) NSMutableArray* arguments; +@property(nonatomic) NSMutableArray* files; @end -@implementation GCDWebServerMultiPartFormRequest - -@synthesize arguments = _arguments, files = _files; +@implementation GCDWebServerMultiPartFormRequest { + GCDWebServerMIMEStreamParser* _parser; +} + (NSString*)mimeType { return @"multipart/form-data"; diff --git a/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.h b/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.h index 9735380..fcf177e 100644 --- a/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.h +++ b/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.h @@ -27,6 +27,8 @@ #import "GCDWebServerDataRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerURLEncodedFormRequest subclass of GCDWebServerRequest * parses the body of the HTTP request as a URL encoded form using @@ -49,3 +51,5 @@ + (NSString*)mimeType; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.m b/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.m index 328fd75..7e0137f 100644 --- a/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.m +++ b/GCDWebServer/Requests/GCDWebServerURLEncodedFormRequest.m @@ -31,16 +31,8 @@ #import "GCDWebServerPrivate.h" -@interface GCDWebServerURLEncodedFormRequest () { -@private - NSDictionary* _arguments; -} -@end - @implementation GCDWebServerURLEncodedFormRequest -@synthesize arguments = _arguments; - + (NSString*)mimeType { return @"application/x-www-form-urlencoded"; } @@ -53,8 +45,6 @@ NSString* charset = GCDWebServerExtractHeaderValueParameter(self.contentType, @"charset"); NSString* string = [[NSString alloc] initWithData:self.data encoding:GCDWebServerStringEncodingFromCharset(charset)]; _arguments = GCDWebServerParseURLEncodedForm(string); - GWS_DCHECK(_arguments); - return YES; } diff --git a/GCDWebServer/Responses/GCDWebServerDataResponse.h b/GCDWebServer/Responses/GCDWebServerDataResponse.h index 6e06cd8..783f596 100644 --- a/GCDWebServer/Responses/GCDWebServerDataResponse.h +++ b/GCDWebServer/Responses/GCDWebServerDataResponse.h @@ -27,11 +27,14 @@ #import "GCDWebServerResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerDataResponse subclass of GCDWebServerResponse reads the body * of the HTTP response from memory. */ @interface GCDWebServerDataResponse : GCDWebServerResponse +@property(nonatomic, copy) NSString* contentType; // Redeclare as non-null /** * Creates a response with data in memory and a given content type. @@ -50,40 +53,40 @@ /** * Creates a data response from text encoded using UTF-8. */ -+ (instancetype)responseWithText:(NSString*)text; ++ (nullable instancetype)responseWithText:(NSString*)text; /** * Creates a data response from HTML encoded using UTF-8. */ -+ (instancetype)responseWithHTML:(NSString*)html; ++ (nullable instancetype)responseWithHTML:(NSString*)html; /** * Creates a data response from an HTML template encoded using UTF-8. * See -initWithHTMLTemplate:variables: for details. */ -+ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; ++ (nullable instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; /** * Creates a data response from a serialized JSON object and the default * "application/json" content type. */ -+ (instancetype)responseWithJSONObject:(id)object; ++ (nullable instancetype)responseWithJSONObject:(id)object; /** * Creates a data response from a serialized JSON object and a custom * content type. */ -+ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type; ++ (nullable instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type; /** * Initializes a data response from text encoded using UTF-8. */ -- (instancetype)initWithText:(NSString*)text; +- (nullable instancetype)initWithText:(NSString*)text; /** * Initializes a data response from HTML encoded using UTF-8. */ -- (instancetype)initWithHTML:(NSString*)html; +- (nullable instancetype)initWithHTML:(NSString*)html; /** * Initializes a data response from an HTML template encoded using UTF-8. @@ -91,18 +94,20 @@ * All occurences of "%variable%" within the HTML template are replaced with * their corresponding values. */ -- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; +- (nullable instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; /** * Initializes a data response from a serialized JSON object and the default * "application/json" content type. */ -- (instancetype)initWithJSONObject:(id)object; +- (nullable instancetype)initWithJSONObject:(id)object; /** * Initializes a data response from a serialized JSON object and a custom * content type. */ -- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type; +- (nullable instancetype)initWithJSONObject:(id)object contentType:(NSString*)type; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Responses/GCDWebServerDataResponse.m b/GCDWebServer/Responses/GCDWebServerDataResponse.m index 3bffb25..b496847 100644 --- a/GCDWebServer/Responses/GCDWebServerDataResponse.m +++ b/GCDWebServer/Responses/GCDWebServerDataResponse.m @@ -31,25 +31,18 @@ #import "GCDWebServerPrivate.h" -@interface GCDWebServerDataResponse () { -@private +@implementation GCDWebServerDataResponse { NSData* _data; BOOL _done; } -@end -@implementation GCDWebServerDataResponse +@dynamic contentType; + (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type { return [[[self class] alloc] initWithData:data contentType:type]; } - (instancetype)initWithData:(NSData*)data contentType:(NSString*)type { - if (data == nil) { - GWS_DNOT_REACHED(); - return nil; - } - if ((self = [super init])) { _data = data; @@ -124,8 +117,7 @@ [variables enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL* stop) { [html replaceOccurrencesOfString:[NSString stringWithFormat:@"%%%@%%", key] withString:value options:0 range:NSMakeRange(0, html.length)]; }]; - id response = [self initWithHTML:html]; - return response; + return [self initWithHTML:html]; } - (instancetype)initWithJSONObject:(id)object { @@ -135,6 +127,7 @@ - (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type { NSData* data = [NSJSONSerialization dataWithJSONObject:object options:0 error:NULL]; if (data == nil) { + GWS_DNOT_REACHED(); return nil; } return [self initWithData:data contentType:type]; diff --git a/GCDWebServer/Responses/GCDWebServerErrorResponse.h b/GCDWebServer/Responses/GCDWebServerErrorResponse.h index f80c7ff..92c834c 100644 --- a/GCDWebServer/Responses/GCDWebServerErrorResponse.h +++ b/GCDWebServer/Responses/GCDWebServerErrorResponse.h @@ -28,6 +28,8 @@ #import "GCDWebServerDataResponse.h" #import "GCDWebServerHTTPStatusCodes.h" +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerDataResponse subclass of GCDWebServerDataResponse generates * an HTML body from an HTTP status code and an error message. @@ -48,13 +50,13 @@ * Creates a client error response with the corresponding HTTP status code * and an underlying NSError. */ -+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); ++ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); /** * Creates a server error response with the corresponding HTTP status code * and an underlying NSError. */ -+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); ++ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); /** * Initializes a client error response with the corresponding HTTP status code. @@ -70,12 +72,14 @@ * Initializes a client error response with the corresponding HTTP status code * and an underlying NSError. */ -- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); +- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); /** * Initializes a server error response with the corresponding HTTP status code * and an underlying NSError. */ -- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); +- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(nullable NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3, 4); @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Responses/GCDWebServerErrorResponse.m b/GCDWebServer/Responses/GCDWebServerErrorResponse.m index ef6a991..f1cd202 100644 --- a/GCDWebServer/Responses/GCDWebServerErrorResponse.m +++ b/GCDWebServer/Responses/GCDWebServerErrorResponse.m @@ -31,10 +31,6 @@ #import "GCDWebServerPrivate.h" -@interface GCDWebServerErrorResponse () -- (instancetype)initWithStatusCode:(NSInteger)statusCode underlyingError:(NSError*)underlyingError messageFormat:(NSString*)format arguments:(va_list)arguments; -@end - @implementation GCDWebServerErrorResponse + (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... { diff --git a/GCDWebServer/Responses/GCDWebServerFileResponse.h b/GCDWebServer/Responses/GCDWebServerFileResponse.h index 050e92f..f7dbd47 100644 --- a/GCDWebServer/Responses/GCDWebServerFileResponse.h +++ b/GCDWebServer/Responses/GCDWebServerFileResponse.h @@ -27,6 +27,8 @@ #import "GCDWebServerResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerFileResponse subclass of GCDWebServerResponse reads the body * of the HTTP response from a file on disk. @@ -36,17 +38,20 @@ * metadata. */ @interface GCDWebServerFileResponse : GCDWebServerResponse +@property(nonatomic, copy) NSString* contentType; // Redeclare as non-null +@property(nonatomic) NSDate* lastModifiedDate; // Redeclare as non-null +@property(nonatomic, copy) NSString* eTag; // Redeclare as non-null /** * Creates a response with the contents of a file. */ -+ (instancetype)responseWithFile:(NSString*)path; ++ (nullable instancetype)responseWithFile:(NSString*)path; /** * Creates a response like +responseWithFile: and sets the "Content-Disposition" * HTTP header for a download if the "attachment" argument is YES. */ -+ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment; ++ (nullable instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment; /** * Creates a response like +responseWithFile: but restricts the file contents @@ -54,26 +59,26 @@ * * See -initWithFile:byteRange: for details. */ -+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range; ++ (nullable instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range; /** * Creates a response like +responseWithFile:byteRange: and sets the * "Content-Disposition" HTTP header for a download if the "attachment" * argument is YES. */ -+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment; ++ (nullable instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment; /** * Initializes a response with the contents of a file. */ -- (instancetype)initWithFile:(NSString*)path; +- (nullable instancetype)initWithFile:(NSString*)path; /** * Initializes a response like +responseWithFile: and sets the * "Content-Disposition" HTTP header for a download if the "attachment" * argument is YES. */ -- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment; +- (nullable instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment; /** * Initializes a response like -initWithFile: but restricts the file contents @@ -86,11 +91,13 @@ * This argument would typically be set to the value of the byteRange property * of the current GCDWebServerRequest. */ -- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range; +- (nullable instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range; /** * This method is the designated initializer for the class. */ -- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment; +- (nullable instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Responses/GCDWebServerFileResponse.m b/GCDWebServer/Responses/GCDWebServerFileResponse.m index 3e717a3..f693e1b 100644 --- a/GCDWebServer/Responses/GCDWebServerFileResponse.m +++ b/GCDWebServer/Responses/GCDWebServerFileResponse.m @@ -35,16 +35,14 @@ #define kFileReadBufferSize (32 * 1024) -@interface GCDWebServerFileResponse () { -@private +@implementation GCDWebServerFileResponse { NSString* _path; NSUInteger _offset; NSUInteger _size; int _file; } -@end -@implementation GCDWebServerFileResponse +@dynamic contentType, lastModifiedDate, eTag; + (instancetype)responseWithFile:(NSString*)path { return [[[self class] alloc] initWithFile:path]; diff --git a/GCDWebServer/Responses/GCDWebServerStreamedResponse.h b/GCDWebServer/Responses/GCDWebServerStreamedResponse.h index 2731b7c..bb48e66 100644 --- a/GCDWebServer/Responses/GCDWebServerStreamedResponse.h +++ b/GCDWebServer/Responses/GCDWebServerStreamedResponse.h @@ -27,12 +27,14 @@ #import "GCDWebServerResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** * The GCDWebServerStreamBlock is called to stream the data for the HTTP body. * The block must return either a chunk of data, an empty NSData when done, or * nil on error and set the "error" argument which is guaranteed to be non-NULL. */ -typedef NSData* (^GCDWebServerStreamBlock)(NSError** error); +typedef NSData* _Nullable (^GCDWebServerStreamBlock)(NSError** error); /** * The GCDWebServerAsyncStreamBlock works like the GCDWebServerStreamBlock @@ -51,6 +53,7 @@ typedef void (^GCDWebServerAsyncStreamBlock)(GCDWebServerBodyReaderCompletionBlo * the body of the HTTP response using a GCD block. */ @interface GCDWebServerStreamedResponse : GCDWebServerResponse +@property(nonatomic, copy) NSString* contentType; // Redeclare as non-null /** * Creates a response with streamed data and a given content type. @@ -73,3 +76,5 @@ typedef void (^GCDWebServerAsyncStreamBlock)(GCDWebServerBodyReaderCompletionBlo - (instancetype)initWithContentType:(NSString*)type asyncStreamBlock:(GCDWebServerAsyncStreamBlock)block; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebServer/Responses/GCDWebServerStreamedResponse.m b/GCDWebServer/Responses/GCDWebServerStreamedResponse.m index 2ed01c1..9387263 100644 --- a/GCDWebServer/Responses/GCDWebServerStreamedResponse.m +++ b/GCDWebServer/Responses/GCDWebServerStreamedResponse.m @@ -31,13 +31,11 @@ #import "GCDWebServerPrivate.h" -@interface GCDWebServerStreamedResponse () { -@private +@implementation GCDWebServerStreamedResponse { GCDWebServerAsyncStreamBlock _block; } -@end -@implementation GCDWebServerStreamedResponse +@dynamic contentType; + (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block { return [[[self class] alloc] initWithContentType:type streamBlock:block]; diff --git a/GCDWebUploader/GCDWebUploader.h b/GCDWebUploader/GCDWebUploader.h index d2c92e9..2332ba1 100644 --- a/GCDWebUploader/GCDWebUploader.h +++ b/GCDWebUploader/GCDWebUploader.h @@ -27,6 +27,8 @@ #import "GCDWebServer.h" +NS_ASSUME_NONNULL_BEGIN + @class GCDWebUploader; /** @@ -84,7 +86,7 @@ /** * Sets the delegate for the uploader. */ -@property(nonatomic, assign) id delegate; +@property(nonatomic, weak, nullable) id delegate; /** * Sets which files are allowed to be operated on depending on their extension. @@ -195,3 +197,5 @@ - (BOOL)shouldCreateDirectoryAtPath:(NSString*)path; @end + +NS_ASSUME_NONNULL_END diff --git a/GCDWebUploader/GCDWebUploader.m b/GCDWebUploader/GCDWebUploader.m index e6dc497..ada6417 100644 --- a/GCDWebUploader/GCDWebUploader.m +++ b/GCDWebUploader/GCDWebUploader.m @@ -46,257 +46,30 @@ #import "GCDWebServerErrorResponse.h" #import "GCDWebServerFileResponse.h" -@interface GCDWebUploader () { -@private - NSString* _uploadDirectory; - NSArray* _allowedExtensions; - BOOL _allowHidden; - NSString* _title; - NSString* _header; - NSString* _prologue; - NSString* _epilogue; - NSString* _footer; -} +NS_ASSUME_NONNULL_BEGIN + +@interface GCDWebUploader (Methods) +- (nullable GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request; +- (nullable GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request; +- (nullable GCDWebServerResponse*)uploadFile:(GCDWebServerMultiPartFormRequest*)request; +- (nullable GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request; +- (nullable GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request; +- (nullable GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request; @end -@implementation GCDWebUploader (Methods) - -// Must match implementation in GCDWebDAVServer -- (BOOL)_checkSandboxedPath:(NSString*)path { - return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory]; -} - -- (BOOL)_checkFileExtension:(NSString*)fileName { - if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) { - return NO; - } - return YES; -} - -- (NSString*)_uniquePathForPath:(NSString*)path { - if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { - NSString* directory = [path stringByDeletingLastPathComponent]; - NSString* file = [path lastPathComponent]; - NSString* base = [file stringByDeletingPathExtension]; - NSString* extension = [file pathExtension]; - int retries = 0; - do { - if (extension.length) { - path = [directory stringByAppendingPathComponent:[[base stringByAppendingFormat:@" (%i)", ++retries] stringByAppendingPathExtension:extension]]; - } else { - path = [directory stringByAppendingPathComponent:[base stringByAppendingFormat:@" (%i)", ++retries]]; - } - } while ([[NSFileManager defaultManager] fileExistsAtPath:path]); - } - return path; -} - -- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request { - NSString* relativePath = [[request query] objectForKey:@"path"]; - NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - BOOL isDirectory = NO; - if (![self _checkSandboxedPath:absolutePath] || ![[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 directory", relativePath]; - } - - NSString* directoryName = [absolutePath lastPathComponent]; - if (!_allowHidden && [directoryName hasPrefix:@"."]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Listing directory name \"%@\" is not allowed", directoryName]; - } - - NSError* error = nil; - NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error]; - if (contents == nil) { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath]; - } - - NSMutableArray* array = [NSMutableArray array]; - for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) { - if (_allowHidden || ![item hasPrefix:@"."]) { - NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL]; - NSString* type = [attributes objectForKey:NSFileType]; - if ([type isEqualToString:NSFileTypeRegular] && [self _checkFileExtension:item]) { - [array addObject:@{ - @"path" : [relativePath stringByAppendingPathComponent:item], - @"name" : item, - @"size" : [attributes objectForKey:NSFileSize] - }]; - } else if ([type isEqualToString:NSFileTypeDirectory]) { - [array addObject:@{ - @"path" : [[relativePath stringByAppendingPathComponent:item] stringByAppendingString:@"/"], - @"name" : item - }]; - } - } - } - return [GCDWebServerDataResponse responseWithJSONObject:array]; -} - -- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request { - NSString* relativePath = [[request query] objectForKey:@"path"]; - NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - BOOL isDirectory = NO; - if (![self _checkSandboxedPath:absolutePath] || ![[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 a directory", relativePath]; - } - - NSString* fileName = [absolutePath lastPathComponent]; - if (([fileName hasPrefix:@"."] && !_allowHidden) || ![self _checkFileExtension:fileName]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading file name \"%@\" is not allowed", fileName]; - } - - if ([self.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate webUploader:self didDownloadFileAtPath:absolutePath]; - }); - } - return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES]; -} - -- (GCDWebServerResponse*)uploadFile:(GCDWebServerMultiPartFormRequest*)request { - NSRange range = [[request.headers objectForKey:@"Accept"] rangeOfString:@"application/json" options:NSCaseInsensitiveSearch]; - NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8"); // Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup) - - GCDWebServerMultiPartFile* file = [request firstFileForControlName:@"files[]"]; - if ((!_allowHidden && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName]; - } - NSString* relativePath = [[request firstArgumentForControlName:@"path"] string]; - NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]]; - if (![self _checkSandboxedPath:absolutePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - - if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath]; - } - - NSError* error = nil; - if (![[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath]; - } - - if ([self.delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate webUploader:self didUploadFileAtPath:absolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType]; -} - -- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request { - NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"]; - NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath]; - BOOL isDirectory = NO; - if (![self _checkSandboxedPath:oldAbsolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath]; - } - - NSString* newRelativePath = [request.arguments objectForKey:@"newPath"]; - NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]]; - if (![self _checkSandboxedPath:newAbsolutePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", newRelativePath]; - } - - NSString* itemName = [newAbsolutePath lastPathComponent]; - if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving to item name \"%@\" is not allowed", itemName]; - } - - if (![self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", oldRelativePath, newRelativePath]; - } - - NSError* error = nil; - if (![[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) { - return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath]; - } - - if ([self.delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate webUploader:self didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; -} - -- (GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request { - NSString* relativePath = [request.arguments objectForKey:@"path"]; - NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; - BOOL isDirectory = NO; - if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - - NSString* itemName = [absolutePath lastPathComponent]; - if (([itemName hasPrefix:@"."] && !_allowHidden) || (!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(webUploader:didDeleteItemAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate webUploader:self didDeleteItemAtPath:absolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; -} - -- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request { - NSString* relativePath = [request.arguments objectForKey:@"path"]; - NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]]; - if (![self _checkSandboxedPath:absolutePath]) { - return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; - } - - NSString* directoryName = [absolutePath lastPathComponent]; - if (!_allowHidden && [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]; - } - - if ([self.delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate webUploader:self didCreateDirectoryAtPath:absolutePath]; - }); - } - return [GCDWebServerDataResponse responseWithJSONObject:@{}]; -} - -@end +NS_ASSUME_NONNULL_END @implementation GCDWebUploader -@synthesize uploadDirectory = _uploadDirectory, allowedFileExtensions = _allowedExtensions, allowHiddenItems = _allowHidden, - title = _title, header = _header, prologue = _prologue, epilogue = _epilogue, footer = _footer; - @dynamic delegate; - (instancetype)initWithUploadDirectory:(NSString*)path { if ((self = [super init])) { - NSBundle* siteBundle = [NSBundle bundleWithPath:[[NSBundle bundleForClass:[GCDWebUploader class]] pathForResource:@"GCDWebUploader" ofType:@"bundle"]]; + NSString* bundlePath = [[NSBundle bundleForClass:[GCDWebUploader class]] pathForResource:@"GCDWebUploader" ofType:@"bundle"]; + if (bundlePath == nil) { + return nil; + } + NSBundle* siteBundle = [NSBundle bundleWithPath:bundlePath]; if (siteBundle == nil) { return nil; } @@ -304,7 +77,7 @@ GCDWebUploader* __unsafe_unretained server = self; // Resource files - [self addGETHandlerForBasePath:@"/" directoryPath:[siteBundle resourcePath] indexFilename:nil cacheAge:3600 allowRangeRequests:NO]; + [self addGETHandlerForBasePath:@"/" directoryPath:(NSString*)[siteBundle resourcePath] indexFilename:nil cacheAge:3600 allowRangeRequests:NO]; // Web page [self addHandlerForMethod:@"GET" @@ -353,7 +126,7 @@ #endif footer = [NSString stringWithFormat:[siteBundle localizedStringForKey:@"FOOTER_FORMAT" value:@"" table:nil], name, version]; } - return [GCDWebServerDataResponse responseWithHTMLTemplate:[siteBundle pathForResource:@"index" ofType:@"html"] + return [GCDWebServerDataResponse responseWithHTMLTemplate:(NSString*)[siteBundle pathForResource:@"index" ofType:@"html"] variables:@{ @"device" : device, @"title" : title, @@ -418,6 +191,234 @@ @end +@implementation GCDWebUploader (Methods) + +// Must match implementation in GCDWebDAVServer +- (BOOL)_checkSandboxedPath:(NSString*)path { + return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory]; +} + +- (BOOL)_checkFileExtension:(NSString*)fileName { + if (_allowedFileExtensions && ![_allowedFileExtensions containsObject:[[fileName pathExtension] lowercaseString]]) { + return NO; + } + return YES; +} + +- (NSString*)_uniquePathForPath:(NSString*)path { + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSString* directory = [path stringByDeletingLastPathComponent]; + NSString* file = [path lastPathComponent]; + NSString* base = [file stringByDeletingPathExtension]; + NSString* extension = [file pathExtension]; + int retries = 0; + do { + if (extension.length) { + path = [directory stringByAppendingPathComponent:(NSString*)[[base stringByAppendingFormat:@" (%i)", ++retries] stringByAppendingPathExtension:extension]]; + } else { + path = [directory stringByAppendingPathComponent:[base stringByAppendingFormat:@" (%i)", ++retries]]; + } + } while ([[NSFileManager defaultManager] fileExistsAtPath:path]); + } + return path; +} + +- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request { + NSString* relativePath = [[request query] objectForKey:@"path"]; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + BOOL isDirectory = NO; + if (![self _checkSandboxedPath:absolutePath] || ![[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 directory", relativePath]; + } + + NSString* directoryName = [absolutePath lastPathComponent]; + if (!_allowHiddenItems && [directoryName hasPrefix:@"."]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Listing directory name \"%@\" is not allowed", directoryName]; + } + + NSError* error = nil; + NSArray* contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error]; + if (contents == nil) { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath]; + } + + NSMutableArray* array = [NSMutableArray array]; + for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) { + if (_allowHiddenItems || ![item hasPrefix:@"."]) { + NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL]; + NSString* type = [attributes objectForKey:NSFileType]; + if ([type isEqualToString:NSFileTypeRegular] && [self _checkFileExtension:item]) { + [array addObject:@{ + @"path" : [relativePath stringByAppendingPathComponent:item], + @"name" : item, + @"size" : [attributes objectForKey:NSFileSize] + }]; + } else if ([type isEqualToString:NSFileTypeDirectory]) { + [array addObject:@{ + @"path" : [[relativePath stringByAppendingPathComponent:item] stringByAppendingString:@"/"], + @"name" : item + }]; + } + } + } + return [GCDWebServerDataResponse responseWithJSONObject:array]; +} + +- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request { + NSString* relativePath = [[request query] objectForKey:@"path"]; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + BOOL isDirectory = NO; + if (![self _checkSandboxedPath:absolutePath] || ![[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 a directory", relativePath]; + } + + NSString* fileName = [absolutePath lastPathComponent]; + if (([fileName hasPrefix:@"."] && !_allowHiddenItems) || ![self _checkFileExtension:fileName]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading file name \"%@\" is not allowed", fileName]; + } + + if ([self.delegate respondsToSelector:@selector(webUploader:didDownloadFileAtPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate webUploader:self didDownloadFileAtPath:absolutePath]; + }); + } + return [GCDWebServerFileResponse responseWithFile:absolutePath isAttachment:YES]; +} + +- (GCDWebServerResponse*)uploadFile:(GCDWebServerMultiPartFormRequest*)request { + NSRange range = [[request.headers objectForKey:@"Accept"] rangeOfString:@"application/json" options:NSCaseInsensitiveSearch]; + NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8"); // Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup) + + GCDWebServerMultiPartFile* file = [request firstFileForControlName:@"files[]"]; + if ((!_allowHiddenItems && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName]; + } + NSString* relativePath = [[request firstArgumentForControlName:@"path"] string]; + NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]]; + if (![self _checkSandboxedPath:absolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; + } + + if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath]; + } + + NSError* error = nil; + if (![[NSFileManager defaultManager] moveItemAtPath:file.temporaryPath toPath:absolutePath error:&error]) { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath]; + } + + if ([self.delegate respondsToSelector:@selector(webUploader:didUploadFileAtPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate webUploader:self didUploadFileAtPath:absolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{} contentType:contentType]; +} + +- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request { + NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"]; + NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath]; + BOOL isDirectory = NO; + if (![self _checkSandboxedPath:oldAbsolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath]; + } + + NSString* newRelativePath = [request.arguments objectForKey:@"newPath"]; + NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]]; + if (![self _checkSandboxedPath:newAbsolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", newRelativePath]; + } + + NSString* itemName = [newAbsolutePath lastPathComponent]; + if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving to item name \"%@\" is not allowed", itemName]; + } + + if (![self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", oldRelativePath, newRelativePath]; + } + + NSError* error = nil; + if (![[NSFileManager defaultManager] moveItemAtPath:oldAbsolutePath toPath:newAbsolutePath error:&error]) { + return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving \"%@\" to \"%@\"", oldRelativePath, newRelativePath]; + } + + if ([self.delegate respondsToSelector:@selector(webUploader:didMoveItemFromPath:toPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate webUploader:self didMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{}]; +} + +- (GCDWebServerResponse*)deleteItem:(GCDWebServerURLEncodedFormRequest*)request { + NSString* relativePath = [request.arguments objectForKey:@"path"]; + NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; + BOOL isDirectory = NO; + if (![self _checkSandboxedPath:absolutePath] || ![[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(webUploader:didDeleteItemAtPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate webUploader:self didDeleteItemAtPath:absolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{}]; +} + +- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request { + NSString* relativePath = [request.arguments objectForKey:@"path"]; + NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]]; + if (![self _checkSandboxedPath:absolutePath]) { + return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", 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]; + } + + if ([self.delegate respondsToSelector:@selector(webUploader:didCreateDirectoryAtPath:)]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate webUploader:self didCreateDirectoryAtPath:absolutePath]; + }); + } + return [GCDWebServerDataResponse responseWithJSONObject:@{}]; +} + +@end + @implementation GCDWebUploader (Subclassing) - (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath { diff --git a/Mac/main.m b/Mac/main.m index 11b990d..780f5da 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -370,7 +370,7 @@ int main(int argc, const char* argv[]) { asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[@"Hello World!" dataUsingEncoding:NSUTF8StringEncoding] contentType:@"text/plain"]; + GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:(NSData*)[@"Hello World!" dataUsingEncoding:NSUTF8StringEncoding] contentType:@"text/plain"]; completionBlock(response); });