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