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