Skip to content

Commit d5811fe

Browse files
committed
Added compatibility with OS X Finder for WebDAV
1 parent b494e40 commit d5811fe

File tree

4 files changed

+158
-40
lines changed

4 files changed

+158
-40
lines changed

GCDWebDAVServer/GCDWebDAVServer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
@property(nonatomic, copy) NSArray* allowedFileExtensions; // Default is nil i.e. all file extensions are allowed
4848
@property(nonatomic) BOOL showHiddenFiles; // Default is NO
4949
- (instancetype)initWithUploadDirectory:(NSString*)path;
50+
- (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
5051
@end
5152

5253
@interface GCDWebDAVServer (Subclassing)

GCDWebDAVServer/GCDWebDAVServer.m

Lines changed: 153 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ typedef NS_ENUM(NSInteger, DAVProperties) {
5151
@interface GCDWebDAVServer () {
5252
@private
5353
NSString* _uploadDirectory;
54+
BOOL _macMode;
5455
id<GCDWebDAVServerDelegate> __unsafe_unretained _delegate;
5556
NSArray* _allowedExtensions;
5657
BOOL _showHidden;
@@ -66,29 +67,17 @@ - (BOOL)_checkFileExtension:(NSString*)fileName {
6667
return YES;
6768
}
6869

69-
- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
70-
GCDWebServerResponse* response = [GCDWebServerResponse response];
71-
[response setValue:@"1" forAdditionalHeader:@"DAV"]; // Class 1
72-
return response;
70+
static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
71+
NSString* userAgentHeader = [request.headers objectForKey:@"User-Agent"];
72+
return ([userAgentHeader hasPrefix:@"WebDAVFS/"] || [userAgentHeader hasPrefix:@"WebDAVLib/"]); // OS X WebDAV client
7373
}
7474

75-
- (GCDWebServerResponse*)performHEAD:(GCDWebServerRequest*)request {
76-
NSString* relativePath = request.path;
77-
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
78-
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
79-
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
80-
}
81-
82-
NSError* error = nil;
83-
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:absolutePath error:&error];
84-
if (!attributes) {
85-
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound underlyingError:error message:@"Failed retrieving attributes for \"%@\"", relativePath];
86-
}
87-
75+
- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
8876
GCDWebServerResponse* response = [GCDWebServerResponse response];
89-
if ([[attributes fileType] isEqualToString:NSFileTypeRegular]) {
90-
[response setValue:GCDWebServerGetMimeTypeForExtension([absolutePath pathExtension]) forAdditionalHeader:@"Content-Type"];
91-
[response setValue:[NSString stringWithFormat:@"%llu", [attributes fileSize]] forAdditionalHeader:@"Content-Length"];
77+
if (_macMode && _IsMacFinder(request)) {
78+
[response setValue:@"1, 2" forAdditionalHeader:@"DAV"]; // Classes 1 and 2
79+
} else {
80+
[response setValue:@"1" forAdditionalHeader:@"DAV"]; // Class 1
9281
}
9382
return response;
9483
}
@@ -374,6 +363,7 @@ - (GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request {
374363

375364
DAVProperties properties = 0;
376365
if (request.data.length) {
366+
BOOL success = YES;
377367
xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
378368
if (document) {
379369
xmlNodePtr rootNode = _XMLChildWithName(document->children, (const xmlChar*)"propfind");
@@ -398,28 +388,37 @@ - (GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request {
398388
node = node->next;
399389
}
400390
} else {
401-
NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
402-
[self logError:@"Invalid DAV properties\n%@", string];
403-
#if !__has_feature(objc_arc)
404-
[string release];
405-
#endif
391+
success = NO;
406392
}
407393
xmlFreeDoc(document);
394+
} else {
395+
success = NO;
396+
}
397+
if (!success) {
398+
NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
399+
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
400+
#if !__has_feature(objc_arc)
401+
[string release];
402+
#endif
408403
}
409404
} else {
410405
properties = kDAVAllProperties;
411406
}
412407

413408
NSString* relativePath = request.path;
414409
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
415-
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
410+
BOOL isDirectory = NO;
411+
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
416412
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
417413
}
418414

419-
NSError* error = nil;
420-
NSArray* items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
421-
if (items == nil) {
422-
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
415+
NSArray* items = nil;
416+
if (isDirectory) {
417+
NSError* error = nil;
418+
items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error];
419+
if (items == nil) {
420+
return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
421+
}
423422
}
424423

425424
NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
@@ -446,15 +445,128 @@ - (GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request {
446445
return response;
447446
}
448447

448+
- (GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request {
449+
if (!_macMode || !_IsMacFinder(request)) {
450+
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"];
451+
}
452+
453+
NSString* relativePath = request.path;
454+
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
455+
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
456+
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
457+
}
458+
459+
NSString* depthHeader = [request.headers objectForKey:@"Depth"];
460+
NSString* timeoutHeader = [request.headers objectForKey:@"Timeout"];
461+
NSString* scope = nil;
462+
NSString* type = nil;
463+
NSString* owner = nil;
464+
NSString* token = nil;
465+
BOOL success = YES;
466+
xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
467+
if (document) {
468+
xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"lockinfo");
469+
if (node) {
470+
xmlNodePtr scopeNode = _XMLChildWithName(node->children, (const xmlChar*)"lockscope");
471+
if (scopeNode && scopeNode->children && scopeNode->children->name) {
472+
scope = [NSString stringWithUTF8String:(const char*)scopeNode->children->name];
473+
}
474+
xmlNodePtr typeNode = _XMLChildWithName(node->children, (const xmlChar*)"locktype");
475+
if (typeNode && typeNode->children && typeNode->children->name) {
476+
type = [NSString stringWithUTF8String:(const char*)typeNode->children->name];
477+
}
478+
xmlNodePtr ownerNode = _XMLChildWithName(node->children, (const xmlChar*)"owner");
479+
if (ownerNode) {
480+
ownerNode = _XMLChildWithName(ownerNode->children, (const xmlChar*)"href");
481+
if (ownerNode && ownerNode->children && ownerNode->children->content) {
482+
owner = [NSString stringWithUTF8String:(const char*)ownerNode->children->content];
483+
}
484+
}
485+
} else {
486+
success = NO;
487+
}
488+
xmlFreeDoc(document);
489+
} else {
490+
success = NO;
491+
}
492+
if (!success) {
493+
NSString* string = [[NSString alloc] initWithData:request.data encoding:NSUTF8StringEncoding];
494+
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Invalid DAV properties:\n%@", string];
495+
#if !__has_feature(objc_arc)
496+
[string release];
497+
#endif
498+
}
499+
500+
if (![scope isEqualToString:@"exclusive"] || ![type isEqualToString:@"write"] || ![depthHeader isEqualToString:@"0"]) {
501+
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depthHeader, relativePath];
502+
}
503+
504+
if (!token) {
505+
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
506+
CFStringRef string = CFUUIDCreateString(kCFAllocatorDefault, uuid);
507+
token = [NSString stringWithFormat:@"urn:uuid:%@", (__bridge NSString*)string];
508+
CFRelease(string);
509+
CFRelease(uuid);
510+
}
511+
512+
NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
513+
[xmlString appendString:@"<D:prop xmlns:D=\"DAV:\">\n"];
514+
[xmlString appendString:@"<D:lockdiscovery>\n<D:activelock>\n"];
515+
[xmlString appendFormat:@"<D:locktype><D:%@/></D:locktype>\n", type];
516+
[xmlString appendFormat:@"<D:lockscope><D:%@/></D:lockscope>\n", scope];
517+
[xmlString appendFormat:@"<D:depth>%@</D:depth>\n", depthHeader];
518+
if (owner) {
519+
[xmlString appendFormat:@"<D:owner><D:href>%@</D:href></D:owner>\n", owner];
520+
}
521+
if (timeoutHeader) {
522+
[xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeoutHeader];
523+
}
524+
[xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
525+
NSString* lockroot = [@"http://" stringByAppendingString:[[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]];
526+
[xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
527+
[xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
528+
[xmlString appendString:@"</D:prop>"];
529+
530+
[self logVerbose:@"WebDAV pretending to lock \"%@\"", relativePath];
531+
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[xmlString dataUsingEncoding:NSUTF8StringEncoding]
532+
contentType:@"application/xml; charset=\"utf-8\""];
533+
return response;
534+
}
535+
536+
- (GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request {
537+
if (!_macMode || !_IsMacFinder(request)) {
538+
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"];
539+
}
540+
541+
NSString* relativePath = request.path;
542+
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
543+
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath]) {
544+
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
545+
}
546+
547+
NSString* tokenHeader = [request.headers objectForKey:@"Lock-Token"];
548+
if (!tokenHeader.length) {
549+
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Missing 'Lock-Token' header"];
550+
}
551+
552+
[self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath];
553+
return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
554+
}
555+
449556
@end
450557

451558
@implementation GCDWebDAVServer
452559

453560
@synthesize uploadDirectory=_uploadDirectory, delegate=_delegate, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden;
454561

455562
- (instancetype)initWithUploadDirectory:(NSString*)path {
563+
return [self initWithUploadDirectory:path macFinderMode:NO];
564+
}
565+
566+
- (instancetype)initWithUploadDirectory:(NSString*)path macFinderMode:(BOOL)macFinderMode {
456567
if ((self = [super init])) {
457568
_uploadDirectory = [[path stringByStandardizingPath] copy];
569+
_macMode = macFinderMode;
458570
GCDWebDAVServer* __unsafe_unretained server = self;
459571

460572
// 9.1 PROPFIND method
@@ -467,12 +579,7 @@ - (instancetype)initWithUploadDirectory:(NSString*)path {
467579
return [server performMKCOL:(GCDWebServerDataRequest*)request];
468580
}];
469581

470-
// 9.4 HEAD method
471-
[self addDefaultHandlerForMethod:@"HEAD" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
472-
return [server performHEAD:request];
473-
}];
474-
475-
// 9.4 GET method
582+
// 9.4 GET & HEAD methods
476583
[self addDefaultHandlerForMethod:@"GET" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
477584
return [server performGET:request];
478585
}];
@@ -497,6 +604,16 @@ - (instancetype)initWithUploadDirectory:(NSString*)path {
497604
return [server performCOPY:request isMove:YES];
498605
}];
499606

607+
// 9.10 LOCK method
608+
[self addDefaultHandlerForMethod:@"LOCK" requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
609+
return [server performLOCK:(GCDWebServerDataRequest*)request];
610+
}];
611+
612+
// 9.11 UNLOCK method
613+
[self addDefaultHandlerForMethod:@"UNLOCK" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
614+
return [server performUNLOCK:request];
615+
}];
616+
500617
// 10.1 OPTIONS method / DAV Header
501618
[self addDefaultHandlerForMethod:@"OPTIONS" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
502619
return [server performOPTIONS:request];

Mac/main.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ int main(int argc, const char* argv[]) {
9797
}
9898

9999
case 3: {
100-
webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]];
100+
webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath] macFinderMode:YES];
101101
break;
102102
}
103103

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Extra built-in features:
1717

1818
Included extensions:
1919
* [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
20-
* [GCDWebDAVServer](GCDWebDAVServer/GCDWebDAVServer.h): subclass of GCDWebServer that implements a class 1 [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server
20+
* [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)
2121

2222
What's not available out of the box but can be implemented on top of the API:
2323
* 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
8787
WebDAV Server in iOS Apps
8888
=========================
8989

90-
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).
90+
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).
9191

9292
Simply instantiate and run a GCDWebDAVServer instance then connect to http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ using a WebDAV client:
9393

@@ -249,6 +249,6 @@ NSString* websitePath = [[NSBundle mainBundle] pathForResource:@"Website" ofType
249249
Final Example: File Downloads and Uploads From iOS App
250250
======================================================
251251

252-
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.
252+
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.
253253

254254
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.

0 commit comments

Comments
 (0)