summaryrefslogtreecommitdiff
path: root/libsoup/soup-hsts-enforcer.c
diff options
context:
space:
mode:
Diffstat (limited to 'libsoup/soup-hsts-enforcer.c')
-rw-r--r--libsoup/soup-hsts-enforcer.c602
1 files changed, 602 insertions, 0 deletions
diff --git a/libsoup/soup-hsts-enforcer.c b/libsoup/soup-hsts-enforcer.c
new file mode 100644
index 00000000..8f3cf2b7
--- /dev/null
+++ b/libsoup/soup-hsts-enforcer.c
@@ -0,0 +1,602 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-hsts-enforcer.c: HTTP Strict Transport Security implementation
+ *
+ * Copyright (C) 2016 Igalia S.L.
+ */
+
+/* TODO Use only internationalized domain names */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "soup-hsts-enforcer.h"
+#include "soup-hsts-enforcer-private.h"
+#include "soup.h"
+
+/**
+ * SECTION:soup-hsts-enforcer
+ * @short_description: Automatic HSTS enforcing for SoupSession
+ *
+ * A #SoupHstsEnforcer stores HSTS policies and enforce them when
+ * required.
+ * #SoupHstsEnforcer implements #SoupSessionFeature, so you can add a
+ * HSTS enforcer to a session with soup_session_add_feature() or
+ * soup_session_add_feature_by_type().
+ *
+ * When the #SoupSession the #SoupHstsEnforcer is attached to sends a
+ * message, the #SoupHstsEnforcer will ask for a redirection to HTTPS if
+ * the destination is a known HSTS host and is contacted over an insecure
+ * transport protocol (HTTP).
+ *
+ * Note that the base #SoupHstsEnforcer class does not support any form
+ * of long-term HSTS policy persistence.
+ **/
+
+static void soup_hsts_enforcer_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
+
+G_DEFINE_TYPE_WITH_CODE (SoupHstsEnforcer, soup_hsts_enforcer, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
+ soup_hsts_enforcer_session_feature_init))
+
+enum {
+ CHANGED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+typedef struct {
+ GHashTable *host_policies;
+ GHashTable *session_policies;
+} SoupHstsEnforcerPrivate;
+#define SOUP_HSTS_ENFORCER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_HSTS_ENFORCER, SoupHstsEnforcerPrivate))
+
+static void
+soup_hsts_enforcer_init (SoupHstsEnforcer *hsts_enforcer)
+{
+ SoupHstsEnforcerPrivate *priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+
+ priv->host_policies = g_hash_table_new_full (soup_str_case_hash,
+ soup_str_case_equal,
+ g_free, NULL);
+
+ priv->session_policies = g_hash_table_new_full (soup_str_case_hash,
+ soup_str_case_equal,
+ g_free, NULL);
+}
+
+static void
+soup_hsts_enforcer_finalize (GObject *object)
+{
+ SoupHstsEnforcerPrivate *priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (object);
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, priv->host_policies);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ soup_hsts_policy_free (value);
+ g_hash_table_destroy (priv->host_policies);
+
+ g_hash_table_iter_init (&iter, priv->session_policies);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ soup_hsts_policy_free (value);
+ g_hash_table_destroy (priv->session_policies);
+
+ G_OBJECT_CLASS (soup_hsts_enforcer_parent_class)->finalize (object);
+}
+
+static gboolean
+soup_hsts_enforcer_real_is_persistent (SoupHstsEnforcer *hsts_enforcer)
+{
+ return FALSE;
+}
+
+static void
+soup_hsts_enforcer_class_init (SoupHstsEnforcerClass *hsts_enforcer_class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (hsts_enforcer_class);
+
+ g_type_class_add_private (hsts_enforcer_class, sizeof (SoupHstsEnforcerPrivate));
+
+ object_class->finalize = soup_hsts_enforcer_finalize;
+
+ hsts_enforcer_class->is_persistent = soup_hsts_enforcer_real_is_persistent;
+
+ /**
+ * SoupHstsEnforcer::changed:
+ * @hsts_enforcer: the #SoupHstsEnforcer
+ * @old_policy: the old #SoupHstsPolicy value
+ * @new_policy: the new #SoupHstsPolicy value
+ *
+ * Emitted when @hsts_enforcer changes. If a policy has been added,
+ * @new_policy will contain the newly-added policy and
+ * @old_policy will be %NULL. If a policy has been deleted,
+ * @old_policy will contain the to-be-deleted policy and
+ * @new_policy will be %NULL. If a policy has been changed,
+ * @old_policy will contain its old value, and @new_policy its
+ * new value.
+ **/
+ signals[CHANGED] =
+ g_signal_new ("changed",
+ G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (SoupHstsEnforcerClass, changed),
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE, 2,
+ SOUP_TYPE_HSTS_POLICY | G_SIGNAL_TYPE_STATIC_SCOPE,
+ SOUP_TYPE_HSTS_POLICY | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+/**
+ * soup_hsts_enforcer_new:
+ *
+ * Creates a new #SoupHstsEnforcer. The base #SoupHstsEnforcer class does
+ * not support persistent storage of HSTS policies; use a subclass for
+ * that.
+ *
+ * Returns: a new #SoupHstsEnforcer
+ *
+ * Since: 2.54
+ **/
+SoupHstsEnforcer *
+soup_hsts_enforcer_new (void)
+{
+ return g_object_new (SOUP_TYPE_HSTS_ENFORCER, NULL);
+}
+
+static void
+soup_hsts_enforcer_changed (SoupHstsEnforcer *hsts_enforcer,
+ SoupHstsPolicy *old, SoupHstsPolicy *new)
+{
+ g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
+
+ g_assert_true (old || new);
+
+ g_signal_emit (hsts_enforcer, signals[CHANGED], 0, old, new);
+}
+
+static void
+soup_hsts_enforcer_remove_expired_host_policies (SoupHstsEnforcer *hsts_enforcer)
+{
+ SoupHstsEnforcerPrivate *priv;
+ SoupHstsPolicy *policy;
+ GList *domains, *p;
+ const char *domain;
+
+ g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
+
+ priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+
+ /* Remove all the expired policies as soon as one is encountered as required by the RFC. */
+ domains = g_hash_table_get_keys (priv->host_policies);
+ for (p = domains; p; p = p->next ) {
+ domain = (const char *) p->data;
+ policy = g_hash_table_lookup (priv->host_policies, domain);
+ if (policy && soup_hsts_policy_is_expired (policy)) {
+ g_hash_table_remove (priv->host_policies, domain);
+ soup_hsts_enforcer_changed (hsts_enforcer, policy, NULL);
+ soup_hsts_policy_free (policy);
+ }
+ }
+ g_list_free (domains);
+}
+
+static void
+soup_hsts_enforcer_remove_host_policy (SoupHstsEnforcer *hsts_enforcer,
+ const gchar *domain)
+{
+ SoupHstsEnforcerPrivate *priv;
+ SoupHstsPolicy *policy;
+
+ g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
+ g_return_if_fail (domain != NULL);
+
+ priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+
+ policy = g_hash_table_lookup (priv->host_policies, domain);
+
+ g_assert_nonnull (policy);
+
+ g_hash_table_remove (priv->host_policies, domain);
+ soup_hsts_enforcer_changed (hsts_enforcer, policy, NULL);
+ soup_hsts_policy_free (policy);
+
+ soup_hsts_enforcer_remove_expired_host_policies (hsts_enforcer);
+}
+
+static void
+soup_hsts_enforcer_replace_policy (SoupHstsEnforcer *hsts_enforcer,
+ SoupHstsPolicy *new_policy)
+{
+ SoupHstsEnforcerPrivate *priv;
+ GHashTable *policies;
+ SoupHstsPolicy *old_policy;
+ const gchar *domain;
+ gboolean is_permanent;
+
+ g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
+ g_return_if_fail (new_policy != NULL);
+
+ g_assert_false (soup_hsts_policy_is_expired (new_policy));
+
+ domain = soup_hsts_policy_get_domain (new_policy);
+ is_permanent = soup_hsts_policy_is_permanent (new_policy);
+
+ g_return_if_fail (domain != NULL);
+
+ priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+ policies = is_permanent ? priv->session_policies :
+ priv->host_policies;
+
+ old_policy = g_hash_table_lookup (policies, domain);
+
+ g_assert_nonnull (old_policy);
+
+ g_hash_table_remove (policies, domain);
+ g_hash_table_insert (policies, g_strdup (domain), new_policy);
+ if (!is_permanent && !soup_hsts_policy_equal (old_policy, new_policy))
+ soup_hsts_enforcer_changed (hsts_enforcer, old_policy, new_policy);
+ soup_hsts_policy_free (old_policy);
+
+ soup_hsts_enforcer_remove_expired_host_policies (hsts_enforcer);
+}
+
+static void
+soup_hsts_enforcer_insert_policy (SoupHstsEnforcer *hsts_enforcer,
+ SoupHstsPolicy *policy)
+{
+ SoupHstsEnforcerPrivate *priv;
+ GHashTable *policies;
+ const gchar *domain;
+ gboolean is_permanent;
+
+ g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
+ g_return_if_fail (policy != NULL);
+
+ g_assert_false (soup_hsts_policy_is_expired (policy));
+
+ domain = soup_hsts_policy_get_domain (policy);
+ is_permanent = soup_hsts_policy_is_permanent (policy);
+
+ g_return_if_fail (domain != NULL);
+
+ priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+ policies = is_permanent ? priv->session_policies :
+ priv->host_policies;
+
+ g_assert_false (g_hash_table_contains (policies, domain));
+
+ g_hash_table_insert (policies, g_strdup (domain), policy);
+ if (!is_permanent)
+ soup_hsts_enforcer_changed (hsts_enforcer, NULL, policy);
+}
+
+/**
+ * soup_hsts_enforcer_set_policy:
+ * @hsts_enforcer: a #SoupHstsEnforcer
+ * @policy: (transfer full): the policy of the HSTS host
+ *
+ * Sets @domain's HSTS policy to @policy. If @policy is expired, any
+ * existing HSTS policy for this host will be removed instead. If a policy
+ * exited for this host, it will be replaced. Otherwise, the new policy
+ * will be inserted.
+ *
+ * This steals @policy.
+ *
+ * Since: 2.54
+ **/
+void
+soup_hsts_enforcer_set_policy (SoupHstsEnforcer *hsts_enforcer,
+ SoupHstsPolicy *policy)
+{
+ SoupHstsEnforcerPrivate *priv;
+ GHashTable *policies;
+ const gchar *domain;
+ gboolean is_permanent;
+
+ g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
+ g_return_if_fail (policy != NULL);
+
+ domain = soup_hsts_policy_get_domain (policy);
+ is_permanent = soup_hsts_policy_is_permanent (policy);
+
+ g_return_if_fail (domain != NULL);
+
+ priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+ policies = is_permanent ? priv->session_policies :
+ priv->host_policies;
+
+ if (!is_permanent && soup_hsts_policy_is_expired (policy)) {
+ soup_hsts_enforcer_remove_host_policy (hsts_enforcer, domain);
+ soup_hsts_policy_free (policy);
+ return;
+ }
+
+ if (g_hash_table_contains (policies, domain))
+ soup_hsts_enforcer_replace_policy (hsts_enforcer, policy);
+ else
+ soup_hsts_enforcer_insert_policy (hsts_enforcer, policy);
+}
+
+static SoupHstsPolicy *
+soup_hsts_enforcer_get_host_policy (SoupHstsEnforcer *hsts_enforcer,
+ const gchar *domain)
+{
+ SoupHstsEnforcerPrivate *priv;
+
+ g_return_val_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer), NULL);
+ g_return_val_if_fail (domain != NULL, NULL);
+
+ priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+
+ return g_hash_table_lookup (priv->host_policies, domain);
+}
+
+static SoupHstsPolicy *
+soup_hsts_enforcer_get_session_policy (SoupHstsEnforcer *hsts_enforcer,
+ const gchar *domain)
+{
+ SoupHstsEnforcerPrivate *priv;
+
+ g_return_val_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer), NULL);
+ g_return_val_if_fail (domain != NULL, NULL);
+
+ priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+
+ return g_hash_table_lookup (priv->session_policies, domain);
+}
+
+/**
+ * soup_hsts_enforcer_set_session_policy:
+ * @hsts_enforcer: a #SoupHstsEnforcer
+ * @domain: policy domain or hostname
+ * @include_sub_domains: %TRUE if the policy applies on sub domains
+ *
+ * Sets a session policy@domain's HSTS policy to @policy. If @policy is expired, any
+ * existing HSTS policy for this host will be removed instead. If a policy
+ * exited for this host, it will be replaced. Otherwise, the new policy
+ * will be inserted.
+ *
+ * Since: 2.54
+ **/
+void
+soup_hsts_enforcer_set_session_policy (SoupHstsEnforcer *hsts_enforcer,
+ const char *domain,
+ gboolean include_sub_domains)
+{
+ SoupHstsPolicy *policy;
+
+ g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
+ g_return_if_fail (domain != NULL);
+
+ policy = soup_hsts_policy_new_permanent (domain, include_sub_domains);
+ soup_hsts_enforcer_set_policy (hsts_enforcer, policy);
+}
+
+static gboolean
+soup_hsts_enforcer_is_valid_host (SoupHstsEnforcer *hsts_enforcer,
+ const gchar *domain)
+{
+ SoupHstsPolicy *policy;
+
+ g_return_val_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer), FALSE);
+ g_return_val_if_fail (domain != NULL, FALSE);
+
+ if (soup_hsts_enforcer_get_session_policy (hsts_enforcer, domain))
+ return TRUE;
+
+ policy = soup_hsts_enforcer_get_host_policy (hsts_enforcer, domain);
+ if (policy)
+ return !soup_hsts_policy_is_expired (policy);
+
+ return FALSE;
+}
+
+static gboolean
+soup_hsts_enforcer_host_includes_sub_domains (SoupHstsEnforcer *hsts_enforcer,
+ const gchar *domain)
+{
+ SoupHstsPolicy *policy;
+ gboolean include_sub_domains = FALSE;
+
+ g_return_val_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer), FALSE);
+ g_return_val_if_fail (domain != NULL, FALSE);
+
+ policy = soup_hsts_enforcer_get_session_policy (hsts_enforcer, domain);
+ if (policy)
+ include_sub_domains |= soup_hsts_policy_includes_sub_domains (policy);
+
+ policy = soup_hsts_enforcer_get_host_policy (hsts_enforcer, domain);
+ if (policy)
+ include_sub_domains |= soup_hsts_policy_includes_sub_domains (policy);
+
+ return include_sub_domains;
+}
+
+static inline const gchar*
+super_domain_of (const gchar *domain)
+{
+ const gchar *iter = domain;
+
+ g_return_val_if_fail (domain != NULL, NULL);
+
+ for (; *iter != '\0' && *iter != '.' ; iter++);
+ for (; *iter == '.' ; iter++);
+
+ if (*iter == '\0')
+ return NULL;
+
+ return iter;
+}
+
+static gboolean
+soup_hsts_enforcer_must_enforce_secure_transport (SoupHstsEnforcer *hsts_enforcer,
+ const gchar *domain)
+{
+ const gchar *super_domain = domain;
+
+ g_return_val_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer), FALSE);
+ g_return_val_if_fail (domain != NULL, FALSE);
+
+ if (soup_hsts_enforcer_is_valid_host (hsts_enforcer, domain))
+ return TRUE;
+
+ while ((super_domain = super_domain_of (super_domain)) != NULL) {
+ if (soup_hsts_enforcer_host_includes_sub_domains (hsts_enforcer, super_domain) &&
+ soup_hsts_enforcer_is_valid_host (hsts_enforcer, super_domain))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/* Processes the 'Strict-Transport-Security' field of a message's response header. */
+static void
+soup_hsts_enforcer_process_sts_header (SoupHstsEnforcer *hsts_enforcer,
+ SoupMessage *msg)
+{
+ SoupHstsPolicy *policy;
+ SoupURI *uri;
+
+ g_return_if_fail (hsts_enforcer != NULL);
+ g_return_if_fail (msg != NULL);
+
+ /* TODO if connection error or warnings received, do nothing. */
+
+ /* TODO if header received on hazardous connection, do nothing. */
+
+ uri = soup_message_get_uri (msg);
+
+ g_return_if_fail (uri != NULL);
+
+ policy = soup_hsts_policy_new_from_response (msg);
+
+ g_return_if_fail (policy != NULL);
+
+ soup_hsts_enforcer_set_policy (hsts_enforcer, policy);
+}
+
+/* Enforces HTTPS when demanded. */
+static gboolean
+soup_hsts_enforcer_should_redirect_to_https (SoupHstsEnforcer *hsts_enforcer,
+ SoupMessage *msg)
+{
+ SoupURI *uri;
+ const gchar *domain;
+
+ g_return_val_if_fail (hsts_enforcer != NULL, FALSE);
+ g_return_val_if_fail (msg != NULL, FALSE);
+
+ uri = soup_message_get_uri (msg);
+
+ g_return_val_if_fail (uri != NULL, FALSE);
+
+ // HSTS secures only HTTP connections.
+ if (uri->scheme != SOUP_URI_SCHEME_HTTP)
+ return FALSE;
+
+ domain = soup_uri_get_host (uri);
+
+ g_return_val_if_fail (domain != NULL, FALSE);
+
+ return soup_hsts_enforcer_must_enforce_secure_transport (hsts_enforcer, domain);
+}
+
+static void
+redirect_to_https (SoupMessage *msg)
+{
+ SoupURI *src_uri, *dst_uri;
+ char *dst;
+
+ src_uri = soup_message_get_uri (msg);
+
+ dst_uri = soup_uri_copy (src_uri);
+ soup_uri_set_scheme (dst_uri, SOUP_URI_SCHEME_HTTPS);
+ dst = soup_uri_to_string (dst_uri, FALSE);
+ soup_uri_free (dst_uri);
+
+ soup_message_set_redirect (msg, 301, dst);
+ g_free (dst);
+}
+
+static void
+process_sts_header (SoupMessage *msg, gpointer user_data)
+{
+ SoupHstsEnforcer *hsts_enforcer = SOUP_HSTS_ENFORCER (user_data);
+
+ g_return_if_fail (hsts_enforcer != NULL);
+ g_return_if_fail (msg != NULL);
+
+ soup_hsts_enforcer_process_sts_header (hsts_enforcer, msg);
+}
+
+static void
+soup_hsts_enforcer_request_queued (SoupSessionFeature *feature,
+ SoupSession *session,
+ SoupMessage *msg)
+{
+ SoupHstsEnforcer *hsts_enforcer = SOUP_HSTS_ENFORCER (feature);
+ SoupURI *uri;
+ const char *scheme;
+
+ g_return_if_fail (hsts_enforcer != NULL);
+ g_return_if_fail (msg != NULL);
+
+ uri = soup_message_get_uri (msg);
+
+ g_return_if_fail (uri != NULL);
+
+ scheme = soup_uri_get_scheme (uri);
+
+ if (scheme == SOUP_URI_SCHEME_HTTP) {
+ if (soup_hsts_enforcer_should_redirect_to_https (hsts_enforcer, msg))
+ redirect_to_https (msg);
+ }
+ else if (scheme == SOUP_URI_SCHEME_HTTPS) {
+ soup_message_add_header_handler (msg, "got-headers",
+ "Strict-Transport-Security",
+ G_CALLBACK (process_sts_header),
+ hsts_enforcer);
+ }
+}
+
+static void
+soup_hsts_enforcer_request_unqueued (SoupSessionFeature *feature,
+ SoupSession *session,
+ SoupMessage *msg)
+{
+ g_signal_handlers_disconnect_by_func (msg, process_sts_header, feature);
+}
+
+static void
+soup_hsts_enforcer_session_feature_init (SoupSessionFeatureInterface *feature_interface,
+ gpointer interface_data)
+{
+ feature_interface->request_queued = soup_hsts_enforcer_request_queued;
+ feature_interface->request_unqueued = soup_hsts_enforcer_request_unqueued;
+}
+
+/**
+ * soup_hsts_enforcer_is_persistent:
+ * @hsts_enforcer: a #SoupHstsEnforcer
+ *
+ * Gets whether @hsts_enforcer stores policies persistenly.
+ *
+ * Returns: %TRUE if @hsts_enforcer storage is persistent or %FALSE otherwise.
+ *
+ * Since: 2.54
+ **/
+gboolean
+soup_hsts_enforcer_is_persistent (SoupHstsEnforcer *hsts_enforcer)
+{
+ g_return_val_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer), FALSE);
+
+ return SOUP_HSTS_ENFORCER_GET_CLASS (hsts_enforcer)->is_persistent (hsts_enforcer);
+}