Merge branch 'master' of https://github.com/szmoore/MCTX3420.git
authorJeremy Tan <[email protected]>
Mon, 2 Sep 2013 00:45:52 +0000 (08:45 +0800)
committerJeremy Tan <[email protected]>
Mon, 2 Sep 2013 00:45:52 +0000 (08:45 +0800)
nginx-configs/README
nginx-configs/sites-enabled/mctxconfig
server/Makefile
server/control.c [new file with mode: 0644]
server/control.h [new file with mode: 0644]
server/fastcgi.c
server/fastcgi.h
testing/qunit/index.html [new file with mode: 0644]
testing/qunit/unit-tests.js [new file with mode: 0644]

index 1c6ade5..99499a5 100644 (file)
@@ -16,3 +16,8 @@ To get the login functionality working, you need to place a .htpasswd file
 under /usr/share/nginx/access (create folder if it doesn't exist). To generate
 the htpasswd file, install the apache2-utils package and use the 'htpasswd'
 executable.
+
+
+P.S: (I always forget these)
+Set file permissions to: 644
+Set folder permissions to: 755
\ No newline at end of file
index 541f6d1..4da2b84 100644 (file)
@@ -70,18 +70,17 @@ server {
                deny all;
        }
        
-       #Login area
-       location ^~ /api/login {
-               auth_basic "Restricted Access";
-               auth_basic_user_file /usr/share/nginx/access/.htpasswd;
-       
-               fastcgi_pass 127.0.0.1:9005;
-               fastcgi_param DOCUMENT_URI_LOCAL login;
-               include fastcgi_params;
-       }
-       
        #MCTX API
        location /api {
+               #Login area
+               location ^~ /api/control {
+                       auth_basic "Restricted Access";
+                       auth_basic_user_file /usr/share/nginx/access/.htpasswd;
+       
+                       fastcgi_pass 127.0.0.1:9005;
+                       fastcgi_param DOCUMENT_URI_LOCAL control;
+                       include fastcgi_params;
+               }
                location ~ ^/api/?([^?]*) {
                        fastcgi_pass 127.0.0.1:9005;
                        fastcgi_param DOCUMENT_URI_LOCAL $1;
index 9c62076..f754c47 100644 (file)
@@ -2,7 +2,7 @@
 CXX = gcc
 FLAGS = -std=c99 -Wall -Werror -pedantic -g
 LIB = -lpthread -lfcgi -lssl -lcrypto
-OBJ = log.o sensor.o fastcgi.o thread.o main.o
+OBJ = log.o control.o sensor.o fastcgi.o thread.o main.o
 RM = rm -f
 
 BIN = server
diff --git a/server/control.c b/server/control.c
new file mode 100644 (file)
index 0000000..12e340a
--- /dev/null
@@ -0,0 +1,99 @@
+#include "common.h"
+#include "control.h"
+
+/**
+ * Handles control of the actuators.
+ */
+void ActuatorHandler(FCGIContext *context, int id, const char *set_value) {
+       char *ptr;
+       
+       switch(id) { //Add new actuators here
+               case ACT_PREG: //Suppose is pressure regulator. 0-700 input (kPa)
+               {
+                       int value = strtol(set_value, &ptr, 10);
+                       if (*ptr == '\0' && value >= 0 && value <= 700) {
+                               FCGI_BeginJSON(context, STATUS_OK);
+                               FCGI_JSONKey("description");
+                               FCGI_JSONValue("\"Set pressure to %d kPa!\"", value);
+                               FCGI_EndJSON();
+                       } else {
+                               FCGI_RejectJSONEx(context, 
+                                       STATUS_ERROR, "Invalid pressure specified.");
+                       }
+               } break;
+               case ACT_SOLENOID1:
+               {
+                       int value = strtol(set_value, &ptr, 10);
+                       if (*ptr == '\0') {
+                               const char *state = "off";
+                               if (value)
+                                       state = "on";
+                               FCGI_BeginJSON(context, STATUS_OK);
+                               FCGI_JSONKey("description");
+                               FCGI_JSONValue("\"Solenoid 1 turned %s!\"", state);
+                               FCGI_EndJSON();
+                       } else {
+                               FCGI_RejectJSON(context);
+                       }
+               } break;
+               default:
+                       FCGI_RejectJSONEx(context, 
+                               STATUS_ERROR, "Invalid actuator id specified.");
+       }
+}
+
+/**
+ * System control handler. This covers control over all aspects of the system.
+ * E.g: Actuators, system commands (start/stop experiment/recording) etc
+ * @param context The context to work in
+ * @param params The input parameters
+ */
+void Control_Handler(FCGIContext *context, char *params) {
+       const char *key, *value, *control_key = NULL;
+       const char *action = NULL, *set_value = NULL;
+       bool force = false;
+       char *ptr;
+       int id = ACT_NONE;
+       
+       while ((params = FCGI_KeyPair(params, &key, &value))) {
+               if (!strcmp(key, "action"))
+                       action = value;
+               else if (!strcmp(key, "key"))
+                       control_key = value;
+               else if (!strcmp(key, "force"))
+                       force = !force;
+               else if (!strcmp(key, "id") && *value) { //Ensure non-empty value
+                       int parsed = strtol(value, &ptr, 10);
+                       if (*ptr == '\0') {
+                               id = parsed;
+                       }
+               } else if (!strcmp(key, "value")) {
+                       set_value = value;
+               }
+       }
+       
+       if (action == NULL) { //Must have an action
+               FCGI_RejectJSON(context);
+       } else if (!strcmp(action, "start")) {
+               FCGI_BeginControl(context, force);
+       } else if (!strcmp(action, "stop")) { //Don't require control key to stop...
+               //EMERGENCY STOP!! TODO - replace!
+               FCGI_BeginJSON(context, STATUS_OK);
+               FCGI_JSONPair("description", "stopped! (not)");
+               FCGI_EndJSON();
+       } else { //Under this section, the user must have the current control key.
+               if (!FCGI_HasControl(context, control_key)) {
+                       FCGI_RejectJSONEx(context, 
+                               STATUS_UNAUTHORIZED, "Invalid control key specified.");
+               } else if (!strcmp(action, "end")) {
+                       FCGI_EndControl(context);
+               } else if (!strcmp(action, "set")) {
+                       if (set_value == NULL || *set_value == '\0') {
+                               FCGI_RejectJSONEx(context, 
+                                       STATUS_ERROR, "Set called but no value specified.");
+                       } else {
+                               ActuatorHandler(context, id, set_value);
+                       }
+               }
+       }
+}
diff --git a/server/control.h b/server/control.h
new file mode 100644 (file)
index 0000000..cc8f327
--- /dev/null
@@ -0,0 +1,7 @@
+#ifndef _CONTROL_H
+#define _CONTROL_H
+
+typedef enum Actuators {ACT_NONE = -1, ACT_PREG = 0, ACT_SOLENOID1} Actuators;
+extern void Control_Handler(FCGIContext *context, char *params);
+
+#endif
index 59a404d..29e85d3 100644 (file)
 
 #include "common.h"
 #include "sensor.h"
-#include "log.h"
+#include "control.h"
 #include "options.h"
 
-#define LOGIN_TIMEOUT 180
+#define CONTROL_TIMEOUT 180
 
 struct FCGIContext {
-       /**The time of last valid logged-in user access*/
-       time_t login_timestamp;
-       char login_key[41];
-       char login_ip[16];
+       /**The time of last valid user access possessing the control key*/
+       time_t control_timestamp;
+       char control_key[41];
+       char control_ip[16];
        /**The name of the current module**/
        const char *current_module;
        /**For debugging purposes?**/
@@ -29,32 +29,30 @@ struct FCGIContext {
 };
 
 /**
- * Handles user logins.
- * @param context The context to work in
- * @param params User specified parameters
- */
-static void LoginHandler(FCGIContext *context, char *params) {
-       const char *key, *value;
-       bool 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) {
-               *(context->login_key) = 0;
-               FCGI_BeginJSON(context, STATUS_OK);
-               FCGI_EndJSON();
-               return;
-       }
+ * Identifies current version info. Useful for testing that the API is running.
+ * TODO - Consider adding info about available sensors and actuators (eg capabilities)?
+ */ 
+static void IdentifyHandler(FCGIContext *context, char *params) {
+       FCGI_BeginJSON(context, STATUS_OK);
+       FCGI_JSONPair("description", "MCTX3420 Server API (2013)");
+       FCGI_JSONPair("build_date", __DATE__ " " __TIME__);
+       FCGI_EndJSON();
+}
 
+/**
+ * Gives the user a key that determines who has control over
+ * the system at any one time. The key can be forcibly generated, revoking
+ * any previous control keys. To be used in conjunction with HTTP 
+ * basic authentication.
+ * This function will generate a JSON response that indicates success/failure.
+ * @param context The context to work in
+ * @param force Whether to force key generation or not.
+ */ 
+void FCGI_BeginControl(FCGIContext *context, bool force) {
        time_t now = time(NULL);
-       if (force || !*(context->login_key) || 
-          (now - context->login_timestamp > LOGIN_TIMEOUT)) 
-       {
+       bool expired = now - context->control_timestamp > CONTROL_TIMEOUT;
+       
+       if (force || !*(context->control_key) || expired) {
                SHA_CTX sha1ctx;
                unsigned char sha1[20];
                int i = rand();
@@ -64,62 +62,55 @@ static void LoginHandler(FCGIContext *context, char *params) {
                SHA1_Update(&sha1ctx, &i, sizeof(i));
                SHA1_Final(sha1, &sha1ctx);
 
-               context->login_timestamp = now;
+               context->control_timestamp = now;
                for (i = 0; i < 20; i++)
-                       sprintf(context->login_key + i * 2, "%02x", sha1[i]);
-               snprintf(context->login_ip, 16, "%s", getenv("REMOTE_ADDR"));
+                       sprintf(context->control_key + i * 2, "%02x", sha1[i]);
+               snprintf(context->control_ip, 16, "%s", getenv("REMOTE_ADDR"));
                FCGI_BeginJSON(context, STATUS_OK);
-               FCGI_JSONPair("key", context->login_key);
-               FCGI_EndJSON();
+               FCGI_JSONPair("key", context->control_key);
+               FCGI_EndJSON();         
        } else {
                char buf[128];
                strftime(buf, 128, "%H:%M:%S %d-%m-%Y",
-                       localtime(&(context->login_timestamp))); 
+                       localtime(&(context->control_timestamp))); 
                FCGI_BeginJSON(context, STATUS_UNAUTHORIZED);
-               FCGI_JSONPair("description", "Already logged in");
-               FCGI_JSONPair("user", context->login_ip); 
-               FCGI_JSONPair("time", buf);
-               FCGI_EndJSON();
-       }
-}
-
-/*TODO: Remove and replace with the actual actuator code*/
-static void ActuatorHandler(FCGIContext *context, char *params) {
-       const char *key, *value, *loginkey = NULL;
-       while ((params = FCGI_KeyPair(params, &key, &value))) {
-               if (!strcmp(key, "key")) {
-                       loginkey = value;
-               }
-       }
-       if (!loginkey || !FCGI_Authorized(context, loginkey)) {
-               FCGI_BeginJSON(context, STATUS_UNAUTHORIZED);
-               FCGI_JSONPair("description", "Invalid key specified.");
-               FCGI_EndJSON();
-       } else {
-               FCGI_BeginJSON(context, STATUS_OK);
-               FCGI_JSONPair("description", "Logged in!");
+               FCGI_JSONPair("description", "Another user already has control");
+               FCGI_JSONPair("current_user", context->control_ip); 
+               FCGI_JSONPair("when", buf);
                FCGI_EndJSON();
        }
 }
 
 /**
  * Given an FCGIContext, determines if the current user (as specified by
- * the key) is authorized or not. If validated, the context login_timestamp is
+ * the key) has control or not. If validated, the context control_timestamp is
  * updated.
  * @param context The context to work in
- * @param key The login key to be validated.
+ * @param key The control key to be validated.
  * @return TRUE if authorized, FALSE if not.
  */
-bool FCGI_Authorized(FCGIContext *context, const char *key) {
+bool FCGI_HasControl(FCGIContext *context, const char *key) {
        time_t now = time(NULL);
-       int result = (now - context->login_timestamp) <= LOGIN_TIMEOUT &&
-                                !strcmp(context->login_key, key);
+       int result = (now - context->control_timestamp) <= CONTROL_TIMEOUT &&
+                                key != NULL && !strcmp(context->control_key, key);
        if (result) {
-               context->login_timestamp = now; //Update the login_timestamp
+               context->control_timestamp = now; //Update the control_timestamp
        }
        return result;
 }
 
+
+/**
+ * Revokes the current control key, if present.
+ * @param context The context to work in
+ */
+void FCGI_EndControl(FCGIContext *context) {
+       *(context->control_key) = 0;
+       FCGI_BeginJSON(context, STATUS_OK);
+       FCGI_EndJSON();
+       return;
+}
+
 /**
  * Extracts a key/value pair from a request string.
  * Note that the input is modified by this function.
@@ -255,18 +246,27 @@ void FCGI_EndJSON()
 }
 
 /**
- * To be used when the input parameters are invalid.
- * Sends a response with HTTP status 400 Bad request, along with
- * JSON data for debugging.
+ * To be used when the input parameters are invalid. The return data will
+ * have a status of STATUS_ERROR, along with other debugging information.
  * @param context The context to work in
- * @param params The parameters that the module handler received.
  */
 void FCGI_RejectJSON(FCGIContext *context)
 {
-       printf("Status: 400 Bad Request\r\n");
-       
-       FCGI_BeginJSON(context, STATUS_ERROR);
-       FCGI_JSONPair("description", "Invalid request");
+       FCGI_RejectJSONEx(context, STATUS_ERROR, "Invalid request");
+}
+
+/**
+ * To be used when the input parameters are rejected. The return data
+ * will also have debugging information provided.
+ * @param context The context to work in
+ * @param status The status the return data should have.
+ * @param description A short description of why the input was rejected.
+ * @param params The parameters that the module handler received.
+ */
+void FCGI_RejectJSONEx(FCGIContext *context, StatusCodes status, const char *description)
+{
+       FCGI_BeginJSON(context, status);
+       FCGI_JSONPair("description", description);
        FCGI_JSONLong("responsenumber", context->response_number);
        FCGI_JSONPair("params", getenv("QUERY_STRING"));
        FCGI_JSONPair("host", getenv("SERVER_HOSTNAME"));
@@ -313,20 +313,19 @@ void * FCGI_RequestLoop (void *data)
                if (lastchar > 0 && module[lastchar] == '/')
                        module[lastchar] = 0;
                
-
-               if (!strcmp("login", module)) {
-                       module_handler = LoginHandler;
+               if (!*module || !strcmp("identify", module)) {
+                       module_handler = IdentifyHandler;
+               } else if (!strcmp("control", module)) {
+                       module_handler = Control_Handler;
                } else if (!strcmp("sensors", module)) {
                        module_handler = Sensor_Handler;
-               } else if (!strcmp("actuators", module)) {
-                       module_handler = ActuatorHandler;
                }
 
                context.current_module = module;
                if (module_handler) {
                        module_handler(&context, params);
                } else {
-                       strncat(module, " [unknown]", BUFSIZ);
+                       strncat(module, " (unhandled)", BUFSIZ);
                        FCGI_RejectJSON(&context);
                }
                context.response_number++;
index ec1db25..c4d2ef5 100644 (file)
@@ -6,17 +6,19 @@
 #ifndef _FASTCGI_H
 #define _FASTCGI_H
  
-/**Status codes that fcgi module handlers can return**/
+/**(HTTP) Status codes that fcgi module handlers can return**/
 typedef enum StatusCodes {
-       STATUS_OK = 0,
-       STATUS_ERROR = -1,
-       STATUS_UNAUTHORIZED = -2
+       STATUS_OK = 200,
+       STATUS_ERROR = 400,
+       STATUS_UNAUTHORIZED = 401
 } StatusCodes;
 
 typedef struct FCGIContext FCGIContext;
-typedef void (*ModuleHandler) (FCGIContext *data, char *params);
+typedef void (*ModuleHandler) (FCGIContext *context, char *params);
 
-extern bool FCGI_Authorized(FCGIContext *context, const char *key);
+extern void FCGI_BeginControl(FCGIContext *context, bool force);
+extern void FCGI_EndControl(FCGIContext *context);
+extern bool FCGI_HasControl(FCGIContext *context, const char *key);
 extern char *FCGI_KeyPair(char *in, const char **key, const char **value);
 extern void FCGI_BeginJSON(FCGIContext *context, StatusCodes status_code);
 extern void FCGI_JSONPair(const char *key, const char *value);
@@ -27,6 +29,7 @@ extern void FCGI_JSONKey(const char *key);
 extern void FCGI_JSONValue(const char *format, ...);
 extern void FCGI_EndJSON();
 extern void FCGI_RejectJSON(FCGIContext *context);
+extern void FCGI_RejectJSONEx(FCGIContext *context, StatusCodes status, const char *description);
 extern void * FCGI_RequestLoop (void *data);
 #define FCGI_PrintRaw FCGI_JSONValue // Functionality is identical
 
diff --git a/testing/qunit/index.html b/testing/qunit/index.html
new file mode 100644 (file)
index 0000000..e666fce
--- /dev/null
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+       <head>
+       <meta charset="utf-8">
+               <title>MCTX3420 2013 Server API unit tests</title>
+               <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
+        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
+        <script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
+    </head>
+<body>
+       <div id="qunit"></div>
+       <div id="qunit-fixture"></div>
+       <script src="unit-tests.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/testing/qunit/unit-tests.js b/testing/qunit/unit-tests.js
new file mode 100644 (file)
index 0000000..2bab3f8
--- /dev/null
@@ -0,0 +1,117 @@
+/**
+ * MCTX3420 2013 - Remote pressurised can experiment.
+ * Unit testing for the server API.
+ * These unit tests use the QUnit unit testing framework.
+ * @requires QUnit and jQuery
+ * @date 28/8/13
+ * @author Jeremy Tan
+ */
+
+var api = location.protocol + "//" +  location.host + "/api/";
+
+/**
+ * Sends an AJAX query to the API
+ * @param {string} module The name of the module to be queried
+ * @param {Object} opts Object holding the parameters, username, password and
+ *                 callback. The parameters should be an object of key/value
+ *                 pairs.
+ * @returns JSON data
+ */
+function query(module, opts) {
+  function buildQuery(opts) {
+    var result = "?";
+    var first = true;
+    
+    for (key in opts) {
+      if (!first) 
+        result += "&";
+      else 
+        first = false;
+      result += encodeURIComponent(key) + 
+                ((opts[key] !== undefined) ? "=" + encodeURIComponent(opts[key]) : "");
+    }
+    return result;
+  }
+  
+  var queryurl = api + module;
+  if (opts.params)
+    queryurl += buildQuery(opts.params);
+  
+  var authfunc;
+  if (opts.username) {
+    authfunc = function(xhr) {
+      xhr.setRequestHeader("Authorization",
+        "Basic " + btoa(opts.username + ":" + opts.password));
+    };
+  }
+  
+  $.ajax({
+    url: queryurl,
+    type: 'GET',
+    dataType: 'json',
+    beforeSend: authfunc
+  }).done(opts.callback)
+    .fail(function(jqXHR) {
+      alert("Request Failed!");
+      ok(false, "Request failed: " + jqXHR.status.toString() + " " + jqXHR.statusText);
+      opts.callback(null);
+    });
+}
+
+QUnit.module("API basics");
+QUnit.asyncTest("Existence (identify)", function () {
+  query("identify", {callback : function(data) {
+   start();
+   ok(data.status >= 0, "Return status");
+   ok(data.description, data.description);
+   ok(data.build_date, data.build_date);
+  }});
+});
+
+QUnit.asyncTest("Invalid module", function () {
+  query("dontexist", {callback : function(data) {
+   start();
+   ok(data.status < 0);
+  }});
+});
+
+QUnit.module("Sensors");
+QUnit.asyncTest("Existence", function() {
+  query("sensors", {params : {id : 0}, callback : function(data) {
+   start();
+   ok(data.status >= 0, "Return status");
+   ok(data.data !== undefined, "Data field existence");
+   var result = "Data: ";
+   for (var i = 0; i < data.data.length; i++) {
+     result += data.data[i][0]  + ":" + data.data[i][1] + ", ";
+   }
+   ok(true, result);
+  }});  
+});
+
+QUnit.asyncTest("Invalid sensor id 1", function() {
+  query("sensors", {params : {id : 999}, callback : function(data) {
+   start();
+   ok(data.status < 0, "Return status");
+  }});  
+});
+
+QUnit.asyncTest("Invalid sensor id 2", function() {
+  query("sensors", {params : {id : ""}, callback : function(data) {
+   start();
+   ok(data.status < 0, "Return status");
+  }});  
+});
+
+QUnit.module("Controls and access");
+QUnit.asyncTest("Gaining access", function() {
+  query("control", {params : {action : "start", force : true}, 
+                  username : "mctxadmin", password : "admin", 
+                  callback : function(data) {
+   start();
+   ok(data.status >= 0, "Return status");
+   
+   var key = data.key;
+   
+  }});
+});

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