#import #import "DAVResponse.h" #import "HTTPLogging.h" // WebDAV specifications: http://webdav.org/specs/rfc4918.html typedef enum { kDAVProperty_ResourceType = (1 << 0), kDAVProperty_CreationDate = (1 << 1), kDAVProperty_LastModified = (1 << 2), kDAVProperty_ContentLength = (1 << 3), kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength } DAVProperties; #define kXMLParseOptions (XML_PARSE_NONET | XML_PARSE_RECOVER | XML_PARSE_NOBLANKS | XML_PARSE_COMPACT | XML_PARSE_NOWARNING | XML_PARSE_NOERROR) static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; @implementation DAVResponse static void _AddPropertyResponse(NSString* itemPath, NSString* resourcePath, DAVProperties properties, NSMutableString* xmlString) { CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL, CFSTR("<&>?+"), kCFStringEncodingUTF8); if (escapedPath) { NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL]; BOOL isDirectory = [[attributes fileType] isEqualToString:NSFileTypeDirectory]; [xmlString appendString:@""]; [xmlString appendFormat:@"%@", escapedPath]; [xmlString appendString:@""]; [xmlString appendString:@""]; if (properties & kDAVProperty_ResourceType) { if (isDirectory) { [xmlString appendString:@""]; } else { [xmlString appendString:@""]; } } if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) { NSDateFormatter* formatter = [[NSDateFormatter alloc] init]; formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"]; formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'"; [xmlString appendFormat:@"%@", [formatter stringFromDate:[attributes fileCreationDate]]]; } if ((properties & kDAVProperty_LastModified) && [attributes objectForKey:NSFileModificationDate]) { NSDateFormatter* formatter = [[NSDateFormatter alloc] init]; formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"]; formatter.dateFormat = @"EEE', 'd' 'MMM' 'yyyy' 'HH:mm:ss' GMT'"; [xmlString appendFormat:@"%@", [formatter stringFromDate:[attributes fileModificationDate]]]; } if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) { [xmlString appendFormat:@"%qu", [attributes fileSize]]; } [xmlString appendString:@""]; [xmlString appendString:@"HTTP/1.1 200 OK"]; [xmlString appendString:@""]; [xmlString appendString:@"\n"]; CFRelease(escapedPath); } } static 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; } - (id) initWithMethod:(NSString*)method headers:(NSDictionary*)headers bodyData:(NSData*)body resourcePath:(NSString*)resourcePath rootPath:(NSString*)rootPath { if ((self = [super init])) { _status = 200; _headers = [[NSMutableDictionary alloc] init]; // 10.1 DAV Header if ([method isEqualToString:@"OPTIONS"]) { if ([[headers objectForKey:@"User-Agent"] hasPrefix:@"WebDAVFS/"]) { // Mac OS X WebDAV support [_headers setObject:@"1, 2" forKey:@"DAV"]; } else { [_headers setObject:@"1" forKey:@"DAV"]; } } // 9.1 PROPFIND Method if ([method isEqualToString:@"PROPFIND"]) { NSInteger depth; NSString* depthHeader = [headers objectForKey:@"Depth"]; if ([depthHeader isEqualToString:@"0"]) { depth = 0; } else if ([depthHeader isEqualToString:@"1"]) { depth = 1; } else { HTTPLogError(@"Unsupported DAV depth \"%@\"", depthHeader); return nil; } DAVProperties properties = 0; xmlDocPtr document = xmlReadMemory(body.bytes, (int)body.length, NULL, NULL, kXMLParseOptions); if (document) { xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"propfind"); if (node) { node = _XMLChildWithName(node->children, (const xmlChar*)"prop"); } if (node) { node = node->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 { HTTPLogWarn(@"Unknown DAV property requested \"%s\"", node->name); } node = node->next; } } else { HTTPLogWarn(@"HTTP Server: Invalid DAV properties\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]); } xmlFreeDoc(document); } if (!properties) { properties = kDAVAllProperties; } NSString* basePath = [rootPath stringByAppendingPathComponent:resourcePath]; if (![basePath hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:basePath]) { return nil; } NSMutableString* xmlString = [NSMutableString stringWithString:@""]; [xmlString appendString:@"\n"]; if (![resourcePath hasPrefix:@"/"]) { resourcePath = [@"/" stringByAppendingString:resourcePath]; } _AddPropertyResponse(basePath, resourcePath, properties, xmlString); if (depth == 1) { if (![resourcePath hasSuffix:@"/"]) { resourcePath = [resourcePath stringByAppendingString:@"/"]; } NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:basePath]; NSString* path; while ((path = [enumerator nextObject])) { _AddPropertyResponse([basePath stringByAppendingPathComponent:path], [resourcePath stringByAppendingString:path], properties, xmlString); [enumerator skipDescendents]; } } [xmlString appendString:@""]; [_headers setObject:@"application/xml; charset=\"utf-8\"" forKey:@"Content-Type"]; _data = [xmlString dataUsingEncoding:NSUTF8StringEncoding]; _status = 207; } // 9.3 MKCOL Method if ([method isEqualToString:@"MKCOL"]) { NSString* path = [rootPath stringByAppendingPathComponent:resourcePath]; if (![path hasPrefix:rootPath]) { return nil; } if (![[NSFileManager defaultManager] fileExistsAtPath:[path stringByDeletingLastPathComponent]]) { HTTPLogError(@"Missing intermediate collection(s) at \"%@\"", path); _status = 409; } else if (![[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:NULL]) { HTTPLogError(@"Failed creating collection at \"%@\"", path); _status = 405; } } // 9.8 COPY Method // 9.9 MOVE Method if ([method isEqualToString:@"MOVE"] || [method isEqualToString:@"COPY"]) { if ([method isEqualToString:@"COPY"] && ![[headers objectForKey:@"Depth"] isEqualToString:@"infinity"]) { HTTPLogError(@"Unsupported DAV depth \"%@\"", [headers objectForKey:@"Depth"]); return nil; } NSString* sourcePath = [rootPath stringByAppendingPathComponent:resourcePath]; if (![sourcePath hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:sourcePath]) { return nil; } NSString* destination = [headers objectForKey:@"Destination"]; NSRange range = [destination rangeOfString:[headers objectForKey:@"Host"]]; if (range.location == NSNotFound) { return nil; } NSString* destinationPath = [rootPath stringByAppendingPathComponent: [[destination substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; if (![destinationPath hasPrefix:rootPath] || [[NSFileManager defaultManager] fileExistsAtPath:destinationPath]) { return nil; } BOOL isDirectory; if (![[NSFileManager defaultManager] fileExistsAtPath:[destinationPath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) { HTTPLogError(@"Invalid destination path \"%@\"", destinationPath); _status = 409; } else { BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:destinationPath]; if (existing && [[headers objectForKey:@"Overwrite"] isEqualToString:@"F"]) { HTTPLogError(@"Pre-existing destination path \"%@\"", destinationPath); _status = 412; } else { if ([method isEqualToString:@"COPY"]) { if ([[NSFileManager defaultManager] copyItemAtPath:sourcePath toPath:destinationPath error:NULL]) { _status = existing ? 204 : 201; } else { HTTPLogError(@"Failed copying \"%@\" to \"%@\"", sourcePath, destinationPath); _status = 403; } } else { if ([[NSFileManager defaultManager] moveItemAtPath:sourcePath toPath:destinationPath error:NULL]) { _status = existing ? 204 : 201; } else { HTTPLogError(@"Failed moving \"%@\" to \"%@\"", sourcePath, destinationPath); _status = 403; } } } } } // 9.10 LOCK Method - TODO: Actually lock the resource if ([method isEqualToString:@"LOCK"]) { NSString* path = [rootPath stringByAppendingPathComponent:resourcePath]; if (![path hasPrefix:rootPath]) { return nil; } NSString* depth = [headers objectForKey:@"Depth"]; NSString* scope = nil; NSString* type = nil; NSString* owner = nil; NSString* token = nil; xmlDocPtr document = xmlReadMemory(body.bytes, (int)body.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 { HTTPLogWarn(@"HTTP Server: Invalid DAV properties\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]); } xmlFreeDoc(document); } else { // No body, see if they're trying to refresh an existing lock. If so, then just fake up the scope, type and depth so we fall // into the lock create case. NSString* lockToken; if ((lockToken = [headers objectForKey:@"If"]) != nil) { scope = @"exclusive"; type = @"write"; depth = @"0"; token = [lockToken stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"(<>)"]]; } } if ([scope isEqualToString:@"exclusive"] && [type isEqualToString:@"write"] && [depth isEqualToString:@"0"] && ([[NSFileManager defaultManager] fileExistsAtPath:path] || [[NSData data] writeToFile:path atomically:YES])) { NSString* timeout = [headers objectForKey:@"Timeout"]; if (!token) { CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); NSString *uuidStr = (__bridge_transfer NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuid); token = [NSString stringWithFormat:@"urn:uuid:%@", uuidStr]; CFRelease(uuid); } NSMutableString* xmlString = [NSMutableString stringWithString:@""]; [xmlString appendString:@"\n"]; [xmlString appendString:@"\n\n"]; [xmlString appendFormat:@"\n", type]; [xmlString appendFormat:@"\n", scope]; [xmlString appendFormat:@"%@\n", depth]; if (owner) { [xmlString appendFormat:@"%@\n", owner]; } if (timeout) { [xmlString appendFormat:@"%@\n", timeout]; } [xmlString appendFormat:@"%@\n", token]; NSString* lockroot = [@"http://" stringByAppendingString:[[headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:resourcePath]]]; [xmlString appendFormat:@"%@\n", lockroot]; [xmlString appendString:@"\n\n"]; [xmlString appendString:@""]; [_headers setObject:@"application/xml; charset=\"utf-8\"" forKey:@"Content-Type"]; _data = [xmlString dataUsingEncoding:NSUTF8StringEncoding]; _status = 200; HTTPLogVerbose(@"Pretending to lock \"%@\"", resourcePath); } else { HTTPLogError(@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depth, resourcePath); _status = 403; } } // 9.11 UNLOCK Method - TODO: Actually unlock the resource if ([method isEqualToString:@"UNLOCK"]) { NSString* path = [rootPath stringByAppendingPathComponent:resourcePath]; if (![path hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:path]) { return nil; } NSString* token = [headers objectForKey:@"Lock-Token"]; _status = token ? 204 : 400; HTTPLogVerbose(@"Pretending to unlock \"%@\"", resourcePath); } } return self; } - (UInt64) contentLength { return _data ? _data.length : 0; } - (UInt64) offset { return _offset; } - (void) setOffset:(UInt64)offset { _offset = offset; } - (NSData*) readDataOfLength:(NSUInteger)lengthParameter { if (_data) { NSUInteger remaining = _data.length - (NSUInteger)_offset; NSUInteger length = lengthParameter < remaining ? lengthParameter : remaining; void* bytes = (void*)(_data.bytes + _offset); _offset += length; return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO]; } return nil; } - (BOOL) isDone { return _data ? _offset == _data.length : YES; } - (NSInteger) status { return _status; } - (NSDictionary*) httpHeaders { return _headers; } @end