Added Session Management
[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  * https://github.com/json-c/json-c/blob/master/linkhash.c
20  * https://github.com/json-c/json-c/blob/master/linkhash.h
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
31 #define AFB_SESSION_JTYPE "AFB_session"
32 #define AFB_SESSION_JLIST "AFB_sessions"
33 #define AFB_SESSION_JINFO "AFB_infos"
34
35
36 #define AFB_CURRENT_SESSION "active-session"  // file link name within sndcard dir
37 #define AFB_DEFAULT_SESSION "current-session" // should be in sync with UI
38
39
40 static struct lh_table *clientCtxs=NULL;    // let's use JsonObject Hashtable to Store Sessions
41
42
43 // verify we can read/write in session dir
44 PUBLIC AFB_error sessionCheckdir (AFB_session *session) {
45
46    int err;
47
48    // in case session dir would not exist create one
49    if (verbose) fprintf (stderr, "AFB:notice checking session dir [%s]\n", session->config->sessiondir);
50    mkdir(session->config->sessiondir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
51
52    // change for session directory
53    err = chdir(session->config->sessiondir);
54    if (err) {
55      fprintf(stderr,"AFB: Fail to chdir to %s error=%s\n", session->config->sessiondir, strerror(err));
56      return err;
57    }
58
59    // verify we can write session in directory
60    json_object *dummy= json_object_new_object();
61    json_object_object_add (dummy, "checked"  , json_object_new_int (getppid()));
62    err = json_object_to_file ("./AFB-probe.json", dummy);
63    if (err < 0) return err;
64
65    return AFB_SUCCESS;
66 }
67
68 // let's return only sessions files
69 STATIC int fileSelect (const struct dirent *entry) {
70    return (strstr (entry->d_name, ".afb") != NULL);
71 }
72
73 STATIC  json_object *checkCardDirExit (AFB_session *session, AFB_request *request ) {
74     int  sessionDir, cardDir;
75
76     // card name should be more than 3 character long !!!!
77     if (strlen (request->plugin) < 3) {
78        return (jsonNewMessage (AFB_FAIL,"Fail invalid plugin=%s", request->plugin));
79     }
80
81     // open session directory
82     sessionDir = open (session->config->sessiondir, O_DIRECTORY);
83     if (sessionDir < 0) {
84           return (jsonNewMessage (AFB_FAIL,"Fail to open directory [%s] error=%s", session->config->sessiondir, strerror(sessionDir)));
85     }
86
87    // create session sndcard directory if it does not exit
88     cardDir = openat (sessionDir, request->plugin,  O_DIRECTORY);
89     if (cardDir < 0) {
90           cardDir  = mkdirat (sessionDir, request->plugin, O_RDWR | S_IRWXU | S_IRGRP);
91           if (cardDir < 0) {
92               return (jsonNewMessage (AFB_FAIL,"Fail to create directory [%s/%s] error=%s", session->config->sessiondir, request->plugin, strerror(cardDir)));
93           }
94     }
95     close (sessionDir);
96     return NULL;
97 }
98
99 // create a session in current directory
100 PUBLIC json_object *sessionList (AFB_session *session, AFB_request *request) {
101     json_object *sessionsJ, *ajgResponse;
102     struct stat fstat;
103     struct dirent **namelist;
104     int  count, sessionDir;
105
106     // if directory for card's sessions does not exist create it
107     ajgResponse = checkCardDirExit (session, request);
108     if (ajgResponse != NULL) return ajgResponse;
109
110     // open session directory
111     sessionDir = open (session->config->sessiondir, O_DIRECTORY);
112     if (sessionDir < 0) {
113           return (jsonNewMessage (AFB_FAIL,"Fail to open directory [%s] error=%s", session->config->sessiondir, strerror(sessionDir)));
114     }
115
116     count = scandirat (sessionDir, request->plugin, &namelist, fileSelect, alphasort);
117     close (sessionDir);
118
119     if (count < 0) {
120         return (jsonNewMessage (AFB_FAIL,"Fail to scan sessions directory [%s/%s] error=%s", session->config->sessiondir, request->plugin, strerror(sessionDir)));
121     }
122     if (count == 0) return (jsonNewMessage (AFB_EMPTY,"[%s] no session at [%s]", request->plugin, session->config->sessiondir));
123
124     // loop on each session file, retrieve its date and push it into json response object
125     sessionsJ = json_object_new_array();
126     while (count--) {
127          json_object *sessioninfo;
128          char timestamp [64];
129          char *filename;
130
131          // extract file name and last modification date
132          filename = namelist[count]->d_name;
133          printf("%s\n", filename);
134          stat(filename,&fstat);
135          strftime (timestamp, sizeof(timestamp), "%c", localtime (&fstat.st_mtime));
136          filename[strlen(filename)-4] = '\0'; // remove .afb extension from filename
137
138          // create an object by session with last update date
139          sessioninfo = json_object_new_object();
140          json_object_object_add (sessioninfo, "date" , json_object_new_string (timestamp));
141          json_object_object_add (sessioninfo, "session" , json_object_new_string (filename));
142          json_object_array_add (sessionsJ, sessioninfo);
143
144          free(namelist[count]);
145     }
146
147     // free scandir structure
148     free(namelist);
149
150     // everything is OK let's build final response
151     ajgResponse = json_object_new_object();
152     json_object_object_add (ajgResponse, "jtype" , json_object_new_string (AFB_SESSION_JLIST));
153     json_object_object_add (ajgResponse, "status"  , jsonNewStatus(AFB_SUCCESS));
154     json_object_object_add (ajgResponse, "data"    , sessionsJ);
155
156     return (ajgResponse);
157 }
158
159 // Create a link toward last used sessionname within sndcard directory
160 STATIC void makeSessionLink (const char *cardname, const char *sessionname) {
161    char linkname [256], filename [256];
162    int err;
163    // create a link to keep track of last uploaded sessionname for this card
164    strncpy (filename, sessionname, sizeof(filename));
165    strncat (filename, ".afb", sizeof(filename));
166
167    strncpy (linkname, cardname, sizeof(linkname));
168    strncat (linkname, "/", sizeof(filename));
169    strncat (linkname, AFB_CURRENT_SESSION, sizeof(linkname));
170    strncat (linkname, ".afb", sizeof(filename));
171    unlink (linkname); // remove previous link if any
172    err = symlink (filename, linkname);
173    if (err < 0) fprintf (stderr, "Fail to create link %s->%s error=%s\n", linkname, filename, strerror(errno));
174 }
175
176 // Load Json session object from disk
177 PUBLIC json_object *sessionFromDisk (AFB_session *session, AFB_request *request, char *name) {
178     json_object *jsonSession, *jtype, *response;
179     const char *ajglabel;
180     char filename [256];
181     int defsession;
182
183     if (name == NULL) {
184         return  (jsonNewMessage (AFB_FATAL,"session name missing &session=MySessionName"));
185     }
186
187     // check for current session request
188     defsession = (strcmp (name, AFB_DEFAULT_SESSION) ==0);
189
190     // if directory for card's sessions does not exist create it
191     response = checkCardDirExit (session, request);
192     if (response != NULL) return response;
193
194     // add name and file extension to session name
195     strncpy (filename, request->plugin, sizeof(filename));
196     strncat (filename, "/", sizeof(filename));
197     if (defsession) strncat (filename, AFB_CURRENT_SESSION, sizeof(filename)-1);
198     else strncat (filename, name, sizeof(filename)-1);
199     strncat (filename, ".afb", sizeof(filename));
200
201     // just upload json object and return without any further processing
202     jsonSession = json_object_from_file (filename);
203
204     if (jsonSession == NULL)  return (jsonNewMessage (AFB_EMPTY,"File [%s] not found", filename));
205
206     // verify that file is a JSON ALSA session type
207     if (!json_object_object_get_ex (jsonSession, "jtype", &jtype)) {
208         json_object_put   (jsonSession);
209         return  (jsonNewMessage (AFB_EMPTY,"File [%s] 'jtype' descriptor not found", filename));
210     }
211
212     // check type value is AFB_SESSION_JTYPE
213     ajglabel = json_object_get_string (jtype);
214     if (strcmp (AFB_SESSION_JTYPE, ajglabel)) {
215        json_object_put   (jsonSession);
216        return  (jsonNewMessage (AFB_FATAL,"File [%s] jtype=[%s] != [%s]", filename, ajglabel, AFB_SESSION_JTYPE));
217     }
218
219     // create a link to keep track of last uploaded session for this card
220     if (!defsession) makeSessionLink (request->plugin, name);
221
222     return (jsonSession);
223 }
224
225 // push Json session object to disk
226 PUBLIC json_object * sessionToDisk (AFB_session *session, AFB_request *request, char *name, json_object *jsonSession) {
227    char filename [256];
228    time_t rawtime;
229    struct tm * timeinfo;
230    int err, defsession;
231    static json_object *response;
232
233    // we should have a session name
234    if (name == NULL) return (jsonNewMessage (AFB_FATAL,"session name missing &session=MySessionName"));
235
236    // check for current session request
237    defsession = (strcmp (name, AFB_DEFAULT_SESSION) ==0);
238
239    // if directory for card's sessions does not exist create it
240    response = checkCardDirExit (session, request);
241    if (response != NULL) return response;
242
243    // add cardname and file extension to session name
244    strncpy (filename, request->plugin, sizeof(filename));
245    strncat (filename, "/", sizeof(filename));
246    if (defsession) strncat (filename, AFB_CURRENT_SESSION, sizeof(filename)-1);
247    else strncat (filename, name, sizeof(filename)-1);
248    strncat (filename, ".afb", sizeof(filename)-1);
249
250
251    json_object_object_add(jsonSession, "jtype", json_object_new_string (AFB_SESSION_JTYPE));
252
253    // add a timestamp and store session on disk
254    time ( &rawtime );  timeinfo = localtime ( &rawtime );
255    // A copy of the string is made and the memory is managed by the json_object
256    json_object_object_add (jsonSession, "timestamp", json_object_new_string (asctime (timeinfo)));
257
258
259    // do we have extra session info ?
260    if (request->post) {
261        static json_object *info, *jtype;
262        const char  *ajglabel;
263
264        // extract session info from args
265        info = json_tokener_parse (request->post);
266        if (!info) {
267             response = jsonNewMessage (AFB_FATAL,"sndcard=%s session=%s invalid json args=%s", request->plugin, name, request->post);
268             goto OnErrorExit;
269        }
270
271        // info is a valid AFB_info type
272        if (!json_object_object_get_ex (info, "jtype", &jtype)) {
273             response = jsonNewMessage (AFB_EMPTY,"sndcard=%s session=%s No 'AFB_type' args=%s", request->plugin, name, request->post);
274             goto OnErrorExit;
275        }
276
277        // check type value is AFB_INFO_JTYPE
278        ajglabel = json_object_get_string (jtype);
279        if (strcmp (AFB_SESSION_JINFO, ajglabel)) {
280               json_object_put   (info); // release info json object
281               response = jsonNewMessage (AFB_FATAL,"File [%s] jtype=[%s] != [%s] data=%s", filename, ajglabel, AFB_SESSION_JTYPE, request->post);
282               goto OnErrorExit;
283        }
284
285        // this is valid info data for our session
286        json_object_object_add (jsonSession, "info", info);
287    }
288
289    // Finally save session on disk
290    err = json_object_to_file (filename, jsonSession);
291    if (err < 0) {
292         response = jsonNewMessage (AFB_FATAL,"Fail save session = [%s] to disk", filename);
293         goto OnErrorExit;
294    }
295
296
297    // create a link to keep track of last uploaded session for this card
298    if (!defsession) makeSessionLink (request->plugin, name);
299
300    // we're donne let's return status message
301    response = jsonNewMessage (AFB_SUCCESS,"Session= [%s] saved on disk", filename);
302    json_object_put (jsonSession);
303    return (response);
304
305 OnErrorExit:
306    json_object_put (jsonSession);
307    return response;
308 }
309
310
311 // Function to handle Cookies and Client session context it relies on json low level
312 // linked list functionalities https://github.com/json-c/json-c/blob/master/linkhash.c
313
314 // Hash client UUID before storing in table
315 STATIC unsigned long ctxUuidHashCB (const void *k1) {
316     unsigned long hash;
317     
318     AFB_clientCtx *ctx = (AFB_clientCtx*) k1;
319     hash = lh_char_hash(ctx->uuid);
320     return (hash);    
321 }
322
323 // Compare client UUIDs within table
324 STATIC int ctxUuidCompCB (const void *k1, const void *k2) {
325     int res;    
326     AFB_clientCtx *ctx1 = (AFB_clientCtx*) k1;
327     AFB_clientCtx *ctx2 = (AFB_clientCtx*) k2;
328     
329     res = lh_char_equal(ctx1->uuid, ctx2->uuid);
330     return (res);    
331 }
332
333 // Free context [XXXX Should be protected again memory abort XXXX]
334 STATIC void ctxUuidFreeCB (struct lh_entry *entry) {
335     AFB_clientCtx *ctx = (AFB_clientCtx*) entry->v;
336
337     // If application add a handle let's free it now
338     if (ctx->handle != NULL) {
339         
340         // Free client handle with a standard Free function, with app callback or ignore it
341         if (ctx->freeHandleCB == NULL) free (ctx->handle); 
342         else if (ctx->freeHandleCB != (void*)-1) ctx->freeHandleCB(ctx->handle); 
343     }
344     free ((void*)entry->v);
345 }
346
347 // Create a new store in RAM, not that is too small it will be automatically extended
348 STATIC struct lh_table *ctxStoreCreate (int nbSession) {
349    lh_table *table; 
350     
351    // function will exit process in case of error !!! 
352    table=lh_table_new (nbSession, "CtxClient", ctxUuidFreeCB, ctxUuidHashCB, ctxUuidCompCB);
353    return (table);
354 }
355
356 // Check if context timeout or not
357 STATIC int ctxStoreToOld (const void *k1, int timeout) {
358     int res;    
359     AFB_clientCtx *ctx = (AFB_clientCtx*) k1;
360
361     res = ((ctx->timeStamp + timeout) < time(NULL));
362     return (res);    
363 }
364
365 // Loop on every entry and remove old context sessions
366 PUBLIC int ctxStoreGarbage (struct lh_table *lht, const int timeout) {
367     struct lh_entry *c;
368     
369     // Loop on every entry within table
370     for(c = lht->head; c != NULL; c = c->next) {
371         if(lht->free_fn) {
372             if(c->k == LH_EMPTY) return lht->count;
373             if(c->k != LH_FREED &&  ctxStoreToOld(c->v, timeout)) lh_table_delete_entry (lht, c);
374         }
375     }
376   
377     // return current size after cleanup
378     return (lht->count);
379 }
380
381 // This function will return exiting client context or newly created client context
382 PUBLIC int ctxClientGet (AFB_request *request) {
383   static int cid=0;
384   AFB_clientCtx *clientCtx=NULL;
385   const char *uuid;
386   uuid_t newuuid;
387   int ret;
388   
389    // if client session store is null create it
390    if (clientCtxs == NULL) {
391        clientCtxs= ctxStoreCreate(CTX_NBCLIENTS);
392    }
393
394     // Check if client as a context or not inside the URL
395     uuid  = MHD_lookup_connection_value(request->connection, MHD_GET_ARGUMENT_KIND, "uuid");
396        
397     // if UUID in query we're restfull with no cookies otherwise check for cookie
398     if (uuid != NULL) request->restfull = TRUE;
399     else {
400         request->restfull = FALSE;
401         uuid = MHD_lookup_connection_value (request->connection, MHD_COOKIE_KIND, COOKIE_NAME);  
402     };
403     
404     
405     if (uuid != NULL)   {
406         // search if client context exist and it not timeout let's use it
407         if ((lh_table_lookup_ex (clientCtxs, uuid, (void**) &clientCtx)) 
408                 && ! ctxStoreToOld (clientCtx, request->config->cntxTimeout)) {
409                 request->client=clientCtx;
410                 if (verbose) fprintf (stderr, "ctxClientGet Old uuid=[%s] token=[%s] timestamp=%d\n"
411                              ,request->client->uuid, request->client->token, request->client->timeStamp);
412                 return;            
413         }
414     }
415
416     
417     // we have no session let's create one otherwise let's clean any exiting values
418     if (clientCtx == NULL) clientCtx = calloc(1, sizeof(AFB_clientCtx)); // init NULL clientContext
419     uuid_generate(newuuid);         // create a new UUID
420     uuid_unparse_lower(newuuid, clientCtx->uuid);
421     clientCtx->cid=cid++;
422         
423     // if table is full at 50% let's clean it up
424     if(clientCtxs->count > (clientCtxs->size*0.5)) ctxStoreGarbage(clientCtxs, request->config->cntxTimeout);
425     
426     // finally add uuid into hashtable
427     ret= lh_table_insert (clientCtxs, (void*)clientCtx->uuid, clientCtx);
428     
429     if (verbose) fprintf (stderr, "ctxClientGet New uuid=[%s] token=[%s] timestamp=%d\n", clientCtx->uuid, clientCtx->token, clientCtx->timeStamp);
430        
431     request->client = clientCtx;
432     return (ret);
433 }
434
435 // Sample Generic Ping Debug API
436 PUBLIC AFB_error ctxTokenCheck (AFB_request *request) {
437     const char *token;
438     
439     // this time have to extract token from query list
440     token = MHD_lookup_connection_value(request->connection, MHD_GET_ARGUMENT_KIND, "token");
441     
442     // if not token is providing we refuse the exchange
443     if ((token == NULL) || (request->client->token == NULL)) return (AFB_FALSE);
444     
445     // compare current token with previous one
446     if ((0 == strcmp (token, request->client->token)) && (!ctxStoreToOld (request->client, request->config->cntxTimeout))) {
447        return (AFB_TRUE);
448     }
449     
450     // Token is not valid let move level of assurance to zero and free attached client handle
451     return (AFB_FALSE);
452 }
453
454 // Free Client Session Context
455 PUBLIC int ctxTokenReset (AFB_request *request) {
456     struct lh_entry* entry;
457     int ret;
458         
459     entry = lh_table_lookup_entry (clientCtxs, request->client->uuid);
460     if (entry == NULL) return FALSE;
461     
462     lh_table_delete_entry (clientCtxs, entry);
463  
464     return (TRUE);
465 }
466
467 // generate a new token
468 PUBLIC char* ctxTokenCreate (AFB_request *request) {
469     int oldTnkValid;
470     const char *ornew;
471     uuid_t newuuid;
472
473     // create a UUID as token value
474     uuid_generate(newuuid); 
475     uuid_unparse_lower(newuuid, request->client->token);
476     
477     // keep track of time for session timeout and further clean up
478     request->client->timeStamp=time(NULL); 
479     
480     // Token is also store in context but it might be convenient for plugin to access it directly
481     return (request->client->token);
482 }
483
484
485 // generate a new token and update client context
486 PUBLIC char* ctxTokenRefresh (AFB_request *request) {
487     int oldTnkValid;
488     const char *oldornew;
489     uuid_t newuuid;
490     
491     // Check if the old token is valid
492     oldTnkValid= ctxTokenCheck (request);
493     
494     // if token is not valid let check for query argument "oldornew"
495     if (!oldTnkValid) {
496         oldornew = MHD_lookup_connection_value(request->connection, MHD_GET_ARGUMENT_KIND, "oldornew");
497         if (oldornew != NULL) oldTnkValid= TRUE;
498     }
499    
500     // No existing token and no request to create one
501     if (oldTnkValid != TRUE) return NULL;
502
503     return (ctxTokenCreate (request));
504 }
505