Sfoglia il codice sorgente

Added compatibility with OS X Finder for WebDAV

Pierre-Olivier Latour 11 anni fa
parent
commit
d5811fe6df
4 ha cambiato i file con 158 aggiunte e 40 eliminazioni
  1. 1 0
      GCDWebDAVServer/GCDWebDAVServer.h
  2. 153 36
      GCDWebDAVServer/GCDWebDAVServer.m
  3. 1 1
      Mac/main.m
  4. 3 3
      README.md

+ 1 - 0
GCDWebDAVServer/GCDWebDAVServer.h

@@ -47,6 +47,7 @@
 @property(nonatomic, copy) NSArray* allowedFileExtensions;  // Default is nil i.e. all file extensions are allowed
 @property(nonatomic) BOOL showHiddenFiles;  // Default is NO
 - (instancetype)initWithUploadDirectory:(NSString*)path;
+- (instancetype)initWithUploadDirectory:(NSString*)path macFinderMode:(BOOL)macFinderMode;  // If Mac Finder mode is ON, WebDAV server can be mounted read-write instead of read-only in OS X Finder
 @end
 
 @interface GCDWebDAVServer (Subclassing)

+ 153 - 36
GCDWebDAVServer/GCDWebDAVServer.m

@@ -51,6 +51,7 @@ typedef NS_ENUM(NSInteger, DAVProperties) {
 @interface GCDWebDAVServer () {
 @private
   NSString* _uploadDirectory;
+  BOOL _macMode;
   id<GCDWebDAVServerDelegate> __unsafe_unretained _delegate;
   NSArray* _allowedExtensions;
   BOOL _showHidden;
@@ -66,29 +67,17 @@ typedef NS_ENUM(NSInteger, DAVProperties) {
   return YES;
 }
 
-- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
-  GCDWebServerResponse* response = [GCDWebServerResponse response];
-  [response setValue:@"1" forAdditionalHeader:@"DAV"];  // Class 1
-  return response;
+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*)performHEAD:(GCDWebServerRequest*)request {
-  NSString* relativePath = request.path;
-  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
-  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
-    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
-  }
-  
-  NSError* error = nil;
-  NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:absolutePath error:&error];
-  if (!attributes) {
-    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound underlyingError:error message:@"Failed retrieving attributes for \"%@\"", relativePath];
-  }
-  
+- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
   GCDWebServerResponse* response = [GCDWebServerResponse response];
-  if ([[attributes fileType] isEqualToString:NSFileTypeRegular]) {
-    [response setValue:GCDWebServerGetMimeTypeForExtension([absolutePath pathExtension]) forAdditionalHeader:@"Content-Type"];
-    [response setValue:[NSString stringWithFormat:@"%llu", [attributes fileSize]] forAdditionalHeader:@"Content-Length"];
+  if (_macMode && _IsMacFinder(request)) {
+    [response setValue:@"1, 2" forAdditionalHeader:@"DAV"];  // Classes 1 and 2
+  } else {
+    [response setValue:@"1" forAdditionalHeader:@"DAV"];  // Class 1
   }
   return response;
 }
@@ -374,6 +363,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
   
   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");
@@ -398,13 +388,18 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
           node = node->next;
         }
       } else {
-        NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
-        [self logError:@"Invalid DAV properties\n%@", string];
-#if !__has_feature(objc_arc)
-        [string release];
-#endif
+        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 !__has_feature(objc_arc)
+      [string release];
+#endif
     }
   } else {
     properties = kDAVAllProperties;
@@ -412,14 +407,18 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
   
   NSString* relativePath = request.path;
   NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
-  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
+  BOOL isDirectory = NO;
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
     return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
   }
   
-  NSError* error = nil;
-  NSArray* items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
-  if (items == nil) {
-    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
+  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:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
@@ -446,6 +445,114 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
   return response;
 }
 
+- (GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request {
+  if (!_macMode || !_IsMacFinder(request)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"];
+  }
+  
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  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 !__has_feature(objc_arc)
+    [string release];
+#endif
+  }
+  
+  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];
+  }
+  
+  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:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
+  [xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
+  [xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
+  [xmlString appendFormat:@"<D:locktype><D:%@/></D:locktype>\n", type];
+  [xmlString appendFormat:@"<D:lockscope><D:%@/></D:lockscope>\n", scope];
+  [xmlString appendFormat:@"<D:depth>%@</D:depth>\n", depthHeader];
+  if (owner) {
+    [xmlString appendFormat:@"<D:owner><D:href>%@</D:href></D:owner>\n", owner];
+  }
+  if (timeoutHeader) {
+    [xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeoutHeader];
+  }
+  [xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
+  NSString* lockroot = [@"http://" stringByAppendingString:[[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]];
+  [xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
+  [xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
+  [xmlString appendString:@"</D:prop>"];
+  
+  [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 (!_macMode || !_IsMacFinder(request)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"];
+  }
+  
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
+  if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+  
+  NSString* tokenHeader = [request.headers objectForKey:@"Lock-Token"];
+  if (!tokenHeader.length) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Missing 'Lock-Token' header"];
+  }
+  
+  [self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath];
+  return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
+}
+
 @end
 
 @implementation GCDWebDAVServer
@@ -453,8 +560,13 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
 @synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden;
 
 - (instancetype)initWithUploadDirectory:(NSString*)path {
+  return [self initWithUploadDirectory:path macFinderMode:NO];
+}
+
+- (instancetype)initWithUploadDirectory:(NSString*)path macFinderMode:(BOOL)macFinderMode {
   if ((self = [super init])) {
     _uploadDirectory = [[path stringByStandardizingPath] copy];
+    _macMode = macFinderMode;
     GCDWebDAVServer* __unsafe_unretained server = self;
     
     // 9.1 PROPFIND method
@@ -467,12 +579,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
       return [server performMKCOL:(GCDWebServerDataRequest*)request];
     }];
     
-    // 9.4 HEAD method
-    [self addDefaultHandlerForMethod:@"HEAD" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
-      return [server performHEAD:request];
-    }];
-    
-    // 9.4 GET method
+    // 9.4 GET & HEAD methods
     [self addDefaultHandlerForMethod:@"GET" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
       return [server performGET:request];
     }];
@@ -497,6 +604,16 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
       return [server performCOPY:request isMove:YES];
     }];
     
+    // 9.10 LOCK method
+    [self addDefaultHandlerForMethod:@"LOCK" requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performLOCK:(GCDWebServerDataRequest*)request];
+    }];
+    
+    // 9.11 UNLOCK method
+    [self addDefaultHandlerForMethod:@"UNLOCK" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
+      return [server performUNLOCK:request];
+    }];
+    
     // 10.1 OPTIONS method / DAV Header
     [self addDefaultHandlerForMethod:@"OPTIONS" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
       return [server performOPTIONS:request];

+ 1 - 1
Mac/main.m

@@ -97,7 +97,7 @@ int main(int argc, const char* argv[]) {
       }
       
       case 3: {
-        webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]];
+        webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath] macFinderMode:YES];
         break;
       }
       

+ 3 - 3
README.md

@@ -17,7 +17,7 @@ Extra built-in features:
 
 Included extensions:
 * [GCDWebUploader](GCDWebUploader/GCDWebUploader.h): subclass of GCDWebServer that implements an interface for uploading and downloading files from an iOS app's sandbox using a web browser
-* [GCDWebDAVServer](GCDWebDAVServer/GCDWebDAVServer.h): subclass of GCDWebServer that implements a class 1 [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server
+* [GCDWebDAVServer](GCDWebDAVServer/GCDWebDAVServer.h): subclass of GCDWebServer that implements a class 1 [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server (with partial class 2 support for OS X Finder)
 
 What's not available out of the box but can be implemented on top of the API:
 * Authentication like [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
@@ -87,7 +87,7 @@ Simply instantiate and run a GCDWebUploader instance then visit http://{YOUR-IOS
 WebDAV Server in iOS Apps
 =========================
 
-GCDWebDAVServer is a subclass of GCDWebServer that provides a class 1 compliant [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server. This lets users upload, download, delete files and create directories from a directory inside your iOS app's sandbox using any WebDAV client like [Transmit](https://panic.com/transmit/) (Mac), [ForkLift](http://binarynights.com/forklift/) (Mac) or [CyberDuck](http://cyberduck.io/) (Mac / Windows).
+GCDWebDAVServer is a subclass of GCDWebServer that provides a class 1 compliant [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server. This lets users upload, download, delete files and create directories from a directory inside your iOS app's sandbox using any WebDAV client like [Transmit](https://panic.com/transmit/) (Mac), [ForkLift](http://binarynights.com/forklift/) (Mac) or [CyberDuck](http://cyberduck.io/) (Mac / Windows). GCDWebDAVServer should also work with the [OS X Finder](http://support.apple.com/kb/PH13859) as it is partially class 2 compliant (but only when the client is the OS X WebDAV implementation).
 
 Simply instantiate and run a GCDWebDAVServer instance then connect to http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ using a WebDAV client:
 
@@ -249,6 +249,6 @@ NSString* websitePath = [[NSBundle mainBundle] pathForResource:@"Website" ofType
 Final Example: File Downloads and Uploads From iOS App
 ======================================================
 
-GCDWebServer was originally written for the [ComicFlow](http://itunes.apple.com/us/app/comicflow/id409290355?mt=8) comic reader app for iPad. It lets users upload, download and organize comic files inside the app using their web browser directly over WiFi.
+GCDWebServer was originally written for the [ComicFlow](http://itunes.apple.com/us/app/comicflow/id409290355?mt=8) comic reader app for iPad. It allow users to connect to their iPad with their web browser over WiFi and then upload, download and organize comic files inside the app.
 
 ComicFlow is [entirely open-source](https://github.com/swisspol/ComicFlow) and you can see how it uses GCDWebUploader in the [WebServer.m](https://github.com/swisspol/ComicFlow/blob/master/Classes/WebServer.m) file.