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-06-02
Revision:
2:a29c32190037
Parent:
0:729320f63c5c
Child:
3:17928786bdb5

File content as of revision 2:a29c32190037:

//
// @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"
#include "Utility.h"

//#define DEBUG

#define FILESEND_BUF_SIZE 1200 // should keep this <= the ethernet frame size

const char * DEFAULT_FILENAME = "index.htm";

// Header information to always send
const char hdr_httpver[] = "HTTP/1.1";              // typically HTTP/1.0 or HTTP/1.1
const char hdr_age[] = "Max-age: 0\r\n";            // expires right away (must be \r\n terminated)
const char hdr_server[] = "Server: Smart_Server v0.1\r\n"; // Server (must be \r\n terminated)
const char hdr_dnt[] = "DNT: 1\r\n";                // Do Not Track
const char hdr_close[] = "Connection: close\r\n";       // tell the client to close the connection (must be \r\n terminated)
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}
};

HTTPServer::HTTPServer(Wifly * _wf, int port, const char * _webroot, int _maxparams, int _maxdynamicpages, PC * _pc)
{
    wifly = _wf;
    webroot = (char *)malloc(strlen(_webroot)+1);
    strcpy(webroot, _webroot);
    maxparams = _maxparams;
    maxdynamicpages = _maxdynamicpages;
    params = (namevalue *)malloc(maxparams * sizeof(namevalue));
    handlers = (handler *)malloc(maxdynamicpages * sizeof(handler));
    pc = _pc;
    paramcount = 0;
    handlercount = 0;
    server = new TCPSocketServer();
    timer = new Timer();
    timer->start();
    server->bind(port);
    server->listen();
    server->set_blocking(false);
    server->accept(client);
}

HTTPServer::~HTTPServer()
{
    free(webroot);
    webroot = NULL;
}

bool HTTPServer::RegisterHandler(const char * path, Handler callback)
{
    if (handlercount < maxdynamicpages && path && callback) {
        handlers[handlercount].path = (char *)malloc(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()
{
#define MAXRECSIZE 4000
    static char buffer[MAXRECSIZE];
    static char * bPtr = buffer;

    static char * queryType = NULL;
    static char * queryString = NULL;
    static char * hostString = NULL;
    static char * contentLength = NULL;
    static char * contentType = NULL;
    static char * postQueryString = NULL;
    static Timer tmr;
    typedef enum { Idle, Receiving, Reply , Waiting, Sending, WaitingToClose } state;
    static state op = Idle;
    int n;

    switch(op) {
        case Reply:
            tmr.reset();
            tmr.start();
            op = Waiting;
            break;
        case Waiting:
            if (tmr.read_ms() > 10) {
                //pc->printf("%06d delay expired, now send.\r\n", timer->read_ms());
                op = Sending;
            }
            break;
        case Sending:
            if (strcmp(queryType, "GET") == 0
            ||  strcmp(queryType, "POST") == 0) {
                if (!(queryString[0] == '.' && queryString[1] == '.')) {
                    const char * fType;
                    bool regHandled = false;

                    // Registered Dynamic Handler
                    // 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, params, paramcount);
                            regHandled = true;
                            break;      // we only execute the first one
                        }
                    }

                    if (!regHandled) {
                        // 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 {
                            //pc->printf("Unsupported file type %s\r\n", queryString);
                            header(404, "Not Found", "Pragma: err - Unsupported type\r\n");
                        }
                    }
                } else {
                    //pc->printf("Unsupported path %s\r\n", queryString);
                    header(400, "Bad Request", "Pragma: err - Unsupported path\r\n");
                }
            } else {
                //pc->printf("Unsupported query type %s\r\n", queryType);
                header(400, "Bad Request", "Pragma: err - Unsupported query type\r\n");
            }
            tmr.reset();
            if (queryType) { 
                free(queryType);
                queryType = NULL;
            }
            if (queryString) {
                free(queryString);
                queryString = NULL;
            }
            if (hostString) {
                free(hostString);
                hostString = NULL;
            }
            if (contentLength) {
                free(contentLength);
                contentLength = NULL;
            }
            if (contentType) {
                free(contentType);
                contentType = NULL;
            }
            if (postQueryString) {
                free(postQueryString);
                postQueryString = NULL;
            }
            op = WaitingToClose;
            break;
        case WaitingToClose:
            if (tmr.read_ms() > 10) {
                //pc->printf("closing after %d\r\n", tmr.read_ms());
                close_connection();
                op = Idle;
            }
            break;
        case Idle:
            //if (server->accept(client) == 0)
            //    op = Receiving;
            //break;
            // Idle and Receiving are the same until I figure out the accept method
            // so it doesn't eat a few chars after the *OPEN*
        case Receiving:
            n = client.receive(bPtr, sizeof(buffer) - (bPtr - buffer));
            if (n < 0) {
                pc->printf("*** client.receive error: %d\r\n", n);
            } else if (n) {
                char * dblCR;
                bPtr[n] = '\0';
                wifly->setConnectionState(true);       // Bad hack to have to do this here....
                //pc->printf("%s", bPtr);
                // Buffer could have partial, but the double \r\n is the key
                //      *OPEN*QueryType QueryString HTTP/1.1....
                //          QueryType:= GET
                //      *OPEN*GET /QueryString HTTP/1.1\r\n
                //      *OPEN*GET /QueryString HTTP/1.1\r\nHeader stuf
                //      *OPEN*GET /QueryString HTTP/1.1\r\nHeader stuff...\r\n\r\n
                dblCR = strstr(buffer,"\r\n\r\n");
                if (dblCR) {  // Have to scan from the beginning in case split on \r
                    #ifdef DEBUG
                    pc->printf("\r\n\r\nThe Header:\r\n%s\r\n\r\n", buffer);
                    #endif
                    char * soRec = buffer;                  // start of the next record of text
                    char * eoRec = strchr(soRec, '\n');     // search for end of a record
                    while (eoRec) {
                        *eoRec = '\0';
                        if (*(eoRec-1) == '\r')
                            *(eoRec-1) = '\0';
                        if (!Extract(soRec, "*OPEN*", &queryType))
                            Extract(soRec, "*CLOS*", &queryType);
                        if (queryType)
                            Extract(soRec, queryType, &queryString);
                        Extract(soRec, "Host: ", &hostString);
                        Extract(soRec, "Content-Length: ", &contentLength);
                        Extract(soRec, "Content-Type: ", &contentType);
                        soRec = eoRec + 1;
                        eoRec = strchr(soRec, '\n');
                    }
                    if (queryString) {
                        // We have enough to try to reply
                        //pc->printf("create reply queryType{%s}, queryString{%s}\r\n", queryType, queryString);
                        // parse params - if any
                        // /file.htm?name1=value1&name2=value2...
                        // /file.htm?name1&name2=value2...
                        paramcount = 0;
                        char * paramDelim = strchr(queryString, '?');
                        if (paramDelim) {
                            *paramDelim++ = '\0';
                            UnescapeString(paramDelim);   // everything after the '?'
                            ParseParameters(paramDelim);    // pointing at the NULL, but there are params beyond
                        }
                        //for (int i=0; i<paramcount; i++)
                        //    pc->printf("param %d '%s'='%s'\r\n", i, params[i].name, params[i].value);
                    } else {
                        pc->printf("not found\r\n");
                    }
                    op = Reply;
                    buffer[0] = 0;
                    bPtr = buffer;
                    
                    // 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.
                    // Should we check the 'Content-Type' to see if it is 
                    //   'application/x-www-form-urlencoded'?
                    int postBytes = atoi(contentLength);
                    bool acceptIt = false;
                    //pc->printf("Content-Length = %d\r\n", postBytes);
                    if (strcmp(queryType, "POST") == 0 && postBytes > 0 ) {
                        if (postBytes) {
                            bool regHandled = false;
                            // Registered Dynamic Handler
                            // Callback and ask if they want to accept this data
                            for (int i=0; i<handlercount; i++) {
                                if (strcmp(handlers[i].path, queryString) == 0) {
                                    acceptIt = (*handlers[i].callback)(this, CONTENT_LENGTH_REQUEST, queryString, params, paramcount);
                                    regHandled = true;
                                    break;      // we only execute the first one
                                }
                            }

                            if (regHandled && acceptIt) {
                                // If so, we'll make space for it
                                postQueryString = (char *)malloc(postBytes + 1);
                                if (postQueryString) {
                                    char * offset;
                                    int len;
                                    
                                    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 ((len = strlen(postQueryString)) < postBytes) {
                                        int n;
                                        offset = postQueryString + len;
                                        n = client.receive(offset, postBytes - len);
                                        if (n >=0) {
                                            offset[n] = '\0';
                                        }
                                    }
                                    if (len >= 0) {
                                        #ifdef DEBUG
                                        pc->printf("POSTDATA: %s\r\n", postQueryString);
                                        #endif
                                        UnescapeString(postQueryString);
                                        ParseParameters(postQueryString);
                                    }
                                }
                            } else {
                                // Simply copy it to the bitbucket
                                int bytesToDump = postBytes;
                                char * bitbucket = (char *)malloc(201);
                                dblCR += 4;
                                while (*dblCR && *dblCR <= ' ')
                                        dblCR++;
                                bytesToDump -= strlen(dblCR);
                                while (bytesToDump > 0) {
                                    int n = (bytesToDump > 200) ? 200 : bytesToDump;
                                    n = client.receive(bitbucket, n);
                                    bytesToDump -= n;
                                }
                                free(bitbucket);
                            }
                        }
                    }
                } else {
                    // received partial, but not the double (\r\n\r\n)
                    bPtr += n;
                }
            }
            break;
        default:
            // not expected to arrive here
            op = Idle;
            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);
    wifly->send(msg, bytes);
#ifdef DEBUG
//    pc->printf("%s", msg);
#endif
}

bool HTTPServer::SendFile(const char * filename, const char * filetype)
{
    FILE * fp;
    
    fp  = fopen(filename,"rb");
    // is supported, now try to open it
    if (fp) { // can open it
        char *fbuffer = (char *)malloc(FILESEND_BUF_SIZE);
        int bytes;

        //pc->printf("sending [%s]\r\n", filename);
        header(200, "OK", filetype);
        // now the file itself
        bytes = fread(fbuffer,sizeof(char),FILESEND_BUF_SIZE,fp);
        while (bytes > 0) {
            send(fbuffer, bytes);
            bytes = fread(fbuffer,sizeof(char),FILESEND_BUF_SIZE,fp);
        }
        free(fbuffer);
        fclose(fp);
        return true;
    } else {
        //pc->printf("Can't open %s\r\n", filename);
        header(404, "Not Found", "Pragma: err - Can't open\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<paramcount; i++) {
        if (strcmp(params[i].name, name) == 0) {
            return params[i].value;
        }
    }
    return NULL;
}

// this=that&who=what&more=stuff...
// ^   ^    ^
void HTTPServer::ParseParameters(char * pName) 
{
    char * pVal;
    char * pNextName;

    // Parse params
    pVal = strchr(pName, '#');      // If there is a '#fragment_id', we can ignore it
    if (pVal)
        *pVal = '\0';    
    do {
        //pc->printf("Parse(%s)\r\n", pName);
        //*pName++ = '\0';   // already '\0' on the first entry
        params[paramcount].name = pName;
        pVal = strchr(pName, '=');
        pNextName = strchr(pName,'&');
        if (pVal) {
            if (pNextName == NULL || (pNextName && pNextName > pVal)) {
                *pVal++ = '\0';
                params[paramcount].value = pVal;
                pName = pVal;
            }
        }
        //pc->printf("  [%s]=[%s]\r\n", params[paramcount].name, params[paramcount].value);
        paramcount++;
        if (pNextName) {
            pName = pNextName;
            *pName++ = '\0';
        } else {
            pName = NULL;
        }
    } while (pName && paramcount < maxparams);
}


void HTTPServer::GetRemoteAddr(char * str, int size)
{
    bool res = false;
    char *p;

    res = wifly->sendCommand("show z\r", "NO", str, 5000);
    wait(0.2);  // how long to wait is fragile, but need the rest of the chars to come in...
    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';
    }
    wifly->exit();
}


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_dnt);
    send(hdr_close);
    send(nl);
}

void HTTPServer::close_connection( )
{
#ifndef DEBUG
    wifly->close();
#else // DEBUG
    if (!wifly->close())
        pc->printf("Couldn't close connection\r\n");
#endif
}

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"
        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
            free(*string);
        container = (char *)malloc(strlen(qs));
        if (container) {
            strcpy(container, qs);
            eqs = strchr(container, ' ');
            if (eqs)
                *eqs = '\0';
            *string = container;
            ret = true;
        } else {
            *string = NULL;     // something bad happened... no memory
        }
    }
    return ret;
}

char * HTTPServer::rewriteWithDefaultFile(char * queryString)
{
    char * temp = (char *)malloc(strlen(queryString) + strlen(DEFAULT_FILENAME) + 1);

    if (temp) {
        *temp = '\0';
        strcpy(temp, queryString);
        strcat(temp, DEFAULT_FILENAME);
        free(queryString);
        return temp;
    } else {
        return queryString;
    }
}

char * HTTPServer::rewritePrependWebroot(char * queryString)
{
    char * temp = (char *)malloc(strlen(webroot) + strlen(queryString) + 1);

    if (temp) {
        *temp = '\0';
        strcpy(temp, webroot);
        if (temp[strlen(temp)-1] == '/' && *queryString == '/')
            temp[strlen(temp)-1] = '\0';
        strcat(temp, queryString);
        free(queryString);
        return temp;
    } else {
        return queryString;
    }
}