2014-04-08 14:19:31 +08:00
/ *
Copyright ( c ) 2012 -2014 , Pierre - Olivier Latour
All rights reserved .
Redistribution and use in source and binary forms , with or without
modification , are permitted provided that the following conditions are met :
* Redistributions of source code must retain the above copyright
notice , this list of conditions and the following disclaimer .
* Redistributions in binary form must reproduce the above copyright
notice , this list of conditions and the following disclaimer in the
documentation and / or other materials provided with the distribution .
* The name of Pierre - Olivier Latour may not be used to endorse
or promote products derived from this software without specific
prior written permission .
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES , INCLUDING , BUT NOT LIMITED TO , THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED . IN NO EVENT SHALL PIERRE - OLIVIER LATOUR BE LIABLE FOR ANY
DIRECT , INDIRECT , INCIDENTAL , SPECIAL , EXEMPLARY , OR CONSEQUENTIAL DAMAGES
( INCLUDING , BUT NOT LIMITED TO , PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES ;
LOSS OF USE , DATA , OR PROFITS ; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY , WHETHER IN CONTRACT , STRICT LIABILITY , OR TORT
( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE , EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE .
* /
// WebDAV specifications : http : // webdav . org / specs / rfc4918 . html
# import < libxml / parser . h >
# import "GCDWebDAVServer.h"
# import "GCDWebServerDataRequest.h"
# import "GCDWebServerFileRequest.h"
# import "GCDWebServerDataResponse.h"
# import "GCDWebServerErrorResponse.h"
# import "GCDWebServerFileResponse.h"
# define kXMLParseOptions ( XML_PARSE _NONET | XML_PARSE _RECOVER | XML_PARSE _NOBLANKS | XML_PARSE _COMPACT | XML_PARSE _NOWARNING | XML_PARSE _NOERROR )
typedef NS_ENUM ( NSInteger , DAVProperties ) {
kDAVProperty_ResourceType = ( 1 < < 0 ) ,
kDAVProperty_CreationDate = ( 1 < < 1 ) ,
kDAVProperty_LastModified = ( 1 < < 2 ) ,
kDAVProperty_ContentLength = ( 1 < < 3 ) ,
kDAVAllProperties = kDAVProperty_ResourceType | kDAVProperty_CreationDate | kDAVProperty_LastModified | kDAVProperty_ContentLength
} ;
@ interface GCDWebDAVServer ( ) {
@ private
NSString * _uploadDirectory ;
id < GCDWebDAVServerDelegate > __unsafe _unretained _delegate ;
NSArray * _allowedExtensions ;
BOOL _showHidden ;
}
@ end
@ implementation GCDWebDAVServer ( Methods )
- ( BOOL ) _checkFileExtension : ( NSString * ) fileName {
if ( _allowedExtensions && ! [ _allowedExtensions containsObject : [ [ fileName pathExtension ] lowercaseString ] ] ) {
return NO ;
}
return YES ;
}
2014-04-09 16:37:15 +08:00
static inline BOOL _IsMacFinder ( GCDWebServerRequest * request ) {
NSString * userAgentHeader = [ request . headers objectForKey : @ "User-Agent" ] ;
return ( [ userAgentHeader hasPrefix : @ "WebDAVFS/" ] || [ userAgentHeader hasPrefix : @ "WebDAVLib/" ] ) ; // OS X WebDAV client
2014-04-08 14:19:31 +08:00
}
2014-04-09 16:37:15 +08:00
- ( GCDWebServerResponse * ) performOPTIONS : ( GCDWebServerRequest * ) request {
2014-04-08 14:19:31 +08:00
GCDWebServerResponse * response = [ GCDWebServerResponse response ] ;
2014-04-09 16:41:54 +08:00
if ( _IsMacFinder ( request ) ) {
2014-04-09 16:37:15 +08:00
[ response setValue : @ "1, 2" forAdditionalHeader : @ "DAV" ] ; // Classes 1 and 2
} else {
[ response setValue : @ "1" forAdditionalHeader : @ "DAV" ] ; // Class 1
2014-04-08 14:19:31 +08:00
}
return response ;
}
- ( GCDWebServerResponse * ) performGET : ( GCDWebServerRequest * ) request {
NSString * relativePath = request . path ;
NSString * absolutePath = [ _uploadDirectory stringByAppendingPathComponent : relativePath ] ;
2014-04-10 04:45:15 +08:00
BOOL isDirectory = NO ;
2014-04-08 14:19:31 +08:00
if ( ! [ absolutePath hasPrefix : _uploadDirectory ] || ! [ [ NSFileManager defaultManager ] fileExistsAtPath : absolutePath isDirectory : & isDirectory ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_NotFound message : @ "\" % @ \ " does not exist" , relativePath ] ;
}
if ( isDirectory ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_BadRequest message : @ "\" % @ \ " is not a file" , relativePath ] ;
}
2014-04-10 04:45:15 +08:00
NSString * fileName = [ absolutePath lastPathComponent ] ;
if ( ( [ fileName hasPrefix : @ "." ] && ! _showHidden ) || ! [ self _checkFileExtension : fileName ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Downlading file name \" % @ \ " is not allowed" , fileName ] ;
}
2014-04-08 14:19:31 +08:00
if ( [ _delegate respondsToSelector : @ selector ( davServer : didDownloadFileAtPath : ) ] ) {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ _delegate davServer : self didDownloadFileAtPath : absolutePath ] ;
} ) ;
}
return [ GCDWebServerFileResponse responseWithFile : absolutePath ] ;
}
- ( GCDWebServerResponse * ) performPUT : ( GCDWebServerFileRequest * ) request {
if ( [ request hasByteRange ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_BadRequest message : @ "Range uploads not supported" ] ;
}
NSString * relativePath = request . path ;
NSString * absolutePath = [ _uploadDirectory stringByAppendingPathComponent : relativePath ] ;
if ( ! [ absolutePath hasPrefix : _uploadDirectory ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_NotFound message : @ "\" % @ \ " does not exist" , relativePath ] ;
}
BOOL isDirectory ;
if ( ! [ [ NSFileManager defaultManager ] fileExistsAtPath : [ absolutePath stringByDeletingLastPathComponent ] isDirectory : & isDirectory ] || ! isDirectory ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Conflict message : @ "Missing intermediate collection(s) for \" % @ \ "" , relativePath ] ;
}
BOOL existing = [ [ NSFileManager defaultManager ] fileExistsAtPath : absolutePath isDirectory : & isDirectory ] ;
if ( existing && isDirectory ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_MethodNotAllowed message : @ "PUT not allowed on existing collection \" % @ \ "" , relativePath ] ;
}
NSString * fileName = [ absolutePath lastPathComponent ] ;
if ( ( [ fileName hasPrefix : @ "." ] && ! _showHidden ) || ! [ self _checkFileExtension : fileName ] ) {
2014-04-10 04:45:15 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Uploading file name \" % @ \ " is not allowed" , fileName ] ;
2014-04-08 14:19:31 +08:00
}
2014-04-10 04:46:43 +08:00
if ( ! [ self shouldUploadFileAtPath : absolutePath withTemporaryFile : request . temporaryPath ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Uploading file to \" % @ \ " is not permitted" , relativePath ] ;
2014-04-08 14:19:31 +08:00
}
[ [ NSFileManager defaultManager ] removeItemAtPath : absolutePath error : NULL ] ;
NSError * error = nil ;
2014-04-10 04:46:43 +08:00
if ( ! [ [ NSFileManager defaultManager ] moveItemAtPath : request . temporaryPath toPath : absolutePath error : & error ] ) {
2014-04-08 14:19:31 +08:00
return [ GCDWebServerErrorResponse responseWithServerError : kGCDWebServerHTTPStatusCode_InternalServerError underlyingError : error message : @ "Failed moving uploaded file to \" % @ \ "" , relativePath ] ;
}
if ( [ _delegate respondsToSelector : @ selector ( davServer : didUploadFileAtPath : ) ] ) {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ _delegate davServer : self didUploadFileAtPath : absolutePath ] ;
} ) ;
}
return [ GCDWebServerResponse responseWithStatusCode : ( existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created ) ] ;
}
- ( GCDWebServerResponse * ) performDELETE : ( GCDWebServerRequest * ) request {
NSString * depthHeader = [ request . headers objectForKey : @ "Depth" ] ;
if ( depthHeader && ! [ depthHeader isEqualToString : @ "infinity" ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_BadRequest message : @ "Unsupported 'Depth' header: %@" , depthHeader ] ;
}
NSString * relativePath = request . path ;
NSString * absolutePath = [ _uploadDirectory stringByAppendingPathComponent : relativePath ] ;
2014-04-10 04:45:15 +08:00
BOOL isDirectory = NO ;
if ( ! [ absolutePath hasPrefix : _uploadDirectory ] || ! [ [ NSFileManager defaultManager ] fileExistsAtPath : absolutePath isDirectory : & isDirectory ] ) {
2014-04-08 14:19:31 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_NotFound message : @ "\" % @ \ " does not exist" , relativePath ] ;
}
2014-04-10 04:45:15 +08:00
NSString * itemName = [ absolutePath lastPathComponent ] ;
if ( ( [ itemName hasPrefix : @ "." ] && ! _showHidden ) || ( ! isDirectory && ! [ self _checkFileExtension : itemName ] ) ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Deleting item name \" % @ \ " is not allowed" , itemName ] ;
}
2014-04-08 14:19:31 +08:00
if ( ! [ self shouldDeleteItemAtPath : absolutePath ] ) {
2014-04-10 04:45:15 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Deleting \" % @ \ " is not permitted" , relativePath ] ;
2014-04-08 14:19:31 +08:00
}
NSError * error = nil ;
if ( ! [ [ NSFileManager defaultManager ] removeItemAtPath : absolutePath error : & error ] ) {
return [ GCDWebServerErrorResponse responseWithServerError : kGCDWebServerHTTPStatusCode_InternalServerError underlyingError : error message : @ "Failed deleting \" % @ \ "" , relativePath ] ;
}
if ( [ _delegate respondsToSelector : @ selector ( davServer : didDeleteItemAtPath : ) ] ) {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ _delegate davServer : self didDeleteItemAtPath : absolutePath ] ;
} ) ;
}
return [ GCDWebServerResponse responseWithStatusCode : kGCDWebServerHTTPStatusCode_NoContent ] ;
}
- ( GCDWebServerResponse * ) performMKCOL : ( GCDWebServerDataRequest * ) request {
if ( [ request hasBody ] && ( request . contentLength > 0 ) ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_UnsupportedMediaType message : @ "Unexpected request body for MKCOL method" ] ;
}
NSString * relativePath = request . path ;
NSString * absolutePath = [ _uploadDirectory stringByAppendingPathComponent : relativePath ] ;
if ( ! [ absolutePath hasPrefix : _uploadDirectory ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_NotFound message : @ "\" % @ \ " does not exist" , relativePath ] ;
}
BOOL isDirectory ;
if ( ! [ [ NSFileManager defaultManager ] fileExistsAtPath : [ absolutePath stringByDeletingLastPathComponent ] isDirectory : & isDirectory ] || ! isDirectory ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Conflict message : @ "Missing intermediate collection(s) for \" % @ \ "" , relativePath ] ;
}
NSString * directoryName = [ absolutePath lastPathComponent ] ;
if ( ! _showHidden && [ directoryName hasPrefix : @ "." ] ) {
2014-04-10 04:45:15 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Creating directory name \" % @ \ " is not allowed" , directoryName ] ;
2014-04-08 14:19:31 +08:00
}
if ( ! [ self shouldCreateDirectoryAtPath : absolutePath ] ) {
2014-04-10 04:45:15 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Creating directory \" % @ \ " is not permitted" , relativePath ] ;
2014-04-08 14:19:31 +08:00
}
NSError * error = nil ;
if ( ! [ [ NSFileManager defaultManager ] createDirectoryAtPath : absolutePath withIntermediateDirectories : NO attributes : nil error : & error ] ) {
return [ GCDWebServerErrorResponse responseWithServerError : kGCDWebServerHTTPStatusCode_InternalServerError underlyingError : error message : @ "Failed creating directory \" % @ \ "" , relativePath ] ;
}
if ( [ _delegate respondsToSelector : @ selector ( davServer : didCreateDirectoryAtPath : ) ] ) {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ _delegate davServer : self didCreateDirectoryAtPath : absolutePath ] ;
} ) ;
}
return [ GCDWebServerResponse responseWithStatusCode : kGCDWebServerHTTPStatusCode_Created ] ;
}
- ( GCDWebServerResponse * ) performCOPY : ( GCDWebServerRequest * ) request isMove : ( BOOL ) isMove {
if ( ! isMove ) {
NSString * depthHeader = [ request . headers objectForKey : @ "Depth" ] ; // TODO : Support "Depth: 0"
if ( depthHeader && ! [ depthHeader isEqualToString : @ "infinity" ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_BadRequest message : @ "Unsupported 'Depth' header: %@" , depthHeader ] ;
}
}
NSString * srcRelativePath = request . path ;
NSString * srcAbsolutePath = [ _uploadDirectory stringByAppendingPathComponent : srcRelativePath ] ;
if ( ! [ srcAbsolutePath hasPrefix : _uploadDirectory ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_NotFound message : @ "\" % @ \ " does not exist" , srcRelativePath ] ;
}
NSString * dstRelativePath = [ request . headers objectForKey : @ "Destination" ] ;
NSRange range = [ dstRelativePath rangeOfString : [ request . headers objectForKey : @ "Host" ] ] ;
if ( ( dstRelativePath = = nil ) || ( range . location = = NSNotFound ) ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_BadRequest message : @ "Malformed 'Destination' header: %@" , dstRelativePath ] ;
}
dstRelativePath = [ [ dstRelativePath substringFromIndex : ( range . location + range . length ) ] stringByReplacingPercentEscapesUsingEncoding : NSUTF8StringEncoding ] ;
NSString * dstAbsolutePath = [ _uploadDirectory stringByAppendingPathComponent : dstRelativePath ] ;
if ( ! [ dstAbsolutePath hasPrefix : _uploadDirectory ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_NotFound message : @ "\" % @ \ " does not exist" , srcRelativePath ] ;
}
BOOL isDirectory ;
if ( ! [ [ NSFileManager defaultManager ] fileExistsAtPath : [ dstAbsolutePath stringByDeletingLastPathComponent ] isDirectory : & isDirectory ] || ! isDirectory ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Conflict message : @ "Invalid destination \" % @ \ "" , dstRelativePath ] ;
}
2014-04-10 04:45:15 +08:00
NSString * itemName = [ dstAbsolutePath lastPathComponent ] ;
if ( ( ! _showHidden && [ itemName hasPrefix : @ "." ] ) || ( ! isDirectory && ! [ self _checkFileExtension : itemName ] ) ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "%@ to item name \" % @ \ " is not allowed" , isMove ? @ "Moving" : @ "Copying" , itemName ] ;
2014-04-08 14:19:31 +08:00
}
NSString * overwriteHeader = [ request . headers objectForKey : @ "Overwrite" ] ;
BOOL existing = [ [ NSFileManager defaultManager ] fileExistsAtPath : dstAbsolutePath ] ;
if ( existing && ( ( isMove && ! [ overwriteHeader isEqualToString : @ "T" ] ) || ( ! isMove && [ overwriteHeader isEqualToString : @ "F" ] ) ) ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_PreconditionFailed message : @ "Destination \" % @ \ " already exists" , dstRelativePath ] ;
}
if ( isMove ) {
if ( ! [ self shouldMoveItemFromPath : srcAbsolutePath toPath : dstAbsolutePath ] ) {
2014-04-10 04:45:15 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Moving \" % @ \ " to \" % @ \ " is not permitted" , srcRelativePath , dstRelativePath ] ;
2014-04-08 14:19:31 +08:00
}
} else {
if ( ! [ self shouldCopyItemFromPath : srcAbsolutePath toPath : dstAbsolutePath ] ) {
2014-04-10 04:45:15 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Copying \" % @ \ " to \" % @ \ " is not permitted" , srcRelativePath , dstRelativePath ] ;
2014-04-08 14:19:31 +08:00
}
}
NSError * error = nil ;
if ( isMove ) {
[ [ NSFileManager defaultManager ] removeItemAtPath : dstAbsolutePath error : NULL ] ;
if ( ! [ [ NSFileManager defaultManager ] moveItemAtPath : srcAbsolutePath toPath : dstAbsolutePath error : & error ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden underlyingError : error message : @ "Failed copying \" % @ \ " to \" % @ \ "" , srcRelativePath , dstRelativePath ] ;
}
} else {
if ( ! [ [ NSFileManager defaultManager ] copyItemAtPath : srcAbsolutePath toPath : dstAbsolutePath error : & error ] ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden underlyingError : error message : @ "Failed copying \" % @ \ " to \" % @ \ "" , srcRelativePath , dstRelativePath ] ;
}
}
if ( isMove ) {
if ( [ _delegate respondsToSelector : @ selector ( davServer : didMoveItemFromPath : toPath : ) ] ) {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ _delegate davServer : self didMoveItemFromPath : srcAbsolutePath toPath : dstAbsolutePath ] ;
} ) ;
}
} else {
if ( [ _delegate respondsToSelector : @ selector ( davServer : didCopyItemFromPath : toPath : ) ] ) {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ _delegate davServer : self didCopyItemFromPath : srcAbsolutePath toPath : dstAbsolutePath ] ;
} ) ;
}
}
return [ GCDWebServerResponse responseWithStatusCode : ( existing ? kGCDWebServerHTTPStatusCode_NoContent : kGCDWebServerHTTPStatusCode_Created ) ] ;
}
static inline xmlNodePtr _XMLChildWithName ( xmlNodePtr child , const xmlChar * name ) {
while ( child ) {
if ( ( child -> type = = XML_ELEMENT _NODE ) && ! xmlStrcmp ( child -> name , name ) ) {
return child ;
}
child = child -> next ;
}
return NULL ;
}
- ( void ) _addPropertyResponseForItem : ( NSString * ) itemPath resource : ( NSString * ) resourcePath properties : ( DAVProperties ) properties xmlString : ( NSMutableString * ) xmlString {
CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes ( kCFAllocatorDefault , ( __bridge CFStringRef ) resourcePath , NULL , CFSTR ( "<&>?+" ) , kCFStringEncodingUTF8 ) ;
if ( escapedPath ) {
NSDictionary * attributes = [ [ NSFileManager defaultManager ] attributesOfItemAtPath : itemPath error : NULL ] ;
NSString * type = [ attributes objectForKey : NSFileType ] ;
BOOL isFile = [ type isEqualToString : NSFileTypeRegular ] ;
BOOL isDirectory = [ type isEqualToString : NSFileTypeDirectory ] ;
if ( ( isFile && [ self _checkFileExtension : itemPath ] ) || isDirectory ) {
[ xmlString appendString : @ "<D:response>" ] ;
[ xmlString appendFormat : @ "<D:href>%@</D:href>" , escapedPath ] ;
[ xmlString appendString : @ "<D:propstat>" ] ;
[ xmlString appendString : @ "<D:prop>" ] ;
if ( properties & kDAVProperty_ResourceType ) {
if ( isDirectory ) {
[ xmlString appendString : @ "<D:resourcetype><D:collection/></D:resourcetype>" ] ;
} else {
[ xmlString appendString : @ "<D:resourcetype/>" ] ;
}
}
if ( ( properties & kDAVProperty_CreationDate ) && [ attributes objectForKey : NSFileCreationDate ] ) {
NSDateFormatter * formatter = [ [ NSDateFormatter alloc ] init ] ;
formatter . locale = [ [ NSLocale alloc ] initWithLocaleIdentifier : @ "en_US" ] ;
formatter . timeZone = [ NSTimeZone timeZoneWithName : @ "GMT" ] ;
formatter . dateFormat = @ "yyyy-MM-dd'T'HH:mm:ss'+00:00'" ;
[ xmlString appendFormat : @ "<D:creationdate>%@</D:creationdate>" , [ formatter stringFromDate : [ attributes fileCreationDate ] ] ] ;
}
if ( ( properties & kDAVProperty_LastModified ) && [ attributes objectForKey : NSFileModificationDate ] ) {
NSDateFormatter * formatter = [ [ NSDateFormatter alloc ] init ] ;
formatter . locale = [ [ NSLocale alloc ] initWithLocaleIdentifier : @ "en_US" ] ;
formatter . timeZone = [ NSTimeZone timeZoneWithName : @ "GMT" ] ;
formatter . dateFormat = @ "EEE', 'd' 'MMM' 'yyyy' 'HH:mm:ss' GMT'" ;
[ xmlString appendFormat : @ "<D:getlastmodified>%@</D:getlastmodified>" , [ formatter stringFromDate : [ attributes fileModificationDate ] ] ] ;
}
if ( ( properties & kDAVProperty_ContentLength ) && ! isDirectory && [ attributes objectForKey : NSFileSize ] ) {
[ xmlString appendFormat : @ "<D:getcontentlength>%llu</D:getcontentlength>" , [ attributes fileSize ] ] ;
}
[ xmlString appendString : @ "</D:prop>" ] ;
[ xmlString appendString : @ "<D:status>HTTP/1.1 200 OK</D:status>" ] ;
[ xmlString appendString : @ "</D:propstat>" ] ;
[ xmlString appendString : @ "</D:response>\n" ] ;
}
CFRelease ( escapedPath ) ;
}
}
- ( GCDWebServerResponse * ) performPROPFIND : ( GCDWebServerDataRequest * ) request {
NSInteger depth ;
NSString * depthHeader = [ request . headers objectForKey : @ "Depth" ] ;
if ( [ depthHeader isEqualToString : @ "0" ] ) {
depth = 0 ;
} else if ( [ depthHeader isEqualToString : @ "1" ] ) {
depth = 1 ;
} else {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_BadRequest message : @ "Unsupported 'Depth' header: %@" , depthHeader ] ; // TODO : Return 403 / propfind - finite - depth for "infinity" depth
}
DAVProperties properties = 0 ;
if ( request . data . length ) {
2014-04-09 16:37:15 +08:00
BOOL success = YES ;
2014-04-08 14:19:31 +08:00
xmlDocPtr document = xmlReadMemory ( request . data . bytes , ( int ) request . data . length , NULL , NULL , kXMLParseOptions ) ;
if ( document ) {
xmlNodePtr rootNode = _XMLChildWithName ( document -> children , ( const xmlChar * ) "propfind" ) ;
xmlNodePtr allNode = rootNode ? _XMLChildWithName ( rootNode -> children , ( const xmlChar * ) "allprop" ) : NULL ;
xmlNodePtr propNode = rootNode ? _XMLChildWithName ( rootNode -> children , ( const xmlChar * ) "prop" ) : NULL ;
if ( allNode ) {
properties = kDAVAllProperties ;
} else if ( propNode ) {
xmlNodePtr node = propNode -> children ;
while ( node ) {
if ( ! xmlStrcmp ( node -> name , ( const xmlChar * ) "resourcetype" ) ) {
properties | = kDAVProperty_ResourceType ;
} else if ( ! xmlStrcmp ( node -> name , ( const xmlChar * ) "creationdate" ) ) {
properties | = kDAVProperty_CreationDate ;
} else if ( ! xmlStrcmp ( node -> name , ( const xmlChar * ) "getlastmodified" ) ) {
properties | = kDAVProperty_LastModified ;
} else if ( ! xmlStrcmp ( node -> name , ( const xmlChar * ) "getcontentlength" ) ) {
properties | = kDAVProperty_ContentLength ;
} else {
[ self logWarning : @ "Unknown DAV property requested \" % s \ "" , node -> name ] ;
}
node = node -> next ;
}
} else {
2014-04-09 16:37:15 +08:00
success = NO ;
2014-04-08 14:19:31 +08:00
}
xmlFreeDoc ( document ) ;
2014-04-09 16:37:15 +08:00
} 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
2014-04-08 14:19:31 +08:00
}
} else {
properties = kDAVAllProperties ;
}
NSString * relativePath = request . path ;
NSString * absolutePath = [ _uploadDirectory stringByAppendingPathComponent : relativePath ] ;
2014-04-09 16:37:15 +08:00
BOOL isDirectory = NO ;
if ( ! [ absolutePath hasPrefix : _uploadDirectory ] || ! [ [ NSFileManager defaultManager ] fileExistsAtPath : absolutePath isDirectory : & isDirectory ] ) {
2014-04-08 14:19:31 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_NotFound message : @ "\" % @ \ " does not exist" , relativePath ] ;
}
2014-04-10 04:45:15 +08:00
NSString * itemName = [ absolutePath lastPathComponent ] ;
if ( ( [ itemName hasPrefix : @ "." ] && ! _showHidden ) || ( ! isDirectory && ! [ self _checkFileExtension : itemName ] ) ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Retrieving properties for item name \" % @ \ " is not allowed" , itemName ] ;
}
2014-04-09 16:37:15 +08:00
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 ] ;
}
2014-04-08 14:19:31 +08:00
}
NSMutableString * xmlString = [ NSMutableString stringWithString : @ "<?xml version=\" 1.0 \ " encoding=\" utf -8 \ " ?>" ] ;
[ xmlString appendString : @ "<D:multistatus xmlns:D=\" DAV : \ ">\n" ] ;
if ( ! [ relativePath hasPrefix : @ "/" ] ) {
relativePath = [ @ "/" stringByAppendingString : relativePath ] ;
}
[ self _addPropertyResponseForItem : absolutePath resource : relativePath properties : properties xmlString : xmlString ] ;
if ( depth = = 1 ) {
if ( ! [ relativePath hasSuffix : @ "/" ] ) {
relativePath = [ relativePath stringByAppendingString : @ "/" ] ;
}
for ( NSString * item in items ) {
if ( _showHidden || ! [ item hasPrefix : @ "." ] ) {
[ self _addPropertyResponseForItem : [ absolutePath stringByAppendingPathComponent : item ] resource : [ relativePath stringByAppendingString : item ] properties : properties xmlString : xmlString ] ;
}
}
}
[ xmlString appendString : @ "</D:multistatus>" ] ;
GCDWebServerDataResponse * response = [ GCDWebServerDataResponse responseWithData : [ xmlString dataUsingEncoding : NSUTF8StringEncoding ]
contentType : @ "application/xml; charset=\" utf -8 \ "" ] ;
response . statusCode = kGCDWebServerHTTPStatusCode_MultiStatus ;
return response ;
}
2014-04-09 16:37:15 +08:00
- ( GCDWebServerResponse * ) performLOCK : ( GCDWebServerDataRequest * ) request {
2014-04-09 16:41:54 +08:00
if ( ! _IsMacFinder ( request ) ) {
2014-04-09 16:37:15 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_MethodNotAllowed message : @ "LOCK method only allowed for Mac Finder" ] ;
}
NSString * relativePath = request . path ;
NSString * absolutePath = [ _uploadDirectory stringByAppendingPathComponent : relativePath ] ;
2014-04-10 04:45:15 +08:00
BOOL isDirectory = NO ;
if ( ! [ absolutePath hasPrefix : _uploadDirectory ] || ! [ [ NSFileManager defaultManager ] fileExistsAtPath : absolutePath isDirectory : & isDirectory ] ) {
2014-04-09 16:37:15 +08:00
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 ] ;
}
2014-04-10 04:45:15 +08:00
NSString * itemName = [ absolutePath lastPathComponent ] ;
if ( ( ! _showHidden && [ itemName hasPrefix : @ "." ] ) || ( ! isDirectory && ! [ self _checkFileExtension : itemName ] ) ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Locking item name \" % @ \ " is not allowed" , itemName ] ;
}
2014-04-09 16:37:15 +08:00
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 {
2014-04-09 16:41:54 +08:00
if ( ! _IsMacFinder ( request ) ) {
2014-04-09 16:37:15 +08:00
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_MethodNotAllowed message : @ "UNLOCK method only allowed for Mac Finder" ] ;
}
NSString * relativePath = request . path ;
NSString * absolutePath = [ _uploadDirectory stringByAppendingPathComponent : relativePath ] ;
2014-04-10 04:45:15 +08:00
BOOL isDirectory = NO ;
if ( ! [ absolutePath hasPrefix : _uploadDirectory ] || ! [ [ NSFileManager defaultManager ] fileExistsAtPath : absolutePath isDirectory : & isDirectory ] ) {
2014-04-09 16:37:15 +08:00
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" ] ;
}
2014-04-10 04:45:15 +08:00
NSString * itemName = [ absolutePath lastPathComponent ] ;
if ( ( ! _showHidden && [ itemName hasPrefix : @ "." ] ) || ( ! isDirectory && ! [ self _checkFileExtension : itemName ] ) ) {
return [ GCDWebServerErrorResponse responseWithClientError : kGCDWebServerHTTPStatusCode_Forbidden message : @ "Unlocking item name \" % @ \ " is not allowed" , itemName ] ;
}
2014-04-09 16:37:15 +08:00
[ self logVerbose : @ "WebDAV pretending to unlock \" % @ \ "" , relativePath ] ;
return [ GCDWebServerResponse responseWithStatusCode : kGCDWebServerHTTPStatusCode_NoContent ] ;
}
2014-04-08 14:19:31 +08:00
@ end
@ implementation GCDWebDAVServer
@ synthesize uploadDirectory = _uploadDirectory , delegate = _delegate , allowedFileExtensions = _allowedExtensions , showHiddenFiles = _showHidden ;
2014-04-08 14:45:33 +08:00
- ( instancetype ) initWithUploadDirectory : ( NSString * ) path {
2014-04-08 14:19:31 +08:00
if ( ( self = [ super init ] ) ) {
_uploadDirectory = [ [ path stringByStandardizingPath ] copy ] ;
2014-04-10 02:10:45 +08:00
# if __has _feature ( objc_arc )
2014-04-08 14:19:31 +08:00
GCDWebDAVServer * __unsafe _unretained server = self ;
2014-04-10 02:10:45 +08:00
# else
__block GCDWebDAVServer * server = self ;
# endif
2014-04-08 14:19:31 +08:00
// 9.1 PROPFIND method
[ self addDefaultHandlerForMethod : @ "PROPFIND" requestClass : [ GCDWebServerDataRequest class ] processBlock : ^ GCDWebServerResponse * ( GCDWebServerRequest * request ) {
return [ server performPROPFIND : ( GCDWebServerDataRequest * ) request ] ;
} ] ;
// 9.3 MKCOL Method
[ self addDefaultHandlerForMethod : @ "MKCOL" requestClass : [ GCDWebServerDataRequest class ] processBlock : ^ GCDWebServerResponse * ( GCDWebServerRequest * request ) {
return [ server performMKCOL : ( GCDWebServerDataRequest * ) request ] ;
} ] ;
2014-04-09 16:37:15 +08:00
// 9.4 GET & HEAD methods
2014-04-08 14:19:31 +08:00
[ self addDefaultHandlerForMethod : @ "GET" requestClass : [ GCDWebServerRequest class ] processBlock : ^ GCDWebServerResponse * ( GCDWebServerRequest * request ) {
return [ server performGET : request ] ;
} ] ;
// 9.6 DELETE method
[ self addDefaultHandlerForMethod : @ "DELETE" requestClass : [ GCDWebServerRequest class ] processBlock : ^ GCDWebServerResponse * ( GCDWebServerRequest * request ) {
return [ server performDELETE : request ] ;
} ] ;
// 9.7 PUT method
[ self addDefaultHandlerForMethod : @ "PUT" requestClass : [ GCDWebServerFileRequest class ] processBlock : ^ GCDWebServerResponse * ( GCDWebServerRequest * request ) {
return [ server performPUT : ( GCDWebServerFileRequest * ) request ] ;
} ] ;
// 9.8 COPY method
[ self addDefaultHandlerForMethod : @ "COPY" requestClass : [ GCDWebServerRequest class ] processBlock : ^ GCDWebServerResponse * ( GCDWebServerRequest * request ) {
return [ server performCOPY : request isMove : NO ] ;
} ] ;
// 9.9 MOVE method
[ self addDefaultHandlerForMethod : @ "MOVE" requestClass : [ GCDWebServerRequest class ] processBlock : ^ GCDWebServerResponse * ( GCDWebServerRequest * request ) {
return [ server performCOPY : request isMove : YES ] ;
} ] ;
2014-04-09 16:37:15 +08:00
// 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 ] ;
} ] ;
2014-04-08 14:19:31 +08:00
// 10.1 OPTIONS method / DAV Header
[ self addDefaultHandlerForMethod : @ "OPTIONS" requestClass : [ GCDWebServerRequest class ] processBlock : ^ GCDWebServerResponse * ( GCDWebServerRequest * request ) {
return [ server performOPTIONS : request ] ;
} ] ;
}
return self ;
}
# if ! __has _feature ( objc_arc )
- ( void ) dealloc {
[ _uploadDirectory release ] ;
[ _allowedExtensions release ] ;
[ super dealloc ] ;
}
# endif
@ end
@ implementation GCDWebDAVServer ( Subclassing )
- ( BOOL ) shouldUploadFileAtPath : ( NSString * ) path withTemporaryFile : ( NSString * ) tempPath {
return YES ;
}
- ( BOOL ) shouldMoveItemFromPath : ( NSString * ) fromPath toPath : ( NSString * ) toPath {
return YES ;
}
- ( BOOL ) shouldCopyItemFromPath : ( NSString * ) fromPath toPath : ( NSString * ) toPath {
return YES ;
}
- ( BOOL ) shouldDeleteItemAtPath : ( NSString * ) path {
return YES ;
}
- ( BOOL ) shouldCreateDirectoryAtPath : ( NSString * ) path {
return YES ;
}
@ end