summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libsoup/soup-hsts-enforcer-db.c314
-rw-r--r--libsoup/soup-hsts-enforcer-db.h45
-rw-r--r--libsoup/soup-hsts-enforcer-private.h14
-rw-r--r--libsoup/soup-hsts-enforcer.c602
-rw-r--r--libsoup/soup-hsts-enforcer.h53
-rw-r--r--libsoup/soup-hsts-policy.c479
-rw-r--r--libsoup/soup-hsts-policy.h59
-rw-r--r--libsoup/soup-types.h2
-rw-r--r--libsoup/soup.h3
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>