/* * Copyright (c) 2005 Apple Computer, Inc. All rights reserved. * * @APPLE_LICENSE_HEADER_START@ * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ */ /* * CFHTTPServer.c * CFNetwork * * Copyright (c) 2003 Apple Computer, Inc. All rights reserved. * */ #pragma mark Description /* The http server is comprised of two objects: a HttpServer and a HttpConnection. The HttpServer is primarily responsible for listening and accepting new connections. As individual connections are established, HttpConnections are created in order to maintain that single connection's instance. The HttpServer then maintains the list of these HttpConnection's. Individual requests and responses are handled on a per-HttpConnection basis. Although handled on individual HttpConnection's, clients adding responses for a given request do so through the HttpServer's interface. The HttpServer will then pair the response to the request on the proper HttpConnection instance. All responses are sent out in the order in which the requests were received. A set of responses can be held up should a response not be available for the head queued item. This can take place since responses are not required to be queued at the moment that a request is received. When the response is added to the queue, two items are used. First there is the queue of ordered requests on the connection. This ordered array is maintained in order to send the responses in the correct order. The second part is a dictionary mapping an individual request to its response. A response is comprised of a set of headers (CFHTTPMessageRef) and a stream (CFReadStreamRef) which acts as the body. Every outgoing response has both of these elements. This means that _CFHTTPServerAddResponse creates a stream for the body of the response and then calls _CFHTTPServerAddStreamedResponse. Some cheap object model: ------------ maintains ---------------- receives --------- |HttpServer|------------@|HttpConnection|----------@|request| ------------ ---------------- --------- | | @ vends | ---------- | |response|----------------- ---------- */ #pragma mark - #pragma mark Includes #include #include #include #if !defined(__WIN32__) #include #include #else #include #define SOCK_MAXADDRLEN 255 #endif #if 0 #pragma mark - #pragma mark Constant Strings #endif #ifdef __CONSTANT_CFSTRINGS__ #define _kCFHTTPServerDescribeFormat CFSTR("{server=%@, connections=%@, info=%@}") #define _kCFHTTPServerPtrFormat CFSTR("<0x%x>") #define _kCFHTTPServerContentLengthHeader CFSTR("Content-length") #define _kCFHTTPServerContentLengthFormat CFSTR("%d") #define _kCFHTTPServerConnectionDescribeFormat CFSTR("<_HttpConnection 0x%x>{server=0x%x, timer=%@, inStream=%@, outStream=%@, responses=%@, requests=%@, buffered=%@}") #define _kCFHTTPServerTransferEncodingHeader CFSTR("Transfer-Encoding") #define _kCFHTTPServerTransferEncodingChunked CFSTR("chunked") #define _kCFHTTPServerConnectionHeader CFSTR("Connection") #define _kCFHTTPServerConnectionClose CFSTR("close") #else static CONST_STRING_DECL(_kCFHTTPServerDescribeFormat, "{server=%@, connections=%@, info=%@}") static CONST_STRING_DECL(_kCFHTTPServerPtrFormat, "<0x%x>") static CONST_STRING_DECL(_kCFHTTPServerContentLengthHeader, "Content-length") static CONST_STRING_DECL(_kCFHTTPServerContentLengthFormat, "%d") static CONST_STRING_DECL(_kCFHTTPServerConnectionDescribeFormat, "<_HttpConnection 0x%x>{server=0x%x, timer=%@, inStream=%@, outStream=%@, responses=%@, requests=%@, buffered=%@}") static CONST_STRING_DECL(_kCFHTTPServerTransferEncodingHeader, "Transfer-Encoding") static CONST_STRING_DECL(_kCFHTTPServerTransferEncodingChunked, "chunked") static CONST_STRING_DECL(_kCFHTTPServerConnectionHeader, "Connection") static CONST_STRING_DECL(_kCFHTTPServerConnectionClose, "close") #endif /* __CONSTANT_CFSTRINGS__ */ #pragma mark - #pragma mark Type Declarations typedef struct { CFRuntimeBase _base; // CFRuntimeBase for CF types _CFServerRef _server; // Underlying server object. CFMutableArrayRef _connections; // All outstanding HttpConnection's _CFHTTPServerCallBacks _callbacks; // Callback functions for user _CFHTTPServerContext _ctxt; // User's context for callback } HttpServer; typedef struct { CFAllocatorRef _alloc; // Allocator used to allocate this UInt32 _rc; // Number of times retained. HttpServer* _server; // Reference back to the owning server context. CFDataRef _peer; // Peer's address CFRunLoopTimerRef _timer; // Timer for controlling timeouts CFReadStreamRef _inStream; // Incoming data stream CFWriteStreamRef _outStream; // Outgoing data stream CFMutableDictionaryRef _responses; // Responses keyed by their requests CFMutableArrayRef _requests; // Ordered incoming requests CFMutableDataRef _bufferedBytes; // Bytes bound for delivery but not yet sent } HttpConnection; #pragma mark - #pragma mark Static Function Declarations // Functions for HttpServer object static void _HttpServerRelease(_CFHTTPServerRef server); static CFStringRef _HttpServerCopyDescription(_CFHTTPServerRef server); // Functions for HttpConnection object static HttpConnection* _HttpConnectionCreate(CFAllocatorRef alloc, HttpServer* server, CFSocketNativeHandle s); static HttpConnection* _HttpConnectionRetain(HttpConnection* connection); static void _HttpConnectionRelease(HttpConnection* connection); static CFStringRef _HttpConnectionCopyDescription(HttpConnection* connection); // Handlers for HttpConnection object static void _HttpConnectionHandleRequest(HttpConnection* connection); static void _HttpConnectionHandleHasBytesAvailable(HttpConnection* connection); static void _HttpConnectionHandleCanAcceptBytes(HttpConnection* connection); static void _HttpConnectionHandleErrorOccurred(HttpConnection* connection, const CFStreamError* error); static void _HttpConnectionHandleTimeOut(HttpConnection* connection); static const void* _ArrayRetainCallBack(CFAllocatorRef allocator, const HttpConnection* connection); static void _ArrayReleaseCallBack(CFAllocatorRef allocator, const HttpConnection* connection); // CFType callbacks -- call into HttpConnection's handlers static void _ReadStreamCallBack(CFReadStreamRef inStream, CFStreamEventType type, HttpConnection* connection); static void _WriteStreamCallBack(CFWriteStreamRef outStream, CFStreamEventType type, HttpConnection* connection); static void _TimerCallBack(CFRunLoopTimerRef timer, HttpConnection* connection); // Functions for manipulating HttpServer's array of HttpConnection's static void _HttpServerAddConnection(HttpServer* server, HttpConnection* connection); static void _HttpServerRemoveConnection(HttpServer* server, HttpConnection* connection); // Handlers for HttpServer object static void _HttpServerHandleNewConnection(HttpServer* server, CFSocketNativeHandle sock); static void _HttpServerHandleError(HttpServer* server, const CFStreamError* error); // Server callback -- call into HttpServer's handlers static void _ServerCallBack(_CFServerRef server, CFSocketNativeHandle sock, const CFStreamError* error, HttpServer* httpServer); // General use function static CFNumberRef _CFNumberCreateWithString(CFAllocatorRef allocator, CFStringRef string); #if 0 #pragma mark - #pragma mark Extern Function Declarations #endif extern void _CFSocketStreamCreatePair(CFAllocatorRef alloc, CFStringRef host, UInt32 port, CFSocketNativeHandle s, const CFSocketSignature* sig, CFReadStreamRef* readStream, CFWriteStreamRef* writeStream); #pragma mark - #pragma mark Static Variable Definitions // A shorter timeout should be used for a more heavily used server. #define kTimeOutInSeconds ((CFTimeInterval)60.0) #define kBufferSize ((CFIndex)8192) #define kReadEvents ((CFOptionFlags)(kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred)) #define kWriteEvents ((CFOptionFlags)(kCFStreamEventCanAcceptBytes | kCFStreamEventErrorOccurred)) static CFTypeID _HttpServerTypeId = _kCFRuntimeNotATypeID; #pragma mark - #pragma mark Extern Function Definitions (API) /* CF_EXPORT */ CFTypeID _CFHTTPServerGetTypeID(void) { if (_HttpServerTypeId == _kCFRuntimeNotATypeID) { static const CFRuntimeClass HttpServerClass = { 0, // version "_CFHTTPServer", // class name NULL, // init NULL, // copy (void(*)(CFTypeRef))_HttpServerRelease, // finalize NULL, // equal NULL, // hash NULL, // copy formatting description (CFStringRef(*)(CFTypeRef))_HttpServerCopyDescription // copy debug description }; _HttpServerTypeId = _CFRuntimeRegisterClass(&HttpServerClass); } return _HttpServerTypeId; } /* CF_EXPORT */ _CFHTTPServerRef _CFHTTPServerCreate(CFAllocatorRef alloc, const _CFHTTPServerCallBacks* callbacks, _CFHTTPServerContext* context) { HttpServer* server = NULL; do { _CFServerContext ctxt = { 0, NULL, (CFAllocatorRetainCallBack)CFRetain, (CFAllocatorReleaseCallBack)CFRelease, (CFAllocatorCopyDescriptionCallBack)CFCopyDescription }; CFArrayCallBacks arrayCallBacks = { 0, (CFArrayRetainCallBack)_ArrayRetainCallBack, (CFArrayReleaseCallBack)_ArrayReleaseCallBack, (CFArrayCopyDescriptionCallBack)_HttpConnectionCopyDescription, NULL // Default pointer comparison }; CFTypeID id = _CFHTTPServerGetTypeID(); // Ask CF to allocate the instance and then return it. if (id != _kCFRuntimeNotATypeID) { server = (HttpServer*)_CFRuntimeCreateInstance(alloc, id, sizeof(HttpServer) - sizeof(CFRuntimeBase), NULL); } // Fail if unable to create the server if (server == NULL) break; server->_server = NULL; server->_connections = NULL; memset(&server->_callbacks, 0, sizeof(server->_callbacks)); memset(&server->_ctxt, 0, sizeof(server->_ctxt)); // Set the info on the callback context ctxt.info = server; // Create the server server->_server = _CFServerCreate(alloc, (_CFServerCallBack)_ServerCallBack, &ctxt); // Require server in order to create. if (server->_server == NULL) break; server->_connections = CFArrayCreateMutable(alloc, 0, &arrayCallBacks); // Require the list of outstanding Http connections if (server->_connections == NULL) break; // Save the user's callbacks and context. memcpy(&(server->_callbacks), callbacks, sizeof(server->_callbacks)); memcpy(&(server->_ctxt), context, sizeof(server->_ctxt)); // If there is info and a retain function, retain the info. if (server->_ctxt.info && server->_ctxt.retain) server->_ctxt.info = (void *)(server->_ctxt.retain(server->_ctxt.info)); return (_CFHTTPServerRef)server; } while (0); // Something failed, so clean up. if (server) { _CFHTTPServerInvalidate((_CFHTTPServerRef)server); CFRelease((_CFHTTPServerRef)server); } return NULL; } /* static */ void _HttpServerRelease(_CFHTTPServerRef server) { // Invalidate the server which will release server and outstanding connections. _CFHTTPServerInvalidate(server); } /* static */ CFStringRef _HttpServerCopyDescription(_CFHTTPServerRef server) { CFStringRef info, result, serverDescription = NULL; HttpServer* s = (HttpServer*)server; CFAllocatorRef alloc = CFGetAllocator(server); if (s->_server) serverDescription = CFCopyDescription(s->_server); // Set the user's context based upon supplied "copyDescription" if (s->_ctxt.copyDescription) info = s->_ctxt.copyDescription(s->_ctxt.info); else info = CFStringCreateWithFormat(alloc, NULL, _kCFHTTPServerPtrFormat, (UInt32)(s->_ctxt.info)); // Create the debug string result = CFStringCreateWithFormat(alloc, NULL, _kCFHTTPServerDescribeFormat, (UInt32)s, serverDescription, s->_connections, info); if (serverDescription) CFRelease(serverDescription); CFRelease(info); return result; } /* CF_EXPORT */ Boolean _CFHTTPServerStart(_CFHTTPServerRef server, CFStringRef name, CFStringRef type, UInt32 port) { HttpServer* s = (HttpServer*)server; // Nothing special needed for the HTTP server. return _CFServerStart(s->_server, name, type, port); } /* CF_EXPORT */ void _CFHTTPServerInvalidate(_CFHTTPServerRef server) { HttpServer* s = (HttpServer*)server; // Release the user's context info pointer. if (s->_ctxt.info && s->_ctxt.release) s->_ctxt.release(s->_ctxt.info); // Clear out the context, so nothing can be called. memset(&(s->_ctxt), 0, sizeof(s->_ctxt)); // Guarantee that there will be no user callbacks. memset(&s->_callbacks, 0, sizeof(s->_callbacks)); // Close out any outstanding connections. if (s->_connections) { CFRelease(s->_connections); s->_connections = NULL; } // If the server has been created, invalidate it and delete it. if (s->_server) { _CFServerInvalidate(s->_server); CFRelease(s->_server); s->_server = NULL; } } /* CF_EXPORT */ UInt32 _CFHTTPServerGetPort(_CFHTTPServerRef server) { return ((HttpServer*)server)->_server ? _CFServerGetPort(((HttpServer*)server)->_server) : 0; } /* CF_EXPORT */ CFDataRef _CFHTTPServerCopyPeerAddressForRequest(_CFHTTPServerRef server, CFHTTPMessageRef request) { CFIndex i, count; HttpServer* s = (HttpServer*)server; // Prepare to look for the given request in the connections count = CFArrayGetCount(s->_connections); // Start the search for (i = 0; i < count; i++) { // **FIXME** This is somewhat incestuous. The server should not be reaching // into the connections. There should really be a HttpConnection method for // adding a response. // Pull out the current connection HttpConnection* c = (HttpConnection*)CFArrayGetValueAtIndex(s->_connections, i); // Check to see if the connection knows of the request CFIndex j = CFArrayGetFirstIndexOfValue(c->_requests, CFRangeMake(0, CFArrayGetCount(c->_requests)), request); // Handle the response if it was found if (j != kCFNotFound) { // return the copy that was found return (c->_peer == NULL) ? NULL : CFDataCreateCopy(CFGetAllocator(server), c->_peer); } } return NULL; } /* CF_EXPORT */ void _CFHTTPServerAddResponse(_CFHTTPServerRef server, CFHTTPMessageRef request, CFHTTPMessageRef response) { CFDataRef body; UInt8* bytes; CFReadStreamRef stream; CFIndex length; CFStringRef contentLength; CFAllocatorRef alloc = CFGetAllocator(server); // Make a copy of the response response = CFHTTPMessageCreateCopy(alloc, response); // Get the body and its length body = CFHTTPMessageCopyBody(response); if (body == NULL) body = CFDataCreate(alloc, NULL, 0); length = CFDataGetLength(body); // Pull the body off the response since the stream will be used CFHTTPMessageSetBody(response, NULL); // Allocate the buffer for the body bytes = (UInt8*)CFAllocatorAllocate(alloc, length, 0); // Copy the body into the buffer for streaming memmove(bytes, CFDataGetBytePtr(body), length); // Don't need the body anymore CFRelease(body); // Create the stream for the body stream = CFReadStreamCreateWithBytesNoCopy(alloc, bytes, length, alloc); // Check to see if there is a content length header. contentLength = CFHTTPMessageCopyHeaderFieldValue(response, _kCFHTTPServerContentLengthHeader); // If not, add one. if (contentLength == NULL) { // Create the header value with the length contentLength = CFStringCreateWithFormat(alloc, NULL, _kCFHTTPServerContentLengthFormat, length); // Add the header CFHTTPMessageSetHeaderFieldValue(response, _kCFHTTPServerContentLengthHeader, contentLength); } CFRelease(contentLength); // Add the streamed response _CFHTTPServerAddStreamedResponse(server, request, response, stream); // No longer needed now that it's in the queue CFRelease(stream); CFRelease(response); } /* CF_EXPORT */ void _CFHTTPServerAddStreamedResponse(_CFHTTPServerRef server, CFHTTPMessageRef request, CFHTTPMessageRef response, CFReadStreamRef body) { CFArrayRef list; CFIndex i, count; HttpServer* s = (HttpServer*)server; CFAllocatorRef alloc = CFGetAllocator(server); // Things to be put into the response list for a request CFTypeRef objs[] = {NULL, body}; // Create a copy 'cause it may need adjustment objs[0] = CFHTTPMessageCreateCopy(alloc, response); // Create the response list for the request list = CFArrayCreate(alloc, objs, sizeof(objs) / sizeof(objs[0]), &kCFTypeArrayCallBacks); // Prepare to look for the given request in the connections count = CFArrayGetCount(s->_connections); // Start the search for (i = 0; i < count; i++) { // **FIXME** This is somewhat incestuous. The server should not be reaching // into the connections. There should really be a HttpConnection method for // adding a response. // Pull out the current connection HttpConnection* c = (HttpConnection*)CFArrayGetValueAtIndex(s->_connections, i); // Check to see if the connection knows of the request CFIndex j = CFArrayGetFirstIndexOfValue(c->_requests, CFRangeMake(0, CFArrayGetCount(c->_requests)), request); // Handle the response if it was found if (j != kCFNotFound) { // Add the response list to the connection for the given request CFDictionaryAddValue(c->_responses, request, list); // If the request was the head of the request queue and the stream can send, pump it. if ((j == 0) && CFWriteStreamCanAcceptBytes(c->_outStream)) _HttpConnectionHandleCanAcceptBytes(c); // Everything has been handled break; } } // List has been handled, so it's not needed anymore. CFRelease(list); CFRelease(objs[0]); } #pragma mark - #pragma mark Static Function Definitions /* static */ HttpConnection* _HttpConnectionCreate(CFAllocatorRef alloc, HttpServer* server, CFSocketNativeHandle s) { HttpConnection* connection = NULL; do { uint8_t name[SOCK_MAXADDRLEN]; int namelen = sizeof(name); CFRunLoopRef rl = CFRunLoopGetCurrent(); CFRunLoopTimerContext timerCtxt = { 0, NULL, NULL, NULL, (CFStringRef (*)(const void*))_HttpConnectionCopyDescription }; CFStreamClientContext streamCtxt = { 0, NULL, NULL, NULL, (CFStringRef (*)(void*))_HttpConnectionCopyDescription }; // Allocate the buffer for the connection. connection = CFAllocatorAllocate(alloc, sizeof(connection[0]), 0); // Fail if unable to create the connection if (connection == NULL) break; memset(connection, 0, sizeof(connection[0])); // Save the allocator for deallocating later. connection->_alloc = alloc ? CFRetain(alloc) : NULL; // Bump the retain count. _HttpConnectionRetain(connection); // Make sure the server is saved for the callback. connection->_server = (HttpServer*)CFRetain((_CFHTTPServerRef)server); if (0 == getpeername(s, (struct sockaddr *)name, &namelen)) connection->_peer = CFDataCreate(alloc, name, namelen); // Set the info pointer for the contexts to be the connection. timerCtxt.info = connection; streamCtxt.info = connection; // Create the timer for detecting dead connections connection->_timer = CFRunLoopTimerCreate(alloc, CFAbsoluteTimeGetCurrent() + kTimeOutInSeconds, kTimeOutInSeconds, 0, 0, (CFRunLoopTimerCallBack)_TimerCallBack, &timerCtxt); // Make sure it succeeded if (connection->_timer == NULL) break; // Add the timer to the run loop CFRunLoopAddTimer(rl, connection->_timer, kCFRunLoopCommonModes); // Create a pair of streams for performing HTTP. _CFSocketStreamCreatePair(alloc, NULL, 0, s, NULL, &(connection->_inStream), &(connection->_outStream)); // Make sure both were created if ((connection->_inStream == NULL) || (connection->_outStream == NULL)) break; // Relinquish the socket to the streams CFReadStreamSetProperty(connection->_inStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); CFWriteStreamSetProperty(connection->_outStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); // Set the client to for each of the streams and for the proper events. CFReadStreamSetClient(connection->_inStream, kReadEvents, (CFReadStreamClientCallBack)_ReadStreamCallBack, &streamCtxt); CFWriteStreamSetClient(connection->_outStream, kWriteEvents, (CFWriteStreamClientCallBack)_WriteStreamCallBack, &streamCtxt); // Schedule both on the run loop CFReadStreamScheduleWithRunLoop(connection->_inStream, rl, kCFRunLoopCommonModes); CFWriteStreamScheduleWithRunLoop(connection->_outStream, rl, kCFRunLoopCommonModes); // Open up the streams CFReadStreamOpen(connection->_inStream); CFWriteStreamOpen(connection->_outStream); // Create the dictionary mapping requests to responses connection->_responses = CFDictionaryCreateMutable(alloc, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); // Make sure it worked if (connection->_responses == NULL) break; // Create the list of all outstanding, incoming requests connection->_requests = CFArrayCreateMutable(alloc, 0, &kCFTypeArrayCallBacks); // Make sure the list was created if (connection->_requests == NULL) break; // Create a buffer for any buffered bytes which will be sent out connection->_bufferedBytes = CFDataCreateMutable(alloc, 0); // Make sure there is a buffer if (connection->_bufferedBytes == NULL) break; // It's all good return connection; } while (0); // Something failed, so clean up. if (connection) _HttpConnectionRelease(connection); return NULL; } /* static */ HttpConnection* _HttpConnectionRetain(HttpConnection* connection) { // Bump the retain count. connection->_rc++; return connection; } /* static */ void _HttpConnectionRelease(HttpConnection* connection) { // Decrease the retain count. connection->_rc--; // Destroy the object if not being held. if (connection->_rc == 0) { // Hold locally so deallocation can happen and then safely release. CFAllocatorRef alloc = connection->_alloc; CFRunLoopRef runLoop = CFRunLoopGetCurrent(); if (connection->_server) CFRelease((_CFHTTPServerRef)connection->_server); if (connection->_peer) CFRelease(connection->_peer); // Check if the read stream exists. if (connection->_inStream) { // Unschedule, close, and release it. CFReadStreamSetClient(connection->_inStream, 0, NULL, NULL); CFReadStreamUnscheduleFromRunLoop(connection->_inStream, runLoop, kCFRunLoopCommonModes); CFReadStreamClose(connection->_inStream); CFRelease(connection->_inStream); } // Check if the write stream exists. if (connection->_outStream) { // Unschedule, close, and release it. CFWriteStreamSetClient(connection->_outStream, 0, NULL, NULL); CFWriteStreamUnscheduleFromRunLoop(connection->_outStream, runLoop, kCFRunLoopCommonModes); CFWriteStreamClose(connection->_outStream); CFRelease(connection->_outStream); } // If the timer exists, toss it too. if (connection->_timer != NULL) { CFRunLoopRemoveTimer(runLoop, connection->_timer, kCFRunLoopCommonModes); CFRunLoopTimerInvalidate(connection->_timer); CFRelease(connection->_timer); } // Toss the dictionary of requests and responses if (connection->_responses) CFRelease(connection->_responses); // Toss the list of incoming requests if (connection->_requests) CFRelease(connection->_requests); // Toss the buffered bytes if (connection->_bufferedBytes) CFRelease(connection->_bufferedBytes); // Free the memory in use by the connection. CFAllocatorDeallocate(alloc, connection); // Release the allocator. if (alloc) CFRelease(alloc); } } /* static */ CFStringRef _HttpConnectionCopyDescription(HttpConnection* connection) { CFStringRef result; // Create the debug string result = CFStringCreateWithFormat(connection->_alloc, NULL, _kCFHTTPServerConnectionDescribeFormat, (UInt32)connection, (UInt32)connection->_server, connection->_timer, connection->_inStream, connection->_outStream, connection->_responses, connection->_requests, connection->_bufferedBytes); return result; } /* static */ void _HttpConnectionHandleRequest(HttpConnection* connection) { assert(0 != CFArrayGetCount(connection->_requests)); // Get the message with which to work (the last one) CFHTTPMessageRef msg = (CFHTTPMessageRef)CFArrayGetValueAtIndex(connection->_requests, CFArrayGetCount(connection->_requests) - 1); while (msg) { // Use to see if it is a chunked request CFStringRef encoding = CFHTTPMessageCopyHeaderFieldValue(msg, _kCFHTTPServerTransferEncodingHeader); // Assume not chunked Boolean chunked = FALSE; // If there is encoding, cheaply check for chunked. if (encoding) { chunked = CFStringFindWithOptions(encoding, _kCFHTTPServerTransferEncodingChunked, CFRangeMake(0, CFStringGetLength(encoding)), kCFCompareCaseInsensitive, NULL); CFRelease(encoding); } // If it's chunked, bail 'cause the API just isn't ready for // these types of requests yet. if (chunked) { // Establish an error CFStreamError error = {kCFStreamErrorDomainHTTP, kCFStreamErrorHTTPParseFailure}; // Handle it just like an error. _HttpConnectionHandleErrorOccurred(connection, &error); break; } // No chunking info so use size else { // Assume zero length to start. SInt32 size = 0; // Grab the body for testing CFDataRef body = CFHTTPMessageCopyBody(msg); // Get the length of the current body on the message CFIndex length = body ? CFDataGetLength(body) : 0; // Get the size to see if everything is there. CFStringRef value = CFHTTPMessageCopyHeaderFieldValue(msg, _kCFHTTPServerContentLengthHeader); // Need to convert the value if there was a header if (value) { // Convert the header value to a CFNumber CFNumberRef num = _CFNumberCreateWithString(connection->_alloc, value); // If that succeeded, turn it into the actual size if (num) { // Pull out the true expected count of bytes CFNumberGetValue(num, kCFNumberSInt32Type, &size); CFRelease(num); } // Received a bad content-length header else { if (body) CFRelease(body); CFRelease(value); // Establish an error CFStreamError error = {kCFStreamErrorDomainHTTP, kCFStreamErrorHTTPParseFailure}; // Handle it just like an error. _HttpConnectionHandleErrorOccurred(connection, &error); break; } CFRelease(value); } // If enough bytes haven't arrived, bail out now. if (length < size) { if (body) CFRelease(body); break; } // If the message is just right, inform the client of the message // and then exit since this is no more to process. else if (length == size) { if (body) CFRelease(body); // Inform the client of the incoming request if (connection->_server->_callbacks.didReceiveRequestCallBack != NULL) { CFRetain(msg); connection->_server->_callbacks.didReceiveRequestCallBack((_CFHTTPServerRef)connection->_server, msg, connection->_server->_ctxt.info); CFRelease(msg); } break; } // There are too many bytes in the body else { // Need to make new and truncate, 'cause the current one is too long. CFDataRef newBody = CFDataCreate(connection->_alloc, CFDataGetBytePtr(body), size); // Create a new request to capture the leftover bytes. CFHTTPMessageRef newMsg = CFHTTPMessageCreateEmpty(connection->_alloc, TRUE); // Set the new body on the first request CFHTTPMessageSetBody(msg, newBody); // Toss the new body since it's retained by the request. CFRelease(newBody); // Inform the client of the incoming request if (connection->_server->_callbacks.didReceiveRequestCallBack != NULL) { CFRetain(msg); connection->_server->_callbacks.didReceiveRequestCallBack((_CFHTTPServerRef)connection->_server, msg, connection->_server->_ctxt.info); CFRelease(msg); } // Move on to the new message to handle it msg = newMsg; // Put the new request in the requests list. CFArrayAppendValue(connection->_requests, msg); // Drop the retain count now since it's being held by the queue. CFRelease(msg); // Add the leftover bytes from the first request to the new one if (!CFHTTPMessageAppendBytes(msg, CFDataGetBytePtr(body) + size, length - size)) { // Establish an error CFStreamError error = {kCFStreamErrorDomainHTTP, kCFStreamErrorHTTPParseFailure}; // Handle it just like an error. _HttpConnectionHandleErrorOccurred(connection, &error); CFRelease(body); break; } // Don't need the body. CFRelease(body); // Check to see if the new message is complete too. if (!CFHTTPMessageIsHeaderComplete(msg)) break; // Not done so bail. // There is enough there so inform the client. else { // Assume the client is willing to take on the request. Boolean handle = TRUE; // Check the client to make sure this new message should be processed. if (connection->_server->_callbacks.acceptNewRequestCallBack) { handle = connection->_server->_callbacks.acceptNewRequestCallBack((_CFHTTPServerRef)connection->_server, newMsg, connection->_peer, connection->_server->_ctxt.info); } if (!handle) { // Remove the connection from the pool _HttpServerRemoveConnection(connection->_server, connection); // Bail now because the current connection has been killed break; } } } } } } /* static */ void _HttpConnectionHandleHasBytesAvailable(HttpConnection* connection) { CFIndex bytes; UInt8 buffer[kBufferSize]; CFHTTPMessageRef msg; // Get the count of requests currently known. CFIndex i = CFArrayGetCount(connection->_requests); // If there is, grab the last one with which to work if (i != 0) msg = (CFHTTPMessageRef)CFArrayGetValueAtIndex(connection->_requests, --i); else { // There was no requests, so create a new one with which to work msg = CFHTTPMessageCreateEmpty(connection->_alloc, TRUE); CFArrayAppendValue(connection->_requests, msg); CFRelease(msg); } // Try to read bytes off the wire bytes = CFReadStreamRead(connection->_inStream, buffer, sizeof(buffer)); // Did it succeed? if (bytes >= 0) { Boolean complete = CFHTTPMessageIsHeaderComplete(msg); // Tickle the timer CFRunLoopTimerSetNextFireDate(connection->_timer, CFAbsoluteTimeGetCurrent() + kTimeOutInSeconds); // Attach read bytes to current request if (!CFHTTPMessageAppendBytes(msg, buffer, bytes)) { // Establish an error CFStreamError error = {kCFStreamErrorDomainHTTP, kCFStreamErrorHTTPParseFailure}; // Handle it just like an error. _HttpConnectionHandleErrorOccurred(connection, &error); return; } // If the request is complete, handle it as appropriate. if (CFHTTPMessageIsHeaderComplete(msg)) { // Assume the client is willing to take on the request. Boolean handle = TRUE; // Check the client for sure (each message is checked once when it crosses // over from incomplete to complete. if (!complete && connection->_server->_callbacks.acceptNewRequestCallBack) { handle = connection->_server->_callbacks.acceptNewRequestCallBack((_CFHTTPServerRef)connection->_server, msg, connection->_peer, connection->_server->_ctxt.info); } if (handle) _HttpConnectionHandleRequest(connection); else { // Remove the connection from the pool _HttpServerRemoveConnection(connection->_server, connection); } } } // Let error conditions come in naturally through the event dispatch. } /* static */ void _HttpConnectionHandleCanAcceptBytes(HttpConnection* connection) { // How are responses handled (read the "Description" at the top)? // // Responses have two parts: a CFHTTPMessageRef containing only headers and a CFReadStreamRef // which is a stream to the body contents. These responses will be sent in the order in // which their respective requests were vended. // // A local buffer on the connection is used for all writing to the wire. Buffered bytes are // always sent first. An empty buffer signals the start of a new response. If the buffer is // empty, the first, queued response's headers are serialized and placed in the buffer. // // Bytes in the buffer are sent to the wire. If all bytes in the buffer were written, the // response's stream is read for bytes. The read bytes are placed into the connection's // buffer. This buffer will be used for writing when this function is called again. // // If the response's stream has been exhausted, that request-response pair is removed from // the connection's queue, and the buffer is left empty. Since the buffer is empty, the // next response will be handled when this function is called again. // // At the end of each response, the headers are checked for the proper termination of the // open connection. If a "Connection: close" header exists or if in default mode under // HTTP version 1.0, the connection will be terminated and dequeued from the server. // Check to make sure there are queued items. if (CFArrayGetCount(connection->_requests) != 0) { // Pull off the request and its related response information CFHTTPMessageRef request = (CFHTTPMessageRef)CFArrayGetValueAtIndex(connection->_requests, 0); CFArrayRef list = request ? (CFArrayRef)CFDictionaryGetValue(connection->_responses, request) : NULL; CFHTTPMessageRef response = list ? (CFHTTPMessageRef)CFArrayGetValueAtIndex(list, 0) : NULL; CFReadStreamRef stream = list ? (CFReadStreamRef)CFArrayGetValueAtIndex(list, 1) : NULL; // Only handle if there is a response ready to go if (list != NULL) { CFIndex bytesWritten; // If there are no buffered bytes, need to start the next request. if (CFDataGetLength(connection->_bufferedBytes) == 0) { // Serialize if for sending CFDataRef serialized = CFHTTPMessageCopySerializedMessage(response); // Get rid of the old one before getting the new CFRelease(connection->_bufferedBytes); // Use a mutable copy, because it gets sized down as bytes are sent. connection->_bufferedBytes = CFDataCreateMutableCopy(connection->_alloc, 0, serialized); // Release the original. CFRelease(serialized); } // Try writing the entire buffer bytesWritten = CFWriteStreamWrite(connection->_outStream, CFDataGetBytePtr(connection->_bufferedBytes), CFDataGetLength(connection->_bufferedBytes)); // If successfully wrote, continue on. if (bytesWritten > 0) { // Compute the new size of the buffer after the write CFIndex newSize = CFDataGetLength(connection->_bufferedBytes) - bytesWritten; // Tickle the timer CFRunLoopTimerSetNextFireDate(connection->_timer, CFAbsoluteTimeGetCurrent() + kTimeOutInSeconds); // Move the remaining bytes down in the buffer memmove(CFDataGetMutableBytePtr(connection->_bufferedBytes), CFDataGetBytePtr(connection->_bufferedBytes) + bytesWritten, newSize); // Resize the buffer to indicate what is left CFDataSetLength(connection->_bufferedBytes, newSize); // If nothing left in the buffer, fill the buffer if (newSize == 0) { CFIndex bytesRead; // If the response's stream isn't open yet, open it. if (CFReadStreamGetStatus(stream) == kCFStreamStatusNotOpen) CFReadStreamOpen(stream); // Size the buffer for a full read CFDataSetLength(connection->_bufferedBytes, kBufferSize); // Try reading a full buffer into the buffer bytesRead = CFReadStreamRead(stream, CFDataGetMutableBytePtr(connection->_bufferedBytes), kBufferSize); // Size the buffer to what ever size was read if successful if (bytesRead >= 0) CFDataSetLength(connection->_bufferedBytes, bytesRead); // Was there an error? if (bytesRead < 0) { // Get the error from the read stream CFStreamError error = CFReadStreamGetError(stream); // Inform the client of the error. _HttpConnectionHandleErrorOccurred(connection, &error); } // Was this the end of the response's stream? else if (bytesRead == 0) { // Get the HTTP version and the connection header from the response. CFStringRef close = CFHTTPMessageCopyHeaderFieldValue(response, _kCFHTTPServerConnectionHeader); CFStringRef version = CFHTTPMessageCopyVersion(response); // If no header, check the original request for one. if (close == NULL) close = CFHTTPMessageCopyHeaderFieldValue(request, _kCFHTTPServerConnectionHeader); // Inform the client of a successful send of the response. if (connection->_server->_callbacks.didSendResponseCallBack != NULL) { connection->_server->_callbacks.didSendResponseCallBack((_CFHTTPServerRef)connection->_server, request, response, connection->_server->_ctxt.info); } // Remove the request-response pair from the conneciton's queue CFDictionaryRemoveValue(connection->_responses, request); CFArrayRemoveValueAtIndex(connection->_requests, 0); // If there was a header and it said, "close," or if there was no header and HTTP version // 1.0 is being used, then close the connection and remove it from the server. if (((close != NULL) && CFStringCompare(close, _kCFHTTPServerConnectionClose, kCFCompareCaseInsensitive) == kCFCompareEqualTo) || ((close == NULL) && (version != NULL) && CFStringCompare(version, kCFHTTPVersion1_1, kCFCompareCaseInsensitive) != kCFCompareEqualTo)) { _HttpServerRemoveConnection(connection->_server, connection); } if (close != NULL) CFRelease(close); if (version != NULL) CFRelease(version); } } } } } } /* static */ void _HttpConnectionHandleErrorOccurred(HttpConnection* connection, const CFStreamError* error) { CFArrayRef requests = CFArrayCreateCopy(connection->_alloc, connection->_requests); CFIndex i, count = CFArrayGetCount(requests); // Error-out each request in the queue for (i = 0; i < count; i++) { // Get the request and the response pair CFHTTPMessageRef request = (CFHTTPMessageRef)CFArrayGetValueAtIndex(connection->_requests, i); CFArrayRef list = (CFArrayRef)CFDictionaryGetValue(connection->_responses, request); // If there is a response and there is a client, inform the client of the error. if ((list != NULL) && (connection->_server->_callbacks.errorCallBack != NULL)) { connection->_server->_callbacks.errorCallBack((_CFHTTPServerRef)connection->_server, error, request, (CFHTTPMessageRef)CFArrayGetValueAtIndex(list, 0), connection->_server->_ctxt.info); } } CFRelease(requests); // Remove the connection from the pool _HttpServerRemoveConnection(connection->_server, connection); } /* static */ void _HttpConnectionHandleTimeOut(HttpConnection* connection) { // Establish an error CFStreamError error = {kCFStreamErrorDomainCFHTTPServer, kCFStreamErrorCFHTTPServerTimeout}; // Handle it just like an error. _HttpConnectionHandleErrorOccurred(connection, &error); } /* static */ const void* _ArrayRetainCallBack(CFAllocatorRef allocator, const HttpConnection* connection) { return _HttpConnectionRetain((HttpConnection*)connection); } /* static */ void _ArrayReleaseCallBack(CFAllocatorRef allocator, const HttpConnection* connection) { return _HttpConnectionRelease((HttpConnection*)connection); } /* static */ void _ReadStreamCallBack(CFReadStreamRef inStream, CFStreamEventType type, HttpConnection* connection) { assert(inStream == connection->_inStream); // Dispatch the event properly. switch (type) { case kCFStreamEventHasBytesAvailable: _HttpConnectionHandleHasBytesAvailable(connection); break; case kCFStreamEventErrorOccurred: { CFStreamError error = CFReadStreamGetError(inStream); _HttpConnectionHandleErrorOccurred(connection, &error); } break; default: break; } } /* static */ void _WriteStreamCallBack(CFWriteStreamRef outStream, CFStreamEventType type, HttpConnection* connection) { assert(outStream == connection->_outStream); // Dispatch the event properly. switch (type) { case kCFStreamEventCanAcceptBytes: _HttpConnectionHandleCanAcceptBytes(connection); break; case kCFStreamEventErrorOccurred: { CFStreamError error = CFWriteStreamGetError(outStream); _HttpConnectionHandleErrorOccurred(connection, &error); } break; default: break; } } /* static */ void _TimerCallBack(CFRunLoopTimerRef timer, HttpConnection* connection) { assert(timer == connection->_timer); // Dispatch the timer event. _HttpConnectionHandleTimeOut(connection); } /* static */ void _HttpServerAddConnection(HttpServer* server, HttpConnection* connection) { // Add the given connection to the list CFArrayAppendValue(server->_connections, connection); } /* static */ void _HttpServerRemoveConnection(HttpServer* server, HttpConnection* connection) { // Find the given connection in the list of connections CFMutableArrayRef connections = server->_connections; CFIndex i = CFArrayGetFirstIndexOfValue(connections, CFRangeMake(0, CFArrayGetCount(connections)), connection); // If it existed, remove it from the list. if (i != kCFNotFound) CFArrayRemoveValueAtIndex(connections, i); } /* static */ void _HttpServerHandleNewConnection(HttpServer* server, CFSocketNativeHandle sock) { CFAllocatorRef alloc = CFGetAllocator((_CFHTTPServerRef)server); // Assume the server will allow the connection. Boolean accepted = TRUE; // Find out if the client cares if (server->_callbacks.acceptNewConnectionCallBack) { uint8_t name[SOCK_MAXADDRLEN]; int namelen = sizeof(name); CFDataRef peer = NULL; // Get the address of the peer. **FIXME** this is less than optimal // since the peer name is copied again later when the connection is // created. if (0 == getpeername(sock, (struct sockaddr *)name, &namelen)) peer = CFDataCreate(alloc, name, namelen); // Fail if the peer couldn't be established. if (!peer) accepted = FALSE; else { // See what the client says. accepted = server->_callbacks.acceptNewConnectionCallBack((_CFHTTPServerRef)server, peer, server->_ctxt.info); CFRelease(peer); } } if (accepted) { // Create a new incoming connection HttpConnection* connection = _HttpConnectionCreate(alloc, server, sock); // Add the connection to the server if it created. if (connection != NULL) { _HttpServerAddConnection(server, connection); _HttpConnectionRelease(connection); } else { // Create an error for the bad situation CFStreamError error = {kCFStreamErrorDomainCFHTTPServer, kCFStreamErrorCFHTTPServerInternal}; // Handle the error _HttpServerHandleError(server, &error); } } } /* static */ void _HttpServerHandleError(HttpServer* server, const CFStreamError* error) { // Inform the user of an error. if (server->_callbacks.errorCallBack != NULL) server->_callbacks.errorCallBack((_CFHTTPServerRef)server, error, NULL, NULL, server->_ctxt.info); } /* static */ void _ServerCallBack(_CFServerRef server, CFSocketNativeHandle sock, const CFStreamError* error, HttpServer* httpServer) { if (error->error == 0) _HttpServerHandleNewConnection(httpServer, sock); else _HttpServerHandleError(httpServer, error); } /* static */ CFNumberRef _CFNumberCreateWithString(CFAllocatorRef allocator, CFStringRef string) { CFIndex i, length = CFStringGetLength(string); UniChar* buffer = CFAllocatorAllocate(allocator, length * sizeof(buffer[0]), 0); SInt32 value = 0; CFStringGetCharacters(string, CFRangeMake(0, length), buffer); for (i = 0; i < length; i++) { UniChar c = buffer[i]; if ((c < '0') || (c > '9') || ((value * 10) < value)) { CFAllocatorDeallocate(allocator, buffer); return NULL; } value *= 10; value += (c - '0'); } CFAllocatorDeallocate(allocator, buffer); return CFNumberCreate(allocator, kCFNumberSInt32Type, &value); }