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