mirror of
https://github.com/swisspol/GCDWebServer.git
synced 2026-01-31 00:00:04 +08:00
Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e59cf4b6df | ||
|
|
9e8f0e00f3 | ||
|
|
d7650a71e0 | ||
|
|
420ddc3eac | ||
|
|
143e38c968 | ||
|
|
8e9fe4c4c1 | ||
|
|
95bccff2f7 | ||
|
|
780a608d6c | ||
|
|
18d93bbf47 | ||
|
|
b35ebd7d58 | ||
|
|
a11b047233 | ||
|
|
4eac9d4f8e | ||
|
|
d1e2a1a12f | ||
|
|
54d5abd3a8 | ||
|
|
a9fee8d7e2 | ||
|
|
6b15bdaa4e | ||
|
|
3771cf4e92 | ||
|
|
a5d83abdd0 | ||
|
|
f1e9f1a37c | ||
|
|
00b5ec87ba | ||
|
|
cf94e70a42 | ||
|
|
d47409c776 | ||
|
|
a9db13475b | ||
|
|
17fad0f1b9 | ||
|
|
5493d9e803 | ||
|
|
12b1edb958 | ||
|
|
7544a6dc4e | ||
|
|
f9621c8aac | ||
|
|
7a93b27478 | ||
|
|
9d48f9ec12 | ||
|
|
7c6e85cf9a | ||
|
|
24fbd161d8 | ||
|
|
0ae0d4175a | ||
|
|
6d550a02b7 | ||
|
|
a7f46b762f | ||
|
|
d1c7f9a323 | ||
|
|
93bfe65211 | ||
|
|
8ab53f74d5 | ||
|
|
04a69787bf | ||
|
|
dfd019de7d | ||
|
|
4db631fa27 | ||
|
|
ba03d756c6 | ||
|
|
04f59a9214 | ||
|
|
40ea252ad6 | ||
|
|
c193860468 | ||
|
|
94ad8c745e | ||
|
|
56c096996f | ||
|
|
8cbaf0f867 | ||
|
|
295901c0b3 | ||
|
|
2dda0c98ce | ||
|
|
70a38c8b01 | ||
|
|
1b12a7bd14 | ||
|
|
75e6332500 | ||
|
|
420ed719e8 | ||
|
|
3b75f9dd20 | ||
|
|
f01307b2a7 | ||
|
|
1f5e650423 | ||
|
|
d404112a88 | ||
|
|
dd3f539f74 | ||
|
|
0c51d09b69 | ||
|
|
0c53c52dd4 | ||
|
|
a687b52563 | ||
|
|
c8c34aa61f | ||
|
|
ed709d1476 | ||
|
|
3dc7cb0ec4 | ||
|
|
142f007e58 | ||
|
|
c8d2b225ba | ||
|
|
f7d6da55cd | ||
|
|
5a0c274807 | ||
|
|
591da12aa3 | ||
|
|
72475429e4 | ||
|
|
01da5969e4 | ||
|
|
46890a0642 | ||
|
|
143ca5b99f | ||
|
|
519866bc03 | ||
|
|
a93cac5ea6 | ||
|
|
43b578677f | ||
|
|
10cbe27e50 | ||
|
|
b1169ce7d1 | ||
|
|
766072eb89 | ||
|
|
0807cf5fe6 | ||
|
|
c6701cd474 | ||
|
|
b6866bee8e | ||
|
|
075774b6c0 | ||
|
|
7743d18006 | ||
|
|
633d40f155 | ||
|
|
5d82a80a34 | ||
|
|
3e5fe3f956 | ||
|
|
5a26a09d8e | ||
|
|
51e8dbe475 | ||
|
|
4a3d4537a3 | ||
|
|
a5208bd60f | ||
|
|
2597d6da00 | ||
|
|
1ca5a56952 | ||
|
|
80cff01154 | ||
|
|
1e17d5c455 | ||
|
|
ce1eb3c29a | ||
|
|
f11647ee7f | ||
|
|
60f281fd30 | ||
|
|
181984ba6d | ||
|
|
4685fae29c | ||
|
|
8619799e62 | ||
|
|
b1061378fb | ||
|
|
9bc2bfa506 | ||
|
|
078c5e7bc3 | ||
|
|
3f304b3c14 | ||
|
|
80348079a6 | ||
|
|
099b2a03e6 | ||
|
|
f0c63f4776 | ||
|
|
4e3aa3bc5c | ||
|
|
c5d82b85ed | ||
|
|
b1181d2e40 | ||
|
|
d931e04d47 | ||
|
|
016c4da6d2 | ||
|
|
38dd39a789 | ||
|
|
748f6a8bc9 |
@@ -29,30 +29,128 @@
|
||||
|
||||
@class GCDWebDAVServer;
|
||||
|
||||
// These methods are always called on main thread
|
||||
/**
|
||||
* Delegate methods for GCDWebDAVServer.
|
||||
*
|
||||
* @warning These methods are always called on the main thread in a serialized way.
|
||||
*/
|
||||
@protocol GCDWebDAVServerDelegate <GCDWebServerDelegate>
|
||||
@optional
|
||||
|
||||
/**
|
||||
* This method is called whenever a file has been downloaded.
|
||||
*/
|
||||
- (void)davServer:(GCDWebDAVServer*)server didDownloadFileAtPath:(NSString*)path;
|
||||
|
||||
/**
|
||||
* This method is called whenever a file has been uploaded.
|
||||
*/
|
||||
- (void)davServer:(GCDWebDAVServer*)server didUploadFileAtPath:(NSString*)path;
|
||||
|
||||
/**
|
||||
* This method is called whenever a file or directory has been moved.
|
||||
*/
|
||||
- (void)davServer:(GCDWebDAVServer*)server didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||
|
||||
/**
|
||||
* This method is called whenever a file or directory has been copied.
|
||||
*/
|
||||
- (void)davServer:(GCDWebDAVServer*)server didCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||
|
||||
/**
|
||||
* This method is called whenever a file or directory has been deleted.
|
||||
*/
|
||||
- (void)davServer:(GCDWebDAVServer*)server didDeleteItemAtPath:(NSString*)path;
|
||||
|
||||
/**
|
||||
* This method is called whenever a directory has been created.
|
||||
*/
|
||||
- (void)davServer:(GCDWebDAVServer*)server didCreateDirectoryAtPath:(NSString*)path;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The GCDWebDAVServer subclass of GCDWebServer implements a class 1 compliant
|
||||
* WebDAV server. It is also partially class 2 compliant but only when the
|
||||
* client is the OS X WebDAV implementation (so it can work with the OS X Finder).
|
||||
*
|
||||
* See the README.md file for more information about the features of GCDWebDAVServer.
|
||||
*/
|
||||
@interface GCDWebDAVServer : GCDWebServer
|
||||
|
||||
/**
|
||||
* Returns the upload directory as specified when the server was initialized.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* uploadDirectory;
|
||||
|
||||
/**
|
||||
* Sets the delegate for the server.
|
||||
*/
|
||||
@property(nonatomic, assign) id<GCDWebDAVServerDelegate> delegate;
|
||||
@property(nonatomic, copy) NSArray* allowedFileExtensions; // Default is nil i.e. all file extensions are allowed
|
||||
@property(nonatomic) BOOL showHiddenFiles; // Default is NO
|
||||
|
||||
/**
|
||||
* Sets which files are allowed to be operated on depending on their extension.
|
||||
*
|
||||
* The default value is nil i.e. all file extensions are allowed.
|
||||
*/
|
||||
@property(nonatomic, copy) NSArray* allowedFileExtensions;
|
||||
|
||||
/**
|
||||
* Sets if files and directories whose name start with a period are allowed to
|
||||
* be operated on.
|
||||
*
|
||||
* The default value is NO.
|
||||
*/
|
||||
@property(nonatomic) BOOL allowHiddenItems;
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)initWithUploadDirectory:(NSString*)path;
|
||||
|
||||
@end
|
||||
|
||||
// These methods can be called from any thread
|
||||
/**
|
||||
* Hooks to customize the behavior of GCDWebDAVServer.
|
||||
*
|
||||
* @warning These methods can be called on any GCD thread.
|
||||
*/
|
||||
@interface GCDWebDAVServer (Subclassing)
|
||||
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath; // Default implementation returns YES
|
||||
- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath; // Default implementation returns YES
|
||||
- (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath; // Default implementation returns YES
|
||||
- (BOOL)shouldDeleteItemAtPath:(NSString*)path; // Default implementation returns YES
|
||||
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path; // Default implementation returns YES
|
||||
|
||||
/**
|
||||
* This method is called to check if a file upload is allowed to complete.
|
||||
* The uploaded file is available for inspection at "tempPath".
|
||||
*
|
||||
* The default implementation returns YES.
|
||||
*/
|
||||
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath;
|
||||
|
||||
/**
|
||||
* This method is called to check if a file or directory is allowed to be moved.
|
||||
*
|
||||
* The default implementation returns YES.
|
||||
*/
|
||||
- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||
|
||||
/**
|
||||
* This method is called to check if a file or directory is allowed to be copied.
|
||||
*
|
||||
* The default implementation returns YES.
|
||||
*/
|
||||
- (BOOL)shouldCopyItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||
|
||||
/**
|
||||
* This method is called to check if a file or directory is allowed to be deleted.
|
||||
*
|
||||
* The default implementation returns YES.
|
||||
*/
|
||||
- (BOOL)shouldDeleteItemAtPath:(NSString*)path;
|
||||
|
||||
/**
|
||||
* This method is called to check if a directory is allowed to be created.
|
||||
*
|
||||
* The default implementation returns YES.
|
||||
*/
|
||||
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path;
|
||||
|
||||
@end
|
||||
|
||||
@@ -55,12 +55,17 @@ typedef NS_ENUM(NSInteger, DAVProperties) {
|
||||
@private
|
||||
NSString* _uploadDirectory;
|
||||
NSArray* _allowedExtensions;
|
||||
BOOL _showHidden;
|
||||
BOOL _allowHidden;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebDAVServer (Methods)
|
||||
|
||||
// Must match implementation in GCDWebUploader
|
||||
- (BOOL)_checkSandboxedPath:(NSString*)path {
|
||||
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
|
||||
}
|
||||
|
||||
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
||||
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
||||
return NO;
|
||||
@@ -87,12 +92,12 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
NSString* itemName = [absolutePath lastPathComponent];
|
||||
if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
if (([itemName hasPrefix:@"."] && !_allowHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading item name \"%@\" is not allowed", itemName];
|
||||
}
|
||||
|
||||
@@ -116,7 +121,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
if (![absolutePath hasPrefix:_uploadDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
BOOL isDirectory;
|
||||
@@ -130,7 +135,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
}
|
||||
|
||||
NSString* fileName = [absolutePath lastPathComponent];
|
||||
if (([fileName hasPrefix:@"."] && !_showHidden) || ![self _checkFileExtension:fileName]) {
|
||||
if (([fileName hasPrefix:@"."] && !_allowHidden) || ![self _checkFileExtension:fileName]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file name \"%@\" is not allowed", fileName];
|
||||
}
|
||||
|
||||
@@ -161,12 +166,12 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
NSString* itemName = [absolutePath lastPathComponent];
|
||||
if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
if (([itemName hasPrefix:@"."] && !_allowHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
|
||||
}
|
||||
|
||||
@@ -194,7 +199,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
if (![absolutePath hasPrefix:_uploadDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
BOOL isDirectory;
|
||||
@@ -203,7 +208,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
}
|
||||
|
||||
NSString* directoryName = [absolutePath lastPathComponent];
|
||||
if (!_showHidden && [directoryName hasPrefix:@"."]) {
|
||||
if (!_allowHidden && [directoryName hasPrefix:@"."]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
|
||||
}
|
||||
|
||||
@@ -243,7 +248,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
|
||||
NSString* srcRelativePath = request.path;
|
||||
NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:srcRelativePath];
|
||||
if (![srcAbsolutePath hasPrefix:_uploadDirectory]) {
|
||||
if (![self _checkSandboxedPath:srcAbsolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
||||
}
|
||||
|
||||
@@ -254,7 +259,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
}
|
||||
dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath];
|
||||
if (![dstAbsolutePath hasPrefix:_uploadDirectory]) {
|
||||
if (![self _checkSandboxedPath:dstAbsolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
||||
}
|
||||
|
||||
@@ -264,7 +269,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
||||
}
|
||||
|
||||
NSString* itemName = [dstAbsolutePath lastPathComponent];
|
||||
if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"%@ to item name \"%@\" is not allowed", isMove ? @"Moving" : @"Copying", itemName];
|
||||
}
|
||||
|
||||
@@ -425,12 +430,12 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
NSString* itemName = [absolutePath lastPathComponent];
|
||||
if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
if (([itemName hasPrefix:@"."] && !_allowHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Retrieving properties for item name \"%@\" is not allowed", itemName];
|
||||
}
|
||||
|
||||
@@ -454,7 +459,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
relativePath = [relativePath stringByAppendingString:@"/"];
|
||||
}
|
||||
for (NSString* item in items) {
|
||||
if (_showHidden || ![item hasPrefix:@"."]) {
|
||||
if (_allowHidden || ![item hasPrefix:@"."]) {
|
||||
[self _addPropertyResponseForItem:[absolutePath stringByAppendingPathComponent:item] resource:[relativePath stringByAppendingString:item] properties:properties xmlString:xmlString];
|
||||
}
|
||||
}
|
||||
@@ -475,7 +480,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
@@ -525,7 +530,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
}
|
||||
|
||||
NSString* itemName = [absolutePath lastPathComponent];
|
||||
if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Locking item name \"%@\" is not allowed", itemName];
|
||||
}
|
||||
|
||||
@@ -575,7 +580,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
NSString* relativePath = request.path;
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
@@ -585,7 +590,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
}
|
||||
|
||||
NSString* itemName = [absolutePath lastPathComponent];
|
||||
if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Unlocking item name \"%@\" is not allowed", itemName];
|
||||
}
|
||||
|
||||
@@ -597,7 +602,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
||||
|
||||
@implementation GCDWebDAVServer
|
||||
|
||||
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden;
|
||||
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, allowHiddenItems=_allowHidden;
|
||||
|
||||
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
||||
if ((self = [super init])) {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# http://guides.cocoapods.org/syntax/podspec.html
|
||||
# Verify Podspec with:
|
||||
# sudo gem update cocoapods
|
||||
# pod spec lint GCDWebServer.podspec --verbose
|
||||
# Add to source line:
|
||||
# :tag => s.version.to_s
|
||||
# http://guides.cocoapods.org/making/getting-setup-with-trunk.html
|
||||
# $ sudo gem update cocoapods
|
||||
# (optional) $ pod trunk register {email} {name} --description={computer}
|
||||
# $ pod trunk push
|
||||
# DELETE THIS SECTION BEFORE PROCEEDING!
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'GCDWebServer'
|
||||
s.version = '2.2'
|
||||
s.version = '3.0.1'
|
||||
s.author = { 'Pierre-Olivier Latour' => 'info@pol-online.net' }
|
||||
s.license = { :type => 'BSD', :file => 'LICENSE' }
|
||||
s.homepage = 'https://github.com/swisspol/GCDWebServer'
|
||||
s.summary = 'Lightweight GCD based HTTP server for OS X & iOS (includes web based uploader & WebDAV server)'
|
||||
|
||||
s.source = { :git => 'https://github.com/swisspol/GCDWebServer.git' }
|
||||
s.source = { :git => 'https://github.com/swisspol/GCDWebServer.git', :tag => s.version.to_s }
|
||||
s.ios.deployment_target = '5.0'
|
||||
s.osx.deployment_target = '10.7'
|
||||
s.requires_arc = true
|
||||
@@ -22,10 +22,12 @@ Pod::Spec.new do |s|
|
||||
|
||||
s.subspec 'Core' do |cs|
|
||||
cs.source_files = 'GCDWebServer/**/*.{h,m}'
|
||||
cs.private_header_files = "GCDWebServer/Core/GCDWebServerPrivate.h"
|
||||
cs.requires_arc = true
|
||||
cs.ios.library = 'z'
|
||||
cs.ios.frameworks = 'MobileCoreServices', 'CFNetwork'
|
||||
cs.osx.library = 'z'
|
||||
cs.compiler_flags = '-DNDEBUG' # TODO: Only set this for Release configuration
|
||||
cs.osx.framework = 'SystemConfiguration'
|
||||
end
|
||||
|
||||
s.subspec 'WebDAV' do |cs|
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
E28BAE4918F99C810095C089 /* GCDWebServerErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */; };
|
||||
E28BAE4A18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */; };
|
||||
E28BAE4B18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */; };
|
||||
E28BAE4C18F99C810095C089 /* GCDWebServerStreamingResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3318F99C810095C089 /* GCDWebServerStreamingResponse.m */; };
|
||||
E28BAE4D18F99C810095C089 /* GCDWebServerStreamingResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3318F99C810095C089 /* GCDWebServerStreamingResponse.m */; };
|
||||
E28BAE4C18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */; };
|
||||
E28BAE4D18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */; };
|
||||
E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
|
||||
E2A0E80B18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
|
||||
E2A0E80D18F35C9A00C580B1 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */; };
|
||||
@@ -112,6 +112,8 @@
|
||||
E22112981690B7AA0048D2B2 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; };
|
||||
E221129A1690B7B10048D2B2 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
E221129C1690B7BA0048D2B2 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/System/Library/Frameworks/MobileCoreServices.framework; sourceTree = DEVELOPER_DIR; };
|
||||
E26DC18719E84B2200C68DDC /* GCDWebServer.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = GCDWebServer.podspec; sourceTree = "<group>"; };
|
||||
E26DC18819E84BC000C68DDC /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
E28BAE1618F99C810095C089 /* GCDWebServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServer.h; sourceTree = "<group>"; };
|
||||
E28BAE1718F99C810095C089 /* GCDWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServer.m; sourceTree = "<group>"; };
|
||||
E28BAE1818F99C810095C089 /* GCDWebServerConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerConnection.h; sourceTree = "<group>"; };
|
||||
@@ -138,8 +140,8 @@
|
||||
E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerErrorResponse.m; sourceTree = "<group>"; };
|
||||
E28BAE3018F99C810095C089 /* GCDWebServerFileResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerFileResponse.h; sourceTree = "<group>"; };
|
||||
E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerFileResponse.m; sourceTree = "<group>"; };
|
||||
E28BAE3218F99C810095C089 /* GCDWebServerStreamingResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerStreamingResponse.h; sourceTree = "<group>"; };
|
||||
E28BAE3318F99C810095C089 /* GCDWebServerStreamingResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerStreamingResponse.m; sourceTree = "<group>"; };
|
||||
E28BAE3218F99C810095C089 /* GCDWebServerStreamedResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerStreamedResponse.h; sourceTree = "<group>"; };
|
||||
E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerStreamedResponse.m; sourceTree = "<group>"; };
|
||||
E2A0E80818F3432600C580B1 /* GCDWebDAVServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebDAVServer.h; sourceTree = "<group>"; };
|
||||
E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebDAVServer.m; sourceTree = "<group>"; };
|
||||
E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.1.sdk/usr/lib/libxml2.dylib; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -183,6 +185,8 @@
|
||||
08FB7794FE84155DC02AAC07 /* LittleCMS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E26DC18819E84BC000C68DDC /* README.md */,
|
||||
E26DC18719E84B2200C68DDC /* GCDWebServer.podspec */,
|
||||
E28BAE1418F99C810095C089 /* GCDWebServer */,
|
||||
E2A0E80718F3432600C580B1 /* GCDWebDAVServer */,
|
||||
E2BE850618E77ECA0061360B /* GCDWebUploader */,
|
||||
@@ -300,8 +304,8 @@
|
||||
E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */,
|
||||
E28BAE3018F99C810095C089 /* GCDWebServerFileResponse.h */,
|
||||
E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */,
|
||||
E28BAE3218F99C810095C089 /* GCDWebServerStreamingResponse.h */,
|
||||
E28BAE3318F99C810095C089 /* GCDWebServerStreamingResponse.m */,
|
||||
E28BAE3218F99C810095C089 /* GCDWebServerStreamedResponse.h */,
|
||||
E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */,
|
||||
);
|
||||
path = Responses;
|
||||
sourceTree = "<group>";
|
||||
@@ -370,7 +374,7 @@
|
||||
08FB7793FE84155DC02AAC07 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0510;
|
||||
LastUpgradeCheck = 0610;
|
||||
};
|
||||
buildConfigurationList = 1DEB928908733DD80010E9CD /* Build configuration list for PBXProject "GCDWebServer" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
@@ -437,7 +441,7 @@
|
||||
E28BAE3618F99C810095C089 /* GCDWebServerConnection.m in Sources */,
|
||||
E28BAE3C18F99C810095C089 /* GCDWebServerResponse.m in Sources */,
|
||||
E28BAE4018F99C810095C089 /* GCDWebServerFileRequest.m in Sources */,
|
||||
E28BAE4C18F99C810095C089 /* GCDWebServerStreamingResponse.m in Sources */,
|
||||
E28BAE4C18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */,
|
||||
E28BAE3E18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */,
|
||||
E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */,
|
||||
E28BAE4218F99C810095C089 /* GCDWebServerMultiPartFormRequest.m in Sources */,
|
||||
@@ -457,7 +461,7 @@
|
||||
E28BAE4B18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */,
|
||||
E28BAE3918F99C810095C089 /* GCDWebServerFunctions.m in Sources */,
|
||||
E28BAE4118F99C810095C089 /* GCDWebServerFileRequest.m in Sources */,
|
||||
E28BAE4D18F99C810095C089 /* GCDWebServerStreamingResponse.m in Sources */,
|
||||
E28BAE4D18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */,
|
||||
E28BAE3F18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */,
|
||||
E2BE850B18E77ECA0061360B /* GCDWebUploader.m in Sources */,
|
||||
E22112951690B64F0048D2B2 /* AppDelegate.m in Sources */,
|
||||
@@ -490,7 +494,6 @@
|
||||
1DEB928608733DD80010E9CD /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.7;
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
SDKROOT = macosx;
|
||||
@@ -500,7 +503,6 @@
|
||||
1DEB928708733DD80010E9CD /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.7;
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
SDKROOT = macosx;
|
||||
@@ -512,6 +514,7 @@
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1";
|
||||
GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = __GCDWEBSERVER_ENABLE_TESTING__;
|
||||
HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -531,6 +534,7 @@
|
||||
"-Wno-padded",
|
||||
"-Wno-documentation",
|
||||
"-Wno-documentation-unknown-command",
|
||||
"-Wno-objc-missing-property-synthesis",
|
||||
);
|
||||
};
|
||||
name = Debug;
|
||||
@@ -539,7 +543,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = NDEBUG;
|
||||
GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = __GCDWEBSERVER_ENABLE_TESTING__;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
|
||||
@@ -550,10 +553,9 @@
|
||||
E22112761690B4DF0048D2B2 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 5.1.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
PROVISIONING_PROFILE = "";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -564,10 +566,9 @@
|
||||
E22112771690B4DF0048D2B2 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 5.1.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
|
||||
PRODUCT_NAME = GCDWebServer;
|
||||
PROVISIONING_PROFILE = "";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -30,8 +30,14 @@
|
||||
#import "GCDWebServerRequest.h"
|
||||
#import "GCDWebServerResponse.h"
|
||||
|
||||
/**
|
||||
* Log levels used by GCDWebServer.
|
||||
*
|
||||
* @warning kGCDWebServerLogLevel_Debug is only functional if the preprocessor
|
||||
* constant "DEBUG" is is non-zero at build time.
|
||||
*/
|
||||
typedef NS_ENUM(int, GCDWebServerLogLevel) {
|
||||
kGCDWebServerLogLevel_Debug = 0, // Only available if "NDEBUG" is not defined when building
|
||||
kGCDWebServerLogLevel_Debug = 0,
|
||||
kGCDWebServerLogLevel_Verbose,
|
||||
kGCDWebServerLogLevel_Info,
|
||||
kGCDWebServerLogLevel_Warning,
|
||||
@@ -39,81 +45,505 @@ typedef NS_ENUM(int, GCDWebServerLogLevel) {
|
||||
kGCDWebServerLogLevel_Exception,
|
||||
};
|
||||
|
||||
/**
|
||||
* The GCDWebServerMatchBlock is called for every handler added to the
|
||||
* GCDWebServer whenever a new HTTP request has started (i.e. HTTP headers have
|
||||
* been received). The block is passed the basic info for the request (HTTP method,
|
||||
* URL, headers...) and must decide if it wants to handle it or not.
|
||||
*
|
||||
* If the handler can handle the request, the block must return a new
|
||||
* GCDWebServerRequest instance created with the same basic info.
|
||||
* Otherwise, it simply returns nil.
|
||||
*/
|
||||
typedef GCDWebServerRequest* (^GCDWebServerMatchBlock)(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery);
|
||||
|
||||
/**
|
||||
* The GCDWebServerProcessBlock is called after the HTTP request has been fully
|
||||
* received (i.e. the entire HTTP body has been read). The block is passed the
|
||||
* GCDWebServerRequest created at the previous step by the GCDWebServerMatchBlock.
|
||||
*
|
||||
* The block must return 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 GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* request);
|
||||
|
||||
extern NSString* const GCDWebServerOption_Port; // NSNumber / NSUInteger (default is 0 i.e. use a random port)
|
||||
extern NSString* const GCDWebServerOption_BonjourName; // NSString (default is empty string i.e. use computer name)
|
||||
extern NSString* const GCDWebServerOption_MaxPendingConnections; // NSNumber / NSUInteger (default is 16)
|
||||
extern NSString* const GCDWebServerOption_ServerName; // NSString (default is server class name)
|
||||
extern NSString* const GCDWebServerOption_ConnectionClass; // Subclass of GCDWebServerConnection (default is GCDWebServerConnection class)
|
||||
extern NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET; // NSNumber / BOOL (default is YES)
|
||||
extern NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval; // NSNumber / double (default is 1.0 seconds - set to <=0.0 to disable coaslescing of -webServerDidConnect: / -webServerDidDisconnect:)
|
||||
/**
|
||||
* 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 default value is 0 i.e. let the OS pick a random port.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_Port;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_BonjourName;
|
||||
|
||||
/**
|
||||
* The Bonjour service type used by the GCDWebServer (NSString).
|
||||
*
|
||||
* The default value is "_http._tcp", the service type for HTTP web servers.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_BonjourType;
|
||||
|
||||
/**
|
||||
* The maximum number of incoming HTTP requests that can be queued waiting to
|
||||
* be handled before new ones are dropped (NSNumber / NSUInteger).
|
||||
*
|
||||
* The default value is 16.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_MaxPendingConnections;
|
||||
|
||||
/**
|
||||
* The value for "Server" HTTP header used by the GCDWebServer (NSString).
|
||||
*
|
||||
* The default value is the GCDWebServer class name.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_ServerName;
|
||||
|
||||
/**
|
||||
* The authentication method used by the GCDWebServer
|
||||
* (one of "GCDWebServerAuthenticationMethod_...").
|
||||
*
|
||||
* The default value is nil i.e. authentication is disabled.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_AuthenticationMethod;
|
||||
|
||||
/**
|
||||
* The authentication realm used by the GCDWebServer (NSString).
|
||||
*
|
||||
* The default value is the same as the GCDWebServerOption_ServerName option.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_AuthenticationRealm;
|
||||
|
||||
/**
|
||||
* The authentication accounts used by the GCDWebServer
|
||||
* (NSDictionary of username / password pairs).
|
||||
*
|
||||
* The default value is nil i.e. no accounts.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_AuthenticationAccounts;
|
||||
|
||||
/**
|
||||
* The class used by the GCDWebServer when instantiating GCDWebServerConnection
|
||||
* (subclass of GCDWebServerConnection).
|
||||
*
|
||||
* The default value is the GCDWebServerConnection class.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_ConnectionClass;
|
||||
|
||||
/**
|
||||
* Allow the GCDWebServer to pretend "HEAD" requests are actually "GET" ones
|
||||
* and automatically discard the HTTP body of the response (NSNumber / BOOL).
|
||||
*
|
||||
* The default value is YES.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET;
|
||||
|
||||
/**
|
||||
* The interval expressed in seconds used by the GCDWebServer to decide how to
|
||||
* coalesce calls to -webServerDidConnect: and -webServerDidDisconnect:
|
||||
* (NSNumber / double). Coalescing will be disabled if the interval is <= 0.0.
|
||||
*
|
||||
* The default value is 1.0 second.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
extern NSString* const GCDWebServerOption_AutomaticallySuspendInBackground; // NSNumber / BOOL (default is YES)
|
||||
|
||||
/**
|
||||
* Enables the GCDWebServer to automatically suspend itself (as if -stop was
|
||||
* called) when the iOS app goes into the background and the last
|
||||
* GCDWebServerConnection is closed, then resume itself (as if -start was called)
|
||||
* when the iOS app comes back to the foreground (NSNumber / BOOL).
|
||||
*
|
||||
* See the README.md file for more information about this option.
|
||||
*
|
||||
* The default value is YES.
|
||||
*
|
||||
* @warning The running property will be NO while the GCDWebServer is suspended.
|
||||
*/
|
||||
extern NSString* const GCDWebServerOption_AutomaticallySuspendInBackground;
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* HTTP Basic Authentication scheme (see https://tools.ietf.org/html/rfc2617).
|
||||
*
|
||||
* @warning Use of this authentication scheme is not recommended as the
|
||||
* passwords are sent in clear.
|
||||
*/
|
||||
extern NSString* const GCDWebServerAuthenticationMethod_Basic;
|
||||
|
||||
/**
|
||||
* HTTP Digest Access Authentication scheme (see https://tools.ietf.org/html/rfc2617).
|
||||
*/
|
||||
extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||
|
||||
@class GCDWebServer;
|
||||
|
||||
// These methods are always called on main thread
|
||||
/**
|
||||
* Delegate methods for GCDWebServer.
|
||||
*
|
||||
* @warning These methods are always called on the main thread in a serialized way.
|
||||
*/
|
||||
@protocol GCDWebServerDelegate <NSObject>
|
||||
@optional
|
||||
|
||||
/**
|
||||
* This method is called after the server has successfully started.
|
||||
*/
|
||||
- (void)webServerDidStart:(GCDWebServer*)server;
|
||||
- (void)webServerDidConnect:(GCDWebServer*)server; // Called when first connection is opened
|
||||
- (void)webServerDidDisconnect:(GCDWebServer*)server; // Called when last connection is closed
|
||||
|
||||
/**
|
||||
* This method is called after the Bonjour registration for the server has
|
||||
* successfully completed.
|
||||
*/
|
||||
- (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server;
|
||||
|
||||
/**
|
||||
* This method is called when the first GCDWebServerConnection is opened by the
|
||||
* server to serve a series of HTTP requests.
|
||||
*
|
||||
* A series of HTTP requests is considered ongoing as long as new HTTP requests
|
||||
* keep coming (and new GCDWebServerConnection instances keep being opened),
|
||||
* until before the last HTTP request has been responded to (and the
|
||||
* corresponding last GCDWebServerConnection closed).
|
||||
*/
|
||||
- (void)webServerDidConnect:(GCDWebServer*)server;
|
||||
|
||||
/**
|
||||
* This method is called when the last GCDWebServerConnection is closed after
|
||||
* the server has served a series of HTTP requests.
|
||||
*
|
||||
* The GCDWebServerOption_ConnectedStateCoalescingInterval option can be used
|
||||
* to have the server wait some extra delay before considering that the series
|
||||
* of HTTP requests has ended (in case there some latency between consecutive
|
||||
* requests). This effectively coalesces the calls to -webServerDidConnect:
|
||||
* and -webServerDidDisconnect:.
|
||||
*/
|
||||
- (void)webServerDidDisconnect:(GCDWebServer*)server;
|
||||
|
||||
/**
|
||||
* This method is called after the server has stopped.
|
||||
*/
|
||||
- (void)webServerDidStop:(GCDWebServer*)server;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The GCDWebServer class listens for incoming HTTP requests on a given port,
|
||||
* then passes each one to a "handler" capable of generating an HTTP response
|
||||
* for it, which is then sent back to the client.
|
||||
*
|
||||
* GCDWebServer instances can be created and used from any thread but it's
|
||||
* recommended to have the main thread's runloop be running so internal callbacks
|
||||
* can be handled e.g. for Bonjour registration.
|
||||
*
|
||||
* See the README.md file for more information about the architecture of GCDWebServer.
|
||||
*/
|
||||
@interface GCDWebServer : NSObject
|
||||
|
||||
/**
|
||||
* Sets the delegate for the server.
|
||||
*/
|
||||
@property(nonatomic, assign) id<GCDWebServerDelegate> delegate;
|
||||
|
||||
/**
|
||||
* Returns YES if the server is currently running.
|
||||
*/
|
||||
@property(nonatomic, readonly, getter=isRunning) BOOL running;
|
||||
@property(nonatomic, readonly) NSUInteger port; // Only non-zero if running
|
||||
@property(nonatomic, readonly) NSString* bonjourName; // Only non-nil if Bonjour registration is active
|
||||
|
||||
/**
|
||||
* Returns the port used by the server.
|
||||
*
|
||||
* @warning This property is only valid if the server is running.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSUInteger port;
|
||||
|
||||
/**
|
||||
* Returns the Bonjour name 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* bonjourName;
|
||||
|
||||
/**
|
||||
* Returns the Bonjour service type used by the server.
|
||||
*
|
||||
* @warning This property is only valid if the server is running and Bonjour
|
||||
* registration has successfully completed, which can take up to a few seconds.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* bonjourType;
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)init;
|
||||
|
||||
/**
|
||||
* Adds to the server a handler that generates responses synchronously when handling incoming HTTP requests.
|
||||
*
|
||||
* Handlers are called in a LIFO queue, so if multiple handlers can potentially
|
||||
* respond to a given request, the latest added one wins.
|
||||
*
|
||||
* @warning Addling handlers while the server is running is not allowed.
|
||||
*/
|
||||
- (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.
|
||||
*
|
||||
* @warning Removing handlers while the server is running is not allowed.
|
||||
*/
|
||||
- (void)removeAllHandlers;
|
||||
|
||||
- (BOOL)start; // Default is port 8080 (OS X & iOS Simulator) or 80 (iOS) and computer / device name for Bonjour
|
||||
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name; // Pass nil name to disable Bonjour or empty string to use computer name
|
||||
- (BOOL)startWithOptions:(NSDictionary*)options;
|
||||
- (void)stop; // Does not abort any currently opened connections
|
||||
/**
|
||||
* Starts the server with explicit options. This method is the designated way
|
||||
* to start the server.
|
||||
*
|
||||
* Returns NO if the server failed to start and sets "error" argument if not NULL.
|
||||
*/
|
||||
- (BOOL)startWithOptions:(NSDictionary*)options error:(NSError**)error;
|
||||
|
||||
/**
|
||||
* Stops the server and prevents it to accepts new HTTP requests.
|
||||
*
|
||||
* @warning Stopping the server does not abort GCDWebServerConnection instances
|
||||
* currently handling already received HTTP requests. These connections will
|
||||
* continue to execute normally until completion.
|
||||
*/
|
||||
- (void)stop;
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (Extensions)
|
||||
@property(nonatomic, readonly) NSURL* serverURL; // Only non-nil if server is running
|
||||
@property(nonatomic, readonly) NSURL* bonjourServerURL; // Only non-nil if server is running and Bonjour registration is active
|
||||
|
||||
/**
|
||||
* Returns the server's URL.
|
||||
*
|
||||
* @warning This property is only valid if the server is running.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSURL* serverURL;
|
||||
|
||||
/**
|
||||
* Returns the server's Bonjour URL.
|
||||
*
|
||||
* @warning This property is only valid if the server is running and Bonjour
|
||||
* registration has successfully completed, which can take up to a few seconds.
|
||||
* Also be aware this property will not automatically update if the Bonjour hostname
|
||||
* has been dynamically changed after the server started running (this should be rare).
|
||||
*/
|
||||
@property(nonatomic, readonly) NSURL* bonjourServerURL;
|
||||
|
||||
/**
|
||||
* Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS)
|
||||
* using the default Bonjour name.
|
||||
*
|
||||
* Returns NO if the server failed to start.
|
||||
*/
|
||||
- (BOOL)start;
|
||||
|
||||
/**
|
||||
* Starts the server on a given port and with a specific Bonjour name.
|
||||
* Pass a nil Bonjour name to disable Bonjour entirely or an empty string to
|
||||
* use the default name.
|
||||
*
|
||||
* Returns NO if the server failed to start.
|
||||
*/
|
||||
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name;
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
- (BOOL)runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only)
|
||||
|
||||
/**
|
||||
* Runs the server synchronously using -startWithPort:bonjourName: until a
|
||||
* SIGINT signal is received i.e. Ctrl-C. This method is intended to be used
|
||||
* by command line tools.
|
||||
*
|
||||
* Returns NO if the server failed to start.
|
||||
*
|
||||
* @warning This method must be used from the main thread only.
|
||||
*/
|
||||
- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name;
|
||||
|
||||
/**
|
||||
* Runs the server synchronously using -startWithOptions: until a SIGTERM or
|
||||
* SIGINT signal is received i.e. Ctrl-C in Terminal. This method is intended to
|
||||
* be used by command line tools.
|
||||
*
|
||||
* Returns NO if the server failed to start and sets "error" argument if not NULL.
|
||||
*
|
||||
* @warning This method must be used from the main thread only.
|
||||
*/
|
||||
- (BOOL)runWithOptions:(NSDictionary*)options error:(NSError**)error;
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (Handlers)
|
||||
|
||||
/**
|
||||
* Adds a default handler to the server to handle all incoming HTTP requests
|
||||
* with a given HTTP method and generate responses synchronously.
|
||||
*/
|
||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Path is case-insensitive
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block; // Regular expression is case-insensitive
|
||||
|
||||
/**
|
||||
* Adds a default handler to the server to handle all incoming HTTP requests
|
||||
* with a given HTTP method and generate responses asynchronously.
|
||||
*/
|
||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||
* HTTP method and a specific case-insensitive path and generate responses
|
||||
* synchronously.
|
||||
*/
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||
* HTTP method and a specific case-insensitive path and generate responses
|
||||
* asynchronously.
|
||||
*/
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||
* HTTP method and a path matching a case-insensitive regular expression and
|
||||
* generate responses synchronously.
|
||||
*/
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||
* HTTP method and a path matching a case-insensitive regular expression and
|
||||
* generate responses asynchronously.
|
||||
*/
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block;
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (GETHandlers)
|
||||
- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge; // Path is case-insensitive
|
||||
- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; // Path is case-insensitive
|
||||
- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests; // Base path is recursive and case-sensitive
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to respond to incoming "GET" HTTP requests
|
||||
* with a specific case-insensitive path with in-memory data.
|
||||
*/
|
||||
- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to respond to incoming "GET" HTTP requests
|
||||
* with a specific case-insensitive path with a file.
|
||||
*/
|
||||
- (void)addGETHandlerForPath:(NSString*)path filePath:(NSString*)filePath isAttachment:(BOOL)isAttachment cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests;
|
||||
|
||||
/**
|
||||
* Adds a handler to the server to respond to incoming "GET" HTTP requests
|
||||
* with a case-insensitive path inside a base path with the corresponding file
|
||||
* inside a local directory. If no local file matches the request path, a 401
|
||||
* HTTP status code is returned to the client.
|
||||
*
|
||||
* The "indexFilename" argument allows to specify an "index" file name to use
|
||||
* when the request path corresponds to a directory.
|
||||
*/
|
||||
- (void)addGETHandlerForBasePath:(NSString*)basePath directoryPath:(NSString*)directoryPath indexFilename:(NSString*)indexFilename cacheAge:(NSUInteger)cacheAge allowRangeRequests:(BOOL)allowRangeRequests;
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServer (Logging)
|
||||
|
||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
+ (void)setLogLevel:(GCDWebServerLogLevel)level; // Default level is DEBUG or INFO if "NDEBUG" is defined when building (it can also be set at runtime with the "logLevel" environment variable)
|
||||
|
||||
/**
|
||||
* Sets the current log level below which logged messages are discarded.
|
||||
*
|
||||
* The default level is INFO (or DEBUG if the preprocessor constant "DEBUG"
|
||||
* is non-zero at build time).
|
||||
* It can also be set at run time with the "logLevel" environment variable.
|
||||
*/
|
||||
+ (void)setLogLevel:(GCDWebServerLogLevel)level;
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Logs a message with the kGCDWebServerLogLevel_Verbose level.
|
||||
*/
|
||||
- (void)logVerbose:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
|
||||
/**
|
||||
* Logs a message with the kGCDWebServerLogLevel_Info level.
|
||||
*/
|
||||
- (void)logInfo:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
|
||||
/**
|
||||
* Logs a message with the kGCDWebServerLogLevel_Warning level.
|
||||
*/
|
||||
- (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
|
||||
/**
|
||||
* Logs a message with the kGCDWebServerLogLevel_Error level.
|
||||
*/
|
||||
- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
|
||||
/**
|
||||
* Logs an exception with the kGCDWebServerLogLevel_Exception level.
|
||||
*/
|
||||
- (void)logException:(NSException*)exception;
|
||||
|
||||
@end
|
||||
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
|
||||
@interface GCDWebServer (Testing)
|
||||
@property(nonatomic, getter=isRecordingEnabled) BOOL recordingEnabled; // Creates files in the current directory containing the raw data for all requests and responses (directory most NOT contain prior recordings)
|
||||
- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port; // Returns number of failed tests or -1 if server failed to start
|
||||
|
||||
/**
|
||||
* Activates recording of HTTP requests and responses which create files in the
|
||||
* current directory containing the raw data for all requests and responses.
|
||||
*
|
||||
* @warning The current directory must not contain any prior recording files.
|
||||
*/
|
||||
@property(nonatomic, getter=isRecordingEnabled) BOOL recordingEnabled;
|
||||
|
||||
/**
|
||||
* Runs tests by playing back pre-recorded HTTP requests in the given directory
|
||||
* and comparing the generated responses with the pre-recorded ones.
|
||||
*
|
||||
* Returns the number of failed tests or -1 if server failed to start.
|
||||
*/
|
||||
- (NSInteger)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
@@ -43,44 +43,14 @@
|
||||
#define kDefaultPort 8080
|
||||
#endif
|
||||
|
||||
@interface GCDWebServer () {
|
||||
@private
|
||||
id<GCDWebServerDelegate> __unsafe_unretained _delegate;
|
||||
dispatch_queue_t _syncQueue;
|
||||
NSMutableArray* _handlers;
|
||||
NSInteger _activeConnections; // Accessed only with _syncQueue
|
||||
BOOL _connected;
|
||||
CFRunLoopTimerRef _connectedTimer;
|
||||
|
||||
NSDictionary* _options;
|
||||
NSString* _serverName;
|
||||
Class _connectionClass;
|
||||
BOOL _mapHEADToGET;
|
||||
CFTimeInterval _disconnectDelay;
|
||||
NSUInteger _port;
|
||||
dispatch_source_t _source;
|
||||
CFNetServiceRef _service;
|
||||
#if TARGET_OS_IPHONE
|
||||
BOOL _suspendInBackground;
|
||||
UIBackgroundTaskIdentifier _backgroundTask;
|
||||
#endif
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
BOOL _recording;
|
||||
#endif
|
||||
}
|
||||
@end
|
||||
|
||||
@interface GCDWebServerHandler () {
|
||||
@private
|
||||
GCDWebServerMatchBlock _matchBlock;
|
||||
GCDWebServerProcessBlock _processBlock;
|
||||
}
|
||||
@end
|
||||
|
||||
NSString* const GCDWebServerOption_Port = @"Port";
|
||||
NSString* const GCDWebServerOption_BonjourName = @"BonjourName";
|
||||
NSString* const GCDWebServerOption_BonjourType = @"BonjourType";
|
||||
NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections";
|
||||
NSString* const GCDWebServerOption_ServerName = @"ServerName";
|
||||
NSString* const GCDWebServerOption_AuthenticationMethod = @"AuthenticationMethod";
|
||||
NSString* const GCDWebServerOption_AuthenticationRealm = @"AuthenticationRealm";
|
||||
NSString* const GCDWebServerOption_AuthenticationAccounts = @"AuthenticationAccounts";
|
||||
NSString* const GCDWebServerOption_ConnectionClass = @"ConnectionClass";
|
||||
NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET = @"AutomaticallyMapHEADToGET";
|
||||
NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval = @"ConnectedStateCoalescingInterval";
|
||||
@@ -88,11 +58,14 @@ NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval = @"Connecte
|
||||
NSString* const GCDWebServerOption_AutomaticallySuspendInBackground = @"AutomaticallySuspendInBackground";
|
||||
#endif
|
||||
|
||||
NSString* const GCDWebServerAuthenticationMethod_Basic = @"Basic";
|
||||
NSString* const GCDWebServerAuthenticationMethod_DigestAccess = @"DigestAccess";
|
||||
|
||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
#ifdef NDEBUG
|
||||
GCDWebServerLogLevel GCDLogLevel = kGCDWebServerLogLevel_Info;
|
||||
#else
|
||||
#if DEBUG
|
||||
GCDWebServerLogLevel GCDLogLevel = kGCDWebServerLogLevel_Debug;
|
||||
#else
|
||||
GCDWebServerLogLevel GCDLogLevel = kGCDWebServerLogLevel_Info;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -123,30 +96,86 @@ static void _SignalHandler(int signal) {
|
||||
|
||||
#endif
|
||||
|
||||
#if !TARGET_OS_IPHONE || defined(__GCDWEBSERVER_ENABLE_TESTING__)
|
||||
|
||||
// This utility function is used to ensure scheduled callbacks on the main thread are called when running the server synchronously
|
||||
// https://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html
|
||||
// The main queue works with the application’s run loop to interleave the execution of queued tasks with the execution of other event sources attached to the run loop
|
||||
// TODO: Ensure all scheduled blocks on the main queue are also executed
|
||||
static void _ExecuteMainThreadRunLoopSources() {
|
||||
SInt32 result;
|
||||
do {
|
||||
result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true);
|
||||
} while (result == kCFRunLoopRunHandledSource);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@interface GCDWebServerHandler () {
|
||||
@private
|
||||
GCDWebServerMatchBlock _matchBlock;
|
||||
GCDWebServerAsyncProcessBlock _asyncProcessBlock;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerHandler
|
||||
|
||||
@synthesize matchBlock=_matchBlock, processBlock=_processBlock;
|
||||
@synthesize matchBlock=_matchBlock, asyncProcessBlock=_asyncProcessBlock;
|
||||
|
||||
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock {
|
||||
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock {
|
||||
if ((self = [super init])) {
|
||||
_matchBlock = [matchBlock copy];
|
||||
_processBlock = [processBlock copy];
|
||||
_asyncProcessBlock = [processBlock copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_matchBlock);
|
||||
ARC_RELEASE(_processBlock);
|
||||
ARC_RELEASE(_asyncProcessBlock);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServer () {
|
||||
@private
|
||||
id<GCDWebServerDelegate> __unsafe_unretained _delegate;
|
||||
dispatch_queue_t _syncQueue;
|
||||
dispatch_semaphore_t _sourceSemaphore;
|
||||
NSMutableArray* _handlers;
|
||||
NSInteger _activeConnections; // Accessed through _syncQueue only
|
||||
BOOL _connected; // Accessed on main thread only
|
||||
CFRunLoopTimerRef _disconnectTimer; // Accessed on main thread only
|
||||
|
||||
NSDictionary* _options;
|
||||
NSString* _serverName;
|
||||
NSString* _authenticationRealm;
|
||||
NSMutableDictionary* _authenticationBasicAccounts;
|
||||
NSMutableDictionary* _authenticationDigestAccounts;
|
||||
Class _connectionClass;
|
||||
BOOL _mapHEADToGET;
|
||||
CFTimeInterval _disconnectDelay;
|
||||
NSUInteger _port;
|
||||
dispatch_source_t _source;
|
||||
CFNetServiceRef _registrationService;
|
||||
CFNetServiceRef _resolutionService;
|
||||
#if TARGET_OS_IPHONE
|
||||
BOOL _suspendInBackground;
|
||||
UIBackgroundTaskIdentifier _backgroundTask;
|
||||
#endif
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
BOOL _recording;
|
||||
#endif
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServer
|
||||
|
||||
@synthesize delegate=_delegate, handlers=_handlers, port=_port, serverName=_serverName, shouldAutomaticallyMapHEADToGET=_mapHEADToGET;
|
||||
@synthesize delegate=_delegate, handlers=_handlers, port=_port, serverName=_serverName, authenticationRealm=_authenticationRealm,
|
||||
authenticationBasicAccounts=_authenticationBasicAccounts, authenticationDigestAccounts=_authenticationDigestAccounts,
|
||||
shouldAutomaticallyMapHEADToGET=_mapHEADToGET;
|
||||
|
||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
|
||||
@@ -163,19 +192,11 @@ static void _SignalHandler(int signal) {
|
||||
GCDWebServerInitializeFunctions();
|
||||
}
|
||||
|
||||
static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
@autoreleasepool {
|
||||
[(ARC_BRIDGE GCDWebServer*)info _didDisconnect];
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
_syncQueue = dispatch_queue_create([NSStringFromClass([self class]) UTF8String], DISPATCH_QUEUE_SERIAL);
|
||||
_sourceSemaphore = dispatch_semaphore_create(0);
|
||||
_handlers = [[NSMutableArray alloc] init];
|
||||
CFRunLoopTimerContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
||||
_connectedTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, HUGE_VAL, HUGE_VAL, 0, 0, _ConnectedTimerCallBack, &context);
|
||||
CFRunLoopAddTimer(CFRunLoopGetMain(), _connectedTimer, kCFRunLoopCommonModes);
|
||||
#if TARGET_OS_IPHONE
|
||||
_backgroundTask = UIBackgroundTaskInvalid;
|
||||
#endif
|
||||
@@ -186,15 +207,11 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
- (void)dealloc {
|
||||
DCHECK(_connected == NO);
|
||||
DCHECK(_activeConnections == 0);
|
||||
DCHECK(_options == nil); // The server can never be dealloc'ed while running because of the retain-cycle with the dispatch source
|
||||
DCHECK(_disconnectTimer == NULL); // The server can never be dealloc'ed while the disconnect timer is pending because of the retain-cycle
|
||||
|
||||
_delegate = nil;
|
||||
if (_options) {
|
||||
[self stop];
|
||||
}
|
||||
|
||||
CFRunLoopTimerInvalidate(_connectedTimer);
|
||||
CFRelease(_connectedTimer);
|
||||
ARC_RELEASE(_handlers);
|
||||
ARC_DISPATCH_RELEASE(_sourceSemaphore);
|
||||
ARC_DISPATCH_RELEASE(_syncQueue);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
@@ -242,8 +259,10 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
DCHECK(_activeConnections >= 0);
|
||||
if (_activeConnections == 0) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (_disconnectDelay > 0.0) {
|
||||
CFRunLoopTimerSetNextFireDate(_connectedTimer, HUGE_VAL);
|
||||
if (_disconnectTimer) {
|
||||
CFRunLoopTimerInvalidate(_disconnectTimer);
|
||||
CFRelease(_disconnectTimer);
|
||||
_disconnectTimer = NULL;
|
||||
}
|
||||
if (_connected == NO) {
|
||||
[self _didConnect];
|
||||
@@ -296,8 +315,18 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
_activeConnections -= 1;
|
||||
if (_activeConnections == 0) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (_disconnectDelay > 0.0) {
|
||||
CFRunLoopTimerSetNextFireDate(_connectedTimer, CFAbsoluteTimeGetCurrent() + _disconnectDelay);
|
||||
if ((_disconnectDelay > 0.0) && (_source != NULL)) {
|
||||
if (_disconnectTimer) {
|
||||
CFRunLoopTimerInvalidate(_disconnectTimer);
|
||||
CFRelease(_disconnectTimer);
|
||||
}
|
||||
_disconnectTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + _disconnectDelay, 0.0, 0, 0, ^(CFRunLoopTimerRef timer) {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
[self _didDisconnect];
|
||||
CFRelease(_disconnectTimer);
|
||||
_disconnectTimer = NULL;
|
||||
});
|
||||
CFRunLoopAddTimer(CFRunLoopGetMain(), _disconnectTimer, kCFRunLoopCommonModes);
|
||||
} else {
|
||||
[self _didDisconnect];
|
||||
}
|
||||
@@ -307,13 +336,24 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
}
|
||||
|
||||
- (NSString*)bonjourName {
|
||||
CFStringRef name = _service ? CFNetServiceGetName(_service) : NULL;
|
||||
CFStringRef name = _resolutionService ? CFNetServiceGetName(_resolutionService) : NULL;
|
||||
return name && CFStringGetLength(name) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, name)) : nil;
|
||||
}
|
||||
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock {
|
||||
- (NSString*)bonjourType {
|
||||
CFStringRef type = _resolutionService ? CFNetServiceGetType(_resolutionService) : NULL;
|
||||
return type && CFStringGetLength(type) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, type)) : nil;
|
||||
}
|
||||
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock {
|
||||
[self addHandlerWithMatchBlock:matchBlock asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
completionBlock(processBlock(request));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock asyncProcessBlock:(GCDWebServerAsyncProcessBlock)processBlock {
|
||||
DCHECK(_options == nil);
|
||||
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock];
|
||||
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock asyncProcessBlock:processBlock];
|
||||
[_handlers insertObject:handler atIndex:0];
|
||||
ARC_RELEASE(handler);
|
||||
}
|
||||
@@ -323,13 +363,32 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||
[_handlers removeAllObjects];
|
||||
}
|
||||
|
||||
static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
||||
static void _NetServiceRegisterCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
@autoreleasepool {
|
||||
if (error->error) {
|
||||
LOG_ERROR(@"Bonjour error %i (domain %i)", (int)error->error, (int)error->domain);
|
||||
LOG_ERROR(@"Bonjour registration error %i (domain %i)", (int)error->error, (int)error->domain);
|
||||
} else {
|
||||
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
|
||||
LOG_VERBOSE(@"Bonjour registration complete for %@", [server class]);
|
||||
CFNetServiceResolveWithTimeout(server->_resolutionService, 1.0, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _NetServiceResolveCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
@autoreleasepool {
|
||||
if (error->error) {
|
||||
if ((error->domain != kCFStreamErrorDomainNetServices) && (error->error != kCFNetServicesErrorTimeout)) {
|
||||
LOG_ERROR(@"Bonjour resolution error %i (domain %i)", (int)error->error, (int)error->domain);
|
||||
}
|
||||
} else {
|
||||
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
|
||||
LOG_INFO(@"%@ now reachable at %@", [server class], server.bonjourServerURL);
|
||||
if ([server.delegate respondsToSelector:@selector(webServerDidCompleteBonjourRegistration:)]) {
|
||||
[server.delegate webServerDidCompleteBonjourRegistration:server];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,10 +398,22 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
return value ? value : defaultValue;
|
||||
}
|
||||
|
||||
- (BOOL)_start {
|
||||
static inline NSString* _EncodeBase64(NSString* string) {
|
||||
NSData* data = [string dataUsingEncoding:NSUTF8StringEncoding];
|
||||
#if (TARGET_OS_IPHONE && !(__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0)) || (!TARGET_OS_IPHONE && !(__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9))
|
||||
if (![data respondsToSelector:@selector(base64EncodedDataWithOptions:)]) {
|
||||
return [data base64Encoding];
|
||||
}
|
||||
#endif
|
||||
return ARC_AUTORELEASE([[NSString alloc] initWithData:[data base64EncodedDataWithOptions:0] encoding:NSASCIIStringEncoding]);
|
||||
}
|
||||
|
||||
- (BOOL)_start:(NSError**)error {
|
||||
DCHECK(_source == NULL);
|
||||
|
||||
NSUInteger port = [_GetOption(_options, GCDWebServerOption_Port, @0) unsignedIntegerValue];
|
||||
NSString* name = _GetOption(_options, GCDWebServerOption_BonjourName, @"");
|
||||
NSString* bonjourName = _GetOption(_options, GCDWebServerOption_BonjourName, @"");
|
||||
NSString* bonjourType = _GetOption(_options, GCDWebServerOption_BonjourType, @"_http._tcp");
|
||||
NSUInteger maxPendingConnections = [_GetOption(_options, GCDWebServerOption_MaxPendingConnections, @16) unsignedIntegerValue];
|
||||
int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (listeningSocket > 0) {
|
||||
@@ -359,6 +430,22 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
if (listen(listeningSocket, (int)maxPendingConnections) == 0) {
|
||||
LOG_DEBUG(@"Did open listening socket %i", listeningSocket);
|
||||
_serverName = [_GetOption(_options, GCDWebServerOption_ServerName, NSStringFromClass([self class])) copy];
|
||||
NSString* authenticationMethod = _GetOption(_options, GCDWebServerOption_AuthenticationMethod, nil);
|
||||
if ([authenticationMethod isEqualToString:GCDWebServerAuthenticationMethod_Basic]) {
|
||||
_authenticationRealm = [_GetOption(_options, GCDWebServerOption_AuthenticationRealm, _serverName) copy];
|
||||
_authenticationBasicAccounts = [[NSMutableDictionary alloc] init];
|
||||
NSDictionary* accounts = _GetOption(_options, GCDWebServerOption_AuthenticationAccounts, @{});
|
||||
[accounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* password, BOOL* stop) {
|
||||
[_authenticationBasicAccounts setObject:_EncodeBase64([NSString stringWithFormat:@"%@:%@", username, password]) forKey:username];
|
||||
}];
|
||||
} else if ([authenticationMethod isEqualToString:GCDWebServerAuthenticationMethod_DigestAccess]) {
|
||||
_authenticationRealm = [_GetOption(_options, GCDWebServerOption_AuthenticationRealm, _serverName) copy];
|
||||
_authenticationDigestAccounts = [[NSMutableDictionary alloc] init];
|
||||
NSDictionary* accounts = _GetOption(_options, GCDWebServerOption_AuthenticationAccounts, @{});
|
||||
[accounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* password, BOOL* stop) {
|
||||
[_authenticationDigestAccounts setObject:GCDWebServerComputeMD5Digest(@"%@:%@:%@", username, _authenticationRealm, password) forKey:username];
|
||||
}];
|
||||
}
|
||||
_connectionClass = _GetOption(_options, GCDWebServerOption_ConnectionClass, [GCDWebServerConnection class]);
|
||||
_mapHEADToGET = [_GetOption(_options, GCDWebServerOption_AutomaticallyMapHEADToGET, @YES) boolValue];
|
||||
_disconnectDelay = [_GetOption(_options, GCDWebServerOption_ConnectedStateCoalescingInterval, @1.0) doubleValue];
|
||||
@@ -373,6 +460,7 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
LOG_DEBUG(@"Did close listening socket %i", listeningSocket);
|
||||
}
|
||||
}
|
||||
dispatch_semaphore_signal(_sourceSemaphore);
|
||||
|
||||
});
|
||||
dispatch_source_set_event_handler(_source, ^{
|
||||
@@ -422,14 +510,21 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
_port = port;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
_service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (ARC_BRIDGE CFStringRef)name, (SInt32)_port);
|
||||
if (_service) {
|
||||
if (bonjourName) {
|
||||
_registrationService = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), (ARC_BRIDGE CFStringRef)bonjourType, (ARC_BRIDGE CFStringRef)(bonjourName.length ? bonjourName : _serverName), (SInt32)_port);
|
||||
if (_registrationService) {
|
||||
CFNetServiceClientContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
||||
CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFStreamError error = {0};
|
||||
CFNetServiceRegisterWithOptions(_service, 0, &error);
|
||||
|
||||
CFNetServiceSetClient(_registrationService, _NetServiceRegisterCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFStreamError streamError = {0};
|
||||
CFNetServiceRegisterWithOptions(_registrationService, 0, &streamError);
|
||||
|
||||
_resolutionService = CFNetServiceCreateCopy(kCFAllocatorDefault, _registrationService);
|
||||
if (_resolutionService) {
|
||||
CFNetServiceSetClient(_resolutionService, _NetServiceResolveCallBack, &context);
|
||||
CFNetServiceScheduleWithRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed creating CFNetService");
|
||||
}
|
||||
@@ -443,15 +538,24 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
});
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed listening on socket: %s (%i)", strerror(errno), errno);
|
||||
if (error) {
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
}
|
||||
LOG_ERROR(@"Failed starting listening socket: %s (%i)", strerror(errno), errno);
|
||||
close(listeningSocket);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed binding socket: %s (%i)", strerror(errno), errno);
|
||||
if (error) {
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
}
|
||||
LOG_ERROR(@"Failed binding listening socket: %s (%i)", strerror(errno), errno);
|
||||
close(listeningSocket);
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR(@"Failed creating socket: %s (%i)", strerror(errno), errno);
|
||||
if (error) {
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
}
|
||||
LOG_ERROR(@"Failed creating listening socket: %s (%i)", strerror(errno), errno);
|
||||
}
|
||||
return (_source ? YES : NO);
|
||||
}
|
||||
@@ -459,20 +563,44 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
- (void)_stop {
|
||||
DCHECK(_source != NULL);
|
||||
|
||||
if (_service) {
|
||||
CFNetServiceUnscheduleFromRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFNetServiceSetClient(_service, NULL, NULL);
|
||||
CFRelease(_service);
|
||||
_service = NULL;
|
||||
if (_registrationService) {
|
||||
if (_resolutionService) {
|
||||
CFNetServiceUnscheduleFromRunLoop(_resolutionService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFNetServiceSetClient(_resolutionService, NULL, NULL);
|
||||
CFNetServiceCancel(_resolutionService);
|
||||
CFRelease(_resolutionService);
|
||||
_resolutionService = NULL;
|
||||
}
|
||||
CFNetServiceUnscheduleFromRunLoop(_registrationService, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||
CFNetServiceSetClient(_registrationService, NULL, NULL);
|
||||
CFNetServiceCancel(_registrationService);
|
||||
CFRelease(_registrationService);
|
||||
_registrationService = NULL;
|
||||
}
|
||||
|
||||
dispatch_source_cancel(_source); // This will close the socket
|
||||
dispatch_source_cancel(_source);
|
||||
dispatch_semaphore_wait(_sourceSemaphore, DISPATCH_TIME_FOREVER); // Wait until the cancellation handler has been called which guarantees the listening socket is closed
|
||||
ARC_DISPATCH_RELEASE(_source);
|
||||
_source = NULL;
|
||||
_port = 0;
|
||||
|
||||
ARC_RELEASE(_serverName);
|
||||
_serverName = nil;
|
||||
ARC_RELEASE(_authenticationRealm);
|
||||
_authenticationRealm = nil;
|
||||
ARC_RELEASE(_authenticationBasicAccounts);
|
||||
_authenticationBasicAccounts = nil;
|
||||
ARC_RELEASE(_authenticationDigestAccounts);
|
||||
_authenticationDigestAccounts = nil;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (_disconnectTimer) {
|
||||
CFRunLoopTimerInvalidate(_disconnectTimer);
|
||||
CFRelease(_disconnectTimer);
|
||||
_disconnectTimer = NULL;
|
||||
[self _didDisconnect];
|
||||
}
|
||||
});
|
||||
|
||||
LOG_INFO(@"%@ stopped", [self class]);
|
||||
if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) {
|
||||
@@ -482,17 +610,6 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)start {
|
||||
return [self startWithPort:kDefaultPort bonjourName:@""];
|
||||
}
|
||||
|
||||
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name {
|
||||
NSMutableDictionary* options = [NSMutableDictionary dictionary];
|
||||
[options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port];
|
||||
[options setValue:name forKey:GCDWebServerOption_BonjourName];
|
||||
return [self startWithOptions:options];
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
- (void)_didEnterBackground:(NSNotification*)notification {
|
||||
@@ -507,20 +624,20 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
DCHECK([NSThread isMainThread]);
|
||||
LOG_DEBUG(@"Will enter foreground");
|
||||
if (!_source) {
|
||||
[self _start]; // TODO: There's probably nothing we can do on failure
|
||||
[self _start:NULL]; // TODO: There's probably nothing we can do on failure
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (BOOL)startWithOptions:(NSDictionary*)options {
|
||||
- (BOOL)startWithOptions:(NSDictionary*)options error:(NSError**)error {
|
||||
if (_options == nil) {
|
||||
_options = [options copy];
|
||||
#if TARGET_OS_IPHONE
|
||||
_suspendInBackground = [_GetOption(_options, GCDWebServerOption_AutomaticallySuspendInBackground, @YES) boolValue];
|
||||
if (((_suspendInBackground == NO) || ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground)) && ![self _start])
|
||||
if (((_suspendInBackground == NO) || ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground)) && ![self _start:error])
|
||||
#else
|
||||
if (![self _start])
|
||||
if (![self _start:error])
|
||||
#endif
|
||||
{
|
||||
ARC_RELEASE(_options);
|
||||
@@ -541,7 +658,7 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
}
|
||||
|
||||
- (BOOL)isRunning {
|
||||
return (_source ? YES : NO);
|
||||
return (_options ? YES : NO);
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
@@ -581,35 +698,57 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
}
|
||||
|
||||
- (NSURL*)bonjourServerURL {
|
||||
if (_source && _service) {
|
||||
CFStringRef name = CFNetServiceGetName(_service);
|
||||
if (name && CFStringGetLength(name)) {
|
||||
if (_source && _resolutionService) {
|
||||
NSString* name = (ARC_BRIDGE NSString*)CFNetServiceGetTargetHost(_resolutionService);
|
||||
if (name.length) {
|
||||
name = [name substringToIndex:(name.length - 1)]; // Strip trailing period at end of domain
|
||||
if (_port != 80) {
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@.local:%i/", name, (int)_port]];
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%i/", name, (int)_port]];
|
||||
} else {
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@.local/", name]];
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/", name]];
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)start {
|
||||
return [self startWithPort:kDefaultPort bonjourName:@""];
|
||||
}
|
||||
|
||||
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name {
|
||||
NSMutableDictionary* options = [NSMutableDictionary dictionary];
|
||||
[options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port];
|
||||
[options setValue:name forKey:GCDWebServerOption_BonjourName];
|
||||
return [self startWithOptions:options error:NULL];
|
||||
}
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
|
||||
- (BOOL)runWithPort:(NSUInteger)port {
|
||||
- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name {
|
||||
NSMutableDictionary* options = [NSMutableDictionary dictionary];
|
||||
[options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port];
|
||||
[options setValue:name forKey:GCDWebServerOption_BonjourName];
|
||||
return [self runWithOptions:options error:NULL];
|
||||
}
|
||||
|
||||
- (BOOL)runWithOptions:(NSDictionary*)options error:(NSError**)error {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
BOOL success = NO;
|
||||
_run = YES;
|
||||
void (*handler)(int) = signal(SIGINT, _SignalHandler);
|
||||
if (handler != SIG_ERR) {
|
||||
if ([self startWithPort:port bonjourName:@""]) {
|
||||
void (*termHandler)(int) = signal(SIGTERM, _SignalHandler);
|
||||
void (*intHandler)(int) = signal(SIGINT, _SignalHandler);
|
||||
if ((termHandler != SIG_ERR) && (intHandler != SIG_ERR)) {
|
||||
if ([self startWithOptions:options error:error]) {
|
||||
while (_run) {
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
|
||||
}
|
||||
[self stop];
|
||||
success = YES;
|
||||
}
|
||||
signal(SIGINT, handler);
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
signal(SIGINT, intHandler);
|
||||
signal(SIGTERM, termHandler);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -621,6 +760,12 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
@implementation GCDWebServer (Handlers)
|
||||
|
||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
||||
[self addDefaultHandlerForMethod:method requestClass:aClass asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
completionBlock(block(request));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block {
|
||||
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
|
||||
|
||||
if (![requestMethod isEqualToString:method]) {
|
||||
@@ -628,10 +773,16 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
}
|
||||
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
} processBlock:block];
|
||||
} asyncProcessBlock:block];
|
||||
}
|
||||
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
||||
[self addHandlerForMethod:method path:path requestClass:aClass asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
completionBlock(block(request));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block {
|
||||
if ([path hasPrefix:@"/"] && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) {
|
||||
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
|
||||
|
||||
@@ -643,13 +794,19 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
}
|
||||
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
} processBlock:block];
|
||||
} asyncProcessBlock:block];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block {
|
||||
[self addHandlerForMethod:method pathRegex:regex requestClass:aClass asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
completionBlock(block(request));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass asyncProcessBlock:(GCDWebServerAsyncProcessBlock)block {
|
||||
NSRegularExpression* expression = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:NULL];
|
||||
if (expression && [aClass isSubclassOfClass:[GCDWebServerRequest class]]) {
|
||||
[self addHandlerWithMatchBlock:^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) {
|
||||
@@ -657,12 +814,25 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
if (![requestMethod isEqualToString:method]) {
|
||||
return nil;
|
||||
}
|
||||
if ([expression firstMatchInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)] == nil) {
|
||||
|
||||
NSArray* matches = [expression matchesInString:urlPath options:0 range:NSMakeRange(0, urlPath.length)];
|
||||
if (matches.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
return ARC_AUTORELEASE([[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]);
|
||||
|
||||
NSMutableArray* captures = [NSMutableArray array];
|
||||
for (NSTextCheckingResult* result in matches) {
|
||||
// Start at 1; index 0 is the whole string
|
||||
for (NSUInteger i = 1; i < result.numberOfRanges; i++) {
|
||||
[captures addObject:[urlPath substringWithRange:[result rangeAtIndex:i]]];
|
||||
}
|
||||
}
|
||||
|
||||
GCDWebServerRequest* request = [[aClass alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery];
|
||||
[request setAttribute:captures forKey:GCDWebServerRequestAttribute_RegexCaptures];
|
||||
return ARC_AUTORELEASE(request);
|
||||
|
||||
} processBlock:block];
|
||||
} asyncProcessBlock:block];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
@@ -673,10 +843,10 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
@implementation GCDWebServer (GETHandlers)
|
||||
|
||||
- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge {
|
||||
GCDWebServerResponse* response = [GCDWebServerDataResponse responseWithData:staticData contentType:contentType];
|
||||
response.cacheControlMaxAge = cacheAge;
|
||||
[self addHandlerForMethod:@"GET" path:path requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
GCDWebServerResponse* response = [GCDWebServerDataResponse responseWithData:staticData contentType:contentType];
|
||||
response.cacheControlMaxAge = cacheAge;
|
||||
return response;
|
||||
|
||||
}];
|
||||
@@ -746,17 +916,18 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
|
||||
GCDWebServerResponse* response = nil;
|
||||
NSString* filePath = [directoryPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]];
|
||||
BOOL isDirectory;
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]) {
|
||||
if (isDirectory) {
|
||||
NSString* fileType = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL] fileType];
|
||||
if (fileType) {
|
||||
if ([fileType isEqualToString:NSFileTypeDirectory]) {
|
||||
if (indexFilename) {
|
||||
NSString* indexPath = [filePath stringByAppendingPathComponent:indexFilename];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath isDirectory:&isDirectory] && !isDirectory) {
|
||||
NSString* indexType = [[[NSFileManager defaultManager] attributesOfItemAtPath:indexPath error:NULL] fileType];
|
||||
if ([indexType isEqualToString:NSFileTypeRegular]) {
|
||||
return [GCDWebServerFileResponse responseWithFile:indexPath];
|
||||
}
|
||||
}
|
||||
response = [server _responseWithContentsOfDirectory:filePath];
|
||||
} else {
|
||||
} else if ([fileType isEqualToString:NSFileTypeRegular]) {
|
||||
if (allowRangeRequests) {
|
||||
response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange];
|
||||
[response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"];
|
||||
@@ -793,37 +964,33 @@ static inline id _GetOption(NSDictionary* options, NSString* key, id defaultValu
|
||||
- (void)logVerbose:(NSString*)format, ... {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
LOG_VERBOSE(@"%@", ARC_AUTORELEASE([[NSString alloc] initWithFormat:format arguments:arguments]));
|
||||
va_end(arguments);
|
||||
LOG_VERBOSE(@"%@", message);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
- (void)logInfo:(NSString*)format, ... {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
LOG_INFO(@"%@", ARC_AUTORELEASE([[NSString alloc] initWithFormat:format arguments:arguments]));
|
||||
va_end(arguments);
|
||||
LOG_INFO(@"%@", message);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
- (void)logWarning:(NSString*)format, ... {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
LOG_WARNING(@"%@", ARC_AUTORELEASE([[NSString alloc] initWithFormat:format arguments:arguments]));
|
||||
va_end(arguments);
|
||||
LOG_WARNING(@"%@", message);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
- (void)logError:(NSString*)format, ... {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments];
|
||||
LOG_ERROR(@"%@", ARC_AUTORELEASE([[NSString alloc] initWithFormat:format arguments:arguments]));
|
||||
va_end(arguments);
|
||||
LOG_ERROR(@"%@", message);
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
- (void)logException:(NSException*)exception {
|
||||
LOG_EXCEPTION(exception);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -866,7 +1033,7 @@ static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData,
|
||||
while (1) {
|
||||
ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length);
|
||||
if (result < 0) {
|
||||
length = NSNotFound;
|
||||
length = NSUIntegerMax;
|
||||
break;
|
||||
} else if (result == 0) {
|
||||
break;
|
||||
@@ -876,7 +1043,7 @@ static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData,
|
||||
outData.length = 2 * outData.length;
|
||||
}
|
||||
}
|
||||
if (length != NSNotFound) {
|
||||
if (length != NSUIntegerMax) {
|
||||
outData.length = length;
|
||||
response = _CreateHTTPMessageFromData(outData, NO);
|
||||
} else {
|
||||
@@ -899,10 +1066,12 @@ static void _LogResult(NSString* format, ...) {
|
||||
ARC_RELEASE(message);
|
||||
}
|
||||
|
||||
- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port {
|
||||
- (NSInteger)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path {
|
||||
DCHECK([NSThread isMainThread]);
|
||||
NSArray* ignoredHeaders = @[@"Date", @"Etag"]; // Dates are always different by definition and ETags depend on file system node IDs
|
||||
NSInteger result = -1;
|
||||
if ([self startWithPort:port bonjourName:nil]) {
|
||||
if ([self startWithOptions:options error:NULL]) {
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
|
||||
result = 0;
|
||||
NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
|
||||
@@ -927,7 +1096,7 @@ static void _LogResult(NSString* format, ...) {
|
||||
if (responseData) {
|
||||
CFHTTPMessageRef expectedResponse = _CreateHTTPMessageFromData(responseData, NO);
|
||||
if (expectedResponse) {
|
||||
CFHTTPMessageRef actualResponse = _CreateHTTPMessageFromPerformingRequest(requestData, port);
|
||||
CFHTTPMessageRef actualResponse = _CreateHTTPMessageFromPerformingRequest(requestData, self.port);
|
||||
if (actualResponse) {
|
||||
success = YES;
|
||||
|
||||
@@ -958,13 +1127,18 @@ static void _LogResult(NSString* format, ...) {
|
||||
}
|
||||
}
|
||||
|
||||
NSString* expectedContentLength = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(expectedResponse, CFSTR("Content-Length")));
|
||||
NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse));
|
||||
NSString* actualContentLength = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(actualResponse, CFSTR("Content-Length")));
|
||||
NSData* actualBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(actualResponse));
|
||||
if ([actualContentLength isEqualToString:expectedContentLength] && (actualBody.length > expectedBody.length)) { // Handle web browser closing connection before retrieving entire body (e.g. when playing a video file)
|
||||
actualBody = [actualBody subdataWithRange:NSMakeRange(0, expectedBody.length)];
|
||||
}
|
||||
if (![actualBody isEqualToData:expectedBody]) {
|
||||
_LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length);
|
||||
success = NO;
|
||||
#if !TARGET_OS_IPHONE
|
||||
#ifndef NDEBUG
|
||||
#if DEBUG
|
||||
if (GCDWebServerIsTextContentType([expectedHeaders objectForKey:@"Content-Type"])) {
|
||||
NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]];
|
||||
NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]];
|
||||
@@ -1000,9 +1174,12 @@ static void _LogResult(NSString* format, ...) {
|
||||
++result;
|
||||
}
|
||||
}
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
}
|
||||
|
||||
[self stop];
|
||||
|
||||
_ExecuteMainThreadRunLoopSources();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -29,23 +29,146 @@
|
||||
|
||||
@class GCDWebServerHandler;
|
||||
|
||||
/**
|
||||
* The GCDWebServerConnection class is instantiated by GCDWebServer to handle
|
||||
* each new HTTP connection. Each instance stays alive until the connection is
|
||||
* closed.
|
||||
*
|
||||
* You cannot use this class directly, but it is made public so you can
|
||||
* subclass it to override some hooks. Use the GCDWebServerOption_ConnectionClass
|
||||
* option for GCDWebServer to install your custom subclass.
|
||||
*
|
||||
* @warning The GCDWebServerConnection retains the GCDWebServer until the
|
||||
* connection is closed.
|
||||
*/
|
||||
@interface GCDWebServerConnection : NSObject
|
||||
|
||||
/**
|
||||
* Returns the GCDWebServer that owns the connection.
|
||||
*/
|
||||
@property(nonatomic, readonly) GCDWebServer* server;
|
||||
@property(nonatomic, readonly) NSData* localAddressData; // struct sockaddr
|
||||
|
||||
/**
|
||||
* Returns the address of the local peer (i.e. server) of the connection
|
||||
* as a raw "struct sockaddr".
|
||||
*/
|
||||
@property(nonatomic, readonly) NSData* localAddressData;
|
||||
|
||||
/**
|
||||
* Returns the address of the local peer (i.e. server) of the connection
|
||||
* as a dotted string.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* localAddressString;
|
||||
@property(nonatomic, readonly) NSData* remoteAddressData; // struct sockaddr
|
||||
|
||||
/**
|
||||
* Returns the address of the remote peer (i.e. client) of the connection
|
||||
* as a raw "struct sockaddr".
|
||||
*/
|
||||
@property(nonatomic, readonly) NSData* remoteAddressData;
|
||||
|
||||
/**
|
||||
* Returns the address of the remote peer (i.e. client) of the connection
|
||||
* as a dotted string.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* remoteAddressString;
|
||||
|
||||
/**
|
||||
* Returns the total number of bytes received from the remote peer (i.e. client)
|
||||
* so far.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSUInteger totalBytesRead;
|
||||
|
||||
/**
|
||||
* Returns the total number of bytes sent to the remote peer (i.e. client) so far.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSUInteger totalBytesWritten;
|
||||
|
||||
@end
|
||||
|
||||
// These methods can be called from any thread
|
||||
/**
|
||||
* Hooks to customize the behavior of GCDWebServer HTTP connections.
|
||||
*
|
||||
* @warning These methods can be called on any GCD thread.
|
||||
* Be sure to also call "super" when overriding them.
|
||||
*/
|
||||
@interface GCDWebServerConnection (Subclassing)
|
||||
- (BOOL)open; // Return NO to reject connection e.g. after validating local or remote addresses
|
||||
- (void)didReadBytes:(const void*)bytes length:(NSUInteger)length; // Called after data has been read from the connection
|
||||
- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length; // Called after data has been written to the connection
|
||||
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block; // Only called if the request can be processed
|
||||
- (GCDWebServerResponse*)replaceResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request; // Default implementation replaces any response matching the "ETag" or "Last-Modified-Date" header of the request by a barebone "Not-Modified" (304) one
|
||||
- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; // If request headers was malformed, "request" will be nil
|
||||
|
||||
/**
|
||||
* This method is called when the connection is opened.
|
||||
*
|
||||
* Return NO to reject the connection e.g. after validating the local
|
||||
* or remote address.
|
||||
*/
|
||||
- (BOOL)open;
|
||||
|
||||
/**
|
||||
* This method is called whenever data has been received
|
||||
* from the remote peer (i.e. client).
|
||||
*
|
||||
* @warning Do not attempt to modify this data.
|
||||
*/
|
||||
- (void)didReadBytes:(const void*)bytes length:(NSUInteger)length;
|
||||
|
||||
/**
|
||||
* This method is called whenever data has been sent
|
||||
* to the remote peer (i.e. client).
|
||||
*
|
||||
* @warning Do not attempt to modify this data.
|
||||
*/
|
||||
- (void)didWriteBytes:(const void*)bytes length:(NSUInteger)length;
|
||||
|
||||
/**
|
||||
* This method is called after the HTTP headers have been received to
|
||||
* allow replacing the request URL by another one.
|
||||
*
|
||||
* The default implementation returns the original URL.
|
||||
*/
|
||||
- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary*)headers;
|
||||
|
||||
/**
|
||||
* Assuming a valid HTTP request was received, this method is called before
|
||||
* the request is processed.
|
||||
*
|
||||
* Return a non-nil GCDWebServerResponse to bypass the request processing entirely.
|
||||
*
|
||||
* The default implementation checks for HTTP authentication if applicable
|
||||
* and returns a barebone 401 status code response if authentication failed.
|
||||
*/
|
||||
- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request;
|
||||
|
||||
/**
|
||||
* Assuming a valid HTTP request was received and -preflightRequest: returned nil,
|
||||
* this method is called to process the request by executing the handler's
|
||||
* process block.
|
||||
*/
|
||||
- (void)processRequest:(GCDWebServerRequest*)request completion:(GCDWebServerCompletionBlock)completion;
|
||||
|
||||
/**
|
||||
* Assuming a valid HTTP request was received and either -preflightRequest:
|
||||
* or -processRequest:completion: returned a non-nil GCDWebServerResponse,
|
||||
* this method is called to override the response.
|
||||
*
|
||||
* You can either modify the current response and return it, or return a
|
||||
* completely new one.
|
||||
*
|
||||
* The default implementation replaces any response matching the "ETag" or
|
||||
* "Last-Modified-Date" header of the request by a barebone "Not-Modified" (304)
|
||||
* one.
|
||||
*/
|
||||
- (GCDWebServerResponse*)overrideResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request;
|
||||
|
||||
/**
|
||||
* This method is called if any error happens while validing or processing
|
||||
* the request or if no GCDWebServerResponse was generated during processing.
|
||||
*
|
||||
* @warning If the request was invalid (e.g. the HTTP headers were malformed),
|
||||
* the "request" argument will be nil.
|
||||
*/
|
||||
- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode;
|
||||
|
||||
/**
|
||||
* Called when the connection is closed.
|
||||
*/
|
||||
- (void)close;
|
||||
|
||||
@end
|
||||
|
||||
@@ -48,6 +48,7 @@ static NSData* _CRLFData = nil;
|
||||
static NSData* _CRLFCRLFData = nil;
|
||||
static NSData* _continueData = nil;
|
||||
static NSData* _lastChunkData = nil;
|
||||
static NSString* _digestAuthenticationNonce = nil;
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
static int32_t _connectionCounter = 0;
|
||||
#endif
|
||||
@@ -357,6 +358,11 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
if (_lastChunkData == nil) {
|
||||
_lastChunkData = [[NSData alloc] initWithBytes:"0\r\n\r\n" length:5];
|
||||
}
|
||||
if (_digestAuthenticationNonce == nil) {
|
||||
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
|
||||
_digestAuthenticationNonce = ARC_RETAIN(GCDWebServerComputeMD5Digest(@"%@", ARC_BRIDGE_RELEASE(CFUUIDCreateString(kCFAllocatorDefault, uuid))));
|
||||
CFRelease(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_initializeResponseHeadersWithStatusCode:(NSInteger)statusCode {
|
||||
@@ -367,25 +373,37 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Date"), (ARC_BRIDGE CFStringRef)GCDWebServerFormatRFC822([NSDate date]));
|
||||
}
|
||||
|
||||
- (void)_startProcessingRequest {
|
||||
DCHECK(_responseMessage == NULL);
|
||||
|
||||
GCDWebServerResponse* preflightResponse = [self preflightRequest:_request];
|
||||
if (preflightResponse) {
|
||||
[self _finishProcessingRequest:preflightResponse];
|
||||
} else {
|
||||
[self processRequest:_request completion:^(GCDWebServerResponse* processResponse) {
|
||||
[self _finishProcessingRequest:processResponse];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
- (void)_processRequest {
|
||||
- (void)_finishProcessingRequest:(GCDWebServerResponse*)response {
|
||||
DCHECK(_responseMessage == NULL);
|
||||
BOOL hasBody = NO;
|
||||
|
||||
GCDWebServerResponse* response = [self processRequest:_request withBlock:_handler.processBlock];
|
||||
if (response) {
|
||||
response = [self replaceResponse:response forRequest:_request];
|
||||
if (response) {
|
||||
if ([response hasBody]) {
|
||||
[response prepareForReading];
|
||||
hasBody = !_virtualHEAD;
|
||||
}
|
||||
NSError* error = nil;
|
||||
if (hasBody && ![response performOpen:&error]) {
|
||||
LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error);
|
||||
} else {
|
||||
_response = ARC_RETAIN(response);
|
||||
}
|
||||
response = [self overrideResponse:response forRequest:_request];
|
||||
}
|
||||
if (response) {
|
||||
if ([response hasBody]) {
|
||||
[response prepareForReading];
|
||||
hasBody = !_virtualHEAD;
|
||||
}
|
||||
NSError* error = nil;
|
||||
if (hasBody && ![response performOpen:&error]) {
|
||||
LOG_ERROR(@"Failed opening response body for socket %i: %@", _socket, error);
|
||||
} else {
|
||||
_response = ARC_RETAIN(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,7 +425,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
if (_response.contentType != nil) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType));
|
||||
}
|
||||
if (_response.contentLength != NSNotFound) {
|
||||
if (_response.contentLength != NSUIntegerMax) {
|
||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]);
|
||||
}
|
||||
if (_response.usesChunkedTransferEncoding) {
|
||||
@@ -462,7 +480,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
|
||||
NSError* localError = nil;
|
||||
if ([_request performClose:&localError]) {
|
||||
[self _processRequest];
|
||||
[self _startProcessingRequest];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
@@ -471,7 +489,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
}];
|
||||
} else {
|
||||
if ([_request performClose:&error]) {
|
||||
[self _processRequest];
|
||||
[self _startProcessingRequest];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
@@ -492,7 +510,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
|
||||
NSError* localError = nil;
|
||||
if ([_request performClose:&localError]) {
|
||||
[self _processRequest];
|
||||
[self _startProcessingRequest];
|
||||
} else {
|
||||
LOG_ERROR(@"Failed closing request body for socket %i: %@", _socket, error);
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_InternalServerError];
|
||||
@@ -513,11 +531,15 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
requestMethod = @"GET";
|
||||
_virtualHEAD = YES;
|
||||
}
|
||||
NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage)); // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones
|
||||
NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(_requestMessage));
|
||||
if (requestURL) {
|
||||
requestURL = [self rewriteRequestURL:requestURL withMethod:requestMethod headers:requestHeaders];
|
||||
DCHECK(requestURL);
|
||||
}
|
||||
NSString* requestPath = requestURL ? GCDWebServerUnescapeURLString(ARC_BRIDGE_RELEASE(CFURLCopyPath((CFURLRef)requestURL))) : nil; // Don't use -[NSURL path] which strips the ending slash
|
||||
NSString* queryString = requestURL ? ARC_BRIDGE_RELEASE(CFURLCopyQueryString((CFURLRef)requestURL, NULL)) : nil; // Don't use -[NSURL query] to make sure query is not unescaped;
|
||||
NSDictionary* requestQuery = queryString ? GCDWebServerParseURLEncodedForm(queryString) : @{};
|
||||
NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage)); // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones
|
||||
if (requestMethod && requestURL && requestHeaders && requestPath && requestQuery) {
|
||||
for (_handler in _server.handlers) {
|
||||
_request = ARC_RETAIN(_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery));
|
||||
@@ -529,9 +551,9 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
if ([_request hasBody]) {
|
||||
[_request prepareForWriting];
|
||||
if (_request.usesChunkedTransferEncoding || (extraData.length <= _request.contentLength)) {
|
||||
NSString* expectHeader = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(_requestMessage, CFSTR("Expect")));
|
||||
NSString* expectHeader = [requestHeaders objectForKey:@"Expect"];
|
||||
if (expectHeader) {
|
||||
if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) {
|
||||
if ([expectHeader caseInsensitiveCompare:@"100-continue"] == NSOrderedSame) { // TODO: Actually validate request before continuing
|
||||
[self _writeData:_continueData withCompletionBlock:^(BOOL success) {
|
||||
|
||||
if (success) {
|
||||
@@ -559,7 +581,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
||||
[self abortRequest:_request withStatusCode:kGCDWebServerHTTPStatusCode_BadRequest];
|
||||
}
|
||||
} else {
|
||||
[self _processRequest];
|
||||
[self _startProcessingRequest];
|
||||
}
|
||||
} else {
|
||||
_request = [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:requestPath query:requestQuery];
|
||||
@@ -666,11 +688,11 @@ static NSString* _StringFromAddressData(NSData* data) {
|
||||
_connectionIndex = OSAtomicIncrement32(&_connectionCounter);
|
||||
|
||||
_requestPath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
|
||||
_requestFD = open([_requestPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY);
|
||||
_requestFD = open([_requestPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
DCHECK(_requestFD > 0);
|
||||
|
||||
_responsePath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
|
||||
_responseFD = open([_responsePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY);
|
||||
_responseFD = open([_responsePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
DCHECK(_responseFD > 0);
|
||||
}
|
||||
#endif
|
||||
@@ -704,16 +726,69 @@ static NSString* _StringFromAddressData(NSData* data) {
|
||||
#endif
|
||||
}
|
||||
|
||||
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block {
|
||||
LOG_DEBUG(@"Connection on socket %i processing request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
|
||||
- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary*)headers {
|
||||
return url;
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc2617
|
||||
- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request {
|
||||
LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
|
||||
GCDWebServerResponse* response = nil;
|
||||
if (_server.authenticationBasicAccounts) {
|
||||
__block BOOL authenticated = NO;
|
||||
NSString* authorizationHeader = [request.headers objectForKey:@"Authorization"];
|
||||
if ([authorizationHeader hasPrefix:@"Basic "]) {
|
||||
NSString* basicAccount = [authorizationHeader substringFromIndex:6];
|
||||
[_server.authenticationBasicAccounts enumerateKeysAndObjectsUsingBlock:^(NSString* username, NSString* digest, BOOL* stop) {
|
||||
if ([basicAccount isEqualToString:digest]) {
|
||||
authenticated = YES;
|
||||
*stop = YES;
|
||||
}
|
||||
}];
|
||||
}
|
||||
if (!authenticated) {
|
||||
response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Unauthorized];
|
||||
[response setValue:[NSString stringWithFormat:@"Basic realm=\"%@\"", _server.authenticationRealm] forAdditionalHeader:@"WWW-Authenticate"];
|
||||
}
|
||||
} else if (_server.authenticationDigestAccounts) {
|
||||
BOOL authenticated = NO;
|
||||
BOOL isStaled = NO;
|
||||
NSString* authorizationHeader = [request.headers objectForKey:@"Authorization"];
|
||||
if ([authorizationHeader hasPrefix:@"Digest "]) {
|
||||
NSString* realm = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"realm");
|
||||
if ([realm isEqualToString:_server.authenticationRealm]) {
|
||||
NSString* nonce = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"nonce");
|
||||
if ([nonce isEqualToString:_digestAuthenticationNonce]) {
|
||||
NSString* username = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"username");
|
||||
NSString* uri = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"uri");
|
||||
NSString* actualResponse = GCDWebServerExtractHeaderValueParameter(authorizationHeader, @"response");
|
||||
NSString* ha1 = [_server.authenticationDigestAccounts objectForKey:username];
|
||||
NSString* ha2 = GCDWebServerComputeMD5Digest(@"%@:%@", request.method, uri); // We cannot use "request.path" as the query string is required
|
||||
NSString* expectedResponse = GCDWebServerComputeMD5Digest(@"%@:%@:%@", ha1, _digestAuthenticationNonce, ha2);
|
||||
if ([actualResponse isEqualToString:expectedResponse]) {
|
||||
authenticated = YES;
|
||||
}
|
||||
} else if (nonce.length) {
|
||||
isStaled = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!authenticated) {
|
||||
response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_Unauthorized];
|
||||
[response setValue:[NSString stringWithFormat:@"Digest realm=\"%@\", nonce=\"%@\"%@", _server.authenticationRealm, _digestAuthenticationNonce, isStaled ? @", stale=TRUE" : @""] forAdditionalHeader:@"WWW-Authenticate"]; // TODO: Support Quality of Protection ("qop")
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
- (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);
|
||||
@try {
|
||||
response = block(request);
|
||||
_handler.asyncProcessBlock(request, completion);
|
||||
}
|
||||
@catch (NSException* exception) {
|
||||
LOG_EXCEPTION(exception);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
|
||||
@@ -731,7 +806,7 @@ static inline BOOL _CompareResources(NSString* responseETag, NSString* requestET
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (GCDWebServerResponse*)replaceResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request {
|
||||
- (GCDWebServerResponse*)overrideResponse:(GCDWebServerResponse*)response forRequest:(GCDWebServerRequest*)request {
|
||||
if ((response.statusCode >= 200) && (response.statusCode < 300) && _CompareResources(response.eTag, request.ifNoneMatch, response.lastModifiedDate, request.ifModifiedSince)) {
|
||||
NSInteger code = [request.method isEqualToString:@"HEAD"] || [request.method isEqualToString:@"GET"] ? kGCDWebServerHTTPStatusCode_NotModified : kGCDWebServerHTTPStatusCode_PreconditionFailed;
|
||||
GCDWebServerResponse* newResponse = [GCDWebServerResponse responseWithStatusCode:code];
|
||||
|
||||
@@ -31,14 +31,69 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Converts a file extension to the corresponding MIME type.
|
||||
* If there is no match, "application/octet-stream" is returned.
|
||||
*/
|
||||
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension);
|
||||
|
||||
/**
|
||||
* Add percent-escapes to a string so it can be used in a URL.
|
||||
* The legal characters ":@/?&=+" are also escaped to ensure compatibility
|
||||
* with URL encoded forms and URL queries.
|
||||
*/
|
||||
NSString* GCDWebServerEscapeURLString(NSString* string);
|
||||
|
||||
/**
|
||||
* Unescapes a URL percent-encoded string.
|
||||
*/
|
||||
NSString* GCDWebServerUnescapeURLString(NSString* string);
|
||||
|
||||
/**
|
||||
* Extracts the unescaped names and values from an
|
||||
* "application/x-www-form-urlencoded" form.
|
||||
* http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
|
||||
*/
|
||||
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form);
|
||||
NSString* GCDWebServerGetPrimaryIPv4Address(); // Returns IPv4 address of primary connected service on OS X or of WiFi interface on iOS if connected
|
||||
|
||||
/**
|
||||
* On OS X, returns the IPv4 address as a dotted string of the primary connected
|
||||
* service or nil if not available.
|
||||
*
|
||||
* On iOS, returns the IPv4 address as a dotted string of the WiFi interface
|
||||
* if connected or nil otherwise.
|
||||
*/
|
||||
NSString* GCDWebServerGetPrimaryIPv4Address();
|
||||
|
||||
/**
|
||||
* Converts a date into a string using RFC822 formatting.
|
||||
* https://tools.ietf.org/html/rfc822#section-5
|
||||
* https://tools.ietf.org/html/rfc1123#section-5.2.14
|
||||
*/
|
||||
NSString* GCDWebServerFormatRFC822(NSDate* date);
|
||||
|
||||
/**
|
||||
* Converts a RFC822 formatted string into a date.
|
||||
* https://tools.ietf.org/html/rfc822#section-5
|
||||
* https://tools.ietf.org/html/rfc1123#section-5.2.14
|
||||
*
|
||||
* @warning Timezones other than GMT are not supported by this function.
|
||||
*/
|
||||
NSDate* GCDWebServerParseRFC822(NSString* string);
|
||||
|
||||
/**
|
||||
* Converts a date into a string using IOS 8601 formatting.
|
||||
* http://tools.ietf.org/html/rfc3339#section-5.6
|
||||
*/
|
||||
NSString* GCDWebServerFormatISO8601(NSDate* date);
|
||||
|
||||
/**
|
||||
* Converts a ISO 8601 formatted string into a date.
|
||||
* http://tools.ietf.org/html/rfc3339#section-5.6
|
||||
*
|
||||
* @warning Only "calendar" variant is supported at this time and timezones
|
||||
* other than GMT are not supported either.
|
||||
*/
|
||||
NSDate* GCDWebServerParseISO8601(NSString* string);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#else
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
#endif
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
|
||||
#import <ifaddrs.h>
|
||||
#import <net/if.h>
|
||||
@@ -42,8 +43,7 @@ static NSDateFormatter* _dateFormatterRFC822 = nil;
|
||||
static NSDateFormatter* _dateFormatterISO8601 = nil;
|
||||
static dispatch_queue_t _dateFormatterQueue = NULL;
|
||||
|
||||
// HTTP/1.1 server must use RFC822
|
||||
// TODO: Handle RFC 850 and ANSI C's asctime() format (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3)
|
||||
// TODO: Handle RFC 850 and ANSI C's asctime() format
|
||||
void GCDWebServerInitializeFunctions() {
|
||||
DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread
|
||||
if (_dateFormatterRFC822 == nil) {
|
||||
@@ -79,13 +79,11 @@ NSString* GCDWebServerNormalizeHeaderValue(NSString* value) {
|
||||
}
|
||||
|
||||
NSString* GCDWebServerTruncateHeaderValue(NSString* value) {
|
||||
DCHECK([value isEqualToString:GCDWebServerNormalizeHeaderValue(value)]);
|
||||
NSRange range = [value rangeOfString:@";"];
|
||||
return range.location != NSNotFound ? [value substringToIndex:range.location] : value;
|
||||
}
|
||||
|
||||
NSString* GCDWebServerExtractHeaderValueParameter(NSString* value, NSString* name) {
|
||||
DCHECK([value isEqualToString:GCDWebServerNormalizeHeaderValue(value)]);
|
||||
NSString* parameter = nil;
|
||||
NSScanner* scanner = [[NSScanner alloc] initWithString:value];
|
||||
[scanner setCaseSensitive:NO]; // Assume parameter names are case-insensitive
|
||||
@@ -188,7 +186,6 @@ NSString* GCDWebServerUnescapeURLString(NSString* string) {
|
||||
return ARC_BRIDGE_RELEASE(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
|
||||
}
|
||||
|
||||
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
|
||||
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
|
||||
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
|
||||
NSScanner* scanner = [[NSScanner alloc] initWithString:form];
|
||||
@@ -201,15 +198,19 @@ NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
|
||||
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
||||
|
||||
NSString* value = nil;
|
||||
if (![scanner scanUpToString:@"&" intoString:&value]) {
|
||||
break;
|
||||
[scanner scanUpToString:@"&" intoString:&value];
|
||||
if (value == nil) {
|
||||
value = @"";
|
||||
}
|
||||
|
||||
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||
NSString* unescapedKey = key ? GCDWebServerUnescapeURLString(key) : nil;
|
||||
value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||
if (key && value) {
|
||||
[parameters setObject:GCDWebServerUnescapeURLString(value) forKey:GCDWebServerUnescapeURLString(key)];
|
||||
NSString* unescapedValue = value ? GCDWebServerUnescapeURLString(value) : nil;
|
||||
if (unescapedKey && unescapedValue) {
|
||||
[parameters setObject:unescapedValue forKey:unescapedKey];
|
||||
} else {
|
||||
LOG_WARNING(@"Failed parsing URL encoded form for key \"%@\" and value \"%@\"", key, value);
|
||||
DNOT_REACHED();
|
||||
}
|
||||
|
||||
@@ -266,3 +267,22 @@ NSString* GCDWebServerGetPrimaryIPv4Address() {
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) {
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
const char* string = [ARC_AUTORELEASE([[NSString alloc] initWithFormat:format arguments:arguments]) UTF8String];
|
||||
va_end(arguments);
|
||||
unsigned char md5[CC_MD5_DIGEST_LENGTH];
|
||||
CC_MD5(string, (CC_LONG)strlen(string), md5);
|
||||
char buffer[2 * CC_MD5_DIGEST_LENGTH + 1];
|
||||
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; ++i) {
|
||||
unsigned char byte = md5[i];
|
||||
unsigned char byteHi = (byte & 0xF0) >> 4;
|
||||
buffer[2 * i + 0] = byteHi >= 10 ? 'a' + byteHi - 10 : '0' + byteHi;
|
||||
unsigned char byteLo = byte & 0x0F;
|
||||
buffer[2 * i + 1] = byteLo >= 10 ? 'a' + byteLo - 10 : '0' + byteLo;
|
||||
}
|
||||
buffer[2 * CC_MD5_DIGEST_LENGTH] = 0;
|
||||
return [NSString stringWithUTF8String:buffer];
|
||||
}
|
||||
|
||||
@@ -30,12 +30,18 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
* Convenience constants for "informational" HTTP status codes.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerInformationalHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_Continue = 100,
|
||||
kGCDWebServerHTTPStatusCode_SwitchingProtocols = 101,
|
||||
kGCDWebServerHTTPStatusCode_Processing = 102
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience constants for "successful" HTTP status codes.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_OK = 200,
|
||||
kGCDWebServerHTTPStatusCode_Created = 201,
|
||||
@@ -48,6 +54,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_AlreadyReported = 208
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience constants for "redirection" HTTP status codes.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_MultipleChoices = 300,
|
||||
kGCDWebServerHTTPStatusCode_MovedPermanently = 301,
|
||||
@@ -59,6 +68,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_PermanentRedirect = 308
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience constants for "client error" HTTP status codes.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_BadRequest = 400,
|
||||
kGCDWebServerHTTPStatusCode_Unauthorized = 401,
|
||||
@@ -87,6 +99,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience constants for "server error" HTTP status codes.
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) {
|
||||
kGCDWebServerHTTPStatusCode_InternalServerError = 500,
|
||||
kGCDWebServerHTTPStatusCode_NotImplemented = 501,
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <TargetConditionals.h>
|
||||
#import <AvailabilityMacros.h>
|
||||
#import <os/object.h>
|
||||
|
||||
#if __has_feature(objc_arc)
|
||||
#define ARC_BRIDGE __bridge
|
||||
@@ -35,7 +34,7 @@
|
||||
#define ARC_RELEASE(__OBJECT__)
|
||||
#define ARC_AUTORELEASE(__OBJECT__) __OBJECT__
|
||||
#define ARC_DEALLOC(__OBJECT__)
|
||||
#if (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_6_0)) || (!TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_8))
|
||||
#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE
|
||||
#define ARC_DISPATCH_RETAIN(__OBJECT__)
|
||||
#define ARC_DISPATCH_RELEASE(__OBJECT__)
|
||||
#else
|
||||
@@ -67,7 +66,7 @@
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
#import "GCDWebServerErrorResponse.h"
|
||||
#import "GCDWebServerFileResponse.h"
|
||||
#import "GCDWebServerStreamingResponse.h"
|
||||
#import "GCDWebServerStreamedResponse.h"
|
||||
|
||||
#ifdef __GCDWEBSERVER_LOGGING_HEADER__
|
||||
|
||||
@@ -85,13 +84,7 @@ extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_
|
||||
#define LOG_ERROR(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Error) GCDLogMessage(kGCDWebServerLogLevel_Error, __VA_ARGS__); } while (0)
|
||||
#define LOG_EXCEPTION(__EXCEPTION__) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Exception) GCDLogMessage(kGCDWebServerLogLevel_Exception, @"%@", __EXCEPTION__); } while (0)
|
||||
|
||||
#ifdef NDEBUG
|
||||
|
||||
#define DCHECK(__CONDITION__)
|
||||
#define DNOT_REACHED()
|
||||
#define LOG_DEBUG(...)
|
||||
|
||||
#else
|
||||
#if DEBUG
|
||||
|
||||
#define DCHECK(__CONDITION__) \
|
||||
do { \
|
||||
@@ -102,6 +95,12 @@ extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_
|
||||
#define DNOT_REACHED() abort()
|
||||
#define LOG_DEBUG(...) do { if (GCDLogLevel <= kGCDWebServerLogLevel_Debug) GCDLogMessage(kGCDWebServerLogLevel_Debug, __VA_ARGS__); } while (0)
|
||||
|
||||
#else
|
||||
|
||||
#define DCHECK(__CONDITION__)
|
||||
#define DNOT_REACHED()
|
||||
#define LOG_DEBUG(...)
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -111,7 +110,11 @@ extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_
|
||||
#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
|
||||
|
||||
static inline BOOL GCDWebServerIsValidByteRange(NSRange range) {
|
||||
return ((range.location != NSNotFound) || (range.length > 0));
|
||||
return ((range.location != NSUIntegerMax) || (range.length > 0));
|
||||
}
|
||||
|
||||
static inline NSError* GCDWebServerMakePosixError(int code) {
|
||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithUTF8String:strerror(code)]}];
|
||||
}
|
||||
|
||||
extern void GCDWebServerInitializeFunctions();
|
||||
@@ -121,6 +124,7 @@ extern NSString* GCDWebServerExtractHeaderValueParameter(NSString* header, NSStr
|
||||
extern NSStringEncoding GCDWebServerStringEncodingFromCharset(NSString* charset);
|
||||
extern BOOL GCDWebServerIsTextContentType(NSString* type);
|
||||
extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType);
|
||||
extern NSString* GCDWebServerComputeMD5Digest(NSString* format, ...) NS_FORMAT_FUNCTION(1,2);
|
||||
|
||||
@interface GCDWebServerConnection ()
|
||||
- (id)initWithServer:(GCDWebServer*)server localAddress:(NSData*)localAddress remoteAddress:(NSData*)remoteAddress socket:(CFSocketNativeHandle)socket;
|
||||
@@ -129,6 +133,9 @@ extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType);
|
||||
@interface GCDWebServer ()
|
||||
@property(nonatomic, readonly) NSArray* handlers;
|
||||
@property(nonatomic, readonly) NSString* serverName;
|
||||
@property(nonatomic, readonly) NSString* authenticationRealm;
|
||||
@property(nonatomic, readonly) NSDictionary* authenticationBasicAccounts;
|
||||
@property(nonatomic, readonly) NSDictionary* authenticationDigestAccounts;
|
||||
@property(nonatomic, readonly) BOOL shouldAutomaticallyMapHEADToGET;
|
||||
- (void)willStartConnection:(GCDWebServerConnection*)connection;
|
||||
- (void)didEndConnection:(GCDWebServerConnection*)connection;
|
||||
@@ -136,8 +143,7 @@ extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType);
|
||||
|
||||
@interface GCDWebServerHandler : NSObject
|
||||
@property(nonatomic, readonly) GCDWebServerMatchBlock matchBlock;
|
||||
@property(nonatomic, readonly) GCDWebServerProcessBlock processBlock;
|
||||
- (id)initWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
||||
@property(nonatomic, readonly) GCDWebServerAsyncProcessBlock asyncProcessBlock;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerRequest ()
|
||||
@@ -146,6 +152,7 @@ extern NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType);
|
||||
- (BOOL)performOpen:(NSError**)error;
|
||||
- (BOOL)performWriteData:(NSData*)data error:(NSError**)error;
|
||||
- (BOOL)performClose:(NSError**)error;
|
||||
- (void)setAttribute:(id)attribute forKey:(NSString*)key;
|
||||
@end
|
||||
|
||||
@interface GCDWebServerResponse ()
|
||||
|
||||
@@ -27,25 +27,156 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
* Attribute key to retrieve an NSArray containing NSStrings from a GCDWebServerRequest
|
||||
* with the contents of any regular expression captures done on the request path.
|
||||
*
|
||||
* @warning This attribute will only be set on the request if adding a handler using
|
||||
* -addHandlerForMethod:pathRegex:requestClass:processBlock:.
|
||||
*/
|
||||
extern NSString* const GCDWebServerRequestAttribute_RegexCaptures;
|
||||
|
||||
/**
|
||||
* This protocol is used by the GCDWebServerConnection to communicate with
|
||||
* the GCDWebServerRequest and write the received HTTP body data.
|
||||
*
|
||||
* Note that multiple GCDWebServerBodyWriter objects can be chained together
|
||||
* internally e.g. to automatically decode gzip encoded content before
|
||||
* passing it on to the GCDWebServerRequest.
|
||||
*
|
||||
* @warning These methods can be called on any GCD thread.
|
||||
*/
|
||||
@protocol GCDWebServerBodyWriter <NSObject>
|
||||
- (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
||||
- (BOOL)close:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
||||
|
||||
/**
|
||||
* This method is called before any body data is received.
|
||||
*
|
||||
* It should return YES on success or NO on failure and set the "error" argument
|
||||
* which is guaranteed to be non-NULL.
|
||||
*/
|
||||
- (BOOL)open:(NSError**)error;
|
||||
|
||||
/**
|
||||
* This method is called whenever body data has been received.
|
||||
*
|
||||
* It should return YES on success or NO on failure and set the "error" argument
|
||||
* which is guaranteed to be non-NULL.
|
||||
*/
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error;
|
||||
|
||||
/**
|
||||
* This method is called after all body data has been received.
|
||||
*
|
||||
* It should return YES on success or NO on failure and set the "error" argument
|
||||
* which is guaranteed to be non-NULL.
|
||||
*/
|
||||
- (BOOL)close:(NSError**)error;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The GCDWebServerRequest class is instantiated by the GCDWebServerConnection
|
||||
* after the HTTP headers have been received. Each instance wraps a single HTTP
|
||||
* request. If a body is present, the methods from the GCDWebServerBodyWriter
|
||||
* protocol will be called by the GCDWebServerConnection to receive it.
|
||||
*
|
||||
* The default implementation of the GCDWebServerBodyWriter protocol on the class
|
||||
* simply ignores the body data.
|
||||
*
|
||||
* @warning GCDWebServerRequest instances can be created and used on any GCD thread.
|
||||
*/
|
||||
@interface GCDWebServerRequest : NSObject <GCDWebServerBodyWriter>
|
||||
|
||||
/**
|
||||
* Returns the HTTP method for the request.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* method;
|
||||
|
||||
/**
|
||||
* Returns the URL for the request.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSURL* URL;
|
||||
|
||||
/**
|
||||
* Returns the HTTP headers for the request.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSDictionary* headers;
|
||||
|
||||
/**
|
||||
* Returns the path component of the URL for the request.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* path;
|
||||
@property(nonatomic, readonly) NSDictionary* query; // May be nil
|
||||
@property(nonatomic, readonly) NSString* contentType; // Automatically parsed from headers (nil if request has no body or set to "application/octet-stream" if a body is present without a "Content-Type" header)
|
||||
@property(nonatomic, readonly) NSUInteger contentLength; // Automatically parsed from headers (NSNotFound if request has no "Content-Length" header)
|
||||
@property(nonatomic, readonly) NSDate* ifModifiedSince; // Automatically parsed from headers (nil if request has no "If-Modified-Since" header or it is malformatted)
|
||||
@property(nonatomic, readonly) NSString* ifNoneMatch; // Automatically parsed from headers (nil if request has no "If-None-Match" header)
|
||||
@property(nonatomic, readonly) NSRange byteRange; // Automatically parsed from headers ([NSNotFound, 0] if request has no "Range" header, [offset, length] for byte range from beginning or [NSNotFound, -length] from end)
|
||||
@property(nonatomic, readonly) BOOL acceptsGzipContentEncoding; // Automatically parsed from headers
|
||||
|
||||
/**
|
||||
* Returns the parsed and unescaped query component of the URL for the request.
|
||||
*
|
||||
* @warning This property will be nil if there is no query in the URL.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSDictionary* query;
|
||||
|
||||
/**
|
||||
* Returns the content type for the body of the request parsed from the
|
||||
* "Content-Type" header.
|
||||
*
|
||||
* This property will be nil if the request has no body or set to
|
||||
* "application/octet-stream" if a body is present but there was no
|
||||
* "Content-Type" header.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* contentType;
|
||||
|
||||
/**
|
||||
* Returns the content length for the body of the request parsed from the
|
||||
* "Content-Length" header.
|
||||
*
|
||||
* This property will be set to "NSUIntegerMax" if the request has no body or
|
||||
* if there is a body but no "Content-Length" header, typically because
|
||||
* chunked transfer encoding is used.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSUInteger contentLength;
|
||||
|
||||
/**
|
||||
* Returns the parsed "If-Modified-Since" header or nil if absent or malformed.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSDate* ifModifiedSince;
|
||||
|
||||
/**
|
||||
* Returns the parsed "If-None-Match" header or nil if absent or malformed.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* ifNoneMatch;
|
||||
|
||||
/**
|
||||
* Returns the parsed "Range" header or (NSUIntegerMax, 0) if absent or malformed.
|
||||
* The range will be set to (offset, length) if expressed from the beginning
|
||||
* of the entity body, or (NSUIntegerMax, length) if expressed from its end.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSRange byteRange;
|
||||
|
||||
/**
|
||||
* Returns YES if the client supports gzip content encoding according to the
|
||||
* "Accept-Encoding" header.
|
||||
*/
|
||||
@property(nonatomic, readonly) BOOL acceptsGzipContentEncoding;
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query;
|
||||
- (BOOL)hasBody; // Convenience method that checks if "contentType" is not nil
|
||||
- (BOOL)hasByteRange; // Convenience method that checks "byteRange"
|
||||
|
||||
/**
|
||||
* Convenience method that checks if the contentType property is defined.
|
||||
*/
|
||||
- (BOOL)hasBody;
|
||||
|
||||
/**
|
||||
* Convenience method that checks if the byteRange property is defined.
|
||||
*/
|
||||
- (BOOL)hasByteRange;
|
||||
|
||||
/**
|
||||
* Retrieves an attribute associated with this request using the given key.
|
||||
*
|
||||
* @return The attribute value for the key.
|
||||
*/
|
||||
- (id)attributeForKey:(NSString*)key;
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
NSString* const GCDWebServerRequestAttribute_RegexCaptures = @"GCDWebServerRequestAttribute_RegexCaptures";
|
||||
|
||||
#define kZlibErrorDomain @"ZlibErrorDomain"
|
||||
#define kGZipInitialBufferSize (256 * 1024)
|
||||
|
||||
@@ -152,6 +154,7 @@
|
||||
|
||||
BOOL _opened;
|
||||
NSMutableArray* _decoders;
|
||||
NSMutableDictionary* _attributes;
|
||||
id<GCDWebServerBodyWriter> __unsafe_unretained _writer;
|
||||
}
|
||||
@end
|
||||
@@ -187,14 +190,14 @@
|
||||
if (_type == nil) {
|
||||
_type = kGCDWebServerDefaultMimeType;
|
||||
}
|
||||
_length = NSNotFound;
|
||||
_length = NSUIntegerMax;
|
||||
} else {
|
||||
if (_type) {
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
_length = NSNotFound;
|
||||
_length = NSUIntegerMax;
|
||||
}
|
||||
|
||||
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
|
||||
@@ -203,7 +206,7 @@
|
||||
}
|
||||
_noneMatch = ARC_RETAIN([_headers objectForKey:@"If-None-Match"]);
|
||||
|
||||
_range = NSMakeRange(NSNotFound, 0);
|
||||
_range = NSMakeRange(NSUIntegerMax, 0);
|
||||
NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
|
||||
if (rangeHeader) {
|
||||
if ([rangeHeader hasPrefix:@"bytes="]) {
|
||||
@@ -222,13 +225,13 @@
|
||||
_range.location = startValue;
|
||||
_range.length = NSUIntegerMax;
|
||||
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
|
||||
_range.location = NSNotFound;
|
||||
_range.location = NSUIntegerMax;
|
||||
_range.length = endValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((_range.location == NSNotFound) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
|
||||
if ((_range.location == NSUIntegerMax) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
|
||||
LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
|
||||
}
|
||||
}
|
||||
@@ -238,6 +241,7 @@
|
||||
}
|
||||
|
||||
_decoders = [[NSMutableArray alloc] init];
|
||||
_attributes = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -252,6 +256,7 @@
|
||||
ARC_RELEASE(_modifiedSince);
|
||||
ARC_RELEASE(_noneMatch);
|
||||
ARC_RELEASE(_decoders);
|
||||
ARC_RELEASE(_attributes);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
@@ -264,6 +269,10 @@
|
||||
return GCDWebServerIsValidByteRange(_range);
|
||||
}
|
||||
|
||||
- (id)attributeForKey:(NSString*)key {
|
||||
return [_attributes objectForKey:key];
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
return YES;
|
||||
}
|
||||
@@ -307,6 +316,10 @@
|
||||
return [_writer close:error];
|
||||
}
|
||||
|
||||
- (void)setAttribute:(id)attribute forKey:(NSString*)key {
|
||||
[_attributes setValue:attribute forKey:key];
|
||||
}
|
||||
|
||||
- (NSString*)description {
|
||||
NSMutableString* description = [NSMutableString stringWithFormat:@"%@ %@", _method, _path];
|
||||
for (NSString* argument in [[_query allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
|
||||
@@ -27,29 +27,163 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
* This protocol is used by the GCDWebServerConnection to communicate with
|
||||
* the GCDWebServerResponse and read the HTTP body data to send.
|
||||
*
|
||||
* Note that multiple GCDWebServerBodyReader objects can be chained together
|
||||
* internally e.g. to automatically apply gzip encoding to the content before
|
||||
* passing it on to the GCDWebServerResponse.
|
||||
*
|
||||
* @warning These methods can be called on any GCD thread.
|
||||
*/
|
||||
@protocol GCDWebServerBodyReader <NSObject>
|
||||
- (BOOL)open:(NSError**)error; // Return NO on error ("error" is guaranteed to be non-NULL)
|
||||
- (NSData*)readData:(NSError**)error; // Must return nil on error or empty NSData if at end ("error" is guaranteed to be non-NULL)
|
||||
|
||||
/**
|
||||
* This method is called before any body data is sent.
|
||||
*
|
||||
* It should return YES on success or NO on failure and set the "error" argument
|
||||
* which is guaranteed to be non-NULL.
|
||||
*/
|
||||
- (BOOL)open:(NSError**)error;
|
||||
|
||||
/**
|
||||
* This method is called whenever body data is sent.
|
||||
*
|
||||
* It should return a non-empty NSData if there is body data available,
|
||||
* or an empty NSData there is no more body data, or nil on error and set
|
||||
* the "error" argument which is guaranteed to be non-NULL.
|
||||
*/
|
||||
- (NSData*)readData:(NSError**)error;
|
||||
|
||||
/**
|
||||
* This method is called after all body data has been sent.
|
||||
*/
|
||||
- (void)close;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The GCDWebServerResponse class is used to wrap a single HTTP response.
|
||||
* It is instantiated by the handler of the GCDWebServer that handled the request.
|
||||
* If a body is present, the methods from the GCDWebServerBodyReader protocol
|
||||
* will be called by the GCDWebServerConnection to send it.
|
||||
*
|
||||
* The default implementation of the GCDWebServerBodyReader protocol
|
||||
* on the class simply returns an empty body.
|
||||
*
|
||||
* @warning GCDWebServerResponse instances can be created and used on any GCD thread.
|
||||
*/
|
||||
@interface GCDWebServerResponse : NSObject <GCDWebServerBodyReader>
|
||||
@property(nonatomic, copy) NSString* contentType; // Default is nil i.e. no body (must be set if a body is present)
|
||||
@property(nonatomic) NSUInteger contentLength; // Default is NSNotFound i.e. undefined (if a body is present but length is undefined, chunked transfer encoding will be enabled)
|
||||
@property(nonatomic) NSInteger statusCode; // Default is 200
|
||||
@property(nonatomic) NSUInteger cacheControlMaxAge; // Default is 0 seconds i.e. "Cache-Control: no-cache"
|
||||
@property(nonatomic, retain) NSDate* lastModifiedDate; // Default is nil i.e. no "Last-Modified" header
|
||||
@property(nonatomic, copy) NSString* eTag; // Default is nil i.e. no "ETag" header
|
||||
@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled; // Default is disabled
|
||||
|
||||
/**
|
||||
* Sets the content type for the body of the response.
|
||||
*
|
||||
* The default value is nil i.e. the response has no body.
|
||||
*
|
||||
* @warning This property must be set if a body is present.
|
||||
*/
|
||||
@property(nonatomic, copy) NSString* contentType;
|
||||
|
||||
/**
|
||||
* Sets the content length for the body of the response. If a body is present
|
||||
* but this property is set to "NSUIntegerMax", this means the length of the body
|
||||
* cannot be known ahead of time. Chunked transfer encoding will be
|
||||
* automatically enabled by the GCDWebServerConnection to comply with HTTP/1.1
|
||||
* specifications.
|
||||
*
|
||||
* The default value is "NSUIntegerMax" i.e. the response has no body or its length
|
||||
* is undefined.
|
||||
*/
|
||||
@property(nonatomic) NSUInteger contentLength;
|
||||
|
||||
/**
|
||||
* Sets the HTTP status code for the response.
|
||||
*
|
||||
* The default value is 200 i.e. "OK".
|
||||
*/
|
||||
@property(nonatomic) NSInteger statusCode;
|
||||
|
||||
/**
|
||||
* Sets the caching hint for the response using the "Cache-Control" header.
|
||||
* This value is expressed in seconds.
|
||||
*
|
||||
* The default value is 0 i.e. "no-cache".
|
||||
*/
|
||||
@property(nonatomic) NSUInteger cacheControlMaxAge;
|
||||
|
||||
/**
|
||||
* Sets the last modified date for the response using the "Last-Modified" header.
|
||||
*
|
||||
* The default value is nil.
|
||||
*/
|
||||
@property(nonatomic, retain) NSDate* lastModifiedDate;
|
||||
|
||||
/**
|
||||
* Sets the ETag for the response using the "ETag" header.
|
||||
*
|
||||
* The default value is nil.
|
||||
*/
|
||||
@property(nonatomic, copy) NSString* eTag;
|
||||
|
||||
/**
|
||||
* Enables gzip encoding for the response body.
|
||||
*
|
||||
* The default value is NO.
|
||||
*
|
||||
* @warning Enabling gzip encoding will remove any "Content-Length" header
|
||||
* since the length of the body is not known anymore. The client will still
|
||||
* be able to determine the body length when connection is closed per
|
||||
* HTTP/1.1 specifications.
|
||||
*/
|
||||
@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled;
|
||||
|
||||
/**
|
||||
* Creates an empty response.
|
||||
*/
|
||||
+ (instancetype)response;
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)init;
|
||||
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header; // Pass nil value to remove header
|
||||
- (BOOL)hasBody; // Convenience method that checks if "contentType" is not nil
|
||||
|
||||
/**
|
||||
* Sets an additional HTTP header on the response.
|
||||
* Pass a nil value to remove an additional header.
|
||||
*
|
||||
* @warning Do not attempt to override the primary headers used
|
||||
* by GCDWebServerResponse like "Content-Type", "ETag", etc...
|
||||
*/
|
||||
- (void)setValue:(NSString*)value forAdditionalHeader:(NSString*)header;
|
||||
|
||||
/**
|
||||
* Convenience method that checks if the contentType property is defined.
|
||||
*/
|
||||
- (BOOL)hasBody;
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerResponse (Extensions)
|
||||
|
||||
/**
|
||||
* Creates a empty response with a specific HTTP status code.
|
||||
*/
|
||||
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode;
|
||||
|
||||
/**
|
||||
* Creates an HTTP redirect response to a new URL.
|
||||
*/
|
||||
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
||||
|
||||
/**
|
||||
* Initializes an empty response with a specific HTTP status code.
|
||||
*/
|
||||
- (instancetype)initWithStatusCode:(NSInteger)statusCode;
|
||||
|
||||
/**
|
||||
* Initializes an HTTP redirect response to a new URL.
|
||||
*/
|
||||
- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
||||
|
||||
@end
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
||||
if ((self = [super initWithResponse:response reader:reader])) {
|
||||
response.contentLength = NSNotFound; // Make sure "Content-Length" header is not set since we don't know it (client will determine body length when connection is closed)
|
||||
response.contentLength = NSUIntegerMax; // Make sure "Content-Length" header is not set since we don't know it
|
||||
[response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
|
||||
}
|
||||
return self;
|
||||
@@ -180,7 +180,7 @@
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
_type = nil;
|
||||
_length = NSNotFound;
|
||||
_length = NSUIntegerMax;
|
||||
_status = kGCDWebServerHTTPStatusCode_OK;
|
||||
_maxAge = 0;
|
||||
_headers = [[NSMutableDictionary alloc] init];
|
||||
@@ -208,7 +208,7 @@
|
||||
}
|
||||
|
||||
- (BOOL)usesChunkedTransferEncoding {
|
||||
return (_type != nil) && (_length == NSNotFound);
|
||||
return (_type != nil) && (_length == NSUIntegerMax);
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
@@ -259,7 +259,7 @@
|
||||
if (_type) {
|
||||
[description appendFormat:@"\nContent Type = %@", _type];
|
||||
}
|
||||
if (_length != NSNotFound) {
|
||||
if (_length != NSUIntegerMax) {
|
||||
[description appendFormat:@"\nContent Length = %lu", (unsigned long)_length];
|
||||
}
|
||||
[description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge];
|
||||
|
||||
@@ -27,11 +27,34 @@
|
||||
|
||||
#import "GCDWebServerRequest.h"
|
||||
|
||||
/**
|
||||
* The GCDWebServerDataRequest subclass of GCDWebServerRequest stores the body
|
||||
* of the HTTP request in memory.
|
||||
*/
|
||||
@interface GCDWebServerDataRequest : GCDWebServerRequest
|
||||
|
||||
/**
|
||||
* Returns the data for the request body.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSData* data;
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerDataRequest (Extensions)
|
||||
@property(nonatomic, readonly) NSString* text; // Text encoding is extracted from Content-Type or defaults to UTF-8 - Returns nil on error
|
||||
@property(nonatomic, readonly) id jsonObject; // Returns nil on error
|
||||
|
||||
/**
|
||||
* Returns the data for the request body interpreted as text. If the content
|
||||
* type of the body is not a text one, or if an error occurs, nil is returned.
|
||||
*
|
||||
* The text encoding used to interpret the data is extracted from the
|
||||
* "Content-Type" header or defaults to UTF-8.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* text;
|
||||
|
||||
/**
|
||||
* Returns the data for the request body interpreted as a JSON object. If the
|
||||
* content type of the body is not JSON, or if an error occurs, nil is returned.
|
||||
*/
|
||||
@property(nonatomic, readonly) id jsonObject;
|
||||
|
||||
@end
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
if (self.contentLength != NSNotFound) {
|
||||
if (self.contentLength != NSUIntegerMax) {
|
||||
_data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
|
||||
} else {
|
||||
_data = [[NSMutableData alloc] init];
|
||||
@@ -97,7 +97,8 @@
|
||||
|
||||
- (id)jsonObject {
|
||||
if (_jsonObject == nil) {
|
||||
if ([self.contentType isEqualToString:@"application/json"] || [self.contentType isEqualToString:@"text/json"] || [self.contentType isEqualToString:@"text/javascript"]) {
|
||||
NSString* mimeType = GCDWebServerTruncateHeaderValue(self.contentType);
|
||||
if ([mimeType isEqualToString:@"application/json"] || [mimeType isEqualToString:@"text/json"] || [mimeType isEqualToString:@"text/javascript"]) {
|
||||
_jsonObject = ARC_RETAIN([NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL]);
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
|
||||
@@ -27,6 +27,19 @@
|
||||
|
||||
#import "GCDWebServerRequest.h"
|
||||
|
||||
/**
|
||||
* The GCDWebServerFileRequest subclass of GCDWebServerRequest stores the body
|
||||
* of the HTTP request to a file on disk.
|
||||
*/
|
||||
@interface GCDWebServerFileRequest : GCDWebServerRequest
|
||||
|
||||
/**
|
||||
* Returns the path to the temporary file containing the request body.
|
||||
*
|
||||
* @warning This temporary file will be automatically deleted when the
|
||||
* GCDWebServerFileRequest is deallocated. If you want to preserve this file,
|
||||
* you must move it to a different location beforehand.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* temporaryPath;
|
||||
|
||||
@end
|
||||
|
||||
@@ -34,10 +34,6 @@
|
||||
}
|
||||
@end
|
||||
|
||||
static inline NSError* _MakePosixError(int code) {
|
||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
|
||||
}
|
||||
|
||||
@implementation GCDWebServerFileRequest
|
||||
|
||||
@synthesize temporaryPath=_temporaryPath;
|
||||
@@ -59,7 +55,7 @@ static inline NSError* _MakePosixError(int code) {
|
||||
- (BOOL)open:(NSError**)error {
|
||||
_file = open([_temporaryPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
if (_file <= 0) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
@@ -67,7 +63,7 @@ static inline NSError* _MakePosixError(int code) {
|
||||
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||
if (write(_file, data.bytes, data.length) != (ssize_t)data.length) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
@@ -75,7 +71,7 @@ static inline NSError* _MakePosixError(int code) {
|
||||
|
||||
- (BOOL)close:(NSError**)error {
|
||||
if (close(_file) < 0) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return NO;
|
||||
}
|
||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||
|
||||
@@ -27,23 +27,106 @@
|
||||
|
||||
#import "GCDWebServerRequest.h"
|
||||
|
||||
/**
|
||||
* The GCDWebServerMultiPart class is an abstract class that wraps the content
|
||||
* of a part.
|
||||
*/
|
||||
@interface GCDWebServerMultiPart : NSObject
|
||||
@property(nonatomic, readonly) NSString* contentType; // Defaults to "text/plain" per specification if undefined
|
||||
|
||||
/**
|
||||
* Returns the control name retrieved from the part headers.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* controlName;
|
||||
|
||||
/**
|
||||
* Returns the content type retrieved from the part headers or "text/plain"
|
||||
* if not available (per HTTP specifications).
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* contentType;
|
||||
|
||||
/**
|
||||
* Returns the MIME type component of the content type for the part.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* mimeType;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The GCDWebServerMultiPartArgument subclass of GCDWebServerMultiPart wraps
|
||||
* the content of a part as data in memory.
|
||||
*/
|
||||
@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart
|
||||
|
||||
/**
|
||||
* Returns the data for the part.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSData* data;
|
||||
@property(nonatomic, readonly) NSString* string; // May be nil (only valid for text mime types)
|
||||
|
||||
/**
|
||||
* Returns the data for the part interpreted as text. If the content
|
||||
* type of the part is not a text one, or if an error occurs, nil is returned.
|
||||
*
|
||||
* The text encoding used to interpret the data is extracted from the
|
||||
* "Content-Type" header or defaults to UTF-8.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* string;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The GCDWebServerMultiPartFile subclass of GCDWebServerMultiPart wraps
|
||||
* the content of a part as a file on disk.
|
||||
*/
|
||||
@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart
|
||||
@property(nonatomic, readonly) NSString* fileName; // May be nil
|
||||
|
||||
/**
|
||||
* Returns the file name retrieved from the part headers.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* fileName;
|
||||
|
||||
/**
|
||||
* Returns the path to the temporary file containing the part data.
|
||||
*
|
||||
* @warning This temporary file will be automatically deleted when the
|
||||
* GCDWebServerMultiPartFile is deallocated. If you want to preserve this file,
|
||||
* you must move it to a different location beforehand.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* temporaryPath;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The GCDWebServerMultiPartFormRequest subclass of GCDWebServerRequest
|
||||
* parses the body of the HTTP request as a multipart encoded form.
|
||||
*/
|
||||
@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest
|
||||
@property(nonatomic, readonly) NSDictionary* arguments;
|
||||
@property(nonatomic, readonly) NSDictionary* files;
|
||||
|
||||
/**
|
||||
* Returns the argument parts from the multipart encoded form as
|
||||
* name / GCDWebServerMultiPartArgument pairs.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSArray* arguments;
|
||||
|
||||
/**
|
||||
* Returns the files parts from the multipart encoded form as
|
||||
* name / GCDWebServerMultiPartFile pairs.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSArray* files;
|
||||
|
||||
/**
|
||||
* Returns the MIME type for multipart encoded forms
|
||||
* i.e. "multipart/form-data".
|
||||
*/
|
||||
+ (NSString*)mimeType;
|
||||
|
||||
/**
|
||||
* Returns the first argument for a given control name or nil if not found.
|
||||
*/
|
||||
- (GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name;
|
||||
|
||||
/**
|
||||
* Returns the first file for a given control name or nil if not found.
|
||||
*/
|
||||
- (GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name;
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,13 +29,19 @@
|
||||
|
||||
#define kMultiPartBufferSize (256 * 1024)
|
||||
|
||||
enum {
|
||||
typedef enum {
|
||||
kParserState_Undefined = 0,
|
||||
kParserState_Start,
|
||||
kParserState_Headers,
|
||||
kParserState_Content,
|
||||
kParserState_End
|
||||
};
|
||||
} ParserState;
|
||||
|
||||
@interface GCDWebServerMIMEStreamParser : NSObject
|
||||
- (id)initWithBoundary:(NSString*)boundary defaultControlName:(NSString*)name arguments:(NSMutableArray*)arguments files:(NSMutableArray*)files;
|
||||
- (BOOL)appendBytes:(const void*)bytes length:(NSUInteger)length;
|
||||
- (BOOL)isAtEnd;
|
||||
@end
|
||||
|
||||
static NSData* _newlineData = nil;
|
||||
static NSData* _newlinesData = nil;
|
||||
@@ -43,6 +49,7 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@interface GCDWebServerMultiPart () {
|
||||
@private
|
||||
NSString* _controlName;
|
||||
NSString* _contentType;
|
||||
NSString* _mimeType;
|
||||
}
|
||||
@@ -50,17 +57,19 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@implementation GCDWebServerMultiPart
|
||||
|
||||
@synthesize contentType=_contentType, mimeType=_mimeType;
|
||||
@synthesize controlName=_controlName, contentType=_contentType, mimeType=_mimeType;
|
||||
|
||||
- (id)initWithContentType:(NSString*)contentType {
|
||||
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type {
|
||||
if ((self = [super init])) {
|
||||
_contentType = [contentType copy];
|
||||
_controlName = [name copy];
|
||||
_contentType = [type copy];
|
||||
_mimeType = ARC_RETAIN(GCDWebServerTruncateHeaderValue(_contentType));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_controlName);
|
||||
ARC_RELEASE(_contentType);
|
||||
ARC_RELEASE(_mimeType);
|
||||
|
||||
@@ -80,8 +89,8 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@synthesize data=_data, string=_string;
|
||||
|
||||
- (id)initWithContentType:(NSString*)contentType data:(NSData*)data {
|
||||
if ((self = [super initWithContentType:contentType])) {
|
||||
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type data:(NSData*)data {
|
||||
if ((self = [super initWithControlName:name contentType:type])) {
|
||||
_data = ARC_RETAIN(data);
|
||||
|
||||
if ([self.contentType hasPrefix:@"text/"]) {
|
||||
@@ -116,8 +125,8 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@synthesize fileName=_fileName, temporaryPath=_temporaryPath;
|
||||
|
||||
- (id)initWithContentType:(NSString*)contentType fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath {
|
||||
if ((self = [super initWithContentType:contentType])) {
|
||||
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath {
|
||||
if ((self = [super initWithControlName:name contentType:type])) {
|
||||
_fileName = [fileName copy];
|
||||
_temporaryPath = [temporaryPath copy];
|
||||
}
|
||||
@@ -139,26 +148,25 @@ static NSData* _dashNewlineData = nil;
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerMultiPartFormRequest () {
|
||||
@interface GCDWebServerMIMEStreamParser () {
|
||||
@private
|
||||
NSData* _boundary;
|
||||
NSString* _defaultcontrolName;
|
||||
ParserState _state;
|
||||
NSMutableData* _data;
|
||||
NSMutableArray* _arguments;
|
||||
NSMutableArray* _files;
|
||||
|
||||
NSUInteger _parserState;
|
||||
NSMutableData* _parserData;
|
||||
NSString* _controlName;
|
||||
NSString* _fileName;
|
||||
NSString* _contentType;
|
||||
NSString* _tmpPath;
|
||||
int _tmpFile;
|
||||
|
||||
NSMutableDictionary* _arguments;
|
||||
NSMutableDictionary* _files;
|
||||
GCDWebServerMIMEStreamParser* _subParser;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerMultiPartFormRequest
|
||||
|
||||
@synthesize arguments=_arguments, files=_files;
|
||||
@implementation GCDWebServerMIMEStreamParser
|
||||
|
||||
+ (void)initialize {
|
||||
if (_newlineData == nil) {
|
||||
@@ -175,49 +183,50 @@ static NSData* _dashNewlineData = nil;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString*)mimeType {
|
||||
return @"multipart/form-data";
|
||||
}
|
||||
|
||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
||||
if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
|
||||
NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
|
||||
if (boundary) {
|
||||
NSData* data = [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding];
|
||||
_boundary = ARC_RETAIN(data);
|
||||
}
|
||||
if (_boundary == nil) {
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
|
||||
_arguments = [[NSMutableDictionary alloc] init];
|
||||
_files = [[NSMutableDictionary alloc] init];
|
||||
- (id)initWithBoundary:(NSString*)boundary defaultControlName:(NSString*)name arguments:(NSMutableArray*)arguments files:(NSMutableArray*)files {
|
||||
NSData* data = boundary.length ? [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding] : nil;
|
||||
if (data == nil) {
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
if ((self = [super init])) {
|
||||
_boundary = ARC_RETAIN(data);
|
||||
_defaultcontrolName = ARC_RETAIN(name);
|
||||
_arguments = ARC_RETAIN(arguments);
|
||||
_files = ARC_RETAIN(files);
|
||||
_data = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize];
|
||||
_state = kParserState_Start;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_boundary);
|
||||
ARC_RELEASE(_defaultcontrolName);
|
||||
ARC_RELEASE(_data);
|
||||
ARC_RELEASE(_arguments);
|
||||
ARC_RELEASE(_files);
|
||||
ARC_RELEASE(_boundary);
|
||||
|
||||
ARC_RELEASE(_controlName);
|
||||
ARC_RELEASE(_fileName);
|
||||
ARC_RELEASE(_contentType);
|
||||
if (_tmpFile > 0) {
|
||||
close(_tmpFile);
|
||||
unlink([_tmpPath fileSystemRepresentation]);
|
||||
}
|
||||
ARC_RELEASE(_tmpPath);
|
||||
ARC_RELEASE(_subParser);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
_parserData = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize];
|
||||
_parserState = kParserState_Start;
|
||||
return YES;
|
||||
}
|
||||
|
||||
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
|
||||
- (BOOL)_parseData {
|
||||
BOOL success = YES;
|
||||
|
||||
if (_parserState == kParserState_Headers) {
|
||||
NSRange range = [_parserData rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _parserData.length)];
|
||||
if (_state == kParserState_Headers) {
|
||||
NSRange range = [_data rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _data.length)];
|
||||
if (range.location != NSNotFound) {
|
||||
|
||||
ARC_RELEASE(_controlName);
|
||||
@@ -228,29 +237,48 @@ static NSData* _dashNewlineData = nil;
|
||||
_contentType = nil;
|
||||
ARC_RELEASE(_tmpPath);
|
||||
_tmpPath = nil;
|
||||
CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true);
|
||||
const char* temp = "GET / HTTP/1.0\r\n";
|
||||
CFHTTPMessageAppendBytes(message, (const UInt8*)temp, strlen(temp));
|
||||
CFHTTPMessageAppendBytes(message, _parserData.bytes, range.location + range.length);
|
||||
if (CFHTTPMessageIsHeaderComplete(message)) {
|
||||
NSString* controlName = nil;
|
||||
NSString* fileName = nil;
|
||||
NSDictionary* headers = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(message));
|
||||
NSString* contentDisposition = GCDWebServerNormalizeHeaderValue([headers objectForKey:@"Content-Disposition"]);
|
||||
if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) {
|
||||
controlName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"name");
|
||||
fileName = GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename");
|
||||
ARC_RELEASE(_subParser);
|
||||
_subParser = nil;
|
||||
NSString* headers = [[NSString alloc] initWithData:[_data subdataWithRange:NSMakeRange(0, range.location)] encoding:NSUTF8StringEncoding];
|
||||
if (headers) {
|
||||
for (NSString* header in [headers componentsSeparatedByString:@"\r\n"]) {
|
||||
NSRange subRange = [header rangeOfString:@":"];
|
||||
if (subRange.location != NSNotFound) {
|
||||
NSString* name = [header substringToIndex:subRange.location];
|
||||
NSString* value = [[header substringFromIndex:(subRange.location + subRange.length)] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
if ([name caseInsensitiveCompare:@"Content-Type"] == NSOrderedSame) {
|
||||
_contentType = ARC_RETAIN(GCDWebServerNormalizeHeaderValue(value));
|
||||
} else if ([name caseInsensitiveCompare:@"Content-Disposition"] == NSOrderedSame) {
|
||||
NSString* contentDisposition = GCDWebServerNormalizeHeaderValue(value);
|
||||
if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) {
|
||||
_controlName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"name"));
|
||||
_fileName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename"));
|
||||
} else if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"file"]) {
|
||||
_controlName = ARC_RETAIN(_defaultcontrolName);
|
||||
_fileName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
_controlName = [controlName copy];
|
||||
_fileName = [fileName copy];
|
||||
_contentType = ARC_RETAIN(GCDWebServerNormalizeHeaderValue([headers objectForKey:@"Content-Type"]));
|
||||
if (_contentType == nil) {
|
||||
_contentType = @"text/plain";
|
||||
}
|
||||
ARC_RELEASE(headers);
|
||||
} else {
|
||||
LOG_ERROR(@"Failed decoding headers in part of 'multipart/form-data'");
|
||||
DNOT_REACHED();
|
||||
}
|
||||
CFRelease(message);
|
||||
if (_controlName) {
|
||||
if (_fileName) {
|
||||
if ([GCDWebServerTruncateHeaderValue(_contentType) isEqualToString:@"multipart/mixed"]) {
|
||||
NSString* boundary = GCDWebServerExtractHeaderValueParameter(_contentType, @"boundary");
|
||||
_subParser = [[GCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:_controlName arguments:_arguments files:_files];
|
||||
if (_subParser == nil) {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
} else if (_fileName) {
|
||||
NSString* path = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
|
||||
_tmpFile = open([path fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
if (_tmpFile > 0) {
|
||||
@@ -265,29 +293,36 @@ static NSData* _dashNewlineData = nil;
|
||||
success = NO;
|
||||
}
|
||||
|
||||
[_parserData replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0];
|
||||
_parserState = kParserState_Content;
|
||||
[_data replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0];
|
||||
_state = kParserState_Content;
|
||||
}
|
||||
}
|
||||
|
||||
if ((_parserState == kParserState_Start) || (_parserState == kParserState_Content)) {
|
||||
NSRange range = [_parserData rangeOfData:_boundary options:0 range:NSMakeRange(0, _parserData.length)];
|
||||
if ((_state == kParserState_Start) || (_state == kParserState_Content)) {
|
||||
NSRange range = [_data rangeOfData:_boundary options:0 range:NSMakeRange(0, _data.length)];
|
||||
if (range.location != NSNotFound) {
|
||||
NSRange subRange = NSMakeRange(range.location + range.length, _parserData.length - range.location - range.length);
|
||||
NSRange subRange1 = [_parserData rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange];
|
||||
NSRange subRange2 = [_parserData rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange];
|
||||
NSRange subRange = NSMakeRange(range.location + range.length, _data.length - range.location - range.length);
|
||||
NSRange subRange1 = [_data rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange];
|
||||
NSRange subRange2 = [_data rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange];
|
||||
if ((subRange1.location != NSNotFound) || (subRange2.location != NSNotFound)) {
|
||||
|
||||
if (_parserState == kParserState_Content) {
|
||||
const void* dataBytes = _parserData.bytes;
|
||||
if (_state == kParserState_Content) {
|
||||
const void* dataBytes = _data.bytes;
|
||||
NSUInteger dataLength = range.location - 2;
|
||||
if (_tmpPath) {
|
||||
if (_subParser) {
|
||||
if (![_subParser appendBytes:dataBytes length:(dataLength + 2)] || ![_subParser isAtEnd]) {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
ARC_RELEASE(_subParser);
|
||||
_subParser = nil;
|
||||
} else if (_tmpPath) {
|
||||
ssize_t result = write(_tmpFile, dataBytes, dataLength);
|
||||
if (result == (ssize_t)dataLength) {
|
||||
if (close(_tmpFile) == 0) {
|
||||
_tmpFile = 0;
|
||||
GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithContentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
|
||||
[_files setObject:file forKey:_controlName];
|
||||
GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithControlName:_controlName contentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
|
||||
[_files addObject:file];
|
||||
ARC_RELEASE(file);
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
@@ -301,85 +336,150 @@ static NSData* _dashNewlineData = nil;
|
||||
_tmpPath = nil;
|
||||
} else {
|
||||
NSData* data = [[NSData alloc] initWithBytes:(void*)dataBytes length:dataLength];
|
||||
GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithContentType:_contentType data:data];
|
||||
[_arguments setObject:argument forKey:_controlName];
|
||||
GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithControlName:_controlName contentType:_contentType data:data];
|
||||
[_arguments addObject:argument];
|
||||
ARC_RELEASE(argument);
|
||||
ARC_RELEASE(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (subRange1.location != NSNotFound) {
|
||||
[_parserData replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0];
|
||||
_parserState = kParserState_Headers;
|
||||
[_data replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0];
|
||||
_state = kParserState_Headers;
|
||||
success = [self _parseData];
|
||||
} else {
|
||||
_parserState = kParserState_End;
|
||||
_state = kParserState_End;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSUInteger margin = 2 * _boundary.length;
|
||||
if (_tmpPath && (_parserData.length > margin)) {
|
||||
NSUInteger length = _parserData.length - margin;
|
||||
ssize_t result = write(_tmpFile, _parserData.bytes, length);
|
||||
if (result == (ssize_t)length) {
|
||||
[_parserData replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
if (_data.length > margin) {
|
||||
NSUInteger length = _data.length - margin;
|
||||
if (_subParser) {
|
||||
if ([_subParser appendBytes:_data.bytes length:length]) {
|
||||
[_data replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
} else if (_tmpPath) {
|
||||
ssize_t result = write(_tmpFile, _data.bytes, length);
|
||||
if (result == (ssize_t)length) {
|
||||
[_data replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
success = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)appendBytes:(const void*)bytes length:(NSUInteger)length {
|
||||
[_data appendBytes:bytes length:length];
|
||||
return [self _parseData];
|
||||
}
|
||||
|
||||
- (BOOL)isAtEnd {
|
||||
return (_state == kParserState_End);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerMultiPartFormRequest () {
|
||||
@private
|
||||
GCDWebServerMIMEStreamParser* _parser;
|
||||
NSMutableArray* _arguments;
|
||||
NSMutableArray* _files;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerMultiPartFormRequest
|
||||
|
||||
@synthesize arguments=_arguments, files=_files;
|
||||
|
||||
+ (NSString*)mimeType {
|
||||
return @"multipart/form-data";
|
||||
}
|
||||
|
||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
||||
if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
|
||||
_arguments = [[NSMutableArray alloc] init];
|
||||
_files = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
ARC_RELEASE(_arguments);
|
||||
ARC_RELEASE(_files);
|
||||
|
||||
ARC_DEALLOC(super);
|
||||
}
|
||||
|
||||
- (BOOL)open:(NSError**)error {
|
||||
NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
|
||||
_parser = [[GCDWebServerMIMEStreamParser alloc] initWithBoundary:boundary defaultControlName:nil arguments:_arguments files:_files];
|
||||
if (_parser == nil) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed starting to parse multipart form data"}];
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||
[_parserData appendBytes:data.bytes length:data.length];
|
||||
if (![self _parseData]) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}];
|
||||
if (![_parser appendBytes:data.bytes length:data.length]) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed continuing to parse multipart form data"}];
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)close:(NSError**)error {
|
||||
ARC_RELEASE(_parserData);
|
||||
_parserData = nil;
|
||||
ARC_RELEASE(_controlName);
|
||||
_controlName = nil;
|
||||
ARC_RELEASE(_fileName);
|
||||
_fileName = nil;
|
||||
ARC_RELEASE(_contentType);
|
||||
_contentType = nil;
|
||||
if (_tmpFile > 0) {
|
||||
close(_tmpFile);
|
||||
unlink([_tmpPath fileSystemRepresentation]);
|
||||
_tmpFile = 0;
|
||||
}
|
||||
ARC_RELEASE(_tmpPath);
|
||||
_tmpPath = nil;
|
||||
if (_parserState != kParserState_End) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}];
|
||||
BOOL atEnd = [_parser isAtEnd];
|
||||
ARC_RELEASE(_parser);
|
||||
_parser = nil;
|
||||
if (!atEnd) {
|
||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed finishing to parse multipart form data"}];
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (GCDWebServerMultiPartArgument*)firstArgumentForControlName:(NSString*)name {
|
||||
for (GCDWebServerMultiPartArgument* argument in _arguments) {
|
||||
if ([argument.controlName isEqualToString:name]) {
|
||||
return argument;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (GCDWebServerMultiPartFile*)firstFileForControlName:(NSString*)name {
|
||||
for (GCDWebServerMultiPartFile* file in _files) {
|
||||
if ([file.controlName isEqualToString:name]) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString*)description {
|
||||
NSMutableString* description = [NSMutableString stringWithString:[super description]];
|
||||
if (_arguments.count) {
|
||||
[description appendString:@"\n"];
|
||||
for (NSString* key in [[_arguments allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
GCDWebServerMultiPartArgument* argument = [_arguments objectForKey:key];
|
||||
[description appendFormat:@"\n%@ (%@)\n", key, argument.contentType];
|
||||
for (GCDWebServerMultiPartArgument* argument in _arguments) {
|
||||
[description appendFormat:@"\n%@ (%@)\n", argument.controlName, argument.contentType];
|
||||
[description appendString:GCDWebServerDescribeData(argument.data, argument.contentType)];
|
||||
}
|
||||
}
|
||||
if (_files.count) {
|
||||
[description appendString:@"\n"];
|
||||
for (NSString* key in [[_files allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
GCDWebServerMultiPartFile* file = [_files objectForKey:key];
|
||||
[description appendFormat:@"\n%@ (%@): %@\n{%@}", key, file.contentType, file.fileName, file.temporaryPath];
|
||||
for (GCDWebServerMultiPartFile* file in _files) {
|
||||
[description appendFormat:@"\n%@ (%@): %@\n{%@}", file.controlName, file.contentType, file.fileName, file.temporaryPath];
|
||||
}
|
||||
}
|
||||
return description;
|
||||
|
||||
@@ -27,7 +27,25 @@
|
||||
|
||||
#import "GCDWebServerDataRequest.h"
|
||||
|
||||
/**
|
||||
* The GCDWebServerURLEncodedFormRequest subclass of GCDWebServerRequest
|
||||
* parses the body of the HTTP request as a URL encoded form using
|
||||
* GCDWebServerParseURLEncodedForm().
|
||||
*/
|
||||
@interface GCDWebServerURLEncodedFormRequest : GCDWebServerDataRequest
|
||||
@property(nonatomic, readonly) NSDictionary* arguments; // Text encoding is extracted from Content-Type or defaults to UTF-8
|
||||
|
||||
/**
|
||||
* Returns the unescaped control names and values for the URL encoded form.
|
||||
*
|
||||
* The text encoding used to interpret the data is extracted from the
|
||||
* "Content-Type" header or defaults to UTF-8.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSDictionary* arguments;
|
||||
|
||||
/**
|
||||
* Returns the MIME type for URL encoded forms
|
||||
* i.e. "application/x-www-form-urlencoded".
|
||||
*/
|
||||
+ (NSString*)mimeType;
|
||||
|
||||
@end
|
||||
|
||||
@@ -27,20 +27,82 @@
|
||||
|
||||
#import "GCDWebServerResponse.h"
|
||||
|
||||
/**
|
||||
* The GCDWebServerDataResponse subclass of GCDWebServerResponse reads the body
|
||||
* of the HTTP response from memory.
|
||||
*/
|
||||
@interface GCDWebServerDataResponse : GCDWebServerResponse
|
||||
|
||||
/**
|
||||
* Creates a response with data in memory and a given content type.
|
||||
*/
|
||||
+ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type;
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type;
|
||||
|
||||
@end
|
||||
|
||||
@interface GCDWebServerDataResponse (Extensions)
|
||||
|
||||
/**
|
||||
* Creates a data response from text encoded using UTF-8.
|
||||
*/
|
||||
+ (instancetype)responseWithText:(NSString*)text;
|
||||
|
||||
/**
|
||||
* Creates a data response from HTML encoded using UTF-8.
|
||||
*/
|
||||
+ (instancetype)responseWithHTML:(NSString*)html;
|
||||
|
||||
/**
|
||||
* Creates a data response from an HTML template encoded using UTF-8.
|
||||
* See -initWithHTMLTemplate:variables: for details.
|
||||
*/
|
||||
+ (instancetype)responseWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;
|
||||
|
||||
/**
|
||||
* Creates a data response from a serialized JSON object and the default
|
||||
* "application/json" content type.
|
||||
*/
|
||||
+ (instancetype)responseWithJSONObject:(id)object;
|
||||
|
||||
/**
|
||||
* Creates a data response from a serialized JSON object and a custom
|
||||
* content type.
|
||||
*/
|
||||
+ (instancetype)responseWithJSONObject:(id)object contentType:(NSString*)type;
|
||||
- (instancetype)initWithText:(NSString*)text; // Encodes using UTF-8
|
||||
- (instancetype)initWithHTML:(NSString*)html; // Encodes using UTF-8
|
||||
- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables; // Simple template system that replaces all occurences of "%variable%" with corresponding value (encodes using UTF-8)
|
||||
|
||||
/**
|
||||
* Initializes a data response from text encoded using UTF-8.
|
||||
*/
|
||||
- (instancetype)initWithText:(NSString*)text;
|
||||
|
||||
/**
|
||||
* Initializes a data response from HTML encoded using UTF-8.
|
||||
*/
|
||||
- (instancetype)initWithHTML:(NSString*)html;
|
||||
|
||||
/**
|
||||
* Initializes a data response from an HTML template encoded using UTF-8.
|
||||
*
|
||||
* All occurences of "%variable%" within the HTML template are replaced with
|
||||
* their corresponding values.
|
||||
*/
|
||||
- (instancetype)initWithHTMLTemplate:(NSString*)path variables:(NSDictionary*)variables;
|
||||
|
||||
/**
|
||||
* Initializes a data response from a serialized JSON object and the default
|
||||
* "application/json" content type.
|
||||
*/
|
||||
- (instancetype)initWithJSONObject:(id)object;
|
||||
|
||||
/**
|
||||
* Initializes a data response from a serialized JSON object and a custom
|
||||
* content type.
|
||||
*/
|
||||
- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type;
|
||||
|
||||
@end
|
||||
|
||||
@@ -28,14 +28,54 @@
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
#import "GCDWebServerHTTPStatusCodes.h"
|
||||
|
||||
// Returns responses with an HTML body containing the error message
|
||||
/**
|
||||
* The GCDWebServerDataResponse subclass of GCDWebServerDataResponse generates
|
||||
* an HTML body from an HTTP status code and an error message.
|
||||
*/
|
||||
@interface GCDWebServerErrorResponse : GCDWebServerDataResponse
|
||||
|
||||
/**
|
||||
* Creates a client error response with the corresponding HTTP status code.
|
||||
*/
|
||||
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
|
||||
|
||||
/**
|
||||
* Creates a server error response with the corresponding HTTP status code.
|
||||
*/
|
||||
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
|
||||
|
||||
/**
|
||||
* Creates a client error response with the corresponding HTTP status code
|
||||
* and an underlying NSError.
|
||||
*/
|
||||
+ (instancetype)responseWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
|
||||
|
||||
/**
|
||||
* Creates a server error response with the corresponding HTTP status code
|
||||
* and an underlying NSError.
|
||||
*/
|
||||
+ (instancetype)responseWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
|
||||
|
||||
/**
|
||||
* Initializes a client error response with the corresponding HTTP status code.
|
||||
*/
|
||||
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
|
||||
|
||||
/**
|
||||
* Initializes a server error response with the corresponding HTTP status code.
|
||||
*/
|
||||
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode message:(NSString*)format, ... NS_FORMAT_FUNCTION(2,3);
|
||||
|
||||
/**
|
||||
* Initializes a client error response with the corresponding HTTP status code
|
||||
* and an underlying NSError.
|
||||
*/
|
||||
- (instancetype)initWithClientError:(GCDWebServerClientErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
|
||||
|
||||
/**
|
||||
* Initializes a server error response with the corresponding HTTP status code
|
||||
* and an underlying NSError.
|
||||
*/
|
||||
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
|
||||
|
||||
@end
|
||||
|
||||
@@ -27,13 +27,70 @@
|
||||
|
||||
#import "GCDWebServerResponse.h"
|
||||
|
||||
/**
|
||||
* The GCDWebServerFileResponse subclass of GCDWebServerResponse reads the body
|
||||
* of the HTTP response from a file on disk.
|
||||
*
|
||||
* It will automatically set the contentType, lastModifiedDate and eTag
|
||||
* properties of the GCDWebServerResponse according to the file extension and
|
||||
* metadata.
|
||||
*/
|
||||
@interface GCDWebServerFileResponse : GCDWebServerResponse
|
||||
|
||||
/**
|
||||
* Creates a response with the contents of a file.
|
||||
*/
|
||||
+ (instancetype)responseWithFile:(NSString*)path;
|
||||
|
||||
/**
|
||||
* Creates a response like +responseWithFile: and sets the "Content-Disposition"
|
||||
* HTTP header for a download if the "attachment" argument is YES.
|
||||
*/
|
||||
+ (instancetype)responseWithFile:(NSString*)path isAttachment:(BOOL)attachment;
|
||||
|
||||
/**
|
||||
* Creates a response like +responseWithFile: but restricts the file contents
|
||||
* to a specific byte range.
|
||||
*
|
||||
* See -initWithFile:byteRange: for details.
|
||||
*/
|
||||
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range;
|
||||
|
||||
/**
|
||||
* Creates a response like +responseWithFile:byteRange: and sets the
|
||||
* "Content-Disposition" HTTP header for a download if the "attachment"
|
||||
* argument is YES.
|
||||
*/
|
||||
+ (instancetype)responseWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
|
||||
|
||||
/**
|
||||
* Initializes a response with the contents of a file.
|
||||
*/
|
||||
- (instancetype)initWithFile:(NSString*)path;
|
||||
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment; // If in attachment mode, "Content-Disposition" header will be set accordingly
|
||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range; // Pass [NSNotFound, 0] to disable byte range entirely, [offset, length] to enable byte range from beginning of file or [NSNotFound, -length] from end of file
|
||||
|
||||
/**
|
||||
* Initializes a response like +responseWithFile: and sets the
|
||||
* "Content-Disposition" HTTP header for a download if the "attachment"
|
||||
* argument is YES.
|
||||
*/
|
||||
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment;
|
||||
|
||||
/**
|
||||
* Initializes a response like -initWithFile: but restricts the file contents
|
||||
* to a specific byte range. This range should be set to (NSUIntegerMax, 0) for
|
||||
* the full file, (offset, length) if expressed from the beginning of the file,
|
||||
* or (NSUIntegerMax, length) if expressed from the end of the file. The "offset"
|
||||
* and "length" values will be automatically adjusted to be compatible with the
|
||||
* actual size of the file.
|
||||
*
|
||||
* This argument would typically be set to the value of the byteRange property
|
||||
* of the current GCDWebServerRequest.
|
||||
*/
|
||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range;
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
|
||||
|
||||
@end
|
||||
|
||||
@@ -40,10 +40,6 @@
|
||||
}
|
||||
@end
|
||||
|
||||
static inline NSError* _MakePosixError(int code) {
|
||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
|
||||
}
|
||||
|
||||
@implementation GCDWebServerFileResponse
|
||||
|
||||
+ (instancetype)responseWithFile:(NSString*)path {
|
||||
@@ -63,11 +59,11 @@ static inline NSError* _MakePosixError(int code) {
|
||||
}
|
||||
|
||||
- (instancetype)initWithFile:(NSString*)path {
|
||||
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:NO];
|
||||
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:NO];
|
||||
}
|
||||
|
||||
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
||||
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:attachment];
|
||||
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:attachment];
|
||||
}
|
||||
|
||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range {
|
||||
@@ -85,46 +81,58 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
if (GCDWebServerIsValidByteRange(range)) {
|
||||
if (range.location != NSNotFound) {
|
||||
range.location = MIN(range.location, (NSUInteger)info.st_size);
|
||||
range.length = MIN(range.length, (NSUInteger)info.st_size - range.location);
|
||||
#ifndef __LP64__
|
||||
if (info.st_size >= (off_t)4294967295) { // In 32 bit mode, we can't handle files greater than 4 GiBs (don't use "NSUIntegerMax" here to avoid potential unsigned to signed conversion issues)
|
||||
DNOT_REACHED();
|
||||
ARC_RELEASE(self);
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
NSUInteger fileSize = (NSUInteger)info.st_size;
|
||||
|
||||
BOOL hasByteRange = GCDWebServerIsValidByteRange(range);
|
||||
if (hasByteRange) {
|
||||
if (range.location != NSUIntegerMax) {
|
||||
range.location = MIN(range.location, fileSize);
|
||||
range.length = MIN(range.length, fileSize - range.location);
|
||||
} else {
|
||||
range.length = MIN(range.length, (NSUInteger)info.st_size);
|
||||
range.location = (NSUInteger)info.st_size - range.length;
|
||||
range.length = MIN(range.length, fileSize);
|
||||
range.location = fileSize - range.length;
|
||||
}
|
||||
if (range.length == 0) {
|
||||
ARC_RELEASE(self);
|
||||
return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
|
||||
}
|
||||
} else {
|
||||
range.location = 0;
|
||||
range.length = fileSize;
|
||||
}
|
||||
|
||||
if ((self = [super init])) {
|
||||
_path = [path copy];
|
||||
if (range.location != NSNotFound) {
|
||||
_offset = range.location;
|
||||
_size = range.length;
|
||||
_offset = range.location;
|
||||
_size = range.length;
|
||||
if (hasByteRange) {
|
||||
[self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent];
|
||||
[self setValue:[NSString stringWithFormat:@"bytes %i-%i/%i", (int)range.location, (int)(range.location + range.length - 1), (int)info.st_size] forAdditionalHeader:@"Content-Range"];
|
||||
LOG_DEBUG(@"Using content bytes range [%i-%i] for file \"%@\"", (int)range.location, (int)(range.location + range.length - 1), path);
|
||||
} else {
|
||||
_offset = 0;
|
||||
_size = (NSUInteger)info.st_size;
|
||||
[self setValue:[NSString stringWithFormat:@"bytes %lu-%lu/%lu", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), (unsigned long)fileSize] forAdditionalHeader:@"Content-Range"];
|
||||
LOG_DEBUG(@"Using content bytes range [%lu-%lu] for file \"%@\"", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), path);
|
||||
}
|
||||
|
||||
if (attachment) { // TODO: Use http://tools.ietf.org/html/rfc5987 to encode file names with special characters instead of using lossy conversion to ISO 8859-1
|
||||
NSData* data = [[path lastPathComponent] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
|
||||
NSString* fileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
|
||||
if (fileName) {
|
||||
[self setValue:[NSString stringWithFormat:@"attachment; filename=\"%@\"", fileName] forAdditionalHeader:@"Content-Disposition"];
|
||||
ARC_RELEASE(fileName);
|
||||
if (attachment) {
|
||||
NSString* fileName = [path lastPathComponent];
|
||||
NSData* data = [[fileName stringByReplacingOccurrencesOfString:@"\"" withString:@""] dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
|
||||
NSString* lossyFileName = data ? [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding] : nil;
|
||||
if (lossyFileName) {
|
||||
NSString* value = [NSString stringWithFormat:@"attachment; filename=\"%@\"; filename*=UTF-8''%@", lossyFileName, GCDWebServerEscapeURLString(fileName)];
|
||||
[self setValue:value forAdditionalHeader:@"Content-Disposition"];
|
||||
ARC_RELEASE(lossyFileName);
|
||||
} else {
|
||||
DNOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
self.contentType = GCDWebServerGetMimeTypeForExtension([path pathExtension]);
|
||||
self.contentLength = (range.location != NSNotFound ? range.length : (NSUInteger)info.st_size);
|
||||
self.contentType = GCDWebServerGetMimeTypeForExtension([_path pathExtension]);
|
||||
self.contentLength = _size;
|
||||
self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec);
|
||||
self.eTag = [NSString stringWithFormat:@"%llu/%li/%li", info.st_ino, info.st_mtimespec.tv_sec, info.st_mtimespec.tv_nsec];
|
||||
}
|
||||
@@ -140,11 +148,11 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
||||
- (BOOL)open:(NSError**)error {
|
||||
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
|
||||
if (_file <= 0) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return NO;
|
||||
}
|
||||
if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
close(_file);
|
||||
return NO;
|
||||
}
|
||||
@@ -156,7 +164,7 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
||||
NSMutableData* data = [[NSMutableData alloc] initWithLength:length];
|
||||
ssize_t result = read(_file, data.mutableBytes, length);
|
||||
if (result < 0) {
|
||||
*error = _MakePosixError(errno);
|
||||
*error = GCDWebServerMakePosixError(errno);
|
||||
return nil;
|
||||
}
|
||||
if (result > 0) {
|
||||
|
||||
@@ -25,11 +25,29 @@
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import "GCDWebServerStreamingResponse.h"
|
||||
#import "GCDWebServerStreamedResponse.h"
|
||||
|
||||
/**
|
||||
* The GCDWebServerStreamBlock is called to stream the data for the HTTP body.
|
||||
* The block must return empty NSData when done or nil on error and set the
|
||||
* "error" argument which is guaranteed to be non-NULL.
|
||||
*/
|
||||
typedef NSData* (^GCDWebServerStreamBlock)(NSError** error);
|
||||
|
||||
@interface GCDWebServerStreamingResponse : GCDWebServerResponse
|
||||
/**
|
||||
* The GCDWebServerStreamedResponse subclass of GCDWebServerResponse streams
|
||||
* the body of the HTTP response using a GCD block.
|
||||
*/
|
||||
@interface GCDWebServerStreamedResponse : GCDWebServerResponse
|
||||
|
||||
/**
|
||||
* Creates a response with streamed data and a given content type.
|
||||
*/
|
||||
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
|
||||
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block; // Block must return empty NSData when done or nil on error and set the "error" argument accordingly
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block;
|
||||
|
||||
@end
|
||||
@@ -27,13 +27,13 @@
|
||||
|
||||
#import "GCDWebServerPrivate.h"
|
||||
|
||||
@interface GCDWebServerStreamingResponse () {
|
||||
@interface GCDWebServerStreamedResponse () {
|
||||
@private
|
||||
GCDWebServerStreamBlock _block;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation GCDWebServerStreamingResponse
|
||||
@implementation GCDWebServerStreamedResponse
|
||||
|
||||
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
|
||||
return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]);
|
||||
@@ -29,33 +29,169 @@
|
||||
|
||||
@class GCDWebUploader;
|
||||
|
||||
// These methods are always called on main thread
|
||||
/**
|
||||
* Delegate methods for GCDWebUploader.
|
||||
*
|
||||
* @warning These methods are always called on the main thread in a serialized way.
|
||||
*/
|
||||
@protocol GCDWebUploaderDelegate <GCDWebServerDelegate>
|
||||
@optional
|
||||
|
||||
/**
|
||||
* This method is called whenever a file has been downloaded.
|
||||
*/
|
||||
- (void)webUploader:(GCDWebUploader*)uploader didDownloadFileAtPath:(NSString*)path;
|
||||
|
||||
/**
|
||||
* This method is called whenever a file has been uploaded.
|
||||
*/
|
||||
- (void)webUploader:(GCDWebUploader*)uploader didUploadFileAtPath:(NSString*)path;
|
||||
|
||||
/**
|
||||
* This method is called whenever a file or directory has been moved.
|
||||
*/
|
||||
- (void)webUploader:(GCDWebUploader*)uploader didMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||
|
||||
/**
|
||||
* This method is called whenever a file or directory has been deleted.
|
||||
*/
|
||||
- (void)webUploader:(GCDWebUploader*)uploader didDeleteItemAtPath:(NSString*)path;
|
||||
|
||||
/**
|
||||
* This method is called whenever a directory has been created.
|
||||
*/
|
||||
- (void)webUploader:(GCDWebUploader*)uploader didCreateDirectoryAtPath:(NSString*)path;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The GCDWebUploader subclass of GCDWebServer implements an HTML 5 web browser
|
||||
* interface for uploading or downloading files, and moving or deleting files
|
||||
* or directories.
|
||||
*
|
||||
* See the README.md file for more information about the features of GCDWebUploader.
|
||||
*
|
||||
* @warning For GCDWebUploader to work, "GCDWebUploader.bundle" must be added
|
||||
* to the resources of the Xcode target.
|
||||
*/
|
||||
@interface GCDWebUploader : GCDWebServer
|
||||
|
||||
/**
|
||||
* Returns the upload directory as specified when the uploader was initialized.
|
||||
*/
|
||||
@property(nonatomic, readonly) NSString* uploadDirectory;
|
||||
|
||||
/**
|
||||
* Sets the delegate for the uploader.
|
||||
*/
|
||||
@property(nonatomic, assign) id<GCDWebUploaderDelegate> delegate;
|
||||
@property(nonatomic, copy) NSArray* allowedFileExtensions; // Default is nil i.e. all file extensions are allowed
|
||||
@property(nonatomic) BOOL showHiddenFiles; // Default is NO
|
||||
@property(nonatomic, copy) NSString* title; // Default is application name (must be HTML escaped)
|
||||
@property(nonatomic, copy) NSString* header; // Default is same as title (must be HTML escaped)
|
||||
@property(nonatomic, copy) NSString* prologue; // Default is mini help (must be raw HTML)
|
||||
@property(nonatomic, copy) NSString* epilogue; // Default is nothing (must be raw HTML)
|
||||
@property(nonatomic, copy) NSString* footer; // Default is application name and version (must be HTML escaped)
|
||||
|
||||
/**
|
||||
* Sets which files are allowed to be operated on depending on their extension.
|
||||
*
|
||||
* The default value is nil i.e. all file extensions are allowed.
|
||||
*/
|
||||
@property(nonatomic, copy) NSArray* allowedFileExtensions;
|
||||
|
||||
/**
|
||||
* Sets if files and directories whose name start with a period are allowed to
|
||||
* be operated on.
|
||||
*
|
||||
* The default value is NO.
|
||||
*/
|
||||
@property(nonatomic) BOOL allowHiddenItems;
|
||||
|
||||
/**
|
||||
* Sets the title for the uploader web interface.
|
||||
*
|
||||
* The default value is the application name.
|
||||
*
|
||||
* @warning Any reserved HTML characters in the string value for this property
|
||||
* must have been replaced by character entities e.g. "&" becomes "&".
|
||||
*/
|
||||
@property(nonatomic, copy) NSString* title;
|
||||
|
||||
/**
|
||||
* Sets the header for the uploader web interface.
|
||||
*
|
||||
* The default value is the same as the title property.
|
||||
*
|
||||
* @warning Any reserved HTML characters in the string value for this property
|
||||
* must have been replaced by character entities e.g. "&" becomes "&".
|
||||
*/
|
||||
@property(nonatomic, copy) NSString* header;
|
||||
|
||||
/**
|
||||
* Sets the prologue for the uploader web interface.
|
||||
*
|
||||
* The default value is a short help text.
|
||||
*
|
||||
* @warning The string value for this property must be raw HTML
|
||||
* e.g. "<p>Some text</p>"
|
||||
*/
|
||||
@property(nonatomic, copy) NSString* prologue;
|
||||
|
||||
/**
|
||||
* Sets the epilogue for the uploader web interface.
|
||||
*
|
||||
* The default value is nil i.e. no epilogue.
|
||||
*
|
||||
* @warning The string value for this property must be raw HTML
|
||||
* e.g. "<p>Some text</p>"
|
||||
*/
|
||||
@property(nonatomic, copy) NSString* epilogue;
|
||||
|
||||
/**
|
||||
* Sets the footer for the uploader web interface.
|
||||
*
|
||||
* The default value is the application name and version.
|
||||
*
|
||||
* @warning Any reserved HTML characters in the string value for this property
|
||||
* must have been replaced by character entities e.g. "&" becomes "&".
|
||||
*/
|
||||
@property(nonatomic, copy) NSString* footer;
|
||||
|
||||
/**
|
||||
* This method is the designated initializer for the class.
|
||||
*/
|
||||
- (instancetype)initWithUploadDirectory:(NSString*)path;
|
||||
|
||||
@end
|
||||
|
||||
// These methods can be called from any thread
|
||||
/**
|
||||
* Hooks to customize the behavior of GCDWebUploader.
|
||||
*
|
||||
* @warning These methods can be called on any GCD thread.
|
||||
*/
|
||||
@interface GCDWebUploader (Subclassing)
|
||||
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath; // Default implementation returns YES
|
||||
- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath; // Default implementation returns YES
|
||||
- (BOOL)shouldDeleteItemAtPath:(NSString*)path; // Default implementation returns YES
|
||||
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path; // Default implementation returns YES
|
||||
|
||||
/**
|
||||
* This method is called to check if a file upload is allowed to complete.
|
||||
* The uploaded file is available for inspection at "tempPath".
|
||||
*
|
||||
* The default implementation returns YES.
|
||||
*/
|
||||
- (BOOL)shouldUploadFileAtPath:(NSString*)path withTemporaryFile:(NSString*)tempPath;
|
||||
|
||||
/**
|
||||
* This method is called to check if a file or directory is allowed to be moved.
|
||||
*
|
||||
* The default implementation returns YES.
|
||||
*/
|
||||
- (BOOL)shouldMoveItemFromPath:(NSString*)fromPath toPath:(NSString*)toPath;
|
||||
|
||||
/**
|
||||
* This method is called to check if a file or directory is allowed to be deleted.
|
||||
*
|
||||
* The default implementation returns YES.
|
||||
*/
|
||||
- (BOOL)shouldDeleteItemAtPath:(NSString*)path;
|
||||
|
||||
/**
|
||||
* This method is called to check if a directory is allowed to be created.
|
||||
*
|
||||
* The default implementation returns YES.
|
||||
*/
|
||||
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path;
|
||||
|
||||
@end
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
@private
|
||||
NSString* _uploadDirectory;
|
||||
NSArray* _allowedExtensions;
|
||||
BOOL _showHidden;
|
||||
BOOL _allowHidden;
|
||||
NSString* _title;
|
||||
NSString* _header;
|
||||
NSString* _prologue;
|
||||
@@ -57,6 +57,11 @@
|
||||
|
||||
@implementation GCDWebUploader (Methods)
|
||||
|
||||
// Must match implementation in GCDWebDAVServer
|
||||
- (BOOL)_checkSandboxedPath:(NSString*)path {
|
||||
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
|
||||
}
|
||||
|
||||
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
||||
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
||||
return NO;
|
||||
@@ -85,8 +90,8 @@
|
||||
- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request {
|
||||
NSString* relativePath = [[request query] objectForKey:@"path"];
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory;
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
BOOL isDirectory = NO;
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
if (!isDirectory) {
|
||||
@@ -94,7 +99,7 @@
|
||||
}
|
||||
|
||||
NSString* directoryName = [absolutePath lastPathComponent];
|
||||
if (!_showHidden && [directoryName hasPrefix:@"."]) {
|
||||
if (!_allowHidden && [directoryName hasPrefix:@"."]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Listing directory name \"%@\" is not allowed", directoryName];
|
||||
}
|
||||
|
||||
@@ -106,7 +111,7 @@
|
||||
|
||||
NSMutableArray* array = [NSMutableArray array];
|
||||
for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) {
|
||||
if (_showHidden || ![item hasPrefix:@"."]) {
|
||||
if (_allowHidden || ![item hasPrefix:@"."]) {
|
||||
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL];
|
||||
NSString* type = [attributes objectForKey:NSFileType];
|
||||
if ([type isEqualToString:NSFileTypeRegular] && [self _checkFileExtension:item]) {
|
||||
@@ -129,8 +134,8 @@
|
||||
- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request {
|
||||
NSString* relativePath = [[request query] objectForKey:@"path"];
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory;
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
BOOL isDirectory = NO;
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
if (isDirectory) {
|
||||
@@ -138,7 +143,7 @@
|
||||
}
|
||||
|
||||
NSString* fileName = [absolutePath lastPathComponent];
|
||||
if (([fileName hasPrefix:@"."] && !_showHidden) || ![self _checkFileExtension:fileName]) {
|
||||
if (([fileName hasPrefix:@"."] && !_allowHidden) || ![self _checkFileExtension:fileName]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Downlading file name \"%@\" is not allowed", fileName];
|
||||
}
|
||||
|
||||
@@ -154,12 +159,15 @@
|
||||
NSRange range = [[request.headers objectForKey:@"Accept"] rangeOfString:@"application/json" options:NSCaseInsensitiveSearch];
|
||||
NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8"); // Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup)
|
||||
|
||||
GCDWebServerMultiPartFile* file = [request.files objectForKey:@"files[]"];
|
||||
if ((!_showHidden && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) {
|
||||
GCDWebServerMultiPartFile* file = [request firstFileForControlName:@"files[]"];
|
||||
if ((!_allowHidden && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", file.fileName];
|
||||
}
|
||||
NSString* relativePath = [(GCDWebServerMultiPartArgument*)[request.arguments objectForKey:@"path"] string];
|
||||
NSString* relativePath = [[request firstArgumentForControlName:@"path"] string];
|
||||
NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]];
|
||||
if (![self _checkSandboxedPath:absolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath];
|
||||
@@ -181,16 +189,19 @@
|
||||
- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request {
|
||||
NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"];
|
||||
NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath];
|
||||
BOOL isDirectory;
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
|
||||
BOOL isDirectory = NO;
|
||||
if (![self _checkSandboxedPath:oldAbsolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
|
||||
}
|
||||
|
||||
NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
|
||||
NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]];
|
||||
if (![self _checkSandboxedPath:newAbsolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", newRelativePath];
|
||||
}
|
||||
|
||||
NSString* itemName = [newAbsolutePath lastPathComponent];
|
||||
if ((!_showHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
if ((!_allowHidden && [itemName hasPrefix:@"."]) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Moving to item name \"%@\" is not allowed", itemName];
|
||||
}
|
||||
|
||||
@@ -215,12 +226,12 @@
|
||||
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||
BOOL isDirectory = NO;
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
NSString* itemName = [absolutePath lastPathComponent];
|
||||
if (([itemName hasPrefix:@"."] && !_showHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
if (([itemName hasPrefix:@"."] && !_allowHidden) || (!isDirectory && ![self _checkFileExtension:itemName])) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
|
||||
}
|
||||
|
||||
@@ -244,9 +255,12 @@
|
||||
- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request {
|
||||
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
||||
NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]];
|
||||
if (![self _checkSandboxedPath:absolutePath]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||
}
|
||||
|
||||
NSString* directoryName = [absolutePath lastPathComponent];
|
||||
if (!_showHidden && [directoryName hasPrefix:@"."]) {
|
||||
if (!_allowHidden && [directoryName hasPrefix:@"."]) {
|
||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
|
||||
}
|
||||
|
||||
@@ -271,7 +285,7 @@
|
||||
|
||||
@implementation GCDWebUploader
|
||||
|
||||
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden,
|
||||
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, allowHiddenItems=_allowHidden,
|
||||
title=_title, header=_header, prologue=_prologue, epilogue=_epilogue, footer=_footer;
|
||||
|
||||
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
||||
@@ -308,6 +322,9 @@
|
||||
NSString* title = server.title;
|
||||
if (title == nil) {
|
||||
title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
||||
if (title == nil) {
|
||||
title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
|
||||
}
|
||||
#if !TARGET_OS_IPHONE
|
||||
if (title == nil) {
|
||||
title = [[NSProcessInfo processInfo] processName];
|
||||
|
||||
119
Mac/main.m
119
Mac/main.m
@@ -31,9 +31,10 @@
|
||||
|
||||
#import "GCDWebServerDataRequest.h"
|
||||
#import "GCDWebServerURLEncodedFormRequest.h"
|
||||
#import "GCDWebServerMultiPartFormRequest.h"
|
||||
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
#import "GCDWebServerStreamingResponse.h"
|
||||
#import "GCDWebServerStreamedResponse.h"
|
||||
|
||||
#import "GCDWebDAVServer.h"
|
||||
|
||||
@@ -47,9 +48,11 @@ typedef enum {
|
||||
kMode_WebServer = 0,
|
||||
kMode_HTMLPage,
|
||||
kMode_HTMLForm,
|
||||
kMode_HTMLFileUpload,
|
||||
kMode_WebDAV,
|
||||
kMode_WebUploader,
|
||||
kMode_StreamingResponse
|
||||
kMode_StreamingResponse,
|
||||
kMode_AsyncResponse
|
||||
} Mode;
|
||||
|
||||
@interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate>
|
||||
@@ -65,6 +68,10 @@ typedef enum {
|
||||
[self _logDelegateCall:_cmd];
|
||||
}
|
||||
|
||||
- (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server {
|
||||
[self _logDelegateCall:_cmd];
|
||||
}
|
||||
|
||||
- (void)webServerDidConnect:(GCDWebServer*)server {
|
||||
[self _logDelegateCall:_cmd];
|
||||
}
|
||||
@@ -130,9 +137,13 @@ int main(int argc, const char* argv[]) {
|
||||
BOOL recording = NO;
|
||||
NSString* rootDirectory = NSHomeDirectory();
|
||||
NSString* testDirectory = nil;
|
||||
NSString* authenticationMethod = nil;
|
||||
NSString* authenticationRealm = nil;
|
||||
NSString* authenticationUser = nil;
|
||||
NSString* authenticationPassword = nil;
|
||||
|
||||
if (argc == 1) {
|
||||
fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory]\n\n", basename((char*)argv[0]));
|
||||
fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | htmlFileUpload | webDAV | webUploader | streamingResponse | asyncResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password]\n\n", basename((char*)argv[0]));
|
||||
} else {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (argv[i][0] != '-') {
|
||||
@@ -146,12 +157,16 @@ int main(int argc, const char* argv[]) {
|
||||
mode = kMode_HTMLPage;
|
||||
} else if (!strcmp(argv[i], "htmlForm")) {
|
||||
mode = kMode_HTMLForm;
|
||||
} else if (!strcmp(argv[i], "htmlFileUpload")) {
|
||||
mode = kMode_HTMLFileUpload;
|
||||
} else if (!strcmp(argv[i], "webDAV")) {
|
||||
mode = kMode_WebDAV;
|
||||
} else if (!strcmp(argv[i], "webUploader")) {
|
||||
mode = kMode_WebUploader;
|
||||
} else if (!strcmp(argv[i], "streamingResponse")) {
|
||||
mode = kMode_StreamingResponse;
|
||||
} else if (!strcmp(argv[i], "asyncResponse")) {
|
||||
mode = kMode_AsyncResponse;
|
||||
}
|
||||
} else if (!strcmp(argv[i], "-record")) {
|
||||
recording = YES;
|
||||
@@ -161,6 +176,18 @@ int main(int argc, const char* argv[]) {
|
||||
} else if (!strcmp(argv[i], "-tests") && (i + 1 < argc)) {
|
||||
++i;
|
||||
testDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath];
|
||||
} else if (!strcmp(argv[i], "-authenticationMethod") && (i + 1 < argc)) {
|
||||
++i;
|
||||
authenticationMethod = [NSString stringWithUTF8String:argv[i]];
|
||||
} else if (!strcmp(argv[i], "-authenticationRealm") && (i + 1 < argc)) {
|
||||
++i;
|
||||
authenticationRealm = [NSString stringWithUTF8String:argv[i]];
|
||||
} else if (!strcmp(argv[i], "-authenticationUser") && (i + 1 < argc)) {
|
||||
++i;
|
||||
authenticationUser = [NSString stringWithUTF8String:argv[i]];
|
||||
} else if (!strcmp(argv[i], "-authenticationPassword") && (i + 1 < argc)) {
|
||||
++i;
|
||||
authenticationPassword = [NSString stringWithUTF8String:argv[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +250,48 @@ int main(int argc, const char* argv[]) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Implements HTML file upload
|
||||
case kMode_HTMLFileUpload: {
|
||||
fprintf(stdout, "Running in HTML File Upload mode");
|
||||
webServer = [[GCDWebServer alloc] init];
|
||||
NSString* formHTML = @" \
|
||||
<form name=\"input\" action=\"/\" method=\"post\" enctype=\"multipart/form-data\"> \
|
||||
<input type=\"hidden\" name=\"secret\" value=\"42\"> \
|
||||
<input type=\"file\" name=\"files\" multiple><br/> \
|
||||
<input type=\"submit\" value=\"Submit\"> \
|
||||
</form> \
|
||||
";
|
||||
[webServer addHandlerForMethod:@"GET"
|
||||
path:@"/"
|
||||
requestClass:[GCDWebServerRequest class]
|
||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
NSString* html = [NSString stringWithFormat:@"<html><body>%@</body></html>", formHTML];
|
||||
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||
|
||||
}];
|
||||
[webServer addHandlerForMethod:@"POST"
|
||||
path:@"/"
|
||||
requestClass:[GCDWebServerMultiPartFormRequest class]
|
||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
NSMutableString* string = [NSMutableString string];
|
||||
for (GCDWebServerMultiPartArgument* argument in [(GCDWebServerMultiPartFormRequest*)request arguments]) {
|
||||
[string appendFormat:@"%@ = %@<br>", argument.controlName, argument.string];
|
||||
}
|
||||
for (GCDWebServerMultiPartFile* file in [(GCDWebServerMultiPartFormRequest*)request files]) {
|
||||
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:file.temporaryPath error:NULL];
|
||||
[string appendFormat:@"%@ = "%@" (%@ | %llu %@)<br>", file.controlName, file.fileName, file.mimeType,
|
||||
attributes.fileSize >= 1000 ? attributes.fileSize / 1000 : attributes.fileSize,
|
||||
attributes.fileSize >= 1000 ? @"KB" : @"Bytes"];
|
||||
};
|
||||
NSString* html = [NSString stringWithFormat:@"<html><body><p>%@</p><hr>%@</body></html>", string, formHTML];
|
||||
return [GCDWebServerDataResponse responseWithHTML:html];
|
||||
|
||||
}];
|
||||
break;
|
||||
}
|
||||
|
||||
// Serve home directory through WebDAV
|
||||
case kMode_WebDAV: {
|
||||
fprintf(stdout, "Running in WebDAV mode from \"%s\"", [rootDirectory UTF8String]);
|
||||
@@ -247,7 +316,7 @@ int main(int argc, const char* argv[]) {
|
||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
__block int countDown = 10;
|
||||
return [GCDWebServerStreamingResponse responseWithContentType:@"text/plain" streamBlock:^NSData *(NSError** error) {
|
||||
return [GCDWebServerStreamedResponse responseWithContentType:@"text/plain" streamBlock:^NSData *(NSError** error) {
|
||||
|
||||
usleep(100 * 1000);
|
||||
if (countDown) {
|
||||
@@ -262,6 +331,24 @@ int main(int argc, const char* argv[]) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Test async responses
|
||||
case kMode_AsyncResponse: {
|
||||
fprintf(stdout, "Running in Async Response mode");
|
||||
webServer = [[GCDWebServer alloc] init];
|
||||
[webServer addHandlerForMethod:@"GET"
|
||||
path:@"/"
|
||||
requestClass:[GCDWebServerRequest class]
|
||||
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithData:[@"Hello World!" dataUsingEncoding:NSUTF8StringEncoding] contentType:@"text/plain"];
|
||||
completionBlock(response);
|
||||
});
|
||||
|
||||
}];
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
#if __has_feature(objc_arc)
|
||||
fprintf(stdout, " (ARC is ON)\n");
|
||||
@@ -271,23 +358,39 @@ int main(int argc, const char* argv[]) {
|
||||
|
||||
if (webServer) {
|
||||
Delegate* delegate = [[Delegate alloc] init];
|
||||
webServer.delegate = delegate;
|
||||
if (testDirectory) {
|
||||
#if DEBUG
|
||||
webServer.delegate = delegate;
|
||||
#endif
|
||||
fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]);
|
||||
result = (int)[webServer runTestsInDirectory:testDirectory withPort:8080];
|
||||
result = (int)[webServer runTestsWithOptions:@{GCDWebServerOption_Port: @8080} inDirectory:testDirectory];
|
||||
} else {
|
||||
webServer.delegate = delegate;
|
||||
if (recording) {
|
||||
fprintf(stdout, "<RECORDING ENABLED>\n");
|
||||
webServer.recordingEnabled = YES;
|
||||
}
|
||||
fprintf(stdout, "\n");
|
||||
if ([webServer runWithPort:8080]) {
|
||||
NSMutableDictionary* options = [NSMutableDictionary dictionary];
|
||||
[options setObject:@8080 forKey:GCDWebServerOption_Port];
|
||||
[options setObject:@"" forKey:GCDWebServerOption_BonjourName];
|
||||
if (authenticationUser && authenticationPassword) {
|
||||
[options setValue:authenticationRealm forKey:GCDWebServerOption_AuthenticationRealm];
|
||||
[options setObject:@{authenticationUser: authenticationPassword} forKey:GCDWebServerOption_AuthenticationAccounts];
|
||||
if ([authenticationMethod isEqualToString:@"Basic"]) {
|
||||
[options setObject:GCDWebServerAuthenticationMethod_Basic forKey:GCDWebServerOption_AuthenticationMethod];
|
||||
} else if ([authenticationMethod isEqualToString:@"Digest"]) {
|
||||
[options setObject:GCDWebServerAuthenticationMethod_DigestAccess forKey:GCDWebServerOption_AuthenticationMethod];
|
||||
}
|
||||
}
|
||||
if ([webServer runWithOptions:options error:NULL]) {
|
||||
result = 0;
|
||||
}
|
||||
}
|
||||
webServer.delegate = nil;
|
||||
#if !__has_feature(objc_arc)
|
||||
[webServer release];
|
||||
[delegate release];
|
||||
[webServer release];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
155
README.md
155
README.md
@@ -2,30 +2,31 @@ Overview
|
||||
========
|
||||
|
||||
[](https://travis-ci.org/swisspol/GCDWebServer)
|
||||
[](http://cocoadocs.org/docsets/GCDWebServer)
|
||||
[](http://cocoadocs.org/docsets/GCDWebServer)
|
||||
|
||||
GCDWebServer is a modern and lightweight GCD based HTTP 1.1 server designed to be embedded in OS X & iOS apps. It was written from scratch with the following goals in mind:
|
||||
* Easy to use and understand architecture with only 4 core classes: server, connection, request and response (see "Understanding GCDWebServer's Architecture" below)
|
||||
* Well designed API for easy integration and customization
|
||||
* Entirely built with an event-driven design using [Grand Central Dispatch](http://en.wikipedia.org/wiki/Grand_Central_Dispatch) for maximal performance and concurrency
|
||||
* Elegant and easy to use architecture with only 4 core classes: server, connection, request and response (see "Understanding GCDWebServer's Architecture" below)
|
||||
* Well designed API with fully documented headers for easy integration and customization
|
||||
* Entirely built with an event-driven design using [Grand Central Dispatch](http://en.wikipedia.org/wiki/Grand_Central_Dispatch) for best performance and concurrency
|
||||
* No dependencies on third-party source code
|
||||
* Available under a friendly [New BSD License](LICENSE)
|
||||
|
||||
Extra built-in features:
|
||||
* Allow implementation of fully asynchronous handlers of incoming HTTP requests
|
||||
* Minimize memory usage with disk streaming of large HTTP request or response bodies
|
||||
* Parser for [web forms](http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4) submitted using "application/x-www-form-urlencoded" or "multipart/form-data" encodings (including file uploads)
|
||||
* [JSON](http://www.json.org/) parsing and serialization for request and response HTTP bodies
|
||||
* [Chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) for request and response HTTP bodies
|
||||
* [HTTP compression](https://en.wikipedia.org/wiki/HTTP_compression) with gzip for request and response HTTP bodies
|
||||
* [HTTP range](https://en.wikipedia.org/wiki/Byte_serving) support for requests of local files
|
||||
* [Basic](https://en.wikipedia.org/wiki/Basic_access_authentication) and [Digest Access](https://en.wikipedia.org/wiki/Digest_access_authentication) authentications for password protection
|
||||
* Automatically handle transitions between foreground, background and suspended modes in iOS apps
|
||||
|
||||
Included extensions:
|
||||
* [GCDWebUploader](GCDWebUploader/GCDWebUploader.h): subclass of ```GCDWebServer``` that implements an interface for uploading and downloading files from an iOS app's sandbox using a web browser
|
||||
* [GCDWebUploader](GCDWebUploader/GCDWebUploader.h): subclass of ```GCDWebServer``` that implements an interface for uploading and downloading files using a web browser
|
||||
* [GCDWebDAVServer](GCDWebDAVServer/GCDWebDAVServer.h): subclass of ```GCDWebServer``` that implements a class 1 [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server (with partial class 2 support for OS X Finder)
|
||||
|
||||
What's not available out of the box but can be implemented on top of the API:
|
||||
* Authentication like [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
|
||||
* Web forms submitted using "multipart/mixed"
|
||||
|
||||
What's not supported (but not really required from an embedded HTTP server):
|
||||
* Keep-alive connections
|
||||
* HTTPS
|
||||
@@ -37,29 +38,30 @@ Requirements:
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
Download or checkout the source for GCDWebServer then add the entire "GCDWebServer" subfolder to your Xcode project. If you intend to use one of the extensions like GCDWebDAVServer or GCDWebUploader, add these subfolders as well.
|
||||
Download or check out the [latest release](https://github.com/swisspol/GCDWebServer/releases) of GCDWebServer then add the entire "GCDWebServer" subfolder to your Xcode project. If you intend to use one of the extensions like GCDWebDAVServer or GCDWebUploader, add these subfolders as well.
|
||||
|
||||
Alternatively, you can install GCDWebServer using [CocoaPods](http://cocoapods.org/) by simply adding this line to your Xcode project's Podfile:
|
||||
```
|
||||
pod "GCDWebServer", "~> 2.0"
|
||||
pod "GCDWebServer", "~> 3.0"
|
||||
```
|
||||
If you want to use GCDWebUploader, use this line instead:
|
||||
```
|
||||
pod "GCDWebServer/WebUploader", "~> 2.0"
|
||||
pod "GCDWebServer/WebUploader", "~> 3.0"
|
||||
```
|
||||
Or this line for GCDWebDAVServer:
|
||||
```
|
||||
pod "GCDWebServer/WebDAV", "~> 2.0"
|
||||
pod "GCDWebServer/WebDAV", "~> 3.0"
|
||||
```
|
||||
|
||||
Hello World
|
||||
===========
|
||||
|
||||
These codes snippets show how to implement a custom HTTP server that runs on port 8080 and returns a "Hello World" HTML page to any request. Since GCDWebServer uses GCD blocks to handle requests, no subclassing or delegates are needed, which results in very clean code.
|
||||
These code snippets show how to implement a custom HTTP server that runs on port 8080 and returns a "Hello World" HTML page to any request. Since GCDWebServer uses GCD blocks to handle requests, no subclassing or delegates are needed, which results in very clean code.
|
||||
|
||||
**OS X version (command line tool):**
|
||||
```objectivec
|
||||
#import "GCDWebServer.h"
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
|
||||
int main(int argc, const char* argv[]) {
|
||||
@autoreleasepool {
|
||||
@@ -76,22 +78,27 @@ 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)
|
||||
[webServer runWithPort:8080];
|
||||
|
||||
// Destroy server (unnecessary if using ARC)
|
||||
[webServer release];
|
||||
// Use convenience method that runs server on port 8080
|
||||
// until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
|
||||
[webServer runWithPort:8080 bonjourName:nil];
|
||||
NSLog(@"Visit %@ in your web browser", webServer.serverURL);
|
||||
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**iOS Version:**
|
||||
**iOS version:**
|
||||
```objectivec
|
||||
#import "GCDWebServer.h"
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
|
||||
static GCDWebServer* _webServer = nil; // This should really be an ivar of your application delegate class
|
||||
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||
GCDWebServer* _webServer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||
|
||||
@@ -109,9 +116,67 @@ static GCDWebServer* _webServer = nil; // This should really be an ivar of your
|
||||
|
||||
// Start server on port 8080
|
||||
[_webServer startWithPort:8080 bonjourName:nil];
|
||||
NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
**OS X Swift version (command line tool):**
|
||||
|
||||
***webServer.swift***
|
||||
```swift
|
||||
import Foundation
|
||||
|
||||
let webServer = GCDWebServer()
|
||||
|
||||
webServer.addDefaultHandlerForMethod("GET", requestClass: GCDWebServerRequest.self) { request in
|
||||
return GCDWebServerDataResponse(HTML:"<html><body><p>Hello World</p></body></html>")
|
||||
}
|
||||
|
||||
webServer.runWithPort(8080, bonjourName: nil)
|
||||
|
||||
println("Visit \(webServer.serverURL) in your web browser")
|
||||
```
|
||||
|
||||
***WebServer-Bridging-Header.h***
|
||||
```objectivec
|
||||
#import "GCDWebServer.h"
|
||||
#import "GCDWebServerDataResponse.h"
|
||||
```
|
||||
|
||||
Asynchronous HTTP Responses
|
||||
===========================
|
||||
|
||||
New in GCDWebServer 3.0 is the ability to process HTTP requests aysnchronously i.e. add handlers to the server that generate their ```GCDWebServerResponse``` asynchronously. This is achieved by adding handlers that use a ```GCDWebServerAsyncProcessBlock``` instead of a ```GCDWebServerProcessBlock```. Here's an example:
|
||||
|
||||
***Synchronous version***
|
||||
```objectivec
|
||||
[webServer addDefaultHandlerForMethod:@"GET"
|
||||
requestClass:[GCDWebServerRequest class]
|
||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||
|
||||
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
|
||||
return response;
|
||||
|
||||
}];
|
||||
```
|
||||
|
||||
***Asynchronous version***
|
||||
```objectivec
|
||||
[webServer addDefaultHandlerForMethod:@"GET"
|
||||
requestClass:[GCDWebServerRequest class]
|
||||
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
|
||||
|
||||
// Do some async operation like network access or file I/O (simulated here using dispatch_after())
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello World</p></body></html>"];
|
||||
completionBlock(response);
|
||||
});
|
||||
|
||||
}];
|
||||
```
|
||||
|
||||
Web Based Uploads in iOS Apps
|
||||
@@ -119,12 +184,17 @@ Web Based Uploads in iOS Apps
|
||||
|
||||
GCDWebUploader is a subclass of ```GCDWebServer``` that provides a ready-to-use HTML 5 file uploader & downloader. This lets users upload, download, delete files and create directories from a directory inside your iOS app's sandbox using a clean user interface in their web browser.
|
||||
|
||||
Simply instantiate and run a GCDWebUploader instance then visit http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ from your web browser:
|
||||
Simply instantiate and run a ```GCDWebUploader``` instance then visit ```http://{YOUR-IOS-DEVICE-IP-ADDRESS}/``` from your web browser:
|
||||
|
||||
```objectivec
|
||||
#import "GCDWebUploader.h"
|
||||
|
||||
static GCDWebUploader* _webUploader = nil; // This should really be an ivar of your application delegate class
|
||||
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||
GCDWebUploader* _webUploader;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||
@@ -133,6 +203,8 @@ static GCDWebUploader* _webUploader = nil; // This should really be an ivar of
|
||||
NSLog(@"Visit %@ in your web browser", _webUploader.serverURL);
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
WebDAV Server in iOS Apps
|
||||
@@ -142,12 +214,17 @@ GCDWebDAVServer is a subclass of ```GCDWebServer``` that provides a class 1 comp
|
||||
|
||||
GCDWebDAVServer should also work with the [OS X Finder](http://support.apple.com/kb/PH13859) as it is partially class 2 compliant (but only when the client is the OS X WebDAV implementation).
|
||||
|
||||
Simply instantiate and run a GCDWebDAVServer instance then connect to http://{YOUR-IOS-DEVICE-IP-ADDRESS}/ using a WebDAV client:
|
||||
Simply instantiate and run a ```GCDWebDAVServer``` instance then connect to ```http://{YOUR-IOS-DEVICE-IP-ADDRESS}/``` using a WebDAV client:
|
||||
|
||||
```objectivec
|
||||
#import "GCDWebDAVServer.h"
|
||||
|
||||
static GCDWebDAVServer* _davServer = nil; // This should really be an ivar of your application delegate class
|
||||
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||
GCDWebDAVServer* _davServer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||
@@ -156,6 +233,8 @@ static GCDWebDAVServer* _davServer = nil; // This should really be an ivar of y
|
||||
NSLog(@"Visit %@ in your WebDAV client", _davServer.serverURL);
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
Serving a Static Website
|
||||
@@ -172,7 +251,6 @@ int main(int argc, const char* argv[]) {
|
||||
GCDWebServer* webServer = [[GCDWebServer alloc] init];
|
||||
[webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
|
||||
[webServer runWithPort:8080];
|
||||
[webServer release]; // Remove if using ARC
|
||||
|
||||
}
|
||||
return 0;
|
||||
@@ -204,14 +282,14 @@ GCDWebServer relies on "handlers" to process incoming web requests and generatin
|
||||
|
||||
Handlers require 2 GCD blocks:
|
||||
* The ```GCDWebServerMatchBlock``` is called on every handler added to the ```GCDWebServer``` instance whenever a web request has started (i.e. HTTP headers have been received). It is passed the basic info for the web request (HTTP method, URL, headers...) and must decide if it wants to handle it or not. If yes, it must return a new ```GCDWebServerRequest``` instance (see above) created with this info. Otherwise, it simply returns nil.
|
||||
* The ```GCDWebServerProcessBlock``` is called after the web request has been fully received and is passed the ```GCDWebServerRequest``` instance created at the previous step. It must return a ```GCDWebServerResponse``` instance (see above) or nil on error, which will result in a 500 HTTP status code returned to the client. It's however recommended to return an instance of [GCDWebServerErrorResponse](GCDWebServer/Responses/GCDWebServerErrorResponse.h) on error so more useful information can be returned to the client.
|
||||
* The ```GCDWebServerProcessBlock``` or ```GCDWebServerAsyncProcessBlock``` is called after the web request has been fully received and is passed the ```GCDWebServerRequest``` instance created at the previous step. It must return synchronously (if using ```GCDWebServerProcessBlock```) or asynchronously (if using ```GCDWebServerAsyncProcessBlock```) a ```GCDWebServerResponse``` instance (see above) or nil on error, which will result in a 500 HTTP status code returned to the client. It's however recommended to return an instance of [GCDWebServerErrorResponse](GCDWebServer/Responses/GCDWebServerErrorResponse.h) on error so more useful information can be returned to the client.
|
||||
|
||||
Note that most methods on ```GCDWebServer``` to add handlers only require the ```GCDWebServerProcessBlock``` as they already provide a built-in ```GCDWebServerMatchBlock``` e.g. to match a URL path with a Regex.
|
||||
Note that most methods on ```GCDWebServer``` to add handlers only require the ```GCDWebServerProcessBlock``` or ```GCDWebServerAsyncProcessBlock``` as they already provide a built-in ```GCDWebServerMatchBlock``` e.g. to match a URL path with a Regex.
|
||||
|
||||
GCDWebServer & Background Mode for iOS Apps
|
||||
===========================================
|
||||
|
||||
When doing networking operations in iOS apps, you must handle carefully [what happens when iOS puts the app in the background](https://developer.apple.com/library/ios/technotes/tn2277/_index.html). Typically you must stop any server while the app is in the background and restart them when it comes back to the foreground. This can become quite complex considering the server might have ongoing connections when it needs to be stopped.
|
||||
When doing networking operations in iOS apps, you must handle carefully [what happens when iOS puts the app in the background](https://developer.apple.com/library/ios/technotes/tn2277/_index.html). Typically you must stop any network servers while the app is in the background and restart them when the app comes back to the foreground. This can become quite complex considering servers might have ongoing connections when they need to be stopped.
|
||||
|
||||
Fortunately, GCDWebServer does all of this automatically for you:
|
||||
- GCDWebServer begins a [background task](https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html) whenever the first HTTP connection is opened and ends it only when the last one is closed. This prevents iOS from suspending the app after it goes in the background, which would immediately kill HTTP connections to the client.
|
||||
@@ -222,6 +300,25 @@ Fortunately, GCDWebServer does all of this automatically for you:
|
||||
|
||||
HTTP connections are often initiated in batches (or bursts), for instance when loading a web page with multiple resources. This makes it difficult to accurately detect when the *very last* HTTP connection has been closed: it's possible 2 consecutive HTTP connections part of the same batch would be separated by a small delay instead of overlapping. It would be bad for the client if GCDWebServer suspended itself right in between. The ```GCDWebServerOption_ConnectedStateCoalescingInterval``` option solves this problem elegantly by forcing GCDWebServer to wait some extra delay before performing any action after the last HTTP connection has been closed, just in case a new HTTP connection is initiated within this delay.
|
||||
|
||||
Debug Builds & Custom Logging
|
||||
=============================
|
||||
|
||||
When building GCDWebServer in "Debug" mode versus "Release" mode, GCDWebServer logs a lot more information and also performs a number of internal consistency checks. To enable this behavior, define the preprocessor constant ```DEBUG=1``` when compiling GCDWebServer. In Xcode target settings, this can be done by adding ```DEBUG=1``` to the build setting ```GCC_PREPROCESSOR_DEFINITIONS``` when building in Debug configuration.
|
||||
|
||||
It's also possible to replace the logging system used by GCDWebServer by a custom one. Simply define the preprocessor constant ```__GCDWEBSERVER_LOGGING_HEADER__``` to the name of a header file (e.g. "MyLogging.h") that defines these macros:
|
||||
|
||||
```
|
||||
#define LOG_DEBUG(...) // Should not do anything unless the preprocessor constant DEBUG is non-zero
|
||||
#define LOG_VERBOSE(...)
|
||||
#define LOG_INFO(...)
|
||||
#define LOG_WARNING(...)
|
||||
#define LOG_ERROR(...)
|
||||
#define LOG_EXCEPTION(__EXCEPTION__)
|
||||
|
||||
#define DCHECK(__CONDITION__) // Should not do anything unless the preprocessor constant DEBUG is non-zero
|
||||
#define DNOT_REACHED() // Should not do anything unless the preprocessor constant DEBUG is non-zero
|
||||
```
|
||||
|
||||
Advanced Example 1: Implementing HTTP Redirects
|
||||
===============================================
|
||||
|
||||
@@ -320,4 +417,4 @@ Final Example: File Downloads and Uploads From iOS App
|
||||
|
||||
GCDWebServer was originally written for the [ComicFlow](http://itunes.apple.com/us/app/comicflow/id409290355?mt=8) comic reader app for iPad. It allow users to connect to their iPad with their web browser over WiFi and then upload, download and organize comic files inside the app.
|
||||
|
||||
ComicFlow is [entirely open-source](https://github.com/swisspol/ComicFlow) and you can see how it uses GCDWebUploader in the [WebServer.m](https://github.com/swisspol/ComicFlow/blob/master/Classes/WebServer.m) file.
|
||||
ComicFlow is [entirely open-source](https://github.com/swisspol/ComicFlow) and you can see how it uses GCDWebServer in the [WebServer.h](https://github.com/swisspol/ComicFlow/blob/master/Classes/WebServer.h) and [WebServer.m](https://github.com/swisspol/ComicFlow/blob/master/Classes/WebServer.m) files.
|
||||
|
||||
36
Run-Tests.sh
36
Run-Tests.sh
@@ -23,26 +23,52 @@ function runTests {
|
||||
rm -rf "$PAYLOAD_DIR"
|
||||
ditto -x -k "$PAYLOAD_ZIP" "$PAYLOAD_DIR"
|
||||
TZ=GMT find "$PAYLOAD_DIR" -type d -exec SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" '{}' \; # ZIP archives do not preserve directories dates
|
||||
if [ "$4" != "" ]; then
|
||||
cp -f "$4" "$PAYLOAD_DIR/Payload"
|
||||
pushd "$PAYLOAD_DIR/Payload"
|
||||
SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" `basename "$4"`
|
||||
popd
|
||||
fi
|
||||
logLevel=2 $1 -mode "$2" -root "$PAYLOAD_DIR/Payload" -tests "$3"
|
||||
}
|
||||
|
||||
# Build for iOS in manual memory management mode (TODO: run tests on iOS)
|
||||
# Build for iOS in manual memory management mode and for oldest deployment target (TODO: run tests on iOS)
|
||||
rm -rf "$MRC_BUILD_DIR"
|
||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" "IPHONEOS_DEPLOYMENT_TARGET=5.1.1" > /dev/null
|
||||
|
||||
# Build for iOS in manual memory management mode and for default deployment target (TODO: run tests on iOS)
|
||||
rm -rf "$MRC_BUILD_DIR"
|
||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" > /dev/null
|
||||
|
||||
# Build for iOS in ARC mode (TODO: run tests on iOS)
|
||||
# Build for iOS in ARC mode and for oldest deployment target (TODO: run tests on iOS)
|
||||
rm -rf "$ARC_BUILD_DIR"
|
||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" "IPHONEOS_DEPLOYMENT_TARGET=5.1.1" > /dev/null
|
||||
|
||||
# Build for iOS in ARC mode and for default deployment target (TODO: run tests on iOS)
|
||||
rm -rf "$ARC_BUILD_DIR"
|
||||
xcodebuild -sdk "$IOS_SDK" -target "$IOS_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null
|
||||
|
||||
# Build for OS X in manual memory management mode
|
||||
# Build for OS X in manual memory management mode and for oldest deployment target
|
||||
rm -rf "$MRC_BUILD_DIR"
|
||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" "MACOSX_DEPLOYMENT_TARGET=10.7" > /dev/null
|
||||
|
||||
# Build for OS X in manual memory management mode and for default deployment target
|
||||
rm -rf "$MRC_BUILD_DIR"
|
||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" > /dev/null
|
||||
|
||||
# Build for OS X in ARC mode
|
||||
# Build for OS X in ARC mode and for oldest deployment target
|
||||
rm -rf "$ARC_BUILD_DIR"
|
||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" "MACOSX_DEPLOYMENT_TARGET=10.7" > /dev/null
|
||||
|
||||
# Build for OS X in ARC mode and for default deployment target
|
||||
rm -rf "$ARC_BUILD_DIR"
|
||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null
|
||||
|
||||
# Run tests
|
||||
runTests $MRC_PRODUCT "htmlForm" "Tests/HTMLForm"
|
||||
runTests $ARC_PRODUCT "htmlForm" "Tests/HTMLForm"
|
||||
runTests $MRC_PRODUCT "htmlFileUpload" "Tests/HTMLFileUpload"
|
||||
runTests $ARC_PRODUCT "htmlFileUpload" "Tests/HTMLFileUpload"
|
||||
runTests $MRC_PRODUCT "webServer" "Tests/WebServer"
|
||||
runTests $ARC_PRODUCT "webServer" "Tests/WebServer"
|
||||
runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Transmit"
|
||||
@@ -53,6 +79,8 @@ runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
||||
runTests $ARC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
||||
runTests $MRC_PRODUCT "webUploader" "Tests/WebUploader"
|
||||
runTests $ARC_PRODUCT "webUploader" "Tests/WebUploader"
|
||||
runTests $MRC_PRODUCT "webServer" "Tests/WebServer-Sample-Movie" "Tests/Sample-Movie.mp4"
|
||||
runTests $ARC_PRODUCT "webServer" "Tests/WebServer-Sample-Movie" "Tests/Sample-Movie.mp4"
|
||||
|
||||
# Done
|
||||
echo "\nAll tests completed successfully!"
|
||||
|
||||
9
Tests/HTMLFileUpload/001-200.response
Normal file
9
Tests/HTMLFileUpload/001-200.response
Normal file
@@ -0,0 +1,9 @@
|
||||
HTTP/1.1 200 OK
|
||||
Cache-Control: no-cache
|
||||
Content-Length: 299
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Connection: Close
|
||||
Server: GCDWebServer
|
||||
Date: Fri, 25 Apr 2014 14:15:11 GMT
|
||||
|
||||
<html><body> <form name="input" action="/" method="post" enctype="multipart/form-data"> <input type="hidden" name="secret" value="42"> <input type="file" name="files" multiple><br/> <input type="submit" value="Submit"> </form> </body></html>
|
||||
10
Tests/HTMLFileUpload/001-GET.request
Normal file
10
Tests/HTMLFileUpload/001-GET.request
Normal file
@@ -0,0 +1,10 @@
|
||||
GET / HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: max-age=0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
DNT: 1
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
|
||||
9
Tests/HTMLFileUpload/002-200.response
Normal file
9
Tests/HTMLFileUpload/002-200.response
Normal file
@@ -0,0 +1,9 @@
|
||||
HTTP/1.1 200 OK
|
||||
Cache-Control: no-cache
|
||||
Content-Length: 447
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Connection: Close
|
||||
Server: GCDWebServer
|
||||
Date: Fri, 25 Apr 2014 14:15:21 GMT
|
||||
|
||||
<html><body><p>secret = 42<br>files = "hero_mba_11.jpg" (image/jpeg | 106 KB)<br>files = "Test File.txt" (text/plain | 21 Bytes)<br></p><hr> <form name="input" action="/" method="post" enctype="multipart/form-data"> <input type="hidden" name="secret" value="42"> <input type="file" name="files" multiple><br/> <input type="submit" value="Submit"> </form> </body></html>
|
||||
BIN
Tests/HTMLFileUpload/002-POST.request
Normal file
BIN
Tests/HTMLFileUpload/002-POST.request
Normal file
Binary file not shown.
9
Tests/HTMLForm/001-200.response
Normal file
9
Tests/HTMLForm/001-200.response
Normal file
@@ -0,0 +1,9 @@
|
||||
HTTP/1.1 200 OK
|
||||
Cache-Control: no-cache
|
||||
Content-Length: 293
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Connection: Close
|
||||
Server: GCDWebServer
|
||||
Date: Fri, 25 Apr 2014 14:12:09 GMT
|
||||
|
||||
<html><body> <form name="input" action="/" method="post" enctype="application/x-www-form-urlencoded"> Value: <input type="text" name="value"> <input type="submit" value="Submit"> </form> </body></html>
|
||||
10
Tests/HTMLForm/001-GET.request
Normal file
10
Tests/HTMLForm/001-GET.request
Normal file
@@ -0,0 +1,10 @@
|
||||
GET / HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: max-age=0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
DNT: 1
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
|
||||
9
Tests/HTMLForm/002-200.response
Normal file
9
Tests/HTMLForm/002-200.response
Normal file
@@ -0,0 +1,9 @@
|
||||
HTTP/1.1 200 OK
|
||||
Cache-Control: no-cache
|
||||
Content-Length: 47
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Connection: Close
|
||||
Server: GCDWebServer
|
||||
Date: Fri, 25 Apr 2014 14:12:20 GMT
|
||||
|
||||
<html><body><p>Hellø Wörld!</p></body></html>
|
||||
15
Tests/HTMLForm/002-POST.request
Normal file
15
Tests/HTMLForm/002-POST.request
Normal file
@@ -0,0 +1,15 @@
|
||||
POST / HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Content-Length: 30
|
||||
Cache-Control: max-age=0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||
Origin: http://localhost:8080
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
|
||||
value=Hell%C3%B8+W%C3%B6rld%21
|
||||
BIN
Tests/Sample-Movie.mp4
Normal file
BIN
Tests/Sample-Movie.mp4
Normal file
Binary file not shown.
BIN
Tests/WebServer-Sample-Movie/001-200.response
Normal file
BIN
Tests/WebServer-Sample-Movie/001-200.response
Normal file
Binary file not shown.
12
Tests/WebServer-Sample-Movie/001-GET.request
Normal file
12
Tests/WebServer-Sample-Movie/001-GET.request
Normal file
@@ -0,0 +1,12 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: no-cache
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
|
||||
Pragma: no-cache
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
|
||||
BIN
Tests/WebServer-Sample-Movie/002-206.response
Normal file
BIN
Tests/WebServer-Sample-Movie/002-206.response
Normal file
Binary file not shown.
13
Tests/WebServer-Sample-Movie/002-GET.request
Normal file
13
Tests/WebServer-Sample-Movie/002-GET.request
Normal file
@@ -0,0 +1,13 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: no-cache
|
||||
Pragma: no-cache
|
||||
Accept-Encoding: identity;q=1, *;q=0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Accept: */*
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/Sample-Movie.mp4
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
Range: bytes=0-
|
||||
|
||||
BIN
Tests/WebServer-Sample-Movie/003-206.response
Normal file
BIN
Tests/WebServer-Sample-Movie/003-206.response
Normal file
Binary file not shown.
13
Tests/WebServer-Sample-Movie/003-GET.request
Normal file
13
Tests/WebServer-Sample-Movie/003-GET.request
Normal file
@@ -0,0 +1,13 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Cache-Control: no-cache
|
||||
Pragma: no-cache
|
||||
Accept-Encoding: identity;q=1, *;q=0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Accept: */*
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/Sample-Movie.mp4
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
Range: bytes=3391326-
|
||||
|
||||
BIN
Tests/WebServer-Sample-Movie/004-206.response
Normal file
BIN
Tests/WebServer-Sample-Movie/004-206.response
Normal file
Binary file not shown.
12
Tests/WebServer-Sample-Movie/004-GET.request
Normal file
12
Tests/WebServer-Sample-Movie/004-GET.request
Normal file
@@ -0,0 +1,12 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Accept-Encoding: identity;q=1, *;q=0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Accept: */*
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/Sample-Movie.mp4
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
Range: bytes=168-3391487
|
||||
If-Range: 75279017/1388563200/0
|
||||
|
||||
BIN
Tests/WebServer-Sample-Movie/005-206.response
Normal file
BIN
Tests/WebServer-Sample-Movie/005-206.response
Normal file
Binary file not shown.
12
Tests/WebServer-Sample-Movie/005-GET.request
Normal file
12
Tests/WebServer-Sample-Movie/005-GET.request
Normal file
@@ -0,0 +1,12 @@
|
||||
GET /Sample-Movie.mp4 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: keep-alive
|
||||
Accept-Encoding: identity;q=1, *;q=0
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
|
||||
Accept: */*
|
||||
DNT: 1
|
||||
Referer: http://localhost:8080/Sample-Movie.mp4
|
||||
Accept-Language: en-US,en;q=0.8,fr;q=0.6
|
||||
Range: bytes=168-1023
|
||||
If-Range: 75279017/1388563200/0
|
||||
|
||||
@@ -4,7 +4,7 @@ Server: GCDWebUploader
|
||||
Content-Type: text/plain
|
||||
Last-Modified: Thu, 10 Apr 2014 11:10:14 GMT
|
||||
Date: Sat, 12 Apr 2014 05:36:17 GMT
|
||||
Content-Disposition: attachment; filename="Copy.txt"
|
||||
Content-Disposition: attachment; filename="Copy.txt"; filename*=UTF-8''Copy.txt
|
||||
Content-Length: 271
|
||||
Cache-Control: no-cache
|
||||
Etag: 73598983/1397128214/0
|
||||
|
||||
@@ -30,15 +30,12 @@
|
||||
|
||||
@interface AppDelegate () <GCDWebUploaderDelegate> {
|
||||
@private
|
||||
UIWindow* _window;
|
||||
GCDWebUploader* _webServer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
@synthesize window=_window;
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
|
||||
- (void)dealloc {
|
||||
@@ -50,14 +47,15 @@
|
||||
#endif
|
||||
|
||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||
_window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
CGRect bounds = ([UIScreen instancesRespondToSelector:@selector(nativeBounds)] ? [[UIScreen mainScreen] nativeBounds] : [[UIScreen mainScreen] bounds]);
|
||||
_window = [[UIWindow alloc] initWithFrame:bounds];
|
||||
_window.backgroundColor = [UIColor whiteColor];
|
||||
[_window makeKeyAndVisible];
|
||||
|
||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||
_webServer = [[GCDWebUploader alloc] initWithUploadDirectory:documentsPath];
|
||||
_webServer.delegate = self;
|
||||
_webServer.showHiddenFiles = YES;
|
||||
_webServer.allowHiddenItems = YES;
|
||||
[_webServer start];
|
||||
|
||||
return YES;
|
||||
|
||||
Reference in New Issue
Block a user