tvOS: Add WebDAV server for adding files more easily (#17135)
Some checks are pending
CI 3DS / build (push) Waiting to run
CI Android / build (push) Waiting to run
CI DOS/DJGPP / build (push) Waiting to run
CI Emscripten / build (push) Waiting to run
CI GameCube / build (push) Waiting to run
CI Linux (i686) / build (push) Waiting to run
CI Windows (MSVC) / msvc (Debug, x64, 2019) (push) Waiting to run
CI Windows (MSVC) / msvc (Debug, x64, 2022) (push) Waiting to run
CI Windows (MSVC) / msvc (Release, x64, 2019) (push) Waiting to run
CI Windows (MSVC) / msvc (Release, x64, 2022) (push) Waiting to run
CI Windows (MSVC) / msvc (Release, x64, UWP) (push) Waiting to run
CI Windows (MSVC) / msvc (ReleaseAngle, x64, UWP) (push) Waiting to run
CI macOS / build (push) Waiting to run
CI Miyoo ARM32 / build (push) Waiting to run
CI PS2 / build (push) Waiting to run
CI PS4/ORBIS / build (push) Waiting to run
CI PSP / build (push) Waiting to run
CI PSVita / build (push) Waiting to run
CI RS90 Odbeta MIPS32 / build (push) Waiting to run
CI RetroFW MIPS32 / build (push) Waiting to run
CI Switch/libnx / build (push) Waiting to run
CI Wii / build (push) Waiting to run
CI WiiU / build (push) Waiting to run
CI Windows i686 (MXE) / build (push) Waiting to run
CI Windows x64 (MXE) / build (push) Waiting to run
RetroArch CI / linux-c89 (push) Waiting to run
CI webOS / build (push) Waiting to run

Also update to latest (last) version of GCDWebServer.
This commit is contained in:
Eric Warmenhoven 2024-10-30 00:07:47 -04:00 committed by GitHub
parent 3b465be243
commit cd574f7f33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1053 additions and 68 deletions

View File

@ -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 */,

View File

@ -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

View File

@ -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

View File

@ -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.
*/

View File

@ -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,20 +86,26 @@ 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);
if (_builtInLoggerBlock) {
_builtInLoggerBlock(level, message);
} else {
fprintf(stderr, "[%s] %s\n", levelNames[level], [message UTF8String]);
}
}
}
#endif
@ -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,7 +900,8 @@ 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) {
[self
addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
if (![requestMethod isEqualToString:method]) {
return nil;
}
@ -890,7 +921,8 @@ 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) {
[self
addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
if (![requestMethod isEqualToString:method]) {
return nil;
}
@ -917,7 +949,8 @@ 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) {
[self
addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
if (![requestMethod isEqualToString:method]) {
return nil;
}
@ -1015,7 +1048,8 @@ 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) {
[self
addHandlerWithMatchBlock:^GCDWebServerRequest*(NSString* requestMethod, NSURL* requestURL, NSDictionary<NSString*, NSString*>* requestHeaders, NSString* urlPath, NSDictionary<NSString*, NSString*>* urlQuery) {
if (![requestMethod isEqualToString:@"GET"]) {
return nil;
}
@ -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);

View File

@ -795,7 +795,7 @@ static inline BOOL _CompareResources(NSString* responseETag, NSString* requestET
GWS_DCHECK((statusCode >= 400) && (statusCode < 600));
[self _initializeResponseHeadersWithStatusCode:statusCode];
[self writeHeadersWithCompletionBlock:^(BOOL success){
; // Nothing more to do
// Nothing more to do
}];
GWS_LOG_DEBUG(@"Connection aborted with status code %i on socket %i", (int)statusCode, _socket);
}

View File

@ -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];

View File

@ -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

View File

@ -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]) {

View File

@ -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

View File

@ -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];
}

View File

@ -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:^{