[libsoup/hsts: 26/38] HSTS: Rewrite the HSTS feature and add tests
- From: Claudio Saavedra <csaavedra src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libsoup/hsts: 26/38] HSTS: Rewrite the HSTS feature and add tests
- Date: Wed, 13 Feb 2019 10:08:09 +0000 (UTC)
commit e87e36195197b5bb0b5fec62b0e2a46aecc5f947
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 it 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/libsoup-2.4-docs.sgml | 2 +
docs/reference/libsoup-2.4-sections.txt | 52 +++
docs/reference/meson.build | 1 +
libsoup/meson.build | 6 +
libsoup/soup-hsts-enforcer-db.c | 146 +++++----
libsoup/soup-hsts-enforcer-db.h | 33 +-
libsoup/soup-hsts-enforcer-private.h | 7 +-
libsoup/soup-hsts-enforcer.c | 559 +++++++++++++++-----------------
libsoup/soup-hsts-enforcer.h | 75 +++--
libsoup/soup-hsts-policy.c | 456 +++++++++++---------------
libsoup/soup-hsts-policy.h | 80 ++---
libsoup/soup-types.h | 4 +-
tests/hsts-db-test.c | 176 ++++++++++
tests/hsts-test.c | 410 +++++++++++++++++++++++
tests/meson.build | 2 +
15 files changed, 1300 insertions(+), 709 deletions(-)
---
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..6b99afa1 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_session_policy
+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/docs/reference/meson.build b/docs/reference/meson.build
index 12105016..a652c422 100644
--- a/docs/reference/meson.build
+++ b/docs/reference/meson.build
@@ -40,6 +40,7 @@ ignore_headers = [
'soup-socket-private.h',
'soup-value-utils.h',
'soup-xmlrpc-old.h'
+ 'soup-hsts-enforcer-private.h'
]
mkdb_args = [
diff --git a/libsoup/meson.build b/libsoup/meson.build
index 5f2a2156..932f667b 100644
--- a/libsoup/meson.build
+++ b/libsoup/meson.build
@@ -32,6 +32,9 @@ soup_sources = [
'soup-directory-input-stream.c',
'soup-filter-input-stream.c',
'soup-form.c',
+ 'soup-hsts-enforcer.c',
+ 'soup-hsts-enforcer-db.c',
+ 'soup-hsts-policy.c',
'soup-headers.c',
'soup-init.c',
'soup-io-stream.c',
@@ -123,6 +126,9 @@ soup_introspection_headers = [
'soup-date.h',
'soup-form.h',
'soup-headers.h',
+ 'soup-hsts-enforcer.h',
+ 'soup-hsts-enforcer-db.h',
+ 'soup-hsts-policy.h',
'soup-logger.h',
'soup-message.h',
'soup-message-body.h',
diff --git a/libsoup/soup-hsts-enforcer-db.c b/libsoup/soup-hsts-enforcer-db.c
index 319f118d..3e5add95 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,30 +35,29 @@ 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))
+};
-G_DEFINE_TYPE (SoupHstsEnforcerDB, soup_hsts_enforcer_db, SOUP_TYPE_HSTS_ENFORCER)
+G_DEFINE_TYPE_WITH_CODE (SoupHSTSEnforcerDB, soup_hsts_enforcer_db, SOUP_TYPE_HSTS_ENFORCER,
+ G_ADD_PRIVATE(SoupHSTSEnforcerDB))
-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_instance_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);
+ sqlite3_close (priv->db);
G_OBJECT_CLASS (soup_hsts_enforcer_db_parent_class)->finalize (object);
}
@@ -66,8 +66,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 +83,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 +97,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.66
**/
-SoupHstsEnforcer *
+SoupHSTSEnforcer *
soup_hsts_enforcer_db_new (const char *filename)
{
g_return_val_if_fail (filename != NULL, NULL);
@@ -121,30 +122,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 +158,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;
@@ -178,10 +183,12 @@ try_create_table (sqlite3 *db)
}
}
+typedef int (*ExecQueryCallback) (void *, int, char**, char**);
+
static void
exec_query_with_try_create_table (sqlite3 *db,
const char *sql,
- int (*callback)(void*,int,char**,char**),
+ ExecQueryCallback callback,
void *argument)
{
char *error = NULL;
@@ -204,10 +211,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,28 +233,31 @@ 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;
+ /* Session policies do not need to be stored in the database. */
+ if ((old_policy && soup_hsts_policy_is_session_policy (old_policy)) ||
+ (new_policy && soup_hsts_policy_is_session_policy (new_policy)))
+ return;
+
if (priv->db == NULL) {
if (open_db (hsts_enforcer))
return;
@@ -257,6 +266,7 @@ soup_hsts_enforcer_db_changed (SoupHstsEnforcer *hsts_enforcer,
if (old_policy && !new_policy) {
query = sqlite3_mprintf (QUERY_DELETE,
old_policy->domain);
+ g_assert (query);
exec_query_with_try_create_table (priv->db, query, NULL, NULL);
sqlite3_free (query);
}
@@ -269,29 +279,44 @@ 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);
+ g_assert (query);
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));
-
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 +324,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..029a800c 100644
--- a/libsoup/soup-hsts-enforcer-db.h
+++ b/libsoup/soup-hsts-enforcer-db.h
@@ -1,45 +1,50 @@
/* -*- 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
-#define SOUP_HSTS_ENFORCER_DB_H 1
+#ifndef __SOUP_HSTS_ENFORCER_DB_H__
+#define __SOUP_HSTS_ENFORCER_DB_H__ 1
#include <libsoup/soup-hsts-enforcer.h>
G_BEGIN_DECLS
#define SOUP_TYPE_HSTS_ENFORCER_DB (soup_hsts_enforcer_db_get_type ())
-#define SOUP_HSTS_ENFORCER_DB(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj),
SOUP_TYPE_HSTS_ENFORCER_DB, SoupHstsEnforcerDB))
-#define SOUP_HSTS_ENFORCER_DB_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_HSTS_ENFORCER_DB,
SoupHstsEnforcerDBClass))
+#define SOUP_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_66
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_66
+SoupHSTSEnforcer *soup_hsts_enforcer_db_new (const char *filename);
G_END_DECLS
-#endif /* SOUP_HSTS_ENFORCER_DB_H */
+#endif /* __SOUP_HSTS_ENFORCER_DB_H__ */
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..37060cb8 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 */
@@ -11,39 +12,40 @@
#include <config.h>
#endif
-#include <errno.h>
-#include <stdio.h>
-#include <string.h>
-
#include "soup-hsts-enforcer.h"
#include "soup-hsts-enforcer-private.h"
#include "soup.h"
/**
* SECTION:soup-hsts-enforcer
- * @short_description: Automatic HSTS enforcing for SoupSession
+ * @short_description: Automatic HTTP Strict Transport Security enforcing
+ * for #SoupSession
+ *
+ * A #SoupHSTSEnforcer stores HSTS policies and enforces them when
+ * required. #SoupHSTSEnforcer implements #SoupSessionFeature, so you
+ * can add an HSTS enforcer to a session with
+ * soup_session_add_feature() or soup_session_add_feature_by_type().
+ *
+ * #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.
*
- * A #SoupHstsEnforcer stores HSTS policies and enforce them when
- * required.
- * #SoupHstsEnforcer implements #SoupSessionFeature, so you can add a
- * HSTS enforcer to a session with soup_session_add_feature() or
- * soup_session_add_feature_by_type().
+ * When the #SoupSession the #SoupHSTSEnforcer is attached to 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.
*
- * When the #SoupSession the #SoupHstsEnforcer is attached to sends a
- * message, the #SoupHstsEnforcer will ask for a redirection to HTTPS if
- * the destination is a known HSTS host and is contacted over an insecure
- * transport protocol (HTTP).
+ * Note that #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_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
- soup_hsts_enforcer_session_feature_init))
-
enum {
CHANGED,
LAST_SIGNAL
@@ -51,30 +53,34 @@ 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))
+};
+
+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)
+ G_ADD_PRIVATE(SoupHSTSEnforcer))
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_instance_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 +98,56 @@ 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));
-
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
@@ -121,12 +156,15 @@ soup_hsts_enforcer_class_init (SoupHstsEnforcerClass *hsts_enforcer_class)
* @new_policy will be %NULL. If a policy has been changed,
* @old_policy will contain its old value, and @new_policy its
* new value.
+ *
+ * Note that you shouldn't modify the policies from a callback to
+ * this signal.
**/
signals[CHANGED] =
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,295 +175,230 @@ soup_hsts_enforcer_class_init (SoupHstsEnforcerClass *hsts_enforcer_class)
/**
* soup_hsts_enforcer_new:
*
- * Creates a new #SoupHstsEnforcer. The base #SoupHstsEnforcer class does
- * not support persistent storage of HSTS policies; use a subclass for
- * that.
+ * Creates a new #SoupHSTSEnforcer. The base #SoupHSTSEnforcer class
+ * does not support persistent storage of HSTS policies, see
+ * #SoupHSTSEnforcerDB for that.
*
- * Returns: a new #SoupHstsEnforcer
+ * Returns: a new #SoupHSTSEnforcer
*
- * Since: 2.54
+ * Since: 2.66
**/
-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_assert (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.
+ */
+ 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;
- gboolean is_permanent;
-
- g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
- g_return_if_fail (new_policy != NULL);
+ SoupHSTSPolicy *old_policy;
+ const char *domain;
+ gboolean is_session_policy;
- g_assert_false (soup_hsts_policy_is_expired (new_policy));
+ g_assert (!soup_hsts_policy_is_expired (new_policy));
domain = soup_hsts_policy_get_domain (new_policy);
- is_permanent = soup_hsts_policy_is_permanent (new_policy);
+ is_session_policy = soup_hsts_policy_is_session_policy (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_session_policy ? hsts_enforcer->priv->session_policies :
+ hsts_enforcer->priv->host_policies;
old_policy = g_hash_table_lookup (policies, domain);
+ g_assert (old_policy);
- g_assert_nonnull (old_policy);
-
- g_hash_table_remove (policies, domain);
- g_hash_table_insert (policies, g_strdup (domain), new_policy);
- if (!is_permanent && !soup_hsts_policy_equal (old_policy, new_policy))
+ g_hash_table_replace (policies, g_strdup (domain), soup_hsts_policy_copy (new_policy));
+ if (!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;
- gboolean is_permanent;
+ const char *domain;
+ gboolean is_session_policy;
g_return_if_fail (SOUP_IS_HSTS_ENFORCER (hsts_enforcer));
g_return_if_fail (policy != NULL);
- g_assert_false (soup_hsts_policy_is_expired (policy));
+ g_assert (!soup_hsts_policy_is_expired (policy));
domain = soup_hsts_policy_get_domain (policy);
- is_permanent = soup_hsts_policy_is_permanent (policy);
+ is_session_policy = soup_hsts_policy_is_session_policy (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_session_policy ? hsts_enforcer->priv->session_policies :
+ hsts_enforcer->priv->host_policies;
- g_assert_false (g_hash_table_contains (policies, domain));
+ g_assert (!g_hash_table_contains (policies, domain));
- g_hash_table_insert (policies, g_strdup (domain), policy);
- if (!is_permanent)
- soup_hsts_enforcer_changed (hsts_enforcer, NULL, policy);
+ g_hash_table_insert (policies, g_strdup (domain), soup_hsts_policy_copy (policy));
+ soup_hsts_enforcer_changed (hsts_enforcer, NULL, policy);
}
/**
* soup_hsts_enforcer_set_policy:
- * @hsts_enforcer: a #SoupHstsEnforcer
- * @policy: (transfer full): the policy of the HSTS host
- *
- * Sets @domain's HSTS policy to @policy. If @policy is expired, any
- * existing HSTS policy for this host will be removed instead. If a policy
- * exited for this host, it will be replaced. Otherwise, the new policy
- * will be inserted.
+ * @hsts_enforcer: a #SoupHSTSEnforcer
+ * @policy: (transfer none): the policy of the HSTS host
*
- * This steals @policy.
+ * Sets @policy to @hsts_enforcer. If @policy is expired, any
+ * existing HSTS policy for its 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 session policy, that
+ * is, one created with soup_hsts_policy_new_session_policy(), the policy
+ * will not expire and will be enforced during the lifetime of
+ * @hsts_enforcer's #SoupSession.
*
- * Since: 2.54
+ * Since: 2.66
**/
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;
- gboolean is_permanent;
+ const char *domain;
+ gboolean is_session_policy;
+ 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_session_policy = soup_hsts_policy_is_session_policy (policy);
+ policies = is_session_policy ? hsts_enforcer->priv->session_policies :
+ hsts_enforcer->priv->host_policies;
- if (!is_permanent && soup_hsts_policy_is_expired (policy)) {
+ if (!is_session_policy && 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.66
**/
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_session_policy (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 (domain);
for (; *iter != '\0' && *iter != '.' ; iter++);
for (; *iter == '.' ; iter++);
@@ -437,39 +410,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,64 +444,70 @@ 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_header_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);
-
- uri = soup_message_get_uri (msg);
-
- g_return_val_if_fail (uri != NULL, FALSE);
+ guint original_port;
- // 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);
+ 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);
- g_return_val_if_fail (domain != NULL, FALSE);
-
- 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, host))
+ 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_header_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
@@ -542,29 +515,8 @@ 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 +524,8 @@ 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, message_restarted_cb, feature);
+ g_signal_handlers_disconnect_by_func (msg, got_sts_header_cb, feature);
}
static void
@@ -585,18 +538,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.66
**/
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.66
+ **/
+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..0eecb733 100644
--- a/libsoup/soup-hsts-enforcer.h
+++ b/libsoup/soup-hsts-enforcer.h
@@ -1,53 +1,74 @@
/* -*- 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
-#define SOUP_HSTS_ENFORCER_H 1
+#ifndef __SOUP_HSTS_ENFORCER_H__
+#define __SOUP_HSTS_ENFORCER_H__ 1
#include <libsoup/soup-types.h>
G_BEGIN_DECLS
-#define SOUP_TYPE_HSTS_ENFORCER (soup_hsts_enforcer_get_type ())
-#define SOUP_HSTS_ENFORCER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_HSTS_ENFORCER,
SoupHstsEnforcer))
-#define SOUP_HSTS_ENFORCER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_HSTS_ENFORCER,
SoupHstsEnforcerClass))
-#define SOUP_IS_HSTS_ENFORCER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_HSTS_ENFORCER))
+#define SOUP_TYPE_HSTS_ENFORCER (soup_hsts_enforcer_get_type ())
+#define SOUP_HSTS_ENFORCER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj),
SOUP_TYPE_HSTS_ENFORCER, SoupHSTSEnforcer))
+#define SOUP_HSTS_ENFORCER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass),
SOUP_TYPE_HSTS_ENFORCER, SoupHSTSEnforcerClass))
+#define SOUP_IS_HSTS_ENFORCER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_HSTS_ENFORCER))
#define SOUP_IS_HSTS_ENFORCER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_HSTS_ENFORCER))
-#define SOUP_HSTS_ENFORCER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_HSTS_ENFORCER,
SoupHstsEnforcerClass))
+#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 *enforcer,
+ SoupHSTSPolicy *old_policy,
+ SoupHSTSPolicy *new_policy);
/* Padding for future expansion */
void (*_libsoup_reserved1) (void);
void (*_libsoup_reserved2) (void);
-} SoupHstsEnforcerClass;
-
-SOUP_AVAILABLE_IN_2_54
-GType soup_hsts_enforcer_get_type (void);
-SOUP_AVAILABLE_IN_2_54
-SoupHstsEnforcer *soup_hsts_enforcer_new (void);
-SOUP_AVAILABLE_IN_2_54
-gboolean soup_hsts_enforcer_is_persistent (SoupHstsEnforcer *hsts_enforcer);
-
-SOUP_AVAILABLE_IN_2_54
-void soup_hsts_enforcer_set_session_policy (SoupHstsEnforcer *hsts_enforcer,
- const char *domain,
- gboolean include_sub_domains);
+ void (*_libsoup_reserved3) (void);
+ void (*_libsoup_reserved4) (void);
+} SoupHSTSEnforcerClass;
+
+SOUP_AVAILABLE_IN_2_66
+GType soup_hsts_enforcer_get_type (void);
+SOUP_AVAILABLE_IN_2_66
+SoupHSTSEnforcer *soup_hsts_enforcer_new (void);
+SOUP_AVAILABLE_IN_2_66
+gboolean soup_hsts_enforcer_is_persistent (SoupHSTSEnforcer *hsts_enforcer);
+SOUP_AVAILABLE_IN_2_66
+gboolean soup_hsts_enforcer_has_valid_policy (SoupHSTSEnforcer *hsts_enforcer,
+ const char *domain);
+SOUP_AVAILABLE_IN_2_66
+void soup_hsts_enforcer_set_session_policy (SoupHSTSEnforcer *hsts_enforcer,
+ const char *domain,
+ gboolean include_subdomains);
G_END_DECLS
-#endif /* SOUP_HSTS_ENFORCER_H */
+#endif /* __SOUP_HSTS_ENFORCER_H__ */
diff --git a/libsoup/soup-hsts-policy.c b/libsoup/soup-hsts-policy.c
index e2989dbb..ce98cbb3 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.66
**/
-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.66
**/
-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.
+ * Returns: whether the policies are equal.
*
- * Return value: whether the cookies are equal.
- *
- * Since: 2.24
+ * Since: 2.66
*/
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,19 @@ 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.66
**/
-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,66 +167,134 @@ 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.66
+ **/
+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;
}
/**
- * soup_hsts_policy_new_permanent:
+ * soup_hsts_policy_new_session_policy:
* @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 session #SoupHSTSPolicy with the given attributes.
+ * A session policy is a policy that is valid during the lifetime of
+ * the #SoupHSTSEnforcer it is added to. Contrary to regular policies,
+ * it has no expiration date and is not stored in persistent
+ * enforcers. These policies are useful for user-agent to load their
+ * own or user-defined rules.
*
* @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.66
**/
-SoupHstsPolicy *
-soup_hsts_policy_new_permanent (const char *domain,
- gboolean include_sub_domains)
+SoupHSTSPolicy *
+soup_hsts_policy_new_session_policy (const char *domain,
+ gboolean include_subdomains)
{
- return soup_hsts_policy_new (domain, NULL, include_sub_domains);
+ SoupHSTSPolicy *policy;
+
+ policy = soup_hsts_policy_new_full (domain, 0, NULL, include_subdomains);
+
+ 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.66
**/
-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 *uri;
+ 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);
+ uri = soup_message_get_uri (msg);
+
+ 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 (uri->host, max_age, include_subdomains);
+ out:
+ soup_header_free_param_list (params);
+ return policy;
}
return NULL;
@@ -392,88 +302,92 @@ 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.66
**/
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
- *
- * Gets whether @policy is expired.
+ * @policy: a #SoupHSTSPolicy
*
- * Permanent policies never expire.
+ * Gets whether @policy is expired. Permanent policies never
+ * expire.
*
- * Return value: whether @policy is expired.
+ * Returns: %TRUE if @policy is expired, %FALSE otherwise.
*
- * Since: 2.54
+ * Since: 2.66
**/
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.66
**/
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
- *
- * Gets whether @policy is permanent (not expirable).
+ * soup_hsts_policy_is_session_policy:
+ * @policy: a #SoupHSTSPolicy
*
- * A permanent policy never expires and should not be saved by a persistent
- * #SoupHstsEnforcer so the user agent can control them.
+ * Gets whether @policy is a non-permanent, non-expirable session policy.
+ * see soup_hsts_policy_new_session_policy() for details.
*
- * Return value: whether @policy is permanent.
+ * Returns: %TRUE if @policy is permanent, %FALSE otherwise
*
- * Since: 2.54
+ * Since: 2.66
**/
gboolean
-soup_hsts_policy_is_permanent (SoupHstsPolicy *policy)
+soup_hsts_policy_is_session_policy (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.66
**/
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..01becb4c 100644
--- a/libsoup/soup-hsts-policy.h
+++ b/libsoup/soup-hsts-policy.h
@@ -1,59 +1,61 @@
/* -*- 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
-#define SOUP_HSTS_POLICY_H 1
+#ifndef __SOUP_HSTS_POLICY_H__
+#define __SOUP_HSTS_POLICY_H__ 1
#include <libsoup/soup-types.h>
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
-GType soup_hsts_policy_get_type (void);
+SOUP_AVAILABLE_IN_2_66
+GType soup_hsts_policy_get_type (void);
#define SOUP_TYPE_HSTS_POLICY (soup_hsts_policy_get_type())
#define SOUP_HSTS_POLICY_MAX_AGE_PAST (0)
-SOUP_AVAILABLE_IN_2_54
-SoupHstsPolicy *soup_hsts_policy_new (const char *domain,
- SoupDate *expiry_date,
- gboolean include_sub_domains);
-SOUP_AVAILABLE_IN_2_54
-SoupHstsPolicy *soup_hsts_policy_new_with_max_age (const char *domain,
- int max_age,
- gboolean include_sub_domains);
-SOUP_AVAILABLE_IN_2_54
-SoupHstsPolicy *soup_hsts_policy_new_permanent (const char *domain,
- gboolean include_sub_domains);
-SOUP_AVAILABLE_IN_2_54
-SoupHstsPolicy *soup_hsts_policy_new_from_response (SoupMessage *msg);
-
-SOUP_AVAILABLE_IN_2_54
-SoupHstsPolicy *soup_hsts_policy_copy (SoupHstsPolicy *policy);
-SOUP_AVAILABLE_IN_2_54
-gboolean soup_hsts_policy_equal (SoupHstsPolicy *policy1,
- SoupHstsPolicy *policy2);
-
-SOUP_AVAILABLE_IN_2_54
-const char *soup_hsts_policy_get_domain (SoupHstsPolicy *policy);
-SOUP_AVAILABLE_IN_2_54
-gboolean soup_hsts_policy_is_expired (SoupHstsPolicy *policy);
-SOUP_AVAILABLE_IN_2_54
-gboolean soup_hsts_policy_includes_sub_domains (SoupHstsPolicy *policy);
-SOUP_AVAILABLE_IN_2_54
-gboolean soup_hsts_policy_is_permanent (SoupHstsPolicy *policy);
-
-SOUP_AVAILABLE_IN_2_54
-void soup_hsts_policy_free (SoupHstsPolicy *policy);
+SOUP_AVAILABLE_IN_2_66
+SoupHSTSPolicy *soup_hsts_policy_new (const char *domain,
+ unsigned long max_age,
+ gboolean include_subdomains);
+SOUP_AVAILABLE_IN_2_66
+SoupHSTSPolicy *soup_hsts_policy_new_full (const char *domain,
+ unsigned long max_age,
+ SoupDate *expires,
+ gboolean include_subdomains);
+SOUP_AVAILABLE_IN_2_66
+SoupHSTSPolicy *soup_hsts_policy_new_session_policy (const char *domain,
+ gboolean include_subdomains);
+SOUP_AVAILABLE_IN_2_66
+SoupHSTSPolicy *soup_hsts_policy_new_from_response (SoupMessage *msg);
+
+SOUP_AVAILABLE_IN_2_66
+SoupHSTSPolicy *soup_hsts_policy_copy (SoupHSTSPolicy *policy);
+SOUP_AVAILABLE_IN_2_66
+gboolean soup_hsts_policy_equal (SoupHSTSPolicy *policy1,
+ SoupHSTSPolicy *policy2);
+SOUP_AVAILABLE_IN_2_66
+const char *soup_hsts_policy_get_domain (SoupHSTSPolicy *policy);
+SOUP_AVAILABLE_IN_2_66
+gboolean soup_hsts_policy_is_expired (SoupHSTSPolicy *policy);
+SOUP_AVAILABLE_IN_2_66
+gboolean soup_hsts_policy_includes_subdomains (SoupHSTSPolicy *policy);
+SOUP_AVAILABLE_IN_2_66
+gboolean soup_hsts_policy_is_session_policy (SoupHSTSPolicy *policy);
+
+SOUP_AVAILABLE_IN_2_66
+void soup_hsts_policy_free (SoupHSTSPolicy *policy);
G_END_DECLS
-#endif /* SOUP_HSTS_POLICY_H */
+#endif /* __SOUP_HSTS_POLICY_H__ */
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/hsts-db-test.c b/tests/hsts-db-test.c
new file mode 100644
index 00000000..9ae37e65
--- /dev/null
+++ b/tests/hsts-db-test.c
@@ -0,0 +1,176 @@
+#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);
+ 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 = NULL;
+
+ 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..bc5a66bf
--- /dev/null
+++ b/tests/hsts-test.c
@@ -0,0 +1,410 @@
+/* -*- 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 "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-directives") == 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 include
+ 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);
+ session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY);
+ 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)
+{
+ int i;
+ for (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);
+ session_get_uri (session, uri, SOUP_STATUS_OK);
+ soup_test_session_abort_unref (session);
+ g_free (uri);
+ }
+}
+
+static void
+do_hsts_case_insensitive_directives_test (void)
+{
+ SoupSession *session = hsts_session_new (NULL);
+ session_get_uri (session, "https://localhost/case-insensitive-directives", 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 食狮.中国.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 = NULL;
+
+ 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-directives", do_hsts_case_insensitive_directives_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;
+}
diff --git a/tests/meson.build b/tests/meson.build
index 8176f29b..58e8fe29 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -24,6 +24,8 @@ tests = [
['date', true],
['forms', true],
['header-parsing', true],
+ ['hsts', true],
+ ['hsts-db', true],
['misc', true],
['multipart', true],
['no-ssl', true],
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]