afb-session: optimization
[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         pthread_mutex_lock(&sessions.mutex);
165         prv = &sessions.heads[(int)session->idx];
166         while (*prv)
167                 if (*prv != session)
168                         prv = &((*prv)->next);
169                 else {
170                         *prv = session->next;
171                         sessions.count--;
172                         pthread_mutex_destroy(&session->mutex);
173                         free(session);
174                         break;
175                 }
176         pthread_mutex_unlock(&sessions.mutex);
177 }
178
179 // Check if context timeout or not
180 static int is_expired (struct afb_session *ctx, time_t now)
181 {
182         assert (ctx != NULL);
183         return ctx->expiration < now;
184 }
185
186 // Check if context is active or not
187 static int is_active (struct afb_session *ctx, time_t now)
188 {
189         assert (ctx != NULL);
190         return ctx->uuid[0] != 0 && ctx->expiration >= now;
191 }
192
193 // Loop on every entry and remove old context sessions.hash
194 static time_t cleanup ()
195 {
196         struct afb_session *session, *next;
197         int idx;
198         time_t now;
199
200         // Loop on Sessions Table and remove anything that is older than timeout
201         now = NOW;
202         for (idx = 0 ; idx < HEADCOUNT; idx++) {
203                 session = sessions.heads[idx];
204                 while (session) {
205                         next = session->next;
206                         if (is_expired(session, now))
207                                 afb_session_close(session);
208                         session = next;
209                 }
210         }
211         return now;
212 }
213
214 static struct afb_session *add_session (const char *uuid, int timeout, time_t now, int idx)
215 {
216         struct afb_session *session;
217
218         if (!AFB_SESSION_TIMEOUT_IS_VALID(timeout)
219          || (uuid && strlen(uuid) >= sizeof session->uuid)) {
220                 errno = EINVAL;
221                 return NULL;
222         }
223
224         if (sessions.count >= sessions.max) {
225                 errno = EBUSY;
226                 return NULL;
227         }
228
229         /* allocates a new one */
230         session = calloc(1, sizeof *session);
231         if (session == NULL) {
232                 errno = ENOMEM;
233                 return NULL;
234         }
235
236         pthread_mutex_init(&session->mutex, NULL);
237         session->refcount = 1;
238         strcpy(session->uuid, uuid);
239         strcpy(session->token, sessions.initok);
240
241         /* init timeout */
242         if (timeout == AFB_SESSION_TIMEOUT_DEFAULT)
243                 timeout = sessions.timeout;
244         session->timeout = timeout;
245         session->expiration = now + timeout;
246         if (timeout == AFB_SESSION_TIMEOUT_INFINITE || session->expiration < 0) {
247                 session->expiration = (time_t)(~(time_t)0);
248                 if (session->expiration < 0)
249                         session->expiration = (time_t)(((unsigned long long)session->expiration) >> 1);
250         }
251
252         session->idx = (char)idx;
253         session->next = sessions.heads[idx];
254         sessions.heads[idx] = session;
255         sessions.count++;
256
257         return session;
258 }
259
260 static struct afb_session *new_session (int timeout, time_t now)
261 {
262         int idx;
263         char uuid[37];
264
265         do {
266                 new_uuid(uuid);
267                 idx = pearson4(uuid);
268         } while(search(uuid, idx));
269         return add_session(uuid, timeout, now, idx);
270 }
271
272 /* Creates a new session with 'timeout' */
273 struct afb_session *afb_session_create (int timeout)
274 {
275         time_t now;
276         struct afb_session *session;
277
278         /* cleaning */
279         pthread_mutex_lock(&sessions.mutex);
280         now = cleanup();
281         session = new_session(timeout, now);
282         pthread_mutex_unlock(&sessions.mutex);
283
284         return session;
285 }
286
287 /* Searchs the session of 'uuid' */
288 struct afb_session *afb_session_search (const char *uuid)
289 {
290         struct afb_session *session;
291
292         /* cleaning */
293         pthread_mutex_lock(&sessions.mutex);
294         cleanup();
295         session = search(uuid, pearson4(uuid));
296         pthread_mutex_unlock(&sessions.mutex);
297         return session;
298
299 }
300
301 /* This function will return exiting session or newly created session */
302 struct afb_session *afb_session_get (const char *uuid, int timeout, int *created)
303 {
304         int idx;
305         struct afb_session *session;
306         time_t now;
307
308         /* cleaning */
309         pthread_mutex_lock(&sessions.mutex);
310         now = cleanup();
311
312         /* search for an existing one not too old */
313         if (!uuid)
314                 session = new_session(timeout, now);
315         else {
316                 idx = pearson4(uuid);
317                 session = search(uuid, idx);
318                 if (session != NULL) {
319                         __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
320                         pthread_mutex_unlock(&sessions.mutex);
321                         if (created)
322                                 *created = 0;
323                         return session;
324                 }
325                 session = add_session (uuid, timeout, now, idx);
326         }
327         pthread_mutex_unlock(&sessions.mutex);
328
329         if (created)
330                 *created = !!session;
331
332         return session;
333 }
334
335 /* increase the use count on the session */
336 struct afb_session *afb_session_addref(struct afb_session *session)
337 {
338         if (session != NULL)
339                 __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
340         return session;
341 }
342
343 /* decrease the use count of the session */
344 void afb_session_unref(struct afb_session *session)
345 {
346         if (session != NULL) {
347                 assert(session->refcount != 0);
348                 if (!__atomic_sub_fetch(&session->refcount, 1, __ATOMIC_RELAXED)) {
349                         if (session->uuid[0] == 0)
350                                 destroy (session);
351                 }
352         }
353 }
354
355 // Free Client Session Context
356 void afb_session_close (struct afb_session *session)
357 {
358         assert(session != NULL);
359         if (session->uuid[0] != 0) {
360                 session->uuid[0] = 0;
361                 remove_all_cookies(session);
362                 if (session->refcount == 0)
363                         destroy (session);
364         }
365 }
366
367 // Sample Generic Ping Debug API
368 int afb_session_check_token (struct afb_session *session, const char *token)
369 {
370         assert(session != NULL);
371         assert(token != NULL);
372
373         // compare current token with previous one
374         if (!is_active (session, NOW))
375                 return 0;
376
377         if (session->token[0] && strcmp (token, session->token) != 0)
378                 return 0;
379
380         return 1;
381 }
382
383 // generate a new token and update client context
384 void afb_session_new_token (struct afb_session *session)
385 {
386         assert(session != NULL);
387
388         // Old token was valid let's regenerate a new one
389         new_uuid(session->token);
390
391         // keep track of time for session timeout and further clean up
392         if (session->timeout != 0)
393                 session->expiration = NOW + session->timeout;
394 }
395
396 /* Returns the uuid of 'session' */
397 const char *afb_session_uuid (struct afb_session *session)
398 {
399         assert(session != NULL);
400         return session->uuid;
401 }
402
403 /* Returns the token of 'session' */
404 const char *afb_session_token (struct afb_session *session)
405 {
406         assert(session != NULL);
407         return session->token;
408 }
409
410 /**
411  * Get the index of the 'key' in the cookies array.
412  * @param key the key to scan
413  * @return the index of the list for key within cookies
414  */
415 static int cookeyidx(const void *key)
416 {
417         intptr_t x = (intptr_t)key;
418         unsigned r = (unsigned)((x >> 5) ^ (x >> 15));
419         return r & COOKEYMASK;
420 }
421
422 /**
423  * Set, get, replace, remove a cookie of 'key' for the 'session'
424  *
425  * The behaviour of this function depends on its parameters:
426  *
427  * @param session the session
428  * @param key     the key of the cookie
429  * @param makecb  the creation function
430  * @param freecb  the release function
431  * @param closure an argument
432  * @param replace a boolean enforcing replecement of the previous value
433  *
434  * @return the value of the cookie
435  *
436  * The 'key' is a pointer and compared as pointers.
437  *
438  * For getting the current value of the cookie:
439  *
440  *   afb_session_cookie(session, key, NULL, NULL, NULL, 0)
441  *
442  * For storing the value of the cookie
443  *
444  *   afb_session_cookie(session, key, NULL, NULL, value, 1)
445  */
446 void *afb_session_cookie(struct afb_session *session, const void *key, void *(*makecb)(void *closure), void (*freecb)(void *item), void *closure, int replace)
447 {
448         int idx;
449         void *value;
450         struct cookie *cookie, **prv;
451
452         /* get key hashed index */
453         idx = cookeyidx(key);
454
455         /* lock session and search for the cookie of 'key' */
456         lock(session);
457         prv = &session->cookies[idx];
458         for (;;) {
459                 cookie = *prv;
460                 if (!cookie) {
461                         /* 'key' not found, create value using 'closure' and 'makecb' */
462                         value = makecb ? makecb(closure) : closure;
463                         /* store the the only if it has some meaning */
464                         if (replace || makecb || freecb) {
465                                 cookie = malloc(sizeof *cookie);
466                                 if (!cookie) {
467                                         errno = ENOMEM;
468                                         /* calling freecb if there is no makecb may have issue */
469                                         if (makecb && freecb)
470                                                 freecb(value);
471                                         value = NULL;
472                                 } else {
473                                         cookie->key = key;
474                                         cookie->value = value;
475                                         cookie->freecb = freecb;
476                                         cookie->next = NULL;
477                                         *prv = cookie;
478                                 }
479                         }
480                         break;
481                 } else if (cookie->key == key) {
482                         /* cookie of key found */
483                         if (!replace)
484                                 /* not replacing, get the value */
485                                 value = cookie->value;
486                         else {
487                                 /* create value using 'closure' and 'makecb' */
488                                 value = makecb ? makecb(closure) : closure;
489
490                                 /* free previous value is needed */
491                                 if (cookie->value != value && cookie->freecb)
492                                         cookie->freecb(cookie->value);
493
494                                 /* store the value and its releaser */
495                                 cookie->value = value;
496                                 cookie->freecb = freecb;
497
498                                 /* but if both are NULL drop the cookie */
499                                 if (!value && !freecb) {
500                                         *prv = cookie->next;
501                                         free(cookie);
502                                 }
503                         }
504                         break;
505                 } else {
506                         prv = &(cookie->next);
507                 }
508         }
509
510         /* unlock the session and return the value */
511         unlock(session);
512         return value;
513 }
514
515 void *afb_session_get_cookie(struct afb_session *session, const void *key)
516 {
517         return afb_session_cookie(session, key, NULL, NULL, NULL, 0);
518 }
519
520 int afb_session_set_cookie(struct afb_session *session, const void *key, void *value, void (*freecb)(void*))
521 {
522         return -(value != afb_session_cookie(session, key, NULL, freecb, value, 1));
523 }
524