GCDWebDAVServer.m 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. /*
  2. Copyright (c) 2012-2019, Pierre-Olivier Latour
  3. All rights reserved.
  4. Redistribution and use in source and binary forms, with or without
  5. modification, are permitted provided that the following conditions are met:
  6. * Redistributions of source code must retain the above copyright
  7. notice, this list of conditions and the following disclaimer.
  8. * Redistributions in binary form must reproduce the above copyright
  9. notice, this list of conditions and the following disclaimer in the
  10. documentation and/or other materials provided with the distribution.
  11. * The name of Pierre-Olivier Latour may not be used to endorse
  12. or promote products derived from this software without specific
  13. prior written permission.
  14. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  15. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  16. WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  17. DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
  18. DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  19. (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  20. LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  21. ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  22. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  23. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  24. */
  25. #if !__has_feature(objc_arc)
  26. #error GCDWebDAVServer requires ARC
  27. #endif
  28. // WebDAV specifications: http://webdav.org/specs/rfc4918.html
  29. // Requires "HEADER_SEARCH_PATHS = $(SDKROOT)/usr/include/libxml2" in Xcode build settings
  30. #import <libxml/parser.h>
  31. #import "GCDWebDAVServer.h"
  32. #import "GCDWebServerFunctions.h"
  33. #import "GCDWebServerDataRequest.h"
  34. #import "GCDWebServerFileRequest.h"
  35. #import "GCDWebServerDataResponse.h"
  36. #import "GCDWebServerErrorResponse.h"
  37. #import "GCDWebServerFileResponse.h"
  38. #define kXMLParseOptions (XML_PARSE_NONET | XML_PARSE_RECOVER | XML_PARSE_NOBLANKS | XML_PARSE_COMPACT | XML_PARSE_NOWARNING | XML_PARSE_NOERROR)
  39. typedef NS_ENUM(NSInteger, DAVProperties) {
  40. kDAVProperty_ResourceType = (1 << 0),
  41. kDAVProperty_CreationDate = (1 << 1),
  42. kDAVProperty_LastModified = (1 << 2),
  43. kDAVProperty_ContentLength = (1 << 3),
  44. kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength
  45. };
  46. NS_ASSUME_NONNULL_BEGIN
  47. @interface GCDWebDAVServer (Methods)
  48. - (nullable GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request;
  49. - (nullable GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request;
  50. - (nullable GCDWebServerResponse*)performPUT:(GCDWebServerFileRequest*)request;
  51. - (nullable GCDWebServerResponse*)performDELETE:(GCDWebServerRequest*)request;
  52. - (nullable GCDWebServerResponse*)performMKCOL:(GCDWebServerDataRequest*)request;
  53. - (nullable GCDWebServerResponse*)performCOPY:(GCDWebServerRequest*)request isMove:(BOOL)isMove;
  54. - (nullable GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request;
  55. - (nullable GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request;
  56. - (nullable GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request;
  57. @end
  58. NS_ASSUME_NONNULL_END
  59. @implementation GCDWebDAVServer
  60. @dynamic delegate;
  61. - (instancetype)initWithUploadDirectory:(NSString*)path {
  62. if ((self = [super init])) {
  63. _uploadDirectory = [path copy];
  64. GCDWebDAVServer* __unsafe_unretained server = self;
  65. // 9.1 PROPFIND method
  66. [self addDefaultHandlerForMethod:@"PROPFIND"
  67. requestClass:[GCDWebServerDataRequest class]
  68. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  69. return [server performPROPFIND:(GCDWebServerDataRequest*)request];
  70. }];
  71. // 9.3 MKCOL Method
  72. [self addDefaultHandlerForMethod:@"MKCOL"
  73. requestClass:[GCDWebServerDataRequest class]
  74. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  75. return [server performMKCOL:(GCDWebServerDataRequest*)request];
  76. }];
  77. // 9.4 GET & HEAD methods
  78. [self addDefaultHandlerForMethod:@"GET"
  79. requestClass:[GCDWebServerRequest class]
  80. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  81. return [server performGET:request];
  82. }];
  83. // 9.6 DELETE method
  84. [self addDefaultHandlerForMethod:@"DELETE"
  85. requestClass:[GCDWebServerRequest class]
  86. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  87. return [server performDELETE:request];
  88. }];
  89. // 9.7 PUT method
  90. [self addDefaultHandlerForMethod:@"PUT"
  91. requestClass:[GCDWebServerFileRequest class]
  92. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  93. return [server performPUT:(GCDWebServerFileRequest*)request];
  94. }];
  95. // 9.8 COPY method
  96. [self addDefaultHandlerForMethod:@"COPY"
  97. requestClass:[GCDWebServerRequest class]
  98. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  99. return [server performCOPY:request isMove:NO];
  100. }];
  101. // 9.9 MOVE method
  102. [self addDefaultHandlerForMethod:@"MOVE"
  103. requestClass:[GCDWebServerRequest class]
  104. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  105. return [server performCOPY:request isMove:YES];
  106. }];
  107. // 9.10 LOCK method
  108. [self addDefaultHandlerForMethod:@"LOCK"
  109. requestClass:[GCDWebServerDataRequest class]
  110. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  111. return [server performLOCK:(GCDWebServerDataRequest*)request];
  112. }];
  113. // 9.11 UNLOCK method
  114. [self addDefaultHandlerForMethod:@"UNLOCK"
  115. requestClass:[GCDWebServerRequest class]
  116. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  117. return [server performUNLOCK:request];
  118. }];
  119. // 10.1 OPTIONS method / DAV Header
  120. [self addDefaultHandlerForMethod:@"OPTIONS"
  121. requestClass:[GCDWebServerRequest class]
  122. processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
  123. return [server performOPTIONS:request];
  124. }];
  125. }
  126. return self;
  127. }
  128. @end
  129. @implementation GCDWebDAVServer (Methods)
  130. - (BOOL)_checkFileExtension:(NSString*)fileName {
  131. if (_allowedFileExtensions && ![_allowedFileExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
  132. return NO;
  133. }
  134. return YES;
  135. }
  136. static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
  137. NSString* userAgentHeader = [request.headers objectForKey:@"User-Agent"];
  138. return ([userAgentHeader hasPrefix:@"WebDAVFS/"] || [userAgentHeader hasPrefix:@"WebDAVLib/"]); // OS X WebDAV client
  139. }
  140. - (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
  141. GCDWebServerResponse* response = [GCDWebServerResponse response];
  142. if (_IsMacFinder(request)) {
  143. [response setValue:@"1, 2" forAdditionalHeader:@"DAV"]; // Classes 1 and 2
  144. } else {
  145. [response setValue:@"1" forAdditionalHeader:@"DAV"]; // Class 1
  146. }
  147. return response;
  148. }
  149. - (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request {
  150. NSString* relativePath = request.path;
  151. NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
  152. BOOL isDirectory = NO;
  153. if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
  154. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
  155. }
  156. NSString* itemName = [absolutePath lastPathComponent];
  157. if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
  158. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading item name \"%@\" is not allowed", itemName];
  159. }
  160. // 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
  161. if (isDirectory) {
  162. return [GCDWebServerResponse response];
  163. }
  164. if ([self.delegate respondsToSelector:@selector(davServer:didDownloadFileAtPath:)]) {
  165. dispatch_async(dispatch_get_main_queue(), ^{
  166. [self.delegate davServer:self didDownloadFileAtPath:absolutePath];
  167. });
  168. }
  169. if ([request hasByteRange]) {
  170. return [GCDWebServerFileResponse responseWithFile:absolutePath byteRange:request.byteRange];
  171. }
  172. return [GCDWebServerFileResponse responseWithFile:absolutePath];
  173. }
  174. - (GCDWebServerResponse*)performPUT:(GCDWebServerFileRequest*)request {
  175. if ([request hasByteRange]) {
  176. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Range uploads not supported"];
  177. }
  178. NSString* relativePath = request.path;
  179. NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
  180. BOOL isDirectory;
  181. if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
  182. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
  183. }
  184. BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory];
  185. if (existing && isDirectory) {
  186. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"PUT not allowed on existing collection \"%@\"", relativePath];
  187. }
  188. NSString* fileName = [absolutePath lastPathComponent];
  189. if (([fileName hasPrefix:@"."] && !_allowHiddenItems) || ![self _checkFileExtension:fileName]) {
  190. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file name \"%@\" is not allowed", fileName];
  191. }
  192. if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.temporaryPath]) {
  193. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not permitted", relativePath];
  194. }
  195. [[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL];
  196. NSError* error = nil;
  197. if (![[NSFileManager defaultManager] moveItemAtPath:request.temporaryPath toPath:absolutePath error:&error]) {
  198. return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
  199. }
  200. if ([self.delegate respondsToSelector:@selector(davServer:didUploadFileAtPath:)]) {
  201. dispatch_async(dispatch_get_main_queue(), ^{
  202. [self.delegate davServer:self didUploadFileAtPath:absolutePath];
  203. });
  204. }
  205. return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
  206. }
  207. - (GCDWebServerResponse*)performDELETE:(GCDWebServerRequest*)request {
  208. NSString* depthHeader = [request.headers objectForKey:@"Depth"];
  209. if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
  210. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
  211. }
  212. NSString* relativePath = request.path;
  213. NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
  214. BOOL isDirectory = NO;
  215. if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
  216. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
  217. }
  218. NSString* itemName = [absolutePath lastPathComponent];
  219. if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
  220. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
  221. }
  222. if (![self shouldDeleteItemAtPath:absolutePath]) {
  223. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
  224. }
  225. NSError* error = nil;
  226. if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
  227. return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
  228. }
  229. if ([self.delegate respondsToSelector:@selector(davServer:didDeleteItemAtPath:)]) {
  230. dispatch_async(dispatch_get_main_queue(), ^{
  231. [self.delegate davServer:self didDeleteItemAtPath:absolutePath];
  232. });
  233. }
  234. return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
  235. }
  236. - (GCDWebServerResponse*)performMKCOL:(GCDWebServerDataRequest*)request {
  237. if ([request hasBody] && (request.contentLength > 0)) {
  238. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_UnsupportedMediaType message:@"Unexpected request body for MKCOL method"];
  239. }
  240. NSString* relativePath = request.path;
  241. NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
  242. BOOL isDirectory;
  243. if (![[NSFileManager defaultManager] fileExistsAtPath:[absolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
  244. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Missing intermediate collection(s) for \"%@\"", relativePath];
  245. }
  246. NSString* directoryName = [absolutePath lastPathComponent];
  247. if (!_allowHiddenItems && [directoryName hasPrefix:@"."]) {
  248. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
  249. }
  250. if (![self shouldCreateDirectoryAtPath:absolutePath]) {
  251. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
  252. }
  253. NSError* error = nil;
  254. if (![[NSFileManager defaultManager] createDirectoryAtPath:absolutePath withIntermediateDirectories:NO attributes:nil error:&error]) {
  255. return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed creating directory \"%@\"", relativePath];
  256. }
  257. #ifdef __GCDWEBSERVER_ENABLE_TESTING__
  258. NSString* creationDateHeader = [request.headers objectForKey:@"X-GCDWebServer-CreationDate"];
  259. if (creationDateHeader) {
  260. NSDate* date = GCDWebServerParseISO8601(creationDateHeader);
  261. if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate : date} ofItemAtPath:absolutePath error:&error]) {
  262. return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed setting creation date for directory \"%@\"", relativePath];
  263. }
  264. }
  265. #endif
  266. if ([self.delegate respondsToSelector:@selector(davServer:didCreateDirectoryAtPath:)]) {
  267. dispatch_async(dispatch_get_main_queue(), ^{
  268. [self.delegate davServer:self didCreateDirectoryAtPath:absolutePath];
  269. });
  270. }
  271. return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Created];
  272. }
  273. - (GCDWebServerResponse*)performCOPY:(GCDWebServerRequest*)request isMove:(BOOL)isMove {
  274. if (!isMove) {
  275. NSString* depthHeader = [request.headers objectForKey:@"Depth"]; // TODO: Support "Depth: 0"
  276. if (depthHeader && ![depthHeader isEqualToString:@"infinity"]) {
  277. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader];
  278. }
  279. }
  280. NSString* srcRelativePath = request.path;
  281. NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(srcRelativePath)];
  282. NSString* dstRelativePath = [request.headers objectForKey:@"Destination"];
  283. NSRange range = [dstRelativePath rangeOfString:(NSString*)[request.headers objectForKey:@"Host"]];
  284. if ((dstRelativePath == nil) || (range.location == NSNotFound)) {
  285. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Malformed 'Destination' header: %@", dstRelativePath];
  286. }
  287. #pragma clang diagnostic push
  288. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  289. dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
  290. #pragma clang diagnostic pop
  291. NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(dstRelativePath)];
  292. if (!dstAbsolutePath) {
  293. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
  294. }
  295. BOOL isDirectory;
  296. if (![[NSFileManager defaultManager] fileExistsAtPath:[dstAbsolutePath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
  297. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Conflict message:@"Invalid destination \"%@\"", dstRelativePath];
  298. }
  299. NSString* srcName = [srcAbsolutePath lastPathComponent];
  300. if ((!_allowHiddenItems && [srcName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:srcName])) {
  301. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ from item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", srcName];
  302. }
  303. NSString* dstName = [dstAbsolutePath lastPathComponent];
  304. if ((!_allowHiddenItems && [dstName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:dstName])) {
  305. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", dstName];
  306. }
  307. NSString* overwriteHeader = [request.headers objectForKey:@"Overwrite"];
  308. BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:dstAbsolutePath];
  309. if (existing && ((isMove && ![overwriteHeader isEqualToString:@"T"]) || (!isMove && [overwriteHeader isEqualToString:@"F"]))) {
  310. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_PreconditionFailed message:@"Destination \"%@\" already exists", dstRelativePath];
  311. }
  312. if (isMove) {
  313. if (![self shouldMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
  314. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
  315. }
  316. } else {
  317. if (![self shouldCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
  318. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
  319. }
  320. }
  321. NSError* error = nil;
  322. if (isMove) {
  323. [[NSFileManager defaultManager] removeItemAtPath:dstAbsolutePath error:NULL];
  324. if (![[NSFileManager defaultManager] moveItemAtPath:srcAbsolutePath toPath:dstAbsolutePath error:&error]) {
  325. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
  326. }
  327. } else {
  328. if (![[NSFileManager defaultManager] copyItemAtPath:srcAbsolutePath toPath:dstAbsolutePath error:&error]) {
  329. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden underlyingError:error message:@"Failed copying \"%@\" to \"%@\"", srcRelativePath, dstRelativePath];
  330. }
  331. }
  332. if (isMove) {
  333. if ([self.delegate respondsToSelector:@selector(davServer:didMoveItemFromPath:toPath:)]) {
  334. dispatch_async(dispatch_get_main_queue(), ^{
  335. [self.delegate davServer:self didMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
  336. });
  337. }
  338. } else {
  339. if ([self.delegate respondsToSelector:@selector(davServer:didCopyItemFromPath:toPath:)]) {
  340. dispatch_async(dispatch_get_main_queue(), ^{
  341. [self.delegate davServer:self didCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
  342. });
  343. }
  344. }
  345. return [GCDWebServerResponse responseWithStatusCode:(existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created)];
  346. }
  347. static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name) {
  348. while (child) {
  349. if ((child->type == XML_ELEMENT_NODE) && !xmlStrcmp(child->name, name)) {
  350. return child;
  351. }
  352. child = child->next;
  353. }
  354. return NULL;
  355. }
  356. - (void)_addPropertyResponseForItem:(NSString*)itemPath resource:(NSString*)resourcePath properties:(DAVProperties)properties xmlString:(NSMutableString*)xmlString {
  357. #pragma clang diagnostic push
  358. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  359. CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL, CFSTR("<&>?+"), kCFStringEncodingUTF8);
  360. #pragma clang diagnostic pop
  361. if (escapedPath) {
  362. NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL];
  363. NSString* type = [attributes objectForKey:NSFileType];
  364. BOOL isFile = [type isEqualToString:NSFileTypeRegular];
  365. BOOL isDirectory = [type isEqualToString:NSFileTypeDirectory];
  366. if ((isFile && [self _checkFileExtension:itemPath]) || isDirectory) {
  367. [xmlString appendString:@"<D:response>"];
  368. [xmlString appendFormat:@"<D:href>%@</D:href>", escapedPath];
  369. [xmlString appendString:@"<D:propstat>"];
  370. [xmlString appendString:@"<D:prop>"];
  371. if (properties & kDAVProperty_ResourceType) {
  372. if (isDirectory) {
  373. [xmlString appendString:@"<D:resourcetype><D:collection/></D:resourcetype>"];
  374. } else {
  375. [xmlString appendString:@"<D:resourcetype/>"];
  376. }
  377. }
  378. if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) {
  379. [xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", GCDWebServerFormatISO8601((NSDate*)[attributes fileCreationDate])];
  380. }
  381. 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
  382. [xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", GCDWebServerFormatRFC822((NSDate*)[attributes fileModificationDate])];
  383. }
  384. if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) {
  385. [xmlString appendFormat:@"<D:getcontentlength>%llu</D:getcontentlength>", [attributes fileSize]];
  386. }
  387. [xmlString appendString:@"</D:prop>"];
  388. [xmlString appendString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
  389. [xmlString appendString:@"</D:propstat>"];
  390. [xmlString appendString:@"</D:response>\n"];
  391. }
  392. CFRelease(escapedPath);
  393. } else {
  394. [self logError:@"Failed escaping path: %@", itemPath];
  395. }
  396. }
  397. - (GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request {
  398. NSInteger depth;
  399. NSString* depthHeader = [request.headers objectForKey:@"Depth"];
  400. if ([depthHeader isEqualToString:@"0"]) {
  401. depth = 0;
  402. } else if ([depthHeader isEqualToString:@"1"]) {
  403. depth = 1;
  404. } else {
  405. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Unsupported 'Depth' header: %@", depthHeader]; // TODO: Return 403 / propfind-finite-depth for "infinity" depth
  406. }
  407. DAVProperties properties = 0;
  408. if (request.data.length) {
  409. BOOL success = YES;
  410. xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
  411. if (document) {
  412. xmlNodePtr rootNode = _XMLChildWithName(document->children, (const xmlChar*)"propfind");
  413. xmlNodePtr allNode = rootNode ? _XMLChildWithName(rootNode->children, (const xmlChar*)"allprop") : NULL;
  414. xmlNodePtr propNode = rootNode ? _XMLChildWithName(rootNode->children, (const xmlChar*)"prop") : NULL;
  415. if (allNode) {
  416. properties = kDAVAllProperties;
  417. } else if (propNode) {
  418. xmlNodePtr node = propNode->children;
  419. while (node) {
  420. if (!xmlStrcmp(node->name, (const xmlChar*)"resourcetype")) {
  421. properties |= kDAVProperty_ResourceType;
  422. } else if (!xmlStrcmp(node->name, (const xmlChar*)"creationdate")) {
  423. properties |= kDAVProperty_CreationDate;
  424. } else if (!xmlStrcmp(node->name, (const xmlChar*)"getlastmodified")) {
  425. properties |= kDAVProperty_LastModified;
  426. } else if (!xmlStrcmp(node->name, (const xmlChar*)"getcontentlength")) {
  427. properties |= kDAVProperty_ContentLength;
  428. } else {
  429. [self logWarning:@"Unknown DAV property requested \"%s\"", node->name];
  430. }
  431. node = node->next;
  432. }
  433. } else {
  434. success = NO;
  435. }
  436. xmlFreeDoc(document);
  437. } else {
  438. success = NO;
  439. }
  440. if (!success) {
  441. NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
  442. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
  443. }
  444. } else {
  445. properties = kDAVAllProperties;
  446. }
  447. NSString* relativePath = request.path;
  448. NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
  449. BOOL isDirectory = NO;
  450. if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
  451. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
  452. }
  453. NSString* itemName = [absolutePath lastPathComponent];
  454. if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
  455. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Retrieving properties for item name \"%@\" is not allowed", itemName];
  456. }
  457. NSArray* items = nil;
  458. if (isDirectory) {
  459. NSError* error = nil;
  460. items = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error] sortedArrayUsingSelector:@selector(localizedStandardCompare:)];
  461. if (items == nil) {
  462. return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
  463. }
  464. }
  465. NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
  466. [xmlString appendString:@"<D:multistatus xmlns:D=\"DAV:\">\n"];
  467. if (![relativePath hasPrefix:@"/"]) {
  468. relativePath = [@"/" stringByAppendingString:relativePath];
  469. }
  470. [self _addPropertyResponseForItem:absolutePath resource:relativePath properties:properties xmlString:xmlString];
  471. if (depth == 1) {
  472. if (![relativePath hasSuffix:@"/"]) {
  473. relativePath = [relativePath stringByAppendingString:@"/"];
  474. }
  475. for (NSString* item in items) {
  476. if (_allowHiddenItems || ![item hasPrefix:@"."]) {
  477. [self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString];
  478. }
  479. }
  480. }
  481. [xmlString appendString:@"</D:multistatus>"];
  482. GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:(NSData*)[xmlString dataUsingEncoding:NSUTF8StringEncoding]
  483. contentType:@"application/xml; charset=\"utf-8\""];
  484. response.statusCode = kGCDWebServerHTTPStatusCode_MultiStatus;
  485. return response;
  486. }
  487. - (GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request {
  488. if (!_IsMacFinder(request)) {
  489. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"];
  490. }
  491. NSString* relativePath = request.path;
  492. NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
  493. BOOL isDirectory = NO;
  494. if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
  495. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
  496. }
  497. NSString* depthHeader = [request.headers objectForKey:@"Depth"];
  498. NSString* timeoutHeader = [request.headers objectForKey:@"Timeout"];
  499. NSString* scope = nil;
  500. NSString* type = nil;
  501. NSString* owner = nil;
  502. NSString* token = nil;
  503. BOOL success = YES;
  504. xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
  505. if (document) {
  506. xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"lockinfo");
  507. if (node) {
  508. xmlNodePtr scopeNode = _XMLChildWithName(node->children, (const xmlChar*)"lockscope");
  509. if (scopeNode && scopeNode->children && scopeNode->children->name) {
  510. scope = [NSString stringWithUTF8String:(const char*)scopeNode->children->name];
  511. }
  512. xmlNodePtr typeNode = _XMLChildWithName(node->children, (const xmlChar*)"locktype");
  513. if (typeNode && typeNode->children && typeNode->children->name) {
  514. type = [NSString stringWithUTF8String:(const char*)typeNode->children->name];
  515. }
  516. xmlNodePtr ownerNode = _XMLChildWithName(node->children, (const xmlChar*)"owner");
  517. if (ownerNode) {
  518. ownerNode = _XMLChildWithName(ownerNode->children, (const xmlChar*)"href");
  519. if (ownerNode && ownerNode->children && ownerNode->children->content) {
  520. owner = [NSString stringWithUTF8String:(const char*)ownerNode->children->content];
  521. }
  522. }
  523. } else {
  524. success = NO;
  525. }
  526. xmlFreeDoc(document);
  527. } else {
  528. success = NO;
  529. }
  530. if (!success) {
  531. NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
  532. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
  533. }
  534. if (![scope isEqualToString:@"exclusive"] || ![type isEqualToString:@"write"] || ![depthHeader isEqualToString:@"0"]) {
  535. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depthHeader, relativePath];
  536. }
  537. NSString* itemName = [absolutePath lastPathComponent];
  538. if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
  539. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking item name \"%@\" is not allowed", itemName];
  540. }
  541. #ifdef __GCDWEBSERVER_ENABLE_TESTING__
  542. NSString* lockTokenHeader = [request.headers objectForKey:@"X-GCDWebServer-LockToken"];
  543. if (lockTokenHeader) {
  544. token = lockTokenHeader;
  545. }
  546. #endif
  547. if (!token) {
  548. CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
  549. CFStringRef string = CFUUIDCreateString(kCFAllocatorDefault, uuid);
  550. token = [NSString stringWithFormat:@"urn:uuid:%@", (__bridge NSString*)string];
  551. CFRelease(string);
  552. CFRelease(uuid);
  553. }
  554. NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
  555. [xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
  556. [xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
  557. [xmlString appendFormat:@"<D:locktype><D:%@/></D:locktype>\n", type];
  558. [xmlString appendFormat:@"<D:lockscope><D:%@/></D:lockscope>\n", scope];
  559. [xmlString appendFormat:@"<D:depth>%@</D:depth>\n", depthHeader];
  560. if (owner) {
  561. [xmlString appendFormat:@"<D:owner><D:href>%@</D:href></D:owner>\n", owner];
  562. }
  563. if (timeoutHeader) {
  564. [xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeoutHeader];
  565. }
  566. [xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
  567. NSString* lockroot = [@"http://" stringByAppendingString:[(NSString*)[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]];
  568. [xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
  569. [xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
  570. [xmlString appendString:@"</D:prop>"];
  571. [self logVerbose:@"WebDAV pretending to lock \"%@\"", relativePath];
  572. GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:(NSData*)[xmlString dataUsingEncoding:NSUTF8StringEncoding]
  573. contentType:@"application/xml; charset=\"utf-8\""];
  574. return response;
  575. }
  576. - (GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request {
  577. if (!_IsMacFinder(request)) {
  578. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"];
  579. }
  580. NSString* relativePath = request.path;
  581. NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
  582. BOOL isDirectory = NO;
  583. if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
  584. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
  585. }
  586. NSString* tokenHeader = [request.headers objectForKey:@"Lock-Token"];
  587. if (!tokenHeader.length) {
  588. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Missing 'Lock-Token' header"];
  589. }
  590. NSString* itemName = [absolutePath lastPathComponent];
  591. if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
  592. return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Unlocking item name \"%@\" is not allowed", itemName];
  593. }
  594. [self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath];
  595. return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
  596. }
  597. @end
  598. @implementation GCDWebDAVServer (Subclassing)
  599. - (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath {
  600. return YES;
  601. }
  602. - (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
  603. return YES;
  604. }
  605. - (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath {
  606. return YES;
  607. }
  608. - (BOOL)shouldDeleteItemAtPath:(NSString*)path {
  609. return YES;
  610. }
  611. - (BOOL)shouldCreateDirectoryAtPath:(NSString*)path {
  612. return YES;
  613. }
  614. @end