// // WQWebDAVServer.m // FilzaSlop // // Small in-process WebDAV/HTTP server. Filza's stock WebDAV mode launches a // root launch daemon; that cannot work in a normally signed app. This server // serves the same virtual root using the sandbox extensions consumed by // worse_query, entirely inside the Filza process. // #import "WQWebDAVServer.h" #import "MCMFilzaIntegration.h" #import #import #import #import #include #include #include #include #include #include #include #include #include #include static NSString *const kWQServerName = @"FilzaSlop WQ"; static const NSInteger kWQDefaultPort = 11111; static const NSUInteger kWQMaxBodyLength = 1024UL * 1024UL * 1024UL; static int gListen4 = -1; static int gListen6 = -1; static BOOL gRunning; static NSString *gRootPath; static NSString *gUsername; static NSString *gPassword; static dispatch_queue_t gConnectionQueue; static NSNetService *gBonjourService; static NSInteger gBoundPort; static AVAudioPlayer *gSilentPlayer; static UIBackgroundTaskIdentifier gBackgroundTask; static BOOL gBackgroundTaskActive; static NSObject *gServerLock; #pragma mark - Small helpers static NSString *WQHTTPDate(NSDate *date) { static NSDateFormatter *formatter; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ formatter = [NSDateFormatter new]; formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; formatter.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss 'GMT'"; }); @synchronized (formatter) { return [formatter stringFromDate:date ?: [NSDate date]]; } } static NSString *WQXMLEscape(NSString *text) { NSMutableString *value = [text mutableCopy] ?: [NSMutableString string]; [value replaceOccurrencesOfString:@"&" withString:@"&" options:0 range:NSMakeRange(0, value.length)]; [value replaceOccurrencesOfString:@"<" withString:@"<" options:0 range:NSMakeRange(0, value.length)]; [value replaceOccurrencesOfString:@">" withString:@">" options:0 range:NSMakeRange(0, value.length)]; [value replaceOccurrencesOfString:@"\"" withString:@""" options:0 range:NSMakeRange(0, value.length)]; [value replaceOccurrencesOfString:@"'" withString:@"'" options:0 range:NSMakeRange(0, value.length)]; return value; } static NSString *WQEncodeComponent(NSString *component) { NSCharacterSet *allowed = [NSCharacterSet characterSetWithCharactersInString: @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&'()*+,;=:@"]; return [component stringByAddingPercentEncodingWithAllowedCharacters:allowed] ?: component; } static NSString *WQEncodeHref(NSString *relativePath, BOOL directory) { if (relativePath.length == 0) return directory ? @"/" : @"/"; NSMutableArray *components = [NSMutableArray array]; for (NSString *component in [relativePath componentsSeparatedByString:@"/"]) { if (component.length == 0) continue; [components addObject:WQEncodeComponent(component)]; } NSString *encoded = [components componentsJoinedByString:@"/"]; if (directory) encoded = [encoded stringByAppendingString:@"/"]; return [@"/" stringByAppendingString:encoded]; } static NSString *WQRelativePath(NSString *absolutePath) { NSString *root = gRootPath; if (!root.length) return absolutePath.lastPathComponent ?: @""; if ([absolutePath isEqualToString:root]) return @""; NSString *prefix = [root stringByAppendingString:@"/"]; if ([absolutePath hasPrefix:prefix]) return [absolutePath substringFromIndex:prefix.length]; return absolutePath.lastPathComponent ?: @""; } static NSString *WQMimeType(NSString *path) { static NSDictionary *types; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ types = @{ @"html": @"text/html; charset=utf-8", @"htm": @"text/html; charset=utf-8", @"txt": @"text/plain; charset=utf-8", @"log": @"text/plain; charset=utf-8", @"md": @"text/markdown; charset=utf-8", @"css": @"text/css; charset=utf-8", @"js": @"application/javascript", @"json": @"application/json", @"xml": @"application/xml", @"plist": @"application/x-plist", @"mobileprovision": @"application/octet-stream", @"png": @"image/png", @"jpg": @"image/jpeg", @"jpeg": @"image/jpeg", @"gif": @"image/gif", @"heic": @"image/heic", @"heif": @"image/heif", @"webp": @"image/webp", @"svg": @"image/svg+xml", @"pdf": @"application/pdf", @"zip": @"application/zip", @"gz": @"application/gzip", @"tgz": @"application/gzip", @"tar": @"application/x-tar", @"deb": @"application/x-debian-package", @"mp3": @"audio/mpeg", @"m4a": @"audio/mp4", @"wav": @"audio/wav", @"mp4": @"video/mp4", @"mov": @"video/quicktime", @"m4v": @"video/x-m4v", @"dylib": @"application/octet-stream", @"framework": @"application/octet-stream", @"sqlite": @"application/vnd.sqlite3", @"db": @"application/vnd.sqlite3", }; }); return types[path.pathExtension.lowercaseString] ?: @"application/octet-stream"; } static NSString *WQStatusText(NSInteger status) { switch (status) { case 200: return @"OK"; case 201: return @"Created"; case 204: return @"No Content"; case 206: return @"Partial Content"; case 207: return @"Multi-Status"; case 301: return @"Moved Permanently"; case 400: return @"Bad Request"; case 401: return @"Unauthorized"; case 403: return @"Forbidden"; case 404: return @"Not Found"; case 405: return @"Method Not Allowed"; case 409: return @"Conflict"; case 412: return @"Precondition Failed"; case 416: return @"Range Not Satisfiable"; case 423: return @"Locked"; case 500: return @"Internal Server Error"; case 501: return @"Not Implemented"; case 507: return @"Insufficient Storage"; default: return @"Error"; } } static NSString *WQPrimaryIPv4(void) { struct ifaddrs *interfaces = NULL; if (getifaddrs(&interfaces) != 0) return nil; NSString *fallback = nil; for (struct ifaddrs *cursor = interfaces; cursor; cursor = cursor->ifa_next) { if (!cursor->ifa_addr || cursor->ifa_addr->sa_family != AF_INET) continue; NSString *name = [NSString stringWithUTF8String:cursor->ifa_name ?: ""]; if ([name isEqualToString:@"lo0"]) continue; char address[INET_ADDRSTRLEN] = {0}; struct sockaddr_in *sin = (struct sockaddr_in *)cursor->ifa_addr; if (!inet_ntop(AF_INET, &sin->sin_addr, address, sizeof(address))) continue; NSString *value = [NSString stringWithUTF8String:address]; if (!value.length) continue; if ([name isEqualToString:@"en0"] || [name isEqualToString:@"en1"]) return value; if (!fallback && ![value hasPrefix:@"169.254."]) fallback = value; } freeifaddrs(interfaces); return fallback; } NSInteger WQWebDAVPort(void) { id value = [NSUserDefaults.standardUserDefaults objectForKey:@"air-port"]; if (!value) { Class prefsClass = NSClassFromString(@"TGPreferences"); id prefs = [prefsClass respondsToSelector:NSSelectorFromString(@"sharedInstance")] ? ((id(*)(id, SEL))objc_msgSend)(prefsClass, NSSelectorFromString(@"sharedInstance")) : nil; if ([prefs respondsToSelector:NSSelectorFromString(@"objectForPreferenceKey:")]) value = ((id(*)(id, SEL, id))objc_msgSend)(prefs, NSSelectorFromString(@"objectForPreferenceKey:"), @"air-port"); } NSInteger port = [value respondsToSelector:@selector(integerValue)] ? [value integerValue] : kWQDefaultPort; if (port < 1024 || port > 65535) port = kWQDefaultPort; return port; } NSString *WQWebDAVURL(void) { NSString *ip = WQPrimaryIPv4() ?: @"127.0.0.1"; NSInteger port = gBoundPort > 0 ? gBoundPort : WQWebDAVPort(); return [NSString stringWithFormat:@"http://%@:%ld/", ip, (long)port]; } BOOL WQWebDAVIsRunning(void) { return gRunning && (gListen4 >= 0 || gListen6 >= 0); } #pragma mark - Socket reader @interface WQSocketReader : NSObject @property (nonatomic) int descriptor; @property (nonatomic, strong) NSMutableData *buffer; @end @implementation WQSocketReader - (instancetype)initWithDescriptor:(int)descriptor { self = [super init]; if (!self) return nil; _descriptor = descriptor; _buffer = [NSMutableData data]; return self; } - (BOOL)fill { uint8_t chunk[65536]; ssize_t got = recv(self.descriptor, chunk, sizeof(chunk), 0); if (got <= 0) return NO; [self.buffer appendBytes:chunk length:(NSUInteger)got]; return YES; } - (NSData *)readUntil:(NSData *)delimiter { while (YES) { NSRange range = [self.buffer rangeOfData:delimiter options:0 range:NSMakeRange(0, self.buffer.length)]; if (range.location != NSNotFound) { NSData *value = [self.buffer subdataWithRange: NSMakeRange(0, range.location)]; NSUInteger consumed = NSMaxRange(range); [self.buffer replaceBytesInRange:NSMakeRange(0, consumed) withBytes:NULL length:0]; return value; } if (![self fill]) return nil; } } - (NSData *)readExact:(NSUInteger)length { while (self.buffer.length < length) { if (![self fill]) return nil; } NSData *value = [self.buffer subdataWithRange:NSMakeRange(0, length)]; [self.buffer replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0]; return value; } @end #pragma mark - Request @interface WQHTTPRequest : NSObject @property (nonatomic, copy) NSString *method; @property (nonatomic, copy) NSString *rawPath; @property (nonatomic, copy) NSString *version; @property (nonatomic, strong) NSDictionary *headers; @property (nonatomic, strong) NSData *body; @end @implementation WQHTTPRequest @end static NSData *WQCRLF(void) { static NSData *value; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ value = [@"\r\n" dataUsingEncoding:NSASCIIStringEncoding]; }); return value; } static NSData *WQHeaderDelimiter(void) { static NSData *value; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ value = [@"\r\n\r\n" dataUsingEncoding:NSASCIIStringEncoding]; }); return value; } #pragma mark - Responses static void WQSendAll(int descriptor, const void *bytes, NSUInteger length) { const uint8_t *cursor = bytes; NSUInteger remaining = length; while (remaining > 0) { ssize_t sent = send(descriptor, cursor, remaining, 0); if (sent <= 0) { if (errno == EINTR) continue; break; } cursor += sent; remaining -= (NSUInteger)sent; } } static void WQRespond(int descriptor, NSInteger status, NSDictionary *extraHeaders, NSData *body, NSInteger contentLengthOverride) { NSMutableString *head = [NSMutableString stringWithFormat: @"HTTP/1.1 %ld %@\r\n", (long)status, WQStatusText(status)]; [head appendFormat:@"Date: %@\r\n", WQHTTPDate([NSDate date])]; [head appendFormat:@"Server: %@\r\n", kWQServerName]; [head appendString:@"Connection: close\r\n"]; [head appendString:@"Accept-Ranges: bytes\r\n"]; BOOL hasLength = NO; for (NSString *key in extraHeaders) { if ([key caseInsensitiveCompare:@"Content-Length"] == NSOrderedSame) hasLength = YES; [head appendFormat:@"%@: %@\r\n", key, extraHeaders[key]]; } if (!hasLength) { NSInteger length = contentLengthOverride >= 0 ? contentLengthOverride : (NSInteger)body.length; [head appendFormat:@"Content-Length: %ld\r\n", (long)length]; } [head appendString:@"\r\n"]; NSData *headData = [head dataUsingEncoding:NSUTF8StringEncoding]; WQSendAll(descriptor, headData.bytes, headData.length); if (body.length > 0) WQSendAll(descriptor, body.bytes, body.length); } #pragma mark - Path handling static NSString *WQResolvePath(NSString *requestPath) { NSString *decoded = [requestPath stringByRemovingPercentEncoding] ?: requestPath; if (![decoded hasPrefix:@"/"]) return nil; NSMutableArray *components = [NSMutableArray array]; for (NSString *component in [decoded componentsSeparatedByString:@"/"]) { if (component.length == 0 || [component isEqualToString:@"."]) continue; if ([component isEqualToString:@".."]) { if (components.count > 0) [components removeLastObject]; continue; } [components addObject:component]; } NSString *relative = [components componentsJoinedByString:@"/"]; if (relative.length == 0) return gRootPath; return [gRootPath stringByAppendingPathComponent:relative]; } static BOOL WQFileIsDirectory(NSString *path) { BOOL directory = NO; BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&directory]; return exists && directory; } static NSDictionary *WQBasicHeaders(NSString *contentType) { NSMutableDictionary *headers = [NSMutableDictionary dictionary]; headers[@"Content-Type"] = contentType ?: @"text/plain; charset=utf-8"; return headers; } #pragma mark - Request body static NSData *WQReadRequestBody(WQSocketReader *reader, WQHTTPRequest *request) { NSString *transferEncoding = request.headers[@"transfer-encoding"]; if ([transferEncoding.lowercaseString containsString:@"chunked"]) { NSMutableData *body = [NSMutableData data]; while (YES) { NSData *lineData = [reader readUntil:WQCRLF()]; if (!lineData) return nil; NSString *line = [[NSString alloc] initWithData:lineData encoding:NSISOLatin1StringEncoding]; NSString *sizePart = [[line componentsSeparatedByString:@";"] firstObject]; unsigned long long size = 0; NSScanner *scanner = [NSScanner scannerWithString:sizePart ?: @""]; if (![scanner scanHexLongLong:&size]) return nil; if (size == 0) { // Consume trailers until an empty line. while (YES) { NSData *trailer = [reader readUntil:WQCRLF()]; if (!trailer) return nil; if (trailer.length == 0) break; } break; } if (body.length + size > kWQMaxBodyLength) return nil; NSData *chunk = [reader readExact:(NSUInteger)size]; if (!chunk) return nil; [body appendData:chunk]; NSData *crlf = [reader readExact:2]; if (!crlf) return nil; } return body; } NSString *lengthValue = request.headers[@"content-length"]; if (!lengthValue.length) return [NSData data]; long long length = lengthValue.longLongValue; if (length <= 0) return [NSData data]; if ((unsigned long long)length > kWQMaxBodyLength) return nil; return [reader readExact:(NSUInteger)length]; } #pragma mark - WebDAV methods static NSString *WQPropResponse(NSString *path, BOOL isDirectory) { NSFileManager *fm = NSFileManager.defaultManager; NSDictionary *attributes = [fm attributesOfItemAtPath:path error:nil]; NSDate *modified = attributes[NSFileModificationDate] ?: [NSDate date]; unsigned long long size = [attributes[NSFileSize] unsignedLongLongValue]; NSString *relative = WQRelativePath(path); NSString *href = WQEncodeHref(relative, isDirectory); NSString *displayName = relative.length == 0 ? (gRootPath.lastPathComponent ?: @"FilzaSlop") : path.lastPathComponent; NSMutableString *xml = [NSMutableString string]; [xml appendString:@""]; [xml appendFormat:@"%@", WQXMLEscape(href)]; [xml appendString:@""]; [xml appendFormat:@"%@", WQXMLEscape(displayName)]; [xml appendString:@"" @"" @""]; [xml appendFormat:@"%@", WQHTTPDate(modified)]; if (isDirectory) { [xml appendString:@""]; [xml appendString:@"httpd/unix-directory"]; } else { [xml appendString:@""]; [xml appendFormat:@"%llu", size]; [xml appendFormat:@"%@", WQXMLEscape(WQMimeType(path))]; } [xml appendString:@"HTTP/1.1 200 OK" @""]; return xml; } static void WQHandlePropfind(int descriptor, WQHTTPRequest *request, NSString *path) { BOOL exists = NO; BOOL directory = WQFileIsDirectory(path); exists = [NSFileManager.defaultManager fileExistsAtPath:path]; if (!exists) { WQRespond(descriptor, 404, WQBasicHeaders(@"text/plain"), [@"Not Found" dataUsingEncoding:NSUTF8StringEncoding], -1); return; } NSString *depth = request.headers[@"depth"] ?: @"1"; NSMutableArray *paths = [NSMutableArray arrayWithObject:path]; if (directory && ![depth isEqualToString:@"0"]) { NSError *error = nil; NSArray *names = [NSFileManager.defaultManager contentsOfDirectoryAtPath:path error:&error]; if (names) { for (NSString *name in names) [paths addObject:[path stringByAppendingPathComponent:name]]; } else { NSLog(@"[WQWebDAV] PROPFIND listing failed %@: %@", path, error.localizedDescription); } } NSMutableString *xml = [NSMutableString stringWithString: @"" @""]; for (NSString *item in paths) { BOOL isDirectory = WQFileIsDirectory(item); NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:item error:nil]; if (!attributes) continue; [xml appendString:WQPropResponse(item, isDirectory)]; } [xml appendString:@""]; NSData *body = [xml dataUsingEncoding:NSUTF8StringEncoding]; NSMutableDictionary *headers = [WQBasicHeaders( @"application/xml; charset=utf-8") mutableCopy]; headers[@"DAV"] = @"1, 2"; WQRespond(descriptor, 207, headers, body, -1); } static void WQHandleGet(int descriptor, WQHTTPRequest *request, NSString *path, BOOL headOnly) { NSFileManager *fm = NSFileManager.defaultManager; BOOL directory = WQFileIsDirectory(path); if (![fm fileExistsAtPath:path]) { WQRespond(descriptor, 404, WQBasicHeaders(@"text/plain"), [@"Not Found" dataUsingEncoding:NSUTF8StringEncoding], -1); return; } if (directory) { NSError *error = nil; NSArray *names = [fm contentsOfDirectoryAtPath:path error:&error]; NSMutableString *html = [NSMutableString stringWithFormat: @"" @"%@

%@

    ", WQXMLEscape(WQRelativePath(path) ?: @"/"), WQXMLEscape(WQRelativePath(path) ?: @"/")]; if (!names) { [html appendFormat:@"

    %@

    ", WQXMLEscape(error.localizedDescription ?: @"Unreadable")]; } for (NSString *name in [names sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) { NSString *child = [path stringByAppendingPathComponent:name]; BOOL childDirectory = WQFileIsDirectory(child); NSString *display = childDirectory ? [name stringByAppendingString:@"/"] : name; NSString *relative = WQRelativePath(child); NSString *href = WQEncodeHref(relative, childDirectory); [html appendFormat:@"
  • %@
  • ", WQXMLEscape(href), WQXMLEscape(display)]; } [html appendString:@"
"]; NSData *body = [html dataUsingEncoding:NSUTF8StringEncoding]; WQRespond(descriptor, 200, WQBasicHeaders(@"text/html; charset=utf-8"), headOnly ? nil : body, (NSInteger)body.length); return; } NSDictionary *attributes = [fm attributesOfItemAtPath:path error:nil]; unsigned long long total = [attributes[NSFileSize] unsignedLongLongValue]; NSMutableDictionary *headers = [WQBasicHeaders(WQMimeType(path)) mutableCopy]; headers[@"Accept-Ranges"] = @"bytes"; NSUInteger start = 0; NSUInteger end = total > 0 ? (NSUInteger)(total - 1) : 0; BOOL partial = NO; NSString *range = request.headers[@"range"]; if (range.length && [range hasPrefix:@"bytes="]) { NSString *spec = [range substringFromIndex:6]; NSArray *parts = [spec componentsSeparatedByString:@"-"]; if (parts.count == 2 && ![spec containsString:@","]) { NSString *startText = parts[0]; NSString *endText = parts[1]; if (startText.length == 0 && endText.length > 0) { unsigned long long suffix = endText.longLongValue; if (suffix > 0 && suffix <= total) { start = (NSUInteger)(total - suffix); end = (NSUInteger)(total - 1); partial = YES; } } else if (startText.length > 0) { unsigned long long first = startText.longLongValue; unsigned long long last = endText.length > 0 ? endText.longLongValue : total - 1; if (first < total) { start = (NSUInteger)first; end = (NSUInteger)MIN(last, total - 1); partial = YES; } } } if (partial && (start > end || start >= total)) { headers[@"Content-Range"] = [NSString stringWithFormat: @"bytes */%llu", total]; WQRespond(descriptor, 416, headers, nil, 0); return; } } NSInteger length = partial ? (NSInteger)(end - start + 1) : (NSInteger)total; if (partial) { headers[@"Content-Range"] = [NSString stringWithFormat: @"bytes %lu-%lu/%llu", (unsigned long)start, (unsigned long)end, total]; } else { start = 0; length = (NSInteger)total; } if (headOnly) { WQRespond(descriptor, partial ? 206 : 200, headers, nil, length); return; } NSData *data = [NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:nil]; if (!data) { WQRespond(descriptor, 500, WQBasicHeaders(@"text/plain"), [@"Unable to read file" dataUsingEncoding:NSUTF8StringEncoding], -1); return; } NSData *body = data; if (partial) { if (start + (NSUInteger)length > data.length) { WQRespond(descriptor, 416, headers, nil, 0); return; } body = [data subdataWithRange:NSMakeRange(start, (NSUInteger)length)]; } WQRespond(descriptor, partial ? 206 : 200, headers, body, -1); } static void WQHandlePut(int descriptor, WQHTTPRequest *request, NSString *path) { if (WQFileIsDirectory(path)) { WQRespond(descriptor, 405, WQBasicHeaders(@"text/plain"), nil, 0); return; } NSString *parent = path.stringByDeletingLastPathComponent; if (!WQFileIsDirectory(parent)) { WQRespond(descriptor, 409, WQBasicHeaders(@"text/plain"), [@"Parent does not exist" dataUsingEncoding:NSUTF8StringEncoding], -1); return; } BOOL existed = [NSFileManager.defaultManager fileExistsAtPath:path]; NSError *error = nil; BOOL written = [request.body writeToFile:path options:NSDataWritingAtomic error:&error]; if (!written) { NSLog(@"[WQWebDAV] PUT failed %@: %@", path, error.localizedDescription); WQRespond(descriptor, 507, WQBasicHeaders(@"text/plain"), [@"Write failed" dataUsingEncoding:NSUTF8StringEncoding], -1); return; } NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:path error:nil]; NSMutableDictionary *headers = [NSMutableDictionary dictionary]; headers[@"ETag"] = [NSString stringWithFormat:@"\"%llx-%llx\"", [attributes[NSFileSize] unsignedLongLongValue], (unsigned long long)[attributes[NSFileModificationDate] timeIntervalSince1970]]; WQRespond(descriptor, existed ? 204 : 201, headers, nil, 0); } static void WQHandleDelete(int descriptor, NSString *path) { NSFileManager *fm = NSFileManager.defaultManager; if (![fm fileExistsAtPath:path]) { WQRespond(descriptor, 404, WQBasicHeaders(@"text/plain"), nil, 0); return; } NSError *error = nil; if (![fm removeItemAtPath:path error:&error]) { NSLog(@"[WQWebDAV] DELETE failed %@: %@", path, error.localizedDescription); WQRespond(descriptor, 403, WQBasicHeaders(@"text/plain"), [@"Delete failed" dataUsingEncoding:NSUTF8StringEncoding], -1); return; } WQRespond(descriptor, 204, @{}, nil, 0); } static void WQHandleMkcol(int descriptor, NSString *path) { if ([NSFileManager.defaultManager fileExistsAtPath:path]) { WQRespond(descriptor, 405, WQBasicHeaders(@"text/plain"), nil, 0); return; } NSError *error = nil; if (![NSFileManager.defaultManager createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:&error]) { WQRespond(descriptor, 409, WQBasicHeaders(@"text/plain"), [error.localizedDescription dataUsingEncoding:NSUTF8StringEncoding], -1); return; } WQRespond(descriptor, 201, @{}, nil, 0); } static void WQHandleMoveCopy(int descriptor, WQHTTPRequest *request, NSString *source, BOOL move) { NSString *destinationHeader = request.headers[@"destination"]; if (!destinationHeader.length) { WQRespond(descriptor, 400, WQBasicHeaders(@"text/plain"), nil, 0); return; } NSString *destinationRequestPath = destinationHeader; NSURL *url = [NSURL URLWithString:destinationHeader]; if (url.path.length) destinationRequestPath = url.path; NSString *destination = WQResolvePath(destinationRequestPath); if (!destination.length || [destination isEqualToString:source]) { WQRespond(descriptor, 403, WQBasicHeaders(@"text/plain"), nil, 0); return; } if (![NSFileManager.defaultManager fileExistsAtPath:source]) { WQRespond(descriptor, 404, WQBasicHeaders(@"text/plain"), nil, 0); return; } if (!WQFileIsDirectory(destination.stringByDeletingLastPathComponent)) { WQRespond(descriptor, 409, WQBasicHeaders(@"text/plain"), nil, 0); return; } BOOL destinationExists = [NSFileManager.defaultManager fileExistsAtPath:destination]; NSString *overwrite = request.headers[@"overwrite"] ?: @"T"; if (destinationExists) { if ([overwrite.uppercaseString isEqualToString:@"F"]) { WQRespond(descriptor, 412, WQBasicHeaders(@"text/plain"), nil, 0); return; } [NSFileManager.defaultManager removeItemAtPath:destination error:nil]; } NSError *error = nil; BOOL ok = move ? [NSFileManager.defaultManager moveItemAtPath:source toPath:destination error:&error] : [NSFileManager.defaultManager copyItemAtPath:source toPath:destination error:&error]; if (!ok) { NSLog(@"[WQWebDAV] %@ failed %@ -> %@: %@", move ? @"MOVE" : @"COPY", source, destination, error.localizedDescription); WQRespond(descriptor, 409, WQBasicHeaders(@"text/plain"), [error.localizedDescription dataUsingEncoding:NSUTF8StringEncoding], -1); return; } WQRespond(descriptor, destinationExists ? 204 : 201, @{}, nil, 0); } static void WQHandleLock(int descriptor, WQHTTPRequest *request, NSString *path) { NSString *token = [NSString stringWithFormat:@"opaquelocktoken:%@", NSUUID.UUID.UUIDString]; NSString *relative = WQRelativePath(path); NSString *href = WQEncodeHref(relative, WQFileIsDirectory(path)); NSString *xml = [NSString stringWithFormat: @"" @"" @"" @"infinity" @"%@" @"%@" @"Second-3600" @"", WQXMLEscape(href), WQXMLEscape(token)]; NSMutableDictionary *headers = [WQBasicHeaders( @"application/xml; charset=utf-8") mutableCopy]; headers[@"Lock-Token"] = [NSString stringWithFormat:@"<%@>", token]; WQRespond(descriptor, 200, headers, [xml dataUsingEncoding:NSUTF8StringEncoding], -1); } static void WQHandleProppatch(int descriptor, WQHTTPRequest *request, NSString *path) { NSString *relative = WQRelativePath(path); NSString *href = WQEncodeHref(relative, WQFileIsDirectory(path)); NSString *xml = [NSString stringWithFormat: @"" @"" @"%@" @"HTTP/1.1 200 OK" @"", WQXMLEscape(href)]; WQRespond(descriptor, 207, WQBasicHeaders(@"application/xml; charset=utf-8"), [xml dataUsingEncoding:NSUTF8StringEncoding], -1); } static void WQHandleOptions(int descriptor) { NSMutableDictionary *headers = [NSMutableDictionary dictionary]; headers[@"DAV"] = @"1, 2"; headers[@"MS-Author-Via"] = @"DAV"; headers[@"Allow"] = @"OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, " @"MKCOL, COPY, MOVE, LOCK, UNLOCK"; headers[@"Content-Type"] = @"text/plain"; WQRespond(descriptor, 200, headers, nil, 0); } #pragma mark - Request loop static BOOL WQCheckAuthentication(WQHTTPRequest *request) { if (!gUsername.length) return YES; NSString *authorization = request.headers[@"authorization"]; if (![authorization hasPrefix:@"Basic "]) return NO; NSString *encoded = [authorization substringFromIndex:6]; NSData *data = [[NSData alloc] initWithBase64EncodedString:encoded options:0]; if (!data) return NO; NSString *pair = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSArray *parts = [pair componentsSeparatedByString:@":"]; if (parts.count < 2) return NO; NSString *user = [parts firstObject]; NSString *password = [[parts subarrayWithRange:NSMakeRange(1, parts.count - 1)] componentsJoinedByString:@":"]; return [user isEqualToString:gUsername] && [password isEqualToString:gPassword ?: @""]; } static void WQHandleClient(int descriptor) { @autoreleasepool { struct timeval timeout = { 30, 0 }; setsockopt(descriptor, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); setsockopt(descriptor, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); int noSigPipe = 1; setsockopt(descriptor, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)); WQSocketReader *reader = [[WQSocketReader alloc] initWithDescriptor:descriptor]; NSData *headData = [reader readUntil:WQHeaderDelimiter()]; if (!headData.length) { close(descriptor); return; } NSString *head = [[NSString alloc] initWithData:headData encoding:NSUTF8StringEncoding]; if (!head) head = [[NSString alloc] initWithData:headData encoding:NSISOLatin1StringEncoding]; if (!head) { close(descriptor); return; } NSArray *lines = [head componentsSeparatedByString:@"\r\n"]; if (lines.count == 0) { close(descriptor); return; } NSArray *requestLine = [lines[0] componentsSeparatedByString:@" "]; if (requestLine.count < 2) { close(descriptor); return; } WQHTTPRequest *request = [WQHTTPRequest new]; request.method = requestLine[0].uppercaseString; request.rawPath = requestLine[1]; request.version = requestLine.count > 2 ? requestLine[2] : @"HTTP/1.1"; NSMutableDictionary *headers = [NSMutableDictionary dictionary]; for (NSUInteger index = 1; index < lines.count; index++) { NSString *line = lines[index]; NSRange colon = [line rangeOfString:@":"]; if (colon.location == NSNotFound) continue; NSString *key = [[line substringToIndex:colon.location] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet] .lowercaseString; NSString *value = [[line substringFromIndex:colon.location + 1] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; if (key.length) headers[key] = value; } request.headers = headers; NSString *pathPart = request.rawPath; NSRange query = [pathPart rangeOfString:@"?"]; if (query.location != NSNotFound) pathPart = [pathPart substringToIndex:query.location]; NSRange fragment = [pathPart rangeOfString:@"#"]; if (fragment.location != NSNotFound) pathPart = [pathPart substringToIndex:fragment.location]; // Read the body only for methods that carry one; a stray huge body on // GET would otherwise stall the connection. if (![request.method isEqualToString:@"GET"] && ![request.method isEqualToString:@"HEAD"] && ![request.method isEqualToString:@"OPTIONS"] && ![request.method isEqualToString:@"DELETE"] && ![request.method isEqualToString:@"PROPFIND"]) { request.body = WQReadRequestBody(reader, request) ?: [NSData data]; } else { request.body = [NSData data]; } if (!WQCheckAuthentication(request)) { NSMutableDictionary *authHeaders = [WQBasicHeaders(@"text/plain") mutableCopy]; authHeaders[@"WWW-Authenticate"] = @"Basic realm=\"FilzaSlop WebDAV\""; WQRespond(descriptor, 401, authHeaders, [@"Authentication required" dataUsingEncoding:NSUTF8StringEncoding], -1); close(descriptor); return; } if ([request.method isEqualToString:@"OPTIONS"]) { WQHandleOptions(descriptor); close(descriptor); return; } NSString *path = WQResolvePath(pathPart); if (!path.length) { WQRespond(descriptor, 400, WQBasicHeaders(@"text/plain"), nil, 0); close(descriptor); return; } NSInteger logStatus = 0; if ([request.method isEqualToString:@"PROPFIND"]) { WQHandlePropfind(descriptor, request, path); } else if ([request.method isEqualToString:@"GET"]) { WQHandleGet(descriptor, request, path, NO); } else if ([request.method isEqualToString:@"HEAD"]) { WQHandleGet(descriptor, request, path, YES); } else if ([request.method isEqualToString:@"PUT"]) { WQHandlePut(descriptor, request, path); } else if ([request.method isEqualToString:@"DELETE"]) { WQHandleDelete(descriptor, path); } else if ([request.method isEqualToString:@"MKCOL"]) { WQHandleMkcol(descriptor, path); } else if ([request.method isEqualToString:@"MOVE"]) { WQHandleMoveCopy(descriptor, request, path, YES); } else if ([request.method isEqualToString:@"COPY"]) { WQHandleMoveCopy(descriptor, request, path, NO); } else if ([request.method isEqualToString:@"LOCK"]) { WQHandleLock(descriptor, request, path); } else if ([request.method isEqualToString:@"UNLOCK"]) { WQRespond(descriptor, 204, @{}, nil, 0); } else if ([request.method isEqualToString:@"PROPPATCH"]) { WQHandleProppatch(descriptor, request, path); } else { WQRespond(descriptor, 405, WQBasicHeaders(@"text/plain"), nil, 0); } (void)logStatus; close(descriptor); } } static void *WQAcceptLoop(void *context) { int listener = (int)(intptr_t)context; while (gRunning) { int client = accept(listener, NULL, NULL); if (client < 0) { if (!gRunning) break; if (errno == EINTR) continue; if (errno == EBADF || errno == EINVAL) break; usleep(10000); continue; } dispatch_async(gConnectionQueue, ^{ WQHandleClient(client); }); } return NULL; } static int WQCreateListener(int family, NSInteger port) { int descriptor = socket(family, SOCK_STREAM, 0); if (descriptor < 0) return -1; int reuse = 1; setsockopt(descriptor, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); int noSigPipe = 1; setsockopt(descriptor, SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)); int result = -1; if (family == AF_INET) { struct sockaddr_in address = {0}; address.sin_family = AF_INET; address.sin_port = htons((uint16_t)port); address.sin_addr.s_addr = htonl(INADDR_ANY); result = bind(descriptor, (struct sockaddr *)&address, sizeof(address)); } else if (family == AF_INET6) { int off = 0; setsockopt(descriptor, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off)); struct sockaddr_in6 address = {0}; address.sin6_family = AF_INET6; address.sin6_port = htons((uint16_t)port); address.sin6_addr = in6addr_any; result = bind(descriptor, (struct sockaddr *)&address, sizeof(address)); } if (result != 0 || listen(descriptor, 32) != 0) { close(descriptor); return -1; } return descriptor; } #pragma mark - Keep-alive / Bonjour static void WQStartKeepAlive(void) { if (!gBackgroundTaskActive) { gBackgroundTask = [UIApplication.sharedApplication beginBackgroundTaskWithName:@"WQWebDAV" expirationHandler:^{ if (gBackgroundTaskActive) { [UIApplication.sharedApplication endBackgroundTask:gBackgroundTask]; gBackgroundTaskActive = NO; } }]; gBackgroundTaskActive = gBackgroundTask != UIBackgroundTaskInvalid; } id keepAliveValue = [NSUserDefaults.standardUserDefaults objectForKey:@"WQWebDAVKeepAlive"]; if (keepAliveValue && ![keepAliveValue boolValue]) return; if (gSilentPlayer) return; AVAudioSession *session = AVAudioSession.sharedInstance; NSError *error = nil; [session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error]; if (error) { NSLog(@"[WQWebDAV] audio session category failed: %@", error.localizedDescription); return; } [session setActive:YES error:&error]; if (error) { NSLog(@"[WQWebDAV] audio session activation failed: %@", error.localizedDescription); return; } // One second of 8 kHz/16-bit silence, looped. The audio background mode is // already declared by Filza, so the process is not suspended while the // WebDAV server is enabled. const uint32_t sampleRate = 8000; const uint32_t dataSize = sampleRate * 2; NSMutableData *wav = [NSMutableData data]; [wav appendBytes:"RIFF" length:4]; uint32_t riffSize = 36 + dataSize; [wav appendBytes:&riffSize length:4]; [wav appendBytes:"WAVEfmt " length:8]; uint32_t fmtSize = 16; [wav appendBytes:&fmtSize length:4]; uint16_t format = 1, channels = 1, blockAlign = 2, bits = 16; uint32_t byteRate = sampleRate * blockAlign; [wav appendBytes:&format length:2]; [wav appendBytes:&channels length:2]; [wav appendBytes:&sampleRate length:4]; [wav appendBytes:&byteRate length:4]; [wav appendBytes:&blockAlign length:2]; [wav appendBytes:&bits length:2]; [wav appendBytes:"data" length:4]; [wav appendBytes:&dataSize length:4]; [wav appendData:[NSMutableData dataWithLength:dataSize]]; gSilentPlayer = [[AVAudioPlayer alloc] initWithData:wav error:&error]; if (!gSilentPlayer) { NSLog(@"[WQWebDAV] silent player failed: %@", error.localizedDescription); return; } gSilentPlayer.numberOfLoops = -1; gSilentPlayer.volume = 0.01f; [gSilentPlayer play]; NSLog(@"[WQWebDAV] background keep-alive active"); } static void WQStopKeepAlive(void) { [gSilentPlayer stop]; gSilentPlayer = nil; [AVAudioSession.sharedInstance setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil]; if (gBackgroundTaskActive) { [UIApplication.sharedApplication endBackgroundTask:gBackgroundTask]; gBackgroundTaskActive = NO; } } static void WQStartBonjour(NSInteger port) { gBonjourService = [[NSNetService alloc] initWithDomain:@"local." type:@"_http._tcp." name:kWQServerName port:(int)port]; gBonjourService.includesPeerToPeer = YES; [gBonjourService publish]; } #pragma mark - Lifecycle BOOL WQWebDAVStart(NSString **error) { @synchronized (gServerLock ?: (gServerLock = [NSObject new])) { if (WQWebDAVIsRunning()) return YES; NSString *root = [NSUserDefaults.standardUserDefaults stringForKey:@"WQWebDAVRoot"]; if (!root.length) root = MCMFilzaVirtualRoot(); root = [root stringByStandardizingPath]; [NSFileManager.defaultManager createDirectoryAtPath:root withIntermediateDirectories:YES attributes:nil error:nil]; if (![NSFileManager.defaultManager fileExistsAtPath:root]) { if (error) *error = @"WebDAV root does not exist"; return NO; } gRootPath = root; gUsername = [NSUserDefaults.standardUserDefaults stringForKey:@"WQWebDAVUser"]; if (!gUsername.length) gUsername = [NSUserDefaults.standardUserDefaults stringForKey:@"air-user"]; gPassword = [NSUserDefaults.standardUserDefaults stringForKey:@"WQWebDAVPassword"]; if (!gPassword.length) gPassword = [NSUserDefaults.standardUserDefaults stringForKey:@"air-password"]; signal(SIGPIPE, SIG_IGN); gRunning = YES; gConnectionQueue = dispatch_queue_create( "com.filzaslop.worsequery.webdav", DISPATCH_QUEUE_CONCURRENT); NSInteger port = WQWebDAVPort(); // Two Filza builds can coexist (public + developer). Walk a small // range so the second one still gets a working server instead of // failing on EADDRINUSE. BOOL bound = NO; for (NSInteger candidate = port; candidate < port + 10; candidate++) { gListen4 = WQCreateListener(AF_INET, candidate); gListen6 = WQCreateListener(AF_INET6, candidate); if (gListen4 >= 0 || gListen6 >= 0) { port = candidate; bound = YES; break; } } if (!bound) { gRunning = NO; if (error) *error = [NSString stringWithFormat: @"Unable to bind ports %ld-%ld (errno=%d)", (long)port, (long)(port + 9), errno]; NSLog(@"[WQWebDAV] bind failed basePort=%ld errno=%d", (long)port, errno); return NO; } gBoundPort = port; pthread_t thread; if (gListen4 >= 0 && pthread_create(&thread, NULL, WQAcceptLoop, (void *)(intptr_t)gListen4) == 0) pthread_detach(thread); if (gListen6 >= 0 && pthread_create(&thread, NULL, WQAcceptLoop, (void *)(intptr_t)gListen6) == 0) pthread_detach(thread); WQStartBonjour(port); WQStartKeepAlive(); NSLog(@"[WQWebDAV] started root=%@ url=%@ ipv4=%d ipv6=%d", root, WQWebDAVURL(), gListen4 >= 0, gListen6 >= 0); return YES; } } void WQWebDAVStop(void) { @synchronized (gServerLock ?: (gServerLock = [NSObject new])) { if (!gRunning && gListen4 < 0 && gListen6 < 0) return; gRunning = NO; if (gListen4 >= 0) { shutdown(gListen4, SHUT_RDWR); close(gListen4); gListen4 = -1; } if (gListen6 >= 0) { shutdown(gListen6, SHUT_RDWR); close(gListen6); gListen6 = -1; } [gBonjourService stop]; gBonjourService = nil; WQStopKeepAlive(); gRootPath = nil; NSLog(@"[WQWebDAV] stopped"); } }