Update cookie parsing/output to match the current httpstate draft better
authorDan Winship <danw@gnome.org>
Thu, 17 Dec 2009 12:28:39 +0000 (13:28 +0100)
committerDan Winship <danw@gnome.org>
Thu, 17 Dec 2009 12:52:18 +0000 (13:52 +0100)
Also fixes
https://bugzilla.gnome.org/show_bug.cgi?id=603496
https://bugzilla.gnome.org/show_bug.cgi?id=604794

libsoup/soup-cookie-jar.c
libsoup/soup-cookie.c

index a20f288..82e9c13 100644 (file)
@@ -63,7 +63,8 @@ enum {
 
 typedef struct {
        gboolean constructed, read_only;
-       GHashTable *domains;
+       GHashTable *domains, *serials;
+       guint serial;
 } SoupCookieJarPrivate;
 #define SOUP_COOKIE_JAR_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_COOKIE_JAR, SoupCookieJarPrivate))
 
@@ -77,8 +78,10 @@ soup_cookie_jar_init (SoupCookieJar *jar)
 {
        SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
 
-       priv->domains = g_hash_table_new_full (g_str_hash, g_str_equal,
+       priv->domains = g_hash_table_new_full (soup_str_case_hash,
+                                              soup_str_case_equal,
                                               g_free, NULL);
+       priv->serials = g_hash_table_new (NULL, NULL);
 }
 
 static void
@@ -100,6 +103,7 @@ finalize (GObject *object)
        while (g_hash_table_iter_next (&iter, &key, &value))
                soup_cookies_free (value);
        g_hash_table_destroy (priv->domains);
+       g_hash_table_destroy (priv->serials);
 
        G_OBJECT_CLASS (soup_cookie_jar_parent_class)->finalize (object);
 }
@@ -227,12 +231,45 @@ soup_cookie_jar_changed (SoupCookieJar *jar,
 {
        SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
 
+       if (old && old != new)
+               g_hash_table_remove (priv->serials, old);
+       if (new) {
+               priv->serial++;
+               g_hash_table_insert (priv->serials, new, GUINT_TO_POINTER (priv->serial));
+       }
+
        if (priv->read_only || !priv->constructed)
                return;
 
        g_signal_emit (jar, signals[CHANGED], 0, old, new);
 }
 
+static int
+compare_cookies (gconstpointer a, gconstpointer b, gpointer jar)
+{
+       SoupCookie *ca = (SoupCookie *)a;
+       SoupCookie *cb = (SoupCookie *)b;
+       SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
+       int alen, blen;
+       guint aserial, bserial;
+
+       /* "Cookies with longer path fields are listed before cookies
+        * with shorter path field."
+        */
+       alen = ca->path ? strlen (ca->path) : 0;
+       blen = cb->path ? strlen (cb->path) : 0;
+       if (alen != blen)
+               return blen - alen;
+
+       /* "Among cookies that have equal length path fields, cookies
+        * with earlier creation dates are listed before cookies with
+        * later creation dates."
+        */
+       aserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, ca));
+       bserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, cb));
+       return aserial - bserial;
+}
+
 /**
  * soup_cookie_jar_get_cookies:
  * @jar: a #SoupCookieJar
@@ -314,9 +351,14 @@ soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri,
        g_slist_free (cookies_to_remove);
 
        if (cookies) {
-               /* FIXME: sort? */
+               cookies = g_slist_sort_with_data (cookies, compare_cookies, jar);
                result = soup_cookies_to_cookie_header (cookies);
                g_slist_free (cookies);
+
+               if (!*result) {
+                       g_free (result);
+                       result = NULL;
+               }
                return result;
        } else
                return NULL;
index c53243e..c9d28b8 100644 (file)
  * Since: 2.24
  **/
 
-/* Our Set-Cookie grammar is something like the following, in terms of
- * RFC 2616 BNF:
- *
- * set-cookie             =  "Set-Cookie:" cookies
- * cookies                =  #cookie
- *
- * cookie                 =  [ NAME "=" ] VALUE *(";" [ cookie-av ] )
- * NAME                   =  cookie-attr
- * VALUE                  =  cookie-comma-value
- * cookie-av              =  "Domain" "=" cookie-value
- *                        |  "Expires" "=" cookie-date-value
- *                        |  "HttpOnly"
- *                        |  "Max-Age" "=" cookie-value
- *                        |  "Path" "=" cookie-value
- *                        |  "Secure"
- *                        |  cookie-attr [ "=" cookie-value ]
- *
- * cookie-attr            =  1*<any CHAR except CTLs or ";" or "," or "=">
- *
- * cookie-value           =  cookie-raw-value | cookie-quoted-string
- * cookie-raw-value       =  *<any CHAR except CTLs or ";" or ",">
- *
- * cookie-comma-value     =  cookie-raw-comma-value | cookie-quoted-string
- * cookie-raw-comma-value =  *<any CHAR except CTLs or ";">
- *
- * cookie-date-value      =  cookie-raw-date-value | cookie-quoted-string
- * cookie-raw-date-value  =  [ token "," ] cookie-raw-value
- *
- * cookie-quoted-string   =  quoted-string [ cookie-raw-value ]
- *
- * NAME is optional, as described in
- * https://bugzilla.mozilla.org/show_bug.cgi?id=169091#c16
- *
- * When VALUE is a quoted-string, the quotes (and any internal
- * backslashes) are considered part of the value, and returned
- * literally. When other cookie-values or cookie-comma-values are
- * quoted-strings, the quotes are NOT part of the value. If a
- * cookie-value or cookie-comma-value has trailing junk after the
- * quoted-string, it is discarded.
- *
- * Note that VALUE and "Expires" are allowed to have commas in them,
- * but anywhere else, a comma indicates a new cookie.
- *
- * The literal strings in cookie-av ("Domain", "Expires", etc) are all
- * case-insensitive. Unrecognized cookie attributes are discarded.
- *
- * Cookies are allowed to have excess ";"s, and in particular, can
- * have a trailing ";".
- */
-
 GType
 soup_cookie_get_type (void)
 {
@@ -204,44 +154,22 @@ unskip_lws (const char *s, const char *start)
 }
 
 #define is_attr_ender(ch) ((ch) < ' ' || (ch) == ';' || (ch) == ',' || (ch) == '=')
-#define is_value_ender(ch, allow_comma) ((ch) < ' ' || (ch) == ';' || (!(allow_comma) && (ch) == ','))
+#define is_value_ender(ch) ((ch) < ' ' || (ch) == ';')
 
 static char *
-parse_value (const char **val_p, gboolean keep_quotes, gboolean allow_comma)
+parse_value (const char **val_p)
 {
        const char *start, *end, *p;
-       char *value, *q;
+       char *value;
 
        p = *val_p;
        if (*p == '=')
                p++;
        start = skip_lws (p);
-       if (*start == '"') {
-               for (p = start + 1; *p && *p != '"'; p++) {
-                       if (*p == '\\' && *(p + 1))
-                               p++;
-               }
-               if (keep_quotes)
-                       value = g_strndup (start, p - start + 1);
-               else {
-                       value = g_malloc (p - (start + 1) + 1);
-                       for (p = start + 1, q = value; *p && *p != '"'; p++, q++) {
-                               if (*p == '\\' && *(p + 1))
-                                       p++;
-                               *q = *p;
-                       }
-                       *q = '\0';
-               }
-
-               /* Skip anything after the quoted-string */
-               while (!is_value_ender (*p, FALSE))
-                       p++;
-       } else {
-               for (p = start; !is_value_ender (*p, allow_comma); p++)
-                       ;
-               end = unskip_lws (p, start);
-               value = g_strndup (start, end - start);
-       }
+       for (p = start; !is_value_ender (*p); p++)
+               ;
+       end = unskip_lws (p, start);
+       value = g_strndup (start, end - start);
 
        *val_p = p;
        return value;
@@ -250,36 +178,19 @@ parse_value (const char **val_p, gboolean keep_quotes, gboolean allow_comma)
 static SoupDate *
 parse_date (const char **val_p)
 {
-       const char *start, *end, *p;
        char *value;
        SoupDate *date;
 
-       p = *val_p + 1;
-       start = skip_lws (p);
-       if (*start == '"')
-               value = parse_value (&p, FALSE, FALSE);
-       else {
-               gboolean allow_comma = TRUE;
-
-               for (p = start; !is_value_ender (*p, allow_comma); p++) {
-                       if (*p == ' ')
-                               allow_comma = FALSE;
-               }
-               end = unskip_lws (p, start);
-               value = g_strndup (start, end - start);
-       }
-
+       value = parse_value (val_p);
        date = soup_date_new_from_string (value);
        g_free (value);
-       *val_p = p;
        return date;
 }
 
 static SoupCookie *
-parse_one_cookie (const char **header_p, SoupURI *origin)
+parse_one_cookie (const char *header, SoupURI *origin)
 {
-       const char *header = *header_p, *p;
-       const char *start, *end;
+       const char *start, *end, *p;
        gboolean has_value;
        SoupCookie *cookie;     
 
@@ -303,7 +214,7 @@ parse_one_cookie (const char **header_p, SoupURI *origin)
        }
 
        /* Parse the VALUE */
-       cookie->value = parse_value (&p, TRUE, TRUE);
+       cookie->value = parse_value (&p);
 
        /* Parse attributes */
        while (*p == ';') {
@@ -316,35 +227,41 @@ parse_one_cookie (const char **header_p, SoupURI *origin)
 #define MATCH_NAME(name) ((end - start == strlen (name)) && !g_ascii_strncasecmp (start, name, end - start))
 
                if (MATCH_NAME ("domain") && has_value) {
-                       cookie->domain = parse_value (&p, FALSE, FALSE);
+                       cookie->domain = parse_value (&p);
+                       if (!*cookie->domain) {
+                               g_free (cookie->domain);
+                               cookie->domain = NULL;
+                       }
                } else if (MATCH_NAME ("expires") && has_value) {
                        cookie->expires = parse_date (&p);
-               } else if (MATCH_NAME ("httponly") && !has_value) {
+               } else if (MATCH_NAME ("httponly")) {
                        cookie->http_only = TRUE;
                } else if (MATCH_NAME ("max-age") && has_value) {
-                       char *max_age = parse_value (&p, FALSE, FALSE);
-                       soup_cookie_set_max_age (cookie, strtoul (max_age, NULL, 10));
-                       g_free (max_age);
+                       char *max_age_str = parse_value (&p), *mae;
+                       long max_age = strtol (max_age_str, &mae, 10);
+                       if (!*mae) {
+                               if (max_age < 0)
+                                       max_age = 0;
+                               soup_cookie_set_max_age (cookie, max_age);
+                       }
+                       g_free (max_age_str);
                } else if (MATCH_NAME ("path") && has_value) {
-                       cookie->path = parse_value (&p, FALSE, FALSE);
-               } else if (MATCH_NAME ("secure") && !has_value) {
+                       cookie->path = parse_value (&p);
+                       if (*cookie->path != '/') {
+                               g_free (cookie->path);
+                               cookie->path = NULL;
+                       }
+               } else if (MATCH_NAME ("secure")) {
                        cookie->secure = TRUE;
                } else {
                        /* Ignore unknown attributes, but we still have
                         * to skip over the value.
                         */
                        if (has_value)
-                               g_free (parse_value (&p, TRUE, FALSE));
+                               g_free (parse_value (&p));
                }
        }
 
-       if (*p == ',') {
-               p = skip_lws (p + 1);
-               if (*p)
-                       *header_p = p;
-       } else
-               *header_p = NULL;
-
        if (cookie->domain) {
                /* Domain must have at least one '.' (not counting an
                 * initial one. (We check this now, rather than
@@ -390,9 +307,11 @@ parse_one_cookie (const char **header_p, SoupURI *origin)
                        char *slash;
 
                        cookie->path = g_strdup (origin->path);
-                       slash = strrchr (cookie->path, '/');
-                       if (slash)
-                               *slash = '\0';
+                       if (strcmp (cookie->path, "/") != 0) {
+                               slash = strrchr (cookie->path, '/');
+                               if (slash)
+                                       *slash = '\0';
+                       }
                }
        }
 
@@ -486,7 +405,7 @@ soup_cookie_new (const char *name, const char *value,
 SoupCookie *
 soup_cookie_parse (const char *cookie, SoupURI *origin)
 {
-       return parse_one_cookie (&cookie, origin);
+       return parse_one_cookie (cookie, origin);
 }
 
 /**
@@ -683,6 +602,9 @@ soup_cookie_set_http_only (SoupCookie *cookie, gboolean http_only)
 static void
 serialize_cookie (SoupCookie *cookie, GString *header, gboolean set_cookie)
 {
+       if (!*cookie->name && !*cookie->value)
+               return;
+
        if (header->len) {
                if (set_cookie)
                        g_string_append (header, ", ");
@@ -690,8 +612,10 @@ serialize_cookie (SoupCookie *cookie, GString *header, gboolean set_cookie)
                        g_string_append (header, "; ");
        }
 
-       g_string_append (header, cookie->name);
-       g_string_append (header, "=");
+       if (set_cookie || *cookie->name) {
+               g_string_append (header, cookie->name);
+               g_string_append (header, "=");
+       }
        g_string_append (header, cookie->value);
        if (!set_cookie)
                return;
@@ -807,21 +731,18 @@ soup_cookies_from_response (SoupMessage *msg)
 
        origin = soup_message_get_uri (msg);
 
-       /* Although parse_one_cookie tries to deal with multiple
-        * comma-separated cookies, it is impossible to do that 100%
-        * reliably, so we try to pass it separate Set-Cookie headers
-        * instead.
+       /* We have to use soup_message_headers_iter rather than
+        * soup_message_headers_get_list() since Set-Cookie isn't
+        * properly mergeable/unmergeable.
         */
        soup_message_headers_iter_init (&iter, msg->response_headers);
        while (soup_message_headers_iter_next (&iter, &name, &value)) {
                if (g_ascii_strcasecmp (name, "Set-Cookie") != 0)
                        continue;
 
-               while (value) {
-                       cookie = parse_one_cookie (&value, origin);
-                       if (cookie)
-                               cookies = g_slist_prepend (cookies, cookie);
-               }
+               cookie = parse_one_cookie (value, origin);
+               if (cookie)
+                       cookies = g_slist_prepend (cookies, cookie);
        }
        return g_slist_reverse (cookies);
 }