gnutls: add a session cache
authorDan Winship <danw@gnome.org>
Fri, 10 Dec 2010 14:55:21 +0000 (15:55 +0100)
committerDan Winship <danw@gnome.org>
Fri, 10 Dec 2010 14:55:21 +0000 (15:55 +0100)
TLS allows a client and server to bypass the full handshake on the
second and successive connections. Implement that, for the client
side, if the server is willing.

tls/gnutls/gtlsbackend-gnutls.c
tls/gnutls/gtlsbackend-gnutls.h
tls/gnutls/gtlsclientconnection-gnutls.c

index 9511ae8..da15b01 100644 (file)
@@ -211,6 +211,126 @@ g_tls_backend_gnutls_get_system_ca_list_gnutls (gnutls_x509_crt_t **cas,
 #endif
 }
 
+/* Session cache support; all the details are sort of arbitrary. Note
+ * that having session_cache_cleanup() be a little bit slow isn't the
+ * end of the world, since it will still be faster than the network
+ * is. (NSS uses a linked list for its cache...)
+ */
+
+G_LOCK_DEFINE_STATIC (session_cache_lock);
+GHashTable *session_cache;
+
+#define SESSION_CACHE_MAX_SIZE 50
+#define SESSION_CACHE_MAX_AGE (60 * 60) /* one hour */
+
+typedef struct {
+  gchar      *session_id;
+  GByteArray *session_data;
+  time_t      last_used;
+} GTlsBackendGnutlsCacheData;
+
+static void
+session_cache_cleanup (void)
+{
+  GHashTableIter iter;
+  gpointer key, value;
+  GTlsBackendGnutlsCacheData *cache_data;
+  time_t expired = time (NULL) - SESSION_CACHE_MAX_AGE;
+
+  g_hash_table_iter_init (&iter, session_cache);
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    {
+      cache_data = value;
+      if (cache_data->last_used < expired)
+       g_hash_table_iter_remove (&iter);
+    }
+}
+
+static void
+cache_data_free (gpointer data)
+{
+  GTlsBackendGnutlsCacheData *cache_data = data;
+
+  g_free (cache_data->session_id);
+  g_byte_array_unref (cache_data->session_data);
+  g_slice_free (GTlsBackendGnutlsCacheData, cache_data);
+}
+
+void
+g_tls_backend_gnutls_cache_session_data (const gchar *session_id,
+                                        guchar      *session_data,
+                                        gsize        session_data_length)
+{
+  GTlsBackendGnutlsCacheData *cache_data;
+
+  G_LOCK (session_cache_lock);
+
+  if (!session_cache)
+    session_cache = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                          NULL, cache_data_free);
+
+  cache_data = g_hash_table_lookup (session_cache, session_id);
+  if (cache_data)
+    {
+      if (cache_data->session_data->len == session_data_length &&
+         memcmp (cache_data->session_data->data,
+                 session_data, session_data_length) == 0)
+       {
+         cache_data->last_used = time (NULL);
+         G_UNLOCK (session_cache_lock);
+         return;
+       }
+
+      g_byte_array_set_size (cache_data->session_data, 0);
+    }
+  else
+    {
+      if (g_hash_table_size (session_cache) >= SESSION_CACHE_MAX_SIZE)
+       session_cache_cleanup ();
+
+      cache_data = g_slice_new (GTlsBackendGnutlsCacheData);
+      cache_data->session_id = g_strdup (session_id);
+      cache_data->session_data = g_byte_array_sized_new (session_data_length);
+
+      g_hash_table_insert (session_cache, cache_data->session_id, cache_data);
+    }
+
+  g_byte_array_append (cache_data->session_data,
+                      session_data, session_data_length);
+  cache_data->last_used = time (NULL);
+  G_UNLOCK (session_cache_lock);
+}
+
+void
+g_tls_backend_gnutls_uncache_session_data (const gchar *session_id)
+{
+  G_LOCK (session_cache_lock);
+  if (session_cache)
+    g_hash_table_remove (session_cache, session_id);
+  G_UNLOCK (session_cache_lock);
+}
+
+GByteArray *
+g_tls_backend_gnutls_lookup_session_data (const gchar *session_id)
+{
+  GTlsBackendGnutlsCacheData *cache_data;
+  GByteArray *session_data = NULL;
+
+  G_LOCK (session_cache_lock);
+  if (session_cache)
+    {
+      cache_data = g_hash_table_lookup (session_cache, session_id);
+      if (cache_data)
+       {
+         cache_data->last_used = time (NULL);
+         session_data = g_byte_array_ref (cache_data->session_data);
+       }
+    }
+  G_UNLOCK (session_cache_lock);
+
+  return session_data;
+}
+
 void
 g_tls_backend_gnutls_register (GIOModule *module)
 {
index 04e664b..97ebd90 100644 (file)
@@ -45,6 +45,12 @@ const GList *g_tls_backend_gnutls_get_system_ca_list_gtls   (void) G_GNUC_CONST;
 void         g_tls_backend_gnutls_get_system_ca_list_gnutls (gnutls_x509_crt_t **cas,
                                                             int                *num_cas);
 
+void         g_tls_backend_gnutls_cache_session_data        (const gchar *session_id,
+                                                            guchar      *session_data,
+                                                            gsize        session_data_length);
+void         g_tls_backend_gnutls_uncache_session_data      (const gchar *session_id);
+GByteArray  *g_tls_backend_gnutls_lookup_session_data       (const gchar *session_id);
+
 G_END_DECLS
 
 #endif /* __G_TLS_BACKEND_GNUTLS_H___ */
index 5b2fb95..888f7e7 100644 (file)
@@ -27,6 +27,7 @@
 #include <string.h>
 
 #include "gtlsclientconnection-gnutls.h"
+#include "gtlsbackend-gnutls.h"
 #include "gtlscertificate-gnutls.h"
 #include <glib/gi18n-lib.h>
 
@@ -47,6 +48,7 @@ static void g_tls_client_connection_gnutls_set_property (GObject      *object,
                                                         guint         prop_id,
                                                         const GValue *value,
                                                         GParamSpec   *pspec);
+static void g_tls_client_connection_gnutls_constructed  (GObject      *object);
 static void g_tls_client_connection_gnutls_finalize     (GObject      *object);
 
 static void     g_tls_client_connection_gnutls_begin_handshake  (GTlsConnectionGnutls  *conn);
@@ -75,6 +77,8 @@ struct _GTlsClientConnectionGnutlsPrivate
   GSocketConnectable *server_identity;
   gboolean use_ssl3;
 
+  char *session_id;
+
   gboolean cert_requested;
   char **accepted_cas;
 };
@@ -89,6 +93,7 @@ g_tls_client_connection_gnutls_class_init (GTlsClientConnectionGnutlsClass *klas
 
   gobject_class->get_property = g_tls_client_connection_gnutls_get_property;
   gobject_class->set_property = g_tls_client_connection_gnutls_set_property;
+  gobject_class->constructed  = g_tls_client_connection_gnutls_constructed;
   gobject_class->finalize     = g_tls_client_connection_gnutls_finalize;
 
   connection_gnutls_class->begin_handshake  = g_tls_client_connection_gnutls_begin_handshake;
@@ -118,6 +123,47 @@ g_tls_client_connection_gnutls_init (GTlsClientConnectionGnutls *gnutls)
 }
 
 static void
+g_tls_client_connection_gnutls_constructed (GObject *object)
+{
+  GTlsClientConnectionGnutls *gnutls = G_TLS_CLIENT_CONNECTION_GNUTLS (object);
+  GSocketConnection *base_conn;
+  GSocketAddress *remote_addr;
+  GInetAddress *iaddr;
+  guint port;
+
+  /* We base the session ID on the IP address rather than on
+   * server-identity, because it's likely that different virtual
+   * servers on the same host will have access to the same session
+   * cache, whereas different hosts serving the same hostname/service
+   * likely won't. Note that session IDs are opaque, and transmitted
+   * in the clear anyway, so there are no security issues if we send
+   * one to the "wrong" server; we'll just fail to get a resumed
+   * session.
+   */
+  g_object_get (G_OBJECT (gnutls), "base-io-stream", &base_conn, NULL);
+  if (G_IS_SOCKET_CONNECTION (base_conn))
+    {
+      remote_addr = g_socket_connection_get_remote_address (base_conn, NULL);
+      if (G_IS_INET_SOCKET_ADDRESS (remote_addr))
+       {
+         GInetSocketAddress *isaddr = G_INET_SOCKET_ADDRESS (remote_addr);
+         gchar *addrstr;
+
+         iaddr = g_inet_socket_address_get_address (isaddr);
+         port = g_inet_socket_address_get_port (isaddr);
+
+         addrstr = g_inet_address_to_string (iaddr);
+         gnutls->priv->session_id = g_strdup_printf ("%s/%d", addrstr, port);
+         g_free (addrstr);
+       }
+    }
+  g_object_unref (base_conn);
+
+  if (G_OBJECT_CLASS (g_tls_client_connection_gnutls_parent_class)->constructed)
+    G_OBJECT_CLASS (g_tls_client_connection_gnutls_parent_class)->constructed (object);
+}
+
+static void
 g_tls_client_connection_gnutls_finalize (GObject *object)
 {
   GTlsClientConnectionGnutls *gnutls = G_TLS_CLIENT_CONNECTION_GNUTLS (object);
@@ -126,6 +172,8 @@ g_tls_client_connection_gnutls_finalize (GObject *object)
     g_object_unref (gnutls->priv->server_identity);
   if (gnutls->priv->accepted_cas)
     g_strfreev (gnutls->priv->accepted_cas);
+  if (gnutls->priv->session_id)
+    g_free (gnutls->priv->session_id);
 
   G_OBJECT_CLASS (g_tls_client_connection_gnutls_parent_class)->finalize (object);
 }
@@ -245,6 +293,20 @@ g_tls_client_connection_gnutls_begin_handshake (GTlsConnectionGnutls *conn)
 {
   GTlsClientConnectionGnutls *gnutls = G_TLS_CLIENT_CONNECTION_GNUTLS (conn);
 
+  /* Try to get a cached session */
+  if (gnutls->priv->session_id)
+    {
+      GByteArray *session_data;
+
+      session_data = g_tls_backend_gnutls_lookup_session_data (gnutls->priv->session_id);
+      if (session_data)
+       {
+         gnutls_session_set_data (g_tls_connection_gnutls_get_session (conn),
+                                  session_data->data, session_data->len);
+         g_byte_array_unref (session_data);
+       }
+    }
+
   gnutls->priv->cert_requested = FALSE;
 }
 
@@ -283,4 +345,21 @@ g_tls_client_connection_gnutls_finish_handshake (GTlsConnectionGnutls  *conn,
       g_set_error_literal (inout_error, G_TLS_ERROR, G_TLS_ERROR_CERTIFICATE_REQUIRED,
                           _("Server required TLS certificate"));
     }
+
+  if (gnutls->priv->session_id)
+    {
+      gnutls_datum session_data;
+
+      if (!*inout_error &&
+         gnutls_session_get_data2 (g_tls_connection_gnutls_get_session (conn),
+                                   &session_data) == 0)
+       {
+         g_tls_backend_gnutls_cache_session_data (gnutls->priv->session_id,
+                                                  session_data.data,
+                                                  session_data.size);
+         gnutls_free (session_data.data);
+       }
+      else
+       g_tls_backend_gnutls_uncache_session_data (gnutls->priv->session_id);
+    }
 }