afb-session: Add timeout features for 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 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         time_t access;
53         pthread_mutex_t mutex;
54         char uuid[37];        // long term authentication of remote client
55         char token[37];       // short term authentication of remote client
56         struct cookie *cookies[COOKEYCOUNT];
57 };
58
59 // Session UUID are store in a simple array [for 10 sessions this should be enough]
60 static struct {
61         pthread_mutex_t mutex;          // declare a mutex to protect hash table
62         struct afb_session **store;     // sessions store
63         int count;                      // current number of sessions
64         int max;
65         int timeout;
66         char initok[37];
67 } sessions;
68
69 /**
70  * Get the index of the 'key' in the cookies array.
71  * @param key the key to scan
72  * @return the index of the list for key within cookies
73  */
74 static int cookeyidx(const void *key)
75 {
76         intptr_t x = (intptr_t)key;
77         unsigned r = (unsigned)((x >> 5) ^ (x >> 15));
78         return r & COOKEYMASK;
79 }
80
81 /* generate a uuid */
82 static void new_uuid(char uuid[37])
83 {
84         uuid_t newuuid;
85         uuid_generate(newuuid);
86         uuid_unparse_lower(newuuid, uuid);
87 }
88
89 static inline void lock(struct afb_session *session)
90 {
91         pthread_mutex_lock(&session->mutex);
92 }
93
94 static inline void unlock(struct afb_session *session)
95 {
96         pthread_mutex_unlock(&session->mutex);
97 }
98
99 // Free context [XXXX Should be protected again memory abort XXXX]
100 static void remove_all_cookies(struct afb_session *session)
101 {
102         int idx;
103         struct cookie *cookie, *next;
104
105         // free cookies
106         for (idx = 0 ; idx < COOKEYCOUNT ; idx++) {
107                 cookie = session->cookies[idx];
108                 session->cookies[idx] = NULL;
109                 while (cookie != NULL) {
110                         next = cookie->next;
111                         if (cookie->freecb != NULL)
112                                 cookie->freecb(cookie->value);
113                         free(cookie);
114                         cookie = next;
115                 }
116         }
117 }
118
119 // Create a new store in RAM, not that is too small it will be automatically extended
120 void afb_session_init (int max_session_count, int timeout, const char *initok)
121 {
122         // let's create as store as hashtable does not have any
123         sessions.store = calloc (1 + (unsigned)max_session_count, sizeof *sessions.store);
124         pthread_mutex_init(&sessions.mutex, NULL);
125         sessions.max = max_session_count;
126         sessions.timeout = timeout;
127         if (initok == NULL)
128                 /* without token, a secret is made to forbid creation of sessions */
129                 new_uuid(sessions.initok);
130         else if (strlen(initok) < sizeof(sessions.store[0]->token))
131                 strcpy(sessions.initok, initok);
132         else {
133                 ERROR("initial token '%s' too long (max length 36)", initok);
134                 exit(1);
135         }
136 }
137
138 const char *afb_session_initial_token()
139 {
140         return sessions.initok;
141 }
142
143 static struct afb_session *search (const char* uuid)
144 {
145         int  idx;
146         struct afb_session *session;
147
148         assert (uuid != NULL);
149
150         pthread_mutex_lock(&sessions.mutex);
151
152         for (idx=0; idx < sessions.max; idx++) {
153                 session = sessions.store[idx];
154                 if (session && (0 == strcmp (uuid, session->uuid)))
155                         goto found;
156         }
157         session = NULL;
158
159 found:
160         pthread_mutex_unlock(&sessions.mutex);
161         return session;
162 }
163
164 static int destroy (struct afb_session *session)
165 {
166         int idx;
167         int status;
168
169         assert (session != NULL);
170
171         pthread_mutex_lock(&sessions.mutex);
172
173         for (idx=0; idx < sessions.max; idx++) {
174                 if (sessions.store[idx] == session) {
175                         sessions.store[idx] = NULL;
176                         sessions.count--;
177                         status = 1;
178                         goto deleted;
179                 }
180         }
181         status = 0;
182 deleted:
183         pthread_mutex_unlock(&sessions.mutex);
184         return status;
185 }
186
187 static int add (struct afb_session *session)
188 {
189         int idx;
190         int status;
191
192         assert (session != NULL);
193
194         pthread_mutex_lock(&sessions.mutex);
195
196         for (idx=0; idx < sessions.max; idx++) {
197                 if (NULL == sessions.store[idx]) {
198                         sessions.store[idx] = session;
199                         sessions.count++;
200                         status = 1;
201                         goto added;
202                 }
203         }
204         status = 0;
205 added:
206         pthread_mutex_unlock(&sessions.mutex);
207         return status;
208 }
209
210 // Check if context timeout or not
211 static int is_expired (struct afb_session *ctx, time_t now)
212 {
213         assert (ctx != NULL);
214         return ctx->expiration < now;
215 }
216
217 // Check if context is active or not
218 static int is_active (struct afb_session *ctx, time_t now)
219 {
220         assert (ctx != NULL);
221         return ctx->uuid[0] != 0 && ctx->expiration >= now;
222 }
223
224 // Loop on every entry and remove old context sessions.hash
225 static void cleanup (time_t now)
226 {
227         struct afb_session *ctx;
228         long idx;
229
230         // Loop on Sessions Table and remove anything that is older than timeout
231         for (idx=0; idx < sessions.max; idx++) {
232                 ctx = sessions.store[idx];
233                 if (ctx != NULL && is_expired(ctx, now)) {
234                         afb_session_close (ctx);
235                 }
236         }
237 }
238
239 static struct afb_session *make_session (const char *uuid, int timeout, time_t now)
240 {
241         struct afb_session *session;
242
243         if (!AFB_SESSION_TIMEOUT_IS_VALID(timeout)
244          || (uuid && strlen(uuid) >= sizeof session->uuid)) {
245                 errno = EINVAL;
246                 goto error;
247         }
248
249         /* allocates a new one */
250         session = calloc(1, sizeof *session);
251         if (session == NULL) {
252                 errno = ENOMEM;
253                 goto error;
254         }
255         pthread_mutex_init(&session->mutex, NULL);
256
257         /* generate the uuid */
258         if (uuid == NULL) {
259                 do { new_uuid(session->uuid); } while(search(session->uuid));
260         } else {
261                 strcpy(session->uuid, uuid);
262         }
263
264         /* init the token */
265         strcpy(session->token, sessions.initok);
266
267         /* init timeout */
268         if (timeout == AFB_SESSION_TIMEOUT_DEFAULT)
269                 timeout = sessions.timeout;
270         session->timeout = timeout;
271         session->expiration = now + timeout;
272         if (timeout == AFB_SESSION_TIMEOUT_INFINITE || session->expiration < 0) {
273                 session->expiration = (time_t)(~(time_t)0);
274                 if (session->expiration < 0)
275                         session->expiration = (time_t)(((unsigned long long)session->expiration) >> 1);
276         }
277         if (!add (session)) {
278                 errno = ENOMEM;
279                 goto error2;
280         }
281
282         session->access = now;
283         session->refcount = 1;
284         return session;
285
286 error2:
287         free(session);
288 error:
289         return NULL;
290 }
291
292 /* Creates a new session with 'timeout' */
293 struct afb_session *afb_session_create (int timeout)
294 {
295         time_t now;
296
297         /* cleaning */
298         now = NOW;
299         cleanup (now);
300
301         return make_session(NULL, timeout, now);
302 }
303
304 /* Searchs the session of 'uuid' */
305 struct afb_session *afb_session_search (const char *uuid)
306 {
307         time_t now;
308         struct afb_session *session;
309
310         /* cleaning */
311         now = NOW;
312         cleanup (now);
313         session = search(uuid);
314         return session;
315
316 }
317
318 /* This function will return exiting session or newly created session */
319 struct afb_session *afb_session_get (const char *uuid, int timeout, int *created)
320 {
321         struct afb_session *session;
322         time_t now;
323
324         /* cleaning */
325         now = NOW;
326         cleanup (now);
327
328         /* search for an existing one not too old */
329         if (uuid != NULL) {
330                 session = search(uuid);
331                 if (session != NULL) {
332                         if (created)
333                                 *created = 0;
334                         session->access = now;
335                         session->refcount++;
336                         return session;
337                 }
338         }
339
340         /* no existing session found, create it */
341         session = make_session(uuid, timeout, now);
342         if (created)
343                 *created = !!session;
344
345         return session;
346 }
347
348 struct afb_session *afb_session_addref(struct afb_session *session)
349 {
350         if (session != NULL)
351                 __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
352         return session;
353 }
354
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                         if (session->uuid[0] == 0) {
361                                 destroy (session);
362                                 pthread_mutex_destroy(&session->mutex);
363                                 free(session);
364                         }
365                 }
366         }
367 }
368
369 // Free Client Session Context
370 void afb_session_close (struct afb_session *session)
371 {
372         assert(session != NULL);
373         if (session->uuid[0] != 0) {
374                 session->uuid[0] = 0;
375                 remove_all_cookies(session);
376                 if (session->refcount == 0) {
377                         destroy (session);
378                         free(session);
379                 }
380         }
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 /* Set, get, replace, remove a cookie key */
427 void *afb_session_cookie(struct afb_session *session, const void *key, void *(*makecb)(void *closure), void (*freecb)(void *item), void *closure, int replace)
428 {
429         int idx;
430         void *value;
431         struct cookie *cookie;
432
433         idx = cookeyidx(key);
434         lock(session);
435         cookie = session->cookies[idx];
436         for (;;) {
437                 if (!cookie) {
438                         value = makecb ? makecb(closure) : closure;
439                         if (replace || makecb || freecb) {
440                                 cookie = malloc(sizeof *cookie);
441                                 if (!cookie) {
442                                         errno = ENOMEM;
443                                         if (freecb)
444                                                 freecb(value);
445                                         value = NULL;
446                                 } else {
447                                         cookie->key = key;
448                                         cookie->value = value;
449                                         cookie->freecb = freecb;
450                                         cookie->next = session->cookies[idx];
451                                         session->cookies[idx] = cookie;
452                                 }
453                         }
454                         break;
455                 } else if (cookie->key == key) {
456                         if (!replace)
457                                 value = cookie->value;
458                         else {
459                                 value = makecb ? makecb(closure) : closure;
460                                 if (cookie->value != value && cookie->freecb)
461                                         cookie->freecb(cookie->value);
462                                 cookie->value = value;
463                                 cookie->freecb = freecb;
464                         }
465                         break;
466                 } else {
467                         cookie = cookie->next;
468                 }
469         }
470         unlock(session);
471         return value;
472 }
473
474 void *afb_session_get_cookie(struct afb_session *session, const void *key)
475 {
476         return afb_session_cookie(session, key, NULL, NULL, NULL, 0);
477 }
478
479 int afb_session_set_cookie(struct afb_session *session, const void *key, void *value, void (*freecb)(void*))
480 {
481         return -(value != afb_session_cookie(session, key, NULL, freecb, value, 1));
482 }
483