| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- /* 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 <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 ()
- - (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
|