[libsoup/wip/tingping/same-site: 4/4] Expose support for same-site cookies



commit 631f04bcbda7dd43d3d368c1965b0484089e1ca5
Author: Patrick Griffis <pgriffis igalia com>
Date:   Tue Nov 12 16:52:57 2019 -0800

    Expose support for same-site cookies
    
    This adds API for web browsers to set extra information to support
    same-site cookies.
    
    Note that usage of SoupSession alone does not provide enough
    information to reasonably use these at the moment and require
    manually setting the information with the extra context a browser
    may have.

 docs/reference/libsoup-2.4-sections.txt |  10 +++
 libsoup/soup-cookie-jar-db.c            |  18 +++-
 libsoup/soup-cookie-jar-text.c          |  45 +++++++++-
 libsoup/soup-cookie-jar.c               | 103 +++++++++++++++++++---
 libsoup/soup-cookie-jar.h               |   9 ++
 libsoup/soup-cookie.c                   |  67 +++++++++++++++
 libsoup/soup-cookie.h                   |  20 +++++
 libsoup/soup-message-private.h          |   3 +
 libsoup/soup-message.c                  | 148 ++++++++++++++++++++++++++++++++
 libsoup/soup-message.h                  |  12 +++
 tests/cookies-test.c                    |   6 +-
 tests/meson.build                       |   1 +
 tests/samesite-test.c                   | 132 ++++++++++++++++++++++++++++
 13 files changed, 554 insertions(+), 20 deletions(-)
---
diff --git a/docs/reference/libsoup-2.4-sections.txt b/docs/reference/libsoup-2.4-sections.txt
index ea20cfb2..dac47136 100644
--- a/docs/reference/libsoup-2.4-sections.txt
+++ b/docs/reference/libsoup-2.4-sections.txt
@@ -41,6 +41,11 @@ SoupMessagePriority
 soup_message_get_priority
 soup_message_set_priority
 <SUBSECTION>
+soup_message_get_site_for_cookies
+soup_message_set_site_for_cookies
+soup_message_get_is_top_level_navigation
+soup_message_set_is_top_level_navigation
+<SUBSECTION>
 SOUP_MESSAGE_METHOD
 SOUP_MESSAGE_URI
 SOUP_MESSAGE_HTTP_VERSION
@@ -924,6 +929,10 @@ soup_cookie_get_secure
 soup_cookie_set_http_only
 soup_cookie_get_http_only
 <SUBSECTION>
+SoupSameSitePolicy
+soup_cookie_set_same_site_policy
+soup_cookie_get_same_site_policy
+<SUBSECTION>
 soup_cookie_applies_to_uri
 soup_cookie_domain_matches
 <SUBSECTION>
@@ -950,6 +959,7 @@ SoupCookieJar
 soup_cookie_jar_new
 soup_cookie_jar_get_cookies
 soup_cookie_jar_get_cookie_list
+soup_cookie_jar_get_cookie_list_with_same_site_info
 soup_cookie_jar_set_cookie
 soup_cookie_jar_set_cookie_with_first_party
 <SUBSECTION>
diff --git a/libsoup/soup-cookie-jar-db.c b/libsoup/soup-cookie-jar-db.c
index 0274038d..a1aa8557 100644
--- a/libsoup/soup-cookie-jar-db.c
+++ b/libsoup/soup-cookie-jar-db.c
@@ -128,9 +128,9 @@ soup_cookie_jar_db_new (const char *filename, gboolean read_only)
                             NULL);
 }
 
-#define QUERY_ALL "SELECT id, name, value, host, path, expiry, lastAccessed, isSecure, isHttpOnly FROM 
moz_cookies;"
-#define CREATE_TABLE "CREATE TABLE moz_cookies (id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT, 
path TEXT,expiry INTEGER, lastAccessed INTEGER, isSecure INTEGER, isHttpOnly INTEGER)"
-#define QUERY_INSERT "INSERT INTO moz_cookies VALUES(NULL, %Q, %Q, %Q, %Q, %d, NULL, %d, %d);"
+#define QUERY_ALL "SELECT id, name, value, host, path, expiry, lastAccessed, isSecure, isHttpOnly, sameSite 
FROM moz_cookies;"
+#define CREATE_TABLE "CREATE TABLE moz_cookies (id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT, 
path TEXT, expiry INTEGER, lastAccessed INTEGER, isSecure INTEGER, isHttpOnly INTEGER, sameSite INTEGER)"
+#define QUERY_INSERT "INSERT INTO moz_cookies VALUES(NULL, %Q, %Q, %Q, %Q, %d, NULL, %d, %d, %d);"
 #define QUERY_DELETE "DELETE FROM moz_cookies WHERE name=%Q AND host=%Q;"
 
 enum {
@@ -143,6 +143,7 @@ enum {
        COL_LAST_ACCESS,
        COL_SECURE,
        COL_HTTP_ONLY,
+       COL_SAME_SITE_POLICY,
        N_COL,
 };
 
@@ -157,6 +158,7 @@ callback (void *data, int argc, char **argv, char **colname)
        time_t now;
        int max_age;
        gboolean http_only = FALSE, secure = FALSE;
+       SoupSameSitePolicy same_site_policy;
 
        now = time (NULL);
 
@@ -172,6 +174,7 @@ callback (void *data, int argc, char **argv, char **colname)
 
        http_only = (g_strcmp0 (argv[COL_HTTP_ONLY], "1") == 0);
        secure = (g_strcmp0 (argv[COL_SECURE], "1") == 0);
+       same_site_policy = g_ascii_strtoll (argv[COL_SAME_SITE_POLICY], NULL, 0);
 
        cookie = soup_cookie_new (name, value, host, path, max_age);
 
@@ -179,6 +182,8 @@ callback (void *data, int argc, char **argv, char **colname)
                soup_cookie_set_secure (cookie, TRUE);
        if (http_only)
                soup_cookie_set_http_only (cookie, TRUE);
+       if (same_site_policy)
+               soup_cookie_set_same_site_policy (cookie, same_site_policy);
 
        soup_cookie_jar_add_cookie (jar, cookie);
 
@@ -241,6 +246,10 @@ open_db (SoupCookieJar *jar)
                sqlite3_free (error);
        }
 
+       /* Migrate old DB to include same-site info. We simply always run this as it
+          will safely handle a column with the same name existing */
+       sqlite3_exec (priv->db, "ALTER TABLE moz_cookies ADD COLUMN sameSite INTEGER DEFAULT 0", NULL, NULL, 
NULL);
+
        return FALSE;
 }
 
@@ -291,7 +300,8 @@ soup_cookie_jar_db_changed (SoupCookieJar *jar,
                                         new_cookie->path,
                                         expires,
                                         new_cookie->secure,
-                                        new_cookie->http_only);
+                                        new_cookie->http_only,
+                                        soup_cookie_get_same_site_policy (new_cookie));
                exec_query_with_try_create_table (priv->db, query, NULL, NULL);
                sqlite3_free (query);
        }
diff --git a/libsoup/soup-cookie-jar-text.c b/libsoup/soup-cookie-jar-text.c
index 46e12e54..26d2c321 100644
--- a/libsoup/soup-cookie-jar-text.c
+++ b/libsoup/soup-cookie-jar-text.c
@@ -121,6 +121,34 @@ soup_cookie_jar_text_new (const char *filename, gboolean read_only)
                             NULL);
 }
 
+static SoupSameSitePolicy
+string_to_same_site_policy (const char *string)
+{
+       if (strcmp (string, "Lax") == 0)
+               return SOUP_SAME_SITE_POLICY_LAX;
+       else if (strcmp (string, "Strict") == 0)
+               return SOUP_SAME_SITE_POLICY_STRICT;
+       else if (strcmp (string, "None") == 0)
+               return SOUP_SAME_SITE_POLICY_NONE;
+       else
+               g_return_val_if_reached (SOUP_SAME_SITE_POLICY_NONE);
+}
+
+static const char *
+same_site_policy_to_string (SoupSameSitePolicy policy)
+{
+       switch (policy) {
+       case SOUP_SAME_SITE_POLICY_STRICT:
+               return "Strict";
+       case SOUP_SAME_SITE_POLICY_LAX:
+               return "Lax";
+       case SOUP_SAME_SITE_POLICY_NONE:
+               return "None";
+       }
+
+       g_return_val_if_reached ("None");
+}
+
 static SoupCookie*
 parse_cookie (char *line, time_t now)
 {
@@ -129,7 +157,8 @@ parse_cookie (char *line, time_t now)
        gboolean http_only;
        gulong expire_time;
        int max_age;
-       char *host, *path, *secure, *expires, *name, *value;
+       char *host, *path, *secure, *expires, *name, *value, *samesite = NULL;
+       gsize result_length;
 
        if (g_str_has_prefix (line, "#HttpOnly_")) {
                http_only = TRUE;
@@ -140,7 +169,8 @@ parse_cookie (char *line, time_t now)
                http_only = FALSE;
 
        result = g_strsplit (line, "\t", -1);
-       if (g_strv_length (result) != 7)
+       result_length = g_strv_length (result);
+       if (result_length < 7)
                goto out;
 
        /* Check this first */
@@ -164,8 +194,14 @@ parse_cookie (char *line, time_t now)
        name = result[5];
        value = result[6];
 
+       if (result_length == 8)
+               samesite = result[7];
+
        cookie = soup_cookie_new (name, value, host, path, max_age);
 
+       if (samesite != NULL)
+               soup_cookie_set_same_site_policy (cookie, string_to_same_site_policy (samesite));
+
        if (strcmp (secure, "FALSE") != 0)
                soup_cookie_set_secure (cookie, TRUE);
        if (http_only)
@@ -219,7 +255,7 @@ write_cookie (FILE *out, SoupCookie *cookie)
 {
        fseek (out, 0, SEEK_END);
 
-       fprintf (out, "%s%s\t%s\t%s\t%s\t%lu\t%s\t%s\n",
+       fprintf (out, "%s%s\t%s\t%s\t%s\t%lu\t%s\t%s\t%s\n",
                 cookie->http_only ? "#HttpOnly_" : "",
                 cookie->domain,
                 *cookie->domain == '.' ? "TRUE" : "FALSE",
@@ -227,7 +263,8 @@ write_cookie (FILE *out, SoupCookie *cookie)
                 cookie->secure ? "TRUE" : "FALSE",
                 (gulong)soup_date_to_time_t (cookie->expires),
                 cookie->name,
-                cookie->value);
+                cookie->value,
+                same_site_policy_to_string (soup_cookie_get_same_site_policy (cookie)));
 }
 
 static void
diff --git a/libsoup/soup-cookie-jar.c b/libsoup/soup-cookie-jar.c
index 82513c3e..42a3556d 100644
--- a/libsoup/soup-cookie-jar.c
+++ b/libsoup/soup-cookie-jar.c
@@ -12,6 +12,7 @@
 #include <string.h>
 
 #include "soup-cookie-jar.h"
+#include "soup-message-private.h"
 #include "soup-misc-private.h"
 #include "soup.h"
 
@@ -297,8 +298,42 @@ compare_cookies (gconstpointer a, gconstpointer b, gpointer jar)
        return aserial - bserial;
 }
 
+static gboolean
+cookie_is_valid_for_same_site_policy (SoupCookie *cookie,
+                                      const char *method,
+                                      SoupURI    *uri,
+                                      SoupURI    *top_level,
+                                      SoupURI    *cookie_uri,
+                                      gboolean    is_top_level_navigation,
+                                      gboolean    for_http)
+{
+       SoupSameSitePolicy policy = soup_cookie_get_same_site_policy (cookie);
+
+       if (policy == SOUP_SAME_SITE_POLICY_NONE)
+               return TRUE;
+
+       if (top_level == NULL)
+               return TRUE;
+
+       if (policy == SOUP_SAME_SITE_POLICY_LAX && is_top_level_navigation &&
+           (SOUP_METHOD_IS_SAFE (method) || for_http == FALSE))
+               return TRUE;
+
+       if (is_top_level_navigation && cookie_uri == NULL)
+               return FALSE;
+
+       return soup_host_matches_host (soup_uri_get_host (cookie_uri ? cookie_uri : top_level), 
soup_uri_get_host (uri));
+}
+
 static GSList *
-get_cookies (SoupCookieJar *jar, SoupURI *uri, gboolean for_http, gboolean copy_cookies)
+get_cookies (SoupCookieJar *jar,
+             SoupURI       *uri,
+             SoupURI       *top_level,
+             SoupURI       *site_for_cookies,
+             const char    *method,
+             gboolean       for_http,
+             gboolean       is_top_level_navigation,
+             gboolean       copy_cookies)
 {
        SoupCookieJarPrivate *priv;
        GSList *cookies, *domain_cookies;
@@ -332,6 +367,9 @@ get_cookies (SoupCookieJar *jar, SoupURI *uri, gboolean for_http, gboolean copy_
                                                     g_strdup (cur),
                                                     new_head);
                        } else if (soup_cookie_applies_to_uri (cookie, uri) &&
+                                  cookie_is_valid_for_same_site_policy (cookie, method, uri, top_level,
+                                                                        site_for_cookies, 
is_top_level_navigation,
+                                                                        for_http) &&
                                   (for_http || !cookie->http_only))
                                cookies = g_slist_append (cookies, copy_cookies ? soup_cookie_copy (cookie) : 
cookie);
 
@@ -386,7 +424,7 @@ soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri,
        g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
        g_return_val_if_fail (uri != NULL, NULL);
 
-       cookies = get_cookies (jar, uri, for_http, FALSE);
+       cookies = get_cookies (jar, uri, NULL, NULL, NULL, for_http, FALSE, FALSE);
 
        if (cookies) {
                char *result = soup_cookies_to_cookie_header (cookies);
@@ -430,7 +468,46 @@ soup_cookie_jar_get_cookie_list (SoupCookieJar *jar, SoupURI *uri, gboolean for_
        g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
        g_return_val_if_fail (uri != NULL, NULL);
 
-       return get_cookies (jar, uri, for_http, TRUE);
+       return get_cookies (jar, uri, NULL, NULL, NULL, for_http, FALSE, TRUE);
+}
+
+/**
+ * soup_cookie_jar_get_cookie_list_with_same_site_info:
+ * @jar: a #SoupCookieJar
+ * @uri: a #SoupURI
+ * @top_level: (nullable): a #SoupURI for the top level document
+ * @site_for_cookies: (nullable): a #SoupURI indicating the origin to get cookies for
+ * @method: (nullable): the HTTP method requesting the cookies, this
+ * should only be %NULL when @for_http is %FALSE
+ * @for_http: whether or not the return value is being passed directly
+ * to an HTTP operation
+ * @is_top_level_navigation: whether or not the HTTP request is part of
+ * top level navigation
+ *
+ * This is an extended version of soup_cookie_jar_get_cookie_list() that
+ * provides more information required to use SameSite cookies. See the
+ * [SameSite cookies spec](https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00)
+ * for more detailed information.
+ *
+ * Return value: (transfer full) (element-type Soup.Cookie): a #GSList
+ * with the cookies in the @jar that would be sent with a request to @uri.
+ *
+ * Since: 2.70
+ */
+GSList *
+soup_cookie_jar_get_cookie_list_with_same_site_info (SoupCookieJar *jar,
+                                                     SoupURI       *uri,
+                                                     SoupURI       *top_level,
+                                                     SoupURI       *site_for_cookies,
+                                                     const char    *method,
+                                                     gboolean      for_http,
+                                                     gboolean      is_top_level_navigation)
+{
+       g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
+       g_return_val_if_fail (uri != NULL, NULL);
+       g_return_val_if_fail (method != NULL || for_http == FALSE, NULL);
+
+       return get_cookies (jar,  uri, top_level, site_for_cookies, g_intern_string (method), for_http, 
is_top_level_navigation, TRUE);
 }
 
 static const char *
@@ -722,15 +799,21 @@ static void
 msg_starting_cb (SoupMessage *msg, gpointer feature)
 {
        SoupCookieJar *jar = SOUP_COOKIE_JAR (feature);
-       char *cookies;
+       GSList *cookies;
 
-       cookies = soup_cookie_jar_get_cookies (jar, soup_message_get_uri (msg), TRUE);
-       if (cookies) {
-               soup_message_headers_replace (msg->request_headers,
-                                             "Cookie", cookies);
-               g_free (cookies);
-       } else
+       cookies = soup_cookie_jar_get_cookie_list_with_same_site_info (jar, soup_message_get_uri (msg),
+                                                                      soup_message_get_first_party (msg),
+                                                                      soup_message_get_site_for_cookies 
(msg),
+                                                                      msg->method,
+                                                                      TRUE, 
soup_message_get_is_top_level_navigation (msg));
+       if (cookies != NULL) {
+               char *cookie_header = soup_cookies_to_cookie_header (cookies);
+               soup_message_headers_replace (msg->request_headers, "Cookie", cookie_header);
+               g_free (cookie_header);
+               g_slist_free_full (cookies, (GDestroyNotify)soup_cookie_free);
+       } else {
                soup_message_headers_remove (msg->request_headers, "Cookie");
+       }
 }
 
 static void
diff --git a/libsoup/soup-cookie-jar.h b/libsoup/soup-cookie-jar.h
index d3ee4f23..639df342 100644
--- a/libsoup/soup-cookie-jar.h
+++ b/libsoup/soup-cookie-jar.h
@@ -59,6 +59,15 @@ SOUP_AVAILABLE_IN_2_40
 GSList        *           soup_cookie_jar_get_cookie_list             (SoupCookieJar             *jar,
                                                                       SoupURI                   *uri,
                                                                       gboolean                   for_http);
+SOUP_AVAILABLE_IN_2_70
+GSList        *           soup_cookie_jar_get_cookie_list_with_same_site_info (
+                                                                      SoupCookieJar             *jar,
+                                                                      SoupURI                   *uri,
+                                                                      SoupURI                   *top_level,
+                                                                      SoupURI                   
*site_for_cookies,
+                                                                      const char                *method,
+                                                                      gboolean                   for_http,
+                                                                      gboolean                   
is_top_level_navigation);
 SOUP_AVAILABLE_IN_2_24
 void                      soup_cookie_jar_set_cookie                  (SoupCookieJar             *jar,
                                                                       SoupURI                   *uri,
diff --git a/libsoup/soup-cookie.c b/libsoup/soup-cookie.c
index 7cea82e5..cf538831 100644
--- a/libsoup/soup-cookie.c
+++ b/libsoup/soup-cookie.c
@@ -88,6 +88,7 @@ soup_cookie_copy (SoupCookie *cookie)
                copy->expires = soup_date_copy(cookie->expires);
        copy->secure = cookie->secure;
        copy->http_only = cookie->http_only;
+       soup_cookie_set_same_site_policy (copy, soup_cookie_get_same_site_policy (cookie));
 
        return copy;
 }
@@ -238,6 +239,18 @@ parse_one_cookie (const char *header, SoupURI *origin)
                        cookie->secure = TRUE;
                        if (has_value)
                                parse_value (&p, FALSE);
+               } else if (MATCH_NAME ("samesite")) {
+                       if (has_value) {
+                               char *policy = parse_value (&p, TRUE);
+                               if (g_ascii_strcasecmp (policy, "Lax") == 0)
+                                       soup_cookie_set_same_site_policy (cookie, SOUP_SAME_SITE_POLICY_LAX);
+                               else if (g_ascii_strcasecmp (policy, "Strict") == 0)
+                                       soup_cookie_set_same_site_policy (cookie, 
SOUP_SAME_SITE_POLICY_STRICT);
+                               /* There is an explicit "None" value which is the default. */
+                               g_free (policy);
+                       }
+                       /* Note that earlier versions of the same-site RFC treated invalid values as strict 
but
+                          the latest revision simply ignores them. */
                } else {
                        /* Ignore unknown attributes, but we still have
                         * to skip over the value.
@@ -708,6 +721,8 @@ soup_cookie_set_http_only (SoupCookie *cookie, gboolean http_only)
 static void
 serialize_cookie (SoupCookie *cookie, GString *header, gboolean set_cookie)
 {
+       SoupSameSitePolicy same_site_policy;
+
        if (!*cookie->name && !*cookie->value)
                return;
 
@@ -743,12 +758,63 @@ serialize_cookie (SoupCookie *cookie, GString *header, gboolean set_cookie)
                g_string_append (header, "; domain=");
                g_string_append (header, cookie->domain);
        }
+
+       same_site_policy = soup_cookie_get_same_site_policy (cookie);
+       if (same_site_policy != SOUP_SAME_SITE_POLICY_NONE) {
+               g_string_append (header, "; SameSite=");
+               if (same_site_policy == SOUP_SAME_SITE_POLICY_LAX)
+                       g_string_append (header, "Lax");
+               else
+                       g_string_append (header, "Strict");
+       }
        if (cookie->secure)
                g_string_append (header, "; secure");
        if (cookie->http_only)
                g_string_append (header, "; HttpOnly");
 }
 
+static const char *same_site_policy_string = "soup-same-site-policy";
+#define SAME_SITE_POLICY_QUARK (g_quark_from_static_string (same_site_policy_string))
+
+/**
+ * soup_cookie_set_same_site_policy:
+ * @cookie: a #SoupCookie
+ * @policy: a #SoupSameSitePolicy
+ *
+ * When used in conjunction with soup_cookie_jar_get_cookie_list_with_same_site_info() this
+ * sets the policy of when this cookie should be exposed.
+ *
+ * Since: 2.70
+ **/
+void
+soup_cookie_set_same_site_policy (SoupCookie         *cookie,
+                                  SoupSameSitePolicy  policy)
+{
+       switch (policy) {
+       case SOUP_SAME_SITE_POLICY_NONE:
+       case SOUP_SAME_SITE_POLICY_STRICT:
+       case SOUP_SAME_SITE_POLICY_LAX:
+               g_dataset_id_set_data (cookie, SAME_SITE_POLICY_QUARK, GUINT_TO_POINTER (policy));
+               break;
+       default:
+               g_return_if_reached ();
+       }
+}
+
+/**
+ * soup_cookie_get_same_site_policy:
+ * @cookie: a #SoupCookie
+ *
+ * Returns: a #SoupSameSitePolicy
+ *
+ * Since: 2.70
+ **/
+SoupSameSitePolicy
+soup_cookie_get_same_site_policy (SoupCookie *cookie)
+{
+       return GPOINTER_TO_UINT (g_dataset_id_get_data (cookie, SAME_SITE_POLICY_QUARK));
+}
+
 /**
  * soup_cookie_to_set_cookie_header:
  * @cookie: a #SoupCookie
@@ -808,6 +874,7 @@ soup_cookie_free (SoupCookie *cookie)
        g_free (cookie->path);
        g_clear_pointer (&cookie->expires, soup_date_free);
 
+       g_dataset_destroy (cookie);
        g_slice_free (SoupCookie, cookie);
 }
 
diff --git a/libsoup/soup-cookie.h b/libsoup/soup-cookie.h
index 21973a49..c00a153d 100644
--- a/libsoup/soup-cookie.h
+++ b/libsoup/soup-cookie.h
@@ -10,6 +10,20 @@
 
 G_BEGIN_DECLS
 
+/**
+ * SoupSameSitePolicy:
+ * @SOUP_SAME_SITE_POLICY_NONE: The cookie is exposed with both cross-site and same-site requests
+ * @SOUP_SAME_SITE_POLICY_LAX: The cookie is withheld on cross-site requests but exposed on cross-site 
navigations
+ * @SOUP_SAME_SITE_POLICY_STRICT: The cookie is only exposed for same-site requests
+ *
+ * Since: 2.70
+ */
+typedef enum {
+       SOUP_SAME_SITE_POLICY_NONE,
+       SOUP_SAME_SITE_POLICY_LAX,
+       SOUP_SAME_SITE_POLICY_STRICT,
+} SoupSameSitePolicy;
+
 struct _SoupCookie {
        char     *name;
        char     *value;
@@ -80,6 +94,12 @@ SOUP_AVAILABLE_IN_2_24
 void        soup_cookie_set_http_only           (SoupCookie  *cookie,
                                                 gboolean     http_only);
 
+SOUP_AVAILABLE_IN_2_70
+void        soup_cookie_set_same_site_policy    (SoupCookie         *cookie,
+                                                 SoupSameSitePolicy  policy);
+SOUP_AVAILABLE_IN_2_70
+SoupSameSitePolicy soup_cookie_get_same_site_policy (SoupCookie     *cookie);
+
 SOUP_AVAILABLE_IN_2_24
 char       *soup_cookie_to_set_cookie_header    (SoupCookie  *cookie);
 SOUP_AVAILABLE_IN_2_24
diff --git a/libsoup/soup-message-private.h b/libsoup/soup-message-private.h
index dd345bd1..0c6f5d97 100644
--- a/libsoup/soup-message-private.h
+++ b/libsoup/soup-message-private.h
@@ -36,6 +36,7 @@ typedef struct {
        GSList            *disabled_features;
 
        SoupURI           *first_party;
+       SoupURI           *site_for_cookies;
 
        GTlsCertificate      *tls_certificate;
        GTlsCertificateFlags  tls_errors;
@@ -43,6 +44,8 @@ typedef struct {
        SoupRequest       *request;
 
        SoupMessagePriority priority;
+
+       gboolean is_top_level_navigation;
 } SoupMessagePrivate;
 
 void             soup_message_cleanup_response (SoupMessage      *msg);
diff --git a/libsoup/soup-message.c b/libsoup/soup-message.c
index f61f58c2..72c1232c 100644
--- a/libsoup/soup-message.c
+++ b/libsoup/soup-message.c
@@ -144,6 +144,8 @@ enum {
        PROP_TLS_CERTIFICATE,
        PROP_TLS_ERRORS,
        PROP_PRIORITY,
+       PROP_SITE_FOR_COOKIES,
+       PROP_IS_TOP_LEVEL_NAVIGATION,
 
        LAST_PROP
 };
@@ -174,6 +176,7 @@ soup_message_finalize (GObject *object)
 
        g_clear_pointer (&priv->uri, soup_uri_free);
        g_clear_pointer (&priv->first_party, soup_uri_free);
+       g_clear_pointer (&priv->site_for_cookies, soup_uri_free);
        g_clear_object (&priv->addr);
 
        g_clear_object (&priv->auth);
@@ -207,6 +210,12 @@ soup_message_set_property (GObject *object, guint prop_id,
        case PROP_URI:
                soup_message_set_uri (msg, g_value_get_boxed (value));
                break;
+       case PROP_SITE_FOR_COOKIES:
+               soup_message_set_site_for_cookies (msg, g_value_get_boxed (value));
+               break;
+       case PROP_IS_TOP_LEVEL_NAVIGATION:
+               soup_message_set_is_top_level_navigation (msg, g_value_get_boolean (value));
+               break;
        case PROP_HTTP_VERSION:
                soup_message_set_http_version (msg, g_value_get_enum (value));
                break;
@@ -270,6 +279,12 @@ soup_message_get_property (GObject *object, guint prop_id,
        case PROP_URI:
                g_value_set_boxed (value, priv->uri);
                break;
+       case PROP_SITE_FOR_COOKIES:
+               g_value_set_boxed (value, priv->site_for_cookies);
+               break;
+       case PROP_IS_TOP_LEVEL_NAVIGATION:
+               g_value_set_boolean (value, priv->is_top_level_navigation);
+               break;
        case PROP_HTTP_VERSION:
                g_value_set_enum (value, priv->http_version);
                break;
@@ -803,6 +818,34 @@ soup_message_class_init (SoupMessageClass *message_class)
                                    "The URI loaded in the application when the message was requested.",
                                    SOUP_TYPE_URI,
                                    G_PARAM_READWRITE));
+       /**
+        * SoupMessage:site-for-cookkies:
+        *
+        * Site used to compare cookies against. Used for SameSite cookie support.
+        *
+        * Since: 2.70
+        */
+       g_object_class_install_property (
+               object_class, PROP_SITE_FOR_COOKIES,
+               g_param_spec_boxed (SOUP_MESSAGE_SITE_FOR_COOKIES,
+                                   "Site for cookies",
+                                   "The URI for the site to compare cookies against",
+                                   SOUP_TYPE_URI,
+                                   G_PARAM_READWRITE));
+       /**
+        * SoupMessage:is-top-level-navigation:
+        *
+        * Set when the message is navigating between top level domains.
+        *
+        * Since: 2.70
+        */
+       g_object_class_install_property (
+               object_class, PROP_IS_TOP_LEVEL_NAVIGATION,
+               g_param_spec_boolean (SOUP_MESSAGE_IS_TOP_LEVEL_NAVIGATION,
+                                    "Is top-level navigation",
+                                    "If the current messsage is navigating between top-levels",
+                                    FALSE,
+                                    G_PARAM_READWRITE));
        /**
         * SOUP_MESSAGE_REQUEST_BODY:
         *
@@ -1939,6 +1982,111 @@ soup_message_set_first_party (SoupMessage *msg,
        g_object_notify (G_OBJECT (msg), SOUP_MESSAGE_FIRST_PARTY);
 }
 
+/**
+ * soup_message_get_site_for_cookies:
+ * @msg: a #SoupMessage
+ *
+ * Gets @msg's site for cookies #SoupURI
+ *
+ * Returns: (transfer none): the @msg's site for cookies #SoupURI
+ *
+ * Since: 2.70
+ **/
+SoupURI *
+soup_message_get_site_for_cookies (SoupMessage *msg)
+{
+       SoupMessagePrivate *priv;
+
+       g_return_val_if_fail (SOUP_IS_MESSAGE (msg), NULL);
+
+       priv = soup_message_get_instance_private (msg);
+       return priv->site_for_cookies;
+}
+
+/**
+ * soup_message_set_site_for_cookies:
+ * @msg: a #SoupMessage
+ * @site_for_cookies: (nullable): the #SoupURI for the @msg's site for cookies
+ *
+ * Sets @site_for_cookies as the policy URL for same-site cookies for @msg.
+ *
+ * It is either the URL of the top-level document or %NULL depending on whether the registrable
+ * domain of this document's URL matches the registrable domain of its parent's/opener's
+ * URL. For the top-level document it is set to the document's URL.
+ *
+ * See the [same-site spec](https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00)
+ * for more information.
+ *
+ * Since: 2.70
+ **/
+void
+soup_message_set_site_for_cookies (SoupMessage *msg,
+                                  SoupURI     *site_for_cookies)
+{
+       SoupMessagePrivate *priv;
+
+       g_return_if_fail (SOUP_IS_MESSAGE (msg));
+
+       priv = soup_message_get_instance_private (msg);
+
+       if (priv->site_for_cookies == site_for_cookies)
+               return;
+
+       if (priv->site_for_cookies) {
+               if (site_for_cookies && soup_uri_equal (priv->site_for_cookies, site_for_cookies))
+                       return;
+
+               soup_uri_free (priv->site_for_cookies);
+       }
+
+       priv->site_for_cookies = site_for_cookies ? soup_uri_copy (site_for_cookies) : NULL;
+       g_object_notify (G_OBJECT (msg), SOUP_MESSAGE_SITE_FOR_COOKIES);
+}
+
+/**
+ * soup_message_set_is_top_level_navigation:
+ * @msg: a #SoupMessage
+ * @is_top_level_navigation: if %TRUE indicate the current request is a top-level navigation
+ *
+ * See the [same-site spec](https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00)
+ * for more information.
+ *
+ * Since: 2.70
+ **/
+void
+soup_message_set_is_top_level_navigation (SoupMessage *msg,
+                                        gboolean     is_top_level_navigation)
+{
+       SoupMessagePrivate *priv;
+
+       g_return_if_fail (SOUP_IS_MESSAGE (msg));
+
+       priv = soup_message_get_instance_private (msg);
+
+       if (priv->is_top_level_navigation == is_top_level_navigation)
+               return;
+
+       priv->is_top_level_navigation = is_top_level_navigation;
+       g_object_notify (G_OBJECT (msg), SOUP_MESSAGE_IS_TOP_LEVEL_NAVIGATION);
+}
+
+/**
+ * soup_message_get_is_top_level_navigation:
+ * @msg: a #SoupMessage
+ *
+ * Since: 2.70
+ **/
+gboolean
+soup_message_get_is_top_level_navigation (SoupMessage *msg)
+{
+       SoupMessagePrivate *priv;
+
+       g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE);
+
+       priv = soup_message_get_instance_private (msg);
+       return priv->is_top_level_navigation;
+}
+
 void
 soup_message_set_https_status (SoupMessage *msg, SoupConnection *conn)
 {
diff --git a/libsoup/soup-message.h b/libsoup/soup-message.h
index 93778961..41004fd9 100644
--- a/libsoup/soup-message.h
+++ b/libsoup/soup-message.h
@@ -69,6 +69,7 @@ GType soup_message_get_type (void);
 #define SOUP_MESSAGE_STATUS_CODE        "status-code"
 #define SOUP_MESSAGE_REASON_PHRASE      "reason-phrase"
 #define SOUP_MESSAGE_FIRST_PARTY        "first-party"
+#define SOUP_MESSAGE_SITE_FOR_COOKIES   "site-for-cookies"
 #define SOUP_MESSAGE_REQUEST_BODY       "request-body"
 #define SOUP_MESSAGE_REQUEST_BODY_DATA  "request-body-data"
 #define SOUP_MESSAGE_REQUEST_HEADERS    "request-headers"
@@ -78,6 +79,7 @@ GType soup_message_get_type (void);
 #define SOUP_MESSAGE_TLS_CERTIFICATE    "tls-certificate"
 #define SOUP_MESSAGE_TLS_ERRORS         "tls-errors"
 #define SOUP_MESSAGE_PRIORITY           "priority"
+#define SOUP_MESSAGE_IS_TOP_LEVEL_NAVIGATION   "is-top-level-navigation"
 
 SOUP_AVAILABLE_IN_2_4
 SoupMessage   *soup_message_new                 (const char        *method,
@@ -126,6 +128,16 @@ SoupURI         *soup_message_get_first_party     (SoupMessage       *msg);
 SOUP_AVAILABLE_IN_2_30
 void             soup_message_set_first_party     (SoupMessage       *msg,
                                                   SoupURI           *first_party);
+SOUP_AVAILABLE_IN_2_70
+SoupURI         *soup_message_get_site_for_cookies (SoupMessage      *msg);
+SOUP_AVAILABLE_IN_2_70
+void             soup_message_set_site_for_cookies (SoupMessage      *msg,
+                                                   SoupURI          *site_for_cookies);
+SOUP_AVAILABLE_IN_2_70
+void             soup_message_set_is_top_level_navigation (SoupMessage      *msg,
+                                                          gboolean          is_top_level_navigation);
+SOUP_AVAILABLE_IN_2_70
+gboolean         soup_message_get_is_top_level_navigation (SoupMessage      *msg);
 
 typedef enum {
        SOUP_MESSAGE_NO_REDIRECT              = (1 << 1),
diff --git a/tests/cookies-test.c b/tests/cookies-test.c
index 1c07b038..d25da0e9 100644
--- a/tests/cookies-test.c
+++ b/tests/cookies-test.c
@@ -278,13 +278,13 @@ do_cookies_parsing_test (void)
 
        msg = soup_message_new_from_uri ("GET", first_party_uri);
        soup_message_headers_append (msg->request_headers, "Echo-Set-Cookie",
-                                    "two=2; HttpOnly; max-age=100");
+                                    "two=2; HttpOnly; max-age=100; SameSite=Invalid");
        soup_session_send_message (session, msg);
        g_object_unref (msg);
 
        msg = soup_message_new_from_uri ("GET", first_party_uri);
        soup_message_headers_append (msg->request_headers, "Echo-Set-Cookie",
-                                    "three=3; httpONLY=Wednesday; max-age=100");
+                                    "three=3; httpONLY=Wednesday; max-age=100; SameSite=Lax");
        soup_session_send_message (session, msg);
        g_object_unref (msg);
 
@@ -302,10 +302,12 @@ do_cookies_parsing_test (void)
                        got2 = TRUE;
                        g_assert_true (soup_cookie_get_http_only (cookie));
                        g_assert_true (soup_cookie_get_expires (cookie) != NULL);
+                       g_assert_cmpint (soup_cookie_get_same_site_policy (cookie), ==, 
SOUP_SAME_SITE_POLICY_NONE);
                } else if (!strcmp (soup_cookie_get_name (cookie), "three")) {
                        got3 = TRUE;
                        g_assert_true (soup_cookie_get_http_only (cookie));
                        g_assert_true (soup_cookie_get_expires (cookie) != NULL);
+                       g_assert_cmpint (soup_cookie_get_same_site_policy (cookie), ==, 
SOUP_SAME_SITE_POLICY_LAX);
                } else {
                        soup_test_assert (FALSE, "got unexpected cookie '%s'",
                                          soup_cookie_get_name (cookie));
diff --git a/tests/meson.build b/tests/meson.build
index 5d6a8f6b..119bf166 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -44,6 +44,7 @@ tests = [
   ['redirect', true, []],
   ['requester', true, []],
   ['resource', true, []],
+  ['samesite', true, []],
   ['session', true, []],
   ['server-auth', true, []],
   ['server', true, []],
diff --git a/tests/samesite-test.c b/tests/samesite-test.c
new file mode 100644
index 00000000..675f1e57
--- /dev/null
+++ b/tests/samesite-test.c
@@ -0,0 +1,132 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+
+#include "test-utils.h"
+
+typedef struct {
+       SoupURI *origin_uri;
+       SoupURI *cross_uri;
+       SoupCookieJar *jar;
+       GSList *cookies;
+} SameSiteFixture;
+
+static void
+same_site_setup (SameSiteFixture *fixture,
+                 gconstpointer    data)
+{
+       SoupCookie *cookie_none, *cookie_lax, *cookie_strict;
+
+       fixture->origin_uri = soup_uri_new ("http://127.0.0.1";);
+       fixture->cross_uri = soup_uri_new ("http://localhost";);
+       fixture->jar = soup_cookie_jar_new ();
+
+       cookie_none = soup_cookie_new ("none", "1", "127.0.0.1", "/", 1000);
+       cookie_lax = soup_cookie_new ("lax", "1", "127.0.0.1", "/", 1000);
+       soup_cookie_set_same_site_policy (cookie_lax, SOUP_SAME_SITE_POLICY_LAX);
+       cookie_strict = soup_cookie_new ("strict", "1", "127.0.0.1", "/", 1000);
+       soup_cookie_set_same_site_policy (cookie_strict, SOUP_SAME_SITE_POLICY_STRICT);
+
+       soup_cookie_jar_add_cookie_with_first_party (fixture->jar, fixture->origin_uri, cookie_none);
+       soup_cookie_jar_add_cookie_with_first_party (fixture->jar, fixture->origin_uri, cookie_lax);
+       soup_cookie_jar_add_cookie_with_first_party (fixture->jar, fixture->origin_uri, cookie_strict);
+}
+
+static void
+same_site_teardown (SameSiteFixture *fixture,
+                    gconstpointer    data)
+{
+       g_object_unref (fixture->jar);
+       soup_uri_free (fixture->origin_uri);
+       soup_uri_free (fixture->cross_uri);
+       g_slist_free_full (fixture->cookies, (GDestroyNotify) soup_cookie_free);
+}
+
+static void
+assert_highest_policy_visible (GSList *cookies, SoupSameSitePolicy policy)
+{
+       GSList *l;
+       size_t size = 0, expected_count;
+       for (l = cookies; l; l = l->next) {
+               g_assert_cmpint (soup_cookie_get_same_site_policy (l->data), <=, policy);
+               ++size;
+       }
+
+       switch (policy) {
+       case SOUP_SAME_SITE_POLICY_STRICT:
+               expected_count = 3;
+               break;
+       case SOUP_SAME_SITE_POLICY_LAX:
+               expected_count = 2;
+               break;
+       case SOUP_SAME_SITE_POLICY_NONE:
+               expected_count = 1;
+               break;
+       }
+
+       g_assert_cmpuint (size, ==, expected_count);
+}
+
+typedef struct {
+       const char *name;
+       gboolean cross_origin;
+       gboolean cookie_uri_is_origin;
+       gboolean top_level_nav;
+       gboolean javascript;
+       gboolean unsafe_method;
+       SoupSameSitePolicy visible_policy;
+} SameSiteTest;
+
+static void
+same_site_test (SameSiteFixture *fixture, gconstpointer user_data)
+{
+       const SameSiteTest *test = user_data;
+       fixture->cookies = soup_cookie_jar_get_cookie_list_with_same_site_info (fixture->jar, 
fixture->origin_uri,
+                                                                               test->cross_origin ? 
fixture->cross_uri : fixture->origin_uri,
+                                                                               test->cookie_uri_is_origin ? 
fixture->origin_uri : NULL,
+                                                                               test->unsafe_method ? "POST" 
: "GET",
+                                                                               test->javascript ? FALSE : 
TRUE,
+                                                                               test->top_level_nav);
+       assert_highest_policy_visible (fixture->cookies, test->visible_policy);
+}
+
+int
+main (int argc, char **argv)
+{
+       int ret, i;
+       SameSiteTest same_site_tests[] = {
+               /* This does not necessarily cover all combinations since some make no sense in real use */
+
+               /* Situations where Strict are passed: */
+               { .name="/same-site/basic", .visible_policy=SOUP_SAME_SITE_POLICY_STRICT },
+               { .name="/same-site/basic-js", .javascript=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT 
},
+               { .name="/same-site/top-level-to-same-site", .top_level_nav=TRUE,  
.cookie_uri_is_origin=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT },
+               { .name="/same-site/top-level-to-same-site-js", .top_level_nav=TRUE, 
.cookie_uri_is_origin=TRUE,  .javascript=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT },
+               { .name="/same-site/unsafe-method", .unsafe_method=TRUE, 
.visible_policy=SOUP_SAME_SITE_POLICY_STRICT },
+               { .name="/same-site/unsafe-method-js", .unsafe_method=TRUE, .javascript=TRUE, 
.visible_policy=SOUP_SAME_SITE_POLICY_STRICT },
+               { .name="/same-site/cross-top-level-to-same-site", .cross_origin=TRUE, .top_level_nav=TRUE, 
.cookie_uri_is_origin=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT },
+               { .name="/same-site/cross-top-level-to-same-site-js", .cross_origin=TRUE, .javascript=TRUE, 
.top_level_nav=TRUE, .cookie_uri_is_origin=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT },
+
+               /* Situations where Lax are passed: */
+               { .name="/same-site/top-level", .top_level_nav=TRUE, 
.visible_policy=SOUP_SAME_SITE_POLICY_LAX },
+               { .name="/same-site/top-level-js", .top_level_nav=TRUE, .javascript=TRUE, 
.visible_policy=SOUP_SAME_SITE_POLICY_LAX },
+               { .name="/same-site/cross-top-level", .cross_origin=TRUE, .top_level_nav=TRUE, 
.visible_policy=SOUP_SAME_SITE_POLICY_LAX },
+               { .name="/same-site/cross-top-level-js", .cross_origin=TRUE, .javascript=TRUE, 
.top_level_nav=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_LAX },
+               { .name="/same-site/cross-unsafe-method-top-level-js", .cross_origin=TRUE, .javascript=TRUE, 
.unsafe_method=TRUE, .top_level_nav=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_LAX },
+
+               /* All same-site blocked: */
+               { .name="/same-site/cross-basic", .cross_origin=TRUE, 
.visible_policy=SOUP_SAME_SITE_POLICY_NONE },
+               { .name="/same-site/cross-basic-js", .cross_origin=TRUE, .javascript=TRUE, 
.visible_policy=SOUP_SAME_SITE_POLICY_NONE },
+               { .name="/same-site/cross-unsafe-method", .cross_origin=TRUE, .unsafe_method=TRUE, 
.visible_policy=SOUP_SAME_SITE_POLICY_NONE },
+               { .name="/same-site/cross-unsafe-method-js", .cross_origin=TRUE, .javascript=TRUE, 
.unsafe_method=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_NONE },
+               { .name="/same-site/cross-unsafe-method-top-level", .cross_origin=TRUE, .unsafe_method=TRUE, 
.top_level_nav=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_NONE },
+       };
+
+       test_init (argc, argv, NULL);
+
+       for (i = 0; i < G_N_ELEMENTS (same_site_tests); ++i)
+               g_test_add (same_site_tests[i].name, SameSiteFixture, &same_site_tests[i],
+                           same_site_setup, same_site_test, same_site_teardown);
+
+       ret = g_test_run ();
+       test_cleanup ();
+       return ret;
+}


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