Coverage Report

Created: 2026-11-12 06:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wget2/libwget/cookie.c
Line
Count
Source
1
/*
2
 * Copyright (c) 2012 Tim Ruehsen
3
 * Copyright (c) 2015-2026 Free Software Foundation, Inc.
4
 *
5
 * This file is part of libwget.
6
 *
7
 * Libwget is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Lesser General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * Libwget is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Lesser General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Lesser General Public License
18
 * along with libwget.  If not, see <https://www.gnu.org/licenses/>.
19
 *
20
 *
21
 * Cookie database routines
22
 *
23
 * Changelog
24
 * 23.10.2012  Tim Ruehsen  created
25
 *
26
 * see https://tools.ietf.org/html/rfc6265
27
 *
28
 */
29
30
#include <config.h>
31
32
#include <xstrtol.h>
33
#include <stdio.h>
34
#include <stdlib.h>
35
#include <string.h>
36
#include <limits.h>
37
#include <ctype.h>
38
#include <time.h>
39
#include <errno.h>
40
#include <inttypes.h>
41
#ifdef WITH_LIBPSL
42
#  include <libpsl.h>
43
#endif
44
45
#include <wget.h>
46
#include "private.h"
47
#include "cookie.h"
48
49
struct wget_cookie_db_st {
50
  wget_vector *
51
    cookies;
52
#ifdef WITH_LIBPSL
53
  psl_ctx_t
54
    *psl; // libpsl Publix Suffix List context
55
#endif
56
  wget_thread_mutex
57
    mutex;
58
  unsigned int
59
    age;
60
  bool
61
    keep_session_cookies : 1; // whether or not session cookies are saved
62
};
63
64
// by this kind of sorting, we can easily see if a domain matches or not (match = supercookie !)
65
66
int wget_cookie_db_load_psl(wget_cookie_db *cookie_db, const char *fname)
67
11.0k
{
68
11.0k
#ifdef WITH_LIBPSL
69
11.0k
  if (!cookie_db)
70
3.67k
    return -1;
71
72
7.34k
  if (fname) {
73
3.67k
    psl_ctx_t *psl = psl_load_file(fname);
74
75
3.67k
    psl_free(cookie_db->psl);
76
3.67k
    cookie_db->psl = psl;
77
3.67k
  } else {
78
3.67k
    psl_free(cookie_db->psl);
79
3.67k
    cookie_db->psl = NULL;
80
3.67k
  }
81
82
7.34k
  return 0;
83
#else
84
  (void) cookie_db; (void) fname;
85
  return -1;
86
#endif
87
11.0k
}
88
89
// this is how we sort the entries in a cookie db
90
WGET_GCC_NONNULL_ALL WGET_GCC_PURE
91
static int compare_cookie(const wget_cookie *c1, const wget_cookie *c2)
92
1.15k
{
93
1.15k
  int n;
94
95
1.15k
  if (!(n = wget_strcmp(c1->domain, c2->domain))) {
96
1.15k
    if (!(n = wget_strcmp(c1->name, c2->name))) {
97
1.15k
      n = wget_strcmp(c1->path, c2->path);
98
1.15k
    }
99
1.15k
  }
100
101
1.15k
  return n;
102
1.15k
}
103
104
// this is how we sort the entries when constructing a Cookie: header field
105
WGET_GCC_NONNULL_ALL WGET_GCC_PURE
106
static int compare_cookie2(const wget_cookie *c1, const wget_cookie *c2)
107
0
{
108
  // RFC 6265 5.4 demands sorting by 1. longer paths first, 2. earlier creation time first.
109
0
  size_t len1 = strlen(c1->path);
110
0
  size_t len2 = strlen(c2->path);
111
112
0
  if (len1 < len2)
113
0
    return 1;
114
115
0
  if (len1 > len2)
116
0
    return -1;
117
118
0
  if (c1->sort_age < c2->sort_age)
119
0
    return -1;
120
121
0
  if (c1->sort_age > c2->sort_age)
122
0
    return 1;
123
124
0
  return 0;
125
0
}
126
127
int wget_cookie_check_psl(const wget_cookie_db *cookie_db, const wget_cookie *cookie)
128
6.28k
{
129
//  wget_thread_mutex_lock(&_cookies_mutex);
130
131
6.28k
#ifdef WITH_LIBPSL
132
6.28k
  int ret;
133
134
6.28k
  if (cookie_db->psl)
135
6.28k
    ret = psl_is_public_suffix(cookie_db->psl, cookie->domain) ? -1 : 0;
136
0
  else
137
0
    ret = 0;
138
#else
139
  (void) cookie_db; (void) cookie;
140
  int ret = 0;
141
#endif
142
143
//  wget_thread_mutex_unlock(&_cookies_mutex);
144
145
6.28k
  return ret;
146
6.28k
}
147
148
int wget_cookie_store_cookie(wget_cookie_db *cookie_db, wget_cookie *cookie)
149
11.0k
{
150
11.0k
  wget_cookie *old;
151
11.0k
  int pos;
152
153
11.0k
  if (!cookie)
154
3.67k
    return WGET_E_INVALID;
155
156
7.40k
  if (!cookie_db) {
157
0
    wget_cookie_free(&cookie);
158
0
    return WGET_E_INVALID;
159
0
  }
160
161
7.40k
  debug_printf("got cookie %s=%s\n", cookie->name, cookie->value);
162
163
7.40k
  if (!cookie->normalized) {
164
4.90k
    debug_printf("cookie '%s' dropped, it wasn't normalized\n", cookie->name);
165
4.90k
    wget_cookie_free(&cookie);
166
4.90k
    return WGET_E_INVALID;
167
4.90k
  }
168
169
2.49k
  if (wget_cookie_check_psl(cookie_db, cookie) != 0) {
170
4
    debug_printf("cookie '%s' dropped, domain '%s' is a public suffix\n", cookie->name, cookie->domain);
171
4
    wget_cookie_free(&cookie);
172
4
    return WGET_E_INVALID;
173
4
  }
174
175
2.49k
  wget_thread_mutex_lock(cookie_db->mutex);
176
177
2.49k
  old = wget_vector_get(cookie_db->cookies, pos = wget_vector_find(cookie_db->cookies, cookie));
178
179
2.49k
  if (old) {
180
1.15k
    debug_printf("replace old cookie %s=%s\n", cookie->name, cookie->value);
181
1.15k
    cookie->creation = old->creation;
182
1.15k
    cookie->sort_age = old->sort_age;
183
1.15k
    wget_vector_replace(cookie_db->cookies, cookie, pos);
184
1.33k
  } else {
185
1.33k
    debug_printf("store new cookie %s=%s\n", cookie->name, cookie->value);
186
1.33k
    cookie->sort_age = ++cookie_db->age;
187
1.33k
    wget_vector_insert_sorted(cookie_db->cookies, cookie);
188
1.33k
  }
189
190
2.49k
  wget_thread_mutex_unlock(cookie_db->mutex);
191
192
2.49k
  return WGET_E_SUCCESS;
193
2.49k
}
194
195
void wget_cookie_store_cookies(wget_cookie_db *cookie_db, wget_vector *cookies)
196
3.61k
{
197
3.61k
  if (cookie_db) {
198
3.61k
    int it;
199
200
7.22k
    for (it = 0; it < wget_vector_size(cookies); it++) {
201
3.61k
      wget_cookie *cookie = wget_vector_get(cookies, it);
202
3.61k
      wget_cookie_store_cookie(cookie_db, cookie); // takes ownership of 'cookie'
203
3.61k
    }
204
205
    // remove all 'cookie' entries without free'ing
206
3.61k
    wget_vector_clear_nofree(cookies);
207
3.61k
  }
208
3.61k
}
209
210
char *wget_cookie_create_request_header(wget_cookie_db *cookie_db, const wget_iri *iri)
211
7.28k
{
212
7.28k
  int it, init = 0;
213
7.28k
  int64_t now = time(NULL);
214
7.28k
  wget_vector *cookies = NULL;
215
7.28k
  wget_buffer buf;
216
217
7.28k
  if (!cookie_db || !iri)
218
3.67k
    return NULL;
219
220
3.61k
  debug_printf("cookie_create_request_header for host=%s path=%s\n", iri->host, iri->path);
221
222
3.61k
  wget_thread_mutex_lock(cookie_db->mutex);
223
224
4.77k
  for (it = 0; it < wget_vector_size(cookie_db->cookies); it++) {
225
1.15k
    wget_cookie *cookie = wget_vector_get(cookie_db->cookies, it);
226
1.15k
    if (!cookie)
227
0
      continue;
228
229
1.15k
    if (cookie->host_only && strcmp(cookie->domain, iri->host)) {
230
0
      debug_printf("cookie host match failed (%s,%s)\n", cookie->domain, iri->host);
231
0
      continue;
232
0
    }
233
234
1.15k
    if (!cookie->host_only && !cookie_domain_match(cookie->domain, iri->host)) {
235
0
      debug_printf("cookie domain match failed (%s,%s)\n", cookie->domain, iri->host);
236
0
      continue;
237
0
    }
238
239
1.15k
    if (cookie->expires && cookie->expires <= now) {
240
152
      debug_printf("cookie expired (%" PRIi64 " <= %" PRIi64 ")\n", cookie->expires, now);
241
152
      continue;
242
152
    }
243
244
1.00k
    if (cookie->secure_only && iri->scheme != WGET_IRI_SCHEME_HTTPS) {
245
0
      debug_printf("cookie ignored, not secure\n");
246
0
      continue;
247
0
    }
248
249
1.00k
    if (!cookie_path_match(cookie->path, iri->path)) {
250
28
      debug_printf("cookie path doesn't match (%s, %s)\n", cookie->path, iri->path);
251
28
      continue;
252
28
    }
253
254
979
    debug_printf("found %s=%s\n", cookie->name, cookie->value);
255
256
979
    if (!cookies)
257
979
      cookies = wget_vector_create(16, (wget_vector_compare_fn *) compare_cookie2);
258
259
    // collect matching cookies (just pointers, no allocation)
260
979
    wget_vector_add(cookies, cookie);
261
979
  }
262
263
  // sort cookies regarding RFC 6265
264
3.61k
  wget_vector_sort(cookies);
265
266
  // now create cookie header value
267
4.59k
  for (it = 0; it < wget_vector_size(cookies); it++) {
268
979
    wget_cookie *cookie = wget_vector_get(cookies, it);
269
979
    if (!cookie)
270
0
      continue;
271
272
979
    if (!init) {
273
979
      wget_buffer_init(&buf, NULL, 128);
274
979
      init = 1;
275
979
    }
276
277
979
    if (buf.length)
278
0
      wget_buffer_printf_append(&buf, "; %s=%s", cookie->name, cookie->value);
279
979
    else
280
979
      wget_buffer_printf_append(&buf, "%s=%s", cookie->name, cookie->value);
281
979
  }
282
283
  // free vector with free'ing the content
284
3.61k
  wget_vector_clear_nofree(cookies);
285
3.61k
  wget_vector_free(&cookies);
286
287
3.61k
  wget_thread_mutex_unlock(cookie_db->mutex);
288
289
3.61k
  return init ? buf.data : NULL;
290
7.28k
}
291
292
wget_cookie_db *wget_cookie_db_init(wget_cookie_db *cookie_db)
293
7.28k
{
294
7.28k
  if (!cookie_db) {
295
7.28k
    cookie_db = wget_calloc(1, sizeof(wget_cookie_db));
296
7.28k
    if (!cookie_db)
297
0
      return NULL;
298
7.28k
  } else
299
0
    memset(cookie_db, 0, sizeof(*cookie_db));
300
301
7.28k
  cookie_db->cookies = wget_vector_create(32, (wget_vector_compare_fn *) compare_cookie);
302
7.28k
  wget_vector_set_destructor(cookie_db->cookies, cookie_free);
303
7.28k
  wget_thread_mutex_init(&cookie_db->mutex);
304
7.28k
#ifdef WITH_LIBPSL
305
7.28k
#if ((PSL_VERSION_MAJOR > 0) || (PSL_VERSION_MAJOR == 0 && PSL_VERSION_MINOR >= 16))
306
7.28k
  cookie_db->psl = psl_latest(NULL);
307
#else
308
  cookie_db->psl = (psl_ctx_t *)psl_builtin();
309
#endif
310
7.28k
#endif
311
312
7.28k
  return cookie_db;
313
7.28k
}
314
315
void wget_cookie_db_deinit(wget_cookie_db *cookie_db)
316
16.7k
{
317
16.7k
  if (cookie_db) {
318
7.28k
#ifdef WITH_LIBPSL
319
7.28k
    psl_free(cookie_db->psl);
320
7.28k
    cookie_db->psl = NULL;
321
7.28k
#endif
322
7.28k
    wget_thread_mutex_lock(cookie_db->mutex);
323
7.28k
    wget_vector_free(&cookie_db->cookies);
324
7.28k
    wget_thread_mutex_unlock(cookie_db->mutex);
325
7.28k
    wget_thread_mutex_destroy(&cookie_db->mutex);
326
7.28k
  }
327
16.7k
}
328
329
void wget_cookie_db_free(wget_cookie_db **cookie_db)
330
16.7k
{
331
16.7k
  if (cookie_db) {
332
16.7k
    wget_cookie_db_deinit(*cookie_db);
333
16.7k
    xfree(*cookie_db);
334
16.7k
  }
335
16.7k
}
336
337
void wget_cookie_set_keep_session_cookies(wget_cookie_db *cookie_db, bool keep)
338
3.67k
{
339
3.67k
  if (cookie_db)
340
3.67k
    cookie_db->keep_session_cookies = keep;
341
3.67k
}
342
343
static int cookie_db_load(wget_cookie_db *cookie_db, FILE *fp)
344
3.61k
{
345
3.61k
  wget_cookie cookie;
346
3.61k
  char *buf = NULL, *linep, *p;
347
3.61k
  size_t bufsize = 0;
348
3.61k
  ssize_t buflen;
349
3.61k
  int64_t now = time(NULL);
350
351
7.67k
  while ((buflen = wget_getline(&buf, &bufsize, fp)) >= 0) {
352
4.06k
    linep = buf;
353
354
4.06k
    while (isspace(*linep)) linep++; // ignore leading whitespace
355
4.06k
    if (!*linep) continue; // skip empty lines
356
357
2.90k
    wget_cookie_init(&cookie);
358
359
2.90k
    if (*linep == '#') {
360
2.33k
      if (strncmp(linep, "#HttpOnly_", 10))
361
2.31k
        continue; // skip comments
362
363
16
      linep = linep + 10;
364
16
      cookie.http_only = 1;
365
572
    } else {
366
572
      cookie.http_only = 0;
367
572
    }
368
369
    // strip off \r\n
370
588
    while (buflen > 0 && (buf[buflen-1] == '\n' || buf[buflen-1] == '\r'))
371
0
      buf[--buflen] = 0;
372
373
    // parse domain
374
2.35k
    for (p = linep; *linep && *linep != '\t';) linep++;
375
588
    if (*p == '.') {
376
6
      p++;
377
6
      cookie.domain_dot = 1;
378
6
    }
379
588
    cookie.domain = wget_strmemdup(p, linep - p);
380
381
    // parse inverse host_only (FALSE: host_only=1)
382
3.52k
    for (p = *linep ? ++linep : linep; *linep && *linep != '\t';) linep++;
383
588
    cookie.host_only = wget_strncasecmp_ascii(p, "TRUE", 4);
384
385
    // parse path
386
5.32k
    for (p = *linep ? ++linep : linep; *linep && *linep != '\t';) linep++;
387
588
    if (p != linep)
388
588
      cookie.path = wget_strmemdup(p, linep - p);
389
0
    else
390
0
      cookie.path = wget_strmemdup("/", 1); // allow empty paths
391
392
    // parse secure_only
393
3.52k
    for (p = *linep ? ++linep : linep; *linep && *linep != '\t';) linep++;
394
588
    cookie.secure_only = !wget_strncasecmp_ascii(p, "TRUE", 4);
395
396
    // parse expires
397
2.80k
    for (p = *linep ? ++linep : linep; *linep && *linep != '\t';) linep++;
398
588
    long long expires;
399
588
    if (xstrtoll(p, NULL, 10, &expires, NULL) != LONGINT_OK)
400
0
      expires = 0;
401
588
    cookie.expires = (int64_t) expires;
402
403
588
    if (cookie.expires && cookie.expires <= now) {
404
      // drop expired cookie
405
0
      wget_cookie_deinit(&cookie);
406
0
      continue;
407
0
    }
408
588
    if (!cookie.expires && !cookie_db->keep_session_cookies) {
409
      // drop session cookies
410
414
      wget_cookie_deinit(&cookie);
411
414
      continue;
412
414
    }
413
414
    // parse name
415
1.26k
    for (p = *linep ? ++linep : linep; *linep && *linep != '\t';) linep++;
416
174
    if (linep == p) {
417
0
      error_printf(_("Incomplete cookie entry: %s\n"), buf);
418
0
      wget_cookie_deinit(&cookie);
419
0
      continue;
420
0
    }
421
174
    cookie.name = wget_strmemdup(p, linep - p);
422
423
    // parse value, until end of line
424
213
    for (p = *linep ? ++linep : linep; *linep;) linep++;
425
174
    cookie.value = wget_strmemdup(p, linep - p);
426
427
174
    if (wget_cookie_normalize(NULL, &cookie) == 0 && wget_cookie_check_psl(cookie_db, &cookie) == 0) {
428
      // The following wget_memdup copies pointers to allocated memory to take ownership of the cookie contents.
429
      // Thus it needs to be accomplished by wget_cookie_init(&cookie), which is done
430
      // near the top of the loop.
431
174
      wget_cookie_store_cookie(cookie_db, wget_memdup(&cookie, sizeof(cookie)));
432
174
    } else
433
0
      wget_cookie_deinit(&cookie);
434
174
  }
435
436
3.61k
  xfree(buf);
437
438
3.61k
  if (ferror(fp)) {
439
0
    return -1;
440
0
  }
441
442
3.61k
  return 0;
443
3.61k
}
444
445
int wget_cookie_db_load(wget_cookie_db *cookie_db, const char *fname)
446
7.28k
{
447
7.28k
  if (!cookie_db || !fname || !*fname)
448
3.67k
    return 0;
449
450
3.61k
  if (wget_update_file(fname, (wget_update_load_fn *) cookie_db_load, NULL, cookie_db)) {
451
0
    error_printf(_("Failed to read cookies\n"));
452
0
    return -1;
453
3.61k
  } else {
454
3.61k
    debug_printf("Fetched cookies from '%s'\n", fname);
455
3.61k
    return 0;
456
3.61k
  }
457
3.61k
}
458
459
// save the cookie store to a flat file
460
461
static int cookie_db_save(wget_cookie_db *cookie_db, FILE *fp)
462
3.61k
{
463
3.61k
  if (wget_vector_size(cookie_db->cookies) > 0) {
464
1.15k
    int it;
465
1.15k
    int64_t now = time(NULL);
466
467
1.15k
    fputs("# HTTP Cookie File\n", fp);
468
1.15k
    fputs("#Generated by libwget " PACKAGE_VERSION ". Edit at your own risk.\n\n", fp);
469
470
2.31k
    for (it = 0; it < wget_vector_size(cookie_db->cookies); it++) {
471
1.15k
      wget_cookie *cookie = wget_vector_get(cookie_db->cookies, it);
472
1.15k
      if (!cookie)
473
0
        continue;
474
475
1.15k
      if (cookie->persistent) {
476
326
        if (cookie->expires <= now)
477
152
          continue;
478
833
      } else if (!cookie_db->keep_session_cookies)
479
419
        continue;
480
481
588
      wget_fprintf(fp, "%s%s%s\t%s\t%s\t%s\t%" PRIi64 "\t%s\t%s\n",
482
588
        cookie->http_only ? "#HttpOnly_" : "",
483
588
        cookie->domain_dot ? "." : "", // compatibility, irrelevant since RFC 6562
484
588
        cookie->domain,
485
588
        cookie->host_only ? "FALSE" : "TRUE",
486
588
        cookie->path, cookie->secure_only ? "TRUE" : "FALSE",
487
588
        cookie->expires,
488
588
        cookie->name, cookie->value);
489
490
588
      if (ferror(fp))
491
0
        return -1;
492
588
    }
493
1.15k
  }
494
495
3.61k
  return 0;
496
3.61k
}
497
498
// Save the HSTS cache to a flat file
499
// Protected by flock()
500
501
int wget_cookie_db_save(wget_cookie_db *cookie_db, const char *fname)
502
7.28k
{
503
7.28k
  int size;
504
505
7.28k
  if (!cookie_db || !fname || !*fname)
506
3.67k
    return -1;
507
508
3.61k
  if (wget_update_file(fname,
509
3.61k
    (wget_update_load_fn *) cookie_db_load,
510
3.61k
    (wget_update_save_fn *) cookie_db_save, cookie_db))
511
0
  {
512
0
    error_printf(_("Failed to write cookie file '%s'\n"), fname);
513
0
    return -1;
514
0
  }
515
516
3.61k
  if ((size = wget_vector_size(cookie_db->cookies)))
517
1.15k
    debug_printf("Saved %d cookie%s into '%s'\n", size, size != 1 ? "s" : "", fname);
518
2.45k
  else
519
2.45k
    debug_printf("No cookies to save. Table is empty.\n");
520
521
3.61k
  return 0;
522
3.61k
}