[libsoup/hsts: 12/25] Add SoupHstsEnforcer



commit bd171267df0ed5313748935223e14fd5a0be1397
Author: Adrien Plazas <kekun plazas laposte net>
Date:   Tue Mar 29 13:30:27 2016 +0200

    Add SoupHstsEnforcer
    
    This is a WIP.
    
    Add the SoupSessionFeature SoupHstsEnforcer to allow a session to
    support HSTS (RFC 6797).
    
    This also adds SoupHstsPolicy and an early version of
    SoupHstsEnforcerDb.

 libsoup/soup-hsts-enforcer-db.c      | 314 ++++++++++++++++++
 libsoup/soup-hsts-enforcer-db.h      |  45 +++
 libsoup/soup-hsts-enforcer-private.h |  14 +
 libsoup/soup-hsts-enforcer.c         | 602 +++++++++++++++++++++++++++++++++++
 libsoup/soup-hsts-enforcer.h         |  53 +++
 libsoup/soup-hsts-policy.c           | 479 ++++++++++++++++++++++++++++
 libsoup/soup-hsts-policy.h           |  59 ++++
 libsoup/soup-types.h                 |   2 +
 libsoup/soup.h                       |   3 +
 9 files changed, 1571 insertions(+)
---
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>



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]