Update cookie parsing/output to match the current httpstate draft better
[platform/upstream/libsoup.git] / libsoup / soup-cookie-jar.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * soup-cookie-jar.c
4  *
5  * Copyright (C) 2008 Red Hat, Inc.
6  */
7
8 #ifdef HAVE_CONFIG_H
9 #include <config.h>
10 #endif
11
12 #include <stdio.h>
13 #include <string.h>
14
15 #include "soup-cookie.h"
16 #include "soup-cookie-jar.h"
17 #include "soup-date.h"
18 #include "soup-marshal.h"
19 #include "soup-message.h"
20 #include "soup-session-feature.h"
21 #include "soup-uri.h"
22
23 /**
24  * SECTION:soup-cookie-jar
25  * @short_description: Automatic cookie handling for #SoupSession
26  *
27  * A #SoupCookieJar stores #SoupCookie<!-- -->s and arrange for them
28  * to be sent with the appropriate #SoupMessage<!-- -->s.
29  * #SoupCookieJar implements #SoupSessionFeature, so you can add a
30  * cookie jar to a session with soup_session_add_feature() or
31  * soup_session_add_feature_by_type().
32  *
33  * Note that the base #SoupCookieJar class does not support any form
34  * of long-term cookie persistence.
35  **/
36
37 static void soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
38 static void request_queued (SoupSessionFeature *feature, SoupSession *session,
39                             SoupMessage *msg);
40 static void request_started (SoupSessionFeature *feature, SoupSession *session,
41                              SoupMessage *msg, SoupSocket *socket);
42 static void request_unqueued (SoupSessionFeature *feature, SoupSession *session,
43                               SoupMessage *msg);
44
45 G_DEFINE_TYPE_WITH_CODE (SoupCookieJar, soup_cookie_jar, G_TYPE_OBJECT,
46                          G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
47                                                 soup_cookie_jar_session_feature_init))
48
49 enum {
50         CHANGED,
51         LAST_SIGNAL
52 };
53
54 static guint signals[LAST_SIGNAL] = { 0 };
55
56 enum {
57         PROP_0,
58
59         PROP_READ_ONLY,
60
61         LAST_PROP
62 };
63
64 typedef struct {
65         gboolean constructed, read_only;
66         GHashTable *domains, *serials;
67         guint serial;
68 } SoupCookieJarPrivate;
69 #define SOUP_COOKIE_JAR_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_COOKIE_JAR, SoupCookieJarPrivate))
70
71 static void set_property (GObject *object, guint prop_id,
72                           const GValue *value, GParamSpec *pspec);
73 static void get_property (GObject *object, guint prop_id,
74                           GValue *value, GParamSpec *pspec);
75
76 static void
77 soup_cookie_jar_init (SoupCookieJar *jar)
78 {
79         SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
80
81         priv->domains = g_hash_table_new_full (soup_str_case_hash,
82                                                soup_str_case_equal,
83                                                g_free, NULL);
84         priv->serials = g_hash_table_new (NULL, NULL);
85 }
86
87 static void
88 constructed (GObject *object)
89 {
90         SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
91
92         priv->constructed = TRUE;
93 }
94
95 static void
96 finalize (GObject *object)
97 {
98         SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
99         GHashTableIter iter;
100         gpointer key, value;
101
102         g_hash_table_iter_init (&iter, priv->domains);
103         while (g_hash_table_iter_next (&iter, &key, &value))
104                 soup_cookies_free (value);
105         g_hash_table_destroy (priv->domains);
106         g_hash_table_destroy (priv->serials);
107
108         G_OBJECT_CLASS (soup_cookie_jar_parent_class)->finalize (object);
109 }
110
111 static void
112 soup_cookie_jar_class_init (SoupCookieJarClass *jar_class)
113 {
114         GObjectClass *object_class = G_OBJECT_CLASS (jar_class);
115
116         g_type_class_add_private (jar_class, sizeof (SoupCookieJarPrivate));
117
118         object_class->constructed = constructed;
119         object_class->finalize = finalize;
120         object_class->set_property = set_property;
121         object_class->get_property = get_property;
122
123         /**
124          * SoupCookieJar::changed
125          * @jar: the #SoupCookieJar
126          * @old_cookie: the old #SoupCookie value
127          * @new_cookie: the new #SoupCookie value
128          *
129          * Emitted when @jar changes. If a cookie has been added,
130          * @new_cookie will contain the newly-added cookie and
131          * @old_cookie will be %NULL. If a cookie has been deleted,
132          * @old_cookie will contain the to-be-deleted cookie and
133          * @new_cookie will be %NULL. If a cookie has been changed,
134          * @old_cookie will contain its old value, and @new_cookie its
135          * new value.
136          **/
137         signals[CHANGED] =
138                 g_signal_new ("changed",
139                               G_OBJECT_CLASS_TYPE (object_class),
140                               G_SIGNAL_RUN_FIRST,
141                               G_STRUCT_OFFSET (SoupCookieJarClass, changed),
142                               NULL, NULL,
143                               soup_marshal_NONE__BOXED_BOXED,
144                               G_TYPE_NONE, 2, 
145                               SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE,
146                               SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE);
147
148         /**
149          * SOUP_COOKIE_JAR_READ_ONLY:
150          *
151          * Alias for the #SoupCookieJar:read-only property. (Whether
152          * or not the cookie jar is read-only.)
153          **/
154         g_object_class_install_property (
155                 object_class, PROP_READ_ONLY,
156                 g_param_spec_boolean (SOUP_COOKIE_JAR_READ_ONLY,
157                                       "Read-only",
158                                       "Whether or not the cookie jar is read-only",
159                                       FALSE,
160                                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
161 }
162
163 static void
164 soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface,
165                                       gpointer interface_data)
166 {
167         feature_interface->request_queued = request_queued;
168         feature_interface->request_started = request_started;
169         feature_interface->request_unqueued = request_unqueued;
170 }
171
172 static void
173 set_property (GObject *object, guint prop_id,
174               const GValue *value, GParamSpec *pspec)
175 {
176         SoupCookieJarPrivate *priv =
177                 SOUP_COOKIE_JAR_GET_PRIVATE (object);
178
179         switch (prop_id) {
180         case PROP_READ_ONLY:
181                 priv->read_only = g_value_get_boolean (value);
182                 break;
183         default:
184                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
185                 break;
186         }
187 }
188
189 static void
190 get_property (GObject *object, guint prop_id,
191               GValue *value, GParamSpec *pspec)
192 {
193         SoupCookieJarPrivate *priv =
194                 SOUP_COOKIE_JAR_GET_PRIVATE (object);
195
196         switch (prop_id) {
197         case PROP_READ_ONLY:
198                 g_value_set_boolean (value, priv->read_only);
199                 break;
200         default:
201                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
202                 break;
203         }
204 }
205
206 /**
207  * soup_cookie_jar_new:
208  *
209  * Creates a new #SoupCookieJar. The base #SoupCookieJar class does
210  * not support persistent storage of cookies; use a subclass for that.
211  *
212  * Returns: a new #SoupCookieJar
213  *
214  * Since: 2.24
215  **/
216 SoupCookieJar *
217 soup_cookie_jar_new (void) 
218 {
219         return g_object_new (SOUP_TYPE_COOKIE_JAR, NULL);
220 }
221
222 void
223 soup_cookie_jar_save (SoupCookieJar *jar)
224 {
225         /* Does nothing, obsolete */
226 }
227
228 static void
229 soup_cookie_jar_changed (SoupCookieJar *jar,
230                          SoupCookie *old, SoupCookie *new)
231 {
232         SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
233
234         if (old && old != new)
235                 g_hash_table_remove (priv->serials, old);
236         if (new) {
237                 priv->serial++;
238                 g_hash_table_insert (priv->serials, new, GUINT_TO_POINTER (priv->serial));
239         }
240
241         if (priv->read_only || !priv->constructed)
242                 return;
243
244         g_signal_emit (jar, signals[CHANGED], 0, old, new);
245 }
246
247 static int
248 compare_cookies (gconstpointer a, gconstpointer b, gpointer jar)
249 {
250         SoupCookie *ca = (SoupCookie *)a;
251         SoupCookie *cb = (SoupCookie *)b;
252         SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
253         int alen, blen;
254         guint aserial, bserial;
255
256         /* "Cookies with longer path fields are listed before cookies
257          * with shorter path field."
258          */
259         alen = ca->path ? strlen (ca->path) : 0;
260         blen = cb->path ? strlen (cb->path) : 0;
261         if (alen != blen)
262                 return blen - alen;
263
264         /* "Among cookies that have equal length path fields, cookies
265          * with earlier creation dates are listed before cookies with
266          * later creation dates."
267          */
268         aserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, ca));
269         bserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, cb));
270         return aserial - bserial;
271 }
272
273 /**
274  * soup_cookie_jar_get_cookies:
275  * @jar: a #SoupCookieJar
276  * @uri: a #SoupURI
277  * @for_http: whether or not the return value is being passed directly
278  * to an HTTP operation
279  *
280  * Retrieves (in Cookie-header form) the list of cookies that would
281  * be sent with a request to @uri.
282  *
283  * If @for_http is %TRUE, the return value will include cookies marked
284  * "HttpOnly" (that is, cookies that the server wishes to keep hidden
285  * from client-side scripting operations such as the JavaScript
286  * document.cookies property). Since #SoupCookieJar sets the Cookie
287  * header itself when making the actual HTTP request, you should
288  * almost certainly be setting @for_http to %FALSE if you are calling
289  * this.
290  *
291  * Return value: the cookies, in string form, or %NULL if there are no
292  * cookies for @uri.
293  *
294  * Since: 2.24
295  **/
296 char *
297 soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri,
298                              gboolean for_http)
299 {
300         SoupCookieJarPrivate *priv;
301         GSList *cookies, *domain_cookies;
302         char *domain, *cur, *next_domain, *result;
303         GSList *new_head, *cookies_to_remove = NULL, *p;
304
305         g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
306         priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
307         g_return_val_if_fail (uri != NULL, NULL);
308
309         if (!SOUP_URI_VALID_FOR_HTTP (uri))
310                 return NULL;
311
312         /* The logic here is a little weird, but the plan is that if
313          * uri->host is "www.foo.com", we will end up looking up
314          * cookies for ".www.foo.com", "www.foo.com", ".foo.com", and
315          * ".com", in that order. (Logic stolen from Mozilla.)
316          */
317         cookies = NULL;
318         domain = cur = g_strdup_printf (".%s", uri->host);
319         next_domain = domain + 1;
320         do {
321                 new_head = domain_cookies = g_hash_table_lookup (priv->domains, cur);
322                 while (domain_cookies) {
323                         GSList *next = domain_cookies->next;
324                         SoupCookie *cookie = domain_cookies->data;
325
326                         if (cookie->expires && soup_date_is_past (cookie->expires)) {
327                                 cookies_to_remove = g_slist_append (cookies_to_remove,
328                                                                     cookie);
329                                 new_head = g_slist_delete_link (new_head, domain_cookies);
330                                 g_hash_table_insert (priv->domains,
331                                                      g_strdup (cur),
332                                                      new_head);
333                         } else if (soup_cookie_applies_to_uri (cookie, uri) &&
334                                    (for_http || !cookie->http_only))
335                                 cookies = g_slist_append (cookies, cookie);
336
337                         domain_cookies = next;
338                 }
339                 cur = next_domain;
340                 if (cur)
341                         next_domain = strchr (cur + 1, '.');
342         } while (cur);
343         g_free (domain);
344
345         for (p = cookies_to_remove; p; p = p->next) {
346                 SoupCookie *cookie = p->data;
347
348                 soup_cookie_jar_changed (jar, cookie, NULL);
349                 soup_cookie_free (cookie);
350         }
351         g_slist_free (cookies_to_remove);
352
353         if (cookies) {
354                 cookies = g_slist_sort_with_data (cookies, compare_cookies, jar);
355                 result = soup_cookies_to_cookie_header (cookies);
356                 g_slist_free (cookies);
357
358                 if (!*result) {
359                         g_free (result);
360                         result = NULL;
361                 }
362                 return result;
363         } else
364                 return NULL;
365 }
366
367 /**
368  * soup_cookie_jar_add_cookie:
369  * @jar: a #SoupCookieJar
370  * @cookie: a #SoupCookie
371  *
372  * Adds @cookie to @jar, emitting the 'changed' signal if we are modifying
373  * an existing cookie or adding a valid new cookie ('valid' means
374  * that the cookie's expire date is not in the past).
375  *
376  * @cookie will be 'stolen' by the jar, so don't free it afterwards.
377  *
378  * Since: 2.26
379  **/
380 void
381 soup_cookie_jar_add_cookie (SoupCookieJar *jar, SoupCookie *cookie)
382 {
383         SoupCookieJarPrivate *priv;
384         GSList *old_cookies, *oc, *prev = NULL;
385         SoupCookie *old_cookie;
386
387         g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
388         g_return_if_fail (cookie != NULL);
389
390         priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
391         old_cookies = g_hash_table_lookup (priv->domains, cookie->domain);
392         for (oc = old_cookies; oc; oc = oc->next) {
393                 old_cookie = oc->data;
394                 if (!strcmp (cookie->name, old_cookie->name) &&
395                     !g_strcmp0 (cookie->path, old_cookie->path)) {
396                         if (cookie->expires && soup_date_is_past (cookie->expires)) {
397                                 /* The new cookie has an expired date,
398                                  * this is the way the the server has
399                                  * of telling us that we have to
400                                  * remove the cookie.
401                                  */
402                                 old_cookies = g_slist_delete_link (old_cookies, oc);
403                                 g_hash_table_insert (priv->domains,
404                                                      g_strdup (cookie->domain),
405                                                      old_cookies);
406                                 soup_cookie_jar_changed (jar, old_cookie, NULL);
407                                 soup_cookie_free (old_cookie);
408                                 soup_cookie_free (cookie);
409                         } else {
410                                 oc->data = cookie;
411                                 soup_cookie_jar_changed (jar, old_cookie, cookie);
412                                 soup_cookie_free (old_cookie);
413                         }
414
415                         return;
416                 }
417                 prev = oc;
418         }
419
420         /* The new cookie is... a new cookie */
421         if (cookie->expires && soup_date_is_past (cookie->expires)) {
422                 soup_cookie_free (cookie);
423                 return;
424         }
425
426         if (prev)
427                 prev = g_slist_append (prev, cookie);
428         else {
429                 old_cookies = g_slist_append (NULL, cookie);
430                 g_hash_table_insert (priv->domains, g_strdup (cookie->domain),
431                                      old_cookies);
432         }
433
434         soup_cookie_jar_changed (jar, NULL, cookie);
435 }
436
437 /**
438  * soup_cookie_jar_set_cookie:
439  * @jar: a #SoupCookieJar
440  * @uri: the URI setting the cookie
441  * @cookie: the stringified cookie to set
442  *
443  * Adds @cookie to @jar, exactly as though it had appeared in a
444  * Set-Cookie header returned from a request to @uri.
445  *
446  * Since: 2.24
447  **/
448 void
449 soup_cookie_jar_set_cookie (SoupCookieJar *jar, SoupURI *uri,
450                             const char *cookie)
451 {
452         SoupCookie *soup_cookie;
453
454         g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
455         g_return_if_fail (uri != NULL);
456         g_return_if_fail (cookie != NULL);
457
458         if (!SOUP_URI_VALID_FOR_HTTP (uri))
459                 return;
460
461         soup_cookie = soup_cookie_parse (cookie, uri);
462         if (soup_cookie) {
463                 /* will steal or free soup_cookie */
464                 soup_cookie_jar_add_cookie (jar, soup_cookie);
465         }
466 }
467
468 static void
469 process_set_cookie_header (SoupMessage *msg, gpointer user_data)
470 {
471         SoupCookieJar *jar = user_data;
472         GSList *new_cookies, *nc;
473
474         new_cookies = soup_cookies_from_response (msg);
475         for (nc = new_cookies; nc; nc = nc->next)
476                 soup_cookie_jar_add_cookie (jar, nc->data);
477         g_slist_free (new_cookies);
478 }
479
480 static void
481 request_queued (SoupSessionFeature *feature, SoupSession *session,
482                 SoupMessage *msg)
483 {
484         soup_message_add_header_handler (msg, "got-headers",
485                                          "Set-Cookie",
486                                          G_CALLBACK (process_set_cookie_header),
487                                          feature);
488 }
489
490 static void
491 request_started (SoupSessionFeature *feature, SoupSession *session,
492                  SoupMessage *msg, SoupSocket *socket)
493 {
494         SoupCookieJar *jar = SOUP_COOKIE_JAR (feature);
495         char *cookies;
496
497         cookies = soup_cookie_jar_get_cookies (jar, soup_message_get_uri (msg), TRUE);
498         if (cookies) {
499                 soup_message_headers_replace (msg->request_headers,
500                                               "Cookie", cookies);
501                 g_free (cookies);
502         } else
503                 soup_message_headers_remove (msg->request_headers, "Cookie");
504 }
505
506 static void
507 request_unqueued (SoupSessionFeature *feature, SoupSession *session,
508                   SoupMessage *msg)
509 {
510         g_signal_handlers_disconnect_by_func (msg, process_set_cookie_header, feature);
511 }
512
513 /**
514  * soup_cookie_jar_all_cookies:
515  * @jar: a #SoupCookieJar
516  *
517  * Constructs a #GSList with every cookie inside the @jar.
518  * The cookies in the list are a copy of the original, so
519  * you have to free them when you are done with them.
520  *
521  * Return value: a #GSList with all the cookies in the @jar.
522  *
523  * Since: 2.26
524  **/
525 GSList *
526 soup_cookie_jar_all_cookies (SoupCookieJar *jar)
527 {
528         SoupCookieJarPrivate *priv;
529         GHashTableIter iter;
530         GSList *l = NULL;
531         gpointer key, value;
532
533         g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
534
535         priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
536
537         g_hash_table_iter_init (&iter, priv->domains);
538
539         while (g_hash_table_iter_next (&iter, &key, &value)) {
540                 GSList *p, *cookies = value;
541                 for (p = cookies; p; p = p->next)
542                         l = g_slist_prepend (l, soup_cookie_copy (p->data));
543         }
544
545         return l;
546 }
547
548 /**
549  * soup_cookie_jar_delete_cookie:
550  * @jar: a #SoupCookieJar
551  * @cookie: a #SoupCookie
552  *
553  * Deletes @cookie from @jar, emitting the 'changed' signal.
554  *
555  * Since: 2.26
556  **/
557 void
558 soup_cookie_jar_delete_cookie (SoupCookieJar *jar,
559                                SoupCookie    *cookie)
560 {
561         SoupCookieJarPrivate *priv;
562         GSList *cookies, *p;
563         char *domain;
564
565         g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
566         g_return_if_fail (cookie != NULL);
567
568         priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
569
570         domain = g_strdup (cookie->domain);
571
572         cookies = g_hash_table_lookup (priv->domains, domain);
573         if (cookies == NULL)
574                 return;
575
576         for (p = cookies; p; p = p->next ) {
577                 SoupCookie *c = (SoupCookie*)p->data;
578                 if (soup_cookie_equal (cookie, c)) {
579                         cookies = g_slist_delete_link (cookies, p);
580                         g_hash_table_insert (priv->domains,
581                                              domain,
582                                              cookies);
583                         soup_cookie_jar_changed (jar, c, NULL);
584                         soup_cookie_free (c);
585                         return;
586                 }
587         }
588 }