mirror of
https://github.com/swisspol/GCDWebServer.git
synced 2026-02-11 00:00:07 +08:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a5208bd60f |
@@ -29,30 +29,128 @@
|
|||||||
|
|
||||||
@class GCDWebDAVServer;
|
@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>
|
@protocol GCDWebDAVServerDelegate <GCDWebServerDelegate>
|
||||||
@optional
|
@optional
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file has been downloaded.
|
||||||
|
*/
|
||||||
- (void)davServer:(GCDWebDAVServer*)server didDownloadFileAtPath:(NSString*)path;
|
- (void)davServer:(GCDWebDAVServer*)server didDownloadFileAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file has been uploaded.
|
||||||
|
*/
|
||||||
- (void)davServer:(GCDWebDAVServer*)server didUploadFileAtPath:(NSString*)path;
|
- (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;
|
- (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;
|
- (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;
|
- (void)davServer:(GCDWebDAVServer*)server didDeleteItemAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a directory has been created.
|
||||||
|
*/
|
||||||
- (void)davServer:(GCDWebDAVServer*)server didCreateDirectoryAtPath:(NSString*)path;
|
- (void)davServer:(GCDWebDAVServer*)server didCreateDirectoryAtPath:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@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
|
@interface GCDWebDAVServer : GCDWebServer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the upload directory as specified when the server was initialized.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* uploadDirectory;
|
@property(nonatomic, readonly) NSString* uploadDirectory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the delegate for the server.
|
||||||
|
*/
|
||||||
@property(nonatomic, assign) id<GCDWebDAVServerDelegate> delegate;
|
@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;
|
- (instancetype)initWithUploadDirectory:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@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)
|
@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
|
* This method is called to check if a file upload is allowed to complete.
|
||||||
- (BOOL)shouldDeleteItemAtPath:(NSString*)path; // Default implementation returns YES
|
* The uploaded file is available for inspection at "tempPath".
|
||||||
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path; // Default implementation returns YES
|
*
|
||||||
|
* 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
|
@end
|
||||||
|
|||||||
@@ -55,12 +55,17 @@ typedef NS_ENUM(NSInteger, DAVProperties) {
|
|||||||
@private
|
@private
|
||||||
NSString* _uploadDirectory;
|
NSString* _uploadDirectory;
|
||||||
NSArray* _allowedExtensions;
|
NSArray* _allowedExtensions;
|
||||||
BOOL _showHidden;
|
BOOL _allowHidden;
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GCDWebDAVServer (Methods)
|
@implementation GCDWebDAVServer (Methods)
|
||||||
|
|
||||||
|
// Must match implementation in GCDWebUploader
|
||||||
|
- (BOOL)_checkSandboxedPath:(NSString*)path {
|
||||||
|
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
||||||
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
||||||
return NO;
|
return NO;
|
||||||
@@ -87,12 +92,12 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
NSString* relativePath = request.path;
|
NSString* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
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];
|
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* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
BOOL isDirectory;
|
BOOL isDirectory;
|
||||||
@@ -130,7 +135,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* fileName = [absolutePath lastPathComponent];
|
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];
|
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* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
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];
|
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* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
BOOL isDirectory;
|
BOOL isDirectory;
|
||||||
@@ -203,7 +208,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* directoryName = [absolutePath lastPathComponent];
|
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];
|
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* srcRelativePath = request.path;
|
||||||
NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:srcRelativePath];
|
NSString* srcAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:srcRelativePath];
|
||||||
if (![srcAbsolutePath hasPrefix:_uploadDirectory]) {
|
if (![self _checkSandboxedPath:srcAbsolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +259,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
}
|
}
|
||||||
dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
dstRelativePath = [[dstRelativePath substringFromIndex:(range.location + range.length)] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||||
NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath];
|
NSString* dstAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:dstRelativePath];
|
||||||
if (![dstAbsolutePath hasPrefix:_uploadDirectory]) {
|
if (![self _checkSandboxedPath:dstAbsolutePath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", srcRelativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +269,7 @@ static inline BOOL _IsMacFinder(GCDWebServerRequest* request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [dstAbsolutePath lastPathComponent];
|
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];
|
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* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
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];
|
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:@"/"];
|
relativePath = [relativePath stringByAppendingString:@"/"];
|
||||||
}
|
}
|
||||||
for (NSString* item in items) {
|
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];
|
[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* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,7 +530,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
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];
|
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* relativePath = request.path;
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![absolutePath hasPrefix:_uploadDirectory] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,7 +590,7 @@ static inline xmlNodePtr _XMLChildWithName(xmlNodePtr child, const xmlChar* name
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
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];
|
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
|
@implementation GCDWebDAVServer
|
||||||
|
|
||||||
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, showHiddenFiles=_showHidden;
|
@synthesize uploadDirectory=_uploadDirectory, allowedFileExtensions=_allowedExtensions, allowHiddenItems=_allowHidden;
|
||||||
|
|
||||||
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'GCDWebServer'
|
s.name = 'GCDWebServer'
|
||||||
s.version = '2.3'
|
s.version = '2.5'
|
||||||
s.author = { 'Pierre-Olivier Latour' => 'info@pol-online.net' }
|
s.author = { 'Pierre-Olivier Latour' => 'info@pol-online.net' }
|
||||||
s.license = { :type => 'BSD', :file => 'LICENSE' }
|
s.license = { :type => 'BSD', :file => 'LICENSE' }
|
||||||
s.homepage = 'https://github.com/swisspol/GCDWebServer'
|
s.homepage = 'https://github.com/swisspol/GCDWebServer'
|
||||||
@@ -22,6 +22,7 @@ Pod::Spec.new do |s|
|
|||||||
|
|
||||||
s.subspec 'Core' do |cs|
|
s.subspec 'Core' do |cs|
|
||||||
cs.source_files = 'GCDWebServer/**/*.{h,m}'
|
cs.source_files = 'GCDWebServer/**/*.{h,m}'
|
||||||
|
cs.private_header_files = "GCDWebServer/Core/GCDWebServerPrivate.h"
|
||||||
cs.requires_arc = true
|
cs.requires_arc = true
|
||||||
cs.ios.library = 'z'
|
cs.ios.library = 'z'
|
||||||
cs.ios.frameworks = 'MobileCoreServices', 'CFNetwork'
|
cs.ios.frameworks = 'MobileCoreServices', 'CFNetwork'
|
||||||
|
|||||||
@@ -54,8 +54,8 @@
|
|||||||
E28BAE4918F99C810095C089 /* GCDWebServerErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */; };
|
E28BAE4918F99C810095C089 /* GCDWebServerErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */; };
|
||||||
E28BAE4A18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */; };
|
E28BAE4A18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */; };
|
||||||
E28BAE4B18F99C810095C089 /* 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 */; };
|
E28BAE4C18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */; };
|
||||||
E28BAE4D18F99C810095C089 /* GCDWebServerStreamingResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3318F99C810095C089 /* GCDWebServerStreamingResponse.m */; };
|
E28BAE4D18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */; };
|
||||||
E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
|
E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E2A0E80918F3432600C580B1 /* GCDWebDAVServer.m */; };
|
||||||
E2A0E80B18F3432600C580B1 /* 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 */; };
|
E2A0E80D18F35C9A00C580B1 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A0E80C18F35C9A00C580B1 /* libxml2.dylib */; };
|
||||||
@@ -138,8 +138,8 @@
|
|||||||
E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerErrorResponse.m; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
E28BAE3218F99C810095C089 /* GCDWebServerStreamedResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDWebServerStreamedResponse.h; sourceTree = "<group>"; };
|
||||||
E28BAE3318F99C810095C089 /* GCDWebServerStreamingResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDWebServerStreamingResponse.m; 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>"; };
|
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>"; };
|
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; };
|
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; };
|
||||||
@@ -300,8 +300,8 @@
|
|||||||
E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */,
|
E28BAE2F18F99C810095C089 /* GCDWebServerErrorResponse.m */,
|
||||||
E28BAE3018F99C810095C089 /* GCDWebServerFileResponse.h */,
|
E28BAE3018F99C810095C089 /* GCDWebServerFileResponse.h */,
|
||||||
E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */,
|
E28BAE3118F99C810095C089 /* GCDWebServerFileResponse.m */,
|
||||||
E28BAE3218F99C810095C089 /* GCDWebServerStreamingResponse.h */,
|
E28BAE3218F99C810095C089 /* GCDWebServerStreamedResponse.h */,
|
||||||
E28BAE3318F99C810095C089 /* GCDWebServerStreamingResponse.m */,
|
E28BAE3318F99C810095C089 /* GCDWebServerStreamedResponse.m */,
|
||||||
);
|
);
|
||||||
path = Responses;
|
path = Responses;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -437,7 +437,7 @@
|
|||||||
E28BAE3618F99C810095C089 /* GCDWebServerConnection.m in Sources */,
|
E28BAE3618F99C810095C089 /* GCDWebServerConnection.m in Sources */,
|
||||||
E28BAE3C18F99C810095C089 /* GCDWebServerResponse.m in Sources */,
|
E28BAE3C18F99C810095C089 /* GCDWebServerResponse.m in Sources */,
|
||||||
E28BAE4018F99C810095C089 /* GCDWebServerFileRequest.m in Sources */,
|
E28BAE4018F99C810095C089 /* GCDWebServerFileRequest.m in Sources */,
|
||||||
E28BAE4C18F99C810095C089 /* GCDWebServerStreamingResponse.m in Sources */,
|
E28BAE4C18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */,
|
||||||
E28BAE3E18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */,
|
E28BAE3E18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */,
|
||||||
E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */,
|
E2A0E80A18F3432600C580B1 /* GCDWebDAVServer.m in Sources */,
|
||||||
E28BAE4218F99C810095C089 /* GCDWebServerMultiPartFormRequest.m in Sources */,
|
E28BAE4218F99C810095C089 /* GCDWebServerMultiPartFormRequest.m in Sources */,
|
||||||
@@ -457,7 +457,7 @@
|
|||||||
E28BAE4B18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */,
|
E28BAE4B18F99C810095C089 /* GCDWebServerFileResponse.m in Sources */,
|
||||||
E28BAE3918F99C810095C089 /* GCDWebServerFunctions.m in Sources */,
|
E28BAE3918F99C810095C089 /* GCDWebServerFunctions.m in Sources */,
|
||||||
E28BAE4118F99C810095C089 /* GCDWebServerFileRequest.m in Sources */,
|
E28BAE4118F99C810095C089 /* GCDWebServerFileRequest.m in Sources */,
|
||||||
E28BAE4D18F99C810095C089 /* GCDWebServerStreamingResponse.m in Sources */,
|
E28BAE4D18F99C810095C089 /* GCDWebServerStreamedResponse.m in Sources */,
|
||||||
E28BAE3F18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */,
|
E28BAE3F18F99C810095C089 /* GCDWebServerDataRequest.m in Sources */,
|
||||||
E2BE850B18E77ECA0061360B /* GCDWebUploader.m in Sources */,
|
E2BE850B18E77ECA0061360B /* GCDWebUploader.m in Sources */,
|
||||||
E22112951690B64F0048D2B2 /* AppDelegate.m in Sources */,
|
E22112951690B64F0048D2B2 /* AppDelegate.m in Sources */,
|
||||||
|
|||||||
@@ -30,8 +30,14 @@
|
|||||||
#import "GCDWebServerRequest.h"
|
#import "GCDWebServerRequest.h"
|
||||||
#import "GCDWebServerResponse.h"
|
#import "GCDWebServerResponse.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log levels used by GCDWebServer.
|
||||||
|
*
|
||||||
|
* @warning kGCDWebServerLogLevel_Debug is only available if "NDEBUG" is not
|
||||||
|
* defined when building.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(int, GCDWebServerLogLevel) {
|
typedef NS_ENUM(int, GCDWebServerLogLevel) {
|
||||||
kGCDWebServerLogLevel_Debug = 0, // Only available if "NDEBUG" is not defined when building
|
kGCDWebServerLogLevel_Debug = 0,
|
||||||
kGCDWebServerLogLevel_Verbose,
|
kGCDWebServerLogLevel_Verbose,
|
||||||
kGCDWebServerLogLevel_Info,
|
kGCDWebServerLogLevel_Info,
|
||||||
kGCDWebServerLogLevel_Warning,
|
kGCDWebServerLogLevel_Warning,
|
||||||
@@ -39,88 +45,455 @@ typedef NS_ENUM(int, GCDWebServerLogLevel) {
|
|||||||
kGCDWebServerLogLevel_Exception,
|
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);
|
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);
|
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)
|
* The port used by the GCDWebServer (NSNumber / NSUInteger).
|
||||||
extern NSString* const GCDWebServerOption_MaxPendingConnections; // NSNumber / NSUInteger (default is 16)
|
*
|
||||||
extern NSString* const GCDWebServerOption_ServerName; // NSString (default is server class name)
|
* The default value is 0 i.e. let the OS pick a random port.
|
||||||
extern NSString* const GCDWebServerOption_AuthenticationMethod; // One of "GCDWebServerAuthenticationMethod_..." (default is nil i.e. no authentication)
|
*/
|
||||||
extern NSString* const GCDWebServerOption_AuthenticationRealm; // NSString (default is server name)
|
extern NSString* const GCDWebServerOption_Port;
|
||||||
extern NSString* const GCDWebServerOption_AuthenticationAccounts; // NSDictionary of username / password (default is nil i.e. no accounts)
|
|
||||||
extern NSString* const GCDWebServerOption_ConnectionClass; // Subclass of GCDWebServerConnection (default is GCDWebServerConnection class)
|
/**
|
||||||
extern NSString* const GCDWebServerOption_AutomaticallyMapHEADToGET; // NSNumber / BOOL (default is YES)
|
* The Bonjour name used by the GCDWebServer (NSString).
|
||||||
extern NSString* const GCDWebServerOption_ConnectedStateCoalescingInterval; // NSNumber / double (default is 1.0 seconds - set to <=0.0 to disable coaslescing of -webServerDidConnect: / -webServerDidDisconnect:)
|
*
|
||||||
|
* The default value is an empty string i.e. use the computer / device name.
|
||||||
|
*/
|
||||||
|
extern NSString* const GCDWebServerOption_BonjourName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Bonjour service type used by the GCDWebServer (NSString).
|
||||||
|
*
|
||||||
|
* The default value is "_http._tcp", standard HTTP web server.
|
||||||
|
*/
|
||||||
|
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
|
#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
|
#endif
|
||||||
|
|
||||||
extern NSString* const GCDWebServerAuthenticationMethod_Basic; // Not recommended as password is sent in clear
|
/**
|
||||||
|
* 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;
|
extern NSString* const GCDWebServerAuthenticationMethod_DigestAccess;
|
||||||
|
|
||||||
@class GCDWebServer;
|
@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>
|
@protocol GCDWebServerDelegate <NSObject>
|
||||||
@optional
|
@optional
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called after the server has successfully started.
|
||||||
|
*/
|
||||||
- (void)webServerDidStart:(GCDWebServer*)server;
|
- (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;
|
- (void)webServerDidStop:(GCDWebServer*)server;
|
||||||
|
|
||||||
@end
|
@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
|
@interface GCDWebServer : NSObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the delegate for the server.
|
||||||
|
*/
|
||||||
@property(nonatomic, assign) id<GCDWebServerDelegate> delegate;
|
@property(nonatomic, assign) id<GCDWebServerDelegate> delegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns YES if the server is currently running.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly, getter=isRunning) BOOL 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;
|
- (instancetype)init;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a handler to the server to handle 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;
|
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)processBlock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all handlers previously added to the server.
|
||||||
|
*
|
||||||
|
* @warning Removing handlers while the server is running is not allowed.
|
||||||
|
*/
|
||||||
- (void)removeAllHandlers;
|
- (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
|
* Starts the server with explicit options. This method is the designated way
|
||||||
- (BOOL)startWithOptions:(NSDictionary*)options;
|
* to start the server.
|
||||||
- (void)stop; // Does not abort any currently opened connections
|
*
|
||||||
|
* 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
|
@end
|
||||||
|
|
||||||
@interface GCDWebServer (Extensions)
|
@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.
|
||||||
|
*/
|
||||||
|
@property(nonatomic, readonly) NSURL* bonjourServerURL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server on port 8080 (OS X & iOS Simulator) or port 80 (iOS)
|
||||||
|
* using the computer / device name for as the Bonjour name.
|
||||||
|
*
|
||||||
|
* 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 computer / device name.
|
||||||
|
*
|
||||||
|
* Returns NO if the server failed to start.
|
||||||
|
*/
|
||||||
|
- (BOOL)startWithPort:(NSUInteger)port bonjourName:(NSString*)name;
|
||||||
|
|
||||||
#if !TARGET_OS_IPHONE
|
#if !TARGET_OS_IPHONE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name;
|
||||||
- (BOOL)runWithOptions:(NSDictionary*)options; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only)
|
|
||||||
|
/**
|
||||||
|
* 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
|
#endif
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServer (Handlers)
|
@interface GCDWebServer (Handlers)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a default handler to the server to handle all incoming HTTP requests
|
||||||
|
* with a given HTTP method.
|
||||||
|
*/
|
||||||
- (void)addDefaultHandlerForMethod:(NSString*)method requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
- (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 handler to the server to handle incoming HTTP requests with a given
|
||||||
|
* HTTP method and a specific case-insensitive path.
|
||||||
|
*/
|
||||||
|
- (void)addHandlerForMethod:(NSString*)method path:(NSString*)path requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a handler to the server to handle incoming HTTP requests with a given
|
||||||
|
* HTTP method and a path matching a case-insensitive regular expression.
|
||||||
|
*/
|
||||||
|
- (void)addHandlerForMethod:(NSString*)method pathRegex:(NSString*)regex requestClass:(Class)aClass processBlock:(GCDWebServerProcessBlock)block;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServer (GETHandlers)
|
@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
|
@end
|
||||||
|
|
||||||
@interface GCDWebServer (Logging)
|
@interface GCDWebServer (Logging)
|
||||||
|
|
||||||
#ifndef __GCDWEBSERVER_LOGGING_HEADER__
|
#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 either DEBUG or INFO if "NDEBUG" is defined at build-time.
|
||||||
|
* It can also be set at runtime with the "logLevel" environment variable.
|
||||||
|
*/
|
||||||
|
+ (void)setLogLevel:(GCDWebServerLogLevel)level;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a message with the kGCDWebServerLogLevel_Verbose level.
|
||||||
|
*/
|
||||||
- (void)logVerbose:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
- (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);
|
- (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);
|
- (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);
|
- (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs an exception with the kGCDWebServerLogLevel_Exception level.
|
||||||
|
*/
|
||||||
|
- (void)logException:(NSException*)exception;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||||
|
|
||||||
@interface GCDWebServer (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)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path; // 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
|
@end
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -43,45 +43,9 @@
|
|||||||
#define kDefaultPort 8080
|
#define kDefaultPort 8080
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@interface GCDWebServer () {
|
|
||||||
@private
|
|
||||||
id<GCDWebServerDelegate> __unsafe_unretained _delegate;
|
|
||||||
dispatch_queue_t _syncQueue;
|
|
||||||
NSMutableArray* _handlers;
|
|
||||||
NSInteger _activeConnections; // Accessed only with _syncQueue
|
|
||||||
BOOL _connected;
|
|
||||||
CFRunLoopTimerRef _connectedTimer;
|
|
||||||
|
|
||||||
NSDictionary* _options;
|
|
||||||
NSString* _serverName;
|
|
||||||
NSString* _authenticationRealm;
|
|
||||||
NSMutableDictionary* _authenticationBasicAccounts;
|
|
||||||
NSMutableDictionary* _authenticationDigestAccounts;
|
|
||||||
Class _connectionClass;
|
|
||||||
BOOL _mapHEADToGET;
|
|
||||||
CFTimeInterval _disconnectDelay;
|
|
||||||
NSUInteger _port;
|
|
||||||
dispatch_source_t _source;
|
|
||||||
CFNetServiceRef _service;
|
|
||||||
#if TARGET_OS_IPHONE
|
|
||||||
BOOL _suspendInBackground;
|
|
||||||
UIBackgroundTaskIdentifier _backgroundTask;
|
|
||||||
#endif
|
|
||||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
|
||||||
BOOL _recording;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface GCDWebServerHandler () {
|
|
||||||
@private
|
|
||||||
GCDWebServerMatchBlock _matchBlock;
|
|
||||||
GCDWebServerProcessBlock _processBlock;
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
|
|
||||||
NSString* const GCDWebServerOption_Port = @"Port";
|
NSString* const GCDWebServerOption_Port = @"Port";
|
||||||
NSString* const GCDWebServerOption_BonjourName = @"BonjourName";
|
NSString* const GCDWebServerOption_BonjourName = @"BonjourName";
|
||||||
|
NSString* const GCDWebServerOption_BonjourType = @"BonjourType";
|
||||||
NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections";
|
NSString* const GCDWebServerOption_MaxPendingConnections = @"MaxPendingConnections";
|
||||||
NSString* const GCDWebServerOption_ServerName = @"ServerName";
|
NSString* const GCDWebServerOption_ServerName = @"ServerName";
|
||||||
NSString* const GCDWebServerOption_AuthenticationMethod = @"AuthenticationMethod";
|
NSString* const GCDWebServerOption_AuthenticationMethod = @"AuthenticationMethod";
|
||||||
@@ -132,6 +96,28 @@ static void _SignalHandler(int signal) {
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !TARGET_OS_IPHONE || defined(__GCDWEBSERVER_ENABLE_TESTING__)
|
||||||
|
|
||||||
|
// This utility function is used to ensure scheduled callbacks on the main thread are called when running the server synchronously
|
||||||
|
// https://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html
|
||||||
|
// The main queue works with the application’s run loop to interleave the execution of queued tasks with the execution of other event sources attached to the run loop
|
||||||
|
// TODO: Ensure all scheduled blocks on the main queue are also executed
|
||||||
|
static void _ExecuteMainThreadRunLoopSources() {
|
||||||
|
SInt32 result;
|
||||||
|
do {
|
||||||
|
result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true);
|
||||||
|
} while (result == kCFRunLoopRunHandledSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@interface GCDWebServerHandler () {
|
||||||
|
@private
|
||||||
|
GCDWebServerMatchBlock _matchBlock;
|
||||||
|
GCDWebServerProcessBlock _processBlock;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
@implementation GCDWebServerHandler
|
@implementation GCDWebServerHandler
|
||||||
|
|
||||||
@synthesize matchBlock=_matchBlock, processBlock=_processBlock;
|
@synthesize matchBlock=_matchBlock, processBlock=_processBlock;
|
||||||
@@ -153,6 +139,38 @@ static void _SignalHandler(int signal) {
|
|||||||
|
|
||||||
@end
|
@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
|
||||||
|
BOOL _disconnecting; // 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 _service;
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
|
BOOL _suspendInBackground;
|
||||||
|
UIBackgroundTaskIdentifier _backgroundTask;
|
||||||
|
#endif
|
||||||
|
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||||
|
BOOL _recording;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
@implementation GCDWebServer
|
@implementation GCDWebServer
|
||||||
|
|
||||||
@synthesize delegate=_delegate, handlers=_handlers, port=_port, serverName=_serverName, authenticationRealm=_authenticationRealm,
|
@synthesize delegate=_delegate, handlers=_handlers, port=_port, serverName=_serverName, authenticationRealm=_authenticationRealm,
|
||||||
@@ -174,19 +192,23 @@ static void _SignalHandler(int signal) {
|
|||||||
GCDWebServerInitializeFunctions();
|
GCDWebServerInitializeFunctions();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
static void _DisconnectTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
||||||
|
DCHECK([NSThread isMainThread]);
|
||||||
|
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
[(ARC_BRIDGE GCDWebServer*)info _didDisconnect];
|
[server _didDisconnect];
|
||||||
}
|
}
|
||||||
|
server->_disconnecting = NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)init {
|
- (instancetype)init {
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_syncQueue = dispatch_queue_create([NSStringFromClass([self class]) UTF8String], DISPATCH_QUEUE_SERIAL);
|
_syncQueue = dispatch_queue_create([NSStringFromClass([self class]) UTF8String], DISPATCH_QUEUE_SERIAL);
|
||||||
|
_sourceSemaphore = dispatch_semaphore_create(0);
|
||||||
_handlers = [[NSMutableArray alloc] init];
|
_handlers = [[NSMutableArray alloc] init];
|
||||||
CFRunLoopTimerContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
CFRunLoopTimerContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
||||||
_connectedTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, HUGE_VAL, HUGE_VAL, 0, 0, _ConnectedTimerCallBack, &context);
|
_disconnectTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, HUGE_VAL, HUGE_VAL, 0, 0, _DisconnectTimerCallBack, &context);
|
||||||
CFRunLoopAddTimer(CFRunLoopGetMain(), _connectedTimer, kCFRunLoopCommonModes);
|
CFRunLoopAddTimer(CFRunLoopGetMain(), _disconnectTimer, kCFRunLoopCommonModes);
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
_backgroundTask = UIBackgroundTaskInvalid;
|
_backgroundTask = UIBackgroundTaskInvalid;
|
||||||
#endif
|
#endif
|
||||||
@@ -197,15 +219,12 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
|||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
DCHECK(_connected == NO);
|
DCHECK(_connected == NO);
|
||||||
DCHECK(_activeConnections == 0);
|
DCHECK(_activeConnections == 0);
|
||||||
|
DCHECK(_options == nil); // The server can never be dealloc'ed while running because of the retain-cycle with the dispatch source
|
||||||
|
|
||||||
_delegate = nil;
|
CFRunLoopTimerInvalidate(_disconnectTimer);
|
||||||
if (_options) {
|
CFRelease(_disconnectTimer);
|
||||||
[self stop];
|
|
||||||
}
|
|
||||||
|
|
||||||
CFRunLoopTimerInvalidate(_connectedTimer);
|
|
||||||
CFRelease(_connectedTimer);
|
|
||||||
ARC_RELEASE(_handlers);
|
ARC_RELEASE(_handlers);
|
||||||
|
ARC_DISPATCH_RELEASE(_sourceSemaphore);
|
||||||
ARC_DISPATCH_RELEASE(_syncQueue);
|
ARC_DISPATCH_RELEASE(_syncQueue);
|
||||||
|
|
||||||
ARC_DEALLOC(super);
|
ARC_DEALLOC(super);
|
||||||
@@ -253,8 +272,9 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
|||||||
DCHECK(_activeConnections >= 0);
|
DCHECK(_activeConnections >= 0);
|
||||||
if (_activeConnections == 0) {
|
if (_activeConnections == 0) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
if (_disconnectDelay > 0.0) {
|
if (_disconnecting) {
|
||||||
CFRunLoopTimerSetNextFireDate(_connectedTimer, HUGE_VAL);
|
CFRunLoopTimerSetNextFireDate(_disconnectTimer, HUGE_VAL);
|
||||||
|
_disconnecting = NO;
|
||||||
}
|
}
|
||||||
if (_connected == NO) {
|
if (_connected == NO) {
|
||||||
[self _didConnect];
|
[self _didConnect];
|
||||||
@@ -307,8 +327,9 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
|||||||
_activeConnections -= 1;
|
_activeConnections -= 1;
|
||||||
if (_activeConnections == 0) {
|
if (_activeConnections == 0) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
if (_disconnectDelay > 0.0) {
|
if ((_disconnectDelay > 0.0) && (_source != NULL)) {
|
||||||
CFRunLoopTimerSetNextFireDate(_connectedTimer, CFAbsoluteTimeGetCurrent() + _disconnectDelay);
|
CFRunLoopTimerSetNextFireDate(_disconnectTimer, CFAbsoluteTimeGetCurrent() + _disconnectDelay);
|
||||||
|
_disconnecting = YES;
|
||||||
} else {
|
} else {
|
||||||
[self _didDisconnect];
|
[self _didDisconnect];
|
||||||
}
|
}
|
||||||
@@ -322,6 +343,11 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
|||||||
return name && CFStringGetLength(name) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, name)) : nil;
|
return name && CFStringGetLength(name) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, name)) : nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSString*)bonjourType {
|
||||||
|
CFStringRef type = _service ? CFNetServiceGetType(_service) : NULL;
|
||||||
|
return type && CFStringGetLength(type) ? ARC_BRIDGE_RELEASE(CFStringCreateCopy(kCFAllocatorDefault, type)) : nil;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock {
|
- (void)addHandlerWithMatchBlock:(GCDWebServerMatchBlock)matchBlock processBlock:(GCDWebServerProcessBlock)handlerBlock {
|
||||||
DCHECK(_options == nil);
|
DCHECK(_options == nil);
|
||||||
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock];
|
GCDWebServerHandler* handler = [[GCDWebServerHandler alloc] initWithMatchBlock:matchBlock processBlock:handlerBlock];
|
||||||
@@ -335,12 +361,16 @@ static void _ConnectedTimerCallBack(CFRunLoopTimerRef timer, void* info) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* error, void* info) {
|
||||||
|
DCHECK([NSThread isMainThread]);
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
if (error->error) {
|
if (error->error) {
|
||||||
LOG_ERROR(@"Bonjour error %i (domain %i)", (int)error->error, (int)error->domain);
|
LOG_ERROR(@"Bonjour error %i (domain %i)", (int)error->error, (int)error->domain);
|
||||||
} else {
|
} else {
|
||||||
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
|
GCDWebServer* server = (ARC_BRIDGE GCDWebServer*)info;
|
||||||
LOG_INFO(@"%@ now reachable at %@", [server class], server.bonjourServerURL);
|
LOG_INFO(@"%@ now reachable at %@", [server class], server.bonjourServerURL);
|
||||||
|
if ([server.delegate respondsToSelector:@selector(webServerDidCompleteBonjourRegistration:)]) {
|
||||||
|
[server.delegate webServerDidCompleteBonjourRegistration:server];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,10 +389,12 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
#endif
|
#endif
|
||||||
return ARC_AUTORELEASE([[NSString alloc] initWithData:[data base64EncodedDataWithOptions:0] encoding:NSASCIIStringEncoding]);
|
return ARC_AUTORELEASE([[NSString alloc] initWithData:[data base64EncodedDataWithOptions:0] encoding:NSASCIIStringEncoding]);
|
||||||
}
|
}
|
||||||
- (BOOL)_start {
|
|
||||||
|
- (BOOL)_start:(NSError**)error {
|
||||||
DCHECK(_source == NULL);
|
DCHECK(_source == NULL);
|
||||||
NSUInteger port = [_GetOption(_options, GCDWebServerOption_Port, @0) unsignedIntegerValue];
|
NSUInteger port = [_GetOption(_options, GCDWebServerOption_Port, @0) unsignedIntegerValue];
|
||||||
NSString* name = _GetOption(_options, GCDWebServerOption_BonjourName, @"");
|
NSString* name = _GetOption(_options, GCDWebServerOption_BonjourName, @"");
|
||||||
|
NSString* bonjourType = _GetOption(_options, GCDWebServerOption_BonjourType, @"_http._tcp");
|
||||||
NSUInteger maxPendingConnections = [_GetOption(_options, GCDWebServerOption_MaxPendingConnections, @16) unsignedIntegerValue];
|
NSUInteger maxPendingConnections = [_GetOption(_options, GCDWebServerOption_MaxPendingConnections, @16) unsignedIntegerValue];
|
||||||
int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|
int listeningSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||||
if (listeningSocket > 0) {
|
if (listeningSocket > 0) {
|
||||||
@@ -409,6 +441,7 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
LOG_DEBUG(@"Did close listening socket %i", listeningSocket);
|
LOG_DEBUG(@"Did close listening socket %i", listeningSocket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dispatch_semaphore_signal(_sourceSemaphore);
|
||||||
|
|
||||||
});
|
});
|
||||||
dispatch_source_set_event_handler(_source, ^{
|
dispatch_source_set_event_handler(_source, ^{
|
||||||
@@ -459,13 +492,13 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
_service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), CFSTR("_http._tcp"), (ARC_BRIDGE CFStringRef)name, (SInt32)_port);
|
_service = CFNetServiceCreate(kCFAllocatorDefault, CFSTR("local."), (ARC_BRIDGE CFStringRef)bonjourType, (ARC_BRIDGE CFStringRef)name, (SInt32)_port);
|
||||||
if (_service) {
|
if (_service) {
|
||||||
CFNetServiceClientContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
CFNetServiceClientContext context = {0, (ARC_BRIDGE void*)self, NULL, NULL, NULL};
|
||||||
CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
|
CFNetServiceSetClient(_service, _NetServiceClientCallBack, &context);
|
||||||
CFNetServiceScheduleWithRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
CFNetServiceScheduleWithRunLoop(_service, CFRunLoopGetMain(), kCFRunLoopCommonModes);
|
||||||
CFStreamError error = {0};
|
CFStreamError streamError = {0};
|
||||||
CFNetServiceRegisterWithOptions(_service, 0, &error);
|
CFNetServiceRegisterWithOptions(_service, 0, &streamError);
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR(@"Failed creating CFNetService");
|
LOG_ERROR(@"Failed creating CFNetService");
|
||||||
}
|
}
|
||||||
@@ -479,15 +512,24 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
close(listeningSocket);
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
close(listeningSocket);
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
return (_source ? YES : NO);
|
||||||
}
|
}
|
||||||
@@ -502,7 +544,8 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
_service = NULL;
|
_service = 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);
|
ARC_DISPATCH_RELEASE(_source);
|
||||||
_source = NULL;
|
_source = NULL;
|
||||||
_port = 0;
|
_port = 0;
|
||||||
@@ -516,6 +559,14 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
ARC_RELEASE(_authenticationDigestAccounts);
|
ARC_RELEASE(_authenticationDigestAccounts);
|
||||||
_authenticationDigestAccounts = nil;
|
_authenticationDigestAccounts = nil;
|
||||||
|
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (_disconnecting) {
|
||||||
|
CFRunLoopTimerSetNextFireDate(_disconnectTimer, HUGE_VAL);
|
||||||
|
_disconnecting = NO;
|
||||||
|
[self _didDisconnect];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
LOG_INFO(@"%@ stopped", [self class]);
|
LOG_INFO(@"%@ stopped", [self class]);
|
||||||
if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) {
|
if ([_delegate respondsToSelector:@selector(webServerDidStop:)]) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
@@ -524,17 +575,6 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (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
|
#if TARGET_OS_IPHONE
|
||||||
|
|
||||||
- (void)_didEnterBackground:(NSNotification*)notification {
|
- (void)_didEnterBackground:(NSNotification*)notification {
|
||||||
@@ -549,20 +589,20 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
DCHECK([NSThread isMainThread]);
|
DCHECK([NSThread isMainThread]);
|
||||||
LOG_DEBUG(@"Will enter foreground");
|
LOG_DEBUG(@"Will enter foreground");
|
||||||
if (!_source) {
|
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
|
#endif
|
||||||
|
|
||||||
- (BOOL)startWithOptions:(NSDictionary*)options {
|
- (BOOL)startWithOptions:(NSDictionary*)options error:(NSError**)error {
|
||||||
if (_options == nil) {
|
if (_options == nil) {
|
||||||
_options = [options copy];
|
_options = [options copy];
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
_suspendInBackground = [_GetOption(_options, GCDWebServerOption_AutomaticallySuspendInBackground, @YES) boolValue];
|
_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
|
#else
|
||||||
if (![self _start])
|
if (![self _start:error])
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
ARC_RELEASE(_options);
|
ARC_RELEASE(_options);
|
||||||
@@ -636,29 +676,43 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
return nil;
|
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
|
#if !TARGET_OS_IPHONE
|
||||||
|
|
||||||
- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name {
|
- (BOOL)runWithPort:(NSUInteger)port bonjourName:(NSString*)name {
|
||||||
NSMutableDictionary* options = [NSMutableDictionary dictionary];
|
NSMutableDictionary* options = [NSMutableDictionary dictionary];
|
||||||
[options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port];
|
[options setObject:[NSNumber numberWithInteger:port] forKey:GCDWebServerOption_Port];
|
||||||
[options setValue:name forKey:GCDWebServerOption_BonjourName];
|
[options setValue:name forKey:GCDWebServerOption_BonjourName];
|
||||||
return [self runWithOptions:options];
|
return [self runWithOptions:options error:NULL];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)runWithOptions:(NSDictionary*)options {
|
- (BOOL)runWithOptions:(NSDictionary*)options error:(NSError**)error {
|
||||||
DCHECK([NSThread isMainThread]);
|
DCHECK([NSThread isMainThread]);
|
||||||
BOOL success = NO;
|
BOOL success = NO;
|
||||||
_run = YES;
|
_run = YES;
|
||||||
void (*handler)(int) = signal(SIGINT, _SignalHandler);
|
void (*termHandler)(int) = signal(SIGTERM, _SignalHandler);
|
||||||
if (handler != SIG_ERR) {
|
void (*intHandler)(int) = signal(SIGINT, _SignalHandler);
|
||||||
if ([self startWithOptions:options]) {
|
if ((termHandler != SIG_ERR) && (intHandler != SIG_ERR)) {
|
||||||
|
if ([self startWithOptions:options error:error]) {
|
||||||
while (_run) {
|
while (_run) {
|
||||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
|
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
|
||||||
}
|
}
|
||||||
[self stop];
|
[self stop];
|
||||||
success = YES;
|
success = YES;
|
||||||
}
|
}
|
||||||
signal(SIGINT, handler);
|
_ExecuteMainThreadRunLoopSources();
|
||||||
|
signal(SIGINT, intHandler);
|
||||||
|
signal(SIGTERM, termHandler);
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
@@ -722,10 +776,10 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
@implementation GCDWebServer (GETHandlers)
|
@implementation GCDWebServer (GETHandlers)
|
||||||
|
|
||||||
- (void)addGETHandlerForPath:(NSString*)path staticData:(NSData*)staticData contentType:(NSString*)contentType cacheAge:(NSUInteger)cacheAge {
|
- (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) {
|
[self addHandlerForMethod:@"GET" path:path requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||||
|
|
||||||
|
GCDWebServerResponse* response = [GCDWebServerDataResponse responseWithData:staticData contentType:contentType];
|
||||||
|
response.cacheControlMaxAge = cacheAge;
|
||||||
return response;
|
return response;
|
||||||
|
|
||||||
}];
|
}];
|
||||||
@@ -842,37 +896,33 @@ static inline NSString* _EncodeBase64(NSString* string) {
|
|||||||
- (void)logVerbose:(NSString*)format, ... {
|
- (void)logVerbose:(NSString*)format, ... {
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
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);
|
va_end(arguments);
|
||||||
LOG_VERBOSE(@"%@", message);
|
|
||||||
ARC_RELEASE(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)logInfo:(NSString*)format, ... {
|
- (void)logInfo:(NSString*)format, ... {
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
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);
|
va_end(arguments);
|
||||||
LOG_INFO(@"%@", message);
|
|
||||||
ARC_RELEASE(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)logWarning:(NSString*)format, ... {
|
- (void)logWarning:(NSString*)format, ... {
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
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);
|
va_end(arguments);
|
||||||
LOG_WARNING(@"%@", message);
|
|
||||||
ARC_RELEASE(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)logError:(NSString*)format, ... {
|
- (void)logError:(NSString*)format, ... {
|
||||||
va_list arguments;
|
va_list arguments;
|
||||||
va_start(arguments, format);
|
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);
|
va_end(arguments);
|
||||||
LOG_ERROR(@"%@", message);
|
}
|
||||||
ARC_RELEASE(message);
|
|
||||||
|
- (void)logException:(NSException*)exception {
|
||||||
|
LOG_EXCEPTION(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -915,7 +965,7 @@ static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData,
|
|||||||
while (1) {
|
while (1) {
|
||||||
ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length);
|
ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
length = NSNotFound;
|
length = NSUIntegerMax;
|
||||||
break;
|
break;
|
||||||
} else if (result == 0) {
|
} else if (result == 0) {
|
||||||
break;
|
break;
|
||||||
@@ -925,7 +975,7 @@ static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData,
|
|||||||
outData.length = 2 * outData.length;
|
outData.length = 2 * outData.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (length != NSNotFound) {
|
if (length != NSUIntegerMax) {
|
||||||
outData.length = length;
|
outData.length = length;
|
||||||
response = _CreateHTTPMessageFromData(outData, NO);
|
response = _CreateHTTPMessageFromData(outData, NO);
|
||||||
} else {
|
} else {
|
||||||
@@ -949,9 +999,11 @@ static void _LogResult(NSString* format, ...) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (NSInteger)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path {
|
- (NSInteger)runTestsWithOptions:(NSDictionary*)options inDirectory:(NSString*)path {
|
||||||
|
DCHECK([NSThread isMainThread]);
|
||||||
NSArray* ignoredHeaders = @[@"Date", @"Etag"]; // Dates are always different by definition and ETags depend on file system node IDs
|
NSArray* ignoredHeaders = @[@"Date", @"Etag"]; // Dates are always different by definition and ETags depend on file system node IDs
|
||||||
NSInteger result = -1;
|
NSInteger result = -1;
|
||||||
if ([self startWithOptions:options]) {
|
if ([self startWithOptions:options error:NULL]) {
|
||||||
|
_ExecuteMainThreadRunLoopSources();
|
||||||
|
|
||||||
result = 0;
|
result = 0;
|
||||||
NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
|
NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
|
||||||
@@ -1007,8 +1059,13 @@ static void _LogResult(NSString* format, ...) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSString* expectedContentLength = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(expectedResponse, CFSTR("Content-Length")));
|
||||||
NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse));
|
NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse));
|
||||||
|
NSString* actualContentLength = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyHeaderFieldValue(actualResponse, CFSTR("Content-Length")));
|
||||||
NSData* actualBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(actualResponse));
|
NSData* actualBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(actualResponse));
|
||||||
|
if ([actualContentLength isEqualToString:expectedContentLength] && (actualBody.length > expectedBody.length)) { // Handle web browser closing connection before retrieving entire body (e.g. when playing a video file)
|
||||||
|
actualBody = [actualBody subdataWithRange:NSMakeRange(0, expectedBody.length)];
|
||||||
|
}
|
||||||
if (![actualBody isEqualToData:expectedBody]) {
|
if (![actualBody isEqualToData:expectedBody]) {
|
||||||
_LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length);
|
_LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length);
|
||||||
success = NO;
|
success = NO;
|
||||||
@@ -1049,9 +1106,12 @@ static void _LogResult(NSString* format, ...) {
|
|||||||
++result;
|
++result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ExecuteMainThreadRunLoopSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
[self stop];
|
[self stop];
|
||||||
|
|
||||||
|
_ExecuteMainThreadRunLoopSources();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,24 +29,145 @@
|
|||||||
|
|
||||||
@class GCDWebServerHandler;
|
@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
|
@interface GCDWebServerConnection : NSObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the GCDWebServer that owns the connection.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) GCDWebServer* server;
|
@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) 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;
|
@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;
|
@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;
|
@property(nonatomic, readonly) NSUInteger totalBytesWritten;
|
||||||
|
|
||||||
@end
|
@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)
|
@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
|
* This method is called when the connection is opened.
|
||||||
- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request; // Called before request is processed to return an override response bypassing processing or nil to continue - Default implementation checks authentication if applicable
|
*
|
||||||
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block; // Only called if the request can be processed
|
* Return NO to reject the connection e.g. after validating the local
|
||||||
- (GCDWebServerResponse*)overrideResponse:(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
|
* or remote address.
|
||||||
- (void)abortRequest:(GCDWebServerRequest*)request withStatusCode:(NSInteger)statusCode; // If request headers were malformed, "request" will be nil
|
*/
|
||||||
|
- (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.
|
||||||
|
*/
|
||||||
|
- (GCDWebServerResponse*)processRequest:(GCDWebServerRequest*)request withBlock:(GCDWebServerProcessBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assuming a valid HTTP request was received and either -preflightRequest:
|
||||||
|
* or -processRequest:withBlock: 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;
|
- (void)close;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
|||||||
if (_response.contentType != nil) {
|
if (_response.contentType != nil) {
|
||||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType));
|
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Type"), (ARC_BRIDGE CFStringRef)GCDWebServerNormalizeHeaderValue(_response.contentType));
|
||||||
}
|
}
|
||||||
if (_response.contentLength != NSNotFound) {
|
if (_response.contentLength != NSUIntegerMax) {
|
||||||
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]);
|
CFHTTPMessageSetHeaderFieldValue(_responseMessage, CFSTR("Content-Length"), (ARC_BRIDGE CFStringRef)[NSString stringWithFormat:@"%lu", (unsigned long)_response.contentLength]);
|
||||||
}
|
}
|
||||||
if (_response.usesChunkedTransferEncoding) {
|
if (_response.usesChunkedTransferEncoding) {
|
||||||
@@ -522,11 +522,15 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
|
|||||||
requestMethod = @"GET";
|
requestMethod = @"GET";
|
||||||
_virtualHEAD = YES;
|
_virtualHEAD = YES;
|
||||||
}
|
}
|
||||||
|
NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage)); // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones
|
||||||
NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(_requestMessage));
|
NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(_requestMessage));
|
||||||
|
if (requestURL) {
|
||||||
|
requestURL = [self rewriteRequestURL:requestURL withMethod:requestMethod headers:requestHeaders];
|
||||||
|
DCHECK(requestURL);
|
||||||
|
}
|
||||||
NSString* requestPath = requestURL ? GCDWebServerUnescapeURLString(ARC_BRIDGE_RELEASE(CFURLCopyPath((CFURLRef)requestURL))) : nil; // Don't use -[NSURL path] which strips the ending slash
|
NSString* requestPath = requestURL ? GCDWebServerUnescapeURLString(ARC_BRIDGE_RELEASE(CFURLCopyPath((CFURLRef)requestURL))) : nil; // Don't use -[NSURL path] which strips the ending slash
|
||||||
NSString* queryString = requestURL ? ARC_BRIDGE_RELEASE(CFURLCopyQueryString((CFURLRef)requestURL, NULL)) : nil; // Don't use -[NSURL query] to make sure query is not unescaped;
|
NSString* queryString = requestURL ? ARC_BRIDGE_RELEASE(CFURLCopyQueryString((CFURLRef)requestURL, NULL)) : nil; // Don't use -[NSURL query] to make sure query is not unescaped;
|
||||||
NSDictionary* requestQuery = queryString ? GCDWebServerParseURLEncodedForm(queryString) : @{};
|
NSDictionary* requestQuery = queryString ? GCDWebServerParseURLEncodedForm(queryString) : @{};
|
||||||
NSDictionary* requestHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(_requestMessage)); // Header names are case-insensitive but CFHTTPMessageCopyAllHeaderFields() will standardize the common ones
|
|
||||||
if (requestMethod && requestURL && requestHeaders && requestPath && requestQuery) {
|
if (requestMethod && requestURL && requestHeaders && requestPath && requestQuery) {
|
||||||
for (_handler in _server.handlers) {
|
for (_handler in _server.handlers) {
|
||||||
_request = ARC_RETAIN(_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery));
|
_request = ARC_RETAIN(_handler.matchBlock(requestMethod, requestURL, requestHeaders, requestPath, requestQuery));
|
||||||
@@ -675,11 +679,11 @@ static NSString* _StringFromAddressData(NSData* data) {
|
|||||||
_connectionIndex = OSAtomicIncrement32(&_connectionCounter);
|
_connectionIndex = OSAtomicIncrement32(&_connectionCounter);
|
||||||
|
|
||||||
_requestPath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
|
_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);
|
DCHECK(_requestFD > 0);
|
||||||
|
|
||||||
_responsePath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
|
_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);
|
DCHECK(_responseFD > 0);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -713,6 +717,10 @@ static NSString* _StringFromAddressData(NSData* data) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSURL*)rewriteRequestURL:(NSURL*)url withMethod:(NSString*)method headers:(NSDictionary*)headers {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc2617
|
// https://tools.ietf.org/html/rfc2617
|
||||||
- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request {
|
- (GCDWebServerResponse*)preflightRequest:(GCDWebServerRequest*)request {
|
||||||
LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
|
LOG_DEBUG(@"Connection on socket %i preflighting request \"%@ %@\" with %lu bytes body", _socket, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead);
|
||||||
|
|||||||
@@ -31,14 +31,69 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a file extension to the corresponding MIME type.
|
||||||
|
* If there is no match, "application/octet-stream" is returned.
|
||||||
|
*/
|
||||||
NSString* GCDWebServerGetMimeTypeForExtension(NSString* extension);
|
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);
|
NSString* GCDWebServerEscapeURLString(NSString* string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unescapes a URL percent-encoded string.
|
||||||
|
*/
|
||||||
NSString* GCDWebServerUnescapeURLString(NSString* 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);
|
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);
|
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);
|
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);
|
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);
|
NSDate* GCDWebServerParseISO8601(NSString* string);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ static NSDateFormatter* _dateFormatterRFC822 = nil;
|
|||||||
static NSDateFormatter* _dateFormatterISO8601 = nil;
|
static NSDateFormatter* _dateFormatterISO8601 = nil;
|
||||||
static dispatch_queue_t _dateFormatterQueue = NULL;
|
static dispatch_queue_t _dateFormatterQueue = NULL;
|
||||||
|
|
||||||
// HTTP/1.1 server must use RFC822
|
// TODO: Handle RFC 850 and ANSI C's asctime() format
|
||||||
// TODO: Handle RFC 850 and ANSI C's asctime() format (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3)
|
|
||||||
void GCDWebServerInitializeFunctions() {
|
void GCDWebServerInitializeFunctions() {
|
||||||
DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread
|
DCHECK([NSThread isMainThread]); // NSDateFormatter should be initialized on main thread
|
||||||
if (_dateFormatterRFC822 == nil) {
|
if (_dateFormatterRFC822 == nil) {
|
||||||
@@ -187,7 +186,6 @@ NSString* GCDWebServerUnescapeURLString(NSString* string) {
|
|||||||
return ARC_BRIDGE_RELEASE(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8));
|
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) {
|
NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
|
||||||
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
|
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
|
||||||
NSScanner* scanner = [[NSScanner alloc] initWithString:form];
|
NSScanner* scanner = [[NSScanner alloc] initWithString:form];
|
||||||
@@ -200,8 +198,9 @@ NSDictionary* GCDWebServerParseURLEncodedForm(NSString* form) {
|
|||||||
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
[scanner setScanLocation:([scanner scanLocation] + 1)];
|
||||||
|
|
||||||
NSString* value = nil;
|
NSString* value = nil;
|
||||||
if (![scanner scanUpToString:@"&" intoString:&value]) {
|
[scanner scanUpToString:@"&" intoString:&value];
|
||||||
break;
|
if (value == nil) {
|
||||||
|
value = @"";
|
||||||
}
|
}
|
||||||
|
|
||||||
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
key = [key stringByReplacingOccurrencesOfString:@"+" withString:@" "];
|
||||||
|
|||||||
@@ -30,12 +30,18 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "informational" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerInformationalHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerInformationalHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_Continue = 100,
|
kGCDWebServerHTTPStatusCode_Continue = 100,
|
||||||
kGCDWebServerHTTPStatusCode_SwitchingProtocols = 101,
|
kGCDWebServerHTTPStatusCode_SwitchingProtocols = 101,
|
||||||
kGCDWebServerHTTPStatusCode_Processing = 102
|
kGCDWebServerHTTPStatusCode_Processing = 102
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "successful" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_OK = 200,
|
kGCDWebServerHTTPStatusCode_OK = 200,
|
||||||
kGCDWebServerHTTPStatusCode_Created = 201,
|
kGCDWebServerHTTPStatusCode_Created = 201,
|
||||||
@@ -48,6 +54,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerSuccessfulHTTPStatusCode) {
|
|||||||
kGCDWebServerHTTPStatusCode_AlreadyReported = 208
|
kGCDWebServerHTTPStatusCode_AlreadyReported = 208
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "redirection" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_MultipleChoices = 300,
|
kGCDWebServerHTTPStatusCode_MultipleChoices = 300,
|
||||||
kGCDWebServerHTTPStatusCode_MovedPermanently = 301,
|
kGCDWebServerHTTPStatusCode_MovedPermanently = 301,
|
||||||
@@ -59,6 +68,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerRedirectionHTTPStatusCode) {
|
|||||||
kGCDWebServerHTTPStatusCode_PermanentRedirect = 308
|
kGCDWebServerHTTPStatusCode_PermanentRedirect = 308
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "client error" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_BadRequest = 400,
|
kGCDWebServerHTTPStatusCode_BadRequest = 400,
|
||||||
kGCDWebServerHTTPStatusCode_Unauthorized = 401,
|
kGCDWebServerHTTPStatusCode_Unauthorized = 401,
|
||||||
@@ -87,6 +99,9 @@ typedef NS_ENUM(NSInteger, GCDWebServerClientErrorHTTPStatusCode) {
|
|||||||
kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431
|
kGCDWebServerHTTPStatusCode_RequestHeaderFieldsTooLarge = 431
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience constants for "server error" HTTP status codes.
|
||||||
|
*/
|
||||||
typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) {
|
typedef NS_ENUM(NSInteger, GCDWebServerServerErrorHTTPStatusCode) {
|
||||||
kGCDWebServerHTTPStatusCode_InternalServerError = 500,
|
kGCDWebServerHTTPStatusCode_InternalServerError = 500,
|
||||||
kGCDWebServerHTTPStatusCode_NotImplemented = 501,
|
kGCDWebServerHTTPStatusCode_NotImplemented = 501,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
#import "GCDWebServerDataResponse.h"
|
#import "GCDWebServerDataResponse.h"
|
||||||
#import "GCDWebServerErrorResponse.h"
|
#import "GCDWebServerErrorResponse.h"
|
||||||
#import "GCDWebServerFileResponse.h"
|
#import "GCDWebServerFileResponse.h"
|
||||||
#import "GCDWebServerStreamingResponse.h"
|
#import "GCDWebServerStreamedResponse.h"
|
||||||
|
|
||||||
#ifdef __GCDWEBSERVER_LOGGING_HEADER__
|
#ifdef __GCDWEBSERVER_LOGGING_HEADER__
|
||||||
|
|
||||||
@@ -111,7 +111,11 @@ extern void GCDLogMessage(GCDWebServerLogLevel level, NSString* format, ...) NS_
|
|||||||
#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
|
#define kGCDWebServerErrorDomain @"GCDWebServerErrorDomain"
|
||||||
|
|
||||||
static inline BOOL GCDWebServerIsValidByteRange(NSRange range) {
|
static inline BOOL GCDWebServerIsValidByteRange(NSRange range) {
|
||||||
return ((range.location != NSNotFound) || (range.length > 0));
|
return ((range.location != NSUIntegerMax) || (range.length > 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline NSError* GCDWebServerMakePosixError(int code) {
|
||||||
|
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithUTF8String:strerror(code)]}];
|
||||||
}
|
}
|
||||||
|
|
||||||
extern void GCDWebServerInitializeFunctions();
|
extern void GCDWebServerInitializeFunctions();
|
||||||
|
|||||||
@@ -27,25 +27,140 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
@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
|
@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>
|
@interface GCDWebServerRequest : NSObject <GCDWebServerBodyWriter>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the HTTP method for the request.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* method;
|
@property(nonatomic, readonly) NSString* method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL for the request.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSURL* URL;
|
@property(nonatomic, readonly) NSURL* URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the HTTP headers for the request.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSDictionary* headers;
|
@property(nonatomic, readonly) NSDictionary* headers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path component of the URL for the request.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* path;
|
@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)
|
* Returns the parsed and unescaped query component of the URL for the request.
|
||||||
@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)
|
* @warning This property will be nil if there is no query in the URL.
|
||||||
@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
|
@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;
|
- (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;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -187,14 +187,14 @@
|
|||||||
if (_type == nil) {
|
if (_type == nil) {
|
||||||
_type = kGCDWebServerDefaultMimeType;
|
_type = kGCDWebServerDefaultMimeType;
|
||||||
}
|
}
|
||||||
_length = NSNotFound;
|
_length = NSUIntegerMax;
|
||||||
} else {
|
} else {
|
||||||
if (_type) {
|
if (_type) {
|
||||||
DNOT_REACHED();
|
DNOT_REACHED();
|
||||||
ARC_RELEASE(self);
|
ARC_RELEASE(self);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
_length = NSNotFound;
|
_length = NSUIntegerMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
|
NSString* modifiedHeader = [_headers objectForKey:@"If-Modified-Since"];
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
}
|
}
|
||||||
_noneMatch = ARC_RETAIN([_headers objectForKey:@"If-None-Match"]);
|
_noneMatch = ARC_RETAIN([_headers objectForKey:@"If-None-Match"]);
|
||||||
|
|
||||||
_range = NSMakeRange(NSNotFound, 0);
|
_range = NSMakeRange(NSUIntegerMax, 0);
|
||||||
NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
|
NSString* rangeHeader = GCDWebServerNormalizeHeaderValue([_headers objectForKey:@"Range"]);
|
||||||
if (rangeHeader) {
|
if (rangeHeader) {
|
||||||
if ([rangeHeader hasPrefix:@"bytes="]) {
|
if ([rangeHeader hasPrefix:@"bytes="]) {
|
||||||
@@ -222,13 +222,13 @@
|
|||||||
_range.location = startValue;
|
_range.location = startValue;
|
||||||
_range.length = NSUIntegerMax;
|
_range.length = NSUIntegerMax;
|
||||||
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
|
} else if (endString.length && (endValue > 0)) { // The final 500 bytes: "-500"
|
||||||
_range.location = NSNotFound;
|
_range.location = NSUIntegerMax;
|
||||||
_range.length = endValue;
|
_range.length = endValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((_range.location == NSNotFound) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
|
if ((_range.location == NSUIntegerMax) && (_range.length == 0)) { // Ignore "Range" header if syntactically invalid
|
||||||
LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
|
LOG_WARNING(@"Failed to parse 'Range' header \"%@\" for url: %@", rangeHeader, url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,29 +27,163 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#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>
|
@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;
|
- (void)close;
|
||||||
|
|
||||||
@end
|
@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>
|
@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
|
* Sets the content type for the body of the response.
|
||||||
@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
|
* The default value is nil i.e. the response has no body.
|
||||||
@property(nonatomic, copy) NSString* eTag; // Default is nil i.e. no "ETag" header
|
*
|
||||||
@property(nonatomic, getter=isGZipContentEncodingEnabled) BOOL gzipContentEncodingEnabled; // Default is disabled
|
* @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;
|
+ (instancetype)response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
- (instancetype)init;
|
- (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
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerResponse (Extensions)
|
@interface GCDWebServerResponse (Extensions)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a empty response with a specific HTTP status code.
|
||||||
|
*/
|
||||||
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode;
|
+ (instancetype)responseWithStatusCode:(NSInteger)statusCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an HTTP redirect response to a new URL.
|
||||||
|
*/
|
||||||
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
+ (instancetype)responseWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an empty response with a specific HTTP status code.
|
||||||
|
*/
|
||||||
- (instancetype)initWithStatusCode:(NSInteger)statusCode;
|
- (instancetype)initWithStatusCode:(NSInteger)statusCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an HTTP redirect response to a new URL.
|
||||||
|
*/
|
||||||
- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
- (instancetype)initWithRedirect:(NSURL*)location permanent:(BOOL)permanent;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
|
|
||||||
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
- (id)initWithResponse:(GCDWebServerResponse*)response reader:(id<GCDWebServerBodyReader>)reader {
|
||||||
if ((self = [super initWithResponse:response reader:reader])) {
|
if ((self = [super initWithResponse:response reader:reader])) {
|
||||||
response.contentLength = NSNotFound; // Make sure "Content-Length" header is not set since we don't know it (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"];
|
[response setValue:@"gzip" forAdditionalHeader:@"Content-Encoding"];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
- (instancetype)init {
|
- (instancetype)init {
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_type = nil;
|
_type = nil;
|
||||||
_length = NSNotFound;
|
_length = NSUIntegerMax;
|
||||||
_status = kGCDWebServerHTTPStatusCode_OK;
|
_status = kGCDWebServerHTTPStatusCode_OK;
|
||||||
_maxAge = 0;
|
_maxAge = 0;
|
||||||
_headers = [[NSMutableDictionary alloc] init];
|
_headers = [[NSMutableDictionary alloc] init];
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)usesChunkedTransferEncoding {
|
- (BOOL)usesChunkedTransferEncoding {
|
||||||
return (_type != nil) && (_length == NSNotFound);
|
return (_type != nil) && (_length == NSUIntegerMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
if (_type) {
|
if (_type) {
|
||||||
[description appendFormat:@"\nContent Type = %@", _type];
|
[description appendFormat:@"\nContent Type = %@", _type];
|
||||||
}
|
}
|
||||||
if (_length != NSNotFound) {
|
if (_length != NSUIntegerMax) {
|
||||||
[description appendFormat:@"\nContent Length = %lu", (unsigned long)_length];
|
[description appendFormat:@"\nContent Length = %lu", (unsigned long)_length];
|
||||||
}
|
}
|
||||||
[description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge];
|
[description appendFormat:@"\nCache Control Max Age = %lu", (unsigned long)_maxAge];
|
||||||
|
|||||||
@@ -27,11 +27,34 @@
|
|||||||
|
|
||||||
#import "GCDWebServerRequest.h"
|
#import "GCDWebServerRequest.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerDataRequest subclass of GCDWebServerRequest stores the body
|
||||||
|
* of the HTTP request in memory.
|
||||||
|
*/
|
||||||
@interface GCDWebServerDataRequest : GCDWebServerRequest
|
@interface GCDWebServerDataRequest : GCDWebServerRequest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data for the request body.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSData* data;
|
@property(nonatomic, readonly) NSData* data;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerDataRequest (Extensions)
|
@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
|
@end
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
if (self.contentLength != NSNotFound) {
|
if (self.contentLength != NSUIntegerMax) {
|
||||||
_data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
|
_data = [[NSMutableData alloc] initWithCapacity:self.contentLength];
|
||||||
} else {
|
} else {
|
||||||
_data = [[NSMutableData alloc] init];
|
_data = [[NSMutableData alloc] init];
|
||||||
@@ -97,7 +97,8 @@
|
|||||||
|
|
||||||
- (id)jsonObject {
|
- (id)jsonObject {
|
||||||
if (_jsonObject == nil) {
|
if (_jsonObject == nil) {
|
||||||
if ([self.contentType isEqualToString:@"application/json"] || [self.contentType isEqualToString:@"text/json"] || [self.contentType isEqualToString:@"text/javascript"]) {
|
NSString* mimeType = GCDWebServerTruncateHeaderValue(self.contentType);
|
||||||
|
if ([mimeType isEqualToString:@"application/json"] || [mimeType isEqualToString:@"text/json"] || [mimeType isEqualToString:@"text/javascript"]) {
|
||||||
_jsonObject = ARC_RETAIN([NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL]);
|
_jsonObject = ARC_RETAIN([NSJSONSerialization JSONObjectWithData:_data options:0 error:NULL]);
|
||||||
} else {
|
} else {
|
||||||
DNOT_REACHED();
|
DNOT_REACHED();
|
||||||
|
|||||||
@@ -27,6 +27,19 @@
|
|||||||
|
|
||||||
#import "GCDWebServerRequest.h"
|
#import "GCDWebServerRequest.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerFileRequest subclass of GCDWebServerRequest stores the body
|
||||||
|
* of the HTTP request to a file on disk.
|
||||||
|
*/
|
||||||
@interface GCDWebServerFileRequest : GCDWebServerRequest
|
@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;
|
@property(nonatomic, readonly) NSString* temporaryPath;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -34,10 +34,6 @@
|
|||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
static inline NSError* _MakePosixError(int code) {
|
|
||||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation GCDWebServerFileRequest
|
@implementation GCDWebServerFileRequest
|
||||||
|
|
||||||
@synthesize temporaryPath=_temporaryPath;
|
@synthesize temporaryPath=_temporaryPath;
|
||||||
@@ -59,7 +55,7 @@ static inline NSError* _MakePosixError(int code) {
|
|||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
_file = open([_temporaryPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
_file = open([_temporaryPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
if (_file <= 0) {
|
if (_file <= 0) {
|
||||||
*error = _MakePosixError(errno);
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
@@ -67,7 +63,7 @@ static inline NSError* _MakePosixError(int code) {
|
|||||||
|
|
||||||
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||||
if (write(_file, data.bytes, data.length) != (ssize_t)data.length) {
|
if (write(_file, data.bytes, data.length) != (ssize_t)data.length) {
|
||||||
*error = _MakePosixError(errno);
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
@@ -75,7 +71,7 @@ static inline NSError* _MakePosixError(int code) {
|
|||||||
|
|
||||||
- (BOOL)close:(NSError**)error {
|
- (BOOL)close:(NSError**)error {
|
||||||
if (close(_file) < 0) {
|
if (close(_file) < 0) {
|
||||||
*error = _MakePosixError(errno);
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
#ifdef __GCDWEBSERVER_ENABLE_TESTING__
|
||||||
|
|||||||
@@ -27,23 +27,106 @@
|
|||||||
|
|
||||||
#import "GCDWebServerRequest.h"
|
#import "GCDWebServerRequest.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerMultiPart class is an abstract class that wraps the content
|
||||||
|
* of a part.
|
||||||
|
*/
|
||||||
@interface GCDWebServerMultiPart : NSObject
|
@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;
|
@property(nonatomic, readonly) NSString* mimeType;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerMultiPartArgument subclass of GCDWebServerMultiPart wraps
|
||||||
|
* the content of a part as data in memory.
|
||||||
|
*/
|
||||||
@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart
|
@interface GCDWebServerMultiPartArgument : GCDWebServerMultiPart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data for the part.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSData* data;
|
@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
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerMultiPartFile subclass of GCDWebServerMultiPart wraps
|
||||||
|
* the content of a part as a file on disk.
|
||||||
|
*/
|
||||||
@interface GCDWebServerMultiPartFile : GCDWebServerMultiPart
|
@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;
|
@property(nonatomic, readonly) NSString* temporaryPath;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerMultiPartFormRequest subclass of GCDWebServerRequest
|
||||||
|
* parses the body of the HTTP request as a multipart encoded form.
|
||||||
|
*/
|
||||||
@interface GCDWebServerMultiPartFormRequest : GCDWebServerRequest
|
@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;
|
+ (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
|
@end
|
||||||
|
|||||||
@@ -29,13 +29,19 @@
|
|||||||
|
|
||||||
#define kMultiPartBufferSize (256 * 1024)
|
#define kMultiPartBufferSize (256 * 1024)
|
||||||
|
|
||||||
enum {
|
typedef enum {
|
||||||
kParserState_Undefined = 0,
|
kParserState_Undefined = 0,
|
||||||
kParserState_Start,
|
kParserState_Start,
|
||||||
kParserState_Headers,
|
kParserState_Headers,
|
||||||
kParserState_Content,
|
kParserState_Content,
|
||||||
kParserState_End
|
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* _newlineData = nil;
|
||||||
static NSData* _newlinesData = nil;
|
static NSData* _newlinesData = nil;
|
||||||
@@ -43,6 +49,7 @@ static NSData* _dashNewlineData = nil;
|
|||||||
|
|
||||||
@interface GCDWebServerMultiPart () {
|
@interface GCDWebServerMultiPart () {
|
||||||
@private
|
@private
|
||||||
|
NSString* _controlName;
|
||||||
NSString* _contentType;
|
NSString* _contentType;
|
||||||
NSString* _mimeType;
|
NSString* _mimeType;
|
||||||
}
|
}
|
||||||
@@ -50,17 +57,19 @@ static NSData* _dashNewlineData = nil;
|
|||||||
|
|
||||||
@implementation GCDWebServerMultiPart
|
@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])) {
|
if ((self = [super init])) {
|
||||||
_contentType = [contentType copy];
|
_controlName = [name copy];
|
||||||
|
_contentType = [type copy];
|
||||||
_mimeType = ARC_RETAIN(GCDWebServerTruncateHeaderValue(_contentType));
|
_mimeType = ARC_RETAIN(GCDWebServerTruncateHeaderValue(_contentType));
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
|
ARC_RELEASE(_controlName);
|
||||||
ARC_RELEASE(_contentType);
|
ARC_RELEASE(_contentType);
|
||||||
ARC_RELEASE(_mimeType);
|
ARC_RELEASE(_mimeType);
|
||||||
|
|
||||||
@@ -80,8 +89,8 @@ static NSData* _dashNewlineData = nil;
|
|||||||
|
|
||||||
@synthesize data=_data, string=_string;
|
@synthesize data=_data, string=_string;
|
||||||
|
|
||||||
- (id)initWithContentType:(NSString*)contentType data:(NSData*)data {
|
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type data:(NSData*)data {
|
||||||
if ((self = [super initWithContentType:contentType])) {
|
if ((self = [super initWithControlName:name contentType:type])) {
|
||||||
_data = ARC_RETAIN(data);
|
_data = ARC_RETAIN(data);
|
||||||
|
|
||||||
if ([self.contentType hasPrefix:@"text/"]) {
|
if ([self.contentType hasPrefix:@"text/"]) {
|
||||||
@@ -116,8 +125,8 @@ static NSData* _dashNewlineData = nil;
|
|||||||
|
|
||||||
@synthesize fileName=_fileName, temporaryPath=_temporaryPath;
|
@synthesize fileName=_fileName, temporaryPath=_temporaryPath;
|
||||||
|
|
||||||
- (id)initWithContentType:(NSString*)contentType fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath {
|
- (id)initWithControlName:(NSString*)name contentType:(NSString*)type fileName:(NSString*)fileName temporaryPath:(NSString*)temporaryPath {
|
||||||
if ((self = [super initWithContentType:contentType])) {
|
if ((self = [super initWithControlName:name contentType:type])) {
|
||||||
_fileName = [fileName copy];
|
_fileName = [fileName copy];
|
||||||
_temporaryPath = [temporaryPath copy];
|
_temporaryPath = [temporaryPath copy];
|
||||||
}
|
}
|
||||||
@@ -139,26 +148,25 @@ static NSData* _dashNewlineData = nil;
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerMultiPartFormRequest () {
|
@interface GCDWebServerMIMEStreamParser () {
|
||||||
@private
|
@private
|
||||||
NSData* _boundary;
|
NSData* _boundary;
|
||||||
|
NSString* _defaultcontrolName;
|
||||||
|
ParserState _state;
|
||||||
|
NSMutableData* _data;
|
||||||
|
NSMutableArray* _arguments;
|
||||||
|
NSMutableArray* _files;
|
||||||
|
|
||||||
NSUInteger _parserState;
|
|
||||||
NSMutableData* _parserData;
|
|
||||||
NSString* _controlName;
|
NSString* _controlName;
|
||||||
NSString* _fileName;
|
NSString* _fileName;
|
||||||
NSString* _contentType;
|
NSString* _contentType;
|
||||||
NSString* _tmpPath;
|
NSString* _tmpPath;
|
||||||
int _tmpFile;
|
int _tmpFile;
|
||||||
|
GCDWebServerMIMEStreamParser* _subParser;
|
||||||
NSMutableDictionary* _arguments;
|
|
||||||
NSMutableDictionary* _files;
|
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GCDWebServerMultiPartFormRequest
|
@implementation GCDWebServerMIMEStreamParser
|
||||||
|
|
||||||
@synthesize arguments=_arguments, files=_files;
|
|
||||||
|
|
||||||
+ (void)initialize {
|
+ (void)initialize {
|
||||||
if (_newlineData == nil) {
|
if (_newlineData == nil) {
|
||||||
@@ -175,49 +183,50 @@ static NSData* _dashNewlineData = nil;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (NSString*)mimeType {
|
- (id)initWithBoundary:(NSString*)boundary defaultControlName:(NSString*)name arguments:(NSMutableArray*)arguments files:(NSMutableArray*)files {
|
||||||
return @"multipart/form-data";
|
NSData* data = boundary.length ? [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding] : nil;
|
||||||
}
|
if (data == nil) {
|
||||||
|
DNOT_REACHED();
|
||||||
- (instancetype)initWithMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers path:(NSString*)path query:(NSDictionary*)query {
|
ARC_RELEASE(self);
|
||||||
if ((self = [super initWithMethod:method url:url headers:headers path:path query:query])) {
|
return nil;
|
||||||
NSString* boundary = GCDWebServerExtractHeaderValueParameter(self.contentType, @"boundary");
|
}
|
||||||
if (boundary) {
|
if ((self = [super init])) {
|
||||||
NSData* data = [[NSString stringWithFormat:@"--%@", boundary] dataUsingEncoding:NSASCIIStringEncoding];
|
_boundary = ARC_RETAIN(data);
|
||||||
_boundary = ARC_RETAIN(data);
|
_defaultcontrolName = ARC_RETAIN(name);
|
||||||
}
|
_arguments = ARC_RETAIN(arguments);
|
||||||
if (_boundary == nil) {
|
_files = ARC_RETAIN(files);
|
||||||
DNOT_REACHED();
|
_data = [[NSMutableData alloc] initWithCapacity:kMultiPartBufferSize];
|
||||||
ARC_RELEASE(self);
|
_state = kParserState_Start;
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
_arguments = [[NSMutableDictionary alloc] init];
|
|
||||||
_files = [[NSMutableDictionary alloc] init];
|
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
|
ARC_RELEASE(_boundary);
|
||||||
|
ARC_RELEASE(_defaultcontrolName);
|
||||||
|
ARC_RELEASE(_data);
|
||||||
ARC_RELEASE(_arguments);
|
ARC_RELEASE(_arguments);
|
||||||
ARC_RELEASE(_files);
|
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);
|
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
|
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2
|
||||||
- (BOOL)_parseData {
|
- (BOOL)_parseData {
|
||||||
BOOL success = YES;
|
BOOL success = YES;
|
||||||
|
|
||||||
if (_parserState == kParserState_Headers) {
|
if (_state == kParserState_Headers) {
|
||||||
NSRange range = [_parserData rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _parserData.length)];
|
NSRange range = [_data rangeOfData:_newlinesData options:0 range:NSMakeRange(0, _data.length)];
|
||||||
if (range.location != NSNotFound) {
|
if (range.location != NSNotFound) {
|
||||||
|
|
||||||
ARC_RELEASE(_controlName);
|
ARC_RELEASE(_controlName);
|
||||||
@@ -228,7 +237,9 @@ static NSData* _dashNewlineData = nil;
|
|||||||
_contentType = nil;
|
_contentType = nil;
|
||||||
ARC_RELEASE(_tmpPath);
|
ARC_RELEASE(_tmpPath);
|
||||||
_tmpPath = nil;
|
_tmpPath = nil;
|
||||||
NSString* headers = [[NSString alloc] initWithData:[_parserData subdataWithRange:NSMakeRange(0, range.location)] encoding:NSUTF8StringEncoding];
|
ARC_RELEASE(_subParser);
|
||||||
|
_subParser = nil;
|
||||||
|
NSString* headers = [[NSString alloc] initWithData:[_data subdataWithRange:NSMakeRange(0, range.location)] encoding:NSUTF8StringEncoding];
|
||||||
if (headers) {
|
if (headers) {
|
||||||
for (NSString* header in [headers componentsSeparatedByString:@"\r\n"]) {
|
for (NSString* header in [headers componentsSeparatedByString:@"\r\n"]) {
|
||||||
NSRange subRange = [header rangeOfString:@":"];
|
NSRange subRange = [header rangeOfString:@":"];
|
||||||
@@ -242,6 +253,9 @@ static NSData* _dashNewlineData = nil;
|
|||||||
if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) {
|
if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"form-data"]) {
|
||||||
_controlName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"name"));
|
_controlName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"name"));
|
||||||
_fileName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename"));
|
_fileName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename"));
|
||||||
|
} else if ([GCDWebServerTruncateHeaderValue(contentDisposition) isEqualToString:@"file"]) {
|
||||||
|
_controlName = ARC_RETAIN(_defaultcontrolName);
|
||||||
|
_fileName = ARC_RETAIN(GCDWebServerExtractHeaderValueParameter(contentDisposition, @"filename"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -257,7 +271,14 @@ static NSData* _dashNewlineData = nil;
|
|||||||
DNOT_REACHED();
|
DNOT_REACHED();
|
||||||
}
|
}
|
||||||
if (_controlName) {
|
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]];
|
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);
|
_tmpFile = open([path fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||||
if (_tmpFile > 0) {
|
if (_tmpFile > 0) {
|
||||||
@@ -272,29 +293,36 @@ static NSData* _dashNewlineData = nil;
|
|||||||
success = NO;
|
success = NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
[_parserData replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0];
|
[_data replaceBytesInRange:NSMakeRange(0, range.location + range.length) withBytes:NULL length:0];
|
||||||
_parserState = kParserState_Content;
|
_state = kParserState_Content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((_parserState == kParserState_Start) || (_parserState == kParserState_Content)) {
|
if ((_state == kParserState_Start) || (_state == kParserState_Content)) {
|
||||||
NSRange range = [_parserData rangeOfData:_boundary options:0 range:NSMakeRange(0, _parserData.length)];
|
NSRange range = [_data rangeOfData:_boundary options:0 range:NSMakeRange(0, _data.length)];
|
||||||
if (range.location != NSNotFound) {
|
if (range.location != NSNotFound) {
|
||||||
NSRange subRange = NSMakeRange(range.location + range.length, _parserData.length - range.location - range.length);
|
NSRange subRange = NSMakeRange(range.location + range.length, _data.length - range.location - range.length);
|
||||||
NSRange subRange1 = [_parserData rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange];
|
NSRange subRange1 = [_data rangeOfData:_newlineData options:NSDataSearchAnchored range:subRange];
|
||||||
NSRange subRange2 = [_parserData rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange];
|
NSRange subRange2 = [_data rangeOfData:_dashNewlineData options:NSDataSearchAnchored range:subRange];
|
||||||
if ((subRange1.location != NSNotFound) || (subRange2.location != NSNotFound)) {
|
if ((subRange1.location != NSNotFound) || (subRange2.location != NSNotFound)) {
|
||||||
|
|
||||||
if (_parserState == kParserState_Content) {
|
if (_state == kParserState_Content) {
|
||||||
const void* dataBytes = _parserData.bytes;
|
const void* dataBytes = _data.bytes;
|
||||||
NSUInteger dataLength = range.location - 2;
|
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);
|
ssize_t result = write(_tmpFile, dataBytes, dataLength);
|
||||||
if (result == (ssize_t)dataLength) {
|
if (result == (ssize_t)dataLength) {
|
||||||
if (close(_tmpFile) == 0) {
|
if (close(_tmpFile) == 0) {
|
||||||
_tmpFile = 0;
|
_tmpFile = 0;
|
||||||
GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithContentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
|
GCDWebServerMultiPartFile* file = [[GCDWebServerMultiPartFile alloc] initWithControlName:_controlName contentType:_contentType fileName:_fileName temporaryPath:_tmpPath];
|
||||||
[_files setObject:file forKey:_controlName];
|
[_files addObject:file];
|
||||||
ARC_RELEASE(file);
|
ARC_RELEASE(file);
|
||||||
} else {
|
} else {
|
||||||
DNOT_REACHED();
|
DNOT_REACHED();
|
||||||
@@ -308,85 +336,150 @@ static NSData* _dashNewlineData = nil;
|
|||||||
_tmpPath = nil;
|
_tmpPath = nil;
|
||||||
} else {
|
} else {
|
||||||
NSData* data = [[NSData alloc] initWithBytes:(void*)dataBytes length:dataLength];
|
NSData* data = [[NSData alloc] initWithBytes:(void*)dataBytes length:dataLength];
|
||||||
GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithContentType:_contentType data:data];
|
GCDWebServerMultiPartArgument* argument = [[GCDWebServerMultiPartArgument alloc] initWithControlName:_controlName contentType:_contentType data:data];
|
||||||
[_arguments setObject:argument forKey:_controlName];
|
[_arguments addObject:argument];
|
||||||
ARC_RELEASE(argument);
|
ARC_RELEASE(argument);
|
||||||
ARC_RELEASE(data);
|
ARC_RELEASE(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subRange1.location != NSNotFound) {
|
if (subRange1.location != NSNotFound) {
|
||||||
[_parserData replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0];
|
[_data replaceBytesInRange:NSMakeRange(0, subRange1.location + subRange1.length) withBytes:NULL length:0];
|
||||||
_parserState = kParserState_Headers;
|
_state = kParserState_Headers;
|
||||||
success = [self _parseData];
|
success = [self _parseData];
|
||||||
} else {
|
} else {
|
||||||
_parserState = kParserState_End;
|
_state = kParserState_End;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
NSUInteger margin = 2 * _boundary.length;
|
NSUInteger margin = 2 * _boundary.length;
|
||||||
if (_tmpPath && (_parserData.length > margin)) {
|
if (_data.length > margin) {
|
||||||
NSUInteger length = _parserData.length - margin;
|
NSUInteger length = _data.length - margin;
|
||||||
ssize_t result = write(_tmpFile, _parserData.bytes, length);
|
if (_subParser) {
|
||||||
if (result == (ssize_t)length) {
|
if ([_subParser appendBytes:_data.bytes length:length]) {
|
||||||
[_parserData replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
[_data replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
|
||||||
} else {
|
} else {
|
||||||
DNOT_REACHED();
|
DNOT_REACHED();
|
||||||
success = NO;
|
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;
|
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 {
|
- (BOOL)writeData:(NSData*)data error:(NSError**)error {
|
||||||
[_parserData appendBytes:data.bytes length:data.length];
|
if (![_parser appendBytes:data.bytes length:data.length]) {
|
||||||
if (![self _parseData]) {
|
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed continuing to parse multipart form data"}];
|
||||||
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed parsing multipart form data"}];
|
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)close:(NSError**)error {
|
- (BOOL)close:(NSError**)error {
|
||||||
ARC_RELEASE(_parserData);
|
BOOL atEnd = [_parser isAtEnd];
|
||||||
_parserData = nil;
|
ARC_RELEASE(_parser);
|
||||||
ARC_RELEASE(_controlName);
|
_parser = nil;
|
||||||
_controlName = nil;
|
if (!atEnd) {
|
||||||
ARC_RELEASE(_fileName);
|
*error = [NSError errorWithDomain:kGCDWebServerErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Failed finishing to parse multipart form data"}];
|
||||||
_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"}];
|
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
return YES;
|
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 {
|
- (NSString*)description {
|
||||||
NSMutableString* description = [NSMutableString stringWithString:[super description]];
|
NSMutableString* description = [NSMutableString stringWithString:[super description]];
|
||||||
if (_arguments.count) {
|
if (_arguments.count) {
|
||||||
[description appendString:@"\n"];
|
[description appendString:@"\n"];
|
||||||
for (NSString* key in [[_arguments allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
for (GCDWebServerMultiPartArgument* argument in _arguments) {
|
||||||
GCDWebServerMultiPartArgument* argument = [_arguments objectForKey:key];
|
[description appendFormat:@"\n%@ (%@)\n", argument.controlName, argument.contentType];
|
||||||
[description appendFormat:@"\n%@ (%@)\n", key, argument.contentType];
|
|
||||||
[description appendString:GCDWebServerDescribeData(argument.data, argument.contentType)];
|
[description appendString:GCDWebServerDescribeData(argument.data, argument.contentType)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_files.count) {
|
if (_files.count) {
|
||||||
[description appendString:@"\n"];
|
[description appendString:@"\n"];
|
||||||
for (NSString* key in [[_files allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
for (GCDWebServerMultiPartFile* file in _files) {
|
||||||
GCDWebServerMultiPartFile* file = [_files objectForKey:key];
|
[description appendFormat:@"\n%@ (%@): %@\n{%@}", file.controlName, file.contentType, file.fileName, file.temporaryPath];
|
||||||
[description appendFormat:@"\n%@ (%@): %@\n{%@}", key, file.contentType, file.fileName, file.temporaryPath];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return description;
|
return description;
|
||||||
|
|||||||
@@ -27,7 +27,25 @@
|
|||||||
|
|
||||||
#import "GCDWebServerDataRequest.h"
|
#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
|
@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;
|
+ (NSString*)mimeType;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -27,20 +27,82 @@
|
|||||||
|
|
||||||
#import "GCDWebServerResponse.h"
|
#import "GCDWebServerResponse.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GCDWebServerDataResponse subclass of GCDWebServerResponse reads the body
|
||||||
|
* of the HTTP response from memory.
|
||||||
|
*/
|
||||||
@interface GCDWebServerDataResponse : GCDWebServerResponse
|
@interface GCDWebServerDataResponse : GCDWebServerResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a response with data in memory and a given content type.
|
||||||
|
*/
|
||||||
+ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type;
|
+ (instancetype)responseWithData:(NSData*)data contentType:(NSString*)type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type;
|
- (instancetype)initWithData:(NSData*)data contentType:(NSString*)type;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface GCDWebServerDataResponse (Extensions)
|
@interface GCDWebServerDataResponse (Extensions)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a data response from text encoded using UTF-8.
|
||||||
|
*/
|
||||||
+ (instancetype)responseWithText:(NSString*)text;
|
+ (instancetype)responseWithText:(NSString*)text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a data response from HTML encoded using UTF-8.
|
||||||
|
*/
|
||||||
+ (instancetype)responseWithHTML:(NSString*)html;
|
+ (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;
|
+ (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;
|
+ (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)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;
|
- (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;
|
- (instancetype)initWithJSONObject:(id)object contentType:(NSString*)type;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -28,14 +28,54 @@
|
|||||||
#import "GCDWebServerDataResponse.h"
|
#import "GCDWebServerDataResponse.h"
|
||||||
#import "GCDWebServerHTTPStatusCodes.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
|
@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);
|
+ (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);
|
+ (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);
|
+ (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);
|
+ (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);
|
- (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);
|
- (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);
|
- (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);
|
- (instancetype)initWithServerError:(GCDWebServerServerErrorHTTPStatusCode)errorCode underlyingError:(NSError*)underlyingError message:(NSString*)format, ... NS_FORMAT_FUNCTION(3,4);
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -27,13 +27,70 @@
|
|||||||
|
|
||||||
#import "GCDWebServerResponse.h"
|
#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
|
@interface GCDWebServerFileResponse : GCDWebServerResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a response with the contents of a file.
|
||||||
|
*/
|
||||||
+ (instancetype)responseWithFile:(NSString*)path;
|
+ (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;
|
+ (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;
|
+ (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;
|
+ (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;
|
||||||
- (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;
|
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range isAttachment:(BOOL)attachment;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -40,10 +40,6 @@
|
|||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
static inline NSError* _MakePosixError(int code) {
|
|
||||||
return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%s", strerror(code)]}];
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation GCDWebServerFileResponse
|
@implementation GCDWebServerFileResponse
|
||||||
|
|
||||||
+ (instancetype)responseWithFile:(NSString*)path {
|
+ (instancetype)responseWithFile:(NSString*)path {
|
||||||
@@ -63,11 +59,11 @@ static inline NSError* _MakePosixError(int code) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithFile:(NSString*)path {
|
- (instancetype)initWithFile:(NSString*)path {
|
||||||
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:NO];
|
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
- (instancetype)initWithFile:(NSString*)path isAttachment:(BOOL)attachment {
|
||||||
return [self initWithFile:path byteRange:NSMakeRange(NSNotFound, 0) isAttachment:attachment];
|
return [self initWithFile:path byteRange:NSMakeRange(NSUIntegerMax, 0) isAttachment:attachment];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range {
|
- (instancetype)initWithFile:(NSString*)path byteRange:(NSRange)range {
|
||||||
@@ -85,31 +81,41 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
|||||||
ARC_RELEASE(self);
|
ARC_RELEASE(self);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
if (GCDWebServerIsValidByteRange(range)) {
|
#ifndef __LP64__
|
||||||
if (range.location != NSNotFound) {
|
if (info.st_size >= (off_t)4294967295) { // In 32 bit mode, we can't handle files greater than 4 GiBs (don't use "NSUIntegerMax" here to avoid potential unsigned to signed conversion issues)
|
||||||
range.location = MIN(range.location, (NSUInteger)info.st_size);
|
DNOT_REACHED();
|
||||||
range.length = MIN(range.length, (NSUInteger)info.st_size - range.location);
|
ARC_RELEASE(self);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
NSUInteger fileSize = (NSUInteger)info.st_size;
|
||||||
|
|
||||||
|
BOOL hasByteRange = GCDWebServerIsValidByteRange(range);
|
||||||
|
if (hasByteRange) {
|
||||||
|
if (range.location != NSUIntegerMax) {
|
||||||
|
range.location = MIN(range.location, fileSize);
|
||||||
|
range.length = MIN(range.length, fileSize - range.location);
|
||||||
} else {
|
} else {
|
||||||
range.length = MIN(range.length, (NSUInteger)info.st_size);
|
range.length = MIN(range.length, fileSize);
|
||||||
range.location = (NSUInteger)info.st_size - range.length;
|
range.location = fileSize - range.length;
|
||||||
}
|
}
|
||||||
if (range.length == 0) {
|
if (range.length == 0) {
|
||||||
ARC_RELEASE(self);
|
ARC_RELEASE(self);
|
||||||
return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
|
return nil; // TODO: Return 416 status code and "Content-Range: bytes */{file length}" header
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
range.location = 0;
|
||||||
|
range.length = fileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_path = [path copy];
|
_path = [path copy];
|
||||||
if (range.location != NSNotFound) {
|
_offset = range.location;
|
||||||
_offset = range.location;
|
_size = range.length;
|
||||||
_size = range.length;
|
if (hasByteRange) {
|
||||||
[self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent];
|
[self setStatusCode:kGCDWebServerHTTPStatusCode_PartialContent];
|
||||||
[self setValue:[NSString stringWithFormat:@"bytes %i-%i/%i", (int)range.location, (int)(range.location + range.length - 1), (int)info.st_size] forAdditionalHeader:@"Content-Range"];
|
[self setValue:[NSString stringWithFormat:@"bytes %lu-%lu/%lu", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), (unsigned long)fileSize] forAdditionalHeader:@"Content-Range"];
|
||||||
LOG_DEBUG(@"Using content bytes range [%i-%i] for file \"%@\"", (int)range.location, (int)(range.location + range.length - 1), path);
|
LOG_DEBUG(@"Using content bytes range [%lu-%lu] for file \"%@\"", (unsigned long)_offset, (unsigned long)(_offset + _size - 1), path);
|
||||||
} else {
|
|
||||||
_offset = 0;
|
|
||||||
_size = (NSUInteger)info.st_size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
@@ -125,8 +131,8 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.contentType = GCDWebServerGetMimeTypeForExtension([path pathExtension]);
|
self.contentType = GCDWebServerGetMimeTypeForExtension([_path pathExtension]);
|
||||||
self.contentLength = (range.location != NSNotFound ? range.length : (NSUInteger)info.st_size);
|
self.contentLength = _size;
|
||||||
self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec);
|
self.lastModifiedDate = _NSDateFromTimeSpec(&info.st_mtimespec);
|
||||||
self.eTag = [NSString stringWithFormat:@"%llu/%li/%li", info.st_ino, info.st_mtimespec.tv_sec, info.st_mtimespec.tv_nsec];
|
self.eTag = [NSString stringWithFormat:@"%llu/%li/%li", info.st_ino, info.st_mtimespec.tv_sec, info.st_mtimespec.tv_nsec];
|
||||||
}
|
}
|
||||||
@@ -142,11 +148,11 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
|||||||
- (BOOL)open:(NSError**)error {
|
- (BOOL)open:(NSError**)error {
|
||||||
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
|
_file = open([_path fileSystemRepresentation], O_NOFOLLOW | O_RDONLY);
|
||||||
if (_file <= 0) {
|
if (_file <= 0) {
|
||||||
*error = _MakePosixError(errno);
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
|
if (lseek(_file, _offset, SEEK_SET) != (off_t)_offset) {
|
||||||
*error = _MakePosixError(errno);
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
close(_file);
|
close(_file);
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
@@ -158,7 +164,7 @@ static inline NSDate* _NSDateFromTimeSpec(const struct timespec* t) {
|
|||||||
NSMutableData* data = [[NSMutableData alloc] initWithLength:length];
|
NSMutableData* data = [[NSMutableData alloc] initWithLength:length];
|
||||||
ssize_t result = read(_file, data.mutableBytes, length);
|
ssize_t result = read(_file, data.mutableBytes, length);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
*error = _MakePosixError(errno);
|
*error = GCDWebServerMakePosixError(errno);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
if (result > 0) {
|
if (result > 0) {
|
||||||
|
|||||||
@@ -25,11 +25,29 @@
|
|||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import "GCDWebServerStreamingResponse.h"
|
#import "GCDWebServerStreamedResponse.h"
|
||||||
|
|
||||||
typedef NSData* (^GCDWebServerStreamBlock)(NSError** error);
|
/**
|
||||||
|
* The GCDWebServerStreamingBlock is called to stream the data for the HTTP body.
|
||||||
|
* The block must return empty NSData when done or nil on error and set the
|
||||||
|
* "error" argument which is guaranteed to be non-NULL.
|
||||||
|
*/
|
||||||
|
typedef NSData* (^GCDWebServerStreamingBlock)(NSError** error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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:(GCDWebServerStreamingBlock)block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the designated initializer for the class.
|
||||||
|
*/
|
||||||
|
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamingBlock)block;
|
||||||
|
|
||||||
@interface GCDWebServerStreamingResponse : GCDWebServerResponse
|
|
||||||
+ (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
|
|
||||||
@end
|
@end
|
||||||
@@ -27,19 +27,19 @@
|
|||||||
|
|
||||||
#import "GCDWebServerPrivate.h"
|
#import "GCDWebServerPrivate.h"
|
||||||
|
|
||||||
@interface GCDWebServerStreamingResponse () {
|
@interface GCDWebServerStreamedResponse () {
|
||||||
@private
|
@private
|
||||||
GCDWebServerStreamBlock _block;
|
GCDWebServerStreamingBlock _block;
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GCDWebServerStreamingResponse
|
@implementation GCDWebServerStreamedResponse
|
||||||
|
|
||||||
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
|
+ (instancetype)responseWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamingBlock)block {
|
||||||
return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]);
|
return ARC_AUTORELEASE([[[self class] alloc] initWithContentType:type streamBlock:block]);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamBlock)block {
|
- (instancetype)initWithContentType:(NSString*)type streamBlock:(GCDWebServerStreamingBlock)block {
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
_block = [block copy];
|
_block = [block copy];
|
||||||
|
|
||||||
@@ -29,33 +29,169 @@
|
|||||||
|
|
||||||
@class GCDWebUploader;
|
@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>
|
@protocol GCDWebUploaderDelegate <GCDWebServerDelegate>
|
||||||
@optional
|
@optional
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file has been downloaded.
|
||||||
|
*/
|
||||||
- (void)webUploader:(GCDWebUploader*)uploader didDownloadFileAtPath:(NSString*)path;
|
- (void)webUploader:(GCDWebUploader*)uploader didDownloadFileAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a file has been uploaded.
|
||||||
|
*/
|
||||||
- (void)webUploader:(GCDWebUploader*)uploader didUploadFileAtPath:(NSString*)path;
|
- (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;
|
- (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;
|
- (void)webUploader:(GCDWebUploader*)uploader didDeleteItemAtPath:(NSString*)path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called whenever a directory has been created.
|
||||||
|
*/
|
||||||
- (void)webUploader:(GCDWebUploader*)uploader didCreateDirectoryAtPath:(NSString*)path;
|
- (void)webUploader:(GCDWebUploader*)uploader didCreateDirectoryAtPath:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@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
|
@interface GCDWebUploader : GCDWebServer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the upload directory as specified when the uploader was initialized.
|
||||||
|
*/
|
||||||
@property(nonatomic, readonly) NSString* uploadDirectory;
|
@property(nonatomic, readonly) NSString* uploadDirectory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the delegate for the uploader.
|
||||||
|
*/
|
||||||
@property(nonatomic, assign) id<GCDWebUploaderDelegate> delegate;
|
@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)
|
* Sets which files are allowed to be operated on depending on their extension.
|
||||||
@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)
|
* The default value is nil i.e. all file extensions are allowed.
|
||||||
@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)
|
@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;
|
- (instancetype)initWithUploadDirectory:(NSString*)path;
|
||||||
|
|
||||||
@end
|
@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)
|
@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
|
* This method is called to check if a file upload is allowed to complete.
|
||||||
- (BOOL)shouldCreateDirectoryAtPath:(NSString*)path; // Default implementation returns YES
|
* 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
|
@end
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
@private
|
@private
|
||||||
NSString* _uploadDirectory;
|
NSString* _uploadDirectory;
|
||||||
NSArray* _allowedExtensions;
|
NSArray* _allowedExtensions;
|
||||||
BOOL _showHidden;
|
BOOL _allowHidden;
|
||||||
NSString* _title;
|
NSString* _title;
|
||||||
NSString* _header;
|
NSString* _header;
|
||||||
NSString* _prologue;
|
NSString* _prologue;
|
||||||
@@ -57,6 +57,11 @@
|
|||||||
|
|
||||||
@implementation GCDWebUploader (Methods)
|
@implementation GCDWebUploader (Methods)
|
||||||
|
|
||||||
|
// Must match implementation in GCDWebDAVServer
|
||||||
|
- (BOOL)_checkSandboxedPath:(NSString*)path {
|
||||||
|
return [[path stringByStandardizingPath] hasPrefix:_uploadDirectory];
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
- (BOOL)_checkFileExtension:(NSString*)fileName {
|
||||||
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
if (_allowedExtensions && ![_allowedExtensions containsObject:[[fileName pathExtension] lowercaseString]]) {
|
||||||
return NO;
|
return NO;
|
||||||
@@ -85,8 +90,8 @@
|
|||||||
- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request {
|
- (GCDWebServerResponse*)listDirectory:(GCDWebServerRequest*)request {
|
||||||
NSString* relativePath = [[request query] objectForKey:@"path"];
|
NSString* relativePath = [[request query] objectForKey:@"path"];
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
BOOL isDirectory;
|
BOOL isDirectory = NO;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
@@ -94,7 +99,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* directoryName = [absolutePath lastPathComponent];
|
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];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Listing directory name \"%@\" is not allowed", directoryName];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +111,7 @@
|
|||||||
|
|
||||||
NSMutableArray* array = [NSMutableArray array];
|
NSMutableArray* array = [NSMutableArray array];
|
||||||
for (NSString* item in [contents sortedArrayUsingSelector:@selector(localizedStandardCompare:)]) {
|
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];
|
NSDictionary* attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[absolutePath stringByAppendingPathComponent:item] error:NULL];
|
||||||
NSString* type = [attributes objectForKey:NSFileType];
|
NSString* type = [attributes objectForKey:NSFileType];
|
||||||
if ([type isEqualToString:NSFileTypeRegular] && [self _checkFileExtension:item]) {
|
if ([type isEqualToString:NSFileTypeRegular] && [self _checkFileExtension:item]) {
|
||||||
@@ -129,8 +134,8 @@
|
|||||||
- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request {
|
- (GCDWebServerResponse*)downloadFile:(GCDWebServerRequest*)request {
|
||||||
NSString* relativePath = [[request query] objectForKey:@"path"];
|
NSString* relativePath = [[request query] objectForKey:@"path"];
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
BOOL isDirectory;
|
BOOL isDirectory = NO;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
@@ -138,7 +143,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* fileName = [absolutePath lastPathComponent];
|
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];
|
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];
|
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)
|
NSString* contentType = (range.location != NSNotFound ? @"application/json" : @"text/plain; charset=utf-8"); // Required when using iFrame transport (see https://github.com/blueimp/jQuery-File-Upload/wiki/Setup)
|
||||||
|
|
||||||
GCDWebServerMultiPartFile* file = [request.files objectForKey:@"files[]"];
|
GCDWebServerMultiPartFile* file = [request firstFileForControlName:@"files[]"];
|
||||||
if ((!_showHidden && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) {
|
if ((!_allowHidden && [file.fileName hasPrefix:@"."]) || ![self _checkFileExtension:file.fileName]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploaded file name \"%@\" is not allowed", 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]];
|
NSString* absolutePath = [self _uniquePathForPath:[[_uploadDirectory stringByAppendingPathComponent:relativePath] stringByAppendingPathComponent:file.fileName]];
|
||||||
|
if (![self _checkSandboxedPath:absolutePath]) {
|
||||||
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
|
}
|
||||||
|
|
||||||
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
|
if (![self shouldUploadFileAtPath:absolutePath withTemporaryFile:file.temporaryPath]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Uploading file \"%@\" to \"%@\" is not permitted", file.fileName, relativePath];
|
||||||
@@ -181,16 +189,19 @@
|
|||||||
- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request {
|
- (GCDWebServerResponse*)moveItem:(GCDWebServerURLEncodedFormRequest*)request {
|
||||||
NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"];
|
NSString* oldRelativePath = [request.arguments objectForKey:@"oldPath"];
|
||||||
NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath];
|
NSString* oldAbsolutePath = [_uploadDirectory stringByAppendingPathComponent:oldRelativePath];
|
||||||
BOOL isDirectory;
|
BOOL isDirectory = NO;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
|
if (![self _checkSandboxedPath:oldAbsolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:oldAbsolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", oldRelativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
|
NSString* newRelativePath = [request.arguments objectForKey:@"newPath"];
|
||||||
NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]];
|
NSString* newAbsolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:newRelativePath]];
|
||||||
|
if (![self _checkSandboxedPath:newAbsolutePath]) {
|
||||||
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", newRelativePath];
|
||||||
|
}
|
||||||
|
|
||||||
NSString* itemName = [newAbsolutePath lastPathComponent];
|
NSString* itemName = [newAbsolutePath lastPathComponent];
|
||||||
if ((!_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];
|
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* relativePath = [request.arguments objectForKey:@"path"];
|
||||||
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
NSString* absolutePath = [_uploadDirectory stringByAppendingPathComponent:relativePath];
|
||||||
BOOL isDirectory = NO;
|
BOOL isDirectory = NO;
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
if (![self _checkSandboxedPath:absolutePath] || ![[NSFileManager defaultManager] fileExistsAtPath:absolutePath isDirectory:&isDirectory]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString* itemName = [absolutePath lastPathComponent];
|
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];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Deleting item name \"%@\" is not allowed", itemName];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,9 +255,12 @@
|
|||||||
- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request {
|
- (GCDWebServerResponse*)createDirectory:(GCDWebServerURLEncodedFormRequest*)request {
|
||||||
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
NSString* relativePath = [request.arguments objectForKey:@"path"];
|
||||||
NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]];
|
NSString* absolutePath = [self _uniquePathForPath:[_uploadDirectory stringByAppendingPathComponent:relativePath]];
|
||||||
|
if (![self _checkSandboxedPath:absolutePath]) {
|
||||||
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_NotFound message:@"\"%@\" does not exist", relativePath];
|
||||||
|
}
|
||||||
|
|
||||||
NSString* directoryName = [absolutePath lastPathComponent];
|
NSString* directoryName = [absolutePath lastPathComponent];
|
||||||
if (!_showHidden && [directoryName hasPrefix:@"."]) {
|
if (!_allowHidden && [directoryName hasPrefix:@"."]) {
|
||||||
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
|
return [GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"Creating directory name \"%@\" is not allowed", directoryName];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +285,7 @@
|
|||||||
|
|
||||||
@implementation GCDWebUploader
|
@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;
|
title=_title, header=_header, prologue=_prologue, epilogue=_epilogue, footer=_footer;
|
||||||
|
|
||||||
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
- (instancetype)initWithUploadDirectory:(NSString*)path {
|
||||||
|
|||||||
68
Mac/main.m
68
Mac/main.m
@@ -31,9 +31,10 @@
|
|||||||
|
|
||||||
#import "GCDWebServerDataRequest.h"
|
#import "GCDWebServerDataRequest.h"
|
||||||
#import "GCDWebServerURLEncodedFormRequest.h"
|
#import "GCDWebServerURLEncodedFormRequest.h"
|
||||||
|
#import "GCDWebServerMultiPartFormRequest.h"
|
||||||
|
|
||||||
#import "GCDWebServerDataResponse.h"
|
#import "GCDWebServerDataResponse.h"
|
||||||
#import "GCDWebServerStreamingResponse.h"
|
#import "GCDWebServerStreamedResponse.h"
|
||||||
|
|
||||||
#import "GCDWebDAVServer.h"
|
#import "GCDWebDAVServer.h"
|
||||||
|
|
||||||
@@ -47,9 +48,10 @@ typedef enum {
|
|||||||
kMode_WebServer = 0,
|
kMode_WebServer = 0,
|
||||||
kMode_HTMLPage,
|
kMode_HTMLPage,
|
||||||
kMode_HTMLForm,
|
kMode_HTMLForm,
|
||||||
|
kMode_HTMLFileUpload,
|
||||||
kMode_WebDAV,
|
kMode_WebDAV,
|
||||||
kMode_WebUploader,
|
kMode_WebUploader,
|
||||||
kMode_StreamingResponse
|
kMode_StreamingResponse,
|
||||||
} Mode;
|
} Mode;
|
||||||
|
|
||||||
@interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate>
|
@interface Delegate : NSObject <GCDWebServerDelegate, GCDWebDAVServerDelegate, GCDWebUploaderDelegate>
|
||||||
@@ -65,6 +67,10 @@ typedef enum {
|
|||||||
[self _logDelegateCall:_cmd];
|
[self _logDelegateCall:_cmd];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)webServerDidCompleteBonjourRegistration:(GCDWebServer*)server {
|
||||||
|
[self _logDelegateCall:_cmd];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)webServerDidConnect:(GCDWebServer*)server {
|
- (void)webServerDidConnect:(GCDWebServer*)server {
|
||||||
[self _logDelegateCall:_cmd];
|
[self _logDelegateCall:_cmd];
|
||||||
}
|
}
|
||||||
@@ -136,7 +142,7 @@ int main(int argc, const char* argv[]) {
|
|||||||
NSString* authenticationPassword = nil;
|
NSString* authenticationPassword = nil;
|
||||||
|
|
||||||
if (argc == 1) {
|
if (argc == 1) {
|
||||||
fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password]\n\n", basename((char*)argv[0]));
|
fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | htmlFileUpload | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory] [-authenticationMethod Basic | Digest] [-authenticationRealm realm] [-authenticationUser user] [-authenticationPassword password]\n\n", basename((char*)argv[0]));
|
||||||
} else {
|
} else {
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
if (argv[i][0] != '-') {
|
if (argv[i][0] != '-') {
|
||||||
@@ -150,6 +156,8 @@ int main(int argc, const char* argv[]) {
|
|||||||
mode = kMode_HTMLPage;
|
mode = kMode_HTMLPage;
|
||||||
} else if (!strcmp(argv[i], "htmlForm")) {
|
} else if (!strcmp(argv[i], "htmlForm")) {
|
||||||
mode = kMode_HTMLForm;
|
mode = kMode_HTMLForm;
|
||||||
|
} else if (!strcmp(argv[i], "htmlFileUpload")) {
|
||||||
|
mode = kMode_HTMLFileUpload;
|
||||||
} else if (!strcmp(argv[i], "webDAV")) {
|
} else if (!strcmp(argv[i], "webDAV")) {
|
||||||
mode = kMode_WebDAV;
|
mode = kMode_WebDAV;
|
||||||
} else if (!strcmp(argv[i], "webUploader")) {
|
} else if (!strcmp(argv[i], "webUploader")) {
|
||||||
@@ -239,6 +247,48 @@ int main(int argc, const char* argv[]) {
|
|||||||
break;
|
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
|
// Serve home directory through WebDAV
|
||||||
case kMode_WebDAV: {
|
case kMode_WebDAV: {
|
||||||
fprintf(stdout, "Running in WebDAV mode from \"%s\"", [rootDirectory UTF8String]);
|
fprintf(stdout, "Running in WebDAV mode from \"%s\"", [rootDirectory UTF8String]);
|
||||||
@@ -263,7 +313,7 @@ int main(int argc, const char* argv[]) {
|
|||||||
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
|
||||||
|
|
||||||
__block int countDown = 10;
|
__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);
|
usleep(100 * 1000);
|
||||||
if (countDown) {
|
if (countDown) {
|
||||||
@@ -287,11 +337,14 @@ int main(int argc, const char* argv[]) {
|
|||||||
|
|
||||||
if (webServer) {
|
if (webServer) {
|
||||||
Delegate* delegate = [[Delegate alloc] init];
|
Delegate* delegate = [[Delegate alloc] init];
|
||||||
webServer.delegate = delegate;
|
|
||||||
if (testDirectory) {
|
if (testDirectory) {
|
||||||
|
#ifndef NDEBUG
|
||||||
|
webServer.delegate = delegate;
|
||||||
|
#endif
|
||||||
fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]);
|
fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]);
|
||||||
result = (int)[webServer runTestsWithOptions:@{GCDWebServerOption_Port: @8080} inDirectory:testDirectory];
|
result = (int)[webServer runTestsWithOptions:@{GCDWebServerOption_Port: @8080} inDirectory:testDirectory];
|
||||||
} else {
|
} else {
|
||||||
|
webServer.delegate = delegate;
|
||||||
if (recording) {
|
if (recording) {
|
||||||
fprintf(stdout, "<RECORDING ENABLED>\n");
|
fprintf(stdout, "<RECORDING ENABLED>\n");
|
||||||
webServer.recordingEnabled = YES;
|
webServer.recordingEnabled = YES;
|
||||||
@@ -309,13 +362,14 @@ int main(int argc, const char* argv[]) {
|
|||||||
[options setObject:GCDWebServerAuthenticationMethod_DigestAccess forKey:GCDWebServerOption_AuthenticationMethod];
|
[options setObject:GCDWebServerAuthenticationMethod_DigestAccess forKey:GCDWebServerOption_AuthenticationMethod];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ([webServer runWithOptions:options]) {
|
if ([webServer runWithOptions:options error:NULL]) {
|
||||||
result = 0;
|
result = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
webServer.delegate = nil;
|
||||||
#if !__has_feature(objc_arc)
|
#if !__has_feature(objc_arc)
|
||||||
[webServer release];
|
|
||||||
[delegate release];
|
[delegate release];
|
||||||
|
[webServer release];
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
88
README.md
88
README.md
@@ -2,11 +2,13 @@ Overview
|
|||||||
========
|
========
|
||||||
|
|
||||||
[](https://travis-ci.org/swisspol/GCDWebServer)
|
[](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:
|
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)
|
* 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 for easy integration and customization
|
* 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 maximal performance and concurrency
|
* 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
|
* No dependencies on third-party source code
|
||||||
* Available under a friendly [New BSD License](LICENSE)
|
* Available under a friendly [New BSD License](LICENSE)
|
||||||
|
|
||||||
@@ -75,11 +77,10 @@ int main(int argc, const char* argv[]) {
|
|||||||
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Use convenience method that runs server on port 8080 until SIGINT received (i.e. Ctrl-C in Terminal)
|
// Use convenience method that runs server on port 8080
|
||||||
[webServer runWithPort:8080];
|
// until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
|
||||||
|
[webServer runWithPort:8080 bonjourName:nil];
|
||||||
// Destroy server (unnecessary if using ARC)
|
NSLog(@"Visit %@ in your web browser", webServer.serverURL);
|
||||||
[webServer release];
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -91,7 +92,12 @@ int main(int argc, const char* argv[]) {
|
|||||||
#import "GCDWebServer.h"
|
#import "GCDWebServer.h"
|
||||||
#import "GCDWebServerDataResponse.h"
|
#import "GCDWebServerDataResponse.h"
|
||||||
|
|
||||||
static GCDWebServer* _webServer = nil; // This should really be an ivar of your application's delegate class
|
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||||
|
GCDWebServer* _webServer;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||||
|
|
||||||
@@ -109,9 +115,35 @@ static GCDWebServer* _webServer = nil; // This should really be an ivar of your
|
|||||||
|
|
||||||
// Start server on port 8080
|
// Start server on port 8080
|
||||||
[_webServer startWithPort:8080 bonjourName:nil];
|
[_webServer startWithPort:8080 bonjourName:nil];
|
||||||
|
NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
```
|
||||||
|
|
||||||
|
**OS X Swift version (command line tool):**
|
||||||
|
|
||||||
|
***webServer.swift***
|
||||||
|
```swift
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
let webServer = GCDWebServer()
|
||||||
|
|
||||||
|
webServer.addDefaultHandlerForMethod("GET", requestClass: GCDWebServerRequest.self) { request in
|
||||||
|
return GCDWebServerDataResponse(HTML:"<html><body><p>Hello World</p></body></html>")
|
||||||
|
}
|
||||||
|
|
||||||
|
webServer.runWithPort(8080, bonjourName: nil)
|
||||||
|
|
||||||
|
println("Visit \(webServer.serverURL) in your web browser")
|
||||||
|
```
|
||||||
|
|
||||||
|
***WebServer-Bridging-Header.h***
|
||||||
|
```objectivec
|
||||||
|
#import "GCDWebServer.h"
|
||||||
|
#import "GCDWebServerDataResponse.h"
|
||||||
```
|
```
|
||||||
|
|
||||||
Web Based Uploads in iOS Apps
|
Web Based Uploads in iOS Apps
|
||||||
@@ -124,7 +156,12 @@ Simply instantiate and run a ```GCDWebUploader``` instance then visit ```http://
|
|||||||
```objectivec
|
```objectivec
|
||||||
#import "GCDWebUploader.h"
|
#import "GCDWebUploader.h"
|
||||||
|
|
||||||
static GCDWebUploader* _webUploader = nil; // This should really be an ivar of your application's delegate class
|
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||||
|
GCDWebUploader* _webUploader;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||||
@@ -133,6 +170,8 @@ static GCDWebUploader* _webUploader = nil; // This should really be an ivar of
|
|||||||
NSLog(@"Visit %@ in your web browser", _webUploader.serverURL);
|
NSLog(@"Visit %@ in your web browser", _webUploader.serverURL);
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
```
|
```
|
||||||
|
|
||||||
WebDAV Server in iOS Apps
|
WebDAV Server in iOS Apps
|
||||||
@@ -147,7 +186,12 @@ Simply instantiate and run a ```GCDWebDAVServer``` instance then connect to ```h
|
|||||||
```objectivec
|
```objectivec
|
||||||
#import "GCDWebDAVServer.h"
|
#import "GCDWebDAVServer.h"
|
||||||
|
|
||||||
static GCDWebDAVServer* _davServer = nil; // This should really be an ivar of your application's delegate class
|
@interface AppDelegate : NSObject <UIApplicationDelegate> {
|
||||||
|
GCDWebDAVServer* _davServer;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
|
||||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||||
@@ -156,6 +200,8 @@ static GCDWebDAVServer* _davServer = nil; // This should really be an ivar of y
|
|||||||
NSLog(@"Visit %@ in your WebDAV client", _davServer.serverURL);
|
NSLog(@"Visit %@ in your WebDAV client", _davServer.serverURL);
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
```
|
```
|
||||||
|
|
||||||
Serving a Static Website
|
Serving a Static Website
|
||||||
@@ -172,7 +218,6 @@ int main(int argc, const char* argv[]) {
|
|||||||
GCDWebServer* webServer = [[GCDWebServer alloc] init];
|
GCDWebServer* webServer = [[GCDWebServer alloc] init];
|
||||||
[webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
|
[webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:3600 allowRangeRequests:YES];
|
||||||
[webServer runWithPort:8080];
|
[webServer runWithPort:8080];
|
||||||
[webServer release]; // Remove if using ARC
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -222,6 +267,25 @@ Fortunately, GCDWebServer does all of this automatically for you:
|
|||||||
|
|
||||||
HTTP connections are often initiated in batches (or bursts), for instance when loading a web page with multiple resources. This makes it difficult to accurately detect when the *very last* HTTP connection has been closed: it's possible 2 consecutive HTTP connections part of the same batch would be separated by a small delay instead of overlapping. It would be bad for the client if GCDWebServer suspended itself right in between. The ```GCDWebServerOption_ConnectedStateCoalescingInterval``` option solves this problem elegantly by forcing GCDWebServer to wait some extra delay before performing any action after the last HTTP connection has been closed, just in case a new HTTP connection is initiated within this delay.
|
HTTP connections are often initiated in batches (or bursts), for instance when loading a web page with multiple resources. This makes it difficult to accurately detect when the *very last* HTTP connection has been closed: it's possible 2 consecutive HTTP connections part of the same batch would be separated by a small delay instead of overlapping. It would be bad for the client if GCDWebServer suspended itself right in between. The ```GCDWebServerOption_ConnectedStateCoalescingInterval``` option solves this problem elegantly by forcing GCDWebServer to wait some extra delay before performing any action after the last HTTP connection has been closed, just in case a new HTTP connection is initiated within this delay.
|
||||||
|
|
||||||
|
Debug Builds & Custom Logging
|
||||||
|
=============================
|
||||||
|
|
||||||
|
When building GCDWebServer in "Debug" mode versus "Release" mode, GCDWebServer logs a lot more information and also performs a number of internal consistency checks. To disable this behavior, make sure to define the preprocessor constant ```NDEBUG``` when compiling GCDWebServer. In Xcode target settings, this can be done by adding ```NDEBUG``` to the build setting ```GCC_PREPROCESSOR_DEFINITIONS``` when building in Release configuration (this is done automatically for you if you use CocoaPods).
|
||||||
|
|
||||||
|
It's also possible to replace the logging system used by GCDWebServer by a custom one. Simply define the preprocessor constant ```__GCDWEBSERVER_LOGGING_HEADER__``` to the name of a header file (e.g. "MyLogging.h") that defines these macros:
|
||||||
|
|
||||||
|
```
|
||||||
|
#define LOG_DEBUG(...) // Should not do anything if NDEBUG is defined
|
||||||
|
#define LOG_VERBOSE(...)
|
||||||
|
#define LOG_INFO(...)
|
||||||
|
#define LOG_WARNING(...)
|
||||||
|
#define LOG_ERROR(...)
|
||||||
|
#define LOG_EXCEPTION(__EXCEPTION__)
|
||||||
|
|
||||||
|
#define DCHECK(__CONDITION__) // Should not do anything if NDEBUG is defined or abort if __CONDITION__ is false
|
||||||
|
#define DNOT_REACHED() // Should not do anything if NDEBUG is defined
|
||||||
|
```
|
||||||
|
|
||||||
Advanced Example 1: Implementing HTTP Redirects
|
Advanced Example 1: Implementing HTTP Redirects
|
||||||
===============================================
|
===============================================
|
||||||
|
|
||||||
|
|||||||
12
Run-Tests.sh
12
Run-Tests.sh
@@ -23,6 +23,12 @@ function runTests {
|
|||||||
rm -rf "$PAYLOAD_DIR"
|
rm -rf "$PAYLOAD_DIR"
|
||||||
ditto -x -k "$PAYLOAD_ZIP" "$PAYLOAD_DIR"
|
ditto -x -k "$PAYLOAD_ZIP" "$PAYLOAD_DIR"
|
||||||
TZ=GMT find "$PAYLOAD_DIR" -type d -exec SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" '{}' \; # ZIP archives do not preserve directories dates
|
TZ=GMT find "$PAYLOAD_DIR" -type d -exec SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" '{}' \; # ZIP archives do not preserve directories dates
|
||||||
|
if [ "$4" != "" ]; then
|
||||||
|
cp -f "$4" "$PAYLOAD_DIR/Payload"
|
||||||
|
pushd "$PAYLOAD_DIR/Payload"
|
||||||
|
SetFile -d "1/1/2014 00:00:00" -m "1/1/2014 00:00:00" `basename "$4"`
|
||||||
|
popd
|
||||||
|
fi
|
||||||
logLevel=2 $1 -mode "$2" -root "$PAYLOAD_DIR/Payload" -tests "$3"
|
logLevel=2 $1 -mode "$2" -root "$PAYLOAD_DIR/Payload" -tests "$3"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +49,10 @@ rm -rf "$ARC_BUILD_DIR"
|
|||||||
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null
|
xcodebuild -sdk "$OSX_SDK" -target "$OSX_TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null
|
||||||
|
|
||||||
# Run tests
|
# 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 $MRC_PRODUCT "webServer" "Tests/WebServer"
|
||||||
runTests $ARC_PRODUCT "webServer" "Tests/WebServer"
|
runTests $ARC_PRODUCT "webServer" "Tests/WebServer"
|
||||||
runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Transmit"
|
runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Transmit"
|
||||||
@@ -53,6 +63,8 @@ runTests $MRC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
|||||||
runTests $ARC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
runTests $ARC_PRODUCT "webDAV" "Tests/WebDAV-Finder"
|
||||||
runTests $MRC_PRODUCT "webUploader" "Tests/WebUploader"
|
runTests $MRC_PRODUCT "webUploader" "Tests/WebUploader"
|
||||||
runTests $ARC_PRODUCT "webUploader" "Tests/WebUploader"
|
runTests $ARC_PRODUCT "webUploader" "Tests/WebUploader"
|
||||||
|
runTests $MRC_PRODUCT "webServer" "Tests/WebServer-Sample-Movie" "Tests/Sample-Movie.mp4"
|
||||||
|
runTests $ARC_PRODUCT "webServer" "Tests/WebServer-Sample-Movie" "Tests/Sample-Movie.mp4"
|
||||||
|
|
||||||
# Done
|
# Done
|
||||||
echo "\nAll tests completed successfully!"
|
echo "\nAll tests completed successfully!"
|
||||||
|
|||||||
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
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
NSString* documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
||||||
_webServer = [[GCDWebUploader alloc] initWithUploadDirectory:documentsPath];
|
_webServer = [[GCDWebUploader alloc] initWithUploadDirectory:documentsPath];
|
||||||
_webServer.delegate = self;
|
_webServer.delegate = self;
|
||||||
_webServer.showHiddenFiles = YES;
|
_webServer.allowHiddenItems = YES;
|
||||||
[_webServer start];
|
[_webServer start];
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
|
|||||||
Reference in New Issue
Block a user