A simple web server that can be bound to either the EthernetInterface or the WiflyInterface.
Dependents: Smart-WiFly-WebServer WattEye X10Svr SSDP_Server
SW_HTTPServer.cpp
- Committer:
- WiredHome
- Date:
- 2013-12-30
- Revision:
- 32:7ded9bacb546
- Parent:
- 31:8f72be717a3c
- Child:
- 33:ef165a67ab22
File content as of revision 32:7ded9bacb546:
// // @note Copyright © 2013 by Smartware Computing, all rights reserved. // Individuals may use this application for evaluation or non-commercial // purposes. Within this restriction, changes may be made to this application // as long as this copyright notice is retained. The user shall make // clear that their work is a derived work, and not the original. // Users of this application and sources accept this application "as is" and // shall hold harmless Smartware Computing, for any undesired results while // using this application - whether real or imagined. // // author David Smart, Smartware Computing // #include "mbed.h" #include "SW_HTTPServer.h" // define DEBUG before this #define DEBUG "httpd" #include "Utility.h" #define CHUNKSIZE 1600 // When receiving and handing partial to the app, this is the chunk size const char * DEFAULT_FILENAME = "index.htm"; // Header information to always send (must be \r\n terminated) const char hdr_httpver[] = "HTTP/1.0"; // Wifly may not be able to support HTTP/1.1 protocol const char hdr_age[] = "Max-age: 0\r\n"; // expires right away const char hdr_server[] = "Server: Smart_Server v0.1\r\n"; // Server const char hdr_close[] = "Connection: close\r\n"; // tell the client the server closes the connection immediately const char nl[] = "\r\n"; // final \r\n for the termination of the header static const struct { char *ext; char *filetype; } extensions [] = { {".gif", "Content-Type: image/gif\r\n" }, {".jpg", "Content-Type: image/jpeg\r\n" }, {".jpeg","Content-Type: image/jpeg\r\n" }, {".ico", "Content-Type: image/x-icon\r\n" }, {".png", "Content-Type: image/png\r\n" }, {".zip", "Content-Type: image/zip\r\n" }, {".gz", "Content-Type: image/gz\r\n" }, {".tar", "Content-Type: image/tar\r\n" }, {".txt", "Content-Type: plain/text\r\n" }, {".pdf", "Content-Type: application/pdf\r\n" }, {".htm", "Content-Type: text/html\r\n" }, {".html","Content-Type: text/html\r\n" }, {0,0} }; #if 0 && defined(DEBUG) // Haven't learned anything from this in a long time, so disabled. // This uses standard library dynamic memory management, but for an // embedded system there are alternates that may make better sense - // search the web for embedded system malloc alternates. void * HTTPServer::MyMalloc(int x, int y) { pc->printf("[INF HTTP%4d] malloc(%d)\r\n", y, x); return malloc(x); } char HTTPServer::toP(void * x) { char * c = (char *) x; if (*c >= ' ' && *c < 0x7F) // isprint() return *c; else return '.'; } #define mymalloc(x) MyMalloc(x, __LINE__) #define myfree(x) \ pc->printf("[INF HTTP%4d] free(%02x %02x %02x %02x %02x ... %c%c%c%c%c)\r\n", __LINE__, \ *x, *(x+1), *(x+2), *(x+3), *(x+4), \ toP(x), toP(x+1), toP(x+2), toP(x+3), toP(x+4) ); \ free(x); #else #define mymalloc(x) malloc(x) #define myfree(x) free(x) #endif HTTPServer::HTTPServer( Wifly * _wf, int port, const char * _webroot, int maxheaderParams, int _maxqueryParams, int _maxdynamicpages, PC * _pc, int _allocforheader, int _allocforfile) { wifly = _wf; webroot = (char *)malloc(strlen(_webroot)+1); strcpy(webroot, _webroot); if (strlen(webroot)>1 && webroot[strlen(webroot)-1] == '/') // remove trailing '/' webroot[strlen(webroot)-1] = '\0'; maxqueryParams = _maxqueryParams; maxdynamicpages = _maxdynamicpages; headerParams = (namevalue *)malloc(maxheaderParams * sizeof(namevalue)); queryParams = (namevalue *)malloc(maxqueryParams * sizeof(namevalue)); handlers = (handler *)malloc(maxdynamicpages * sizeof(handler)); headerbuffersize = _allocforheader; headerbuffer = (char *)malloc(headerbuffersize); pc = _pc; queryType = NULL; queryString = NULL; postQueryString = NULL; queryParamCount = 0; handlercount = 0; maxheaderbytes = 0; server = new TCPSocketServer(); server->bind(port); server->listen(); server->set_blocking(false, 10); ResetPerformanceData(); PerformanceTimer.start(); } HTTPServer::~HTTPServer() { int i; for (i=0; i<handlercount; i++) myfree(handlers[i].path); myfree(headerbuffer); myfree(handlers); myfree(queryParams); myfree(webroot); webroot = NULL; } int HTTPServer::GetMaxHeaderSize() { return maxheaderbytes; } bool HTTPServer::RegisterHandler(const char * path, Handler callback) { if (handlercount < maxdynamicpages && path && callback) { handlers[handlercount].path = (char *)mymalloc(strlen(path)+1); memcpy(handlers[handlercount].path, path, strlen(path)+1); handlers[handlercount].callback = callback; handlercount++; return true; } else { return false; } } // Poll() // // *OPEN*GET /x=1 HTTP/1.1 // Host: 192.168.1.140 // Connection: keep-alive // Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 // User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36 // Accept-Encoding: gzip,deflate,sdch // Accept-Language: en-US,en;q=0.8 // void HTTPServer::Poll() { typedef enum { Idle, // waiting for a connection ReceivingHeader, // receiving header ReceivingPayload, // receiving a section after the Header Sending, // send the response WaitingToClose, // small timeout to close Reset } state; static state op = Idle; static char * bPtr = headerbuffer; int n; static unsigned int t_ref; // reference point for the PerformanceTimer #if defined(DEBUG) static state lastOp = Reset; if (lastOp != op) { const char *states[] = {"Idle", "ReceivingHeader", "ReceivingPayload", "Sending", "WaitingToClose", "Reset"}; INFO("Poll: %s", states[op]); lastOp = op; } #endif switch(op) { default: // not expected to arrive here op = Idle; break; case Idle: PerformanceTimer.reset(); t_ref = (unsigned int)PerformanceTimer.read_us(); bPtr = headerbuffer; if (0 == server->accept(client)) { op = ReceivingHeader; t_ref = RecordPerformanceData(&perfData.ConnectionAccepted, t_ref); INFO("Accepted at %u", (unsigned int)PerformanceTimer.read_us()); } break; case ReceivingHeader: n = client.receive(bPtr, headerbuffersize - (bPtr - headerbuffer)); if (n < 0) { op = Sending; INFO("*** client.receive() => %d", n); } else if (n) { bPtr[n] = '\0'; switch (ParseHeader(headerbuffer)) { case ACCEPT_ERROR: break; case ACCEPT_COMPLETE: op = Sending; t_ref = RecordPerformanceData(&perfData.HeaderParsed, t_ref); INFO("Header Parsed at %u", (unsigned int)PerformanceTimer.read_us()); break; case ACCEPT_CONTINUE: op = ReceivingPayload; break; } bPtr += n; } break; case ReceivingPayload: // After the header, there is a payload that will be handled op = Sending; break; case Sending: SendResponse(); op = WaitingToClose; t_ref = RecordPerformanceData(&perfData.ResponseSent, t_ref); INFO("Response Sent at %u", (unsigned int)PerformanceTimer.read_us()); break; case WaitingToClose: INFO("Connection closed entry %u", (unsigned int)PerformanceTimer.read_us()); close_connection(); op = Idle; RecordPerformanceData(&perfData.ConnectionClosed, t_ref); INFO("Connection closed exit %u", (unsigned int)PerformanceTimer.read_us()); break; } } const char * HTTPServer::GetSupportedType(const char * filename) { int i; int buflen = strlen(filename); int extlen; for (i=0; extensions[i].ext != 0; i++) { extlen = strlen(extensions[i].ext); if ( !strncmp(&filename[buflen-extlen], extensions[i].ext, extlen)) { return extensions[i].filetype; } } return NULL; } void HTTPServer::send(const char * msg, int bytes) { if (bytes == -1) bytes = strlen(msg); client.send((char *)msg, bytes); } bool HTTPServer::SendFile(const char * filename, const char * filetype) { FILE * fp; fp = fopen(filename,"rb"); if (fp) { // can open it char *fbuffer = (char *)mymalloc(FILESEND_BUF_SIZE); int bytes; if (fbuffer) { header(200, "OK", filetype); bytes = fread(fbuffer,sizeof(char),FILESEND_BUF_SIZE,fp); while (bytes > 0) { send(fbuffer, bytes); bytes = fread(fbuffer,sizeof(char),FILESEND_BUF_SIZE,fp); } myfree(fbuffer); } else { header(500, "Server Error", "Pragma: err - insufficient memory\r\n"); } fclose(fp); return true; } else { header(404, "Not Found", "Pragma: err - Can't open file\r\n"); return false; } } int HTTPServer::HexCharToInt(char c) { if (c >= 'a' && c <= 'f') return (c - 'a' + 10); else if (c >= 'A' && c <= 'F') return (c - 'A' + 10); else if (c >= '0' && c <= '9') return c - '0'; else return 0; } char HTTPServer::HexPairToChar(char * p) { return 16 * HexCharToInt(*p) + HexCharToInt(*(p+1)); } void HTTPServer::UnescapeString(char * encoded) { char *p; // first convert '+' to ' ' p = strchr(encoded, '+'); while (p) { *p = ' '; p = strchr(encoded, '+'); } // then convert hex '%xx' to char 'x' p = strchr(encoded, '%'); while (p) { if (strchr("0123456789ABCDEFabcdef", *(p+1)) && strchr("0123456789ABCDEFabcdef", *(p+2)) ) { *p = HexPairToChar(p+1); p++; // advance past the % char * a = p; char * b = p + 2; do { *a++ = *b++; } while (*b); *a = '\0'; } p = strchr(p, '%'); } } const char * HTTPServer::GetParameter(const char * name) { for (int i=0; i<queryParamCount; i++) { if (strcmp(queryParams[i].name, name) == 0) { return queryParams[i].value; } } return NULL; } // this=that&who=what&more=stuff... // ^ ^ ^ void HTTPServer::ParseParameters(char * pName) { char * pVal; char * pNextName; // Parse queryParams pVal = strchr(pName, '#'); // If there is a '#fragment_id', we can ignore it if (pVal) *pVal = '\0'; do { queryParams[queryParamCount].name = pName; pVal = strchr(pName, '='); pNextName = strchr(pName,'&'); if (pVal) { if (pNextName == NULL || (pNextName && pNextName > pVal)) { *pVal++ = '\0'; queryParams[queryParamCount].value = pVal; pName = pVal; } } queryParamCount++; if (pNextName) { pName = pNextName; *pName++ = '\0'; } else { pName = NULL; } } while (pName && queryParamCount < maxqueryParams); } void HTTPServer::header(int code, const char * code_text, const char * content_type, const char * optional_text) { char http[100]; sprintf(http, "%s %i %s\r\n", hdr_httpver, code, code_text); send(http); send(hdr_age); send(hdr_server); if (content_type) { send(content_type); } if (optional_text) { send(optional_text); } send(hdr_close); send(nl); } bool HTTPServer::close_connection() { bool res; res = server->close(); INFO("close connection returned %d", res); return res; } bool HTTPServer::Extract(char * haystack, char * needle, char ** string) { bool ret = false; // assume failure until proven otherwise char * qs = NULL; char * eqs = NULL; char * container = NULL; char * get = strstr(haystack, needle); // what if not at the front? if (get) { // Seems to be a valid "...GET /QueryString HTTP/1.1" // or "...<needle>param..." qs = get + strlen(needle); // in case the needle didn't have space delimiters while (*qs == ' ') qs++; // /QueryString\0HTTP/1.1\0\0 if (*string) // recycle old string when working a new one myfree(*string); container = (char *)mymalloc(strlen(qs)); if (container) { strcpy(container, qs); eqs = strchr(container, ' '); if (eqs) *eqs = '\0'; *string = container; INFO("Extract(%s) = %s", needle, container); ret = true; } else { *string = NULL; // something bad happened... no memory } } return ret; } char * HTTPServer::rewriteWithDefaultFile(char * queryString) { char * temp = (char *)mymalloc(strlen(queryString) + strlen(DEFAULT_FILENAME) + 1); if (temp) { *temp = '\0'; strcpy(temp, queryString); strcat(temp, DEFAULT_FILENAME); myfree(queryString); return temp; } else { return queryString; } } char * HTTPServer::rewritePrependWebroot(char * queryString) { char * temp = (char *)mymalloc(strlen(webroot) + strlen(queryString) + 2); // save room for '/' if (temp) { char *lastC; *temp = '\0'; strcpy(temp, webroot); lastC = &temp[strlen(temp)-1]; if (*lastC == '/' && *queryString == '/') queryString++; // don't create two '/' else if (*lastC != '/' && *queryString != '/') strcat(temp, "/"); strcat(temp, queryString); myfree(queryString); return temp; } else { return queryString; } } bool HTTPServer::CheckDynamicHandlers() { bool regHandled = false; // If this queryString is in the list of registered handlers, call that for (int i=0; i<handlercount; i++) { if (strcmp(handlers[i].path, queryString) == 0) { (*handlers[i].callback)(this, SEND_PAGE, queryString, queryParams, queryParamCount); regHandled = true; break; // we only execute the first one } } return regHandled; } void HTTPServer::SendResponse() { INFO("SendResponse(%s) at %u", queryType, (unsigned int)PerformanceTimer.read_us()); if (strcmp(queryType, "GET") == 0 || strcmp(queryType, "POST") == 0) { if (!(queryString[0] == '.' && queryString[1] == '.')) { const char * fType; if (!CheckDynamicHandlers()) { // Otherwise, this queryString must be trying to reference a static file if (queryString[strlen(queryString)-1] == '/') { queryString = rewriteWithDefaultFile(queryString); } // see if we support this file type fType = GetSupportedType(queryString); if (fType) { queryString = rewritePrependWebroot(queryString); SendFile(queryString, fType); } else { header(404, "Not Found", "Pragma: err - Unsupported type\r\n"); } } } else { header(400, "Bad Request", "Pragma: err - Unsupported path\r\n"); } } else { header(400, "Bad Request", "Pragma: err - Unsupported query type\r\n"); } INFO(" SendResponse complete at %u", (unsigned int)PerformanceTimer.read_us()); if (queryType) { myfree(queryType); queryType = NULL; } if (queryString) { myfree(queryString); queryString = NULL; } if (postQueryString) { myfree(postQueryString); postQueryString = NULL; } INFO(" SendResponse free at %u", (unsigned int)PerformanceTimer.read_us()); } HTTPServer::CallBackResults HTTPServer::ParseHeader(char * buffer) { char * dblCR; CallBackResults advanceState = ACCEPT_ERROR; int bytecount; // Buffer could have partial, but the double \r\n is the key // GET /QueryString?this=that&sky=blue HTTP/1.1\r\n // GET /QueryString HTTP/1.1\r\nHost: 192.168.1.140\r\nCache-Con // GET /QueryString HTTP/1.1\r\nHost: 192.168.1.140\r\nCache-Control: max-age=0\r\n\r\n dblCR = strstr(buffer,"\r\n\r\n"); if (dblCR) { // Have to scan from the beginning in case split on \r INFO("\r\n==\r\n%s==", buffer); char * soRec = buffer; // start of the next record of text char * eoRec = strchr(soRec, '\n'); // search for end of the current record headerParamCount = 0; bytecount = strlen(buffer); if (bytecount > maxheaderbytes) maxheaderbytes = bytecount; while (eoRec) { *eoRec = '\0'; if (*(eoRec-1) == '\r') *(eoRec-1) = '\0'; // Inspect the supported query types (GET, POST) and ignore (HEAD, PUT, OPTION, DELETE, TRACE, CONNECT] // This is very clumsy if (strstr(soRec, "GET ") == soRec) { Extract(soRec, "GET", &queryString); if (queryString) { queryType = (char *)mymalloc(strlen("GET")+1); strcpy(queryType, "GET"); } } else if (strstr(soRec, "POST ") == soRec) { Extract(soRec, "POST", &queryString); if (queryString) { queryType = (char *)mymalloc(strlen("POST")+1); strcpy(queryType, "POST"); } } // if there is a ": " delimiter, we have a header item to parse into name,value pair // "Connection: keep-alive" becomes "Connection" "keep-alive" char *delim = strstr(soRec, ": "); char *chkSpace = strchr(soRec, ' '); // a field-name has no space ahead of the ":" if (delim && (!chkSpace || (chkSpace && delim < chkSpace)) && headerParamCount < maxheaderParams) { *delim++ = '\0'; // replace ": " with null *delim++ = '\0'; headerParams[headerParamCount].name = soRec; headerParams[headerParamCount].value = delim; INFO("%d: headerParams[%s] = {%s}", headerParamCount, headerParams[headerParamCount].name, headerParams[headerParamCount].value); headerParamCount++; } soRec = eoRec + 1; eoRec = strchr(soRec, '\n'); } if (queryString) { // We have enough to try to reply INFO("create reply queryType{%s}, queryString{%s}", "GET", queryString); // parse queryParams - if any // /file.htm?name1=value1&name2=value2... // /file.htm?name1&name2=value2... queryParamCount = 0; char * paramDelim = strchr(queryString, '?'); if (paramDelim) { *paramDelim++ = '\0'; UnescapeString(paramDelim); // everything after the '?' ParseParameters(paramDelim); // pointing at the NULL, but there are queryParams beyond } } else { ERR("queryString not found in (%s) [this should never happen]", soRec); } advanceState = ACCEPT_COMPLETE; buffer[0] = 0; // This part parses the extra data on a POST method. // Since there has to be a dynamic handler registered for this // it would make sense to move some of this responsibility to // that handler. It could then choose if it wanted to allocate // the requested 'Content-Length' amount of memory. int postBytes = atoi(GetHeaderValue("Content-Length")); CallBackResults acceptIt = ACCEPT_ERROR; if (strcmp(queryType, "POST") == 0 && postBytes > 0 ) { if (postBytes) { int ndxHandler = 0; bool regHandled = false; // Registered Dynamic Handler // Callback and ask if they want to accept this data for (ndxHandler=0; ndxHandler<handlercount; ndxHandler++) { if (strcmp(handlers[ndxHandler].path, queryString) == 0) { acceptIt = (*handlers[ndxHandler].callback)(this, CONTENT_LENGTH_REQUEST, queryString, queryParams, queryParamCount); regHandled = true; break; // only one callback per path allowed } } if (regHandled && acceptIt != ACCEPT_ERROR) { // @todo need to refactor - if the thing is bigger than the buffer, // then we can receive it a chunk at a time, and hand off // the chunks to the callback. May need callbacks that // are: START: extract the filename/object name, // NEXT: a chunk of data, // END: signals that all chunks were delivered. // // If so, we'll make space for it postQueryString = (char *)mymalloc(CHUNKSIZE); INFO("Free space %d", Free()); if (postQueryString) { int ttlReceived = 0; INFO("Processing"); dblCR += 4; // If we slurped up any of the POST, while (*dblCR && *dblCR <= ' ') dblCR++; strcpy(postQueryString, dblCR); // copy that in and then get the rest while (ttlReceived < postBytes) { int len; wait_ms(4); len = client.receive(postQueryString, CHUNKSIZE); if (len >=0) { INFO("Passing %d bytes (%d of %d) in [%s]", len, ttlReceived, postBytes, postQueryString); ttlReceived += len; acceptIt = (*handlers[ndxHandler].callback)(this, DATA_TRANSFER, postQueryString, NULL, len); } else if (len < 0) { INFO("*** receive returned %d ***", len); break; // no more data } } myfree(postQueryString); INFO("done."); } else { ERR("HTTPd: attempt to allocate %d bytes failed.", CHUNKSIZE); } } else { // Simply copy it to the bitbucket int bytesToDump = postBytes; char * bitbucket = (char *)mymalloc(201); dblCR += 4; while (*dblCR && *dblCR <= ' ') dblCR++; bytesToDump -= strlen(dblCR); while (bytesToDump > 0) { int n = (bytesToDump > 200) ? 200 : bytesToDump; n = client.receive(bitbucket, n); if (n < 0) { ERR("to the bitbucket."); break; } bytesToDump -= n; } myfree(bitbucket); } } } } return advanceState; } const char * HTTPServer::GetHeaderValue(const char * hdr) { int i; for (i=0; i<headerParamCount; i++) { if (strcmp(hdr, headerParams[i].name) == 0) return headerParams[i].value; } return NULL; } void HTTPServer::GetPerformanceData(SW_PerformanceData * p) { memcpy(p, &perfData, sizeof(perfData)); } unsigned int HTTPServer::GetPerformanceClock() { return (unsigned int)PerformanceTimer.read_us(); } unsigned int HTTPServer::RecordPerformanceData(SW_PerformanceParam * param, unsigned int refTime) { unsigned int t_now = (unsigned int)PerformanceTimer.read_us(); param->TotalTime_us += (t_now - refTime); param->Samples++; if ((t_now - refTime) > param->MaxTime_us) param->MaxTime_us = (t_now - refTime); return t_now; } void HTTPServer::ResetPerformanceData() { memset(&perfData, 0, sizeof(perfData)); } #ifdef INCLUDE_GETREMOTE bool HTTPServer::GetRemoteAddr(char * str, int strSize) { bool res = false; char *p; if (strSize < 16) { // Can only guard it here w/o modifying Wifly class *str = '\0'; return res; } res = wifly->sendCommand("show z\r", NULL, str, strSize); if (res) { p = strchr(str, '\n'); // truncate after the octets. if (p) *p = '\0'; p = strchr(str, ' '); // or a space if (p) *p = '\0'; p = strchr(str, '<'); // or a < if (p) *p = '\0'; res = true; } wifly->exit(); return res; } #endif