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