1 /*
2 * Copyright (C) 2006 Alexey Proskuryakov (ap@webkit.org)
3 * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
4 * Copyright (C) 2009 Torch Mobile Inc. http://www.torchmobile.com/
5 * Copyright (C) 2009 Google Inc. All rights reserved.
6 * Copyright (C) 2011 Apple Inc. All Rights Reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 *
12 * 1. Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
14 * 2. Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in the
16 * documentation and/or other materials provided with the distribution.
17 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
18 * its contributors may be used to endorse or promote products derived
19 * from this software without specific prior written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
22 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
25 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
28 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32
33 #include "third_party/blink/renderer/platform/network/http_parsers.h"
34
35 #include <memory>
36 #include "net/http/http_content_disposition.h"
37 #include "net/http/http_response_headers.h"
38 #include "net/http/http_util.h"
39 #include "third_party/blink/public/platform/web_string.h"
40 #include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
41 #include "third_party/blink/renderer/platform/network/header_field_tokenizer.h"
42 #include "third_party/blink/renderer/platform/network/http_names.h"
43 #include "third_party/blink/renderer/platform/wtf/date_math.h"
44 #include "third_party/blink/renderer/platform/wtf/math_extras.h"
45 #include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
46 #include "third_party/blink/renderer/platform/wtf/text/character_names.h"
47 #include "third_party/blink/renderer/platform/wtf/text/parsing_utilities.h"
48 #include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
49 #include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
50 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
51 #include "third_party/blink/renderer/platform/wtf/wtf.h"
52
53 namespace blink {
54
55 namespace {
56
ReplaceHeaders()57 const Vector<AtomicString>& ReplaceHeaders() {
58 // The list of response headers that we do not copy from the original
59 // response when generating a ResourceResponse for a MIME payload.
60 // Note: this is called only on the main thread.
61 DEFINE_STATIC_LOCAL(Vector<AtomicString>, headers,
62 ({"content-type", "content-length", "content-disposition",
63 "content-range", "range", "set-cookie"}));
64 return headers;
65 }
66
IsWhitespace(UChar chr)67 bool IsWhitespace(UChar chr) {
68 return (chr == ' ') || (chr == '\t');
69 }
70
71 // true if there is more to parse, after incrementing pos past whitespace.
72 // Note: Might return pos == str.length()
73 // if |matcher| is nullptr, isWhitespace() is used.
SkipWhiteSpace(const String & str,unsigned & pos,WTF::CharacterMatchFunctionPtr matcher=nullptr)74 inline bool SkipWhiteSpace(const String& str,
75 unsigned& pos,
76 WTF::CharacterMatchFunctionPtr matcher = nullptr) {
77 unsigned len = str.length();
78
79 if (matcher) {
80 while (pos < len && matcher(str[pos]))
81 ++pos;
82 } else {
83 while (pos < len && IsWhitespace(str[pos]))
84 ++pos;
85 }
86
87 return pos < len;
88 }
89
90 template <typename CharType>
IsASCIILowerAlphaOrDigit(CharType c)91 inline bool IsASCIILowerAlphaOrDigit(CharType c) {
92 return IsASCIILower(c) || IsASCIIDigit(c);
93 }
94
95 template <typename CharType>
IsASCIILowerAlphaOrDigitOrHyphen(CharType c)96 inline bool IsASCIILowerAlphaOrDigitOrHyphen(CharType c) {
97 return IsASCIILowerAlphaOrDigit(c) || c == '-';
98 }
99
100 // Parse a number with ignoring trailing [0-9.].
101 // Returns false if the source contains invalid characters.
ParseRefreshTime(const String & source,base::TimeDelta & delay)102 bool ParseRefreshTime(const String& source, base::TimeDelta& delay) {
103 int full_stop_count = 0;
104 unsigned number_end = source.length();
105 for (unsigned i = 0; i < source.length(); ++i) {
106 UChar ch = source[i];
107 if (ch == kFullstopCharacter) {
108 // TODO(tkent): According to the HTML specification, we should support
109 // only integers. However we support fractional numbers.
110 if (++full_stop_count == 2)
111 number_end = i;
112 } else if (!IsASCIIDigit(ch)) {
113 return false;
114 }
115 }
116 bool ok;
117 double time = source.Left(number_end).ToDouble(&ok);
118 if (!ok)
119 return false;
120 delay = base::TimeDelta::FromSecondsD(time);
121 return true;
122 }
123
124 } // namespace
125
IsValidHTTPHeaderValue(const String & name)126 bool IsValidHTTPHeaderValue(const String& name) {
127 // FIXME: This should really match name against
128 // field-value in section 4.2 of RFC 2616.
129
130 return name.ContainsOnlyLatin1OrEmpty() && !name.Contains('\r') &&
131 !name.Contains('\n') && !name.Contains('\0');
132 }
133
134 // See RFC 7230, Section 3.2.6.
IsValidHTTPToken(const String & characters)135 bool IsValidHTTPToken(const String& characters) {
136 if (characters.IsEmpty())
137 return false;
138 for (unsigned i = 0; i < characters.length(); ++i) {
139 UChar c = characters[i];
140 if (c > 0x7F || !net::HttpUtil::IsTokenChar(c))
141 return false;
142 }
143 return true;
144 }
145
IsContentDispositionAttachment(const String & content_disposition)146 bool IsContentDispositionAttachment(const String& content_disposition) {
147 return net::HttpContentDisposition(content_disposition.Utf8(), std::string())
148 .is_attachment();
149 }
150
151 // https://html.spec.whatwg.org/C/#attr-meta-http-equiv-refresh
ParseHTTPRefresh(const String & refresh,WTF::CharacterMatchFunctionPtr matcher,base::TimeDelta & delay,String & url)152 bool ParseHTTPRefresh(const String& refresh,
153 WTF::CharacterMatchFunctionPtr matcher,
154 base::TimeDelta& delay,
155 String& url) {
156 unsigned len = refresh.length();
157 unsigned pos = 0;
158 matcher = matcher ? matcher : IsWhitespace;
159
160 if (!SkipWhiteSpace(refresh, pos, matcher))
161 return false;
162
163 while (pos != len && refresh[pos] != ',' && refresh[pos] != ';' &&
164 !matcher(refresh[pos]))
165 ++pos;
166
167 if (pos == len) { // no URL
168 url = String();
169 return ParseRefreshTime(refresh.StripWhiteSpace(), delay);
170 } else {
171 if (!ParseRefreshTime(refresh.Left(pos).StripWhiteSpace(), delay))
172 return false;
173
174 SkipWhiteSpace(refresh, pos, matcher);
175 if (pos < len && (refresh[pos] == ',' || refresh[pos] == ';'))
176 ++pos;
177 SkipWhiteSpace(refresh, pos, matcher);
178 unsigned url_start_pos = pos;
179 if (refresh.FindIgnoringASCIICase("url", url_start_pos) == url_start_pos) {
180 url_start_pos += 3;
181 SkipWhiteSpace(refresh, url_start_pos, matcher);
182 if (refresh[url_start_pos] == '=') {
183 ++url_start_pos;
184 SkipWhiteSpace(refresh, url_start_pos, matcher);
185 } else {
186 url_start_pos = pos; // e.g. "Refresh: 0; url.html"
187 }
188 }
189
190 unsigned url_end_pos = len;
191
192 if (refresh[url_start_pos] == '"' || refresh[url_start_pos] == '\'') {
193 UChar quotation_mark = refresh[url_start_pos];
194 url_start_pos++;
195 while (url_end_pos > url_start_pos) {
196 url_end_pos--;
197 if (refresh[url_end_pos] == quotation_mark)
198 break;
199 }
200
201 // https://bugs.webkit.org/show_bug.cgi?id=27868
202 // Sometimes there is no closing quote for the end of the URL even though
203 // there was an opening quote. If we looped over the entire alleged URL
204 // string back to the opening quote, just go ahead and use everything
205 // after the opening quote instead.
206 if (url_end_pos == url_start_pos)
207 url_end_pos = len;
208 }
209
210 url = refresh.Substring(url_start_pos, url_end_pos - url_start_pos)
211 .StripWhiteSpace();
212 return true;
213 }
214 }
215
ParseDate(const String & value)216 base::Optional<base::Time> ParseDate(const String& value) {
217 return ParseDateFromNullTerminatedCharacters(value.Utf8().c_str());
218 }
219
ExtractMIMETypeFromMediaType(const AtomicString & media_type)220 AtomicString ExtractMIMETypeFromMediaType(const AtomicString& media_type) {
221 unsigned length = media_type.length();
222
223 unsigned pos = 0;
224
225 while (pos < length) {
226 UChar c = media_type[pos];
227 if (c != '\t' && c != ' ')
228 break;
229 ++pos;
230 }
231
232 if (pos == length)
233 return media_type;
234
235 unsigned type_start = pos;
236
237 unsigned type_end = pos;
238 while (pos < length) {
239 UChar c = media_type[pos];
240
241 // While RFC 2616 does not allow it, other browsers allow multiple values in
242 // the HTTP media type header field, Content-Type. In such cases, the media
243 // type string passed here may contain the multiple values separated by
244 // commas. For now, this code ignores text after the first comma, which
245 // prevents it from simply failing to parse such types altogether. Later
246 // for better compatibility we could consider using the first or last valid
247 // MIME type instead.
248 // See https://bugs.webkit.org/show_bug.cgi?id=25352 for more discussion.
249 if (c == ',' || c == ';')
250 break;
251
252 if (c != '\t' && c != ' ')
253 type_end = pos + 1;
254
255 ++pos;
256 }
257
258 return AtomicString(
259 media_type.GetString().Substring(type_start, type_end - type_start));
260 }
261
ParseContentTypeOptionsHeader(const String & value)262 ContentTypeOptionsDisposition ParseContentTypeOptionsHeader(
263 const String& value) {
264 if (value.IsEmpty())
265 return kContentTypeOptionsNone;
266
267 Vector<String> results;
268 value.Split(",", results);
269 if (results.size() && results[0].StripWhiteSpace().LowerASCII() == "nosniff")
270 return kContentTypeOptionsNosniff;
271 return kContentTypeOptionsNone;
272 }
273
IsCacheHeaderSeparator(UChar c)274 static bool IsCacheHeaderSeparator(UChar c) {
275 // See RFC 2616, Section 2.2
276 switch (c) {
277 case '(':
278 case ')':
279 case '<':
280 case '>':
281 case '@':
282 case ',':
283 case ';':
284 case ':':
285 case '\\':
286 case '"':
287 case '/':
288 case '[':
289 case ']':
290 case '?':
291 case '=':
292 case '{':
293 case '}':
294 case ' ':
295 case '\t':
296 return true;
297 default:
298 return false;
299 }
300 }
301
IsControlCharacter(UChar c)302 static bool IsControlCharacter(UChar c) {
303 return c < ' ' || c == 127;
304 }
305
TrimToNextSeparator(const String & str)306 static inline String TrimToNextSeparator(const String& str) {
307 return str.Substring(0, str.Find(IsCacheHeaderSeparator));
308 }
309
ParseCacheHeader(const String & header,Vector<std::pair<String,String>> & result)310 static void ParseCacheHeader(const String& header,
311 Vector<std::pair<String, String>>& result) {
312 const String safe_header = header.RemoveCharacters(IsControlCharacter);
313 wtf_size_t max = safe_header.length();
314 for (wtf_size_t pos = 0; pos < max; /* pos incremented in loop */) {
315 wtf_size_t next_comma_position = safe_header.find(',', pos);
316 wtf_size_t next_equal_sign_position = safe_header.find('=', pos);
317 if (next_equal_sign_position != kNotFound &&
318 (next_equal_sign_position < next_comma_position ||
319 next_comma_position == kNotFound)) {
320 // Get directive name, parse right hand side of equal sign, then add to
321 // map
322 String directive = TrimToNextSeparator(
323 safe_header.Substring(pos, next_equal_sign_position - pos)
324 .StripWhiteSpace());
325 pos += next_equal_sign_position - pos + 1;
326
327 String value = safe_header.Substring(pos, max - pos).StripWhiteSpace();
328 if (value[0] == '"') {
329 // The value is a quoted string
330 wtf_size_t next_double_quote_position = value.find('"', 1);
331 if (next_double_quote_position != kNotFound) {
332 // Store the value as a quoted string without quotes
333 result.push_back(std::pair<String, String>(
334 directive, value.Substring(1, next_double_quote_position - 1)
335 .StripWhiteSpace()));
336 pos += (safe_header.find('"', pos) - pos) +
337 next_double_quote_position + 1;
338 // Move past next comma, if there is one
339 wtf_size_t next_comma_position2 = safe_header.find(',', pos);
340 if (next_comma_position2 != kNotFound)
341 pos += next_comma_position2 - pos + 1;
342 else
343 return; // Parse error if there is anything left with no comma
344 } else {
345 // Parse error; just use the rest as the value
346 result.push_back(std::pair<String, String>(
347 directive,
348 TrimToNextSeparator(
349 value.Substring(1, value.length() - 1).StripWhiteSpace())));
350 return;
351 }
352 } else {
353 // The value is a token until the next comma
354 wtf_size_t next_comma_position2 = value.find(',');
355 if (next_comma_position2 != kNotFound) {
356 // The value is delimited by the next comma
357 result.push_back(std::pair<String, String>(
358 directive,
359 TrimToNextSeparator(
360 value.Substring(0, next_comma_position2).StripWhiteSpace())));
361 pos += (safe_header.find(',', pos) - pos) + 1;
362 } else {
363 // The rest is the value; no change to value needed
364 result.push_back(
365 std::pair<String, String>(directive, TrimToNextSeparator(value)));
366 return;
367 }
368 }
369 } else if (next_comma_position != kNotFound &&
370 (next_comma_position < next_equal_sign_position ||
371 next_equal_sign_position == kNotFound)) {
372 // Add directive to map with empty string as value
373 result.push_back(std::pair<String, String>(
374 TrimToNextSeparator(
375 safe_header.Substring(pos, next_comma_position - pos)
376 .StripWhiteSpace()),
377 ""));
378 pos += next_comma_position - pos + 1;
379 } else {
380 // Add last directive to map with empty string as value
381 result.push_back(std::pair<String, String>(
382 TrimToNextSeparator(
383 safe_header.Substring(pos, max - pos).StripWhiteSpace()),
384 ""));
385 return;
386 }
387 }
388 }
389
ParseCacheControlDirectives(const AtomicString & cache_control_value,const AtomicString & pragma_value)390 CacheControlHeader ParseCacheControlDirectives(
391 const AtomicString& cache_control_value,
392 const AtomicString& pragma_value) {
393 CacheControlHeader cache_control_header;
394 cache_control_header.parsed = true;
395 cache_control_header.max_age = base::nullopt;
396 cache_control_header.stale_while_revalidate = base::nullopt;
397
398 static const char kNoCacheDirective[] = "no-cache";
399 static const char kNoStoreDirective[] = "no-store";
400 static const char kMustRevalidateDirective[] = "must-revalidate";
401 static const char kMaxAgeDirective[] = "max-age";
402 static const char kStaleWhileRevalidateDirective[] = "stale-while-revalidate";
403
404 if (!cache_control_value.IsEmpty()) {
405 Vector<std::pair<String, String>> directives;
406 ParseCacheHeader(cache_control_value, directives);
407
408 wtf_size_t directives_size = directives.size();
409 for (wtf_size_t i = 0; i < directives_size; ++i) {
410 // RFC2616 14.9.1: A no-cache directive with a value is only meaningful
411 // for proxy caches. It should be ignored by a browser level cache.
412 if (EqualIgnoringASCIICase(directives[i].first, kNoCacheDirective) &&
413 directives[i].second.IsEmpty()) {
414 cache_control_header.contains_no_cache = true;
415 } else if (EqualIgnoringASCIICase(directives[i].first,
416 kNoStoreDirective)) {
417 cache_control_header.contains_no_store = true;
418 } else if (EqualIgnoringASCIICase(directives[i].first,
419 kMustRevalidateDirective)) {
420 cache_control_header.contains_must_revalidate = true;
421 } else if (EqualIgnoringASCIICase(directives[i].first,
422 kMaxAgeDirective)) {
423 if (cache_control_header.max_age) {
424 // First max-age directive wins if there are multiple ones.
425 continue;
426 }
427 bool ok;
428 double max_age = directives[i].second.ToDouble(&ok);
429 if (ok)
430 cache_control_header.max_age = base::TimeDelta::FromSecondsD(max_age);
431 } else if (EqualIgnoringASCIICase(directives[i].first,
432 kStaleWhileRevalidateDirective)) {
433 if (cache_control_header.stale_while_revalidate) {
434 // First stale-while-revalidate directive wins if there are multiple
435 // ones.
436 continue;
437 }
438 bool ok;
439 double stale_while_revalidate = directives[i].second.ToDouble(&ok);
440 if (ok) {
441 cache_control_header.stale_while_revalidate =
442 base::TimeDelta::FromSecondsD(stale_while_revalidate);
443 }
444 }
445 }
446 }
447
448 if (!cache_control_header.contains_no_cache) {
449 // Handle Pragma: no-cache
450 // This is deprecated and equivalent to Cache-control: no-cache
451 // Don't bother tokenizing the value, it is not important
452 cache_control_header.contains_no_cache =
453 pragma_value.LowerASCII().Contains(kNoCacheDirective);
454 }
455 return cache_control_header;
456 }
457
ParseCommaDelimitedHeader(const String & header_value,CommaDelimitedHeaderSet & header_set)458 void ParseCommaDelimitedHeader(const String& header_value,
459 CommaDelimitedHeaderSet& header_set) {
460 Vector<String> results;
461 header_value.Split(",", results);
462 for (auto& value : results)
463 header_set.insert(value.StripWhiteSpace(IsWhitespace));
464 }
465
ParseMultipartHeadersFromBody(const char * bytes,wtf_size_t size,ResourceResponse * response,wtf_size_t * end)466 bool ParseMultipartHeadersFromBody(const char* bytes,
467 wtf_size_t size,
468 ResourceResponse* response,
469 wtf_size_t* end) {
470 DCHECK(IsMainThread());
471
472 size_t headers_end_pos =
473 net::HttpUtil::LocateEndOfAdditionalHeaders(bytes, size, 0);
474
475 if (headers_end_pos == std::string::npos)
476 return false;
477
478 *end = static_cast<wtf_size_t>(headers_end_pos);
479
480 // Eat headers and prepend a status line as is required by
481 // HttpResponseHeaders.
482 std::string headers("HTTP/1.1 200 OK\r\n");
483 headers.append(bytes, headers_end_pos);
484
485 auto response_headers = base::MakeRefCounted<net::HttpResponseHeaders>(
486 net::HttpUtil::AssembleRawHeaders(headers));
487
488 std::string mime_type, charset;
489 response_headers->GetMimeTypeAndCharset(&mime_type, &charset);
490 response->SetMimeType(WebString::FromUTF8(mime_type));
491 response->SetTextEncodingName(WebString::FromUTF8(charset));
492
493 // Copy headers listed in replaceHeaders to the response.
494 for (const AtomicString& header : ReplaceHeaders()) {
495 std::string value;
496 StringUTF8Adaptor adaptor(header);
497 base::StringPiece header_string_piece(adaptor.AsStringPiece());
498 size_t iterator = 0;
499
500 response->ClearHttpHeaderField(header);
501 Vector<AtomicString> values;
502 while (response_headers->EnumerateHeader(&iterator, header_string_piece,
503 &value)) {
504 const AtomicString atomic_value = WebString::FromLatin1(value);
505 values.push_back(atomic_value);
506 }
507 response->AddHttpHeaderFieldWithMultipleValues(header, values);
508 }
509 return true;
510 }
511
ParseMultipartFormHeadersFromBody(const char * bytes,wtf_size_t size,HTTPHeaderMap * header_fields,wtf_size_t * end)512 bool ParseMultipartFormHeadersFromBody(const char* bytes,
513 wtf_size_t size,
514 HTTPHeaderMap* header_fields,
515 wtf_size_t* end) {
516 DCHECK_EQ(0u, header_fields->size());
517
518 size_t headers_end_pos =
519 net::HttpUtil::LocateEndOfAdditionalHeaders(bytes, size, 0);
520
521 if (headers_end_pos == std::string::npos)
522 return false;
523
524 *end = static_cast<wtf_size_t>(headers_end_pos);
525
526 // Eat headers and prepend a status line as is required by
527 // HttpResponseHeaders.
528 std::string headers("HTTP/1.1 200 OK\r\n");
529 headers.append(bytes, headers_end_pos);
530
531 auto responseHeaders = base::MakeRefCounted<net::HttpResponseHeaders>(
532 net::HttpUtil::AssembleRawHeaders(headers));
533
534 // Copy selected header fields.
535 const AtomicString* const headerNamePointers[] = {
536 &http_names::kContentDisposition, &http_names::kContentType};
537 for (const AtomicString* headerNamePointer : headerNamePointers) {
538 StringUTF8Adaptor adaptor(*headerNamePointer);
539 size_t iterator = 0;
540 base::StringPiece headerNameStringPiece = adaptor.AsStringPiece();
541 std::string value;
542 while (responseHeaders->EnumerateHeader(&iterator, headerNameStringPiece,
543 &value)) {
544 header_fields->Add(*headerNamePointer, WebString::FromUTF8(value));
545 }
546 }
547
548 return true;
549 }
550
ParseContentRangeHeaderFor206(const String & content_range,int64_t * first_byte_position,int64_t * last_byte_position,int64_t * instance_length)551 bool ParseContentRangeHeaderFor206(const String& content_range,
552 int64_t* first_byte_position,
553 int64_t* last_byte_position,
554 int64_t* instance_length) {
555 return net::HttpUtil::ParseContentRangeHeaderFor206(
556 StringUTF8Adaptor(content_range).AsStringPiece(), first_byte_position,
557 last_byte_position, instance_length);
558 }
559
ParseServerTimingHeader(const String & headerValue)560 std::unique_ptr<ServerTimingHeaderVector> ParseServerTimingHeader(
561 const String& headerValue) {
562 std::unique_ptr<ServerTimingHeaderVector> headers =
563 std::make_unique<ServerTimingHeaderVector>();
564
565 if (!headerValue.IsNull()) {
566 DCHECK(headerValue.Is8Bit());
567
568 HeaderFieldTokenizer tokenizer(headerValue);
569 while (!tokenizer.IsConsumed()) {
570 StringView name;
571 if (!tokenizer.ConsumeToken(ParsedContentType::Mode::kNormal, name)) {
572 break;
573 }
574
575 ServerTimingHeader header(name.ToString());
576
577 while (tokenizer.Consume(';')) {
578 StringView parameter_name;
579 if (!tokenizer.ConsumeToken(ParsedContentType::Mode::kNormal,
580 parameter_name)) {
581 break;
582 }
583
584 String value = "";
585 if (tokenizer.Consume('=')) {
586 tokenizer.ConsumeTokenOrQuotedString(ParsedContentType::Mode::kNormal,
587 value);
588 tokenizer.ConsumeBeforeAnyCharMatch({',', ';'});
589 }
590 header.SetParameter(parameter_name, value);
591 }
592
593 headers->push_back(std::make_unique<ServerTimingHeader>(header));
594
595 if (!tokenizer.Consume(',')) {
596 break;
597 }
598 }
599 }
600 return headers;
601 }
602
603 } // namespace blink
604