From 1ee5e61e33efa0f4074ec0d07da2a90ff626cd32 Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Tue, 29 Oct 2024 21:50:12 -0400 Subject: [PATCH] tvOS: Add WebDAV server for adding files more easily 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 | 146 ++-- .../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, 1051 insertions(+), 66 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 9eea4da96d4..f0a8ab30938 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 = ""; }; 07B7872C29E8FE8F0088B74F /* filters */ = {isa = PBXFileReference; lastKnownFileType = folder; path = filters; sourceTree = ""; }; + 07C5C63D2CD167470030FBEC /* GCDWebDAVServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GCDWebDAVServer.h; sourceTree = ""; }; + 07C5C63E2CD167470030FBEC /* GCDWebDAVServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GCDWebDAVServer.m; sourceTree = ""; }; 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 = ""; }; + 07C5C63F2CD167470030FBEC /* GCDWebDAVServer */ = { + isa = PBXGroup; + children = ( + 07C5C63D2CD167470030FBEC /* GCDWebDAVServer.h */, + 07C5C63E2CD167470030FBEC /* GCDWebDAVServer.m */, + ); + path = GCDWebDAVServer; + sourceTree = ""; + }; 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 00000000000..0df2ce3c5ad --- /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 +@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 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* 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 00000000000..eaac9aaa128 --- /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 + +#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:@""]; + [xmlString appendFormat:@"%@", escapedPath]; + [xmlString appendString:@""]; + [xmlString appendString:@""]; + + if (properties & kDAVProperty_ResourceType) { + if (isDirectory) { + [xmlString appendString:@""]; + } else { + [xmlString appendString:@""]; + } + } + + if ((properties & kDAVProperty_CreationDate) && [attributes objectForKey:NSFileCreationDate]) { + [xmlString appendFormat:@"%@", 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:@"%@", GCDWebServerFormatRFC822((NSDate*)[attributes fileModificationDate])]; + } + + if ((properties & kDAVProperty_ContentLength) && !isDirectory && [attributes objectForKey:NSFileSize]) { + [xmlString appendFormat:@"%llu", [attributes fileSize]]; + } + + [xmlString appendString:@""]; + [xmlString appendString:@"HTTP/1.1 200 OK"]; + [xmlString appendString:@""]; + [xmlString appendString:@"\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:@""]; + [xmlString appendString:@"\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:@""]; + + 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:@""]; + [xmlString appendString:@"\n"]; + [xmlString appendString:@"\n\n"]; + [xmlString appendFormat:@"\n", type]; + [xmlString appendFormat:@"\n", scope]; + [xmlString appendFormat:@"%@\n", depthHeader]; + if (owner) { + [xmlString appendFormat:@"%@\n", owner]; + } + if (timeoutHeader) { + [xmlString appendFormat:@"%@\n", timeoutHeader]; + } + [xmlString appendFormat:@"%@\n", token]; + NSString* lockroot = [@"http://" stringByAppendingString:[(NSString*)[request.headers objectForKey:@"Host"] stringByAppendingString:[@"/" stringByAppendingString:relativePath]]]; + [xmlString appendFormat:@"%@\n", lockroot]; + [xmlString appendString:@"\n\n"]; + [xmlString appendString:@""]; + + [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 473e21fa937..d9d28790842 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). +* +* 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 0b755577bdd..d0df72effb4 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 @@ #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 @@ - (BOOL)_start:(NSError**)error { 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 @@ - (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass } - (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block { - [self addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* 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* requestHeaders, NSString* urlPath, NSDictionary* 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 @@ - (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass: - (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* requestHeaders, NSString* urlPath, NSDictionary* 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* requestHeaders, NSString* urlPath, NSDictionary* 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 @@ - (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex request - (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* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { - if (![requestMethod isEqualToString:method]) { - return nil; - } + [self + addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* 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; - } + 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]]; + 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]; + 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 @@ - (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path { - (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* requestHeaders, NSString* urlPath, NSDictionary* 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* requestHeaders, NSString* urlPath, NSDictionary* 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 @@ + (void)setLogLevel:(int)level { #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 b48edc61e12..5740794598e 100644 --- a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerConnection.m +++ b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerConnection.m @@ -794,8 +794,8 @@ - (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)sta 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 cf53153944e..7b314d698fa 100644 --- a/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerFunctions.m +++ b/pkg/apple/WebServer/GCDWebServer/Core/GCDWebServerFunctions.m @@ -31,7 +31,7 @@ #import #if TARGET_OS_IPHONE -#import +#import #else #import #endif @@ -302,7 +302,10 @@ BOOL GCDWebServerIsTextContentType(NSString* type) { 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 7ef7d917356..2e1365bfb39 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 08015d3e1b1..93d0200bb75 100644 --- a/pkg/apple/WebServer/GCDWebUploader/GCDWebUploader.m +++ b/pkg/apple/WebServer/GCDWebUploader/GCDWebUploader.m @@ -325,12 +325,17 @@ - (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request { 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 a70fc7a0da8..82693274147 100644 --- a/pkg/apple/WebServer/WebServer.h +++ b/pkg/apple/WebServer/WebServer.h @@ -7,18 +7,20 @@ // #import +#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 61f3377fe74..61a3719e366 100644 --- a/pkg/apple/WebServer/WebServer.m +++ b/pkg/apple/WebServer/WebServer.m @@ -30,20 +30,40 @@ -(instancetype)init { #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 757937eee0f..dafaeb2aa06 100644 --- a/ui/drivers/cocoa/cocoa_common.m +++ b/ui/drivers/cocoa/cocoa_common.m @@ -713,7 +713,7 @@ -(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; #if TARGET_OS_TV - [[WebServer sharedInstance] startUploader]; + [[WebServer sharedInstance] startServers]; [WebServer sharedInstance].webUploader.delegate = self; #endif } @@ -783,7 +783,7 @@ - (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server #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:^{