summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/reference/Makefile.am3
-rw-r--r--docs/reference/libsoup-2.4-docs.sgml2
-rw-r--r--docs/reference/libsoup-2.4-sections.txt52
-rw-r--r--libsoup/soup-hsts-enforcer-db.c132
-rw-r--r--libsoup/soup-hsts-enforcer-db.h27
-rw-r--r--libsoup/soup-hsts-enforcer-private.h7
-rw-r--r--libsoup/soup-hsts-enforcer.c516
-rw-r--r--libsoup/soup-hsts-enforcer.h59
-rw-r--r--libsoup/soup-hsts-policy.c438
-rw-r--r--libsoup/soup-hsts-policy.h44
-rw-r--r--libsoup/soup-types.h4
-rw-r--r--libsoup/soup-version.h.in15
-rw-r--r--tests/Makefile.am2
-rw-r--r--tests/hsts-db-test.c177
-rw-r--r--tests/hsts-test.c408
15 files changed, 1243 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 a9b21384..4e9cc455 100644
--- a/docs/reference/libsoup-2.4-sections.txt
+++ b/docs/reference/libsoup-2.4-sections.txt
@@ -1343,3 +1343,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 5bd5b285..9e4c8399 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/libsoup/soup-version.h.in b/libsoup/soup-version.h.in
index 0af245be..7334dd54 100644
--- a/libsoup/soup-version.h.in
+++ b/libsoup/soup-version.h.in
@@ -66,6 +66,7 @@ G_BEGIN_DECLS
#define SOUP_VERSION_2_56 (G_ENCODE_VERSION (2, 56))
#define SOUP_VERSION_2_58 (G_ENCODE_VERSION (2, 58))
#define SOUP_VERSION_2_62 (G_ENCODE_VERSION (2, 62))
+#define SOUP_VERSION_2_64 (G_ENCODE_VERSION (2, 64))
/* evaluates to the current stable version; for development cycles,
* this means the next stable target
@@ -374,6 +375,20 @@ G_BEGIN_DECLS
# define SOUP_AVAILABLE_IN_2_62 _SOUP_EXTERN
#endif
+#if SOUP_VERSION_MIN_REQUIRED >= SOUP_VERSION_2_64
+# define SOUP_DEPRECATED_IN_2_64 G_DEPRECATED
+# define SOUP_DEPRECATED_IN_2_64_FOR(f) G_DEPRECATED_FOR(f)
+#else
+# define SOUP_DEPRECATED_IN_2_64
+# define SOUP_DEPRECATED_IN_2_64_FOR(f)
+#endif
+
+#if SOUP_VERSION_MAX_ALLOWED < SOUP_VERSION_2_64
+# define SOUP_AVAILABLE_IN_2_64 G_UNAVAILABLE(2, 64) _SOUP_EXTERN
+#else
+# define SOUP_AVAILABLE_IN_2_64 _SOUP_EXTERN
+#endif
+
SOUP_AVAILABLE_IN_2_42
guint soup_get_major_version (void);
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;
+}