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