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