[libsoup/hsts: 3/36] HSTS: Rewrite the HSTS feature and add tests



commit 29d2ff23bc38013e553f40c37cf6af1fc8b298c5
Author: Claudio Saavedra <csaavedra igalia com>
Date:   Thu Jun 7 10:38:17 2018 +0300

    HSTS: Rewrite the HSTS feature and add tests
    
    This is a comprehensive rework of the HSTS enforcer and related
    classes, based upon Adrien Plazas work. A summary of the most
    relevant changes:
    
    SoupHSTSEnforcer:
    
    - The enforcer will listen on headers both on message queueing
      and restarting. This is necessary in order to be able to enforce
      HSTS redirections on messages that are restarted for whatever
      reason.
    - Instead of causing a redirection, the URI will be overwritten
      directly on the message before this one is sent. Redirections
      are for use on the server side, and the tests added show that
      it is not a reliable way to do HSTS enforcing. Currently,
      the only way to find out that a HSTS policy has been enforced
      is by listening to the SoupMessage:uri property changes, but
      this might be impractical, so this could be revisited in the
      future.
    - soup_hsts_enforcer_policy() will not steal the given policy.
      Doing so is prone to leaks and not customary.
    - SoupHSTSEnforcerClass now has a has_valid_policy() vfunc.
      It currently works exactly as before, but the idea here is to
      make it possible for subclasses to implement their own check
      for existence of valid policies for domains, instead of all
      subclasses having to add their policies to the base
      SoupHSTSEnforcer class. This will be useful when having a large
      number of pre-loaded HSTS policies (either in SoupHSTSEnforcerDB
      or in an enforcer using libhsts as a backend) to avoid having
      potentially thousands of policies in memory at all times.
    - HSTS headers are parsed using soup's available utilities,
      instead of parsing them by hand. The specification is carefully
      followed so as to not accept any header that is not fully
      compliant.
    
    SoupHSTSEnforcerDB:
    
    - Store the max-age attribute in the database. This was done before
      errata 5372 was reported to RFC 6797, and its necessity will
      depend on how the errata is treated.
    
    Other:
    
    - Added tests for both enforcer classes that cover most of the
      specification.
    - Added the gtk-doc documentation and update all the documentation
      comments.
    - Rename SoupHsts classes to SoupHSTS for consistent naming and
      other minor renaming of parameters and methods.

 docs/reference/Makefile.am              |   3 +-
 docs/reference/libsoup-2.4-docs.sgml    |   2 +
 docs/reference/libsoup-2.4-sections.txt |  52 ++++
 libsoup/soup-hsts-enforcer-db.c         | 132 ++++----
 libsoup/soup-hsts-enforcer-db.h         |  27 +-
 libsoup/soup-hsts-enforcer-private.h    |   7 +-
 libsoup/soup-hsts-enforcer.c            | 516 +++++++++++++++-----------------
 libsoup/soup-hsts-enforcer.h            |  59 ++--
 libsoup/soup-hsts-policy.c              | 438 +++++++++++----------------
 libsoup/soup-hsts-policy.h              |  44 +--
 libsoup/soup-types.h                    |   4 +-
 tests/Makefile.am                       |   2 +
 tests/hsts-db-test.c                    | 177 +++++++++++
 tests/hsts-test.c                       | 408 +++++++++++++++++++++++++
 14 files changed, 1228 insertions(+), 643 deletions(-)
---
diff --git a/docs/reference/Makefile.am b/docs/reference/Makefile.am
index 1baf1135..df33a4f0 100644
--- a/docs/reference/Makefile.am
+++ b/docs/reference/Makefile.am
@@ -46,7 +46,8 @@ IGNORE_HFILES= soup.h soup-autocleanups.h soup-enum-types.h \
        soup-misc-private.h soup-proxy-uri-resolver.h \
        soup-proxy-resolver-wrapper.h soup-proxy-uri-resolver.h \
        soup-cache-private.h soup-cache-client-input-stream.h \
-       soup-socket-private.h soup-value-utils.h soup-xmlrpc-old.h
+       soup-socket-private.h soup-value-utils.h soup-xmlrpc-old.h \
+       soup-hsts-enforcer-private.h
 
 # Images to copy into HTML directory.
 HTML_IMAGES = 
diff --git a/docs/reference/libsoup-2.4-docs.sgml b/docs/reference/libsoup-2.4-docs.sgml
index 7bd4858e..36215abe 100644
--- a/docs/reference/libsoup-2.4-docs.sgml
+++ b/docs/reference/libsoup-2.4-docs.sgml
@@ -54,6 +54,8 @@
     <xi:include href="xml/soup-cookie-jar.xml"/>
     <xi:include href="xml/soup-cookie-jar-text.xml"/>
     <xi:include href="xml/soup-cookie-jar-db.xml"/>
+    <xi:include href="xml/soup-hsts-enforcer.xml"/>
+    <xi:include href="xml/soup-hsts-enforcer-db.xml"/>
     <xi:include href="xml/soup-logger.xml"/>
     <xi:include href="xml/soup-proxy-resolver-default.xml"/>
   </chapter>
diff --git a/docs/reference/libsoup-2.4-sections.txt b/docs/reference/libsoup-2.4-sections.txt
index 354c0783..477adbf3 100644
--- a/docs/reference/libsoup-2.4-sections.txt
+++ b/docs/reference/libsoup-2.4-sections.txt
@@ -1345,3 +1345,55 @@ soup_websocket_error_get_quark
 soup_websocket_error_get_type
 soup_websocket_state_get_type
 </SECTION>
+
+<SECTION>
+<FILE>soup-hsts-enforcer</FILE>
+<TITLE>SoupHSTSEnforcer</TITLE>
+SoupHSTSEnforcer
+SoupHSTSEnforcerClass
+soup_hsts_enforcer_new
+soup_hsts_enforcer_is_persistent
+soup_hsts_enforcer_has_valid_policy
+soup_hsts_enforcer_set_session_policy
+<SUBSECTION>
+SoupHSTSPolicy
+soup_hsts_policy_new
+soup_hsts_policy_new_full
+soup_hsts_policy_new_permanent
+soup_hsts_policy_new_from_response
+soup_hsts_policy_copy
+soup_hsts_policy_equal
+soup_hsts_policy_free
+<SUBSECTION>
+soup_hsts_policy_get_domain
+soup_hsts_policy_is_expired
+soup_hsts_policy_includes_subdomains
+soup_hsts_policy_is_permanet
+SOUP_HSTS_POLICY_MAX_AGE_PAST
+<SUBSECTION Standard>
+SOUP_HSTS_ENFORCER
+SOUP_HSTS_ENFORCER_CLASS
+SOUP_HSTS_ENFORCER_GET_CLASS
+SOUP_TYPE_HSTS_ENFORCER
+SOUP_IS_HSTS_ENFORCER
+SOUP_IS_HSTS_ENFORCER_CLASS
+soup_hsts_enforcer_get_type
+SOUP_TYPE_HSTS_POLICY
+soup_hsts_policy_get_type
+</SECTION>
+
+<SECTION>
+<FILE>soup-hsts-enforcer-db</FILE>
+<TITLE>SoupHSTSEnforcerDB</TITLE>
+SoupHSTSEnforcerDB
+soup_hsts_enforcer_db_new
+<SUBSECTION Standard>
+SoupHSTSEnforcerDBClass
+SOUP_HSTS_ENFORCER_DB
+SOUP_HSTS_ENFORCER_DB_CLASS
+SOUP_HSTS_ENFORCER_DB_GET_CLASS
+SOUP_TYPE_HSTS_ENFORCER_DB
+SOUP_IS_HSTS_ENFORCER_DB
+SOUP_IS_HSTS_ENFORCER_DB_CLASS
+soup_hsts_enforcer_db_get_type
+</SECTION>
diff --git a/libsoup/soup-hsts-enforcer-db.c b/libsoup/soup-hsts-enforcer-db.c
index 319f118d..fc211827 100644
--- a/libsoup/soup-hsts-enforcer-db.c
+++ b/libsoup/soup-hsts-enforcer-db.c
@@ -1,9 +1,10 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
 /*
- * soup-hsts-enforcer-db.c: database-based HSTS policy storage
+ * soup-hsts-enforcer-db.c: persistent HTTP Strict Transport Security feature
  *
  * Using soup-cookie-jar-db as template
- * Copyright (C) 2016 Igalia S.L.
+ * Copyright (C) 2016, 2017, 2018 Igalia S.L.
+ * Copyright (C) 2017, 2018 Metrological Group B.V.
  */
 
 #ifdef HAVE_CONFIG_H
@@ -20,10 +21,10 @@
 
 /**
  * SECTION:soup-hsts-enforcer-db
- * @short_description: Database-based HSTS Enforcer
+ * @short_description: Persistent HTTP Strict Transport Security enforcer
  *
- * #SoupHstsEnforcerDB is a #SoupHstsEnforcer that reads HSTS policies from
- * and writes them to a sqlite database.
+ * #SoupHSTSEnforcerDB is a #SoupHSTSEnforcer that uses a SQLite
+ * database as a backend for persistency.
  **/
 
 enum {
@@ -34,27 +35,27 @@ enum {
        LAST_PROP
 };
 
-typedef struct {
+struct _SoupHSTSEnforcerDBPrivate {
        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))
+#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)
+G_DEFINE_TYPE (SoupHSTSEnforcerDB, soup_hsts_enforcer_db, SOUP_TYPE_HSTS_ENFORCER)
 
-static void load (SoupHstsEnforcer *hsts_enforcer);
+static void load (SoupHSTSEnforcer *hsts_enforcer);
 
 static void
-soup_hsts_enforcer_db_init (SoupHstsEnforcerDB *db)
+soup_hsts_enforcer_db_init (SoupHSTSEnforcerDB *db)
 {
+       db->priv = SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (db);
 }
 
 static void
 soup_hsts_enforcer_db_finalize (GObject *object)
 {
-       SoupHstsEnforcerDBPrivate *priv =
-               SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (object);
+       SoupHSTSEnforcerDBPrivate *priv = SOUP_HSTS_ENFORCER_DB (object)->priv;
 
        g_free (priv->filename);
        g_clear_pointer (&priv->db, sqlite3_close);
@@ -66,8 +67,7 @@ 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);
+       SoupHSTSEnforcerDBPrivate *priv = SOUP_HSTS_ENFORCER_DB (object)->priv;
 
        switch (prop_id) {
        case PROP_FILENAME:
@@ -84,8 +84,7 @@ 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);
+       SoupHSTSEnforcerDBPrivate *priv = SOUP_HSTS_ENFORCER_DB (object)->priv;
 
        switch (prop_id) {
        case PROP_FILENAME:
@@ -99,19 +98,22 @@ soup_hsts_enforcer_db_get_property (GObject *object, guint prop_id,
 
 /**
  * soup_hsts_enforcer_db_new:
- * @filename: the filename to read to/write from, or %NULL
+ * @filename: the filename of the database to read/write from.
  *
- * Creates a #SoupHstsEnforcerDB.
+ * 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.
+ * @filename will be read in during the initialization of a
+ * #SoupHSTSEnforcerDB, in order to create an initial set of HSTS
+ * policies. If the file doesn't exist, a new database will be created
+ * and initialized. Changes to the policies during the lifetime of a
+ * #SoupHSTSEnforcerDB will be written to @filename when
+ * #SoupHSTSEnforcer::changed is emitted.
  *
- * Return value: the new #SoupHstsEnforcer
+ * Return value: the new #SoupHSTSEnforcer
  *
- * Since: 2.54
+ * Since: 2.64
  **/
-SoupHstsEnforcer *
+SoupHSTSEnforcer *
 soup_hsts_enforcer_db_new (const char *filename)
 {
        g_return_val_if_fail (filename != NULL, NULL);
@@ -121,30 +123,32 @@ soup_hsts_enforcer_db_new (const char *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_ALL "SELECT id, host, max_age, expiry, include_subdomains FROM soup_hsts_policies;"
+#define CREATE_TABLE "CREATE TABLE soup_hsts_policies (id INTEGER PRIMARY KEY, host TEXT UNIQUE, max_age 
INTEGER, expiry INTEGER, include_subdomains INTEGER)"
+#define QUERY_INSERT "INSERT OR REPLACE INTO soup_hsts_policies VALUES((SELECT id FROM soup_hsts_policies 
WHERE host=%Q), %Q, %d, %d, %d);"
 #define QUERY_DELETE "DELETE FROM soup_hsts_policies WHERE host=%Q;"
 
 enum {
        COL_ID,
        COL_HOST,
+       COL_MAX_AGE,
        COL_EXPIRY,
-       COL_SUB_DOMAINS,
+       COL_SUBDOMAINS,
        N_COL,
 };
 
 static int
-callback (void *data, int argc, char **argv, char **colname)
+query_all_callback (void *data, int argc, char **argv, char **colname)
 {
-       SoupHstsPolicy *policy = NULL;
-       SoupHstsEnforcer *hsts_enforcer = SOUP_HSTS_ENFORCER (data);
+       SoupHSTSPolicy *policy = NULL;
+       SoupHSTSEnforcer *hsts_enforcer = SOUP_HSTS_ENFORCER (data);
 
        char *host;
        gulong expire_time;
+       unsigned long max_age;
        time_t now;
        SoupDate *expires;
-       gboolean include_sub_domains = FALSE;
+       gboolean include_subdomains = FALSE;
 
        now = time (NULL);
 
@@ -155,13 +159,15 @@ callback (void *data, int argc, char **argv, char **colname)
                return 0;
 
        expires = soup_date_new_from_time_t (expire_time);
-       include_sub_domains = (g_strcmp0 (argv[COL_SUB_DOMAINS], "1") == 0);
+       max_age = strtoul (argv[COL_MAX_AGE], NULL, 10);
+       include_subdomains = (g_strcmp0 (argv[COL_SUBDOMAINS], "1") == 0);
 
-       policy = soup_hsts_policy_new (host, expires, include_sub_domains);
+       policy = soup_hsts_policy_new_full (host, max_age, expires, include_subdomains);
 
-       if (policy)
+       if (policy) {
                soup_hsts_enforcer_set_policy (hsts_enforcer, policy);
-       else
+               soup_hsts_policy_free (policy);
+       } else
                soup_date_free (expires);
 
        return 0;
@@ -204,10 +210,9 @@ try_exec:
 
 /* Follows sqlite3 convention; returns TRUE on error */
 static gboolean
-open_db (SoupHstsEnforcer *hsts_enforcer)
+open_db (SoupHSTSEnforcer *hsts_enforcer)
 {
-       SoupHstsEnforcerDBPrivate *priv =
-               SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (hsts_enforcer);
+       SoupHSTSEnforcerDBPrivate *priv = SOUP_HSTS_ENFORCER_DB (hsts_enforcer)->priv;
 
        char *error = NULL;
 
@@ -227,26 +232,24 @@ open_db (SoupHstsEnforcer *hsts_enforcer)
 }
 
 static void
-load (SoupHstsEnforcer *hsts_enforcer)
+load (SoupHSTSEnforcer *hsts_enforcer)
 {
-       SoupHstsEnforcerDBPrivate *priv =
-               SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (hsts_enforcer);
+       SoupHSTSEnforcerDBPrivate *priv = SOUP_HSTS_ENFORCER_DB (hsts_enforcer)->priv;
 
        if (priv->db == NULL) {
                if (open_db (hsts_enforcer))
                        return;
        }
 
-       exec_query_with_try_create_table (priv->db, QUERY_ALL, callback, hsts_enforcer);
+       exec_query_with_try_create_table (priv->db, QUERY_ALL, query_all_callback, hsts_enforcer);
 }
 
 static void
-soup_hsts_enforcer_db_changed (SoupHstsEnforcer *hsts_enforcer,
-                              SoupHstsPolicy   *old_policy,
-                              SoupHstsPolicy   *new_policy)
+soup_hsts_enforcer_db_changed (SoupHSTSEnforcer *hsts_enforcer,
+                              SoupHSTSPolicy   *old_policy,
+                              SoupHSTSPolicy   *new_policy)
 {
-       SoupHstsEnforcerDBPrivate *priv =
-               SOUP_HSTS_ENFORCER_DB_GET_PRIVATE (hsts_enforcer);
+       SoupHSTSEnforcerDBPrivate *priv = SOUP_HSTS_ENFORCER_DB (hsts_enforcer)->priv;
        char *query;
 
        if (priv->db == NULL) {
@@ -269,29 +272,45 @@ soup_hsts_enforcer_db_changed (SoupHstsEnforcer *hsts_enforcer,
                query = sqlite3_mprintf (QUERY_INSERT,
                                         new_policy->domain,
                                         new_policy->domain,
+                                        new_policy->max_age,
                                         expires,
-                                        new_policy->include_sub_domains);
+                                        new_policy->include_subdomains);
                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)
+soup_hsts_enforcer_db_is_persistent (SoupHSTSEnforcer *hsts_enforcer)
 {
        return TRUE;
 }
 
+static gboolean
+soup_hsts_enforcer_db_has_valid_policy (SoupHSTSEnforcer *hsts_enforcer,
+                                       const char *domain)
+{
+       /* TODO: In the future we should not load the full contents of
+          this database into the enforcer, and instead query the
+          database on request here. Loading the entire database for a
+          potentially large amount of domains is probably not the
+          best approach.
+       */
+
+       return SOUP_HSTS_ENFORCER_CLASS (soup_hsts_enforcer_db_parent_class)->has_valid_policy 
(hsts_enforcer, domain);
+}
+
 static void
-soup_hsts_enforcer_db_class_init (SoupHstsEnforcerDBClass *db_class)
+soup_hsts_enforcer_db_class_init (SoupHSTSEnforcerDBClass *db_class)
 {
-       SoupHstsEnforcerClass *hsts_enforcer_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));
+       g_type_class_add_private (db_class, sizeof (SoupHSTSEnforcerDBPrivate));
 
        hsts_enforcer_class->is_persistent = soup_hsts_enforcer_db_is_persistent;
+       hsts_enforcer_class->has_valid_policy = soup_hsts_enforcer_db_has_valid_policy;
        hsts_enforcer_class->changed       = soup_hsts_enforcer_db_changed;
 
        object_class->finalize     = soup_hsts_enforcer_db_finalize;
@@ -299,10 +318,9 @@ soup_hsts_enforcer_db_class_init (SoupHstsEnforcerDBClass *db_class)
        object_class->get_property = soup_hsts_enforcer_db_get_property;
 
        /**
-        * SOUP_HSTS_ENFORCER_DB_FILENAME:
+        * SoupHSTSEnforcerDB:filename:
         *
-        * Alias for the #SoupHstsEnforcerDB:filename property. (The
-        * HSTS policy storage filename.)
+        * The filename of the SQLite database where HSTS policies are stored.
         **/
        g_object_class_install_property (
                object_class, PROP_FILENAME,
diff --git a/libsoup/soup-hsts-enforcer-db.h b/libsoup/soup-hsts-enforcer-db.h
index 38f56e76..cd4b77b4 100644
--- a/libsoup/soup-hsts-enforcer-db.h
+++ b/libsoup/soup-hsts-enforcer-db.h
@@ -1,6 +1,7 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
 /*
- * Copyright (C) 2016 Igalia S.L.
+ * Copyright (C) 2016, 2017, 2018 Igalia S.L.
+ * Copyright (C) 2017, 2018 Metrological Group B.V.
  */
 
 #ifndef SOUP_HSTS_ENFORCER_DB_H
@@ -11,34 +12,38 @@
 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_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))
+#define SOUP_HSTS_ENFORCER_DB_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_HSTS_ENFORCER_DB, 
SoupHSTSEnforcerDBClass))
+
+typedef struct _SoupHSTSEnforcerDBPrivate SoupHSTSEnforcerDBPrivate;
 
 typedef struct {
-       SoupHstsEnforcer parent;
+       SoupHSTSEnforcer parent;
+
+       SoupHSTSEnforcerDBPrivate *priv;
 
-} SoupHstsEnforcerDB;
+} SoupHSTSEnforcerDB;
 
 typedef struct {
-       SoupHstsEnforcerClass parent_class;
+       SoupHSTSEnforcerClass parent_class;
 
        /* Padding for future expansion */
        void (*_libsoup_reserved1) (void);
        void (*_libsoup_reserved2) (void);
        void (*_libsoup_reserved3) (void);
        void (*_libsoup_reserved4) (void);
-} SoupHstsEnforcerDBClass;
+} SoupHSTSEnforcerDBClass;
 
 #define SOUP_HSTS_ENFORCER_DB_FILENAME  "filename"
 
-SOUP_AVAILABLE_IN_2_42
+SOUP_AVAILABLE_IN_2_64
 GType soup_hsts_enforcer_db_get_type (void);
 
-SOUP_AVAILABLE_IN_2_42
-SoupHstsEnforcer *soup_hsts_enforcer_db_new (const char *filename);
+SOUP_AVAILABLE_IN_2_64
+SoupHSTSEnforcer *soup_hsts_enforcer_db_new (const char *filename);
 
 G_END_DECLS
 
diff --git a/libsoup/soup-hsts-enforcer-private.h b/libsoup/soup-hsts-enforcer-private.h
index 274d0560..9d9b247f 100644
--- a/libsoup/soup-hsts-enforcer-private.h
+++ b/libsoup/soup-hsts-enforcer-private.h
@@ -1,6 +1,7 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
 /*
- * Copyright (C) 2016 Igalia S.L.
+ * Copyright (C) 2016, 2017, 2018 Igalia S.L.
+ * Copyright (C) 2017, 2018 Metrological Group B.V.
  */
 
 #ifndef SOUP_HSTS_ENFORCER_PRIVATE_H
@@ -8,7 +9,7 @@
 
 #include <libsoup/soup-types.h>
 
-void soup_hsts_enforcer_set_policy (SoupHstsEnforcer  *hsts_enforcer,
-                                   SoupHstsPolicy    *policy);
+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
index 8f3cf2b7..e19668dc 100644
--- a/libsoup/soup-hsts-enforcer.c
+++ b/libsoup/soup-hsts-enforcer.c
@@ -1,8 +1,9 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
 /*
- * soup-hsts-enforcer.c: HTTP Strict Transport Security implementation
+ * soup-hsts-enforcer.c: HTTP Strict Transport Security enforcer session feature
  *
- * Copyright (C) 2016 Igalia S.L.
+ * Copyright (C) 2016, 2017, 2018 Igalia S.L.
+ * Copyright (C) 2017, 2018 Metrological Group B.V.
  */
 
 /* TODO Use only internationalized domain names */
@@ -21,26 +22,35 @@
 
 /**
  * SECTION:soup-hsts-enforcer
- * @short_description: Automatic HSTS enforcing for SoupSession
+ * @short_description: Automatic HTTP Strict Transport Security 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().
+ * A #SoupHSTSEnforcer stores HSTS policies and enforces 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).
+ * #SoupHSTSEnforcer keeps track of all the HTTPS destinations that,
+ * when connected to, return the Strict-Transport-Security header with
+ * valid values. #SoupHSTSEnforcer will forget those destinations
+ * upon expiry or when the server requests it.
+ *
+ * When the #SoupSession the #SoupHSTSEnforcer is attached to queues
+ * or restarts a message, the #SoupHSTSEnforcer will rewrite the URI
+ * to HTTPS if the destination is a known HSTS host and is contacted
+ * over an insecure transport protocol (HTTP). Users of
+ * #SoupHSTSEnforcer are advised to listen to changes in
+ * SoupMessage:uri in order to be aware of changes in the message URI.
+ *
+ * Note that #SoupHSTSEnforcer does not support any form of long-term
+ * HSTS policy persistence. See #SoupHSTSDBEnforcer for a persistent
+ * enforcer.
  *
- * 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_DEFINE_TYPE_WITH_CODE (SoupHSTSEnforcer, soup_hsts_enforcer, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
                                                soup_hsts_enforcer_session_feature_init))
 
@@ -51,30 +61,31 @@ enum {
 
 static guint signals[LAST_SIGNAL] = { 0 };
 
-typedef struct {
+struct _SoupHSTSEnforcerPrivate {
        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))
+};
+
+#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)
+soup_hsts_enforcer_init (SoupHSTSEnforcer *hsts_enforcer)
 {
-       SoupHstsEnforcerPrivate *priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+       hsts_enforcer->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);
+       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);
+       hsts_enforcer->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);
+       SoupHSTSEnforcerPrivate *priv = SOUP_HSTS_ENFORCER (object)->priv;
        GHashTableIter iter;
        gpointer key, value;
 
@@ -92,27 +103,58 @@ soup_hsts_enforcer_finalize (GObject *object)
 }
 
 static gboolean
-soup_hsts_enforcer_real_is_persistent (SoupHstsEnforcer *hsts_enforcer)
+soup_hsts_enforcer_real_is_persistent (SoupHSTSEnforcer *hsts_enforcer)
+{
+       return FALSE;
+}
+
+static SoupHSTSPolicy *
+soup_hsts_enforcer_get_host_policy (SoupHSTSEnforcer *hsts_enforcer,
+                                   const char *domain)
+{
+       return g_hash_table_lookup (hsts_enforcer->priv->host_policies, domain);
+}
+
+static SoupHSTSPolicy *
+soup_hsts_enforcer_get_session_policy (SoupHSTSEnforcer *hsts_enforcer,
+                                      const char *domain)
+{
+       return g_hash_table_lookup (hsts_enforcer->priv->session_policies, domain);
+}
+
+static gboolean
+soup_hsts_enforcer_real_has_valid_policy (SoupHSTSEnforcer *hsts_enforcer,
+                                         const char *domain)
 {
+       SoupHSTSPolicy *policy;
+
+       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 void
-soup_hsts_enforcer_class_init (SoupHstsEnforcerClass *hsts_enforcer_class)
+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));
+       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;
+       hsts_enforcer_class->has_valid_policy = soup_hsts_enforcer_real_has_valid_policy;
 
        /**
-        * SoupHstsEnforcer::changed:
-        * @hsts_enforcer: the #SoupHstsEnforcer
-        * @old_policy: the old #SoupHstsPolicy value
-        * @new_policy: the new #SoupHstsPolicy value
+        * 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
@@ -126,7 +168,7 @@ soup_hsts_enforcer_class_init (SoupHstsEnforcerClass *hsts_enforcer_class)
                g_signal_new ("changed",
                              G_OBJECT_CLASS_TYPE (object_class),
                              G_SIGNAL_RUN_FIRST,
-                             G_STRUCT_OFFSET (SoupHstsEnforcerClass, changed),
+                             G_STRUCT_OFFSET (SoupHSTSEnforcerClass, changed),
                              NULL, NULL,
                              NULL,
                              G_TYPE_NONE, 2,
@@ -137,124 +179,110 @@ soup_hsts_enforcer_class_init (SoupHstsEnforcerClass *hsts_enforcer_class)
 /**
  * soup_hsts_enforcer_new:
  *
- * Creates a new #SoupHstsEnforcer. The base #SoupHstsEnforcer class does
+ * 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
+ * Returns: a new #SoupHSTSEnforcer
  *
- * Since: 2.54
+ * Since: 2.64
  **/
-SoupHstsEnforcer *
+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)
+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)
+static gboolean
+should_remove_expired_host_policy (G_GNUC_UNUSED gpointer key,
+                                  SoupHSTSPolicy *policy,
+                                  SoupHSTSEnforcer *enforcer)
 {
-       SoupHstsEnforcerPrivate *priv;
-       SoupHstsPolicy *policy;
-       GList *domains, *p;
-       const char *domain;
-
-       g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
+       if (soup_hsts_policy_is_expired (policy)) {
+               /* This will emit the ::changed signal before the
+                  policy is actually removed from the policies hash
+                  table, which could be problematic, or not. On the
+                  other hand, I have my doubts that the ::changed
+                  signal has any use.
+               */
+               soup_hsts_enforcer_changed (enforcer, policy, NULL);
+               soup_hsts_policy_free (policy);
 
-       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);
-               }
+               return TRUE;
        }
-       g_list_free (domains);
+
+       return FALSE;
 }
 
 static void
-soup_hsts_enforcer_remove_host_policy (SoupHstsEnforcer *hsts_enforcer,
-                                      const gchar *domain)
+remove_expired_host_policies (SoupHSTSEnforcer *hsts_enforcer)
 {
-       SoupHstsEnforcerPrivate *priv;
-       SoupHstsPolicy *policy;
-
-       g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
-       g_return_if_fail (domain != NULL);
+       g_hash_table_foreach_remove (hsts_enforcer->priv->host_policies,
+                                    (GHRFunc)should_remove_expired_host_policy,
+                                    hsts_enforcer);
+}
 
-       priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
+static void
+soup_hsts_enforcer_remove_host_policy (SoupHSTSEnforcer *hsts_enforcer,
+                                      const char *domain)
+{
+       SoupHSTSPolicy *policy;
 
-       policy = g_hash_table_lookup (priv->host_policies, domain);
+       policy = g_hash_table_lookup (hsts_enforcer->priv->host_policies, domain);
 
-       g_assert_nonnull (policy);
+       if (!policy)
+               return;
 
-       g_hash_table_remove (priv->host_policies, domain);
+       g_hash_table_remove (hsts_enforcer->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);
+       remove_expired_host_policies (hsts_enforcer);
 }
 
 static void
-soup_hsts_enforcer_replace_policy (SoupHstsEnforcer *hsts_enforcer,
-                                  SoupHstsPolicy *new_policy)
+soup_hsts_enforcer_replace_policy (SoupHSTSEnforcer *hsts_enforcer,
+                                  SoupHSTSPolicy *new_policy)
 {
-       SoupHstsEnforcerPrivate *priv;
        GHashTable *policies;
-       SoupHstsPolicy *old_policy;
-       const gchar *domain;
+       SoupHSTSPolicy *old_policy;
+       const char *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;
+       policies = is_permanent ? hsts_enforcer->priv->session_policies :
+                                 hsts_enforcer->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);
+       g_hash_table_replace (policies, g_strdup (domain), soup_hsts_policy_copy (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);
+       remove_expired_host_policies (hsts_enforcer);
 }
 
 static void
-soup_hsts_enforcer_insert_policy (SoupHstsEnforcer *hsts_enforcer,
-                                 SoupHstsPolicy *policy)
+soup_hsts_enforcer_insert_policy (SoupHSTSEnforcer *hsts_enforcer,
+                                 SoupHSTSPolicy *policy)
 {
-       SoupHstsEnforcerPrivate *priv;
        GHashTable *policies;
-       const gchar *domain;
+       const char *domain;
        gboolean is_permanent;
 
        g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
@@ -267,165 +295,117 @@ soup_hsts_enforcer_insert_policy (SoupHstsEnforcer *hsts_enforcer,
 
        g_return_if_fail (domain != NULL);
 
-       priv = SOUP_HSTS_ENFORCER_GET_PRIVATE (hsts_enforcer);
-       policies = is_permanent ? priv->session_policies :
-                                 priv->host_policies;
+       policies = is_permanent ? hsts_enforcer->priv->session_policies :
+                                 hsts_enforcer->priv->host_policies;
 
        g_assert_false (g_hash_table_contains (policies, domain));
 
-       g_hash_table_insert (policies, g_strdup (domain), policy);
+       g_hash_table_insert (policies, g_strdup (domain), soup_hsts_policy_copy (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
+ * @hsts_enforcer: a #SoupHSTSEnforcer
+ * @policy: (transfer none): 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.
+ * existing HSTS policy for this host will be removed instead. If a
+ * policy existed for this host, it will be replaced. Otherwise, the
+ * new policy will be inserted. If the policy is a permanent one, that
+ * is, one created with soup_hsts_policy_new_permanent(), the policy
+ * will not expire and will be enforced during the lifetime of
+ * @soup_enforcer's #SoupSession.
  *
- * This steals @policy.
- *
- * Since: 2.54
+ * Since: 2.64
  **/
 void
-soup_hsts_enforcer_set_policy (SoupHstsEnforcer *hsts_enforcer,
-                              SoupHstsPolicy *policy)
+soup_hsts_enforcer_set_policy (SoupHSTSEnforcer *hsts_enforcer,
+                              SoupHSTSPolicy *policy)
 {
-       SoupHstsEnforcerPrivate *priv;
        GHashTable *policies;
-       const gchar *domain;
+       const char *domain;
        gboolean is_permanent;
+       SoupHSTSPolicy *current_policy;
 
        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;
+       is_permanent = soup_hsts_policy_is_permanent (policy);
+       policies = is_permanent ? hsts_enforcer->priv->session_policies :
+                                 hsts_enforcer->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))
+       current_policy = g_hash_table_lookup (policies, domain);
+
+       if (current_policy)
                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
+ * @hsts_enforcer: a #SoupHSTSEnforcer
  * @domain: policy domain or hostname
- * @include_sub_domains: %TRUE if the policy applies on sub domains
+ * @include_subdomains: %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.
+ * Sets a session policy for @domain. A session policy is a policy
+ * that is permanent to the lifetime of @hsts_enforcer's #SoupSession
+ * and doesn't expire.
  *
- * Since: 2.54
+ * Since: 2.64
  **/
 void
-soup_hsts_enforcer_set_session_policy (SoupHstsEnforcer *hsts_enforcer,
+soup_hsts_enforcer_set_session_policy (SoupHSTSEnforcer *hsts_enforcer,
                                       const char *domain,
-                                      gboolean include_sub_domains)
+                                      gboolean include_subdomains)
 {
-       SoupHstsPolicy *policy;
+       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);
+       policy = soup_hsts_policy_new_permanent (domain, include_subdomains);
        soup_hsts_enforcer_set_policy (hsts_enforcer, policy);
+       soup_hsts_policy_free (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)
+soup_hsts_enforcer_host_includes_subdomains (SoupHSTSEnforcer *hsts_enforcer,
+                                             const char *domain)
 {
-       SoupHstsPolicy *policy;
-       gboolean include_sub_domains = FALSE;
+       SoupHSTSPolicy *policy;
+       gboolean include_subdomains = 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);
+               include_subdomains |= soup_hsts_policy_includes_subdomains (policy);
 
        policy = soup_hsts_enforcer_get_host_policy (hsts_enforcer, domain);
        if (policy)
-               include_sub_domains |= soup_hsts_policy_includes_sub_domains (policy);
+               include_subdomains |= soup_hsts_policy_includes_subdomains (policy);
 
-       return include_sub_domains;
+       return include_subdomains;
 }
 
-static inline const gchar*
-super_domain_of (const gchar *domain)
+static inline const char*
+super_domain_of (const char *domain)
 {
-       const gchar *iter = domain;
+       const char *iter = domain;
 
-       g_return_val_if_fail (domain != NULL, NULL);
+       g_assert_nonnull (domain);
 
        for (; *iter != '\0' && *iter != '.' ; iter++);
        for (; *iter == '.' ; iter++);
@@ -437,39 +417,33 @@ super_domain_of (const gchar *domain)
 }
 
 static gboolean
-soup_hsts_enforcer_must_enforce_secure_transport (SoupHstsEnforcer *hsts_enforcer,
-                                                 const gchar *domain)
+soup_hsts_enforcer_must_enforce_secure_transport (SoupHSTSEnforcer *hsts_enforcer,
+                                                 const char *domain)
 {
-       const gchar *super_domain = domain;
+       const char *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))
+       if (soup_hsts_enforcer_has_valid_policy (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))
+               if (soup_hsts_enforcer_host_includes_subdomains (hsts_enforcer, super_domain) &&
+                   soup_hsts_enforcer_has_valid_policy (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,
+soup_hsts_enforcer_process_sts_header (SoupHSTSEnforcer *hsts_enforcer,
                                       SoupMessage *msg)
 {
-       SoupHstsPolicy *policy;
+       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);
@@ -477,94 +451,78 @@ soup_hsts_enforcer_process_sts_header (SoupHstsEnforcer *hsts_enforcer,
        g_return_if_fail (uri != NULL);
 
        policy = soup_hsts_policy_new_from_response (msg);
+       if (policy) {
+               soup_hsts_enforcer_set_policy (hsts_enforcer, policy);
+               soup_hsts_policy_free (policy);
+       }
+}
 
-       g_return_if_fail (policy != NULL);
+static void
+got_sts_headers_cb (SoupMessage *msg, gpointer user_data)
+{
+       SoupHSTSEnforcer *hsts_enforcer = SOUP_HSTS_ENFORCER (user_data);
 
-       soup_hsts_enforcer_set_policy (hsts_enforcer, policy);
+       soup_hsts_enforcer_process_sts_header (hsts_enforcer, msg);
 }
 
-/* Enforces HTTPS when demanded. */
-static gboolean
-soup_hsts_enforcer_should_redirect_to_https (SoupHstsEnforcer *hsts_enforcer,
-                                            SoupMessage *msg)
+static void
+rewrite_message_uri_to_https (SoupMessage *msg)
 {
        SoupURI *uri;
-       const gchar *domain;
-
-       g_return_val_if_fail (hsts_enforcer != NULL, FALSE);
-       g_return_val_if_fail (msg != NULL, FALSE);
+       uint original_port;
 
-       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;
+       uri = soup_uri_copy (soup_message_get_uri (msg));
 
-       domain = soup_uri_get_host (uri);
-
-       g_return_val_if_fail (domain != NULL, FALSE);
+       original_port = soup_uri_get_port (uri);
+       /* This will unconditionally rewrite the port to 443. */
+       soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTPS);
+       /* From the RFC: "If the URI contains an explicit port component that
+          is not equal to "80", the port component value MUST be preserved;" */
+       if (original_port != 80)
+               soup_uri_set_port (uri, original_port);
 
-       return soup_hsts_enforcer_must_enforce_secure_transport (hsts_enforcer, domain);
+       soup_message_set_uri (msg, uri);
+       soup_uri_free (uri);
 }
 
 static void
-redirect_to_https (SoupMessage *msg)
+preprocess_request (SoupHSTSEnforcer *enforcer, SoupMessage *msg)
 {
-       SoupURI *src_uri, *dst_uri;
-       char *dst;
+       SoupURI *uri;
+       const char *scheme;
+       const char *host;
 
-       src_uri = soup_message_get_uri (msg);
+       uri = soup_message_get_uri (msg);
+       host = soup_uri_get_host (uri);
 
-       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);
+       if (g_hostname_is_ip_address (host))
+               return;
 
-       soup_message_set_redirect (msg, 301, dst);
-       g_free (dst);
+       scheme = soup_uri_get_scheme (uri);
+       if (scheme == SOUP_URI_SCHEME_HTTP) {
+               if (soup_hsts_enforcer_must_enforce_secure_transport (enforcer, soup_uri_get_host (uri)))
+                       rewrite_message_uri_to_https (msg);
+       } else if (scheme == SOUP_URI_SCHEME_HTTPS) {
+               soup_message_add_header_handler (msg, "got-headers",
+                                                "Strict-Transport-Security",
+                                                G_CALLBACK (got_sts_headers_cb),
+                                                enforcer);
+       }
 }
 
 static void
-process_sts_header (SoupMessage *msg, gpointer user_data)
+message_restarted_cb (SoupMessage *msg, gpointer user_data)
 {
-       SoupHstsEnforcer *hsts_enforcer = SOUP_HSTS_ENFORCER (user_data);
+       preprocess_request (SOUP_HSTS_ENFORCER (user_data), msg);
 
-       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);
-       }
+       g_signal_connect (msg, "restarted", G_CALLBACK (message_restarted_cb), feature);
+       preprocess_request (SOUP_HSTS_ENFORCER (feature), msg);
 }
 
 static void
@@ -572,7 +530,7 @@ soup_hsts_enforcer_request_unqueued (SoupSessionFeature *feature,
                                     SoupSession *session,
                                     SoupMessage *msg)
 {
-       g_signal_handlers_disconnect_by_func (msg, process_sts_header, feature);
+       g_signal_handlers_disconnect_by_func (msg, got_sts_headers_cb, feature);
 }
 
 static void
@@ -585,18 +543,40 @@ soup_hsts_enforcer_session_feature_init (SoupSessionFeatureInterface *feature_in
 
 /**
  * soup_hsts_enforcer_is_persistent:
- * @hsts_enforcer: a #SoupHstsEnforcer
+ * @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
+ * Since: 2.64
  **/
 gboolean
-soup_hsts_enforcer_is_persistent (SoupHstsEnforcer *hsts_enforcer)
+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);
 }
+
+/**
+ * soup_hsts_enforcer_has_valid_policy:
+ * @hsts_enforcer: a #SoupHSTSEnforcer
+ * @domain: a domain.
+ *
+ * Gets whether @hsts_enforcer has a currently valid policy for @domain.
+ *
+ * Returns: %TRUE if access to @domain should happen over HTTPS, false
+ * otherwise.
+ *
+ * Since: 2.64
+ **/
+gboolean
+soup_hsts_enforcer_has_valid_policy (SoupHSTSEnforcer *hsts_enforcer,
+                                    const char *domain)
+{
+       g_return_val_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer), FALSE);
+       g_return_val_if_fail (domain != NULL, FALSE);
+
+       return SOUP_HSTS_ENFORCER_GET_CLASS (hsts_enforcer)->has_valid_policy (hsts_enforcer, domain);
+}
diff --git a/libsoup/soup-hsts-enforcer.h b/libsoup/soup-hsts-enforcer.h
index 1253e234..fbceab46 100644
--- a/libsoup/soup-hsts-enforcer.h
+++ b/libsoup/soup-hsts-enforcer.h
@@ -1,6 +1,7 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
 /*
- * Copyright (C) 2016 Igalia S.L.
+ * Copyright (C) 2016, 2017, 2018 Igalia S.L.
+ * Copyright (C) 2017, 2018 Metrological Group B.V.
  */
 
 #ifndef SOUP_HSTS_ENFORCER_H
@@ -11,43 +12,63 @@
 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_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))
+#define SOUP_HSTS_ENFORCER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_HSTS_ENFORCER, 
SoupHSTSEnforcerClass))
 
-struct _SoupHstsEnforcer {
+typedef struct _SoupHSTSEnforcerPrivate SoupHSTSEnforcerPrivate;
+
+struct _SoupHSTSEnforcer {
        GObject parent;
 
+       SoupHSTSEnforcerPrivate *priv;
 };
 
+/**
+ * SoupHSTSEnforcerClass:
+ * @parent_class: The parent class.
+ * @is_persistent: The @is_persistent function advertises whether the enforcer is persistent or
+ * whether changes made to it will be lost when the underlying #SoupSession is finished.
+ * @has_valid_policy: The @has_valid_policy function is called to check whether there is a valid
+ * policy for the given domain. This method should return %TRUE for #SoupHSTSEnforcer to
+ * change the scheme of the #SoupURI in the #SoupMessage to HTTPS. Implementations might want to
+ * chain up to the @has_valid_policy in the parent class to check, for instance, for runtime
+ * policies.
+ * @changed: the class closure for the #SoupHSTSEnforcer::changed signal.
+ **/
 typedef struct {
        GObjectClass parent_class;
 
-       gboolean (*is_persistent) (SoupHstsEnforcer *hsts_enforcer);
+       gboolean (*is_persistent) (SoupHSTSEnforcer *hsts_enforcer);
+       gboolean (*has_valid_policy) (SoupHSTSEnforcer *hsts_enforcer, const char *domain);
 
        /* signals */
-       void (*changed) (SoupHstsEnforcer *jar,
-                        SoupHstsPolicy   *old_policy,
-                        SoupHstsPolicy   *new_policy);
+       void (*changed) (SoupHSTSEnforcer *jar,
+                        SoupHSTSPolicy   *old_policy,
+                        SoupHSTSPolicy   *new_policy);
 
        /* Padding for future expansion */
        void (*_libsoup_reserved1) (void);
        void (*_libsoup_reserved2) (void);
-} SoupHstsEnforcerClass;
+       void (*_libsoup_reserved3) (void);
+       void (*_libsoup_reserved4) (void);
+} SoupHSTSEnforcerClass;
 
-SOUP_AVAILABLE_IN_2_54
+SOUP_AVAILABLE_IN_2_64
 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,
+SOUP_AVAILABLE_IN_2_64
+SoupHSTSEnforcer *soup_hsts_enforcer_new                           (void);
+SOUP_AVAILABLE_IN_2_64
+gboolean          soup_hsts_enforcer_is_persistent                 (SoupHSTSEnforcer *hsts_enforcer);
+SOUP_AVAILABLE_IN_2_64
+gboolean          soup_hsts_enforcer_has_valid_policy              (SoupHSTSEnforcer *hsts_enforcer,
+                                                                   const char       *domain);
+SOUP_AVAILABLE_IN_2_64
+void              soup_hsts_enforcer_set_session_policy            (SoupHSTSEnforcer *hsts_enforcer,
                                                                    const char       *domain,
-                                                                   gboolean          include_sub_domains);
+                                                                   gboolean          include_subdomains);
 G_END_DECLS
 
 #endif /* SOUP_HSTS_ENFORCER_H */
diff --git a/libsoup/soup-hsts-policy.c b/libsoup/soup-hsts-policy.c
index e2989dbb..52c076b0 100644
--- a/libsoup/soup-hsts-policy.c
+++ b/libsoup/soup-hsts-policy.c
@@ -1,14 +1,16 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
 /*
- * soup-hsts-policy.c
+ * soup-hsts-policy.c: HSTS policy structure
  *
- * Copyright (C) 2016 Igalia S.L.
+ * Copyright (C) 2016, 2017, 2018 Igalia S.L.
+ * Copyright (C) 2017, 2018 Metrological Group B.V.
  */
 
 #ifdef HAVE_CONFIG_H
 #include <config.h>
 #endif
 
+#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
@@ -18,78 +20,80 @@
 /**
  * SECTION:soup-hsts-policy
  * @short_description: HTTP Strict Transport Security policies
- * @see_also: #SoupHstsEnforcer
+ * @see_also: #SoupHSTSEnforcer
  *
- * #SoupHstsPolicy implements HTTP policies, as described by <ulink
+ * #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.
+ * 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
+ * SoupHSTSPolicy:
+ * @domain: The domain or hostname that the policy applies to
+ * @max_age: The maximum age, in seconds, that the policy is valid
+ * @expires: the policy expiration time, or %NULL for a permanent session policy
+ * @include_subdomains: %TRUE if the policy applies on subdomains
  *
  * An HTTP Strict Transport Security policy.
  *
  * @domain give the host or domain that this policy belongs to and applies
  * on.
  *
+ * @max_age contains the 'max-age' value from the Strict Transport
+ * Security header and indicates the time to live of this policy,
+ * in seconds.
+ *
  * @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.
+ * policy is a permanent session policy set by the user agent.
+ *
+ * If @include_subdomains is %TRUE, the Strict Transport Security policy
+ * must also be enforced on subdomains of @domain.
  *
- * Since: 2.54
+ * Since: 2.64
  **/
 
-G_DEFINE_BOXED_TYPE (SoupHstsPolicy, soup_hsts_policy, soup_hsts_policy_copy, soup_hsts_policy_free)
+G_DEFINE_BOXED_TYPE (SoupHSTSPolicy, soup_hsts_policy, soup_hsts_policy_copy, soup_hsts_policy_free)
 
 /**
  * soup_hsts_policy_copy:
- * @policy: a #SoupHstsPolicy
+ * @policy: a #SoupHSTSPolicy
  *
  * Copies @policy.
  *
- * Return value: a copy of @policy
+ * Returns: (transfer full): a copy of @policy
  *
- * Since: 2.54
+ * Since: 2.64
  **/
-SoupHstsPolicy *
-soup_hsts_policy_copy (SoupHstsPolicy *policy)
+SoupHSTSPolicy *
+soup_hsts_policy_copy (SoupHSTSPolicy *policy)
 {
-       SoupHstsPolicy *copy = g_slice_new0 (SoupHstsPolicy);
+       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;
+       copy->max_age = policy->max_age;
+       copy->expires = policy->expires ?
+               soup_date_copy (policy->expires) : NULL;
+       copy->include_subdomains = policy->include_subdomains;
 
        return copy;
 }
 
 /**
  * soup_hsts_policy_equal:
- * @policy1: a #SoupCookie
- * @policy2: a #SoupCookie
+ * @policy1: a #SoupHSTSPolicy
+ * @policy2: a #SoupHSTSPolicy
  *
  * 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.
+ * Returns: whether the policies are equal.
  *
  * Since: 2.24
  */
 gboolean
-soup_hsts_policy_equal (SoupHstsPolicy *policy1, SoupHstsPolicy *policy2)
+soup_hsts_policy_equal (SoupHSTSPolicy *policy1, SoupHSTSPolicy *policy2)
 {
        g_return_val_if_fail (policy1, FALSE);
        g_return_val_if_fail (policy2, FALSE);
@@ -97,7 +101,10 @@ soup_hsts_policy_equal (SoupHstsPolicy *policy1, SoupHstsPolicy *policy2)
        if (strcmp (policy1->domain, policy2->domain))
                return FALSE;
 
-       if (policy1->include_sub_domains != policy2->include_sub_domains)
+       if (policy1->include_subdomains != policy2->include_subdomains)
+               return FALSE;
+
+       if (policy1->max_age != policy2->max_age)
                return FALSE;
 
        if ((policy1->expires && !policy2->expires) ||
@@ -112,186 +119,24 @@ soup_hsts_policy_equal (SoupHstsPolicy *policy1, SoupHstsPolicy *policy2)
        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.
- **/
+/*
+ * Returns: %TRUE if the hostname is suitable for an HSTS host, %FALSE
+ * otherwise. Suitable hostnames are any that is not an IP address.
+ */
 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;
+       /* IP addresses are not valid hostnames, only domain names are. */
+       return hostname && !g_hostname_is_ip_address (hostname);
 }
 
 /**
  * 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
+ * @include_subdomains: %TRUE if the policy applies on subdomains
  *
- * Creates a new #SoupHstsPolicy with the given attributes.
+ * 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.
@@ -300,22 +145,21 @@ soup_hsts_policy_new (const char *domain, SoupDate *expires,
  * 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
+ * If @include_subdomains is %TRUE, the strict transport security policy
  * must also be enforced on all subdomains of @domain.
  *
- * Return value: a new #SoupHstsPolicy.
+ * Returns: a new #SoupHSTSPolicy.
  *
- * Since: 2.54
+ * Since: 2.64
  **/
-SoupHstsPolicy *
-soup_hsts_policy_new_with_max_age (const char *domain, int max_age,
-                                  gboolean include_sub_domains)
+SoupHSTSPolicy *
+soup_hsts_policy_new (const char *domain,
+                     unsigned long max_age,
+                     gboolean include_subdomains)
 {
        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
@@ -325,10 +169,38 @@ soup_hsts_policy_new_with_max_age (const char *domain, int max_age,
        } else
                expires = soup_date_new_from_now (max_age);
 
-       policy = soup_hsts_policy_new (domain, expires, include_sub_domains);
+       return soup_hsts_policy_new_full (domain, max_age, expires, include_subdomains);
+}
+
+/**
+ * soup_hsts_policy_new_full:
+ * @domain: policy domain or hostname
+ * @max_age: max age of the policy
+ * @expires: the date of expiration of the policy or %NULL for a permanent policy
+ * @include_subdomains: %TRUE if the policy applies on subdomains
+ *
+ * Full version of #soup_hsts_policy_new(), to use with an existing
+ * expiration date. See #soup_hsts_policy_new() for details.
+ *
+ * Returns: a new #SoupHSTSPolicy.
+ *
+ * Since: 2.64
+ **/
+SoupHSTSPolicy *
+soup_hsts_policy_new_full (const char *domain,
+                          unsigned long max_age,
+                          SoupDate *expires,
+                          gboolean include_subdomains)
+{
+       SoupHSTSPolicy *policy;
+
+       g_return_val_if_fail (is_hostname_valid (domain), NULL);
 
-       if (!policy)
-               soup_date_free (expires);
+       policy = g_slice_new0 (SoupHSTSPolicy);
+       policy->domain = g_strdup (domain);
+       policy->max_age = max_age;
+       policy->expires = expires;
+       policy->include_subdomains = include_subdomains;
 
        return policy;
 }
@@ -336,55 +208,93 @@ soup_hsts_policy_new_with_max_age (const char *domain, int max_age,
 /**
  * soup_hsts_policy_new_permanent:
  * @domain: policy domain or hostname
- * @include_sub_domains: %TRUE if the policy applies on sub domains
+ * @include_subdomains: %TRUE if the policy applies on sub domains
  *
- * Creates a new #SoupHstsPolicy with the given attributes.
+ * 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
+ * If @include_subdomains is %TRUE, the strict transport security policy
  * must also be enforced on all subdomains of @domain.
  *
- * Return value: a new #SoupHstsPolicy.
+ * Returns: a new #SoupHSTSPolicy.
  *
- * Since: 2.54
+ * Since: 2.64
  **/
-SoupHstsPolicy *
+SoupHSTSPolicy *
 soup_hsts_policy_new_permanent (const char *domain,
-                               gboolean include_sub_domains)
+                               gboolean include_subdomains)
 {
-       return soup_hsts_policy_new (domain, NULL, include_sub_domains);
+       SoupHSTSPolicy *policy;
+
+       g_return_val_if_fail (is_hostname_valid (domain), NULL);
+
+       policy = soup_hsts_policy_new (domain, 0, include_subdomains);
+       g_clear_pointer (&policy->expires, soup_date_free);
+
+       return policy;
 }
 
 /**
  * soup_hsts_policy_new_from_response:
- * @msg: a #SoupMessage containing a "Strict-Transport-Security" response
- * header
+ * @msg: a #SoupMessage
  *
  * 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.
+ * returns a #SoupHSTSPolicy.
  *
- * Return value: (nullable): a new #SoupHstsPolicy, or %NULL if no valid
+ * Returns: (nullable): a new #SoupHSTSPolicy, or %NULL if no valid
  * "Strict-Transport-Security" response header was found.
  *
- * Since: 2.54
+ * Since: 2.64
  **/
-SoupHstsPolicy *
+SoupHSTSPolicy *
 soup_hsts_policy_new_from_response (SoupMessage *msg)
 {
-       SoupURI *origin;
-       const char *name, *value;
        SoupMessageHeadersIter iter;
+       const char *name, *value;
+
+       g_return_val_if_fail (SOUP_IS_MESSAGE (msg), NULL);
 
        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)
+               SoupURI *origin;
+               GHashTable *params;
+               const char *max_age_str;
+               char *endptr;
+               unsigned long max_age;
+               gboolean include_subdomains;
+               gpointer include_subdomains_value = NULL;
+               SoupHSTSPolicy *policy = NULL;
+
+               if (strcmp (name, "Strict-Transport-Security") != 0)
                        continue;
 
                origin = soup_message_get_uri (msg);
-               return parse_one_policy (value, origin);
+
+               params = soup_header_parse_semi_param_list (value);
+
+               max_age_str = g_hash_table_lookup (params, "max-age");
+
+               if (!max_age_str)
+                       goto out;
+               max_age = strtoul (max_age_str, &endptr, 10);
+               if (*endptr != '\0')
+                       goto out;
+
+               include_subdomains = g_hash_table_lookup_extended (params, "includeSubDomains", NULL,
+                                                                  &include_subdomains_value);
+               /* includeSubdomains shouldn't have a value. */
+               if (include_subdomains_value)
+                       goto out;
+               /* if there are extra params, the HSTS spec demands the header to be ignored. */
+               if (g_hash_table_size (params) > (include_subdomains ? 2 : 1))
+                       goto out;
+
+               policy = soup_hsts_policy_new (origin->host, max_age, include_subdomains);
+       out:
+               soup_header_free_param_list (params);
+               return policy;
        }
 
        return NULL;
@@ -392,88 +302,94 @@ soup_hsts_policy_new_from_response (SoupMessage *msg)
 
 /**
  * soup_hsts_policy_get_domain:
- * @policy: a #SoupHstsPolicy
+ * @policy: a #SoupHSTSPolicy
  *
  * Gets @policy's domain.
  *
- * Return value: @policy's domain.
+ * Returns: (transfer none): @policy's domain.
  *
- * Since: 2.54
+ * Since: 2.64
  **/
 const char *
-soup_hsts_policy_get_domain (SoupHstsPolicy *policy)
+soup_hsts_policy_get_domain (SoupHSTSPolicy *policy)
 {
+       g_return_val_if_fail (policy != NULL, NULL);
+
        return policy->domain;
 }
 
 /**
  * soup_hsts_policy_is_expired:
- * @policy: a #SoupHstsPolicy
+ * @policy: a #SoupHSTSPolicy
  *
- * Gets whether @policy is expired.
+ * Gets whether @policy is expired. Permanent policies never
+ * expire.
  *
- * Permanent policies never expire.
+ * Returns: %TRUE if @policy is expired, %FALSE otherwise.
  *
- * Return value: whether @policy is expired.
- *
- * Since: 2.54
+ * Since: 2.64
  **/
 gboolean
-soup_hsts_policy_is_expired (SoupHstsPolicy *policy)
+soup_hsts_policy_is_expired (SoupHSTSPolicy *policy)
 {
+       g_return_val_if_fail (policy != NULL, TRUE);
+
        return policy->expires && soup_date_is_past (policy->expires);
 }
 
 /**
- * soup_hsts_policy_includes_sub_domains:
- * @policy: a #SoupHstsPolicy
+ * soup_hsts_policy_includes_subdomains:
+ * @policy: a #SoupHSTSPolicy
  *
- * Gets whether @policy include its sub-domains.
+ * Gets whether @policy include its subdomains.
  *
- * Return value: whether @policy include its sub-domains.
+ * Returns: %TRUE if @policy includes subdomains, %FALSE otherwise.
  *
- * Since: 2.54
+ * Since: 2.64
  **/
 gboolean
-soup_hsts_policy_includes_sub_domains (SoupHstsPolicy *policy)
+soup_hsts_policy_includes_subdomains (SoupHSTSPolicy *policy)
 {
-       return policy->include_sub_domains;
+       g_return_val_if_fail (policy != NULL, FALSE);
+
+       return policy->include_subdomains;
 }
 
 /**
  * soup_hsts_policy_is_permanent:
- * @policy: a #SoupHstsPolicy
+ * @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.
+ * #SoupHSTSEnforcer so that the user agent can control them.
  *
- * Return value: whether @policy is permanent.
+ * Returns: %TRUE if @policy is permanent, %FALSE otherwise
  *
- * Since: 2.54
+ * Since: 2.64
  **/
 gboolean
-soup_hsts_policy_is_permanent (SoupHstsPolicy *policy)
+soup_hsts_policy_is_permanent (SoupHSTSPolicy *policy)
 {
+       g_return_val_if_fail (policy != NULL, FALSE);
+
        return !policy->expires;
 }
 
 /**
  * soup_hsts_policy_free:
- * @policy: a #SoupHstsPolicy
+ * @policy: (transfer full): a #SoupHSTSPolicy
  *
  * Frees @policy.
  *
- * Since: 2.54
+ * Since: 2.64
  **/
 void
-soup_hsts_policy_free (SoupHstsPolicy *policy)
+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);
+       g_slice_free (SoupHSTSPolicy, policy);
 }
diff --git a/libsoup/soup-hsts-policy.h b/libsoup/soup-hsts-policy.h
index 8492d4a9..40f38b20 100644
--- a/libsoup/soup-hsts-policy.h
+++ b/libsoup/soup-hsts-policy.h
@@ -1,6 +1,7 @@
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
 /*
- * Copyright (C) 2016 Igalia S.L.
+ * Copyright (C) 2016, 2017, 2018 Igalia S.L.
+ * Copyright (C) 2017, 2018 Metrological Group B.V.
  */
 
 #ifndef SOUP_HSTS_POLICY_H
@@ -10,10 +11,11 @@
 
 G_BEGIN_DECLS
 
-struct _SoupHstsPolicy {
+struct _SoupHSTSPolicy {
        char                 *domain;
+       unsigned long         max_age;
        SoupDate             *expires;
-       gboolean              include_sub_domains;
+       gboolean              include_subdomains;
 };
 
 SOUP_AVAILABLE_IN_2_54
@@ -23,36 +25,36 @@ GType soup_hsts_policy_get_type (void);
 #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);
+SoupHSTSPolicy *soup_hsts_policy_new           (const char   *domain,
+                                                unsigned long max_age,
+                                                gboolean      include_subdomains);
 SOUP_AVAILABLE_IN_2_54
-SoupHstsPolicy *soup_hsts_policy_new_with_max_age      (const char *domain,
-                                                        int         max_age,
-                                                        gboolean    include_sub_domains);
+SoupHSTSPolicy *soup_hsts_policy_new_full      (const char   *domain,
+                                                unsigned long max_age,
+                                                SoupDate     *expires,
+                                                gboolean      include_subdomains);
 SOUP_AVAILABLE_IN_2_54
-SoupHstsPolicy *soup_hsts_policy_new_permanent         (const char *domain,
-                                                        gboolean    include_sub_domains);
+SoupHSTSPolicy *soup_hsts_policy_new_permanent         (const char *domain,
+                                                        gboolean    include_subdomains);
 SOUP_AVAILABLE_IN_2_54
-SoupHstsPolicy *soup_hsts_policy_new_from_response     (SoupMessage *msg);
+SoupHSTSPolicy *soup_hsts_policy_new_from_response     (SoupMessage *msg);
 
 SOUP_AVAILABLE_IN_2_54
-SoupHstsPolicy *soup_hsts_policy_copy           (SoupHstsPolicy *policy);
+SoupHSTSPolicy *soup_hsts_policy_copy           (SoupHSTSPolicy *policy);
 SOUP_AVAILABLE_IN_2_54
-gboolean soup_hsts_policy_equal                 (SoupHstsPolicy *policy1,
-                                                 SoupHstsPolicy *policy2);
-
+gboolean soup_hsts_policy_equal                 (SoupHSTSPolicy *policy1,
+                                                 SoupHSTSPolicy *policy2);
 SOUP_AVAILABLE_IN_2_54
-const char *soup_hsts_policy_get_domain         (SoupHstsPolicy *policy);
+const char *soup_hsts_policy_get_domain         (SoupHSTSPolicy *policy);
 SOUP_AVAILABLE_IN_2_54
-gboolean    soup_hsts_policy_is_expired         (SoupHstsPolicy *policy);
+gboolean    soup_hsts_policy_is_expired         (SoupHSTSPolicy *policy);
 SOUP_AVAILABLE_IN_2_54
-gboolean    soup_hsts_policy_includes_sub_domains       (SoupHstsPolicy *policy);
+gboolean    soup_hsts_policy_includes_subdomains       (SoupHSTSPolicy *policy);
 SOUP_AVAILABLE_IN_2_54
-gboolean    soup_hsts_policy_is_permanent       (SoupHstsPolicy *policy);
+gboolean    soup_hsts_policy_is_permanent       (SoupHSTSPolicy *policy);
 
 SOUP_AVAILABLE_IN_2_54
-void        soup_hsts_policy_free               (SoupHstsPolicy *policy);
+void        soup_hsts_policy_free               (SoupHSTSPolicy *policy);
 
 G_END_DECLS
 
diff --git a/libsoup/soup-types.h b/libsoup/soup-types.h
index 8f5b07ab..ed593390 100644
--- a/libsoup/soup-types.h
+++ b/libsoup/soup-types.h
@@ -19,8 +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 _SoupHSTSEnforcer        SoupHSTSEnforcer;
+typedef struct _SoupHSTSPolicy          SoupHSTSPolicy;
 typedef struct _SoupMessage             SoupMessage;
 typedef struct _SoupRequest             SoupRequest;
 typedef struct _SoupRequestHTTP         SoupRequestHTTP;
diff --git a/tests/Makefile.am b/tests/Makefile.am
index c5638e11..68404eac 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -21,6 +21,8 @@ test_programs =                       \
        date-test               \
        forms-test              \
        header-parsing-test     \
+       hsts-test               \
+       hsts-db-test            \
        misc-test               \
        multipart-test          \
        no-ssl-test             \
diff --git a/tests/hsts-db-test.c b/tests/hsts-db-test.c
new file mode 100644
index 00000000..c195f112
--- /dev/null
+++ b/tests/hsts-db-test.c
@@ -0,0 +1,177 @@
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdio.h>
+#include "test-utils.h"
+
+#define DB_FILE "hsts-db.sqlite"
+
+SoupURI *http_uri;
+SoupURI *https_uri;
+
+/* This server pseudo-implements the HSTS spec in order to allow us to
+   test the Soup HSTS feature.
+ */
+static void
+server_callback  (SoupServer *server, SoupMessage *msg,
+                 const char *path, GHashTable *query,
+                 SoupClientContext *context, gpointer data)
+{
+       const char *server_protocol = data;
+
+       if (strcmp (server_protocol, "http") == 0) {
+               char *uri_string;
+               SoupURI *uri = soup_uri_new ("https://localhost";);
+               soup_uri_set_path (uri, path);
+               uri_string = soup_uri_to_string (uri, FALSE);
+               fprintf (stderr, "server is redirecting to HTTPS\n");
+               soup_message_set_redirect (msg, SOUP_STATUS_MOVED_PERMANENTLY, uri_string);
+               soup_uri_free (uri);
+               g_free (uri_string);
+       } else if (strcmp (server_protocol, "https") == 0) {
+               soup_message_set_status (msg, SOUP_STATUS_OK);
+               if (strcmp (path, "/long-lasting") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=31536000");
+               } else if (strcmp (path, "/two-seconds") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=2");
+               } else if (strcmp (path, "/delete") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=0");
+               } else if (strcmp (path, "/subdomains") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=31536000; includeSubDomains");
+               }
+       }
+}
+
+static void
+session_get_uri (SoupSession *session, const char *uri, SoupStatus expected_status)
+{
+       SoupMessage *msg;
+
+       msg = soup_message_new ("GET", uri);
+       soup_message_set_flags (msg, SOUP_MESSAGE_NO_REDIRECT);
+       soup_session_send_message (session, msg);
+       /* g_assert_cmpint (soup_uri_get_port (soup_message_get_uri (msg)), ==, soup_uri_get_port 
(https_uri)); */
+       soup_test_assert_message_status (msg, expected_status);
+       g_object_unref (msg);
+}
+
+/* The HSTS specification does not handle custom ports, so we need to
+ * rewrite the URI in the request and add the port where the server is
+ * listening before it is sent, to be able to connect to the localhost
+ * port where the server is actually running.
+ */
+static void
+rewrite_message_uri (SoupMessage *msg)
+{
+       if (soup_uri_get_scheme (soup_message_get_uri (msg)) == SOUP_URI_SCHEME_HTTP)
+               soup_uri_set_port (soup_message_get_uri (msg), soup_uri_get_port (http_uri));
+       else if (soup_uri_get_scheme (soup_message_get_uri (msg)) == SOUP_URI_SCHEME_HTTPS)
+               soup_uri_set_port (soup_message_get_uri (msg), soup_uri_get_port (https_uri));
+       else
+               g_assert_not_reached();
+}
+
+static void
+on_message_restarted (SoupMessage *msg,
+                    gpointer data)
+{
+       rewrite_message_uri (msg);
+}
+
+static void
+on_request_queued (SoupSession *session,
+                  SoupMessage *msg,
+                  gpointer data)
+{
+       g_signal_connect (msg, "restarted", G_CALLBACK (on_message_restarted), NULL);
+
+       rewrite_message_uri (msg);
+}
+
+static SoupSession *
+hsts_db_session_new (void)
+{
+       SoupHSTSEnforcer *hsts_db = soup_hsts_enforcer_db_new (DB_FILE);
+
+       SoupSession *session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC,
+                                                     SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
+                                                     SOUP_SESSION_ADD_FEATURE, hsts_db,
+                                                     NULL);
+       g_signal_connect (session, "request-queued", G_CALLBACK (on_request_queued), NULL);
+
+       return session;
+}
+
+
+static void
+do_hsts_db_persistency_test (void)
+{
+       SoupSession *session = hsts_db_session_new ();
+       session_get_uri (session, "https://localhost/long-lasting";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+       soup_test_session_abort_unref (session);
+
+       session = hsts_db_session_new ();
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+       soup_test_session_abort_unref (session);
+
+       g_remove (DB_FILE);
+}
+
+static void
+do_hsts_db_subdomains_test (void)
+{
+       SoupSession *session = hsts_db_session_new ();
+       session_get_uri (session, "https://localhost/subdomains";, SOUP_STATUS_OK);
+       soup_test_session_abort_unref (session);
+
+       session = hsts_db_session_new ();
+       session_get_uri (session, "http://subdomain.localhost";, SOUP_STATUS_SSL_FAILED);
+       soup_test_session_abort_unref (session);
+
+       g_remove (DB_FILE);
+}
+
+int
+main (int argc, char **argv)
+{
+       int ret;
+       SoupServer *server;
+       SoupServer *https_server;
+
+       test_init (argc, argv, NULL);
+
+       server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD);
+       soup_server_add_handler (server, NULL, server_callback, "http", NULL);
+       http_uri = soup_test_server_get_uri (server, "http", NULL);
+
+       if (tls_available) {
+               https_server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD);
+               soup_server_add_handler (https_server, NULL, server_callback, "https", NULL);
+               https_uri = soup_test_server_get_uri (https_server, "https", NULL);
+       }
+
+       g_test_add_func ("/hsts-db/basic", do_hsts_db_persistency_test);
+       g_test_add_func ("/hsts-db/subdomains", do_hsts_db_subdomains_test);
+
+       ret = g_test_run ();
+
+       soup_uri_free (http_uri);
+       soup_test_server_quit_unref (server);
+
+       if (tls_available) {
+               soup_uri_free (https_uri);
+               soup_test_server_quit_unref (https_server);
+       }
+
+       test_cleanup ();
+       return ret;
+}
diff --git a/tests/hsts-test.c b/tests/hsts-test.c
new file mode 100644
index 00000000..aa77d6e2
--- /dev/null
+++ b/tests/hsts-test.c
@@ -0,0 +1,408 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Igalia S.L.
+ * Copyright (C) 2018 Metrological Group B.V.
+ */
+
+#include <stdio.h>
+#include "test-utils.h"
+
+SoupURI *http_uri;
+SoupURI *https_uri;
+
+/* This server pseudo-implements the HSTS spec in order to allow us to
+   test the Soup HSTS feature.
+ */
+static void
+server_callback  (SoupServer *server, SoupMessage *msg,
+                 const char *path, GHashTable *query,
+                 SoupClientContext *context, gpointer data)
+{
+       const char *server_protocol = data;
+
+       if (strcmp (server_protocol, "http") == 0) {
+               if (strcmp (path, "/insecure") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=31536000");
+                       soup_message_set_status (msg, SOUP_STATUS_OK);
+               } else {
+                       char *uri_string;
+                       SoupURI *uri = soup_uri_new ("https://localhost";);
+                       soup_uri_set_path (uri, path);
+                       uri_string = soup_uri_to_string (uri, FALSE);
+                       soup_message_set_redirect (msg, SOUP_STATUS_MOVED_PERMANENTLY, uri_string);
+                       soup_uri_free (uri);
+                       g_free (uri_string);
+               }
+       } else if (strcmp (server_protocol, "https") == 0) {
+               soup_message_set_status (msg, SOUP_STATUS_OK);
+               if (strcmp (path, "/long-lasting") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=31536000");
+               } else if (strcmp (path, "/two-seconds") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=2");
+               } else if (strcmp (path, "/three-seconds") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=3");
+               } else if (strcmp (path, "/delete") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=0");
+               } else if (strcmp (path, "/subdomains") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=31536000; includeSubDomains");
+               } else if (strcmp (path, "/multiple-headers") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=31536000; includeSubDomains");
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=0; includeSubDomains");
+               } else if (strcmp (path, "/missing-values") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "");
+               } else if (strcmp (path, "/invalid-values") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=foo");
+               } else if (strcmp (path, "/extra-values-0") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=3600; foo");
+               } else if (strcmp (path, "/extra-values-1") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    " max-age=3600; includeDomains; foo");
+               } else if (strcmp (path, "/extra-values-2") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "max-age=3600; includeDomains; includeDomains");
+               } else if (strcmp (path, "/case-insensitive") == 0) {
+                       soup_message_headers_append (msg->response_headers,
+                                                    "Strict-Transport-Security",
+                                                    "MAX-AGE=3600; includesubdomains");
+               }
+       }
+}
+
+static void
+session_get_uri (SoupSession *session, const char *uri, SoupStatus expected_status)
+{
+       SoupMessage *msg;
+
+       msg = soup_message_new ("GET", uri);
+       soup_message_set_flags (msg, SOUP_MESSAGE_NO_REDIRECT);
+       soup_session_send_message (session, msg);
+       soup_test_assert_message_status (msg, expected_status);
+       g_object_unref (msg);
+}
+
+/* The HSTS specification does not handle custom ports, so we need to
+ * rewrite the URI in the request and add the port where the server is
+ * listening before it is sent, to be able to connect to the localhost
+ * port where the server is actually running.
+ */
+static void
+rewrite_message_uri (SoupMessage *msg)
+{
+       if (soup_uri_get_scheme (soup_message_get_uri (msg)) == SOUP_URI_SCHEME_HTTP)
+               soup_uri_set_port (soup_message_get_uri (msg), soup_uri_get_port (http_uri));
+       else if (soup_uri_get_scheme (soup_message_get_uri (msg)) == SOUP_URI_SCHEME_HTTPS)
+               soup_uri_set_port (soup_message_get_uri (msg), soup_uri_get_port (https_uri));
+       else
+               g_assert_not_reached();
+}
+
+static void
+on_message_restarted (SoupMessage *msg,
+                    gpointer data)
+{
+       rewrite_message_uri (msg);
+}
+
+static void
+on_request_queued (SoupSession *session,
+                  SoupMessage *msg,
+                  gpointer data)
+{
+       g_signal_connect (msg, "restarted", G_CALLBACK (on_message_restarted), NULL);
+
+       rewrite_message_uri (msg);
+}
+
+static SoupSession *
+hsts_session_new (SoupHSTSEnforcer *enforcer)
+{
+       SoupSession *session;
+       if (!enforcer)
+               enforcer = soup_hsts_enforcer_new ();
+
+       session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC,
+                                       SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
+                                       SOUP_SESSION_ADD_FEATURE, enforcer,
+                                       NULL);
+       g_signal_connect (session, "request-queued", G_CALLBACK (on_request_queued), NULL);
+
+       return session;
+}
+
+
+static void
+do_hsts_basic_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+       session_get_uri (session, "https://localhost/long-lasting";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+
+       /* The HSTS headers in the url above doesn't incldue
+          subdomains, so the request should ask for the unchanged
+          HTTP address below, to which the server will respond with a
+          moved permanently status. */
+       session_get_uri (session, "http://subdomain.localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_expire_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+
+       session_get_uri (session, "https://localhost/two-seconds";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+       /* Wait for the policy to expire. */
+       sleep (3);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_delete_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+       session_get_uri (session, "https://localhost/delete";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_replace_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/long-lasting";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+       session_get_uri (session, "https://localhost/two-seconds";, SOUP_STATUS_OK);
+       /* Wait for the policy to expire. */
+       sleep (3);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_update_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/three-seconds";, SOUP_STATUS_OK);
+       sleep (2);
+       session_get_uri (session, "https://localhost/three-seconds";, SOUP_STATUS_OK);
+       sleep (2);
+
+       /* At this point, 4 seconds have elapsed since setting the 3 seconds HSTS
+          rule for the first time, and it should have expired by now, but since it
+          was updated, it should still be valid. */
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_set_and_delete_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/long-lasting";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+       session_get_uri (session, "https://localhost/delete";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_persistency_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/long-lasting";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+       soup_test_session_abort_unref (session);
+
+       session = hsts_session_new (NULL);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_subdomains_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/subdomains";, SOUP_STATUS_OK);
+       /* The enforcer should cause the request to ask for an HTTPS
+          uri, which will fail with an SSL error as there's no server
+          in subdomain.localhost. */
+       session_get_uri (session, "http://subdomain.localhost";, SOUP_STATUS_SSL_FAILED);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_multiple_headers_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/multiple-headers";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost/multiple-headers";, SOUP_STATUS_OK);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_insecure_sts_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "http://localhost/insecure";, SOUP_STATUS_OK);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_missing_values_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/missing-values";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_invalid_values_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/invalid-values";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_extra_values_test (void)
+{
+       for (int i = 0; i < 3; i++) {
+               SoupSession *session = hsts_session_new (NULL);
+               char *uri = g_strdup_printf ("https://localhost/extra-values-%i";, i);
+               session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+               soup_test_session_abort_unref (session);
+               g_free (uri);
+       }
+}
+
+static void
+do_hsts_case_insensitive_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/case-insensitive";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_ip_address_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://127.0.0.1/basic";, SOUP_STATUS_OK);
+       session_get_uri (session, "http://127.0.0.1/";, SOUP_STATUS_MOVED_PERMANENTLY);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_utf8_address_test (void)
+{
+       SoupSession *session = hsts_session_new (NULL);
+       session_get_uri (session, "https://localhost/subdomains";, SOUP_STATUS_OK);
+       /* The enforcer should cause the request to ask for an HTTPS
+          uri, which will fail with an SSL error as there's no server
+          in subdomain.localhost. */
+       session_get_uri (session, "http://食狮.中国.localhost";, SOUP_STATUS_SSL_FAILED);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_hsts_session_policy_test (void)
+{
+       SoupHSTSEnforcer *enforcer = soup_hsts_enforcer_new ();
+       SoupSession *session = hsts_session_new (enforcer);
+
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_MOVED_PERMANENTLY);
+       soup_hsts_enforcer_set_session_policy (enforcer, "localhost", FALSE);
+       session_get_uri (session, "http://localhost";, SOUP_STATUS_OK);
+
+       soup_test_session_abort_unref (session);
+       g_object_unref (enforcer);
+}
+
+int
+main (int argc, char **argv)
+{
+       int ret;
+       SoupServer *server;
+       SoupServer *https_server;
+
+       test_init (argc, argv, NULL);
+
+       server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD);
+       soup_server_add_handler (server, NULL, server_callback, "http", NULL);
+       http_uri = soup_test_server_get_uri (server, "http", NULL);
+
+       if (tls_available) {
+               https_server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD);
+               soup_server_add_handler (https_server, NULL, server_callback, "https", NULL);
+               https_uri = soup_test_server_get_uri (https_server, "https", NULL);
+       }
+
+       g_test_add_func ("/hsts/basic", do_hsts_basic_test);
+       g_test_add_func ("/hsts/expire", do_hsts_expire_test);
+       g_test_add_func ("/hsts/delete", do_hsts_delete_test);
+       g_test_add_func ("/hsts/replace", do_hsts_replace_test);
+       g_test_add_func ("/hsts/update", do_hsts_update_test);
+       g_test_add_func ("/hsts/set_and_delete", do_hsts_set_and_delete_test);
+       g_test_add_func ("/hsts/persistency", do_hsts_persistency_test);
+       g_test_add_func ("/hsts/subdomains", do_hsts_subdomains_test);
+       g_test_add_func ("/hsts/multiple-headers", do_hsts_multiple_headers_test);
+       g_test_add_func ("/hsts/insecure-sts", do_hsts_insecure_sts_test);
+       g_test_add_func ("/hsts/missing-values", do_hsts_missing_values_test);
+       g_test_add_func ("/hsts/invalid-values", do_hsts_invalid_values_test);
+       g_test_add_func ("/hsts/extra-values", do_hsts_extra_values_test);
+       g_test_add_func ("/hsts/case-insensitive", do_hsts_case_insensitive_test);
+       g_test_add_func ("/hsts/ip-address", do_hsts_ip_address_test);
+       g_test_add_func ("/hsts/utf8-address", do_hsts_utf8_address_test);
+       g_test_add_func ("/hsts/session-policy", do_hsts_session_policy_test);
+
+       ret = g_test_run ();
+
+       soup_uri_free (http_uri);
+       soup_test_server_quit_unref (server);
+
+       if (tls_available) {
+               soup_uri_free (https_uri);
+               soup_test_server_quit_unref (https_server);
+       }
+
+       test_cleanup ();
+       return ret;
+}



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