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