50 Commits
2.4 ... 3.0

Author SHA1 Message Date
Pierre-Olivier Latour
8e9fe4c4c1 Update README.md 2014-10-09 12:31:05 -07:00
Pierre-Olivier Latour
95bccff2f7 Fix 2014-10-09 10:16:13 -07:00
Pierre-Olivier Latour
780a608d6c Update README.md 2014-10-09 09:57:08 -07:00
Pierre-Olivier Latour
18d93bbf47 Bumped version 2014-10-09 09:54:08 -07:00
Pierre-Olivier Latour
b35ebd7d58 #92 Added support for async handlers 2014-10-09 09:53:03 -07:00
Pierre-Olivier Latour
a11b047233 Renamed GCDWebServerStreamingBlock to GCDWebServerStreamBlock 2014-10-09 09:43:59 -07:00
Pierre-Olivier Latour
4eac9d4f8e Upgrade to Xcode 6.1 2014-10-09 09:09:34 -07:00
Pierre-Olivier Latour
d1e2a1a12f Bumped version 2014-10-07 13:27:04 -07:00
Pierre-Olivier Latour
54d5abd3a8 Fixed rare race-condition with disconnection timer 2014-10-07 13:26:55 -07:00
Pierre-Olivier Latour
a9fee8d7e2 Fixed rare exception in GCDWebServerParseURLEncodedForm() 2014-10-07 12:16:14 -07:00
Pierre-Olivier Latour
6b15bdaa4e Merge pull request #89 from nickgravelyn/capture-regex-parameters-in-request
Added attributes to GCDWebServerRequest with support to retrieve regex captured variables
2014-09-30 10:52:31 -07:00
Nick Gravelyn
3771cf4e92 Adding an attribute collection to GCDWebServerRequest and adding regular expression captures as an attribute. 2014-09-30 10:45:45 -07:00
Pierre-Olivier Latour
a5d83abdd0 Bumped version to 2.5.2 2014-09-21 10:35:46 -07:00
Pierre-Olivier Latour
f1e9f1a37c Improved handling of symbolic links in -addGETHandlerForBasePath:directoryPath:indexFilename:cacheAge:allowRangeRequests: 2014-09-19 08:19:49 -07:00
Pierre-Olivier Latour
00b5ec87ba #76 Fixed -bonjourServerURL to correctly return hostname instead of service name 2014-09-15 09:00:39 -07:00
Pierre-Olivier Latour
cf94e70a42 Fall back to CFBundleName if CFBundleDisplayName is not available 2014-09-15 08:05:07 -07:00
Pierre-Olivier Latour
d47409c776 Updated for Xcode6 2014-09-15 08:05:00 -07:00
Pierre-Olivier Latour
a9db13475b Bumped version 2014-08-24 12:28:51 -07:00
Pierre-Olivier Latour
17fad0f1b9 Improved automatic detection of when to use dispatch_retain() and dispatch_release() depending on compiler settings 2014-08-24 12:24:16 -07:00
Pierre-Olivier Latour
5493d9e803 Run test against default and oldest supported deployment targets 2014-08-24 12:20:55 -07:00
Pierre-Olivier Latour
12b1edb958 #70 Ensure -isRunning works as expected even if GCDWebServerOption_AutomaticallySuspendInBackground is enabled 2014-07-23 07:40:37 -07:00
Pierre-Olivier Latour
7544a6dc4e Update 2014-07-12 17:11:26 -07:00
Pierre-Olivier Latour
f9621c8aac Bumped version 2014-07-12 16:55:31 -07:00
Pierre-Olivier Latour
7a93b27478 Update README.md 2014-07-03 19:54:25 -07:00
Pierre-Olivier Latour
9d48f9ec12 #57 Validate paths passed to GCDWebDAVServer and GCDWebUploader to ensure they are within the upload directory 2014-06-22 15:53:03 -07:00
Pierre-Olivier Latour
7c6e85cf9a Merge pull request #60 from pvblivs/master
Adding instructions for Swift command line tool
2014-06-22 15:36:21 -07:00
pvblivs
24fbd161d8 Update README.md 2014-06-10 08:28:59 +02:00
pvblivs
0ae0d4175a Adding instructions for Swift command line tool 2014-06-06 16:34:26 +02:00
Pierre-Olivier Latour
6d550a02b7 Merge pull request #55 from mstarinteractive/fix-json-content-type
Fix content-types like "application/json; charset=utf-8"
2014-05-16 12:17:19 -07:00
Braden MacDonald
a7f46b762f Fix content-types like "application/json; charset=utf-8" 2014-05-16 11:55:20 -07:00
Pierre-Olivier Latour
d1c7f9a323 Merge pull request #56 from jaanus/customBonjourType
Can specify a custom Bonjour service type for the server.
2014-05-16 08:31:21 -07:00
jaanus
93bfe65211 Removed unneeded API. If custom Bonjour type is needed, startWithOptions should be used to specify an options dictionary with the custom type. 2014-05-16 17:12:52 +02:00
jaanus
8ab53f74d5 Can specify a custom Bonjour service type for the server. 2014-05-16 11:20:28 +02:00
Pierre-Olivier Latour
04a69787bf Update README.md 2014-05-05 13:43:38 -07:00
Pierre-Olivier Latour
dfd019de7d Update README.md 2014-05-05 13:18:04 -07:00
Pierre-Olivier Latour
4db631fa27 Fixed errno being corrupted by LOG_ERROR() 2014-05-02 22:17:25 -07:00
Pierre-Olivier Latour
ba03d756c6 Merge pull request #52 from tipbit/fix-empty-query-param
Fix GCDWebServerParseURLEncodedForm to allow empty values.
2014-04-30 17:22:56 -07:00
Ewan Mellor
04f59a9214 Fix GCDWebServerParseURLEncodedForm to allow empty values.
If the input string is something like foo=&bar= then both foo and bar
should be in the result, with the empty string as their corresponding value.

The [scanner scanUpToString:@"&" ...] call was returning failure, because
it was already positioned at the &.  In this situation, just set value to the
empty string.
2014-04-30 17:08:42 -07:00
Pierre-Olivier Latour
40ea252ad6 Ensure connected state is updated immediately after calling -stop 2014-04-30 14:04:46 -07:00
Pierre-Olivier Latour
c193860468 Fix 2014-04-30 13:59:26 -07:00
Pierre-Olivier Latour
94ad8c745e No need to call -stop from -dealloc 2014-04-30 13:36:06 -07:00
Pierre-Olivier Latour
56c096996f Fix 2014-04-30 13:06:02 -07:00
Pierre-Olivier Latour
8cbaf0f867 Added video streaming unit test 2014-04-30 11:31:20 -07:00
Pierre-Olivier Latour
295901c0b3 Reject files greater than 4 GiB in 32 bit mode 2014-04-30 09:57:12 -07:00
Pierre-Olivier Latour
2dda0c98ce #50 Use NSUIntegerMax instead of NSNotFound to indicate undefined length 2014-04-30 09:56:18 -07:00
Pierre-Olivier Latour
70a38c8b01 Fix 2014-04-30 09:15:30 -07:00
Pierre-Olivier Latour
1b12a7bd14 Ensure pending scheduled callbacks have executed in "run" APIs 2014-04-29 22:12:28 -07:00
Pierre-Olivier Latour
75e6332500 Don't delay disconnected state update if already stopped 2014-04-29 22:06:13 -07:00
Pierre-Olivier Latour
420ed719e8 Add SIGTERM support to -runWithOptions:error: 2014-04-29 16:20:03 -07:00
Pierre-Olivier Latour
3b75f9dd20 Added -rewriteRequestURL:withMethod:headers: hook 2014-04-28 14:09:15 -07:00
33 changed files with 672 additions and 200 deletions

View File

@@ -61,6 +61,11 @@ typedef NS_ENUM(NSInteger, DAVProperties) {
@implementation GCDWebDAVServer (Methods) @implementation GCDWebDAVServer (Methods)
// Must match implementation in GCDWebUploader
- (BOOL)_checkSandboxedPath:(NSString*)path {
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
}
- (BOOL)_checkFileExtension:(NSString*)fileName { - (BOOL)_checkFileExtension:(NSString*)fileName {
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) { if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
return NO; return NO;
@@ -87,7 +92,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
NSString* relativePath = request.path; NSString* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO; 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]; 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* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
if (![absolutePath hasPrefix:_uploadDirectory]) { if (![self _checkSandboxedPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
} }
BOOL isDirectory; BOOL isDirectory;
@@ -161,7 +166,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
NSString* relativePath = request.path; NSString* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO; 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]; 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* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
if (![absolutePath hasPrefix:_uploadDirectory]) { if (![self _checkSandboxedPath:absolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath]; return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
} }
BOOL isDirectory; BOOL isDirectory;
@@ -243,7 +248,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
NSString* srcRelativePath = request.path; NSString* srcRelativePath = request.path;
NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:srcRelativePath]; NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:srcRelativePath];
if (![srcAbsolutePath hasPrefix:_uploadDirectory]) { if (![self _checkSandboxedPath:srcAbsolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath]; 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]; dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath]; NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath];
if (![dstAbsolutePath hasPrefix:_uploadDirectory]) { if (![self _checkSandboxedPath:dstAbsolutePath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath]; 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* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO; 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]; 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* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO; 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]; 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* relativePath = request.path;
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO; 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]; return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
} }

View File

@@ -1,19 +1,19 @@
# http://guides.cocoapods.org/syntax/podspec.html # http://guides.cocoapods.org/syntax/podspec.html
# Verify Podspec with: # http://guides.cocoapods.org/making/getting-setup-with-trunk.html
# sudo gem update cocoapods # $ sudo gem update cocoapods
# pod spec lint GCDWebServer.podspec --verbose # (optional) $ pod trunk register {email} {name} --description={computer}
# Add to source line: # $ pod trunk push
# :tag => s.version.to_s # DELETE THIS SECTION BEFORE PROCEEDING!
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'GCDWebServer' s.name = 'GCDWebServer'
s.version = '2.4' s.version = '3.0'
s.author = { 'Pierre-Olivier Latour' => 'info@pol-online.net' } s.author = { 'Pierre-Olivier Latour' => 'info@pol-online.net' }
s.license = { :type => 'BSD', :file => 'LICENSE' } s.license = { :type => 'BSD', :file => 'LICENSE' }
s.homepage = 'https://github.com/swisspol/GCDWebServer' 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.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.ios.deployment_target = '5.0'
s.osx.deployment_target = '10.7' s.osx.deployment_target = '10.7'
s.requires_arc = true s.requires_arc = true

View File

@@ -370,7 +370,7 @@
08FB7793FE84155DC02AAC07 /* Project object */ = { 08FB7793FE84155DC02AAC07 /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 0510; LastUpgradeCheck = 0610;
}; };
buildConfigurationList = 1DEB928908733DD80010E9CD /* Build configuration list for PBXProject "GCDWebServer" */; buildConfigurationList = 1DEB928908733DD80010E9CD /* Build configuration list for PBXProject "GCDWebServer" */;
compatibilityVersion = "Xcode 3.2"; compatibilityVersion = "Xcode 3.2";
@@ -490,8 +490,6 @@
1DEB928608733DD80010E9CD /* Debug */ = { 1DEB928608733DD80010E9CD /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
MACOSX_DEPLOYMENT_TARGET = 10.7;
PRODUCT_NAME = GCDWebServer; PRODUCT_NAME = GCDWebServer;
SDKROOT = macosx; SDKROOT = macosx;
}; };
@@ -500,8 +498,6 @@
1DEB928708733DD80010E9CD /* Release */ = { 1DEB928708733DD80010E9CD /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
MACOSX_DEPLOYMENT_TARGET = 10.7;
PRODUCT_NAME = GCDWebServer; PRODUCT_NAME = GCDWebServer;
SDKROOT = macosx; SDKROOT = macosx;
}; };
@@ -550,10 +546,8 @@
E22112761690B4DF0048D2B2 /* Debug */ = { E22112761690B4DF0048D2B2 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
INFOPLIST_FILE = iOS/Info.plist; INFOPLIST_FILE = iOS/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 5.1.1;
PRODUCT_NAME = GCDWebServer; PRODUCT_NAME = GCDWebServer;
PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -564,10 +558,8 @@
E22112771690B4DF0048D2B2 /* Release */ = { E22112771690B4DF0048D2B2 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)";
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
INFOPLIST_FILE = iOS/Info.plist; INFOPLIST_FILE = iOS/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 5.1.1;
PRODUCT_NAME = GCDWebServer; PRODUCT_NAME = GCDWebServer;
PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;

View File

@@ -69,6 +69,19 @@ typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod,
*/ */
typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request); 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). * The port used by the GCDWebServer (NSNumber / NSUInteger).
* *
@@ -77,12 +90,21 @@ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* r
extern NSString* const GCDWebServerOption_Port; 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; 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 * The maximum number of incoming HTTP requests that can be queued waiting to
* be handled before new ones are dropped (NSNumber / NSUInteger). * 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 * then passes each one to a "handler" capable of generating an HTTP response
* for it, which is then sent back to the client. * 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. * See the README.md file for more information about the architecture of GCDWebServer.
*/ */
@interface GCDWebServer : NSObject @interface GCDWebServer : NSObject
@@ -262,13 +288,21 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
*/ */
@property(nonatomic, readonly) NSString* bonjourName; @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. * This method is the designated initializer for the class.
*/ */
- (instancetype)init; - (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 * Handlers are called in a LIFO queue, so if multiple handlers can potentially
* respond to a given request, the latest added one wins. * 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; - (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. * 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 * @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. * 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; @property(nonatomic, readonly) NSURL* bonjourServerURL;
/** /**
* Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS) * 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. * 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. * 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 * 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. * Returns NO if the server failed to start.
*/ */
@@ -351,9 +397,9 @@ extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name; - (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name;
/** /**
* Runs the server synchronously using -startWithOptions: until a SIGINT signal * Runs the server synchronously using -startWithOptions: until a SIGTERM or
* is received i.e. Ctrl-C. This method is intended to be used by command line * SIGINT signal is received i.e. Ctrl-C in Terminal. This method is intended to
* tools. * be used by command line tools.
* *
* Returns NO if the server failed to start and sets "error" argument if not NULL. * 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 * 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; - (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 * 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; - (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 * 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; - (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 @end
@interface GCDWebServer (GETHandlers) @interface GCDWebServer (GETHandlers)

View File

@@ -45,6 +45,7 @@
NSString* const GCDWebServerOption_Port = @"Port"; NSString* const GCDWebServerOption_Port = @"Port";
NSString* const GCDWebServerOption_BonjourName = @"BonjourName"; NSString* const GCDWebServerOption_BonjourName = @"BonjourName";
NSString* const GCDWebServerOption_BonjourType = @"BonjourType";
NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections"; NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections";
NSString* const GCDWebServerOption_ServerName = @"ServerName"; NSString* const GCDWebServerOption_ServerName = @"ServerName";
NSString* const GCDWebServerOption_AuthenticationMethod = @"AuthenticationMethod"; NSString* const GCDWebServerOption_AuthenticationMethod = @"AuthenticationMethod";
@@ -95,28 +96,43 @@ static void _SignalHandler(int signal) {
#endif #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 applications 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 () { @interface GCDWebServerHandler () {
@private @private
GCDWebServerMatchBlock _matchBlock; GCDWebServerMatchBlock _matchBlock;
GCDWebServerProcessBlock _processBlock; GCDWebServerAsyncProcessBlock _asyncProcessBlock;
} }
@end @end
@implementation GCDWebServerHandler @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])) { if ((self = [super init])) {
_matchBlock = [matchBlock copy]; _matchBlock = [matchBlock copy];
_processBlock = [processBlock copy]; _asyncProcessBlock = [processBlock copy];
} }
return self; return self;
} }
- (void)dealloc { - (void)dealloc {
ARC_RELEASE(_matchBlock); ARC_RELEASE(_matchBlock);
ARC_RELEASE(_processBlock); ARC_RELEASE(_asyncProcessBlock);
ARC_DEALLOC(super); ARC_DEALLOC(super);
} }
@@ -129,9 +145,9 @@ static void _SignalHandler(int signal) {
dispatch_queue_t _syncQueue; dispatch_queue_t _syncQueue;
dispatch_semaphore_t _sourceSemaphore; dispatch_semaphore_t _sourceSemaphore;
NSMutableArray* _handlers; NSMutableArray* _handlers;
NSInteger _activeConnections; // Accessed only with _syncQueue NSInteger _activeConnections; // Accessed through _syncQueue only
BOOL _connected; BOOL _connected; // Accessed on main thread only
CFRunLoopTimerRef _connectedTimer; CFRunLoopTimerRef _disconnectTimer; // Accessed on main thread only
NSDictionary* _options; NSDictionary* _options;
NSString* _serverName; NSString* _serverName;
@@ -143,7 +159,8 @@ static void _SignalHandler(int signal) {
CFTimeInterval _disconnectDelay; CFTimeInterval _disconnectDelay;
NSUInteger _port; NSUInteger _port;
dispatch_source_t _source; dispatch_source_t _source;
CFNetServiceRef _service; CFNetServiceRef _registrationService;
CFNetServiceRef _resolutionService;
#if TARGET_OS_IPHONE #if TARGET_OS_IPHONE
BOOL _suspendInBackground; BOOL _suspendInBackground;
UIBackgroundTaskIdentifier _backgroundTask; UIBackgroundTaskIdentifier _backgroundTask;
@@ -175,20 +192,11 @@ static void _SignalHandler(int signal) {
GCDWebServerInitializeFunctions(); GCDWebServerInitializeFunctions();
} }
static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
@autoreleasepool {
[(ARC_BRIDGE GCDWebServer*)info _didDisconnect];
}
}
- (instancetype)init { - (instancetype)init {
if ((self = [super init])) { if ((self = [super init])) {
_syncQueue = dispatch_queue_create([NSStringFromClass([self class]) UTF8String], DISPATCH_QUEUE_SERIAL); _syncQueue = dispatch_queue_create([NSStringFromClass([self class]) UTF8String], DISPATCH_QUEUE_SERIAL);
_sourceSemaphore = dispatch_semaphore_create(0); _sourceSemaphore = dispatch_semaphore_create(0);
_handlers = [[NSMutableArray alloc] init]; _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 #if TARGET_OS_IPHONE
_backgroundTask = UIBackgroundTaskInvalid; _backgroundTask = UIBackgroundTaskInvalid;
#endif #endif
@@ -199,14 +207,9 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
- (void)dealloc { - (void)dealloc {
DCHECK(_connected == NO); DCHECK(_connected == NO);
DCHECK(_activeConnections == 0); 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_RELEASE(_handlers);
ARC_DISPATCH_RELEASE(_sourceSemaphore); ARC_DISPATCH_RELEASE(_sourceSemaphore);
ARC_DISPATCH_RELEASE(_syncQueue); ARC_DISPATCH_RELEASE(_syncQueue);
@@ -256,8 +259,10 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
DCHECK(_activeConnections >= 0); DCHECK(_activeConnections >= 0);
if (_activeConnections == 0) { if (_activeConnections == 0) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (_disconnectDelay > 0.0) { if (_disconnectTimer) {
CFRunLoopTimerSetNextFireDate(_connectedTimer, HUGE_VAL); CFRunLoopTimerInvalidate(_disconnectTimer);
CFRelease(_disconnectTimer);
_disconnectTimer = NULL;
} }
if (_connected == NO) { if (_connected == NO) {
[self _didConnect]; [self _didConnect];
@@ -310,8 +315,18 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
_activeConnections -= 1; _activeConnections -= 1;
if (_activeConnections == 0) { if (_activeConnections == 0) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (_disconnectDelay > 0.0) { if ((_disconnectDelay > 0.0) && (_source != NULL)) {
CFRunLoopTimerSetNextFireDate(_connectedTimer, CFAbsoluteTimeGetCurrent() + _disconnectDelay); 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 { } else {
[self _didDisconnect]; [self _didDisconnect];
} }
@@ -321,13 +336,24 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
} }
- (NSString*)bonjourName { - (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; 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); DCHECK(_options == nil);
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock]; GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock asyncProcessBlock:processBlock];
[_handlers insertObject:handler atIndex:0]; [_handlers insertObject:handler atIndex:0];
ARC_RELEASE(handler); ARC_RELEASE(handler);
} }
@@ -337,11 +363,26 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
[_handlers removeAllObjects]; [_handlers removeAllObjects];
} }
static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) { static void _NetServiceRegisterCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
DCHECK([NSThread isMainThread]); DCHECK([NSThread isMainThread]);
@autoreleasepool { @autoreleasepool {
if (error->error) { 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 { } else {
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info; GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
LOG_INFO(@"%@ now reachable at %@", [server class], server.bonjourServerURL); LOG_INFO(@"%@ now reachable at %@", [server class], server.bonjourServerURL);
@@ -369,8 +410,10 @@ static inline NSString* _EncodeBase64(NSString* string) {
- (BOOL)_start:(NSError**)error { - (BOOL)_start:(NSError**)error {
DCHECK(_source == NULL); DCHECK(_source == NULL);
NSUInteger port = [_GetOption(_options, GCDWebServerOption_Port, @0) unsignedIntegerValue]; 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]; NSUInteger maxPendingConnections = [_GetOption(_options, GCDWebServerOption_MaxPendingConnections, @16) unsignedIntegerValue];
int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listeningSocket > 0) { if (listeningSocket > 0) {
@@ -467,14 +510,21 @@ static inline NSString* _EncodeBase64(NSString* string) {
_port = port; _port = port;
} }
if (name) { if (bonjourName) {
_service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (ARC_BRIDGE CFStringRef)name, (SInt32)_port); _registrationService = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), (ARC_BRIDGE CFStringRef)bonjourType, (ARC_BRIDGE CFStringRef)(bonjourName.length ? bonjourName : _serverName), (SInt32)_port);
if (_service) { if (_registrationService) {
CFNetServiceClientContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL}; 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}; 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 { } else {
LOG_ERROR(@"Failed creating CFNetService"); LOG_ERROR(@"Failed creating CFNetService");
} }
@@ -488,24 +538,24 @@ static inline NSString* _EncodeBase64(NSString* string) {
}); });
} }
} else { } else {
LOG_ERROR(@"Failed starting listening socket: %s (%i)", strerror(errno), errno);
if (error) { if (error) {
*error = GCDWebServerMakePosixError(errno); *error = GCDWebServerMakePosixError(errno);
} }
LOG_ERROR(@"Failed starting listening socket: %s (%i)", strerror(errno), errno);
close(listeningSocket); close(listeningSocket);
} }
} else { } else {
LOG_ERROR(@"Failed binding listening socket: %s (%i)", strerror(errno), errno);
if (error) { if (error) {
*error = GCDWebServerMakePosixError(errno); *error = GCDWebServerMakePosixError(errno);
} }
LOG_ERROR(@"Failed binding listening socket: %s (%i)", strerror(errno), errno);
close(listeningSocket); close(listeningSocket);
} }
} else { } else {
LOG_ERROR(@"Failed creating listening socket: %s (%i)", strerror(errno), errno);
if (error) { if (error) {
*error = GCDWebServerMakePosixError(errno); *error = GCDWebServerMakePosixError(errno);
} }
LOG_ERROR(@"Failed creating listening socket: %s (%i)", strerror(errno), errno);
} }
return (_source ? YES : NO); return (_source ? YES : NO);
} }
@@ -513,11 +563,19 @@ static inline NSString* _EncodeBase64(NSString* string) {
- (void)_stop { - (void)_stop {
DCHECK(_source != NULL); DCHECK(_source != NULL);
if (_service) { if (_registrationService) {
CFNetServiceUnscheduleFromRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes); if (_resolutionService) {
CFNetServiceSetClient(_service, NULL, NULL); CFNetServiceUnscheduleFromRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
CFRelease(_service); CFNetServiceSetClient(_resolutionService, NULL, NULL);
_service = 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); dispatch_source_cancel(_source);
@@ -535,6 +593,15 @@ static inline NSString* _EncodeBase64(NSString* string) {
ARC_RELEASE(_authenticationDigestAccounts); ARC_RELEASE(_authenticationDigestAccounts);
_authenticationDigestAccounts = nil; _authenticationDigestAccounts = nil;
dispatch_async(dispatch_get_main_queue(), ^{
if (_disconnectTimer) {
CFRunLoopTimerInvalidate(_disconnectTimer);
CFRelease(_disconnectTimer);
_disconnectTimer = NULL;
[self _didDisconnect];
}
});
LOG_INFO(@"%@ stopped", [self class]); LOG_INFO(@"%@ stopped", [self class]);
if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) { if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
@@ -591,7 +658,7 @@ static inline NSString* _EncodeBase64(NSString* string) {
} }
- (BOOL)isRunning { - (BOOL)isRunning {
return (_source ? YES : NO); return (_options ? YES : NO);
} }
- (void)stop { - (void)stop {
@@ -631,13 +698,14 @@ static inline NSString* _EncodeBase64(NSString* string) {
} }
- (NSURL*)bonjourServerURL { - (NSURL*)bonjourServerURL {
if (_source && _service) { if (_source && _resolutionService) {
CFStringRef name = CFNetServiceGetName(_service); NSString* name = (ARC_BRIDGE NSString*)CFNetServiceGetTargetHost(_resolutionService);
if (name && CFStringGetLength(name)) { if (name.length) {
name = [name substringToIndex:(name.length - 1)]; // Strip trailing period at end of domain
if (_port != 80) { 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 { } 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]); DCHECK([NSThread isMainThread]);
BOOL success = NO; BOOL success = NO;
_run = YES; _run = YES;
void (*handler)(int) = signal(SIGINT, _SignalHandler); void (*termHandler)(int) = signal(SIGTERM, _SignalHandler);
if (handler != SIG_ERR) { void (*intHandler)(int) = signal(SIGINT, _SignalHandler);
if ((termHandler != SIG_ERR) && (intHandler != SIG_ERR)) {
if ([self startWithOptions:options error:error]) { if ([self startWithOptions:options error:error]) {
while (_run) { while (_run) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true); CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
@@ -677,7 +746,9 @@ static inline NSString* _EncodeBase64(NSString* string) {
[self stop]; [self stop];
success = YES; success = YES;
} }
signal(SIGINT, handler); _ExecuteMainThreadRunLoopSources();
signal(SIGINT, intHandler);
signal(SIGTERM, termHandler);
} }
return success; return success;
} }
@@ -689,6 +760,12 @@ static inline NSString* _EncodeBase64(NSString* string) {
@implementation GCDWebServer (Handlers) @implementation GCDWebServer (Handlers)
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { - (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) { [self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
if (![requestMethod isEqualToString:method]) { 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]); 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 { - (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]]) { if ([path hasPrefix:@"/"] && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) {
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { [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]); return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
} processBlock:block]; } asyncProcessBlock:block];
} else { } else {
DNOT_REACHED(); DNOT_REACHED();
} }
} }
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block { - (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]; NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL];
if (expression && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) { if (expression && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) {
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { [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]) { if (![requestMethod isEqualToString:method]) {
return nil; 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 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 { } else {
DNOT_REACHED(); DNOT_REACHED();
} }
@@ -814,17 +916,18 @@ static inline NSString* _EncodeBase64(NSString* string) {
GCDWebServerResponse* response = nil; GCDWebServerResponse* response = nil;
NSString* filePath = [directoryPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]]; NSString* filePath = [directoryPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]];
BOOL isDirectory; NSString* fileType = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL] fileType];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]) { if (fileType) {
if (isDirectory) { if ([fileType isEqualToString:NSFileTypeDirectory]) {
if (indexFilename) { if (indexFilename) {
NSString* indexPath = [filePath stringByAppendingPathComponent: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]; return [GCDWebServerFileResponse responseWithFile:indexPath];
} }
} }
response = [server _responseWithContentsOfDirectory:filePath]; response = [server _responseWithContentsOfDirectory:filePath];
} else { } else if ([fileType isEqualToString:NSFileTypeRegular]) {
if (allowRangeRequests) { if (allowRangeRequests) {
response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange]; response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange];
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"]; [response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
@@ -930,7 +1033,7 @@ static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData,
while (1) { while (1) {
ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length); ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length);
if (result < 0) { if (result < 0) {
length = NSNotFound; length = NSUIntegerMax;
break; break;
} else if (result == 0) { } else if (result == 0) {
break; break;
@@ -940,7 +1043,7 @@ static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData,
outData.length = 2 * outData.length; outData.length = 2 * outData.length;
} }
} }
if (length != NSNotFound) { if (length != NSUIntegerMax) {
outData.length = length; outData.length = length;
response = _CreateHTTPMessageFromData(outData, NO); response = _CreateHTTPMessageFromData(outData, NO);
} else { } else {
@@ -964,9 +1067,11 @@ static void _LogResult(NSString* format, ...) {
} }
- (NSInteger)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path { - (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 NSArray* ignoredHeaders = @[@"Date", @"Etag"]; // Dates are always different by definition and ETags depend on file system node IDs
NSInteger result = -1; NSInteger result = -1;
if ([self startWithOptions:options error:NULL]) { if ([self startWithOptions:options error:NULL]) {
_ExecuteMainThreadRunLoopSources();
result = 0; result = 0;
NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL]; NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
@@ -1022,8 +1127,13 @@ static void _LogResult(NSString* format, ...) {
} }
} }
NSString* expectedContentLength = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(expectedResponse, CFSTR("Content-Length")));
NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse)); NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse));
NSString* actualContentLength = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(actualResponse, CFSTR("Content-Length")));
NSData* actualBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(actualResponse)); 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]) { if (![actualBody isEqualToData:expectedBody]) {
_LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length); _LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length);
success = NO; success = NO;
@@ -1064,9 +1174,12 @@ static void _LogResult(NSString* format, ...) {
++result; ++result;
} }
} }
_ExecuteMainThreadRunLoopSources();
} }
[self stop]; [self stop];
_ExecuteMainThreadRunLoopSources();
} }
return result; return result;
} }

View File

@@ -117,6 +117,14 @@
*/ */
- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length; - (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 * Assuming a valid HTTP request was received, this method is called before
* the request is processed. * the request is processed.
@@ -130,13 +138,14 @@
/** /**
* Assuming a valid HTTP request was received and -preflightRequest: returned nil, * 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: * 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. * this method is called to override the response.
* *
* You can either modify the current response and return it, or return a * You can either modify the current response and return it, or return a

View File

@@ -373,15 +373,24 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatRFC822([NSDate date])); 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 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
- (void)_processRequest { - (void)_finishProcessingRequest:(GCDWebServerResponse*)response {
DCHECK(_responseMessage == NULL); DCHECK(_responseMessage == NULL);
BOOL hasBody = NO; BOOL hasBody = NO;
GCDWebServerResponse* response = [self preflightRequest:_request];
if (!response) {
response = [self processRequest:_request withBlock:_handler.processBlock];
}
if (response) { if (response) {
response = [self overrideResponse:response forRequest:_request]; response = [self overrideResponse:response forRequest:_request];
} }
@@ -416,7 +425,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
if (_response.contentType != nil) { if (_response.contentType != nil) {
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType)); 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]); CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]);
} }
if (_response.usesChunkedTransferEncoding) { if (_response.usesChunkedTransferEncoding) {
@@ -471,7 +480,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
NSError* localError = nil; NSError* localError = nil;
if ([_request performClose:&localError]) { if ([_request performClose:&localError]) {
[self _processRequest]; [self _startProcessingRequest];
} else { } else {
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
@@ -480,7 +489,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
}]; }];
} else { } else {
if ([_request performClose:&error]) { if ([_request performClose:&error]) {
[self _processRequest]; [self _startProcessingRequest];
} else { } else {
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
@@ -501,7 +510,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
NSError* localError = nil; NSError* localError = nil;
if ([_request performClose:&localError]) { if ([_request performClose:&localError]) {
[self _processRequest]; [self _startProcessingRequest];
} else { } else {
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error); LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError]; [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
@@ -522,11 +531,15 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
requestMethod = @"GET"; requestMethod = @"GET";
_virtualHEAD = YES; _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)); 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* 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; 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* 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) { if (requestMethod && requestURL && requestHeaders && requestPath && requestQuery) {
for (_handler in _server.handlers) { for (_handler in _server.handlers) {
_request = ARC_RETAIN(_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery)); _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]; [self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_BadRequest];
} }
} else { } else {
[self _processRequest]; [self _startProcessingRequest];
} }
} else { } else {
_request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery]; _request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery];
@@ -713,6 +726,10 @@ static NSString* _StringFromAddressData(NSData* data) {
#endif #endif
} }
- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary*)headers {
return url;
}
// https://tools.ietf.org/html/rfc2617 // https://tools.ietf.org/html/rfc2617
- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request { - (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); 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; 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); 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 { @try {
response = block(request); _handler.asyncProcessBlock(request, completion);
} }
@catch (NSException* exception) { @catch (NSException* exception) {
LOG_EXCEPTION(exception); LOG_EXCEPTION(exception);
} }
return response;
} }
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26

View File

@@ -198,15 +198,19 @@ NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
[scanner setScanLocation:([scanner scanLocation] + 1)]; [scanner setScanLocation:([scanner scanLocation] + 1)];
NSString* value = nil; NSString* value = nil;
if (![scanner scanUpToString:@"&" intoString:&value]) { [scanner scanUpToString:@"&" intoString:&value];
break; if (value == nil) {
value = @"";
} }
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "]; key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
NSString* unescapedKey = key ? GCDWebServerUnescapeURLString(key) : nil;
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "]; value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
if (key && value) { NSString* unescapedValue = value ? GCDWebServerUnescapeURLString(value) : nil;
[parameters setObject:GCDWebServerUnescapeURLString(value) forKey:GCDWebServerUnescapeURLString(key)]; if (unescapedKey && unescapedValue) {
[parameters setObject:unescapedValue forKey:unescapedKey];
} else { } else {
LOG_WARNING(@"Failed parsing URL encoded form for key \"%@\" and value \"%@\"", key, value);
DNOT_REACHED(); DNOT_REACHED();
} }

View File

@@ -25,8 +25,7 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
#import <TargetConditionals.h> #import <os/object.h>
#import <AvailabilityMacros.h>
#if __has_feature(objc_arc) #if __has_feature(objc_arc)
#define ARC_BRIDGE __bridge #define ARC_BRIDGE __bridge
@@ -35,7 +34,7 @@
#define ARC_RELEASE(__OBJECT__) #define ARC_RELEASE(__OBJECT__)
#define ARC_AUTORELEASE(__OBJECT__) __OBJECT__ #define ARC_AUTORELEASE(__OBJECT__) __OBJECT__
#define ARC_DEALLOC(__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_RETAIN(__OBJECT__)
#define ARC_DISPATCH_RELEASE(__OBJECT__) #define ARC_DISPATCH_RELEASE(__OBJECT__)
#else #else
@@ -111,7 +110,7 @@ extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_
#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain" #define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
static inline BOOL GCDWebServerIsValidByteRange(NSRange range) { 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) { static inline NSError* GCDWebServerMakePosixError(int code) {
@@ -144,8 +143,7 @@ extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_F
@interface GCDWebServerHandler : NSObject @interface GCDWebServerHandler : NSObject
@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock; @property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock;
@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock; @property(nonatomic, readonly) GCDWebServerAsyncProcessBlock asyncProcessBlock;
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
@end @end
@interface GCDWebServerRequest () @interface GCDWebServerRequest ()
@@ -154,6 +152,7 @@ extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_F
- (BOOL)performOpen:(NSError**)error; - (BOOL)performOpen:(NSError**)error;
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error; - (BOOL)performWriteData:(NSData*)data error:(NSError**)error;
- (BOOL)performClose:(NSError**)error; - (BOOL)performClose:(NSError**)error;
- (void)setAttribute:(id)attribute forKey:(NSString*)key;
@end @end
@interface GCDWebServerResponse () @interface GCDWebServerResponse ()

View File

@@ -27,6 +27,15 @@
#import <Foundation/Foundation.h> #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 * This protocol is used by the GCDWebServerConnection to communicate with
* the GCDWebServerRequest and write the received HTTP body data. * 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 * Returns the content length for the body of the request parsed from the
* "Content-Length" header. * "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 * if there is a body but no "Content-Length" header, typically because
* chunked transfer encoding is used. * chunked transfer encoding is used.
*/ */
@@ -136,9 +145,9 @@
@property(nonatomic, readonly) NSString* ifNoneMatch; @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 * 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; @property(nonatomic, readonly) NSRange byteRange;
@@ -163,4 +172,11 @@
*/ */
- (BOOL)hasByteRange; - (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 @end

View File

@@ -29,6 +29,8 @@
#import "GCDWebServerPrivate.h" #import "GCDWebServerPrivate.h"
NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerRequestAttribute_RegexCaptures";
#define kZlibErrorDomain @"ZlibErrorDomain" #define kZlibErrorDomain @"ZlibErrorDomain"
#define kGZipInitialBufferSize (256 * 1024) #define kGZipInitialBufferSize (256 * 1024)
@@ -152,6 +154,7 @@
BOOL _opened; BOOL _opened;
NSMutableArray* _decoders; NSMutableArray* _decoders;
NSMutableDictionary* _attributes;
id<GCDWebServerBodyWriter> __unsafe_unretained _writer; id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
} }
@end @end
@@ -187,14 +190,14 @@
if (_type == nil) { if (_type == nil) {
_type = kGCDWebServerDefaultMimeType; _type = kGCDWebServerDefaultMimeType;
} }
_length = NSNotFound; _length = NSUIntegerMax;
} else { } else {
if (_type) { if (_type) {
DNOT_REACHED(); DNOT_REACHED();
ARC_RELEASE(self); ARC_RELEASE(self);
return nil; return nil;
} }
_length = NSNotFound; _length = NSUIntegerMax;
} }
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"]; NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
@@ -203,7 +206,7 @@
} }
_noneMatch = ARC_RETAIN([_headers objectForKey:@"If-None-Match"]); _noneMatch = ARC_RETAIN([_headers objectForKey:@"If-None-Match"]);
_range = NSMakeRange(NSNotFound, 0); _range = NSMakeRange(NSUIntegerMax, 0);
NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]); NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
if (rangeHeader) { if (rangeHeader) {
if ([rangeHeader hasPrefix:@"bytes="]) { if ([rangeHeader hasPrefix:@"bytes="]) {
@@ -222,13 +225,13 @@
_range.location = startValue; _range.location = startValue;
_range.length = NSUIntegerMax; _range.length = NSUIntegerMax;
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500" } else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
_range.location = NSNotFound; _range.location = NSUIntegerMax;
_range.length = endValue; _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); LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
} }
} }
@@ -238,6 +241,7 @@
} }
_decoders = [[NSMutableArray alloc] init]; _decoders = [[NSMutableArray alloc] init];
_attributes = [[NSMutableDictionary alloc] init];
} }
return self; return self;
} }
@@ -252,6 +256,7 @@
ARC_RELEASE(_modifiedSince); ARC_RELEASE(_modifiedSince);
ARC_RELEASE(_noneMatch); ARC_RELEASE(_noneMatch);
ARC_RELEASE(_decoders); ARC_RELEASE(_decoders);
ARC_RELEASE(_attributes);
ARC_DEALLOC(super); ARC_DEALLOC(super);
} }
@@ -264,6 +269,10 @@
return GCDWebServerIsValidByteRange(_range); return GCDWebServerIsValidByteRange(_range);
} }
- (id)attributeForKey:(NSString*)key {
return [_attributes objectForKey:key];
}
- (BOOL)open:(NSError**)error { - (BOOL)open:(NSError**)error {
return YES; return YES;
} }
@@ -307,6 +316,10 @@
return [_writer close:error]; return [_writer close:error];
} }
- (void)setAttribute:(id)attribute forKey:(NSString*)key {
[_attributes setValue:attribute forKey:key];
}
- (NSString*)description { - (NSString*)description {
NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path]; NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path];
for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) { for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) {

View File

@@ -87,12 +87,12 @@
/** /**
* Sets the content length for the body of the response. If a body is present * 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 * cannot be known ahead of time. Chunked transfer encoding will be
* automatically enabled by the GCDWebServerConnection to comply with HTTP/1.1 * automatically enabled by the GCDWebServerConnection to comply with HTTP/1.1
* specifications. * 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. * is undefined.
*/ */
@property(nonatomic) NSUInteger contentLength; @property(nonatomic) NSUInteger contentLength;

View File

@@ -81,7 +81,7 @@
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader { - (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
if ((self = [super initWithResponse:response reader: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"]; [response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
} }
return self; return self;
@@ -180,7 +180,7 @@
- (instancetype)init { - (instancetype)init {
if ((self = [super init])) { if ((self = [super init])) {
_type = nil; _type = nil;
_length = NSNotFound; _length = NSUIntegerMax;
_status = kGCDWebServerHTTPStatusCode_OK; _status = kGCDWebServerHTTPStatusCode_OK;
_maxAge = 0; _maxAge = 0;
_headers = [[NSMutableDictionary alloc] init]; _headers = [[NSMutableDictionary alloc] init];
@@ -208,7 +208,7 @@
} }
- (BOOL)usesChunkedTransferEncoding { - (BOOL)usesChunkedTransferEncoding {
return (_type != nil) && (_length == NSNotFound); return (_type != nil) && (_length == NSUIntegerMax);
} }
- (BOOL)open:(NSError**)error { - (BOOL)open:(NSError**)error {
@@ -259,7 +259,7 @@
if (_type) { if (_type) {
[description appendFormat:@"\nContent Type = %@", _type]; [description appendFormat:@"\nContent Type = %@", _type];
} }
if (_length != NSNotFound) { if (_length != NSUIntegerMax) {
[description appendFormat:@"\nContent Length = %lu", (unsigned long)_length]; [description appendFormat:@"\nContent Length = %lu", (unsigned long)_length];
} }
[description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge]; [description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge];

View File

@@ -49,7 +49,7 @@
} }
- (BOOL)open:(NSError**)error { - (BOOL)open:(NSError**)error {
if (self.contentLength != NSNotFound) { if (self.contentLength != NSUIntegerMax) {
_data = [[NSMutableData alloc] initWithCapacity:self.contentLength]; _data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
} else { } else {
_data = [[NSMutableData alloc] init]; _data = [[NSMutableData alloc] init];
@@ -97,7 +97,8 @@
- (id)jsonObject { - (id)jsonObject {
if (_jsonObject == nil) { 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]); _jsonObject = ARC_RETAIN([NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL]);
} else { } else {
DNOT_REACHED(); DNOT_REACHED();

View File

@@ -77,9 +77,9 @@
/** /**
* Initializes a response like -initWithFile: but restricts the file contents * 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, * 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 * and "length" values will be automatically adjusted to be compatible with the
* actual size of the file. * actual size of the file.
* *

View File

@@ -59,11 +59,11 @@
} }
- (instancetype)initWithFile:(NSString*)path { - (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 { - (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 { - (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range {
@@ -81,31 +81,41 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
ARC_RELEASE(self); ARC_RELEASE(self);
return nil; return nil;
} }
if (GCDWebServerIsValidByteRange(range)) { #ifndef __LP64__
if (range.location != NSNotFound) { 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)
range.location = MIN(range.location, (NSUInteger)info.st_size); DNOT_REACHED();
range.length = MIN(range.length, (NSUInteger)info.st_size - range.location); 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 { } else {
range.length = MIN(range.length, (NSUInteger)info.st_size); range.length = MIN(range.length, fileSize);
range.location = (NSUInteger)info.st_size - range.length; range.location = fileSize - range.length;
} }
if (range.length == 0) { if (range.length == 0) {
ARC_RELEASE(self); ARC_RELEASE(self);
return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header 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])) { if ((self = [super init])) {
_path = [path copy]; _path = [path copy];
if (range.location != NSNotFound) { _offset = range.location;
_offset = range.location; _size = range.length;
_size = range.length; if (hasByteRange) {
[self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent]; [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"]; [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 [%i-%i] for file \"%@\"", (int)range.location, (int)(range.location + range.length - 1), path); LOG_DEBUG(@"Using content bytes range [%lu-%lu] for file \"%@\"", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), path);
} else {
_offset = 0;
_size = (NSUInteger)info.st_size;
} }
if (attachment) { if (attachment) {
@@ -121,8 +131,8 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
} }
} }
self.contentType = GCDWebServerGetMimeTypeForExtension([path pathExtension]); self.contentType = GCDWebServerGetMimeTypeForExtension([_path pathExtension]);
self.contentLength = (range.location != NSNotFound ? range.length : (NSUInteger)info.st_size); self.contentLength = _size;
self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec); 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]; self.eTag = [NSString stringWithFormat:@"%llu/%li/%li", info.st_ino, info.st_mtimespec.tv_sec, info.st_mtimespec.tv_nsec];
} }

View File

@@ -28,11 +28,11 @@
#import "GCDWebServerStreamedResponse.h" #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 * The block must return empty NSData when done or nil on error and set the
* "error" argument which is guaranteed to be non-NULL. * "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 * 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. * 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. * This method is the designated initializer for the class.
*/ */
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamingBlock)block; - (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
@end @end

View File

@@ -29,17 +29,17 @@
@interface GCDWebServerStreamedResponse () { @interface GCDWebServerStreamedResponse () {
@private @private
GCDWebServerStreamingBlock _block; GCDWebServerStreamBlock _block;
} }
@end @end
@implementation GCDWebServerStreamedResponse @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]); 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])) { if ((self = [super init])) {
_block = [block copy]; _block = [block copy];

View File

@@ -57,6 +57,11 @@
@implementation GCDWebUploader (Methods) @implementation GCDWebUploader (Methods)
// Must match implementation in GCDWebDAVServer
- (BOOL)_checkSandboxedPath:(NSString*)path {
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
}
- (BOOL)_checkFileExtension:(NSString*)fileName { - (BOOL)_checkFileExtension:(NSString*)fileName {
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) { if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
return NO; return NO;
@@ -85,8 +90,8 @@
- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request { - (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request {
NSString* relativePath = [[request query] objectForKey:@"path"]; NSString* relativePath = [[request query] objectForKey:@"path"];
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory; 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]; return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
} }
if (!isDirectory) { if (!isDirectory) {
@@ -129,8 +134,8 @@
- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request { - (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request {
NSString* relativePath = [[request query] objectForKey:@"path"]; NSString* relativePath = [[request query] objectForKey:@"path"];
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory; 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]; return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
} }
if (isDirectory) { if (isDirectory) {
@@ -160,6 +165,9 @@
} }
NSString* relativePath = [[request firstArgumentForControlName:@"path"] string]; NSString* relativePath = [[request firstArgumentForControlName:@"path"] string];
NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]]; 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]) { if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath]; return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath];
@@ -181,13 +189,16 @@
- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request { - (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request {
NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"]; NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"];
NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath]; NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath];
BOOL isDirectory; BOOL isDirectory = NO;
if (![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) { if (![self _checkSandboxedPath:oldAbsolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath]; return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
} }
NSString* newRelativePath = [request.arguments objectForKey:@"newPath"]; NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]]; 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]; NSString* itemName = [newAbsolutePath lastPathComponent];
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) { if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
@@ -215,7 +226,7 @@
NSString* relativePath = [request.arguments objectForKey:@"path"]; NSString* relativePath = [request.arguments objectForKey:@"path"];
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath]; NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
BOOL isDirectory = NO; 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]; return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
} }
@@ -244,6 +255,9 @@
- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request { - (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request {
NSString* relativePath = [request.arguments objectForKey:@"path"]; NSString* relativePath = [request.arguments objectForKey:@"path"];
NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]]; 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]; NSString* directoryName = [absolutePath lastPathComponent];
if (!_allowHidden && [directoryName hasPrefix:@"."]) { if (!_allowHidden && [directoryName hasPrefix:@"."]) {
@@ -308,6 +322,9 @@
NSString* title = server.title; NSString* title = server.title;
if (title == nil) { if (title == nil) {
title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
if (title == nil) {
title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
}
#if !TARGET_OS_IPHONE #if !TARGET_OS_IPHONE
if (title == nil) { if (title == nil) {
title = [[NSProcessInfo processInfo] processName]; title = [[NSProcessInfo processInfo] processName];

View File

@@ -52,6 +52,7 @@ typedef enum {
kMode_WebDAV, kMode_WebDAV,
kMode_WebUploader, kMode_WebUploader,
kMode_StreamingResponse, kMode_StreamingResponse,
kMode_AsyncResponse
} Mode; } Mode;
@interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate> @interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate>
@@ -142,7 +143,7 @@ int main(int argc, const char* argv[]) {
NSString* authenticationPassword = nil; NSString* authenticationPassword = nil;
if (argc == 1) { 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 { } else {
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
if (argv[i][0] != '-') { if (argv[i][0] != '-') {
@@ -164,6 +165,8 @@ int main(int argc, const char* argv[]) {
mode = kMode_WebUploader; mode = kMode_WebUploader;
} else if (!strcmp(argv[i], "streamingResponse")) { } else if (!strcmp(argv[i], "streamingResponse")) {
mode = kMode_StreamingResponse; mode = kMode_StreamingResponse;
} else if (!strcmp(argv[i], "asyncResponse")) {
mode = kMode_AsyncResponse;
} }
} else if (!strcmp(argv[i], "-record")) { } else if (!strcmp(argv[i], "-record")) {
recording = YES; recording = YES;
@@ -328,6 +331,24 @@ int main(int argc, const char* argv[]) {
break; 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) #if __has_feature(objc_arc)
fprintf(stdout, " (ARC is ON)\n"); fprintf(stdout, " (ARC is ON)\n");
@@ -337,11 +358,14 @@ int main(int argc, const char* argv[]) {
if (webServer) { if (webServer) {
Delegate* delegate = [[Delegate alloc] init]; Delegate* delegate = [[Delegate alloc] init];
webServer.delegate = delegate;
if (testDirectory) { if (testDirectory) {
#ifndef NDEBUG
webServer.delegate = delegate;
#endif
fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]); fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]);
result = (int)[webServer runTestsWithOptions:@{GCDWebServerOption_Port: @8080} inDirectory:testDirectory]; result = (int)[webServer runTestsWithOptions:@{GCDWebServerOption_Port: @8080} inDirectory:testDirectory];
} else { } else {
webServer.delegate = delegate;
if (recording) { if (recording) {
fprintf(stdout, "<RECORDING ENABLED>\n"); fprintf(stdout, "<RECORDING ENABLED>\n");
webServer.recordingEnabled = YES; webServer.recordingEnabled = YES;
@@ -363,9 +387,10 @@ int main(int argc, const char* argv[]) {
result = 0; result = 0;
} }
} }
webServer.delegate = nil;
#if !__has_feature(objc_arc) #if !__has_feature(objc_arc)
[webServer release];
[delegate release]; [delegate release];
[webServer release];
#endif #endif
} }
} }

117
README.md
View File

@@ -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) * Available under a friendly [New BSD License](LICENSE)
Extra built-in features: 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 * 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) * 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 * [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: 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: 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: Or this line for GCDWebDAVServer:
``` ```
pod "GCDWebServer/WebDAV", "~> 2.0" pod "GCDWebServer/WebDAV", "~> 3.0"
``` ```
Hello World 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]; [webServer runWithPort:8080 bonjourName:nil];
NSLog(@"Visit %@ in your web browser", webServer.serverURL);
} }
return 0; return 0;
@@ -90,7 +93,12 @@ int main(int argc, const char* argv[]) {
#import "GCDWebServer.h" #import "GCDWebServer.h"
#import "GCDWebServerDataResponse.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 { - (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 // Start server on port 8080
[_webServer startWithPort:8080 bonjourName:nil]; [_webServer startWithPort:8080 bonjourName:nil];
NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
return YES; 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 Web Based Uploads in iOS Apps
@@ -123,7 +189,12 @@ Simply instantiate and run a ```GCDWebUploader``` instance then visit ```http://
```objectivec ```objectivec
#import "GCDWebUploader.h" #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 { - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; 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); NSLog(@"Visit %@ in your web browser", _webUploader.serverURL);
return YES; return YES;
} }
@end
``` ```
WebDAV Server in iOS Apps WebDAV Server in iOS Apps
@@ -146,7 +219,12 @@ Simply instantiate and run a ```GCDWebDAVServer``` instance then connect to ```h
```objectivec ```objectivec
#import "GCDWebDAVServer.h" #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 { - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; 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); NSLog(@"Visit %@ in your WebDAV client", _davServer.serverURL);
return YES; return YES;
} }
@end
``` ```
Serving a Static Website Serving a Static Website
@@ -202,9 +282,9 @@ GCDWebServer relies on "handlers" to process incoming web requests and generatin
Handlers require 2 GCD blocks: 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 ```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 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. 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 disable this behavior, make sure to define the preprocessor constant ```NDEBUG``` when compiling GCDWebServer. In Xcode target settings, this can be done by adding ```NDEBUG``` to the build setting ```GCC_PREPROCESSOR_DEFINITIONS``` when building in Release configuration (this is done automatically for you if you use CocoaPods).
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 if NDEBUG is defined
#define LOG_VERBOSE(...)
#define LOG_INFO(...)
#define LOG_WARNING(...)
#define LOG_ERROR(...)
#define LOG_EXCEPTION(__EXCEPTION__)
#define DCHECK(__CONDITION__) // Should not do anything if NDEBUG is defined or abort if __CONDITION__ is false
#define DNOT_REACHED() // Should not do anything if NDEBUG is defined
```
Advanced Example 1: Implementing HTTP Redirects Advanced Example 1: Implementing HTTP Redirects
=============================================== ===============================================

View File

@@ -23,22 +23,44 @@ function runTests {
rm -rf "$PAYLOAD_DIR" rm -rf "$PAYLOAD_DIR"
ditto -x -k "$PAYLOAD_ZIP" "$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 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" 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" 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 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" 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 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" 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 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" 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 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 $ARC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
runTests $MRC_PRODUCT "webUploader" "Tests/WebUploader" runTests $MRC_PRODUCT "webUploader" "Tests/WebUploader"
runTests $ARC_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 # Done
echo "\nAll tests completed successfully!" echo "\nAll tests completed successfully!"

BIN
Tests/Sample-Movie.mp4 Normal file

Binary file not shown.

Binary file not shown.

View 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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View 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

Binary file not shown.

View 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