| /* Copyright (c) 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 |
| // 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" |
| |
| // 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 (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; |
| |
| #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]; |
| } |
| |
| - (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; |
| } |
| |
| @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 GTMCFAutorelease(CFHTTPMessageCopySerializedMessage(message_)); |
| } |
| |
| @end |