From: Daniel Carl Date: Mon, 28 Apr 2014 23:14:11 +0000 (+0200) Subject: Merge branch 'master' into feature/hsts X-Git-Url: https://git.owens.tech/rss.xml/rss.xml/git?a=commitdiff_plain;h=410474657f866a65a36d2ddc8f2e50a0cd09fad3;p=vimb.git Merge branch 'master' into feature/hsts --- 410474657f866a65a36d2ddc8f2e50a0cd09fad3 diff --cc src/hsts.c index 3d93ff6,0000000..670699d mode 100644,000000..100644 --- a/src/hsts.c +++ b/src/hsts.c @@@ -1,416 -1,0 +1,415 @@@ +/** + * 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" +#ifdef FEATURE_HSTS +#include "hsts.h" +#include "util.h" +#include "main.h" - #include +#include +#include +#include + +#define HSTS_HEADER_NAME "Strict-Transport-Security" +#define HSTS_FILE_FORMAT "%s\t%s\t%c\n" +#define HSTS_PROVIDER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), HSTS_TYPE_PROVIDER, HSTSProviderPrivate)) + +extern VbCore vb; + +/* private interface of the provider */ +typedef struct _HSTSProviderPrivate { + GHashTable* whitelist; +} HSTSProviderPrivate; + +typedef struct { + SoupDate *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 void free_entry(HSTSEntry *entry); +static void add_host_entry(HSTSProvider *provider, const char *host, + HSTSEntry *entry); +static void add_host_entry_to_file(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); +/* caching related functions */ +static void load_entries(HSTSProvider *provider, const char *file); +static void save_entries(HSTSProvider *provider, const char *file); + + +/** + * 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) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + hsts_provider_parent_class = g_type_class_peek_parent(klass); + g_type_class_add_private(klass, sizeof(HSTSProviderPrivate)); + object_class->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, (GDestroyNotify)free_entry); + + /* load entries from hsts file */ + load_entries(self, vb.files[FILES_HSTS]); +} + +static void hsts_provider_finalize(GObject* obj) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE (obj); + + /* save all the entries in hsts file */ + save_entries(HSTS_PROVIDER(obj), vb.files[FILES_HSTS]); + + 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.2f + */ +static gboolean should_secure_host(HSTSProvider *provider, + const char *host) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); + HSTSEntry *entry; + 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; + /* Try to find the whole congruent matching host in hash table - if + * not found strip of the first label and try to find a superdomain + * match. Specified is a from right to left comparison 8.3, but in the + * end this should be lead to the same result. */ + while (p != NULL) { + entry = g_hash_table_lookup(priv->whitelist, p); + if (entry != NULL) { + /* remove expired entries RFC 6797 8.1.1 */ + if (soup_date_is_past(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); + HSTSEntry *entry; + int max_age = G_MAXINT; + 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 { + entry = g_new(HSTSEntry, 1); + entry->expires_at = soup_date_new_from_now(max_age); + entry->include_sub_domains = include_sub_domains; + + add_host_entry(provider, host, entry); + add_host_entry_to_file(provider, host, entry); + } + } +} + +static void free_entry(HSTSEntry *entry) +{ + soup_date_free(entry->expires_at); + g_free(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) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); + g_hash_table_replace(priv->whitelist, g_hostname_to_unicode(host), entry); +} + +static void add_host_entry_to_file(HSTSProvider *provider, const char *host, + HSTSEntry *entry) +{ + char *date = soup_date_to_string(entry->expires_at, SOUP_DATE_ISO8601_FULL); + + util_file_append( + vb.files[FILES_HSTS], HSTS_FILE_FORMAT, host, date, entry->include_sub_domains ? 'y' : 'n' + ); + g_free(date); +} + +/** + * 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); + + /* only look for HSTS headers sent over https RFC 6797 7.2*/ + 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 + ); + } else if (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); + } +} + +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); +} + +/** + * Reads the entries save in given file and store them in the whitelist. + */ +static void load_entries(HSTSProvider *provider, const char *file) +{ + char **lines, **parts, *host, *line; + int i, len, partlen; + gboolean include_sub_domains; + SoupDate *date; + HSTSEntry *entry; + + lines = util_get_lines(file); + if (!(len = g_strv_length(lines))) { + return; + } + + for (i = len - 1; i >= 0; i--) { + line = lines[i]; + /* skip empty or commented lines */ + if (!*line || *line == '#') { + continue; + } + + parts = g_strsplit(line, "\t", 3); + partlen = g_strv_length(parts); + if (partlen == 3) { + host = parts[0]; + if (g_hostname_is_ip_address(host)) { + continue; + } + date = soup_date_new_from_string(parts[1]); + if (!date) { + continue; + } + include_sub_domains = (*parts[2] == 'y') ? true : false; + + /* built the new entry to add */ + entry = g_new(HSTSEntry, 1); + entry->expires_at = soup_date_new_from_string(parts[1]); + entry->include_sub_domains = include_sub_domains; + + add_host_entry(provider, host, entry); + } else { + g_warning("could not parse hsts line '%s'", line); + } + g_strfreev(parts); + } + g_strfreev(lines); +} + +/** + * Saves all entries of given provider in given file. + */ +static void save_entries(HSTSProvider *provider, const char *file) +{ + GHashTableIter iter; + char *host, *date; + HSTSEntry *entry; + FILE *f; + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); + + if ((f = fopen(file, "w"))) { - flock(fileno(f), LOCK_EX); ++ FLOCK(fileno(f), F_WRLCK); + + g_hash_table_iter_init(&iter, priv->whitelist); + while (g_hash_table_iter_next (&iter, (gpointer)&host, (gpointer)&entry)) { + date = soup_date_to_string(entry->expires_at, SOUP_DATE_ISO8601_FULL); + fprintf(f, HSTS_FILE_FORMAT, host, date, entry->include_sub_domains ? 'y' : 'n'); + g_free(date); + } + - flock(fileno(f), LOCK_UN); ++ FLOCK(fileno(f), F_UNLCK); + fclose(f); + } +} +#endif diff --cc src/session.c index 2482340,4e4f7be..d81bba7 --- a/src/session.c +++ b/src/session.c @@@ -18,12 -18,8 +18,11 @@@ */ #include "config.h" - #include #include "main.h" #include "session.h" +#ifdef FEATURE_HSTS +#include "hsts.h" +#endif #ifdef FEATURE_COOKIE