Coverage Report

Created: 2025-11-16 06:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libsoup/libsoup/cookies/soup-cookie.c
Line
Count
Source
1
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2
/*
3
 * soup-cookie.c
4
 *
5
 * Copyright (C) 2007 Red Hat, Inc.
6
 */
7
8
#ifdef HAVE_CONFIG_H
9
#include <config.h>
10
#endif
11
12
#include <stdlib.h>
13
#include <string.h>
14
15
#include "soup-cookie.h"
16
#include "soup-date-utils-private.h"
17
#include "soup-message-headers-private.h"
18
#include "soup-misc.h"
19
#include "soup-uri-utils-private.h"
20
#include "soup.h"
21
22
/**
23
 * SoupCookie:
24
 *
25
 * Implements HTTP cookies, as described by
26
 * [RFC 6265](http://tools.ietf.org/html/rfc6265.txt).
27
 *
28
 * To have a [class@Session] handle cookies for your appliction
29
 * automatically, use a [class@CookieJar].
30
 *
31
 * @name and @value will be set for all cookies. If the cookie is
32
 * generated from a string that appears to have no name, then @name
33
 * will be the empty string.
34
 *
35
 * @domain and @path give the host or domain, and path within that
36
 * host/domain, to restrict this cookie to. If @domain starts with
37
 * ".", that indicates a domain (which matches the string after the
38
 * ".", or any hostname that has @domain as a suffix). Otherwise, it
39
 * is a hostname and must match exactly.
40
 *
41
 * @expires will be non-%NULL if the cookie uses either the original
42
 * "expires" attribute, or the newer "max-age" attribute. If @expires
43
 * is %NULL, it indicates that neither "expires" nor "max-age" was
44
 * specified, and the cookie expires at the end of the session.
45
 * 
46
 * If @http_only is set, the cookie should not be exposed to untrusted
47
 * code (eg, javascript), so as to minimize the danger posed by
48
 * cross-site scripting attacks.
49
 **/
50
51
struct _SoupCookie {
52
  char      *name;
53
  char      *value;
54
  char      *domain;
55
  char      *path;
56
  GDateTime *expires;
57
  gboolean   secure;
58
  gboolean   http_only;
59
  SoupSameSitePolicy same_site_policy;
60
};
61
62
0
G_DEFINE_BOXED_TYPE (SoupCookie, soup_cookie, soup_cookie_copy, soup_cookie_free)
63
0
64
0
/**
65
0
 * soup_cookie_copy:
66
0
 * @cookie: a #SoupCookie
67
0
 *
68
0
 * Copies @cookie.
69
0
 *
70
0
 * Returns: a copy of @cookie
71
0
 **/
72
0
SoupCookie *
73
0
soup_cookie_copy (SoupCookie *cookie)
74
0
{
75
0
  SoupCookie *copy = g_slice_new0 (SoupCookie);
76
77
0
  copy->name = g_strdup (cookie->name);
78
0
  copy->value = g_strdup (cookie->value);
79
0
  copy->domain = g_strdup (cookie->domain);
80
0
  copy->path = g_strdup (cookie->path);
81
0
  if (cookie->expires)
82
0
    copy->expires = g_date_time_ref (cookie->expires);
83
0
  copy->secure = cookie->secure;
84
0
  copy->http_only = cookie->http_only;
85
0
  copy->same_site_policy = cookie->same_site_policy;
86
87
0
  return copy;
88
0
}
89
90
/**
91
 * soup_cookie_domain_matches:
92
 * @cookie: a #SoupCookie
93
 * @host: a URI
94
 *
95
 * Checks if the @cookie's domain and @host match.
96
 *
97
 * The domains match if @cookie should be sent when making a request to @host,
98
 * or that @cookie should be accepted when receiving a response from @host.
99
 * 
100
 * Returns: %TRUE if the domains match, %FALSE otherwise
101
 **/
102
gboolean
103
soup_cookie_domain_matches (SoupCookie *cookie, const char *host)
104
0
{
105
0
  g_return_val_if_fail (cookie != NULL, FALSE);
106
0
  g_return_val_if_fail (host != NULL, FALSE);
107
108
0
  return soup_host_matches_host (cookie->domain, host);
109
0
}
110
111
static inline gboolean
112
is_white_space (char c)
113
92.2k
{
114
92.2k
  return (c == ' ' || c == '\t');
115
92.2k
}
116
117
static inline const char *
118
skip_lws (const char *s)
119
43.9k
{
120
49.5k
  while (is_white_space (*s))
121
5.51k
    s++;
122
43.9k
  return s;
123
43.9k
}
124
125
static inline const char *
126
unskip_lws (const char *s, const char *start)
127
40.8k
{
128
46.1k
  while (s > start && is_white_space (*(s - 1)))
129
5.32k
    s--;
130
40.8k
  return s;
131
40.8k
}
132
133
10.6M
#define is_attr_ender(ch) ((ch) == '\0' || (ch) == ';' || (ch) == ',' || (ch) == '=')
134
7.06M
#define is_value_ender(ch) ((ch) == '\0' || (ch) == ';')
135
136
static char *
137
parse_value (const char **val_p, gboolean copy)
138
20.6k
{
139
20.6k
  const char *start, *end, *p;
140
20.6k
  char *value;
141
142
20.6k
  p = *val_p;
143
20.6k
  if (*p == '=')
144
17.5k
    p++;
145
20.6k
  start = skip_lws (p);
146
7.06M
  for (p = start; !is_value_ender (*p); p++)
147
7.04M
    ;
148
20.6k
  end = unskip_lws (p, start);
149
150
20.6k
  if (copy)
151
19.9k
    value = g_strndup (start, end - start);
152
776
  else
153
776
    value = NULL;
154
155
20.6k
  *val_p = p;
156
20.6k
  return value;
157
20.6k
}
158
159
static GDateTime *
160
parse_date (const char **val_p)
161
12.8k
{
162
12.8k
  char *value;
163
12.8k
  GDateTime *date;
164
165
12.8k
  value = parse_value (val_p, TRUE);
166
12.8k
  date = soup_date_time_new_from_http_string (value);
167
12.8k
  g_free (value);
168
12.8k
  return date;
169
12.8k
}
170
171
1.81k
#define MAX_AGE_CAP_IN_SECONDS 31536000  // 1 year
172
3.39k
#define MAX_ATTRIBUTE_SIZE 1024
173
174
static SoupCookie *
175
parse_one_cookie (const char *header, GUri *origin)
176
3.22k
{
177
3.22k
  const char *start, *end, *p;
178
3.22k
  gboolean has_value;
179
3.22k
  SoupCookie *cookie;
180
181
3.22k
  cookie = g_slice_new0 (SoupCookie);
182
3.22k
  soup_cookie_set_same_site_policy (cookie, SOUP_SAME_SITE_POLICY_LAX);
183
184
  /* Parse the NAME */
185
3.22k
  start = skip_lws (header);
186
19.0k
  for (p = start; !is_attr_ender (*p); p++)
187
15.8k
    ;
188
3.22k
  if (*p == '=') {
189
53
    end = unskip_lws (p, start);
190
53
    cookie->name = g_strndup (start, end - start);
191
3.17k
  } else {
192
    /* No NAME; Set cookie->name to "" and then rewind to
193
     * re-parse the string as a VALUE.
194
     */
195
3.17k
    cookie->name = g_strdup ("");
196
3.17k
    p = start;
197
3.17k
  }
198
199
  /* Parse the VALUE */
200
3.22k
  cookie->value = parse_value (&p, TRUE);
201
202
3.22k
        if (!*cookie->name && !*cookie->value) {
203
40
            soup_cookie_free (cookie);
204
40
            return NULL;
205
40
        }
206
207
3.18k
  if (strlen (cookie->name) + strlen (cookie->value) > 4096) {
208
1
    soup_cookie_free (cookie);
209
1
    return NULL;
210
1
  }
211
212
  /* Parse attributes */
213
23.2k
  while (*p == ';') {
214
20.0k
    start = skip_lws (p + 1);
215
10.6M
    for (p = start; !is_attr_ender (*p); p++)
216
10.6M
      ;
217
20.0k
    end = unskip_lws (p, start);
218
219
20.0k
    has_value = (*p == '=');
220
111k
#define MATCH_NAME(name) ((end - start == strlen (name)) && !g_ascii_strncasecmp (start, name, end - start))
221
222
20.0k
    if (MATCH_NAME ("domain") && has_value) {
223
709
                        char *new_domain = parse_value (&p, TRUE);
224
709
                        if (strlen (new_domain) > MAX_ATTRIBUTE_SIZE) {
225
204
                            g_free (new_domain);
226
204
                            continue;
227
204
                        }
228
505
      g_free (cookie->domain);
229
505
      cookie->domain = g_steal_pointer (&new_domain);
230
505
      if (!*cookie->domain) {
231
213
        g_free (cookie->domain);
232
213
        cookie->domain = NULL;
233
213
      }
234
19.3k
    } else if (MATCH_NAME ("expires") && has_value) {
235
12.8k
      g_clear_pointer (&cookie->expires, g_date_time_unref);
236
12.8k
      cookie->expires = parse_date (&p);
237
12.8k
    } else if (MATCH_NAME ("httponly")) {
238
445
      cookie->http_only = TRUE;
239
445
      if (has_value)
240
251
        parse_value (&p, FALSE);
241
6.07k
    } else if (MATCH_NAME ("max-age") && has_value) {
242
2.01k
      char *max_age_str = parse_value (&p, TRUE), *mae;
243
2.01k
                        if (strlen (max_age_str) > MAX_ATTRIBUTE_SIZE) {
244
203
                            g_free (max_age_str);
245
203
                            continue;
246
203
                        }
247
1.80k
      long max_age = strtol (max_age_str, &mae, 10);
248
1.80k
      if (!*mae) {
249
1.57k
        if (max_age < 0)
250
276
          max_age = 0;
251
1.57k
        if (max_age > MAX_AGE_CAP_IN_SECONDS)
252
244
          max_age = MAX_AGE_CAP_IN_SECONDS;
253
1.57k
        soup_cookie_set_max_age (cookie, max_age);
254
1.57k
      }
255
1.80k
      g_free (max_age_str);
256
4.06k
    } else if (MATCH_NAME ("path") && has_value) {
257
671
                        char *new_path = parse_value (&p, TRUE);
258
671
                        if (strlen (new_path) > MAX_ATTRIBUTE_SIZE) {
259
204
                            g_free (new_path);
260
204
                            continue;
261
204
                        }
262
467
      g_free (cookie->path);
263
467
      cookie->path = g_steal_pointer (&new_path);
264
467
      if (*cookie->path != '/') {
265
269
        g_free (cookie->path);
266
269
        cookie->path = NULL;
267
269
      }
268
3.39k
    } else if (MATCH_NAME ("secure")) {
269
446
      cookie->secure = TRUE;
270
446
      if (has_value)
271
252
        parse_value (&p, FALSE);
272
2.94k
    } else if (MATCH_NAME ("samesite")) {
273
661
      if (has_value) {
274
467
        char *policy = parse_value (&p, TRUE);
275
467
        if (g_ascii_strcasecmp (policy, "None") == 0)
276
194
          soup_cookie_set_same_site_policy (cookie, SOUP_SAME_SITE_POLICY_NONE);
277
273
        else if (g_ascii_strcasecmp (policy, "Strict") == 0)
278
0
          soup_cookie_set_same_site_policy (cookie, SOUP_SAME_SITE_POLICY_STRICT);
279
        /* There is an explicit "Lax" value which is the default */
280
467
        g_free (policy);
281
467
      }
282
      /* Note that earlier versions of the same-site RFC treated invalid values as strict but
283
         the latest revision assigns invalid SameSite values to Lax. */
284
2.28k
    } else {
285
      /* Ignore unknown attributes, but we still have
286
       * to skip over the value.
287
       */
288
2.28k
      if (has_value)
289
273
        parse_value (&p, FALSE);
290
2.28k
    }
291
20.0k
  }
292
293
3.18k
  if (cookie->domain) {
294
    /* Domain must have at least one '.' (not counting an
295
     * initial one. (We check this now, rather than
296
     * bailing out sooner, because we don't want to force
297
     * any cookies after this one in the Set-Cookie header
298
     * to be discarded.)
299
     */
300
80
    if (!strchr (cookie->domain + 1, '.')) {
301
64
      soup_cookie_free (cookie);
302
64
      return NULL;
303
64
    }
304
305
    /* If the domain string isn't an IP addr, and doesn't
306
     * start with a '.', prepend one.
307
     */
308
16
    if (!g_hostname_is_ip_address (cookie->domain) &&
309
15
        cookie->domain[0] != '.') {
310
14
      char *tmp = g_strdup_printf (".%s", cookie->domain);
311
14
      g_free (cookie->domain);
312
14
      cookie->domain = tmp;
313
14
    }
314
16
  }
315
316
3.12k
  if (origin) {
317
    /* Sanity-check domain */
318
0
    if (cookie->domain) {
319
0
      if (!soup_cookie_domain_matches (cookie, g_uri_get_host (origin))) {
320
0
        soup_cookie_free (cookie);
321
0
        return NULL;
322
0
      }
323
0
    } else
324
0
      cookie->domain = g_strdup (g_uri_get_host (origin));
325
326
    /* The original cookie spec didn't say that pages
327
     * could only set cookies for paths they were under.
328
     * RFC 2109 adds that requirement, but some sites
329
     * depend on the old behavior
330
     * (https://bugzilla.mozilla.org/show_bug.cgi?id=156725#c20).
331
     * So we don't check the path.
332
     */
333
334
0
    if (!cookie->path) {
335
0
                        GUri *normalized_origin = soup_uri_copy_with_normalized_flags (origin);
336
0
      char *slash;
337
0
                        const char *origin_path = g_uri_get_path (normalized_origin);
338
339
0
      slash = strrchr (origin_path, '/');
340
0
      if (!slash || slash == origin_path)
341
0
        cookie->path = g_strdup ("/");
342
0
      else {
343
0
        cookie->path = g_strndup (origin_path,
344
0
                slash - origin_path);
345
0
      }
346
347
0
                        g_uri_unref (normalized_origin);
348
0
    }
349
350
3.12k
  } else if (!cookie->path) {
351
3.11k
    cookie->path = g_strdup ("/");
352
3.11k
  }
353
354
3.12k
  return cookie;
355
3.12k
}
356
357
static SoupCookie *
358
cookie_new_internal (const char *name, const char *value,
359
         const char *domain, const char *path,
360
         int max_age)
361
0
{
362
0
  SoupCookie *cookie;
363
364
0
  cookie = g_slice_new0 (SoupCookie);
365
0
  cookie->name = g_strdup (name);
366
0
  cookie->value = g_strdup (value);
367
0
  cookie->domain = g_strdup (domain);
368
0
  cookie->path = g_strdup (path);
369
0
  soup_cookie_set_max_age (cookie, max_age);
370
0
  cookie->same_site_policy = SOUP_SAME_SITE_POLICY_LAX;
371
372
0
  return cookie;
373
0
}
374
375
/**
376
 * soup_cookie_new:
377
 * @name: cookie name
378
 * @value: cookie value
379
 * @domain: cookie domain or hostname
380
 * @path: cookie path, or %NULL
381
 * @max_age: max age of the cookie, or -1 for a session cookie
382
 *
383
 * Creates a new [struct@Cookie] with the given attributes.
384
 *
385
 * Use [method@Cookie.set_secure] and [method@Cookie.set_http_only] if you
386
 * need to set those attributes on the returned cookie.
387
 *
388
 * If @domain starts with ".", that indicates a domain (which matches
389
 * the string after the ".", or any hostname that has @domain as a
390
 * suffix). Otherwise, it is a hostname and must match exactly.
391
 *
392
 * @max_age is used to set the "expires" attribute on the cookie; pass
393
 * -1 to not include the attribute (indicating that the cookie expires
394
 * with the current session), 0 for an already-expired cookie, or a
395
 * lifetime in seconds. You can use the constants
396
 * %SOUP_COOKIE_MAX_AGE_ONE_HOUR, %SOUP_COOKIE_MAX_AGE_ONE_DAY,
397
 * %SOUP_COOKIE_MAX_AGE_ONE_WEEK and %SOUP_COOKIE_MAX_AGE_ONE_YEAR (or
398
 * multiples thereof) to calculate this value. (If you really care
399
 * about setting the exact time that the cookie will expire, use
400
 * [method@Cookie.set_expires].)
401
 *
402
 * As of version 3.4.0 the default value of a cookie's same-site-policy
403
 * is %SOUP_SAME_SITE_POLICY_LAX.
404
 *
405
 * Returns: a new #SoupCookie.
406
 **/
407
SoupCookie *
408
soup_cookie_new (const char *name, const char *value,
409
     const char *domain, const char *path,
410
     int max_age)
411
0
{
412
0
  g_return_val_if_fail (name != NULL, NULL);
413
0
  g_return_val_if_fail (value != NULL, NULL);
414
415
  /* We ought to return if domain is NULL too, but this used to
416
   * do be incorrectly documented as legal, and it wouldn't
417
   * break anything as long as you called
418
   * soup_cookie_set_domain() immediately after. So we warn but
419
   * don't return, to discourage that behavior but not actually
420
   * break anyone doing it.
421
   */
422
0
  g_warn_if_fail (domain != NULL);
423
424
0
  return cookie_new_internal (name, value, domain, path, max_age);
425
0
}
426
427
/**
428
 * soup_cookie_parse:
429
 * @header: a cookie string (eg, the value of a Set-Cookie header)
430
 * @origin: (nullable): origin of the cookie
431
 *
432
 * Parses @header and returns a [struct@Cookie].
433
 *
434
 * If @header contains multiple cookies, only the first one will be parsed.
435
 *
436
 * If @header does not have "path" or "domain" attributes, they will
437
 * be defaulted from @origin. If @origin is %NULL, path will default
438
 * to "/", but domain will be left as %NULL. Note that this is not a
439
 * valid state for a [struct@Cookie], and you will need to fill in some
440
 * appropriate string for the domain if you want to actually make use
441
 * of the cookie.
442
 *
443
 * As of version 3.4.0 the default value of a cookie's same-site-policy
444
 * is %SOUP_SAME_SITE_POLICY_LAX.
445
 *
446
 * Returns: (nullable): a new #SoupCookie, or %NULL if it could
447
 *   not be parsed, or contained an illegal "domain" attribute for a
448
 *   cookie originating from @origin.
449
 **/
450
SoupCookie *
451
soup_cookie_parse (const char *cookie, GUri *origin)
452
3.22k
{
453
3.22k
        g_return_val_if_fail (cookie != NULL, NULL);
454
3.22k
        g_return_val_if_fail (origin == NULL || g_uri_get_host (origin) != NULL, NULL);
455
456
3.22k
  return parse_one_cookie (cookie, origin);
457
3.22k
}
458
459
/**
460
 * soup_cookie_get_name:
461
 * @cookie: a #SoupCookie
462
 *
463
 * Gets @cookie's name.
464
 *
465
 * Returns: @cookie's name
466
 **/
467
const char *
468
soup_cookie_get_name (SoupCookie *cookie)
469
0
{
470
0
  return cookie->name;
471
0
}
472
473
/**
474
 * soup_cookie_set_name:
475
 * @cookie: a #SoupCookie
476
 * @name: the new name
477
 *
478
 * Sets @cookie's name to @name.
479
 **/
480
void
481
soup_cookie_set_name (SoupCookie *cookie, const char *name)
482
0
{
483
0
  g_free (cookie->name);
484
0
  cookie->name = g_strdup (name);
485
0
}
486
487
/**
488
 * soup_cookie_get_value:
489
 * @cookie: a #SoupCookie
490
 *
491
 * Gets @cookie's value.
492
 *
493
 * Returns: @cookie's value
494
 **/
495
const char *
496
soup_cookie_get_value (SoupCookie *cookie)
497
0
{
498
0
  return cookie->value;
499
0
}
500
501
/**
502
 * soup_cookie_set_value:
503
 * @cookie: a #SoupCookie
504
 * @value: the new value
505
 *
506
 * Sets @cookie's value to @value.
507
 **/
508
void
509
soup_cookie_set_value (SoupCookie *cookie, const char *value)
510
0
{
511
0
  g_free (cookie->value);
512
0
  cookie->value = g_strdup (value);
513
0
}
514
515
/**
516
 * soup_cookie_get_domain:
517
 * @cookie: a #SoupCookie
518
 *
519
 * Gets @cookie's domain.
520
 *
521
 * Returns: @cookie's domain
522
 **/
523
const char *
524
soup_cookie_get_domain (SoupCookie *cookie)
525
0
{
526
0
  return cookie->domain;
527
0
}
528
529
/**
530
 * soup_cookie_set_domain:
531
 * @cookie: a #SoupCookie
532
 * @domain: the new domain
533
 *
534
 * Sets @cookie's domain to @domain.
535
 **/
536
void
537
soup_cookie_set_domain (SoupCookie *cookie, const char *domain)
538
0
{
539
0
  g_free (cookie->domain);
540
0
  cookie->domain = g_strdup (domain);
541
0
}
542
543
/**
544
 * soup_cookie_get_path:
545
 * @cookie: a #SoupCookie
546
 *
547
 * Gets @cookie's path.
548
 *
549
 * Returns: @cookie's path
550
 **/
551
const char *
552
soup_cookie_get_path (SoupCookie *cookie)
553
0
{
554
0
  return cookie->path;
555
0
}
556
557
/**
558
 * soup_cookie_set_path:
559
 * @cookie: a #SoupCookie
560
 * @path: the new path
561
 *
562
 * Sets @cookie's path to @path.
563
 **/
564
void
565
soup_cookie_set_path (SoupCookie *cookie, const char *path)
566
0
{
567
0
  g_free (cookie->path);
568
0
  cookie->path = g_strdup (path);
569
0
}
570
571
/**
572
 * soup_cookie_set_max_age:
573
 * @cookie: a #SoupCookie
574
 * @max_age: the new max age
575
 *
576
 * Sets @cookie's max age to @max_age.
577
 *
578
 * If @max_age is -1, the cookie is a session cookie, and will expire at the end
579
 * of the client's session. Otherwise, it is the number of seconds until the
580
 * cookie expires. You can use the constants %SOUP_COOKIE_MAX_AGE_ONE_HOUR,
581
 * %SOUP_COOKIE_MAX_AGE_ONE_DAY, %SOUP_COOKIE_MAX_AGE_ONE_WEEK and
582
 * %SOUP_COOKIE_MAX_AGE_ONE_YEAR (or multiples thereof) to calculate this value.
583
 * (A value of 0 indicates that the cookie should be considered
584
 * already-expired.)
585
 *
586
 * This sets the same property as [method@Cookie.set_expires].
587
 **/
588
void
589
soup_cookie_set_max_age (SoupCookie *cookie, int max_age)
590
1.57k
{
591
1.57k
  if (cookie->expires)
592
933
    g_date_time_unref (cookie->expires);
593
594
1.57k
  if (max_age == -1)
595
0
    cookie->expires = NULL;
596
1.57k
  else if (max_age == 0) {
597
    /* Use a date way in the past, to protect against
598
     * clock skew.
599
     */
600
1.06k
    cookie->expires = g_date_time_new_from_unix_utc (0);
601
1.06k
  } else {
602
507
                GDateTime *now = g_date_time_new_now_utc ();
603
507
                cookie->expires = g_date_time_add_seconds (now, max_age);
604
507
                g_date_time_unref (now);
605
507
        }
606
1.57k
}
607
608
/**
609
 * SOUP_COOKIE_MAX_AGE_ONE_HOUR:
610
 *
611
 * A constant corresponding to 1 hour.
612
 *
613
 * For use with [ctor@Cookie.new] and [method@Cookie.set_max_age].
614
 **/
615
/**
616
 * SOUP_COOKIE_MAX_AGE_ONE_DAY:
617
 *
618
 * A constant corresponding to 1 day.
619
 *
620
 * For use with [ctor@Cookie.new] and [method@Cookie.set_max_age].
621
 **/
622
/**
623
 * SOUP_COOKIE_MAX_AGE_ONE_WEEK:
624
 *
625
 * A constant corresponding to 1 week.
626
 *
627
 * For use with [ctor@Cookie.new] and [method@Cookie.set_max_age].
628
 **/
629
/**
630
 * SOUP_COOKIE_MAX_AGE_ONE_YEAR:
631
 *
632
 * A constant corresponding to 1 year.
633
 *
634
 * For use with [ctor@Cookie.new] and [method@Cookie.set_max_age].
635
 **/
636
637
/**
638
 * soup_cookie_get_expires:
639
 * @cookie: a #GDateTime
640
 *
641
 * Gets @cookie's expiration time.
642
 *
643
 * Returns: (nullable) (transfer none): @cookie's expiration time, which is
644
 *   owned by @cookie and should not be modified or freed.
645
 **/
646
GDateTime *
647
soup_cookie_get_expires (SoupCookie *cookie)
648
0
{
649
0
  return cookie->expires;
650
0
}
651
652
/**
653
 * soup_cookie_set_expires:
654
 * @cookie: a #SoupCookie
655
 * @expires: the new expiration time, or %NULL
656
 *
657
 * Sets @cookie's expiration time to @expires.
658
 *
659
 * If @expires is %NULL, @cookie will be a session cookie and will expire at the
660
 * end of the client's session.
661
 *
662
 * (This sets the same property as [method@Cookie.set_max_age].)
663
 **/
664
void
665
soup_cookie_set_expires (SoupCookie *cookie, GDateTime *expires)
666
0
{
667
0
  if (cookie->expires)
668
0
    g_date_time_unref (cookie->expires);
669
670
0
  if (expires)
671
0
    cookie->expires = g_date_time_ref (expires);
672
0
  else
673
0
    cookie->expires = NULL;
674
0
}
675
676
/**
677
 * soup_cookie_get_secure:
678
 * @cookie: a #SoupCookie
679
 *
680
 * Gets @cookie's secure attribute.
681
 *
682
 * Returns: @cookie's secure attribute
683
 **/
684
gboolean
685
soup_cookie_get_secure (SoupCookie *cookie)
686
0
{
687
0
  return cookie->secure;
688
0
}
689
690
/**
691
 * soup_cookie_set_secure:
692
 * @cookie: a #SoupCookie
693
 * @secure: the new value for the secure attribute
694
 *
695
 * Sets @cookie's secure attribute to @secure.
696
 *
697
 * If %TRUE, @cookie will only be transmitted from the client to the server over
698
 * secure (https) connections.
699
 **/
700
void
701
soup_cookie_set_secure (SoupCookie *cookie, gboolean secure)
702
0
{
703
0
  cookie->secure = secure;
704
0
}
705
706
/**
707
 * soup_cookie_get_http_only:
708
 * @cookie: a #SoupCookie
709
 *
710
 * Gets @cookie's HttpOnly attribute.
711
 *
712
 * Returns: @cookie's HttpOnly attribute
713
 **/
714
gboolean
715
soup_cookie_get_http_only (SoupCookie *cookie)
716
0
{
717
0
  return cookie->http_only;
718
0
}
719
720
/**
721
 * soup_cookie_set_http_only:
722
 * @cookie: a #SoupCookie
723
 * @http_only: the new value for the HttpOnly attribute
724
 *
725
 * Sets @cookie's HttpOnly attribute to @http_only.
726
 *
727
 * If %TRUE, @cookie will be marked as "http only", meaning it should not be
728
 * exposed to web page scripts or other untrusted code.
729
 **/
730
void
731
soup_cookie_set_http_only (SoupCookie *cookie, gboolean http_only)
732
0
{
733
0
  cookie->http_only = http_only;
734
0
}
735
736
static void
737
serialize_cookie (SoupCookie *cookie, GString *header, gboolean set_cookie)
738
0
{
739
0
  SoupSameSitePolicy same_site_policy;
740
741
0
  if (!*cookie->name && !*cookie->value)
742
0
    return;
743
744
0
  if (header->len) {
745
0
    if (set_cookie)
746
0
      g_string_append (header, ", ");
747
0
    else
748
0
      g_string_append (header, "; ");
749
0
  }
750
751
0
  if (set_cookie || *cookie->name) {
752
0
    g_string_append (header, cookie->name);
753
0
    g_string_append (header, "=");
754
0
  }
755
0
  g_string_append (header, cookie->value);
756
0
  if (!set_cookie)
757
0
    return;
758
759
0
  if (cookie->expires) {
760
0
    char *timestamp;
761
0
    timestamp = soup_date_time_to_string (cookie->expires,
762
0
                  SOUP_DATE_COOKIE);
763
0
                if (timestamp) {
764
0
                        g_string_append (header, "; expires=");
765
0
                        g_string_append (header, timestamp);
766
0
                        g_free (timestamp);
767
0
                }
768
0
  }
769
0
  if (cookie->path) {
770
0
    g_string_append (header, "; path=");
771
0
    g_string_append (header, cookie->path);
772
0
  }
773
0
  if (cookie->domain) {
774
0
    g_string_append (header, "; domain=");
775
0
    g_string_append (header, cookie->domain);
776
0
  }
777
778
0
  same_site_policy = soup_cookie_get_same_site_policy (cookie);
779
0
  if (same_site_policy != SOUP_SAME_SITE_POLICY_NONE) {
780
0
    g_string_append (header, "; SameSite=");
781
0
    if (same_site_policy == SOUP_SAME_SITE_POLICY_LAX)
782
0
      g_string_append (header, "Lax");
783
0
    else
784
0
      g_string_append (header, "Strict");
785
0
  }
786
0
  if (cookie->secure)
787
0
    g_string_append (header, "; secure");
788
0
  if (cookie->http_only)
789
0
    g_string_append (header, "; HttpOnly");
790
0
}
791
792
/**
793
 * soup_cookie_set_same_site_policy:
794
 * @cookie: a #SoupCookie
795
 * @policy: a #SoupSameSitePolicy
796
 *
797
 * When used in conjunction with
798
 * [method@CookieJar.get_cookie_list_with_same_site_info] this sets the policy
799
 * of when this cookie should be exposed.
800
 **/
801
void
802
soup_cookie_set_same_site_policy (SoupCookie         *cookie,
803
                                  SoupSameSitePolicy  policy)
804
3.42k
{
805
3.42k
  switch (policy) {
806
194
  case SOUP_SAME_SITE_POLICY_NONE:
807
194
  case SOUP_SAME_SITE_POLICY_STRICT:
808
3.42k
  case SOUP_SAME_SITE_POLICY_LAX:
809
3.42k
                cookie->same_site_policy = policy;
810
3.42k
    break;
811
0
  default:
812
0
    g_return_if_reached ();
813
3.42k
  }
814
3.42k
}
815
816
/**
817
 * soup_cookie_get_same_site_policy:
818
 * @cookie: a #SoupCookie
819
 *
820
 * Returns the same-site policy for this cookie.
821
 *
822
 * Returns: a #SoupSameSitePolicy
823
 **/
824
SoupSameSitePolicy
825
soup_cookie_get_same_site_policy (SoupCookie *cookie)
826
0
{
827
0
        return cookie->same_site_policy;
828
0
}
829
830
/**
831
 * soup_cookie_to_set_cookie_header:
832
 * @cookie: a #SoupCookie
833
 *
834
 * Serializes @cookie in the format used by the Set-Cookie header.
835
 *
836
 * i.e. for sending a cookie from a [class@Server] to a client.
837
 *
838
 * Returns: the header
839
 **/
840
char *
841
soup_cookie_to_set_cookie_header (SoupCookie *cookie)
842
0
{
843
0
  GString *header = g_string_new (NULL);
844
845
0
  serialize_cookie (cookie, header, TRUE);
846
0
  return g_string_free (header, FALSE);
847
0
}
848
849
/**
850
 * soup_cookie_to_cookie_header:
851
 * @cookie: a #SoupCookie
852
 *
853
 * Serializes @cookie in the format used by the Cookie header (ie, for
854
 * returning a cookie from a [class@Session] to a server).
855
 *
856
 * Returns: the header
857
 **/
858
char *
859
soup_cookie_to_cookie_header (SoupCookie *cookie)
860
0
{
861
0
  GString *header = g_string_new (NULL);
862
863
0
  serialize_cookie (cookie, header, FALSE);
864
0
  return g_string_free (header, FALSE);
865
0
}
866
867
/**
868
 * soup_cookie_free:
869
 * @cookie: a #SoupCookie
870
 *
871
 * Frees @cookie.
872
 **/
873
void
874
soup_cookie_free (SoupCookie *cookie)
875
3.22k
{
876
3.22k
  g_return_if_fail (cookie != NULL);
877
878
3.22k
  g_free (cookie->name);
879
3.22k
  g_free (cookie->value);
880
3.22k
  g_free (cookie->domain);
881
3.22k
  g_free (cookie->path);
882
3.22k
  g_clear_pointer (&cookie->expires, g_date_time_unref);
883
884
3.22k
  g_dataset_destroy (cookie);
885
3.22k
  g_slice_free (SoupCookie, cookie);
886
3.22k
}
887
888
/**
889
 * soup_cookies_from_response:
890
 * @msg: a #SoupMessage containing a "Set-Cookie" response header
891
 *
892
 * Parses @msg's Set-Cookie response headers and returns a [struct@GLib.SList]
893
 * of `SoupCookie`s.
894
 *
895
 * Cookies that do not specify "path" or "domain" attributes will have their
896
 * values defaulted from @msg.
897
 *
898
 * Returns: (element-type SoupCookie) (transfer full): a #GSList of
899
 *   `SoupCookie`s, which can be freed with [method@Cookie.free].
900
 **/
901
GSList *
902
soup_cookies_from_response (SoupMessage *msg)
903
0
{
904
0
  GUri *origin;
905
0
  const char *name, *value;
906
0
  SoupCookie *cookie;
907
0
  GSList *cookies = NULL;
908
0
  SoupMessageHeadersIter iter;
909
910
0
  origin = soup_message_get_uri (msg);
911
912
  /* We have to use soup_message_headers_iter rather than
913
   * soup_message_headers_get_list() since Set-Cookie isn't
914
   * properly mergeable/unmergeable.
915
   */
916
0
  soup_message_headers_iter_init (&iter, soup_message_get_response_headers (msg));
917
0
  while (soup_message_headers_iter_next (&iter, &name, &value)) {
918
0
    if (g_ascii_strcasecmp (name, "Set-Cookie") != 0)
919
0
      continue;
920
921
0
    cookie = parse_one_cookie (value, origin);
922
0
    if (cookie)
923
0
      cookies = g_slist_prepend (cookies, cookie);
924
0
  }
925
0
  return g_slist_reverse (cookies);
926
0
}
927
928
/**
929
 * soup_cookies_from_request:
930
 * @msg: a #SoupMessage containing a "Cookie" request header
931
 *
932
 * Parses @msg's Cookie request header and returns a [struct@GLib.SList] of
933
 * `SoupCookie`s.
934
 *
935
 * As the "Cookie" header, unlike "Set-Cookie", only contains cookie names and
936
 * values, none of the other [struct@Cookie] fields will be filled in. (Thus, you
937
 * can't generally pass a cookie returned from this method directly to
938
 * [func@cookies_to_response].)
939
 *
940
 * Returns: (element-type SoupCookie) (transfer full): a #GSList of
941
 *   `SoupCookie`s, which can be freed with [method@Cookie.free].
942
 **/
943
GSList *
944
soup_cookies_from_request (SoupMessage *msg)
945
0
{
946
0
  SoupCookie *cookie;
947
0
  GSList *cookies = NULL;
948
0
  GHashTable *params;
949
0
  GHashTableIter iter;
950
0
  gpointer name, value;
951
0
  const char *header;
952
953
0
  header = soup_message_headers_get_one_common (soup_message_get_request_headers (msg), SOUP_HEADER_COOKIE);
954
0
  if (!header)
955
0
    return NULL;
956
957
0
  params = soup_header_parse_semi_param_list (header);
958
0
  g_hash_table_iter_init (&iter, params);
959
0
  while (g_hash_table_iter_next (&iter, &name, &value)) {
960
0
    if (name && value) {
961
0
      cookie = cookie_new_internal (name, value,
962
0
                  NULL, NULL, 0);
963
0
      cookies = g_slist_prepend (cookies, cookie);
964
0
    }
965
0
  }
966
0
  soup_header_free_param_list (params);
967
968
0
  return g_slist_reverse (cookies);
969
0
}
970
971
/**
972
 * soup_cookies_to_response:
973
 * @cookies: (element-type SoupCookie): a #GSList of [struct@Cookie]
974
 * @msg: a #SoupMessage
975
 *
976
 * Appends a "Set-Cookie" response header to @msg for each cookie in
977
 * @cookies.
978
 *
979
 * This is in addition to any other "Set-Cookie" headers
980
 * @msg may already have.
981
 **/
982
void
983
soup_cookies_to_response (GSList *cookies, SoupMessage *msg)
984
0
{
985
0
  GString *header;
986
987
0
  header = g_string_new (NULL);
988
0
  while (cookies) {
989
0
    serialize_cookie (cookies->data, header, TRUE);
990
0
    soup_message_headers_append_common (soup_message_get_response_headers (msg),
991
0
                                                    SOUP_HEADER_SET_COOKIE, header->str);
992
0
    g_string_truncate (header, 0);
993
0
    cookies = cookies->next;
994
0
  }
995
0
  g_string_free (header, TRUE);
996
0
}
997
998
/**
999
 * soup_cookies_to_request:
1000
 * @cookies: (element-type SoupCookie): a #GSList of [struct@Cookie]
1001
 * @msg: a #SoupMessage
1002
 *
1003
 * Adds the name and value of each cookie in @cookies to @msg's
1004
 * "Cookie" request.
1005
 *
1006
 * If @msg already has a "Cookie" request header, these cookies will be appended
1007
 * to the cookies already present. Be careful that you do not append the same
1008
 * cookies twice, eg, when requeuing a message.
1009
 **/
1010
void
1011
soup_cookies_to_request (GSList *cookies, SoupMessage *msg)
1012
0
{
1013
0
  GString *header;
1014
1015
0
  header = g_string_new (soup_message_headers_get_one_common (soup_message_get_request_headers (msg),
1016
0
                                                                    SOUP_HEADER_COOKIE));
1017
0
  while (cookies) {
1018
0
    serialize_cookie (cookies->data, header, FALSE);
1019
0
    cookies = cookies->next;
1020
0
  }
1021
0
  soup_message_headers_replace_common (soup_message_get_request_headers (msg),
1022
0
                                             SOUP_HEADER_COOKIE, header->str);
1023
0
  g_string_free (header, TRUE);
1024
0
}
1025
1026
/**
1027
 * soup_cookies_free: (skip)
1028
 * @cookies: (element-type SoupCookie): a #GSList of [struct@Cookie]
1029
 *
1030
 * Frees @cookies.
1031
 **/
1032
void
1033
soup_cookies_free (GSList *cookies)
1034
0
{
1035
0
  g_slist_free_full (cookies, (GDestroyNotify)soup_cookie_free);
1036
0
}
1037
1038
/**
1039
 * soup_cookies_to_cookie_header:
1040
 * @cookies: (element-type SoupCookie): a #GSList of [struct@Cookie]
1041
 *
1042
 * Serializes a [struct@GLib.SList] of [struct@Cookie] into a string suitable for
1043
 * setting as the value of the "Cookie" header.
1044
 *
1045
 * Returns: the serialization of @cookies
1046
 **/
1047
char *
1048
soup_cookies_to_cookie_header (GSList *cookies)
1049
0
{
1050
0
  GString *str;
1051
1052
0
  g_return_val_if_fail (cookies != NULL, NULL);
1053
1054
0
  str = g_string_new (NULL);
1055
0
  while (cookies) {
1056
0
    serialize_cookie (cookies->data, str, FALSE);
1057
0
    cookies = cookies->next;
1058
0
  }
1059
1060
0
  return g_string_free (str, FALSE);
1061
0
}
1062
1063
/**
1064
 * soup_cookie_applies_to_uri:
1065
 * @cookie: a #SoupCookie
1066
 * @uri: a #GUri
1067
 *
1068
 * Tests if @cookie should be sent to @uri.
1069
 *
1070
 * (At the moment, this does not check that @cookie's domain matches
1071
 * @uri, because it assumes that the caller has already done that.
1072
 * But don't rely on that; it may change in the future.)
1073
 *
1074
 * Returns: %TRUE if @cookie should be sent to @uri, %FALSE if not
1075
 **/
1076
gboolean
1077
soup_cookie_applies_to_uri (SoupCookie *cookie, GUri *uri)
1078
0
{
1079
0
  int plen;
1080
1081
0
        g_return_val_if_fail (cookie != NULL, FALSE);
1082
0
        g_return_val_if_fail (uri != NULL, FALSE);
1083
1084
0
  if (cookie->secure && !soup_uri_is_https (uri))
1085
0
    return FALSE;
1086
1087
0
  if (cookie->expires && soup_date_time_is_past (cookie->expires))
1088
0
    return FALSE;
1089
1090
0
  plen = strlen (cookie->path);
1091
0
  if (plen == 0)
1092
0
    return TRUE;
1093
1094
0
        GUri *normalized_uri = soup_uri_copy_with_normalized_flags (uri);
1095
0
        const char *uri_path = g_uri_get_path (normalized_uri);
1096
0
  if (strncmp (cookie->path, uri_path, plen) != 0 ||
1097
0
      (cookie->path[plen - 1] != '/' && uri_path[plen] &&
1098
0
             uri_path[plen] != '/')) {
1099
0
                     g_uri_unref (normalized_uri);
1100
0
                     return FALSE;
1101
0
        }
1102
1103
0
        g_uri_unref (normalized_uri);
1104
0
  return TRUE;
1105
0
}
1106
1107
/**
1108
 * soup_cookie_equal:
1109
 * @cookie1: a #SoupCookie
1110
 * @cookie2: a #SoupCookie
1111
 *
1112
 * Tests if @cookie1 and @cookie2 are equal.
1113
 *
1114
 * Note that currently, this does not check that the cookie domains
1115
 * match. This may change in the future.
1116
 *
1117
 * Returns: whether the cookies are equal.
1118
 */
1119
gboolean
1120
soup_cookie_equal (SoupCookie *cookie1, SoupCookie *cookie2)
1121
0
{
1122
0
  g_return_val_if_fail (cookie1, FALSE);
1123
0
  g_return_val_if_fail (cookie2, FALSE);
1124
1125
0
  return (!strcmp (cookie1->name, cookie2->name) &&
1126
0
    !strcmp (cookie1->value, cookie2->value) &&
1127
0
    !g_strcmp0 (cookie1->path, cookie2->path));
1128
0
}