Some minor X-Content-Type-Options parsing issues
[WebKit.git] / Source / WebCore / platform / network / HTTPParsers.cpp
1 /*
2  * Copyright (C) 2006 Alexey Proskuryakov (ap@webkit.org)
3  * Copyright (C) 2006-2017 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 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 "config.h"
34 #include "HTTPParsers.h"
35
36 #include "HTTPHeaderNames.h"
37 #include <wtf/DateMath.h>
38 #include <wtf/Language.h>
39 #include <wtf/NeverDestroyed.h>
40 #include <wtf/text/CString.h>
41 #include <wtf/text/StringBuilder.h>
42 #include <wtf/text/WTFString.h>
43 #include <wtf/unicode/CharacterNames.h>
44
45
46 namespace WebCore {
47 using namespace WTF;
48
49 // true if there is more to parse, after incrementing pos past whitespace.
50 // Note: Might return pos == str.length()
51 static inline bool skipWhiteSpace(const String& str, unsigned& pos)
52 {
53     unsigned len = str.length();
54
55     while (pos < len && (str[pos] == '\t' || str[pos] == ' '))
56         ++pos;
57
58     return pos < len;
59 }
60
61 // Returns true if the function can match the whole token (case insensitive)
62 // incrementing pos on match, otherwise leaving pos unchanged.
63 // Note: Might return pos == str.length()
64 static inline bool skipToken(const String& str, unsigned& pos, const char* token)
65 {
66     unsigned len = str.length();
67     unsigned current = pos;
68
69     while (current < len && *token) {
70         if (toASCIILower(str[current]) != *token++)
71             return false;
72         ++current;
73     }
74
75     if (*token)
76         return false;
77
78     pos = current;
79     return true;
80 }
81
82 // True if the expected equals sign is seen and there is more to follow.
83 static inline bool skipEquals(const String& str, unsigned &pos)
84 {
85     return skipWhiteSpace(str, pos) && str[pos++] == '=' && skipWhiteSpace(str, pos);
86 }
87
88 // True if a value present, incrementing pos to next space or semicolon, if any.
89 // Note: might return pos == str.length().
90 static inline bool skipValue(const String& str, unsigned& pos)
91 {
92     unsigned start = pos;
93     unsigned len = str.length();
94     while (pos < len) {
95         if (str[pos] == ' ' || str[pos] == '\t' || str[pos] == ';')
96             break;
97         ++pos;
98     }
99     return pos != start;
100 }
101
102 // See RFC 7230, Section 3.1.2.
103 bool isValidReasonPhrase(const String& value)
104 {
105     for (unsigned i = 0; i < value.length(); ++i) {
106         UChar c = value[i];
107         if (c == 0x7F || c > 0xFF || (c < 0x20 && c != '\t'))
108             return false;
109     }
110     return true;
111 }
112
113 // See https://fetch.spec.whatwg.org/#concept-header
114 bool isValidHTTPHeaderValue(const String& value)
115 {
116     UChar c = value[0];
117     if (c == ' ' || c == '\t')
118         return false;
119     c = value[value.length() - 1];
120     if (c == ' ' || c == '\t')
121         return false;
122     for (unsigned i = 0; i < value.length(); ++i) {
123         c = value[i];
124         ASSERT(c <= 0xFF);
125         if (c == 0x00 || c == 0x0A || c == 0x0D)
126             return false;
127     }
128     return true;
129 }
130
131 // See RFC 7230, Section 3.2.6.
132 static bool isDelimiterCharacter(const UChar c)
133 {
134     // DQUOTE and "(),/:;<=>?@[\]{}"
135     return (c == '"' || c == '(' || c == ')' || c == ',' || c == '/' || c == ':' || c == ';'
136         || c == '<' || c == '=' || c == '>' || c == '?' || c == '@' || c == '[' || c == '\\'
137         || c == ']' || c == '{' || c == '}');
138 }
139
140 // See RFC 7231, Section 5.3.2.
141 bool isValidAcceptHeaderValue(const String& value)
142 {
143     for (unsigned i = 0; i < value.length(); ++i) {
144         UChar c = value[i];
145
146         // First check for alphanumeric for performance reasons then whitelist four delimiter characters.
147         if (isASCIIAlphanumeric(c) || c == ',' || c == '/' || c == ';' || c == '=')
148             continue;
149
150         ASSERT(c <= 0xFF);
151         if (c == 0x7F || (c < 0x20 && c != '\t'))
152             return false;
153
154         if (isDelimiterCharacter(c))
155             return false;
156     }
157     
158     return true;
159 }
160
161 // See RFC 7231, Section 5.3.5 and 3.1.3.2.
162 bool isValidLanguageHeaderValue(const String& value)
163 {
164     for (unsigned i = 0; i < value.length(); ++i) {
165         UChar c = value[i];
166         if (isASCIIAlphanumeric(c) || c == ' ' || c == '*' || c == ',' || c == '-' || c == '.' || c == ';' || c == '=')
167             continue;
168         return false;
169     }
170     
171     // FIXME: Validate further by splitting into language tags and optional quality
172     // values (q=) and then check each language tag.
173     // Language tags https://tools.ietf.org/html/rfc7231#section-3.1.3.1
174     // Language tag syntax https://tools.ietf.org/html/bcp47#section-2.1
175     return true;
176 }
177
178 // See RFC 7230, Section 3.2.6.
179 bool isValidHTTPToken(const String& value)
180 {
181     if (value.isEmpty())
182         return false;
183     auto valueStringView = StringView(value);
184     for (UChar c : valueStringView.codeUnits()) {
185         if (c <= 0x20 || c >= 0x7F
186             || c == '(' || c == ')' || c == '<' || c == '>' || c == '@'
187             || c == ',' || c == ';' || c == ':' || c == '\\' || c == '"'
188             || c == '/' || c == '[' || c == ']' || c == '?' || c == '='
189             || c == '{' || c == '}')
190         return false;
191     }
192     return true;
193 }
194
195 static const size_t maxInputSampleSize = 128;
196 static String trimInputSample(const char* p, size_t length)
197 {
198     String s = String(p, std::min<size_t>(length, maxInputSampleSize));
199     if (length > maxInputSampleSize)
200         s.append(horizontalEllipsis);
201     return s;
202 }
203
204 bool parseHTTPRefresh(const String& refresh, double& delay, String& url)
205 {
206     unsigned len = refresh.length();
207     unsigned pos = 0;
208     
209     if (!skipWhiteSpace(refresh, pos))
210         return false;
211     
212     while (pos != len && refresh[pos] != ',' && refresh[pos] != ';')
213         ++pos;
214     
215     if (pos == len) { // no URL
216         url = String();
217         bool ok;
218         delay = refresh.stripWhiteSpace().toDouble(&ok);
219         return ok;
220     } else {
221         bool ok;
222         delay = refresh.left(pos).stripWhiteSpace().toDouble(&ok);
223         if (!ok)
224             return false;
225         
226         ++pos;
227         skipWhiteSpace(refresh, pos);
228         unsigned urlStartPos = pos;
229         if (refresh.findIgnoringASCIICase("url", urlStartPos) == urlStartPos) {
230             urlStartPos += 3;
231             skipWhiteSpace(refresh, urlStartPos);
232             if (refresh[urlStartPos] == '=') {
233                 ++urlStartPos;
234                 skipWhiteSpace(refresh, urlStartPos);
235             } else
236                 urlStartPos = pos;  // e.g. "Refresh: 0; url.html"
237         }
238
239         unsigned urlEndPos = len;
240
241         if (refresh[urlStartPos] == '"' || refresh[urlStartPos] == '\'') {
242             UChar quotationMark = refresh[urlStartPos];
243             urlStartPos++;
244             while (urlEndPos > urlStartPos) {
245                 urlEndPos--;
246                 if (refresh[urlEndPos] == quotationMark)
247                     break;
248             }
249             
250             // https://bugs.webkit.org/show_bug.cgi?id=27868
251             // Sometimes there is no closing quote for the end of the URL even though there was an opening quote.
252             // If we looped over the entire alleged URL string back to the opening quote, just use everything
253             // after the opening quote instead.
254             if (urlEndPos == urlStartPos)
255                 urlEndPos = len;
256         }
257
258         url = refresh.substring(urlStartPos, urlEndPos - urlStartPos).stripWhiteSpace();
259         return true;
260     }
261 }
262
263 std::optional<WallTime> parseHTTPDate(const String& value)
264 {
265     double dateInMillisecondsSinceEpoch = parseDateFromNullTerminatedCharacters(value.utf8().data());
266     if (!std::isfinite(dateInMillisecondsSinceEpoch))
267         return std::nullopt;
268     // This assumes system_clock epoch equals Unix epoch which is true for all implementations but unspecified.
269     // FIXME: The parsing function should be switched to WallTime too.
270     return WallTime::fromRawSeconds(dateInMillisecondsSinceEpoch / 1000.0);
271 }
272
273 // FIXME: This function doesn't comply with RFC 6266.
274 // For example, this function doesn't handle the interaction between " and ;
275 // that arises from quoted-string, nor does this function properly unquote
276 // attribute values. Further this function appears to process parameter names
277 // in a case-sensitive manner. (There are likely other bugs as well.)
278 String filenameFromHTTPContentDisposition(const String& value)
279 {
280     for (auto& keyValuePair : value.split(';')) {
281         size_t valueStartPos = keyValuePair.find('=');
282         if (valueStartPos == notFound)
283             continue;
284
285         String key = keyValuePair.left(valueStartPos).stripWhiteSpace();
286
287         if (key.isEmpty() || key != "filename")
288             continue;
289         
290         String value = keyValuePair.substring(valueStartPos + 1).stripWhiteSpace();
291
292         // Remove quotes if there are any
293         if (value[0] == '\"')
294             value = value.substring(1, value.length() - 2);
295
296         return value;
297     }
298
299     return String();
300 }
301
302 String extractMIMETypeFromMediaType(const String& mediaType)
303 {
304     StringBuilder mimeType;
305     unsigned length = mediaType.length();
306     mimeType.reserveCapacity(length);
307     for (unsigned i = 0; i < length; i++) {
308         UChar c = mediaType[i];
309
310         if (c == ';')
311             break;
312
313         // While RFC 2616 does not allow it, other browsers allow multiple values in the HTTP media
314         // type header field, Content-Type. In such cases, the media type string passed here may contain
315         // the multiple values separated by commas. For now, this code ignores text after the first comma,
316         // which prevents it from simply failing to parse such types altogether. Later for better
317         // compatibility we could consider using the first or last valid MIME type instead.
318         // See https://bugs.webkit.org/show_bug.cgi?id=25352 for more discussion.
319         if (c == ',')
320             break;
321
322         // FIXME: The following is not correct. RFC 2616 allows linear white space before and
323         // after the MIME type, but not within the MIME type itself. And linear white space
324         // includes only a few specific ASCII characters; a small subset of isSpaceOrNewline.
325         // See https://bugs.webkit.org/show_bug.cgi?id=8644 for a bug tracking part of this.
326         if (isSpaceOrNewline(c))
327             continue;
328
329         mimeType.append(c);
330     }
331
332     if (mimeType.length() == length)
333         return mediaType;
334     return mimeType.toString();
335 }
336
337 String extractCharsetFromMediaType(const String& mediaType)
338 {
339     unsigned int pos, len;
340     findCharsetInMediaType(mediaType, pos, len);
341     return mediaType.substring(pos, len);
342 }
343
344 void findCharsetInMediaType(const String& mediaType, unsigned int& charsetPos, unsigned int& charsetLen, unsigned int start)
345 {
346     charsetPos = start;
347     charsetLen = 0;
348
349     size_t pos = start;
350     unsigned length = mediaType.length();
351     
352     while (pos < length) {
353         pos = mediaType.findIgnoringASCIICase("charset", pos);
354         if (pos == notFound || pos == 0) {
355             charsetLen = 0;
356             return;
357         }
358         
359         // is what we found a beginning of a word?
360         if (mediaType[pos-1] > ' ' && mediaType[pos-1] != ';') {
361             pos += 7;
362             continue;
363         }
364         
365         pos += 7;
366
367         // skip whitespace
368         while (pos != length && mediaType[pos] <= ' ')
369             ++pos;
370     
371         if (mediaType[pos++] != '=') // this "charset" substring wasn't a parameter name, but there may be others
372             continue;
373
374         while (pos != length && (mediaType[pos] <= ' ' || mediaType[pos] == '"' || mediaType[pos] == '\''))
375             ++pos;
376
377         // we don't handle spaces within quoted parameter values, because charset names cannot have any
378         unsigned endpos = pos;
379         while (pos != length && mediaType[endpos] > ' ' && mediaType[endpos] != '"' && mediaType[endpos] != '\'' && mediaType[endpos] != ';')
380             ++endpos;
381
382         charsetPos = pos;
383         charsetLen = endpos - pos;
384         return;
385     }
386 }
387
388 XSSProtectionDisposition parseXSSProtectionHeader(const String& header, String& failureReason, unsigned& failurePosition, String& reportURL)
389 {
390     static NeverDestroyed<String> failureReasonInvalidToggle(MAKE_STATIC_STRING_IMPL("expected 0 or 1"));
391     static NeverDestroyed<String> failureReasonInvalidSeparator(MAKE_STATIC_STRING_IMPL("expected semicolon"));
392     static NeverDestroyed<String> failureReasonInvalidEquals(MAKE_STATIC_STRING_IMPL("expected equals sign"));
393     static NeverDestroyed<String> failureReasonInvalidMode(MAKE_STATIC_STRING_IMPL("invalid mode directive"));
394     static NeverDestroyed<String> failureReasonInvalidReport(MAKE_STATIC_STRING_IMPL("invalid report directive"));
395     static NeverDestroyed<String> failureReasonDuplicateMode(MAKE_STATIC_STRING_IMPL("duplicate mode directive"));
396     static NeverDestroyed<String> failureReasonDuplicateReport(MAKE_STATIC_STRING_IMPL("duplicate report directive"));
397     static NeverDestroyed<String> failureReasonInvalidDirective(MAKE_STATIC_STRING_IMPL("unrecognized directive"));
398
399     unsigned pos = 0;
400
401     if (!skipWhiteSpace(header, pos))
402         return XSSProtectionDisposition::Enabled;
403
404     if (header[pos] == '0')
405         return XSSProtectionDisposition::Disabled;
406
407     if (header[pos++] != '1') {
408         failureReason = failureReasonInvalidToggle;
409         return XSSProtectionDisposition::Invalid;
410     }
411
412     XSSProtectionDisposition result = XSSProtectionDisposition::Enabled;
413     bool modeDirectiveSeen = false;
414     bool reportDirectiveSeen = false;
415
416     while (1) {
417         // At end of previous directive: consume whitespace, semicolon, and whitespace.
418         if (!skipWhiteSpace(header, pos))
419             return result;
420
421         if (header[pos++] != ';') {
422             failureReason = failureReasonInvalidSeparator;
423             failurePosition = pos;
424             return XSSProtectionDisposition::Invalid;
425         }
426
427         if (!skipWhiteSpace(header, pos))
428             return result;
429
430         // At start of next directive.
431         if (skipToken(header, pos, "mode")) {
432             if (modeDirectiveSeen) {
433                 failureReason = failureReasonDuplicateMode;
434                 failurePosition = pos;
435                 return XSSProtectionDisposition::Invalid;
436             }
437             modeDirectiveSeen = true;
438             if (!skipEquals(header, pos)) {
439                 failureReason = failureReasonInvalidEquals;
440                 failurePosition = pos;
441                 return XSSProtectionDisposition::Invalid;
442             }
443             if (!skipToken(header, pos, "block")) {
444                 failureReason = failureReasonInvalidMode;
445                 failurePosition = pos;
446                 return XSSProtectionDisposition::Invalid;
447             }
448             result = XSSProtectionDisposition::BlockEnabled;
449         } else if (skipToken(header, pos, "report")) {
450             if (reportDirectiveSeen) {
451                 failureReason = failureReasonDuplicateReport;
452                 failurePosition = pos;
453                 return XSSProtectionDisposition::Invalid;
454             }
455             reportDirectiveSeen = true;
456             if (!skipEquals(header, pos)) {
457                 failureReason = failureReasonInvalidEquals;
458                 failurePosition = pos;
459                 return XSSProtectionDisposition::Invalid;
460             }
461             size_t startPos = pos;
462             if (!skipValue(header, pos)) {
463                 failureReason = failureReasonInvalidReport;
464                 failurePosition = pos;
465                 return XSSProtectionDisposition::Invalid;
466             }
467             reportURL = header.substring(startPos, pos - startPos);
468             failurePosition = startPos; // If later semantic check deems unacceptable.
469         } else {
470             failureReason = failureReasonInvalidDirective;
471             failurePosition = pos;
472             return XSSProtectionDisposition::Invalid;
473         }
474     }
475 }
476
477 ContentTypeOptionsDisposition parseContentTypeOptionsHeader(StringView header)
478 {
479     StringView leftToken = header.left(header.find(','));
480     if (equalLettersIgnoringASCIICase(stripLeadingAndTrailingHTTPSpaces(leftToken), "nosniff"))
481         return ContentTypeOptionsNosniff;
482     return ContentTypeOptionsNone;
483 }
484
485 // For example: "HTTP/1.1 200 OK" => "OK".
486 // Note that HTTP/2 does not include a reason phrase, so we return the empty atom.
487 AtomicString extractReasonPhraseFromHTTPStatusLine(const String& statusLine)
488 {
489     StringView view = statusLine;
490     size_t spacePos = view.find(' ');
491
492     // Remove status code from the status line.
493     spacePos = view.find(' ', spacePos + 1);
494     if (spacePos == notFound)
495         return emptyAtom();
496
497     return view.substring(spacePos + 1).toAtomicString();
498 }
499
500 XFrameOptionsDisposition parseXFrameOptionsHeader(const String& header)
501 {
502     XFrameOptionsDisposition result = XFrameOptionsNone;
503
504     if (header.isEmpty())
505         return result;
506
507     for (auto& currentHeader : header.split(',')) {
508         currentHeader = currentHeader.stripWhiteSpace();
509         XFrameOptionsDisposition currentValue = XFrameOptionsNone;
510         if (equalLettersIgnoringASCIICase(currentHeader, "deny"))
511             currentValue = XFrameOptionsDeny;
512         else if (equalLettersIgnoringASCIICase(currentHeader, "sameorigin"))
513             currentValue = XFrameOptionsSameOrigin;
514         else if (equalLettersIgnoringASCIICase(currentHeader, "allowall"))
515             currentValue = XFrameOptionsAllowAll;
516         else
517             currentValue = XFrameOptionsInvalid;
518
519         if (result == XFrameOptionsNone)
520             result = currentValue;
521         else if (result != currentValue)
522             return XFrameOptionsConflict;
523     }
524     return result;
525 }
526
527 bool parseRange(const String& range, long long& rangeOffset, long long& rangeEnd, long long& rangeSuffixLength)
528 {
529     // The format of "Range" header is defined in RFC 2616 Section 14.35.1.
530     // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
531     // We don't support multiple range requests.
532
533     rangeOffset = rangeEnd = rangeSuffixLength = -1;
534
535     // The "bytes" unit identifier should be present.
536     static const unsigned bytesLength = 6;
537     if (!startsWithLettersIgnoringASCIICase(range, "bytes="))
538         return false;
539     // FIXME: The rest of this should use StringView.
540     String byteRange = range.substring(bytesLength);
541
542     // The '-' character needs to be present.
543     int index = byteRange.find('-');
544     if (index == -1)
545         return false;
546
547     // If the '-' character is at the beginning, the suffix length, which specifies the last N bytes, is provided.
548     // Example:
549     //     -500
550     if (!index) {
551         String suffixLengthString = byteRange.substring(index + 1).stripWhiteSpace();
552         bool ok;
553         long long value = suffixLengthString.toInt64Strict(&ok);
554         if (ok)
555             rangeSuffixLength = value;
556         return true;
557     }
558
559     // Otherwise, the first-byte-position and the last-byte-position are provied.
560     // Examples:
561     //     0-499
562     //     500-
563     String firstBytePosStr = byteRange.left(index).stripWhiteSpace();
564     bool ok;
565     long long firstBytePos = firstBytePosStr.toInt64Strict(&ok);
566     if (!ok)
567         return false;
568
569     String lastBytePosStr = byteRange.substring(index + 1).stripWhiteSpace();
570     long long lastBytePos = -1;
571     if (!lastBytePosStr.isEmpty()) {
572         lastBytePos = lastBytePosStr.toInt64Strict(&ok);
573         if (!ok)
574             return false;
575     }
576
577     if (firstBytePos < 0 || !(lastBytePos == -1 || lastBytePos >= firstBytePos))
578         return false;
579
580     rangeOffset = firstBytePos;
581     rangeEnd = lastBytePos;
582     return true;
583 }
584
585 // HTTP/1.1 - RFC 2616
586 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
587 // Request-Line = Method SP Request-URI SP HTTP-Version CRLF
588 size_t parseHTTPRequestLine(const char* data, size_t length, String& failureReason, String& method, String& url, HTTPVersion& httpVersion)
589 {
590     method = String();
591     url = String();
592     httpVersion = Unknown;
593
594     const char* space1 = 0;
595     const char* space2 = 0;
596     const char* p;
597     size_t consumedLength;
598
599     for (p = data, consumedLength = 0; consumedLength < length; p++, consumedLength++) {
600         if (*p == ' ') {
601             if (!space1)
602                 space1 = p;
603             else if (!space2)
604                 space2 = p;
605         } else if (*p == '\n')
606             break;
607     }
608
609     // Haven't finished header line.
610     if (consumedLength == length) {
611         failureReason = "Incomplete Request Line"_s;
612         return 0;
613     }
614
615     // RequestLine does not contain 3 parts.
616     if (!space1 || !space2) {
617         failureReason = "Request Line does not appear to contain: <Method> <Url> <HTTPVersion>."_s;
618         return 0;
619     }
620
621     // The line must end with "\r\n".
622     const char* end = p + 1;
623     if (*(end - 2) != '\r') {
624         failureReason = "Request line does not end with CRLF"_s;
625         return 0;
626     }
627
628     // Request Method.
629     method = String(data, space1 - data); // For length subtract 1 for space, but add 1 for data being the first character.
630
631     // Request URI.
632     url = String(space1 + 1, space2 - space1 - 1); // For length subtract 1 for space.
633
634     // HTTP Version.
635     String httpVersionString(space2 + 1, end - space2 - 3); // For length subtract 1 for space, and 2 for "\r\n".
636     if (httpVersionString.length() != 8 || !httpVersionString.startsWith("HTTP/1."))
637         httpVersion = Unknown;
638     else if (httpVersionString[7] == '0')
639         httpVersion = HTTP_1_0;
640     else if (httpVersionString[7] == '1')
641         httpVersion = HTTP_1_1;
642     else
643         httpVersion = Unknown;
644
645     return end - data;
646 }
647
648 static inline bool isValidHeaderNameCharacter(const char* character)
649 {
650     // https://tools.ietf.org/html/rfc7230#section-3.2
651     // A header name should only contain one or more of
652     // alphanumeric or ! # $ % & ' * + - . ^ _ ` | ~
653     if (isASCIIAlphanumeric(*character))
654         return true;
655     switch (*character) {
656     case '!':
657     case '#':
658     case '$':
659     case '%':
660     case '&':
661     case '\'':
662     case '*':
663     case '+':
664     case '-':
665     case '.':
666     case '^':
667     case '_':
668     case '`':
669     case '|':
670     case '~':
671         return true;
672     default:
673         return false;
674     }
675 }
676
677 size_t parseHTTPHeader(const char* start, size_t length, String& failureReason, StringView& nameStr, String& valueStr, bool strict)
678 {
679     const char* p = start;
680     const char* end = start + length;
681
682     Vector<char> name;
683     Vector<char> value;
684
685     bool foundFirstNameChar = false;
686     const char* namePtr = nullptr;
687     size_t nameSize = 0;
688
689     nameStr = StringView();
690     valueStr = String();
691
692     for (; p < end; p++) {
693         switch (*p) {
694         case '\r':
695             if (name.isEmpty()) {
696                 if (p + 1 < end && *(p + 1) == '\n')
697                     return (p + 2) - start;
698                 failureReason = makeString("CR doesn't follow LF in header name at ", trimInputSample(p, end - p));
699                 return 0;
700             }
701             failureReason = makeString("Unexpected CR in header name at ", trimInputSample(name.data(), name.size()));
702             return 0;
703         case '\n':
704             failureReason = makeString("Unexpected LF in header name at ", trimInputSample(name.data(), name.size()));
705             return 0;
706         case ':':
707             break;
708         default:
709             if (!isValidHeaderNameCharacter(p)) {
710                 if (name.size() < 1)
711                     failureReason = "Unexpected start character in header name";
712                 else
713                     failureReason = makeString("Unexpected character in header name at ", trimInputSample(name.data(), name.size()));
714                 return 0;
715             }
716             name.append(*p);
717             if (!foundFirstNameChar) {
718                 namePtr = p;
719                 foundFirstNameChar = true;
720             }
721             continue;
722         }
723         if (*p == ':') {
724             ++p;
725             break;
726         }
727     }
728
729     nameSize = name.size();
730     nameStr = StringView(reinterpret_cast<const LChar*>(namePtr), nameSize);
731
732     for (; p < end && *p == 0x20; p++) { }
733
734     for (; p < end; p++) {
735         switch (*p) {
736         case '\r':
737             break;
738         case '\n':
739             if (strict) {
740                 failureReason = makeString("Unexpected LF in header value at ", trimInputSample(value.data(), value.size()));
741                 return 0;
742             }
743             break;
744         default:
745             value.append(*p);
746         }
747         if (*p == '\r' || (!strict && *p == '\n')) {
748             ++p;
749             break;
750         }
751     }
752     if (p >= end || (strict && *p != '\n')) {
753         failureReason = makeString("CR doesn't follow LF after header value at ", trimInputSample(p, end - p));
754         return 0;
755     }
756     valueStr = String::fromUTF8(value.data(), value.size());
757     if (valueStr.isNull()) {
758         failureReason = "Invalid UTF-8 sequence in header value"_s;
759         return 0;
760     }
761     return p - start;
762 }
763
764 size_t parseHTTPRequestBody(const char* data, size_t length, Vector<unsigned char>& body)
765 {
766     body.clear();
767     body.append(data, length);
768
769     return length;
770 }
771
772 void parseAccessControlExposeHeadersAllowList(const String& headerValue, HTTPHeaderSet& headerSet)
773 {
774     for (auto& header : headerValue.split(',')) {
775         String strippedHeader = header.stripWhiteSpace();
776         if (!strippedHeader.isEmpty())
777             headerSet.add(strippedHeader);
778     }
779 }
780
781 // Implements <https://fetch.spec.whatwg.org/#forbidden-header-name>.
782 bool isForbiddenHeaderName(const String& name)
783 {
784     HTTPHeaderName headerName;
785     if (findHTTPHeaderName(name, headerName)) {
786         switch (headerName) {
787         case HTTPHeaderName::AcceptCharset:
788         case HTTPHeaderName::AcceptEncoding:
789         case HTTPHeaderName::AccessControlRequestHeaders:
790         case HTTPHeaderName::AccessControlRequestMethod:
791         case HTTPHeaderName::Connection:
792         case HTTPHeaderName::ContentLength:
793         case HTTPHeaderName::Cookie:
794         case HTTPHeaderName::Cookie2:
795         case HTTPHeaderName::Date:
796         case HTTPHeaderName::DNT:
797         case HTTPHeaderName::Expect:
798         case HTTPHeaderName::Host:
799         case HTTPHeaderName::KeepAlive:
800         case HTTPHeaderName::Origin:
801         case HTTPHeaderName::Referer:
802         case HTTPHeaderName::TE:
803         case HTTPHeaderName::Trailer:
804         case HTTPHeaderName::TransferEncoding:
805         case HTTPHeaderName::Upgrade:
806         case HTTPHeaderName::Via:
807             return true;
808         default:
809             break;
810         }
811     }
812     return startsWithLettersIgnoringASCIICase(name, "sec-") || startsWithLettersIgnoringASCIICase(name, "proxy-");
813 }
814
815 // Implements <https://fetch.spec.whatwg.org/#forbidden-response-header-name>.
816 bool isForbiddenResponseHeaderName(const String& name)
817 {
818     return equalLettersIgnoringASCIICase(name, "set-cookie") || equalLettersIgnoringASCIICase(name, "set-cookie2");
819 }
820
821 // Implements <https://fetch.spec.whatwg.org/#forbidden-method>.
822 bool isForbiddenMethod(const String& name)
823 {
824     return equalLettersIgnoringASCIICase(name, "connect") || equalLettersIgnoringASCIICase(name, "trace") || equalLettersIgnoringASCIICase(name, "track");
825 }
826
827 bool isSimpleHeader(const String& name, const String& value)
828 {
829     HTTPHeaderName headerName;
830     if (!findHTTPHeaderName(name, headerName))
831         return false;
832     return isCrossOriginSafeRequestHeader(headerName, value);
833 }
834
835 bool isCrossOriginSafeHeader(HTTPHeaderName name, const HTTPHeaderSet& accessControlExposeHeaderSet)
836 {
837     switch (name) {
838     case HTTPHeaderName::CacheControl:
839     case HTTPHeaderName::ContentLanguage:
840     case HTTPHeaderName::ContentLength:
841     case HTTPHeaderName::ContentType:
842     case HTTPHeaderName::Expires:
843     case HTTPHeaderName::LastModified:
844     case HTTPHeaderName::Pragma:
845     case HTTPHeaderName::Accept:
846         return true;
847     case HTTPHeaderName::SetCookie:
848     case HTTPHeaderName::SetCookie2:
849         return false;
850     default:
851         break;
852     }
853     return accessControlExposeHeaderSet.contains(httpHeaderNameString(name).toStringWithoutCopying());
854 }
855
856 bool isCrossOriginSafeHeader(const String& name, const HTTPHeaderSet& accessControlExposeHeaderSet)
857 {
858 #ifndef ASSERT_DISABLED
859     HTTPHeaderName headerName;
860     ASSERT(!findHTTPHeaderName(name, headerName));
861 #endif
862     return accessControlExposeHeaderSet.contains(name);
863 }
864
865 // Implements https://fetch.spec.whatwg.org/#cors-safelisted-request-header
866 bool isCrossOriginSafeRequestHeader(HTTPHeaderName name, const String& value)
867 {
868     switch (name) {
869     case HTTPHeaderName::Accept:
870         return isValidAcceptHeaderValue(value);
871     case HTTPHeaderName::AcceptLanguage:
872     case HTTPHeaderName::ContentLanguage:
873         return isValidLanguageHeaderValue(value);
874     case HTTPHeaderName::ContentType: {
875         // Preflight is required for MIME types that can not be sent via form submission.
876         String mimeType = extractMIMETypeFromMediaType(value);
877         return equalLettersIgnoringASCIICase(mimeType, "application/x-www-form-urlencoded") || equalLettersIgnoringASCIICase(mimeType, "multipart/form-data") || equalLettersIgnoringASCIICase(mimeType, "text/plain");
878     }
879     default:
880         // FIXME: Should we also make safe other headers (DPR, Downlink, Save-Data...)? That would require validating their values.
881         return false;
882     }
883 }
884
885 // Implements <https://fetch.spec.whatwg.org/#concept-method-normalize>.
886 String normalizeHTTPMethod(const String& method)
887 {
888     const ASCIILiteral methods[] = { "DELETE"_s, "GET"_s, "HEAD"_s, "OPTIONS"_s, "POST"_s, "PUT"_s };
889     for (auto value : methods) {
890         if (equalIgnoringASCIICase(method, value.characters())) {
891             // Don't bother allocating a new string if it's already all uppercase.
892             if (method == value)
893                 break;
894             return value;
895         }
896     }
897     return method;
898 }
899
900 CrossOriginResourcePolicy parseCrossOriginResourcePolicyHeader(StringView header)
901 {
902     auto strippedHeader = stripLeadingAndTrailingHTTPSpaces(header);
903
904     if (strippedHeader.isEmpty())
905         return CrossOriginResourcePolicy::None;
906
907     if (strippedHeader == "same-origin")
908         return CrossOriginResourcePolicy::SameOrigin;
909
910     if (strippedHeader == "same-site")
911         return CrossOriginResourcePolicy::SameSite;
912
913     return CrossOriginResourcePolicy::Invalid;
914 }
915
916 }