diff options
-rw-r--r-- | libsoup/soup-hsts-enforcer-db.c | 314 | ||||
-rw-r--r-- | libsoup/soup-hsts-enforcer-db.h | 45 | ||||
-rw-r--r-- | libsoup/soup-hsts-enforcer-private.h | 14 | ||||
-rw-r--r-- | libsoup/soup-hsts-enforcer.c | 602 | ||||
-rw-r--r-- | libsoup/soup-hsts-enforcer.h | 53 | ||||
-rw-r--r-- | libsoup/soup-hsts-policy.c | 479 | ||||
-rw-r--r-- | libsoup/soup-hsts-policy.h | 59 | ||||
-rw-r--r-- | libsoup/soup-types.h | 2 | ||||
-rw-r--r-- | libsoup/soup.h | 3 |
9 files changed, 1571 insertions, 0 deletions
diff --git a/libsoup/soup-hsts-enforcer-db.c b/libsoup/soup-hsts-enforcer-db.c new file mode 100644 index 00000000..319f118d --- /dev/null +++ b/libsoup/soup-hsts-enforcer-db.c @@ -0,0 +1,314 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-hsts-enforcer-db.c: database-based HSTS policy storage + * + * Using soup-cookie-jar-db as template + * Copyright (C) 2016 Igalia S.L. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> + +#include <sqlite3.h> + +#include "soup-hsts-enforcer-db.h" +#include "soup-hsts-enforcer-private.h" +#include "soup.h" + +/** + * SECTION:soup-hsts-enforcer-db + * @short_description: Database-based HSTS Enforcer + * + * #SoupHstsEnforcerDB is a #SoupHstsEnforcer that reads HSTS policies from + * and writes them to a sqlite database. + **/ + +enum { + PROP_0, + + PROP_FILENAME, + + LAST_PROP +}; + +typedef struct { + char *filename; + sqlite3 *db; +} SoupHstsEnforcerDBPrivate; + +#define SOUP_HSTS_ENFORCER_DB_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_HSTS_ENFORCER_DB, SoupHstsEnforcerDBPrivate)) + +G_DEFINE_TYPE (SoupHstsEnforcerDB, soup_hsts_enforcer_db, SOUP_TYPE_HSTS_ENFORCER) + +static void load (SoupHstsEnforcer *hsts_enforcer); + +static void +soup_hsts_enforcer_db_init (SoupHstsEnforcerDB *db) +{ +} + +static void +soup_hsts_enforcer_db_finalize (GObject *object) +{ + SoupHstsEnforcerDBPrivate *priv = + SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (object); + + g_free (priv->filename); + g_clear_pointer (&priv->db, sqlite3_close); + + G_OBJECT_CLASS (soup_hsts_enforcer_db_parent_class)->finalize (object); +} + +static void +soup_hsts_enforcer_db_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + SoupHstsEnforcerDBPrivate *priv = + SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (object); + + switch (prop_id) { + case PROP_FILENAME: + priv->filename = g_value_dup_string (value); + load (SOUP_HSTS_ENFORCER (object)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_hsts_enforcer_db_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + SoupHstsEnforcerDBPrivate *priv = + SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (object); + + switch (prop_id) { + case PROP_FILENAME: + g_value_set_string (value, priv->filename); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/** + * soup_hsts_enforcer_db_new: + * @filename: the filename to read to/write from, or %NULL + * + * Creates a #SoupHstsEnforcerDB. + * + * @filename will be read in at startup to create an initial set of HSTS + * policies. Changes to the policies will be written to @filename when the + * 'changed' signal is emitted from the HSTS enforcer. + * + * Return value: the new #SoupHstsEnforcer + * + * Since: 2.54 + **/ +SoupHstsEnforcer * +soup_hsts_enforcer_db_new (const char *filename) +{ + g_return_val_if_fail (filename != NULL, NULL); + + return g_object_new (SOUP_TYPE_HSTS_ENFORCER_DB, + SOUP_HSTS_ENFORCER_DB_FILENAME, filename, + NULL); +} + +#define QUERY_ALL "SELECT id, host, expiry, includeSubDomains FROM soup_hsts_policies;" +#define CREATE_TABLE "CREATE TABLE soup_hsts_policies (id INTEGER PRIMARY KEY, host TEXT UNIQUE, expiry INTEGER, includeSubDomains INTEGER)" +#define QUERY_INSERT "INSERT OR REPLACE INTO soup_hsts_policies VALUES((SELECT id FROM soup_hsts_policies WHERE host=%Q), %Q, %d, %d);" +#define QUERY_DELETE "DELETE FROM soup_hsts_policies WHERE host=%Q;" + +enum { + COL_ID, + COL_HOST, + COL_EXPIRY, + COL_SUB_DOMAINS, + N_COL, +}; + +static int +callback (void *data, int argc, char **argv, char **colname) +{ + SoupHstsPolicy *policy = NULL; + SoupHstsEnforcer *hsts_enforcer = SOUP_HSTS_ENFORCER (data); + + char *host; + gulong expire_time; + time_t now; + SoupDate *expires; + gboolean include_sub_domains = FALSE; + + now = time (NULL); + + host = argv[COL_HOST]; + expire_time = strtoul (argv[COL_EXPIRY], NULL, 10); + + if (now >= expire_time) + return 0; + + expires = soup_date_new_from_time_t (expire_time); + include_sub_domains = (g_strcmp0 (argv[COL_SUB_DOMAINS], "1") == 0); + + policy = soup_hsts_policy_new (host, expires, include_sub_domains); + + if (policy) + soup_hsts_enforcer_set_policy (hsts_enforcer, policy); + else + soup_date_free (expires); + + return 0; +} + +static void +try_create_table (sqlite3 *db) +{ + char *error = NULL; + + if (sqlite3_exec (db, CREATE_TABLE, NULL, NULL, &error)) { + g_warning ("Failed to execute query: %s", error); + sqlite3_free (error); + } +} + +static void +exec_query_with_try_create_table (sqlite3 *db, + const char *sql, + int (*callback)(void*,int,char**,char**), + void *argument) +{ + char *error = NULL; + gboolean try_create = TRUE; + +try_exec: + if (sqlite3_exec (db, sql, callback, argument, &error)) { + if (try_create) { + try_create = FALSE; + try_create_table (db); + sqlite3_free (error); + error = NULL; + goto try_exec; + } else { + g_warning ("Failed to execute query: %s", error); + sqlite3_free (error); + } + } +} + +/* Follows sqlite3 convention; returns TRUE on error */ +static gboolean +open_db (SoupHstsEnforcer *hsts_enforcer) +{ + SoupHstsEnforcerDBPrivate *priv = + SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (hsts_enforcer); + + char *error = NULL; + + if (sqlite3_open (priv->filename, &priv->db)) { + sqlite3_close (priv->db); + priv->db = NULL; + g_warning ("Can't open %s", priv->filename); + return TRUE; + } + + if (sqlite3_exec (priv->db, "PRAGMA synchronous = OFF; PRAGMA secure_delete = 1;", NULL, NULL, &error)) { + g_warning ("Failed to execute query: %s", error); + sqlite3_free (error); + } + + return FALSE; +} + +static void +load (SoupHstsEnforcer *hsts_enforcer) +{ + SoupHstsEnforcerDBPrivate *priv = + SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (hsts_enforcer); + + if (priv->db == NULL) { + if (open_db (hsts_enforcer)) + return; + } + + exec_query_with_try_create_table (priv->db, QUERY_ALL, callback, hsts_enforcer); +} + +static void +soup_hsts_enforcer_db_changed (SoupHstsEnforcer *hsts_enforcer, + SoupHstsPolicy *old_policy, + SoupHstsPolicy *new_policy) +{ + SoupHstsEnforcerDBPrivate *priv = + SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (hsts_enforcer); + char *query; + + if (priv->db == NULL) { + if (open_db (hsts_enforcer)) + return; + } + + if (old_policy && !new_policy) { + query = sqlite3_mprintf (QUERY_DELETE, + old_policy->domain); + exec_query_with_try_create_table (priv->db, query, NULL, NULL); + sqlite3_free (query); + } + + /* Insert the new policy or update the existing one. */ + if (new_policy && new_policy->expires) { + gulong expires; + + expires = (gulong)soup_date_to_time_t (new_policy->expires); + query = sqlite3_mprintf (QUERY_INSERT, + new_policy->domain, + new_policy->domain, + expires, + new_policy->include_sub_domains); + exec_query_with_try_create_table (priv->db, query, NULL, NULL); + sqlite3_free (query); + } +} + +static gboolean +soup_hsts_enforcer_db_is_persistent (SoupHstsEnforcer *hsts_enforcer) +{ + return TRUE; +} + +static void +soup_hsts_enforcer_db_class_init (SoupHstsEnforcerDBClass *db_class) +{ + SoupHstsEnforcerClass *hsts_enforcer_class = + SOUP_HSTS_ENFORCER_CLASS (db_class); + GObjectClass *object_class = G_OBJECT_CLASS (db_class); + + g_type_class_add_private (db_class, sizeof (SoupHstsEnforcerDBPrivate)); + + hsts_enforcer_class->is_persistent = soup_hsts_enforcer_db_is_persistent; + hsts_enforcer_class->changed = soup_hsts_enforcer_db_changed; + + object_class->finalize = soup_hsts_enforcer_db_finalize; + object_class->set_property = soup_hsts_enforcer_db_set_property; + object_class->get_property = soup_hsts_enforcer_db_get_property; + + /** + * SOUP_HSTS_ENFORCER_DB_FILENAME: + * + * Alias for the #SoupHstsEnforcerDB:filename property. (The + * HSTS policy storage filename.) + **/ + g_object_class_install_property ( + object_class, PROP_FILENAME, + g_param_spec_string (SOUP_HSTS_ENFORCER_DB_FILENAME, + "Filename", + "HSTS policy storage filename", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} diff --git a/libsoup/soup-hsts-enforcer-db.h b/libsoup/soup-hsts-enforcer-db.h new file mode 100644 index 00000000..38f56e76 --- /dev/null +++ b/libsoup/soup-hsts-enforcer-db.h @@ -0,0 +1,45 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2016 Igalia S.L. + */ + +#ifndef SOUP_HSTS_ENFORCER_DB_H +#define SOUP_HSTS_ENFORCER_DB_H 1 + +#include <libsoup/soup-hsts-enforcer.h> + +G_BEGIN_DECLS + +#define SOUP_TYPE_HSTS_ENFORCER_DB (soup_hsts_enforcer_db_get_type ()) +#define SOUP_HSTS_ENFORCER_DB(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_HSTS_ENFORCER_DB, SoupHstsEnforcerDB)) +#define SOUP_HSTS_ENFORCER_DB_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_HSTS_ENFORCER_DB, SoupHstsEnforcerDBClass)) +#define SOUP_IS_HSTS_ENFORCER_DB(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_HSTS_ENFORCER_DB)) +#define SOUP_IS_HSTS_ENFORCER_DB_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_HSTS_ENFORCER_DB)) +#define SOUP_HSTS_ENFORCER_DB_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_HSTS_ENFORCER_DB, SoupHstsEnforcerDBClass)) + +typedef struct { + SoupHstsEnforcer parent; + +} SoupHstsEnforcerDB; + +typedef struct { + SoupHstsEnforcerClass parent_class; + + /* Padding for future expansion */ + void (*_libsoup_reserved1) (void); + void (*_libsoup_reserved2) (void); + void (*_libsoup_reserved3) (void); + void (*_libsoup_reserved4) (void); +} SoupHstsEnforcerDBClass; + +#define SOUP_HSTS_ENFORCER_DB_FILENAME "filename" + +SOUP_AVAILABLE_IN_2_42 +GType soup_hsts_enforcer_db_get_type (void); + +SOUP_AVAILABLE_IN_2_42 +SoupHstsEnforcer *soup_hsts_enforcer_db_new (const char *filename); + +G_END_DECLS + +#endif /* SOUP_HSTS_ENFORCER_DB_H */ diff --git a/libsoup/soup-hsts-enforcer-private.h b/libsoup/soup-hsts-enforcer-private.h new file mode 100644 index 00000000..274d0560 --- /dev/null +++ b/libsoup/soup-hsts-enforcer-private.h @@ -0,0 +1,14 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2016 Igalia S.L. + */ + +#ifndef SOUP_HSTS_ENFORCER_PRIVATE_H +#define SOUP_HSTS_ENFORCER_PRIVATE_H 1 + +#include <libsoup/soup-types.h> + +void soup_hsts_enforcer_set_policy (SoupHstsEnforcer *hsts_enforcer, + SoupHstsPolicy *policy); + +#endif /* SOUP_HSTS_ENFORCER_PRIVATE_H */ 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); +} diff --git a/libsoup/soup-hsts-enforcer.h b/libsoup/soup-hsts-enforcer.h new file mode 100644 index 00000000..1253e234 --- /dev/null +++ b/libsoup/soup-hsts-enforcer.h @@ -0,0 +1,53 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2016 Igalia S.L. + */ + +#ifndef SOUP_HSTS_ENFORCER_H +#define SOUP_HSTS_ENFORCER_H 1 + +#include <libsoup/soup-types.h> + +G_BEGIN_DECLS + +#define SOUP_TYPE_HSTS_ENFORCER (soup_hsts_enforcer_get_type ()) +#define SOUP_HSTS_ENFORCER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_HSTS_ENFORCER, SoupHstsEnforcer)) +#define SOUP_HSTS_ENFORCER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_HSTS_ENFORCER, SoupHstsEnforcerClass)) +#define SOUP_IS_HSTS_ENFORCER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_HSTS_ENFORCER)) +#define SOUP_IS_HSTS_ENFORCER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_HSTS_ENFORCER)) +#define SOUP_HSTS_ENFORCER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_HSTS_ENFORCER, SoupHstsEnforcerClass)) + +struct _SoupHstsEnforcer { + GObject parent; + +}; + +typedef struct { + GObjectClass parent_class; + + gboolean (*is_persistent) (SoupHstsEnforcer *hsts_enforcer); + + /* signals */ + void (*changed) (SoupHstsEnforcer *jar, + SoupHstsPolicy *old_policy, + SoupHstsPolicy *new_policy); + + /* Padding for future expansion */ + void (*_libsoup_reserved1) (void); + void (*_libsoup_reserved2) (void); +} SoupHstsEnforcerClass; + +SOUP_AVAILABLE_IN_2_54 +GType soup_hsts_enforcer_get_type (void); +SOUP_AVAILABLE_IN_2_54 +SoupHstsEnforcer *soup_hsts_enforcer_new (void); +SOUP_AVAILABLE_IN_2_54 +gboolean soup_hsts_enforcer_is_persistent (SoupHstsEnforcer *hsts_enforcer); + +SOUP_AVAILABLE_IN_2_54 +void soup_hsts_enforcer_set_session_policy (SoupHstsEnforcer *hsts_enforcer, + const char *domain, + gboolean include_sub_domains); +G_END_DECLS + +#endif /* SOUP_HSTS_ENFORCER_H */ diff --git a/libsoup/soup-hsts-policy.c b/libsoup/soup-hsts-policy.c new file mode 100644 index 00000000..e2989dbb --- /dev/null +++ b/libsoup/soup-hsts-policy.c @@ -0,0 +1,479 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-hsts-policy.c + * + * Copyright (C) 2016 Igalia S.L. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include <string.h> + +#include "soup-hsts-policy.h" +#include "soup.h" + +/** + * SECTION:soup-hsts-policy + * @short_description: HTTP Strict Transport Security policies + * @see_also: #SoupHstsEnforcer + * + * #SoupHstsPolicy implements HTTP policies, as described by <ulink + * url="http://tools.ietf.org/html/rfc6797">RFC 6797</ulink>. + * + * To have a #SoupSession handle HSTS policies for your appliction + * automatically, use a #SoupHstsEnforcer. + **/ + +/** + * SoupHstsPolicy: + * @domain: the "domain" attribute, or else the hostname that the + * policy came from. + * @expires: the policy expiration time, or %NULL for a session policy + * @include_sub_domains: %TRUE if the policy applies on sub domains + * + * An HTTP Strict Transport Security policy. + * + * @domain give the host or domain that this policy belongs to and applies + * on. + * + * @expires will be non-%NULL if the policy has been set by the host and + * hence has an expiry time. If @expires is %NULL, it indicates that the + * policy is a session policy set by the user agent. + * + * If @include_sub_domains is set, the strict transport security policy + * must also be enforced on all subdomains of @domain. + * + * Since: 2.54 + **/ + +G_DEFINE_BOXED_TYPE (SoupHstsPolicy, soup_hsts_policy, soup_hsts_policy_copy, soup_hsts_policy_free) + +/** + * soup_hsts_policy_copy: + * @policy: a #SoupHstsPolicy + * + * Copies @policy. + * + * Return value: a copy of @policy + * + * Since: 2.54 + **/ +SoupHstsPolicy * +soup_hsts_policy_copy (SoupHstsPolicy *policy) +{ + SoupHstsPolicy *copy = g_slice_new0 (SoupHstsPolicy); + + copy->domain = g_strdup (policy->domain); + copy->expires = policy->expires ? soup_date_copy(policy->expires) + : NULL; + copy->include_sub_domains = policy->include_sub_domains; + + return copy; +} + +/** + * soup_hsts_policy_equal: + * @policy1: a #SoupCookie + * @policy2: a #SoupCookie + * + * Tests if @policy1 and @policy2 are equal. + * + * Note that currently, this does not check that the cookie domains + * match. This may change in the future. + * + * Return value: whether the cookies are equal. + * + * Since: 2.24 + */ +gboolean +soup_hsts_policy_equal (SoupHstsPolicy *policy1, SoupHstsPolicy *policy2) +{ + g_return_val_if_fail (policy1, FALSE); + g_return_val_if_fail (policy2, FALSE); + + if (strcmp (policy1->domain, policy2->domain)) + return FALSE; + + if (policy1->include_sub_domains != policy2->include_sub_domains) + return FALSE; + + if ((policy1->expires && !policy2->expires) || + (!policy1->expires && policy2->expires)) + return FALSE; + + if (policy1->expires && policy2->expires && + soup_date_to_time_t (policy1->expires) != + soup_date_to_time_t (policy2->expires)) + return FALSE; + + return TRUE; +} + +static inline const char * +skip_lws (const char *s) +{ + while (g_ascii_isspace (*s)) + s++; + return s; +} + +static inline const char * +unskip_lws (const char *s, const char *start) +{ + while (s > start && g_ascii_isspace (*(s - 1))) + s--; + return s; +} + +#define is_attr_ender(ch) ((ch) < ' ' || (ch) == ';' || (ch) == ',' || (ch) == '=') +#define is_value_ender(ch) ((ch) < ' ' || (ch) == ';') + +static char * +parse_value (const char **val_p, gboolean copy) +{ + const char *start, *end, *p; + char *value; + + p = *val_p; + if (*p == '=') + p++; + start = skip_lws (p); + for (p = start; !is_value_ender (*p); p++) + ; + end = unskip_lws (p, start); + + if (copy) + value = g_strndup (start, end - start); + else + value = NULL; + + *val_p = p; + return value; +} + +static SoupHstsPolicy * +parse_one_policy (const char *header, SoupURI *origin) +{ + const char *start, *end, *p; + gboolean has_value; + long max_age = -1; + gboolean include_sub_domains = FALSE; + + g_return_val_if_fail (origin == NULL || origin->host, NULL); + + p = start = skip_lws (header); + + /* Parse directives */ + do { + if (*p == ';') + p++; + + start = skip_lws (p); + for (p = start; !is_attr_ender (*p); p++) + ; + end = unskip_lws (p, start); + + has_value = (*p == '='); +#define MATCH_NAME(name) ((end - start == strlen (name)) && !g_ascii_strncasecmp (start, name, end - start)) + + if (MATCH_NAME ("max-age") && has_value) { + char *max_age_str, *max_age_end; + + /* Repeated directives make the policy invalid. */ + if (max_age >= 0) + goto fail; + + max_age_str = parse_value (&p, TRUE); + max_age = strtol (max_age_str, &max_age_end, 10); + g_free (max_age_str); + + if (*max_age_end == '\0') { + /* Invalid 'max-age' directive makes the policy invalid. */ + if (max_age < 0) + goto fail; + } + } else if (MATCH_NAME ("includeSubDomains")) { + /* Repeated directives make the policy invalid. */ + if (include_sub_domains) + goto fail; + + /* The 'includeSubDomains' directive can't have a value. */ + if (has_value) + goto fail; + + include_sub_domains = TRUE; + } else { + /* Unknown directives must be skipped. */ + if (has_value) + parse_value (&p, FALSE); + } + } while (*p == ';'); + + /* No 'max-age' directive makes the policy invalid. */ + if (max_age < 0) + goto fail; + + return soup_hsts_policy_new_with_max_age (origin->host, max_age, + include_sub_domains); + +fail: + return NULL; +} + +/** + * Return value: %TRUE if the hostname is suitable for an HSTS host, %FALSE + * otherwise. + **/ +static gboolean +is_hostname_valid (const char *hostname) +{ + if (!hostname) + return FALSE; + + /* Hostnames must have at least one '.' + */ + if (!strchr (hostname, '.')) + return FALSE; + + /* IP addresses are not valid hostnames, only domain names are. + */ + if (g_hostname_is_ip_address (hostname)) + return FALSE; + + /* The hostname should be a valid domain name. + */ + return TRUE; +} + +/** + * soup_hsts_policy_new: + * @domain: policy domain or hostname + * @expires: (transfer full): the expiry date of the policy + * @include_sub_domains: %TRUE if the policy applies on sub domains + * + * Creates a new #SoupHstsPolicy with the given attributes. + * + * @domain is a domain on which the strict transport security policy + * represented by this object must be enforced. + * + * @expires is the date and time when the policy should be considered + * expired. + * + * If @include_sub_domains is %TRUE, the strict transport security policy + * must also be enforced on all subdomains of @domain. + * + * Return value: a new #SoupHstsPolicy. + * + * Since: 2.54 + **/ +SoupHstsPolicy * +soup_hsts_policy_new (const char *domain, SoupDate *expires, + gboolean include_sub_domains) +{ + SoupHstsPolicy *policy; + + g_return_val_if_fail (is_hostname_valid (domain), NULL); + + policy = g_slice_new0 (SoupHstsPolicy); + policy->domain = g_strdup (domain); + policy->expires = expires; + policy->include_sub_domains = include_sub_domains; + + return policy; +} + +/** + * soup_hsts_policy_new_with_max_age: + * @domain: policy domain or hostname + * @max_age: max age of the policy + * @include_sub_domains: %TRUE if the policy applies on sub domains + * + * Creates a new #SoupHstsPolicy with the given attributes. + * + * @domain is a domain on which the strict transport security policy + * represented by this object must be enforced. + * + * @max_age is used to set the "expires" attribute on the policy; pass + * SOUP_HSTS_POLICY_MAX_AGE_PAST for an already-expired policy, or a + * lifetime in seconds. + * + * If @include_sub_domains is %TRUE, the strict transport security policy + * must also be enforced on all subdomains of @domain. + * + * Return value: a new #SoupHstsPolicy. + * + * Since: 2.54 + **/ +SoupHstsPolicy * +soup_hsts_policy_new_with_max_age (const char *domain, int max_age, + gboolean include_sub_domains) +{ + SoupDate *expires; + SoupHstsPolicy *policy; + + g_return_val_if_fail (is_hostname_valid (domain), NULL); + g_return_val_if_fail (max_age >= 0, NULL); + + if (max_age == SOUP_HSTS_POLICY_MAX_AGE_PAST) { + /* Use a date way in the past, to protect against + * clock skew. + */ + expires = soup_date_new (1970, 1, 1, 0, 0, 0); + } else + expires = soup_date_new_from_now (max_age); + + policy = soup_hsts_policy_new (domain, expires, include_sub_domains); + + if (!policy) + soup_date_free (expires); + + return policy; +} + +/** + * soup_hsts_policy_new_permanent: + * @domain: policy domain or hostname + * @include_sub_domains: %TRUE if the policy applies on sub domains + * + * Creates a new #SoupHstsPolicy with the given attributes. + * + * @domain is a domain on which the strict transport security policy + * represented by this object must be enforced. + * + * If @include_sub_domains is %TRUE, the strict transport security policy + * must also be enforced on all subdomains of @domain. + * + * Return value: a new #SoupHstsPolicy. + * + * Since: 2.54 + **/ +SoupHstsPolicy * +soup_hsts_policy_new_permanent (const char *domain, + gboolean include_sub_domains) +{ + return soup_hsts_policy_new (domain, NULL, include_sub_domains); +} + +/** + * soup_hsts_policy_new_from_response: + * @msg: a #SoupMessage containing a "Strict-Transport-Security" response + * header + * + * Parses @msg's first "Strict-Transport-Security" response header and + * returns a #SoupHstsPolicy, or %NULL if no valid + * "Strict-Transport-Security" response header was found. + * + * Return value: (nullable): a new #SoupHstsPolicy, or %NULL if no valid + * "Strict-Transport-Security" response header was found. + * + * Since: 2.54 + **/ +SoupHstsPolicy * +soup_hsts_policy_new_from_response (SoupMessage *msg) +{ + SoupURI *origin; + const char *name, *value; + SoupMessageHeadersIter iter; + + soup_message_headers_iter_init (&iter, msg->response_headers); + while (soup_message_headers_iter_next (&iter, &name, &value)) { + if (g_ascii_strcasecmp (name, "Strict-Transport-Security") != 0) + continue; + + origin = soup_message_get_uri (msg); + return parse_one_policy (value, origin); + } + + return NULL; +} + +/** + * soup_hsts_policy_get_domain: + * @policy: a #SoupHstsPolicy + * + * Gets @policy's domain. + * + * Return value: @policy's domain. + * + * Since: 2.54 + **/ +const char * +soup_hsts_policy_get_domain (SoupHstsPolicy *policy) +{ + return policy->domain; +} + +/** + * soup_hsts_policy_is_expired: + * @policy: a #SoupHstsPolicy + * + * Gets whether @policy is expired. + * + * Permanent policies never expire. + * + * Return value: whether @policy is expired. + * + * Since: 2.54 + **/ +gboolean +soup_hsts_policy_is_expired (SoupHstsPolicy *policy) +{ + return policy->expires && soup_date_is_past (policy->expires); +} + +/** + * soup_hsts_policy_includes_sub_domains: + * @policy: a #SoupHstsPolicy + * + * Gets whether @policy include its sub-domains. + * + * Return value: whether @policy include its sub-domains. + * + * Since: 2.54 + **/ +gboolean +soup_hsts_policy_includes_sub_domains (SoupHstsPolicy *policy) +{ + return policy->include_sub_domains; +} + +/** + * soup_hsts_policy_is_permanent: + * @policy: a #SoupHstsPolicy + * + * Gets whether @policy is permanent (not expirable). + * + * A permanent policy never expires and should not be saved by a persistent + * #SoupHstsEnforcer so the user agent can control them. + * + * Return value: whether @policy is permanent. + * + * Since: 2.54 + **/ +gboolean +soup_hsts_policy_is_permanent (SoupHstsPolicy *policy) +{ + return !policy->expires; +} + +/** + * soup_hsts_policy_free: + * @policy: a #SoupHstsPolicy + * + * Frees @policy. + * + * Since: 2.54 + **/ +void +soup_hsts_policy_free (SoupHstsPolicy *policy) +{ + g_return_if_fail (policy != NULL); + + g_free (policy->domain); + g_clear_pointer (&policy->expires, soup_date_free); + + g_slice_free (SoupHstsPolicy, policy); +} diff --git a/libsoup/soup-hsts-policy.h b/libsoup/soup-hsts-policy.h new file mode 100644 index 00000000..8492d4a9 --- /dev/null +++ b/libsoup/soup-hsts-policy.h @@ -0,0 +1,59 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2016 Igalia S.L. + */ + +#ifndef SOUP_HSTS_POLICY_H +#define SOUP_HSTS_POLICY_H 1 + +#include <libsoup/soup-types.h> + +G_BEGIN_DECLS + +struct _SoupHstsPolicy { + char *domain; + SoupDate *expires; + gboolean include_sub_domains; +}; + +SOUP_AVAILABLE_IN_2_54 +GType soup_hsts_policy_get_type (void); +#define SOUP_TYPE_HSTS_POLICY (soup_hsts_policy_get_type()) + +#define SOUP_HSTS_POLICY_MAX_AGE_PAST (0) + +SOUP_AVAILABLE_IN_2_54 +SoupHstsPolicy *soup_hsts_policy_new (const char *domain, + SoupDate *expiry_date, + gboolean include_sub_domains); +SOUP_AVAILABLE_IN_2_54 +SoupHstsPolicy *soup_hsts_policy_new_with_max_age (const char *domain, + int max_age, + gboolean include_sub_domains); +SOUP_AVAILABLE_IN_2_54 +SoupHstsPolicy *soup_hsts_policy_new_permanent (const char *domain, + gboolean include_sub_domains); +SOUP_AVAILABLE_IN_2_54 +SoupHstsPolicy *soup_hsts_policy_new_from_response (SoupMessage *msg); + +SOUP_AVAILABLE_IN_2_54 +SoupHstsPolicy *soup_hsts_policy_copy (SoupHstsPolicy *policy); +SOUP_AVAILABLE_IN_2_54 +gboolean soup_hsts_policy_equal (SoupHstsPolicy *policy1, + SoupHstsPolicy *policy2); + +SOUP_AVAILABLE_IN_2_54 +const char *soup_hsts_policy_get_domain (SoupHstsPolicy *policy); +SOUP_AVAILABLE_IN_2_54 +gboolean soup_hsts_policy_is_expired (SoupHstsPolicy *policy); +SOUP_AVAILABLE_IN_2_54 +gboolean soup_hsts_policy_includes_sub_domains (SoupHstsPolicy *policy); +SOUP_AVAILABLE_IN_2_54 +gboolean soup_hsts_policy_is_permanent (SoupHstsPolicy *policy); + +SOUP_AVAILABLE_IN_2_54 +void soup_hsts_policy_free (SoupHstsPolicy *policy); + +G_END_DECLS + +#endif /* SOUP_HSTS_POLICY_H */ diff --git a/libsoup/soup-types.h b/libsoup/soup-types.h index 37a47ece..8f5b07ab 100644 --- a/libsoup/soup-types.h +++ b/libsoup/soup-types.h @@ -19,6 +19,8 @@ typedef struct _SoupAuthDomain SoupAuthDomain; typedef struct _SoupCookie SoupCookie; typedef struct _SoupCookieJar SoupCookieJar; typedef struct _SoupDate SoupDate; +typedef struct _SoupHstsEnforcer SoupHstsEnforcer; +typedef struct _SoupHstsPolicy SoupHstsPolicy; typedef struct _SoupMessage SoupMessage; typedef struct _SoupRequest SoupRequest; typedef struct _SoupRequestHTTP SoupRequestHTTP; diff --git a/libsoup/soup.h b/libsoup/soup.h index 4a3ac2ef..46ca6acf 100644 --- a/libsoup/soup.h +++ b/libsoup/soup.h @@ -29,6 +29,9 @@ extern "C" { #include <libsoup/soup-enum-types.h> #include <libsoup/soup-form.h> #include <libsoup/soup-headers.h> +#include <libsoup/soup-hsts-enforcer.h> +#include <libsoup/soup-hsts-enforcer-db.h> +#include <libsoup/soup-hsts-policy.h> #include <libsoup/soup-logger.h> #include <libsoup/soup-message.h> #include <libsoup/soup-method.h> |