diff options
Diffstat (limited to 'Foundation/GTMHTTPServer.m')
-rw-r--r-- | Foundation/GTMHTTPServer.m | 584 |
1 files changed, 584 insertions, 0 deletions
diff --git a/Foundation/GTMHTTPServer.m b/Foundation/GTMHTTPServer.m new file mode 100644 index 0000000..daa6a4e --- /dev/null +++ b/Foundation/GTMHTTPServer.m @@ -0,0 +1,584 @@ +// +// GTMHTTPServer.m +// +// Copyright 2008 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 +// http://developer.apple.com/samplecode/CocoaHTTPServer/index.html +// + +#import <netinet/in.h> +#import <sys/socket.h> +#import <unistd.h> + +#define GTMHTTPSERVER_DEFINE_GLOBALS +#import "GTMHTTPServer.h" +#import "GTMDebugSelectorValidation.h" +#import "GTMGarbageCollection.h" +#import "GTMDefines.h" + +@interface GTMHTTPServer (PrivateMethods) +- (void)acceptedConnectionNotification:(NSNotification *)notification; +- (NSMutableDictionary *)newConnectionWithFileHandle:(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 (PrivateMethods) +- (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; + GTMAssertSelectorNilOrImplementedWithReturnTypeAndArguments(delegate_, + @selector(httpServer:handleRequest:), + // return type + @encode(GTMHTTPResponseMessage *), + // args + @encode(GTMHTTPServer *), + @encode(GTMHTTPRequestMessage *), + NULL); + localhostOnly_ = YES; + connections_ = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)dealloc { + [self stop]; + [super dealloc]; +} + +- (void)finalize { + [self stop]; + [super finalize]; +} + +- (id)delegate { + return delegate_; +} + +- (uint16_t)port { + return port_; +} + +- (void)setPort:(uint16_t)port { + port_ = port; +} + +- (BOOL)localhostOnly { + return localhostOnly_; +} + +- (void)setLocalhostOnly:(BOOL)yesno { + localhostOnly_ = yesno; +} + +- (BOOL)start:(NSError **)error { + _GTMDevAssert(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; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, + (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]; + _GTMDevAssert(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 newConnectionWithFileHandle:newConnection]; + [connections_ addObject:connDict]; +} + +- (NSMutableDictionary *)newConnectionWithFileHandle:(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 = [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; + } + + // 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]; + return; + } + + // 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]; + return; + } + + 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); + } + + if (!response) { + [self closeConnection:connDict]; + return; + } + + // 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]; +} + +- (NSMutableDictionary *)lookupConnection:(NSFileHandle *)fileHandle { + NSUInteger max = [connections_ count]; + for (NSUInteger x = 0; x < max; ++x) { + NSMutableDictionary *connDict = [connections_ objectAtIndex:x]; + if (fileHandle == [connDict objectForKey:kFileHandle]) { + return connDict; + } + } + return nil; +} + +- (void)closeConnection:(NSMutableDictionary *)connDict { + // remove the notification + NSFileHandle *connectionHandle = [connDict objectForKey:kFileHandle]; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center removeObserver:self + name:NSFileHandleReadCompletionNotification + object:connectionHandle]; + + // 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) { + // TODO: let the delegate know about the exception (but do it on the main + // thread) + _GTMDevLog(@"exception while sending reply: %@", e); + } + + // 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 { + CFRelease(message_); + [super dealloc]; +} + +- (NSString *)version { + return [GTMNSMakeCollectable(CFHTTPMessageCopyVersion(message_)) autorelease]; +} + +- (NSURL *)URL { + return [GTMNSMakeCollectable(CFHTTPMessageCopyRequestURL(message_)) autorelease]; +} + +- (NSString *)method { + return [GTMNSMakeCollectable(CFHTTPMessageCopyRequestMethod(message_)) autorelease]; +} + +- (NSData *)body { + return [GTMNSMakeCollectable(CFHTTPMessageCopyBody(message_)) autorelease]; +} + +- (NSDictionary *)allHeaderFieldValues { + return GTMNSMakeCollectable(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], [data length]) ? YES : NO; +} + +- (NSString *)headerFieldValueForKey:(NSString *)key { + CFStringRef value = NULL; + if (key) { + value = CFHTTPMessageCopyHeaderFieldValue(message_, (CFStringRef)key); + } + return [GTMNSMakeCollectable(value) autorelease]; +} + +- (UInt32)contentLength { + return [[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]; +} + +- (void)dealloc { + if (message_) { + CFRelease(message_); + } + [super dealloc]; +} + ++ (id)responseWithHTMLString:(NSString *)htmlString { + return [self responseWithBody:[htmlString dataUsingEncoding:NSUTF8StringEncoding] + contentType:@"text/html; charset=UTF-8" + statusCode:200]; +} + ++ (id)responseWithBody:(NSData *)body + contentType:(NSString *)contentType + statusCode:(int)statusCode { + return [[[[self class] alloc] initWithBody:body + contentType:contentType + statusCode:statusCode] autorelease]; +} + ++ (id)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); +} + +- (NSString *)description { + CFStringRef desc = CFCopyDescription(message_); + NSString *result = + [NSString stringWithFormat:@"%@<%p>{ message=%@ }", [self class], self, desc]; + CFRelease(desc); + return result; +} + +@end + +@implementation GTMHTTPResponseMessage (PrivateMethods) + +- (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; +} + +- (NSData *)serializedData { + return [GTMNSMakeCollectable(CFHTTPMessageCopySerializedMessage(message_)) autorelease]; +} + +@end |