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