A simple web server that can be bound to either the EthernetInterface or the WiflyInterface.

Dependents:   Smart-WiFly-WebServer WattEye X10Svr SSDP_Server

Revision:
44:71f09e4255f4
Parent:
43:3fc773c2986e
Child:
45:360c7c1d07b7
--- a/SW_HTTPServer.cpp	Mon Feb 02 03:01:00 2015 +0000
+++ b/SW_HTTPServer.cpp	Sat Mar 26 20:38:59 2016 +0000
@@ -1,5 +1,5 @@
 //
-// @note Copyright © 2014 by Smartware Computing, all rights reserved.
+// @note Copyright © 2014-2016 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
@@ -12,7 +12,7 @@
 //
 #include "mbed.h"
 
-#define DEBUG "HTTP"
+//#define DEBUG "HTTP"
 #include <cstdio>
 #if (defined(DEBUG) && !defined(TARGET_LPC11U24))
 #define DBG(x, ...)  std::printf("[DBG %s %3d] "x"\r\n", DEBUG, __LINE__, ##__VA_ARGS__);
@@ -35,12 +35,13 @@
 const char * DEFAULT_FILENAME = "index.htm";
 
 // Header information to always send (must be \r\n terminated)
-const char hdr_httpver[] = "HTTP/1.0";                  // supported 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 char hdr_httpver[] = "HTTP/1.1";                       // supported HTTP/1.1 protocol (sort of)
+static const char nl[]          = "\r\n";                           // final \r\n for the termination of the header
 
+// Header items that are sent if the user does not provide their own options.
+static const char hdr_age[]     = "Max-age: 0\r\n";                 // expires right away
+static const char hdr_server[]  = "Server: Smart_Server v0.2\r\n";  // Server
+static const char hdr_close[]   = "Connection: close\r\n";          // tell the client the server closes the connection immediately
 
 static const struct {
     char *ext;
@@ -59,9 +60,28 @@
     {".pdf", "Content-Type: application/pdf\r\n"    },
     {".htm", "Content-Type: text/html\r\n"          },
     {".html","Content-Type: text/html\r\n"          },
+    {".xml", "Content-Type: text/xml\r\n"           },
     {0,0}
 };
 
+typedef struct {
+    char * queryType;
+    int notused;
+} QueryMethod;
+
+// Be sure to include a trailing space.
+static const QueryMethod QueryMethodList[] = {
+    {"GET ", 0},
+    {"POST ", 0},
+    {"HEAD ", 0},
+    {"PUT ", 0},
+    {"OPTION ", 0},
+    {"DELETE ", 0},
+    {"TRACE ", 0},
+    {"CONNECT ", 0},
+    {NULL, 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
@@ -105,6 +125,7 @@
     strcpy(webroot, _webroot);
     if (strlen(webroot)>1 && webroot[strlen(webroot)-1] == '/') // remove trailing '/'
         webroot[strlen(webroot)-1] = '\0';
+    filenameAliasList = NULL;
     maxheaderParams = _maxheaderParams;
     headerParams = (namevalue *)malloc(maxheaderParams * sizeof(namevalue));
 
@@ -128,12 +149,8 @@
     server = new TCPSocketServer();
     server->bind(port);
     server->listen();
-    #ifndef CC3000_H
-    server->set_blocking(false, 10);
-    client.set_blocking(false, 100);             //@TODO client is separate from server. any way to combine?
-    #else
-    #warning CC3000 detected
-    #endif
+    server->set_blocking(false);
+    client.set_blocking(false);             //@TODO client is separate from server. any way to combine?
     ResetPerformanceData();
     PerformanceTimer.start();
 }
@@ -151,6 +168,11 @@
     webroot = NULL;
 }
 
+void HTTPServer::RegisterFilenameAliasList(const namevalue * namevaluelist)
+{
+    filenameAliasList = namevaluelist;
+}
+
 int HTTPServer::GetMaxHeaderSize()
 {
     return maxheaderbytes;
@@ -220,9 +242,9 @@
 
         case ReceivingHeader:
             n = client.receive(bPtr, headerbuffersize - (bPtr - headerbuffer));
+            INFO("%sclient.receive() returned %d, from %s", (n<0) ? "*** " : "", n, client.get_address());
             if (n < 0) {
                 op = Sending;
-                INFO("*** client.receive() => %d", n);
             } else if (n) {
                 bPtr[n] = '\0';
                 switch (ParseHeader(headerbuffer)) {
@@ -265,11 +287,10 @@
             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());
+            INFO("Connection closed exit: %u\r\n\r\n", (unsigned int)PerformanceTimer.read_us());
             break;
     }
 }
@@ -295,22 +316,54 @@
 {
     if (bytes == -1)
         bytes = strlen(msg);
-    //INFO("Sending %d bytes", bytes);
+    INFO("Sending %d bytes", bytes);
+    //INFO("send:\r\n%s", msg);
     client.send((char *)msg, bytes);
+    INFO("client.send returned: %d", r);
 }
 
+const char * HTTPServer::FindAlias(const HTTPServer::namevalue * haystack, const char * needle)
+{
+    while (haystack && haystack->name) {
+        if (strcmp(haystack->name,needle) == 0)
+            return haystack->value;
+        haystack++;
+    }
+    return needle;
+}
+
+uint32_t HTTPServer::FileSize(const char * filename)
+{
+    uint32_t size = 0;
+    FILE * fh;
+
+    filename = FindAlias(filenameAliasList, filename);
+    fh = fopen(filename, "r");
+    if (fh) {
+        fseek(fh, 0, SEEK_END); // seek to end of file
+        size = ftell(fh);       // get current file pointer
+        fclose(fh);
+    }
+    return size;
+}
 
 bool HTTPServer::SendFile(const char * filename, const char * filetype)
 {
     FILE * fp;
 
+    INFO("SendFile(%s,...)", filename);
+    filename = FindAlias(filenameAliasList, filename);
+    INFO("   Alias(%s,...)", filename);
     fp  = fopen(filename,"rb");
     if (fp) { // can open it
         char *fbuffer = (char *)mymalloc(FILESEND_BUF_SIZE);
         int bytes;
 
         if (fbuffer) {
-            header(200, "OK", filetype);
+            char ContentLen[30];
+            snprintf(ContentLen, sizeof(ContentLen), "Content-Length: %u\r\n", FileSize(filename));
+            header(OK, "OK", filetype, ContentLen);
+            header("");
             bytes = fread(fbuffer,sizeof(char),FILESEND_BUF_SIZE,fp);
             while (bytes > 0) {
                 send(fbuffer, bytes);
@@ -318,12 +371,14 @@
             }
             myfree(fbuffer);
         } else {
-            header(500, "Server Error", "Pragma: err - insufficient memory\r\n");
+            header(Server_Error, "Server Error", NULL, "Pragma: err - insufficient memory\r\n");
+            header("");
         }
         fclose(fp);
         return true;
     } else {
-        header(404, "Not Found", "Pragma: err - Can't open file\r\n");
+        header(Not_Found, "Not Found", NULL, "Pragma: err - Can't open file\r\n");
+        header("");
         return false;
     }
 }
@@ -387,7 +442,7 @@
     return NULL;
 }
 
-HTTPServer::namevalue * HTTPServer::GetParameter(int index)
+const HTTPServer::namevalue * HTTPServer::GetParameter(int index)
 {
     if (index < queryParamCount)
         return &queryParams[index];
@@ -454,25 +509,34 @@
 }
 
 
-void HTTPServer::header(int code, const char * code_text, const char * content_type, const char * optional_text)
+void HTTPServer::header(HTTPServer::HeaderCodes 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);
-    INFO("header(%s)", http);
+    INFO("header(%d, %s, %s, ...)", code, code_text, content_type);
+    snprintf(http, sizeof(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);
+        if (*optional_text != '\0')
+            send(optional_text);
+    } else {
+        send(hdr_age);
+        send(hdr_server);
+        send(hdr_close);
+        header("");
     }
-    send(hdr_close);
-    send(nl);
 }
 
+void HTTPServer::header(const char * partialheader)
+{
+    if (!partialheader || *partialheader == '\0')
+        send(nl);
+    else
+        send(partialheader);
+}
 
 bool HTTPServer::close_connection()
 {
@@ -491,8 +555,10 @@
     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"
+        // Seems to be a valid "GET /QueryString HTTP/1.1"
+        // or                  "POST /upnp/control/metainfo1 HTTP/1.0"
         // or "...<needle>param..."
         qs = get + strlen(needle);  // in case the needle didn't have space delimiters
         while (*qs == ' ')
@@ -575,7 +641,7 @@
 void HTTPServer::SendResponse()
 {
     INFO("SendResponse(%s) at %u", queryType, (unsigned int)PerformanceTimer.read_us());
-    if (strcmp(queryType, "GET") == 0 ||  strcmp(queryType, "POST") == 0) {
+    if (strcmp(queryType, "GET ") == 0 ||  strcmp(queryType, "POST ") == 0) {
         if (!(queryString[0] == '.' && queryString[1] == '.')) {
             const char * fType;
 
@@ -584,20 +650,25 @@
                 if (queryString[strlen(queryString)-1] == '/') {
                     queryString = rewriteWithDefaultFile(queryString);
                 }
+                INFO("  queryString: %s", queryString);
                 // see if we support this file type
                 fType = GetSupportedType(queryString);
                 if (fType) {
                     queryString = rewritePrependWebroot(queryString);
+                    INFO("  SendFile(%s,%s)", queryString, fType);
                     SendFile(queryString, fType);
                 } else {
-                    header(404, "Not Found", "Pragma: err - Unsupported type\r\n");
+                    header(Not_Found, "Not Found", NULL, "Pragma: err - Unsupported type\r\n");
+                    header("");
                 }
             }
         } else {
-            header(400, "Bad Request", "Pragma: err - Unsupported path\r\n");
+            header(Bad_Request, "Bad Request", NULL, "Pragma: err - Unsupported path\r\n");
+            header("");
         }
     } else {
-        header(400, "Bad Request", "Pragma: err - Unsupported query type\r\n");
+        header(Bad_Request, "Bad Request", NULL, "Pragma: err - Unsupported query type\r\n");
+        header("");
     }
     INFO("  SendResponse complete at %u", (unsigned int)PerformanceTimer.read_us());
 
@@ -630,7 +701,7 @@
     //      POST /dyn2 HTTP/1.2\r\nAccept: text/html, application/xhtml+xml, */*\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);
+        INFO("\r\n==\r\n%s\r\n==", buffer);
         char * soRec = buffer;                  // start of the next record of text
         char * eoRec = strchr(soRec, '\n');     // search for end of the current record
 
@@ -642,10 +713,23 @@
             *eoRec = '\0';
             if (*(eoRec-1) == '\r')
                 *(eoRec-1) = '\0';
+            INFO("method: %s", soRec);
+            #if 1
+            const QueryMethod * qm = QueryMethodList;
+            while (*qm->queryType) {
+                if (strstr(soRec, qm->queryType) == soRec) {
+                    Extract(soRec, qm->queryType, &queryString);
+                    if (queryString) {
+                        queryType = (char *)mymalloc(strlen(qm->queryType)+1);
+                        strcpy(queryType, qm->queryType);
+                    }
+                }
+                qm++;
+            }
+            #else
             // Inspect the supported query types (GET, POST) and ignore (HEAD, PUT, OPTION, DELETE, TRACE, CONNECT]
-            // This is very clumsy
-            INFO("method: %s", soRec);
-             if (strstr(soRec, "GET ") == soRec) {
+            // This is presently very clumsy
+            if (strstr(soRec, "GET ") == soRec) {
                 Extract(soRec, "GET", &queryString);
                 if (queryString) {
                     queryType = (char *)mymalloc(strlen("GET")+1);
@@ -658,12 +742,13 @@
                     strcpy(queryType, "POST");
                 }
             }
-
+            #endif
+            
             // 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 ":"
-            INFO("hpc:%d,mhp:%d, {%s}", headerParamCount, maxheaderParams, soRec);
+            //INFO("hpc:%d,mhp:%d, {%s}", headerParamCount, maxheaderParams, soRec);
             if (delim
                     && (!chkSpace || (chkSpace && delim < chkSpace))
                     && headerParamCount < maxheaderParams) {
@@ -677,6 +762,8 @@
             }
             soRec = eoRec + 1;
             eoRec = strchr(soRec, '\n');
+            if (soRec > dblCR)      // Just walked past the end of the header
+                break;
         }
         
         if (queryString) {
@@ -695,7 +782,7 @@
         } else {
             ERR("queryString not found in (%s) [this should never happen]", soRec);
         }
-        advanceState = ACCEPT_COMPLETE;
+        advanceState = ACCEPT_COMPLETE; // Should be ACCEPT_CONTINUE and the stuff below moves out of here
         buffer[0] = 0;
 
         // This part parses the extra data on a POST method.
@@ -705,96 +792,98 @@
         // the requested 'Content-Length' amount of memory.
         int postBytes = atoi(GetHeaderValue("Content-Length"));
         CallBackResults acceptIt = ACCEPT_ERROR;
-        if (strcmp(queryType, "POST") == 0 && postBytes > 0 ) {
-            INFO("parse POST data %d bytes", postBytes);
-            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++) {
-                    INFO("is '%s' a handler for '%s' ?", handlers[ndxHandler].path, queryString);
-                    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
-                    }
+        INFO("Content-Length: %d", postBytes);
+        if (strcmp(queryType, "POST ") == 0 ) {
+            INFO("parse POST data %d bytes", postBytes);        // We might have no idea how much data is coming...
+            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++) {
+                INFO("is '%s' a handler for '%s' ?", handlers[ndxHandler].path, queryString);
+                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
                 }
-                INFO("reghandled: %d, acceptIt: %d", regHandled, acceptIt);
-                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: DATA_TRANSFER: self-detect to extract the filename/object name,
-                    //            DATA_TRANSFER: subsequent chunk of data,
-                    //            DATA_TRANSFER_END:   signals that last chunk is enclosed.
-                    //
-                    // If so, we'll make space for it
-                    postQueryString = (char *)mymalloc(CHUNK_SIZE);
-                    //INFO("Free space %d", Free());
-                    INFO("postQueryString %p", postQueryString);
-                    if (postQueryString) {
-                        int len = 0;
-                        int ttlCount;
-                        Timer escapePlan;
-                        bool escape = false;
-                        
-                        INFO("Processing tail...");
-                        escapePlan.start();
-                        dblCR += 4;     // There may be some after the double CR that we need
-                        ttlCount = strlen(dblCR);
-                        strcpy(postQueryString, dblCR);
-                        INFO("  {%s}", postQueryString);
-                        while (ttlCount < postBytes && !escape) {
-                            INFO("ttlCount: %d < postBytes: %d, of chunk %d", ttlCount, postBytes, CHUNK_SIZE);
-                            len = client.receive_all(postQueryString + ttlCount, CHUNK_SIZE - ttlCount);
-                            if (len > 0) {
-                                INFO("  len: %d, ttlCount: %d < postBytes %d, {%s}", len, ttlCount, postBytes, postQueryString + ttlCount);
-                                ttlCount += len;
-                                postQueryString[ttlCount] = '\0';    // Whether binary or ASCII, this is ok as it's after the data
-                                INFO("  postBytes %d: [%s], [%d]", postBytes, postQueryString, ndxHandler);
-                                escapePlan.reset();
-                            } else if (len < 0) {
-                                INFO("*** connection closed ***");
-                                break;      // no more data, before the plan
-                            } else { // n == 0
-                                ;
-                            }
-                            if (escapePlan.read_ms() > HANG_TIMEOUT_MS) {
-                                escape = true;
-                                WARN("Escape plan activated.");
-                            }
+            }
+            INFO("reghandled: %d, acceptIt: %d", regHandled, acceptIt);
+            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: DATA_TRANSFER: self-detect to extract the filename/object name,
+                //            DATA_TRANSFER: subsequent chunk of data,
+                //            DATA_TRANSFER_END:   signals that last chunk is enclosed.
+                //
+                // If so, we'll make space for it
+                postQueryString = (char *)mymalloc(CHUNK_SIZE);
+                //INFO("Free space %d", Free());
+                INFO("postQueryString %p", postQueryString);
+                if (postQueryString) {
+                    int len = 0;
+                    int ttlCount;
+                    Timer escapePlan;
+                    bool escape = false;
+                    
+                    INFO("Processing tail...");
+                    escapePlan.start();
+                    dblCR += 4;     // There may be some after the double CR that we need
+                    ttlCount = strlen(dblCR);
+                    strcpy(postQueryString, dblCR);
+                    //INFO("  {%s}", postQueryString);
+                    while ((!postBytes || ttlCount < postBytes) && !escape) {
+                        INFO("ttlCount: %d < postBytes: %d, of max chunk alloc %d", ttlCount, postBytes, CHUNK_SIZE);
+                        len = client.receive(postQueryString + ttlCount, CHUNK_SIZE - ttlCount);
+                        if (len > 0) {
+                            ttlCount += len;
+                            postQueryString[ttlCount] = '\0';    // Whether binary or ASCII, this is ok as it's after the data
+                            INFO("  postBytes %d: [%s], [%d]", postBytes, postQueryString, ndxHandler);
+                            escapePlan.reset();
+                        } else if (len < 0) {
+                            INFO("*** connection closed ***");
+                            break;      // no more data, before the plan
+                        } else { // n == 0
+                            ;
                         }
-                        postParamCount = 0;
-                        INFO("post: %s", postQueryString);
-                        ParseParameters(postParams, &postParamCount, maxPostParams, postQueryString);
-                        acceptIt = (*handlers[ndxHandler].callback)(this, DATA_TRANSFER, queryString, queryParams, queryParamCount);
-                        INFO("..processing exit");
-                        acceptIt = (*handlers[ndxHandler].callback)(this, DATA_TRANSFER_END, NULL, NULL, 0);
-                    } else {
-                        ERR("attempt to allocate %d failed.", CHUNK_SIZE);
+                        if (escapePlan.read_ms() > HANG_TIMEOUT_MS) {   // if no Content-Length, we wait...
+                            escape = true;
+                            WARN("Escape plan activated.");
+                        }
+                        if (postBytes > 0 && ttlCount >= postBytes)
+                            break;
                     }
+                    //postParamCount = 0;
+                    //INFO("post: %s", postQueryString);
+                    //We're after the header, so there is "body" stuff which could be anything...
+                    //but probably html or xml stuff...
+                    //ParseParameters(postParams, &postParamCount, maxPostParams, postQueryString);
+                    acceptIt = (*handlers[ndxHandler].callback)(this, DATA_TRANSFER, postQueryString, NULL, ttlCount);
+                    INFO("..processing exit");
+                    acceptIt = (*handlers[ndxHandler].callback)(this, DATA_TRANSFER_END, NULL, NULL, 0);
                 } else {
-                    // Simply copy it to the bitbucket
-                    WARN("to the bit bucket...");
-                    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;
+                    ERR("attempt to allocate %d failed.", CHUNK_SIZE);
+                }
+            } else {
+                // Simply copy it to the bitbucket
+                WARN("No handler, so to the bit bucket it goes ...");
+                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;
                     }
-                    myfree(bitbucket);
+                    bytesToDump -= n;
                 }
+                myfree(bitbucket);
             }
         }
     }