Update copyright dates
[src/app-framework-binder.git] / src / afb-session.c
index 17fddd5..80bad1b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015, 2016, 2017 "IoT.bzh"
+ * Copyright (C) 2015-2020 "IoT.bzh"
  * Author "Fulup Ar Foll"
  * Author: José Bollo <jose.bollo@iot.bzh>
  *
 #include <time.h>
 #include <pthread.h>
 #include <stdlib.h>
+#include <stdint.h>
+#include <limits.h>
 #include <string.h>
-#include <uuid/uuid.h>
-#include <assert.h>
 #include <errno.h>
 
-#include <json-c/json.h>
-
 #include "afb-session.h"
+#include "afb-hook.h"
 #include "verbose.h"
+#include "pearson.h"
+#include "uuid.h"
+
+#define SESSION_COUNT_MIN 5
+#define SESSION_COUNT_MAX 1000
 
-#define COOKEYCOUNT  8
-#define COOKEYMASK   (COOKEYCOUNT - 1)
+/*
+ * Handling of  cookies.
+ * Cookies are stored by session.
+ * The cookie count COOKIECOUNT must be a power of 2, possible values: 1, 2, 4, 8, 16, 32, 64, ...
+ * For low memory profile, small values are better, 1 is possible.
+ */
+#define COOKIECOUNT    8
+#define COOKIEMASK     (COOKIECOUNT - 1)
 
-#define NOW (time(NULL))
+#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_now())
 
+/**
+ * structure for a cookie added to sessions
+ */
 struct cookie
 {
-       struct cookie *next;
-       const void *key;
-       void *value;
-       void (*freecb)(void*);
+       struct cookie *next;    /**< link to next cookie */
+       const void *key;        /**< pointer key */
+       void *value;            /**< value */
+       void (*freecb)(void*);  /**< function to call when session is closed */
 };
 
+/**
+ * structure for session
+ */
 struct afb_session
 {
-       unsigned refcount;
-       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 cookie *cookies[COOKEYCOUNT];
+       struct afb_session *next; /**< link to the next */
+       uint16_t refcount;      /**< count of reference to the session */
+       uint16_t id;            /**< local id of the session */
+       int timeout;            /**< timeout of the session */
+       time_t expiration;      /**< expiration time of the session */
+       pthread_mutex_t mutex;  /**< mutex of the session */
+       struct cookie *cookies[COOKIECOUNT]; /**< cookies of the session */
+       char *lang;             /**< current language setting for the session */
+       uint8_t closed: 1;      /**< is the session closed ? */
+       uint8_t autoclose: 1;   /**< close the session when unreferenced */
+       uint8_t notinset: 1;    /**< session removed from the set of sessions */
+       uint8_t hash;           /**< hash value of the uuid */
+       uuid_stringz_t uuid;    /**< identification of client session */
 };
 
-// Session UUID are store in a simple array [for 10 sessions this should be enough]
+/**
+ * structure for managing sessions
+ */
 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
-       int max;
-       int timeout;
-       char initok[37];
-} sessions;
+       uint16_t count;         /**< current number of sessions */
+       uint16_t max;           /**< maximum count of sessions */
+       uint16_t genid;         /**< for generating ids */
+       int timeout;            /**< common initial timeout */
+       struct afb_session *first; /**< sessions */
+       pthread_mutex_t mutex;  /**< declare a mutex to protect hash table */
+} sessions = {
+       .count = 0,
+       .max = 10,
+       .genid = 1,
+       .timeout = 3600,
+       .first = 0,
+       .mutex = PTHREAD_MUTEX_INITIALIZER
+};
 
 /**
- * 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
+ * Get the actual raw time
  */
-static int cookeyidx(const void *key)
+static inline time_t time_now()
 {
-       intptr_t x = (intptr_t)key;
-       unsigned r = (unsigned)((x >> 5) ^ (x >> 15));
-       return r & COOKEYMASK;
+       struct timespec ts;
+       clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
+       return ts.tv_sec;
 }
 
-/* generate a uuid */
-static void new_uuid(char uuid[37])
+/* lock the set of sessions for exclusive access */
+static inline void sessionset_lock()
 {
-       uuid_t newuuid;
-       uuid_generate(newuuid);
-       uuid_unparse_lower(newuuid, uuid);
+       pthread_mutex_lock(&sessions.mutex);
 }
 
-// Free context [XXXX Should be protected again memory abort XXXX]
-static void free_data (struct afb_session *session)
+/* unlock the set of sessions of exclusive access */
+static inline void sessionset_unlock()
 {
-       int idx;
-       struct cookie *cookie, *next;
-
-       // free cookies
-       for (idx = 0 ; idx < COOKEYCOUNT ; idx++) {
-               cookie = session->cookies[idx];
-               session->cookies[idx] = NULL;
-               while (cookie != NULL) {
-                       next = cookie->next;
-                       if (cookie->freecb != NULL)
-                               cookie->freecb(cookie->value);
-                       free(cookie);
-                       cookie = next;
-               }
-       }
+       pthread_mutex_unlock(&sessions.mutex);
 }
 
-// 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)
+/*
+ * search within the set of sessions the session of 'uuid'.
+ * 'hashidx' is the precomputed hash for 'uuid'
+ * return the session or NULL
+ */
+static struct afb_session *sessionset_search(const char *uuid, uint8_t hashidx)
 {
-       // let's create as store as hashtable does not have any
-       sessions.store = calloc (1 + (unsigned)max_session_count, sizeof(struct afb_session));
-       sessions.max = max_session_count;
-       sessions.timeout = timeout;
-       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))
-               strcpy(sessions.initok, initok);
-       else {
-               ERROR("initial token '%s' too long (max length 36)", initok);
-               exit(1);
-       }
+       struct afb_session *session;
+
+       session = sessions.first;
+       while (session && hashidx != session->hash && strcmp(uuid, session->uuid))
+               session = session->next;
+
+       return session;
 }
 
-static struct afb_session *search (const char* uuid)
+/*
+ * search within the set of sessions the session of 'id'.
+ * return the session or NULL
+ */
+static struct afb_session *sessionset_search_id(uint16_t id)
 {
-       int  idx;
        struct afb_session *session;
 
-       assert (uuid != NULL);
+       session = sessions.first;
+       while (session && id != session->id)
+               session = session->next;
 
-       pthread_mutex_lock(&sessions.mutex);
+       return session;
+}
 
-       for (idx=0; idx < sessions.max; idx++) {
-               session = sessions.store[idx];
-               if (session && (0 == strcmp (uuid, session->uuid)))
-                       goto found;
+/* add 'session' to the set of sessions */
+static int sessionset_add(struct afb_session *session, uint8_t hashidx)
+{
+       /* check availability */
+       if (sessions.max && sessions.count >= sessions.max) {
+               errno = EBUSY;
+               return -1;
        }
-       session = NULL;
 
-found:
-       pthread_mutex_unlock(&sessions.mutex);
-       return session;
+       /* add the session */
+       session->next = sessions.first;
+       sessions.first = session;
+       sessions.count++;
+       return 0;
 }
 
-static int destroy (struct afb_session *session)
+/* make a new uuid not used in the set of sessions */
+static uint8_t sessionset_make_uuid (uuid_stringz_t uuid)
 {
-       int idx;
-       int status;
+       uint8_t hashidx;
 
-       assert (session != NULL);
+       do {
+               uuid_new_stringz(uuid);
+               hashidx = pearson4(uuid);
+       } while(sessionset_search(uuid, hashidx));
+       return hashidx;
+}
 
-       pthread_mutex_lock(&sessions.mutex);
+/* lock the 'session' for exclusive access */
+static inline void session_lock(struct afb_session *session)
+{
+       pthread_mutex_lock(&session->mutex);
+}
 
-       for (idx=0; idx < sessions.max; idx++) {
-               if (sessions.store[idx] == session) {
-                       sessions.store[idx] = NULL;
-                       sessions.count--;
-                       status = 1;
-                       goto deleted;
-               }
-       }
-       status = 0;
-deleted:
-       pthread_mutex_unlock(&sessions.mutex);
-       return status;
+/* unlock the 'session' of exclusive access */
+static inline void session_unlock(struct afb_session *session)
+{
+       pthread_mutex_unlock(&session->mutex);
 }
 
-static int add (struct afb_session *session)
+/* close the 'session' */
+static void session_close(struct afb_session *session)
 {
        int idx;
-       int status;
-
-       assert (session != NULL);
-
-       pthread_mutex_lock(&sessions.mutex);
+       struct cookie *cookie;
 
-       for (idx=0; idx < sessions.max; idx++) {
-               if (NULL == sessions.store[idx]) {
-                       sessions.store[idx] = session;
-                       sessions.count++;
-                       status = 1;
-                       goto added;
+       /* close only one time */
+       if (!session->closed) {
+               /* close it now */
+               session->closed = 1;
+
+#if WITH_AFB_HOOK
+               /* emit the hook */
+               afb_hook_session_close(session);
+#endif
+
+               /* release cookies */
+               for (idx = 0 ; idx < COOKIECOUNT ; idx++) {
+                       while ((cookie = session->cookies[idx])) {
+                               session->cookies[idx] = cookie->next;
+                               if (cookie->freecb != NULL)
+                                       cookie->freecb(cookie->value);
+                               free(cookie);
+                       }
                }
        }
-       status = 0;
-added:
-       pthread_mutex_unlock(&sessions.mutex);
-       return status;
 }
 
-// Check if context timeout or not
-static int is_expired (struct afb_session *ctx, time_t now)
+/* destroy the 'session' */
+static void session_destroy (struct afb_session *session)
 {
-       assert (ctx != NULL);
-       return ctx->expiration < now;
+#if WITH_AFB_HOOK
+       afb_hook_session_destroy(session);
+#endif
+       pthread_mutex_destroy(&session->mutex);
+       free(session->lang);
+       free(session);
 }
 
-// Check if context is active or not
-static int is_active (struct afb_session *ctx, time_t now)
+/* update expiration of 'session' according to 'now' */
+static void session_update_expiration(struct afb_session *session, time_t now)
 {
-       assert (ctx != NULL);
-       return ctx->uuid[0] != 0 && ctx->expiration >= now;
-}
+       time_t expiration;
 
-// Loop on every entry and remove old context sessions.hash
-static void cleanup (time_t now)
-{
-       struct afb_session *ctx;
-       long idx;
+       /* compute expiration */
+       expiration = now + afb_session_timeout(session);
+       if (expiration < 0)
+               expiration = MAX_EXPIRATION;
 
-       // 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);
-               }
-       }
+       /* record the expiration */
+       session->expiration = expiration;
 }
 
-static struct afb_session *make_session (const char *uuid, int timeout, time_t now)
+/*
+ * Add a new session with the 'uuid' (of 'hashidx')
+ * and the 'timeout' starting from 'now'.
+ * Add it to the set of sessions
+ * Return the created session
+ */
+static struct afb_session *session_add(const char *uuid, int timeout, time_t now, uint8_t hashidx)
 {
        struct afb_session *session;
 
+       /* check arguments */
+       if (!AFB_SESSION_TIMEOUT_IS_VALID(timeout)
+        || (uuid && strlen(uuid) >= sizeof session->uuid)) {
+               errno = EINVAL;
+               return NULL;
+       }
+
        /* allocates a new one */
-       session = calloc(1, sizeof(struct afb_session));
+       session = calloc(1, sizeof *session);
        if (session == NULL) {
                errno = ENOMEM;
-               goto error;
-       }
-
-       /* 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 */
-       strcpy(session->token, sessions.initok);
+       /* initialize */
+       pthread_mutex_init(&session->mutex, NULL);
+       session->refcount = 1;
+       strcpy(session->uuid, uuid);
        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;
+       session_update_expiration(session, now);
+       session->id = ++sessions.genid;
+       while (session->id == 0 || sessionset_search_id(session->id) != NULL)
+               session->id = ++sessions.genid;
+
+       /* add */
+       if (sessionset_add(session, hashidx)) {
+               free(session);
+               return NULL;
        }
 
-       session->access = now;
-       session->refcount = 1;
-       return session;
+#if WITH_AFB_HOOK
+       afb_hook_session_create(session);
+#endif
 
-error2:
-       free(session);
-error:
-       return NULL;
+       return session;
 }
 
-struct afb_session *afb_session_create (const char *uuid, int timeout)
+/* Remove expired sessions and return current time (now) */
+static time_t sessionset_cleanup (int force)
 {
+       struct afb_session *session, **prv;
        time_t now;
 
-       /* cleaning */
+       /* Loop on Sessions Table and remove anything that is older than timeout */
        now = NOW;
-       cleanup (now);
+       prv = &sessions.first;
+       while ((session = *prv)) {
+               session_lock(session);
+               if (force || session->expiration < now)
+                       session_close(session);
+               if (!session->closed) {
+                       prv = &session->next;
+                       session_unlock(session);
+               } else {
+                       *prv = session->next;
+                       sessions.count--;
+                       session->notinset = 1;
+                       if (session->refcount)
+                               session_unlock(session);
+                       else
+                               session_destroy(session);
+               }
+       }
+       return now;
+}
 
-       /* search for an existing one not too old */
-       if (uuid != NULL && search(uuid) != NULL) {
-               errno = EEXIST;
-               return NULL;
+/**
+ * Initialize the session manager with a 'max_session_count',
+ * an initial common 'timeout'
+ *
+ * @param max_session_count  maximum allowed session count in the same time
+ * @param timeout            the initial default timeout of sessions
+ */
+int afb_session_init (int max_session_count, int timeout)
+{
+       /* init the sessionset (after cleanup) */
+       sessionset_lock();
+       sessionset_cleanup(1);
+       if (max_session_count > SESSION_COUNT_MAX)
+               sessions.max = SESSION_COUNT_MAX;
+       else if (max_session_count < SESSION_COUNT_MIN)
+               sessions.max = SESSION_COUNT_MIN;
+       else
+               sessions.max = (uint16_t)max_session_count;
+       sessions.timeout = timeout;
+       sessionset_unlock();
+       return 0;
+}
+
+/**
+ * Iterate the sessions and call 'callback' with
+ * the 'closure' for each session.
+ */
+void afb_session_foreach(void (*callback)(void *closure, struct afb_session *session), void *closure)
+{
+       struct afb_session *session;
+
+       /* Loop on Sessions Table and remove anything that is older than timeout */
+       sessionset_lock();
+       session = sessions.first;
+       while (session) {
+               if (!session->closed)
+                       callback(closure, session);
+               session = session->next;
        }
+       sessionset_unlock();
+}
 
-       return make_session(uuid, timeout, now);
+/**
+ * Cleanup the sessionset of its closed or expired sessions
+ */
+void afb_session_purge()
+{
+       sessionset_lock();
+       sessionset_cleanup(0);
+       sessionset_unlock();
 }
 
-// This function will return exiting session or newly created session
-struct afb_session *afb_session_get (const char *uuid, int *created)
+/* Searchs the session of 'uuid' */
+struct afb_session *afb_session_search (const char *uuid)
 {
        struct afb_session *session;
+
+       sessionset_lock();
+       sessionset_cleanup(0);
+       session = sessionset_search(uuid, pearson4(uuid));
+       session = afb_session_addref(session);
+       sessionset_unlock();
+       return session;
+
+}
+
+/**
+ * Creates a new session with 'timeout'
+ */
+struct afb_session *afb_session_create (int timeout)
+{
+       return afb_session_get(NULL, timeout, NULL);
+}
+
+/**
+ * Returns the timeout of 'session' in seconds
+ */
+int afb_session_timeout(struct afb_session *session)
+{
+       int timeout;
+
+       /* compute timeout */
+       timeout = session->timeout;
+       if (timeout == AFB_SESSION_TIMEOUT_DEFAULT)
+               timeout = sessions.timeout;
+       if (timeout < 0)
+               timeout = INT_MAX;
+       return timeout;
+}
+
+/**
+ * Returns the second remaining before expiration of 'session'
+ */
+int afb_session_what_remains(struct afb_session *session)
+{
+       int diff = (int)(session->expiration - NOW);
+       return diff < 0 ? 0 : diff;
+}
+
+/* This function will return exiting session or newly created session */
+struct afb_session *afb_session_get (const char *uuid, int timeout, int *created)
+{
+       uuid_stringz_t _uuid_;
+       uint8_t hashidx;
+       struct afb_session *session;
        time_t now;
+       int c;
 
        /* cleaning */
-       now = NOW;
-       cleanup (now);
+       sessionset_lock();
+       now = sessionset_cleanup(0);
 
        /* search for an existing one not too old */
-       if (uuid != NULL) {
-               session = search(uuid);
-               if (session != NULL) {
-                       *created = 0;
-                       session->access = now;
-                       session->refcount++;
-                       return session;
+       if (!uuid) {
+               hashidx = sessionset_make_uuid(_uuid_);
+               uuid = _uuid_;
+       } else {
+               hashidx = pearson4(uuid);
+               session = sessionset_search(uuid, hashidx);
+               if (session) {
+                       /* session found */
+                       afb_session_addref(session);
+                       c = 0;
+                       goto end;
                }
        }
+       /* create the session */
+       session = session_add(uuid, timeout, now, hashidx);
+       c = 1;
+end:
+       sessionset_unlock();
+       if (created)
+               *created = c;
 
-       *created = 1;
-       return make_session(uuid, sessions.timeout, now);
+       return session;
 }
 
+/* increase the use count on 'session' (can be NULL) */
 struct afb_session *afb_session_addref(struct afb_session *session)
 {
-       if (session != NULL)
+       if (session != NULL) {
+#if WITH_AFB_HOOK
+               afb_hook_session_addref(session);
+#endif
+               session_lock(session);
                session->refcount++;
+               session_unlock(session);
+       }
        return session;
 }
 
+/* decrease the use count of 'session' (can be NULL) */
 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 (session == NULL)
+               return;
+
+#if WITH_AFB_HOOK
+       afb_hook_session_unref(session);
+#endif
+       session_lock(session);
+       if (!--session->refcount) {
+               if (session->autoclose)
+                       session_close(session);
+               if (session->notinset) {
+                       session_destroy(session);
+                       return;
                }
        }
+       session_unlock(session);
 }
 
-// Free Client Session Context
+/* close 'session' */
 void afb_session_close (struct afb_session *session)
 {
-       assert(session != NULL);
-       if (session->uuid[0] != 0) {
-               session->uuid[0] = 0;
-               free_data (session);
-               if (session->refcount == 0) {
-                       destroy (session);
-                       free(session);
-               }
-       }
+       session_lock(session);
+       session_close(session);
+       session_unlock(session);
 }
 
-// Sample Generic Ping Debug API
-int afb_session_check_token (struct afb_session *session, const char *token)
+/**
+ * Set the 'autoclose' flag of the 'session'
+ *
+ * A session whose autoclose flag is true will close as
+ * soon as it is no more referenced.
+ *
+ * @param session    the session to set
+ * @param autoclose  the value to set
+ */
+void afb_session_set_autoclose(struct afb_session *session, int autoclose)
 {
-       assert(session != NULL);
-       assert(token != NULL);
-
-       // compare current token with previous one
-       if (!is_active (session, NOW))
-               return 0;
-
-       if (session->token[0] && strcmp (token, session->token) != 0)
-               return 0;
-
-       return 1;
+       session->autoclose = !!autoclose;
 }
 
-// generate a new token and update client context
-void afb_session_new_token (struct afb_session *session)
+/* is 'session' closed? */
+int afb_session_is_closed (struct afb_session *session)
 {
-       assert(session != NULL);
-
-       // Old token was valid let's regenerate a new one
-       new_uuid(session->token);
-
-       // keep track of time for session timeout and further clean up
-       if (session->timeout != 0)
-               session->expiration = NOW + session->timeout;
+       return session->closed;
 }
 
+/* Returns the uuid of 'session' */
 const char *afb_session_uuid (struct afb_session *session)
 {
-       assert(session != NULL);
        return session->uuid;
 }
 
-const char *afb_session_token (struct afb_session *session)
+/* Returns the local id of 'session' */
+uint16_t afb_session_id (struct afb_session *session)
 {
-       assert(session != NULL);
-       return session->token;
+       return session->id;
 }
 
-void *afb_session_get_cookie(struct afb_session *session, const void *key)
+/**
+ * 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
+ */
+#if COOKIEMASK
+static int cookeyidx(const void *key)
+{
+       intptr_t x = (intptr_t)key;
+       unsigned r = (unsigned)((x >> 5) ^ (x >> 15));
+       return r & COOKIEMASK;
+}
+#else
+#  define cookeyidx(key) 0
+#endif
+
+/**
+ * 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 replacement 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)
 {
-       struct cookie *cookie;
        int idx;
+       void *value;
+       struct cookie *cookie, **prv;
 
+       /* get key hashed index */
        idx = cookeyidx(key);
-       cookie = session->cookies[idx];
-       while(cookie != NULL) {
-               if (cookie->key == key)
-                       return cookie->value;
-               cookie = cookie->next;
+
+       /* lock session and search for the cookie of 'key' */
+       session_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);
+
+                               /* if both value and freecb are NULL drop the cookie */
+                               if (!value && !freecb) {
+                                       *prv = cookie->next;
+                                       free(cookie);
+                               } else {
+                                       /* store the value and its releaser */
+                                       cookie->value = value;
+                                       cookie->freecb = freecb;
+                               }
+                       }
+                       break;
+               } else {
+                       prv = &(cookie->next);
+               }
        }
-       return NULL;
+
+       /* unlock the session and return the value */
+       session_unlock(session);
+       return value;
 }
 
+/**
+ * Get the cookie of 'key' in the 'session'.
+ *
+ * @param session  the session to search in
+ * @param key      the key of the data to retrieve
+ *
+ * @return the data staored for the key or NULL if the key isn't found
+ */
+void *afb_session_get_cookie(struct afb_session *session, const void *key)
+{
+       return afb_session_cookie(session, key, NULL, NULL, NULL, 0);
+}
+
+/**
+ * Set the cookie of 'key' in the 'session' to the 'value' that can be
+ * cleaned using 'freecb' (if not null).
+ *
+ * @param session  the session to set
+ * @param key      the key of the data to store
+ * @param value    the value to store at key
+ * @param freecb   a function to use when the cookie value is to remove (or null)
+ *
+ * @return 0 in case of success or -1 in case of error
+ */
 int afb_session_set_cookie(struct afb_session *session, const void *key, void *value, void (*freecb)(void*))
 {
-       struct cookie *cookie;
-       int idx;
+       return -(value != afb_session_cookie(session, key, NULL, freecb, value, 1));
+}
 
-       /* search for a replacement */
-       idx = cookeyidx(key);
-       cookie = session->cookies[idx];
-       while(cookie != NULL) {
-               if (cookie->key == key) {
-                       if (cookie->value != value && cookie->freecb)
-                               cookie->freecb(cookie->value);
-                       cookie->value = value;
-                       cookie->freecb = freecb;
-                       return 0;
-               }
-               cookie = cookie->next;
-       }
+/**
+ * Set the language attached to the session
+ *
+ * @param session the session to set
+ * @param lang    the language specifiction to set to session
+ *
+ * @return 0 in case of success or -1 in case of error
+ */
+int afb_session_set_language(struct afb_session *session, const char *lang)
+{
+       char *oldl, *newl;
 
-       /* allocates */
-       cookie = malloc(sizeof *cookie);
-       if (cookie == NULL) {
-               errno = ENOMEM;
+       newl = strdup(lang);
+       if (newl == NULL)
                return -1;
-       }
 
-       cookie->key = key;
-       cookie->value = value;
-       cookie->freecb = freecb;
-       cookie->next = session->cookies[idx];
-       session->cookies[idx] = cookie;
+       oldl = session->lang;
+       session->lang = newl;
+       free(oldl);
        return 0;
 }
 
+/**
+ * Get the language attached to the session
+ *
+ * @param session the session to query
+ * @param lang    a default language specifiction
+ *
+ * @return the langauage specification to use for session
+ */
+const char *afb_session_get_language(struct afb_session *session, const char *lang)
+{
+       return session->lang ?: lang;
+}