From c586dc8747882770973b6488c9f5f9e6e3f08d6c Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 12 Jul 2018 12:03:50 -0700 Subject: Separate Xcode project and tests for GoogleUtilities (#1521) --- .../Tests/Network/third_party/GTMHTTPServer.m | 630 +++++++++++++++++++++ 1 file changed, 630 insertions(+) create mode 100644 GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.m (limited to 'GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.m') diff --git a/GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.m b/GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.m new file mode 100644 index 0000000..526d8c5 --- /dev/null +++ b/GoogleUtilities/Example/Tests/Network/third_party/GTMHTTPServer.m @@ -0,0 +1,630 @@ +/* Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Based a little on HTTPServer, part of the CocoaHTTPServer sample code found at +// https://opensource.apple.com/source/HTTPServer/HTTPServer-11/CocoaHTTPServer/ +// License for the CocoaHTTPServer sample code: +// +// Software License Agreement (BSD License) +// +// Copyright (c) 2011, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the +// following disclaimer. +// +// * Neither the name of Deusty nor the names of its +// contributors may be used to endorse or promote products +// derived from this software without specific prior +// written permission of Deusty, LLC. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// + +#import +#import +#import + +#define GTMHTTPSERVER_DEFINE_GLOBALS +#import "GTMHTTPServer.h" + +// avoid some of GTM's promiscuous dependencies +#ifndef _GTMDevLog +#define _GTMDevLog NSLog +#endif + +#ifndef GTM_STATIC_CAST +#define GTM_STATIC_CAST(type, object) ((type *)(object)) +#endif + +#ifndef GTMCFAutorelease +#define GTMCFAutorelease(x) ([(id)x autorelease]) +#endif + +@interface GTMHTTPServer (PrivateMethods) +- (void)acceptedConnectionNotification:(NSNotification *)notification; +- (NSMutableDictionary *)connectionWithFileHandle:(NSFileHandle *)fileHandle; +- (void)dataAvailableNotification:(NSNotification *)notification; +- (NSMutableDictionary *)lookupConnection:(NSFileHandle *)fileHandle; +- (void)closeConnection:(NSMutableDictionary *)connDict; +- (void)sendResponseOnNewThread:(NSMutableDictionary *)connDict; +- (void)sentResponse:(NSMutableDictionary *)connDict; +@end + +// keys for our connection dictionaries +static NSString *kFileHandle = @"FileHandle"; +static NSString *kRequest = @"Request"; +static NSString *kResponse = @"Response"; + +@interface GTMHTTPRequestMessage (PrivateHelpers) +- (BOOL)isHeaderComplete; +- (BOOL)appendData:(NSData *)data; +- (NSString *)headerFieldValueForKey:(NSString *)key; +- (UInt32)contentLength; +- (void)setBody:(NSData *)body; +@end + +@interface GTMHTTPResponseMessage () +- (id)initWithBody:(NSData *)body contentType:(NSString *)contentType statusCode:(int)statusCode; +- (NSData *)serializedData; +@end + +@implementation GTMHTTPServer + +- (id)init { + return [self initWithDelegate:nil]; +} + +- (id)initWithDelegate:(id)delegate { + self = [super init]; + if (self) { + if (!delegate) { + _GTMDevLog(@"missing delegate"); + [self release]; + return nil; + } + delegate_ = delegate; + +#ifndef NS_BLOCK_ASSERTIONS + BOOL isDelegateOK = [delegate_ respondsToSelector:@selector(httpServer:handleRequest:)]; + NSAssert(isDelegateOK, @"GTMHTTPServer delegate lacks handleRequest sel"); +#endif + + localhostOnly_ = YES; + connections_ = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)dealloc { + [self stop]; + [connections_ release]; + [super dealloc]; +} + +#if !TARGET_OS_IPHONE +- (void)finalize { + [self stop]; + [super finalize]; +} +#endif + +- (id)delegate { + return delegate_; +} + +- (uint16_t)port { + return port_; +} + +- (void)setPort:(uint16_t)port { + port_ = port; +} + +- (BOOL)reusePort { + return reusePort_; +} + +- (void)setReusePort:(BOOL)yesno { + reusePort_ = yesno; +} + +- (BOOL)localhostOnly { + return localhostOnly_; +} + +- (void)setLocalhostOnly:(BOOL)yesno { + localhostOnly_ = yesno; +} + +- (BOOL)start:(NSError **)error { + NSAssert(listenHandle_ == nil, @"start called when we already have a listenHandle_"); + + if (error) *error = NULL; + + NSInteger startFailureCode = 0; + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd <= 0) { + // COV_NF_START - we'd need to use up *all* sockets to test this? + startFailureCode = kGTMHTTPServerSocketCreateFailedError; + goto startFailed; + // COV_NF_END + } + + // enable address reuse quicker after we are done w/ our socket + int yes = 1; + int sock_opt = reusePort_ ? SO_REUSEPORT : SO_REUSEADDR; + if (setsockopt(fd, SOL_SOCKET, sock_opt, (void *)&yes, (socklen_t)sizeof(yes)) != 0) { + _GTMDevLog(@"failed to mark the socket as reusable"); // COV_NF_LINE + } + + // bind + struct sockaddr_in addr; + bzero(&addr, sizeof(addr)); + addr.sin_len = sizeof(addr); + addr.sin_family = AF_INET; + addr.sin_port = htons(port_); + if (localhostOnly_) { + addr.sin_addr.s_addr = htonl(0x7F000001); + } else { + // COV_NF_START - testing this could cause a leopard firewall prompt during tests. + addr.sin_addr.s_addr = htonl(INADDR_ANY); + // COV_NF_END + } + if (bind(fd, (struct sockaddr *)(&addr), (socklen_t)sizeof(addr)) != 0) { + startFailureCode = kGTMHTTPServerBindFailedError; + goto startFailed; + } + + // collect the port back out + if (port_ == 0) { + socklen_t len = (socklen_t)sizeof(addr); + if (getsockname(fd, (struct sockaddr *)(&addr), &len) == 0) { + port_ = ntohs(addr.sin_port); + } + } + + // tell it to listen for connections + if (listen(fd, 5) != 0) { + // COV_NF_START + startFailureCode = kGTMHTTPServerListenFailedError; + goto startFailed; + // COV_NF_END + } + + // now use a filehandle to accept connections + listenHandle_ = [[NSFileHandle alloc] initWithFileDescriptor:fd closeOnDealloc:YES]; + if (listenHandle_ == nil) { + // COV_NF_START - we'd need to run out of memory to test this? + startFailureCode = kGTMHTTPServerHandleCreateFailedError; + goto startFailed; + // COV_NF_END + } + + // setup notifications for connects + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(acceptedConnectionNotification:) + name:NSFileHandleConnectionAcceptedNotification + object:listenHandle_]; + [listenHandle_ acceptConnectionInBackgroundAndNotify]; + + // TODO: maybe hit the delegate incase it wants to register w/ NSNetService, + // or just know we're up and running? + + return YES; + +startFailed: + if (error) { + *error = [[[NSError alloc] initWithDomain:kGTMHTTPServerErrorDomain + code:startFailureCode + userInfo:nil] autorelease]; + } + if (fd > 0) { + close(fd); + } + return NO; +} + +- (void)stop { + if (listenHandle_) { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center removeObserver:self + name:NSFileHandleConnectionAcceptedNotification + object:listenHandle_]; + [listenHandle_ release]; + listenHandle_ = nil; + // TODO: maybe hit the delegate in case it wants to unregister w/ + // NSNetService, or just know we've stopped running? + } + [connections_ removeAllObjects]; +} + +- (NSUInteger)activeRequestCount { + return [connections_ count]; +} + +- (NSString *)description { + NSString *result = + [NSString stringWithFormat:@"%@<%p>{ port=%d localHostOnly=%@ status=%@ }", [self class], + self, port_, (localhostOnly_ ? @"YES" : @"NO"), + (listenHandle_ != nil ? @"Started" : @"Stopped")]; + return result; +} + +@end + +@implementation GTMHTTPServer (PrivateMethods) + +- (void)acceptedConnectionNotification:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + NSFileHandle *newConnection = [userInfo objectForKey:NSFileHandleNotificationFileHandleItem]; + NSAssert1(newConnection != nil, @"failed to get the connection in the notification: %@", + notification); + + // make sure we accept more... + [listenHandle_ acceptConnectionInBackgroundAndNotify]; + + // TODO: could let the delegate look at the address, before we start working + // on it. + + NSMutableDictionary *connDict = [self connectionWithFileHandle:newConnection]; + [connections_ addObject:connDict]; +} + +- (NSMutableDictionary *)connectionWithFileHandle:(NSFileHandle *)fileHandle { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + + [result setObject:fileHandle forKey:kFileHandle]; + + GTMHTTPRequestMessage *request = [[[GTMHTTPRequestMessage alloc] init] autorelease]; + [result setObject:request forKey:kRequest]; + + // setup for data notifications + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(dataAvailableNotification:) + name:NSFileHandleReadCompletionNotification + object:fileHandle]; + [fileHandle readInBackgroundAndNotify]; + + return result; +} + +- (void)dataAvailableNotification:(NSNotification *)notification { + NSFileHandle *connectionHandle = GTM_STATIC_CAST(NSFileHandle, [notification object]); + NSMutableDictionary *connDict = [self lookupConnection:connectionHandle]; + if (connDict == nil) return; // we are no longer tracking this one + + NSDictionary *userInfo = [notification userInfo]; + NSData *readData = [userInfo objectForKey:NSFileHandleNotificationDataItem]; + if ([readData length] == 0) { + // remote side closed + [self closeConnection:connDict]; + return; + } + + // Use a local pool to keep memory down incase the runloop we're in doesn't + // drain until it gets a UI event. + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + @try { + // Like Apple's sample, we just keep adding data until we get a full header + // and any referenced body. + + GTMHTTPRequestMessage *request = [connDict objectForKey:kRequest]; + [request appendData:readData]; + + // Is the header complete yet? + if (![request isHeaderComplete]) { + // more data... + [connectionHandle readInBackgroundAndNotify]; + } else { + // Do we have all the body? + UInt32 contentLength = [request contentLength]; + NSData *body = [request body]; + NSUInteger bodyLength = [body length]; + if (contentLength > bodyLength) { + // need more data... + [connectionHandle readInBackgroundAndNotify]; + } else { + if (contentLength < bodyLength) { + // We got extra (probably someone trying to pipeline on us), trim + // and let the extra data go... + NSData *newBody = [NSData dataWithBytes:[body bytes] length:contentLength]; + [request setBody:newBody]; + _GTMDevLog(@"Got %lu extra bytes on http request, ignoring them", + (unsigned long)(bodyLength - contentLength)); + } + + GTMHTTPResponseMessage *response = nil; + @try { + // Off to the delegate + response = [delegate_ httpServer:self handleRequest:request]; + } @catch (NSException *e) { + _GTMDevLog(@"Exception trying to handle http request: %@", e); + } // COV_NF_LINE - radar 5851992 only reachable w/ an uncaught exception which isn't + // testable + + if (response) { + // We don't support connection reuse, so we add (force) the header to + // close every connection. + [response setValue:@"close" forHeaderField:@"Connection"]; + + // spawn thread to send reply (since we do a blocking send) + [connDict setObject:response forKey:kResponse]; + [NSThread detachNewThreadSelector:@selector(sendResponseOnNewThread:) + toTarget:self + withObject:connDict]; + } else { + // No response, shut it down + [self closeConnection:connDict]; + } + } + } + } @catch (NSException *e) { // COV_NF_START + _GTMDevLog(@"exception while read data: %@", e); + // exception while dealing with the connection, close it + } // COV_NF_END + @finally { + [pool drain]; + } +} + +- (NSMutableDictionary *)lookupConnection:(NSFileHandle *)fileHandle { + NSMutableDictionary *result = nil; + for (NSMutableDictionary *connDict in connections_) { + if (fileHandle == [connDict objectForKey:kFileHandle]) { + result = connDict; + break; + } + } + return result; +} + +- (void)closeConnection:(NSMutableDictionary *)connDict { + // remove the notification + NSFileHandle *connectionHandle = [connDict objectForKey:kFileHandle]; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center removeObserver:self name:NSFileHandleReadCompletionNotification object:connectionHandle]; + // in a non GC world, we're fine just letting the connect get closed when + // the object is release when it comes out of connections_, but in a GC world + // it won't get cleaned up + [connectionHandle closeFile]; + + // remove it from the list + [connections_ removeObject:connDict]; +} + +- (void)sendResponseOnNewThread:(NSMutableDictionary *)connDict { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + @try { + GTMHTTPResponseMessage *response = [connDict objectForKey:kResponse]; + NSFileHandle *connectionHandle = [connDict objectForKey:kFileHandle]; + NSData *serialized = [response serializedData]; + [connectionHandle writeData:serialized]; + } @catch (NSException *e) { // COV_NF_START - causing an exception here is to hard in a test + // TODO: let the delegate know about the exception (but do it on the main + // thread) + _GTMDevLog(@"exception while sending reply: %@", e); + } // COV_NF_END + + // back to the main thread to close things down + [self performSelectorOnMainThread:@selector(sentResponse:) withObject:connDict waitUntilDone:NO]; + + [pool release]; +} + +- (void)sentResponse:(NSMutableDictionary *)connDict { + // make sure we're still tracking this connection (in case server was stopped) + NSFileHandle *connection = [connDict objectForKey:kFileHandle]; + NSMutableDictionary *connDict2 = [self lookupConnection:connection]; + if (connDict != connDict2) return; + + // TODO: message the delegate that it was sent + + // close it down + [self closeConnection:connDict]; +} + +@end + +#pragma mark - + +@implementation GTMHTTPRequestMessage + +- (id)init { + self = [super init]; + if (self) { + message_ = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, YES); + } + return self; +} + +- (void)dealloc { + if (message_) { + CFRelease(message_); + } + [super dealloc]; +} + +- (NSString *)version { + return GTMCFAutorelease(CFHTTPMessageCopyVersion(message_)); +} + +- (NSURL *)URL { + return GTMCFAutorelease(CFHTTPMessageCopyRequestURL(message_)); +} + +- (NSString *)method { + return GTMCFAutorelease(CFHTTPMessageCopyRequestMethod(message_)); +} + +- (NSData *)body { + return GTMCFAutorelease(CFHTTPMessageCopyBody(message_)); +} + +- (NSDictionary *)allHeaderFieldValues { + return GTMCFAutorelease(CFHTTPMessageCopyAllHeaderFields(message_)); +} + +- (NSString *)description { + CFStringRef desc = CFCopyDescription(message_); + NSString *result = [NSString stringWithFormat:@"%@<%p>{ message=%@ }", [self class], self, desc]; + CFRelease(desc); + return result; +} + +@end + +@implementation GTMHTTPRequestMessage (PrivateHelpers) + +- (BOOL)isHeaderComplete { + return CFHTTPMessageIsHeaderComplete(message_) ? YES : NO; +} + +- (BOOL)appendData:(NSData *)data { + return CFHTTPMessageAppendBytes(message_, [data bytes], (CFIndex)[data length]) ? YES : NO; +} + +- (NSString *)headerFieldValueForKey:(NSString *)key { + CFStringRef value = NULL; + if (key) { + value = CFHTTPMessageCopyHeaderFieldValue(message_, (CFStringRef)key); + } + return GTMCFAutorelease(value); +} + +- (UInt32)contentLength { + return (UInt32)[[self headerFieldValueForKey:@"Content-Length"] intValue]; +} + +- (void)setBody:(NSData *)body { + if (!body) { + body = [NSData data]; // COV_NF_LINE - can only happen in we fail to make the new data object + } + CFHTTPMessageSetBody(message_, (CFDataRef)body); +} + +@end + +#pragma mark - + +@implementation GTMHTTPResponseMessage + +- (id)init { + return [self initWithBody:nil contentType:nil statusCode:0]; +} + +- (id)initWithBody:(NSData *)body contentType:(NSString *)contentType statusCode:(int)statusCode { + self = [super init]; + if (self) { + if ((statusCode < 100) || (statusCode > 599)) { + [self release]; + return nil; + } + message_ = + CFHTTPMessageCreateResponse(kCFAllocatorDefault, statusCode, NULL, kCFHTTPVersion1_0); + if (!message_) { + // COV_NF_START + [self release]; + return nil; + // COV_NF_END + } + NSUInteger bodyLength = 0; + if (body) { + bodyLength = [body length]; + CFHTTPMessageSetBody(message_, (CFDataRef)body); + } + if ([contentType length] == 0) { + contentType = @"text/html"; + } + NSString *bodyLenStr = [NSString stringWithFormat:@"%lu", (unsigned long)bodyLength]; + [self setValue:bodyLenStr forHeaderField:@"Content-Length"]; + [self setValue:contentType forHeaderField:@"Content-Type"]; + } + return self; +} + +- (void)dealloc { + if (message_) { + CFRelease(message_); + } + [super dealloc]; +} + ++ (instancetype)responseWithString:(NSString *)plainText { + NSData *body = [plainText dataUsingEncoding:NSUTF8StringEncoding]; + return [self responseWithBody:body contentType:@"text/plain; charset=UTF-8" statusCode:200]; +} + ++ (instancetype)responseWithHTMLString:(NSString *)htmlString { + return [self responseWithBody:[htmlString dataUsingEncoding:NSUTF8StringEncoding] + contentType:@"text/html; charset=UTF-8" + statusCode:200]; +} + ++ (instancetype)responseWithBody:(NSData *)body + contentType:(NSString *)contentType + statusCode:(int)statusCode { + return [[[[self class] alloc] initWithBody:body contentType:contentType statusCode:statusCode] + autorelease]; +} + ++ (instancetype)emptyResponseWithCode:(int)statusCode { + return + [[[[self class] alloc] initWithBody:nil contentType:nil statusCode:statusCode] autorelease]; +} + +- (void)setValue:(NSString *)value forHeaderField:(NSString *)headerField { + if ([headerField length] == 0) return; + if (value == nil) { + value = @""; + } + CFHTTPMessageSetHeaderFieldValue(message_, (CFStringRef)headerField, (CFStringRef)value); +} + +- (void)setHeaderValuesFromDictionary:(NSDictionary *)dict { + for (id key in dict) { + id value = [dict valueForKey:key]; + [self setValue:value forHeaderField:key]; + } +} + +- (NSString *)description { + CFStringRef desc = CFCopyDescription(message_); + NSString *result = [NSString stringWithFormat:@"%@<%p>{ message=%@ }", [self class], self, desc]; + CFRelease(desc); + return result; +} + +- (NSData *)serializedData { + return GTMCFAutorelease(CFHTTPMessageCopySerializedMessage(message_)); +} + +@end -- cgit v1.2.3