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