Initial HTTP cookie support imported from development git repo,
authorDan Winship <danw@src.gnome.org>
Wed, 9 Apr 2008 02:02:02 +0000 (02:02 +0000)
committerDan Winship <danw@src.gnome.org>
Wed, 9 Apr 2008 02:02:02 +0000 (02:02 +0000)
including patches from Xan Lopez.

TODO: make sure the logic in soup_cookie_jar_get_cookies() is
right. Add a test program to tests/.

* libsoup/soup-cookie.c: Code for parsing and generating HTTP
cookies.

* libsoup/soup-cookie-jar.c: Code for managing SoupCookies and
integrating cookie management with a SoupSession.

* libsoup/soup-date.c (soup_date_is_past): New, checks if a
SoupDate refers to a time in the past

* libsoup/soup-dns.c (soup_dns_is_ip_address): New, checks if a
string is a valid IP address

* libsoup/soup-headers.c (soup_header_parse_semi_param_list): New,
like soup_header_parse_param_list, but for semicolon-delimited
data.

svn path=/trunk/; revision=1135

14 files changed:
ChangeLog
libsoup/Makefile.am
libsoup/soup-cookie-jar.c [new file with mode: 0644]
libsoup/soup-cookie-jar.h [new file with mode: 0644]
libsoup/soup-cookie.c [new file with mode: 0644]
libsoup/soup-cookie.h [new file with mode: 0644]
libsoup/soup-date.c
libsoup/soup-date.h
libsoup/soup-dns.c
libsoup/soup-dns.h
libsoup/soup-headers.c
libsoup/soup-headers.h
libsoup/soup-types.h
libsoup/soup.h

index 5ab21b4..3ef59cb 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,29 @@
 2008-04-08  Dan Winship  <danw@gnome.org>
 
+       Initial HTTP cookie support imported from development git repo,
+       including patches from Xan Lopez.
+
+       TODO: make sure the logic in soup_cookie_jar_get_cookies() is
+       right. Add a test program to tests/.
+
+       * libsoup/soup-cookie.c: Code for parsing and generating HTTP
+       cookies.
+
+       * libsoup/soup-cookie-jar.c: Code for managing SoupCookies and
+       integrating cookie management with a SoupSession.
+
+       * libsoup/soup-date.c (soup_date_is_past): New, checks if a
+       SoupDate refers to a time in the past
+
+       * libsoup/soup-dns.c (soup_dns_is_ip_address): New, checks if a
+       string is a valid IP address
+
+       * libsoup/soup-headers.c (soup_header_parse_semi_param_list): New,
+       like soup_header_parse_param_list, but for semicolon-delimited
+       data.
+
+2008-04-08  Dan Winship  <danw@gnome.org>
+
        * libsoup/soup-auth-manager.c: Make this a GObject and
        specifically a SoupSessionFeature. Add an "authenticate" signal,
        and emit that rather than explicitly calling into the SoupSession
index 1568c15..97f3bf6 100644 (file)
@@ -51,6 +51,8 @@ soup_headers =                        \
        soup-auth-domain.h      \
        soup-auth-domain-basic.h  \
        soup-auth-domain-digest.h \
+       soup-cookie.h           \
+       soup-cookie-jar.h       \
        soup-date.h             \
        soup-form.h             \
        soup-headers.h          \
@@ -109,6 +111,8 @@ libsoup_2_4_la_SOURCES =            \
        soup-auth-manager-ntlm.c        \
        soup-connection.h               \
        soup-connection.c               \
+       soup-cookie.c                   \
+       soup-cookie-jar.c               \
        soup-date.c                     \
        soup-dns.h                      \
        soup-dns.c                      \
diff --git a/libsoup/soup-cookie-jar.c b/libsoup/soup-cookie-jar.c
new file mode 100644 (file)
index 0000000..2503f20
--- /dev/null
@@ -0,0 +1,311 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-cookie-jar.c
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+
+#include "soup-cookie.h"
+#include "soup-cookie-jar.h"
+#include "soup-date.h"
+#include "soup-message.h"
+#include "soup-session-feature.h"
+#include "soup-uri.h"
+
+/**
+ * SECTION:soup-cookie-jar
+ * @short_description: 
+ *
+ * A #SoupCookieJar stores #SoupCookie<!-- -->s and arrange for them
+ * to be sent with the appropriate #SoupMessage<!-- -->s.
+ * #SoupCookieJar implements #SoupSessionFeature, so you can add a
+ * cookie jar to a session with soup_session_add_feature() or
+ * soup_session_add_feature_by_type().
+ *
+ * Note that the base #SoupCookieJar class does not support any form
+ * of long-term cookie persistence.
+ **/
+
+static void soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
+static void request_queued (SoupSessionFeature *feature, SoupSession *session,
+                           SoupMessage *msg);
+static void request_started (SoupSessionFeature *feature, SoupSession *session,
+                            SoupMessage *msg, SoupSocket *socket);
+static void request_unqueued (SoupSessionFeature *feature, SoupSession *session,
+                             SoupMessage *msg);
+
+G_DEFINE_TYPE_WITH_CODE (SoupCookieJar, soup_cookie_jar, G_TYPE_OBJECT,
+                        G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
+                                               soup_cookie_jar_session_feature_init));
+
+typedef struct {
+       GHashTable *domains;
+} SoupCookieJarPrivate;
+#define SOUP_COOKIE_JAR_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_COOKIE_JAR, SoupCookieJarPrivate))
+
+static void
+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,
+                                              g_free, NULL);
+}
+
+static void
+finalize (GObject *object)
+{
+       SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
+       GHashTableIter iter;
+       gpointer key, value;
+
+       g_hash_table_iter_init (&iter, priv->domains);
+       while (g_hash_table_iter_next (&iter, &key, &value))
+               soup_cookies_free (value);
+       g_hash_table_destroy (priv->domains);
+
+       G_OBJECT_CLASS (soup_cookie_jar_parent_class)->finalize (object);
+}
+
+static void
+soup_cookie_jar_class_init (SoupCookieJarClass *jar_class)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (jar_class);
+
+       g_type_class_add_private (jar_class, sizeof (SoupCookieJarPrivate));
+
+       object_class->finalize = finalize;
+}
+
+static void
+soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface,
+                                     gpointer interface_data)
+{
+       feature_interface->request_queued = request_queued;
+       feature_interface->request_started = request_started;
+       feature_interface->request_unqueued = request_unqueued;
+}
+
+/**
+ * soup_cookie_jar_new:
+ *
+ * Creates a new #SoupCookieJar.
+ *
+ * Returns: a new #SoupCookieJar
+ **/
+SoupCookieJar *
+soup_cookie_jar_new (void) 
+{
+       return g_object_new (SOUP_TYPE_COOKIE_JAR, NULL);
+}
+
+/**
+ * soup_cookie_jar_save:
+ * @jar: a SoupCookieJar
+ *
+ * Tells @jar to save the state of its (non-session) cookies to some
+ * sort of permanent storage.
+ **/
+void
+soup_cookie_jar_save (SoupCookieJar *jar)
+{
+       if (SOUP_COOKIE_JAR_GET_CLASS (jar)->save)
+               SOUP_COOKIE_JAR_GET_CLASS (jar)->save (jar);
+}
+
+/**
+ * soup_cookie_jar_get_cookies:
+ * @jar: a #SoupCookieJar
+ * @uri: a #SoupURI
+ * @for_http: whether or not the return value is being passed directly
+ * to an HTTP operation
+ *
+ * Retrieves (in Cookie-header form) the list of cookies that would
+ * be sent with a request to @uri.
+ *
+ * If @for_http is %TRUE, the return value will include cookies marked
+ * "HttpOnly" (that is, cookies that the server wishes to keep hidden
+ * from client-side scripting operations such as the JavaScript
+ * document.cookies property). Since #SoupCookieJar sets the Cookie
+ * header itself when making the actual HTTP request, you should
+ * almost certainly be setting @for_http to %FALSE if you are calling
+ * this.
+ *
+ * Return value: the cookies, in string form, or %NULL if there are no
+ * cookies for @uri.
+ **/
+char *
+soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri,
+                            gboolean for_http)
+{
+       SoupCookieJarPrivate *priv;
+       GSList *cookies, *domain_cookies;
+       char *domain, *cur, *next, *result;
+
+       g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
+       priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
+
+       /* The logic here is a little weird, but the plan is that if
+        * uri->host is "www.foo.com", we will end up looking up
+        * cookies for ".www.foo.com", "www.foo.com", ".foo.com", and
+        * ".com", in that order. (Logic stolen from Mozilla.)
+        */
+       cookies = NULL;
+       domain = cur = g_strdup_printf (".%s", uri->host);
+       next = domain + 1;
+       do {
+               domain_cookies = g_hash_table_lookup (priv->domains, cur);
+               while (domain_cookies) {
+                       SoupCookie *cookie = domain_cookies->data;
+
+                       if (soup_cookie_applies_to_uri (cookie, uri) &&
+                           (for_http || !cookie->http_only))
+                               cookies = g_slist_append (cookies, cookie);
+                       domain_cookies = domain_cookies->next;
+               }
+               cur = next;
+               if (cur)
+                       next = strchr (cur + 1, '.');
+       } while (cur);
+       g_free (domain);
+
+       if (cookies) {
+               /* FIXME: sort? */
+               result = soup_cookies_to_cookie_header (cookies);
+               g_slist_free (cookies);
+               return result;
+       } else
+               return NULL;
+}
+
+static GSList *
+get_cookies_for_domain (SoupCookieJar *jar, const char *domain)
+{
+       SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
+       GSList *cookies, *orig_cookies, *c;
+       SoupCookie *cookie;
+
+       cookies = g_hash_table_lookup (priv->domains, domain);
+       c = orig_cookies = cookies;
+       while (c) {
+               cookie = c->data;
+               c = c->next;
+               if (cookie->expires && soup_date_is_past (cookie->expires)) {
+                       cookies = g_slist_remove (cookies, cookie);
+                       soup_cookie_free (cookie);
+               }
+       }
+
+       if (cookies != orig_cookies)
+               g_hash_table_insert (priv->domains, g_strdup (domain), cookies);
+       return cookies;
+}
+
+static void
+set_cookie (SoupCookieJar *jar, SoupCookie *cookie)
+{
+       SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
+       GSList *old_cookies, *oc, *prev = NULL;
+       SoupCookie *old_cookie;
+
+       old_cookies = get_cookies_for_domain (jar, cookie->domain);
+       for (oc = old_cookies; oc; oc = oc->next) {
+               old_cookie = oc->data;
+               if (!strcmp (cookie->name, old_cookie->name)) {
+                       /* The new cookie is a replacement for an old
+                        * cookie. It might be pre-expired, but we
+                        * don't worry about that here;
+                        * get_cookies_for_domain() will delete it
+                        * later.
+                        */
+                       soup_cookie_free (old_cookie);
+                       oc->data = cookie;
+                       return;
+               }
+               prev = oc;
+       }
+
+       /* The new cookie is... a new cookie */
+       if (cookie->expires && soup_date_is_past (cookie->expires))
+               soup_cookie_free (cookie);
+       else if (prev)
+               prev = g_slist_append (prev, cookie);
+       else {
+               old_cookies = g_slist_append (NULL, cookie);
+               g_hash_table_insert (priv->domains, g_strdup (cookie->domain),
+                                    old_cookies);
+       }
+}
+
+/**
+ * soup_cookie_jar_set_cookie:
+ * @jar: a #SoupCookieJar
+ * @uri: the URI setting the cookie
+ * @cookie: the stringified cookie to set
+ *
+ * Adds @cookie to @jar, exactly as though it had appeared in a
+ * Set-Cookie header returned from a request to @uri.
+ **/
+void
+soup_cookie_jar_set_cookie (SoupCookieJar *jar, SoupURI *uri,
+                           const char *cookie)
+{
+       SoupCookie *soup_cookie;
+
+       g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
+       g_return_if_fail (cookie != NULL);
+
+       soup_cookie = soup_cookie_parse (cookie, uri);
+       set_cookie (jar, soup_cookie);
+       /* set_cookie will steal or free soup_cookie */
+}
+
+static void
+process_set_cookie_header (SoupMessage *msg, gpointer user_data)
+{
+       SoupCookieJar *jar = user_data;
+       GSList *new_cookies, *nc;
+
+       new_cookies = soup_cookies_from_response (msg);
+       for (nc = new_cookies; nc; nc = nc->next)
+               set_cookie (jar, nc->data);
+       g_slist_free (new_cookies);
+}
+
+static void
+request_queued (SoupSessionFeature *feature, SoupSession *session,
+               SoupMessage *msg)
+{
+       soup_message_add_header_handler (msg, "got-headers",
+                                        "Set-Cookie",
+                                        G_CALLBACK (process_set_cookie_header),
+                                        feature);
+}
+
+static void
+request_started (SoupSessionFeature *feature, SoupSession *session,
+                SoupMessage *msg, SoupSocket *socket)
+{
+       SoupCookieJar *jar = SOUP_COOKIE_JAR (feature);
+       char *cookies;
+
+       cookies = soup_cookie_jar_get_cookies (jar, soup_message_get_uri (msg), TRUE);
+       soup_message_headers_replace (msg->request_headers,
+                                     "Cookie", cookies);
+       g_free (cookies);
+}
+
+static void
+request_unqueued (SoupSessionFeature *feature, SoupSession *session,
+                 SoupMessage *msg)
+{
+       g_signal_handlers_disconnect_by_func (msg, process_set_cookie_header, feature);
+}
+
diff --git a/libsoup/soup-cookie-jar.h b/libsoup/soup-cookie-jar.h
new file mode 100644 (file)
index 0000000..03e99f7
--- /dev/null
@@ -0,0 +1,48 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2008 Red Hat, Inc.
+ */
+
+#ifndef SOUP_COOKIE_JAR_H
+#define SOUP_COOKIE_JAR_H 1
+
+#include <libsoup/soup-types.h>
+
+#define SOUP_TYPE_COOKIE_JAR            (soup_cookie_jar_get_type ())
+#define SOUP_COOKIE_JAR(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_COOKIE_JAR, SoupCookieJar))
+#define SOUP_COOKIE_JAR_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_COOKIE_JAR, SoupCookieJarClass))
+#define SOUP_IS_COOKIE_JAR(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_COOKIE_JAR))
+#define SOUP_IS_COOKIE_JAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_COOKIE_JAR))
+#define SOUP_COOKIE_JAR_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_COOKIE_JAR, SoupCookieJarClass))
+
+typedef struct {
+       GObject parent;
+
+} SoupCookieJar;
+
+typedef struct {
+       GObjectClass parent_class;
+
+       void (*save) (SoupCookieJar *jar);
+
+       /* Padding for future expansion */
+       void (*_libsoup_reserved1) (void);
+       void (*_libsoup_reserved2) (void);
+       void (*_libsoup_reserved3) (void);
+       void (*_libsoup_reserved4) (void);
+} SoupCookieJarClass;
+
+GType          soup_cookie_jar_get_type    (void);
+
+SoupCookieJar *soup_cookie_jar_new         (void);
+
+void           soup_cookie_jar_save        (SoupCookieJar *jar);
+
+char          *soup_cookie_jar_get_cookies (SoupCookieJar *jar,
+                                           SoupURI       *uri,
+                                           gboolean       for_http);
+void           soup_cookie_jar_set_cookie  (SoupCookieJar *jar,
+                                           SoupURI       *uri,
+                                           const char    *cookie);
+
+#endif /* SOUP_COOKIE_JAR_H */
diff --git a/libsoup/soup-cookie.c b/libsoup/soup-cookie.c
new file mode 100644 (file)
index 0000000..f29ab88
--- /dev/null
@@ -0,0 +1,856 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-cookie.c
+ *
+ * Copyright (C) 2007 Red Hat, Inc.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "soup-cookie.h"
+#include "soup-date.h"
+#include "soup-dns.h"
+#include "soup-headers.h"
+#include "soup-message.h"
+#include "soup-message-headers.h"
+#include "soup-uri.h"
+
+/**
+ * SECTION:soup-cookie
+ * @short_description: HTTP Cookies
+ * @see_also: #SoupMessage
+ *
+ * #SoupCookie implements HTTP cookies, primarily as described by
+ * <ulink
+ * url="http://wp.netscape.com/newsref/std/cookie_spec.html">the
+ * original Netscape cookie specification</ulink>, but with slight
+ * modifications based on <ulink
+ * url="http://www.ietf.org/rfc/rfc2109.txt">RFC 2109</ulink>, <ulink
+ * url="http://msdn2.microsoft.com/en-us/library/ms533046.aspx">Microsoft's
+ * HttpOnly extension attribute</ulink>, and observed real-world usage
+ * (and, in particular, based on what Firefox does).
+ **/
+
+/**
+ * SoupCookie:
+ * @name: the cookie name
+ * @value: the cookie value
+ * @domain: the "domain" attribute, or %NULL
+ * @path: the "path" attribute, or %NULL
+ * @expires: the cookie expiration time, or %NULL for a session cookie
+ * @secure: %TRUE if the cookie should only be tranferred over SSL
+ * @http_only: %TRUE if the cookie should not be exposed to scripts
+ *
+ * An HTTP cookie.
+ *
+ * @name and @value will be set for all cookies. If the cookie is
+ * generated from a string that appears to have no name, then @name
+ * will be the empty string.
+ *
+ * @domain and @path give the host or domain, and path within that
+ * host/domain, to restrict this cookie to. If @domain starts with
+ * ".", that indicates a domain (which matches the string after the
+ * ".", or any hostname that has @domain as a suffix). Otherwise, it
+ * is a hostname and must match exactly.
+ *
+ * @expires will be non-%NULL if the cookie uses either the original
+ * "expires" attribute, or the "max-age" attribute specified in RFC
+ * 2109. If @expires is %NULL, it indicates that neither "expires" nor
+ * "max-age" was specified, and the cookie expires at the end of the
+ * session.
+ * 
+ * If @http_only is set, the cookie should not be exposed to untrusted
+ * code (eg, javascript), so as to minimize the danger posed by
+ * cross-site scripting attacks.
+ **/
+
+/* 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 ";".
+ */
+
+static gboolean
+domain_matches (const char *domain, const char *host)
+{
+       char *match;
+       int dlen;
+
+       if (!g_ascii_strcasecmp (domain, host))
+               return TRUE;
+       if (*domain != '.')
+               return FALSE;
+       dlen = strlen (domain);
+       while ((match = strstr (host, domain))) {
+               if (!match[dlen])
+                       return TRUE;
+               host = match + 1;
+       }
+       return FALSE;
+}
+
+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, allow_comma) ((ch) < ' ' || (ch) == ';' || (!(allow_comma) && (ch) == ','))
+
+static char *
+parse_value (const char **val_p, gboolean keep_quotes, gboolean allow_comma)
+{
+       const char *start, *end, *p;
+       char *value, *q;
+
+       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);
+       }
+
+       *val_p = p;
+       return value;
+}
+
+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);
+       }
+
+       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)
+{
+       const char *header = *header_p, *p;
+       const char *start, *end;
+       gboolean has_value;
+       SoupCookie *cookie;     
+
+       cookie = g_slice_new0 (SoupCookie);
+
+       /* Parse the NAME */
+       start = skip_lws (header);
+       for (p = start; !is_attr_ender (*p); p++)
+               ;
+       if (*p == '=') {
+               end = unskip_lws (p, start);
+               cookie->name = g_strndup (start, end - start);
+       } else {
+               /* No NAME; Set cookie->name to "" and then rewind to
+                * re-parse the string as a VALUE.
+                */
+               cookie->name = g_strdup ("");
+               p = start;
+       }
+
+       /* Parse the VALUE */
+       cookie->value = parse_value (&p, TRUE, TRUE);
+
+       /* Parse attributes */
+       while (*p == ';') {
+               start = skip_lws (p + 1);
+               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 ("domain") && has_value) {
+                       cookie->domain = parse_value (&p, FALSE, FALSE);
+               } else if (MATCH_NAME ("expires") && has_value) {
+                       cookie->expires = parse_date (&p);
+               } else if (MATCH_NAME ("httponly") && !has_value) {
+                       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);
+               } else if (MATCH_NAME ("path") && has_value) {
+                       cookie->path = parse_value (&p, FALSE, FALSE);
+               } else if (MATCH_NAME ("secure") && !has_value) {
+                       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));
+               }
+       }
+
+       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
+                * bailing out sooner, because we don't want to force
+                * any cookies after this one in the Set-Cookie header
+                * to be discarded.)
+                */
+               if (!strchr (cookie->domain + 1, '.')) {
+                       soup_cookie_free (cookie);
+                       return NULL;
+               }
+
+               /* If the domain string isn't an IP addr, and doesn't
+                * start with a '.', prepend one.
+                */
+               if (!soup_dns_is_ip_address (cookie->domain) &&
+                   cookie->domain[0] != '.') {
+                       char *tmp = g_strdup_printf (".%s", cookie->domain);
+                       g_free (cookie->domain);
+                       cookie->domain = tmp;
+               }
+       }
+
+       if (origin) {
+               /* Sanity-check domain */
+               if (cookie->domain) {
+                       if (!domain_matches (cookie->domain, origin->host)) {
+                               soup_cookie_free (cookie);
+                               return NULL;
+                       }
+               } else
+                       cookie->domain = g_strdup (origin->host);
+
+               /* The original cookie spec didn't say that pages
+                * could only set cookies for paths they were under.
+                * RFC 2109 adds that requirement, but some sites
+                * depend on the old behavior
+                * (https://bugzilla.mozilla.org/show_bug.cgi?id=156725#c20).
+                * So we don't check the path.
+                */
+
+               if (!cookie->path) {
+                       char *slash;
+
+                       cookie->path = g_strdup (origin->path);
+                       slash = strrchr (cookie->path, '/');
+                       if (slash)
+                               *slash = '\0';
+               }
+       }
+
+       return cookie;
+}
+
+/**
+ * soup_cookie_new:
+ * @name: cookie name
+ * @value: cookie value
+ * @domain: cookie domain, or %NULL
+ * @path: cookie path, or %NULL
+ * @max_age: max age of the cookie, or -1 for a session cookie
+ *
+ * Creates a new #SoupCookie with the given attributes. (Use
+ * soup_cookie_set_secure() and soup_cookie_set_http_only() if you
+ * need to set those attributes on the returned cookie.)
+ *
+ * @max_age is used to set the "expires" attribute on the cookie; pass
+ * -1 to not include the attribute (indicating that the cookie expires
+ * with the current session), 0 for an already-expired cookie, or a
+ * lifetime in seconds. You can use the constants
+ * %SOUP_COOKIE_MAX_AGE_ONE_HOUR, %SOUP_COOKIE_MAX_AGE_ONE_DAY,
+ * %SOUP_COOKIE_MAX_AGE_ONE_WEEK and %SOUP_COOKIE_MAX_AGE_ONE_YEAR (or
+ * multiples thereof) to calculate this value. (If you really care
+ * about setting the exact time that the cookie will expire, use
+ * soup_cookie_set_expires().)
+ *
+ * Return value: a new #SoupCookie.
+ **/
+SoupCookie *
+soup_cookie_new (const char *name, const char *value,
+                const char *domain, const char *path,
+                int max_age)
+{
+       SoupCookie *cookie;     
+
+       cookie = g_slice_new0 (SoupCookie);
+       cookie->name = g_strdup (name);
+       cookie->value = g_strdup (value);
+       cookie->domain = g_strdup (domain);
+       cookie->path = g_strdup (path);
+       soup_cookie_set_max_age (cookie, max_age);
+
+       return cookie;
+}
+
+/**
+ * soup_cookie_parse:
+ * @cookie: a cookie string (eg, the value of a Set-Cookie header)
+ * @origin: origin of the cookie, or %NULL
+ *
+ * Parses @cookie and returns a #SoupCookie. (If @cookie contains
+ * multiple cookies, only the first one will be parsed.)
+ *
+ * If @cookie does not have "path" or "domain" attributes, they will
+ * be defaulted from @origin. If @origin is %NULL, path will default
+ * to "/", but domain will be left as %NULL. Note that this is not a
+ * valid state for a #SoupCookie, and you will need to fill in some
+ * appropriate string for the domain if you want to actually make use
+ * of the cookie.
+ *
+ * Return value: a new #SoupCookie, or %NULL if it could not be
+ * parsed, or contained an illegal "domain" attribute for a cookie
+ * originating from @origin.
+ **/
+SoupCookie *
+soup_cookie_parse (const char *cookie, SoupURI *origin)
+{
+       return parse_one_cookie (&cookie, origin);
+}
+
+/**
+ * soup_cookie_set_name:
+ * @cookie: a #SoupCookie
+ * @name: the new name
+ *
+ * Sets @cookie's name to @name
+ **/
+void
+soup_cookie_set_name (SoupCookie *cookie, const char *name)
+{
+       g_free (cookie->name);
+       cookie->name = g_strdup (name);
+}
+
+/**
+ * soup_cookie_set_value:
+ * @cookie: a #SoupCookie
+ * @value: the new value
+ *
+ * Sets @cookie's value to @value
+ **/
+void
+soup_cookie_set_value (SoupCookie *cookie, const char *value)
+{
+       g_free (cookie->value);
+       cookie->value = g_strdup (value);
+}
+
+/**
+ * soup_cookie_set_domain:
+ * @cookie: a #SoupCookie
+ * @domain: the new domain
+ *
+ * Sets @cookie's domain to @domain
+ **/
+void
+soup_cookie_set_domain (SoupCookie *cookie, const char *domain)
+{
+       g_free (cookie->domain);
+       cookie->domain = g_strdup (domain);
+}
+
+/**
+ * soup_cookie_set_path:
+ * @cookie: a #SoupCookie
+ * @path: the new path
+ *
+ * Sets @cookie's path to @path
+ **/
+void
+soup_cookie_set_path (SoupCookie *cookie, const char *path)
+{
+       g_free (cookie->path);
+       cookie->path = g_strdup (path);
+}
+
+/**
+ * soup_cookie_set_max_age:
+ * @cookie: a #SoupCookie
+ * @max_age: the new max age
+ *
+ * Sets @cookie's max age to @max_age. If @max_age is -1, the cookie
+ * is a session cookie, and will expire at the end of the client's
+ * session. Otherwise, it is the number of seconds until the cookie
+ * expires. You can use the constants %SOUP_COOKIE_MAX_AGE_ONE_HOUR,
+ * %SOUP_COOKIE_MAX_AGE_ONE_DAY, %SOUP_COOKIE_MAX_AGE_ONE_WEEK and
+ * %SOUP_COOKIE_MAX_AGE_ONE_YEAR (or multiples thereof) to calculate
+ * this value. (A value of 0 indicates that the cookie should be
+ * considered already-expired.)
+ *
+ * (This sets the same property as soup_cookie_set_expires().)
+ **/
+void
+soup_cookie_set_max_age (SoupCookie *cookie, int max_age)
+{
+       if (cookie->expires)
+               soup_date_free (cookie->expires);
+
+       if (max_age == -1)
+               cookie->expires = NULL;
+       else if (max_age == 0) {
+               /* Use a date way in the past, to protect against
+                * clock skew.
+                */
+               cookie->expires = soup_date_new (1970, 1, 1, 0, 0, 0);
+       } else
+               cookie->expires = soup_date_new_from_now (max_age);
+}
+
+/**
+ * soup_cookie_set_expires:
+ * @cookie: a #SoupCookie
+ * @expires: the new expiration time, or %NULL
+ *
+ * Sets @cookie's expiration time to @expires. If @expires is %NULL,
+ * @cookie will be a session cookie and will expire at the end of the
+ * client's session.
+ *
+ * (This sets the same property as soup_cookie_set_max_age().)
+ **/
+void
+soup_cookie_set_expires (SoupCookie *cookie, SoupDate *expires)
+{
+       if (cookie->expires)
+               soup_date_free (cookie->expires);
+
+       if (expires)
+               cookie->expires = soup_date_copy (expires);
+       else
+               cookie->expires = NULL;
+}
+
+/**
+ * soup_cookie_set_secure:
+ * @cookie: a #SoupCookie
+ * @secure: the new value for the secure attribute
+ *
+ * Sets @cookie's secure attribute to @secure. If %TRUE, @cookie will
+ * only be transmitted from the client to the server over secure
+ * (https) connections.
+ **/
+void
+soup_cookie_set_secure (SoupCookie *cookie, gboolean secure)
+{
+       cookie->secure = secure;
+}
+
+/**
+ * soup_cookie_set_http_only:
+ * @cookie: a #SoupCookie
+ * @http_only: the new value for the HttpOnly attribute
+ *
+ * Sets @cookie's HttpOnly attribute to @http_only. If %TRUE, @cookie
+ * will be marked as "http only", meaning it should not be exposed to
+ * web page scripts or other untrusted code.
+ **/
+void
+soup_cookie_set_http_only (SoupCookie *cookie, gboolean http_only)
+{
+       cookie->http_only = http_only;
+}
+
+static void
+serialize_cookie (SoupCookie *cookie, GString *header, gboolean set_cookie)
+{
+       if (header->len) {
+               if (set_cookie)
+                       g_string_append (header, ", ");
+               else
+                       g_string_append (header, "; ");
+       }
+
+       g_string_append (header, cookie->name);
+       g_string_append (header, "=");
+       g_string_append (header, cookie->value);
+       if (!set_cookie)
+               return;
+
+       if (cookie->expires) {
+               char *timestamp;
+
+               g_string_append (header, "; expires=");
+               timestamp = soup_date_to_string (cookie->expires,
+                                                SOUP_DATE_COOKIE);
+               g_string_append (header, timestamp);
+               g_free (timestamp);
+       }
+       if (cookie->path) {
+               g_string_append (header, "; path=");
+               g_string_append (header, cookie->path);
+       }
+       if (cookie->domain) {
+               g_string_append (header, "; domain=");
+               g_string_append (header, cookie->domain);
+       }
+       if (cookie->secure)
+               g_string_append (header, "; secure");
+       if (cookie->secure)
+               g_string_append (header, "; HttpOnly");
+}
+
+/**
+ * soup_cookie_to_set_cookie_header:
+ * @cookie: a #SoupCookie
+ *
+ * Serializes @cookie in the format used by the Set-Cookie header
+ * (ie, for sending a cookie from a #SoupServer to a client).
+ *
+ * Return value: the header
+ **/
+char *
+soup_cookie_to_set_cookie_header (SoupCookie *cookie)
+{
+       GString *header = g_string_new (NULL);
+
+       serialize_cookie (cookie, header, TRUE);
+       return g_string_free (header, FALSE);
+}
+
+/**
+ * soup_cookie_to_cookie_header:
+ * @cookie: a #SoupCookie
+ *
+ * Serializes @cookie in the format used by the Cookie header (ie, for
+ * returning a cookie from a #SoupSession to a server).
+ *
+ * Return value: the header
+ **/
+char *
+soup_cookie_to_cookie_header (SoupCookie *cookie)
+{
+       GString *header = g_string_new (NULL);
+
+       serialize_cookie (cookie, header, FALSE);
+       return g_string_free (header, FALSE);
+}
+
+/**
+ * soup_cookie_free:
+ * @cookie: a #SoupCookie
+ *
+ * Frees @cookie
+ **/
+void
+soup_cookie_free (SoupCookie *cookie)
+{
+       g_return_if_fail (cookie != NULL);
+
+       g_free (cookie->name);
+       g_free (cookie->value);
+       g_free (cookie->domain);
+       g_free (cookie->path);
+
+       g_slice_free (SoupCookie, cookie);
+}
+
+/**
+ * soup_cookies_from_response:
+ * @msg: a #SoupMessage containing a "Set-Cookie" response header
+ *
+ * Parses @msg's Set-Cookie response headers and returns a #GSList of
+ * #SoupCookie<!-- -->s. Cookies that do not specify "path" or
+ * "domain" attributes will have their values defaulted from @origin.
+ *
+ * Return value: a #GSList of #SoupCookie<!-- -->s, which can be freed
+ * with soup_cookies_free().
+ **/
+GSList *
+soup_cookies_from_response (SoupMessage *msg)
+{
+       SoupURI *origin;
+       const char *name, *value;
+       SoupCookie *cookie;
+       GSList *cookies = NULL;
+       SoupMessageHeadersIter iter;
+
+       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.
+        */
+       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);
+               }
+       }
+       return g_slist_reverse (cookies);
+}
+
+/**
+ * soup_cookies_from_request:
+ * @msg: a #SoupMessage containing a "Cookie" request header
+ *
+ * Parses @msg's Cookie request header and returns a #GSList of
+ * #SoupCookie<!-- -->s. As the "Cookie" header, unlike "Set-Cookie",
+ * only contains cookie names and values, none of the other
+ * #SoupCookie fields will be filled in. (Thus, you can't generally
+ * pass a cookie returned from this method directly to
+ * soup_cookies_to_response().)
+ *
+ * Return value: a #GSList of #SoupCookie<!-- -->s, which can be freed
+ * with soup_cookies_free().
+ **/
+GSList *
+soup_cookies_from_request (SoupMessage *msg)
+{
+       SoupCookie *cookie;
+       GSList *cookies = NULL;
+       GHashTable *params;
+       GHashTableIter iter;
+       gpointer name, value;
+
+       params = soup_header_parse_semi_param_list (soup_message_headers_get (msg->request_headers, "Cookie"));
+       g_hash_table_iter_init (&iter, params);
+       while (g_hash_table_iter_next (&iter, &name, &value)) {
+               cookie = soup_cookie_new (name, value, NULL, NULL, 0);
+               cookies = g_slist_prepend (cookies, cookie);
+       }
+       soup_header_free_param_list (params);
+
+       return g_slist_reverse (cookies);
+}
+
+/**
+ * soup_cookies_to_response:
+ * @cookies: a #GSList of #SoupCookie
+ * @msg: a #SoupMessage
+ *
+ * Appends a "Set-Cookie" response header to @msg for each cookie in
+ * @cookies. (This is in addition to any other "Set-Cookie" headers
+ * @msg may already have.)
+ **/
+void
+soup_cookies_to_response (GSList *cookies, SoupMessage *msg)
+{
+       GString *header;
+
+       header = g_string_new (NULL);
+       while (cookies) {
+               serialize_cookie (cookies->data, header, TRUE);
+               soup_message_headers_append (msg->response_headers,
+                                            "Set-Cookie", header->str);
+               g_string_truncate (header, 0);
+               cookies = cookies->next;
+       }
+       g_string_free (header, TRUE);
+}
+
+/**
+ * soup_cookies_to_request:
+ * @cookies: a #GSList of #SoupCookie
+ * @msg: a #SoupMessage
+ *
+ * Adds the name and value of each cookie in @cookies to @msg's
+ * "Cookie" request. (If @msg already has a "Cookie" request header,
+ * these cookies will be appended to the cookies already present. Be
+ * careful that you do not append the same cookies twice, eg, when
+ * requeuing a message.)
+ **/
+void
+soup_cookies_to_request (GSList *cookies, SoupMessage *msg)
+{
+       GString *header;
+
+       header = g_string_new (soup_message_headers_get (msg->request_headers,
+                                                        "Cookie"));
+       while (cookies) {
+               serialize_cookie (cookies->data, header, FALSE);
+               cookies = cookies->next;
+       }
+       soup_message_headers_replace (msg->request_headers,
+                                     "Cookie", header->str);
+       g_string_free (header, TRUE);
+}
+
+/**
+ * soup_cookies_free:
+ * @cookies: a #GSList of #SoupCookie
+ *
+ * Frees @cookies.
+ **/
+void
+soup_cookies_free (GSList *cookies)
+{
+       GSList *c;
+
+       for (c = cookies; c; c = c->next)
+               soup_cookie_free (c->data);
+       g_slist_free (cookies);
+}
+
+/**
+ * soup_cookies_to_cookie_header:
+ * @cookies: a #GSList of #SoupCookie
+ *
+ * Serializes a #GSList of #SoupCookie into a string suitable for
+ * setting as the value of the "Cookie" header.
+ *
+ * Return value: the serialization of @cookies
+ **/
+char *
+soup_cookies_to_cookie_header (GSList *cookies)
+{
+       GString *str;
+
+       g_return_val_if_fail (cookies != NULL, NULL);
+
+       str = g_string_new (NULL);
+       while (cookies) {
+               serialize_cookie (cookies->data, str, FALSE);
+               cookies = cookies->next;
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+/**
+ * soup_cookie_applies_to_uri:
+ * @cookie: a #SoupCookie
+ * @uri: a #SoupURI
+ *
+ * Tests if @cookie should be sent to @uri.
+ *
+ * (At the moment, this does not check that @cookie's domain matches
+ * @uri, because it assumes that the caller has already done that.
+ * But don't rely on that; it may change in the future.)
+ *
+ * Return value: %TRUE if @cookie should be sent to @uri, %FALSE if
+ * not
+ **/
+gboolean
+soup_cookie_applies_to_uri (SoupCookie *cookie, SoupURI *uri)
+{
+       int plen;
+
+       if (cookie->secure && uri->scheme != SOUP_URI_SCHEME_HTTPS)
+               return FALSE;
+
+       if (cookie->expires && soup_date_is_past (cookie->expires))
+               return FALSE;
+
+       /* The spec claims "/foo would match /foobar", but fortunately
+        * no one is really that crazy.
+        */
+       plen = strlen (cookie->path);
+       if (strncmp (cookie->path, uri->path, plen) != 0)
+               return FALSE;
+       if (uri->path[plen] && uri->path[plen] != '/')
+               return FALSE;
+
+       return TRUE;
+}
diff --git a/libsoup/soup-cookie.h b/libsoup/soup-cookie.h
new file mode 100644 (file)
index 0000000..84b4602
--- /dev/null
@@ -0,0 +1,75 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* 
+ * Copyright 2007, 2008 Red Hat, Inc.
+ */
+
+#ifndef SOUP_COOKIE_H
+#define SOUP_COOKIE_H 1
+
+#include <libsoup/soup-types.h>
+
+G_BEGIN_DECLS
+
+struct _SoupCookie {
+       char     *name;
+       char     *value;
+       char     *domain;
+       char     *path;
+       SoupDate *expires;
+       gboolean  secure;
+       gboolean  http_only;
+};
+
+#define SOUP_COOKIE_MAX_AGE_ONE_HOUR (60 * 60)
+#define SOUP_COOKIE_MAX_AGE_ONE_DAY  (SOUP_COOKIE_MAX_AGE_ONE_HOUR * 24)
+#define SOUP_COOKIE_MAX_AGE_ONE_WEEK (SOUP_COOKIE_MAX_AGE_ONE_DAY * 7)
+#define SOUP_COOKIE_MAX_AGE_ONE_YEAR (SOUP_COOKIE_MAX_AGE_ONE_DAY * 365.2422)
+
+SoupCookie *soup_cookie_new                     (const char  *name,
+                                                const char  *value,
+                                                const char  *domain,
+                                                const char  *path,
+                                                int          max_age);
+SoupCookie *soup_cookie_parse                   (const char  *header,
+                                                SoupURI     *origin);
+
+void        soup_cookie_set_name                (SoupCookie  *cookie,
+                                                const char  *name);
+void        soup_cookie_set_value               (SoupCookie  *cookie,
+                                                const char  *value);
+void        soup_cookie_set_domain              (SoupCookie  *cookie,
+                                                const char  *domain);
+void        soup_cookie_set_path                (SoupCookie  *cookie,
+                                                const char  *path);
+void        soup_cookie_set_max_age             (SoupCookie  *cookie,
+                                                int          max_age);
+void        soup_cookie_set_expires             (SoupCookie  *cookie,
+                                                SoupDate    *expires);
+void        soup_cookie_set_secure              (SoupCookie  *cookie,
+                                                gboolean     secure);
+void        soup_cookie_set_http_only           (SoupCookie  *cookie,
+                                                gboolean     secure);
+
+char       *soup_cookie_to_set_cookie_header    (SoupCookie  *cookie);
+char       *soup_cookie_to_cookie_header        (SoupCookie  *cookie);
+
+void        soup_cookie_free                    (SoupCookie  *cookie);
+
+GSList     *soup_cookies_from_response          (SoupMessage *msg);
+GSList     *soup_cookies_from_request           (SoupMessage *msg);
+
+void        soup_cookies_to_response            (GSList      *cookies,
+                                                SoupMessage *msg);
+void        soup_cookies_to_request             (GSList      *cookies,
+                                                SoupMessage *msg);
+
+void        soup_cookies_free                   (GSList      *cookies);
+
+char       *soup_cookies_to_cookie_header       (GSList      *cookies);
+
+gboolean    soup_cookie_applies_to_uri          (SoupCookie  *cookie,
+                                                SoupURI     *uri);
+
+G_END_DECLS
+
+#endif /* SOUP_COOKIE_H */
index 49a0350..4aa3c3b 100644 (file)
@@ -566,6 +566,24 @@ soup_date_to_time_t (SoupDate *date)
 }
 
 /**
+ * soup_date_is_past:
+ * @date: a #SoupDate
+ *
+ * Determines if @date is in the past.
+ *
+ * Return value: %TRUE if @date is in the past
+ **/
+gboolean
+soup_date_is_past (SoupDate *date)
+{
+       /* optimization */
+       if (date->year < 2008)
+               return TRUE;
+
+       return soup_date_to_time_t (date) < time (NULL);
+}
+
+/**
  * soup_date_copy:
  * @date: a #SoupDate
  *
index a079e8e..043cd0a 100644 (file)
@@ -12,7 +12,7 @@
 
 G_BEGIN_DECLS
 
-typedef struct {
+struct _SoupDate {
        int      year;
        int      month;
        int      day;
@@ -23,7 +23,7 @@ typedef struct {
 
        gboolean utc;
        int      offset;
-} SoupDate;
+};
 
 typedef enum {
        SOUP_DATE_HTTP = 1,
@@ -52,6 +52,8 @@ char     *soup_date_to_string       (SoupDate       *date,
                                     SoupDateFormat  format);
 time_t    soup_date_to_time_t       (SoupDate       *date);
 
+gboolean  soup_date_is_past         (SoupDate       *date);
+
 SoupDate *soup_date_copy            (SoupDate       *date);
 void      soup_date_free            (SoupDate       *date);
 
index 34fc399..94c76e7 100644 (file)
@@ -303,6 +303,31 @@ soup_dns_ntop (struct sockaddr *sa)
        }
 }
 
+gboolean
+soup_dns_is_ip_address (const char *name)
+{
+       struct sockaddr_in sin;
+#ifdef HAVE_IPV6
+       struct sockaddr_in6 sin6;
+
+       if (inet_pton (AF_INET, name, &sin.sin_addr) > 0 ||
+           inet_pton (AF_INET6, name, &sin6.sin6_addr) > 0)
+               return TRUE;
+#else /* !HAVE_IPV6 */
+#if defined(HAVE_INET_PTON)
+       if (inet_pton (AF_INET, name, &sin.sin_addr) > 0)
+               return TRUE;
+#elif defined(HAVE_INET_ATON)
+       if (inet_aton (name, &sin.sin_addr) != 0)
+               return TRUE;
+#else
+       if (inet_addr (entry->entry_name) != INADDR_NONE)
+               return TRUE;
+#endif
+#endif /* HAVE_IPV6 */
+       return FALSE;
+}
+
 static void
 resolve_address (SoupDNSCacheEntry *entry)
 {
index 6519a9c..5e5584a 100644 (file)
@@ -14,6 +14,7 @@
 
 void             soup_dns_init                 (void);
 char            *soup_dns_ntop                 (struct sockaddr *sa);
+gboolean         soup_dns_is_ip_address        (const char *name);
 
 typedef struct SoupDNSLookup SoupDNSLookup;
 
index b132003..662404b 100644 (file)
@@ -367,23 +367,23 @@ unskip_lws (const char *s, const char *start)
 }
 
 static const char *
-skip_commas (const char *s)
+skip_delims (const char *s, char delim)
 {
-       /* The grammar allows for multiple commas */
-       while (g_ascii_isspace (*s) || *s == ',')
+       /* The grammar allows for multiple delimiters */
+       while (g_ascii_isspace (*s) || *s == delim)
                s++;
        return s;
 }
 
 static const char *
-skip_item (const char *s)
+skip_item (const char *s, char delim)
 {
        gboolean quoted = FALSE;
        const char *start = s;
 
        /* A list item ends at the last non-whitespace character
-        * before a comma which is not inside a quoted-string. Or at
-        * the end of the string.
+        * before a delimiter which is not inside a quoted-string. Or
+        * at the end of the string.
         */
 
        while (*s) {
@@ -393,7 +393,7 @@ skip_item (const char *s)
                        if (*s == '\\' && *(s + 1))
                                s++;
                } else {
-                       if (*s == ',')
+                       if (*s == delim)
                                break;
                }
                s++;
@@ -402,6 +402,22 @@ skip_item (const char *s)
        return unskip_lws (s, start);
 }
 
+static GSList *
+parse_list (const char *header, char delim)
+{
+       GSList *list = NULL;
+       const char *end;
+
+       header = skip_delims (header, delim);
+       while (*header) {
+               end = skip_item (header, delim);
+               list = g_slist_prepend (list, g_strndup (header, end - header));
+               header = skip_delims (end, delim);
+       }
+
+       return g_slist_reverse (list);
+}
+
 /**
  * soup_header_parse_list:
  * @header: a header value
@@ -415,17 +431,7 @@ skip_item (const char *s)
 GSList *
 soup_header_parse_list (const char *header)
 {
-       GSList *list = NULL;
-       const char *end;
-
-       header = skip_commas (header);
-       while (*header) {
-               end = skip_item (header);
-               list = g_slist_prepend (list, g_strndup (header, end - header));
-               header = skip_commas (end);
-       }
-
-       return g_slist_reverse (list);
+       return parse_list (header, ',');
 }
 
 typedef struct {
@@ -569,13 +575,13 @@ soup_header_contains (const char *header, const char *token)
        const char *end;
        guint len = strlen (token);
 
-       header = skip_commas (header);
+       header = skip_delims (header, ',');
        while (*header) {
-               end = skip_item (header);
+               end = skip_item (header, ',');
                if (end - header == len &&
                    !g_ascii_strncasecmp (header, token, len))
                        return TRUE;
-               header = skip_commas (end);
+               header = skip_delims (end, ',');
        }
 
        return FALSE;
@@ -596,26 +602,14 @@ decode_quoted_string (char *quoted_string)
        *dst = '\0';
 }
 
-/**
- * soup_header_parse_param_list:
- * @header: a header value
- *
- * Parses a header which is a list of something like
- *   token [ "=" ( token | quoted-string ) ]
- *
- * Tokens that don't have an associated value will still be added to
- * the resulting hash table, but with a %NULL value.
- * 
- * Return value: a #GHashTable of list elements.
- **/
-GHashTable *
-soup_header_parse_param_list (const char *header)
+static GHashTable *
+parse_param_list (const char *header, char delim)
 {
        GHashTable *params;
        GSList *list, *iter;
        char *item, *eq, *name_end, *value;
 
-       list = soup_header_parse_list (header);
+       list = parse_list (header, delim);
        if (!list)
                return NULL;
 
@@ -651,6 +645,45 @@ soup_header_parse_param_list (const char *header)
 }
 
 /**
+ * soup_header_parse_param_list:
+ * @header: a header value
+ *
+ * Parses a header which is a comma-delimited list of something like
+ *
+ *   token [ "=" ( token | quoted-string ) ]
+ *
+ * Tokens that don't have an associated value will still be added to
+ * the resulting hash table, but with a %NULL value.
+ * 
+ * Return value: a #GHashTable of list elements.
+ **/
+GHashTable *
+soup_header_parse_param_list (const char *header)
+{
+       return parse_param_list (header, ',');
+}
+
+/**
+ * soup_header_parse_semi_param_list:
+ * @header: a header value
+ *
+ * Parses a header which is a semicolon-delimited list of something
+ * like
+ *
+ *   token [ "=" ( token | quoted-string ) ]
+ *
+ * Tokens that don't have an associated value will still be added to
+ * the resulting hash table, but with a %NULL value.
+ * 
+ * Return value: a #GHashTable of list elements.
+ **/
+GHashTable *
+soup_header_parse_semi_param_list (const char *header)
+{
+       return parse_param_list (header, ';');
+}
+
+/**
  * soup_header_free_param_list:
  * @param_list: a #GHashTable returned from soup_header_parse_param_list()
  *
index d040e75..80b56e3 100644 (file)
@@ -42,8 +42,9 @@ void        soup_header_free_list           (GSList           *list);
 gboolean    soup_header_contains            (const char       *header,
                                             const char       *token);
 
-GHashTable *soup_header_parse_param_list    (const char       *header);
-void        soup_header_free_param_list     (GHashTable       *param_list);
+GHashTable *soup_header_parse_param_list      (const char       *header);
+GHashTable *soup_header_parse_semi_param_list (const char       *header);
+void        soup_header_free_param_list       (GHashTable       *param_list);
 
 G_END_DECLS
 
index 929506c..5f44e37 100644 (file)
@@ -16,6 +16,8 @@ G_BEGIN_DECLS
 typedef struct _SoupAddress           SoupAddress;
 typedef struct _SoupAuth              SoupAuth;
 typedef struct _SoupAuthDomain        SoupAuthDomain;
+typedef struct _SoupCookie            SoupCookie;
+typedef struct _SoupDate              SoupDate;
 typedef struct _SoupMessage           SoupMessage;
 typedef struct _SoupServer            SoupServer;
 typedef struct _SoupSession           SoupSession;
index eeb254a..f096f39 100644 (file)
@@ -15,6 +15,8 @@ extern "C" {
 #include <libsoup/soup-auth-domain.h>
 #include <libsoup/soup-auth-domain-basic.h>
 #include <libsoup/soup-auth-domain-digest.h>
+#include <libsoup/soup-cookie.h>
+#include <libsoup/soup-cookie-jar.h>
 #include <libsoup/soup-date.h>
 #include <libsoup/soup-enum-types.h>
 #include <libsoup/soup-form.h>