afb-session: Return an initialization status
[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 <string.h>
25 #include <uuid/uuid.h>
26 #include <assert.h>
27 #include <errno.h>
28
29 #include <json-c/json.h>
30
31 #include "afb-session.h"
32 #include "verbose.h"
33
34 #define SIZEUUID        37
35 #define HEADCOUNT       16
36 #define COOKIECOUNT     8
37 #define COOKIEMASK      (COOKIECOUNT - 1)
38
39 #define _MAXEXP_        ((time_t)(~(time_t)0))
40 #define _MAXEXP2_       ((time_t)((((unsigned long long)_MAXEXP_) >> 1)))
41 #define MAX_EXPIRATION  (_MAXEXP_ >= 0 ? _MAXEXP_ : _MAXEXP2_)
42 #define NOW             (time(NULL))
43
44 struct cookie
45 {
46         struct cookie *next;
47         const void *key;
48         void *value;
49         void (*freecb)(void*);
50 };
51
52 struct afb_session
53 {
54         struct afb_session *next; /* link to the next */
55         unsigned refcount;
56         int timeout;
57         time_t expiration;      // expiration time of the token
58         pthread_mutex_t mutex;
59         struct cookie *cookies[COOKIECOUNT];
60         char autoclose;
61         char idx;
62         char uuid[SIZEUUID];    // long term authentication of remote client
63         char token[SIZEUUID];   // short term authentication of remote client
64 };
65
66 // Session UUID are store in a simple array [for 10 sessions this should be enough]
67 static struct {
68         pthread_mutex_t mutex;  // declare a mutex to protect hash table
69         struct afb_session *heads[HEADCOUNT]; // sessions
70         int count;      // current number of sessions
71         int max;
72         int timeout;
73         char initok[SIZEUUID];
74 } sessions;
75
76 /* generate a uuid */
77 static void new_uuid(char uuid[SIZEUUID])
78 {
79         uuid_t newuuid;
80         uuid_generate(newuuid);
81         uuid_unparse_lower(newuuid, uuid);
82 }
83
84 static inline void lock(struct afb_session *session)
85 {
86         pthread_mutex_lock(&session->mutex);
87 }
88
89 static inline void unlock(struct afb_session *session)
90 {
91         pthread_mutex_unlock(&session->mutex);
92 }
93
94 // Free context [XXXX Should be protected again memory abort XXXX]
95 static void close_session(struct afb_session *session)
96 {
97         int idx;
98         struct cookie *cookie;
99
100         /* free cookies */
101         for (idx = 0 ; idx < COOKIECOUNT ; idx++) {
102                 while ((cookie = session->cookies[idx])) {
103                         session->cookies[idx] = cookie->next;
104                         if (cookie->freecb != NULL)
105                                 cookie->freecb(cookie->value);
106                         free(cookie);
107                 }
108         }
109 }
110
111 /* tiny hash function inspired from pearson */
112 static int pearson4(const char *text)
113 {
114         static uint8_t T[16] = {
115                  4,  1,  6,  0,  9, 14, 11,  5,
116                  2,  3, 12, 15, 10,  7,  8, 13
117         };
118         uint8_t r, c;
119
120         for (r = 0; (c = (uint8_t)*text) ; text++) {
121                 r = T[r ^ (15 & c)];
122                 r = T[r ^ (c >> 4)];
123         }
124         return r; // % HEADCOUNT;
125 }
126
127 /**
128  * Initialize the session manager with a 'max_session_count',
129  * an initial common 'timeout' and an initial common token 'initok'.
130  *
131  * @param max_session_count  maximum allowed session count in the same time
132  * @param timeout            the initial default timeout of sessions
133  * @param initok             the initial default token of sessions
134  * 
135  */
136 int afb_session_init (int max_session_count, int timeout, const char *initok)
137 {
138         pthread_mutex_init(&sessions.mutex, NULL);
139         sessions.max = max_session_count;
140         sessions.timeout = timeout;
141         if (initok == NULL)
142                 /* without token, a secret is made to forbid creation of sessions */
143                 new_uuid(sessions.initok);
144         else if (strlen(initok) < sizeof sessions.initok)
145                 strcpy(sessions.initok, initok);
146         else {
147                 ERROR("initial token '%s' too long (max length %d)", initok, ((int)(sizeof sessions.initok)) - 1);
148                 errno = EINVAL;
149                 return -1;
150         }
151         return 0;
152 }
153
154 const char *afb_session_initial_token()
155 {
156         return sessions.initok;
157 }
158
159 static struct afb_session *search (const char* uuid, int idx)
160 {
161         struct afb_session *session;
162
163         session = sessions.heads[idx];
164         while (session && strcmp(uuid, session->uuid))
165                 session = session->next;
166
167         return session;
168 }
169
170 static void destroy (struct afb_session *session)
171 {
172         struct afb_session **prv;
173
174         assert (session != NULL);
175
176         close_session(session);
177         pthread_mutex_lock(&sessions.mutex);
178         prv = &sessions.heads[(int)session->idx];
179         while (*prv)
180                 if (*prv != session)
181                         prv = &((*prv)->next);
182                 else {
183                         *prv = session->next;
184                         sessions.count--;
185                         pthread_mutex_destroy(&session->mutex);
186                         free(session);
187                         break;
188                 }
189         pthread_mutex_unlock(&sessions.mutex);
190 }
191
192 // Loop on every entry and remove old context sessions.hash
193 static time_t cleanup ()
194 {
195         struct afb_session *session, *next;
196         int idx;
197         time_t now;
198
199         // Loop on Sessions Table and remove anything that is older than timeout
200         now = NOW;
201         for (idx = 0 ; idx < HEADCOUNT; idx++) {
202                 session = sessions.heads[idx];
203                 while (session) {
204                         next = session->next;
205                         if (session->expiration < now)
206                                 afb_session_close(session);
207                         session = next;
208                 }
209         }
210         return now;
211 }
212
213 static void update_timeout(struct afb_session *session, time_t now, int timeout)
214 {
215         time_t expiration;
216
217         /* compute expiration */
218         if (timeout == AFB_SESSION_TIMEOUT_INFINITE)
219                 expiration = MAX_EXPIRATION;
220         else {
221                 if (timeout == AFB_SESSION_TIMEOUT_DEFAULT)
222                         expiration = now + sessions.timeout;
223                 else
224                         expiration = now + timeout;
225                 if (expiration < 0)
226                         expiration = MAX_EXPIRATION;
227         }
228
229         /* record the values */
230         session->timeout = timeout;
231         session->expiration = expiration;
232 }
233
234 static void update_expiration(struct afb_session *session, time_t now)
235 {
236         update_timeout(session, now, session->timeout);
237 }
238
239 static struct afb_session *add_session (const char *uuid, int timeout, time_t now, int idx)
240 {
241         struct afb_session *session;
242
243         /* check arguments */
244         if (!AFB_SESSION_TIMEOUT_IS_VALID(timeout)
245          || (uuid && strlen(uuid) >= sizeof session->uuid)) {
246                 errno = EINVAL;
247                 return NULL;
248         }
249
250         /* check session count */
251         if (sessions.count >= sessions.max) {
252                 errno = EBUSY;
253                 return NULL;
254         }
255
256         /* allocates a new one */
257         session = calloc(1, sizeof *session);
258         if (session == NULL) {
259                 errno = ENOMEM;
260                 return NULL;
261         }
262
263         /* initialize */
264         pthread_mutex_init(&session->mutex, NULL);
265         session->refcount = 1;
266         strcpy(session->uuid, uuid);
267         strcpy(session->token, sessions.initok);
268         update_timeout(session, now, timeout);
269
270         /* link */
271         session->idx = (char)idx;
272         session->next = sessions.heads[idx];
273         sessions.heads[idx] = session;
274         sessions.count++;
275
276         return session;
277 }
278
279 /* create a new session for the given timeout */
280 static struct afb_session *new_session (int timeout, time_t now)
281 {
282         int idx;
283         char uuid[SIZEUUID];
284
285         do {
286                 new_uuid(uuid);
287                 idx = pearson4(uuid);
288         } while(search(uuid, idx));
289         return add_session(uuid, timeout, now, idx);
290 }
291
292 /* Creates a new session with 'timeout' */
293 struct afb_session *afb_session_create (int timeout)
294 {
295         time_t now;
296         struct afb_session *session;
297
298         /* cleaning */
299         pthread_mutex_lock(&sessions.mutex);
300         now = cleanup();
301         session = new_session(timeout, now);
302         pthread_mutex_unlock(&sessions.mutex);
303
304         return session;
305 }
306
307 /* Searchs the session of 'uuid' */
308 struct afb_session *afb_session_search (const char *uuid)
309 {
310         struct afb_session *session;
311
312         /* cleaning */
313         pthread_mutex_lock(&sessions.mutex);
314         cleanup();
315         session = search(uuid, pearson4(uuid));
316         if (session)
317                 __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
318         pthread_mutex_unlock(&sessions.mutex);
319         return session;
320
321 }
322
323 /* This function will return exiting session or newly created session */
324 struct afb_session *afb_session_get (const char *uuid, int timeout, int *created)
325 {
326         int idx;
327         struct afb_session *session;
328         time_t now;
329
330         /* cleaning */
331         pthread_mutex_lock(&sessions.mutex);
332         now = cleanup();
333
334         /* search for an existing one not too old */
335         if (!uuid)
336                 session = new_session(timeout, now);
337         else {
338                 idx = pearson4(uuid);
339                 session = search(uuid, idx);
340                 if (session) {
341                         __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
342                         pthread_mutex_unlock(&sessions.mutex);
343                         if (created)
344                                 *created = 0;
345                         return session;
346                 }
347                 session = add_session (uuid, timeout, now, idx);
348         }
349         pthread_mutex_unlock(&sessions.mutex);
350
351         if (created)
352                 *created = !!session;
353
354         return session;
355 }
356
357 /* increase the use count on the session */
358 struct afb_session *afb_session_addref(struct afb_session *session)
359 {
360         if (session != NULL)
361                 __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
362         return session;
363 }
364
365 /* decrease the use count of the session */
366 void afb_session_unref(struct afb_session *session)
367 {
368         if (session != NULL) {
369                 assert(session->refcount != 0);
370                 if (!__atomic_sub_fetch(&session->refcount, 1, __ATOMIC_RELAXED)) {
371                         pthread_mutex_lock(&session->mutex);
372                         if (session->autoclose || session->uuid[0] == 0)
373                                 destroy (session);
374                         else
375                                 pthread_mutex_unlock(&session->mutex);
376                 }
377         }
378 }
379
380 // close Client Session Context
381 void afb_session_close (struct afb_session *session)
382 {
383         assert(session != NULL);
384         pthread_mutex_lock(&session->mutex);
385         if (session->uuid[0] != 0) {
386                 session->uuid[0] = 0;
387                 if (session->refcount)
388                         close_session(session);
389                 else {
390                         destroy (session);
391                         return;
392                 }
393         }
394         pthread_mutex_unlock(&session->mutex);
395 }
396
397 /* set the autoclose flag */
398 void afb_session_set_autoclose(struct afb_session *session, int autoclose)
399 {
400         assert(session != NULL);
401         session->autoclose = (char)!!autoclose;
402 }
403
404 // is the session active?
405 int afb_session_is_active (struct afb_session *session)
406 {
407         assert(session != NULL);
408         return !!session->uuid[0];
409 }
410
411 // is the session closed?
412 int afb_session_is_closed (struct afb_session *session)
413 {
414         assert(session != NULL);
415         return !session->uuid[0];
416 }
417
418 // Sample Generic Ping Debug API
419 int afb_session_check_token (struct afb_session *session, const char *token)
420 {
421         assert(session != NULL);
422         assert(token != NULL);
423
424         if (!session->uuid[0])
425                 return 0;
426
427         if (session->expiration < NOW)
428                 return 0;
429
430         if (session->token[0] && strcmp (token, session->token) != 0)
431                 return 0;
432
433         return 1;
434 }
435
436 // generate a new token and update client context
437 void afb_session_new_token (struct afb_session *session)
438 {
439         assert(session != NULL);
440
441         // Old token was valid let's regenerate a new one
442         new_uuid(session->token);
443
444         // keep track of time for session timeout and further clean up
445         update_expiration(session, NOW);
446 }
447
448 /* Returns the uuid of 'session' */
449 const char *afb_session_uuid (struct afb_session *session)
450 {
451         assert(session != NULL);
452         return session->uuid;
453 }
454
455 /* Returns the token of 'session' */
456 const char *afb_session_token (struct afb_session *session)
457 {
458         assert(session != NULL);
459         return session->token;
460 }
461
462 /**
463  * Get the index of the 'key' in the cookies array.
464  * @param key the key to scan
465  * @return the index of the list for key within cookies
466  */
467 static int cookeyidx(const void *key)
468 {
469         intptr_t x = (intptr_t)key;
470         unsigned r = (unsigned)((x >> 5) ^ (x >> 15));
471         return r & COOKIEMASK;
472 }
473
474 /**
475  * Set, get, replace, remove a cookie of 'key' for the 'session'
476  *
477  * The behaviour of this function depends on its parameters:
478  *
479  * @param session       the session
480  * @param key           the key of the cookie
481  * @param makecb        the creation function or NULL
482  * @param freecb        the release function or NULL
483  * @param closure       an argument for makecb or the value if makecb==NULL
484  * @param replace       a boolean enforcing replecement of the previous value
485  *
486  * @return the value of the cookie
487  *
488  * The 'key' is a pointer and compared as pointers.
489  *
490  * For getting the current value of the cookie:
491  *
492  *   afb_session_cookie(session, key, NULL, NULL, NULL, 0)
493  *
494  * For storing the value of the cookie
495  *
496  *   afb_session_cookie(session, key, NULL, NULL, value, 1)
497  */
498 void *afb_session_cookie(struct afb_session *session, const void *key, void *(*makecb)(void *closure), void (*freecb)(void *item), void *closure, int replace)
499 {
500         int idx;
501         void *value;
502         struct cookie *cookie, **prv;
503
504         /* get key hashed index */
505         idx = cookeyidx(key);
506
507         /* lock session and search for the cookie of 'key' */
508         lock(session);
509         prv = &session->cookies[idx];
510         for (;;) {
511                 cookie = *prv;
512                 if (!cookie) {
513                         /* 'key' not found, create value using 'closure' and 'makecb' */
514                         value = makecb ? makecb(closure) : closure;
515                         /* store the the only if it has some meaning */
516                         if (replace || makecb || freecb) {
517                                 cookie = malloc(sizeof *cookie);
518                                 if (!cookie) {
519                                         errno = ENOMEM;
520                                         /* calling freecb if there is no makecb may have issue */
521                                         if (makecb && freecb)
522                                                 freecb(value);
523                                         value = NULL;
524                                 } else {
525                                         cookie->key = key;
526                                         cookie->value = value;
527                                         cookie->freecb = freecb;
528                                         cookie->next = NULL;
529                                         *prv = cookie;
530                                 }
531                         }
532                         break;
533                 } else if (cookie->key == key) {
534                         /* cookie of key found */
535                         if (!replace)
536                                 /* not replacing, get the value */
537                                 value = cookie->value;
538                         else {
539                                 /* create value using 'closure' and 'makecb' */
540                                 value = makecb ? makecb(closure) : closure;
541
542                                 /* free previous value is needed */
543                                 if (cookie->value != value && cookie->freecb)
544                                         cookie->freecb(cookie->value);
545
546                                 /* store the value and its releaser */
547                                 cookie->value = value;
548                                 cookie->freecb = freecb;
549
550                                 /* but if both are NULL drop the cookie */
551                                 if (!value && !freecb) {
552                                         *prv = cookie->next;
553                                         free(cookie);
554                                 }
555                         }
556                         break;
557                 } else {
558                         prv = &(cookie->next);
559                 }
560         }
561
562         /* unlock the session and return the value */
563         unlock(session);
564         return value;
565 }
566
567 void *afb_session_get_cookie(struct afb_session *session, const void *key)
568 {
569         return afb_session_cookie(session, key, NULL, NULL, NULL, 0);
570 }
571
572 int afb_session_set_cookie(struct afb_session *session, const void *key, void *value, void (*freecb)(void*))
573 {
574         return -(value != afb_session_cookie(session, key, NULL, freecb, value, 1));
575 }
576