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