--- /dev/null
+/* -*- 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);
+}
+
--- /dev/null
+/* -*- 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;
+}