Change @purpose to @brief (Doxygen warning) and work on unit tests
[matches/MCTX3420.git] / server / fastcgi.c
1 /**
2  * @file fastcgi.c
3  * @brief Runs the FCGI request loop to handle web interface requests.
4  *
5  * fcgi_stdio.h must be included before all else so the stdio function
6  * redirection works ok.
7  */
8
9 #include <fcgi_stdio.h>
10 #include <openssl/sha.h>
11 #include <time.h>
12
13 #include "common.h"
14 #include "sensor.h"
15 #include "control.h"
16 #include "options.h"
17
18 /**The time period (in seconds) before the control key expires @ */
19 #define CONTROL_TIMEOUT 180
20
21 /**Contextual information related to FCGI requests*/
22 struct FCGIContext {
23         /**The time of last valid user access possessing the control key*/
24         time_t control_timestamp;
25         char control_key[41];
26         char control_ip[16];
27         /**The name of the current module**/
28         const char *current_module;
29         /**For debugging purposes?**/
30         int response_number;
31 };
32
33 /**
34  * Identifies current version info. Useful for testing that the API is running.
35  * TODO - Consider adding info about available sensors and actuators (eg capabilities)?
36  */ 
37 static void IdentifyHandler(FCGIContext *context, char *params) {
38         FCGI_BeginJSON(context, STATUS_OK);
39         FCGI_JSONPair("description", "MCTX3420 Server API (2013)");
40         FCGI_JSONPair("build_date", __DATE__ " " __TIME__);
41         FCGI_EndJSON();
42 }
43
44 /**
45  * Gives the user a key that determines who has control over
46  * the system at any one time. The key can be forcibly generated, revoking
47  * any previous control keys. To be used in conjunction with HTTP 
48  * basic authentication.
49  * This function will generate a JSON response that indicates success/failure.
50  * @param context The context to work in
51  * @param force Whether to force key generation or not.
52  */ 
53 void FCGI_BeginControl(FCGIContext *context, bool force) {
54         time_t now = time(NULL);
55         bool expired = now - context->control_timestamp > CONTROL_TIMEOUT;
56         
57         if (force || !*(context->control_key) || expired) {
58                 SHA_CTX sha1ctx;
59                 unsigned char sha1[20];
60                 int i = rand();
61
62                 SHA1_Init(&sha1ctx);
63                 SHA1_Update(&sha1ctx, &now, sizeof(now));
64                 SHA1_Update(&sha1ctx, &i, sizeof(i));
65                 SHA1_Final(sha1, &sha1ctx);
66
67                 context->control_timestamp = now;
68                 for (i = 0; i < 20; i++)
69                         sprintf(context->control_key + i * 2, "%02x", sha1[i]);
70                 snprintf(context->control_ip, 16, "%s", getenv("REMOTE_ADDR"));
71                 FCGI_BeginJSON(context, STATUS_OK);
72                 FCGI_JSONPair("key", context->control_key);
73                 FCGI_EndJSON();         
74         } else {
75                 char buf[128];
76                 strftime(buf, 128, "%H:%M:%S %d-%m-%Y",
77                         localtime(&(context->control_timestamp))); 
78                 FCGI_BeginJSON(context, STATUS_UNAUTHORIZED);
79                 FCGI_JSONPair("description", "Another user already has control");
80                 FCGI_JSONPair("current_user", context->control_ip); 
81                 FCGI_JSONPair("when", buf);
82                 FCGI_EndJSON();
83         }
84 }
85
86 /**
87  * Given an FCGIContext, determines if the current user (as specified by
88  * the key) has control or not. If validated, the context control_timestamp is
89  * updated.
90  * @param context The context to work in
91  * @param key The control key to be validated.
92  * @return TRUE if authorized, FALSE if not.
93  */
94 bool FCGI_HasControl(FCGIContext *context, const char *key) {
95         time_t now = time(NULL);
96         int result = (now - context->control_timestamp) <= CONTROL_TIMEOUT &&
97                                  key != NULL && !strcmp(context->control_key, key);
98         if (result) {
99                 context->control_timestamp = now; //Update the control_timestamp
100         }
101         return result;
102 }
103
104
105 /**
106  * Revokes the current control key, if present.
107  * @param context The context to work in
108  */
109 void FCGI_EndControl(FCGIContext *context) {
110         *(context->control_key) = 0;
111         FCGI_BeginJSON(context, STATUS_OK);
112         FCGI_EndJSON();
113         return;
114 }
115
116 /**
117  * Extracts a key/value pair from a request string.
118  * Note that the input is modified by this function.
119  * @param in The string from which to extract the pair
120  * @param key A pointer to a variable to hold the key string
121  * @param value A pointer to a variable to hold the value string
122  * @return A pointer to the start of the next search location, or NULL if
123  *         the EOL is reached.
124  */
125 char *FCGI_KeyPair(char *in, const char **key, const char **value)
126 {
127         char *ptr;
128         if (!in || !*in) { //Invalid input or string is EOL
129                 return NULL;
130         }
131
132         *key = in;
133         //Find either = or &, whichever comes first
134         if ((ptr = strpbrk(in, "=&"))) {
135                 if (*ptr == '&') { //No value specified
136                         *value = ptr;
137                         *ptr++ = 0;
138                 } else {
139                         //Stopped at an '=' sign
140                         *ptr++ = 0;
141                         *value = ptr;
142                         if ((ptr = strchr(ptr,'&'))) {
143                                 *ptr++ = 0;
144                         } else {
145                                 ptr = "";
146                         }
147                 }
148         } else { //No value specified and no other pair
149                 ptr = "";
150                 *value = ptr;
151         }
152         return ptr;
153 }
154
155 /**
156  * Begins a response to the client in JSON format.
157  * @param context The context to work in.
158  * @param status_code The status code to be returned.
159  */
160 void FCGI_BeginJSON(FCGIContext *context, StatusCodes status_code)
161 {
162         printf("Content-type: application/json; charset=utf-8\r\n\r\n");
163         printf("{\r\n");
164         printf("\t\"module\" : \"%s\"", context->current_module);
165         FCGI_JSONLong("status", status_code);
166
167         // Jeremy: Should we include a timestamp in the JSON; something like this?
168         double start_time = g_options.start_time.tv_sec + 1e-6*(g_options.start_time.tv_usec);
169         struct timeval now;
170         gettimeofday(&now, NULL);
171         double current_time = now.tv_sec + 1e-6*(now.tv_usec);
172         FCGI_JSONDouble("start_time", start_time);
173         FCGI_JSONDouble("current_time", current_time);
174         FCGI_JSONDouble("running_time", current_time - start_time);
175         
176 }
177
178 /**
179  * Adds a key/value pair to a JSON response. The response must have already
180  * been initiated by FCGI_BeginJSON. Note that characters are not escaped.
181  * @param key The key of the JSON entry
182  * @param value The value associated with the key.
183  */
184 void FCGI_JSONPair(const char *key, const char *value)
185 {
186         printf(",\r\n\t\"%s\" : \"%s\"", key, value);
187 }
188
189 /**
190  * Similar to FCGI_JSONPair except for signed integer values.
191  * @param key The key of the JSON entry
192  * @param value The value associated with the key
193  */
194 void FCGI_JSONLong(const char *key, long value)
195 {
196         printf(",\r\n\t\"%s\" : %ld", key, value);
197 }
198
199 /**
200  * Similar to FCGI_JsonPair except for floating point values.
201  * @param key The key of the JSON entry
202  * @param value The value associated with the key
203  */
204 void FCGI_JSONDouble(const char *key, double value)
205 {
206         printf(",\r\n\t\"%s\" : %f", key, value);
207 }
208
209 /**
210  * Similar to FCGI_JsonPair except for boolean values.
211  * @param key The key of the JSON entry
212  * @param value The value associated with the key
213  */
214 void FCGI_JSONBool(const char *key, bool value)
215 {
216         printf(",\r\n\t\"%s\" : %s", key, value ? "true" : "false");
217 }
218
219 /**
220  * Begins a JSON entry by writing the key. To be used in conjunction
221  * with FCGI_JsonValue.
222  * @param key The key of the JSON entry
223  */
224 void FCGI_JSONKey(const char *key)
225 {
226         printf(",\r\n\t\"%s\" : ", key);
227 }
228
229 /**
230  * Ends a JSON response that was initiated by FCGI_BeginJSON.
231  */
232 void FCGI_EndJSON() 
233 {
234         printf("\r\n}\r\n");
235 }
236
237 /**
238  * To be used when the input parameters are invalid. The return data will
239  * have a status of STATUS_ERROR, along with other debugging information.
240  * @param context The context to work in
241  */
242 void FCGI_RejectJSON(FCGIContext *context)
243 {
244         FCGI_RejectJSONEx(context, STATUS_ERROR, "Invalid request");
245 }
246
247 /**
248  * To be used when the input parameters are rejected. The return data
249  * will also have debugging information provided.
250  * @param context The context to work in
251  * @param status The status the return data should have.
252  * @param description A short description of why the input was rejected.
253  */
254 void FCGI_RejectJSONEx(FCGIContext *context, StatusCodes status, const char *description)
255 {
256         description = !description ? "" : description;
257         
258         Log(LOGINFO, "%s: Rejected query with: %d: %s", context->current_module, status, description);
259         FCGI_BeginJSON(context, status);
260         FCGI_JSONPair("description", description);
261         FCGI_JSONLong("responsenumber", context->response_number);
262         FCGI_JSONPair("params", getenv("QUERY_STRING"));
263         FCGI_JSONPair("host", getenv("SERVER_HOSTNAME"));
264         FCGI_JSONPair("user", getenv("REMOTE_USER"));
265         FCGI_JSONPair("ip", getenv("REMOTE_ADDR"));
266         FCGI_EndJSON();
267 }
268
269 /**
270  * Generates a response to the client as described by the format parameter and
271  * extra arguments (exactly like printf). To be used when none of the other
272  * predefined functions will work exactly as needed. Extra care should be taken
273  * to ensure the correctness of the output.
274  * @param format The format string
275  * @param ... Any extra arguments as required by the format string.
276  */
277 void FCGI_PrintRaw(const char *format, ...)
278 {
279         va_list list;
280         va_start(list, format);
281         vprintf(format, list);
282         va_end(list);
283 }
284
285 /**
286  * Main FCGI request loop that receives/responds to client requests.
287  * @param data Reserved.
288  * @returns NULL (void* required for consistency with pthreads, although at the moment this runs in the main thread anyway)
289  * TODO: Get this to exit with the rest of the program!
290  */ 
291 void * FCGI_RequestLoop (void *data)
292 {
293         FCGIContext context = {0};
294         
295         Log(LOGDEBUG, "First request...");
296         //TODO: The FCGI_Accept here is blocking. 
297         //              That means that if another thread terminates the program, this thread
298         //               will not terminate until the next request is made.
299         while (FCGI_Accept() >= 0) {
300
301                 if (Thread_Runstate() != RUNNING)
302                 {
303                         //TODO: Yeah... deal with this better :P
304                         Log(LOGERR, "FIXME; FCGI gets request after other threads have finished.");
305                         printf("Content-type: text/plain\r\n\r\n+++OUT OF CHEESE ERROR+++\n");
306                         break;
307                 }
308                 
309                 Log(LOGDEBUG, "Got request #%d", context.response_number);
310                 ModuleHandler module_handler = NULL;
311                 char module[BUFSIZ], params[BUFSIZ];
312                 
313                 //strncpy doesn't zero-truncate properly
314                 snprintf(module, BUFSIZ, "%s", getenv("DOCUMENT_URI_LOCAL"));
315                 snprintf(params, BUFSIZ, "%s", getenv("QUERY_STRING"));
316                 
317                 //Remove trailing slashes (if present) from module query
318                 size_t lastchar = strlen(module) - 1;
319                 if (lastchar > 0 && module[lastchar] == '/')
320                         module[lastchar] = 0;
321                 
322                 if (!*module || !strcmp("identify", module)) {
323                         module_handler = IdentifyHandler;
324                 } else if (!strcmp("control", module)) {
325                         module_handler = Control_Handler;
326                 } else if (!strcmp("sensors", module)) {
327                         module_handler = Sensor_Handler;
328                 }
329
330                 context.current_module = module;
331                 if (module_handler) {
332                         module_handler(&context, params);
333                 } else {
334                         strncat(module, " (unhandled)", BUFSIZ);
335                         FCGI_RejectJSON(&context);
336                 }
337                 context.response_number++;
338
339                 Log(LOGDEBUG, "Waiting for request #%d", context.response_number);
340         }
341
342         Log(LOGDEBUG, "Thread exiting.");
343         Thread_QuitProgram(false);
344         // NOTE: Don't call pthread_exit, because this runs in the main thread. Just return.
345         return NULL;
346 }

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