refactoring (in progress, tbf)
[src/app-framework-binder.git] / src / session.c
1 /*
2  * Copyright (C) 2015 "IoT.bzh"
3  * Author "Fulup Ar Foll"
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * Reference:
19  * http://stackoverflow.com/questions/25971505/how-to-delete-element-from-hsearch
20  *
21  */
22
23
24 #include "local-def.h"
25 #include <dirent.h>
26 #include <string.h>
27 #include <time.h>
28 #include <sys/stat.h>
29 #include <sys/types.h>
30 #include <pthread.h>
31 #include <search.h>
32 #include <assert.h>
33
34 #include "afb-apis.h"
35 #include "session.h"
36
37 #define NOW (time(NULL))
38
39 // Session UUID are store in a simple array [for 10 sessions this should be enough]
40 static struct {
41   pthread_mutex_t mutex;          // declare a mutex to protect hash table
42   AFB_clientCtx **store;          // sessions store
43   int count;                      // current number of sessions
44   int max;
45   int timeout;
46   int apicount;
47   const char *initok;
48 } sessions;
49
50 static const char key_uuid[] = "uuid";
51 static const char key_token[] = "token";
52
53 // Free context [XXXX Should be protected again memory abort XXXX]
54 static void ctxUuidFreeCB (AFB_clientCtx *client)
55 {
56     int idx;
57
58     // If application add a handle let's free it now
59     if (client->contexts != NULL) {
60
61         // Free client handle with a standard Free function, with app callback or ignore it
62         for (idx=0; idx < sessions.apicount; idx ++) {
63             if (client->contexts[idx] != NULL) {
64                 afb_apis_free_context(idx, client->contexts[idx]);
65             }
66         }
67     }
68 }
69
70 // Create a new store in RAM, not that is too small it will be automatically extended
71 void ctxStoreInit (int nbSession, int timeout, int apicount, const char *initok)
72 {
73         // let's create as store as hashtable does not have any
74         sessions.store = calloc (1 + (unsigned)nbSession, sizeof(AFB_clientCtx));
75         sessions.max = nbSession;
76         sessions.timeout = timeout;
77         sessions.apicount = apicount;
78         if (strlen(initok) >= 37) {
79                 fprintf(stderr, "Error: initial token '%s' too long (max length 36)", initok);
80                 exit(1);
81         }
82         sessions.initok = initok;
83 }
84
85 static AFB_clientCtx *ctxStoreSearch (const char* uuid)
86 {
87     int  idx;
88     AFB_clientCtx *client;
89
90     assert (uuid != NULL);
91
92     pthread_mutex_lock(&sessions.mutex);
93
94     for (idx=0; idx < sessions.max; idx++) {
95         client = sessions.store[idx];
96         if (client && (0 == strcmp (uuid, client->uuid)))
97                 goto found;
98     }
99     client = NULL;
100
101 found:
102     pthread_mutex_unlock(&sessions.mutex);
103     return client;
104 }
105
106 static AFB_error ctxStoreDel (AFB_clientCtx *client)
107 {
108     int idx;
109     int status;
110
111     assert (client != NULL);
112
113     pthread_mutex_lock(&sessions.mutex);
114
115     for (idx=0; idx < sessions.max; idx++) {
116         if (sessions.store[idx] == client) {
117                 sessions.store[idx]=NULL;
118                 sessions.count--;
119                 ctxUuidFreeCB (client);
120                 status = AFB_SUCCESS;
121                 goto deleted;
122         }
123     }
124     status = AFB_FAIL;
125 deleted:
126     pthread_mutex_unlock(&sessions.mutex);
127     return status;
128 }
129
130 static AFB_error ctxStoreAdd (AFB_clientCtx *client)
131 {
132     int idx;
133     int status;
134     if (client == NULL)
135         return AFB_FAIL;
136
137     //fprintf (stderr, "ctxStoreAdd request uuid=%s count=%d\n", client->uuid, sessions.count);
138
139     pthread_mutex_lock(&sessions.mutex);
140
141     for (idx=0; idx < sessions.max; idx++) {
142         if (NULL == sessions.store[idx]) {
143                 sessions.store[idx]= client;
144                 sessions.count++;
145                 status = AFB_SUCCESS;
146                 goto added;
147         }
148     }
149     status = AFB_FAIL;
150
151 added:
152     pthread_mutex_unlock(&sessions.mutex);
153     return status;
154 }
155
156 // Check if context timeout or not
157 static int ctxStoreTooOld (AFB_clientCtx *ctx, time_t now)
158 {
159     return ctx->timeStamp <= now;
160 }
161
162 // Loop on every entry and remove old context sessions.hash
163 void ctxStoreGarbage ()
164 {
165     AFB_clientCtx *ctx;
166     long idx;
167     time_t now = NOW;
168
169     // Loop on Sessions Table and remove anything that is older than timeout
170     for (idx=0; idx < sessions.max; idx++) {
171         ctx = sessions.store[idx];
172         if ((ctx != NULL) && (ctxStoreTooOld(ctx, now))) {
173             ctxStoreDel (ctx);
174         }
175     }
176 }
177
178 // This function will return exiting client context or newly created client context
179 AFB_clientCtx *ctxClientGet (AFB_request *request)
180 {
181   AFB_clientCtx *clientCtx=NULL;
182   const char *uuid;
183   uuid_t newuuid;
184
185     if (request->config->token == NULL) return NULL;
186
187     // Check if client as a context or not inside the URL
188     uuid  = NULL; //MHD_lookup_connection_value(request->connection, MHD_GET_ARGUMENT_KIND, key_uuid);
189
190     // if UUID in query we're restfull with no cookies otherwise check for cookie
191     if (uuid != NULL)
192         request->restfull = TRUE;
193     else {
194         char cookie[64];
195         request->restfull = FALSE;
196         snprintf(cookie, sizeof cookie, "%s-%d", COOKIE_NAME, request->config->httpdPort);
197         uuid = NULL; //MHD_lookup_connection_value (request->connection, MHD_COOKIE_KIND, cookie);
198     };
199
200     // Warning when no cookie defined MHD_lookup_connection_value may return something !!!
201     if ((uuid != NULL) && (strnlen (uuid, 10) >= 10))   {
202         // search if client context exist and it not timeout let's use it
203         clientCtx = ctxStoreSearch (uuid);
204
205         if (clientCtx) {
206             if (ctxStoreTooOld (clientCtx, NOW)) {
207                  // this session is too old let's delete it
208                 ctxStoreDel (clientCtx);
209                 clientCtx = NULL;
210             } else {
211                 return clientCtx;
212             }
213         }
214     }
215
216     // we have no session let's create one otherwise let's clean any exiting values
217     if (clientCtx == NULL) {
218         clientCtx = calloc(1, sizeof(AFB_clientCtx)); // init NULL clientContext
219         clientCtx->contexts = calloc ((unsigned)sessions.apicount, sizeof (void*));
220     }
221
222     uuid_generate(newuuid);         // create a new UUID
223     uuid_unparse_lower(newuuid, clientCtx->uuid);
224
225     // if table is full at 50% let's clean it up
226     if(sessions.count > (sessions.max / 2)) ctxStoreGarbage();
227
228     // finally add uuid into hashtable
229     if (AFB_SUCCESS != ctxStoreAdd (clientCtx)) {
230         free (clientCtx);
231         return NULL;
232     }
233     return clientCtx;
234 }
235
236 // Sample Generic Ping Debug API
237 AFB_error ctxTokenCheck (AFB_clientCtx *clientCtx, AFB_request *request)
238 {
239     const char *token;
240
241     if (clientCtx->contexts == NULL)
242         return AFB_EMPTY;
243
244     // this time have to extract token from query list
245     token = NULL; //MHD_lookup_connection_value(request->connection, MHD_GET_ARGUMENT_KIND, key_token);
246
247     // if not token is providing we refuse the exchange
248     if ((token == NULL) || (clientCtx->token == NULL))
249         return AFB_FALSE;
250
251     // compare current token with previous one
252     if ((0 == strcmp (token, clientCtx->token)) && (!ctxStoreTooOld (clientCtx, NOW))) {
253        return AFB_SUCCESS;
254     }
255
256     // Token is not valid let move level of assurance to zero and free attached client handle
257     return AFB_FAIL;
258 }
259
260 // Free Client Session Context
261 AFB_error ctxTokenReset (AFB_clientCtx *clientCtx, AFB_request *request)
262 {
263     if (clientCtx == NULL)
264        return AFB_EMPTY;
265     //if (verbose) fprintf (stderr, "ctxClientReset New uuid=[%s] token=[%s] timestamp=%d\n", clientCtx->uuid, clientCtx->token, clientCtx->timeStamp);
266
267     // Search for an existing client with the same UUID
268     clientCtx = ctxStoreSearch (clientCtx->uuid);
269     if (clientCtx == NULL)
270        return AFB_FALSE;
271
272     // Remove client from table
273     ctxStoreDel (clientCtx);
274
275     return AFB_SUCCESS;
276 }
277
278 // generate a new token
279 AFB_error ctxTokenCreate (AFB_clientCtx *clientCtx, AFB_request *request)
280 {
281     uuid_t newuuid;
282     const char *token;
283
284     if (clientCtx == NULL)
285        return AFB_EMPTY;
286
287     // if config->token!="" then verify that we have the right initial share secret
288     if (request->config->token[0] != '\0') {
289
290         // check for initial token secret and return if not presented
291         token = NULL; //MHD_lookup_connection_value(request->connection, MHD_GET_ARGUMENT_KIND, key_token);
292         if (token == NULL)
293            return AFB_UNAUTH;
294
295         // verify that it fits with initial tokens fit
296         if (strcmp(request->config->token, token))
297            return AFB_UNAUTH;
298     }
299
300     // create a UUID as token value
301     uuid_generate(newuuid);
302     uuid_unparse_lower(newuuid, clientCtx->token);
303
304     // keep track of time for session timeout and further clean up
305     clientCtx->timeStamp = time(NULL) + sessions.timeout;
306
307     // Token is also store in context but it might be convenient for plugin to access it directly
308     return AFB_SUCCESS;
309 }
310
311
312 // generate a new token and update client context
313 AFB_error ctxTokenRefresh (AFB_clientCtx *clientCtx, AFB_request *request)
314 {
315     uuid_t newuuid;
316
317     if (clientCtx == NULL)
318         return AFB_EMPTY;
319
320     // Check if the old token is valid
321     if (ctxTokenCheck (clientCtx, request) != AFB_SUCCESS)
322         return AFB_FAIL;
323
324     // Old token was valid let's regenerate a new one
325     uuid_generate(newuuid);         // create a new UUID
326     uuid_unparse_lower(newuuid, clientCtx->token);
327
328     // keep track of time for session timeout and further clean up
329     clientCtx->timeStamp = time(NULL) + sessions.timeout;
330
331     return AFB_SUCCESS;
332 }
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405 // This function will return exiting client context or newly created client context
406 AFB_clientCtx *_ctxClientGet (const char *uuid)
407 {
408         uuid_t newuuid;
409         AFB_clientCtx *clientCtx;
410
411         /* search for an existing one not too old */
412         clientCtx = uuid != NULL ? ctxStoreSearch (uuid) : NULL;
413         if (clientCtx) {
414             if (!ctxStoreTooOld (clientCtx, NOW))
415                 return clientCtx;
416             ctxStoreDel (clientCtx);
417         }
418
419         /* mimic old behaviour */
420         if (sessions.initok == NULL)
421                 return NULL;
422
423         /* cleanup before creating */
424         if(2 * sessions.count >= sessions.max)
425                 ctxStoreGarbage();
426
427         /* returns a new one */
428         clientCtx = calloc(1, sizeof(AFB_clientCtx)); // init NULL clientContext
429         if (clientCtx != NULL) {
430                 clientCtx->contexts = calloc ((unsigned)sessions.apicount, sizeof (void*));
431                 if (clientCtx->contexts != NULL) {
432                         /* generate the uuid */
433                         uuid_generate(newuuid);
434                         uuid_unparse_lower(newuuid, clientCtx->uuid);
435                         clientCtx->timeStamp = time(NULL) + sessions.timeout;
436                         strcpy(clientCtx->token, sessions.initok);
437                         if (AFB_SUCCESS == ctxStoreAdd (clientCtx))
438                                 return clientCtx;
439                         free(clientCtx->contexts);
440                 }
441                 free(clientCtx);
442         }
443         return NULL;
444 }
445
446 // Free Client Session Context
447 AFB_error _ctxClientDel (AFB_clientCtx *clientCtx)
448 {
449         assert(clientCtx != NULL);
450         return ctxStoreDel (clientCtx);
451 }
452
453 // Sample Generic Ping Debug API
454 AFB_error _ctxTokenCheck (AFB_clientCtx *clientCtx, const char *token)
455 {
456         assert(clientCtx != NULL);
457         assert(token != NULL);
458
459         // compare current token with previous one
460         if (ctxStoreTooOld (clientCtx, NOW))
461                 return AFB_FAIL;
462         if (!clientCtx->token[0] || 0 == strcmp (token, clientCtx->token)) {
463                 clientCtx->timeStamp = time(NULL) + sessions.timeout;
464                 return AFB_SUCCESS;
465         }
466
467         // Token is not valid let move level of assurance to zero and free attached client handle
468         return AFB_FAIL;
469 }
470
471 // generate a new token and update client context
472 AFB_error _ctxTokenNew (AFB_clientCtx *clientCtx)
473 {
474         uuid_t newuuid;
475
476         assert(clientCtx != NULL);
477
478         // Old token was valid let's regenerate a new one
479         uuid_generate(newuuid);         // create a new UUID
480         uuid_unparse_lower(newuuid, clientCtx->token);
481
482         // keep track of time for session timeout and further clean up
483         clientCtx->timeStamp = time(NULL) + sessions.timeout;
484
485         return AFB_SUCCESS;
486 }
487