Coverage Report

Created: 2026-09-14 06:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libsoup/libsoup/soup-headers.c
Line
Count
Source
1
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2
/*
3
 * soup-headers.c: HTTP message header parsing
4
 *
5
 * Copyright (C) 2001-2003, Ximian, 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-misc.h"
16
#include "soup-headers.h"
17
#include "soup-message-headers-private.h"
18
#include "soup.h"
19
20
/**
21
 * soup_headers_parse:
22
 * @str: the header string (including the Request-Line or Status-Line,
23
 *   but not the trailing blank line)
24
 * @len: length of @str
25
 * @dest: #SoupMessageHeaders to store the header values in
26
 *
27
 * Parses the headers of an HTTP request or response in @str and
28
 * stores the results in @dest.
29
 *
30
 * Beware that @dest may be modified even on failure.
31
 *
32
 * This is a low-level method; normally you would use
33
 * [func@headers_parse_request] or [func@headers_parse_response].
34
 *
35
 * Returns: success or failure
36
 **/
37
gboolean
38
soup_headers_parse (const char *str, int len, SoupMessageHeaders *dest)
39
776
{
40
776
  const char *headers_start;
41
776
  char *headers_copy, *name, *name_end, *value, *value_end;
42
776
  char *eol, *sol, *p;
43
776
  gsize copy_len;
44
776
  gboolean success = FALSE;
45
46
776
  g_return_val_if_fail (str != NULL, FALSE);
47
776
  g_return_val_if_fail (dest != NULL, FALSE);
48
49
776
        if (len <= 0 || len > MAX_HEADERS_BUFFER_SIZE)
50
6
                return FALSE;
51
52
  /* As per RFC 2616 section 19.3, we treat '\n' as the
53
   * line terminator, and '\r', if it appears, merely as
54
   * ignorable trailing whitespace.
55
   */
56
57
  /* No '\0's are allowed */
58
770
  if (memchr (str, '\0', len))
59
12
    return FALSE;
60
61
  /* Skip over the Request-Line / Status-Line */
62
758
  headers_start = memchr (str, '\n', len);
63
758
  if (!headers_start)
64
0
    return FALSE;
65
66
  /* We work on a copy of the headers, which we can write '\0's
67
   * into, so that we don't have to individually g_strndup and
68
   * then g_free each header name and value.
69
   */
70
758
  copy_len = len - (headers_start - str);
71
758
  headers_copy = g_malloc (copy_len + 1);
72
758
  memcpy (headers_copy, headers_start, copy_len);
73
758
  headers_copy[copy_len] = '\0';
74
758
  value_end = headers_copy;
75
76
1.17M
  while (*(value_end + 1)) {
77
1.16M
    name = value_end + 1;
78
1.16M
    name_end = strchr (name, ':');
79
80
    /* Reject if there is no ':', or the header name is
81
     * empty, or it contains whitespace.
82
     */
83
1.16M
    if (!name_end ||
84
1.16M
        name_end == name ||
85
1.16M
        name + strcspn (name, " \t\r\n") < name_end) {
86
      /* Ignore this line. Note that if it has
87
       * continuation lines, we'll end up ignoring
88
       * them too since they'll start with spaces.
89
       */
90
85.4k
      value_end = strchr (name, '\n');
91
85.4k
      if (!value_end)
92
51
        goto done;
93
85.3k
      continue;
94
85.4k
    }
95
96
    /* Find the end of the value; ie, an end-of-line that
97
     * isn't followed by a continuation line.
98
     */
99
1.08M
    value = name_end + 1;
100
1.08M
    value_end = strchr (name, '\n');
101
1.08M
    if (!value_end)
102
4
      goto done;
103
1.09M
    while (*(value_end + 1) == ' ' || *(value_end + 1) == '\t') {
104
13.6k
      value_end = strchr (value_end + 1, '\n');
105
13.6k
      if (!value_end)
106
9
        goto done;
107
13.6k
    }
108
109
1.08M
    *name_end = '\0';
110
1.08M
    *value_end = '\0';
111
112
    /* Skip leading whitespace */
113
1.09M
    while (value < value_end &&
114
112k
           (*value == ' ' || *value == '\t' ||
115
105k
      *value == '\r' || *value == '\n'))
116
11.4k
      value++;
117
118
    /* Collapse continuation lines */
119
1.09M
    while ((eol = strchr (value, '\n'))) {
120
      /* find start of next line */
121
11.8k
      sol = eol + 1;
122
24.3k
      while (*sol == ' ' || *sol == '\t')
123
12.4k
        sol++;
124
125
      /* back up over trailing whitespace on current line */
126
22.9k
      while (eol[-1] == ' ' || eol[-1] == '\t' || eol[-1] == '\r')
127
11.1k
        eol--;
128
129
      /* Delete all but one SP */
130
11.8k
      *eol = ' ';
131
11.8k
      memmove (eol + 1, sol, strlen (sol) + 1);
132
11.8k
    }
133
134
    /* clip trailing whitespace */
135
1.08M
    eol = strchr (value, '\0');
136
1.09M
    while (eol > value &&
137
108k
           (eol[-1] == ' ' || eol[-1] == '\t' || eol[-1] == '\r'))
138
7.16k
      eol--;
139
1.08M
    *eol = '\0';
140
141
    /* convert (illegal) '\r's to spaces */
142
1.08M
    for (p = strchr (value, '\r'); p; p = strchr (p, '\r'))
143
2.50k
      *p = ' ';
144
145
1.08M
    if (!soup_message_headers_append_untrusted_data (dest, name, value))
146
36
                        goto done;
147
1.08M
        }
148
658
  success = TRUE;
149
150
758
done:
151
758
  g_free (headers_copy);
152
758
  return success;
153
658
}
154
155
/**
156
 * soup_headers_parse_request:
157
 * @str: the headers (up to, but not including, the trailing blank line)
158
 * @len: length of @str
159
 * @req_headers: #SoupMessageHeaders to store the header values in
160
 * @req_method: (out) (optional): if non-%NULL, will be filled in with the
161
 *   request method
162
 * @req_path: (out) (optional): if non-%NULL, will be filled in with the
163
 *   request path
164
 * @ver: (out) (optional): if non-%NULL, will be filled in with the HTTP
165
 *   version
166
 *
167
 * Parses the headers of an HTTP request in @str and stores the
168
 * results in @req_method, @req_path, @ver, and @req_headers.
169
 *
170
 * Beware that @req_headers may be modified even on failure.
171
 *
172
 * Returns: %SOUP_STATUS_OK if the headers could be parsed, or an
173
 *   HTTP error to be returned to the client if they could not be.
174
 **/
175
guint
176
soup_headers_parse_request (const char          *str, 
177
          int                  len, 
178
          SoupMessageHeaders  *req_headers,
179
          char               **req_method,
180
          char               **req_path,
181
          SoupHTTPVersion     *ver) 
182
1.24k
{
183
1.24k
  const char *method, *method_end, *path, *path_end;
184
1.24k
  const char *version, *version_end, *headers;
185
1.24k
  unsigned long major_version, minor_version;
186
1.24k
  char *p;
187
188
1.24k
  g_return_val_if_fail (str != NULL, SOUP_STATUS_BAD_REQUEST);
189
190
  /* RFC 2616 4.1 "servers SHOULD ignore any empty line(s)
191
   * received where a Request-Line is expected."
192
   */
193
1.66k
  while (len > 0 && (*str == '\r' || *str == '\n')) {
194
420
    str++;
195
420
    len--;
196
420
  }
197
1.24k
  if (!len)
198
13
    return SOUP_STATUS_BAD_REQUEST;
199
200
  /* RFC 2616 19.3 "[servers] SHOULD accept any amount of SP or
201
   * HT characters between [Request-Line] fields"
202
   */
203
204
1.22k
  method = method_end = str;
205
1.05M
  while (method_end < str + len && *method_end != ' ' && *method_end != '\t')
206
1.05M
    method_end++;
207
1.22k
  if (method_end >= str + len)
208
36
    return SOUP_STATUS_BAD_REQUEST;
209
210
1.19k
  path = method_end;
211
2.76k
  while (path < str + len && (*path == ' ' || *path == '\t'))
212
1.56k
    path++;
213
1.19k
  if (path >= str + len)
214
16
    return SOUP_STATUS_BAD_REQUEST;
215
216
1.17k
  path_end = path;
217
2.56k
  while (path_end < str + len && *path_end != ' ' && *path_end != '\t')
218
1.38k
    path_end++;
219
1.17k
  if (path_end >= str + len)
220
27
    return SOUP_STATUS_BAD_REQUEST;
221
222
1.15k
  version = path_end;
223
2.67k
  while (version < str + len && (*version == ' ' || *version == '\t'))
224
1.52k
    version++;
225
1.15k
  if (version + 8 >= str + len)
226
31
    return SOUP_STATUS_BAD_REQUEST;
227
228
1.11k
  if (strncmp (version, "HTTP/", 5) != 0 ||
229
1.07k
      !g_ascii_isdigit (version[5]))
230
47
    return SOUP_STATUS_BAD_REQUEST;
231
1.07k
  major_version = strtoul (version + 5, &p, 10);
232
1.07k
  if (p + 1 >= str + len || *p != '.' || !g_ascii_isdigit (p[1]))
233
13
    return SOUP_STATUS_BAD_REQUEST;
234
1.05k
  minor_version = strtoul (p + 1, &p, 10);
235
1.05k
  version_end = p;
236
1.05k
  if (major_version != 1)
237
126
    return SOUP_STATUS_HTTP_VERSION_NOT_SUPPORTED;
238
933
  if (minor_version > 1)
239
120
    return SOUP_STATUS_HTTP_VERSION_NOT_SUPPORTED;
240
241
813
  headers = version_end;
242
1.21k
  while (headers < str + len && (*headers == '\r' || *headers == ' '))
243
399
    headers++;
244
813
  if (headers >= str + len || *headers != '\n')
245
36
    return SOUP_STATUS_BAD_REQUEST;
246
247
  // Ensure pointer location for Request-Line end matches location of 'headers'
248
777
  p = strchr(str, '\n');
249
777
  if (p != headers)
250
1
    return SOUP_STATUS_BAD_REQUEST;
251
252
776
  if (!soup_headers_parse (str, len, req_headers))
253
118
    return SOUP_STATUS_BAD_REQUEST;
254
255
658
  if (soup_message_headers_get_expectations (req_headers) &
256
658
      SOUP_EXPECTATION_UNRECOGNIZED)
257
0
    return SOUP_STATUS_EXPECTATION_FAILED;
258
  /* RFC 2616 14.10 */
259
658
  if (minor_version == 0)
260
403
    soup_message_headers_clean_connection_headers (req_headers);
261
262
658
  if (req_method)
263
658
    *req_method = g_strndup (method, method_end - method);
264
658
  if (req_path)
265
658
    *req_path = g_strndup (path, path_end - path);
266
658
  if (ver)
267
658
    *ver = (minor_version == 0) ? SOUP_HTTP_1_0 : SOUP_HTTP_1_1;
268
269
658
  return SOUP_STATUS_OK;
270
658
}
271
272
/**
273
 * soup_headers_parse_status_line:
274
 * @status_line: an HTTP Status-Line
275
 * @ver: (out) (optional): if non-%NULL, will be filled in with the HTTP
276
 *   version
277
 * @status_code: (out) (optional): if non-%NULL, will be filled in with
278
 *   the status code
279
 * @reason_phrase: (out) (optional): if non-%NULL, will be filled in with
280
 *   the reason phrase
281
 *
282
 * Parses the HTTP Status-Line string in @status_line into @ver,
283
 * @status_code, and @reason_phrase.
284
 *
285
 * @status_line must be terminated by either "\0" or "\r\n".
286
 *
287
 * Returns: %TRUE if @status_line was parsed successfully.
288
 **/
289
gboolean
290
soup_headers_parse_status_line (const char       *status_line,
291
        SoupHTTPVersion  *ver,
292
        guint            *status_code,
293
        char            **reason_phrase)
294
0
{
295
0
  unsigned long major_version, minor_version, code;
296
0
  const char *code_start, *code_end, *phrase_start, *phrase_end;
297
0
  char *p;
298
299
0
  g_return_val_if_fail (status_line != NULL, FALSE);
300
301
0
  if (strncmp (status_line, "HTTP/", 5) == 0 &&
302
0
      g_ascii_isdigit (status_line[5])) {
303
0
    major_version = strtoul (status_line + 5, &p, 10);
304
0
    if (*p != '.' || !g_ascii_isdigit (p[1]))
305
0
      return FALSE;
306
0
    minor_version = strtoul (p + 1, &p, 10);
307
0
    if (major_version != 1)
308
0
      return FALSE;
309
0
    if (minor_version > 1)
310
0
      return FALSE;
311
0
    if (ver)
312
0
      *ver = (minor_version == 0) ? SOUP_HTTP_1_0 : SOUP_HTTP_1_1;
313
0
  } else if (!strncmp (status_line, "ICY", 3)) {
314
    /* Shoutcast not-quite-HTTP format */
315
0
    if (ver)
316
0
      *ver = SOUP_HTTP_1_0;
317
0
    p = (char *)status_line + 3;
318
0
  } else
319
0
    return FALSE;
320
321
0
  code_start = p;
322
0
  while (*code_start == ' ' || *code_start == '\t')
323
0
    code_start++;
324
0
  code_end = code_start;
325
0
  while (*code_end >= '0' && *code_end <= '9')
326
0
    code_end++;
327
0
  if (code_end != code_start + 3)
328
0
    return FALSE;
329
0
  code = atoi (code_start);
330
0
  if (code < 100 || code > 999)
331
0
    return FALSE;
332
0
  if (status_code)
333
0
    *status_code = code;
334
335
0
  phrase_start = code_end;
336
0
  while (*phrase_start == ' ' || *phrase_start == '\t')
337
0
    phrase_start++;
338
0
  phrase_end = phrase_start + strcspn (phrase_start, "\n");
339
0
  while (phrase_end > phrase_start &&
340
0
         (phrase_end[-1] == '\r' || phrase_end[-1] == ' ' || phrase_end[-1] == '\t'))
341
0
    phrase_end--;
342
0
  if (reason_phrase)
343
0
    *reason_phrase = g_strndup (phrase_start, phrase_end - phrase_start);
344
345
0
  return TRUE;
346
0
}
347
348
/**
349
 * soup_headers_parse_response:
350
 * @str: the headers (up to, but not including, the trailing blank line)
351
 * @len: length of @str
352
 * @headers: #SoupMessageHeaders to store the header values in
353
 * @ver: (out) (optional): if non-%NULL, will be filled in with the HTTP
354
 *   version
355
 * @status_code: (out) (optional): if non-%NULL, will be filled in with
356
 *   the status code
357
 * @reason_phrase: (out) (optional): if non-%NULL, will be filled in with
358
 *   the reason phrase
359
 *
360
 * Parses the headers of an HTTP response in @str and stores the
361
 * results in @ver, @status_code, @reason_phrase, and @headers.
362
 *
363
 * Beware that @headers may be modified even on failure.
364
 *
365
 * Returns: success or failure.
366
 **/
367
gboolean
368
soup_headers_parse_response (const char          *str, 
369
           int                  len, 
370
           SoupMessageHeaders  *headers,
371
           SoupHTTPVersion     *ver,
372
           guint               *status_code,
373
           char               **reason_phrase)
374
0
{
375
0
  SoupHTTPVersion version;
376
377
0
  g_return_val_if_fail (str != NULL, FALSE);
378
379
  /* Workaround for broken servers that send extra line breaks
380
   * after a response, which we then see prepended to the next
381
   * response on that connection.
382
   */
383
0
  while (len > 0 && (*str == '\r' || *str == '\n')) {
384
0
    str++;
385
0
    len--;
386
0
  }
387
0
  if (!len)
388
0
    return FALSE;
389
390
0
  if (!soup_headers_parse (str, len, headers)) 
391
0
    return FALSE;
392
393
0
  if (!soup_headers_parse_status_line (str, 
394
0
               &version, 
395
0
               status_code, 
396
0
               reason_phrase))
397
0
    return FALSE;
398
0
  if (ver)
399
0
    *ver = version;
400
401
  /* RFC 2616 14.10 */
402
0
  if (version == SOUP_HTTP_1_0)
403
0
    soup_message_headers_clean_connection_headers (headers);
404
405
0
  return TRUE;
406
0
}
407
408
409
/*
410
 * Parsing of specific HTTP header types
411
 */
412
413
static const char *
414
skip_lws (const char *s)
415
0
{
416
0
  while (g_ascii_isspace (*s))
417
0
    s++;
418
0
  return s;
419
0
}
420
421
static const char *
422
unskip_lws (const char *s, const char *start)
423
0
{
424
0
  while (s > start && g_ascii_isspace (*(s - 1)))
425
0
    s--;
426
0
  return s;
427
0
}
428
429
static const char *
430
skip_delims (const char *s, char delim)
431
0
{
432
  /* The grammar allows for multiple delimiters */
433
0
  while (g_ascii_isspace (*s) || *s == delim)
434
0
    s++;
435
0
  return s;
436
0
}
437
438
static const char *
439
skip_item (const char *s, char delim)
440
0
{
441
0
  gboolean quoted = FALSE;
442
0
  const char *start = s;
443
444
  /* A list item ends at the last non-whitespace character
445
   * before a delimiter which is not inside a quoted-string. Or
446
   * at the end of the string.
447
   */
448
449
0
  while (*s) {
450
0
    if (*s == '"')
451
0
      quoted = !quoted;
452
0
    else if (quoted) {
453
0
      if (*s == '\\' && *(s + 1))
454
0
        s++;
455
0
    } else {
456
0
      if (*s == delim)
457
0
        break;
458
0
    }
459
0
    s++;
460
0
  }
461
462
0
  return unskip_lws (s, start);
463
0
}
464
465
static GSList *
466
parse_list (const char *header, char delim)
467
0
{
468
0
  GSList *list = NULL;
469
0
  const char *end;
470
471
0
  header = skip_delims (header, delim);
472
0
  while (*header) {
473
0
    end = skip_item (header, delim);
474
0
    list = g_slist_prepend (list, g_strndup (header, end - header));
475
0
    header = skip_delims (end, delim);
476
0
  }
477
478
0
  return g_slist_reverse (list);
479
0
}
480
481
/**
482
 * soup_header_parse_list:
483
 * @header: a header value
484
 *
485
 * Parses a header whose content is described by RFC2616 as `#something`.
486
 *
487
 * "something" does not itself contain commas, except as part of quoted-strings.
488
 *
489
 * Returns: (transfer full) (element-type utf8): a #GSList of
490
 *   list elements, as allocated strings
491
 **/
492
GSList *
493
soup_header_parse_list (const char *header)
494
0
{
495
0
  g_return_val_if_fail (header != NULL, NULL);
496
497
0
  return parse_list (header, ',');
498
0
}
499
500
typedef struct {
501
  char *item;
502
  double qval;
503
} QualityItem;
504
505
static int
506
sort_by_qval (const void *a, const void *b)
507
0
{
508
0
  QualityItem *qia = (QualityItem *)a;
509
0
  QualityItem *qib = (QualityItem *)b;
510
511
0
  if (qia->qval == qib->qval)
512
0
    return 0;
513
0
  else if (qia->qval < qib->qval)
514
0
    return 1;
515
0
  else
516
0
    return -1;
517
0
}
518
519
/**
520
 * soup_header_parse_quality_list:
521
 * @header: a header value
522
 * @unacceptable: (out) (optional) (transfer full) (element-type utf8): on
523
 *   return, will contain a list of unacceptable values
524
 *
525
 * Parses a header whose content is a list of items with optional
526
 * "qvalue"s (eg, Accept, Accept-Charset, Accept-Encoding,
527
 * Accept-Language, TE).
528
 *
529
 * If @unacceptable is not %NULL, then on return, it will contain the
530
 * items with qvalue 0. Either way, those items will be removed from
531
 * the main list.
532
 *
533
 * Returns: (transfer full) (element-type utf8): a #GSList of
534
 *   acceptable values (as allocated strings), highest-qvalue first.
535
 **/
536
GSList *
537
soup_header_parse_quality_list (const char *header, GSList **unacceptable)
538
0
{
539
0
  GSList *unsorted;
540
0
  QualityItem *array;
541
0
  GSList *sorted, *iter;
542
0
  char *semi;
543
0
  const char *param, *equal, *value;
544
0
  double qval;
545
0
  int n;
546
547
0
  g_return_val_if_fail (header != NULL, NULL);
548
549
0
  if (unacceptable)
550
0
    *unacceptable = NULL;
551
552
0
  unsorted = soup_header_parse_list (header);
553
0
  array = g_new0 (QualityItem, g_slist_length (unsorted));
554
0
  for (iter = unsorted, n = 0; iter; iter = iter->next) {
555
0
    qval = 1.0;
556
0
    for (semi = strchr (iter->data, ';'); semi; semi = strchr (semi + 1, ';')) {
557
0
      param = skip_lws (semi + 1);
558
0
      if (*param != 'q')
559
0
        continue;
560
0
      equal = skip_lws (param + 1);
561
0
      if (!equal || *equal != '=')
562
0
        continue;
563
0
      value = skip_lws (equal + 1);
564
0
      if (!value)
565
0
        continue;
566
567
0
      if (value[0] != '0' && value[0] != '1')
568
0
        continue;
569
0
      qval = (double)(value[0] - '0');
570
0
      if (value[0] == '0' && value[1] == '.') {
571
0
        if (g_ascii_isdigit (value[2])) {
572
0
          qval += (double)(value[2] - '0') / 10;
573
0
          if (g_ascii_isdigit (value[3])) {
574
0
            qval += (double)(value[3] - '0') / 100;
575
0
            if (g_ascii_isdigit (value[4]))
576
0
              qval += (double)(value[4] - '0') / 1000;
577
0
          }
578
0
        }
579
0
      }
580
581
0
      *semi = '\0';
582
0
      break;
583
0
    }
584
585
0
    if (qval == 0.0) {
586
0
      if (unacceptable) {
587
0
        *unacceptable = g_slist_prepend (*unacceptable,
588
0
                 g_steal_pointer (&iter->data));
589
0
      }
590
0
    } else {
591
0
      array[n].item = g_steal_pointer (&iter->data);
592
0
      array[n].qval = qval;
593
0
      n++;
594
0
    }
595
0
  }
596
0
  g_slist_free_full (unsorted, g_free);
597
598
0
  qsort (array, n, sizeof (QualityItem), sort_by_qval);
599
0
  sorted = NULL;
600
0
  while (n--)
601
0
    sorted = g_slist_prepend (sorted, array[n].item);
602
0
  g_free (array);
603
604
0
  return sorted;
605
0
}
606
607
/**
608
 * soup_header_free_list: (skip)
609
 * @list: a #GSList returned from [func@header_parse_list] or
610
 * [func@header_parse_quality_list]
611
 *
612
 * Frees @list.
613
 **/
614
void
615
soup_header_free_list (GSList *list)
616
0
{
617
0
  g_slist_free_full (list, g_free);
618
0
}
619
620
/**
621
 * soup_header_contains:
622
 * @header: An HTTP header suitable for parsing with
623
 *   [func@header_parse_list]
624
 * @token: a token
625
 *
626
 * Parses @header to see if it contains the token @token (matched
627
 * case-insensitively).
628
 *
629
 * Note that this can't be used with lists that have qvalues.
630
 *
631
 * Returns: whether or not @header contains @token
632
 **/
633
gboolean
634
soup_header_contains (const char *header, const char *token)
635
0
{
636
0
  const char *end;
637
0
  guint len;
638
639
0
  g_return_val_if_fail (header != NULL, FALSE);
640
0
  g_return_val_if_fail (token != NULL, FALSE);
641
642
0
  len = strlen (token);
643
644
0
  header = skip_delims (header, ',');
645
0
  while (*header) {
646
0
    end = skip_item (header, ',');
647
0
    if (end - header == len &&
648
0
        !g_ascii_strncasecmp (header, token, len))
649
0
      return TRUE;
650
0
    header = skip_delims (end, ',');
651
0
  }
652
653
0
  return FALSE;
654
0
}
655
656
/**
657
 * soup_header_contains_case_sensitive:
658
 * @header: An HTTP header suitable for parsing with
659
 *   [func@header_parse_list]
660
 * @token: a token
661
 *
662
 * Parses @header to see if it contains the token @token (matched
663
 * case-sensitively).
664
 *
665
 * Note that this can't be used with lists that have qvalues.
666
 *
667
 * Returns: whether or not @header contains @token
668
 *
669
 * Since: 3.8
670
 **/
671
gboolean
672
soup_header_contains_case_sensitive (const char *header, const char *token)
673
0
{
674
0
  const char *end;
675
0
  guint len;
676
677
0
  g_return_val_if_fail (header != NULL, FALSE);
678
0
  g_return_val_if_fail (token != NULL, FALSE);
679
680
0
  len = strlen (token);
681
682
0
  header = skip_delims (header, ',');
683
0
  while (*header) {
684
0
    end = skip_item (header, ',');
685
0
    if (end - header == len &&
686
0
        !strncmp (header, token, len)) {
687
0
      return TRUE;
688
0
    }
689
0
    header = skip_delims (end, ',');
690
0
  }
691
692
0
  return FALSE;
693
0
}
694
695
static void
696
decode_quoted_string_inplace (GString *quoted_gstring)
697
0
{
698
0
  char *quoted_string = quoted_gstring->str;
699
0
  char *src, *dst;
700
701
0
  src = quoted_string + 1;
702
0
  dst = quoted_string;
703
0
  while (*src && *src != '"') {
704
0
    if (*src == '\\' && *(src + 1))
705
0
      src++;
706
0
    *dst++ = *src++;
707
0
  }
708
0
  *dst = '\0';
709
0
}
710
711
static gboolean
712
decode_rfc5987_inplace (GString *encoded_gstring)
713
0
{
714
0
  char *q, *decoded;
715
0
  gboolean iso_8859_1 = FALSE;
716
0
  const char *encoded_string = encoded_gstring->str;
717
718
0
  q = strchr (encoded_string, '\'');
719
0
  if (!q)
720
0
    return FALSE;
721
0
  if (g_ascii_strncasecmp (encoded_string, "UTF-8",
722
0
         q - encoded_string) == 0)
723
0
    ;
724
0
  else if (g_ascii_strncasecmp (encoded_string, "iso-8859-1",
725
0
              q - encoded_string) == 0)
726
0
    iso_8859_1 = TRUE;
727
0
  else
728
0
    return FALSE;
729
730
0
  q = strchr (q + 1, '\'');
731
0
  if (!q)
732
0
    return FALSE;
733
734
0
  decoded = g_uri_unescape_string (q + 1, NULL);
735
0
  if (decoded == NULL)
736
0
    return FALSE;
737
738
0
  if (iso_8859_1) {
739
0
    char *utf8 =  g_convert_with_fallback (decoded, -1, "UTF-8",
740
0
                   "iso-8859-1", "_",
741
0
                   NULL, NULL, NULL);
742
0
    g_free (decoded);
743
0
    if (!utf8)
744
0
      return FALSE;
745
0
    decoded = utf8;
746
0
  }
747
748
0
  g_string_assign (encoded_gstring, decoded);
749
0
  g_free (decoded);
750
0
  return TRUE;
751
0
}
752
753
static GHashTable *
754
parse_param_list (const char *header, char delim, gboolean strict)
755
0
{
756
0
  GHashTable *params;
757
0
  GSList *list, *iter;
758
759
0
  params = g_hash_table_new_full (soup_str_case_hash, 
760
0
          soup_str_case_equal,
761
0
          g_free, g_free);
762
763
0
  list = parse_list (header, delim);
764
0
  for (iter = list; iter; iter = iter->next) {
765
0
    char *item, *eq, *name_end;
766
0
    gboolean override, duplicated;
767
0
    GString *parsed_value = NULL;
768
769
0
    item = iter->data;
770
0
    override = FALSE;
771
772
0
    eq = strchr (item, '=');
773
0
    if (eq) {
774
0
      name_end = (char *)unskip_lws (eq, item);
775
0
      if (name_end == item) {
776
        /* That's no good... */
777
0
        g_free (item);
778
0
        continue;
779
0
      }
780
781
0
      *name_end = '\0';
782
783
0
      parsed_value = g_string_new ((char *)skip_lws (eq + 1));
784
785
0
      if (name_end[-1] == '*' && name_end > item + 1) {
786
0
        name_end[-1] = '\0';
787
0
        if (!decode_rfc5987_inplace (parsed_value)) {
788
0
          g_string_free (parsed_value, TRUE);
789
0
          g_free (item);
790
0
          continue;
791
0
        }
792
0
        override = TRUE;
793
0
      } else if (parsed_value->str[0] == '"')
794
0
        decode_quoted_string_inplace (parsed_value);
795
0
    }
796
797
0
    duplicated = g_hash_table_lookup_extended (params, item, NULL, NULL);
798
799
0
    if (strict && duplicated) {
800
0
      g_clear_pointer (&params, soup_header_free_param_list);
801
0
      g_slist_foreach (iter, (GFunc)g_free, NULL);
802
0
      if (parsed_value)
803
0
        g_string_free (parsed_value, TRUE);
804
0
      break;
805
0
    } else if (override || !duplicated) {
806
0
      g_hash_table_replace (params, item, parsed_value ? g_string_free (parsed_value, FALSE) : NULL);
807
0
    } else {
808
0
      if (parsed_value)
809
0
        g_string_free (parsed_value, TRUE);
810
0
      g_free (item);
811
0
    }
812
0
  }
813
814
0
  g_slist_free (list);
815
0
  return params;
816
0
}
817
818
/**
819
 * soup_header_parse_param_list:
820
 * @header: a header value
821
 *
822
 * Parses a header which is a comma-delimited list of something like:
823
 * `token [ "=" ( token | quoted-string ) ]`.
824
 *
825
 * Tokens that don't have an associated value will still be added to
826
 * the resulting hash table, but with a %NULL value.
827
 * 
828
 * This also handles RFC5987 encoding (which in HTTP is mostly used
829
 * for giving UTF8-encoded filenames in the Content-Disposition
830
 * header).
831
 *
832
 * Returns: (element-type utf8 utf8) (transfer full): a
833
 *   #GHashTable of list elements, which can be freed with
834
 *   [func@header_free_param_list].
835
 **/
836
GHashTable *
837
soup_header_parse_param_list (const char *header)
838
0
{
839
0
  g_return_val_if_fail (header != NULL, NULL);
840
841
0
  return parse_param_list (header, ',', FALSE);
842
0
}
843
844
/**
845
 * soup_header_parse_semi_param_list:
846
 * @header: a header value
847
 *
848
 * Parses a header which is a semicolon-delimited list of something
849
 * like: `token [ "=" ( token | quoted-string ) ]`.
850
 *
851
 * Tokens that don't have an associated value will still be added to
852
 * the resulting hash table, but with a %NULL value.
853
 * 
854
 * This also handles RFC5987 encoding (which in HTTP is mostly used
855
 * for giving UTF8-encoded filenames in the Content-Disposition
856
 * header).
857
 *
858
 * Returns: (element-type utf8 utf8) (transfer full): a
859
 *   #GHashTable of list elements, which can be freed with
860
 *   [func@header_free_param_list].
861
 **/
862
GHashTable *
863
soup_header_parse_semi_param_list (const char *header)
864
0
{
865
0
  g_return_val_if_fail (header != NULL, NULL);
866
867
0
  return parse_param_list (header, ';', FALSE);
868
0
}
869
870
/**
871
 * soup_header_parse_param_list_strict:
872
 * @header: a header value
873
 *
874
 * A strict version of [func@header_parse_param_list]
875
 * that bails out if there are duplicate parameters.
876
 *
877
 * Note that this function will treat RFC5987-encoded
878
 * parameters as duplicated if an ASCII version is also
879
 * present. For header fields that might contain
880
 * RFC5987-encoded parameters, use
881
 * [func@header_parse_param_list] instead.
882
 *
883
 * Returns: (element-type utf8 utf8) (transfer full) (nullable):
884
 *   a #GHashTable of list elements, which can be freed with
885
 *   [func@header_free_param_list] or %NULL if there are duplicate
886
 *   elements.
887
 **/
888
GHashTable *
889
soup_header_parse_param_list_strict (const char *header)
890
0
{
891
0
  g_return_val_if_fail (header != NULL, NULL);
892
893
0
  return parse_param_list (header, ',', TRUE);
894
0
}
895
896
/**
897
 * soup_header_parse_semi_param_list_strict:
898
 * @header: a header value
899
 *
900
 * A strict version of [func@header_parse_semi_param_list]
901
 * that bails out if there are duplicate parameters.
902
 *
903
 * Note that this function will treat RFC5987-encoded
904
 * parameters as duplicated if an ASCII version is also
905
 * present. For header fields that might contain
906
 * RFC5987-encoded parameters, use
907
 * [func@header_parse_semi_param_list] instead.
908
 *
909
 * Returns: (element-type utf8 utf8) (transfer full) (nullable):
910
 *   a #GHashTable of list elements, which can be freed with
911
 *   [func@header_free_param_list] or %NULL if there are duplicate
912
 *   elements.
913
 **/
914
GHashTable *
915
soup_header_parse_semi_param_list_strict (const char *header)
916
0
{
917
0
  g_return_val_if_fail (header != NULL, NULL);
918
919
0
  return parse_param_list (header, ';', TRUE);
920
0
}
921
922
/**
923
 * soup_header_free_param_list:
924
 * @param_list: (element-type utf8 utf8): a #GHashTable returned from
925
 *   [func@header_parse_param_list] or [func@header_parse_semi_param_list]
926
 *
927
 * Frees @param_list.
928
 **/
929
void
930
soup_header_free_param_list (GHashTable *param_list)
931
0
{
932
0
  g_return_if_fail (param_list != NULL);
933
934
0
  g_hash_table_destroy (param_list);
935
0
}
936
937
static void
938
append_param_rfc5987 (GString    *string,
939
          const char *name,
940
          const char *value)
941
0
{
942
0
  char *encoded;
943
944
0
  g_string_append (string, name);
945
0
  g_string_append (string, "*=UTF-8''");
946
0
  encoded = g_uri_escape_string (value, "!#$&+-.^_`|~", FALSE);
947
0
  g_string_append (string, encoded);
948
0
  g_free (encoded);
949
0
}
950
951
static void
952
append_param_quoted (GString    *string,
953
         const char *name,
954
         const char *value)
955
0
{
956
0
  gsize len;
957
958
0
  g_string_append (string, name);
959
0
  g_string_append (string, "=\"");
960
0
  while (*value) {
961
0
    while (*value == '\\' || *value == '"') {
962
0
      g_string_append_c (string, '\\');
963
0
      g_string_append_c (string, *value++);
964
0
    }
965
0
    len = strcspn (value, "\\\"");
966
0
    g_string_append_len (string, value, len);
967
0
    value += len;
968
0
  }
969
0
  g_string_append_c (string, '"');
970
0
}
971
972
static void
973
append_param_internal (GString    *string,
974
           const char *name,
975
           const char *value,
976
           gboolean    allow_token)
977
0
{
978
0
  const char *v;
979
0
  gboolean use_token = allow_token;
980
981
0
  for (v = value; *v; v++) {
982
0
    if (*v & 0x80) {
983
0
      if (g_utf8_validate (value, -1, NULL)) {
984
0
        append_param_rfc5987 (string, name, value);
985
0
        return;
986
0
      } else {
987
0
        use_token = FALSE;
988
0
        break;
989
0
      }
990
0
    } else if (!soup_char_is_token (*v))
991
0
      use_token = FALSE;
992
0
  }
993
994
0
  if (use_token) {
995
0
    g_string_append (string, name);
996
0
    g_string_append_c (string, '=');
997
0
    g_string_append (string, value);
998
0
  } else
999
0
    append_param_quoted (string, name, value);
1000
0
}
1001
1002
/**
1003
 * soup_header_g_string_append_param_quoted:
1004
 * @string: a #GString being used to construct an HTTP header value
1005
 * @name: a parameter name
1006
 * @value: a parameter value
1007
 *
1008
 * Appends something like `name="value"` to
1009
 * @string, taking care to escape any quotes or backslashes in @value.
1010
 *
1011
 * If @value is (non-ASCII) UTF-8, this will instead use RFC 5987
1012
 * encoding, just like [func@header_g_string_append_param].
1013
 **/
1014
void
1015
soup_header_g_string_append_param_quoted (GString    *string,
1016
            const char *name,
1017
            const char *value)
1018
0
{
1019
0
  g_return_if_fail (string != NULL);
1020
0
  g_return_if_fail (name != NULL);
1021
0
  g_return_if_fail (value != NULL);
1022
1023
0
  append_param_internal (string, name, value, FALSE);
1024
0
}
1025
1026
/**
1027
 * soup_header_g_string_append_param:
1028
 * @string: a #GString being used to construct an HTTP header value
1029
 * @name: a parameter name
1030
 * @value: (nullable): a parameter value, or %NULL
1031
 *
1032
 * Appends something like `name=value` to @string, taking care to quote @value
1033
 * if needed, and if so, to escape any quotes or backslashes in @value.
1034
 *
1035
 * Alternatively, if @value is a non-ASCII UTF-8 string, it will be
1036
 * appended using RFC5987 syntax. Although in theory this is supposed
1037
 * to work anywhere in HTTP that uses this style of parameter, in
1038
 * reality, it can only be used portably with the Content-Disposition
1039
 * "filename" parameter.
1040
 *
1041
 * If @value is %NULL, this will just append @name to @string.
1042
 **/
1043
void
1044
soup_header_g_string_append_param (GString    *string,
1045
           const char *name,
1046
           const char *value)
1047
0
{
1048
0
  g_return_if_fail (string != NULL);
1049
0
  g_return_if_fail (name != NULL);
1050
1051
0
  if (!value) {
1052
0
    g_string_append (string, name);
1053
0
    return;
1054
0
  }
1055
1056
0
  append_param_internal (string, name, value, TRUE);
1057
0
}