Put FastCGI code into server framework
authorSam Moore <[email protected]>
Mon, 19 Aug 2013 12:56:33 +0000 (20:56 +0800)
committerSam Moore <[email protected]>
Mon, 19 Aug 2013 12:56:33 +0000 (20:56 +0800)
Implement basic SensorHandler function.
Can now get data from (simulated still) sensor displayed in web browser.

SensorHandler deals with argument checking, identifies the sensor,
sends the JSON, etc, calls Sensor_Query appropriately.

Sensor_Query actually fills a buffer with requested number of data points.

Example of use:
http://server.address/api/sensor?id=0

Use run.sh to start fastcgi server under valgrind.

12 files changed:
MCTX3420.pod
server/Makefile
server/common.h
server/fastcgi.c [new file with mode: 0644]
server/fastcgi.h [new file with mode: 0644]
server/index.html [new file with mode: 0644]
server/main.c
server/query.c [deleted file]
server/query.h [deleted file]
server/run.sh [new file with mode: 0755]
server/sensor.c
server/sensor.h

index e796d02..c319c2f 100644 (file)
Binary files a/MCTX3420.pod and b/MCTX3420.pod differ
index 009128b..f1084af 100644 (file)
@@ -1,8 +1,8 @@
-# Makefile for rpi side server
+# Makefile for server software
 CXX = gcc
 FLAGS = -std=c99 -Wall -Werror -pedantic -g
-LIB = -lpthread
-OBJ = log.o sensor.o query.o main.o
+LIB = -lpthread -lfcgi -lssl
+OBJ = log.o sensor.o fastcgi.o main.o
 RM = rm -f
 
 BIN = server
index 2a6184a..e5bcdbd 100644 (file)
@@ -16,5 +16,6 @@
 #include <errno.h>
 #include <pthread.h>
 #include <unistd.h>
+#include <assert.h>
 
 #endif //_COMMON_H
diff --git a/server/fastcgi.c b/server/fastcgi.c
new file mode 100644 (file)
index 0000000..1b758a4
--- /dev/null
@@ -0,0 +1,269 @@
+/**
+ * @file fastcgi.c
+ * @purpose Runs the FCGI request loop to handle web interface requests.
+ *
+ * fcgi_stdio.h must be included before all else so the stdio function
+ * redirection works ok.
+ */
+
+#include <fcgi_stdio.h>
+#include <openssl/sha.h>
+#include "fastcgi.h"
+#include "common.h"
+#include "sensor.h"
+#include "log.h"
+#include <time.h>
+
+static void LoginHandler(void *data, char *params) {
+       static char loginkey[41] = {0}, ip[256];
+       static time_t timestamp = 0;
+       const char *key, *value;
+       int force = 0, end = 0;
+
+       while ((params = FCGI_KeyPair(params, &key, &value))) {
+               if (!strcmp(key, "force"))
+                       force = !force;
+               else if (!strcmp(key, "end"))
+                       end = !end;
+       }
+
+       if (end) {
+               *loginkey = 0;
+               FCGI_BeginJSON(200, "login");
+               FCGI_EndJSON();
+               return;
+       }
+
+       time_t now = time(NULL);
+       if (force || !*loginkey || (now - timestamp > 180)) {
+               SHA_CTX sha1ctx;
+               unsigned char sha1[20];
+               int i = rand();
+
+               SHA1_Init(&sha1ctx);
+               SHA1_Update(&sha1ctx, &now, sizeof(now));
+               SHA1_Update(&sha1ctx, &i, sizeof(i));
+               SHA1_Final(sha1, &sha1ctx);
+
+               timestamp = now;
+               for (i = 0; i < 20; i++)
+                       sprintf(loginkey+i*2, "%02x", sha1[i]);
+               sprintf(ip, "%s", getenv("REMOTE_ADDR"));
+               FCGI_BeginJSON(200, "login");
+               FCGI_BuildJSON("key", loginkey);
+               FCGI_EndJSON();
+       } else {
+               char buf[128];
+               strftime(buf, 128, "%H:%M:%S %d-%m-%Y",localtime(&timestamp)); 
+               FCGI_BeginJSON(401, "login");
+               FCGI_BuildJSON("description", "Already logged in");
+               FCGI_BuildJSON("user", ip); 
+               FCGI_BuildJSON("time", buf);
+               FCGI_EndJSON();
+       }
+}
+
+/**
+ * Handle a request to the sensor module
+ * @param data - Data to pass to module (?)
+ * @param params - Parameters passed
+ */
+static void SensorHandler(void * data, char * params)
+{
+       static DataPoint buffer[SENSOR_QUERYBUFSIZ];
+       StatusCodes status = STATUS_OK;
+       const char * key; const char * value;
+
+       int sensor_id = SENSOR_NONE;
+
+       do
+       {
+               params = FCGI_KeyPair(params, &key, &value);
+               Log(LOGDEBUG, "Got key=%s and value=%s", key, value);
+               if (strcmp(key, "id") == 0)
+               {
+                       if (sensor_id != SENSOR_NONE)
+                       {
+                               Log(LOGERR, "Only one sensor id should be specified");
+                               status = STATUS_BADREQUEST;
+                               break;
+                       }
+                       //TODO: Use human readable sensor identifier string for API?
+                       sensor_id = atoi(value);
+                       if (sensor_id == 0 && strcmp(value, "0") != 0)
+                       {
+                               Log(LOGERR, "Sensor id not an integer; %s", value);
+                               status = STATUS_BADREQUEST;
+                               break;
+                       }
+               }
+               else
+               {
+                       Log(LOGERR, "Unknown key \"%s\" (value = %s)", key, value);
+                       status = STATUS_BADREQUEST;
+                       break;
+               }               
+       }
+       while (params != NULL && *params != '\0');
+       
+       if (sensor_id == SENSOR_NONE)
+       {
+               Log(LOGERR, "No sensor id specified");
+               status = STATUS_BADREQUEST;
+       }
+       if (sensor_id >= NUMSENSORS || sensor_id < 0)
+       {
+               Log(LOGERR, "Invalid sensor id %d", sensor_id);
+               status = STATUS_BADREQUEST;
+       }
+
+       FCGI_BeginJSON(status, "sensor");
+       
+       if (status != STATUS_BADREQUEST)
+       {
+               FCGI_BuildJSON(key, value); // should spit back sensor ID
+               //Log(LOGDEBUG, "Call Sensor_Query...");
+               int amount_read = Sensor_Query(&(g_sensors[sensor_id]), buffer, SENSOR_QUERYBUFSIZ);
+               //Log(LOGDEBUG, "Read %d DataPoints", amount_read);
+               //Log(LOGDEBUG, "Produce JSON response");
+               printf(",\r\n\t\"data\" : [");
+               for (int i = 0; i < amount_read; ++i)
+               {
+                       printf("[%f,%f]", buffer[i].time, buffer[i].value);
+                       if (i+1 < amount_read)
+                               printf(",");
+               }
+               printf("]");
+               //Log(LOGDEBUG, "Done producing JSON response");
+       }
+       FCGI_EndJSON();         
+       
+}
+
+/**
+ * Extracts a key/value pair from a request string.
+ * Note that the input is modified by this function.
+ * @param in The string from which to extract the pair
+ * @param key A pointer to a variable to hold the key string
+ * @param value A pointer to a variable to hold the value string
+ * @return A pointer to the start of the next search location, or NULL if
+ *         the EOL is reached.
+ */
+char *FCGI_KeyPair(char *in, const char **key, const char **value)
+{
+       char *ptr;
+       if (!in || !*in) { //Invalid input or string is EOL
+               return NULL;
+       }
+
+       *key = in;
+       //Find either = or &, whichever comes first
+       if ((ptr = strpbrk(in, "=&"))) {
+               if (*ptr == '&') { //No value specified
+                       *value = ptr;
+                       *ptr++ = 0;
+               } else {
+                       //Stopped at an '=' sign
+                       *ptr++ = 0;
+                       *value = ptr;
+                       if ((ptr = strchr(ptr,'&'))) {
+                               *ptr++ = 0;
+                       } else {
+                               ptr = "";
+                       }
+               }
+       } else { //No value specified and no other pair
+               ptr = "";
+               *value = ptr;
+       }
+       return ptr;
+}
+
+/**
+ * Begins a response to the client in JSON format.
+ * @param status_code The HTTP status code to be returned.
+ * @param module The name of the module that initiated the response.
+ */
+void FCGI_BeginJSON(StatusCodes status_code, const char *module)
+{
+       switch (status_code) {
+               case STATUS_OK:
+                       break;
+               case STATUS_UNAUTHORIZED:
+                       printf("Status: 401 Unauthorized\r\n");
+                       break;
+               default:
+                       printf("Status: 400 Bad Request\r\n");
+       }
+       printf("Content-type: application/json; charset=utf-8\r\n\r\n");
+       printf("{\r\n");
+       printf("\t\"module\" : \"%s\"", module);
+}
+
+/**
+ * Adds a key/value pair to a JSON response. The response must have already
+ * been initiated by FCGI_BeginJSON. Note that characters are not escaped.
+ * @param key The key of the JSON entry
+ * &param value The value associated with the key.
+ */
+void FCGI_BuildJSON(const char *key, const char *value)
+{
+       printf(",\r\n\t\"%s\" : \"%s\"", key, value);
+}
+
+/**
+ * Ends a JSON response that was initiated by FCGI_BeginJSON.
+ */
+void FCGI_EndJSON() 
+{
+       printf("\r\n}\r\n");
+}
+
+/**
+ * Main FCGI request loop that receives/responds to client requests.
+ * @param data A data field to be passed to the selected module handler.
+ */ 
+void FCGI_RequestLoop (void * data)
+{
+       int count = 0;
+       while (FCGI_Accept() >= 0)   {
+               ModuleHandler module_handler = NULL;
+               char module[BUFSIZ], params[BUFSIZ];
+
+               //strncpy doesn't zero-truncate properly
+               snprintf(module, BUFSIZ, "%s", getenv("DOCUMENT_URI_LOCAL"));
+               snprintf(params, BUFSIZ, "%s", getenv("QUERY_STRING"));
+               
+               //Remove trailing slashes (if present) from module query
+               size_t lastchar = strlen(module) - 1;
+               if (lastchar > 0 && module[lastchar] == '/')
+                       module[lastchar] = 0;
+               
+
+               if (!strcmp("sensors", module)) {
+                       module_handler = SensorHandler;
+               } else if (!strcmp("login", module)) {
+                       module_handler = LoginHandler;
+               } else if (!strcmp("actuators", module)) {
+                       
+               }
+
+               if (module_handler) {
+                       module_handler(data, params);
+               } else {
+                       char buf[BUFSIZ];
+                       
+                       FCGI_BeginJSON(400, module);
+                       FCGI_BuildJSON("description", "400 Invalid response");
+                       snprintf(buf, BUFSIZ, "%d", count);
+                       FCGI_BuildJSON("request-number", buf);
+                       FCGI_BuildJSON("params", params);
+                       FCGI_BuildJSON("host", getenv("SERVER_HOSTNAME"));
+                       FCGI_BuildJSON("user", getenv("REMOTE_USER"));
+                       FCGI_BuildJSON("userip", getenv("REMOTE_ADDR"));
+                       FCGI_EndJSON();
+               }
+
+               count++;
+       }
+}
diff --git a/server/fastcgi.h b/server/fastcgi.h
new file mode 100644 (file)
index 0000000..c43927d
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * @file fastcgi.h
+ * @purpose Headers for the fastcgi web interface
+ */
+#ifndef _FASTCGI_H
+#define _FASTCGI_H
+/**HTTP status codes that fcgi module handlers can return**/
+typedef enum StatusCodes {
+       STATUS_OK = 200, 
+       STATUS_BADREQUEST = 400,
+       STATUS_UNAUTHORIZED = 401
+} StatusCodes;
+
+typedef void (*ModuleHandler) (void *data, char *params);
+
+extern char *FCGI_KeyPair(char *in, const char **key, const char **value);
+extern void FCGI_BeginJSON(StatusCodes status_code, const char *module);
+extern void FCGI_BuildJSON(const char *key, const char *value);
+extern void FCGI_EndJSON();
+extern void FCGI_RequestLoop (void *data);
+
+#define SENSOR_QUERYBUFSIZ 10
+
+#endif //_FASTCGI_H
diff --git a/server/index.html b/server/index.html
new file mode 100644 (file)
index 0000000..a3ff3ec
--- /dev/null
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="utf-8">
+               <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
+               <title>FastCGI API Test</title>
+               <style>
+                       body {
+                               font-family: "Trebuchet MS", "Verdana", "Arial", "Sans";
+                               font-size: 12px;
+                               margin: 1em;
+                       }
+                       h2 {
+                               border-bottom: 1px solid gray;
+                       }
+                       .io {
+                               border: 1px solid gray;
+                               padding: 0.5em;
+                               margin: 1em;
+                               min-height: 5em;
+                               background-color: GhostWhite;
+                       }
+               </style>
+               
+               <script>
+               $(document).ready(function()
+               {
+                       $('#inputquery').submit(function () 
+                       {
+                               $('#output').text("Submitting query...");
+                               var query = $('#inputquery').find('input[name="query"]').val();
+                               
+                               var d = new Date();
+                               var start = d.getMilliseconds();
+                               var domain = document.domain == "mctx.us.to" ? "mctx.us.to:8080" : document.domain;
+                               $.getJSON('http://'+domain+'/api/'+query, function(data) {
+                                       var items = [];
+                                       var timeDiff = d.getMilliseconds() - start; //Not precise at all, use web console
+                                       
+                                       $.each(data, function(key, val) {
+                                               items.push('<li>"' + key + '" : "' + val + '"</li>'); 
+                                       });
+                                       
+                                       
+                                       $('#output').html("Response ok (" + timeDiff + "ms)! Output:<br>");
+                                       $('<ul/>', {
+                                               html: items.join("\n")
+                                       }).appendTo('#output');
+               
+                               }).fail(function(jqXHR) {
+                                       $('#output').text("Query failed with response code: " + jqXHR.status);
+                               });
+                               return false;
+                       });
+               });
+               </script>
+       </head>
+       
+       <body>
+               <h1>FastCGI API Test</h1>
+               The API is located at: <a href="http://mctx.us.to:8080/api/">http://mctx.us.to:8080/api/</a><br>
+               <h2>Input</h2>
+               Place a query string here. Examples include:<br>
+               <ul>
+                       <li><pre>sensors?key=value&amp;key2</pre></li>
+                       <li><pre>doesntexist?f</pre></li>
+               </ul>
+               Response times are inaccurate via JavaScript. Use the web console of
+               your browser to determine how long the query takes.<br>
+               Hopefully this doesn't break!
+               <div class="io">
+                       <form id="inputquery" name="input" action="#">
+                               Query string: <input type="text" name="query"><br>
+                               <input type="submit" value="Submit">
+                       </form>
+               </div>
+               
+               <h2>Output</h2>
+               <div id="output" class="io">
+               </div>
+       </body>
+</html>
index 5e2c6c8..596fb39 100644 (file)
@@ -9,7 +9,7 @@
 #include <signal.h> // for signal handling
 
 // --- Custom headers --- //
-#include "query.h"
+#include "fastcgi.h"
 #include "log.h"
 #include "options.h"
 #include "sensor.h"
@@ -74,7 +74,7 @@ int main(int argc, char ** argv)
        }
 
        // run request thread in the main thread
-       Query_Main(NULL); //TODO: Replace with FastCGI code
+       FCGI_RequestLoop(NULL);
        return 0;
 }
 
diff --git a/server/query.c b/server/query.c
deleted file mode 100644 (file)
index 86696bb..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * @file query.c
- * @purpose Temporary file to run a test thread that will query a sensors thread
- * Code will probably be combined with Jeremy's FastCGI API
- */
-
-
-
-#include "query.h"
-
-#include "sensor.h"
-#include "log.h"
-
-static DataPoint buffer[QUERY_BUFSIZ];
-
-/**
- * Query sensor with id
- * @param id - The index of the sensor in g_sensors
- */
-void QuerySensor(int id) //TODO: This code will form the SensorHandler FastCGI function (I think?)
-{
-       Sensor * s = g_sensors+id;
-
-       int amount_read = 0;
-       //CRITICAL SECTION (Don't access file while sensor thread is writing to it!)
-       pthread_mutex_lock(&(s->mutex));
-
-               fseek(s->file, -QUERY_BUFSIZ*sizeof(DataPoint), SEEK_END);
-               amount_read = fread(&buffer, sizeof(DataPoint), QUERY_BUFSIZ, s->file);
-               Log(LOGDEBUG, "Read %d data points", amount_read);
-               
-       pthread_mutex_unlock(&(s->mutex));
-       //End critical section
-
-       // So... we have a buffer
-       // I guess we'll want to JSON it or something?
-       // Just print it out for now
-       for (int i = 0; i < amount_read; ++i)
-       {
-               printf("%f\t%f\n", buffer[i].time, buffer[i].value);
-       }
-
-       // Will want to handle case where there actually wasn't anything new to respond with
-       // (In case we have a sensor that is slower than the rate of jQuery requests)
-       if (amount_read == 0)
-       {
-               Log(LOGWARN, "No data points read from sensor%s file");
-               printf("# No data\n");
-       }
-}
-
-/**
- * Test function to simulate responding to HTTP requests
- * @param args - IGNORED (void* required to pass function to pthread_create)
- * @returns NULL (void* required to pass function to pthread_create)
- */
-void * Query_Main(void * args)
-{
-       while (true) //TODO: Exit condition
-       {
-               
-               for (int i = 0; i < NUMSENSORS; ++i)
-               {
-                       printf("# Sensor %d\n", i);
-                       QuerySensor(i);
-                       printf("\n");   
-               }
-               usleep(REQUEST_RATE);
-       }
-}
diff --git a/server/query.h b/server/query.h
deleted file mode 100644 (file)
index 55a4991..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @file query.h
- * @purpose Header for query stuff
- */
-
-#ifndef _QUERY_H
-#define _QUERY_H
-
-#define REQUEST_RATE 2000000 // rate to make requests
-#define QUERY_BUFSIZ 10 // size of query buffer
-
-extern void * Query_Main(void * args);
-
-#endif //QUERY_H
diff --git a/server/run.sh b/server/run.sh
new file mode 100755 (executable)
index 0000000..756f9cf
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/bash
+# Use this to quickly test run the server in valgrind
+spawn-fcgi -p9005 -n /usr/bin/valgrind ./server
index 83ffaaa..ab687af 100644 (file)
@@ -64,9 +64,10 @@ void Sensor_Init(Sensor * s, int id)
        s->read_offset = 0;
        s->id = id;
 
-       #define FILENAMESIZE 4
+       #define FILENAMESIZE BUFSIZ
        char filename[FILENAMESIZE];
-       if (s->id >= pow(10, FILENAMESIZE))
+       //if (s->id >= pow(10, FILENAMESIZE))
+       if (false)
        {
                Fatal("Too many sensors! FILENAMESIZE is %d; increase it and recompile.", FILENAMESIZE);
        }
@@ -116,7 +117,7 @@ void * Sensor_Main(void * arg)
                        {
                                Fatal("Wrote %d data points and expected to write %d to \"%s\" - %s", amount_written, SENSOR_DATABUFSIZ, strerror(errno));
                        }
-                       Log(LOGDEBUG, "Wrote %d data points for sensor %d", amount_written, s->id);
+                       //Log(LOGDEBUG, "Wrote %d data points for sensor %d", amount_written, s->id);
                pthread_mutex_unlock(&(s->mutex));
                // End of critical section
 
@@ -126,4 +127,24 @@ void * Sensor_Main(void * arg)
        return NULL;
 }
 
+/**
+ * Fill buffer with most recent sensor data
+ * @param s - Sensor to use
+ * @param buffer - Buffer to fill
+ * @param bufsiz - Size of buffer to fill
+ * @returns The number of DataPoints actually read
+ */
+int Sensor_Query(Sensor * s, DataPoint * buffer, int bufsiz)
+{
+       int amount_read = 0;
+       //CRITICAL SECTION (Don't access file while sensor thread is writing to it!)
+       pthread_mutex_lock(&(s->mutex));
+               
+               fseek(s->file, -bufsiz*sizeof(DataPoint), SEEK_END);
+               amount_read = fread(buffer, sizeof(DataPoint), bufsiz, s->file);
+               //Log(LOGDEBUG, "Read %d data points", amount_read);            
+       pthread_mutex_unlock(&(s->mutex));
+       return amount_read;
+}
+
 
index ae17ce3..da23044 100644 (file)
@@ -31,14 +31,14 @@ typedef struct
 typedef struct
 {
        /** ID number of the sensor **/
-       enum {SENSOR_TEST0=0, SENSOR_TEST1=1} id;
+       enum {SENSOR_TEST0=0, SENSOR_TEST1=1, SENSOR_NONE} id;
        /** Buffer to store data from the sensor **/
        DataPoint buffer[SENSOR_DATABUFSIZ];
        /** Index of last point written in the data buffer **/
        int write_index;
        /** Offset position in binary file for query thread to read from**/
        int read_offset;
-       /** File to write data into when buffer is full **/
+       /** Binary file to write data into when buffer is full **/
        FILE * file;
        /** Thread running the sensor **/
        pthread_t thread;
@@ -54,6 +54,8 @@ extern Sensor g_sensors[];
 extern void Sensor_Init(Sensor * s, int id); // Initialise sensor
 extern void * Sensor_Main(void * args); // main loop for sensor thread; pass a Sensor* cast to void*
 
+extern int Sensor_Query(Sensor * s, DataPoint * buffer, int bufsiz); // fill buffer with sensor data
+
 
 #endif //_SENSOR_H
 

UCC git Repository :: git.ucc.asn.au