1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
5 * Copyright (C) 2008 Red Hat, Inc.
15 #include "soup-cookie.h"
16 #include "soup-cookie-jar.h"
17 #include "soup-date.h"
18 #include "soup-enum-types.h"
19 #include "soup-marshal.h"
20 #include "soup-message.h"
21 #include "soup-session-feature.h"
25 * SECTION:soup-cookie-jar
26 * @short_description: Automatic cookie handling for #SoupSession
28 * A #SoupCookieJar stores #SoupCookie<!-- -->s and arrange for them
29 * to be sent with the appropriate #SoupMessage<!-- -->s.
30 * #SoupCookieJar implements #SoupSessionFeature, so you can add a
31 * cookie jar to a session with soup_session_add_feature() or
32 * soup_session_add_feature_by_type().
34 * Note that the base #SoupCookieJar class does not support any form
35 * of long-term cookie persistence.
38 static void soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
39 static void request_queued (SoupSessionFeature *feature, SoupSession *session,
41 static void request_started (SoupSessionFeature *feature, SoupSession *session,
42 SoupMessage *msg, SoupSocket *socket);
43 static void request_unqueued (SoupSessionFeature *feature, SoupSession *session,
45 static gboolean is_persistent (SoupCookieJar *jar);
47 G_DEFINE_TYPE_WITH_CODE (SoupCookieJar, soup_cookie_jar, G_TYPE_OBJECT,
48 G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
49 soup_cookie_jar_session_feature_init))
56 static guint signals[LAST_SIGNAL] = { 0 };
68 gboolean constructed, read_only;
69 GHashTable *domains, *serials;
71 SoupCookieJarAcceptPolicy accept_policy;
72 } SoupCookieJarPrivate;
73 #define SOUP_COOKIE_JAR_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_COOKIE_JAR, SoupCookieJarPrivate))
75 static void set_property (GObject *object, guint prop_id,
76 const GValue *value, GParamSpec *pspec);
77 static void get_property (GObject *object, guint prop_id,
78 GValue *value, GParamSpec *pspec);
81 soup_cookie_jar_init (SoupCookieJar *jar)
83 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
85 priv->domains = g_hash_table_new_full (soup_str_case_hash,
88 priv->serials = g_hash_table_new (NULL, NULL);
89 priv->accept_policy = SOUP_COOKIE_JAR_ACCEPT_ALWAYS;
93 constructed (GObject *object)
95 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
97 priv->constructed = TRUE;
101 finalize (GObject *object)
103 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
107 g_hash_table_iter_init (&iter, priv->domains);
108 while (g_hash_table_iter_next (&iter, &key, &value))
109 soup_cookies_free (value);
110 g_hash_table_destroy (priv->domains);
111 g_hash_table_destroy (priv->serials);
113 G_OBJECT_CLASS (soup_cookie_jar_parent_class)->finalize (object);
117 soup_cookie_jar_class_init (SoupCookieJarClass *jar_class)
119 GObjectClass *object_class = G_OBJECT_CLASS (jar_class);
121 g_type_class_add_private (jar_class, sizeof (SoupCookieJarPrivate));
123 object_class->constructed = constructed;
124 object_class->finalize = finalize;
125 object_class->set_property = set_property;
126 object_class->get_property = get_property;
128 jar_class->is_persistent = is_persistent;
131 * SoupCookieJar::changed:
132 * @jar: the #SoupCookieJar
133 * @old_cookie: the old #SoupCookie value
134 * @new_cookie: the new #SoupCookie value
136 * Emitted when @jar changes. If a cookie has been added,
137 * @new_cookie will contain the newly-added cookie and
138 * @old_cookie will be %NULL. If a cookie has been deleted,
139 * @old_cookie will contain the to-be-deleted cookie and
140 * @new_cookie will be %NULL. If a cookie has been changed,
141 * @old_cookie will contain its old value, and @new_cookie its
145 g_signal_new ("changed",
146 G_OBJECT_CLASS_TYPE (object_class),
148 G_STRUCT_OFFSET (SoupCookieJarClass, changed),
150 _soup_marshal_NONE__BOXED_BOXED,
152 SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE,
153 SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE);
156 * SOUP_COOKIE_JAR_READ_ONLY:
158 * Alias for the #SoupCookieJar:read-only property. (Whether
159 * or not the cookie jar is read-only.)
161 g_object_class_install_property (
162 object_class, PROP_READ_ONLY,
163 g_param_spec_boolean (SOUP_COOKIE_JAR_READ_ONLY,
165 "Whether or not the cookie jar is read-only",
167 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
170 * SOUP_COOKIE_JAR_ACCEPT_POLICY:
172 * Alias for the #SoupCookieJar:accept-policy property.
177 * SoupCookieJar:accept-policy:
179 * The policy the jar should follow to accept or reject cookies
183 g_object_class_install_property (
184 object_class, PROP_ACCEPT_POLICY,
185 g_param_spec_enum (SOUP_COOKIE_JAR_ACCEPT_POLICY,
187 "The policy the jar should follow to accept or reject cookies",
188 SOUP_TYPE_COOKIE_JAR_ACCEPT_POLICY,
189 SOUP_COOKIE_JAR_ACCEPT_ALWAYS,
194 soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface,
195 gpointer interface_data)
197 feature_interface->request_queued = request_queued;
198 feature_interface->request_started = request_started;
199 feature_interface->request_unqueued = request_unqueued;
203 set_property (GObject *object, guint prop_id,
204 const GValue *value, GParamSpec *pspec)
206 SoupCookieJarPrivate *priv =
207 SOUP_COOKIE_JAR_GET_PRIVATE (object);
211 priv->read_only = g_value_get_boolean (value);
213 case PROP_ACCEPT_POLICY:
214 priv->accept_policy = g_value_get_enum (value);
217 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
223 get_property (GObject *object, guint prop_id,
224 GValue *value, GParamSpec *pspec)
226 SoupCookieJarPrivate *priv =
227 SOUP_COOKIE_JAR_GET_PRIVATE (object);
231 g_value_set_boolean (value, priv->read_only);
233 case PROP_ACCEPT_POLICY:
234 g_value_set_enum (value, priv->accept_policy);
237 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
243 * soup_cookie_jar_new:
245 * Creates a new #SoupCookieJar. The base #SoupCookieJar class does
246 * not support persistent storage of cookies; use a subclass for that.
248 * Returns: a new #SoupCookieJar
253 soup_cookie_jar_new (void)
255 return g_object_new (SOUP_TYPE_COOKIE_JAR, NULL);
259 * soup_cookie_jar_save:
260 * @jar: a #SoupCookieJar
262 * This function exists for backward compatibility, but does not do
263 * anything any more; cookie jars are saved automatically when they
267 soup_cookie_jar_save (SoupCookieJar *jar)
269 /* Does nothing, obsolete */
273 is_persistent (SoupCookieJar *jar)
279 soup_cookie_jar_changed (SoupCookieJar *jar,
280 SoupCookie *old, SoupCookie *new)
282 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
284 if (old && old != new)
285 g_hash_table_remove (priv->serials, old);
288 g_hash_table_insert (priv->serials, new, GUINT_TO_POINTER (priv->serial));
291 if (priv->read_only || !priv->constructed)
294 g_signal_emit (jar, signals[CHANGED], 0, old, new);
298 compare_cookies (gconstpointer a, gconstpointer b, gpointer jar)
300 SoupCookie *ca = (SoupCookie *)a;
301 SoupCookie *cb = (SoupCookie *)b;
302 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
304 guint aserial, bserial;
306 /* "Cookies with longer path fields are listed before cookies
307 * with shorter path field."
309 alen = ca->path ? strlen (ca->path) : 0;
310 blen = cb->path ? strlen (cb->path) : 0;
314 /* "Among cookies that have equal length path fields, cookies
315 * with earlier creation dates are listed before cookies with
316 * later creation dates."
318 aserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, ca));
319 bserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, cb));
320 return aserial - bserial;
324 * soup_cookie_jar_get_cookies:
325 * @jar: a #SoupCookieJar
327 * @for_http: whether or not the return value is being passed directly
328 * to an HTTP operation
330 * Retrieves (in Cookie-header form) the list of cookies that would
331 * be sent with a request to @uri.
333 * If @for_http is %TRUE, the return value will include cookies marked
334 * "HttpOnly" (that is, cookies that the server wishes to keep hidden
335 * from client-side scripting operations such as the JavaScript
336 * document.cookies property). Since #SoupCookieJar sets the Cookie
337 * header itself when making the actual HTTP request, you should
338 * almost certainly be setting @for_http to %FALSE if you are calling
341 * Return value: the cookies, in string form, or %NULL if there are no
347 soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri,
350 SoupCookieJarPrivate *priv;
351 GSList *cookies, *domain_cookies;
352 char *domain, *cur, *next_domain, *result;
353 GSList *new_head, *cookies_to_remove = NULL, *p;
355 g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
356 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
357 g_return_val_if_fail (uri != NULL, NULL);
362 /* The logic here is a little weird, but the plan is that if
363 * uri->host is "www.foo.com", we will end up looking up
364 * cookies for ".www.foo.com", "www.foo.com", ".foo.com", and
365 * ".com", in that order. (Logic stolen from Mozilla.)
368 domain = cur = g_strdup_printf (".%s", uri->host);
369 next_domain = domain + 1;
371 new_head = domain_cookies = g_hash_table_lookup (priv->domains, cur);
372 while (domain_cookies) {
373 GSList *next = domain_cookies->next;
374 SoupCookie *cookie = domain_cookies->data;
376 if (cookie->expires && soup_date_is_past (cookie->expires)) {
377 cookies_to_remove = g_slist_append (cookies_to_remove,
379 new_head = g_slist_delete_link (new_head, domain_cookies);
380 g_hash_table_insert (priv->domains,
383 } else if (soup_cookie_applies_to_uri (cookie, uri) &&
384 (for_http || !cookie->http_only))
385 cookies = g_slist_append (cookies, cookie);
387 domain_cookies = next;
391 next_domain = strchr (cur + 1, '.');
395 for (p = cookies_to_remove; p; p = p->next) {
396 SoupCookie *cookie = p->data;
398 soup_cookie_jar_changed (jar, cookie, NULL);
399 soup_cookie_free (cookie);
401 g_slist_free (cookies_to_remove);
404 cookies = g_slist_sort_with_data (cookies, compare_cookies, jar);
405 result = soup_cookies_to_cookie_header (cookies);
406 g_slist_free (cookies);
418 * soup_cookie_jar_add_cookie:
419 * @jar: a #SoupCookieJar
420 * @cookie: a #SoupCookie
422 * Adds @cookie to @jar, emitting the 'changed' signal if we are modifying
423 * an existing cookie or adding a valid new cookie ('valid' means
424 * that the cookie's expire date is not in the past).
426 * @cookie will be 'stolen' by the jar, so don't free it afterwards.
431 soup_cookie_jar_add_cookie (SoupCookieJar *jar, SoupCookie *cookie)
433 SoupCookieJarPrivate *priv;
434 GSList *old_cookies, *oc, *last = NULL;
435 SoupCookie *old_cookie;
437 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
438 g_return_if_fail (cookie != NULL);
440 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
441 old_cookies = g_hash_table_lookup (priv->domains, cookie->domain);
442 for (oc = old_cookies; oc; oc = oc->next) {
443 old_cookie = oc->data;
444 if (!strcmp (cookie->name, old_cookie->name) &&
445 !g_strcmp0 (cookie->path, old_cookie->path)) {
446 if (cookie->expires && soup_date_is_past (cookie->expires)) {
447 /* The new cookie has an expired date,
448 * this is the way the the server has
449 * of telling us that we have to
452 old_cookies = g_slist_delete_link (old_cookies, oc);
453 g_hash_table_insert (priv->domains,
454 g_strdup (cookie->domain),
456 soup_cookie_jar_changed (jar, old_cookie, NULL);
457 soup_cookie_free (old_cookie);
458 soup_cookie_free (cookie);
461 soup_cookie_jar_changed (jar, old_cookie, cookie);
462 soup_cookie_free (old_cookie);
470 /* The new cookie is... a new cookie */
471 if (cookie->expires && soup_date_is_past (cookie->expires)) {
472 soup_cookie_free (cookie);
477 last->next = g_slist_append (NULL, cookie);
479 old_cookies = g_slist_append (NULL, cookie);
480 g_hash_table_insert (priv->domains, g_strdup (cookie->domain),
484 soup_cookie_jar_changed (jar, NULL, cookie);
488 * soup_cookie_jar_set_cookie:
489 * @jar: a #SoupCookieJar
490 * @uri: the URI setting the cookie
491 * @cookie: the stringified cookie to set
493 * Adds @cookie to @jar, exactly as though it had appeared in a
494 * Set-Cookie header returned from a request to @uri.
496 * Keep in mind that if the #SoupCookieJarAcceptPolicy
497 * %SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY is set you'll need to use
498 * soup_cookie_jar_set_cookie_with_first_party(), otherwise the jar
499 * will have no way of knowing if the cookie is being set by a third
505 soup_cookie_jar_set_cookie (SoupCookieJar *jar, SoupURI *uri,
508 SoupCookie *soup_cookie;
509 SoupCookieJarPrivate *priv;
511 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
512 g_return_if_fail (uri != NULL);
513 g_return_if_fail (cookie != NULL);
518 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
519 if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER)
522 g_return_if_fail (priv->accept_policy != SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY);
524 soup_cookie = soup_cookie_parse (cookie, uri);
526 /* will steal or free soup_cookie */
527 soup_cookie_jar_add_cookie (jar, soup_cookie);
532 * soup_cookie_jar_set_cookie_with_first_party:
533 * @jar: a #SoupCookieJar
534 * @uri: the URI setting the cookie
535 * @first_party: the URI for the main document
536 * @cookie: the stringified cookie to set
538 * Adds @cookie to @jar, exactly as though it had appeared in a
539 * Set-Cookie header returned from a request to @uri. @first_party
540 * will be used to reject cookies coming from third party resources in
541 * case such a security policy is set in the @jar.
546 soup_cookie_jar_set_cookie_with_first_party (SoupCookieJar *jar,
548 SoupURI *first_party,
551 SoupCookie *soup_cookie;
552 SoupCookieJarPrivate *priv;
554 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
555 g_return_if_fail (uri != NULL);
556 g_return_if_fail (first_party != NULL);
557 g_return_if_fail (cookie != NULL);
562 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
563 if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER)
566 soup_cookie = soup_cookie_parse (cookie, uri);
568 if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_ALWAYS ||
569 soup_cookie_domain_matches (soup_cookie, first_party->host)) {
570 /* will steal or free soup_cookie */
571 soup_cookie_jar_add_cookie (jar, soup_cookie);
573 soup_cookie_free (soup_cookie);
579 process_set_cookie_header (SoupMessage *msg, gpointer user_data)
581 SoupCookieJar *jar = user_data;
582 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
583 GSList *new_cookies, *nc;
585 if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER)
588 new_cookies = soup_cookies_from_response (msg);
589 for (nc = new_cookies; nc; nc = nc->next) {
590 SoupURI *first_party = soup_message_get_first_party (msg);
592 if ((priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY &&
593 first_party != NULL && first_party->host &&
594 soup_cookie_domain_matches (nc->data, first_party->host)) ||
595 priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_ALWAYS)
596 soup_cookie_jar_add_cookie (jar, nc->data);
598 soup_cookie_free (nc->data);
600 g_slist_free (new_cookies);
604 request_queued (SoupSessionFeature *feature, SoupSession *session,
607 soup_message_add_header_handler (msg, "got-headers",
609 G_CALLBACK (process_set_cookie_header),
614 request_started (SoupSessionFeature *feature, SoupSession *session,
615 SoupMessage *msg, SoupSocket *socket)
617 SoupCookieJar *jar = SOUP_COOKIE_JAR (feature);
620 cookies = soup_cookie_jar_get_cookies (jar, soup_message_get_uri (msg), TRUE);
622 soup_message_headers_replace (msg->request_headers,
626 soup_message_headers_remove (msg->request_headers, "Cookie");
630 request_unqueued (SoupSessionFeature *feature, SoupSession *session,
633 g_signal_handlers_disconnect_by_func (msg, process_set_cookie_header, feature);
637 * soup_cookie_jar_all_cookies:
638 * @jar: a #SoupCookieJar
640 * Constructs a #GSList with every cookie inside the @jar.
641 * The cookies in the list are a copy of the original, so
642 * you have to free them when you are done with them.
644 * Return value: (transfer full) (element-type Soup.Cookie): a #GSList
645 * with all the cookies in the @jar.
650 soup_cookie_jar_all_cookies (SoupCookieJar *jar)
652 SoupCookieJarPrivate *priv;
657 g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
659 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
661 g_hash_table_iter_init (&iter, priv->domains);
663 while (g_hash_table_iter_next (&iter, &key, &value)) {
664 GSList *p, *cookies = value;
665 for (p = cookies; p; p = p->next)
666 l = g_slist_prepend (l, soup_cookie_copy (p->data));
673 * soup_cookie_jar_delete_cookie:
674 * @jar: a #SoupCookieJar
675 * @cookie: a #SoupCookie
677 * Deletes @cookie from @jar, emitting the 'changed' signal.
682 soup_cookie_jar_delete_cookie (SoupCookieJar *jar,
685 SoupCookieJarPrivate *priv;
689 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
690 g_return_if_fail (cookie != NULL);
692 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
694 domain = g_strdup (cookie->domain);
696 cookies = g_hash_table_lookup (priv->domains, domain);
700 for (p = cookies; p; p = p->next ) {
701 SoupCookie *c = (SoupCookie*)p->data;
702 if (soup_cookie_equal (cookie, c)) {
703 cookies = g_slist_delete_link (cookies, p);
704 g_hash_table_insert (priv->domains,
707 soup_cookie_jar_changed (jar, c, NULL);
708 soup_cookie_free (c);
715 * SoupCookieJarAcceptPolicy:
716 * @SOUP_COOKIE_JAR_ACCEPT_ALWAYS: accept all cookies unconditionally.
717 * @SOUP_COOKIE_JAR_ACCEPT_NEVER: reject all cookies unconditionally.
718 * @SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY: accept all cookies set by
719 * the main document loaded in the application using libsoup. An
720 * example of the most common case, web browsers, would be: If
721 * http://www.example.com is the page loaded, accept all cookies set
722 * by example.com, but if a resource from http://www.third-party.com
723 * is loaded from that page reject any cookie that it could try to
724 * set. For libsoup to be able to tell apart first party cookies from
725 * the rest, the application must call soup_message_set_first_party()
726 * on each outgoing #SoupMessage, setting the #SoupURI of the main
727 * document. If no first party is set in a message when this policy is
728 * in effect, cookies will be assumed to be third party by default.
734 * soup_cookie_jar_get_accept_policy:
735 * @jar: a #SoupCookieJar
737 * Gets @jar's #SoupCookieJarAcceptPolicy
739 * Returns: the #SoupCookieJarAcceptPolicy set in the @jar
743 SoupCookieJarAcceptPolicy
744 soup_cookie_jar_get_accept_policy (SoupCookieJar *jar)
746 SoupCookieJarPrivate *priv;
748 g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), SOUP_COOKIE_JAR_ACCEPT_ALWAYS);
750 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
751 return priv->accept_policy;
755 * soup_cookie_jar_set_accept_policy:
756 * @jar: a #SoupCookieJar
757 * @policy: a #SoupCookieJarAcceptPolicy
759 * Sets @policy as the cookie acceptance policy for @jar.
764 soup_cookie_jar_set_accept_policy (SoupCookieJar *jar,
765 SoupCookieJarAcceptPolicy policy)
767 SoupCookieJarPrivate *priv;
769 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
771 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
773 if (priv->accept_policy != policy) {
774 priv->accept_policy = policy;
775 g_object_notify (G_OBJECT (jar), SOUP_COOKIE_JAR_ACCEPT_POLICY);
780 * soup_cookie_jar_is_persistent:
781 * @jar: a #SoupCookieJar
783 * Gets whether @jar stores cookies persistenly.
785 * Returns: %TRUE if @jar storage is persistent or %FALSE otherwise.
790 soup_cookie_jar_is_persistent (SoupCookieJar *jar)
792 g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), FALSE);
794 return SOUP_COOKIE_JAR_GET_CLASS (jar)->is_persistent (jar);