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

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