Added new session feature for HSTS (#79).
authorDaniel Carl <danielcarl@gmx.de>
Wed, 23 Apr 2014 21:47:43 +0000 (23:47 +0200)
committerDaniel Carl <danielcarl@gmx.de>
Thu, 24 Apr 2014 23:04:45 +0000 (01:04 +0200)
The main stuff for the new session feature to handle HTTP strict Transport
Security is added.

config.mk
src/hsts.c [new file with mode: 0644]
src/hsts.h [new file with mode: 0644]
src/main.c
src/main.h
src/session.c
src/session.h

index ed2e551..bb4a95d 100644 (file)
--- a/config.mk
+++ b/config.mk
@@ -40,8 +40,8 @@ LIBFLAGS = $(shell pkg-config --libs $(LIBS))
 
 # normal compiler flags
 CFLAGS  += $(shell pkg-config --cflags $(LIBS))
-CFLAGS  += -Wall -pipe -ansi -std=c99 -pedantic
-CFLAGS  += -Wno-overlength-strings
+CFLAGS  += -Wall -pipe -ansi -std=c99
+CFLAGS  += -Wno-overlength-strings -Werror=format-security
 CFLAGS  += ${CPPFLAGS}
 LDFLAGS += ${LIBFLAGS}
 
diff --git a/src/hsts.c b/src/hsts.c
new file mode 100644 (file)
index 0000000..3207c26
--- /dev/null
@@ -0,0 +1,317 @@
+/**
+ * vimb - a webkit based vim like browser.
+ *
+ * Copyright (C) 2012-2014 Daniel Carl
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+
+#include "config.h"
+#include "hsts.h"
+#include <string.h>
+#include <glib-object.h>
+#include <libsoup/soup.h>
+
+#define HSTS_HEADER_NAME "Strict-Transport-Security"
+#define HSTS_PROVIDER_GET_PRIVATE(o)  (G_TYPE_INSTANCE_GET_PRIVATE((o), HSTS_TYPE_PROVIDER, HSTSProviderPrivate))
+
+/* private interface of the provider */
+typedef struct _HSTSProviderPrivate {
+    GHashTable* whitelist;
+} HSTSProviderPrivate;
+
+typedef struct {
+    gint64   expires_at;
+    gboolean include_sub_domains;
+} HSTSEntry;
+
+static void hsts_provider_class_init(HSTSProviderClass *klass);
+static void hsts_provider_init(HSTSProvider *self);
+static void hsts_provider_finalize(GObject* obj);
+static gboolean should_secure_host(HSTSProvider *provider,
+    const char *host);
+static void process_hsts_header(SoupMessage *msg, gpointer data);
+static void parse_hsts_header(HSTSProvider *provider,
+    const char *host, const char *header);
+static HSTSEntry *get_new_entry(gint64 max_age, gboolean include_sub_domains);
+static void add_host_entry(HSTSProvider *provider, const char *host,
+    HSTSEntry *entry);
+static void remove_host_entry(HSTSProvider *provider, const char *host);
+/* session feature related functions */
+static void session_feature_init(
+    SoupSessionFeatureInterface *inteface, gpointer data);
+static void request_queued(SoupSessionFeature *feature,
+    SoupSession *session, SoupMessage *msg);
+static void request_started(SoupSessionFeature *feature,
+    SoupSession *session, SoupMessage *msg, SoupSocket *socket);
+static void request_unqueued(SoupSessionFeature *feature,
+    SoupSession *session, SoupMessage *msg);
+
+
+/**
+ * Generates a new hsts provider instance.
+ * Unref the instance with g_object_unref if no more used.
+ */
+HSTSProvider *hsts_provider_new(void)
+{
+    return g_object_new(HSTS_TYPE_PROVIDER, NULL);
+}
+
+G_DEFINE_TYPE_WITH_CODE(
+    HSTSProvider, hsts_provider, G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE(SOUP_TYPE_SESSION_FEATURE, session_feature_init)
+)
+
+static void hsts_provider_class_init(HSTSProviderClass *klass)
+{
+    hsts_provider_parent_class = g_type_class_peek_parent(klass);
+    g_type_class_add_private(klass, sizeof(HSTSProviderPrivate));
+    G_OBJECT_CLASS(klass)->finalize = hsts_provider_finalize;
+}
+
+static void hsts_provider_init(HSTSProvider *self)
+{
+    /* Initialize private fields */
+    HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(self);
+    priv->whitelist = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+}
+
+static void hsts_provider_finalize(GObject* obj)
+{
+    HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE (obj);
+
+    g_hash_table_destroy(priv->whitelist);
+    G_OBJECT_CLASS(hsts_provider_parent_class)->finalize(obj);
+}
+
+/**
+ * Checks if given host is a known https host according to RFC 6797 8.3
+ */
+static gboolean should_secure_host(HSTSProvider *provider,
+    const char *host)
+{
+    HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider);
+    char *canonical, *p;
+    gboolean result = false, is_subdomain = false;
+
+    /* ip is not allowed for hsts */
+    if (g_hostname_is_ip_address(host)) {
+        return false;
+    }
+
+    canonical = g_hostname_to_ascii(host);
+    /* don't match empty host */
+    if (*canonical) {
+        p = canonical;
+        while (p != NULL) {
+            HSTSEntry *entry = g_hash_table_lookup(priv->whitelist, p);
+            if (entry != NULL) {
+                /* remove expired entries */
+                if (g_get_real_time() > entry->expires_at) {
+                    remove_host_entry(provider, p);
+                } else if(!is_subdomain || entry->include_sub_domains) {
+                    result = true;
+                    break;
+                }
+            }
+
+            is_subdomain = true;
+            /* test without the first domain part */
+            if ((p = strchr(p, '.'))) {
+                p++;
+            }
+        }
+    }
+    g_free(canonical);
+
+    return result;
+}
+
+static void process_hsts_header(SoupMessage *msg, gpointer data)
+{
+    HSTSProvider *provider = (HSTSProvider*)data;
+    SoupURI *uri           = soup_message_get_uri(msg);
+    const char *header, *host = soup_uri_get_host(uri);
+    SoupMessageHeaders *hdrs;
+
+    if (!g_hostname_is_ip_address(host)
+        && (soup_message_get_flags(msg) & SOUP_MESSAGE_CERTIFICATE_TRUSTED)
+    ){
+        g_object_get(G_OBJECT(msg), SOUP_MESSAGE_RESPONSE_HEADERS, &hdrs, NULL);
+
+        /* TODO according to RFC 6797 8.1 we must only use the first header */
+        header = soup_message_headers_get_one(hdrs, HSTS_HEADER_NAME);
+        if (header) {
+            parse_hsts_header(provider, host, header);
+        }
+    }
+}
+
+/**
+ * Parses the hsts directives from given header like specified in RFC 6797 6.1
+ */
+static void parse_hsts_header(HSTSProvider *provider,
+    const char *host, const char *header)
+{
+    GHashTable *directives = soup_header_parse_semi_param_list(header);
+    gint64 max_age = 0;
+    gboolean include_sub_domains = false;
+    GHashTableIter iter;
+    gpointer key, value;
+    gboolean success = true;
+
+    HSTSProviderClass *klass = g_type_class_ref(HSTS_TYPE_PROVIDER);
+
+    g_hash_table_iter_init(&iter, directives);
+    while (g_hash_table_iter_next(&iter, &key, &value)) {
+        /* parse the max-age directive */
+        if (!g_ascii_strncasecmp(key, "max-age", 7)) {
+            /* max age needs a value */
+            if (value) {
+                max_age = g_ascii_strtoll(value, NULL, 10);
+                if (max_age < 0) {
+                    success = false;
+                    break;
+                }
+            } else {
+                success = false;
+                break;
+            }
+        } else if (g_ascii_strncasecmp(key, "includeSubDomains", 17)) {
+            /* includeSubDomains must not have a value */
+            if (!value) {
+                include_sub_domains = true;
+            } else {
+                success = false;
+                break;
+            }
+        }
+    }
+    soup_header_free_param_list(directives);
+    g_type_class_unref(klass);
+
+    if (success) {
+        /* remove host if max-age = 0 RFC 6797 6.1.1 */
+        if (max_age == 0) {
+            remove_host_entry(provider, host);
+        } else {
+            add_host_entry(provider, host, get_new_entry(max_age, include_sub_domains));
+        }
+    }
+}
+
+/**
+ * Create a new hsts entry for given data.
+ * Returned entry have to be freed if no more used.
+ */
+static HSTSEntry *get_new_entry(gint64 max_age, gboolean include_sub_domains)
+{
+    HSTSEntry *entry  = g_new(HSTSEntry, 1);
+    entry->expires_at = g_get_real_time();
+    if (max_age > (G_MAXINT64 - entry->expires_at)/G_USEC_PER_SEC) {
+        entry->expires_at = G_MAXINT64;
+    } else {
+        entry->expires_at += max_age * G_USEC_PER_SEC;
+    }
+    entry->include_sub_domains = include_sub_domains;
+
+    return entry;
+}
+
+
+/**
+ * Adds the host to the known host, if it already exists it replaces it with
+ * the information contained in entry according to RFC 6797 8.1.
+ */
+static void add_host_entry(HSTSProvider *provider, const char *host,
+    HSTSEntry *entry)
+{
+    if (g_hostname_is_ip_address(host)) {
+        return;
+    }
+
+    HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider);
+    g_hash_table_replace(priv->whitelist, g_hostname_to_unicode(host), entry);
+}
+
+/**
+ * Removes stored entry for given host.
+ */
+static void remove_host_entry(HSTSProvider *provider, const char *host)
+{
+    HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider);
+    char *canonical           = g_hostname_to_unicode(host);
+
+    g_hash_table_remove(priv->whitelist, canonical);
+    g_free(canonical);
+}
+
+/**
+ * Initialise the SoupSessionFeature interface.
+ */
+static void session_feature_init(
+    SoupSessionFeatureInterface *inteface, gpointer data)
+{
+    inteface->request_queued   = request_queued;
+    inteface->request_started  = request_started;
+    inteface->request_unqueued = request_unqueued;
+}
+
+/**
+ * Check if the host is known and switch the URI scheme to https.
+ */
+static void request_queued(SoupSessionFeature *feature,
+    SoupSession *session, SoupMessage *msg)
+{
+    HSTSProvider *provider = HSTS_PROVIDER(feature);
+    SoupURI *uri           = soup_message_get_uri(msg);
+    if (soup_uri_get_scheme(uri) == SOUP_URI_SCHEME_HTTP
+        && should_secure_host(provider, soup_uri_get_host(uri))
+    ) {
+        soup_uri_set_scheme(uri, SOUP_URI_SCHEME_HTTPS);
+        /* change port if the is explicitly set */
+        if (soup_uri_get_port(uri) == 80) {
+            soup_uri_set_port(uri, 443);
+        }
+        soup_session_requeue_message(session, msg);
+    }
+
+    /* Only look for HSTS headers sent over https */
+    if (soup_uri_get_scheme(uri) == SOUP_URI_SCHEME_HTTPS) {
+        soup_message_add_header_handler(
+            msg, "got-headers", HSTS_HEADER_NAME, G_CALLBACK(process_hsts_header), feature
+        );
+    }
+}
+
+static void request_started(SoupSessionFeature *feature,
+    SoupSession *session, SoupMessage *msg, SoupSocket *socket)
+{
+    HSTSProvider *provider = HSTS_PROVIDER(feature);
+    SoupURI *uri           = soup_message_get_uri(msg);
+    const char *host       = soup_uri_get_host(uri);
+    if (should_secure_host(provider, host)) {
+        if (soup_uri_get_scheme(uri) != SOUP_URI_SCHEME_HTTPS
+            || !(soup_message_get_flags(msg) & SOUP_MESSAGE_CERTIFICATE_TRUSTED)
+        ) {
+            soup_session_cancel_message(session, msg, SOUP_STATUS_SSL_FAILED);
+        }
+    }
+}
+
+static void request_unqueued(SoupSessionFeature *feature,
+    SoupSession *session, SoupMessage *msg)
+{
+    g_signal_handlers_disconnect_by_func(msg, process_hsts_header, feature);
+}
diff --git a/src/hsts.h b/src/hsts.h
new file mode 100644 (file)
index 0000000..3bb4030
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * vimb - a webkit based vim like browser.
+ *
+ * Copyright (C) 2012-2014 Daniel Carl
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+
+#ifndef _HSTS_H
+#define _HSTS_H
+
+#include <glib-object.h>
+
+#define HSTS_TYPE_PROVIDER            (hsts_provider_get_type())
+#define HSTS_PROVIDER(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), HSTS_TYPE_PROVIDER, HSTSProvider))
+#define HSTS_PROVIDER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), HSTS_TYPE_PROVIDER, HSTSProviderClass))
+#define HSTS_IS_PROVIDER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), HSTS_TYPE_PROVIDER))
+#define HSTS_IS_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), HSTS_TYPE_PROVIDER))
+#define HSTS_PROVIDER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), HSTS_TYPE_PROVIDER, HSTSProviderClass))
+
+#ifndef false
+    #define false FALSE
+#endif
+#ifndef true
+    #define true TRUE
+#endif
+
+/* public interface of the provider */
+typedef struct {
+    GObject parent_instance;
+} HSTSProvider;
+
+/* class members of the provider */
+typedef struct {
+    GObjectClass parent_class;
+} HSTSProviderClass;
+
+
+GType hsts_provider_get_type(void);
+HSTSProvider *hsts_provider_new(void);
+
+#endif /* end of include guard: _HSTS_H */
index 6149d5d..9ea0f8f 100644 (file)
@@ -371,6 +371,7 @@ void vb_quit(void)
     mode_cleanup();
     setting_cleanup();
     history_cleanup();
+    session_cleanup();
 
     for (int i = 0; i < FILES_LAST; i++) {
         g_free(vb.files[i]);
index 3e2eef9..83779ac 100644 (file)
@@ -201,6 +201,7 @@ typedef enum {
     FILES_QUEUE,
 #endif
     FILES_USER_STYLE,
+    FILES_HSTS,
     FILES_LAST
 } VbFile;
 
index 8b217c5..b56cda1 100644 (file)
@@ -21,6 +21,7 @@
 #include <sys/file.h>
 #include "main.h"
 #include "session.h"
+#include "hsts.h"
 
 #ifdef FEATURE_COOKIE
 
@@ -49,6 +50,7 @@ static void cookiejar_set_property(GObject *self, guint prop_id,
     const GValue *value, GParamSpec *pspec);
 #endif
 
+static HSTSProvider *hsts;
 extern VbCore vb;
 
 
@@ -66,6 +68,16 @@ void session_init(void)
         SOUP_SESSION_FEATURE(cookiejar_new(vb.files[FILES_COOKIE], false))
     );
 #endif
+    hsts = hsts_provider_new();
+    soup_session_add_feature(
+        vb.session,
+        SOUP_SESSION_FEATURE(hsts)
+    );
+}
+
+void session_cleanup(void)
+{
+    g_object_unref(hsts);
 }
 
 #ifdef FEATURE_COOKIE
index 5721bea..dcc4cf7 100644 (file)
@@ -21,5 +21,6 @@
 #define _SESSION_H
 
 void session_init(void);
+void session_cleanup(void);
 
 #endif /* end of include guard: _SESSION_H */