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,
46 G_DEFINE_TYPE_WITH_CODE (SoupCookieJar, soup_cookie_jar, G_TYPE_OBJECT,
47 G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
48 soup_cookie_jar_session_feature_init))
55 static guint signals[LAST_SIGNAL] = { 0 };
67 gboolean constructed, read_only;
68 GHashTable *domains, *serials;
70 SoupCookieJarAcceptPolicy accept_policy;
71 } SoupCookieJarPrivate;
72 #define SOUP_COOKIE_JAR_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_COOKIE_JAR, SoupCookieJarPrivate))
74 static void set_property (GObject *object, guint prop_id,
75 const GValue *value, GParamSpec *pspec);
76 static void get_property (GObject *object, guint prop_id,
77 GValue *value, GParamSpec *pspec);
80 soup_cookie_jar_init (SoupCookieJar *jar)
82 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
84 priv->domains = g_hash_table_new_full (soup_str_case_hash,
87 priv->serials = g_hash_table_new (NULL, NULL);
88 priv->accept_policy = SOUP_COOKIE_JAR_ACCEPT_ALWAYS;
92 constructed (GObject *object)
94 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
96 priv->constructed = TRUE;
100 finalize (GObject *object)
102 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
106 g_hash_table_iter_init (&iter, priv->domains);
107 while (g_hash_table_iter_next (&iter, &key, &value))
108 soup_cookies_free (value);
109 g_hash_table_destroy (priv->domains);
110 g_hash_table_destroy (priv->serials);
112 G_OBJECT_CLASS (soup_cookie_jar_parent_class)->finalize (object);
116 soup_cookie_jar_class_init (SoupCookieJarClass *jar_class)
118 GObjectClass *object_class = G_OBJECT_CLASS (jar_class);
120 g_type_class_add_private (jar_class, sizeof (SoupCookieJarPrivate));
122 object_class->constructed = constructed;
123 object_class->finalize = finalize;
124 object_class->set_property = set_property;
125 object_class->get_property = get_property;
128 * SoupCookieJar::changed
129 * @jar: the #SoupCookieJar
130 * @old_cookie: the old #SoupCookie value
131 * @new_cookie: the new #SoupCookie value
133 * Emitted when @jar changes. If a cookie has been added,
134 * @new_cookie will contain the newly-added cookie and
135 * @old_cookie will be %NULL. If a cookie has been deleted,
136 * @old_cookie will contain the to-be-deleted cookie and
137 * @new_cookie will be %NULL. If a cookie has been changed,
138 * @old_cookie will contain its old value, and @new_cookie its
142 g_signal_new ("changed",
143 G_OBJECT_CLASS_TYPE (object_class),
145 G_STRUCT_OFFSET (SoupCookieJarClass, changed),
147 soup_marshal_NONE__BOXED_BOXED,
149 SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE,
150 SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE);
153 * SOUP_COOKIE_JAR_READ_ONLY:
155 * Alias for the #SoupCookieJar:read-only property. (Whether
156 * or not the cookie jar is read-only.)
158 g_object_class_install_property (
159 object_class, PROP_READ_ONLY,
160 g_param_spec_boolean (SOUP_COOKIE_JAR_READ_ONLY,
162 "Whether or not the cookie jar is read-only",
164 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
167 * SOUP_COOKIE_JAR_ACCEPT_POLICY:
169 * Alias for the #SoupCookieJar:accept-policy property.
174 * SoupCookieJar:accept-policy:
176 * The policy the jar should follow to accept or reject cookies
180 g_object_class_install_property (
181 object_class, PROP_ACCEPT_POLICY,
182 g_param_spec_enum (SOUP_COOKIE_JAR_ACCEPT_POLICY,
184 "The policy the jar should follow to accept or reject cookies",
185 SOUP_TYPE_COOKIE_JAR_ACCEPT_POLICY,
186 SOUP_COOKIE_JAR_ACCEPT_ALWAYS,
191 soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface,
192 gpointer interface_data)
194 feature_interface->request_queued = request_queued;
195 feature_interface->request_started = request_started;
196 feature_interface->request_unqueued = request_unqueued;
200 set_property (GObject *object, guint prop_id,
201 const GValue *value, GParamSpec *pspec)
203 SoupCookieJarPrivate *priv =
204 SOUP_COOKIE_JAR_GET_PRIVATE (object);
208 priv->read_only = g_value_get_boolean (value);
210 case PROP_ACCEPT_POLICY:
211 priv->accept_policy = g_value_get_enum (value);
214 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
220 get_property (GObject *object, guint prop_id,
221 GValue *value, GParamSpec *pspec)
223 SoupCookieJarPrivate *priv =
224 SOUP_COOKIE_JAR_GET_PRIVATE (object);
228 g_value_set_boolean (value, priv->read_only);
230 case PROP_ACCEPT_POLICY:
231 g_value_set_enum (value, priv->accept_policy);
234 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
240 * soup_cookie_jar_new:
242 * Creates a new #SoupCookieJar. The base #SoupCookieJar class does
243 * not support persistent storage of cookies; use a subclass for that.
245 * Returns: a new #SoupCookieJar
250 soup_cookie_jar_new (void)
252 return g_object_new (SOUP_TYPE_COOKIE_JAR, NULL);
256 soup_cookie_jar_save (SoupCookieJar *jar)
258 /* Does nothing, obsolete */
262 soup_cookie_jar_changed (SoupCookieJar *jar,
263 SoupCookie *old, SoupCookie *new)
265 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
267 if (old && old != new)
268 g_hash_table_remove (priv->serials, old);
271 g_hash_table_insert (priv->serials, new, GUINT_TO_POINTER (priv->serial));
274 if (priv->read_only || !priv->constructed)
277 g_signal_emit (jar, signals[CHANGED], 0, old, new);
281 compare_cookies (gconstpointer a, gconstpointer b, gpointer jar)
283 SoupCookie *ca = (SoupCookie *)a;
284 SoupCookie *cb = (SoupCookie *)b;
285 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
287 guint aserial, bserial;
289 /* "Cookies with longer path fields are listed before cookies
290 * with shorter path field."
292 alen = ca->path ? strlen (ca->path) : 0;
293 blen = cb->path ? strlen (cb->path) : 0;
297 /* "Among cookies that have equal length path fields, cookies
298 * with earlier creation dates are listed before cookies with
299 * later creation dates."
301 aserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, ca));
302 bserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, cb));
303 return aserial - bserial;
307 * soup_cookie_jar_get_cookies:
308 * @jar: a #SoupCookieJar
310 * @for_http: whether or not the return value is being passed directly
311 * to an HTTP operation
313 * Retrieves (in Cookie-header form) the list of cookies that would
314 * be sent with a request to @uri.
316 * If @for_http is %TRUE, the return value will include cookies marked
317 * "HttpOnly" (that is, cookies that the server wishes to keep hidden
318 * from client-side scripting operations such as the JavaScript
319 * document.cookies property). Since #SoupCookieJar sets the Cookie
320 * header itself when making the actual HTTP request, you should
321 * almost certainly be setting @for_http to %FALSE if you are calling
324 * Return value: the cookies, in string form, or %NULL if there are no
330 soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri,
333 SoupCookieJarPrivate *priv;
334 GSList *cookies, *domain_cookies;
335 char *domain, *cur, *next_domain, *result;
336 GSList *new_head, *cookies_to_remove = NULL, *p;
338 g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
339 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
340 g_return_val_if_fail (uri != NULL, NULL);
345 /* The logic here is a little weird, but the plan is that if
346 * uri->host is "www.foo.com", we will end up looking up
347 * cookies for ".www.foo.com", "www.foo.com", ".foo.com", and
348 * ".com", in that order. (Logic stolen from Mozilla.)
351 domain = cur = g_strdup_printf (".%s", uri->host);
352 next_domain = domain + 1;
354 new_head = domain_cookies = g_hash_table_lookup (priv->domains, cur);
355 while (domain_cookies) {
356 GSList *next = domain_cookies->next;
357 SoupCookie *cookie = domain_cookies->data;
359 if (cookie->expires && soup_date_is_past (cookie->expires)) {
360 cookies_to_remove = g_slist_append (cookies_to_remove,
362 new_head = g_slist_delete_link (new_head, domain_cookies);
363 g_hash_table_insert (priv->domains,
366 } else if (soup_cookie_applies_to_uri (cookie, uri) &&
367 (for_http || !cookie->http_only))
368 cookies = g_slist_append (cookies, cookie);
370 domain_cookies = next;
374 next_domain = strchr (cur + 1, '.');
378 for (p = cookies_to_remove; p; p = p->next) {
379 SoupCookie *cookie = p->data;
381 soup_cookie_jar_changed (jar, cookie, NULL);
382 soup_cookie_free (cookie);
384 g_slist_free (cookies_to_remove);
387 cookies = g_slist_sort_with_data (cookies, compare_cookies, jar);
388 result = soup_cookies_to_cookie_header (cookies);
389 g_slist_free (cookies);
401 * soup_cookie_jar_add_cookie:
402 * @jar: a #SoupCookieJar
403 * @cookie: a #SoupCookie
405 * Adds @cookie to @jar, emitting the 'changed' signal if we are modifying
406 * an existing cookie or adding a valid new cookie ('valid' means
407 * that the cookie's expire date is not in the past).
409 * @cookie will be 'stolen' by the jar, so don't free it afterwards.
414 soup_cookie_jar_add_cookie (SoupCookieJar *jar, SoupCookie *cookie)
416 SoupCookieJarPrivate *priv;
417 GSList *old_cookies, *oc, *last = NULL;
418 SoupCookie *old_cookie;
420 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
421 g_return_if_fail (cookie != NULL);
423 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
424 old_cookies = g_hash_table_lookup (priv->domains, cookie->domain);
425 for (oc = old_cookies; oc; oc = oc->next) {
426 old_cookie = oc->data;
427 if (!strcmp (cookie->name, old_cookie->name) &&
428 !g_strcmp0 (cookie->path, old_cookie->path)) {
429 if (cookie->expires && soup_date_is_past (cookie->expires)) {
430 /* The new cookie has an expired date,
431 * this is the way the the server has
432 * of telling us that we have to
435 old_cookies = g_slist_delete_link (old_cookies, oc);
436 g_hash_table_insert (priv->domains,
437 g_strdup (cookie->domain),
439 soup_cookie_jar_changed (jar, old_cookie, NULL);
440 soup_cookie_free (old_cookie);
441 soup_cookie_free (cookie);
444 soup_cookie_jar_changed (jar, old_cookie, cookie);
445 soup_cookie_free (old_cookie);
453 /* The new cookie is... a new cookie */
454 if (cookie->expires && soup_date_is_past (cookie->expires)) {
455 soup_cookie_free (cookie);
460 last->next = g_slist_append (NULL, cookie);
462 old_cookies = g_slist_append (NULL, cookie);
463 g_hash_table_insert (priv->domains, g_strdup (cookie->domain),
467 soup_cookie_jar_changed (jar, NULL, cookie);
471 * soup_cookie_jar_set_cookie:
472 * @jar: a #SoupCookieJar
473 * @uri: the URI setting the cookie
474 * @cookie: the stringified cookie to set
476 * Adds @cookie to @jar, exactly as though it had appeared in a
477 * Set-Cookie header returned from a request to @uri.
479 * Keep in mind that if the #SoupCookieJarAcceptPolicy
480 * %SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY is set you'll need to use
481 * soup_cookie_jar_set_cookie_with_first_party(), otherwise the jar
482 * will have no way of knowing if the cookie is being set by a third
488 soup_cookie_jar_set_cookie (SoupCookieJar *jar, SoupURI *uri,
491 SoupCookie *soup_cookie;
492 SoupCookieJarPrivate *priv;
494 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
495 g_return_if_fail (uri != NULL);
496 g_return_if_fail (cookie != NULL);
501 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
502 if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER)
505 g_return_if_fail (priv->accept_policy != SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY);
507 soup_cookie = soup_cookie_parse (cookie, uri);
509 /* will steal or free soup_cookie */
510 soup_cookie_jar_add_cookie (jar, soup_cookie);
515 * soup_cookie_jar_set_cookie_with_first_party:
516 * @jar: a #SoupCookieJar
517 * @uri: the URI setting the cookie
518 * @first_party: the URI for the main document
519 * @cookie: the stringified cookie to set
521 * Adds @cookie to @jar, exactly as though it had appeared in a
522 * Set-Cookie header returned from a request to @uri. @first_party
523 * will be used to reject cookies coming from third party resources in
524 * case such a security policy is set in the @jar.
529 soup_cookie_jar_set_cookie_with_first_party (SoupCookieJar *jar,
531 SoupURI *first_party,
534 SoupCookie *soup_cookie;
535 SoupCookieJarPrivate *priv;
537 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
538 g_return_if_fail (uri != NULL);
539 g_return_if_fail (first_party != NULL);
540 g_return_if_fail (cookie != NULL);
545 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
546 if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER)
549 soup_cookie = soup_cookie_parse (cookie, uri);
551 if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_ALWAYS ||
552 soup_cookie_domain_matches (soup_cookie, first_party->host)) {
553 /* will steal or free soup_cookie */
554 soup_cookie_jar_add_cookie (jar, soup_cookie);
556 soup_cookie_free (soup_cookie);
562 process_set_cookie_header (SoupMessage *msg, gpointer user_data)
564 SoupCookieJar *jar = user_data;
565 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
566 GSList *new_cookies, *nc;
568 if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER)
571 new_cookies = soup_cookies_from_response (msg);
572 for (nc = new_cookies; nc; nc = nc->next) {
573 SoupURI *first_party = soup_message_get_first_party (msg);
575 if ((priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY &&
576 first_party != NULL && first_party->host &&
577 soup_cookie_domain_matches (nc->data, first_party->host)) ||
578 priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_ALWAYS)
579 soup_cookie_jar_add_cookie (jar, nc->data);
581 soup_cookie_free (nc->data);
583 g_slist_free (new_cookies);
587 request_queued (SoupSessionFeature *feature, SoupSession *session,
590 soup_message_add_header_handler (msg, "got-headers",
592 G_CALLBACK (process_set_cookie_header),
597 request_started (SoupSessionFeature *feature, SoupSession *session,
598 SoupMessage *msg, SoupSocket *socket)
600 SoupCookieJar *jar = SOUP_COOKIE_JAR (feature);
603 cookies = soup_cookie_jar_get_cookies (jar, soup_message_get_uri (msg), TRUE);
605 soup_message_headers_replace (msg->request_headers,
609 soup_message_headers_remove (msg->request_headers, "Cookie");
613 request_unqueued (SoupSessionFeature *feature, SoupSession *session,
616 g_signal_handlers_disconnect_by_func (msg, process_set_cookie_header, feature);
620 * soup_cookie_jar_all_cookies:
621 * @jar: a #SoupCookieJar
623 * Constructs a #GSList with every cookie inside the @jar.
624 * The cookies in the list are a copy of the original, so
625 * you have to free them when you are done with them.
627 * Return value: (transfer full): a #GSList with all the cookies in
633 soup_cookie_jar_all_cookies (SoupCookieJar *jar)
635 SoupCookieJarPrivate *priv;
640 g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
642 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
644 g_hash_table_iter_init (&iter, priv->domains);
646 while (g_hash_table_iter_next (&iter, &key, &value)) {
647 GSList *p, *cookies = value;
648 for (p = cookies; p; p = p->next)
649 l = g_slist_prepend (l, soup_cookie_copy (p->data));
656 * soup_cookie_jar_delete_cookie:
657 * @jar: a #SoupCookieJar
658 * @cookie: a #SoupCookie
660 * Deletes @cookie from @jar, emitting the 'changed' signal.
665 soup_cookie_jar_delete_cookie (SoupCookieJar *jar,
668 SoupCookieJarPrivate *priv;
672 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
673 g_return_if_fail (cookie != NULL);
675 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
677 domain = g_strdup (cookie->domain);
679 cookies = g_hash_table_lookup (priv->domains, domain);
683 for (p = cookies; p; p = p->next ) {
684 SoupCookie *c = (SoupCookie*)p->data;
685 if (soup_cookie_equal (cookie, c)) {
686 cookies = g_slist_delete_link (cookies, p);
687 g_hash_table_insert (priv->domains,
690 soup_cookie_jar_changed (jar, c, NULL);
691 soup_cookie_free (c);
698 * SoupCookieJarAcceptPolicy:
699 * @SOUP_COOKIE_JAR_ACCEPT_ALWAYS: accept all cookies unconditionally.
700 * @SOUP_COOKIE_JAR_ACCEPT_NEVER: reject all cookies unconditionally.
701 * @SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY: accept all cookies set by
702 * the main document loaded in the application using libsoup. An
703 * example of the most common case, web browsers, would be: If
704 * http://www.example.com is the page loaded, accept all cookies set
705 * by example.com, but if a resource from http://www.third-party.com
706 * is loaded from that page reject any cookie that it could try to
707 * set. For libsoup to be able to tell apart first party cookies from
708 * the rest, the application must call soup_message_set_first_party()
709 * on each outgoing #SoupMessage, setting the #SoupURI of the main
710 * document. If no first party is set in a message when this policy is
711 * in effect, cookies will be assumed to be third party by default.
717 * soup_cookie_jar_get_accept_policy:
718 * @jar: a #SoupCookieJar
720 * Returns: the #SoupCookieJarAcceptPolicy set in the @jar
724 SoupCookieJarAcceptPolicy
725 soup_cookie_jar_get_accept_policy (SoupCookieJar *jar)
727 SoupCookieJarPrivate *priv;
729 g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), SOUP_COOKIE_JAR_ACCEPT_ALWAYS);
731 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
732 return priv->accept_policy;
736 * soup_cookie_jar_set_accept_policy:
737 * @jar: a #SoupCookieJar
738 * @policy: a #SoupCookieJarAcceptPolicy
740 * Sets @policy as the cookie acceptance policy for @jar.
745 soup_cookie_jar_set_accept_policy (SoupCookieJar *jar,
746 SoupCookieJarAcceptPolicy policy)
748 SoupCookieJarPrivate *priv;
750 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
752 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
754 if (priv->accept_policy != policy) {
755 priv->accept_policy = policy;
756 g_object_notify (G_OBJECT (jar), SOUP_COOKIE_JAR_ACCEPT_POLICY);