MEDIUM: ssl: add shared memory session cache implementation.

This SSL session cache was developped at Exceliance and is the same that
was proposed for stunnel and stud. It makes use of a shared memory area
between the processes so that sessions can be handled by any process. It
is only useful when haproxy runs with nbproc > 1, but it does not hurt
performance at all with nbproc = 1. The aim is to totally replace OpenSSL's
internal cache.

The cache is optimized for Linux >= 2.6 and specifically for x86 platforms.
On Linux/x86, it makes use of futexes for inter-process locking, with some
x86 assembly for the locked instructions. On other architectures, GCC
builtins are used instead, which are available starting from gcc 4.1.

On other operating systems, the locks fall back to pthread mutexes so
libpthread is automatically linked. It is not recommended since pthreads
are much slower than futexes. The lib is only linked if SSL is enabled.
diff --git a/Makefile b/Makefile
index e927fca..3c8bf3a 100644
--- a/Makefile
+++ b/Makefile
@@ -26,6 +26,7 @@
 #   USE_VSYSCALL         : enable vsyscall on Linux x86, bypassing libc
 #   USE_GETADDRINFO      : use getaddrinfo() to resolve IPv6 host names.
 #   USE_OPENSSL          : enable use of OpenSSL. Recommended, but see below.
+#   USE_FUTEX            : enable use of futex on kernel 2.6. Automatic.
 #
 # Options can be forced by specifying "USE_xxx=1" or can be disabled by using
 # "USE_xxx=" (empty string).
@@ -220,6 +221,7 @@
   USE_SEPOLL      = implicit
   USE_TPROXY      = implicit
   USE_LIBCRYPT    = implicit
+  USE_FUTEX       = implicit
 else
 ifeq ($(TARGET),linux2628)
   # This is for standard Linux >= 2.6.28 with netfilter, epoll, tproxy and splice
@@ -232,6 +234,7 @@
   USE_LIBCRYPT    = implicit
   USE_LINUX_SPLICE= implicit
   USE_LINUX_TPROXY= implicit
+  USE_FUTEX       = implicit
 else
 ifeq ($(TARGET),solaris)
   # This is for Solaris 8
@@ -471,7 +474,12 @@
 # in the standard path.
 OPTIONS_CFLAGS  += -DUSE_OPENSSL
 OPTIONS_LDFLAGS += -lssl
-OPTIONS_OBJS  += src/ssl_sock.o
+OPTIONS_OBJS  += src/ssl_sock.o src/shctx.o
+ifneq ($(USE_FUTEX),)
+OPTIONS_CFLAGS  += -DUSE_SYSCALL_FUTEX
+else
+OPTIONS_LDFLAGS += -lpthread
+endif
 endif
 
 ifneq ($(USE_PCRE),)
diff --git a/include/proto/shctx.h b/include/proto/shctx.h
new file mode 100644
index 0000000..000cc05
--- /dev/null
+++ b/include/proto/shctx.h
@@ -0,0 +1,69 @@
+/*
+ * shctx.h - shared context management functions for SSL
+ *
+ * Copyright (C) 2011-2012 EXCELIANCE
+ *
+ * Author: Emeric Brun - emeric@exceliance.fr
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version
+ * 2 of the License, or (at your option) any later version.
+ */
+
+#ifndef SHCTX_H
+#define SHCTX_H
+#include <openssl/ssl.h>
+#include <stdint.h>
+
+#ifndef SHSESS_MAX_FOOTER_LEN
+#define SHSESS_MAX_FOOTER_LEN sizeof(uint32_t) \
+				+ EVP_MAX_MD_SIZE
+#endif
+
+#ifndef SHSESS_MAX_DATA_LEN
+#define SHSESS_MAX_DATA_LEN 512
+#endif
+
+#ifndef SHCTX_DEFAULT_SIZE
+#define SHCTX_DEFAULT_SIZE 20000
+#endif
+
+#define SHSESS_MAX_ENCODED_LEN SSL_MAX_SSL_SESSION_ID_LENGTH \
+				+ SHSESS_MAX_DATA_LEN \
+				+ SHSESS_MAX_FOOTER_LEN
+
+
+
+/* Callback called on a new session event:
+ * session contains the sessionid zeros padded to SSL_MAX_SSL_SESSION_ID_LENGTH
+ *                                               followed by ASN1 session encoding.
+ * len is set to SSL_MAX_SSL_SESSION_ID_LENGTH + ASN1 session length
+ * len is always less than SSL_MAX_SSL_SESSION_ID_LENGTH + SHSESS_MAX_DATA_LEN.
+ * Remaining Bytes from len to SHSESS_MAX_ENCODED_LEN can be used to add a footer.
+ * cdate is the creation date timestamp.
+ */
+void shsess_set_new_cbk(void (*func)(unsigned char *session, unsigned int len, long cdate));
+
+/* Add a session into the cache,
+ * session contains the sessionid zeros padded to SSL_MAX_SSL_SESSION_ID_LENGTH
+ *                                             followed by ASN1 session encoding.
+ * len is set to SSL_MAX_SSL_SESSION_ID_LENGTH + ASN1 data length.
+ *            if len greater than SHSESS_MAX_ENCODED_LEN, session is not added.
+ * if cdate not 0, on get events session creation date will be reset to cdate */
+void shctx_sess_add(const unsigned char *session, unsigned int session_len, long cdate);
+
+/* Allocate shared memory context.
+ * size is maximum cached sessions.
+ *      if set less or equal to 0, SHCTX_DEFAULT_SIZE is used.
+ * Returns: -1 on alloc failure, size if it performs context alloc,
+ * and 0 if cache is already allocated */
+int shared_context_init(int size);
+
+/* Set shared cache callbacks on an ssl context.
+ * Set session cache mode to server and disable openssl internal cache.
+ * Shared context MUST be firstly initialized */
+void shared_context_set_cache(SSL_CTX *ctx);
+
+#endif /* SHCTX_H */
+
diff --git a/src/shctx.c b/src/shctx.c
new file mode 100644
index 0000000..5fe2e7e
--- /dev/null
+++ b/src/shctx.c
@@ -0,0 +1,424 @@
+/*
+ * shctx.c - shared context management functions for SSL
+ *
+ * Copyright (C) 2011-2012 EXCELIANCE
+ *
+ * Author: Emeric Brun - emeric@exceliance.fr
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version
+ * 2 of the License, or (at your option) any later version.
+ */
+
+#include <sys/mman.h>
+#ifdef USE_SYSCALL_FUTEX
+#include <unistd.h>
+#include <linux/futex.h>
+#include <sys/syscall.h>
+#else /* USE_SYSCALL_FUTEX */
+#include <pthread.h>
+#endif /* USE_SYSCALL_FUTEX */
+
+#include "ebmbtree.h"
+#include "proto/shctx.h"
+
+struct shared_session {
+	struct ebmb_node key;
+	unsigned char key_data[SSL_MAX_SSL_SESSION_ID_LENGTH];
+	long c_date;
+	int data_len;
+	unsigned char data[SHSESS_MAX_DATA_LEN];
+	struct shared_session *p;
+	struct shared_session *n;
+};
+
+
+struct shared_context {
+#ifdef USE_SYSCALL_FUTEX
+	unsigned int waiters;
+#else /* USE_SYSCALL_FUTEX */
+	pthread_mutex_t mutex;
+#endif
+	struct shared_session active;
+	struct shared_session free;
+};
+
+/* Static shared context */
+static struct shared_context *shctx = NULL;
+
+/* Callbacks */
+static void (*shared_session_new_cbk)(unsigned char *session, unsigned int session_len, long cdate);
+
+
+/* Lock functions */
+#ifdef USE_SYSCALL_FUTEX
+#if defined (__i586__) || defined (__x86_64__)
+static inline unsigned int xchg(unsigned int *ptr, unsigned int x)
+{
+	__asm volatile("lock xchgl %0,%1"
+		     : "=r" (x), "+m" (*ptr)
+		     : "0" (x)
+		     : "memory");
+	return x;
+}
+
+static inline unsigned int cmpxchg(unsigned int *ptr, unsigned int old, unsigned int new)
+{
+	unsigned int ret;
+
+	__asm volatile("lock cmpxchgl %2,%1"
+		     : "=a" (ret), "+m" (*ptr)
+		     : "r" (new), "0" (old)
+		     : "memory");
+	return ret;
+}
+
+static inline unsigned char atomic_dec(unsigned int *ptr)
+{
+	unsigned char ret;
+	__asm volatile("lock decl %0\n"
+		     "setne %1\n"
+		     : "+m" (*ptr), "=qm" (ret)
+		     :
+		     : "memory");
+	return ret;
+}
+
+#else /* if no x86_64 or i586 arch: use less optimized gcc >= 4.1 built-ins */
+static inline unsigned int xchg(unsigned int *ptr, unsigned int x)
+{
+	return __sync_lock_test_and_set(ptr, x);
+}
+
+static inline unsigned int cmpxchg(unsigned int *ptr, unsigned int old, unsigned int new)
+{
+	return __sync_val_compare_and_swap(ptr, old, new);
+}
+
+static inline unsigned char atomic_dec(unsigned int *ptr)
+{
+	return __sync_sub_and_fetch(ptr, 1) ? 1 : 0;
+}
+
+#endif
+
+static inline void shared_context_lock(void)
+{
+	unsigned int x;
+
+	x = cmpxchg(&shctx->waiters, 0, 1);
+	if (x) {
+		if (x != 2)
+			x = xchg(&shctx->waiters, 2);
+
+		while (x) {
+			syscall(SYS_futex, &shctx->waiters, FUTEX_WAIT, 2, NULL, 0, 0);
+			x = xchg(&shctx->waiters, 2);
+		}
+	}
+}
+
+static inline void shared_context_unlock(void)
+{
+	if (atomic_dec(&shctx->waiters)) {
+		shctx->waiters = 0;
+		syscall(SYS_futex, &shctx->waiters, FUTEX_WAKE, 1, NULL, 0, 0);
+	}
+}
+
+#else /* USE_SYSCALL_FUTEX */
+
+#define shared_context_lock(v) pthread_mutex_lock(&shctx->mutex)
+#define shared_context_unlock(v) pthread_mutex_unlock(&shctx->mutex)
+
+#endif
+
+/* List Macros */
+
+#define shsess_unset(s)		(s)->n->p = (s)->p; \
+				(s)->p->n = (s)->n;
+
+#define shsess_set_free(s)	shsess_unset(s) \
+				(s)->p = &shctx->free; \
+				(s)->n = shctx->free.n; \
+				shctx->free.n->p = s; \
+				shctx->free.n = s;
+
+
+#define shsess_set_active(s)	shsess_unset(s) \
+				(s)->p = &shctx->active; \
+				(s)->n = shctx->active.n; \
+				shctx->active.n->p = s; \
+				shctx->active.n = s;
+
+
+#define shsess_get_next()	(shctx->free.p == shctx->free.n) ? \
+				     shctx->active.p : shctx->free.p;
+
+/* Tree Macros */
+
+#define shsess_tree_delete(s)	ebmb_delete(&(s)->key);
+
+#define shsess_tree_insert(s)	(struct shared_session *)ebmb_insert(&shctx->active.key.node.branches, \
+								     &(s)->key, SSL_MAX_SSL_SESSION_ID_LENGTH);
+
+#define shsess_tree_lookup(k)	(struct shared_session *)ebmb_lookup(&shctx->active.key.node.branches, \
+								     (k), SSL_MAX_SSL_SESSION_ID_LENGTH);
+
+/* Other Macros */
+
+#define shsess_set_key(s,k,l)	{ memcpy((s)->key_data, (k), (l)); \
+				  if ((l) < SSL_MAX_SSL_SESSION_ID_LENGTH) \
+					memset((s)->key_data+(l), 0, SSL_MAX_SSL_SESSION_ID_LENGTH-(l)); };
+
+
+/* SSL context callbacks */
+
+/* SSL callback used on new session creation */
+int shctx_new_cb(SSL *ssl, SSL_SESSION *sess)
+{
+	struct shared_session *shsess;
+	unsigned char *data,*p;
+	unsigned int data_len;
+	unsigned char encsess[SHSESS_MAX_ENCODED_LEN];
+	(void)ssl;
+
+	/* check if session reserved size in aligned buffer is large enougth for the ASN1 encode session */
+	data_len=i2d_SSL_SESSION(sess, NULL);
+	if(data_len > SHSESS_MAX_DATA_LEN)
+		return 0;
+
+	/* process ASN1 session encoding before the lock: lower cost */
+	p = data = encsess+SSL_MAX_SSL_SESSION_ID_LENGTH;
+	i2d_SSL_SESSION(sess, &p);
+
+	shared_context_lock();
+
+	shsess = shsess_get_next();
+
+	shsess_tree_delete(shsess);
+
+	shsess_set_key(shsess, sess->session_id, sess->session_id_length);
+
+	/* it returns the already existing node or current node if none, never returns null */
+	shsess = shsess_tree_insert(shsess);
+
+	/* store ASN1 encoded session into cache */
+	shsess->data_len = data_len;
+	memcpy(shsess->data, data, data_len);
+
+	/* store creation date */
+	shsess->c_date = SSL_SESSION_get_time(sess);
+
+	shsess_set_active(shsess);
+
+	shared_context_unlock();
+
+	if (shared_session_new_cbk) { /* if user level callback is set */
+		/* copy sessionid padded with 0 into the sessionid + data aligned buffer */
+		memcpy(encsess, sess->session_id, sess->session_id_length);
+		if (sess->session_id_length < SSL_MAX_SSL_SESSION_ID_LENGTH)
+			memset(encsess+sess->session_id_length, 0, SSL_MAX_SSL_SESSION_ID_LENGTH-sess->session_id_length);
+
+		shared_session_new_cbk(encsess, SSL_MAX_SSL_SESSION_ID_LENGTH+data_len, SSL_SESSION_get_time(sess));
+	}
+
+	return 0; /* do not increment session reference count */
+}
+
+/* SSL callback used on lookup an existing session cause none found in internal cache */
+SSL_SESSION *shctx_get_cb(SSL *ssl, unsigned char *key, int key_len, int *do_copy)
+{
+	struct shared_session *shsess;
+	unsigned char data[SHSESS_MAX_DATA_LEN], *p;
+	unsigned char tmpkey[SSL_MAX_SSL_SESSION_ID_LENGTH];
+	unsigned int data_len;
+	long cdate;
+	SSL_SESSION *sess;
+	(void)ssl;
+
+	/* allow the session to be freed automatically by openssl */
+	*do_copy = 0;
+
+	/* tree key is zeros padded sessionid */
+	if (key_len < SSL_MAX_SSL_SESSION_ID_LENGTH) {
+		memcpy(tmpkey, key, key_len);
+		memset(tmpkey + key_len, 0, SSL_MAX_SSL_SESSION_ID_LENGTH - key_len);
+		key = tmpkey;
+	}
+
+	/* lock cache */
+	shared_context_lock();
+
+	/* lookup for session */
+	shsess = shsess_tree_lookup(key);
+	if (!shsess) {
+		/* no session found: unlock cache and exit */
+		shared_context_unlock();
+		return NULL;
+	}
+
+	/* backup creation date to reset in session after ASN1 decode */
+	cdate = shsess->c_date;
+
+	/* copy ASN1 session data to decode outside the lock */
+	data_len = shsess->data_len;
+	memcpy(data, shsess->data, shsess->data_len);
+
+	shsess_set_active(shsess);
+
+	shared_context_unlock();
+
+	/* decode ASN1 session */
+	p = data;
+	sess = d2i_SSL_SESSION(NULL, (const unsigned char **)&p, data_len);
+
+	/* reset creation date */
+	if (sess)
+		SSL_SESSION_set_time(sess, cdate);
+
+	return sess;
+}
+
+/* SSL callback used to signal session is no more used in internal cache */
+void shctx_remove_cb(SSL_CTX *ctx, SSL_SESSION *sess)
+{
+	struct shared_session *shsess;
+	unsigned char tmpkey[SSL_MAX_SSL_SESSION_ID_LENGTH];
+	unsigned char *key = sess->session_id;
+	(void)ctx;
+
+	/* tree key is zeros padded sessionid */
+	if (sess->session_id_length < SSL_MAX_SSL_SESSION_ID_LENGTH) {
+		memcpy(tmpkey, sess->session_id, sess->session_id_length);
+		memset(tmpkey+sess->session_id_length, 0, SSL_MAX_SSL_SESSION_ID_LENGTH - sess->session_id_length);
+		key = tmpkey;
+	}
+
+	shared_context_lock();
+
+	/* lookup for session */
+	shsess = shsess_tree_lookup(key);
+	if (shsess) {
+		shsess_set_free(shsess);
+	}
+
+	/* unlock cache */
+	shared_context_unlock();
+}
+
+/* User level function called to add a session to the cache (remote updates) */
+void shctx_sess_add(const unsigned char *encsess, unsigned int len, long cdate)
+{
+	struct shared_session *shsess;
+
+	/* check buffer is at least padded key long + 1 byte
+		and data_len not too long */
+	if ((len <= SSL_MAX_SSL_SESSION_ID_LENGTH)
+		 || (len > SHSESS_MAX_DATA_LEN+SSL_MAX_SSL_SESSION_ID_LENGTH))
+		return;
+
+	shared_context_lock();
+
+	shsess = shsess_get_next();
+
+	shsess_tree_delete(shsess);
+
+	shsess_set_key(shsess, encsess, SSL_MAX_SSL_SESSION_ID_LENGTH);
+
+	/* it returns the already existing node or current node if none, never returns null */
+	shsess = shsess_tree_insert(shsess);
+
+	/* store into cache and update earlier on session get events */
+	if (cdate)
+		shsess->c_date = (long)cdate;
+
+	/* copy ASN1 session data into cache */
+	shsess->data_len = len-SSL_MAX_SSL_SESSION_ID_LENGTH;
+	memcpy(shsess->data, encsess+SSL_MAX_SSL_SESSION_ID_LENGTH, shsess->data_len);
+
+	shsess_set_active(shsess);
+
+	shared_context_unlock();
+}
+
+/* Function used to set a callback on new session creation */
+void shsess_set_new_cbk(void (*func)(unsigned char *, unsigned int, long))
+{
+	shared_session_new_cbk = func;
+}
+
+/* Allocate shared memory context.
+ * size is maximum cached sessions.
+ * if set less or equal to 0, SHCTX_DEFAULT_SIZE is used.
+ * Returns: -1 on alloc failure, size if it performs context alloc,
+ * and 0 if cache is already allocated */
+int shared_context_init(int size)
+{
+	int i;
+#ifndef USE_SYSCALL_FUTEX
+	pthread_mutexattr_t attr;
+#endif /* USE_SYSCALL_FUTEX */
+	struct shared_session *prev,*cur;
+
+	if (shctx)
+		return 0;
+
+	if (size<=0)
+		size = SHCTX_DEFAULT_SIZE;
+
+	shctx = (struct shared_context *)mmap(NULL, sizeof(struct shared_context)+(size*sizeof(struct shared_session)),
+	                                      PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+	if (!shctx || shctx == MAP_FAILED) {
+		shctx = NULL;
+		return -1;
+	}
+
+#ifdef USE_SYSCALL_FUTEX
+	shctx->waiters = 0;
+#else
+	pthread_mutexattr_init(&attr);
+	pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
+	pthread_mutex_init(&shctx->mutex, &attr);
+#endif
+	memset(&shctx->active.key, 0, sizeof(struct ebmb_node));
+	memset(&shctx->free.key, 0, sizeof(struct ebmb_node));
+
+	/* No duplicate authorized in tree: */
+	//shctx->active.key.node.branches.b[1] = (void *)1;
+	shctx->active.key.node.branches = EB_ROOT_UNIQUE;
+
+	cur = &shctx->active;
+	cur->n = cur->p = cur;
+
+	cur = &shctx->free;
+	for (i = 0 ; i < size ; i++) {
+		prev = cur;
+		cur = (struct shared_session *)((char *)prev + sizeof(struct shared_session));
+		prev->n = cur;
+		cur->p = prev;
+	}
+	cur->n = &shctx->free;
+	shctx->free.p = cur;
+
+	return size;
+}
+
+
+/* Set session cache mode to server and disable openssl internal cache.
+ * Set shared cache callbacks on an ssl context.
+ * Shared context MUST be firstly initialized */
+void shared_context_set_cache(SSL_CTX *ctx)
+{
+	SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER |
+	                                    SSL_SESS_CACHE_NO_INTERNAL |
+	                                    SSL_SESS_CACHE_NO_AUTO_CLEAR);
+	if (!shctx)
+		return;
+
+	/* Set callbacks */
+	SSL_CTX_sess_set_new_cb(ctx, shctx_new_cb);
+	SSL_CTX_sess_set_get_cb(ctx, shctx_get_cb);
+	SSL_CTX_sess_set_remove_cb(ctx, shctx_remove_cb);
+}