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