--- /dev/null
+/**
+ * @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(×tamp));
+ 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
+ * ¶m 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++;
+ }
+}
--- /dev/null
+<!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&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>
+++ /dev/null
-/**
- * @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);
- }
-}
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);
}
{
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
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;
+}
+