From: Sam Moore Date: Mon, 19 Aug 2013 12:56:33 +0000 (+0800) Subject: Put FastCGI code into server framework X-Git-Url: https://git.ucc.asn.au/?a=commitdiff_plain;h=ab0ae45f79caaae86b36f8230f9399138c9e73bc;p=matches%2FMCTX3420.git Put FastCGI code into server framework 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. --- diff --git a/MCTX3420.pod b/MCTX3420.pod index e796d02..c319c2f 100644 Binary files a/MCTX3420.pod and b/MCTX3420.pod differ diff --git a/server/Makefile b/server/Makefile index 009128b..f1084af 100644 --- a/server/Makefile +++ b/server/Makefile @@ -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 diff --git a/server/common.h b/server/common.h index 2a6184a..e5bcdbd 100644 --- a/server/common.h +++ b/server/common.h @@ -16,5 +16,6 @@ #include #include #include +#include #endif //_COMMON_H diff --git a/server/fastcgi.c b/server/fastcgi.c new file mode 100644 index 0000000..1b758a4 --- /dev/null +++ b/server/fastcgi.c @@ -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 +#include +#include "fastcgi.h" +#include "common.h" +#include "sensor.h" +#include "log.h" +#include + +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++; + } +} diff --git a/server/fastcgi.h b/server/fastcgi.h new file mode 100644 index 0000000..c43927d --- /dev/null +++ b/server/fastcgi.h @@ -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 index 0000000..a3ff3ec --- /dev/null +++ b/server/index.html @@ -0,0 +1,82 @@ + + + + + + FastCGI API Test + + + + + + +

FastCGI API Test

+ The API is located at: http://mctx.us.to:8080/api/
+

Input

+ Place a query string here. Examples include:
+
    +
  • sensors?key=value&key2
  • +
  • doesntexist?f
  • +
+ Response times are inaccurate via JavaScript. Use the web console of + your browser to determine how long the query takes.
+ Hopefully this doesn't break! +
+
+ Query string:
+ +
+
+ +

Output

+
+
+ + diff --git a/server/main.c b/server/main.c index 5e2c6c8..596fb39 100644 --- a/server/main.c +++ b/server/main.c @@ -9,7 +9,7 @@ #include // 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 index 86696bb..0000000 --- a/server/query.c +++ /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 index 55a4991..0000000 --- a/server/query.h +++ /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 index 0000000..756f9cf --- /dev/null +++ b/server/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Use this to quickly test run the server in valgrind +spawn-fcgi -p9005 -n /usr/bin/valgrind ./server diff --git a/server/sensor.c b/server/sensor.c index 83ffaaa..ab687af 100644 --- a/server/sensor.c +++ b/server/sensor.c @@ -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; +} + diff --git a/server/sensor.h b/server/sensor.h index ae17ce3..da23044 100644 --- a/server/sensor.h +++ b/server/sensor.h @@ -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