diff --git a/.gitignore b/.gitignore index 35cfb4d..f8bf09a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store xcuserdata project.xcworkspace + +Tests/Payload diff --git a/CGDWebServer/GCDWebServer.h b/CGDWebServer/GCDWebServer.h index e773c18..657c06a 100644 --- a/CGDWebServer/GCDWebServer.h +++ b/CGDWebServer/GCDWebServer.h @@ -79,7 +79,9 @@ NSString* GCDWebServerGetPrimaryIPv4Address(); // Returns IPv4 address of prima @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 #if !TARGET_OS_IPHONE +@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) - (BOOL)runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only) +- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port; // Returns number of failed tests or -1 if server failed to start #endif @end diff --git a/CGDWebServer/GCDWebServer.m b/CGDWebServer/GCDWebServer.m index 9516a3f..4e53eaf 100644 --- a/CGDWebServer/GCDWebServer.m +++ b/CGDWebServer/GCDWebServer.m @@ -53,6 +53,9 @@ NSUInteger _port; dispatch_source_t _source; CFNetServiceRef _service; +#if !TARGET_OS_IPHONE + BOOL _recording; +#endif } @end @@ -152,9 +155,13 @@ NSDate* GCDWebServerParseHTTPDate(NSString* string) { return date; } -NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType) { - if ([contentType hasPrefix:@"text/"] || [contentType isEqualToString:@"application/json"] || [contentType isEqualToString:@"application/xml"]) { - NSString* charset = GCDWebServerExtractHeaderValueParameter(contentType, @"charset"); +static inline BOOL _IsTextContentType(NSString* type) { + return ([type hasPrefix:@"text/"] || [type hasPrefix:@"application/json"] || [type hasPrefix:@"application/xml"]); +} + +NSString* GCDWebServerDescribeData(NSData* data, NSString* type) { + if (_IsTextContentType(type)) { + NSString* charset = GCDWebServerExtractHeaderValueParameter(type, @"charset"); NSString* string = [[NSString alloc] initWithData:data encoding:GCDWebServerStringEncodingFromCharset(charset)]; if (string) { return ARC_AUTORELEASE(string); @@ -529,6 +536,18 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er @implementation GCDWebServer (Extensions) +#if !TARGET_OS_IPHONE + +- (void)setRecordingEnabled:(BOOL)flag { + _recording = flag; +} + +- (BOOL)isRecordingEnabled { + return _recording; +} + +#endif + - (NSURL*)serverURL { if (_source) { NSString* ipAddress = GCDWebServerGetPrimaryIPv4Address(); @@ -576,6 +595,150 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er return success; } +static CFHTTPMessageRef _CreateHTTPMessageFromFileDump(NSString* path, BOOL isRequest) { + NSData* data = [NSData dataWithContentsOfFile:path]; + if (data) { + CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, isRequest); + if (CFHTTPMessageAppendBytes(message, data.bytes, data.length)) { + return message; + } + CFRelease(message); + } + return NULL; +} + +static CFHTTPMessageRef _CreateHTTPMessageFromHTTPRequestResponse(CFHTTPMessageRef request) { + CFHTTPMessageRef response = NULL; + CFReadStreamRef stream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request); + if (CFReadStreamOpen(stream)) { + CFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault, 0); + CFDataSetLength(data, 256 * 1024); + CFIndex length = 0; + while (1) { + CFIndex result = CFReadStreamRead(stream, CFDataGetMutableBytePtr(data) + length, CFDataGetLength(data) - length); + if (result <= 0) { + break; + } + length += result; + if (length >= CFDataGetLength(data)) { + CFDataSetLength(data, 2 * CFDataGetLength(data)); + } + } + if (CFReadStreamGetStatus(stream) == kCFStreamStatusAtEnd) { + response = (CFHTTPMessageRef)CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader); + if (response) { + CFDataSetLength(data, length); + CFHTTPMessageSetBody(response, data); + } + } + CFRelease(data); + CFReadStreamClose(stream); + CFRelease(stream); + } + return response; +} + +static void _LogResult(NSString* format, ...) { + va_list arguments; + va_start(arguments, format); + NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments]; + va_end(arguments); + fprintf(stdout, "%s\n", [message UTF8String]); + ARC_RELEASE(message); +} + +- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port { + NSInteger result = -1; + if ([self startWithPort:port bonjourName:nil]) { + + result = 0; + NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL]; + for (NSString* requestFile in files) { + if (![requestFile hasSuffix:@".request"]) { + continue; + } + @autoreleasepool { + NSString* index = [[requestFile componentsSeparatedByString:@"-"] firstObject]; + BOOL success = NO; + CFHTTPMessageRef request = _CreateHTTPMessageFromFileDump([path stringByAppendingPathComponent:requestFile], YES); + if (request) { + _LogResult(@"[%i] %@ %@", (int)[index integerValue], ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(request)), [ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(request)) path]); + NSString* prefix = [index stringByAppendingString:@"-"]; + for (NSString* responseFile in files) { + if ([responseFile hasPrefix:prefix] && [responseFile hasSuffix:@".response"]) { + CFHTTPMessageRef expectedResponse = _CreateHTTPMessageFromFileDump([path stringByAppendingPathComponent:responseFile], NO); + if (expectedResponse) { + CFHTTPMessageRef actualResponse = _CreateHTTPMessageFromHTTPRequestResponse(request); + if (actualResponse) { + success = YES; + + CFIndex expectedStatusCode = CFHTTPMessageGetResponseStatusCode(expectedResponse); + CFIndex actualStatusCode = CFHTTPMessageGetResponseStatusCode(actualResponse); + if (actualStatusCode != expectedStatusCode) { + _LogResult(@" Status code not matching:\n Expected: %i\n Actual: %i", (int)expectedStatusCode, (int)actualStatusCode); + success = NO; + } + + NSDictionary* expectedHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(expectedResponse)); + NSDictionary* actualHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(actualResponse)); + for (NSString* expectedHeader in expectedHeaders) { + if ([expectedHeader isEqualToString:@"Date"]) { + continue; + } + NSString* expectedValue = [expectedHeaders objectForKey:expectedHeader]; + NSString* actualValue = [actualHeaders objectForKey:expectedHeader]; + if (![actualValue isEqualToString:expectedValue]) { + _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", expectedHeader, expectedValue, actualValue); + success = NO; + } + } + for (NSString* actualHeader in actualHeaders) { + if (![expectedHeaders objectForKey:actualHeader]) { + _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", actualHeader, nil, [actualHeaders objectForKey:actualHeader]); + success = NO; + } + } + + NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse)); + NSData* actualBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(actualResponse)); + if (![actualBody isEqualToData:expectedBody]) { + _LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length); + success = NO; + + if (_IsTextContentType([expectedHeaders objectForKey:@"Content-Type"])) { + NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; + NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; + if ([expectedBody writeToFile:expectedPath atomically:YES] && [actualBody writeToFile:actualPath atomically:YES]) { + NSTask* task = [[NSTask alloc] init]; + [task setLaunchPath:@"/usr/bin/opendiff"]; + [task setArguments:@[expectedPath, actualPath]]; + [task launch]; + ARC_RELEASE(task); + } + } + } + + CFRelease(actualResponse); + } + CFRelease(expectedResponse); + } + break; + } + } + CFRelease(request); + } + _LogResult(@""); + if (!success) { + ++result; + } + } + } + + [self stop]; + } + return result; +} + #endif @end diff --git a/CGDWebServer/GCDWebServerConnection.m b/CGDWebServer/GCDWebServerConnection.m index 55f4cc9..d12603c 100644 --- a/CGDWebServer/GCDWebServerConnection.m +++ b/CGDWebServer/GCDWebServerConnection.m @@ -25,7 +25,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#import #import +#if !TARGET_OS_IPHONE +#import +#endif #import "GCDWebServerPrivate.h" @@ -45,6 +49,9 @@ static NSData* _CRLFData = nil; static NSData* _CRLFCRLFData = nil; static NSData* _continueData = nil; static NSData* _lastChunkData = nil; +#if !TARGET_OS_IPHONE +static int32_t _connectionCounter = 0; +#endif @interface GCDWebServerConnection () { @private @@ -62,6 +69,14 @@ static NSData* _lastChunkData = nil; CFHTTPMessageRef _responseMessage; GCDWebServerResponse* _response; NSInteger _statusCode; + +#if !TARGET_OS_IPHONE + NSUInteger _connectionIndex; + NSString* _requestPath; + int _requestFD; + NSString* _responsePath; + int _responseFD; +#endif } @end @@ -77,6 +92,18 @@ static NSData* _lastChunkData = nil; LOG_DEBUG(@"Connection received %zu bytes on socket %i", size, _socket); _bytesRead += size; [self didUpdateBytesRead]; +#if !TARGET_OS_IPHONE + if (_requestFD > 0) { + bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { + return (write(_requestFD, chunkBytes, chunkSize) == (ssize_t)chunkSize); + }); + if (!success) { + LOG_ERROR(@"Failed recording request data: %s (%i)", strerror(errno), errno); + close(_requestFD); + _requestFD = 0; + } + } +#endif block(buffer); } else { if (_bytesRead > 0) { @@ -100,8 +127,8 @@ static NSData* _lastChunkData = nil; if (buffer) { NSMutableData* data = [[NSMutableData alloc] initWithCapacity:dispatch_data_get_size(buffer)]; - dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* bufferChunk, size_t size) { - [data appendBytes:bufferChunk length:size]; + dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { + [data appendBytes:chunkBytes length:chunkSize]; return true; }); block(data); @@ -119,8 +146,8 @@ static NSData* _lastChunkData = nil; if (buffer) { NSMutableData* data = [NSMutableData dataWithCapacity:kHeadersReadBuffer]; - dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* bufferChunk, size_t size) { - [data appendBytes:bufferChunk length:size]; + dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { + [data appendBytes:chunkBytes length:chunkSize]; return true; }); NSRange range = [data rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(0, data.length)]; @@ -158,7 +185,7 @@ static NSData* _lastChunkData = nil; if (buffer) { if (dispatch_data_get_size(buffer) <= length) { - bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* chunkBytes, size_t chunkSize) { + bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { NSData* data = [NSData dataWithBytesNoCopy:(void*)chunkBytes length:chunkSize freeWhenDone:NO]; NSError* error = nil; if (![_request performWriteData:data error:&error]) { @@ -245,7 +272,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { [self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) { if (buffer) { - dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* chunkBytes, size_t chunkSize) { + dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { [chunkData appendBytes:chunkBytes length:chunkSize]; return true; }); @@ -263,6 +290,9 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { - (void)_writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block { size_t size = dispatch_data_get_size(buffer); +#if !TARGET_OS_IPHONE + ARC_DISPATCH_RETAIN(buffer); +#endif dispatch_write(_socket, buffer, kGCDWebServerGCDQueue, ^(dispatch_data_t data, int error) { @autoreleasepool { @@ -271,12 +301,27 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { LOG_DEBUG(@"Connection sent %zu bytes on socket %i", size, _socket); _bytesWritten += size; [self didUpdateBytesWritten]; +#if !TARGET_OS_IPHONE + if (_responseFD > 0) { + bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { + return (write(_responseFD, chunkBytes, chunkSize) == (ssize_t)chunkSize); + }); + if (!success) { + LOG_ERROR(@"Failed recording response data: %s (%i)", strerror(errno), errno); + close(_responseFD); + _responseFD = 0; + } + } +#endif block(YES); } else { LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error); block(NO); } } +#if !TARGET_OS_IPHONE + ARC_DISPATCH_RELEASE(buffer); +#endif }); } @@ -285,7 +330,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { #if !__has_feature(objc_arc) [data retain]; #endif - dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_main_queue(), ^{ + dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, kGCDWebServerGCDQueue, ^{ #if __has_feature(objc_arc) [data self]; // Keeps ARC from releasing data too early #else @@ -655,6 +700,11 @@ static NSString* _StringFromAddressData(NSData* data) { } ARC_RELEASE(_response); +#if !TARGET_OS_IPHONE + ARC_RELEASE(_requestPath); + ARC_RELEASE(_responsePath); +#endif + ARC_DEALLOC(super); } @@ -664,6 +714,21 @@ static NSString* _StringFromAddressData(NSData* data) { - (void)open { LOG_DEBUG(@"Did open connection on socket %i", _socket); + +#if !TARGET_OS_IPHONE + if (_server.recordingEnabled) { + _connectionIndex = OSAtomicIncrement32(&_connectionCounter); + + _requestPath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]); + _requestFD = open([_requestPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY); + DCHECK(_requestFD > 0); + + _responsePath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]); + _responseFD = open([_responsePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY); + DCHECK(_responseFD > 0); + } +#endif + [self _readRequestHeaders]; } @@ -732,6 +797,38 @@ static inline BOOL _CompareResources(NSString* responseETag, NSString* requestET } else { LOG_DEBUG(@"Did close connection on socket %i", _socket); } + +#if !TARGET_OS_IPHONE + if (_requestPath) { + BOOL success = NO; + NSError* error = nil; + if (_requestFD > 0) { + close(_requestFD); + NSString* name = [NSString stringWithFormat:@"%03lu-%@.request", (unsigned long)_connectionIndex, _virtualHEAD ? @"HEAD" : _request.method]; + success = [[NSFileManager defaultManager] moveItemAtPath:_requestPath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error]; + } + if (!success) { + LOG_ERROR(@"Failed saving recorded request: %@", error); + DNOT_REACHED(); + } + unlink([_requestPath fileSystemRepresentation]); + } + + if (_responsePath) { + BOOL success = NO; + NSError* error = nil; + if (_responseFD > 0) { + close(_responseFD); + NSString* name = [NSString stringWithFormat:@"%03lu-%i.response", (unsigned long)_connectionIndex, (int)_statusCode]; + success = [[NSFileManager defaultManager] moveItemAtPath:_responsePath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error]; + } + if (!success) { + LOG_ERROR(@"Failed saving recorded response: %@", error); + DNOT_REACHED(); + } + unlink([_responsePath fileSystemRepresentation]); + } +#endif if (_request) { LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten); } else { diff --git a/CGDWebServer/GCDWebServerPrivate.h b/CGDWebServer/GCDWebServerPrivate.h index 06cc961..b2238ef 100644 --- a/CGDWebServer/GCDWebServerPrivate.h +++ b/CGDWebServer/GCDWebServerPrivate.h @@ -36,8 +36,10 @@ #define ARC_AUTORELEASE(__OBJECT__) __OBJECT__ #define ARC_DEALLOC(__OBJECT__) #if (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_6_0)) || (!TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_8)) +#define ARC_DISPATCH_RETAIN(__OBJECT__) #define ARC_DISPATCH_RELEASE(__OBJECT__) #else +#define ARC_DISPATCH_RETAIN(__OBJECT__) dispatch_retain(__OBJECT__) #define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__) #endif #else @@ -47,6 +49,7 @@ #define ARC_RELEASE(__OBJECT__) [__OBJECT__ release] #define ARC_AUTORELEASE(__OBJECT__) [__OBJECT__ autorelease] #define ARC_DEALLOC(__OBJECT__) [__OBJECT__ dealloc] +#define ARC_DISPATCH_RETAIN(__OBJECT__) dispatch_retain(__OBJECT__) #define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__) #endif diff --git a/Mac/main.m b/Mac/main.m index 9a39b23..74e0e86 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -25,6 +25,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#import + #import "GCDWebServer.h" #import "GCDWebServerDataRequest.h" @@ -37,22 +39,71 @@ #import "GCDWebUploader.h" +typedef enum { + kMode_WebServer = 0, + kMode_HTMLPage, + kMode_HTMLForm, + kMode_WebDAV, + kMode_WebUploader, + kMode_StreamingResponse +} Mode; + int main(int argc, const char* argv[]) { - BOOL success = NO; - int mode = (argc == 2 ? MIN(MAX(atoi(argv[1]), 0), 5) : 0); + int result = -1; @autoreleasepool { + Mode mode = kMode_WebServer; + BOOL recording = NO; + NSString* rootDirectory = NSHomeDirectory(); + NSString* testDirectory = nil; + + if (argc == 1) { + fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory]\n\n", basename((char*)argv[0])); + } else { + for (int i = 1; i < argc; ++i) { + if (argv[i][0] != '-') { + continue; + } + if (!strcmp(argv[i], "-mode") && (i + 1 < argc)) { + ++i; + if (!strcmp(argv[i], "webServer")) { + mode = kMode_WebServer; + } else if (!strcmp(argv[i], "htmlPage")) { + mode = kMode_HTMLPage; + } else if (!strcmp(argv[i], "htmlForm")) { + mode = kMode_HTMLForm; + } else if (!strcmp(argv[i], "webDAV")) { + mode = kMode_WebDAV; + } else if (!strcmp(argv[i], "webUploader")) { + mode = kMode_WebUploader; + } else if (!strcmp(argv[i], "streamingResponse")) { + mode = kMode_StreamingResponse; + } + } else if (!strcmp(argv[i], "-record")) { + recording = YES; + } else if (!strcmp(argv[i], "-root") && (i + 1 < argc)) { + ++i; + rootDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath]; + } else if (!strcmp(argv[i], "-tests") && (i + 1 < argc)) { + ++i; + testDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath]; + } + } + } + GCDWebServer* webServer = nil; switch (mode) { // Simply serve contents of home directory - case 0: { + case kMode_WebServer: { + fprintf(stdout, "Running in Web Server mode from \"%s\"", [rootDirectory UTF8String]); webServer = [[GCDWebServer alloc] init]; - [webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:0 allowRangeRequests:YES]; + [webServer addGETHandlerForBasePath:@"/" directoryPath:rootDirectory indexFilename:nil cacheAge:0 allowRangeRequests:YES]; break; } // Renders a HTML page - case 1: { + case kMode_HTMLPage: { + fprintf(stdout, "Running in HTML Page mode"); webServer = [[GCDWebServer alloc] init]; [webServer addDefaultHandlerForMethod:@"GET" requestClass:[GCDWebServerRequest class] @@ -65,7 +116,8 @@ int main(int argc, const char* argv[]) { } // Implements an HTML form - case 2: { + case kMode_HTMLForm: { + fprintf(stdout, "Running in HTML Form mode"); webServer = [[GCDWebServer alloc] init]; [webServer addHandlerForMethod:@"GET" path:@"/" @@ -96,17 +148,23 @@ int main(int argc, const char* argv[]) { break; } - case 3: { - webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]]; + // Serve home directory through WebDAV + case kMode_WebDAV: { + fprintf(stdout, "Running in WebDAV mode from \"%s\"", [rootDirectory UTF8String]); + webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:rootDirectory]; break; } - case 4: { - webServer = [[GCDWebUploader alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]]; + // Serve home directory through web uploader + case kMode_WebUploader: { + fprintf(stdout, "Running in Web Uploader mode from \"%s\"", [rootDirectory UTF8String]); + webServer = [[GCDWebUploader alloc] initWithUploadDirectory:rootDirectory]; break; } - case 5: { + // Test streaming responses + case kMode_StreamingResponse: { + fprintf(stdout, "Running in Streaming Response mode"); webServer = [[GCDWebServer alloc] init]; [webServer addHandlerForMethod:@"GET" path:@"/" @@ -130,10 +188,30 @@ int main(int argc, const char* argv[]) { } } - success = [webServer runWithPort:8080]; -#if !__has_feature(objc_arc) - [webServer release]; +#if __has_feature(objc_arc) + fprintf(stdout, " (ARC is ON)\n"); +#else + fprintf(stdout, " (ARC is OFF)\n"); #endif + + if (webServer) { + if (testDirectory) { + fprintf(stdout, "\n\n", [testDirectory UTF8String]); + result = (int)[webServer runTestsInDirectory:testDirectory withPort:8080]; + } else { + if (recording) { + fprintf(stdout, "\n"); + webServer.recordingEnabled = YES; + } + fprintf(stdout, "\n"); + if ([webServer runWithPort:8080]) { + result = 0; + } + } +#if !__has_feature(objc_arc) + [webServer release]; +#endif + } } - return success ? 0 : -1; + return result; } diff --git a/Run-Tests.sh b/Run-Tests.sh new file mode 100755 index 0000000..93eea48 --- /dev/null +++ b/Run-Tests.sh @@ -0,0 +1,32 @@ +#!/bin/sh -ex + +TARGET="GCDWebServer (Mac)" +CONFIGURATION="Release" +PAYLOAD_ZIP="Tests/Payload.zip" +PAYLOAD_DIR="/tmp/payload" + +MRC_BUILD_DIR="/tmp/GCDWebServer-MRC" +MRC_PRODUCT="$MRC_BUILD_DIR/$CONFIGURATION/GCDWebServer" +ARC_BUILD_DIR="/tmp/GCDWebServer-ARC" +ARC_PRODUCT="$ARC_BUILD_DIR/$CONFIGURATION/GCDWebServer" + +function runTests { + rm -rf "$PAYLOAD_DIR" + ditto -x -k "$PAYLOAD_ZIP" "$PAYLOAD_DIR" + logLevel=2 $1 -root "$PAYLOAD_DIR" -tests "$2" +} + +# Build in manual memory management mode +rm -rf "MRC_BUILD_DIR" +xcodebuild -target "$TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" > /dev/null + +# Build in ARC mode +rm -rf "ARC_BUILD_DIR" +xcodebuild -target "$TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null + +# Run tests +runTests $MRC_PRODUCT "WebServer" +runTests $ARC_PRODUCT "WebServer" + +# Done +echo "\nAll tests completed successfully!" diff --git a/Tests/Payload.zip b/Tests/Payload.zip new file mode 100644 index 0000000..0157687 Binary files /dev/null and b/Tests/Payload.zip differ diff --git a/Tests/WebServer/001-200.response b/Tests/WebServer/001-200.response new file mode 100644 index 0000000..e786789 --- /dev/null +++ b/Tests/WebServer/001-200.response @@ -0,0 +1,16 @@ +HTTP/1.1 200 OK +Cache-Control: no-cache +Content-Length: 221 +Content-Type: text/html; charset=utf-8 +Connection: Close +Server: GCDWebServer +Date: Fri, 11 Apr 2014 02:42:22 GMT + + + + + diff --git a/Tests/WebServer/001-GET.request b/Tests/WebServer/001-GET.request new file mode 100644 index 0000000..e310d25 --- /dev/null +++ b/Tests/WebServer/001-GET.request @@ -0,0 +1,9 @@ +GET / HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +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/33.0.1750.152 Safari/537.36 +DNT: 1 +Accept-Encoding: gzip,deflate,sdch +Accept-Language: en-US,en;q=0.8,fr;q=0.6 + diff --git a/Tests/WebServer/002-200.response b/Tests/WebServer/002-200.response new file mode 100644 index 0000000..e5ef469 --- /dev/null +++ b/Tests/WebServer/002-200.response @@ -0,0 +1,14 @@ +HTTP/1.1 200 OK +Connection: Close +Server: GCDWebServer +Content-Type: text/plain +Last-Modified: Thu, 10 Apr 2014 11:10:14 GMT +Date: Fri, 11 Apr 2014 02:42:24 GMT +Accept-Ranges: bytes +Content-Length: 271 +Cache-Control: no-cache +Etag: 73212403/1397128214/0 + +For the colorful. + +Color is more than just a hue. It expresses a feeling. Makes a statement. Declares an allegiance. Color reveals your personality. iPhone 5c, in five anything-but-shy colors, does just that. It’s not just for lovers of color. It’s for the colorful. diff --git a/Tests/WebServer/002-GET.request b/Tests/WebServer/002-GET.request new file mode 100644 index 0000000..99f5fe7 --- /dev/null +++ b/Tests/WebServer/002-GET.request @@ -0,0 +1,10 @@ +GET /Copy.txt HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +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/33.0.1750.152 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 + diff --git a/Tests/WebServer/003-200.response b/Tests/WebServer/003-200.response new file mode 100644 index 0000000..638a6d2 --- /dev/null +++ b/Tests/WebServer/003-200.response @@ -0,0 +1,15 @@ +HTTP/1.1 200 OK +Cache-Control: no-cache +Content-Length: 218 +Content-Type: text/html; charset=utf-8 +Connection: Close +Server: GCDWebServer +Date: Fri, 11 Apr 2014 02:42:27 GMT + + + + + diff --git a/Tests/WebServer/003-GET.request b/Tests/WebServer/003-GET.request new file mode 100644 index 0000000..3ae76ee --- /dev/null +++ b/Tests/WebServer/003-GET.request @@ -0,0 +1,10 @@ +GET /images/ HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +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/33.0.1750.152 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 + diff --git a/Tests/WebServer/004-200.response b/Tests/WebServer/004-200.response new file mode 100644 index 0000000..aafbef8 Binary files /dev/null and b/Tests/WebServer/004-200.response differ diff --git a/Tests/WebServer/004-GET.request b/Tests/WebServer/004-GET.request new file mode 100644 index 0000000..ff8422f --- /dev/null +++ b/Tests/WebServer/004-GET.request @@ -0,0 +1,10 @@ +GET /images/capable_green_ipad_l.png HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +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/33.0.1750.152 Safari/537.36 +DNT: 1 +Referer: http://localhost:8080/images/ +Accept-Encoding: gzip,deflate,sdch +Accept-Language: en-US,en;q=0.8,fr;q=0.6 + diff --git a/Tests/WebServer/005-200.response b/Tests/WebServer/005-200.response new file mode 100644 index 0000000..4964ddc Binary files /dev/null and b/Tests/WebServer/005-200.response differ diff --git a/Tests/WebServer/005-GET.request b/Tests/WebServer/005-GET.request new file mode 100644 index 0000000..de90302 --- /dev/null +++ b/Tests/WebServer/005-GET.request @@ -0,0 +1,10 @@ +GET /images/hero_mba_11.jpg HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +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/33.0.1750.152 Safari/537.36 +DNT: 1 +Referer: http://localhost:8080/images/ +Accept-Encoding: gzip,deflate,sdch +Accept-Language: en-US,en;q=0.8,fr;q=0.6 + diff --git a/Tests/WebServer/006-304.response b/Tests/WebServer/006-304.response new file mode 100644 index 0000000..5207fc5 --- /dev/null +++ b/Tests/WebServer/006-304.response @@ -0,0 +1,7 @@ +HTTP/1.1 304 Not Modified +Last-Modified: Thu, 10 Apr 2014 21:46:56 GMT +Etag: 73209474/1397166416/0 +Connection: Close +Server: GCDWebServer +Date: Fri, 11 Apr 2014 02:42:34 GMT + diff --git a/Tests/WebServer/006-GET.request b/Tests/WebServer/006-GET.request new file mode 100644 index 0000000..9a666f9 --- /dev/null +++ b/Tests/WebServer/006-GET.request @@ -0,0 +1,12 @@ +GET /images/capable_green_ipad_l.png HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +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/33.0.1750.152 Safari/537.36 +DNT: 1 +Referer: http://localhost:8080/images/ +Accept-Encoding: gzip,deflate,sdch +Accept-Language: en-US,en;q=0.8,fr;q=0.6 +If-None-Match: 73209474/1397166416/0 +If-Modified-Since: Thu, 10 Apr 2014 21:46:56 GMT + diff --git a/Tests/WebServer/007-304.response b/Tests/WebServer/007-304.response new file mode 100644 index 0000000..c0ffdd2 --- /dev/null +++ b/Tests/WebServer/007-304.response @@ -0,0 +1,7 @@ +HTTP/1.1 304 Not Modified +Last-Modified: Thu, 10 Apr 2014 21:51:14 GMT +Etag: 73212154/1397166674/0 +Connection: Close +Server: GCDWebServer +Date: Fri, 11 Apr 2014 02:42:37 GMT + diff --git a/Tests/WebServer/007-GET.request b/Tests/WebServer/007-GET.request new file mode 100644 index 0000000..17d717e --- /dev/null +++ b/Tests/WebServer/007-GET.request @@ -0,0 +1,12 @@ +GET /images/hero_mba_11.jpg HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +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/33.0.1750.152 Safari/537.36 +DNT: 1 +Referer: http://localhost:8080/images/ +Accept-Encoding: gzip,deflate,sdch +Accept-Language: en-US,en;q=0.8,fr;q=0.6 +If-None-Match: 73212154/1397166674/0 +If-Modified-Since: Thu, 10 Apr 2014 21:51:14 GMT + diff --git a/Tests/WebServer/008-200.response b/Tests/WebServer/008-200.response new file mode 100644 index 0000000..3b27d34 --- /dev/null +++ b/Tests/WebServer/008-200.response @@ -0,0 +1,14 @@ +HTTP/1.1 200 OK +Cache-Control: no-cache +Content-Length: 199 +Content-Type: text/html; charset=utf-8 +Connection: Close +Server: GCDWebServer +Date: Fri, 11 Apr 2014 02:42:40 GMT + + + + + diff --git a/Tests/WebServer/008-GET.request b/Tests/WebServer/008-GET.request new file mode 100644 index 0000000..f7bfb55 --- /dev/null +++ b/Tests/WebServer/008-GET.request @@ -0,0 +1,10 @@ +GET /PDF%20Reports/ HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +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/33.0.1750.152 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 + diff --git a/Tests/WebServer/009-200.response b/Tests/WebServer/009-200.response new file mode 100644 index 0000000..0d94abd Binary files /dev/null and b/Tests/WebServer/009-200.response differ diff --git a/Tests/WebServer/009-GET.request b/Tests/WebServer/009-GET.request new file mode 100644 index 0000000..cc75d37 --- /dev/null +++ b/Tests/WebServer/009-GET.request @@ -0,0 +1,10 @@ +GET /PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +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/33.0.1750.152 Safari/537.36 +DNT: 1 +Referer: http://localhost:8080/PDF%20Reports/ +Accept-Encoding: gzip,deflate,sdch +Accept-Language: en-US,en;q=0.8,fr;q=0.6 + diff --git a/Tests/WebServer/010-304.response b/Tests/WebServer/010-304.response new file mode 100644 index 0000000..dbc7e32 --- /dev/null +++ b/Tests/WebServer/010-304.response @@ -0,0 +1,7 @@ +HTTP/1.1 304 Not Modified +Last-Modified: Wed, 01 May 2013 12:01:13 GMT +Etag: 73212107/1367409673/0 +Connection: Close +Server: GCDWebServer +Date: Fri, 11 Apr 2014 02:42:42 GMT + diff --git a/Tests/WebServer/010-GET.request b/Tests/WebServer/010-GET.request new file mode 100644 index 0000000..7d8867f --- /dev/null +++ b/Tests/WebServer/010-GET.request @@ -0,0 +1,13 @@ +GET /PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36 +Accept: */* +DNT: 1 +Referer: http://localhost:8080/PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf +Accept-Encoding: gzip,deflate,sdch +Accept-Language: en-US,en;q=0.8,fr;q=0.6 +Range: bytes=0-32767 +If-None-Match: 73212107/1367409673/0 +If-Modified-Since: Wed, 01 May 2013 12:01:13 GMT + diff --git a/Tests/WebServer/011-304.response b/Tests/WebServer/011-304.response new file mode 100644 index 0000000..dbc7e32 --- /dev/null +++ b/Tests/WebServer/011-304.response @@ -0,0 +1,7 @@ +HTTP/1.1 304 Not Modified +Last-Modified: Wed, 01 May 2013 12:01:13 GMT +Etag: 73212107/1367409673/0 +Connection: Close +Server: GCDWebServer +Date: Fri, 11 Apr 2014 02:42:42 GMT + diff --git a/Tests/WebServer/011-GET.request b/Tests/WebServer/011-GET.request new file mode 100644 index 0000000..f9bea7d --- /dev/null +++ b/Tests/WebServer/011-GET.request @@ -0,0 +1,13 @@ +GET /PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf HTTP/1.1 +Host: localhost:8080 +Connection: keep-alive +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36 +Accept: */* +DNT: 1 +Referer: http://localhost:8080/PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf +Accept-Encoding: gzip,deflate,sdch +Accept-Language: en-US,en;q=0.8,fr;q=0.6 +Range: bytes=32768-181951 +If-None-Match: 73212107/1367409673/0 +If-Modified-Since: Wed, 01 May 2013 12:01:13 GMT + diff --git a/Tests/WebServer/012-200.response b/Tests/WebServer/012-200.response new file mode 100644 index 0000000..b33aff2 --- /dev/null +++ b/Tests/WebServer/012-200.response @@ -0,0 +1,8 @@ +HTTP/1.1 200 OK +Cache-Control: no-cache +Content-Length: 221 +Content-Type: text/html; charset=utf-8 +Connection: Close +Server: GCDWebServer +Date: Fri, 11 Apr 2014 02:43:00 GMT + diff --git a/Tests/WebServer/012-HEAD.request b/Tests/WebServer/012-HEAD.request new file mode 100644 index 0000000..c870918 --- /dev/null +++ b/Tests/WebServer/012-HEAD.request @@ -0,0 +1,5 @@ +HEAD / HTTP/1.1 +User-Agent: curl/7.30.0 +Host: localhost:8080 +Accept: */* +