Coverage Report

Created: 2026-11-12 06:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wget2/libwget/cookie_parse.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 parsing routines
22
 *
23
 * Changelog
24
 * 23.10.2012  Tim Ruehsen  created
25
 * 14.08.2019  Tim Ruehsen  split out from cookie.c
26
 *
27
 * see https://tools.ietf.org/html/rfc6265
28
 *
29
 */
30
31
#include <config.h>
32
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 <inttypes.h>
40
41
#include <c-ctype.h>
42
#include <xstrtol.h>
43
44
#include <wget.h>
45
#include "private.h"
46
#include "cookie.h"
47
48
bool cookie_domain_match(const char *domain, const char *host)
49
4.87k
{
50
4.87k
  size_t domain_length, host_length;
51
4.87k
  const char *p;
52
53
4.87k
  debug_printf("domain_match(%s,%s)\n", domain, host);
54
55
4.87k
  if (!strcmp(domain, host))
56
0
    return true; // an exact match
57
58
4.87k
  domain_length = strlen(domain);
59
4.87k
  host_length = strlen(host);
60
61
4.87k
  if (domain_length >= host_length)
62
4.74k
    return false; // host is too short
63
64
124
  p = host + host_length - domain_length;
65
124
  if (!strcmp(p, domain) && p[-1] == '.')
66
4
    return true;
67
68
120
  return false;
69
124
}
70
71
bool cookie_path_match(const char *cookie_path, const char *request_path)
72
1.00k
{
73
1.00k
  const char *last_slash;
74
1.00k
  size_t cookie_path_length, iri_path_length;
75
1.00k
  bool cookie_path_slash = false;
76
77
1.00k
  if (*cookie_path == '/') {
78
1.00k
    cookie_path++;
79
1.00k
    cookie_path_slash = true;
80
1.00k
  }
81
82
1.00k
  if (request_path && *request_path == '/')
83
0
    request_path++;
84
85
1.00k
  debug_printf("path_match(/%s,/%s)\n", cookie_path, request_path ? request_path : "");
86
87
  // algorithm as described in RFC 6265 5.1.4
88
89
//  if (!request_path || *request_path != '/' || !(last_slash = strrchr(request_path + 1, '/'))) {
90
//    request_path = "/";
91
//    iri_path_length = 1;
92
1.00k
  if (!request_path || !(last_slash = strrchr(request_path, '/'))) {
93
1.00k
    request_path = "";
94
1.00k
    iri_path_length = 0;
95
1.00k
  } else {
96
0
    iri_path_length = last_slash - request_path;
97
0
  }
98
99
1.00k
  cookie_path_length = strlen(cookie_path);
100
101
1.00k
  if (iri_path_length < cookie_path_length)
102
    // cookie-path is not a prefix of request-path
103
28
    return false;
104
105
979
  if (iri_path_length == 0 && cookie_path_length == 0)
106
    // slash matches slash
107
979
    return true;
108
109
0
  if (!strncmp(cookie_path, request_path, cookie_path_length)) {
110
0
    if (!request_path[cookie_path_length])
111
      // the cookie-path and the request-path are identical
112
0
      return true;
113
114
0
    if ((cookie_path_length > 0 && cookie_path[cookie_path_length - 1] == '/') || cookie_path_slash)
115
      // the cookie-path is a prefix of the request-path, and the last
116
      // character of the cookie-path is %x2F ("/").
117
0
      return true;
118
119
0
    if (request_path[cookie_path_length] == '/')
120
      // the cookie-path is a prefix of the request-path, and the first
121
      // character of the request-path that is not included in the cookie-
122
      // path is a %x2F ("/") character.
123
0
      return true;
124
0
  }
125
126
0
  return false;
127
0
}
128
129
wget_cookie *wget_cookie_init(wget_cookie *cookie)
130
13.4k
{
131
13.4k
  if (!cookie) {
132
10.5k
    cookie = wget_calloc(1, sizeof(wget_cookie));
133
10.5k
    if (!cookie)
134
0
      return NULL;
135
10.5k
  } else
136
2.90k
    memset(cookie, 0, sizeof(*cookie));
137
138
13.4k
  cookie->last_access = cookie->creation = time(NULL);
139
140
13.4k
  return cookie;
141
13.4k
}
142
143
void wget_cookie_deinit(wget_cookie *cookie)
144
11.1k
{
145
11.1k
  if (cookie) {
146
11.1k
    xfree(cookie->name);
147
11.1k
    xfree(cookie->value);
148
11.1k
    xfree(cookie->domain);
149
11.1k
    xfree(cookie->path);
150
11.1k
  }
151
11.1k
}
152
153
void wget_cookie_free(wget_cookie **cookie)
154
8.21k
{
155
8.21k
  if (cookie) {
156
8.21k
    wget_cookie_deinit(*cookie);
157
8.21k
    xfree(*cookie);
158
8.21k
  }
159
8.21k
}
160
161
// for vector destruction
162
void cookie_free(void *cookie)
163
2.49k
{
164
2.49k
  if (cookie) {
165
2.49k
    wget_cookie_deinit(cookie);
166
2.49k
    xfree(cookie);
167
2.49k
  }
168
2.49k
}
169
170
/*
171
int wget_cookie_equals(wget_cookie *cookie1, wget_cookie *cookie2)
172
{
173
  if (!cookie1)
174
    return !cookie2;
175
176
  if (!cookie2)
177
    return 0;
178
179
  if (wget_strcmp(cookie1->name, cookie2->name) ||
180
    wget_strcmp(cookie1->value, cookie2->value) ||
181
    wget_strcmp(cookie1->domain, cookie2->domain) ||
182
    wget_strcmp(cookie1->path, cookie2->path) ||
183
    cookie1->domain_dot != cookie2->domain_dot ||
184
    cookie1->normalized != cookie2->normalized ||
185
    cookie1->persistent != cookie2->persistent ||
186
    cookie1->host_only != cookie2->host_only ||
187
    cookie1->secure_only != cookie2->secure_only ||
188
    cookie1->http_only != cookie2->http_only)
189
  {
190
    return 0;
191
  }
192
193
  return 1;
194
}
195
*/
196
197
char *wget_cookie_to_setcookie(wget_cookie *cookie)
198
7.34k
{
199
7.34k
  char expires[32] = "";
200
201
7.34k
  if (!cookie)
202
3.72k
    return wget_strdup("(null)");
203
204
3.61k
  if (cookie->expires)
205
105
    wget_http_print_date(cookie->expires, expires, sizeof(expires)); // date format from RFC 6265
206
207
3.61k
  return wget_aprintf("%s=%s%s%s%s%s; domain=%s%s%s%s",
208
3.61k
    cookie->name, cookie->value,
209
3.61k
    *expires ? "; expires=" : "", *expires ? expires : "",
210
3.61k
    cookie->path ? "; path=" : "", cookie->path ? cookie->path : "",
211
3.61k
    cookie->host_only ? "" : ".", cookie->domain,
212
3.61k
    cookie->http_only ? "; HttpOnly" : "",
213
3.61k
    cookie->secure_only ? "; Secure" : "");
214
7.34k
}
215
216
/*
217
 RFC 6265
218
219
 set-cookie-header = "Set-Cookie:" SP set-cookie-string
220
 set-cookie-string = cookie-pair *( ";" SP cookie-av )
221
 cookie-pair       = cookie-name "=" cookie-value
222
 cookie-name       = token
223
 cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
224
 cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
225
                       ; US-ASCII characters excluding CTLs,
226
                       ; whitespace DQUOTE, comma, semicolon,
227
                       ; and backslash
228
 token             = <token, defined in [RFC2616], Section 2.2>
229
230
 cookie-av         = expires-av / max-age-av / domain-av /
231
                     path-av / secure-av / httponly-av /
232
                     extension-av
233
 expires-av        = "Expires=" sane-cookie-date
234
 sane-cookie-date  = <rfc1123-date, defined in [RFC2616], Section 3.3.1>
235
 max-age-av        = "Max-Age=" non-zero-digit *DIGIT
236
                       ; In practice, both expires-av and max-age-av
237
                       ; are limited to dates representable by the
238
                       ; user agent.
239
 non-zero-digit    = %x31-39
240
                       ; digits 1 through 9
241
 domain-av         = "Domain=" domain-value
242
 domain-value      = <subdomain>
243
                       ; defined in [RFC1034], Section 3.5, as
244
                       ; enhanced by [RFC1123], Section 2.1
245
 path-av           = "Path=" path-value
246
 path-value        = <any CHAR except CTLs or ";">
247
 secure-av         = "Secure"
248
 httponly-av       = "HttpOnly"
249
 extension-av      = <any CHAR except CTLs or ";">
250
*/
251
const char *wget_cookie_parse_setcookie(const char *s, wget_cookie **_cookie)
252
10.5k
{
253
10.5k
  const char *name, *p;
254
10.5k
  wget_cookie *cookie = wget_cookie_init(NULL);
255
256
  // remove leading whitespace from cookie name
257
11.3k
  while (c_isspace(*s)) s++;
258
259
  // s = wget_http_parse_token(s, &cookie->name);
260
  // also accept UTF-8 (NON-ASCII) characters in cookie name
261
29.6k
  for (p = s; (*s >= 32 && *s <= 126 && *s != '=' && *s != ';') || *s < 0; s++);
262
263
  // remove trailing whitespace from cookie name
264
11.0k
  while (s > p && c_isspace(s[-1])) s--;
265
10.5k
  cookie->name = wget_strmemdup(p, s - p);
266
267
  // advance to next delimiter
268
11.2k
  while (c_isspace(*s)) s++;
269
270
10.5k
  if (cookie->name && *cookie->name && *s == '=') {
271
    // *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
272
273
    // skip over delimiter and remove leading whitespace from cookie value
274
10.5k
    for (s++; c_isspace(*s);) s++;
275
276
/* RFC compliance is too strict
277
    if (*s == '\"')
278
      s++;
279
    // cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
280
    for (p = s; *s > 32 && *s <= 126 && *s != '\\' && *s != ',' && *s != ';' && *s != '\"'; s++);
281
*/
282
283
    // also accept UTF-8 (NON-ASCII) characters in cookie value
284
27.4k
    for (p = s; (*s >= 32 && *s <= 126 && *s != ';') || *s < 0; s++);
285
286
    // remove trailing whitespace from cookie value
287
10.0k
    while (s > p && c_isspace(s[-1])) s--;
288
289
9.50k
    cookie->value = wget_strmemdup(p, s - p);
290
291
26.1k
    do {
292
      // find next delimiter
293
32.7k
      while (*s && *s != ';') s++;
294
26.1k
      if (!*s) break;
295
296
      // skip delimiter and remove leading spaces from attribute name
297
24.9k
      for (s++; c_isspace(*s);) s++;
298
24.4k
      if (!*s) break;
299
300
24.1k
      s = wget_http_parse_token(s, &name);
301
302
24.1k
      if (name) {
303
        // find next delimiter
304
26.0k
        while (*s && *s != '=' && *s != ';') s++;
305
        // if (!*s) break;
306
307
24.1k
        if (*s == '=') {
308
          // find end of value
309
19.8k
          for (s++; c_isspace(*s);) s++;
310
205k
          for (p = s; (*s >= 32 && *s <= 126 && *s != ';') || *s < 0; s++);
311
312
19.2k
          if (!wget_strcasecmp_ascii(name, "expires")) {
313
6.90k
            cookie->expires = wget_http_parse_full_date(p);
314
12.3k
          } else if (!wget_strcasecmp_ascii(name, "max-age")) {
315
2.05k
            long offset;
316
2.05k
            if (xstrtol(p, NULL, 10, &offset, NULL) != LONGINT_OK)
317
519
              offset = 0;
318
319
            // limit offset to avoid integer overflow
320
2.05k
            if (offset > INT_MAX)
321
260
              offset = INT_MAX;
322
1.79k
            else if (offset < 0)
323
616
              offset = 0;
324
325
2.05k
            cookie->maxage = time(NULL) + offset;
326
10.2k
          } else if (!wget_strcasecmp_ascii(name, "domain")) {
327
6.50k
            if (p != s) {
328
6.12k
              if (*p == '.') { // RFC 6265 5.2.3
329
976
                do { p++; } while (*p == '.');
330
539
                cookie->domain_dot = 1;
331
539
              } else
332
5.58k
                cookie->domain_dot = 0;
333
334
              // remove trailing whitespace from attribute value
335
6.50k
              while (s > p && c_isspace(s[-1])) s--;
336
337
6.12k
              xfree(cookie->domain);
338
6.12k
              cookie->domain = wget_strmemdup(p, s - p);
339
6.12k
            }
340
6.50k
          } else if (!wget_strcasecmp_ascii(name, "path")) {
341
            // remove trailing whitespace from attribute value
342
2.03k
            while (s > p && c_isspace(s[-1])) s--;
343
344
1.55k
            xfree(cookie->path);
345
1.55k
            cookie->path = wget_strmemdup(p, s - p);
346
2.19k
          } else if (!wget_strcasecmp_ascii(name, "secure")) {
347
            // here we ignore the value
348
384
            cookie->secure_only = 1;
349
1.80k
          } else if (!wget_strcasecmp_ascii(name, "httponly")) {
350
            // here we ignore the value
351
384
            cookie->http_only = 1;
352
1.42k
          } else {
353
1.42k
            debug_printf("Unsupported cookie-av '%s'\n", name);
354
1.42k
          }
355
19.2k
        } else if (!wget_strcasecmp_ascii(name, "secure")) {
356
392
          cookie->secure_only = 1;
357
4.54k
        } else if (!wget_strcasecmp_ascii(name, "httponly")) {
358
396
          cookie->http_only = 1;
359
4.14k
        } else {
360
4.14k
          debug_printf("Unsupported cookie-av '%s'\n", name);
361
4.14k
        }
362
363
24.1k
        xfree(name);
364
24.1k
      }
365
24.1k
    } while (*s);
366
367
9.50k
  } else {
368
1.02k
    wget_cookie_free(&cookie);
369
1.02k
    error_printf(_("Cookie without name or assignment ignored\n"));
370
1.02k
  }
371
372
10.5k
  if (_cookie)
373
10.5k
    *_cookie = cookie;
374
0
  else
375
0
    wget_cookie_free(&cookie);
376
377
10.5k
  return s;
378
10.5k
}
379
380
// normalize/sanitize and store cookies
381
static int cookie_normalize_cookie(const wget_iri *iri, wget_cookie *cookie)
382
7.40k
{
383
/*
384
  debug_printf("normalize cookie %s=%s\n", cookie->name, cookie->value);
385
  debug_printf("<  %s=%s\n", cookie->name, cookie->value);
386
  debug_printf("<  expires=%" PRId64" max-age=%" PRId64 "\n", cookie->expires, cookie->maxage);
387
  debug_printf("<  domain=%s\n", cookie->domain);
388
  debug_printf("<  path=%s\n", cookie->path);
389
  debug_printf("<  normalized=%d persistent=%d hostonly=%d secure=%d httponly=%d\n",
390
    cookie->normalized, cookie->persistent, cookie->host_only, cookie->secure_only, cookie->http_only);
391
*/
392
7.40k
  if (!cookie)
393
0
    return -1;
394
395
7.40k
  cookie->normalized = 0;
396
397
7.40k
  if (cookie->maxage)
398
446
    cookie->expires = cookie->maxage;
399
400
7.40k
  cookie->persistent = cookie->expires != 0;
401
402
  // convert domain to lowercase
403
7.40k
  wget_strtolower((char *)cookie->domain);
404
405
7.40k
  if (iri) {
406
    // cookies comes from a HTTP header and needs checking
407
408
    // RFC 6265 4.1.2.5. The Secure Attribute
409
    // If the secure-only-flag is true, then the user agent MUST NOT save the
410
    // cookie unless the request-uri's scheme is https.
411
7.22k
    if (cookie->secure_only && iri->scheme != WGET_IRI_SCHEME_HTTPS) {
412
32
      debug_printf("Secure cookie requires secure origin: %s %s\n", cookie->name, iri->host);
413
32
      return -1; // ignore cookie
414
32
    }
415
416
    // check prefixes as proposed in https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes-00
417
7.19k
    if (!wget_strncmp(cookie->name, "__Secure-", 9)) {
418
2
      if (!cookie->secure_only || iri->scheme != WGET_IRI_SCHEME_HTTPS) {
419
2
        debug_printf("Cookie prefix requires secure origin: %s %s\n", cookie->name, iri->host);
420
2
        return -1; // ignore cookie
421
2
      }
422
2
    }
423
7.19k
    else if (!wget_strncmp(cookie->name, "__Host-", 7)) {
424
2
      if (!cookie->secure_only || iri->scheme != WGET_IRI_SCHEME_HTTPS) {
425
2
        debug_printf("Cookie prefix requires secure origin: %s %s\n", cookie->name, iri->host);
426
2
        return -1; // ignore cookie
427
2
      }
428
0
      if (!cookie->host_only) {
429
0
        debug_printf("Cookie prefix requires hostonly flag: %s %s\n", cookie->name, iri->host);
430
0
        return -1; // ignore cookie
431
0
      }
432
0
      if (wget_strcmp(cookie->path, "/")) {
433
0
        debug_printf("Cookie prefix requires path \"/\": %s %s\n", cookie->name, iri->host);
434
0
        return -1; // ignore cookie
435
0
      }
436
0
    }
437
438
7.19k
    if (cookie->domain && *cookie->domain) {
439
4.87k
      if (!strcmp(cookie->domain, iri->host)) {
440
4
        cookie->host_only = 1;
441
4.87k
      } else if (cookie_domain_match(cookie->domain, iri->host)) {
442
4
        cookie->host_only = 0;
443
4.86k
      } else {
444
4.86k
        debug_printf("Domain mismatch: %s %s\n", cookie->domain, iri->host);
445
4.86k
        return -1; // ignore cookie
446
4.86k
      }
447
4.87k
    } else {
448
2.31k
      xfree(cookie->domain);
449
2.31k
      cookie->domain = wget_strdup(iri->host);
450
2.31k
      cookie->host_only = 1;
451
2.31k
    }
452
453
2.32k
    if (!cookie->path || *cookie->path != '/') {
454
2.26k
      const char *p = iri->path ? strrchr(iri->path, '/') : NULL;
455
456
2.26k
      xfree(cookie->path);
457
458
2.26k
      if (p && p != iri->path) {
459
0
        cookie->path = wget_strmemdup(iri->path, p - iri->path);
460
2.26k
      } else {
461
2.26k
        cookie->path = wget_strdup("/");
462
        // err_printf(_("Unexpected URI without '/': %s\n"), iri->path);
463
        // return -1; // ignore cookie
464
2.26k
      }
465
2.26k
    }
466
2.32k
  }
467
468
2.49k
  cookie->normalized = 1;
469
470
/*
471
  debug_printf(">  %s=%s\n", cookie->name, cookie->value);
472
  debug_printf(">  expires=%" PRId64 " max-age=%" PRId64 "\n", cookie->expires, cookie->maxage);
473
  debug_printf(">  domain=%s\n", cookie->domain);
474
  debug_printf(">  path=%s\n", cookie->path);
475
  debug_printf(">  normalized=%d persistent=%d hostonly=%d secure=%d httponly=%d\n",
476
    cookie->normalized, cookie->persistent, cookie->host_only, cookie->secure_only, cookie->http_only);
477
*/
478
479
2.49k
  return 0;
480
7.40k
}
481
482
int wget_cookie_normalize(const wget_iri *iri, wget_cookie *cookie)
483
3.78k
{
484
//  wget_thread_mutex_lock(&_cookies_mutex);
485
486
3.78k
  int ret = cookie_normalize_cookie(iri, cookie);
487
488
//  wget_thread_mutex_unlock(&_cookies_mutex);
489
490
3.78k
  return ret;
491
3.78k
}
492
493
void wget_cookie_normalize_cookies(const wget_iri *iri, const wget_vector *cookies)
494
3.61k
{
495
//  wget_thread_mutex_lock(&_cookies_mutex);
496
497
7.22k
  for (int it = 0; it < wget_vector_size(cookies); it++)
498
3.61k
    cookie_normalize_cookie(iri, wget_vector_get(cookies, it));
499
500
//  wget_thread_mutex_unlock(&_cookies_mutex);
501
3.61k
}