Dependencies: FatFileSystem mbed WeatherMeters SDFileSystem
Diff: HTTPServer.cpp
- Revision:
- 0:1a61c61d0845
diff -r 000000000000 -r 1a61c61d0845 HTTPServer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/HTTPServer.cpp Tue Apr 03 18:43:13 2012 +0000 @@ -0,0 +1,2233 @@ +/* Copyright 2011 Adam Green (http://mbed.org/users/AdamGreen/) + + 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. +*/ +/* Implementation of HTTP Server functionality. */ +#include "HTTPServer.h" +#include "network.h" +#include "debug.h" + + +// HTTP_TRACE to 1/0 to enable/disable logging for the HTTP server. +#define HTTP_TRACE 0 +#if HTTP_TRACE + #define TRACE printf +#else + static void __trace(...) + { + return; + } + #define TRACE __trace +#endif // HTTP_TRACE + + + +// Maximum size of buffer to be allocated on the stack for building filename +// from URI that can be used in mbed fopen() call. +#define HTTP_MAX_FILENAME 64 + +// Maximum size of a request line to be buffered. +#define HTTP_MAX_REQUEST_LINE 128 + + +// Utility macro to determine the element count of an array. +#define ARRAYSIZE(X) (sizeof(X)/sizeof(X[0])) + + + +// Default filenames to be used when the requested URI is "/" +static const char* g_DefaultFilenames[] = +{ + "/index.html", + "/index.htm" +}; + + + +// Static-Scope Function Prototypes. +static void NetworkPrintRemoteAddress(tcp_pcb* pPCB); + + + +// Structure used to represent each header to be sent. It includes both a +// pointer to the constant string and the length of the constant string so +// that a memcpy() can be used for moving into buffer instead of strcpy(). +struct SHeader +{ + // Header string. + const char* pHeaderString; + // Length of header string, not including the NULL terminator. + unsigned int HeaderLength; +}; + + +// Structure which stores information about each HTTP client connection. +class CHTTPContext : protected IHTTPRequestHandlerContext +{ +public: + // Constructors/Destructors + CHTTPContext(CHTTPServer* pHTTPServer, tcp_pcb* pPCB); + ~CHTTPContext(); + + // IHTTPRequestHandlerContext methods. + virtual int BeginRequestHeaders(); + virtual void WriteRequestHeader(const char* pHeaderName, + const char* pHeaderValue); + virtual void EndRequestHeaders(); + virtual int BeginRequestContent(size_t ContentSize); + virtual void WriteRequestContentBlock(const void* pBlockBuffer, + size_t BlockBufferSize); + virtual void EndRequestContent(); + virtual const char* GetStatusLine(size_t* pStatusLineLength); + virtual const char* GetResponseHeaders(size_t* pResponseHeaderLength); + virtual int BeginResponseContent(); + virtual size_t ReadResponseContentBlock(char* pBuffer, + size_t BytesToRead); + virtual void EndResponseContent(); + virtual void Release(); + + CHTTPServer* Server() { return m_pHTTPServer; } + CHTTPContext* GetNext() { return m_pNext; } + void RemovedFromWaitingList(CHTTPContext* pPrev) + { + if (pPrev) + { + pPrev->m_pNext = this->m_pNext; + } + m_pNext = NULL; + m_WaitingForBuffer = 0; + } + void AddToWaitingList(CHTTPContext* pPrev) + { + if (pPrev) + { + assert ( !pPrev->m_pNext ); + pPrev->m_pNext = this; + } + m_WaitingForBuffer = 1; + } + int IsWaitingForBuffer() { return m_WaitingForBuffer; } + + void CloseConnection(); + void FreeContext(int ForcedFree); + void BufferAvailable(); + +protected: + // Static methods for lwIP callbacks. + static err_t HTTPRecv(void* pvArg, tcp_pcb* pPCB, pbuf* pBuf, err_t err); + static err_t HTTPSent(void* pvArg, tcp_pcb* pPCB, u16_t SendCount); + static void HTTPError(void* pvArg, err_t Error); + static err_t HTTPPoll(void* pvArg, tcp_pcb* pPCB); + + // Protected helper methods. + void ProcessRequestData(pbuf* pBuf); + void BufferRequestLines(pbuf* pBuf); + void ProcessRequestContent(pbuf* pBuf); + void ParseRequestLine(); + void ParseFirstRequestLine(); + void ParseHeaderRequestLine(); + void StartRequestHandler(const char* pURI); + void StartGetRequest(const char* pURI); + void StartHeadRequest(const char* pURI); + void StartPostRequest(const char* pURI); + void StartBadRequest(const char* pRequest); + void OpenFile(const char* pURI); + void PrepareResponse(); + void PrepareResponseHeaders(); + void ReadContent(); + void SendResponse(); + int WriteData(); + + // States used to track the HTTP request parsing progress. + enum EParseState { + STATE_FIRST_LINE, + STATE_HEADERS, + STATE_CONTENT, + STATE_DONE + }; + + // Type of HTTP request that has been made by the client. + enum ERequestType + { + TYPE_GET_0_9, + TYPE_GET_1_0, + TYPE_HEAD, + TYPE_POST, + TYPE_BAD_REQUEST + }; + + // Pointer to parent HTTP Server object. + CHTTPServer* m_pHTTPServer; + // Pointer to the tcp_pcb 'socket' for this context. + tcp_pcb* m_pPCB; + // The next context in the buffer waiting queue. + CHTTPContext* m_pNext; + // Handle of file to be sent to the client. + FILE* m_pFile; + // Pointer to the request handler context. May point to self for + // default GET/HEAD request handling. + IHTTPRequestHandlerContext* m_pRequestHandlerContext; + // Content-Type description header field. + const char* m_pContentType; + // HTML text to be sent back in response to error. + const char* m_pErrorHTML; + // Queue of buffers used by this context. BufferQueueHead and + // BufferQueueTail track the current buffers being written and/or + // waiting for acknowledgment from remote machine. + SBuffer* m_BufferQueue[2]; + // Array of pointers to constant header strings to be sent before response + // content. Status Line, Server:, Other Headers, /r/n + SHeader m_HeaderArray[4]; + // The length of the m_pContentType string. + size_t m_ContentTypeLength; + // The length of the m_pErrorHTML string. + size_t m_ErrorHTMLLength; + // The current read offset into the m_pErrorHTML string. + size_t m_ErrorHTMLOffset; + // UNDONE: Many of these fields could be packed into a single field to save memory. + // Are we waiting for a buffer already? + int m_WaitingForBuffer; + // Are we in the process of closing down the connection? + int m_ClosingConnection; + // Should request headers be sent to handler? + int m_SendRequestHeadersToHandler; + // Should request content be sent to handler? + int m_SendRequestContentToHandler; + // Content-Length of request content body. + unsigned int m_RequestContentLength; + // The current state of the request parsing code. + EParseState m_ParseState; + // The type of request being made by the client. + ERequestType m_RequestType; + // UNDONE: Check for memory savings across these structures. + // Offset into m_LineBuffer where next character should be placed. + unsigned short m_LineBufferOffset; + // Offset into current header being sent to client. + unsigned short m_HeaderOffset; + // Head and tail indices for SentBufferQueue. + // Head is the buffer currently being written. + unsigned char m_BufferQueueHead; + // Tail is the buffer currently in the process of being acknowledged. + unsigned char m_BufferQueueTail; + // Number of tcp_write() retries attempted so far. + unsigned char m_RetryCount; + // Current index into HeaderArray[] of next header to be sent to client. + unsigned char m_HeaderIndex; + // Each line of the request is buffered here. + char m_LineBuffer[HTTP_MAX_REQUEST_LINE+1]; +}; + + +CHTTPContext::CHTTPContext(CHTTPServer* pHTTPServer, tcp_pcb* pPCB) +{ + m_pHTTPServer = pHTTPServer; + m_pPCB = pPCB; + m_pNext = NULL; + m_pFile = NULL; + m_pRequestHandlerContext = NULL; + m_pContentType = NULL; + m_pErrorHTML = NULL; + memset(m_BufferQueue, 0, sizeof(m_BufferQueue)); + memset(m_HeaderArray, 0, sizeof(m_HeaderArray)); + m_ContentTypeLength = 0; + m_ErrorHTMLLength = 0; + m_ErrorHTMLOffset = 0; + m_WaitingForBuffer = 0; + m_ClosingConnection = 0; + m_SendRequestHeadersToHandler = 0; + m_SendRequestContentToHandler = 0; + m_RequestContentLength = 0; + m_ParseState = STATE_FIRST_LINE; + m_RequestType = TYPE_BAD_REQUEST; + m_LineBufferOffset = 0; + m_HeaderOffset = 0; + m_BufferQueueHead = 0; + m_BufferQueueTail = 0; + m_RetryCount = 0; + m_HeaderIndex = 0; + m_LineBuffer[0] = '\0'; + + // Associate this context with the lwIP PCB object. + tcp_arg(pPCB, this); + + // Setup the callbacks to be called whenever any activity occurs on this new + // client PCB. + tcp_recv(pPCB, HTTPRecv); + tcp_sent(pPCB, HTTPSent); + tcp_err(pPCB, HTTPError); + + // There might be scenarios where the application can't queue up more work + // for this client PCB (due to things like out of memory) so that activity + // could cease on this PCB when there were no more calls to functions like + // HTTPSent(). To make sure this doesn't happen, have lwIP callback into + // the HTTPPoll() function every 4 iterations of the coarse TCP timer (2 + // seconds). HTTPPoll() can take care of retrying any previously failed + // operations. + tcp_poll(pPCB, HTTPPoll, 4); +} + + +// Destructor +CHTTPContext::~CHTTPContext() +{ + // Make sure that everything was cleaned up before destructor was called. + assert ( NULL == m_pFile ); + assert ( NULL == m_pPCB ); + assert ( NULL == m_pRequestHandlerContext ); +} + + +int CHTTPContext::BeginRequestHeaders() +{ + // This context already parses out the Content-Length header that it needs. + return 0; +} + +void CHTTPContext::WriteRequestHeader(const char* pHeaderName, + const char* pHeaderValue) +{ + static const int ShouldNotBeCalled = 0; + + (void)pHeaderName; + (void)pHeaderValue; + + assert ( ShouldNotBeCalled ); +} + +void CHTTPContext::EndRequestHeaders() +{ + static const int ShouldNotBeCalled = 0; + + assert ( ShouldNotBeCalled ); +} + +int CHTTPContext::BeginRequestContent(size_t ContentSize) +{ + (void)ContentSize; + + // Only processing GET and HEAD requests so don't care about content sent + // in request. + return 0; +} + +void CHTTPContext::WriteRequestContentBlock(const void* pBlockBuffer, + size_t BlockBufferSize) +{ + static const int ShouldNotBeCalled = 0; + + (void)pBlockBuffer; + (void)BlockBufferSize; + + assert ( ShouldNotBeCalled ); +} + +void CHTTPContext::EndRequestContent() +{ + static const int ShouldNotBeCalled = 0; + + assert ( ShouldNotBeCalled ); +} + +const char* CHTTPContext::GetStatusLine(size_t* pStatusLineLength) +{ + static const char Ok[] = "HTTP/1.0 200 OK\r\n"; + static const char NotFound[] = "HTTP/1.0 404 Not Found\r\n"; + static const char BadRequest[] = "HTTP/1.0 400 Bad Request\r\n"; + static const char NotImplemented[] = "HTTP/1.0 501 Not Implemented\r\n"; + + switch (m_RequestType) + { + case TYPE_HEAD: + case TYPE_GET_1_0: + if (m_pFile) + { + *pStatusLineLength = sizeof(Ok) - 1; + return Ok; + } + else + { + *pStatusLineLength = sizeof(NotFound) - 1; + return NotFound; + } + break; + case TYPE_GET_0_9: + // Shouldn't get called for such a request. + assert ( TYPE_GET_0_9 != m_RequestType ); + return NULL; + case TYPE_BAD_REQUEST: + *pStatusLineLength = sizeof(BadRequest) - 1; + return BadRequest; + default: + // This server has no default handling for any other type of request. + *pStatusLineLength = sizeof(NotImplemented) - 1; + return NotImplemented; + } +} + +const char* CHTTPContext::GetResponseHeaders(size_t* pResponseHeaderLength) +{ + // Additional error text will be returned as HTML text. + static const char ContentTypeHTML[] = "Content-type: text/html\r\n"; + static const char NotImplementedHTML[] = "<html>" + "<body><h2>501: Not Implemented.</h2></body>" + "</html>\r\n"; + static const char BadRequestHTML[] = "<html>" + "<body><h2>400: Bad Request.</h2></body>" + "</html>\r\n"; + + switch (m_RequestType) + { + case TYPE_HEAD: + case TYPE_GET_1_0: + *pResponseHeaderLength = m_ContentTypeLength; + return m_pContentType; + case TYPE_GET_0_9: + // Shouldn't get called for such a request. + assert ( TYPE_GET_0_9 != m_RequestType ); + return NULL; + case TYPE_BAD_REQUEST: + // Will send error back as HTML text. + m_pErrorHTML = BadRequestHTML; + m_ErrorHTMLLength = sizeof(BadRequestHTML) - 1; + *pResponseHeaderLength = sizeof(ContentTypeHTML) - 1; + return ContentTypeHTML; + default: + // This server has no default handling for any other type of request. + m_pErrorHTML = NotImplementedHTML; + m_ErrorHTMLLength = sizeof(NotImplementedHTML) - 1; + *pResponseHeaderLength = sizeof(ContentTypeHTML) - 1; + return ContentTypeHTML; + } +} + +int CHTTPContext::BeginResponseContent() +{ + // Have response content to send back so return 1; + assert ( m_pFile || m_pErrorHTML ); + return 1; +} + +size_t CHTTPContext::ReadResponseContentBlock(char* pBuffer, + size_t BytesToRead) +{ + if (m_pFile) + { + // Read content from file. + return fread(pBuffer, 1, BytesToRead, m_pFile); + } + else + { + // Read content from HTML text for error. + size_t BytesLeft = m_ErrorHTMLLength - m_ErrorHTMLOffset; + + if (BytesToRead > BytesLeft) + { + BytesToRead = BytesLeft; + } + memcpy(pBuffer, &m_pErrorHTML[m_ErrorHTMLOffset], BytesToRead); + m_ErrorHTMLOffset += BytesToRead; + + return BytesToRead; + } +} + +void CHTTPContext::EndResponseContent() +{ + if (m_pFile) + { + fclose(m_pFile); + m_pFile = NULL; + } +} + +void CHTTPContext::Release() +{ + // Just make sure that any files are closed. + EndResponseContent(); + + return; +} + + +/* Called by lwIP whenever data is sent to an active connection. The main task + of this callback is to acknowledge application receipt of the data, parse + the received data to determine which file is being requested by the client, + open the requested file, and start sending it back to the remote client. + + Parameters: + pvArg is the value set on the active PCB through the call to tcp_arg in + HTTPAccept() which is a pointer to the HTTP context for this + connection. + pPCB is a pointer to the PCB that has just received some data from the + client. + pBuf is a pointer to a PBUF which contains a linked list of the data + received from the remote client. + err is passed in from the lwIP stack to indicate if an error occurred. + + Returns: + ERR_ABRT if we end up aborting the connection and ERR_OK otherwise. +*/ +err_t CHTTPContext::HTTPRecv(void* pvArg, tcp_pcb* pPCB, pbuf* pBuf, err_t err) +{ + CHTTPContext* pHTTPContext = (CHTTPContext*)pvArg; + + // Validate parameters. + assert ( pPCB ); + assert ( ERR_OK == err ); + + // If the client has closed their end then pBuf will be NULL so the server + // should shutdown its end as well. + if (!pBuf) + { + TRACE("HTTP: Closing as client closed its end for connection: "); + NetworkPrintRemoteAddress(pPCB); + TRACE("\r\n"); + + pHTTPContext->CloseConnection(); + pHTTPContext->FreeContext(TRUE); + + return ERR_OK; + } + + // Let lwIP know that the HTTP application has received the data. + tcp_recved(pPCB, pBuf->tot_len); + + // Just return if we have already freed the client context from a close + // attempt. + if (!pvArg) + { + TRACE("HTTP: Ignoring HTTPRecv() as client context was already freed for: "); + NetworkPrintRemoteAddress(pPCB); + TRACE("\r\n"); + goto Error; + } + + // The PCB shouldn't change out underneath this object! + assert ( pPCB == pHTTPContext->m_pPCB ); + + pHTTPContext->ProcessRequestData(pBuf); + +Error: + // Release the pbuf as the application no longer requires any of its data + pbuf_free(pBuf); + + return ERR_OK; +} + + +/* Called by lwIP whenever sent data is acknowledged by the remote machine. + The main task of this callback is to update the byte count of unacknowledged + bytes and send more data once the previously sent data has been acknowledged. + + Parameters: + pvArg is the value set on the active PCB through the call to tcp_arg in + HTTPAccept() which is a pointer to the HTTP context for this + connection. + pPCB is a pointer to the PCB that has just received acknowledgement from + the remote client of sent data. + AcknowledgeCount is the number of bytes that the remote client has + acknowledged receipt of. + + Returns: + ERR_ABRT if we end up aborting the connection and ERR_OK otherwise. +*/ +err_t CHTTPContext::HTTPSent(void* pvArg, tcp_pcb* pPCB, u16_t AcknowledgeCount) +{ + CHTTPContext* pHTTPContext = (CHTTPContext*)pvArg; + + // Validate parameters. + assert ( pvArg ); + assert ( pPCB ); + assert ( AcknowledgeCount ); + + // Loop through all of the buffers in the sent state to update their + // unacknowledged count since this acknowledgment can span buffers. + while (AcknowledgeCount > 0) + { + SBuffer* pSentBuffer = pHTTPContext->m_BufferQueue[pHTTPContext->m_BufferQueueTail]; + + // We should have a buffer in the sent state if we are getting back + // acknowledgments from the remote machine. + assert ( pSentBuffer ); + + // Update outstanding byte count for this buffer. + if (AcknowledgeCount >= pSentBuffer->UnacknowledgedBytes) + { + // This callback acknowledges all of the data in the buffer at the + // tail of the queue and possibly spans into the next buffer as + // well. It is also possible that there is still more data to be + // written from this buffer. + AcknowledgeCount -= pSentBuffer->UnacknowledgedBytes; + pSentBuffer->UnacknowledgedBytes = 0; + + if (0 == pSentBuffer->BytesToWrite) + { + // Free the buffer which might cause another client context to be + // invoked if it was waiting for a buffer. + pHTTPContext->m_BufferQueue[pHTTPContext->m_BufferQueueTail] = NULL; + pHTTPContext->m_BufferQueueTail = (pHTTPContext->m_BufferQueueTail + 1) & 1; + pHTTPContext->m_pHTTPServer->FreeBuffer(pSentBuffer); + + // Check to see if this context is in the closing state and all sent + // buffers are now free. + if (pHTTPContext->m_ClosingConnection && + !pHTTPContext->m_BufferQueue[pHTTPContext->m_BufferQueueTail]) + { + // Free the context and return immediately since the context is + // no longer valid. + assert ( !pHTTPContext->m_BufferQueue[0] ); + assert ( !pHTTPContext->m_BufferQueue[1] ); + assert ( !pHTTPContext->m_WaitingForBuffer ); + + pHTTPContext->FreeContext(FALSE); + return ERR_OK; + } + } + else + { + // We still have data to write from this buffer so there should + // no more bytes to acknowledge in another buffer. + assert ( 0 == AcknowledgeCount ); + } + } + else + { + // This callback is just acknowledging a portion of the data in + // this buffer. + pSentBuffer->UnacknowledgedBytes -= AcknowledgeCount; + AcknowledgeCount = 0; + } + } + + // Attempt to send more response data to this client. + pHTTPContext->SendResponse(); + + return ERR_OK; +} + + +/* Called by lwIP every 2 seconds (4 iterations of the tcp_slowtmr) as + specified in the tcp_poll() call in HTTPInit(). Can be used to retry + sending of response data or closing of the PCB. + + Parameters: + pvArg is the value set on the active PCB through the call to tcp_arg in + HTTPAccept() which is a pointer to the HTTP context for this + connection. It will be NULL if we have previously failed to close + the PCB. + pPCB is a pointer to the low level lwIP API socket object for this + connection. + + Returns: + ERR_ABRT if we end up aborting the connection and ERR_OK otherwise. +*/ +err_t CHTTPContext::HTTPPoll(void* pvArg, tcp_pcb* pPCB) +{ + CHTTPContext* pHTTPContext = (CHTTPContext*)pvArg; + + // Validate parameters. + assert ( pPCB ); + + // Check to see if we need to retry closing the PCB. + if (!pHTTPContext) + { + err_t CloseResult; + + TRACE("HTTP: Retrying close for connection to: "); + NetworkPrintRemoteAddress(pPCB); + TRACE("\r\n"); + + CloseResult = tcp_close(pPCB); + if (ERR_OK != CloseResult) + { + // This is the second failed attempt to close the PCB, force an + // abort. + tcp_abort(pPCB); + return ERR_ABRT; + } + else + { + return ERR_OK; + } + } + + // Close the connection if the client hasn't sent its request within the polling interval. + if (STATE_DONE != pHTTPContext->m_ParseState) + { + TRACE("HTTP: Closing connection due to no HTTP request from connection to: "); + NetworkPrintRemoteAddress(pPCB); + TRACE("\r\n"); + + pHTTPContext->CloseConnection(); + pHTTPContext->FreeContext(TRUE); + + return ERR_OK; + } + + // If we have unacknowledged bytes outstanding then we will still + // receive a HTTPSent() callback from lwIP so no need to retry data sends + // from here. + // NOTE: This version will wait forever for a request from the client. + if (!pHTTPContext->m_BufferQueue[pHTTPContext->m_BufferQueueTail] && + pHTTPContext->m_BufferQueue[pHTTPContext->m_BufferQueueHead]) + { + if (pHTTPContext->m_RetryCount++ > 8) + { + // We have already retried sending the data 8 times, so close + // the connection. + TRACE("HTTP: Failed multiple retries to send data for connection to: "); + NetworkPrintRemoteAddress(pPCB); + TRACE("\r\n"); + + pHTTPContext->CloseConnection(); + } + else + { + // Retry sending the response data again. + TRACE("HTTP: Retrying to send data for connection to: "); + NetworkPrintRemoteAddress(pPCB); + TRACE("\r\n"); + + pHTTPContext->SendResponse(); + } + } + + return ERR_OK; +} + + +/* Called by lwIP whenever the PCB is closed or reset by the remote client. + Gives the application the opportunity to free up the context object. + + Parameters: + pvArg is the value set on the active PCB through the call to tcp_arg in + HTTPAccept() which is a pointer to the HTTP context for this + connection. + Error is the error that caused the connection to be shutdown. It can be + set to ERR_ABRT or ERR_RST. + + Returns: + Nothing. +*/ +void CHTTPContext::HTTPError(void* pvArg, err_t Error) +{ + CHTTPContext* pHTTPContext = (CHTTPContext*)pvArg; + + // Unused parameters. + (void)Error; + + TRACE("HTTP: Forcing connection context to be freed.\r\n"); + + // Free the context if it is non-NULL. + if (pHTTPContext) + { + pHTTPContext->FreeContext(TRUE); + } +} + + +/* Attempts to close the PCB connection. The context is placed into a closing + state where it waits for the data written to be acknowledged by the remote + machine before freeing up its memory. + + Parameters: + None. + Returns: + Nothing. +*/ +void CHTTPContext::CloseConnection() +{ + err_t CloseResult = ERR_MEM; + + // Validate parameters. + assert ( m_pPCB ); + + TRACE("HTTP: Closing connection to: "); + NetworkPrintRemoteAddress(m_pPCB); + TRACE("\r\n"); + + // Flag the context as being in the closing state. + m_ClosingConnection = 1; + + // Have no more data to send so no need for poll callback unless the + // tcp_close() below should fail and require retrying. + tcp_poll(m_pPCB, NULL, 0); + + // Atempt to close the connection. + CloseResult = tcp_close(m_pPCB); + if (ERR_OK != CloseResult) + { + // Close wasn't successful so re-enable the HTTPPoll() callback. + tcp_poll(m_pPCB, HTTPPoll, 4); + } +} + + +/* Frees up the resources used by the context. + + Parameters: + ForcedFree indicates whether a shutdown is being forced and therefore it is + OK to free the buffers even though they might indicate they still have + outstanding data. + + Returns: + Nothing. +*/ +void CHTTPContext::FreeContext(int ForcedFree) +{ + size_t i; + + if (m_pPCB) + { + TRACE("HTTP: Freeing context for connection to: "); + NetworkPrintRemoteAddress(m_pPCB); + TRACE("\r\n"); + } + else + { + TRACE("HTTP: Freeing connection due to unexpected close.\r\n"); + } + + // Free the HTTP context record and any resources it owned. + if (m_WaitingForBuffer) + { + m_pHTTPServer->RemoveWaitingContext(this); + } + for (i = 0 ; i < ARRAYSIZE(m_BufferQueue) ; i++) + { + SBuffer* pBuffer = m_BufferQueue[i]; + if (pBuffer) + { + if (ForcedFree) + { + // 0 out the counts if we are forcing a free. + pBuffer->BytesToWrite = 0; + pBuffer->UnacknowledgedBytes = 0; + } + + // Free the buffer. + m_pHTTPServer->FreeBuffer(m_BufferQueue[i]); + m_BufferQueue[i] = NULL; + } + } + if (m_pRequestHandlerContext) + { + m_pRequestHandlerContext->Release(); + m_pRequestHandlerContext = NULL; + } + + // Clear the callbacks for the PCB as we are getting ready to close it. + if (m_pPCB) + { + // Keep the polling callback enabled incase the previous tcp_close() + // attempt failed. + tcp_arg(m_pPCB, NULL); + tcp_accept(m_pPCB, NULL); + tcp_recv(m_pPCB, NULL); + tcp_sent(m_pPCB, NULL); + tcp_err(m_pPCB, NULL); + m_pPCB = NULL; + } + + delete this; +} + + +/* Processes the HTTP request data as it comes in from the client. + + Parameters: + pBuf points to the buffer of inbound TCP/IP data from the connection. + + Returns: + Nothing. +*/ +void CHTTPContext::ProcessRequestData(pbuf* pBuf) +{ + // Validate parameters. + assert ( pBuf ); + + switch (m_ParseState) + { + case STATE_FIRST_LINE: + case STATE_HEADERS: + BufferRequestLines(pBuf); + break; + case STATE_CONTENT: + ProcessRequestContent(pBuf); + break; + case STATE_DONE: + // We should have received everything already so discard data and + // return. + return; + } + + if (STATE_DONE == m_ParseState) + { + // Let the application interface know that we are done sending it + // the request content. + if (m_SendRequestContentToHandler) + { + m_pRequestHandlerContext->EndRequestContent(); + } + + // And prepare response headers and content to send back to client. + PrepareResponse(); + + // Attempt to allocate a buffer and fill it in with header and data + // content. + ReadContent(); + + // Attempt to send first segments of response data. + SendResponse(); + } +} + + +/* Buffers up the HTTP requests a line at a time and then processes it once + a complete line has been buffered. + + Parameters: + pBuf points to the buffer of inbound TCP/IP data from the connection. + + Returns: + Nothing. +*/ +void CHTTPContext::BufferRequestLines(pbuf* pBuf) +{ + // Validate parameters. + assert ( pBuf ); + + // Loop through all chunks in the pbuf list and parse its content a line + // at a time. + while (pBuf) + { + char* pCurr = (char*)pBuf->payload; + u16_t BytesLeft = pBuf->len; + + // Loop through the characters in this chunk. + while (BytesLeft) + { + char CurrChar = *pCurr; + + if ('\n' == CurrChar) + { + // Have encountered end of line. + m_LineBuffer[m_LineBufferOffset] = '\0'; + + ParseRequestLine(); + + // Reset pointer to start buffering next line. + m_LineBufferOffset = 0; + } + else if ('\r' != CurrChar && + m_LineBufferOffset < ARRAYSIZE(m_LineBuffer)-1) + { + // Copy character into line buffer. Above conditional protects + // from buffer overflow on the write and leaves room for NULL + // terminator. + m_LineBuffer[m_LineBufferOffset++] = CurrChar; + } + + // Advance to the next character in this chunk. + pCurr++; + BytesLeft--; + } + + // Advance to the next chunk in the pbuf list. + pBuf = pBuf->next; + } +} + + +/* Processes the HTTP request content data as it comes in from the client. + + Parameters: + pBuf points to the buffer of inbound TCP/IP data from the connection. + + Returns: + Nothing. +*/ +void CHTTPContext::ProcessRequestContent(pbuf* pBuf) +{ + // Validate parameters. + assert ( pBuf ); + + while (pBuf && m_RequestContentLength > 0); + { + size_t BytesToWrite = pBuf->len; + if (BytesToWrite > m_RequestContentLength) + { + // Make sure that we don't send the application interface more + // content than we told it we would send. + BytesToWrite = m_RequestContentLength; + } + if (m_SendRequestContentToHandler) + { + // Only send the application interface the request content if + // it has indicated that it wants to receive the data. + m_pRequestHandlerContext->WriteRequestContentBlock(pBuf->payload, BytesToWrite); + } + m_RequestContentLength -= BytesToWrite; + + // Advance to next chunk in the pbuf list. + pBuf = pBuf->next; + } + + if (0 == m_RequestContentLength) + { + // Have finished receiving the request content so advance to the next + // state where response data is sent to the client. + m_ParseState = STATE_DONE; + } +} + + +/* Attempts to parse the current line in m_LineBuffer. + + Parameters: + None. + + Returns: + Nothing. +*/ +void CHTTPContext::ParseRequestLine() +{ + switch(m_ParseState) + { + case STATE_FIRST_LINE: + ParseFirstRequestLine(); + break; + case STATE_HEADERS: + ParseHeaderRequestLine(); + break; + default: + assert ( FALSE ); + break; + } +} + + +/* Attempts to parse the first request line found in m_LineBuffer which should + contain the request method, URI, etc. + + Parameters: + None. + + Returns: + Nothing. +*/ +void CHTTPContext::ParseFirstRequestLine() +{ + static const char GetMethod[] = "GET "; + static const char HeadMethod[] = "HEAD "; + static const char PostMethod[] = "POST "; + const char* pURIStart = NULL; + const char* pURISrc = NULL; + char* pURIDest = NULL; + char UnescapedChar = 0; + int CharsToEscape = 0; + + // Advance to next state. + m_ParseState = STATE_HEADERS; + + // This is the first line which indicates method being requested + // (ie. GET, HEAD, POST). + if (0 == strncmp(m_LineBuffer, GetMethod, sizeof(GetMethod)-1)) + { + // Assume a HTTP/1.0 GET request for now. Will know later if we + // need to downgrade it to a 0.9 request when version isn't sent. + m_RequestType = TYPE_GET_1_0; + pURISrc = &m_LineBuffer[sizeof(GetMethod)-1]; + } + else if (0 == strncmp(m_LineBuffer, HeadMethod, sizeof(HeadMethod)-1)) + { + m_RequestType = TYPE_HEAD; + pURISrc = &m_LineBuffer[sizeof(HeadMethod)-1]; + } + else if (0 == strncmp(m_LineBuffer, PostMethod, sizeof(PostMethod)-1)) + { + m_RequestType = TYPE_POST; + pURISrc = &m_LineBuffer[sizeof(PostMethod)-1]; + } + else + { + // Don't recognize this method so mark it as bad. + m_RequestType = TYPE_BAD_REQUEST; + + // See if the application recognizes and wants to handle it by sending + // it the full request line. + StartBadRequest(m_LineBuffer); + return; + } + + // Skip additional whitespace that might precede URI even though there + // shouldn't be any just to be lenient. + while (' ' == *pURISrc) + { + pURISrc++; + } + pURIStart = pURISrc; + pURIDest = (char*)(void*)pURISrc; + + // Save away the URI character by character until whitespace or end + // of line is encountered. + while(' ' != *pURISrc && + '\0' != *pURISrc) + { + char CurrChar = *pURISrc; + + // Unescape % HEX HEX portions of the URI + if ('%' == CurrChar) + { + CharsToEscape = 2; + UnescapedChar = 0; + } + else if (CharsToEscape) + { + UnescapedChar <<= 4; + + if (CurrChar >= 'a' && CurrChar <= 'f') + { + CurrChar = (CurrChar - 'a') + 10; + } + else if (CurrChar >= 'A' && CurrChar <= 'F') + { + CurrChar = (CurrChar - 'A') + 10; + } + else if (CurrChar >= '0' && CurrChar <= '9') + { + CurrChar = CurrChar - '0'; + } + else + { + // Not a hex digit. + CurrChar = 0; + } + + UnescapedChar |= (CurrChar & 0xF); + + if (0 == --CharsToEscape) + { + *pURIDest++ = UnescapedChar; + } + } + else + { + *pURIDest++ = CurrChar; + } + pURISrc++; + } + + if (' ' != *pURISrc) + { + // Protocol string didn't follow URI so must be a HTTP/0.9 request. + if (TYPE_GET_1_0 == m_RequestType) + { + m_RequestType = TYPE_GET_0_9; + // This will have been the last request line for a v0.9 request so + // advance to the finished state. + m_ParseState = STATE_DONE; + } + else + { + m_RequestType = TYPE_BAD_REQUEST; + + // See if the application recognizes and wants to handle it by sending + // it the full request line. + StartBadRequest(m_LineBuffer); + return; + } + } + *pURIDest = '\0'; + + StartRequestHandler(pURIStart); +} + + +/* Attempts to parse the header request lines until a blank request line is + encountered which indicates that start of the content. + + Parameters: + None. + + Returns: + Nothing. +*/ +void CHTTPContext::ParseHeaderRequestLine() +{ + static const char ContentLengthName[] = "Content-Length"; + char* pName = m_LineBuffer; + char* pValue = NULL; + char* pCurr = m_LineBuffer; + + if ('\0' == m_LineBuffer[0]) + { + // Let the request handler know that we have reached the last header. + if (m_SendRequestHeadersToHandler) + { + m_pRequestHandlerContext->EndRequestHeaders(); + } + + if (m_RequestContentLength) + { + // Determine if the request handler is interested in any request + // content data that might follow the headers. + m_ParseState = STATE_CONTENT; + m_SendRequestContentToHandler = m_pRequestHandlerContext->BeginRequestContent(m_RequestContentLength); + } + else + { + // There is no request content from the client so server can start + // to send its response now. + m_ParseState = STATE_DONE; + } + + return; + } + + // Find the colon character which terminates the field name. A whitespace + // character at the beginning of a header line indicates a line + // continuation so handle that as well. + while (':' != *pCurr && + ' ' != *pCurr && + '\t' != *pCurr ) + { + pCurr++; + } + *pCurr++ = '\0'; + + // Skip over whitespace to find start of value. + while (' ' == *pCurr || + '\t' == *pCurr) + { + pCurr++; + } + pValue = pCurr; + + // Check for the Content-Length field which tells the server if it should + // expect the request to have a content body. + if (0 == strcasecmp(pName, ContentLengthName)) + { + m_RequestContentLength = strtoul(pValue, NULL, 10); + } + + // Send the header to request handler context interface if it has requested + // the server to do so. + if (m_SendRequestHeadersToHandler) + { + m_pRequestHandlerContext->WriteRequestHeader(pName, pValue); + } +} + + +/* Kicks off the process of handling client specified request. + + Parameters: + pURI is the name of the URI being requested. + + Returns: + Nothing. +*/ +void CHTTPContext::StartRequestHandler(const char* pURI) +{ + // Start request by first letting application provider a handler and if it + // doesn't use this object's default request handler interface instead. + switch (m_RequestType) + { + case TYPE_GET_0_9: + case TYPE_GET_1_0: + StartGetRequest(pURI); + break; + case TYPE_HEAD: + StartHeadRequest(pURI); + break; + case TYPE_POST: + StartPostRequest(pURI); + break; + default: + assert ( FALSE ); + break; + } + + // Should have a request handler now: either from application or this + // context object. + assert ( m_pRequestHandlerContext ); + + // Ask the request handler interface if it wants each request header sent + // to it. There will be no header for HTTP/0.9 GET request which skips the + // header state. + if (STATE_HEADERS == m_ParseState) + { + m_SendRequestHeadersToHandler = m_pRequestHandlerContext->BeginRequestHeaders(); + } +} + + +/* Handles GET requests by first allowing the application to handler it via a + register request handler. If it doesn't want to handle the request then the + server will attempt to satisfy the request from the file system that was + registers at bind time. + + Parameters: + pURI is the name of the URI being requested. + + Returns: + Nothing. +*/ +void CHTTPContext::StartGetRequest(const char* pURI) +{ + // Load the requested file if possible or the file not found + // content if requested file was not found. + TRACE("HTTP: GET '%s' from: ", pURI); + NetworkPrintRemoteAddress(m_pPCB); + TRACE("\r\n"); + + if (m_pHTTPServer->m_pRequestHandler) + { + // First let the application attempt to handle the GET request. + m_pRequestHandlerContext = m_pHTTPServer->m_pRequestHandler->HandleGetRequest(pURI); + } + if (!m_pRequestHandlerContext) + { + // The application doesn't want to handle the GET request so use the + // default server behaviour. + OpenFile(pURI); + m_pRequestHandlerContext = this; + } +} + + +/* Handles HEAD requests by first allowing the application to handler it via a + register request handler. If it doesn't want to handle the request then the + server will attempt to satisfy the request from the file system that was + registers at bind time. + + Parameters: + pURI is the name of the URI being requested. + + Returns: + Nothing. +*/ +void CHTTPContext::StartHeadRequest(const char* pURI) +{ + // Load the requested file if possible or the file not found + // content if requested file was not found. + TRACE("HTTP: HEAD '%s' from: ", pURI); + NetworkPrintRemoteAddress(m_pPCB); + TRACE("\r\n"); + + if (m_pHTTPServer->m_pRequestHandler) + { + // First let the application attempt to handle the HEAD request. + m_pRequestHandlerContext = m_pHTTPServer->m_pRequestHandler->HandleHeadRequest(pURI); + } + + if (!m_pRequestHandlerContext) + { + // The application doesn't want to handle the HEAD request so use the + // default server behaviour. + OpenFile(pURI); + m_pRequestHandlerContext = this; + } +} + + +/* Handles POST requests by allowing the application to handler it via a + registered request handler. If it doesn't want to handle the request then + the server will treat it as an unimplemented request. + + Parameters: + pURI is the name of the URI being requested. + + Returns: + Nothing. +*/ +void CHTTPContext::StartPostRequest(const char* pURI) +{ + // Load the requested file if possible or the file not found + // content if requested file was not found. + TRACE("HTTP: POST '%s' from: ", pURI); + NetworkPrintRemoteAddress(m_pPCB); + TRACE("\r\n"); + + if (m_pHTTPServer->m_pRequestHandler) + { + // First let the application attempt to handle the POST request. + m_pRequestHandlerContext = m_pHTTPServer->m_pRequestHandler->HandlePostRequest(pURI); + } + + if (!m_pRequestHandlerContext) + { + // The application doesn't want to handle the POST request. + m_pRequestHandlerContext = this; + } +} + + +/* Handles unknown/bad HTTP client requests by sending back an appropriate + error response. + + Parameters: + pRequest is the complete request line which wasn't recognized by the server. + + Returns: + Nothing. +*/ +void CHTTPContext::StartBadRequest(const char* pRequest) +{ + if (m_pHTTPServer->m_pRequestHandler) + { + // First let the application attempt to handle this request instead. + m_pRequestHandlerContext = m_pHTTPServer->m_pRequestHandler->HandleBadRequest(pRequest); + } + if (!m_pRequestHandlerContext) + { + // The application doesn't want to handle the bad request so just + // return an appropriate bad request response. + m_pRequestHandlerContext = this; + } + + // Ask the request handler interface if it wants each request header sent + // to it. + m_SendRequestHeadersToHandler = m_pRequestHandlerContext->BeginRequestHeaders(); +} + + +/* Called from HTTPRecv() to open the specified file that has been requested by + the HTTP client. In addition to opening the specified file, it will also + fill in the headers to match the content being sent vack. It also handles + sending back the correct header if the requested file isn't found. + + Parameters: + pURI is the name of the URI being requested. + + Returns: + Nothing. +*/ +void CHTTPContext::OpenFile(const char* pURI) +{ + static const char NotFoundHTML[] = "<html>" + "<body><h2>404: File not found.</h2></body>" + "</html>\r\n"; + // The content types supported by this HTTP server. + static const char ContentHTML[] = "Content-type: text/html\r\n"; + static const char ContentGIF[] = "Content-type: image/gif\r\n"; + static const char ContentPNG[] = "Content-type: image/png\r\n"; + static const char ContentJPG[] = "Content-type: image/jpeg\r\n"; + static const char ContentBMP[] = "Content-type: image/bmp\r\n"; + static const char ContentICO[] = "Content-type: image/x-icon\r\n"; + static const char ContentStream[] = "Content-type: application/octet-stream\r\n"; + static const char ContentJScript[] = "Content-type: application/x-javascript\r\n"; + static const char ContentCSS[] = "Content-type: text/css\r\n"; + static const char ContentFlash[] = "Content-type: application/x-shockwave-flash\r\n"; + static const char ContentXML[] = "Content-type: text/xml\r\n"; + static const char ContentDefault[] = "Content-type: text/plain\r\n"; + + // Table used to map file extensions to above content type headers. + #define CONTENT_HEADER(X) X, sizeof(X)-1 + static const struct + { + // File extension. + const char* pExtension; + // Content header to be used for this file extension type. + const char* pContentHeader; + // Length of pContentHeader string without NULL terminator. + unsigned int ContentHeaderLength; + } ContentMapping[] = + { + {".html", CONTENT_HEADER(ContentHTML)}, + {".htm", CONTENT_HEADER(ContentHTML)}, + {".gif", CONTENT_HEADER(ContentGIF)}, + {".png", CONTENT_HEADER(ContentPNG)}, + {".jpg", CONTENT_HEADER(ContentJPG)}, + {".bmp", CONTENT_HEADER(ContentBMP)}, + {".ico", CONTENT_HEADER(ContentICO)}, + {".class", CONTENT_HEADER(ContentStream)}, + {".cls", CONTENT_HEADER(ContentStream)}, + {".js", CONTENT_HEADER(ContentJScript)}, + {".css", CONTENT_HEADER(ContentCSS)}, + {".swf", CONTENT_HEADER(ContentFlash)}, + {".xml", CONTENT_HEADER(ContentXML)} + }; + // Local variables. + size_t i; + char LocalFilename[HTTP_MAX_FILENAME+1]; + + // Attempt to open the file. + if (0 == strcmp(pURI, "/")) + { + // Iterate through the list of default root filenames. + for (i = 0 ; !m_pFile && i < ARRAYSIZE(g_DefaultFilenames) ; i++) + { + snprintf(LocalFilename, ARRAYSIZE(LocalFilename), "/%s%s", m_pHTTPServer->m_pRootPathname, g_DefaultFilenames[i]); + m_pFile = fopen(LocalFilename, "r"); + } + } + else + { + // Attempt to open the specified URI. + snprintf(LocalFilename, ARRAYSIZE(LocalFilename), "/%s%s", m_pHTTPServer->m_pRootPathname, pURI); + m_pFile = fopen(LocalFilename, "r"); + } + + // Setup the content type field based on whether the file was found or not. + if (!m_pFile) + { + // The file open failed so return a 404 error with HTML. + m_pContentType = ContentHTML; + m_ContentTypeLength = sizeof(ContentHTML) - 1; + m_pErrorHTML = NotFoundHTML; + m_ErrorHTMLLength = sizeof(NotFoundHTML) - 1; + } + else + { + const char* pExtension; + + // Configure the content type header based on filename extension. + pExtension = strrchr(LocalFilename, '.'); + for (i = 0 ; pExtension && i < ARRAYSIZE(ContentMapping) ; i++) + { + if (0 == strcmp(pExtension, ContentMapping[i].pExtension)) + { + m_pContentType = ContentMapping[i].pContentHeader; + m_ContentTypeLength = ContentMapping[i].ContentHeaderLength; + break; + } + } + + // If the type couldn't be found default to plain. + if (!m_pContentType) + { + m_pContentType = ContentDefault; + m_ContentTypeLength = sizeof(ContentDefault) - 1; + } + } +} + + +/* Prepares the headers and content to be sent back to the HTTP client as a + response to its latest request. It then starts sending out the first set of + response packets. + + Parameters: + None. + + Returns: + Nothing. +*/ +void CHTTPContext::PrepareResponse() +{ + assert ( STATE_DONE == m_ParseState ); + + PrepareResponseHeaders(); + + if (!m_pRequestHandlerContext->BeginResponseContent()) + { + // The application has no content to send so release it. + m_pRequestHandlerContext->Release(); + m_pRequestHandlerContext = NULL; + } +} + + +/* Prepares the headers and content to be sent back to the HTTP client as a + response to its latest request. It then starts sending out the first set of + response packets. + + Parameters: + None. + + Returns: + Nothing. +*/ +void CHTTPContext::PrepareResponseHeaders() +{ + static const char BlankLine[] = "\r\n"; + const char* pStatusLine = NULL; + const char* pExtraHeaders = NULL; + size_t StatusLineLength = 0; + size_t ExtraHeadersLength = 0; + size_t i = 0; + + if (TYPE_GET_0_9 == m_RequestType) + { + // Don't return any headers for a HTTP/0.9 GET request. + m_HeaderArray[0].pHeaderString = NULL; + m_HeaderArray[0].HeaderLength = 0; + return; + } + + // Prepare the response status. + pStatusLine = m_pRequestHandlerContext->GetStatusLine(&StatusLineLength); + + // Application must return a status line. + assert ( pStatusLine && StatusLineLength > 0 ); + + m_HeaderArray[0].pHeaderString = pStatusLine; + m_HeaderArray[0].HeaderLength = StatusLineLength; + + // The second header returned will always be the server type, no + // matter what response we send back. + m_HeaderArray[1].pHeaderString = m_pHTTPServer->m_ServerHeader; + m_HeaderArray[1].HeaderLength = m_pHTTPServer->m_ServerHeaderLength; + + // Find out what other headers the application would like to send back to + // the HTTP client. + pExtraHeaders = m_pRequestHandlerContext->GetResponseHeaders(&ExtraHeadersLength); + if (pExtraHeaders) + { + m_HeaderArray[2].pHeaderString = pExtraHeaders; + m_HeaderArray[2].HeaderLength = ExtraHeadersLength; + i = 3; + } + else + { + // Won't be using all of the elements of m_HeaderArray. + i = 2; + } + + // Finally send the blank line header which separates the headers from the + // entity body. + m_HeaderArray[i].pHeaderString = BlankLine; + m_HeaderArray[i].HeaderLength = sizeof(BlankLine) -1; +} + +/* Attempts to allocate a SBuffer object for reading the data into. If such a + buffer is available then read HTTP headers and file data into the buffer. + If the buffer can't be allocated, then add this client to a queue to be + processed later. + + Parameters: + None. + + Returns: + Nothing. +*/ +void CHTTPContext::ReadContent() +{ + // Local variables. + SBuffer* pWriteBuffer = m_BufferQueue[m_BufferQueueHead]; + unsigned int BytesLeftInBuffer; + char* pData; + unsigned int i; + + + // Return now if there is already a buffer filled with data in the process + // of being written out to the connection. Also return if there is no more + // data to be sent to this client. + if (pWriteBuffer || + (m_HeaderIndex >= ARRAYSIZE(m_HeaderArray) && + !m_pRequestHandlerContext)) + { + return; + } + + // Attempt to allocate a new SBuffer. + pWriteBuffer = m_pHTTPServer->AllocateBuffer(this); + if (!pWriteBuffer) + { + // There is no buffer available at this time. Will be called back + // later when one becomes available. + return; + } + assert ( 0 == pWriteBuffer->BytesToWrite ); + assert ( 0 == pWriteBuffer->UnacknowledgedBytes ); + + // Buffer is free. Now fill it with headers and file data. + BytesLeftInBuffer = sizeof(pWriteBuffer->Data); + pData = pWriteBuffer->Data; + pWriteBuffer->pWrite = pData; + + // Copy remaining headers into free buffer. + for (i = m_HeaderIndex ; + i < ARRAYSIZE(m_HeaderArray) ; + i++) + { + unsigned int BytesToCopy; + unsigned int HeaderBytesLeft; + + assert ( i == m_HeaderIndex ); + + if (!m_HeaderArray[i].pHeaderString) + { + // Not all (or any) of the headers were used so mark as done and + // no need to copy any more so break out of loop. + m_HeaderIndex = ARRAYSIZE(m_HeaderArray); + break; + } + + // Determine how many bytes are to be copied. + HeaderBytesLeft = m_HeaderArray[i].HeaderLength - + m_HeaderOffset; + if (HeaderBytesLeft > BytesLeftInBuffer) + { + BytesToCopy = BytesLeftInBuffer; + } + else + { + BytesToCopy = HeaderBytesLeft; + } + + // Perform the copy. + memcpy(pData, + m_HeaderArray[i].pHeaderString + m_HeaderOffset, + BytesToCopy); + + // Update buffer information. + pData += BytesToCopy; + BytesLeftInBuffer -= BytesToCopy; + HeaderBytesLeft -= BytesToCopy; + if (0 == HeaderBytesLeft) + { + // Advance to next header. + m_HeaderIndex++; + m_HeaderOffset = 0; + } + else + { + // Advance offset within this header and exit since buffer is full. + assert ( 0 == BytesLeftInBuffer ); + + m_HeaderOffset += BytesToCopy; + break; + } + } + + // Copy file data into buffer for sending. + if (m_pRequestHandlerContext) + { + size_t BytesRead; + + BytesRead = m_pRequestHandlerContext->ReadResponseContentBlock(pData, BytesLeftInBuffer); + if (BytesRead < BytesLeftInBuffer) + { + // Must have hit end of file, so close it. + m_pRequestHandlerContext->EndResponseContent(); + m_pRequestHandlerContext->Release(); + m_pRequestHandlerContext = NULL; + } + pData += BytesRead; + BytesLeftInBuffer -= BytesRead; + } + + // Update the buffer valid byte count. + pWriteBuffer->BytesToWrite = ARRAYSIZE(pWriteBuffer->Data) - + BytesLeftInBuffer; + + // Update the client context to indicate that it now has a buffer ready to + // be written out to the client connection. + m_BufferQueue[m_BufferQueueHead] = pWriteBuffer; +} + + +/* Attempts to send HTTP client data already read into buffer and also reads + in more data to the buffer for sending as necessary. This data will be + composed of HTTP headers and file data. + + Parameters: + None. + + Returns: + Nothing. +*/ +void CHTTPContext::SendResponse() +{ + int ConnectionClosed; + + // Attempt to send any data still in write buffer. + ConnectionClosed = WriteData(); + + // Return now if the connection was closed. + if (ConnectionClosed) + { + return; + } + + // Read in more content if possible. + ReadContent(); + + // UNDONE: Once I have a performance test available, try removing this to see if it has any impact on performance. + // Also want to wait for DMA based driver. + // Attempt to send data that was just read into buffer. + WriteData(); +} + + +/* Sends any remaining response data to the HTTP client and closes the + connection once the data has been sent. + + Parameters: + None. + + Returns: + 1 if the last chunk was sent and the connection closed and 0 otherwise. +*/ +int CHTTPContext::WriteData() +{ + // Local variables. + err_t WriteResult = ERR_MEM; + size_t BytesToWrite; + size_t SendBufferSize; + SBuffer* pWriteBuffer = m_BufferQueue[m_BufferQueueHead]; + + // Just return if there isn't a write buffer with data to send. + if (!pWriteBuffer) + { + return 0; + } + + // Don't try to send more bytes than what the connection will accept. + BytesToWrite = pWriteBuffer->BytesToWrite; + SendBufferSize = tcp_sndbuf(m_pPCB); + if (BytesToWrite > SendBufferSize) + { + BytesToWrite = SendBufferSize; + } + + // Keep attempting to send response data and shrinking the write until it + // succeeds or it shrinks down to 0 bytes. + while (BytesToWrite > 0) + { + // Attempt to write the response data out to the PCB. + WriteResult = tcp_write(m_pPCB, + pWriteBuffer->pWrite, + BytesToWrite, + 0); + if (ERR_OK == WriteResult) + { + // Write was successful so update buffer descriptors in the context + // objects. + pWriteBuffer->pWrite += BytesToWrite; + pWriteBuffer->BytesToWrite -= BytesToWrite; + pWriteBuffer->UnacknowledgedBytes += BytesToWrite; + + // Successfully queued up data for sending so reset retry count. + m_RetryCount = 0; + break; + } + else if (ERR_MEM == WriteResult) + { + // Failed to perform the write due to out of memory error. + // Check to see if the send queue if full. If not, half the write + // size and try again. + if (0 == tcp_sndqueuelen(m_pPCB)) + { + break; + } + // UNDONE: Rather than half it, subtract the size of a MSS. + BytesToWrite >>= 1; + } + else + { + // Received unexpected error. + break; + } + } + + if (0 == pWriteBuffer->BytesToWrite) + { + // There are no more bytes to write from the buffer so advance the + // header pointer to the next item in the buffer queue. + m_BufferQueueHead = (m_BufferQueueHead + 1) & 1; + + // If there are no more bytes to send and the client has finished sending + // its request then we should attempt to close the connection. + if (m_HeaderIndex >= ARRAYSIZE(m_HeaderArray) && + !m_pRequestHandlerContext && + STATE_DONE == m_ParseState) + { + TRACE("HTTP: Response send completed to: "); + NetworkPrintRemoteAddress(m_pPCB); + TRACE("\r\n"); + CloseConnection(); + + return 1; + } + } + + return 0; +} + + +/* Called for a context which was waiting for a buffer when a buffer is freed + by another connection. + + Parameters: + None. + + Returns: + Nothing. +*/ +void CHTTPContext::BufferAvailable() +{ + // Allow the context to allocate the buffer and fill it in with data. + ReadContent(); + + // Allow the context to send the data that was just read into the + // buffer. + SendResponse(); + + // Tell lwIP to send the segments for this PCB as it normally only + // queues up segments automatically for the PCB whose callback is + // currently being called. + tcp_output(m_pPCB); +} + + + +/* Constructor */ +CHTTPServer::CHTTPServer() +{ + m_pHTTPListenPCB = NULL; + m_pContextWaitingHead = NULL; + m_pContextWaitingTail = NULL; + m_pRootPathname = NULL; + m_pRequestHandler = NULL; + m_ServerHeaderLength = 0;; + m_ServerHeader[0] = '\0'; + memset(m_BufferPool, 0, sizeof(m_BufferPool)); +} + + +/* Attach IRequestHandler to the HTTP server to allow application to add + custom GET/POST handling. + + Parameters: + pHandler is a pointer to the application specific request handler object + to be used by the HTTP server when it receives GET/POST requests. + + Returns: + Returns 0 on successful attachment and a positive value otherwise. +*/ +int CHTTPServer::AttachRequestHandler(IHTTPRequestHandler* pHandler) +{ + m_pRequestHandler = pHandler; + + return 0; +} + + +/* Initializes the HTTP server using the raw lwIP APIs. This consists of + creating a PCB (low level lwIP socket), binding it to port 80 and then + listening for connections on this port. When subsequent connection comes + into the HTTP server, a callback will be made into the HTTPAccept() + function that is registered here as well. + + Parameters: + pRootPathame is the pathname of the default root directory from which the + HTTP GET requests are satisfied. + pServerName is the name of the server to be returned to the HTTP client + in the Server header. + BindPort is the TCP/IP port to which the HTTP server should listen for + incoming requests. The default port for HTTP would be 80. + + Returns: + 0 on success and a positive error code otherwise. +*/ +int CHTTPServer::Bind(const char* pRootPathname, + const char* pServerName, + unsigned short BindPort) +{ + int Return = 1; + tcp_pcb* pcb = NULL; + int Length = -1; + err_t Result; + + // Validate parameters. + assert ( pRootPathname && pServerName ); + + // Create server header string. + Length = snprintf(m_ServerHeader, ARRAYSIZE(m_ServerHeader), "Server: %s\r\n", pServerName); + if (Length < 0 || Length >= (int)ARRAYSIZE(m_ServerHeader)) + { + printf("error: '%s' is too long for the HTTP server name string.\r\n", pServerName); + goto Error; + } + m_ServerHeaderLength = (size_t)Length; + + // Save away root directory name. + m_pRootPathname = pRootPathname; + + // Create the PCB (low level lwIP socket) for the HTTP server to listen to + // on well known HTTP port 80. + pcb = tcp_new(); + if (!pcb) + { + printf("error: Failed to create new connection for HTTP server to listen upon.\r\n"); + goto Error; + } + + // Bind the PCB to port 80. + Result = tcp_bind(pcb, IP_ADDR_ANY, BindPort); + if (ERR_OK != Result) + { + printf("error: HTTP server failed to bind to port %d.\r\n", BindPort); + goto Error; + } + + // Listen on this PCB. + m_pHTTPListenPCB = tcp_listen(pcb); + if (!m_pHTTPListenPCB) + { + printf("error: HTTP server failed to listen on port %d.\r\n", BindPort); + goto Error; + } + + // pcb was freed by successful tcp_listen() call. + pcb = NULL; + + // When connections come in on port 80, we want lwIP to call our + // HTTPAccept() function to handle the connection. + tcp_arg(m_pHTTPListenPCB, this); + tcp_accept(m_pHTTPListenPCB, HTTPAccept); + + // Return to the main program since there is nothing left to do until a + // client tries to make a connection on port 80, at which time lwIP will + // get the connection packet from the Ethernet driver and make the callback + // into HTTPAccept(). + + Return = 0; +Error: + if (pcb) + { + tcp_close(pcb); + } + return Return; +} + + +/* Called by lwIP whenever a connection is made on the PCB which is listening + on port 80 for HTTP requests. The main task of this callback is to create + any necessary state to track this unique HTTP client connection and then + setup the callbacks to be called when activity occurs on this client + connection. + + Parameters: + pvArg is the value set on the listening PCB through the call to tcp_arg in + HTTPInit() which is a pointer to the intitialized network object. + pNewPCB is a pointer to the new PCB that was just allocated by the lwIP + stack for this new client connection on port 80. + err is passed in from the lwIP stack to indicate if an error occurred. + + Returns: + ERR_MEM if we don't have enough memory for this connection and ERR_OK + otherwise. +*/ +err_t CHTTPServer::HTTPAccept(void* pvArg, tcp_pcb* pNewPCB, err_t err) +{ + CHTTPServer* pHTTPServer = (CHTTPServer*)pvArg; + CHTTPContext* pHTTPContext = NULL; + (void)err; + + // Validate parameters. + assert ( pvArg ); + assert ( pNewPCB ); + + // Let user know that we have just accepted a new connection. + TRACE("HTTP: Accepting client connection from: "); + NetworkPrintRemoteAddress(pNewPCB); + TRACE("\r\n"); + + // Let the lwIP stack know that the HTTP application has successfuly + // received the client connection on the listening PCB. + tcp_accepted(pHTTPServer->m_pHTTPListenPCB); + + // UNDONE: Remove this new call and use a pool instead. + // Allocate memory to track context for this client connection and + // associate with the PCB. + pHTTPContext = new CHTTPContext(pHTTPServer, pNewPCB); + if (!pHTTPContext) + { + TRACE("HTTP: Failed to allocate client context for: "); + NetworkPrintRemoteAddress(pNewPCB); + TRACE("\r\n"); + + // lwIP will shutdown this connection when it get back a return value + // other than ERR_OK. + return ERR_MEM; + } + + return ERR_OK; +} + + +/* Removes the specified context from the linked list of waiting contexts. + + Parameters: + pHTTPContext is a pointer to the context object for the client connection + to be removed. + + Returns: + Nothing. +*/ +void CHTTPServer::RemoveWaitingContext(CHTTPContext* pHTTPContext) +{ + // Find this node in the waiting queue and remove it. + CHTTPContext* pPrev = NULL; + CHTTPContext* pCurr = m_pContextWaitingHead; + while(pCurr) + { + CHTTPContext* pNext = pCurr->GetNext(); + + if (pCurr == pHTTPContext) + { + // Found this context. Now remove it and exit loop. + pCurr->RemovedFromWaitingList(pPrev); + if (!pPrev) + { + // This node was at the head so update the head pointer. + m_pContextWaitingHead = pNext; + } + if (!pNext) + { + // This node was at the tail so reset the tail to previous node. + m_pContextWaitingTail = pPrev; + } + break; + } + pPrev = pCurr; + pCurr = pNext; + } + // We should always find this context in the list. + assert ( pCurr == pHTTPContext ); +} + + +/* Attempts to allocate a SBuffer object into which content can be placed for + sending back to the HTTP client. If it fails to find a free buffer in the + server's pool then it will add the client context to a queue and return + NULL. + + Parameters: + pHTTPContext is a pointer to the context asking for a buffer object. + + Returns: + A pointer to a free SBuffer object or NULL if none are free. +*/ +SBuffer* CHTTPServer::AllocateBuffer(CHTTPContext* pHTTPContext) +{ + CHTTPContext* pPrev = NULL; + + // Validate parameters. + assert ( pHTTPContext ); + + // Local variables. + size_t i; + + // Just return NULL if this context is already waiting for a buffer. + if (pHTTPContext->IsWaitingForBuffer()) + { + return NULL; + } + + // Search for a free buffer in the pool. + for (i = 0 ; i < ARRAYSIZE(m_BufferPool) ; i++) + { + // A buffer is free if it has no bytes left to write to the connection + // and the remote machine has acknowledged all previously written bytes + // from this buffer. + SBuffer* pBuffer = &(m_BufferPool[i]); + + if (0 == pBuffer->BytesToWrite && + 0 == pBuffer->UnacknowledgedBytes) + { + return pBuffer; + } + } + + // Get here if there wasn't a free buffer in the pool so add this context + // to the queue to be serviced later. + assert (!pHTTPContext->GetNext()); + if (m_pContextWaitingTail) + { + // Add to end of list. + pPrev = m_pContextWaitingTail; + m_pContextWaitingTail = pHTTPContext; + } + else + { + // Adding entry to empty queue. + assert (!m_pContextWaitingHead); + + m_pContextWaitingHead = pHTTPContext; + m_pContextWaitingTail = pHTTPContext; + } + pHTTPContext->AddToWaitingList(pPrev); + + // Let calling context know that it will have to wait until later for a + // buffer. + return NULL; +} + + +/* Frees a SBuffer object back into the server's pool. If there is a client + context waiting in the queue, then callback into the context and let + it use the buffer now that it is free. + + Parameters: + pBuffer is a pointer to the buffer to be freed. + + Returns: + Nothing. +*/ +void CHTTPServer::FreeBuffer(SBuffer* pBuffer) +{ + // Validate parameters. + assert ( pBuffer ); + + // The SBuffer shouldn't be in use when this routine is called. + assert ( 0 == pBuffer->BytesToWrite ); + assert ( 0 == pBuffer->UnacknowledgedBytes ); + + // See if there are other client connection waiting for a buffer to become + // free. If so, give it an apportunity to make use of this one. + if (m_pContextWaitingHead) + { + CHTTPContext* pHTTPContext = m_pContextWaitingHead; + + // Remove the context from the waiting list. + m_pContextWaitingHead = pHTTPContext->GetNext(); + if (!m_pContextWaitingHead) + { + // We just emptied the list so clear tail pointer as well. + assert ( m_pContextWaitingTail == pHTTPContext ); + + m_pContextWaitingTail = NULL; + } + + // Let context know that it is no longer in a list. + pHTTPContext->RemovedFromWaitingList(NULL); + pHTTPContext->BufferAvailable(); + } +} + + +/* Prints out the remote machine IP address and port number. + + Parameters: + pPCB is a pointer to the TCP protocol control block. + + Returns: + Nothing. +*/ +static void NetworkPrintRemoteAddress(tcp_pcb* pPCB) +{ + // Validate paramters. + assert ( pPCB ); + + if (HTTP_TRACE) + { + SNetwork_PrintAddress(&pPCB->remote_ip); + printf(":%u", pPCB->remote_port); + } +}