hammerspoon/Pods/CocoaHTTPServer/Extensions/WebDAV/DAVResponse.m

373 lines
16 KiB
Objective-C

#import <libxml/parser.h>
#import "DAVResponse.h"
#import "HTTPLogging.h"
// WebDAV specifications: http://webdav.org/specs/rfc4918.html
typedef enum {
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
} DAVProperties;
#define kXMLParseOptions (XML_PARSE_NONET | XML_PARSE_RECOVER | XML_PARSE_NOBLANKS | XML_PARSE_COMPACT | XML_PARSE_NOWARNING | XML_PARSE_NOERROR)
static const int httpLogLevel = HTTP_LOG_LEVEL_WARN;
@implementation DAVResponse
static void _AddPropertyResponse(NSString* itemPath, NSString* resourcePath, DAVProperties properties, NSMutableString* xmlString) {
CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL,
CFSTR("<&>?+"), kCFStringEncodingUTF8);
if (escapedPath) {
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:NULL];
BOOL isDirectory = [[attributes fileType] isEqualToString:NSFileTypeDirectory];
[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>%qu</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);
}
}
static 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;
}
- (id) initWithMethod:(NSString*)method headers:(NSDictionary*)headers bodyData:(NSData*)body resourcePath:(NSString*)resourcePath rootPath:(NSString*)rootPath {
if ((self = [super init])) {
_status = 200;
_headers = [[NSMutableDictionary alloc] init];
// 10.1 DAV Header
if ([method isEqualToString:@"OPTIONS"]) {
if ([[headers objectForKey:@"User-Agent"] hasPrefix:@"WebDAVFS/"]) { // Mac OS X WebDAV support
[_headers setObject:@"1, 2" forKey:@"DAV"];
} else {
[_headers setObject:@"1" forKey:@"DAV"];
}
}
// 9.1 PROPFIND Method
if ([method isEqualToString:@"PROPFIND"]) {
NSInteger depth;
NSString* depthHeader = [headers objectForKey:@"Depth"];
if ([depthHeader isEqualToString:@"0"]) {
depth = 0;
} else if ([depthHeader isEqualToString:@"1"]) {
depth = 1;
} else {
HTTPLogError(@"Unsupported DAV depth \"%@\"", depthHeader);
return nil;
}
DAVProperties properties = 0;
xmlDocPtr document = xmlReadMemory(body.bytes, (int)body.length, NULL, NULL, kXMLParseOptions);
if (document) {
xmlNodePtr node = _XMLChildWithName(document->children, (const xmlChar*)"propfind");
if (node) {
node = _XMLChildWithName(node->children, (const xmlChar*)"prop");
}
if (node) {
node = node->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 {
HTTPLogWarn(@"Unknown DAV property requested \"%s\"", node->name);
}
node = node->next;
}
} else {
HTTPLogWarn(@"HTTP Server: Invalid DAV properties\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
}
xmlFreeDoc(document);
}
if (!properties) {
properties = kDAVAllProperties;
}
NSString* basePath = [rootPath stringByAppendingPathComponent:resourcePath];
if (![basePath hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:basePath]) {
return nil;
}
NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
[xmlString appendString:@"<D:multistatus xmlns:D=\"DAV:\">\n"];
if (![resourcePath hasPrefix:@"/"]) {
resourcePath = [@"/" stringByAppendingString:resourcePath];
}
_AddPropertyResponse(basePath, resourcePath, properties, xmlString);
if (depth == 1) {
if (![resourcePath hasSuffix:@"/"]) {
resourcePath = [resourcePath stringByAppendingString:@"/"];
}
NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:basePath];
NSString* path;
while ((path = [enumerator nextObject])) {
_AddPropertyResponse([basePath stringByAppendingPathComponent:path], [resourcePath stringByAppendingString:path], properties, xmlString);
[enumerator skipDescendents];
}
}
[xmlString appendString:@"</D:multistatus>"];
[_headers setObject:@"application/xml; charset=\"utf-8\"" forKey:@"Content-Type"];
_data = [xmlString dataUsingEncoding:NSUTF8StringEncoding];
_status = 207;
}
// 9.3 MKCOL Method
if ([method isEqualToString:@"MKCOL"]) {
NSString* path = [rootPath stringByAppendingPathComponent:resourcePath];
if (![path hasPrefix:rootPath]) {
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:[path stringByDeletingLastPathComponent]]) {
HTTPLogError(@"Missing intermediate collection(s) at \"%@\"", path);
_status = 409;
} else if (![[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:NULL]) {
HTTPLogError(@"Failed creating collection at \"%@\"", path);
_status = 405;
}
}
// 9.8 COPY Method
// 9.9 MOVE Method
if ([method isEqualToString:@"MOVE"] || [method isEqualToString:@"COPY"]) {
if ([method isEqualToString:@"COPY"] && ![[headers objectForKey:@"Depth"] isEqualToString:@"infinity"]) {
HTTPLogError(@"Unsupported DAV depth \"%@\"", [headers objectForKey:@"Depth"]);
return nil;
}
NSString* sourcePath = [rootPath stringByAppendingPathComponent:resourcePath];
if (![sourcePath hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:sourcePath]) {
return nil;
}
NSString* destination = [headers objectForKey:@"Destination"];
NSRange range = [destination rangeOfString:[headers objectForKey:@"Host"]];
if (range.location == NSNotFound) {
return nil;
}
NSString* destinationPath = [rootPath stringByAppendingPathComponent:
[[destination substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
if (![destinationPath hasPrefix:rootPath] || [[NSFileManager defaultManager] fileExistsAtPath:destinationPath]) {
return nil;
}
BOOL isDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:[destinationPath stringByDeletingLastPathComponent] isDirectory:&isDirectory] || !isDirectory) {
HTTPLogError(@"Invalid destination path \"%@\"", destinationPath);
_status = 409;
} else {
BOOL existing = [[NSFileManager defaultManager] fileExistsAtPath:destinationPath];
if (existing && [[headers objectForKey:@"Overwrite"] isEqualToString:@"F"]) {
HTTPLogError(@"Pre-existing destination path \"%@\"", destinationPath);
_status = 412;
} else {
if ([method isEqualToString:@"COPY"]) {
if ([[NSFileManager defaultManager] copyItemAtPath:sourcePath toPath:destinationPath error:NULL]) {
_status = existing ? 204 : 201;
} else {
HTTPLogError(@"Failed copying \"%@\" to \"%@\"", sourcePath, destinationPath);
_status = 403;
}
} else {
if ([[NSFileManager defaultManager] moveItemAtPath:sourcePath toPath:destinationPath error:NULL]) {
_status = existing ? 204 : 201;
} else {
HTTPLogError(@"Failed moving \"%@\" to \"%@\"", sourcePath, destinationPath);
_status = 403;
}
}
}
}
}
// 9.10 LOCK Method - TODO: Actually lock the resource
if ([method isEqualToString:@"LOCK"]) {
NSString* path = [rootPath stringByAppendingPathComponent:resourcePath];
if (![path hasPrefix:rootPath]) {
return nil;
}
NSString* depth = [headers objectForKey:@"Depth"];
NSString* scope = nil;
NSString* type = nil;
NSString* owner = nil;
NSString* token = nil;
xmlDocPtr document = xmlReadMemory(body.bytes, (int)body.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 {
HTTPLogWarn(@"HTTP Server: Invalid DAV properties\n%@", [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding]);
}
xmlFreeDoc(document);
} else {
// No body, see if they're trying to refresh an existing lock. If so, then just fake up the scope, type and depth so we fall
// into the lock create case.
NSString* lockToken;
if ((lockToken = [headers objectForKey:@"If"]) != nil) {
scope = @"exclusive";
type = @"write";
depth = @"0";
token = [lockToken stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"(<>)"]];
}
}
if ([scope isEqualToString:@"exclusive"] && [type isEqualToString:@"write"] && [depth isEqualToString:@"0"] &&
([[NSFileManager defaultManager] fileExistsAtPath:path] || [[NSData data] writeToFile:path atomically:YES])) {
NSString* timeout = [headers objectForKey:@"Timeout"];
if (!token) {
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
NSString *uuidStr = (__bridge_transfer NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuid);
token = [NSString stringWithFormat:@"urn:uuid:%@", uuidStr];
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", depth];
if (owner) {
[xmlString appendFormat:@"<D:owner><D:href>%@</D:href></D:owner>\n", owner];
}
if (timeout) {
[xmlString appendFormat:@"<D:timeout>%@</D:timeout>\n", timeout];
}
[xmlString appendFormat:@"<D:locktoken><D:href>%@</D:href></D:locktoken>\n", token];
NSString* lockroot = [@"http://" stringByAppendingString:[[headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:resourcePath]]];
[xmlString appendFormat:@"<D:lockroot><D:href>%@</D:href></D:lockroot>\n", lockroot];
[xmlString appendString:@"</D:activelock>\n</D:lockdiscovery>\n"];
[xmlString appendString:@"</D:prop>"];
[_headers setObject:@"application/xml; charset=\"utf-8\"" forKey:@"Content-Type"];
_data = [xmlString dataUsingEncoding:NSUTF8StringEncoding];
_status = 200;
HTTPLogVerbose(@"Pretending to lock \"%@\"", resourcePath);
} else {
HTTPLogError(@"Locking request \"%@/%@/%@\" for \"%@\" is not allowed", scope, type, depth, resourcePath);
_status = 403;
}
}
// 9.11 UNLOCK Method - TODO: Actually unlock the resource
if ([method isEqualToString:@"UNLOCK"]) {
NSString* path = [rootPath stringByAppendingPathComponent:resourcePath];
if (![path hasPrefix:rootPath] || ![[NSFileManager defaultManager] fileExistsAtPath:path]) {
return nil;
}
NSString* token = [headers objectForKey:@"Lock-Token"];
_status = token ? 204 : 400;
HTTPLogVerbose(@"Pretending to unlock \"%@\"", resourcePath);
}
}
return self;
}
- (UInt64) contentLength {
return _data ? _data.length : 0;
}
- (UInt64) offset {
return _offset;
}
- (void) setOffset:(UInt64)offset {
_offset = offset;
}
- (NSData*) readDataOfLength:(NSUInteger)lengthParameter {
if (_data) {
NSUInteger remaining = _data.length - (NSUInteger)_offset;
NSUInteger length = lengthParameter < remaining ? lengthParameter : remaining;
void* bytes = (void*)(_data.bytes + _offset);
_offset += length;
return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO];
}
return nil;
}
- (BOOL) isDone {
return _data ? _offset == _data.length : YES;
}
- (NSInteger) status {
return _status;
}
- (NSDictionary*) httpHeaders {
return _headers;
}
@end