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