Coverage Report

Created: 2025-11-16 06:19

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