From cd574f7f337a3d97268ca220abea15b2fd433325 Mon Sep 17 00:00:00 2001
From: Eric Warmenhoven <eric@warmenhoven.org>
Date: Wed, 30 Oct 2024 00:07:47 -0400
Subject: [PATCH] tvOS: Add WebDAV server for adding files more easily (#17135)

Also update to latest (last) version of GCDWebServer.
---
 .../RetroArch_iOS13.xcodeproj/project.pbxproj |  14 +
 .../GCDWebDAVServer/GCDWebDAVServer.h         | 160 ++++
 .../GCDWebDAVServer/GCDWebDAVServer.m         | 717 ++++++++++++++++++
 .../GCDWebServer/Core/GCDWebServer.h          |  22 +
 .../GCDWebServer/Core/GCDWebServer.m          | 150 ++--
 .../Core/GCDWebServerConnection.m             |   4 +-
 .../GCDWebServer/Core/GCDWebServerFunctions.m |   5 +-
 .../GCDWebServer/Core/GCDWebServerResponse.h  |   2 +-
 .../WebServer/GCDWebUploader/GCDWebUploader.m |  11 +-
 pkg/apple/WebServer/WebServer.h               |   6 +-
 pkg/apple/WebServer/WebServer.m               |  26 +-
 ui/drivers/cocoa/cocoa_common.m               |   4 +-
 12 files changed, 1053 insertions(+), 68 deletions(-)
 create mode 100644 pkg/apple/WebServer/GCDWebDAVServer/GCDWebDAVServer.h
 create mode 100644 pkg/apple/WebServer/GCDWebDAVServer/GCDWebDAVServer.m

diff --git a/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj b/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj
index 9eea4da96d..f0a8ab3093 100644
--- a/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj
+++ b/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj
@@ -31,6 +31,7 @@
 		076CA50D2B695C2C00840EA5 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 076CA50C2B695C2C00840EA5 /* libz.tbd */; };
 		077D61D42C8CCF7400E492B4 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9292D6E428F549D000E47A75 /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		07B7872D29E8FE8F0088B74F /* filters in Resources */ = {isa = PBXBuildFile; fileRef = 07B7872C29E8FE8F0088B74F /* filters */; };
+		07C5C6402CD167470030FBEC /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 07C5C63E2CD167470030FBEC /* GCDWebDAVServer.m */; };
 		07F7FB022A2DA8B800037C04 /* filters in Resources */ = {isa = PBXBuildFile; fileRef = 07F7FB012A2DA8B800037C04 /* filters */; };
 		9204BE0D1D319EF300BD49DB /* griffin_objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 50521A431AA23BF500185CC9 /* griffin_objc.m */; };
 		9204BE101D319EF300BD49DB /* griffin.c in Sources */ = {isa = PBXBuildFile; fileRef = 501232C9192E5FC40063A359 /* griffin.c */; settings = {COMPILER_FLAGS = "-include $(DERIVED_FILE_DIR)/git_version.h"; }; };
@@ -182,6 +183,8 @@
 		076CA50C2B695C2C00840EA5 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS17.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; };
 		077AB2C82BFB0E28002BBE2F /* AppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = AppStore.xcconfig; path = iOS/AppStore.xcconfig; sourceTree = "<group>"; };
 		07B7872C29E8FE8F0088B74F /* filters */ = {isa = PBXFileReference; lastKnownFileType = folder; path = filters; sourceTree = "<group>"; };
+		07C5C63D2CD167470030FBEC /* GCDWebDAVServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GCDWebDAVServer.h; sourceTree = "<group>"; };
+		07C5C63E2CD167470030FBEC /* GCDWebDAVServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GCDWebDAVServer.m; sourceTree = "<group>"; };
 		07F7FB012A2DA8B800037C04 /* filters */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = filters; path = iOS/filters; sourceTree = SOURCE_ROOT; };
 		501232C9192E5FC40063A359 /* griffin.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = griffin.c; path = ../../griffin/griffin.c; sourceTree = SOURCE_ROOT; };
 		501881EB184BAD6D006F665D /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
@@ -535,6 +538,15 @@
 			path = RetroArchTopShelfExtension;
 			sourceTree = "<group>";
 		};
+		07C5C63F2CD167470030FBEC /* GCDWebDAVServer */ = {
+			isa = PBXGroup;
+			children = (
+				07C5C63D2CD167470030FBEC /* GCDWebDAVServer.h */,
+				07C5C63E2CD167470030FBEC /* GCDWebDAVServer.m */,
+			);
+			path = GCDWebDAVServer;
+			sourceTree = "<group>";
+		};
 		83D632D719ECFCC4009E3161 /* iOS */ = {
 			isa = PBXGroup;
 			children = (
@@ -1166,6 +1178,7 @@
 			children = (
 				92CC05C021FE3C6D00FF79F0 /* WebServer.h */,
 				92CC05C121FE3C6D00FF79F0 /* WebServer.m */,
+				07C5C63F2CD167470030FBEC /* GCDWebDAVServer */,
 				92CC057E21FE3C1700FF79F0 /* GCDWebServer */,
 				92CC059E21FE3C1700FF79F0 /* GCDWebUploader */,
 			);
@@ -1611,6 +1624,7 @@
 				92CC05A921FE3C1700FF79F0 /* GCDWebServer.m in Sources */,
 				926C77EA21FD20C100103EDE /* griffin_objc.m in Sources */,
 				926C77EB21FD20C400103EDE /* griffin.c in Sources */,
+				07C5C6402CD167470030FBEC /* GCDWebDAVServer.m in Sources */,
 				92CC05B521FE3C1700FF79F0 /* GCDWebServerURLEncodedFormRequest.m in Sources */,
 				92CC05B121FE3C1700FF79F0 /* GCDWebServerDataResponse.m in Sources */,
 				92CC05C321FE3C6D00FF79F0 /* WebServer.m in Sources */,
diff --git a/pkg/apple/WebServer/GCDWebDAVServer/GCDWebDAVServer.h b/pkg/apple/WebServer/GCDWebDAVServer/GCDWebDAVServer.h
new file mode 100644
index 0000000000..0df2ce3c5a
--- /dev/null
+++ b/pkg/apple/WebServer/GCDWebDAVServer/GCDWebDAVServer.h
@@ -0,0 +1,160 @@
+/*
+ Copyright (c) 2012-2019, 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.
+ */
+
+#import "GCDWebServer.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class GCDWebDAVServer;
+
+/**
+ *  Delegate methods for GCDWebDAVServer.
+ *
+ *  @warning These methods are always called on the main thread in a serialized way.
+ */
+@protocol GCDWebDAVServerDelegate <GCDWebServerDelegate>
+@optional
+
+/**
+ *  This method is called whenever a file has been downloaded.
+ */
+- (void)davServer:(GCDWebDAVServer*)server didDownloadFileAtPath:(NSString*)path;
+
+/**
+ *  This method is called whenever a file has been uploaded.
+ */
+- (void)davServer:(GCDWebDAVServer*)server didUploadFileAtPath:(NSString*)path;
+
+/**
+ *  This method is called whenever a file or directory has been moved.
+ */
+- (void)davServer:(GCDWebDAVServer*)server didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
+
+/**
+ *  This method is called whenever a file or directory has been copied.
+ */
+- (void)davServer:(GCDWebDAVServer*)server didCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
+
+/**
+ *  This method is called whenever a file or directory has been deleted.
+ */
+- (void)davServer:(GCDWebDAVServer*)server didDeleteItemAtPath:(NSString*)path;
+
+/**
+ *  This method is called whenever a directory has been created.
+ */
+- (void)davServer:(GCDWebDAVServer*)server didCreateDirectoryAtPath:(NSString*)path;
+
+@end
+
+/**
+ *  The GCDWebDAVServer subclass of GCDWebServer implements a class 1 compliant
+ *  WebDAV server. It is also partially class 2 compliant but only when the
+ *  client is the OS X WebDAV implementation (so it can work with the OS X Finder).
+ *
+ *  See the README.md file for more information about the features of GCDWebDAVServer.
+ */
+@interface GCDWebDAVServer : GCDWebServer
+
+/**
+ *  Returns the upload directory as specified when the server was initialized.
+ */
+@property(nonatomic, readonly) NSString* uploadDirectory;
+
+/**
+ *  Sets the delegate for the server.
+ */
+@property(nonatomic, weak, nullable) id<GCDWebDAVServerDelegate> delegate;
+
+/**
+ *  Sets which files are allowed to be operated on depending on their extension.
+ *
+ *  The default value is nil i.e. all file extensions are allowed.
+ */
+@property(nonatomic, copy) NSArray<NSString*>* allowedFileExtensions;
+
+/**
+ *  Sets if files and directories whose name start with a period are allowed to
+ *  be operated on.
+ *
+ *  The default value is NO.
+ */
+@property(nonatomic) BOOL allowHiddenItems;
+
+/**
+ *  This method is the designated initializer for the class.
+ */
+- (instancetype)initWithUploadDirectory:(NSString*)path;
+
+@end
+
+/**
+ *  Hooks to customize the behavior of GCDWebDAVServer.
+ *
+ *  @warning These methods can be called on any GCD thread.
+ */
+@interface GCDWebDAVServer (Subclassing)
+
+/**
+ *  This method is called to check if a file upload is allowed to complete.
+ *  The uploaded file is available for inspection at "tempPath".
+ *
+ *  The default implementation returns YES.
+ */
+- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath;
+
+/**
+ *  This method is called to check if a file or directory is allowed to be moved.
+ *
+ *  The default implementation returns YES.
+ */
+- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
+
+/**
+ *  This method is called to check if a file or directory is allowed to be copied.
+ *
+ *  The default implementation returns YES.
+ */
+- (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
+
+/**
+ *  This method is called to check if a file or directory is allowed to be deleted.
+ *
+ *  The default implementation returns YES.
+ */
+- (BOOL)shouldDeleteItemAtPath:(NSString*)path;
+
+/**
+ *  This method is called to check if a directory is allowed to be created.
+ *
+ *  The default implementation returns YES.
+ */
+- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/pkg/apple/WebServer/GCDWebDAVServer/GCDWebDAVServer.m b/pkg/apple/WebServer/GCDWebDAVServer/GCDWebDAVServer.m
new file mode 100644
index 0000000000..eaac9aaa12
--- /dev/null
+++ b/pkg/apple/WebServer/GCDWebDAVServer/GCDWebDAVServer.m
@@ -0,0 +1,717 @@
+/*
+ Copyright (c) 2012-2019, 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.
+ */
+
+#if !__has_feature(objc_arc)
+#error GCDWebDAVServer requires ARC
+#endif
+
+// WebDAV specifications: http://webdav.org/specs/rfc4918.html
+
+// Requires "HEADER_SEARCH_PATHS = $(SDKROOT)/usr/include/libxml2" in Xcode build settings
+#import <libxml/parser.h>
+
+#import "GCDWebDAVServer.h"
+
+#import "GCDWebServerFunctions.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
+};
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GCDWebDAVServer (Methods)
+- (nullable GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request;
+- (nullable GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request;
+- (nullable GCDWebServerResponse*)performPUT:(GCDWebServerFileRequest*)request;
+- (nullable GCDWebServerResponse*)performDELETE:(GCDWebServerRequest*)request;
+- (nullable GCDWebServerResponse*)performMKCOL:(GCDWebServerDataRequest*)request;
+- (nullable GCDWebServerResponse*)performCOPY:(GCDWebServerRequest*)request isMove:(BOOL)isMove;
+- (nullable GCDWebServerResponse*)performPROPFIND:(GCDWebServerDataRequest*)request;
+- (nullable GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request;
+- (nullable GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request;
+@end
+
+NS_ASSUME_NONNULL_END
+
+@implementation GCDWebDAVServer
+
+@dynamic delegate;
+
+- (instancetype)initWithUploadDirectory:(NSString*)path {
+  if ((self = [super init])) {
+    _uploadDirectory = [path copy];
+    GCDWebDAVServer* __unsafe_unretained server = self;
+
+    // 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];
+                        }];
+
+    // 9.4 GET & HEAD methods
+    [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];
+                        }];
+
+    // 9.10 LOCK method
+    [self addDefaultHandlerForMethod:@"LOCK"
+                        requestClass:[GCDWebServerDataRequest class]
+                        processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
+                          return [server performLOCK:(GCDWebServerDataRequest*)request];
+                        }];
+
+    // 9.11 UNLOCK method
+    [self addDefaultHandlerForMethod:@"UNLOCK"
+                        requestClass:[GCDWebServerRequest class]
+                        processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
+                          return [server performUNLOCK:request];
+                        }];
+
+    // 10.1 OPTIONS method / DAV Header
+    [self addDefaultHandlerForMethod:@"OPTIONS"
+                        requestClass:[GCDWebServerRequest class]
+                        processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
+                          return [server performOPTIONS:request];
+                        }];
+  }
+  return self;
+}
+
+@end
+
+@implementation GCDWebDAVServer (Methods)
+
+- (BOOL)_checkFileExtension:(NSString*)fileName {
+  if (_allowedFileExtensions && ![_allowedFileExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
+    return NO;
+  }
+  return YES;
+}
+
+static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
+  NSString* userAgentHeader = [request.headers objectForKey:@"User-Agent"];
+  return ([userAgentHeader hasPrefix:@"WebDAVFS/"] || [userAgentHeader hasPrefix:@"WebDAVLib/"]);  // OS X WebDAV client
+}
+
+- (GCDWebServerResponse*)performOPTIONS:(GCDWebServerRequest*)request {
+  GCDWebServerResponse* response = [GCDWebServerResponse response];
+  if (_IsMacFinder(request)) {
+    [response setValue:@"1, 2" forAdditionalHeader:@"DAV"];  // Classes 1 and 2
+  } else {
+    [response setValue:@"1" forAdditionalHeader:@"DAV"];  // Class 1
+  }
+  return response;
+}
+
+- (GCDWebServerResponse*)performGET:(GCDWebServerRequest*)request {
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
+  BOOL isDirectory = NO;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+
+  NSString* itemName = [absolutePath lastPathComponent];
+  if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading item name \"%@\" is not allowed", itemName];
+  }
+
+  // Because HEAD requests are mapped to GET ones, we need to handle directories but it's OK to return nothing per http://webdav.org/specs/rfc4918.html#rfc.section.9.4
+  if (isDirectory) {
+    return [GCDWebServerResponse response];
+  }
+
+  if ([self.delegate respondsToSelector:@selector(davServer:didDownloadFileAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [self.delegate davServer:self didDownloadFileAtPath:absolutePath];
+    });
+  }
+
+  if ([request hasByteRange]) {
+    return [GCDWebServerFileResponse responseWithFile:absolutePath byteRange:request.byteRange];
+  }
+
+  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:GCDWebServerNormalizePath(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:@"."] && !_allowHiddenItems) || ![self _checkFileExtension:fileName]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file name \"%@\" is not allowed", fileName];
+  }
+
+  if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:request.temporaryPath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file to \"%@\" is not permitted", relativePath];
+  }
+
+  [[NSFileManager defaultManager] removeItemAtPath:absolutePath error:NULL];
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] moveItemAtPath:request.temporaryPath toPath:absolutePath error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed moving uploaded file to \"%@\"", relativePath];
+  }
+
+  if ([self.delegate respondsToSelector:@selector(davServer:didUploadFileAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [self.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:GCDWebServerNormalizePath(relativePath)];
+  BOOL isDirectory = NO;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+
+  NSString* itemName = [absolutePath lastPathComponent];
+  if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
+  }
+
+  if (![self shouldDeleteItemAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting \"%@\" is not permitted", relativePath];
+  }
+
+  NSError* error = nil;
+  if (![[NSFileManager defaultManager] removeItemAtPath:absolutePath error:&error]) {
+    return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed deleting \"%@\"", relativePath];
+  }
+
+  if ([self.delegate respondsToSelector:@selector(davServer:didDeleteItemAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [self.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:GCDWebServerNormalizePath(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 (!_allowHiddenItems && [directoryName hasPrefix:@"."]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
+  }
+
+  if (![self shouldCreateDirectoryAtPath:absolutePath]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory \"%@\" is not permitted", relativePath];
+  }
+
+  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];
+  }
+#ifdef __GCDWEBSERVER_ENABLE_TESTING__
+  NSString* creationDateHeader = [request.headers objectForKey:@"X-GCDWebServer-CreationDate"];
+  if (creationDateHeader) {
+    NSDate* date = GCDWebServerParseISO8601(creationDateHeader);
+    if (!date || ![[NSFileManager defaultManager] setAttributes:@{NSFileCreationDate : date} ofItemAtPath:absolutePath error:&error]) {
+      return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed setting creation date for directory \"%@\"", relativePath];
+    }
+  }
+#endif
+
+  if ([self.delegate respondsToSelector:@selector(davServer:didCreateDirectoryAtPath:)]) {
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [self.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:GCDWebServerNormalizePath(srcRelativePath)];
+
+  NSString* dstRelativePath = [request.headers objectForKey:@"Destination"];
+  NSRange range = [dstRelativePath rangeOfString:(NSString*)[request.headers objectForKey:@"Host"]];
+  if ((dstRelativePath == nil) || (range.location == NSNotFound)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_BadRequest message:@"Malformed 'Destination' header: %@", dstRelativePath];
+  }
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+  dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+#pragma clang diagnostic pop
+  NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(dstRelativePath)];
+  if (!dstAbsolutePath) {
+    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];
+  }
+
+  NSString* srcName = [srcAbsolutePath lastPathComponent];
+  if ((!_allowHiddenItems && [srcName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:srcName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ from item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", srcName];
+  }
+
+  NSString* dstName = [dstAbsolutePath lastPathComponent];
+  if ((!_allowHiddenItems && [dstName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:dstName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", dstName];
+  }
+
+  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]) {
+      return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
+    }
+  } else {
+    if (![self shouldCopyItemFromPath:srcAbsolutePath toPath:dstAbsolutePath]) {
+      return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Copying \"%@\" to \"%@\" is not permitted", srcRelativePath, dstRelativePath];
+    }
+  }
+
+  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 ([self.delegate respondsToSelector:@selector(davServer:didMoveItemFromPath:toPath:)]) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        [self.delegate davServer:self didMoveItemFromPath:srcAbsolutePath toPath:dstAbsolutePath];
+      });
+    }
+  } else {
+    if ([self.delegate respondsToSelector:@selector(davServer:didCopyItemFromPath:toPath:)]) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        [self.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 {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+  CFStringRef escapedPath = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)resourcePath, NULL, CFSTR("<&>?+"), kCFStringEncodingUTF8);
+#pragma clang diagnostic pop
+  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]) {
+        [xmlString appendFormat:@"<D:creationdate>%@</D:creationdate>", GCDWebServerFormatISO8601((NSDate*)[attributes fileCreationDate])];
+      }
+
+      if ((properties & kDAVProperty_LastModified) && isFile && [attributes objectForKey:NSFileModificationDate]) {  // Last modification date is not useful for directories as it changes implicitely and 'Last-Modified' header is not provided for directories anyway
+        [xmlString appendFormat:@"<D:getlastmodified>%@</D:getlastmodified>", GCDWebServerFormatRFC822((NSDate*)[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);
+  } else {
+    [self logError:@"Failed escaping path: %@", itemPath];
+  }
+}
+
+- (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) {
+    BOOL success = YES;
+    xmlDocPtr document = xmlReadMemory(request.data.bytes, (int)request.data.length, NULL, NULL, kXMLParseOptions);
+    if (document) {
+      xmlNodePtr rootNode = _XMLChildWithName(document->children, (const xmlChar*)"propfind");
+      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 {
+        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];
+    }
+  } else {
+    properties = kDAVAllProperties;
+  }
+
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
+  BOOL isDirectory = NO;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
+  }
+
+  NSString* itemName = [absolutePath lastPathComponent];
+  if (([itemName hasPrefix:@"."] && !_allowHiddenItems) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Retrieving properties for item name \"%@\" is not allowed", itemName];
+  }
+
+  NSArray* items = nil;
+  if (isDirectory) {
+    NSError* error = nil;
+    items = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:absolutePath error:&error] sortedArrayUsingSelector:@selector(localizedStandardCompare:)];
+    if (items == nil) {
+      return [GCDWebServerErrorResponse responseWithServerError:kGCDWebServerHTTPStatusCode_InternalServerError underlyingError:error message:@"Failed listing directory \"%@\"", relativePath];
+    }
+  }
+
+  NSMutableString* xmlString = [NSMutableString stringWithString:@"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"];
+  [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 (_allowHiddenItems || ![item hasPrefix:@"."]) {
+        [self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString];
+      }
+    }
+  }
+  [xmlString appendString:@"</D:multistatus>"];
+
+  GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:(NSData*)[xmlString dataUsingEncoding:NSUTF8StringEncoding]
+                                                                      contentType:@"application/xml; charset=\"utf-8\""];
+  response.statusCode = kGCDWebServerHTTPStatusCode_MultiStatus;
+  return response;
+}
+
+- (GCDWebServerResponse*)performLOCK:(GCDWebServerDataRequest*)request {
+  if (!_IsMacFinder(request)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"LOCK method only allowed for Mac Finder"];
+  }
+
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
+  BOOL isDirectory = NO;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    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 (![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];
+  }
+
+  NSString* itemName = [absolutePath lastPathComponent];
+  if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking item name \"%@\" is not allowed", itemName];
+  }
+
+#ifdef __GCDWEBSERVER_ENABLE_TESTING__
+  NSString* lockTokenHeader = [request.headers objectForKey:@"X-GCDWebServer-LockToken"];
+  if (lockTokenHeader) {
+    token = lockTokenHeader;
+  }
+#endif
+  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:[(NSString*)[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:(NSData*)[xmlString dataUsingEncoding:NSUTF8StringEncoding]
+                                                                      contentType:@"application/xml; charset=\"utf-8\""];
+  return response;
+}
+
+- (GCDWebServerResponse*)performUNLOCK:(GCDWebServerRequest*)request {
+  if (!_IsMacFinder(request)) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_MethodNotAllowed message:@"UNLOCK method only allowed for Mac Finder"];
+  }
+
+  NSString* relativePath = request.path;
+  NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(relativePath)];
+  BOOL isDirectory = NO;
+  if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
+    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"];
+  }
+
+  NSString* itemName = [absolutePath lastPathComponent];
+  if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Unlocking item name \"%@\" is not allowed", itemName];
+  }
+
+  [self logVerbose:@"WebDAV pretending to unlock \"%@\"", relativePath];
+  return [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NoContent];
+}
+
+@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
diff --git a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServer.h b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServer.h
index 473e21fa93..d9d2879084 100644
--- a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServer.h
+++ b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServer.h
@@ -69,6 +69,13 @@ typedef GCDWebServerResponse* _Nullable (^GCDWebServerProcessBlock)(__kindof GCD
 typedef void (^GCDWebServerCompletionBlock)(GCDWebServerResponse* _Nullable response);
 typedef void (^GCDWebServerAsyncProcessBlock)(__kindof GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock);
 
+/**
+ *  The GCDWebServerBuiltInLoggerBlock is used to override the built-in logger at runtime.
+ *  The block will be passed the log level and the log message, see setLogLevel for
+ *  documentation of the log levels for the built-in logger.
+ */
+typedef void (^GCDWebServerBuiltInLoggerBlock)(int level, NSString* _Nonnull message);
+
 /**
  *  The port used by the GCDWebServer (NSNumber / NSUInteger).
  *
@@ -85,6 +92,13 @@ extern NSString* const GCDWebServerOption_Port;
  */
 extern NSString* const GCDWebServerOption_BonjourName;
 
+/**
+*  The Bonjour TXT Data used by the GCDWebServer (NSDictionary<NSString, NSString>).
+*
+*  The default value is nil.
+*/
+extern NSString* const GCDWebServerOption_BonjourTXTData;
+
 /**
  *  The Bonjour service type used by the GCDWebServer (NSString).
  *
@@ -573,6 +587,14 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
  */
 + (void)setLogLevel:(int)level;
 
+/**
+ *  Set a logger to be used instead of the built-in logger which logs to stderr.
+ *
+ *  IMPORTANT: In order for this override to work, you should not be specifying
+ *  a custom logger at compile time with "__GCDWEBSERVER_LOGGING_HEADER__".
+ */
++ (void)setBuiltInLogger:(GCDWebServerBuiltInLoggerBlock)block;
+
 /**
  *  Logs a message to the logging facility at the VERBOSE level.
  */
diff --git a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServer.m b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServer.m
index 0b755577bd..d0df72effb 100644
--- a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServer.m
+++ b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServer.m
@@ -53,6 +53,7 @@
 NSString* const GCDWebServerOption_Port = @"Port";
 NSString* const GCDWebServerOption_BonjourName = @"BonjourName";
 NSString* const GCDWebServerOption_BonjourType = @"BonjourType";
+NSString* const GCDWebServerOption_BonjourTXTData = @"BonjourTXTData";
 NSString* const GCDWebServerOption_RequestNATPortMapping = @"RequestNATPortMapping";
 NSString* const GCDWebServerOption_BindToLocalhost = @"BindToLocalhost";
 NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections";
@@ -85,18 +86,24 @@ static BOOL _run;
 
 #ifdef __GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__
 
+static GCDWebServerBuiltInLoggerBlock _builtInLoggerBlock;
+
 void GCDWebServerLogMessage(GCDWebServerLoggingLevel level, NSString* format, ...) {
   static const char* levelNames[] = {"DEBUG", "VERBOSE", "INFO", "WARNING", "ERROR"};
   static int enableLogging = -1;
   if (enableLogging < 0) {
     enableLogging = (isatty(STDERR_FILENO) ? 1 : 0);
   }
-  if (enableLogging) {
+  if (_builtInLoggerBlock || enableLogging) {
     va_list arguments;
     va_start(arguments, format);
     NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
     va_end(arguments);
-    fprintf(stderr, "[%s] %s\n", levelNames[level], [message UTF8String]);
+    if (_builtInLoggerBlock) {
+      _builtInLoggerBlock(level, message);
+    } else {
+      fprintf(stderr, "[%s] %s\n", levelNames[level], [message UTF8String]);
+    }
   }
 }
 
@@ -584,6 +591,29 @@ static inline NSString* _EncodeBase64(NSString* string) {
       CFNetServiceSetClient(_registrationService, _NetServiceRegisterCallBack, &context);
       CFNetServiceScheduleWithRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
       CFStreamError streamError = {0};
+      
+      NSDictionary* txtDataDictionary = _GetOption(_options, GCDWebServerOption_BonjourTXTData, nil);
+      if (txtDataDictionary != nil) {
+        NSUInteger count = txtDataDictionary.count;
+        CFStringRef keys[count];
+        CFStringRef values[count];
+        NSUInteger index = 0;
+        for (NSString *key in txtDataDictionary) {
+          NSString *value = txtDataDictionary[key];
+          keys[index] = (__bridge CFStringRef)(key);
+          values[index] = (__bridge CFStringRef)(value);
+          index ++;
+        }
+        CFDictionaryRef txtDictionary = CFDictionaryCreate(CFAllocatorGetDefault(), (void *)keys, (void *)values, count, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+        if (txtDictionary != NULL) {
+          CFDataRef txtData = CFNetServiceCreateTXTDataWithDictionary(nil, txtDictionary);
+          Boolean setTXTDataResult = CFNetServiceSetTXTData(_registrationService, txtData);
+          if (!setTXTDataResult) {
+            GWS_LOG_ERROR(@"Failed setting TXTData");
+          }
+        }
+      }
+      
       CFNetServiceRegisterWithOptions(_registrationService, 0, &streamError);
 
       _resolutionService = CFNetServiceCreateCopy(kCFAllocatorDefault, _registrationService);
@@ -870,13 +900,14 @@ static inline NSString* _EncodeBase64(NSString* string) {
 }
 
 - (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block {
-  [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
-    if (![requestMethod isEqualToString:method]) {
-      return nil;
-    }
-    return [(GCDWebServerRequest*)[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
-  }
-               asyncProcessBlock:block];
+  [self
+      addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
+        if (![requestMethod isEqualToString:method]) {
+          return nil;
+        }
+        return [(GCDWebServerRequest*)[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
+      }
+             asyncProcessBlock:block];
 }
 
 - (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
@@ -890,16 +921,17 @@ static inline NSString* _EncodeBase64(NSString* string) {
 
 - (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block {
   if ([path hasPrefix:@"/"] && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) {
-    [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
-      if (![requestMethod isEqualToString:method]) {
-        return nil;
-      }
-      if ([urlPath caseInsensitiveCompare:path] != NSOrderedSame) {
-        return nil;
-      }
-      return [(GCDWebServerRequest*)[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
-    }
-                 asyncProcessBlock:block];
+    [self
+        addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
+          if (![requestMethod isEqualToString:method]) {
+            return nil;
+          }
+          if ([urlPath caseInsensitiveCompare:path] != NSOrderedSame) {
+            return nil;
+          }
+          return [(GCDWebServerRequest*)[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
+        }
+               asyncProcessBlock:block];
   } else {
     GWS_DNOT_REACHED();
   }
@@ -917,34 +949,35 @@ static inline NSString* _EncodeBase64(NSString* string) {
 - (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block {
   NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL];
   if (expression && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) {
-    [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
-      if (![requestMethod isEqualToString:method]) {
-        return nil;
-      }
-
-      NSArray* matches = [expression matchesInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)];
-      if (matches.count == 0) {
-        return nil;
-      }
-
-      NSMutableArray* captures = [NSMutableArray array];
-      for (NSTextCheckingResult* result in matches) {
-        // Start at 1; index 0 is the whole string
-        for (NSUInteger i = 1; i < result.numberOfRanges; i++) {
-          NSRange range = [result rangeAtIndex:i];
-          // range is {NSNotFound, 0} "if one of the capture groups did not participate in this particular match"
-          // see discussion in -[NSRegularExpression firstMatchInString:options:range:]
-          if (range.location != NSNotFound) {
-            [captures addObject:[urlPath substringWithRange:range]];
+    [self
+        addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
+          if (![requestMethod isEqualToString:method]) {
+            return nil;
           }
-        }
-      }
 
-      GCDWebServerRequest* request = [(GCDWebServerRequest*)[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
-      [request setAttribute:captures forKey:GCDWebServerRequestAttribute_RegexCaptures];
-      return request;
-    }
-                 asyncProcessBlock:block];
+          NSArray* matches = [expression matchesInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)];
+          if (matches.count == 0) {
+            return nil;
+          }
+
+          NSMutableArray* captures = [NSMutableArray array];
+          for (NSTextCheckingResult* result in matches) {
+            // Start at 1; index 0 is the whole string
+            for (NSUInteger i = 1; i < result.numberOfRanges; i++) {
+              NSRange range = [result rangeAtIndex:i];
+              // range is {NSNotFound, 0} "if one of the capture groups did not participate in this particular match"
+              // see discussion in -[NSRegularExpression firstMatchInString:options:range:]
+              if (range.location != NSNotFound) {
+                [captures addObject:[urlPath substringWithRange:range]];
+              }
+            }
+          }
+
+          GCDWebServerRequest* request = [(GCDWebServerRequest*)[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
+          [request setAttribute:captures forKey:GCDWebServerRequestAttribute_RegexCaptures];
+          return request;
+        }
+               asyncProcessBlock:block];
   } else {
     GWS_DNOT_REACHED();
   }
@@ -1015,15 +1048,16 @@ static inline NSString* _EncodeBase64(NSString* string) {
 - (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests {
   if ([basePath hasPrefix:@"/"] && [basePath hasSuffix:@"/"]) {
     GCDWebServer* __unsafe_unretained server = self;
-    [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
-      if (![requestMethod isEqualToString:@"GET"]) {
-        return nil;
-      }
-      if (![urlPath hasPrefix:basePath]) {
-        return nil;
-      }
-      return [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
-    }
+    [self
+        addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
+          if (![requestMethod isEqualToString:@"GET"]) {
+            return nil;
+          }
+          if (![urlPath hasPrefix:basePath]) {
+            return nil;
+          }
+          return [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
+        }
         processBlock:^GCDWebServerResponse*(GCDWebServerRequest* request) {
           GCDWebServerResponse* response = nil;
           NSString* filePath = [directoryPath stringByAppendingPathComponent:GCDWebServerNormalizePath([request.path substringFromIndex:basePath.length])];
@@ -1071,6 +1105,14 @@ static inline NSString* _EncodeBase64(NSString* string) {
 #endif
 }
 
++ (void)setBuiltInLogger:(GCDWebServerBuiltInLoggerBlock)block {
+#if defined(__GCDWEBSERVER_LOGGING_FACILITY_BUILTIN__)
+  _builtInLoggerBlock = block;
+#else
+  GWS_DNOT_REACHED();  // Built-in logger must be enabled in order to override
+#endif
+}
+
 - (void)logVerbose:(NSString*)format, ... {
   va_list arguments;
   va_start(arguments, format);
diff --git a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerConnection.m b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerConnection.m
index b48edc61e1..5740794598 100644
--- a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerConnection.m
+++ b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerConnection.m
@@ -794,8 +794,8 @@ static inline BOOL _CompareResources(NSString* responseETag, NSString* requestET
   GWS_DCHECK(_responseMessage == NULL);
   GWS_DCHECK((statusCode >= 400) && (statusCode < 600));
   [self _initializeResponseHeadersWithStatusCode:statusCode];
-  [self writeHeadersWithCompletionBlock:^(BOOL success) {
-    ;  // Nothing more to do
+  [self writeHeadersWithCompletionBlock:^(BOOL success){
+      // Nothing more to do
   }];
   GWS_LOG_DEBUG(@"Connection aborted with status code %i on socket %i", (int)statusCode, _socket);
 }
diff --git a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerFunctions.m b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerFunctions.m
index cf53153944..7b314d698f 100644
--- a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerFunctions.m
+++ b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerFunctions.m
@@ -31,7 +31,7 @@
 
 #import <TargetConditionals.h>
 #if TARGET_OS_IPHONE
-#import <MobileCoreServices/MobileCoreServices.h>
+#import <CoreServices/CoreServices.h>
 #else
 #import <SystemConfiguration/SystemConfiguration.h>
 #endif
@@ -302,7 +302,10 @@ NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) {
   const char* string = [[[NSString alloc] initWithFormat:format arguments:arguments] UTF8String];
   va_end(arguments);
   unsigned char md5[CC_MD5_DIGEST_LENGTH];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
   CC_MD5(string, (CC_LONG)strlen(string), md5);
+#pragma clang diagnostic pop
   char buffer[2 * CC_MD5_DIGEST_LENGTH + 1];
   for (int i = 0; i < CC_MD5_DIGEST_LENGTH; ++i) {
     unsigned char byte = md5[i];
diff --git a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerResponse.h b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerResponse.h
index 7ef7d91735..2e1365bfb3 100644
--- a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerResponse.h
+++ b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerResponse.h
@@ -33,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
  *  The GCDWebServerBodyReaderCompletionBlock is passed by GCDWebServer to the
  *  GCDWebServerBodyReader object when reading data from it asynchronously.
  */
-typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* data, NSError* _Nullable error);
+typedef void (^GCDWebServerBodyReaderCompletionBlock)(NSData* _Nullable data, NSError* _Nullable error);
 
 /**
  *  This protocol is used by the GCDWebServerConnection to communicate with
diff --git a/pkg/apple/WebServer/GCDWebUploader/GCDWebUploader.m b/pkg/apple/WebServer/GCDWebUploader/GCDWebUploader.m
index 08015d3e1b..93d0200bb7 100644
--- a/pkg/apple/WebServer/GCDWebUploader/GCDWebUploader.m
+++ b/pkg/apple/WebServer/GCDWebUploader/GCDWebUploader.m
@@ -325,12 +325,17 @@ NS_ASSUME_NONNULL_END
     return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
   }
 
+  NSString* oldItemName = [oldAbsolutePath lastPathComponent];
+  if ((!_allowHiddenItems && [oldItemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:oldItemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving from item name \"%@\" is not allowed", oldItemName];
+  }
+
   NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
   NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:GCDWebServerNormalizePath(newRelativePath)]];
 
-  NSString* itemName = [newAbsolutePath lastPathComponent];
-  if ((!_allowHiddenItems && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
-    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving to item name \"%@\" is not allowed", itemName];
+  NSString* newItemName = [newAbsolutePath lastPathComponent];
+  if ((!_allowHiddenItems && [newItemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:newItemName])) {
+    return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving to item name \"%@\" is not allowed", newItemName];
   }
 
   if (![self shouldMoveItemFromPath:oldAbsolutePath toPath:newAbsolutePath]) {
diff --git a/pkg/apple/WebServer/WebServer.h b/pkg/apple/WebServer/WebServer.h
index a70fc7a0da..8269327414 100644
--- a/pkg/apple/WebServer/WebServer.h
+++ b/pkg/apple/WebServer/WebServer.h
@@ -7,18 +7,20 @@
 //
 
 #import <Foundation/Foundation.h>
+#import "GCDWebDAVServer.h"
 #import "GCDWebUploader.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
 @interface WebServer : NSObject
 
+@property (nonatomic,readonly,strong) GCDWebDAVServer* webDAVServer;
 @property (nonatomic,readonly,strong) GCDWebUploader* webUploader;
 
 +(WebServer*)sharedInstance;
 
--(void)startUploader;
--(void)stopUploader;
+-(void)startServers;
+-(void)stopServers;
 
 @end
 
diff --git a/pkg/apple/WebServer/WebServer.m b/pkg/apple/WebServer/WebServer.m
index 61f3377fe7..61a3719e36 100644
--- a/pkg/apple/WebServer/WebServer.m
+++ b/pkg/apple/WebServer/WebServer.m
@@ -30,20 +30,40 @@
 #elif TARGET_OS_TV
         NSString* docsPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
 #endif
+        docsPath = [docsPath stringByAppendingPathComponent:@"RetroArch"];
         _webUploader = [[GCDWebUploader alloc] initWithUploadDirectory:docsPath];
         _webUploader.allowHiddenItems = YES;
+        _webDAVServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:docsPath];
+        _webDAVServer.allowHiddenItems = YES;
     }
     return self;
 }
 
--(void)startUploader {
+-(void)startServers {
+    if ( _webDAVServer.isRunning ) {
+        [_webDAVServer stop];
+    }
+    NSDictionary *webDAVSeverOptions = @{
+        GCDWebServerOption_ServerName : @"RetroArch",
+        GCDWebServerOption_BonjourName : @"RetroArch",
+        GCDWebServerOption_BonjourType : @"_webdav._tcp",
+        GCDWebServerOption_Port : @(8080)
+    };
+    [_webDAVServer startWithOptions:webDAVSeverOptions error:nil];
+
     if ( _webUploader.isRunning ) {
         [_webUploader stop];
     }
-    [_webUploader start];
+    NSDictionary *webSeverOptions = @{
+        GCDWebServerOption_ServerName : @"RetroArch",
+        GCDWebServerOption_BonjourName : @"RetroArch",
+        GCDWebServerOption_BonjourType : @"_http._tcp",
+        GCDWebServerOption_Port : @(80)
+    };
+    [_webUploader startWithOptions:webSeverOptions error:nil];
 }
 
--(void)stopUploader {
+-(void)stopServers {
     [_webUploader stop];
 }
 
diff --git a/ui/drivers/cocoa/cocoa_common.m b/ui/drivers/cocoa/cocoa_common.m
index 757937eee0..dafaeb2aa0 100644
--- a/ui/drivers/cocoa/cocoa_common.m
+++ b/ui/drivers/cocoa/cocoa_common.m
@@ -713,7 +713,7 @@ void cocoa_file_load_with_detect_core(const char *filename);
 {
     [super viewWillAppear:animated];
 #if TARGET_OS_TV
-    [[WebServer sharedInstance] startUploader];
+    [[WebServer sharedInstance] startServers];
     [WebServer sharedInstance].webUploader.delegate = self;
 #endif
 }
@@ -783,7 +783,7 @@ void cocoa_file_load_with_detect_core(const char *filename);
 #elif TARGET_OS_IOS
         [alert addAction:[UIAlertAction actionWithTitle:@"Stop Server" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
             [[WebServer sharedInstance] webUploader].delegate = nil;
-            [[WebServer sharedInstance] stopUploader];
+            [[WebServer sharedInstance] stopServers];
         }]];
 #endif
         [self presentViewController:alert animated:YES completion:^{