mirror of
https://github.com/swisspol/GCDWebServer.git
synced 2026-02-11 00:00:07 +08:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e59cf4b6df | ||
|
|
9e8f0e00f3 | ||
|
|
d7650a71e0 | ||
|
|
420ddc3eac | ||
|
|
143e38c968 | ||
|
|
8e9fe4c4c1 | ||
|
|
95bccff2f7 | ||
|
|
780a608d6c | ||
|
|
18d93bbf47 | ||
|
|
b35ebd7d58 | ||
|
|
a11b047233 | ||
|
|
4eac9d4f8e | ||
|
|
d1e2a1a12f | ||
|
|
54d5abd3a8 | ||
|
|
a9fee8d7e2 | ||
|
|
6b15bdaa4e | ||
|
|
3771cf4e92 | ||
|
|
a5d83abdd0 | ||
|
|
f1e9f1a37c | ||
|
|
00b5ec87ba | ||
|
|
cf94e70a42 | ||
|
|
d47409c776 | ||
|
|
a9db13475b | ||
|
|
17fad0f1b9 | ||
|
|
5493d9e803 | ||
|
|
12b1edb958 | ||
|
|
7544a6dc4e | ||
|
|
f9621c8aac | ||
|
|
7a93b27478 | ||
|
|
9d48f9ec12 | ||
|
|
7c6e85cf9a | ||
|
|
24fbd161d8 | ||
|
|
0ae0d4175a | ||
|
|
6d550a02b7 | ||
|
|
a7f46b762f | ||
|
|
d1c7f9a323 | ||
|
|
93bfe65211 | ||
|
|
8ab53f74d5 | ||
|
|
04a69787bf | ||
|
|
dfd019de7d | ||
|
|
4db631fa27 | ||
|
|
ba03d756c6 | ||
|
|
04f59a9214 | ||
|
|
40ea252ad6 | ||
|
|
c193860468 | ||
|
|
94ad8c745e | ||
|
|
56c096996f | ||
|
|
8cbaf0f867 | ||
|
|
295901c0b3 | ||
|
|
2dda0c98ce | ||
|
|
70a38c8b01 | ||
|
|
1b12a7bd14 | ||
|
|
75e6332500 | ||
|
|
420ed719e8 | ||
|
|
3b75f9dd20 | ||
|
|
f01307b2a7 | ||
|
|
1f5e650423 | ||
|
|
d404112a88 | ||
|
|
dd3f539f74 | ||
|
|
0c51d09b69 | ||
|
|
0c53c52dd4 | ||
|
|
a687b52563 | ||
|
|
c8c34aa61f | ||
|
|
ed709d1476 | ||
|
|
3dc7cb0ec4 | ||
|
|
142f007e58 | ||
|
|
c8d2b225ba | ||
|
|
f7d6da55cd |
@@ -61,6 +61,11 @@ typedef NS_ENUM(NSInteger, DAVProperties) {
|
||||
|
||||
@implementation GCDWebDAVServer (Methods)
|
||||
|
||||
// Must match implementation in GCDWebUploader
|
||||
- (BOOL)_checkSandboxedPath:(NSString*)path {
|
||||
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
|
||||
}
|
||||
|
||||
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
||||
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
||||
return NO;
|
||||
@@ -87,7 +92,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
@@ -116,7 +121,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
if (![absolutePath hasPrefix:_uploadDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
BOOL isDirectory;
|
||||
@@ -161,7 +166,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
@@ -194,7 +199,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
if (![absolutePath hasPrefix:_uploadDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
BOOL isDirectory;
|
||||
@@ -243,7 +248,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
|
||||
NSString* srcRelativePath = request.path;
|
||||
NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:srcRelativePath];
|
||||
if (![srcAbsolutePath hasPrefix:_uploadDirectory]) {
|
||||
if (![self _checkSandboxedPath:srcAbsolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
||||
}
|
||||
|
||||
@@ -254,7 +259,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
}
|
||||
dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath];
|
||||
if (![dstAbsolutePath hasPrefix:_uploadDirectory]) {
|
||||
if (![self _checkSandboxedPath:dstAbsolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
||||
}
|
||||
|
||||
@@ -425,7 +430,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
@@ -475,7 +480,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
@@ -575,7 +580,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# http://guides.cocoapods.org/syntax/podspec.html
|
||||
# Verify Podspec with:
|
||||
# sudo gem update cocoapods
|
||||
# pod spec lint GCDWebServer.podspec --verbose
|
||||
# Add to source line:
|
||||
# :tag => s.version.to_s
|
||||
# http://guides.cocoapods.org/making/getting-setup-with-trunk.html
|
||||
# $ sudo gem update cocoapods
|
||||
# (optional) $ pod trunk register {email} {name} --description={computer}
|
||||
# $ pod trunk push
|
||||
# DELETE THIS SECTION BEFORE PROCEEDING!
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'GCDWebServer'
|
||||
s.version = '2.3.2'
|
||||
s.version = '3.0.1'
|
||||
s.author = { 'Pierre-Olivier Latour' => 'info@pol-online.net' }
|
||||
s.license = { :type => 'BSD', :file => 'LICENSE' }
|
||||
s.homepage = 'https://github.com/swisspol/GCDWebServer'
|
||||
s.summary = 'Lightweight GCD based HTTP server for OS X & iOS (includes web based uploader & WebDAV server)'
|
||||
|
||||
s.source = { :git => 'https://github.com/swisspol/GCDWebServer.git' }
|
||||
s.source = { :git => 'https://github.com/swisspol/GCDWebServer.git', :tag => s.version.to_s }
|
||||
s.ios.deployment_target = '5.0'
|
||||
s.osx.deployment_target = '10.7'
|
||||
s.requires_arc = true
|
||||
@@ -28,7 +28,6 @@ Pod::Spec.new do |s|
|
||||
cs.ios.frameworks = 'MobileCoreServices', 'CFNetwork'
|
||||
cs.osx.library = 'z'
|
||||
cs.osx.framework = 'SystemConfiguration'
|
||||
cs.compiler_flags = '-DNDEBUG' # TODO: Only set this for Release configuration
|
||||
end
|
||||
|
||||
s.subspec 'WebDAV' do |cs|
|
||||
|
||||
@@ -112,6 +112,8 @@
|
||||
E22112981690B7AA0048D2B2 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; };
|
||||
E221129A1690B7B10048D2B2 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/MobileCoreServices.framework; sourceTree = DEVELOPER_DIR; };
|
||||
E26DC18719E84B2200C68DDC /* GCDWebServer.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = GCDWebServer.podspec; sourceTree = "<group>"; };
|
||||
E26DC18819E84BC000C68DDC /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
E28BAE1618F99C810095C089 /* GCDWebServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServer.h; sourceTree = "<group>"; };
|
||||
E28BAE1718F99C810095C089 /* GCDWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServer.m; sourceTree = "<group>"; };
|
||||
E28BAE1818F99C810095C089 /* GCDWebServerConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerConnection.h; sourceTree = "<group>"; };
|
||||
@@ -183,6 +185,8 @@
|
||||
08FB7794FE84155DC02AAC07 /* LittleCMS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E26DC18819E84BC000C68DDC /* README.md */,
|
||||
E26DC18719E84B2200C68DDC /* GCDWebServer.podspec */,
|
||||
E28BAE1418F99C810095C089 /* GCDWebServer */,
|
||||
E2A0E80718F3432600C580B1 /* GCDWebDAVServer */,
|
||||
E2BE850618E77ECA0061360B /* GCDWebUploader */,
|
||||
@@ -370,7 +374,7 @@
|
||||
08FB7793FE84155DC02AAC07 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0510;
|
||||
LastUpgradeCheck = 0610;
|
||||
};
|
||||
buildConfigurationList = 1DEB928908733DD80010E9CD /* Build configuration list for PBXProject "GCDWebServer" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
@@ -490,7 +494,6 @@
|
||||
1DEB928608733DD80010E9CD /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.7;
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
SDKROOT = macosx;
|
||||
@@ -500,7 +503,6 @@
|
||||
1DEB928708733DD80010E9CD /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.7;
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
SDKROOT = macosx;
|
||||
@@ -512,6 +514,7 @@
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1";
|
||||
GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = __GCDWEBSERVER_ENABLE_TESTING__;
|
||||
HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -531,6 +534,7 @@
|
||||
"-Wno-padded",
|
||||
"-Wno-documentation",
|
||||
"-Wno-documentation-unknown-command",
|
||||
"-Wno-objc-missing-property-synthesis",
|
||||
);
|
||||
};
|
||||
name = Debug;
|
||||
@@ -539,7 +543,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = NDEBUG;
|
||||
GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = __GCDWEBSERVER_ENABLE_TESTING__;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
|
||||
@@ -550,10 +553,9 @@
|
||||
E22112761690B4DF0048D2B2 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 5.1.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
PROVISIONING_PROFILE = "";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -564,10 +566,9 @@
|
||||
E22112771690B4DF0048D2B2 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 5.1.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
PROVISIONING_PROFILE = "";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
/**
|
||||
* Log levels used by GCDWebServer.
|
||||
*
|
||||
* @warning kGCDWebServerLogLevel_Debug is only available if "NDEBUG" is not
|
||||
* defined when building.
|
||||
* @warning kGCDWebServerLogLevel_Debug is only functional if the preprocessor
|
||||
* constant "DEBUG" is is non-zero at build time.
|
||||
*/
|
||||
typedef NS_ENUM(int, GCDWebServerLogLevel) {
|
||||
kGCDWebServerLogLevel_Debug = 0,
|
||||
@@ -69,6 +69,19 @@ typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod,
|
||||
*/
|
||||
typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request);
|
||||
|
||||
/**
|
||||
* The GCDWebServerAsynchronousProcessBlock works like the GCDWebServerProcessBlock
|
||||
* except the GCDWebServerResponse can be returned to the server at a later time
|
||||
* allowing for asynchronous generation of the response.
|
||||
*
|
||||
* The block must eventually call "completionBlock" passing a GCDWebServerResponse
|
||||
* or nil on error, which will result in a 500 HTTP status code returned to the client.
|
||||
* It's however recommended to return a GCDWebServerErrorResponse on error so more
|
||||
* useful information can be returned to the client.
|
||||
*/
|
||||
typedef void (^GCDWebServerCompletionBlock)(GCDWebServerResponse* response);
|
||||
typedef void (^GCDWebServerAsyncProcessBlock)(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock);
|
||||
|
||||
/**
|
||||
* The port used by the GCDWebServer (NSNumber / NSUInteger).
|
||||
*
|
||||
@@ -77,12 +90,21 @@ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* r
|
||||
extern NSString* const GCDWebServerOption_Port;
|
||||
|
||||
/**
|
||||
* The Bonjour name used by the GCDWebServer (NSString).
|
||||
* The Bonjour name used by the GCDWebServer (NSString). If set to an empty string,
|
||||
* the name will automatically take the value of the GCDWebServerOption_ServerName
|
||||
* option. If this option is set to nil, Bonjour will be disabled.
|
||||
*
|
||||
* The default value is an empty string i.e. use the computer / device name.
|
||||
* The default value is an empty string.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_BonjourName;
|
||||
|
||||
/**
|
||||
* The Bonjour service type used by the GCDWebServer (NSString).
|
||||
*
|
||||
* The default value is "_http._tcp", the service type for HTTP web servers.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_BonjourType;
|
||||
|
||||
/**
|
||||
* The maximum number of incoming HTTP requests that can be queued waiting to
|
||||
* be handled before new ones are dropped (NSNumber / NSUInteger).
|
||||
@@ -233,6 +255,10 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
* then passes each one to a "handler" capable of generating an HTTP response
|
||||
* for it, which is then sent back to the client.
|
||||
*
|
||||
* GCDWebServer instances can be created and used from any thread but it's
|
||||
* recommended to have the main thread's runloop be running so internal callbacks
|
||||
* can be handled e.g. for Bonjour registration.
|
||||
*
|
||||
* See the README.md file for more information about the architecture of GCDWebServer.
|
||||
*/
|
||||
@interface GCDWebServer : NSObject
|
||||
@@ -262,13 +288,21 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* bonjourName;
|
||||
|
||||
/**
|
||||
* Returns the Bonjour service type used by the server.
|
||||
*
|
||||
* @warning This property is only valid if the server is running and Bonjour
|
||||
* registration has successfully completed, which can take up to a few seconds.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* bonjourType;
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)init;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to handle incoming HTTP requests.
|
||||
* Adds to the server a handler that generates responses synchronously when handling incoming HTTP requests.
|
||||
*
|
||||
* Handlers are called in a LIFO queue, so if multiple handlers can potentially
|
||||
* respond to a given request, the latest added one wins.
|
||||
@@ -277,6 +311,16 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
*/
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
||||
|
||||
/**
|
||||
* Adds to the server a handler that generates responses asynchronously when handling incoming HTTP requests.
|
||||
*
|
||||
* Handlers are called in a LIFO queue, so if multiple handlers can potentially
|
||||
* respond to a given request, the latest added one wins.
|
||||
*
|
||||
* @warning Addling handlers while the server is running is not allowed.
|
||||
*/
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock;
|
||||
|
||||
/**
|
||||
* Removes all handlers previously added to the server.
|
||||
*
|
||||
@@ -288,9 +332,9 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
* Starts the server with explicit options. This method is the designated way
|
||||
* to start the server.
|
||||
*
|
||||
* Returns NO if the server failed to start.
|
||||
* Returns NO if the server failed to start and sets "error" argument if not NULL.
|
||||
*/
|
||||
- (BOOL)startWithOptions:(NSDictionary*)options;
|
||||
- (BOOL)startWithOptions:(NSDictionary*)options error:(NSError**)error;
|
||||
|
||||
/**
|
||||
* Stops the server and prevents it to accepts new HTTP requests.
|
||||
@@ -317,12 +361,14 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
*
|
||||
* @warning This property is only valid if the server is running and Bonjour
|
||||
* registration has successfully completed, which can take up to a few seconds.
|
||||
* Also be aware this property will not automatically update if the Bonjour hostname
|
||||
* has been dynamically changed after the server started running (this should be rare).
|
||||
*/
|
||||
@property(nonatomic, readonly) NSURL* bonjourServerURL;
|
||||
|
||||
/**
|
||||
* Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS)
|
||||
* using the computer / device name for as the Bonjour name.
|
||||
* using the default Bonjour name.
|
||||
*
|
||||
* Returns NO if the server failed to start.
|
||||
*/
|
||||
@@ -331,7 +377,7 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
/**
|
||||
* Starts the server on a given port and with a specific Bonjour name.
|
||||
* Pass a nil Bonjour name to disable Bonjour entirely or an empty string to
|
||||
* use the computer / device name.
|
||||
* use the default name.
|
||||
*
|
||||
* Returns NO if the server failed to start.
|
||||
*/
|
||||
@@ -351,15 +397,15 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name;
|
||||
|
||||
/**
|
||||
* Runs the server synchronously using -startWithOptions: until a SIGINT signal
|
||||
* is received i.e. Ctrl-C. This method is intended to be used by command line
|
||||
* tools.
|
||||
* Runs the server synchronously using -startWithOptions: until a SIGTERM or
|
||||
* SIGINT signal is received i.e. Ctrl-C in Terminal. This method is intended to
|
||||
* be used by command line tools.
|
||||
*
|
||||
* Returns NO if the server failed to start.
|
||||
* Returns NO if the server failed to start and sets "error" argument if not NULL.
|
||||
*
|
||||
* @warning This method must be used from the main thread only.
|
||||
*/
|
||||
- (BOOL)runWithOptions:(NSDictionary*)options;
|
||||
- (BOOL)runWithOptions:(NSDictionary*)options error:(NSError**)error;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -369,22 +415,44 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
|
||||
/**
|
||||
* Adds a default handler to the server to handle all incoming HTTP requests
|
||||
* with a given HTTP method.
|
||||
* with a given HTTP method and generate responses synchronously.
|
||||
*/
|
||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||
|
||||
/**
|
||||
* Adds a default handler to the server to handle all incoming HTTP requests
|
||||
* with a given HTTP method and generate responses asynchronously.
|
||||
*/
|
||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||
* HTTP method and a specific case-insensitive path.
|
||||
* HTTP method and a specific case-insensitive path and generate responses
|
||||
* synchronously.
|
||||
*/
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||
* HTTP method and a path matching a case-insensitive regular expression.
|
||||
* HTTP method and a specific case-insensitive path and generate responses
|
||||
* asynchronously.
|
||||
*/
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||
* HTTP method and a path matching a case-insensitive regular expression and
|
||||
* generate responses synchronously.
|
||||
*/
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||
* HTTP method and a path matching a case-insensitive regular expression and
|
||||
* generate responses asynchronously.
|
||||
*/
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block;
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (GETHandlers)
|
||||
@@ -421,8 +489,9 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
/**
|
||||
* Sets the current log level below which logged messages are discarded.
|
||||
*
|
||||
* The default level is either DEBUG or INFO if "NDEBUG" is defined at build-time.
|
||||
* It can also be set at runtime with the "logLevel" environment variable.
|
||||
* The default level is INFO (or DEBUG if the preprocessor constant "DEBUG"
|
||||
* is non-zero at build time).
|
||||
* It can also be set at run time with the "logLevel" environment variable.
|
||||
*/
|
||||
+ (void)setLogLevel:(GCDWebServerLogLevel)level;
|
||||
|
||||
|
||||
@@ -43,45 +43,9 @@
|
||||
#define kDefaultPort 8080
|
||||
#endif
|
||||
|
||||
@interface GCDWebServer () {
|
||||
@private
|
||||
id<GCDWebServerDelegate> __unsafe_unretained _delegate;
|
||||
dispatch_queue_t _syncQueue;
|
||||
NSMutableArray* _handlers;
|
||||
NSInteger _activeConnections; // Accessed only with _syncQueue
|
||||
BOOL _connected;
|
||||
CFRunLoopTimerRef _connectedTimer;
|
||||
|
||||
NSDictionary* _options;
|
||||
NSString* _serverName;
|
||||
NSString* _authenticationRealm;
|
||||
NSMutableDictionary* _authenticationBasicAccounts;
|
||||
NSMutableDictionary* _authenticationDigestAccounts;
|
||||
Class _connectionClass;
|
||||
BOOL _mapHEADToGET;
|
||||
CFTimeInterval _disconnectDelay;
|
||||
NSUInteger _port;
|
||||
dispatch_source_t _source;
|
||||
CFNetServiceRef _service;
|
||||
#if TARGET_OS_IPHONE
|
||||
BOOL _suspendInBackground;
|
||||
UIBackgroundTaskIdentifier _backgroundTask;
|
||||
#endif
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
BOOL _recording;
|
||||
#endif
|
||||
}
|
||||
@end
|
||||
|
||||
@interface GCDWebServerHandler () {
|
||||
@private
|
||||
GCDWebServerMatchBlock _matchBlock;
|
||||
GCDWebServerProcessBlock _processBlock;
|
||||
}
|
||||
@end
|
||||
|
||||
NSString* const GCDWebServerOption_Port = @"Port";
|
||||
NSString* const GCDWebServerOption_BonjourName = @"BonjourName";
|
||||
NSString* const GCDWebServerOption_BonjourType = @"BonjourType";
|
||||
NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections";
|
||||
NSString* const GCDWebServerOption_ServerName = @"ServerName";
|
||||
NSString* const GCDWebServerOption_AuthenticationMethod = @"AuthenticationMethod";
|
||||
@@ -98,10 +62,10 @@ NSString* const GCDWebServerAuthenticationMethod_Basic = @"Basic";
|
||||
NSString* const GCDWebServerAuthenticationMethod_DigestAccess = @"DigestAccess";
|
||||
|
||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
#ifdef NDEBUG
|
||||
GCDWebServerLogLevel GCDLogLevel = kGCDWebServerLogLevel_Info;
|
||||
#else
|
||||
#if DEBUG
|
||||
GCDWebServerLogLevel GCDLogLevel = kGCDWebServerLogLevel_Debug;
|
||||
#else
|
||||
GCDWebServerLogLevel GCDLogLevel = kGCDWebServerLogLevel_Info;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -132,27 +96,81 @@ static void _SignalHandler(int signal) {
|
||||
|
||||
#endif
|
||||
|
||||
#if !TARGET_OS_IPHONE || defined(__GCDWEBSERVER_ENABLE_TESTING__)
|
||||
|
||||
// This utility function is used to ensure scheduled callbacks on the main thread are called when running the server synchronously
|
||||
// https://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html
|
||||
// The main queue works with the application’s run loop to interleave the execution of queued tasks with the execution of other event sources attached to the run loop
|
||||
// TODO: Ensure all scheduled blocks on the main queue are also executed
|
||||
static void _ExecuteMainThreadRunLoopSources() {
|
||||
SInt32 result;
|
||||
do {
|
||||
result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true);
|
||||
} while (result == kCFRunLoopRunHandledSource);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@interface GCDWebServerHandler () {
|
||||
@private
|
||||
GCDWebServerMatchBlock _matchBlock;
|
||||
GCDWebServerAsyncProcessBlock _asyncProcessBlock;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerHandler
|
||||
|
||||
@synthesize matchBlock=_matchBlock, processBlock=_processBlock;
|
||||
@synthesize matchBlock=_matchBlock, asyncProcessBlock=_asyncProcessBlock;
|
||||
|
||||
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock {
|
||||
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock {
|
||||
if ((self = [super init])) {
|
||||
_matchBlock = [matchBlock copy];
|
||||
_processBlock = [processBlock copy];
|
||||
_asyncProcessBlock = [processBlock copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_matchBlock);
|
||||
ARC_RELEASE(_processBlock);
|
||||
ARC_RELEASE(_asyncProcessBlock);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServer () {
|
||||
@private
|
||||
id<GCDWebServerDelegate> __unsafe_unretained _delegate;
|
||||
dispatch_queue_t _syncQueue;
|
||||
dispatch_semaphore_t _sourceSemaphore;
|
||||
NSMutableArray* _handlers;
|
||||
NSInteger _activeConnections; // Accessed through _syncQueue only
|
||||
BOOL _connected; // Accessed on main thread only
|
||||
CFRunLoopTimerRef _disconnectTimer; // Accessed on main thread only
|
||||
|
||||
NSDictionary* _options;
|
||||
NSString* _serverName;
|
||||
NSString* _authenticationRealm;
|
||||
NSMutableDictionary* _authenticationBasicAccounts;
|
||||
NSMutableDictionary* _authenticationDigestAccounts;
|
||||
Class _connectionClass;
|
||||
BOOL _mapHEADToGET;
|
||||
CFTimeInterval _disconnectDelay;
|
||||
NSUInteger _port;
|
||||
dispatch_source_t _source;
|
||||
CFNetServiceRef _registrationService;
|
||||
CFNetServiceRef _resolutionService;
|
||||
#if TARGET_OS_IPHONE
|
||||
BOOL _suspendInBackground;
|
||||
UIBackgroundTaskIdentifier _backgroundTask;
|
||||
#endif
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
BOOL _recording;
|
||||
#endif
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer
|
||||
|
||||
@synthesize delegate=_delegate, handlers=_handlers, port=_port, serverName=_serverName, authenticationRealm=_authenticationRealm,
|
||||
@@ -174,19 +192,11 @@ static void _SignalHandler(int signal) {
|
||||
GCDWebServerInitializeFunctions();
|
||||
}
|
||||
|
||||
static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
@autoreleasepool {
|
||||
[(ARC_BRIDGE GCDWebServer*)info _didDisconnect];
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
_syncQueue = dispatch_queue_create([NSStringFromClass([self class]) UTF8String], DISPATCH_QUEUE_SERIAL);
|
||||
_sourceSemaphore = dispatch_semaphore_create(0);
|
||||
_handlers = [[NSMutableArray alloc] init];
|
||||
CFRunLoopTimerContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
||||
_connectedTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, HUGE_VAL, HUGE_VAL, 0, 0, _ConnectedTimerCallBack, &context);
|
||||
CFRunLoopAddTimer(CFRunLoopGetMain(), _connectedTimer, kCFRunLoopCommonModes);
|
||||
#if TARGET_OS_IPHONE
|
||||
_backgroundTask = UIBackgroundTaskInvalid;
|
||||
#endif
|
||||
@@ -197,15 +207,11 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
- (void)dealloc {
|
||||
DCHECK(_connected == NO);
|
||||
DCHECK(_activeConnections == 0);
|
||||
DCHECK(_options == nil); // The server can never be dealloc'ed while running because of the retain-cycle with the dispatch source
|
||||
DCHECK(_disconnectTimer == NULL); // The server can never be dealloc'ed while the disconnect timer is pending because of the retain-cycle
|
||||
|
||||
_delegate = nil;
|
||||
if (_options) {
|
||||
[self stop];
|
||||
}
|
||||
|
||||
CFRunLoopTimerInvalidate(_connectedTimer);
|
||||
CFRelease(_connectedTimer);
|
||||
ARC_RELEASE(_handlers);
|
||||
ARC_DISPATCH_RELEASE(_sourceSemaphore);
|
||||
ARC_DISPATCH_RELEASE(_syncQueue);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
@@ -253,8 +259,10 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
DCHECK(_activeConnections >= 0);
|
||||
if (_activeConnections == 0) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (_disconnectDelay > 0.0) {
|
||||
CFRunLoopTimerSetNextFireDate(_connectedTimer, HUGE_VAL);
|
||||
if (_disconnectTimer) {
|
||||
CFRunLoopTimerInvalidate(_disconnectTimer);
|
||||
CFRelease(_disconnectTimer);
|
||||
_disconnectTimer = NULL;
|
||||
}
|
||||
if (_connected == NO) {
|
||||
[self _didConnect];
|
||||
@@ -307,8 +315,18 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
_activeConnections -= 1;
|
||||
if (_activeConnections == 0) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (_disconnectDelay > 0.0) {
|
||||
CFRunLoopTimerSetNextFireDate(_connectedTimer, CFAbsoluteTimeGetCurrent() + _disconnectDelay);
|
||||
if ((_disconnectDelay > 0.0) && (_source != NULL)) {
|
||||
if (_disconnectTimer) {
|
||||
CFRunLoopTimerInvalidate(_disconnectTimer);
|
||||
CFRelease(_disconnectTimer);
|
||||
}
|
||||
_disconnectTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + _disconnectDelay, 0.0, 0, 0, ^(CFRunLoopTimerRef timer) {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
[self _didDisconnect];
|
||||
CFRelease(_disconnectTimer);
|
||||
_disconnectTimer = NULL;
|
||||
});
|
||||
CFRunLoopAddTimer(CFRunLoopGetMain(), _disconnectTimer, kCFRunLoopCommonModes);
|
||||
} else {
|
||||
[self _didDisconnect];
|
||||
}
|
||||
@@ -318,13 +336,24 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
}
|
||||
|
||||
- (NSString*)bonjourName {
|
||||
CFStringRef name = _service ? CFNetServiceGetName(_service) : NULL;
|
||||
CFStringRef name = _resolutionService ? CFNetServiceGetName(_resolutionService) : NULL;
|
||||
return name && CFStringGetLength(name) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, name)) : nil;
|
||||
}
|
||||
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock {
|
||||
- (NSString*)bonjourType {
|
||||
CFStringRef type = _resolutionService ? CFNetServiceGetType(_resolutionService) : NULL;
|
||||
return type && CFStringGetLength(type) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, type)) : nil;
|
||||
}
|
||||
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock {
|
||||
[self addHandlerWithMatchBlock:matchBlock asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
completionBlock(processBlock(request));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock {
|
||||
DCHECK(_options == nil);
|
||||
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock];
|
||||
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock asyncProcessBlock:processBlock];
|
||||
[_handlers insertObject:handler atIndex:0];
|
||||
ARC_RELEASE(handler);
|
||||
}
|
||||
@@ -334,11 +363,26 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
[_handlers removeAllObjects];
|
||||
}
|
||||
|
||||
static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
||||
static void _NetServiceRegisterCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
@autoreleasepool {
|
||||
if (error->error) {
|
||||
LOG_ERROR(@"Bonjour error %i (domain %i)", (int)error->error, (int)error->domain);
|
||||
LOG_ERROR(@"Bonjour registration error %i (domain %i)", (int)error->error, (int)error->domain);
|
||||
} else {
|
||||
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
|
||||
LOG_VERBOSE(@"Bonjour registration complete for %@", [server class]);
|
||||
CFNetServiceResolveWithTimeout(server->_resolutionService, 1.0, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _NetServiceResolveCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
@autoreleasepool {
|
||||
if (error->error) {
|
||||
if ((error->domain != kCFStreamErrorDomainNetServices) && (error->error != kCFNetServicesErrorTimeout)) {
|
||||
LOG_ERROR(@"Bonjour resolution error %i (domain %i)", (int)error->error, (int)error->domain);
|
||||
}
|
||||
} else {
|
||||
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
|
||||
LOG_INFO(@"%@ now reachable at %@", [server class], server.bonjourServerURL);
|
||||
@@ -363,10 +407,13 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
#endif
|
||||
return ARC_AUTORELEASE([[NSString alloc] initWithData:[data base64EncodedDataWithOptions:0] encoding:NSASCIIStringEncoding]);
|
||||
}
|
||||
- (BOOL)_start {
|
||||
|
||||
- (BOOL)_start:(NSError**)error {
|
||||
DCHECK(_source == NULL);
|
||||
|
||||
NSUInteger port = [_GetOption(_options, GCDWebServerOption_Port, @0) unsignedIntegerValue];
|
||||
NSString* name = _GetOption(_options, GCDWebServerOption_BonjourName, @"");
|
||||
NSString* bonjourName = _GetOption(_options, GCDWebServerOption_BonjourName, @"");
|
||||
NSString* bonjourType = _GetOption(_options, GCDWebServerOption_BonjourType, @"_http._tcp");
|
||||
NSUInteger maxPendingConnections = [_GetOption(_options, GCDWebServerOption_MaxPendingConnections, @16) unsignedIntegerValue];
|
||||
int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (listeningSocket > 0) {
|
||||
@@ -413,6 +460,7 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
LOG_DEBUG(@"Did close listening socket %i", listeningSocket);
|
||||
}
|
||||
}
|
||||
dispatch_semaphore_signal(_sourceSemaphore);
|
||||
|
||||
});
|
||||
dispatch_source_set_event_handler(_source, ^{
|
||||
@@ -462,14 +510,21 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
_port = port;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
_service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (ARC_BRIDGE CFStringRef)name, (SInt32)_port);
|
||||
if (_service) {
|
||||
if (bonjourName) {
|
||||
_registrationService = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), (ARC_BRIDGE CFStringRef)bonjourType, (ARC_BRIDGE CFStringRef)(bonjourName.length ? bonjourName : _serverName), (SInt32)_port);
|
||||
if (_registrationService) {
|
||||
CFNetServiceClientContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
||||
CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFStreamError error = {0};
|
||||
CFNetServiceRegisterWithOptions(_service, 0, &error);
|
||||
|
||||
CFNetServiceSetClient(_registrationService, _NetServiceRegisterCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFStreamError streamError = {0};
|
||||
CFNetServiceRegisterWithOptions(_registrationService, 0, &streamError);
|
||||
|
||||
_resolutionService = CFNetServiceCreateCopy(kCFAllocatorDefault, _registrationService);
|
||||
if (_resolutionService) {
|
||||
CFNetServiceSetClient(_resolutionService, _NetServiceResolveCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed creating CFNetService");
|
||||
}
|
||||
@@ -483,15 +538,24 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed listening on socket: %s (%i)", strerror(errno), errno);
|
||||
if (error) {
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
}
|
||||
LOG_ERROR(@"Failed starting listening socket: %s (%i)", strerror(errno), errno);
|
||||
close(listeningSocket);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed binding socket: %s (%i)", strerror(errno), errno);
|
||||
if (error) {
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
}
|
||||
LOG_ERROR(@"Failed binding listening socket: %s (%i)", strerror(errno), errno);
|
||||
close(listeningSocket);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed creating socket: %s (%i)", strerror(errno), errno);
|
||||
if (error) {
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
}
|
||||
LOG_ERROR(@"Failed creating listening socket: %s (%i)", strerror(errno), errno);
|
||||
}
|
||||
return (_source ? YES : NO);
|
||||
}
|
||||
@@ -499,14 +563,23 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
- (void)_stop {
|
||||
DCHECK(_source != NULL);
|
||||
|
||||
if (_service) {
|
||||
CFNetServiceUnscheduleFromRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFNetServiceSetClient(_service, NULL, NULL);
|
||||
CFRelease(_service);
|
||||
_service = NULL;
|
||||
if (_registrationService) {
|
||||
if (_resolutionService) {
|
||||
CFNetServiceUnscheduleFromRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFNetServiceSetClient(_resolutionService, NULL, NULL);
|
||||
CFNetServiceCancel(_resolutionService);
|
||||
CFRelease(_resolutionService);
|
||||
_resolutionService = NULL;
|
||||
}
|
||||
CFNetServiceUnscheduleFromRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFNetServiceSetClient(_registrationService, NULL, NULL);
|
||||
CFNetServiceCancel(_registrationService);
|
||||
CFRelease(_registrationService);
|
||||
_registrationService = NULL;
|
||||
}
|
||||
|
||||
dispatch_source_cancel(_source); // This will close the socket
|
||||
dispatch_source_cancel(_source);
|
||||
dispatch_semaphore_wait(_sourceSemaphore, DISPATCH_TIME_FOREVER); // Wait until the cancellation handler has been called which guarantees the listening socket is closed
|
||||
ARC_DISPATCH_RELEASE(_source);
|
||||
_source = NULL;
|
||||
_port = 0;
|
||||
@@ -520,6 +593,15 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
ARC_RELEASE(_authenticationDigestAccounts);
|
||||
_authenticationDigestAccounts = nil;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (_disconnectTimer) {
|
||||
CFRunLoopTimerInvalidate(_disconnectTimer);
|
||||
CFRelease(_disconnectTimer);
|
||||
_disconnectTimer = NULL;
|
||||
[self _didDisconnect];
|
||||
}
|
||||
});
|
||||
|
||||
LOG_INFO(@"%@ stopped", [self class]);
|
||||
if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@@ -542,20 +624,20 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
LOG_DEBUG(@"Will enter foreground");
|
||||
if (!_source) {
|
||||
[self _start]; // TODO: There's probably nothing we can do on failure
|
||||
[self _start:NULL]; // TODO: There's probably nothing we can do on failure
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (BOOL)startWithOptions:(NSDictionary*)options {
|
||||
- (BOOL)startWithOptions:(NSDictionary*)options error:(NSError**)error {
|
||||
if (_options == nil) {
|
||||
_options = [options copy];
|
||||
#if TARGET_OS_IPHONE
|
||||
_suspendInBackground = [_GetOption(_options, GCDWebServerOption_AutomaticallySuspendInBackground, @YES) boolValue];
|
||||
if (((_suspendInBackground == NO) || ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground)) && ![self _start])
|
||||
if (((_suspendInBackground == NO) || ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground)) && ![self _start:error])
|
||||
#else
|
||||
if (![self _start])
|
||||
if (![self _start:error])
|
||||
#endif
|
||||
{
|
||||
ARC_RELEASE(_options);
|
||||
@@ -576,7 +658,7 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
}
|
||||
|
||||
- (BOOL)isRunning {
|
||||
return (_source ? YES : NO);
|
||||
return (_options ? YES : NO);
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
@@ -616,13 +698,14 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
}
|
||||
|
||||
- (NSURL*)bonjourServerURL {
|
||||
if (_source && _service) {
|
||||
CFStringRef name = CFNetServiceGetName(_service);
|
||||
if (name && CFStringGetLength(name)) {
|
||||
if (_source && _resolutionService) {
|
||||
NSString* name = (ARC_BRIDGE NSString*)CFNetServiceGetTargetHost(_resolutionService);
|
||||
if (name.length) {
|
||||
name = [name substringToIndex:(name.length - 1)]; // Strip trailing period at end of domain
|
||||
if (_port != 80) {
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@.local:%i/", name, (int)_port]];
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", name, (int)_port]];
|
||||
} else {
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@.local/", name]];
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", name]];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -637,7 +720,7 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
NSMutableDictionary* options = [NSMutableDictionary dictionary];
|
||||
[options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port];
|
||||
[options setValue:name forKey:GCDWebServerOption_BonjourName];
|
||||
return [self startWithOptions:options];
|
||||
return [self startWithOptions:options error:NULL];
|
||||
}
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
@@ -646,23 +729,26 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
NSMutableDictionary* options = [NSMutableDictionary dictionary];
|
||||
[options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port];
|
||||
[options setValue:name forKey:GCDWebServerOption_BonjourName];
|
||||
return [self runWithOptions:options];
|
||||
return [self runWithOptions:options error:NULL];
|
||||
}
|
||||
|
||||
- (BOOL)runWithOptions:(NSDictionary*)options {
|
||||
- (BOOL)runWithOptions:(NSDictionary*)options error:(NSError**)error {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
BOOL success = NO;
|
||||
_run = YES;
|
||||
void (*handler)(int) = signal(SIGINT, _SignalHandler);
|
||||
if (handler != SIG_ERR) {
|
||||
if ([self startWithOptions:options]) {
|
||||
void (*termHandler)(int) = signal(SIGTERM, _SignalHandler);
|
||||
void (*intHandler)(int) = signal(SIGINT, _SignalHandler);
|
||||
if ((termHandler != SIG_ERR) && (intHandler != SIG_ERR)) {
|
||||
if ([self startWithOptions:options error:error]) {
|
||||
while (_run) {
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
|
||||
}
|
||||
[self stop];
|
||||
success = YES;
|
||||
}
|
||||
signal(SIGINT, handler);
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
signal(SIGINT, intHandler);
|
||||
signal(SIGTERM, termHandler);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -674,6 +760,12 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
@implementation GCDWebServer (Handlers)
|
||||
|
||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
||||
[self addDefaultHandlerForMethod:method requestClass:aClass asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
completionBlock(block(request));
|
||||
}];
|
||||
}
|
||||
|
||||
- (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]) {
|
||||
@@ -681,10 +773,16 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
}
|
||||
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
} processBlock:block];
|
||||
} asyncProcessBlock:block];
|
||||
}
|
||||
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
||||
[self addHandlerForMethod:method path:path requestClass:aClass asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
completionBlock(block(request));
|
||||
}];
|
||||
}
|
||||
|
||||
- (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) {
|
||||
|
||||
@@ -696,13 +794,19 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
}
|
||||
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
} processBlock:block];
|
||||
} asyncProcessBlock:block];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
||||
[self addHandlerForMethod:method pathRegex:regex requestClass:aClass asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
completionBlock(block(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) {
|
||||
@@ -710,12 +814,25 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
if (![requestMethod isEqualToString:method]) {
|
||||
return nil;
|
||||
}
|
||||
if ([expression firstMatchInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)] == nil) {
|
||||
|
||||
NSArray* matches = [expression matchesInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)];
|
||||
if (matches.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
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++) {
|
||||
[captures addObject:[urlPath substringWithRange:[result rangeAtIndex:i]]];
|
||||
}
|
||||
}
|
||||
|
||||
GCDWebServerRequest* request = [[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
|
||||
[request setAttribute:captures forKey:GCDWebServerRequestAttribute_RegexCaptures];
|
||||
return ARC_AUTORELEASE(request);
|
||||
|
||||
} processBlock:block];
|
||||
} asyncProcessBlock:block];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
@@ -799,17 +916,18 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
|
||||
GCDWebServerResponse* response = nil;
|
||||
NSString* filePath = [directoryPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]];
|
||||
BOOL isDirectory;
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]) {
|
||||
if (isDirectory) {
|
||||
NSString* fileType = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL] fileType];
|
||||
if (fileType) {
|
||||
if ([fileType isEqualToString:NSFileTypeDirectory]) {
|
||||
if (indexFilename) {
|
||||
NSString* indexPath = [filePath stringByAppendingPathComponent:indexFilename];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath isDirectory:&isDirectory] && !isDirectory) {
|
||||
NSString* indexType = [[[NSFileManager defaultManager] attributesOfItemAtPath:indexPath error:NULL] fileType];
|
||||
if ([indexType isEqualToString:NSFileTypeRegular]) {
|
||||
return [GCDWebServerFileResponse responseWithFile:indexPath];
|
||||
}
|
||||
}
|
||||
response = [server _responseWithContentsOfDirectory:filePath];
|
||||
} else {
|
||||
} else if ([fileType isEqualToString:NSFileTypeRegular]) {
|
||||
if (allowRangeRequests) {
|
||||
response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange];
|
||||
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
|
||||
@@ -915,7 +1033,7 @@ static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData,
|
||||
while (1) {
|
||||
ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length);
|
||||
if (result < 0) {
|
||||
length = NSNotFound;
|
||||
length = NSUIntegerMax;
|
||||
break;
|
||||
} else if (result == 0) {
|
||||
break;
|
||||
@@ -925,7 +1043,7 @@ static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData,
|
||||
outData.length = 2 * outData.length;
|
||||
}
|
||||
}
|
||||
if (length != NSNotFound) {
|
||||
if (length != NSUIntegerMax) {
|
||||
outData.length = length;
|
||||
response = _CreateHTTPMessageFromData(outData, NO);
|
||||
} else {
|
||||
@@ -949,9 +1067,11 @@ static void _LogResult(NSString* format, ...) {
|
||||
}
|
||||
|
||||
- (NSInteger)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
NSArray* ignoredHeaders = @[@"Date", @"Etag"]; // Dates are always different by definition and ETags depend on file system node IDs
|
||||
NSInteger result = -1;
|
||||
if ([self startWithOptions:options]) {
|
||||
if ([self startWithOptions:options error:NULL]) {
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
|
||||
result = 0;
|
||||
NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
|
||||
@@ -1007,13 +1127,18 @@ static void _LogResult(NSString* format, ...) {
|
||||
}
|
||||
}
|
||||
|
||||
NSString* expectedContentLength = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(expectedResponse, CFSTR("Content-Length")));
|
||||
NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse));
|
||||
NSString* actualContentLength = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(actualResponse, CFSTR("Content-Length")));
|
||||
NSData* actualBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(actualResponse));
|
||||
if ([actualContentLength isEqualToString:expectedContentLength] && (actualBody.length > expectedBody.length)) { // Handle web browser closing connection before retrieving entire body (e.g. when playing a video file)
|
||||
actualBody = [actualBody subdataWithRange:NSMakeRange(0, expectedBody.length)];
|
||||
}
|
||||
if (![actualBody isEqualToData:expectedBody]) {
|
||||
_LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length);
|
||||
success = NO;
|
||||
#if !TARGET_OS_IPHONE
|
||||
#ifndef NDEBUG
|
||||
#if DEBUG
|
||||
if (GCDWebServerIsTextContentType([expectedHeaders objectForKey:@"Content-Type"])) {
|
||||
NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]];
|
||||
NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]];
|
||||
@@ -1049,9 +1174,12 @@ static void _LogResult(NSString* format, ...) {
|
||||
++result;
|
||||
}
|
||||
}
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
}
|
||||
|
||||
[self stop];
|
||||
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -117,6 +117,14 @@
|
||||
*/
|
||||
- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length;
|
||||
|
||||
/**
|
||||
* This method is called after the HTTP headers have been received to
|
||||
* allow replacing the request URL by another one.
|
||||
*
|
||||
* The default implementation returns the original URL.
|
||||
*/
|
||||
- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary*)headers;
|
||||
|
||||
/**
|
||||
* Assuming a valid HTTP request was received, this method is called before
|
||||
* the request is processed.
|
||||
@@ -130,13 +138,14 @@
|
||||
|
||||
/**
|
||||
* Assuming a valid HTTP request was received and -preflightRequest: returned nil,
|
||||
* this method is called to process the request.
|
||||
* this method is called to process the request by executing the handler's
|
||||
* process block.
|
||||
*/
|
||||
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block;
|
||||
- (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion;
|
||||
|
||||
/**
|
||||
* Assuming a valid HTTP request was received and either -preflightRequest:
|
||||
* or -processRequest:withBlock: returned a non-nil GCDWebServerResponse,
|
||||
* or -processRequest:completion: returned a non-nil GCDWebServerResponse,
|
||||
* this method is called to override the response.
|
||||
*
|
||||
* You can either modify the current response and return it, or return a
|
||||
|
||||
@@ -373,15 +373,24 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatRFC822([NSDate date]));
|
||||
}
|
||||
|
||||
- (void)_startProcessingRequest {
|
||||
DCHECK(_responseMessage == NULL);
|
||||
|
||||
GCDWebServerResponse* preflightResponse = [self preflightRequest:_request];
|
||||
if (preflightResponse) {
|
||||
[self _finishProcessingRequest:preflightResponse];
|
||||
} else {
|
||||
[self processRequest:_request completion:^(GCDWebServerResponse* processResponse) {
|
||||
[self _finishProcessingRequest:processResponse];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
- (void)_processRequest {
|
||||
- (void)_finishProcessingRequest:(GCDWebServerResponse*)response {
|
||||
DCHECK(_responseMessage == NULL);
|
||||
BOOL hasBody = NO;
|
||||
|
||||
GCDWebServerResponse* response = [self preflightRequest:_request];
|
||||
if (!response) {
|
||||
response = [self processRequest:_request withBlock:_handler.processBlock];
|
||||
}
|
||||
if (response) {
|
||||
response = [self overrideResponse:response forRequest:_request];
|
||||
}
|
||||
@@ -416,7 +425,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
if (_response.contentType != nil) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType));
|
||||
}
|
||||
if (_response.contentLength != NSNotFound) {
|
||||
if (_response.contentLength != NSUIntegerMax) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]);
|
||||
}
|
||||
if (_response.usesChunkedTransferEncoding) {
|
||||
@@ -471,7 +480,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
|
||||
NSError* localError = nil;
|
||||
if ([_request performClose:&localError]) {
|
||||
[self _processRequest];
|
||||
[self _startProcessingRequest];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
@@ -480,7 +489,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
}];
|
||||
} else {
|
||||
if ([_request performClose:&error]) {
|
||||
[self _processRequest];
|
||||
[self _startProcessingRequest];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
@@ -501,7 +510,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
|
||||
NSError* localError = nil;
|
||||
if ([_request performClose:&localError]) {
|
||||
[self _processRequest];
|
||||
[self _startProcessingRequest];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
@@ -522,11 +531,15 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
requestMethod = @"GET";
|
||||
_virtualHEAD = YES;
|
||||
}
|
||||
NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage)); // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones
|
||||
NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(_requestMessage));
|
||||
if (requestURL) {
|
||||
requestURL = [self rewriteRequestURL:requestURL withMethod:requestMethod headers:requestHeaders];
|
||||
DCHECK(requestURL);
|
||||
}
|
||||
NSString* requestPath = requestURL ? GCDWebServerUnescapeURLString(ARC_BRIDGE_RELEASE(CFURLCopyPath((CFURLRef)requestURL))) : nil; // Don't use -[NSURL path] which strips the ending slash
|
||||
NSString* queryString = requestURL ? ARC_BRIDGE_RELEASE(CFURLCopyQueryString((CFURLRef)requestURL, NULL)) : nil; // Don't use -[NSURL query] to make sure query is not unescaped;
|
||||
NSDictionary* requestQuery = queryString ? GCDWebServerParseURLEncodedForm(queryString) : @{};
|
||||
NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage)); // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones
|
||||
if (requestMethod && requestURL && requestHeaders && requestPath && requestQuery) {
|
||||
for (_handler in _server.handlers) {
|
||||
_request = ARC_RETAIN(_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery));
|
||||
@@ -568,7 +581,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_BadRequest];
|
||||
}
|
||||
} else {
|
||||
[self _processRequest];
|
||||
[self _startProcessingRequest];
|
||||
}
|
||||
} else {
|
||||
_request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery];
|
||||
@@ -713,6 +726,10 @@ static NSString* _StringFromAddressData(NSData* data) {
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary*)headers {
|
||||
return url;
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc2617
|
||||
- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request {
|
||||
LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
|
||||
@@ -764,16 +781,14 @@ static NSString* _StringFromAddressData(NSData* data) {
|
||||
return response;
|
||||
}
|
||||
|
||||
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block {
|
||||
- (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion {
|
||||
LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
|
||||
GCDWebServerResponse* response = nil;
|
||||
@try {
|
||||
response = block(request);
|
||||
_handler.asyncProcessBlock(request, completion);
|
||||
}
|
||||
@catch (NSException* exception) {
|
||||
LOG_EXCEPTION(exception);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
|
||||
|
||||
@@ -198,15 +198,19 @@ NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
|
||||
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
||||
|
||||
NSString* value = nil;
|
||||
if (![scanner scanUpToString:@"&" intoString:&value]) {
|
||||
break;
|
||||
[scanner scanUpToString:@"&" intoString:&value];
|
||||
if (value == nil) {
|
||||
value = @"";
|
||||
}
|
||||
|
||||
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||
NSString* unescapedKey = key ? GCDWebServerUnescapeURLString(key) : nil;
|
||||
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||
if (key && value) {
|
||||
[parameters setObject:GCDWebServerUnescapeURLString(value) forKey:GCDWebServerUnescapeURLString(key)];
|
||||
NSString* unescapedValue = value ? GCDWebServerUnescapeURLString(value) : nil;
|
||||
if (unescapedKey && unescapedValue) {
|
||||
[parameters setObject:unescapedValue forKey:unescapedKey];
|
||||
} else {
|
||||
LOG_WARNING(@"Failed parsing URL encoded form for key \"%@\" and value \"%@\"", key, value);
|
||||
DNOT_REACHED();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <TargetConditionals.h>
|
||||
#import <AvailabilityMacros.h>
|
||||
#import <os/object.h>
|
||||
|
||||
#if __has_feature(objc_arc)
|
||||
#define ARC_BRIDGE __bridge
|
||||
@@ -35,7 +34,7 @@
|
||||
#define ARC_RELEASE(__OBJECT__)
|
||||
#define ARC_AUTORELEASE(__OBJECT__) __OBJECT__
|
||||
#define ARC_DEALLOC(__OBJECT__)
|
||||
#if (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_6_0)) || (!TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_8))
|
||||
#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE
|
||||
#define ARC_DISPATCH_RETAIN(__OBJECT__)
|
||||
#define ARC_DISPATCH_RELEASE(__OBJECT__)
|
||||
#else
|
||||
@@ -85,13 +84,7 @@ extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_
|
||||
#define LOG_ERROR(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Error) GCDLogMessage(kGCDWebServerLogLevel_Error, __VA_ARGS__); } while (0)
|
||||
#define LOG_EXCEPTION(__EXCEPTION__) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Exception) GCDLogMessage(kGCDWebServerLogLevel_Exception, @"%@", __EXCEPTION__); } while (0)
|
||||
|
||||
#ifdef NDEBUG
|
||||
|
||||
#define DCHECK(__CONDITION__)
|
||||
#define DNOT_REACHED()
|
||||
#define LOG_DEBUG(...)
|
||||
|
||||
#else
|
||||
#if DEBUG
|
||||
|
||||
#define DCHECK(__CONDITION__) \
|
||||
do { \
|
||||
@@ -102,6 +95,12 @@ extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_
|
||||
#define DNOT_REACHED() abort()
|
||||
#define LOG_DEBUG(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Debug) GCDLogMessage(kGCDWebServerLogLevel_Debug, __VA_ARGS__); } while (0)
|
||||
|
||||
#else
|
||||
|
||||
#define DCHECK(__CONDITION__)
|
||||
#define DNOT_REACHED()
|
||||
#define LOG_DEBUG(...)
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -111,7 +110,11 @@ extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_
|
||||
#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
|
||||
|
||||
static inline BOOL GCDWebServerIsValidByteRange(NSRange range) {
|
||||
return ((range.location != NSNotFound) || (range.length > 0));
|
||||
return ((range.location != NSUIntegerMax) || (range.length > 0));
|
||||
}
|
||||
|
||||
static inline NSError* GCDWebServerMakePosixError(int code) {
|
||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithUTF8String:strerror(code)]}];
|
||||
}
|
||||
|
||||
extern void GCDWebServerInitializeFunctions();
|
||||
@@ -140,8 +143,7 @@ extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_F
|
||||
|
||||
@interface GCDWebServerHandler : NSObject
|
||||
@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock;
|
||||
@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock;
|
||||
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
||||
@property(nonatomic, readonly) GCDWebServerAsyncProcessBlock asyncProcessBlock;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerRequest ()
|
||||
@@ -150,6 +152,7 @@ extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_F
|
||||
- (BOOL)performOpen:(NSError**)error;
|
||||
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error;
|
||||
- (BOOL)performClose:(NSError**)error;
|
||||
- (void)setAttribute:(id)attribute forKey:(NSString*)key;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerResponse ()
|
||||
|
||||
@@ -27,6 +27,15 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
* Attribute key to retrieve an NSArray containing NSStrings from a GCDWebServerRequest
|
||||
* with the contents of any regular expression captures done on the request path.
|
||||
*
|
||||
* @warning This attribute will only be set on the request if adding a handler using
|
||||
* -addHandlerForMethod:pathRegex:requestClass:processBlock:.
|
||||
*/
|
||||
extern NSString* const GCDWebServerRequestAttribute_RegexCaptures;
|
||||
|
||||
/**
|
||||
* This protocol is used by the GCDWebServerConnection to communicate with
|
||||
* the GCDWebServerRequest and write the received HTTP body data.
|
||||
@@ -119,7 +128,7 @@
|
||||
* Returns the content length for the body of the request parsed from the
|
||||
* "Content-Length" header.
|
||||
*
|
||||
* This property will be set to "NSNotFound" if the request has no body or
|
||||
* This property will be set to "NSUIntegerMax" if the request has no body or
|
||||
* if there is a body but no "Content-Length" header, typically because
|
||||
* chunked transfer encoding is used.
|
||||
*/
|
||||
@@ -136,9 +145,9 @@
|
||||
@property(nonatomic, readonly) NSString* ifNoneMatch;
|
||||
|
||||
/**
|
||||
* Returns the parsed "Range" header or (NSNotFound, 0) if absent or malformed.
|
||||
* Returns the parsed "Range" header or (NSUIntegerMax, 0) if absent or malformed.
|
||||
* The range will be set to (offset, length) if expressed from the beginning
|
||||
* of the entity body, or (NSNotFound, -length) if expressed from its end.
|
||||
* of the entity body, or (NSUIntegerMax, length) if expressed from its end.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSRange byteRange;
|
||||
|
||||
@@ -163,4 +172,11 @@
|
||||
*/
|
||||
- (BOOL)hasByteRange;
|
||||
|
||||
/**
|
||||
* Retrieves an attribute associated with this request using the given key.
|
||||
*
|
||||
* @return The attribute value for the key.
|
||||
*/
|
||||
- (id)attributeForKey:(NSString*)key;
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerRequestAttribute_RegexCaptures";
|
||||
|
||||
#define kZlibErrorDomain @"ZlibErrorDomain"
|
||||
#define kGZipInitialBufferSize (256 * 1024)
|
||||
|
||||
@@ -152,6 +154,7 @@
|
||||
|
||||
BOOL _opened;
|
||||
NSMutableArray* _decoders;
|
||||
NSMutableDictionary* _attributes;
|
||||
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
|
||||
}
|
||||
@end
|
||||
@@ -187,14 +190,14 @@
|
||||
if (_type == nil) {
|
||||
_type = kGCDWebServerDefaultMimeType;
|
||||
}
|
||||
_length = NSNotFound;
|
||||
_length = NSUIntegerMax;
|
||||
} else {
|
||||
if (_type) {
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
_length = NSNotFound;
|
||||
_length = NSUIntegerMax;
|
||||
}
|
||||
|
||||
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
|
||||
@@ -203,7 +206,7 @@
|
||||
}
|
||||
_noneMatch = ARC_RETAIN([_headers objectForKey:@"If-None-Match"]);
|
||||
|
||||
_range = NSMakeRange(NSNotFound, 0);
|
||||
_range = NSMakeRange(NSUIntegerMax, 0);
|
||||
NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
|
||||
if (rangeHeader) {
|
||||
if ([rangeHeader hasPrefix:@"bytes="]) {
|
||||
@@ -222,13 +225,13 @@
|
||||
_range.location = startValue;
|
||||
_range.length = NSUIntegerMax;
|
||||
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
|
||||
_range.location = NSNotFound;
|
||||
_range.location = NSUIntegerMax;
|
||||
_range.length = endValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((_range.location == NSNotFound) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
|
||||
if ((_range.location == NSUIntegerMax) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
|
||||
LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
|
||||
}
|
||||
}
|
||||
@@ -238,6 +241,7 @@
|
||||
}
|
||||
|
||||
_decoders = [[NSMutableArray alloc] init];
|
||||
_attributes = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -252,6 +256,7 @@
|
||||
ARC_RELEASE(_modifiedSince);
|
||||
ARC_RELEASE(_noneMatch);
|
||||
ARC_RELEASE(_decoders);
|
||||
ARC_RELEASE(_attributes);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
@@ -264,6 +269,10 @@
|
||||
return GCDWebServerIsValidByteRange(_range);
|
||||
}
|
||||
|
||||
- (id)attributeForKey:(NSString*)key {
|
||||
return [_attributes objectForKey:key];
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
return YES;
|
||||
}
|
||||
@@ -307,6 +316,10 @@
|
||||
return [_writer close:error];
|
||||
}
|
||||
|
||||
- (void)setAttribute:(id)attribute forKey:(NSString*)key {
|
||||
[_attributes setValue:attribute forKey:key];
|
||||
}
|
||||
|
||||
- (NSString*)description {
|
||||
NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path];
|
||||
for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
|
||||
@@ -87,12 +87,12 @@
|
||||
|
||||
/**
|
||||
* Sets the content length for the body of the response. If a body is present
|
||||
* but this property is set to "NSNotFound", this means the length of the body
|
||||
* but this property is set to "NSUIntegerMax", this means the length of the body
|
||||
* cannot be known ahead of time. Chunked transfer encoding will be
|
||||
* automatically enabled by the GCDWebServerConnection to comply with HTTP/1.1
|
||||
* specifications.
|
||||
*
|
||||
* The default value is "NSNotFound" i.e. the response has no body or its length
|
||||
* The default value is "NSUIntegerMax" i.e. the response has no body or its length
|
||||
* is undefined.
|
||||
*/
|
||||
@property(nonatomic) NSUInteger contentLength;
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
||||
if ((self = [super initWithResponse:response reader:reader])) {
|
||||
response.contentLength = NSNotFound; // Make sure "Content-Length" header is not set since we don't know it
|
||||
response.contentLength = NSUIntegerMax; // Make sure "Content-Length" header is not set since we don't know it
|
||||
[response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
|
||||
}
|
||||
return self;
|
||||
@@ -180,7 +180,7 @@
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
_type = nil;
|
||||
_length = NSNotFound;
|
||||
_length = NSUIntegerMax;
|
||||
_status = kGCDWebServerHTTPStatusCode_OK;
|
||||
_maxAge = 0;
|
||||
_headers = [[NSMutableDictionary alloc] init];
|
||||
@@ -208,7 +208,7 @@
|
||||
}
|
||||
|
||||
- (BOOL)usesChunkedTransferEncoding {
|
||||
return (_type != nil) && (_length == NSNotFound);
|
||||
return (_type != nil) && (_length == NSUIntegerMax);
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
@@ -259,7 +259,7 @@
|
||||
if (_type) {
|
||||
[description appendFormat:@"\nContent Type = %@", _type];
|
||||
}
|
||||
if (_length != NSNotFound) {
|
||||
if (_length != NSUIntegerMax) {
|
||||
[description appendFormat:@"\nContent Length = %lu", (unsigned long)_length];
|
||||
}
|
||||
[description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge];
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
if (self.contentLength != NSNotFound) {
|
||||
if (self.contentLength != NSUIntegerMax) {
|
||||
_data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
|
||||
} else {
|
||||
_data = [[NSMutableData alloc] init];
|
||||
@@ -97,7 +97,8 @@
|
||||
|
||||
- (id)jsonObject {
|
||||
if (_jsonObject == nil) {
|
||||
if ([self.contentType isEqualToString:@"application/json"] || [self.contentType isEqualToString:@"text/json"] || [self.contentType isEqualToString:@"text/javascript"]) {
|
||||
NSString* mimeType = GCDWebServerTruncateHeaderValue(self.contentType);
|
||||
if ([mimeType isEqualToString:@"application/json"] || [mimeType isEqualToString:@"text/json"] || [mimeType isEqualToString:@"text/javascript"]) {
|
||||
_jsonObject = ARC_RETAIN([NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL]);
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
|
||||
@@ -34,10 +34,6 @@
|
||||
}
|
||||
@end
|
||||
|
||||
static inline NSError* _MakePosixError(int code) {
|
||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
|
||||
}
|
||||
|
||||
@implementation GCDWebServerFileRequest
|
||||
|
||||
@synthesize temporaryPath=_temporaryPath;
|
||||
@@ -59,7 +55,7 @@ static inline NSError* _MakePosixError(int code) {
|
||||
- (BOOL)open:(NSError**)error {
|
||||
_file = open([_temporaryPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
if (_file <= 0) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
@@ -67,7 +63,7 @@ static inline NSError* _MakePosixError(int code) {
|
||||
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||
if (write(_file, data.bytes, data.length) != (ssize_t)data.length) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
@@ -75,7 +71,7 @@ static inline NSError* _MakePosixError(int code) {
|
||||
|
||||
- (BOOL)close:(NSError**)error {
|
||||
if (close(_file) < 0) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return NO;
|
||||
}
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
*/
|
||||
@interface GCDWebServerMultiPart : NSObject
|
||||
|
||||
/**
|
||||
* Returns the control name retrieved from the part headers.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* controlName;
|
||||
|
||||
/**
|
||||
* Returns the content type retrieved from the part headers or "text/plain"
|
||||
* if not available (per HTTP specifications).
|
||||
@@ -100,13 +105,13 @@
|
||||
* Returns the argument parts from the multipart encoded form as
|
||||
* name / GCDWebServerMultiPartArgument pairs.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSDictionary* arguments;
|
||||
@property(nonatomic, readonly) NSArray* arguments;
|
||||
|
||||
/**
|
||||
* Returns the files parts from the multipart encoded form as
|
||||
* name / GCDWebServerMultiPartFile pairs.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSDictionary* files;
|
||||
@property(nonatomic, readonly) NSArray* files;
|
||||
|
||||
/**
|
||||
* Returns the MIME type for multipart encoded forms
|
||||
@@ -114,4 +119,14 @@
|
||||
*/
|
||||
+ (NSString*)mimeType;
|
||||
|
||||
/**
|
||||
* Returns the first argument for a given control name or nil if not found.
|
||||
*/
|
||||
- (GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name;
|
||||
|
||||
/**
|
||||
* Returns the first file for a given control name or nil if not found.
|
||||
*/
|
||||
- (GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name;
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,13 +29,19 @@
|
||||
|
||||
#define kMultiPartBufferSize (256 * 1024)
|
||||
|
||||
enum {
|
||||
typedef enum {
|
||||
kParserState_Undefined = 0,
|
||||
kParserState_Start,
|
||||
kParserState_Headers,
|
||||
kParserState_Content,
|
||||
kParserState_End
|
||||
};
|
||||
} ParserState;
|
||||
|
||||
@interface GCDWebServerMIMEStreamParser : NSObject
|
||||
- (id)initWithBoundary:(NSString*)boundary defaultControlName:(NSString*)name arguments:(NSMutableArray*)arguments files:(NSMutableArray*)files;
|
||||
- (BOOL)appendBytes:(const void*)bytes length:(NSUInteger)length;
|
||||
- (BOOL)isAtEnd;
|
||||
@end
|
||||
|
||||
static NSData* _newlineData = nil;
|
||||
static NSData* _newlinesData = nil;
|
||||
@@ -43,6 +49,7 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@interface GCDWebServerMultiPart () {
|
||||
@private
|
||||
NSString* _controlName;
|
||||
NSString* _contentType;
|
||||
NSString* _mimeType;
|
||||
}
|
||||
@@ -50,17 +57,19 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@implementation GCDWebServerMultiPart
|
||||
|
||||
@synthesize contentType=_contentType, mimeType=_mimeType;
|
||||
@synthesize controlName=_controlName, contentType=_contentType, mimeType=_mimeType;
|
||||
|
||||
- (id)initWithContentType:(NSString*)contentType {
|
||||
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type {
|
||||
if ((self = [super init])) {
|
||||
_contentType = [contentType copy];
|
||||
_controlName = [name copy];
|
||||
_contentType = [type copy];
|
||||
_mimeType = ARC_RETAIN(GCDWebServerTruncateHeaderValue(_contentType));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_controlName);
|
||||
ARC_RELEASE(_contentType);
|
||||
ARC_RELEASE(_mimeType);
|
||||
|
||||
@@ -80,8 +89,8 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@synthesize data=_data, string=_string;
|
||||
|
||||
- (id)initWithContentType:(NSString*)contentType data:(NSData*)data {
|
||||
if ((self = [super initWithContentType:contentType])) {
|
||||
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type data:(NSData*)data {
|
||||
if ((self = [super initWithControlName:name contentType:type])) {
|
||||
_data = ARC_RETAIN(data);
|
||||
|
||||
if ([self.contentType hasPrefix:@"text/"]) {
|
||||
@@ -116,8 +125,8 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@synthesize fileName=_fileName, temporaryPath=_temporaryPath;
|
||||
|
||||
- (id)initWithContentType:(NSString*)contentType fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath {
|
||||
if ((self = [super initWithContentType:contentType])) {
|
||||
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath {
|
||||
if ((self = [super initWithControlName:name contentType:type])) {
|
||||
_fileName = [fileName copy];
|
||||
_temporaryPath = [temporaryPath copy];
|
||||
}
|
||||
@@ -139,26 +148,25 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerMultiPartFormRequest () {
|
||||
@interface GCDWebServerMIMEStreamParser () {
|
||||
@private
|
||||
NSData* _boundary;
|
||||
NSString* _defaultcontrolName;
|
||||
ParserState _state;
|
||||
NSMutableData* _data;
|
||||
NSMutableArray* _arguments;
|
||||
NSMutableArray* _files;
|
||||
|
||||
NSUInteger _parserState;
|
||||
NSMutableData* _parserData;
|
||||
NSString* _controlName;
|
||||
NSString* _fileName;
|
||||
NSString* _contentType;
|
||||
NSString* _tmpPath;
|
||||
int _tmpFile;
|
||||
|
||||
NSMutableDictionary* _arguments;
|
||||
NSMutableDictionary* _files;
|
||||
GCDWebServerMIMEStreamParser* _subParser;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerMultiPartFormRequest
|
||||
|
||||
@synthesize arguments=_arguments, files=_files;
|
||||
@implementation GCDWebServerMIMEStreamParser
|
||||
|
||||
+ (void)initialize {
|
||||
if (_newlineData == nil) {
|
||||
@@ -175,49 +183,50 @@ static NSData* _dashNewlineData = nil;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString*)mimeType {
|
||||
return @"multipart/form-data";
|
||||
}
|
||||
|
||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
||||
if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
|
||||
NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
|
||||
if (boundary) {
|
||||
NSData* data = [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding];
|
||||
_boundary = ARC_RETAIN(data);
|
||||
}
|
||||
if (_boundary == nil) {
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
|
||||
_arguments = [[NSMutableDictionary alloc] init];
|
||||
_files = [[NSMutableDictionary alloc] init];
|
||||
- (id)initWithBoundary:(NSString*)boundary defaultControlName:(NSString*)name arguments:(NSMutableArray*)arguments files:(NSMutableArray*)files {
|
||||
NSData* data = boundary.length ? [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding] : nil;
|
||||
if (data == nil) {
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
if ((self = [super init])) {
|
||||
_boundary = ARC_RETAIN(data);
|
||||
_defaultcontrolName = ARC_RETAIN(name);
|
||||
_arguments = ARC_RETAIN(arguments);
|
||||
_files = ARC_RETAIN(files);
|
||||
_data = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize];
|
||||
_state = kParserState_Start;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_boundary);
|
||||
ARC_RELEASE(_defaultcontrolName);
|
||||
ARC_RELEASE(_data);
|
||||
ARC_RELEASE(_arguments);
|
||||
ARC_RELEASE(_files);
|
||||
ARC_RELEASE(_boundary);
|
||||
|
||||
ARC_RELEASE(_controlName);
|
||||
ARC_RELEASE(_fileName);
|
||||
ARC_RELEASE(_contentType);
|
||||
if (_tmpFile > 0) {
|
||||
close(_tmpFile);
|
||||
unlink([_tmpPath fileSystemRepresentation]);
|
||||
}
|
||||
ARC_RELEASE(_tmpPath);
|
||||
ARC_RELEASE(_subParser);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
_parserData = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize];
|
||||
_parserState = kParserState_Start;
|
||||
return YES;
|
||||
}
|
||||
|
||||
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
|
||||
- (BOOL)_parseData {
|
||||
BOOL success = YES;
|
||||
|
||||
if (_parserState == kParserState_Headers) {
|
||||
NSRange range = [_parserData rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _parserData.length)];
|
||||
if (_state == kParserState_Headers) {
|
||||
NSRange range = [_data rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _data.length)];
|
||||
if (range.location != NSNotFound) {
|
||||
|
||||
ARC_RELEASE(_controlName);
|
||||
@@ -228,7 +237,9 @@ static NSData* _dashNewlineData = nil;
|
||||
_contentType = nil;
|
||||
ARC_RELEASE(_tmpPath);
|
||||
_tmpPath = nil;
|
||||
NSString* headers = [[NSString alloc] initWithData:[_parserData subdataWithRange:NSMakeRange(0, range.location)] encoding:NSUTF8StringEncoding];
|
||||
ARC_RELEASE(_subParser);
|
||||
_subParser = nil;
|
||||
NSString* headers = [[NSString alloc] initWithData:[_data subdataWithRange:NSMakeRange(0, range.location)] encoding:NSUTF8StringEncoding];
|
||||
if (headers) {
|
||||
for (NSString* header in [headers componentsSeparatedByString:@"\r\n"]) {
|
||||
NSRange subRange = [header rangeOfString:@":"];
|
||||
@@ -242,6 +253,9 @@ static NSData* _dashNewlineData = nil;
|
||||
if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) {
|
||||
_controlName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"name"));
|
||||
_fileName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename"));
|
||||
} else if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"file"]) {
|
||||
_controlName = ARC_RETAIN(_defaultcontrolName);
|
||||
_fileName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -257,7 +271,14 @@ static NSData* _dashNewlineData = nil;
|
||||
DNOT_REACHED();
|
||||
}
|
||||
if (_controlName) {
|
||||
if (_fileName) {
|
||||
if ([GCDWebServerTruncateHeaderValue(_contentType) isEqualToString:@"multipart/mixed"]) {
|
||||
NSString* boundary = GCDWebServerExtractHeaderValueParameter(_contentType, @"boundary");
|
||||
_subParser = [[GCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:_controlName arguments:_arguments files:_files];
|
||||
if (_subParser == nil) {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
} else if (_fileName) {
|
||||
NSString* path = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
|
||||
_tmpFile = open([path fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
if (_tmpFile > 0) {
|
||||
@@ -272,29 +293,36 @@ static NSData* _dashNewlineData = nil;
|
||||
success = NO;
|
||||
}
|
||||
|
||||
[_parserData replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0];
|
||||
_parserState = kParserState_Content;
|
||||
[_data replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0];
|
||||
_state = kParserState_Content;
|
||||
}
|
||||
}
|
||||
|
||||
if ((_parserState == kParserState_Start) || (_parserState == kParserState_Content)) {
|
||||
NSRange range = [_parserData rangeOfData:_boundary options:0 range:NSMakeRange(0, _parserData.length)];
|
||||
if ((_state == kParserState_Start) || (_state == kParserState_Content)) {
|
||||
NSRange range = [_data rangeOfData:_boundary options:0 range:NSMakeRange(0, _data.length)];
|
||||
if (range.location != NSNotFound) {
|
||||
NSRange subRange = NSMakeRange(range.location + range.length, _parserData.length - range.location - range.length);
|
||||
NSRange subRange1 = [_parserData rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange];
|
||||
NSRange subRange2 = [_parserData rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange];
|
||||
NSRange subRange = NSMakeRange(range.location + range.length, _data.length - range.location - range.length);
|
||||
NSRange subRange1 = [_data rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange];
|
||||
NSRange subRange2 = [_data rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange];
|
||||
if ((subRange1.location != NSNotFound) || (subRange2.location != NSNotFound)) {
|
||||
|
||||
if (_parserState == kParserState_Content) {
|
||||
const void* dataBytes = _parserData.bytes;
|
||||
if (_state == kParserState_Content) {
|
||||
const void* dataBytes = _data.bytes;
|
||||
NSUInteger dataLength = range.location - 2;
|
||||
if (_tmpPath) {
|
||||
if (_subParser) {
|
||||
if (![_subParser appendBytes:dataBytes length:(dataLength + 2)] || ![_subParser isAtEnd]) {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
ARC_RELEASE(_subParser);
|
||||
_subParser = nil;
|
||||
} else if (_tmpPath) {
|
||||
ssize_t result = write(_tmpFile, dataBytes, dataLength);
|
||||
if (result == (ssize_t)dataLength) {
|
||||
if (close(_tmpFile) == 0) {
|
||||
_tmpFile = 0;
|
||||
GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithContentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
|
||||
[_files setObject:file forKey:_controlName];
|
||||
GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithControlName:_controlName contentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
|
||||
[_files addObject:file];
|
||||
ARC_RELEASE(file);
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
@@ -308,85 +336,150 @@ static NSData* _dashNewlineData = nil;
|
||||
_tmpPath = nil;
|
||||
} else {
|
||||
NSData* data = [[NSData alloc] initWithBytes:(void*)dataBytes length:dataLength];
|
||||
GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithContentType:_contentType data:data];
|
||||
[_arguments setObject:argument forKey:_controlName];
|
||||
GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithControlName:_controlName contentType:_contentType data:data];
|
||||
[_arguments addObject:argument];
|
||||
ARC_RELEASE(argument);
|
||||
ARC_RELEASE(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (subRange1.location != NSNotFound) {
|
||||
[_parserData replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0];
|
||||
_parserState = kParserState_Headers;
|
||||
[_data replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0];
|
||||
_state = kParserState_Headers;
|
||||
success = [self _parseData];
|
||||
} else {
|
||||
_parserState = kParserState_End;
|
||||
_state = kParserState_End;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSUInteger margin = 2 * _boundary.length;
|
||||
if (_tmpPath && (_parserData.length > margin)) {
|
||||
NSUInteger length = _parserData.length - margin;
|
||||
ssize_t result = write(_tmpFile, _parserData.bytes, length);
|
||||
if (result == (ssize_t)length) {
|
||||
[_parserData replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
if (_data.length > margin) {
|
||||
NSUInteger length = _data.length - margin;
|
||||
if (_subParser) {
|
||||
if ([_subParser appendBytes:_data.bytes length:length]) {
|
||||
[_data replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
} else if (_tmpPath) {
|
||||
ssize_t result = write(_tmpFile, _data.bytes, length);
|
||||
if (result == (ssize_t)length) {
|
||||
[_data replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)appendBytes:(const void*)bytes length:(NSUInteger)length {
|
||||
[_data appendBytes:bytes length:length];
|
||||
return [self _parseData];
|
||||
}
|
||||
|
||||
- (BOOL)isAtEnd {
|
||||
return (_state == kParserState_End);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerMultiPartFormRequest () {
|
||||
@private
|
||||
GCDWebServerMIMEStreamParser* _parser;
|
||||
NSMutableArray* _arguments;
|
||||
NSMutableArray* _files;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerMultiPartFormRequest
|
||||
|
||||
@synthesize arguments=_arguments, files=_files;
|
||||
|
||||
+ (NSString*)mimeType {
|
||||
return @"multipart/form-data";
|
||||
}
|
||||
|
||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
||||
if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
|
||||
_arguments = [[NSMutableArray alloc] init];
|
||||
_files = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_arguments);
|
||||
ARC_RELEASE(_files);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
|
||||
_parser = [[GCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:nil arguments:_arguments files:_files];
|
||||
if (_parser == nil) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed starting to parse multipart form data"}];
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||
[_parserData appendBytes:data.bytes length:data.length];
|
||||
if (![self _parseData]) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}];
|
||||
if (![_parser appendBytes:data.bytes length:data.length]) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed continuing to parse multipart form data"}];
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)close:(NSError**)error {
|
||||
ARC_RELEASE(_parserData);
|
||||
_parserData = nil;
|
||||
ARC_RELEASE(_controlName);
|
||||
_controlName = nil;
|
||||
ARC_RELEASE(_fileName);
|
||||
_fileName = nil;
|
||||
ARC_RELEASE(_contentType);
|
||||
_contentType = nil;
|
||||
if (_tmpFile > 0) {
|
||||
close(_tmpFile);
|
||||
unlink([_tmpPath fileSystemRepresentation]);
|
||||
_tmpFile = 0;
|
||||
}
|
||||
ARC_RELEASE(_tmpPath);
|
||||
_tmpPath = nil;
|
||||
if (_parserState != kParserState_End) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}];
|
||||
BOOL atEnd = [_parser isAtEnd];
|
||||
ARC_RELEASE(_parser);
|
||||
_parser = nil;
|
||||
if (!atEnd) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed finishing to parse multipart form data"}];
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name {
|
||||
for (GCDWebServerMultiPartArgument* argument in _arguments) {
|
||||
if ([argument.controlName isEqualToString:name]) {
|
||||
return argument;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name {
|
||||
for (GCDWebServerMultiPartFile* file in _files) {
|
||||
if ([file.controlName isEqualToString:name]) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString*)description {
|
||||
NSMutableString* description = [NSMutableString stringWithString:[super description]];
|
||||
if (_arguments.count) {
|
||||
[description appendString:@"\n"];
|
||||
for (NSString* key in [[_arguments allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
GCDWebServerMultiPartArgument* argument = [_arguments objectForKey:key];
|
||||
[description appendFormat:@"\n%@ (%@)\n", key, argument.contentType];
|
||||
for (GCDWebServerMultiPartArgument* argument in _arguments) {
|
||||
[description appendFormat:@"\n%@ (%@)\n", argument.controlName, argument.contentType];
|
||||
[description appendString:GCDWebServerDescribeData(argument.data, argument.contentType)];
|
||||
}
|
||||
}
|
||||
if (_files.count) {
|
||||
[description appendString:@"\n"];
|
||||
for (NSString* key in [[_files allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
GCDWebServerMultiPartFile* file = [_files objectForKey:key];
|
||||
[description appendFormat:@"\n%@ (%@): %@\n{%@}", key, file.contentType, file.fileName, file.temporaryPath];
|
||||
for (GCDWebServerMultiPartFile* file in _files) {
|
||||
[description appendFormat:@"\n%@ (%@): %@\n{%@}", file.controlName, file.contentType, file.fileName, file.temporaryPath];
|
||||
}
|
||||
}
|
||||
return description;
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest
|
||||
|
||||
/**
|
||||
* Returns the unescaped names and values for the URL encoded form.
|
||||
* Returns the unescaped control names and values for the URL encoded form.
|
||||
*
|
||||
* The text encoding used to interpret the data is extracted from the
|
||||
* "Content-Type" header or defaults to UTF-8.
|
||||
|
||||
@@ -77,9 +77,9 @@
|
||||
|
||||
/**
|
||||
* Initializes a response like -initWithFile: but restricts the file contents
|
||||
* to a specific byte range. This range should be set to (NSNotFound, 0) for
|
||||
* to a specific byte range. This range should be set to (NSUIntegerMax, 0) for
|
||||
* the full file, (offset, length) if expressed from the beginning of the file,
|
||||
* or (NSNotFound, -length) if expressed from the end of the file. The "offset"
|
||||
* or (NSUIntegerMax, length) if expressed from the end of the file. The "offset"
|
||||
* and "length" values will be automatically adjusted to be compatible with the
|
||||
* actual size of the file.
|
||||
*
|
||||
|
||||
@@ -40,10 +40,6 @@
|
||||
}
|
||||
@end
|
||||
|
||||
static inline NSError* _MakePosixError(int code) {
|
||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
|
||||
}
|
||||
|
||||
@implementation GCDWebServerFileResponse
|
||||
|
||||
+ (instancetype)responseWithFile:(NSString*)path {
|
||||
@@ -63,11 +59,11 @@ static inline NSError* _MakePosixError(int code) {
|
||||
}
|
||||
|
||||
- (instancetype)initWithFile:(NSString*)path {
|
||||
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:NO];
|
||||
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:NO];
|
||||
}
|
||||
|
||||
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
||||
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:attachment];
|
||||
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:attachment];
|
||||
}
|
||||
|
||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range {
|
||||
@@ -85,31 +81,41 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
if (GCDWebServerIsValidByteRange(range)) {
|
||||
if (range.location != NSNotFound) {
|
||||
range.location = MIN(range.location, (NSUInteger)info.st_size);
|
||||
range.length = MIN(range.length, (NSUInteger)info.st_size - range.location);
|
||||
#ifndef __LP64__
|
||||
if (info.st_size >= (off_t)4294967295) { // In 32 bit mode, we can't handle files greater than 4 GiBs (don't use "NSUIntegerMax" here to avoid potential unsigned to signed conversion issues)
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
NSUInteger fileSize = (NSUInteger)info.st_size;
|
||||
|
||||
BOOL hasByteRange = GCDWebServerIsValidByteRange(range);
|
||||
if (hasByteRange) {
|
||||
if (range.location != NSUIntegerMax) {
|
||||
range.location = MIN(range.location, fileSize);
|
||||
range.length = MIN(range.length, fileSize - range.location);
|
||||
} else {
|
||||
range.length = MIN(range.length, (NSUInteger)info.st_size);
|
||||
range.location = (NSUInteger)info.st_size - range.length;
|
||||
range.length = MIN(range.length, fileSize);
|
||||
range.location = fileSize - range.length;
|
||||
}
|
||||
if (range.length == 0) {
|
||||
ARC_RELEASE(self);
|
||||
return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
|
||||
}
|
||||
} else {
|
||||
range.location = 0;
|
||||
range.length = fileSize;
|
||||
}
|
||||
|
||||
if ((self = [super init])) {
|
||||
_path = [path copy];
|
||||
if (range.location != NSNotFound) {
|
||||
_offset = range.location;
|
||||
_size = range.length;
|
||||
_offset = range.location;
|
||||
_size = range.length;
|
||||
if (hasByteRange) {
|
||||
[self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent];
|
||||
[self setValue:[NSString stringWithFormat:@"bytes %i-%i/%i", (int)range.location, (int)(range.location + range.length - 1), (int)info.st_size] forAdditionalHeader:@"Content-Range"];
|
||||
LOG_DEBUG(@"Using content bytes range [%i-%i] for file \"%@\"", (int)range.location, (int)(range.location + range.length - 1), path);
|
||||
} else {
|
||||
_offset = 0;
|
||||
_size = (NSUInteger)info.st_size;
|
||||
[self setValue:[NSString stringWithFormat:@"bytes %lu-%lu/%lu", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), (unsigned long)fileSize] forAdditionalHeader:@"Content-Range"];
|
||||
LOG_DEBUG(@"Using content bytes range [%lu-%lu] for file \"%@\"", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), path);
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
@@ -125,8 +131,8 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
||||
}
|
||||
}
|
||||
|
||||
self.contentType = GCDWebServerGetMimeTypeForExtension([path pathExtension]);
|
||||
self.contentLength = (range.location != NSNotFound ? range.length : (NSUInteger)info.st_size);
|
||||
self.contentType = GCDWebServerGetMimeTypeForExtension([_path pathExtension]);
|
||||
self.contentLength = _size;
|
||||
self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec);
|
||||
self.eTag = [NSString stringWithFormat:@"%llu/%li/%li", info.st_ino, info.st_mtimespec.tv_sec, info.st_mtimespec.tv_nsec];
|
||||
}
|
||||
@@ -142,11 +148,11 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
||||
- (BOOL)open:(NSError**)error {
|
||||
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
|
||||
if (_file <= 0) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return NO;
|
||||
}
|
||||
if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
close(_file);
|
||||
return NO;
|
||||
}
|
||||
@@ -158,7 +164,7 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
||||
NSMutableData* data = [[NSMutableData alloc] initWithLength:length];
|
||||
ssize_t result = read(_file, data.mutableBytes, length);
|
||||
if (result < 0) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return nil;
|
||||
}
|
||||
if (result > 0) {
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
#import "GCDWebServerStreamedResponse.h"
|
||||
|
||||
/**
|
||||
* The GCDWebServerStreamingBlock is called to stream the data for the HTTP body.
|
||||
* The GCDWebServerStreamBlock is called to stream the data for the HTTP body.
|
||||
* The block must return empty NSData when done or nil on error and set the
|
||||
* "error" argument which is guaranteed to be non-NULL.
|
||||
*/
|
||||
typedef NSData* (^GCDWebServerStreamingBlock)(NSError** error);
|
||||
typedef NSData* (^GCDWebServerStreamBlock)(NSError** error);
|
||||
|
||||
/**
|
||||
* The GCDWebServerStreamedResponse subclass of GCDWebServerResponse streams
|
||||
@@ -43,11 +43,11 @@ typedef NSData* (^GCDWebServerStreamingBlock)(NSError** error);
|
||||
/**
|
||||
* Creates a response with streamed data and a given content type.
|
||||
*/
|
||||
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamingBlock)block;
|
||||
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamingBlock)block;
|
||||
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,17 +29,17 @@
|
||||
|
||||
@interface GCDWebServerStreamedResponse () {
|
||||
@private
|
||||
GCDWebServerStreamingBlock _block;
|
||||
GCDWebServerStreamBlock _block;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerStreamedResponse
|
||||
|
||||
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamingBlock)block {
|
||||
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
|
||||
return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]);
|
||||
}
|
||||
|
||||
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamingBlock)block {
|
||||
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
|
||||
if ((self = [super init])) {
|
||||
_block = [block copy];
|
||||
|
||||
|
||||
@@ -57,6 +57,11 @@
|
||||
|
||||
@implementation GCDWebUploader (Methods)
|
||||
|
||||
// Must match implementation in GCDWebDAVServer
|
||||
- (BOOL)_checkSandboxedPath:(NSString*)path {
|
||||
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
|
||||
}
|
||||
|
||||
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
||||
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
||||
return NO;
|
||||
@@ -85,8 +90,8 @@
|
||||
- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request {
|
||||
NSString* relativePath = [[request query] objectForKey:@"path"];
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory;
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
BOOL isDirectory = NO;
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
if (!isDirectory) {
|
||||
@@ -129,8 +134,8 @@
|
||||
- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request {
|
||||
NSString* relativePath = [[request query] objectForKey:@"path"];
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory;
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
BOOL isDirectory = NO;
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
if (isDirectory) {
|
||||
@@ -154,12 +159,15 @@
|
||||
NSRange range = [[request.headers objectForKey:@"Accept"] rangeOfString:@"application/json" options:NSCaseInsensitiveSearch];
|
||||
NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8"); // Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup)
|
||||
|
||||
GCDWebServerMultiPartFile* file = [request.files objectForKey:@"files[]"];
|
||||
GCDWebServerMultiPartFile* file = [request firstFileForControlName:@"files[]"];
|
||||
if ((!_allowHidden && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName];
|
||||
}
|
||||
NSString* relativePath = [(GCDWebServerMultiPartArgument*)[request.arguments objectForKey:@"path"] string];
|
||||
NSString* relativePath = [[request firstArgumentForControlName:@"path"] string];
|
||||
NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]];
|
||||
if (![self _checkSandboxedPath:absolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath];
|
||||
@@ -181,13 +189,16 @@
|
||||
- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request {
|
||||
NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"];
|
||||
NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath];
|
||||
BOOL isDirectory;
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
|
||||
BOOL isDirectory = NO;
|
||||
if (![self _checkSandboxedPath:oldAbsolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
|
||||
}
|
||||
|
||||
NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
|
||||
NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]];
|
||||
if (![self _checkSandboxedPath:newAbsolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", newRelativePath];
|
||||
}
|
||||
|
||||
NSString* itemName = [newAbsolutePath lastPathComponent];
|
||||
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
@@ -215,7 +226,7 @@
|
||||
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
@@ -244,6 +255,9 @@
|
||||
- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request {
|
||||
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
||||
NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]];
|
||||
if (![self _checkSandboxedPath:absolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
NSString* directoryName = [absolutePath lastPathComponent];
|
||||
if (!_allowHidden && [directoryName hasPrefix:@"."]) {
|
||||
@@ -308,6 +322,9 @@
|
||||
NSString* title = server.title;
|
||||
if (title == nil) {
|
||||
title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
||||
if (title == nil) {
|
||||
title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
|
||||
}
|
||||
#if !TARGET_OS_IPHONE
|
||||
if (title == nil) {
|
||||
title = [[NSProcessInfo processInfo] processName];
|
||||
|
||||
81
Mac/main.m
81
Mac/main.m
@@ -31,6 +31,7 @@
|
||||
|
||||
#import "GCDWebServerDataRequest.h"
|
||||
#import "GCDWebServerURLEncodedFormRequest.h"
|
||||
#import "GCDWebServerMultiPartFormRequest.h"
|
||||
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
#import "GCDWebServerStreamedResponse.h"
|
||||
@@ -47,9 +48,11 @@ typedef enum {
|
||||
kMode_WebServer = 0,
|
||||
kMode_HTMLPage,
|
||||
kMode_HTMLForm,
|
||||
kMode_HTMLFileUpload,
|
||||
kMode_WebDAV,
|
||||
kMode_WebUploader,
|
||||
kMode_StreamingResponse
|
||||
kMode_StreamingResponse,
|
||||
kMode_AsyncResponse
|
||||
} Mode;
|
||||
|
||||
@interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate>
|
||||
@@ -140,7 +143,7 @@ int main(int argc, const char* argv[]) {
|
||||
NSString* authenticationPassword = nil;
|
||||
|
||||
if (argc == 1) {
|
||||
fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password]\n\n", basename((char*)argv[0]));
|
||||
fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | htmlFileUpload | webDAV | webUploader | streamingResponse | asyncResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password]\n\n", basename((char*)argv[0]));
|
||||
} else {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (argv[i][0] != '-') {
|
||||
@@ -154,12 +157,16 @@ int main(int argc, const char* argv[]) {
|
||||
mode = kMode_HTMLPage;
|
||||
} else if (!strcmp(argv[i], "htmlForm")) {
|
||||
mode = kMode_HTMLForm;
|
||||
} else if (!strcmp(argv[i], "htmlFileUpload")) {
|
||||
mode = kMode_HTMLFileUpload;
|
||||
} else if (!strcmp(argv[i], "webDAV")) {
|
||||
mode = kMode_WebDAV;
|
||||
} else if (!strcmp(argv[i], "webUploader")) {
|
||||
mode = kMode_WebUploader;
|
||||
} else if (!strcmp(argv[i], "streamingResponse")) {
|
||||
mode = kMode_StreamingResponse;
|
||||
} else if (!strcmp(argv[i], "asyncResponse")) {
|
||||
mode = kMode_AsyncResponse;
|
||||
}
|
||||
} else if (!strcmp(argv[i], "-record")) {
|
||||
recording = YES;
|
||||
@@ -243,6 +250,48 @@ int main(int argc, const char* argv[]) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Implements HTML file upload
|
||||
case kMode_HTMLFileUpload: {
|
||||
fprintf(stdout, "Running in HTML File Upload mode");
|
||||
webServer = [[GCDWebServer alloc] init];
|
||||
NSString* formHTML = @" \
|
||||
<form name=\"input\" action=\"/\" method=\"post\" enctype=\"multipart/form-data\"> \
|
||||
<input type=\"hidden\" name=\"secret\" value=\"42\"> \
|
||||
<input type=\"file\" name=\"files\" multiple><br/> \
|
||||
<input type=\"submit\" value=\"Submit\"> \
|
||||
</form> \
|
||||
";
|
||||
[webServer addHandlerForMethod:@"GET"
|
||||
path:@"/"
|
||||
requestClass:[GCDWebServerRequest class]
|
||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
NSString* html = [NSString stringWithFormat:@"<html><body>%@</body></html>", formHTML];
|
||||
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||
|
||||
}];
|
||||
[webServer addHandlerForMethod:@"POST"
|
||||
path:@"/"
|
||||
requestClass:[GCDWebServerMultiPartFormRequest class]
|
||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
NSMutableString* string = [NSMutableString string];
|
||||
for (GCDWebServerMultiPartArgument* argument in [(GCDWebServerMultiPartFormRequest*)request arguments]) {
|
||||
[string appendFormat:@"%@ = %@<br>", argument.controlName, argument.string];
|
||||
}
|
||||
for (GCDWebServerMultiPartFile* file in [(GCDWebServerMultiPartFormRequest*)request files]) {
|
||||
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:file.temporaryPath error:NULL];
|
||||
[string appendFormat:@"%@ = "%@" (%@ | %llu %@)<br>", file.controlName, file.fileName, file.mimeType,
|
||||
attributes.fileSize >= 1000 ? attributes.fileSize / 1000 : attributes.fileSize,
|
||||
attributes.fileSize >= 1000 ? @"KB" : @"Bytes"];
|
||||
};
|
||||
NSString* html = [NSString stringWithFormat:@"<html><body><p>%@</p><hr>%@</body></html>", string, formHTML];
|
||||
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||
|
||||
}];
|
||||
break;
|
||||
}
|
||||
|
||||
// Serve home directory through WebDAV
|
||||
case kMode_WebDAV: {
|
||||
fprintf(stdout, "Running in WebDAV mode from \"%s\"", [rootDirectory UTF8String]);
|
||||
@@ -282,6 +331,24 @@ int main(int argc, const char* argv[]) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Test async responses
|
||||
case kMode_AsyncResponse: {
|
||||
fprintf(stdout, "Running in Async Response mode");
|
||||
webServer = [[GCDWebServer alloc] init];
|
||||
[webServer addHandlerForMethod:@"GET"
|
||||
path:@"/"
|
||||
requestClass:[GCDWebServerRequest class]
|
||||
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[@"Hello World!" dataUsingEncoding:NSUTF8StringEncoding] contentType:@"text/plain"];
|
||||
completionBlock(response);
|
||||
});
|
||||
|
||||
}];
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
#if __has_feature(objc_arc)
|
||||
fprintf(stdout, " (ARC is ON)\n");
|
||||
@@ -291,11 +358,14 @@ int main(int argc, const char* argv[]) {
|
||||
|
||||
if (webServer) {
|
||||
Delegate* delegate = [[Delegate alloc] init];
|
||||
webServer.delegate = delegate;
|
||||
if (testDirectory) {
|
||||
#if DEBUG
|
||||
webServer.delegate = delegate;
|
||||
#endif
|
||||
fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]);
|
||||
result = (int)[webServer runTestsWithOptions:@{GCDWebServerOption_Port: @8080} inDirectory:testDirectory];
|
||||
} else {
|
||||
webServer.delegate = delegate;
|
||||
if (recording) {
|
||||
fprintf(stdout, "<RECORDING ENABLED>\n");
|
||||
webServer.recordingEnabled = YES;
|
||||
@@ -313,13 +383,14 @@ int main(int argc, const char* argv[]) {
|
||||
[options setObject:GCDWebServerAuthenticationMethod_DigestAccess forKey:GCDWebServerOption_AuthenticationMethod];
|
||||
}
|
||||
}
|
||||
if ([webServer runWithOptions:options]) {
|
||||
if ([webServer runWithOptions:options error:NULL]) {
|
||||
result = 0;
|
||||
}
|
||||
}
|
||||
webServer.delegate = nil;
|
||||
#if !__has_feature(objc_arc)
|
||||
[webServer release];
|
||||
[delegate release];
|
||||
[webServer release];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
119
README.md
119
README.md
@@ -2,6 +2,8 @@ Overview
|
||||
========
|
||||
|
||||
[](https://travis-ci.org/swisspol/GCDWebServer)
|
||||
[](http://cocoadocs.org/docsets/GCDWebServer)
|
||||
[](http://cocoadocs.org/docsets/GCDWebServer)
|
||||
|
||||
GCDWebServer is a modern and lightweight GCD based HTTP 1.1 server designed to be embedded in OS X & iOS apps. It was written from scratch with the following goals in mind:
|
||||
* Elegant and easy to use architecture with only 4 core classes: server, connection, request and response (see "Understanding GCDWebServer's Architecture" below)
|
||||
@@ -11,6 +13,7 @@ GCDWebServer is a modern and lightweight GCD based HTTP 1.1 server designed to b
|
||||
* Available under a friendly [New BSD License](LICENSE)
|
||||
|
||||
Extra built-in features:
|
||||
* Allow implementation of fully asynchronous handlers of incoming HTTP requests
|
||||
* Minimize memory usage with disk streaming of large HTTP request or response bodies
|
||||
* Parser for [web forms](http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4) submitted using "application/x-www-form-urlencoded" or "multipart/form-data" encodings (including file uploads)
|
||||
* [JSON](http://www.json.org/) parsing and serialization for request and response HTTP bodies
|
||||
@@ -39,15 +42,15 @@ Download or check out the [latest release](https://github.com/swisspol/GCDWebSer
|
||||
|
||||
Alternatively, you can install GCDWebServer using [CocoaPods](http://cocoapods.org/) by simply adding this line to your Xcode project's Podfile:
|
||||
```
|
||||
pod "GCDWebServer", "~> 2.0"
|
||||
pod "GCDWebServer", "~> 3.0"
|
||||
```
|
||||
If you want to use GCDWebUploader, use this line instead:
|
||||
```
|
||||
pod "GCDWebServer/WebUploader", "~> 2.0"
|
||||
pod "GCDWebServer/WebUploader", "~> 3.0"
|
||||
```
|
||||
Or this line for GCDWebDAVServer:
|
||||
```
|
||||
pod "GCDWebServer/WebDAV", "~> 2.0"
|
||||
pod "GCDWebServer/WebDAV", "~> 3.0"
|
||||
```
|
||||
|
||||
Hello World
|
||||
@@ -75,8 +78,10 @@ int main(int argc, const char* argv[]) {
|
||||
|
||||
}];
|
||||
|
||||
// Use convenience method that runs server on port 8080 until SIGINT received (i.e. Ctrl-C in Terminal)
|
||||
// Use convenience method that runs server on port 8080
|
||||
// until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
|
||||
[webServer runWithPort:8080 bonjourName:nil];
|
||||
NSLog(@"Visit %@ in your web browser", webServer.serverURL);
|
||||
|
||||
}
|
||||
return 0;
|
||||
@@ -88,7 +93,12 @@ int main(int argc, const char* argv[]) {
|
||||
#import "GCDWebServer.h"
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
|
||||
static GCDWebServer* _webServer = nil; // This should really be an ivar of your application's delegate class
|
||||
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||
GCDWebServer* _webServer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||
|
||||
@@ -106,9 +116,67 @@ static GCDWebServer* _webServer = nil; // This should really be an ivar of your
|
||||
|
||||
// Start server on port 8080
|
||||
[_webServer startWithPort:8080 bonjourName:nil];
|
||||
NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
**OS X Swift version (command line tool):**
|
||||
|
||||
***webServer.swift***
|
||||
```swift
|
||||
import Foundation
|
||||
|
||||
let webServer = GCDWebServer()
|
||||
|
||||
webServer.addDefaultHandlerForMethod("GET", requestClass: GCDWebServerRequest.self) { request in
|
||||
return GCDWebServerDataResponse(HTML:"<html><body><p>Hello World</p></body></html>")
|
||||
}
|
||||
|
||||
webServer.runWithPort(8080, bonjourName: nil)
|
||||
|
||||
println("Visit \(webServer.serverURL) in your web browser")
|
||||
```
|
||||
|
||||
***WebServer-Bridging-Header.h***
|
||||
```objectivec
|
||||
#import "GCDWebServer.h"
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
```
|
||||
|
||||
Asynchronous HTTP Responses
|
||||
===========================
|
||||
|
||||
New in GCDWebServer 3.0 is the ability to process HTTP requests aysnchronously i.e. add handlers to the server that generate their ```GCDWebServerResponse``` asynchronously. This is achieved by adding handlers that use a ```GCDWebServerAsyncProcessBlock``` instead of a ```GCDWebServerProcessBlock```. Here's an example:
|
||||
|
||||
***Synchronous version***
|
||||
```objectivec
|
||||
[webServer addDefaultHandlerForMethod:@"GET"
|
||||
requestClass:[GCDWebServerRequest class]
|
||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
|
||||
return response;
|
||||
|
||||
}];
|
||||
```
|
||||
|
||||
***Asynchronous version***
|
||||
```objectivec
|
||||
[webServer addDefaultHandlerForMethod:@"GET"
|
||||
requestClass:[GCDWebServerRequest class]
|
||||
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
|
||||
// Do some async operation like network access or file I/O (simulated here using dispatch_after())
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
|
||||
completionBlock(response);
|
||||
});
|
||||
|
||||
}];
|
||||
```
|
||||
|
||||
Web Based Uploads in iOS Apps
|
||||
@@ -121,7 +189,12 @@ Simply instantiate and run a ```GCDWebUploader``` instance then visit ```http://
|
||||
```objectivec
|
||||
#import "GCDWebUploader.h"
|
||||
|
||||
static GCDWebUploader* _webUploader = nil; // This should really be an ivar of your application's delegate class
|
||||
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||
GCDWebUploader* _webUploader;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||
@@ -130,6 +203,8 @@ static GCDWebUploader* _webUploader = nil; // This should really be an ivar of
|
||||
NSLog(@"Visit %@ in your web browser", _webUploader.serverURL);
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
WebDAV Server in iOS Apps
|
||||
@@ -144,7 +219,12 @@ Simply instantiate and run a ```GCDWebDAVServer``` instance then connect to ```h
|
||||
```objectivec
|
||||
#import "GCDWebDAVServer.h"
|
||||
|
||||
static GCDWebDAVServer* _davServer = nil; // This should really be an ivar of your application's delegate class
|
||||
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||
GCDWebDAVServer* _davServer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||
@@ -153,6 +233,8 @@ static GCDWebDAVServer* _davServer = nil; // This should really be an ivar of y
|
||||
NSLog(@"Visit %@ in your WebDAV client", _davServer.serverURL);
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
Serving a Static Website
|
||||
@@ -200,9 +282,9 @@ GCDWebServer relies on "handlers" to process incoming web requests and generatin
|
||||
|
||||
Handlers require 2 GCD blocks:
|
||||
* The ```GCDWebServerMatchBlock``` is called on every handler added to the ```GCDWebServer``` instance whenever a web request has started (i.e. HTTP headers have been received). It is passed the basic info for the web request (HTTP method, URL, headers...) and must decide if it wants to handle it or not. If yes, it must return a new ```GCDWebServerRequest``` instance (see above) created with this info. Otherwise, it simply returns nil.
|
||||
* The ```GCDWebServerProcessBlock``` is called after the web request has been fully received and is passed the ```GCDWebServerRequest``` instance created at the previous step. It must return a ```GCDWebServerResponse``` instance (see above) or nil on error, which will result in a 500 HTTP status code returned to the client. It's however recommended to return an instance of [GCDWebServerErrorResponse](GCDWebServer/Responses/GCDWebServerErrorResponse.h) on error so more useful information can be returned to the client.
|
||||
* The ```GCDWebServerProcessBlock``` or ```GCDWebServerAsyncProcessBlock``` is called after the web request has been fully received and is passed the ```GCDWebServerRequest``` instance created at the previous step. It must return synchronously (if using ```GCDWebServerProcessBlock```) or asynchronously (if using ```GCDWebServerAsyncProcessBlock```) a ```GCDWebServerResponse``` instance (see above) or nil on error, which will result in a 500 HTTP status code returned to the client. It's however recommended to return an instance of [GCDWebServerErrorResponse](GCDWebServer/Responses/GCDWebServerErrorResponse.h) on error so more useful information can be returned to the client.
|
||||
|
||||
Note that most methods on ```GCDWebServer``` to add handlers only require the ```GCDWebServerProcessBlock``` as they already provide a built-in ```GCDWebServerMatchBlock``` e.g. to match a URL path with a Regex.
|
||||
Note that most methods on ```GCDWebServer``` to add handlers only require the ```GCDWebServerProcessBlock``` or ```GCDWebServerAsyncProcessBlock``` as they already provide a built-in ```GCDWebServerMatchBlock``` e.g. to match a URL path with a Regex.
|
||||
|
||||
GCDWebServer & Background Mode for iOS Apps
|
||||
===========================================
|
||||
@@ -218,6 +300,25 @@ Fortunately, GCDWebServer does all of this automatically for you:
|
||||
|
||||
HTTP connections are often initiated in batches (or bursts), for instance when loading a web page with multiple resources. This makes it difficult to accurately detect when the *very last* HTTP connection has been closed: it's possible 2 consecutive HTTP connections part of the same batch would be separated by a small delay instead of overlapping. It would be bad for the client if GCDWebServer suspended itself right in between. The ```GCDWebServerOption_ConnectedStateCoalescingInterval``` option solves this problem elegantly by forcing GCDWebServer to wait some extra delay before performing any action after the last HTTP connection has been closed, just in case a new HTTP connection is initiated within this delay.
|
||||
|
||||
Debug Builds & Custom Logging
|
||||
=============================
|
||||
|
||||
When building GCDWebServer in "Debug" mode versus "Release" mode, GCDWebServer logs a lot more information and also performs a number of internal consistency checks. To enable this behavior, define the preprocessor constant ```DEBUG=1``` when compiling GCDWebServer. In Xcode target settings, this can be done by adding ```DEBUG=1``` to the build setting ```GCC_PREPROCESSOR_DEFINITIONS``` when building in Debug configuration.
|
||||
|
||||
It's also possible to replace the logging system used by GCDWebServer by a custom one. Simply define the preprocessor constant ```__GCDWEBSERVER_LOGGING_HEADER__``` to the name of a header file (e.g. "MyLogging.h") that defines these macros:
|
||||
|
||||
```
|
||||
#define LOG_DEBUG(...) // Should not do anything unless the preprocessor constant DEBUG is non-zero
|
||||
#define LOG_VERBOSE(...)
|
||||
#define LOG_INFO(...)
|
||||
#define LOG_WARNING(...)
|
||||
#define LOG_ERROR(...)
|
||||
#define LOG_EXCEPTION(__EXCEPTION__)
|
||||
|
||||
#define DCHECK(__CONDITION__) // Should not do anything unless the preprocessor constant DEBUG is non-zero
|
||||
#define DNOT_REACHED() // Should not do anything unless the preprocessor constant DEBUG is non-zero
|
||||
```
|
||||
|
||||
Advanced Example 1: Implementing HTTP Redirects
|
||||
===============================================
|
||||
|
||||
|
||||
36
Run-Tests.sh
36
Run-Tests.sh
@@ -23,26 +23,52 @@ function runTests {
|
||||
rm -rf "$PAYLOAD_DIR"
|
||||
ditto -x -k "$PAYLOAD_ZIP" "$PAYLOAD_DIR"
|
||||
TZ=GMT find "$PAYLOAD_DIR" -type d -exec SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" '{}' \; # ZIP archives do not preserve directories dates
|
||||
if [ "$4" != "" ]; then
|
||||
cp -f "$4" "$PAYLOAD_DIR/Payload"
|
||||
pushd "$PAYLOAD_DIR/Payload"
|
||||
SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" `basename "$4"`
|
||||
popd
|
||||
fi
|
||||
logLevel=2 $1 -mode "$2" -root "$PAYLOAD_DIR/Payload" -tests "$3"
|
||||
}
|
||||
|
||||
# Build for iOS in manual memory management mode (TODO: run tests on iOS)
|
||||
# Build for iOS in manual memory management mode and for oldest deployment target (TODO: run tests on iOS)
|
||||
rm -rf "$MRC_BUILD_DIR"
|
||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" "IPHONEOS_DEPLOYMENT_TARGET=5.1.1" > /dev/null
|
||||
|
||||
# Build for iOS in manual memory management mode and for default deployment target (TODO: run tests on iOS)
|
||||
rm -rf "$MRC_BUILD_DIR"
|
||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" > /dev/null
|
||||
|
||||
# Build for iOS in ARC mode (TODO: run tests on iOS)
|
||||
# Build for iOS in ARC mode and for oldest deployment target (TODO: run tests on iOS)
|
||||
rm -rf "$ARC_BUILD_DIR"
|
||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" "IPHONEOS_DEPLOYMENT_TARGET=5.1.1" > /dev/null
|
||||
|
||||
# Build for iOS in ARC mode and for default deployment target (TODO: run tests on iOS)
|
||||
rm -rf "$ARC_BUILD_DIR"
|
||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null
|
||||
|
||||
# Build for OS X in manual memory management mode
|
||||
# Build for OS X in manual memory management mode and for oldest deployment target
|
||||
rm -rf "$MRC_BUILD_DIR"
|
||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" "MACOSX_DEPLOYMENT_TARGET=10.7" > /dev/null
|
||||
|
||||
# Build for OS X in manual memory management mode and for default deployment target
|
||||
rm -rf "$MRC_BUILD_DIR"
|
||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" > /dev/null
|
||||
|
||||
# Build for OS X in ARC mode
|
||||
# Build for OS X in ARC mode and for oldest deployment target
|
||||
rm -rf "$ARC_BUILD_DIR"
|
||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" "MACOSX_DEPLOYMENT_TARGET=10.7" > /dev/null
|
||||
|
||||
# Build for OS X in ARC mode and for default deployment target
|
||||
rm -rf "$ARC_BUILD_DIR"
|
||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null
|
||||
|
||||
# Run tests
|
||||
runTests $MRC_PRODUCT "htmlForm" "Tests/HTMLForm"
|
||||
runTests $ARC_PRODUCT "htmlForm" "Tests/HTMLForm"
|
||||
runTests $MRC_PRODUCT "htmlFileUpload" "Tests/HTMLFileUpload"
|
||||
runTests $ARC_PRODUCT "htmlFileUpload" "Tests/HTMLFileUpload"
|
||||
runTests $MRC_PRODUCT "webServer" "Tests/WebServer"
|
||||
runTests $ARC_PRODUCT "webServer" "Tests/WebServer"
|
||||
runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Transmit"
|
||||
@@ -53,6 +79,8 @@ runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
||||
runTests $ARC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
||||
runTests $MRC_PRODUCT "webUploader" "Tests/WebUploader"
|
||||
runTests $ARC_PRODUCT "webUploader" "Tests/WebUploader"
|
||||
runTests $MRC_PRODUCT "webServer" "Tests/WebServer-Sample-Movie" "Tests/Sample-Movie.mp4"
|
||||
runTests $ARC_PRODUCT "webServer" "Tests/WebServer-Sample-Movie" "Tests/Sample-Movie.mp4"
|
||||
|
||||
# Done
|
||||
echo "\nAll tests completed successfully!"
|
||||
|
||||
9
Tests/HTMLFileUpload/001-200.response
Normal file
9
Tests/HTMLFileUpload/001-200.response
Normal file
@@ -0,0 +1,9 @@
|
||||
HTTP/1.1 200 OK
|
||||
Cache-Control: no-cache
|
||||
Content-Length: 299
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Connection: Close
|
||||
Server: GCDWebServer
|
||||
Date: Fri, 25 Apr 2014 14:15:11 GMT
|
||||
|
||||
<html><body> <form name="input" action="/" method="post" enctype="multipart/form-data"> <input type="hidden" name="secret" value="42"> <input type="file" name="files" multiple><br/> <input type="submit" value="Submit"> </form> </body></html>
|
||||
10
Tests/HTMLFileUpload/001-GET.request
Normal file
10
Tests/HTMLFileUpload/001-GET.request
Normal file
@@ -0,0 +1,10 @@
|
||||
GET / HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: max-age=0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
DNT: 1
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
|
||||
9
Tests/HTMLFileUpload/002-200.response
Normal file
9
Tests/HTMLFileUpload/002-200.response
Normal file
@@ -0,0 +1,9 @@
|
||||
HTTP/1.1 200 OK
|
||||
Cache-Control: no-cache
|
||||
Content-Length: 447
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Connection: Close
|
||||
Server: GCDWebServer
|
||||
Date: Fri, 25 Apr 2014 14:15:21 GMT
|
||||
|
||||
<html><body><p>secret = 42<br>files = "hero_mba_11.jpg" (image/jpeg | 106 KB)<br>files = "Test File.txt" (text/plain | 21 Bytes)<br></p><hr> <form name="input" action="/" method="post" enctype="multipart/form-data"> <input type="hidden" name="secret" value="42"> <input type="file" name="files" multiple><br/> <input type="submit" value="Submit"> </form> </body></html>
|
||||
BIN
Tests/HTMLFileUpload/002-POST.request
Normal file
BIN
Tests/HTMLFileUpload/002-POST.request
Normal file
Binary file not shown.
9
Tests/HTMLForm/001-200.response
Normal file
9
Tests/HTMLForm/001-200.response
Normal file
@@ -0,0 +1,9 @@
|
||||
HTTP/1.1 200 OK
|
||||
Cache-Control: no-cache
|
||||
Content-Length: 293
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Connection: Close
|
||||
Server: GCDWebServer
|
||||
Date: Fri, 25 Apr 2014 14:12:09 GMT
|
||||
|
||||
<html><body> <form name="input" action="/" method="post" enctype="application/x-www-form-urlencoded"> Value: <input type="text" name="value"> <input type="submit" value="Submit"> </form> </body></html>
|
||||
10
Tests/HTMLForm/001-GET.request
Normal file
10
Tests/HTMLForm/001-GET.request
Normal file
@@ -0,0 +1,10 @@
|
||||
GET / HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: max-age=0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
DNT: 1
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
|
||||
9
Tests/HTMLForm/002-200.response
Normal file
9
Tests/HTMLForm/002-200.response
Normal file
@@ -0,0 +1,9 @@
|
||||
HTTP/1.1 200 OK
|
||||
Cache-Control: no-cache
|
||||
Content-Length: 47
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Connection: Close
|
||||
Server: GCDWebServer
|
||||
Date: Fri, 25 Apr 2014 14:12:20 GMT
|
||||
|
||||
<html><body><p>Hellø Wörld!</p></body></html>
|
||||
15
Tests/HTMLForm/002-POST.request
Normal file
15
Tests/HTMLForm/002-POST.request
Normal file
@@ -0,0 +1,15 @@
|
||||
POST / HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Content-Length: 30
|
||||
Cache-Control: max-age=0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||
Origin: http://localhost:8080
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
|
||||
value=Hell%C3%B8+W%C3%B6rld%21
|
||||
BIN
Tests/Sample-Movie.mp4
Normal file
BIN
Tests/Sample-Movie.mp4
Normal file
Binary file not shown.
BIN
Tests/WebServer-Sample-Movie/001-200.response
Normal file
BIN
Tests/WebServer-Sample-Movie/001-200.response
Normal file
Binary file not shown.
12
Tests/WebServer-Sample-Movie/001-GET.request
Normal file
12
Tests/WebServer-Sample-Movie/001-GET.request
Normal file
@@ -0,0 +1,12 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: no-cache
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||
Pragma: no-cache
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
|
||||
BIN
Tests/WebServer-Sample-Movie/002-206.response
Normal file
BIN
Tests/WebServer-Sample-Movie/002-206.response
Normal file
Binary file not shown.
13
Tests/WebServer-Sample-Movie/002-GET.request
Normal file
13
Tests/WebServer-Sample-Movie/002-GET.request
Normal file
@@ -0,0 +1,13 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: no-cache
|
||||
Pragma: no-cache
|
||||
Accept-Encoding: identity;q=1, *;q=0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Accept: */*
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/Sample-Movie.mp4
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
Range: bytes=0-
|
||||
|
||||
BIN
Tests/WebServer-Sample-Movie/003-206.response
Normal file
BIN
Tests/WebServer-Sample-Movie/003-206.response
Normal file
Binary file not shown.
13
Tests/WebServer-Sample-Movie/003-GET.request
Normal file
13
Tests/WebServer-Sample-Movie/003-GET.request
Normal file
@@ -0,0 +1,13 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: no-cache
|
||||
Pragma: no-cache
|
||||
Accept-Encoding: identity;q=1, *;q=0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Accept: */*
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/Sample-Movie.mp4
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
Range: bytes=3391326-
|
||||
|
||||
BIN
Tests/WebServer-Sample-Movie/004-206.response
Normal file
BIN
Tests/WebServer-Sample-Movie/004-206.response
Normal file
Binary file not shown.
12
Tests/WebServer-Sample-Movie/004-GET.request
Normal file
12
Tests/WebServer-Sample-Movie/004-GET.request
Normal file
@@ -0,0 +1,12 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Accept-Encoding: identity;q=1, *;q=0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Accept: */*
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/Sample-Movie.mp4
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
Range: bytes=168-3391487
|
||||
If-Range: 75279017/1388563200/0
|
||||
|
||||
BIN
Tests/WebServer-Sample-Movie/005-206.response
Normal file
BIN
Tests/WebServer-Sample-Movie/005-206.response
Normal file
Binary file not shown.
12
Tests/WebServer-Sample-Movie/005-GET.request
Normal file
12
Tests/WebServer-Sample-Movie/005-GET.request
Normal file
@@ -0,0 +1,12 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Accept-Encoding: identity;q=1, *;q=0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Accept: */*
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/Sample-Movie.mp4
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
Range: bytes=168-1023
|
||||
If-Range: 75279017/1388563200/0
|
||||
|
||||
@@ -30,15 +30,12 @@
|
||||
|
||||
@interface AppDelegate () <GCDWebUploaderDelegate> {
|
||||
@private
|
||||
UIWindow* _window;
|
||||
GCDWebUploader* _webServer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
@synthesize window=_window;
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
|
||||
- (void)dealloc {
|
||||
@@ -50,7 +47,8 @@
|
||||
#endif
|
||||
|
||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||
_window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
CGRect bounds = ([UIScreen instancesRespondToSelector:@selector(nativeBounds)] ? [[UIScreen mainScreen] nativeBounds] : [[UIScreen mainScreen] bounds]);
|
||||
_window = [[UIWindow alloc] initWithFrame:bounds];
|
||||
_window.backgroundColor = [UIColor whiteColor];
|
||||
[_window makeKeyAndVisible];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user