Update copyright dates
[src/app-framework-binder.git] / src / afb-session.c
1 /*
2  * Copyright (C) 2015-2020 "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 <limits.h>
26 #include <string.h>
27 #include <errno.h>
28
29 #include "afb-session.h"
30 #include "afb-hook.h"
31 #include "verbose.h"
32 #include "pearson.h"
33 #include "uuid.h"
34
35 #define SESSION_COUNT_MIN 5
36 #define SESSION_COUNT_MAX 1000
37
38 /*
39  * Handling of  cookies.
40  * Cookies are stored by session.
41  * The cookie count COOKIECOUNT must be a power of 2, possible values: 1, 2, 4, 8, 16, 32, 64, ...
42  * For low memory profile, small values are better, 1 is possible.
43  */
44 #define COOKIECOUNT     8
45 #define COOKIEMASK      (COOKIECOUNT - 1)
46
47 #define _MAXEXP_        ((time_t)(~(time_t)0))
48 #define _MAXEXP2_       ((time_t)((((unsigned long long)_MAXEXP_) >> 1)))
49 #define MAX_EXPIRATION  (_MAXEXP_ >= 0 ? _MAXEXP_ : _MAXEXP2_)
50 #define NOW             (time_now())
51
52 /**
53  * structure for a cookie added to sessions
54  */
55 struct cookie
56 {
57         struct cookie *next;    /**< link to next cookie */
58         const void *key;        /**< pointer key */
59         void *value;            /**< value */
60         void (*freecb)(void*);  /**< function to call when session is closed */
61 };
62
63 /**
64  * structure for session
65  */
66 struct afb_session
67 {
68         struct afb_session *next; /**< link to the next */
69         uint16_t refcount;      /**< count of reference to the session */
70         uint16_t id;            /**< local id of the session */
71         int timeout;            /**< timeout of the session */
72         time_t expiration;      /**< expiration time of the session */
73         pthread_mutex_t mutex;  /**< mutex of the session */
74         struct cookie *cookies[COOKIECOUNT]; /**< cookies of the session */
75         char *lang;             /**< current language setting for the session */
76         uint8_t closed: 1;      /**< is the session closed ? */
77         uint8_t autoclose: 1;   /**< close the session when unreferenced */
78         uint8_t notinset: 1;    /**< session removed from the set of sessions */
79         uint8_t hash;           /**< hash value of the uuid */
80         uuid_stringz_t uuid;    /**< identification of client session */
81 };
82
83 /**
84  * structure for managing sessions
85  */
86 static struct {
87         uint16_t count;         /**< current number of sessions */
88         uint16_t max;           /**< maximum count of sessions */
89         uint16_t genid;         /**< for generating ids */
90         int timeout;            /**< common initial timeout */
91         struct afb_session *first; /**< sessions */
92         pthread_mutex_t mutex;  /**< declare a mutex to protect hash table */
93 } sessions = {
94         .count = 0,
95         .max = 10,
96         .genid = 1,
97         .timeout = 3600,
98         .first = 0,
99         .mutex = PTHREAD_MUTEX_INITIALIZER
100 };
101
102 /**
103  * Get the actual raw time
104  */
105 static inline time_t time_now()
106 {
107         struct timespec ts;
108         clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
109         return ts.tv_sec;
110 }
111
112 /* lock the set of sessions for exclusive access */
113 static inline void sessionset_lock()
114 {
115         pthread_mutex_lock(&sessions.mutex);
116 }
117
118 /* unlock the set of sessions of exclusive access */
119 static inline void sessionset_unlock()
120 {
121         pthread_mutex_unlock(&sessions.mutex);
122 }
123
124 /*
125  * search within the set of sessions the session of 'uuid'.
126  * 'hashidx' is the precomputed hash for 'uuid'
127  * return the session or NULL
128  */
129 static struct afb_session *sessionset_search(const char *uuid, uint8_t hashidx)
130 {
131         struct afb_session *session;
132
133         session = sessions.first;
134         while (session && hashidx != session->hash && strcmp(uuid, session->uuid))
135                 session = session->next;
136
137         return session;
138 }
139
140 /*
141  * search within the set of sessions the session of 'id'.
142  * return the session or NULL
143  */
144 static struct afb_session *sessionset_search_id(uint16_t id)
145 {
146         struct afb_session *session;
147
148         session = sessions.first;
149         while (session && id != session->id)
150                 session = session->next;
151
152         return session;
153 }
154
155 /* add 'session' to the set of sessions */
156 static int sessionset_add(struct afb_session *session, uint8_t hashidx)
157 {
158         /* check availability */
159         if (sessions.max && sessions.count >= sessions.max) {
160                 errno = EBUSY;
161                 return -1;
162         }
163
164         /* add the session */
165         session->next = sessions.first;
166         sessions.first = session;
167         sessions.count++;
168         return 0;
169 }
170
171 /* make a new uuid not used in the set of sessions */
172 static uint8_t sessionset_make_uuid (uuid_stringz_t uuid)
173 {
174         uint8_t hashidx;
175
176         do {
177                 uuid_new_stringz(uuid);
178                 hashidx = pearson4(uuid);
179         } while(sessionset_search(uuid, hashidx));
180         return hashidx;
181 }
182
183 /* lock the 'session' for exclusive access */
184 static inline void session_lock(struct afb_session *session)
185 {
186         pthread_mutex_lock(&session->mutex);
187 }
188
189 /* unlock the 'session' of exclusive access */
190 static inline void session_unlock(struct afb_session *session)
191 {
192         pthread_mutex_unlock(&session->mutex);
193 }
194
195 /* close the 'session' */
196 static void session_close(struct afb_session *session)
197 {
198         int idx;
199         struct cookie *cookie;
200
201         /* close only one time */
202         if (!session->closed) {
203                 /* close it now */
204                 session->closed = 1;
205
206 #if WITH_AFB_HOOK
207                 /* emit the hook */
208                 afb_hook_session_close(session);
209 #endif
210
211                 /* release cookies */
212                 for (idx = 0 ; idx < COOKIECOUNT ; idx++) {
213                         while ((cookie = session->cookies[idx])) {
214                                 session->cookies[idx] = cookie->next;
215                                 if (cookie->freecb != NULL)
216                                         cookie->freecb(cookie->value);
217                                 free(cookie);
218                         }
219                 }
220         }
221 }
222
223 /* destroy the 'session' */
224 static void session_destroy (struct afb_session *session)
225 {
226 #if WITH_AFB_HOOK
227         afb_hook_session_destroy(session);
228 #endif
229         pthread_mutex_destroy(&session->mutex);
230         free(session->lang);
231         free(session);
232 }
233
234 /* update expiration of 'session' according to 'now' */
235 static void session_update_expiration(struct afb_session *session, time_t now)
236 {
237         time_t expiration;
238
239         /* compute expiration */
240         expiration = now + afb_session_timeout(session);
241         if (expiration < 0)
242                 expiration = MAX_EXPIRATION;
243
244         /* record the expiration */
245         session->expiration = expiration;
246 }
247
248 /*
249  * Add a new session with the 'uuid' (of 'hashidx')
250  * and the 'timeout' starting from 'now'.
251  * Add it to the set of sessions
252  * Return the created session
253  */
254 static struct afb_session *session_add(const char *uuid, int timeout, time_t now, uint8_t hashidx)
255 {
256         struct afb_session *session;
257
258         /* check arguments */
259         if (!AFB_SESSION_TIMEOUT_IS_VALID(timeout)
260          || (uuid && strlen(uuid) >= sizeof session->uuid)) {
261                 errno = EINVAL;
262                 return NULL;
263         }
264
265         /* allocates a new one */
266         session = calloc(1, sizeof *session);
267         if (session == NULL) {
268                 errno = ENOMEM;
269                 return NULL;
270         }
271
272         /* initialize */
273         pthread_mutex_init(&session->mutex, NULL);
274         session->refcount = 1;
275         strcpy(session->uuid, uuid);
276         session->timeout = timeout;
277         session_update_expiration(session, now);
278         session->id = ++sessions.genid;
279         while (session->id == 0 || sessionset_search_id(session->id) != NULL)
280                 session->id = ++sessions.genid;
281
282         /* add */
283         if (sessionset_add(session, hashidx)) {
284                 free(session);
285                 return NULL;
286         }
287
288 #if WITH_AFB_HOOK
289         afb_hook_session_create(session);
290 #endif
291
292         return session;
293 }
294
295 /* Remove expired sessions and return current time (now) */
296 static time_t sessionset_cleanup (int force)
297 {
298         struct afb_session *session, **prv;
299         time_t now;
300
301         /* Loop on Sessions Table and remove anything that is older than timeout */
302         now = NOW;
303         prv = &sessions.first;
304         while ((session = *prv)) {
305                 session_lock(session);
306                 if (force || session->expiration < now)
307                         session_close(session);
308                 if (!session->closed) {
309                         prv = &session->next;
310                         session_unlock(session);
311                 } else {
312                         *prv = session->next;
313                         sessions.count--;
314                         session->notinset = 1;
315                         if (session->refcount)
316                                 session_unlock(session);
317                         else
318                                 session_destroy(session);
319                 }
320         }
321         return now;
322 }
323
324 /**
325  * Initialize the session manager with a 'max_session_count',
326  * an initial common 'timeout'
327  *
328  * @param max_session_count  maximum allowed session count in the same time
329  * @param timeout            the initial default timeout of sessions
330  */
331 int afb_session_init (int max_session_count, int timeout)
332 {
333         /* init the sessionset (after cleanup) */
334         sessionset_lock();
335         sessionset_cleanup(1);
336         if (max_session_count > SESSION_COUNT_MAX)
337                 sessions.max = SESSION_COUNT_MAX;
338         else if (max_session_count < SESSION_COUNT_MIN)
339                 sessions.max = SESSION_COUNT_MIN;
340         else
341                 sessions.max = (uint16_t)max_session_count;
342         sessions.timeout = timeout;
343         sessionset_unlock();
344         return 0;
345 }
346
347 /**
348  * Iterate the sessions and call 'callback' with
349  * the 'closure' for each session.
350  */
351 void afb_session_foreach(void (*callback)(void *closure, struct afb_session *session), void *closure)
352 {
353         struct afb_session *session;
354
355         /* Loop on Sessions Table and remove anything that is older than timeout */
356         sessionset_lock();
357         session = sessions.first;
358         while (session) {
359                 if (!session->closed)
360                         callback(closure, session);
361                 session = session->next;
362         }
363         sessionset_unlock();
364 }
365
366 /**
367  * Cleanup the sessionset of its closed or expired sessions
368  */
369 void afb_session_purge()
370 {
371         sessionset_lock();
372         sessionset_cleanup(0);
373         sessionset_unlock();
374 }
375
376 /* Searchs the session of 'uuid' */
377 struct afb_session *afb_session_search (const char *uuid)
378 {
379         struct afb_session *session;
380
381         sessionset_lock();
382         sessionset_cleanup(0);
383         session = sessionset_search(uuid, pearson4(uuid));
384         session = afb_session_addref(session);
385         sessionset_unlock();
386         return session;
387
388 }
389
390 /**
391  * Creates a new session with 'timeout'
392  */
393 struct afb_session *afb_session_create (int timeout)
394 {
395         return afb_session_get(NULL, timeout, NULL);
396 }
397
398 /**
399  * Returns the timeout of 'session' in seconds
400  */
401 int afb_session_timeout(struct afb_session *session)
402 {
403         int timeout;
404
405         /* compute timeout */
406         timeout = session->timeout;
407         if (timeout == AFB_SESSION_TIMEOUT_DEFAULT)
408                 timeout = sessions.timeout;
409         if (timeout < 0)
410                 timeout = INT_MAX;
411         return timeout;
412 }
413
414 /**
415  * Returns the second remaining before expiration of 'session'
416  */
417 int afb_session_what_remains(struct afb_session *session)
418 {
419         int diff = (int)(session->expiration - NOW);
420         return diff < 0 ? 0 : diff;
421 }
422
423 /* This function will return exiting session or newly created session */
424 struct afb_session *afb_session_get (const char *uuid, int timeout, int *created)
425 {
426         uuid_stringz_t _uuid_;
427         uint8_t hashidx;
428         struct afb_session *session;
429         time_t now;
430         int c;
431
432         /* cleaning */
433         sessionset_lock();
434         now = sessionset_cleanup(0);
435
436         /* search for an existing one not too old */
437         if (!uuid) {
438                 hashidx = sessionset_make_uuid(_uuid_);
439                 uuid = _uuid_;
440         } else {
441                 hashidx = pearson4(uuid);
442                 session = sessionset_search(uuid, hashidx);
443                 if (session) {
444                         /* session found */
445                         afb_session_addref(session);
446                         c = 0;
447                         goto end;
448                 }
449         }
450         /* create the session */
451         session = session_add(uuid, timeout, now, hashidx);
452         c = 1;
453 end:
454         sessionset_unlock();
455         if (created)
456                 *created = c;
457
458         return session;
459 }
460
461 /* increase the use count on 'session' (can be NULL) */
462 struct afb_session *afb_session_addref(struct afb_session *session)
463 {
464         if (session != NULL) {
465 #if WITH_AFB_HOOK
466                 afb_hook_session_addref(session);
467 #endif
468                 session_lock(session);
469                 session->refcount++;
470                 session_unlock(session);
471         }
472         return session;
473 }
474
475 /* decrease the use count of 'session' (can be NULL) */
476 void afb_session_unref(struct afb_session *session)
477 {
478         if (session == NULL)
479                 return;
480
481 #if WITH_AFB_HOOK
482         afb_hook_session_unref(session);
483 #endif
484         session_lock(session);
485         if (!--session->refcount) {
486                 if (session->autoclose)
487                         session_close(session);
488                 if (session->notinset) {
489                         session_destroy(session);
490                         return;
491                 }
492         }
493         session_unlock(session);
494 }
495
496 /* close 'session' */
497 void afb_session_close (struct afb_session *session)
498 {
499         session_lock(session);
500         session_close(session);
501         session_unlock(session);
502 }
503
504 /**
505  * Set the 'autoclose' flag of the 'session'
506  *
507  * A session whose autoclose flag is true will close as
508  * soon as it is no more referenced.
509  *
510  * @param session    the session to set
511  * @param autoclose  the value to set
512  */
513 void afb_session_set_autoclose(struct afb_session *session, int autoclose)
514 {
515         session->autoclose = !!autoclose;
516 }
517
518 /* is 'session' closed? */
519 int afb_session_is_closed (struct afb_session *session)
520 {
521         return session->closed;
522 }
523
524 /* Returns the uuid of 'session' */
525 const char *afb_session_uuid (struct afb_session *session)
526 {
527         return session->uuid;
528 }
529
530 /* Returns the local id of 'session' */
531 uint16_t afb_session_id (struct afb_session *session)
532 {
533         return session->id;
534 }
535
536 /**
537  * Get the index of the 'key' in the cookies array.
538  * @param key the key to scan
539  * @return the index of the list for key within cookies
540  */
541 #if COOKIEMASK
542 static int cookeyidx(const void *key)
543 {
544         intptr_t x = (intptr_t)key;
545         unsigned r = (unsigned)((x >> 5) ^ (x >> 15));
546         return r & COOKIEMASK;
547 }
548 #else
549 #  define cookeyidx(key) 0
550 #endif
551
552 /**
553  * Set, get, replace, remove a cookie of 'key' for the 'session'
554  *
555  * The behaviour of this function depends on its parameters:
556  *
557  * @param session       the session
558  * @param key           the key of the cookie
559  * @param makecb        the creation function or NULL
560  * @param freecb        the release function or NULL
561  * @param closure       an argument for makecb or the value if makecb==NULL
562  * @param replace       a boolean enforcing replacement of the previous value
563  *
564  * @return the value of the cookie
565  *
566  * The 'key' is a pointer and compared as pointers.
567  *
568  * For getting the current value of the cookie:
569  *
570  *   afb_session_cookie(session, key, NULL, NULL, NULL, 0)
571  *
572  * For storing the value of the cookie
573  *
574  *   afb_session_cookie(session, key, NULL, NULL, value, 1)
575  */
576 void *afb_session_cookie(struct afb_session *session, const void *key, void *(*makecb)(void *closure), void (*freecb)(void *item), void *closure, int replace)
577 {
578         int idx;
579         void *value;
580         struct cookie *cookie, **prv;
581
582         /* get key hashed index */
583         idx = cookeyidx(key);
584
585         /* lock session and search for the cookie of 'key' */
586         session_lock(session);
587         prv = &session->cookies[idx];
588         for (;;) {
589                 cookie = *prv;
590                 if (!cookie) {
591                         /* 'key' not found, create value using 'closure' and 'makecb' */
592                         value = makecb ? makecb(closure) : closure;
593                         /* store the the only if it has some meaning */
594                         if (replace || makecb || freecb) {
595                                 cookie = malloc(sizeof *cookie);
596                                 if (!cookie) {
597                                         errno = ENOMEM;
598                                         /* calling freecb if there is no makecb may have issue */
599                                         if (makecb && freecb)
600                                                 freecb(value);
601                                         value = NULL;
602                                 } else {
603                                         cookie->key = key;
604                                         cookie->value = value;
605                                         cookie->freecb = freecb;
606                                         cookie->next = NULL;
607                                         *prv = cookie;
608                                 }
609                         }
610                         break;
611                 } else if (cookie->key == key) {
612                         /* cookie of key found */
613                         if (!replace)
614                                 /* not replacing, get the value */
615                                 value = cookie->value;
616                         else {
617                                 /* create value using 'closure' and 'makecb' */
618                                 value = makecb ? makecb(closure) : closure;
619
620                                 /* free previous value is needed */
621                                 if (cookie->value != value && cookie->freecb)
622                                         cookie->freecb(cookie->value);
623
624                                 /* if both value and freecb are NULL drop the cookie */
625                                 if (!value && !freecb) {
626                                         *prv = cookie->next;
627                                         free(cookie);
628                                 } else {
629                                         /* store the value and its releaser */
630                                         cookie->value = value;
631                                         cookie->freecb = freecb;
632                                 }
633                         }
634                         break;
635                 } else {
636                         prv = &(cookie->next);
637                 }
638         }
639
640         /* unlock the session and return the value */
641         session_unlock(session);
642         return value;
643 }
644
645 /**
646  * Get the cookie of 'key' in the 'session'.
647  *
648  * @param session  the session to search in
649  * @param key      the key of the data to retrieve
650  *
651  * @return the data staored for the key or NULL if the key isn't found
652  */
653 void *afb_session_get_cookie(struct afb_session *session, const void *key)
654 {
655         return afb_session_cookie(session, key, NULL, NULL, NULL, 0);
656 }
657
658 /**
659  * Set the cookie of 'key' in the 'session' to the 'value' that can be
660  * cleaned using 'freecb' (if not null).
661  *
662  * @param session  the session to set
663  * @param key      the key of the data to store
664  * @param value    the value to store at key
665  * @param freecb   a function to use when the cookie value is to remove (or null)
666  *
667  * @return 0 in case of success or -1 in case of error
668  */
669 int afb_session_set_cookie(struct afb_session *session, const void *key, void *value, void (*freecb)(void*))
670 {
671         return -(value != afb_session_cookie(session, key, NULL, freecb, value, 1));
672 }
673
674 /**
675  * Set the language attached to the session
676  *
677  * @param session the session to set
678  * @param lang    the language specifiction to set to session
679  *
680  * @return 0 in case of success or -1 in case of error
681  */
682 int afb_session_set_language(struct afb_session *session, const char *lang)
683 {
684         char *oldl, *newl;
685
686         newl = strdup(lang);
687         if (newl == NULL)
688                 return -1;
689
690         oldl = session->lang;
691         session->lang = newl;
692         free(oldl);
693         return 0;
694 }
695
696 /**
697  * Get the language attached to the session
698  *
699  * @param session the session to query
700  * @param lang    a default language specifiction
701  *
702  * @return the langauage specification to use for session
703  */
704 const char *afb_session_get_language(struct afb_session *session, const char *lang)
705 {
706         return session->lang ?: lang;
707 }