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