afb-session: Refactor and test unit
[src/app-framework-binder.git] / src / afb-session.c
1 /*
2  * Copyright (C) 2015, 2016, 2017 "IoT.bzh"
3  * Author "Fulup Ar Foll"
4  * Author: José Bollo <jose.bollo@iot.bzh>
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *   http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18
19 #define _GNU_SOURCE
20 #include <stdio.h>
21 #include <time.h>
22 #include <pthread.h>
23 #include <stdlib.h>
24 #include <stdint.h>
25 #include <string.h>
26 #include <uuid/uuid.h>
27 #include <errno.h>
28
29 #include <json-c/json.h>
30
31 #include "afb-session.h"
32 #include "verbose.h"
33
34 #define SIZEUUID        37
35 #define HEADCOUNT       16
36 #define COOKIECOUNT     8
37 #define COOKIEMASK      (COOKIECOUNT - 1)
38
39 #define _MAXEXP_        ((time_t)(~(time_t)0))
40 #define _MAXEXP2_       ((time_t)((((unsigned long long)_MAXEXP_) >> 1)))
41 #define MAX_EXPIRATION  (_MAXEXP_ >= 0 ? _MAXEXP_ : _MAXEXP2_)
42 #define NOW             (time(NULL))
43
44 /* structure for a cookie added to sessions */
45 struct cookie
46 {
47         struct cookie *next;    /* link to next cookie */
48         const void *key;        /* pointer key */
49         void *value;            /* value */
50         void (*freecb)(void*);  /* function to call when session is closed */
51 };
52
53 /*
54  * structure for session
55  */
56 struct afb_session
57 {
58         struct afb_session *next; /* link to the next */
59         unsigned refcount;      /* external reference count of the session */
60         int timeout;            /* timeout of the session */
61         time_t expiration;      /* expiration time of the token */
62         pthread_mutex_t mutex;  /* mutex of the session */
63         struct cookie *cookies[COOKIECOUNT]; /* cookies of the session */
64         uint8_t closed: 1;      /* is the session closed ? */
65         uint8_t autoclose: 1;   /* close the session when unreferenced */
66         uint8_t notinset: 1;    /* session removed from the set of sessions */
67         char uuid[SIZEUUID];    /* long term authentication of remote client */
68         char token[SIZEUUID];   /* short term authentication of remote client */
69 };
70
71 /* Session UUID are store in a simple array [for 10 sessions this should be enough] */
72 static struct {
73         int count;              /* current number of sessions */
74         int max;                /* maximum count of sessions */
75         int timeout;            /* common initial timeout */
76         struct afb_session *heads[HEADCOUNT]; /* sessions */
77         char initok[SIZEUUID];  /* common initial token */
78         pthread_mutex_t mutex;  /* declare a mutex to protect hash table */
79 } sessions = {
80         .count = 0,
81         .max = 10,
82         .timeout = 3600,
83         .heads = { 0 },
84         .initok = { 0 },
85         .mutex = PTHREAD_MUTEX_INITIALIZER
86 };
87
88 /* generate a new fresh 'uuid' */
89 static void new_uuid(char uuid[SIZEUUID])
90 {
91         uuid_t newuuid;
92         uuid_generate(newuuid);
93         uuid_unparse_lower(newuuid, uuid);
94 }
95
96 /*
97  * Returns a tiny hash value for the 'text'.
98  *
99  * Tiny hash function inspired from pearson
100  */
101 static uint8_t pearson4(const char *text)
102 {
103         static uint8_t T[16] = {
104                  4,  1,  6,  0,  9, 14, 11,  5,
105                  2,  3, 12, 15, 10,  7,  8, 13
106         };
107         uint8_t r, c;
108
109         for (r = 0; (c = (uint8_t)*text) ; text++) {
110                 r = T[r ^ (15 & c)];
111                 r = T[r ^ (c >> 4)];
112         }
113         return r; // % HEADCOUNT;
114 }
115
116 /* lock the set of sessions for exclusive access */
117 static inline void sessionset_lock()
118 {
119         pthread_mutex_lock(&sessions.mutex);
120 }
121
122 /* unlock the set of sessions of exclusive access */
123 static inline void sessionset_unlock()
124 {
125         pthread_mutex_unlock(&sessions.mutex);
126 }
127
128 /*
129  * search within the set of sessions the session of 'uuid'.
130  * 'hashidx' is the precomputed hash for 'uuid'
131  * return the session or NULL
132  */
133 static struct afb_session *sessionset_search(const char *uuid, uint8_t hashidx)
134 {
135         struct afb_session *session;
136
137         session = sessions.heads[hashidx];
138         while (session && strcmp(uuid, session->uuid))
139                 session = session->next;
140
141         return session;
142 }
143
144 /* add 'session' to the set of sessions */
145 static int sessionset_add(struct afb_session *session, uint8_t hashidx)
146 {
147         /* check availability */
148         if (sessions.count >= sessions.max) {
149                 errno = EBUSY;
150                 return -1;
151         }
152
153         /* add the session */
154         session->next = sessions.heads[hashidx];
155         sessions.heads[hashidx] = session;
156         sessions.count++;
157         return 0;
158 }
159
160 /* make a new uuid not used in the set of sessions */
161 static uint8_t sessionset_make_uuid (char uuid[SIZEUUID])
162 {
163         uint8_t hashidx;
164
165         do {
166                 new_uuid(uuid);
167                 hashidx = pearson4(uuid);
168         } while(sessionset_search(uuid, hashidx));
169         return hashidx;
170 }
171
172 /* lock the 'session' for exclusive access */
173 static inline void session_lock(struct afb_session *session)
174 {
175         pthread_mutex_lock(&session->mutex);
176 }
177
178 /* unlock the 'session' of exclusive access */
179 static inline void session_unlock(struct afb_session *session)
180 {
181         pthread_mutex_unlock(&session->mutex);
182 }
183
184 /* close the 'session' */
185 static void session_close(struct afb_session *session)
186 {
187         int idx;
188         struct cookie *cookie;
189
190         /* close only one time */
191         if (!session->closed) {
192                 session->closed = 1;
193
194                 /* free cookies */
195                 for (idx = 0 ; idx < COOKIECOUNT ; idx++) {
196                         while ((cookie = session->cookies[idx])) {
197                                 session->cookies[idx] = cookie->next;
198                                 if (cookie->freecb != NULL)
199                                         cookie->freecb(cookie->value);
200                                 free(cookie);
201                         }
202                 }
203         }
204 }
205
206 /* destroy the 'session' */
207 static void session_destroy (struct afb_session *session)
208 {
209         pthread_mutex_destroy(&session->mutex);
210         free(session);
211 }
212
213 /* update expiration of 'session' according to 'now' */
214 static void session_update_expiration(struct afb_session *session, time_t now)
215 {
216         int timeout;
217         time_t expiration;
218
219         /* compute expiration */
220         timeout = session->timeout;
221         if (timeout == AFB_SESSION_TIMEOUT_INFINITE)
222                 expiration = MAX_EXPIRATION;
223         else {
224                 if (timeout == AFB_SESSION_TIMEOUT_DEFAULT)
225                         expiration = now + sessions.timeout;
226                 else
227                         expiration = now + timeout;
228                 if (expiration < 0)
229                         expiration = MAX_EXPIRATION;
230         }
231
232         /* record the expiration */
233         session->expiration = expiration;
234 }
235
236 /*
237  * Add a new session with the 'uuid' (of 'hashidx')
238  * and the 'timeout' starting from 'now'.
239  * Add it to the set of sessions
240  * Return the created session
241  */
242 static struct afb_session *session_add(const char *uuid, int timeout, time_t now, uint8_t hashidx)
243 {
244         struct afb_session *session;
245
246         /* check arguments */
247         if (!AFB_SESSION_TIMEOUT_IS_VALID(timeout)
248          || (uuid && strlen(uuid) >= sizeof session->uuid)) {
249                 errno = EINVAL;
250                 return NULL;
251         }
252
253         /* allocates a new one */
254         session = calloc(1, sizeof *session);
255         if (session == NULL) {
256                 errno = ENOMEM;
257                 return NULL;
258         }
259
260         /* initialize */
261         pthread_mutex_init(&session->mutex, NULL);
262         session->refcount = 1;
263         strcpy(session->uuid, uuid);
264         strcpy(session->token, sessions.initok);
265         session->timeout = timeout;
266         session_update_expiration(session, now);
267
268         /* add */
269         if (sessionset_add(session, hashidx)) {
270                 free(session);
271                 return NULL;
272         }
273
274         return session;
275 }
276
277 /* Remove expired sessions and return current time (now) */
278 static time_t sessionset_cleanup (int force)
279 {
280         struct afb_session *session, **prv;
281         int idx;
282         time_t now;
283
284         /* Loop on Sessions Table and remove anything that is older than timeout */
285         now = NOW;
286         for (idx = 0 ; idx < HEADCOUNT; idx++) {
287                 prv = &sessions.heads[idx];
288                 while ((session = *prv)) {
289                         session_lock(session);
290                         if (force || session->expiration < now)
291                                 session_close(session);
292                         if (!session->closed)
293                                 prv = &session->next;
294                         else {
295                                 *prv = session->next;
296                                 sessions.count--;
297                                 session->notinset = 1;
298                                 if ( !session->refcount) {
299                                         session_destroy(session);
300                                         continue;
301                                 }
302                         }
303                         session_unlock(session);
304                 }
305         }
306         return now;
307 }
308
309 /**
310  * Initialize the session manager with a 'max_session_count',
311  * an initial common 'timeout' and an initial common token 'initok'.
312  *
313  * @param max_session_count  maximum allowed session count in the same time
314  * @param timeout            the initial default timeout of sessions
315  * @param initok             the initial default token of sessions
316  * 
317  */
318 int afb_session_init (int max_session_count, int timeout, const char *initok)
319 {
320         /* check parameters */
321         if (initok && strlen(initok) >= sizeof sessions.initok) {
322                 ERROR("initial token '%s' too long (max length %d)",
323                         initok, ((int)(sizeof sessions.initok)) - 1);
324                 errno = EINVAL;
325                 return -1;
326         }
327
328         /* init the sessionset (after cleanup) */
329         sessionset_lock();
330         sessionset_cleanup(1);
331         sessions.max = max_session_count;
332         sessions.timeout = timeout;
333         if (initok == NULL)
334                 new_uuid(sessions.initok);
335         else
336                 strcpy(sessions.initok, initok);
337         sessionset_unlock();
338         return 0;
339 }
340
341 /**
342  * Cleanup the sessionset of its closed or expired sessions
343  */
344 void afb_session_purge()
345 {
346         sessionset_lock();
347         sessionset_cleanup(0);
348         sessionset_unlock();
349 }
350
351 /**
352  * @return the initial token set at initialization
353  */
354 const char *afb_session_initial_token()
355 {
356         return sessions.initok;
357 }
358
359 /* Searchs the session of 'uuid' */
360 struct afb_session *afb_session_search (const char *uuid)
361 {
362         struct afb_session *session;
363
364         sessionset_lock();
365         sessionset_cleanup(0);
366         session = sessionset_search(uuid, pearson4(uuid));
367         session = afb_session_addref(session);
368         sessionset_unlock();
369         return session;
370
371 }
372
373 /**
374  * Creates a new session with 'timeout'
375  */
376 struct afb_session *afb_session_create (int timeout)
377 {
378         return afb_session_get(NULL, timeout, NULL);
379 }
380
381 /* This function will return exiting session or newly created session */
382 struct afb_session *afb_session_get (const char *uuid, int timeout, int *created)
383 {
384         char _uuid_[SIZEUUID];
385         uint8_t hashidx;
386         struct afb_session *session;
387         time_t now;
388         int c;
389
390         /* cleaning */
391         sessionset_lock();
392         now = sessionset_cleanup(0);
393
394         /* search for an existing one not too old */
395         if (!uuid) {
396                 hashidx = sessionset_make_uuid(_uuid_);
397                 uuid = _uuid_;
398         } else {
399                 hashidx = pearson4(uuid);
400                 session = sessionset_search(uuid, hashidx);
401                 if (session) {
402                         /* session found */
403                         afb_session_addref(session);
404                         c = 0;
405                         goto end;
406                 }
407         }
408         /* create the session */
409         session = session_add(uuid, timeout, now, hashidx);
410         c = 1;
411 end:
412         sessionset_unlock();
413         if (created)
414                 *created = c;
415
416         return session;
417 }
418
419 /* increase the use count on 'session' (can be NULL) */
420 struct afb_session *afb_session_addref(struct afb_session *session)
421 {
422         if (session != NULL)
423                 __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
424         return session;
425 }
426
427 /* decrease the use count of 'session' (can be NULL) */
428 void afb_session_unref(struct afb_session *session)
429 {
430         if (session == NULL)
431                 return;
432
433         session_lock(session);
434         if (!__atomic_sub_fetch(&session->refcount, 1, __ATOMIC_RELAXED)) {
435                 if (session->autoclose)
436                         session_close(session);
437                 if (session->notinset) {
438                         session_destroy(session);
439                         return;
440                 }
441         }
442         session_unlock(session);
443 }
444
445 /* close 'session' */
446 void afb_session_close (struct afb_session *session)
447 {
448         session_lock(session);
449         session_close(session);
450         session_unlock(session);
451 }
452
453 /**
454  * Set the 'autoclose' flag of the 'session'
455  *
456  * A session whose autoclose flag is true will close as
457  * soon as it is no more referenced. 
458  *
459  * @param session    the session to set
460  * @param autoclose  the value to set
461  */
462 void afb_session_set_autoclose(struct afb_session *session, int autoclose)
463 {
464         session->autoclose = !!autoclose;
465 }
466
467 /* is 'session' closed? */
468 int afb_session_is_closed (struct afb_session *session)
469 {
470         return session->closed;
471 }
472
473 /*
474  * check whether the token of 'session' is 'token'
475  * return 1 if true or 0 otherwise
476  */
477 int afb_session_check_token (struct afb_session *session, const char *token)
478 {
479         int r;
480
481         session_unlock(session);
482         r = !session->closed
483           && session->expiration >= NOW
484           && !(session->token[0] && strcmp (token, session->token));
485         session_unlock(session);
486         return r;
487 }
488
489 /* generate a new token and update client context */
490 void afb_session_new_token (struct afb_session *session)
491 {
492         /* Old token was valid let's regenerate a new one */
493         new_uuid(session->token);
494
495         /* keep track of time for session timeout and further clean up */
496         session_update_expiration(session, NOW);
497 }
498
499 /* Returns the uuid of 'session' */
500 const char *afb_session_uuid (struct afb_session *session)
501 {
502         return session->uuid;
503 }
504
505 /* Returns the token of 'session' */
506 const char *afb_session_token (struct afb_session *session)
507 {
508         return session->token;
509 }
510
511 /**
512  * Get the index of the 'key' in the cookies array.
513  * @param key the key to scan
514  * @return the index of the list for key within cookies
515  */
516 static int cookeyidx(const void *key)
517 {
518         intptr_t x = (intptr_t)key;
519         unsigned r = (unsigned)((x >> 5) ^ (x >> 15));
520         return r & COOKIEMASK;
521 }
522
523 /**
524  * Set, get, replace, remove a cookie of 'key' for the 'session'
525  *
526  * The behaviour of this function depends on its parameters:
527  *
528  * @param session       the session
529  * @param key           the key of the cookie
530  * @param makecb        the creation function or NULL
531  * @param freecb        the release function or NULL
532  * @param closure       an argument for makecb or the value if makecb==NULL
533  * @param replace       a boolean enforcing replecement of the previous value
534  *
535  * @return the value of the cookie
536  *
537  * The 'key' is a pointer and compared as pointers.
538  *
539  * For getting the current value of the cookie:
540  *
541  *   afb_session_cookie(session, key, NULL, NULL, NULL, 0)
542  *
543  * For storing the value of the cookie
544  *
545  *   afb_session_cookie(session, key, NULL, NULL, value, 1)
546  */
547 void *afb_session_cookie(struct afb_session *session, const void *key, void *(*makecb)(void *closure), void (*freecb)(void *item), void *closure, int replace)
548 {
549         int idx;
550         void *value;
551         struct cookie *cookie, **prv;
552
553         /* get key hashed index */
554         idx = cookeyidx(key);
555
556         /* lock session and search for the cookie of 'key' */
557         session_lock(session);
558         prv = &session->cookies[idx];
559         for (;;) {
560                 cookie = *prv;
561                 if (!cookie) {
562                         /* 'key' not found, create value using 'closure' and 'makecb' */
563                         value = makecb ? makecb(closure) : closure;
564                         /* store the the only if it has some meaning */
565                         if (replace || makecb || freecb) {
566                                 cookie = malloc(sizeof *cookie);
567                                 if (!cookie) {
568                                         errno = ENOMEM;
569                                         /* calling freecb if there is no makecb may have issue */
570                                         if (makecb && freecb)
571                                                 freecb(value);
572                                         value = NULL;
573                                 } else {
574                                         cookie->key = key;
575                                         cookie->value = value;
576                                         cookie->freecb = freecb;
577                                         cookie->next = NULL;
578                                         *prv = cookie;
579                                 }
580                         }
581                         break;
582                 } else if (cookie->key == key) {
583                         /* cookie of key found */
584                         if (!replace)
585                                 /* not replacing, get the value */
586                                 value = cookie->value;
587                         else {
588                                 /* create value using 'closure' and 'makecb' */
589                                 value = makecb ? makecb(closure) : closure;
590
591                                 /* free previous value is needed */
592                                 if (cookie->value != value && cookie->freecb)
593                                         cookie->freecb(cookie->value);
594
595                                 /* if both value and freecb are NULL drop the cookie */
596                                 if (!value && !freecb) {
597                                         *prv = cookie->next;
598                                         free(cookie);
599                                 } else {
600                                         /* store the value and its releaser */
601                                         cookie->value = value;
602                                         cookie->freecb = freecb;
603                                 }
604                         }
605                         break;
606                 } else {
607                         prv = &(cookie->next);
608                 }
609         }
610
611         /* unlock the session and return the value */
612         session_unlock(session);
613         return value;
614 }
615
616 /**
617  * Get the cookie of 'key' in the 'session'.
618  *
619  * @param session  the session to search in
620  * @param key      the key of the data to retrieve
621  *
622  * @return the data staored for the key or NULL if the key isn't found
623  */
624 void *afb_session_get_cookie(struct afb_session *session, const void *key)
625 {
626         return afb_session_cookie(session, key, NULL, NULL, NULL, 0);
627 }
628
629 /**
630  * Set the cookie of 'key' in the 'session' to the 'value' that can be
631  * cleaned using 'freecb' (if not null).
632  *
633  * @param session  the session to set
634  * @param key      the key of the data to store
635  * @param value    the value to store at key
636  * @param freecb   a function to use when the cookie value is to remove (or null)
637  *
638  * @return the data staored for the key or NULL if the key isn't found
639  * 
640  */
641 int afb_session_set_cookie(struct afb_session *session, const void *key, void *value, void (*freecb)(void*))
642 {
643         return -(value != afb_session_cookie(session, key, NULL, freecb, value, 1));
644 }
645