mirror of
https://github.com/swisspol/GCDWebServer.git
synced 2026-02-11 00:00:07 +08:00
Compare commits
55 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 |
@@ -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.4'
|
||||
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.
|
||||
*
|
||||
@@ -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,9 +397,9 @@ 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 and sets "error" argument if not NULL.
|
||||
*
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
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";
|
||||
@@ -61,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
|
||||
|
||||
@@ -95,28 +96,43 @@ 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;
|
||||
GCDWebServerProcessBlock _processBlock;
|
||||
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);
|
||||
}
|
||||
@@ -129,9 +145,9 @@ static void _SignalHandler(int signal) {
|
||||
dispatch_queue_t _syncQueue;
|
||||
dispatch_semaphore_t _sourceSemaphore;
|
||||
NSMutableArray* _handlers;
|
||||
NSInteger _activeConnections; // Accessed only with _syncQueue
|
||||
BOOL _connected;
|
||||
CFRunLoopTimerRef _connectedTimer;
|
||||
NSInteger _activeConnections; // Accessed through _syncQueue only
|
||||
BOOL _connected; // Accessed on main thread only
|
||||
CFRunLoopTimerRef _disconnectTimer; // Accessed on main thread only
|
||||
|
||||
NSDictionary* _options;
|
||||
NSString* _serverName;
|
||||
@@ -143,7 +159,8 @@ static void _SignalHandler(int signal) {
|
||||
CFTimeInterval _disconnectDelay;
|
||||
NSUInteger _port;
|
||||
dispatch_source_t _source;
|
||||
CFNetServiceRef _service;
|
||||
CFNetServiceRef _registrationService;
|
||||
CFNetServiceRef _resolutionService;
|
||||
#if TARGET_OS_IPHONE
|
||||
BOOL _suspendInBackground;
|
||||
UIBackgroundTaskIdentifier _backgroundTask;
|
||||
@@ -175,20 +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
|
||||
@@ -199,14 +207,9 @@ 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);
|
||||
@@ -256,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];
|
||||
@@ -310,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];
|
||||
}
|
||||
@@ -321,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);
|
||||
}
|
||||
@@ -337,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);
|
||||
@@ -369,8 +410,10 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
|
||||
- (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) {
|
||||
@@ -467,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);
|
||||
|
||||
CFNetServiceSetClient(_registrationService, _NetServiceRegisterCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFStreamError streamError = {0};
|
||||
CFNetServiceRegisterWithOptions(_service, 0, &streamError);
|
||||
CFNetServiceRegisterWithOptions(_registrationService, 0, &streamError);
|
||||
|
||||
_resolutionService = CFNetServiceCreateCopy(kCFAllocatorDefault, _registrationService);
|
||||
if (_resolutionService) {
|
||||
CFNetServiceSetClient(_resolutionService, _NetServiceResolveCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed creating CFNetService");
|
||||
}
|
||||
@@ -488,24 +538,24 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed starting listening 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 listening 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 listening 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);
|
||||
}
|
||||
@@ -513,11 +563,19 @@ 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);
|
||||
@@ -535,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(), ^{
|
||||
@@ -591,7 +658,7 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
}
|
||||
|
||||
- (BOOL)isRunning {
|
||||
return (_source ? YES : NO);
|
||||
return (_options ? YES : NO);
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
@@ -631,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]];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -668,8 +736,9 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
BOOL success = NO;
|
||||
_run = YES;
|
||||
void (*handler)(int) = signal(SIGINT, _SignalHandler);
|
||||
if (handler != SIG_ERR) {
|
||||
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);
|
||||
@@ -677,7 +746,9 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
||||
[self stop];
|
||||
success = YES;
|
||||
}
|
||||
signal(SIGINT, handler);
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
signal(SIGINT, intHandler);
|
||||
signal(SIGTERM, termHandler);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -689,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]) {
|
||||
@@ -696,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) {
|
||||
|
||||
@@ -711,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) {
|
||||
@@ -725,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();
|
||||
}
|
||||
@@ -814,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"];
|
||||
@@ -930,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;
|
||||
@@ -940,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 {
|
||||
@@ -964,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 error:NULL]) {
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
|
||||
result = 0;
|
||||
NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
|
||||
@@ -1022,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"]];
|
||||
@@ -1064,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,7 @@ 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) {
|
||||
@@ -144,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 ()
|
||||
@@ -154,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();
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -59,11 +59,11 @@
|
||||
}
|
||||
|
||||
- (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 {
|
||||
@@ -81,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) {
|
||||
@@ -121,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];
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -160,6 +165,9 @@
|
||||
}
|
||||
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];
|
||||
|
||||
31
Mac/main.m
31
Mac/main.m
@@ -52,6 +52,7 @@ typedef enum {
|
||||
kMode_WebDAV,
|
||||
kMode_WebUploader,
|
||||
kMode_StreamingResponse,
|
||||
kMode_AsyncResponse
|
||||
} Mode;
|
||||
|
||||
@interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate>
|
||||
@@ -142,7 +143,7 @@ int main(int argc, const char* argv[]) {
|
||||
NSString* authenticationPassword = nil;
|
||||
|
||||
if (argc == 1) {
|
||||
fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | htmlFileUpload | 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] != '-') {
|
||||
@@ -164,6 +165,8 @@ int main(int argc, const char* argv[]) {
|
||||
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;
|
||||
@@ -328,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");
|
||||
@@ -337,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;
|
||||
@@ -363,9 +387,10 @@ int main(int argc, const char* argv[]) {
|
||||
result = 0;
|
||||
}
|
||||
}
|
||||
webServer.delegate = nil;
|
||||
#if !__has_feature(objc_arc)
|
||||
[webServer release];
|
||||
[delegate release];
|
||||
[webServer release];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
117
README.md
117
README.md
@@ -13,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
|
||||
@@ -41,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
|
||||
@@ -77,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;
|
||||
@@ -90,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 {
|
||||
|
||||
@@ -108,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
|
||||
@@ -123,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];
|
||||
@@ -132,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
|
||||
@@ -146,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];
|
||||
@@ -155,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
|
||||
@@ -202,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
|
||||
===========================================
|
||||
@@ -220,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
|
||||
===============================================
|
||||
|
||||
|
||||
32
Run-Tests.sh
32
Run-Tests.sh
@@ -23,22 +23,44 @@ 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
|
||||
|
||||
@@ -57,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!"
|
||||
|
||||
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