DAVResponse.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. #import <libxml/parser.h>
  2. #import "DAVResponse.h"
  3. #import "HTTPLogging.h"
  4. // WebDAV specifications: http://webdav.org/specs/rfc4918.html
  5. typedef enum {
  6. kDAVProperty_ResourceType = (1 << 0),
  7. kDAVProperty_CreationDate = (1 << 1),
  8. kDAVProperty_LastModified = (1 << 2),
  9. kDAVProperty_ContentLength = (1 << 3),
  10. kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength
  11. } DAVProperties;
  12. #define kXMLParseOptions (XML_PARSE_NONET | XML_PARSE_RECOVER | XML_PARSE_NOBLANKS | XML_PARSE_COMPACT | XML_PARSE_NOWARNING | XML_PARSE_NOERROR)
  13. static const int httpLogLevel = HTTP_LOG_LEVEL_WARN;
  14. @implementation DAVResponse
  15. static void _AddPropertyResponse(NSString* itemPath, NSString* resourcePath, DAVProperties properties, NSMutableString* xmlString) {
  16. CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL,
  17. CFSTR("<&>?+"), kCFStringEncodingUTF8);
  18. if (escapedPath) {
  19. NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL];
  20. BOOL isDirectory = [[attributes fileType] isEqualToString:NSFileTypeDirectory];
  21. [xmlString appendString:@"<D:response>"];
  22. [xmlString appendFormat:@"<D:href>%@</D:href>", escapedPath];
  23. [xmlString appendString:@"<D:propstat>"];
  24. [xmlString appendString:@"<D:prop>"];
  25. if (properties & kDAVProperty_ResourceType) {
  26. if (isDirectory) {
  27. [xmlString appendString:@"<D:resourcetype><D:collection/></D:resourcetype>"];
  28. } else {
  29. [xmlString appendString:@"<D:resourcetype/>"];
  30. }
  31. }
  32. if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) {
  33. NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
  34. formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
  35. formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
  36. formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'+00:00'";
  37. [xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", [formatter stringFromDate:[attributes fileCreationDate]]];
  38. }
  39. if ((properties & kDAVProperty_LastModified) && [attributes objectForKey:NSFileModificationDate]) {
  40. NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
  41. formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
  42. formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];
  43. formatter.dateFormat = @"EEE', 'd' 'MMM' 'yyyy' 'HH:mm:ss' GMT'";
  44. [xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", [formatter stringFromDate:[attributes fileModificationDate]]];
  45. }
  46. if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) {
  47. [xmlString appendFormat:@"<D:getcontentlength>%qu</D:getcontentlength>", [attributes fileSize]];
  48. }
  49. [xmlString appendString:@"</D:prop>"];
  50. [xmlString appendString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
  51. [xmlString appendString:@"</D:propstat>"];
  52. [xmlString appendString:@"</D:response>\n"];
  53. CFRelease(escapedPath);
  54. }
  55. }
  56. static xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name) {
  57. while (child) {
  58. if ((child->type == XML_ELEMENT_NODE) && !xmlStrcmp(child->name, name)) {
  59. return child;
  60. }
  61. child = child->next;
  62. }
  63. return NULL;
  64. }
  65. - (id) initWithMethod:(NSString*)method headers:(NSDictionary*)headers bodyData:(NSData*)body resourcePath:(NSString*)resourcePath rootPath:(NSString*)rootPath {
  66. if ((self = [super init])) {
  67. _status = 200;
  68. _headers = [[NSMutableDictionary alloc] init];
  69. // 10.1 DAV Header
  70. if ([method isEqualToString:@"OPTIONS"]) {
  71. if ([[headers objectForKey:@"User-Agent"] hasPrefix:@"WebDAVFS/"]) { // Mac OS X WebDAV support
  72. [_headers setObject:@"1, 2" forKey:@"DAV"];
  73. } else {
  74. [_headers setObject:@"1" forKey:@"DAV"];
  75. }
  76. }
  77. // 9.1 PROPFIND Method
  78. if ([method isEqualToString:@"PROPFIND"]) {
  79. NSInteger depth;
  80. NSString* depthHeader = [headers objectForKey:@"Depth"];
  81. if ([depthHeader isEqualToString:@"0"]) {
  82. depth = 0;
  83. } else if ([depthHeader isEqualToString:@"1"]) {
  84. depth = 1;
  85. } else {
  86. HTTPLogError(@"Unsupported DAV depth \"%@\"", depthHeader);
  87. return nil;
  88. }
  89. DAVProperties properties = 0;
  90. xmlDocPtr document = xmlReadMemory(body.bytes, (int)body.length, NULL, NULL, kXMLParseOptions);
  91. if (document) {
  92. xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"propfind");
  93. if (node) {
  94. node = _XMLChildWithName(node->children, (const xmlChar*)"prop");
  95. }
  96. if (node) {
  97. node = node->children;
  98. while (node) {
  99. if (!xmlStrcmp(node->name, (const xmlChar*)"resourcetype")) {
  100. properties |= kDAVProperty_ResourceType;
  101. } else if (!xmlStrcmp(node->name, (const xmlChar*)"creationdate")) {
  102. properties |= kDAVProperty_CreationDate;
  103. } else if (!xmlStrcmp(node->name, (const xmlChar*)"getlastmodified")) {
  104. properties |= kDAVProperty_LastModified;
  105. } else if (!xmlStrcmp(node->name, (const xmlChar*)"getcontentlength")) {
  106. properties |= kDAVProperty_ContentLength;
  107. } else {
  108. HTTPLogWarn(@"Unknown DAV property requested \"%s\"", node->name);
  109. }
  110. node = node->next;
  111. }
  112. } else {
  113. HTTPLogWarn(@"HTTP Server: Invalid DAV properties\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
  114. }
  115. xmlFreeDoc(document);
  116. }
  117. if (!properties) {
  118. properties = kDAVAllProperties;
  119. }
  120. NSString* basePath = [rootPath stringByAppendingPathComponent:resourcePath];
  121. if (![basePath hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:basePath]) {
  122. return nil;
  123. }
  124. NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
  125. [xmlString appendString:@"<D:multistatus xmlns:D=\"DAV:\">\n"];
  126. if (![resourcePath hasPrefix:@"/"]) {
  127. resourcePath = [@"/" stringByAppendingString:resourcePath];
  128. }
  129. _AddPropertyResponse(basePath, resourcePath, properties, xmlString);
  130. if (depth == 1) {
  131. if (![resourcePath hasSuffix:@"/"]) {
  132. resourcePath = [resourcePath stringByAppendingString:@"/"];
  133. }
  134. NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:basePath];
  135. NSString* path;
  136. while ((path = [enumerator nextObject])) {
  137. _AddPropertyResponse([basePath stringByAppendingPathComponent:path], [resourcePath stringByAppendingString:path], properties, xmlString);
  138. [enumerator skipDescendents];
  139. }
  140. }
  141. [xmlString appendString:@"</D:multistatus>"];
  142. [_headers setObject:@"application/xml; charset=\"utf-8\"" forKey:@"Content-Type"];
  143. _data = [xmlString dataUsingEncoding:NSUTF8StringEncoding];
  144. _status = 207;
  145. }
  146. // 9.3 MKCOL Method
  147. if ([method isEqualToString:@"MKCOL"]) {
  148. NSString* path = [rootPath stringByAppendingPathComponent:resourcePath];
  149. if (![path hasPrefix:rootPath]) {
  150. return nil;
  151. }
  152. if (![[NSFileManager defaultManager] fileExistsAtPath:[path stringByDeletingLastPathComponent]]) {
  153. HTTPLogError(@"Missing intermediate collection(s) at \"%@\"", path);
  154. _status = 409;
  155. } else if (![[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:NULL]) {
  156. HTTPLogError(@"Failed creating collection at \"%@\"", path);
  157. _status = 405;
  158. }
  159. }
  160. // 9.8 COPY Method
  161. // 9.9 MOVE Method
  162. if ([method isEqualToString:@"MOVE"] || [method isEqualToString:@"COPY"]) {
  163. if ([method isEqualToString:@"COPY"] && ![[headers objectForKey:@"Depth"] isEqualToString:@"infinity"]) {
  164. HTTPLogError(@"Unsupported DAV depth \"%@\"", [headers objectForKey:@"Depth"]);
  165. return nil;
  166. }
  167. NSString* sourcePath = [rootPath stringByAppendingPathComponent:resourcePath];
  168. if (![sourcePath hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:sourcePath]) {
  169. return nil;
  170. }
  171. NSString* destination = [headers objectForKey:@"Destination"];
  172. NSRange range = [destination rangeOfString:[headers objectForKey:@"Host"]];
  173. if (range.location == NSNotFound) {
  174. return nil;
  175. }
  176. NSString* destinationPath = [rootPath stringByAppendingPathComponent:
  177. [[destination substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
  178. if (![destinationPath hasPrefix:rootPath] || [[NSFileManager defaultManager] fileExistsAtPath:destinationPath]) {
  179. return nil;
  180. }
  181. BOOL isDirectory;
  182. if (![[NSFileManager defaultManager] fileExistsAtPath:[destinationPath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
  183. HTTPLogError(@"Invalid destination path \"%@\"", destinationPath);
  184. _status = 409;
  185. } else {
  186. BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:destinationPath];
  187. if (existing && [[headers objectForKey:@"Overwrite"] isEqualToString:@"F"]) {
  188. HTTPLogError(@"Pre-existing destination path \"%@\"", destinationPath);
  189. _status = 412;
  190. } else {
  191. if ([method isEqualToString:@"COPY"]) {
  192. if ([[NSFileManager defaultManager] copyItemAtPath:sourcePath toPath:destinationPath error:NULL]) {
  193. _status = existing ? 204 : 201;
  194. } else {
  195. HTTPLogError(@"Failed copying \"%@\" to \"%@\"", sourcePath, destinationPath);
  196. _status = 403;
  197. }
  198. } else {
  199. if ([[NSFileManager defaultManager] moveItemAtPath:sourcePath toPath:destinationPath error:NULL]) {
  200. _status = existing ? 204 : 201;
  201. } else {
  202. HTTPLogError(@"Failed moving \"%@\" to \"%@\"", sourcePath, destinationPath);
  203. _status = 403;
  204. }
  205. }
  206. }
  207. }
  208. }
  209. // 9.10 LOCK Method - TODO: Actually lock the resource
  210. if ([method isEqualToString:@"LOCK"]) {
  211. NSString* path = [rootPath stringByAppendingPathComponent:resourcePath];
  212. if (![path hasPrefix:rootPath]) {
  213. return nil;
  214. }
  215. NSString* depth = [headers objectForKey:@"Depth"];
  216. NSString* scope = nil;
  217. NSString* type = nil;
  218. NSString* owner = nil;
  219. NSString* token = nil;
  220. xmlDocPtr document = xmlReadMemory(body.bytes, (int)body.length, NULL, NULL, kXMLParseOptions);
  221. if (document) {
  222. xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"lockinfo");
  223. if (node) {
  224. xmlNodePtr scopeNode = _XMLChildWithName(node->children, (const xmlChar*)"lockscope");
  225. if (scopeNode && scopeNode->children && scopeNode->children->name) {
  226. scope = [NSString stringWithUTF8String:(const char*)scopeNode->children->name];
  227. }
  228. xmlNodePtr typeNode = _XMLChildWithName(node->children, (const xmlChar*)"locktype");
  229. if (typeNode && typeNode->children && typeNode->children->name) {
  230. type = [NSString stringWithUTF8String:(const char*)typeNode->children->name];
  231. }
  232. xmlNodePtr ownerNode = _XMLChildWithName(node->children, (const xmlChar*)"owner");
  233. if (ownerNode) {
  234. ownerNode = _XMLChildWithName(ownerNode->children, (const xmlChar*)"href");
  235. if (ownerNode && ownerNode->children && ownerNode->children->content) {
  236. owner = [NSString stringWithUTF8String:(const char*)ownerNode->children->content];
  237. }
  238. }
  239. } else {
  240. HTTPLogWarn(@"HTTP Server: Invalid DAV properties\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
  241. }
  242. xmlFreeDoc(document);
  243. } else {
  244. // 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
  245. // into the lock create case.
  246. NSString* lockToken;
  247. if ((lockToken = [headers objectForKey:@"If"]) != nil) {
  248. scope = @"exclusive";
  249. type = @"write";
  250. depth = @"0";
  251. token = [lockToken stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"(<>)"]];
  252. }
  253. }
  254. if ([scope isEqualToString:@"exclusive"] && [type isEqualToString:@"write"] && [depth isEqualToString:@"0"] &&
  255. ([[NSFileManager defaultManager] fileExistsAtPath:path] || [[NSData data] writeToFile:path atomically:YES])) {
  256. NSString* timeout = [headers objectForKey:@"Timeout"];
  257. if (!token) {
  258. CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
  259. NSString *uuidStr = (__bridge_transfer NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuid);
  260. token = [NSString stringWithFormat:@"urn:uuid:%@", uuidStr];
  261. CFRelease(uuid);
  262. }
  263. NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
  264. [xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
  265. [xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
  266. [xmlString appendFormat:@"<D:locktype><D:%@/></D:locktype>\n", type];
  267. [xmlString appendFormat:@"<D:lockscope><D:%@/></D:lockscope>\n", scope];
  268. [xmlString appendFormat:@"<D:depth>%@</D:depth>\n", depth];
  269. if (owner) {
  270. [xmlString appendFormat:@"<D:owner><D:href>%@</D:href></D:owner>\n", owner];
  271. }
  272. if (timeout) {
  273. [xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeout];
  274. }
  275. [xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
  276. NSString* lockroot = [@"http://" stringByAppendingString:[[headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:resourcePath]]];
  277. [xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
  278. [xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
  279. [xmlString appendString:@"</D:prop>"];
  280. [_headers setObject:@"application/xml; charset=\"utf-8\"" forKey:@"Content-Type"];
  281. _data = [xmlString dataUsingEncoding:NSUTF8StringEncoding];
  282. _status = 200;
  283. HTTPLogVerbose(@"Pretending to lock \"%@\"", resourcePath);
  284. } else {
  285. HTTPLogError(@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depth, resourcePath);
  286. _status = 403;
  287. }
  288. }
  289. // 9.11 UNLOCK Method - TODO: Actually unlock the resource
  290. if ([method isEqualToString:@"UNLOCK"]) {
  291. NSString* path = [rootPath stringByAppendingPathComponent:resourcePath];
  292. if (![path hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:path]) {
  293. return nil;
  294. }
  295. NSString* token = [headers objectForKey:@"Lock-Token"];
  296. _status = token ? 204 : 400;
  297. HTTPLogVerbose(@"Pretending to unlock \"%@\"", resourcePath);
  298. }
  299. }
  300. return self;
  301. }
  302. - (UInt64) contentLength {
  303. return _data ? _data.length : 0;
  304. }
  305. - (UInt64) offset {
  306. return _offset;
  307. }
  308. - (void) setOffset:(UInt64)offset {
  309. _offset = offset;
  310. }
  311. - (NSData*) readDataOfLength:(NSUInteger)lengthParameter {
  312. if (_data) {
  313. NSUInteger remaining = _data.length - (NSUInteger)_offset;
  314. NSUInteger length = lengthParameter < remaining ? lengthParameter : remaining;
  315. void* bytes = (void*)(_data.bytes + _offset);
  316. _offset += length;
  317. return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO];
  318. }
  319. return nil;
  320. }
  321. - (BOOL) isDone {
  322. return _data ? _offset == _data.length : YES;
  323. }
  324. - (NSInteger) status {
  325. return _status;
  326. }
  327. - (NSDictionary*) httpHeaders {
  328. return _headers;
  329. }
  330. @end