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

Dependents:   Smart-WiFly-WebServer WattEye X10Svr SSDP_Server

Revision:
3:17928786bdb5
Parent:
2:a29c32190037
Child:
4:f34642902056
--- a/SW_HTTPServer.h	Sun Jun 02 18:32:05 2013 +0000
+++ b/SW_HTTPServer.h	Mon Jun 24 20:02:01 2013 +0000
@@ -13,43 +13,97 @@
 #define PC Serial
 #endif
 
+/// This is the default buffer size used to send files. You might
+/// size this to be a little less than the ethernet frame size
+#define FILESEND_BUF_SIZE 1200
+
+
+/// MAX_HEADER_SIZE is the default size to contain the largest header. This is
+/// the size of the URL and query string, and also all the
+/// other header information about the client. This can be
+/// a couple of K, larger if you have big forms as it includes the 
+/// form data that is submitted.
+#define MAX_HEADER_SIZE 1000
+
 /// HTTPServer is a simple web server using the WiFly module.
 /// 
-/// Partially derived from nweb
+/// While simple, it is a capable, web server. The basic mode
+/// of operation is for it to serve static web pages from an available
+/// file system.
+/// 
+/// The default page is index.htm (compile time defined)
+/// standard support to serve a number of standard file types;
+/// gif, jpg, jpeg, ico, png, zip, gz, tar, txt, pdf, htm, html
+/// (this list is also compile time defined)
+///
+/// It can also serve dynamically generated pages, and therefore
+/// respond to form submission. Through the dynamic interface it is
+/// then quite easy to interact with the hardware, reading the inputs
+/// or signaling outputs.
+///
+/// @code
+///     HTTPServer svr(&wifly, HTTP_SERVER_PORT, "/local/", 30, 10, &pc);
+///     svr.RegisterHandler("/dyn1", SimpleDynamicPage);
+///     while (true)
+///        {
+///        svr.Poll();   // this is non blocking process, but variable execution
+///        }
+/// @endcode
+///
+/// This web server used nweb as a starting point, but expanded well beyond there.
 ///  http://www.ibm.com/developerworks/systems/library/es-nweb/sidefile1.html
 ///
-/// Uses a modified WiflyInterface - a number of performance issues
-/// were identified and resolved in the local version.
+/// @note This server uses a modified version of the mbed WiflyInterface - there
+/// were a number of performance issues identified and resolved in the local version.
 ///
-/// Also, if nothing visits its web page for a long time, it seems
-/// to disable. Might be a sleep timer I'm not spotting.
-///
-/// Given: scheme://domain:port/path?query_string#fragment_id
+/// Given: scheme://server:port/path?query_string#fragment_id
 /// @li scheme is "http"
-/// @li domain is whatever IP the server has
+/// @li server is whatever IP the server has
 /// @li port is the registered port
 /// @li /path is the reference to the file (actual or logical) on the server
 /// @li query_string is any combination of name=value pairs
 /// @li fragment_id is a reference to an anchor on the page
 ///
-/// Feature request list:
-/// @todo make it non-blocking.
-/// @todo hunt down lengthy operations - there seems to be a long timeout somewhere.
-/// @todo parse the header similar to the query string, and then make
+/// Features:
+/// @li Serves static pages from a file system. Many normal filetypes are
+///     supported.
+/// @li Compile time configurable for the "default" file, typically index.htm.
+/// @li Provides a registration interface for dynamically generated pages that
+///     can then interact with other hardware.
+/// @li Revised to be Non-blocking, however the execution time is variable
+///     depending on the actions being performed.
+///
+/// Limitations:
+/// @li Supports only a single connection at a time.
+/// @li Rapid requests for page objects (e.g. embedded images) are lost. Still
+///     working to understand this issue.
+///
+/// ToDo:
+/// @li A web page with served objects (img src=...) is rarely served properly. It
+///       might trace to forcing the connection to close, but not yet sure.
+///       Explore "Set Uart Rx Data Buffer" in WiFly manual 2.3.65
+/// @li hunt down lengthy operations - there seems to be a long timeout somewhere.
+/// @li parse the header similar to the query string, and then make
 ///       those parameters accessible - perhaps like environment vars.
-/// @todo move part of the POST method handler to the registered handler, so
+/// @li move part of the POST method handler to the registered handler, so
 ///       it can decide if it should allocate the needed memory.
-/// @todo Add password capability to some web pages
-/// @todo transform the pc serial interface to a log interface, which might
+/// @li Add password capability to some web pages
+/// @li transform the pc serial interface to a log interface, which might
 ///       be more useful.
-/// @todo Add ability to put WiFly in AP mode and then configuration pages
+/// @li Add ability to put WiFly in AP mode and then configuration pages
 ///       to find and join a network.
-/// @todo Add ability to change/update SW in the WiFly module
-/// @todo Add ability to upload a new application to the mbed
+/// @li Add ability to change/update SW in the WiFly module
+/// @li Add ability to upload a new application to the mbed
 ///
 /// History:
 /// @li 20130530 Initial version
 /// @li 20130601 Renamed ip_process to Poll
+/// @li 20130617 Cleaned up some of the documentation changes
+/// @li 20130623 Make it non-blocking. "Poll" takes a variable amount
+///              of time, based on whether it is idle, or how much it
+///              has to do.
+/// @li It now survives overnight and still responds, so the problem with
+///              it going offline after long inactivity appears resolved.
 ///
 /// @note Copyright © 2013 by Smartware Computing, all rights reserved.
 ///     Individuals may use this application for evaluation or non-commercial
@@ -66,26 +120,64 @@
 {
 public:
     /**
-    * namevalue pairs for parameters
+    * name-value pairs for parameters
     */
-    typedef struct {
+    typedef struct NAMEVALUE {
         char * name;
         char * value;
     } namevalue;
     
     /**
-    * Indicates the purpose of the callback
+    * Indicates the purpose of the Handler callback
+    *
+    * Application code in a dynamic page uses this to determine the state 
+    * and therefore the needed operation to be performed.
+    *
+    * @code
+    * bool SimpleDynamicPage(HTTPServer *svr, HTTPServer::CallBackType type, const char * path, const HTTPServer::namevalue *params, int paramcount) {
+    *     char buf[100];
+    *     bool ret = false;
+    *     
+    *     switch (type) {
+    *         case HTTPServer::SEND_PAGE:
+    *             svr->header(200, "OK", "Content-Type: text/html\r\n");
+    *             svr->send("<html><head><title>Dynamic Page</title></head>\r\n");
+    *             svr->send("<body>\r\n");
+    *             svr->send("This page was generated dynamically. Create your own name=value pairs on the URL "
+    *                       "which uses the GET method.<br/>\r\n");
+    *             sprintf(buf, "%d parameters passed to {%s}:<br/>\r\n", paramcount, path);
+    *             svr->send(buf);
+    *             for (int i=0; i<paramcount; i++) {
+    *                 sprintf(buf, "%d: %s = %s<br/>\r\n", i, params[i].name, params[i].value);
+    *                 svr->send(buf);
+    *             }
+    *             svr->send("<br/><a href='/'>back to main</a></body></html>\r\n");
+    *             ret = true;
+    *             break;
+    *         case HTTPServer::CONTENT_LENGTH_REQUEST:
+    *             ret = true;
+    *             break;
+    *         case HTTPServer::DATA_TRANSFER:
+    *             ret = true;
+    *             break;
+    *         default:
+    *             ret = false;
+    *             break;
+    *     }
+    *     return ret;
+    * }
+    * @endcode
     */
-    typedef enum {
-        CONTENT_LENGTH_REQUEST, ///<!- ask the user if they wish to accept the data
-        DATA_TRANSFER,
-        SEND_PAGE,              ///<! the activated method should now send the page
+    typedef enum CALLBACKTYPE {
+        CONTENT_LENGTH_REQUEST, ///< ask the client if they wish to accept the data
+        DATA_TRANSFER,          ///< not currently used, may allow "chunking" the data to the client
+        SEND_PAGE,              ///< the activated method should now send the page
     } CallBackType;
 
     /** 
-    * callback prototype for custom handler
+    * This is the prototype for custom handlers that are activated via a callback
     *
-    * This callback gets overloaded for a few purposes, which can be identified by the CallBackType parameter
+    * This callback gets overloaded for a few purposes, which can be identified by the \see CallBackType parameter
     * @li SEND_PAGE - the callback should now send the html page, using as many svr->send() as needed. 
     *       When the callback returns, it should always indicate true.
     * @li CONTENT_LENGTH_REQUEST - the server is asking the callback if it wants to receive the message,
@@ -108,16 +200,20 @@
     * @param maxparams defines the maximum number of parameters to a dynamic function (and the memory to support them).
     * @param maxdynamicpages defines the maximum number of dynamic pages that can be registered.
     * @param pc is the serial port for debug information (I should transform this to a log interface)
+    * @param allocforheader is the memory allocation to support the largest expected header from a client
+    * @param allocforfile is the memory allocation to support sending a file to the client. This is typically sized to fit 
+    *        an ethernet frame.
     */
-    HTTPServer(Wifly * wifly, int port = 80, const char * webroot = "/", int maxparams = 30, int maxdynamicpages = 10, PC * pc = NULL);
+    HTTPServer(Wifly * wifly, int port = 80, const char * webroot = "/", int maxparams = 30, int maxdynamicpages = 10, PC * pc = NULL,
+        int _allocforheader = MAX_HEADER_SIZE, int _allocforfile = FILESEND_BUF_SIZE);
     
     /**
-    * destructor, which can clean up a few things
+    * Destructor, which can clean up memory.
     */
     ~HTTPServer();
     
     /**
-    * the process to call whenever there is free time, as this basically does
+    * The process to call whenever there is free time, as this basically does
     * all the work to monitor for connections and handle replies.
     *
     * 20130601 Renamed from ip_process to Poll
@@ -125,29 +221,40 @@
     void Poll();
     
     /**
-    * Send a header back to the client and automatically appends a "\r\n". Each parameter must
-    * send a "\r\n" as part of that string.
+    * Send typical header data, and some optional data back to the client. 
+    *
+    * This forms and sends the typical header back to the client. It may also send
+    * optional data (which must end with "\r\n"). It then sends the second newline
+    * sequence that signals the end of the header.
     *
     * @param code is the optional return code; 200 = OK, if not provided then 404 = Not found is returned
     * @param code_text is the text to align with the code (e.g. 404, "Not Found")
     * @param content_type is a pointer to "Content-Type: text/html\r\n" (for example)
-    * @param optional_text is a pointer to any other text that is part of the header
+    * @param optional_text is a pointer to any other text that is part of the header, which must
+    *        have \r\n termination.
     */
     void header(int code = 404, const char * code_text = "Not Found", const char * content_type = NULL, const char * optional_text = NULL);
 
     /**
     * Send text to the client
     *
+    * This sends the specified text to the client. If the number of bytes is not set,
+    * then it calculates the number of bytes as a string. For binary transfers, the
+    * number of bytes to send is required for proper operation.
+    *
     * @param msg is the text string to send
     * @param bytes is the number of bytes to send. If not set, then strlen is calculated.
     */
     void send(const char * msg, int bytes = -1);
     
     /**
-    * Send a file to the client, including the header
+    * Send a referenced file to the client, including the header
+    *
+    * This sends a file from the filesystem to the client. It must be of a supported type
+    * in order to properly create the header.
     *
     * @param filename is the fully qualified path and filename
-    * @param filetype is the header information (e.g. "Content-Type: text/html")
+    * @param filetype is the header information (e.g. "Content-Type: application/pdf")
     * @return true if it thinks it sent ok, false otherwise.
     */
     bool SendFile(const char * filename, const char * filetype);
@@ -155,6 +262,55 @@
     /** 
     * register a handler for a specific URL.
     *
+    * This api lets you register a dynamic handler in the web server. This is
+    * most useful for interactive web pages, rather than simply serving static
+    * pages.
+    *
+    * @code
+    *   
+    *   ...
+    *   svr.RegisterHandler("/dyn1", SimpleDynamicPage);svr.RegisterHandler("/dyn1", SimpleDynamicPage);
+    *   ...
+    *
+    *   bool SimpleDynamicPage(HTTPServer *svr, HTTPServer::CallBackType type, const char * path, const HTTPServer::namevalue *params, int paramcount) {
+    *       char buf[100];
+    *       bool ret = false;
+    *       
+    *       switch (type) {
+    *           case HTTPServer::SEND_PAGE:
+    *               svr->header(200, "OK", "Content-Type: text/html\r\n");
+    *               svr->send("<html><head><title>Dynamic Page</title></head>\r\n");
+    *               svr->send("<body>\r\n");
+    *               svr->send("This page was generated dynamically. Create your own name=value pairs on the URL "
+    *                         "which uses the GET method.<br/>\r\n");
+    *               sprintf(buf, "%d parameters passed to {%s}:<br/>\r\n", paramcount, path);
+    *               svr->send(buf);
+    *               for (int i=0; i<paramcount; i++) {
+    *                   sprintf(buf, "%d: %s = %s<br/>\r\n", i, params[i].name, params[i].value);
+    *                   svr->send(buf);
+    *               }
+    *               svr->send("Stats:<br/>\r\n");
+    *               sprintf(buf,"Free memory space: %d<br/>\r\n", Free());
+    *               svr->send(buf);
+    *               sprintf(buf,"Max Header size: %d<br/>\r\n", svr->GetMaxHeaderSize());
+    *               svr->send(buf);
+    *               svr->send("<br/><a href='/'>back to main</a></body></html>\r\n");
+    *               ret = true;
+    *               break;
+    *           case HTTPServer::CONTENT_LENGTH_REQUEST:
+    *               ret = true;
+    *               break;
+    *           case HTTPServer::DATA_TRANSFER:
+    *               ret = true;
+    *               break;
+    *           default:
+    *               ret = false;
+    *               break;
+    *       }
+    *       return ret;
+    *   }
+    * @endcode
+    *
     * @param path to register
     * @param callback of type Handler
     * @return true if successfully registered
@@ -164,6 +320,16 @@
     /**
     * determine if the named file is a supported type (e.g. .htm, .jpg, ...)
     *
+    * if you pass in a filename, it will attempt to extract the extension
+    * and compare that to the list of supported file types. If it finds a
+    * match, then it will return a pointer to the content-type string.
+    *
+    * @code
+    *   fType = GetSupportedType("mypix.jpg");
+    *   if (fType) {
+    *       ...
+    * @endcode
+    *       
     * @param filename is the filename to test, based on the extension
     * @return pointer to a Content-Type string if supported, or NULL if not.
     */
@@ -172,15 +338,26 @@
     /**
     * search the available parameters for 'name' and if found, return the 'value'
     *
+    * After the querystring is parsed, the server maintains an array of 
+    * name=value pairs. This Get function will search for the passed in name
+    * and provide access to the value.
+    *
+    * @code
+    * BusOut leds(LED1,LED2,LED3,LED4);
+    * ...
+    * leds = atoi(svr->GetParameter("leds"));
+    * @endcode
+    *
     * @param name is the name to search for
     * @return pointer to the value, or NULL
     */
     const char * GetParameter(const char * name);
 
     /**
-    * parse the text string into name=value parameters. This will directly
-    * modify the referenced string. If there is a #fragment_id on the end
-    * of the string, that will be removed.
+    * Parse the text string into name=value parameters. 
+    *
+    * This will directly modify the referenced string. If there is a 
+    * #fragment_id on the end of the string, it will be removed.
     *
     * @param pString is a pointer to the string.
     */
@@ -188,8 +365,17 @@
     
     /**
     * Unescape string converts a coded string "in place" into a normal string
+    *
+    * A query string will have a number of characters replaced for communication
+    * which includes spaces, quotes, question marks and more. Most of them
+    * will be replaced with a %xx format, where xx is the hex code for the 
+    * character. Since the string will only get shorter when this happens
+    * the operation is performed in place.
+    *
     * this    "This%20is%20a%20question%3F%20and%20an%20answer."
+    * 
     * becomes "This is a question? and an answer."
+    *
     * @note '+' is another form of space, so is converted to a space before the %xx
     *
     * @param encoded string to be converted
@@ -198,21 +384,84 @@
     
     /**
     * Get the IP address of the remote node to which we are connected.
-    * @caution this switches the module into, and out of, command mode
-    *          which has quite a time penalty.
+    *
+    * This will get the IP address of the remote node to which we are
+    * currently connected. This is written into the buffer in 
+    * "192.168.100.234" format. If the buffer size is note >= 16 bytes,
+    * it will set the buffer to null.
+    * 
+    * @note This switches the module into, and out of, command mode
+    *          which has quite a time penalty. 
+    *
+    * @param str is the string to write the address into, which should be at
+    *        least as large as "192.168.100.203" (16-bytes).
+    * @param size of the str buffer must be >=16, so it will not buffer overrun.
     */
     void GetRemoteAddr(char * str, int size);
 
     /** 
-    * used to force a connection to close
+    * This is used to force a connection to close
+    *
+    * This switches the module into command mode, performs the close,
+    * then switches it back to data mode. So, this is a time-expensive
+    * command.
     */
     void close_connection();
+    
+    /**
+    * Get the size of the largest header. 
+    *
+    * This is a diagnostic function, so you can resize the allocated 
+    * buffer for your application. With proper sizing, more of the 
+    * system memory is available for your application.
+    *
+    * @code
+    * sprintf(buf,"Max Header size: %d<br/>\r\n", svr->GetMaxHeaderSize());
+    * svr->send(buf);
+    * @endcode
+    *      
+    * @returns size in bytes of the larger header measured.
+    */
+    int GetMaxHeaderSize();
+
+
+    /**
+    * Performance parameter
+    */
+    typedef struct SW_PERFPARAM {
+        unsigned long long TotalTime_us;
+        unsigned long Samples;
+        unsigned long MaxTime_us;
+    } SW_PerformanceParam;
+    
+    /**
+    * Performance metrics
+    */
+    typedef struct SW_PERFDATA {
+        SW_PerformanceParam Header;
+        SW_PerformanceParam SendData;
+        //SW_PerformanceParam SendFile;
+    } SW_PerformanceData;
+    
+    /**
+    * Get performance metrics from the web server.
+    *
+    * This is a diagnostic function, and gathers data on the internal
+    * performance of the server, as it works various actions.
+    *
+    * @param p is a pointer to a SW_PerformanceData structure to be populated
+    */
+    void GetPerformanceData(SW_PerformanceData * p);
+    
+    /**
+    * Reset performance metrics.
+    */
+    void ResetPerformanceData();
 
 private:
     Wifly * wifly;
     char * webroot;
     PC * pc;
-    Timer * timer;
     TCPSocketServer * server;
     TCPSocketConnection client;
     char * rewriteWithDefaultFile(char * queryString);
@@ -220,14 +469,40 @@
     int maxparams;
     namevalue *params;
     int paramcount;
+    int maxheaderbytes;
+    char * headerbuffer;
+    int headerbuffersize;
     
-    typedef struct {
+    Timer timer;              // for performance metrics gathering
+    /**
+    * Records performance data
+    * 
+    * This will take a pointer to a SW_PerformanceParam, and it will
+    * take the time when the performance measurement started. It locally
+    * accesses the current time to measure the elapsed.
+    *
+    * @param param is the performance parameter to update
+    * @param value is the reference time.
+    * @returns the current time which may be used as the reference time
+    *          for further measurements.
+    */
+    int RecordPerformanceData(SW_PerformanceParam * param, int value);
+    SW_PerformanceData perfData;
+    
+    typedef struct HANDLER {
         char * path;
         Handler callback;
     } handler;
     int maxdynamicpages;
     handler *handlers;
     int handlercount;
+
+    char * queryType;
+    char * queryString;
+    char * hostString;
+    char * contentLength;
+    char * contentType;
+    char * postQueryString;
     
     /**
     *  Extract the message from the record, by searching for the needle
@@ -241,6 +516,10 @@
     */
     bool Extract(char * rec, char * needle, char ** string);
 
+    void SendResponse();
+    bool ParseHeader(char * bPtr);
+    bool CheckDynamicHandlers();
+
     int HexCharToInt(char c);
     char HexPairToChar(char * p);
 };