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