afb-stub-ws: autoclose session on disconnection
[src/app-framework-binder.git] / src / afb-session.c
index db81457..6b6ad63 100644 (file)
 #include "afb-session.h"
 #include "verbose.h"
 
-#define NOW (time(NULL))
+#define SIZEUUID       37
+#define HEADCOUNT      16
+#define COOKEYCOUNT    8
+#define COOKEYMASK     (COOKEYCOUNT - 1)
 
-struct value
-{
-       void *value;
-       void (*freecb)(void*);
-};
+#define _MAXEXP_       ((time_t)(~(time_t)0))
+#define _MAXEXP2_      ((time_t)((((unsigned long long)_MAXEXP_) >> 1)))
+#define MAX_EXPIRATION (_MAXEXP_ >= 0 ? _MAXEXP_ : _MAXEXP2_)
+#define NOW            (time(NULL))
 
 struct cookie
 {
@@ -49,296 +51,358 @@ struct cookie
 
 struct afb_session
 {
+       struct afb_session *next; /* link to the next */
        unsigned refcount;
-       unsigned loa;
        int timeout;
-       time_t expiration;    // expiration time of the token
-       time_t access;
-       char uuid[37];        // long term authentication of remote client
-       char token[37];       // short term authentication of remote client
-       struct value *values;
-       struct cookie *cookies;
+       time_t expiration;      // expiration time of the token
+       pthread_mutex_t mutex;
+       struct cookie *cookies[COOKEYCOUNT];
+       char autoclose;
+       char idx;
+       char uuid[SIZEUUID];    // long term authentication of remote client
+       char token[SIZEUUID];   // short term authentication of remote client
 };
 
 // Session UUID are store in a simple array [for 10 sessions this should be enough]
 static struct {
-       pthread_mutex_t mutex;          // declare a mutex to protect hash table
-       struct afb_session **store;          // sessions store
-       int count;                      // current number of sessions
+       pthread_mutex_t mutex;  // declare a mutex to protect hash table
+       struct afb_session *heads[HEADCOUNT]; // sessions
+       int count;      // current number of sessions
        int max;
        int timeout;
-       int apicount;
-       char initok[37];
+       char initok[SIZEUUID];
 } sessions;
 
 /* generate a uuid */
-static void new_uuid(char uuid[37])
+static void new_uuid(char uuid[SIZEUUID])
 {
        uuid_t newuuid;
        uuid_generate(newuuid);
        uuid_unparse_lower(newuuid, uuid);
 }
 
+static inline void lock(struct afb_session *session)
+{
+       pthread_mutex_lock(&session->mutex);
+}
+
+static inline void unlock(struct afb_session *session)
+{
+       pthread_mutex_unlock(&session->mutex);
+}
+
 // Free context [XXXX Should be protected again memory abort XXXX]
-static void free_data (struct afb_session *session)
+static void close_session(struct afb_session *session)
 {
        int idx;
        struct cookie *cookie;
 
-       // If application add a handle let's free it now
-       assert (session->values != NULL);
-
-       // Free session handle with a standard Free function, with app callback or ignore it
-       for (idx=0; idx < sessions.apicount; idx ++)
-               afb_session_set_value(session, idx, NULL, NULL);
-
-       // free cookies
-       cookie = session->cookies;
-       while (cookie != NULL) {
-               session->cookies = cookie->next;
-               if (cookie->value != NULL && cookie->freecb != NULL)
-                       cookie->freecb(cookie->value);
-               free(cookie);
-               cookie = session->cookies;
+       /* free cookies */
+       for (idx = 0 ; idx < COOKEYCOUNT ; idx++) {
+               while ((cookie = session->cookies[idx])) {
+                       session->cookies[idx] = cookie->next;
+                       if (cookie->freecb != NULL)
+                               cookie->freecb(cookie->value);
+                       free(cookie);
+               }
        }
 }
 
+/* tiny hash function inspired from pearson */
+static int pearson4(const char *text)
+{
+       static uint8_t T[16] = {
+                4,  1,  6,  0,  9, 14, 11,  5,
+                2,  3, 12, 15, 10,  7,  8, 13
+       };
+       uint8_t r, c;
+
+       for (r = 0; (c = (uint8_t)*text) ; text++) {
+               r = T[r ^ (15 & c)];
+               r = T[r ^ (c >> 4)];
+       }
+       return r; // % HEADCOUNT;
+}
+
 // Create a new store in RAM, not that is too small it will be automatically extended
-void afb_session_init (int max_session_count, int timeout, const char *initok, int context_count)
+void afb_session_init (int max_session_count, int timeout, const char *initok)
 {
-       // let's create as store as hashtable does not have any
-       sessions.store = calloc (1 + (unsigned)max_session_count, sizeof(struct afb_session));
+       pthread_mutex_init(&sessions.mutex, NULL);
        sessions.max = max_session_count;
        sessions.timeout = timeout;
-       sessions.apicount = context_count;
        if (initok == NULL)
                /* without token, a secret is made to forbid creation of sessions */
                new_uuid(sessions.initok);
-       else if (strlen(initok) < sizeof(sessions.store[0]->token))
+       else if (strlen(initok) < sizeof sessions.initok)
                strcpy(sessions.initok, initok);
        else {
-               ERROR("initial token '%s' too long (max length 36)", initok);
+               ERROR("initial token '%s' too long (max length %d)", initok, ((int)(sizeof sessions.initok)) - 1);
                exit(1);
        }
 }
 
-static struct afb_session *search (const char* uuid)
+const char *afb_session_initial_token()
 {
-       int  idx;
-       struct afb_session *session;
-
-       assert (uuid != NULL);
+       return sessions.initok;
+}
 
-       pthread_mutex_lock(&sessions.mutex);
+static struct afb_session *search (const char* uuid, int idx)
+{
+       struct afb_session *session;
 
-       for (idx=0; idx < sessions.max; idx++) {
-               session = sessions.store[idx];
-               if (session && (0 == strcmp (uuid, session->uuid)))
-                       goto found;
-       }
-       session = NULL;
+       session = sessions.heads[idx];
+       while (session && strcmp(uuid, session->uuid))
+               session = session->next;
 
-found:
-       pthread_mutex_unlock(&sessions.mutex);
        return session;
 }
 
-static int destroy (struct afb_session *session)
+static void destroy (struct afb_session *session)
 {
-       int idx;
-       int status;
+       struct afb_session **prv;
 
        assert (session != NULL);
 
+       close_session(session);
        pthread_mutex_lock(&sessions.mutex);
-
-       for (idx=0; idx < sessions.max; idx++) {
-               if (sessions.store[idx] == session) {
-                       sessions.store[idx] = NULL;
+       prv = &sessions.heads[(int)session->idx];
+       while (*prv)
+               if (*prv != session)
+                       prv = &((*prv)->next);
+               else {
+                       *prv = session->next;
                        sessions.count--;
-                       status = 1;
-                       goto deleted;
+                       pthread_mutex_destroy(&session->mutex);
+                       free(session);
+                       break;
                }
-       }
-       status = 0;
-deleted:
        pthread_mutex_unlock(&sessions.mutex);
-       return status;
 }
 
-static int add (struct afb_session *session)
+// Loop on every entry and remove old context sessions.hash
+static time_t cleanup ()
 {
+       struct afb_session *session, *next;
        int idx;
-       int status;
-
-       assert (session != NULL);
-
-       pthread_mutex_lock(&sessions.mutex);
+       time_t now;
 
-       for (idx=0; idx < sessions.max; idx++) {
-               if (NULL == sessions.store[idx]) {
-                       sessions.store[idx] = session;
-                       sessions.count++;
-                       status = 1;
-                       goto added;
+       // Loop on Sessions Table and remove anything that is older than timeout
+       now = NOW;
+       for (idx = 0 ; idx < HEADCOUNT; idx++) {
+               session = sessions.heads[idx];
+               while (session) {
+                       next = session->next;
+                       if (session->expiration < now)
+                               afb_session_close(session);
+                       session = next;
                }
        }
-       status = 0;
-added:
-       pthread_mutex_unlock(&sessions.mutex);
-       return status;
+       return now;
 }
 
-// Check if context timeout or not
-static int is_expired (struct afb_session *ctx, time_t now)
+static void update_timeout(struct afb_session *session, time_t now, int timeout)
 {
-       assert (ctx != NULL);
-       return ctx->expiration < now;
+       time_t expiration;
+
+       /* compute expiration */
+       if (timeout == AFB_SESSION_TIMEOUT_INFINITE)
+               expiration = MAX_EXPIRATION;
+       else {
+               if (timeout == AFB_SESSION_TIMEOUT_DEFAULT)
+                       expiration = now + sessions.timeout;
+               else
+                       expiration = now + timeout;
+               if (expiration < 0)
+                       expiration = MAX_EXPIRATION;
+       }
+
+       /* record the values */
+       session->timeout = timeout;
+       session->expiration = expiration;
 }
 
-// Check if context is active or not
-static int is_active (struct afb_session *ctx, time_t now)
+static void update_expiration(struct afb_session *session, time_t now)
 {
-       assert (ctx != NULL);
-       return ctx->uuid[0] != 0 && ctx->expiration >= now;
+       update_timeout(session, now, session->timeout);
 }
 
-// Loop on every entry and remove old context sessions.hash
-static void cleanup (time_t now)
+static struct afb_session *add_session (const char *uuid, int timeout, time_t now, int idx)
 {
-       struct afb_session *ctx;
-       long idx;
+       struct afb_session *session;
 
-       // Loop on Sessions Table and remove anything that is older than timeout
-       for (idx=0; idx < sessions.max; idx++) {
-               ctx = sessions.store[idx];
-               if (ctx != NULL && is_expired(ctx, now)) {
-                       afb_session_close (ctx);
-               }
+       /* check arguments */
+       if (!AFB_SESSION_TIMEOUT_IS_VALID(timeout)
+        || (uuid && strlen(uuid) >= sizeof session->uuid)) {
+               errno = EINVAL;
+               return NULL;
        }
-}
 
-static struct afb_session *make_session (const char *uuid, int timeout, time_t now)
-{
-       struct afb_session *session;
+       /* check session count */
+       if (sessions.count >= sessions.max) {
+               errno = EBUSY;
+               return NULL;
+       }
 
        /* allocates a new one */
-       session = calloc(1, sizeof(struct afb_session) + ((unsigned)sessions.apicount * sizeof(*session->values)));
+       session = calloc(1, sizeof *session);
        if (session == NULL) {
                errno = ENOMEM;
-               goto error;
-       }
-       session->values = (void*)(session + 1);
-
-       /* generate the uuid */
-       if (uuid == NULL) {
-               new_uuid(session->uuid);
-       } else {
-               if (strlen(uuid) >= sizeof session->uuid) {
-                       errno = EINVAL;
-                       goto error2;
-               }
-               strcpy(session->uuid, uuid);
+               return NULL;
        }
 
-       /* init the token */
+       /* initialize */
+       pthread_mutex_init(&session->mutex, NULL);
+       session->refcount = 1;
+       strcpy(session->uuid, uuid);
        strcpy(session->token, sessions.initok);
-       session->timeout = timeout;
-       if (timeout != 0)
-               session->expiration = now + timeout;
-       else {
-               session->expiration = (time_t)(~(time_t)0);
-               if (session->expiration < 0)
-                       session->expiration = (time_t)(((unsigned long long)session->expiration) >> 1);
-       }
-       if (!add (session)) {
-               errno = ENOMEM;
-               goto error2;
-       }
+       update_timeout(session, now, timeout);
+
+       /* link */
+       session->idx = (char)idx;
+       session->next = sessions.heads[idx];
+       sessions.heads[idx] = session;
+       sessions.count++;
 
-       session->access = now;
-       session->refcount = 1;
        return session;
+}
 
-error2:
-       free(session);
-error:
-       return NULL;
+/* create a new session for the given timeout */
+static struct afb_session *new_session (int timeout, time_t now)
+{
+       int idx;
+       char uuid[SIZEUUID];
+
+       do {
+               new_uuid(uuid);
+               idx = pearson4(uuid);
+       } while(search(uuid, idx));
+       return add_session(uuid, timeout, now, idx);
 }
 
-struct afb_session *afb_session_create (const char *uuid, int timeout)
+/* Creates a new session with 'timeout' */
+struct afb_session *afb_session_create (int timeout)
 {
        time_t now;
+       struct afb_session *session;
 
        /* cleaning */
-       now = NOW;
-       cleanup (now);
+       pthread_mutex_lock(&sessions.mutex);
+       now = cleanup();
+       session = new_session(timeout, now);
+       pthread_mutex_unlock(&sessions.mutex);
 
-       /* search for an existing one not too old */
-       if (uuid != NULL && search(uuid) != NULL) {
-               errno = EEXIST;
-               return NULL;
-       }
+       return session;
+}
+
+/* Searchs the session of 'uuid' */
+struct afb_session *afb_session_search (const char *uuid)
+{
+       struct afb_session *session;
+
+       /* cleaning */
+       pthread_mutex_lock(&sessions.mutex);
+       cleanup();
+       session = search(uuid, pearson4(uuid));
+       if (session)
+               __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
+       pthread_mutex_unlock(&sessions.mutex);
+       return session;
 
-       return make_session(uuid, timeout, now);
 }
 
-// This function will return exiting session or newly created session
-struct afb_session *afb_session_get (const char *uuid, int *created)
+/* This function will return exiting session or newly created session */
+struct afb_session *afb_session_get (const char *uuid, int timeout, int *created)
 {
+       int idx;
        struct afb_session *session;
        time_t now;
 
        /* cleaning */
-       now = NOW;
-       cleanup (now);
+       pthread_mutex_lock(&sessions.mutex);
+       now = cleanup();
 
        /* search for an existing one not too old */
-       if (uuid != NULL) {
-               session = search(uuid);
-               if (session != NULL) {
-                       *created = 0;
-                       session->access = now;
-                       session->refcount++;
+       if (!uuid)
+               session = new_session(timeout, now);
+       else {
+               idx = pearson4(uuid);
+               session = search(uuid, idx);
+               if (session) {
+                       __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
+                       pthread_mutex_unlock(&sessions.mutex);
+                       if (created)
+                               *created = 0;
                        return session;
                }
+               session = add_session (uuid, timeout, now, idx);
        }
+       pthread_mutex_unlock(&sessions.mutex);
+
+       if (created)
+               *created = !!session;
 
-       *created = 1;
-       return make_session(uuid, sessions.timeout, now);
+       return session;
 }
 
+/* increase the use count on the session */
 struct afb_session *afb_session_addref(struct afb_session *session)
 {
        if (session != NULL)
-               session->refcount++;
+               __atomic_add_fetch(&session->refcount, 1, __ATOMIC_RELAXED);
        return session;
 }
 
+/* decrease the use count of the session */
 void afb_session_unref(struct afb_session *session)
 {
        if (session != NULL) {
                assert(session->refcount != 0);
-               --session->refcount;
-               if (session->refcount == 0 && session->uuid[0] == 0) {
-                       destroy (session);
-                       free(session);
+               if (!__atomic_sub_fetch(&session->refcount, 1, __ATOMIC_RELAXED)) {
+                       pthread_mutex_lock(&session->mutex);
+                       if (session->autoclose || session->uuid[0] == 0)
+                               destroy (session);
+                       else
+                               pthread_mutex_unlock(&session->mutex);
                }
        }
 }
 
-// Free Client Session Context
+// close Client Session Context
 void afb_session_close (struct afb_session *session)
 {
        assert(session != NULL);
+       pthread_mutex_lock(&session->mutex);
        if (session->uuid[0] != 0) {
                session->uuid[0] = 0;
-               free_data (session);
-               if (session->refcount == 0) {
+               if (session->refcount)
+                       close_session(session);
+               else {
                        destroy (session);
-                       free(session);
+                       return;
                }
        }
+       pthread_mutex_unlock(&session->mutex);
+}
+
+/* set the autoclose flag */
+void afb_session_set_autoclose(struct afb_session *session, int autoclose)
+{
+       assert(session != NULL);
+       session->autoclose = (char)!!autoclose;
+}
+
+// is the session active?
+int afb_session_is_active (struct afb_session *session)
+{
+       assert(session != NULL);
+       return !!session->uuid[0];
+}
+
+// is the session closed?
+int afb_session_is_closed (struct afb_session *session)
+{
+       assert(session != NULL);
+       return !session->uuid[0];
 }
 
 // Sample Generic Ping Debug API
@@ -347,8 +411,10 @@ int afb_session_check_token (struct afb_session *session, const char *token)
        assert(session != NULL);
        assert(token != NULL);
 
-       // compare current token with previous one
-       if (!is_active (session, NOW))
+       if (!session->uuid[0])
+               return 0;
+
+       if (session->expiration < NOW)
                return 0;
 
        if (session->token[0] && strcmp (token, session->token) != 0)
@@ -366,96 +432,135 @@ void afb_session_new_token (struct afb_session *session)
        new_uuid(session->token);
 
        // keep track of time for session timeout and further clean up
-       if (session->timeout != 0)
-               session->expiration = NOW + session->timeout;
+       update_expiration(session, NOW);
 }
 
+/* Returns the uuid of 'session' */
 const char *afb_session_uuid (struct afb_session *session)
 {
        assert(session != NULL);
        return session->uuid;
 }
 
+/* Returns the token of 'session' */
 const char *afb_session_token (struct afb_session *session)
 {
        assert(session != NULL);
        return session->token;
 }
 
-unsigned afb_session_get_LOA (struct afb_session *session)
+/**
+ * Get the index of the 'key' in the cookies array.
+ * @param key the key to scan
+ * @return the index of the list for key within cookies
+ */
+static int cookeyidx(const void *key)
 {
-       assert(session != NULL);
-       return session->loa;
+       intptr_t x = (intptr_t)key;
+       unsigned r = (unsigned)((x >> 5) ^ (x >> 15));
+       return r & COOKEYMASK;
 }
 
-void afb_session_set_LOA (struct afb_session *session, unsigned loa)
-{
-       assert(session != NULL);
-       session->loa = loa;
-}
-
-void *afb_session_get_value(struct afb_session *session, int index)
+/**
+ * Set, get, replace, remove a cookie of 'key' for the 'session'
+ *
+ * The behaviour of this function depends on its parameters:
+ *
+ * @param session      the session
+ * @param key          the key of the cookie
+ * @param makecb       the creation function or NULL
+ * @param freecb       the release function or NULL
+ * @param closure      an argument for makecb or the value if makecb==NULL
+ * @param replace      a boolean enforcing replecement of the previous value
+ *
+ * @return the value of the cookie
+ *
+ * The 'key' is a pointer and compared as pointers.
+ *
+ * For getting the current value of the cookie:
+ *
+ *   afb_session_cookie(session, key, NULL, NULL, NULL, 0)
+ *
+ * For storing the value of the cookie
+ *
+ *   afb_session_cookie(session, key, NULL, NULL, value, 1)
+ */
+void *afb_session_cookie(struct afb_session *session, const void *key, void *(*makecb)(void *closure), void (*freecb)(void *item), void *closure, int replace)
 {
-       assert(session != NULL);
-       assert(index >= 0);
-       assert(index < sessions.apicount);
-       return session->values[index].value;
-}
+       int idx;
+       void *value;
+       struct cookie *cookie, **prv;
+
+       /* get key hashed index */
+       idx = cookeyidx(key);
+
+       /* lock session and search for the cookie of 'key' */
+       lock(session);
+       prv = &session->cookies[idx];
+       for (;;) {
+               cookie = *prv;
+               if (!cookie) {
+                       /* 'key' not found, create value using 'closure' and 'makecb' */
+                       value = makecb ? makecb(closure) : closure;
+                       /* store the the only if it has some meaning */
+                       if (replace || makecb || freecb) {
+                               cookie = malloc(sizeof *cookie);
+                               if (!cookie) {
+                                       errno = ENOMEM;
+                                       /* calling freecb if there is no makecb may have issue */
+                                       if (makecb && freecb)
+                                               freecb(value);
+                                       value = NULL;
+                               } else {
+                                       cookie->key = key;
+                                       cookie->value = value;
+                                       cookie->freecb = freecb;
+                                       cookie->next = NULL;
+                                       *prv = cookie;
+                               }
+                       }
+                       break;
+               } else if (cookie->key == key) {
+                       /* cookie of key found */
+                       if (!replace)
+                               /* not replacing, get the value */
+                               value = cookie->value;
+                       else {
+                               /* create value using 'closure' and 'makecb' */
+                               value = makecb ? makecb(closure) : closure;
+
+                               /* free previous value is needed */
+                               if (cookie->value != value && cookie->freecb)
+                                       cookie->freecb(cookie->value);
+
+                               /* store the value and its releaser */
+                               cookie->value = value;
+                               cookie->freecb = freecb;
+
+                               /* but if both are NULL drop the cookie */
+                               if (!value && !freecb) {
+                                       *prv = cookie->next;
+                                       free(cookie);
+                               }
+                       }
+                       break;
+               } else {
+                       prv = &(cookie->next);
+               }
+       }
 
-void afb_session_set_value(struct afb_session *session, int index, void *value, void (*freecb)(void*))
-{
-       struct value prev;
-       assert(session != NULL);
-       assert(index >= 0);
-       assert(index < sessions.apicount);
-       prev = session->values[index];
-       session->values[index] = (struct value){.value = value, .freecb = freecb};
-       if (prev.value != NULL && prev.value != value && prev.freecb != NULL)
-               prev.freecb(prev.value);
+       /* unlock the session and return the value */
+       unlock(session);
+       return value;
 }
 
 void *afb_session_get_cookie(struct afb_session *session, const void *key)
 {
-       struct cookie *cookie;
-
-       cookie = session->cookies;
-       while(cookie != NULL) {
-               if (cookie->key == key)
-                       return cookie->value;
-               cookie = cookie->next;
-       }
-       return NULL;
+       return afb_session_cookie(session, key, NULL, NULL, NULL, 0);
 }
 
 int afb_session_set_cookie(struct afb_session *session, const void *key, void *value, void (*freecb)(void*))
 {
-       struct cookie *cookie;
-
-       /* search for a replacement */
-       cookie = session->cookies;
-       while(cookie != NULL) {
-               if (cookie->key == key) {
-                       if (cookie->value != NULL && cookie->value != value && cookie->freecb != NULL)
-                               cookie->freecb(cookie->value);
-                       cookie->value = value;
-                       cookie->freecb = freecb;
-                       return 0;
-               }
-               cookie = cookie->next;
-       }
-
-       /* allocates */
-       cookie = malloc(sizeof *cookie);
-       if (cookie == NULL) {
-               errno = ENOMEM;
-               return -1;
-       }
-
-       cookie->key = key;
-       cookie->value = value;
-       cookie->freecb = freecb;
-       cookie->next = session->cookies;
-       session->cookies = cookie;
-       return 0;
+       return -(value != afb_session_cookie(session, key, NULL, freecb, value, 1));
 }