LibWeb: Organize Fetch Headers.h/Headers.cpp a bit

Generally just define things in the order they are declared (will make a
change to use ByteString in this file a bit easier to follow). Also make
a couple of free functions be class methods on Header / HeaderList.
This commit is contained in:
Timothy Flynn 2025-11-25 10:57:32 -05:00 committed by Tim Flynn
parent 556364fd76
commit d70224ad2e
Notes: github-actions[bot] 2025-11-26 14:16:37 +00:00
7 changed files with 372 additions and 386 deletions

View file

@ -101,7 +101,7 @@ GC::Ref<PolicyList> Policy::parse_a_responses_content_security_policies(GC::Heap
// 2. For each token returned by extracting header list values given Content-Security-Policy and responses header
// list:
auto enforce_policy_tokens_or_failure = Fetch::Infrastructure::extract_header_list_values("Content-Security-Policy"sv.bytes(), response->header_list());
auto enforce_policy_tokens_or_failure = response->header_list()->extract_header_list_values("Content-Security-Policy"sv.bytes());
auto enforce_policy_tokens = enforce_policy_tokens_or_failure.has<Vector<ByteBuffer>>() ? enforce_policy_tokens_or_failure.get<Vector<ByteBuffer>>() : Vector<ByteBuffer> {};
for (auto const& enforce_policy_token : enforce_policy_tokens) {
// 1. Let policy be the result of parsing token, with a source of "header", and a disposition of "enforce".
@ -115,7 +115,7 @@ GC::Ref<PolicyList> Policy::parse_a_responses_content_security_policies(GC::Heap
// 3. For each token returned by extracting header list values given Content-Security-Policy-Report-Only and
// responses header list:
auto report_policy_tokens_or_failure = Fetch::Infrastructure::extract_header_list_values("Content-Security-Policy-Report-Only"sv.bytes(), response->header_list());
auto report_policy_tokens_or_failure = response->header_list()->extract_header_list_values("Content-Security-Policy-Report-Only"sv.bytes());
auto report_policy_tokens = report_policy_tokens_or_failure.has<Vector<ByteBuffer>>() ? report_policy_tokens_or_failure.get<Vector<ByteBuffer>>() : Vector<ByteBuffer> {};
for (auto const& report_policy_token : report_policy_tokens) {
// 1. Let policy be the result of parsing token, with a source of "header", and a disposition of "report".

View file

@ -495,7 +495,7 @@ GC::Ptr<PendingResponse> main_fetch(JS::Realm& realm, Infrastructure::FetchParam
if (request->response_tainting() == Infrastructure::Request::ResponseTainting::CORS) {
// 1. Let headerNames be the result of extracting header list values given
// `Access-Control-Expose-Headers` and responses header list.
auto header_names_or_failure = Infrastructure::extract_header_list_values("Access-Control-Expose-Headers"sv.bytes(), response->header_list());
auto header_names_or_failure = response->header_list()->extract_header_list_values("Access-Control-Expose-Headers"sv.bytes());
auto header_names = header_names_or_failure.has<Vector<ByteBuffer>>() ? header_names_or_failure.get<Vector<ByteBuffer>>() : Vector<ByteBuffer> {};
// 2. If requests credentials mode is not "include" and headerNames contains `*`, then set
@ -2513,19 +2513,19 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
// NOTE: The CORS check is done on request rather than preflight to ensure the correct credentials mode is used.
if (cors_check(request, response) && Infrastructure::is_ok_status(response->status())) {
// 1. Let methods be the result of extracting header list values given `Access-Control-Allow-Methods` and responses header list.
auto methods_or_failure = Infrastructure::extract_header_list_values("Access-Control-Allow-Methods"sv.bytes(), response->header_list());
auto methods_or_failure = response->header_list()->extract_header_list_values("Access-Control-Allow-Methods"sv.bytes());
// 2. Let headerNames be the result of extracting header list values given `Access-Control-Allow-Headers` and
// responses header list.
auto header_names_or_failure = Infrastructure::extract_header_list_values("Access-Control-Allow-Headers"sv.bytes(), response->header_list());
auto header_names_or_failure = response->header_list()->extract_header_list_values("Access-Control-Allow-Headers"sv.bytes());
// 3. If either methods or headerNames is failure, return a network error.
if (methods_or_failure.has<Infrastructure::ExtractHeaderParseFailure>()) {
if (methods_or_failure.has<Infrastructure::HeaderList::ExtractHeaderParseFailure>()) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, "The Access-Control-Allow-Methods in the CORS-preflight response is syntactically invalid"_string));
return;
}
if (header_names_or_failure.has<Infrastructure::ExtractHeaderParseFailure>()) {
if (header_names_or_failure.has<Infrastructure::HeaderList::ExtractHeaderParseFailure>()) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, "The Access-Control-Allow-Headers in the CORS-preflight response is syntactically invalid"_string));
return;
}

View file

@ -66,27 +66,40 @@ Header Header::from_latin1_pair(StringView name, StringView value)
};
}
// https://fetch.spec.whatwg.org/#extract-header-values
Optional<Vector<ByteBuffer>> Header::extract_header_values() const
{
// FIXME: 1. If parsing headers value, per the ABNF for headers name, fails, then return failure.
// FIXME: 2. Return one or more values resulting from parsing headers value, per the ABNF for headers name.
// For now we only parse some headers that are of the ABNF list form "#something"
if (StringView { name }.is_one_of_ignoring_ascii_case(
"Access-Control-Request-Headers"sv,
"Access-Control-Expose-Headers"sv,
"Access-Control-Allow-Headers"sv,
"Access-Control-Allow-Methods"sv)
&& !value.is_empty()) {
auto split_values = StringView { value }.split_view(',');
Vector<ByteBuffer> trimmed_values;
for (auto const& value : split_values) {
auto trimmed_value = value.trim(" \t"sv);
auto trimmed_value_as_byte_buffer = MUST(ByteBuffer::copy(trimmed_value.bytes()));
trimmed_values.append(move(trimmed_value_as_byte_buffer));
}
return trimmed_values;
}
// This always ignores the ABNF rules for now and returns the header value as a single list item.
return Vector { MUST(ByteBuffer::copy(value)) };
}
GC::Ref<HeaderList> HeaderList::create(JS::VM& vm)
{
return vm.heap().allocate<HeaderList>();
}
// Non-standard
Vector<ByteBuffer> HeaderList::unique_names() const
{
Vector<ByteBuffer> header_names_set;
HashTable<ReadonlyBytes, CaseInsensitiveBytesTraits<u8 const>> header_names_seen;
for (auto const& header : *this) {
if (header_names_seen.contains(header.name))
continue;
header_names_seen.set(header.name);
header_names_set.append(MUST(ByteBuffer::copy(header.name)));
}
return header_names_set;
}
// https://fetch.spec.whatwg.org/#header-list-contains
bool HeaderList::contains(ReadonlyBytes name) const
{
@ -138,60 +151,6 @@ Optional<Vector<String>> HeaderList::get_decode_and_split(ReadonlyBytes name) co
return get_decode_and_split_header_value(*value);
}
// https://fetch.spec.whatwg.org/#header-value-get-decode-and-split
Optional<Vector<String>> get_decode_and_split_header_value(ReadonlyBytes value)
{
// To get, decode, and split a header value value, run these steps:
// 1. Let input be the result of isomorphic decoding value.
auto input = Infra::isomorphic_decode(value);
// 2. Let position be a position variable for input, initially pointing at the start of input.
auto lexer = GenericLexer { input };
// 3. Let values be a list of strings, initially « ».
Vector<String> values;
// 4. Let temporaryValue be the empty string.
StringBuilder temporary_value_builder;
// 5. While true:
while (true) {
// 1. Append the result of collecting a sequence of code points that are not U+0022 (") or U+002C (,) from input, given position, to temporaryValue.
// NOTE: The result might be the empty string.
temporary_value_builder.append(lexer.consume_until(is_any_of("\","sv)));
// 2. If position is not past the end of input and the code point at position within input is U+0022 ("):
if (!lexer.is_eof() && lexer.peek() == '"') {
// 1. Append the result of collecting an HTTP quoted string from input, given position, to temporaryValue.
temporary_value_builder.append(collect_an_http_quoted_string(lexer));
// 2. If position is not past the end of input, then continue.
if (!lexer.is_eof())
continue;
}
// 3. Remove all HTTP tab or space from the start and end of temporaryValue.
auto temporary_value = MUST(String::from_utf8(temporary_value_builder.string_view().trim(HTTP_TAB_OR_SPACE, TrimMode::Both)));
// 4. Append temporaryValue to values.
values.append(move(temporary_value));
// 5. Set temporaryValue to the empty string.
temporary_value_builder.clear();
// 6. If position is past the end of input, then return values.
if (lexer.is_eof())
return values;
// 7. Assert: the code point at position within input is U+002C (,).
VERIFY(lexer.peek() == ',');
// 8. Advance position by 1.
lexer.ignore(1);
}
}
// https://fetch.spec.whatwg.org/#concept-header-list-append
void HeaderList::append(Header header)
{
@ -323,15 +282,48 @@ Vector<Header> HeaderList::sort_and_combine() const
return headers;
}
// https://fetch.spec.whatwg.org/#extract-header-list-values
Variant<Empty, Vector<ByteBuffer>, HeaderList::ExtractHeaderParseFailure> HeaderList::extract_header_list_values(ReadonlyBytes name) const
{
// 1. If list does not contain name, then return null.
if (!contains(name))
return {};
// FIXME: 2. If the ABNF for name allows a single header and list contains more than one, then return failure.
// NOTE: If different error handling is needed, extract the desired header first.
// 3. Let values be an empty list.
auto values = Vector<ByteBuffer> {};
// 4. For each header header list contains whose name is name:
for (auto const& header : *this) {
if (!StringView { header.name }.equals_ignoring_ascii_case(name))
continue;
// 1. Let extract be the result of extracting header values from header.
auto extract = header.extract_header_values();
// 2. If extract is failure, then return failure.
if (!extract.has_value())
return ExtractHeaderParseFailure {};
// 3. Append each value in extract, in order, to values.
values.extend(extract.release_value());
}
// 5. Return values.
return values;
}
// https://fetch.spec.whatwg.org/#header-list-extract-a-length
HeaderList::ExtractLengthResult HeaderList::extract_length() const
Variant<Empty, u64, HeaderList::ExtractLengthFailure> HeaderList::extract_length() const
{
// 1. Let values be the result of getting, decoding, and splitting `Content-Length` from headers.
auto values = get_decode_and_split("Content-Length"sv.bytes());
// 2. If values is null, then return null.
if (!values.has_value())
return Empty {};
return {};
// 3. Let candidateValue be null.
Optional<String> candidate_value;
@ -349,14 +341,13 @@ HeaderList::ExtractLengthResult HeaderList::extract_length() const
}
// 5. If candidateValue is the empty string or has a code point that is not an ASCII digit, then return null.
// NOTE: to_number does this for us.
// 6. Return candidateValue, interpreted as decimal number.
// NOTE: The spec doesn't say anything about trimming here, so we don't trim. If it contains a space, step 5 will cause us to return null.
// FIXME: This will return an empty Optional if it cannot fit into a u64, is this correct?
auto conversion_result = candidate_value.value().to_number<u64>(TrimWhitespace::No);
if (!conversion_result.has_value())
return Empty {};
return ExtractLengthResult { conversion_result.release_value() };
auto result = candidate_value->to_number<u64>(TrimWhitespace::No);
if (!result.has_value())
return {};
return *result;
}
// https://fetch.spec.whatwg.org/#concept-header-extract-mime-type
@ -414,58 +405,20 @@ Optional<MimeSniff::MimeType> HeaderList::extract_mime_type() const
return mime_type;
}
// https://fetch.spec.whatwg.org/#legacy-extract-an-encoding
StringView legacy_extract_an_encoding(Optional<MimeSniff::MimeType> const& mime_type, StringView fallback_encoding)
// Non-standard
Vector<ByteBuffer> HeaderList::unique_names() const
{
// 1. If mimeType is failure, then return fallbackEncoding.
if (!mime_type.has_value())
return fallback_encoding;
// 2. If mimeType["charset"] does not exist, then return fallbackEncoding.
auto charset = mime_type->parameters().get("charset"sv);
if (!charset.has_value())
return fallback_encoding;
// 3. Let tentativeEncoding be the result of getting an encoding from mimeType["charset"].
auto tentative_encoding = TextCodec::get_standardized_encoding(*charset);
// 4. If tentativeEncoding is failure, then return fallbackEncoding.
if (!tentative_encoding.has_value())
return fallback_encoding;
// 5. Return tentativeEncoding.
return *tentative_encoding;
}
// https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set
OrderedHashTable<ByteBuffer> convert_header_names_to_a_sorted_lowercase_set(Span<ReadonlyBytes> header_names)
{
// To convert header names to a sorted-lowercase set, given a list of names headerNames, run these steps:
// 1. Let headerNamesSet be a new ordered set.
Vector<ByteBuffer> header_names_set;
HashTable<ReadonlyBytes, CaseInsensitiveBytesTraits<u8 const>> header_names_seen;
// 2. For each name of headerNames, append the result of byte-lowercasing name to headerNamesSet.
for (auto name : header_names) {
if (header_names_seen.contains(name))
for (auto const& header : *this) {
if (header_names_seen.contains(header.name))
continue;
auto bytes = MUST(ByteBuffer::copy(name));
Infra::byte_lowercase(bytes);
header_names_seen.set(name);
header_names_set.append(move(bytes));
header_names_seen.set(header.name);
header_names_set.append(MUST(ByteBuffer::copy(header.name)));
}
// 3. Return the result of sorting headerNamesSet in ascending order with byte less than.
quick_sort(header_names_set, [](auto const& a, auto const& b) {
return StringView { a } < StringView { b };
});
OrderedHashTable<ByteBuffer> ordered { header_names_set.size() };
for (auto& name : header_names_set) {
auto result = ordered.set(move(name));
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}
return ordered;
return header_names_set;
}
// https://fetch.spec.whatwg.org/#header-name
@ -503,6 +456,268 @@ ByteBuffer normalize_header_value(ReadonlyBytes potential_value)
return MUST(ByteBuffer::copy(trimmed.bytes()));
}
// https://fetch.spec.whatwg.org/#forbidden-header-name
bool is_forbidden_request_header(Header const& header)
{
// A header (name, value) is forbidden request-header if these steps return true:
auto name = StringView { header.name };
// 1. If name is a byte-case-insensitive match for one of:
// [...]
// then return true.
if (name.is_one_of_ignoring_ascii_case(
"Accept-Charset"sv,
"Accept-Encoding"sv,
"Access-Control-Request-Headers"sv,
"Access-Control-Request-Method"sv,
"Connection"sv,
"Content-Length"sv,
"Cookie"sv,
"Cookie2"sv,
"Date"sv,
"DNT"sv,
"Expect"sv,
"Host"sv,
"Keep-Alive"sv,
"Origin"sv,
"Referer"sv,
"Set-Cookie"sv,
"TE"sv,
"Trailer"sv,
"Transfer-Encoding"sv,
"Upgrade"sv,
"Via"sv)) {
return true;
}
// 2. If name when byte-lowercased starts with `proxy-` or `sec-`, then return true.
if (name.starts_with("proxy-"sv, CaseSensitivity::CaseInsensitive)
|| name.starts_with("sec-"sv, CaseSensitivity::CaseInsensitive)) {
return true;
}
// 3. If name is a byte-case-insensitive match for one of:
// - `X-HTTP-Method`
// - `X-HTTP-Method-Override`
// - `X-Method-Override`
// then:
if (name.is_one_of_ignoring_ascii_case(
"X-HTTP-Method"sv,
"X-HTTP-Method-Override"sv,
"X-Method-Override"sv)) {
// 1. Let parsedValues be the result of getting, decoding, and splitting value.
auto parsed_values = get_decode_and_split_header_value(header.value);
// 2. For each method of parsedValues: if the isomorphic encoding of method is a forbidden method, then return true.
// Note: The values returned from get_decode_and_split_header_value have already been decoded.
if (parsed_values.has_value() && any_of(*parsed_values, [](auto method) { return is_forbidden_method(method.bytes()); }))
return true;
}
// 4. Return false.
return false;
}
// https://fetch.spec.whatwg.org/#forbidden-response-header-name
bool is_forbidden_response_header_name(ReadonlyBytes header_name)
{
// A forbidden response-header name is a header name that is a byte-case-insensitive match for one of:
// - `Set-Cookie`
// - `Set-Cookie2`
return StringView { header_name }.is_one_of_ignoring_ascii_case(
"Set-Cookie"sv,
"Set-Cookie2"sv);
}
// https://fetch.spec.whatwg.org/#legacy-extract-an-encoding
StringView legacy_extract_an_encoding(Optional<MimeSniff::MimeType> const& mime_type, StringView fallback_encoding)
{
// 1. If mimeType is failure, then return fallbackEncoding.
if (!mime_type.has_value())
return fallback_encoding;
// 2. If mimeType["charset"] does not exist, then return fallbackEncoding.
auto charset = mime_type->parameters().get("charset"sv);
if (!charset.has_value())
return fallback_encoding;
// 3. Let tentativeEncoding be the result of getting an encoding from mimeType["charset"].
auto tentative_encoding = TextCodec::get_standardized_encoding(*charset);
// 4. If tentativeEncoding is failure, then return fallbackEncoding.
if (!tentative_encoding.has_value())
return fallback_encoding;
// 5. Return tentativeEncoding.
return *tentative_encoding;
}
// https://fetch.spec.whatwg.org/#header-value-get-decode-and-split
Optional<Vector<String>> get_decode_and_split_header_value(ReadonlyBytes value)
{
// To get, decode, and split a header value value, run these steps:
// 1. Let input be the result of isomorphic decoding value.
auto input = Infra::isomorphic_decode(value);
// 2. Let position be a position variable for input, initially pointing at the start of input.
GenericLexer lexer { input };
// 3. Let values be a list of strings, initially « ».
Vector<String> values;
// 4. Let temporaryValue be the empty string.
StringBuilder temporary_value_builder;
// 5. While true:
while (true) {
// 1. Append the result of collecting a sequence of code points that are not U+0022 (") or U+002C (,) from input, given position, to temporaryValue.
// NOTE: The result might be the empty string.
temporary_value_builder.append(lexer.consume_until(is_any_of("\","sv)));
// 2. If position is not past the end of input and the code point at position within input is U+0022 ("):
if (!lexer.is_eof() && lexer.peek() == '"') {
// 1. Append the result of collecting an HTTP quoted string from input, given position, to temporaryValue.
temporary_value_builder.append(collect_an_http_quoted_string(lexer));
// 2. If position is not past the end of input, then continue.
if (!lexer.is_eof())
continue;
}
// 3. Remove all HTTP tab or space from the start and end of temporaryValue.
auto temporary_value = MUST(String::from_utf8(temporary_value_builder.string_view().trim(HTTP_TAB_OR_SPACE, TrimMode::Both)));
// 4. Append temporaryValue to values.
values.append(move(temporary_value));
// 5. Set temporaryValue to the empty string.
temporary_value_builder.clear();
// 6. If position is past the end of input, then return values.
if (lexer.is_eof())
return values;
// 7. Assert: the code point at position within input is U+002C (,).
VERIFY(lexer.peek() == ',');
// 8. Advance position by 1.
lexer.ignore(1);
}
}
// https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set
OrderedHashTable<ByteBuffer> convert_header_names_to_a_sorted_lowercase_set(Span<ReadonlyBytes> header_names)
{
// To convert header names to a sorted-lowercase set, given a list of names headerNames, run these steps:
// 1. Let headerNamesSet be a new ordered set.
Vector<ByteBuffer> header_names_set;
HashTable<ReadonlyBytes, CaseInsensitiveBytesTraits<u8 const>> header_names_seen;
// 2. For each name of headerNames, append the result of byte-lowercasing name to headerNamesSet.
for (auto name : header_names) {
if (header_names_seen.contains(name))
continue;
auto bytes = MUST(ByteBuffer::copy(name));
Infra::byte_lowercase(bytes);
header_names_seen.set(name);
header_names_set.append(move(bytes));
}
// 3. Return the result of sorting headerNamesSet in ascending order with byte less than.
quick_sort(header_names_set, [](auto const& a, auto const& b) {
return StringView { a } < StringView { b };
});
OrderedHashTable<ByteBuffer> ordered { header_names_set.size() };
for (auto& name : header_names_set) {
auto result = ordered.set(move(name));
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}
return ordered;
}
// https://fetch.spec.whatwg.org/#build-a-content-range
ByteString build_content_range(u64 const& range_start, u64 const& range_end, u64 const& full_length)
{
// 1. Let contentRange be `bytes `.
// 2. Append rangeStart, serialized and isomorphic encoded, to contentRange.
// 3. Append 0x2D (-) to contentRange.
// 4. Append rangeEnd, serialized and isomorphic encoded to contentRange.
// 5. Append 0x2F (/) to contentRange.
// 6. Append fullLength, serialized and isomorphic encoded to contentRange.
// 7. Return contentRange.
return ByteString::formatted("bytes {}-{}/{}", String::number(range_start), String::number(range_end), String::number(full_length));
}
// https://fetch.spec.whatwg.org/#simple-range-header-value
Optional<RangeHeaderValue> parse_single_range_header_value(ReadonlyBytes const value, bool const allow_whitespace)
{
// 1. Let data be the isomorphic decoding of value.
auto const data = Infra::isomorphic_decode(value);
// 2. If data does not start with "bytes", then return failure.
if (!data.starts_with_bytes("bytes"sv))
return {};
// 3. Let position be a position variable for data, initially pointing at the 5th code point of data.
GenericLexer lexer { data };
lexer.ignore(5);
// 4. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from data given position.
if (allow_whitespace)
lexer.consume_while(is_http_tab_or_space);
// 5. If the code point at position within data is not U+003D (=), then return failure.
// 6. Advance position by 1.
if (!lexer.consume_specific('='))
return {};
// 7. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from data given position.
if (allow_whitespace)
lexer.consume_while(is_http_tab_or_space);
// 8. Let rangeStart be the result of collecting a sequence of code points that are ASCII digits, from data given position.
auto range_start = lexer.consume_while(is_ascii_digit);
// 9. Let rangeStartValue be rangeStart, interpreted as decimal number, if rangeStart is not the empty string; otherwise null.
auto range_start_value = range_start.to_number<u64>();
// 10. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from data given position.
if (allow_whitespace)
lexer.consume_while(is_http_tab_or_space);
// 11. If the code point at position within data is not U+002D (-), then return failure.
// 12. Advance position by 1.
if (!lexer.consume_specific('-'))
return {};
// 13. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from data given position.
if (allow_whitespace)
lexer.consume_while(is_http_tab_or_space);
// 14. Let rangeEnd be the result of collecting a sequence of code points that are ASCII digits, from data given position.
auto range_end = lexer.consume_while(is_ascii_digit);
// 15. Let rangeEndValue be rangeEnd, interpreted as decimal number, if rangeEnd is not the empty string; otherwise null.
auto range_end_value = range_end.to_number<u64>();
// 16. If position is not past the end of data, then return failure.
if (!lexer.is_eof())
return {};
// 17. If rangeEndValue and rangeStartValue are null, then return failure.
if (!range_end_value.has_value() && !range_start_value.has_value())
return {};
// 18. If rangeStartValue and rangeEndValue are numbers, and rangeStartValue is greater than rangeEndValue, then return failure.
if (range_start_value.has_value() && range_end_value.has_value() && *range_start_value > *range_end_value)
return {};
// 19. Return (rangeStartValue, rangeEndValue).
return RangeHeaderValue { move(range_start_value), move(range_end_value) };
}
// https://fetch.spec.whatwg.org/#cors-safelisted-request-header
bool is_cors_safelisted_request_header(Header const& header)
{
@ -690,237 +905,6 @@ bool is_no_cors_safelisted_request_header(Header const& header)
return is_cors_safelisted_request_header(header);
}
// https://fetch.spec.whatwg.org/#forbidden-header-name
bool is_forbidden_request_header(Header const& header)
{
// A header (name, value) is forbidden request-header if these steps return true:
auto name = StringView { header.name };
// 1. If name is a byte-case-insensitive match for one of:
// [...]
// then return true.
if (name.is_one_of_ignoring_ascii_case(
"Accept-Charset"sv,
"Accept-Encoding"sv,
"Access-Control-Request-Headers"sv,
"Access-Control-Request-Method"sv,
"Connection"sv,
"Content-Length"sv,
"Cookie"sv,
"Cookie2"sv,
"Date"sv,
"DNT"sv,
"Expect"sv,
"Host"sv,
"Keep-Alive"sv,
"Origin"sv,
"Referer"sv,
"Set-Cookie"sv,
"TE"sv,
"Trailer"sv,
"Transfer-Encoding"sv,
"Upgrade"sv,
"Via"sv)) {
return true;
}
// 2. If name when byte-lowercased starts with `proxy-` or `sec-`, then return true.
if (name.starts_with("proxy-"sv, CaseSensitivity::CaseInsensitive)
|| name.starts_with("sec-"sv, CaseSensitivity::CaseInsensitive)) {
return true;
}
// 3. If name is a byte-case-insensitive match for one of:
// - `X-HTTP-Method`
// - `X-HTTP-Method-Override`
// - `X-Method-Override`
// then:
if (name.is_one_of_ignoring_ascii_case(
"X-HTTP-Method"sv,
"X-HTTP-Method-Override"sv,
"X-Method-Override"sv)) {
// 1. Let parsedValues be the result of getting, decoding, and splitting value.
auto parsed_values = get_decode_and_split_header_value(header.value);
// 2. For each method of parsedValues: if the isomorphic encoding of method is a forbidden method, then return true.
// Note: The values returned from get_decode_and_split_header_value have already been decoded.
if (parsed_values.has_value() && any_of(*parsed_values, [](auto method) { return is_forbidden_method(method.bytes()); }))
return true;
}
// 4. Return false.
return false;
}
// https://fetch.spec.whatwg.org/#forbidden-response-header-name
bool is_forbidden_response_header_name(ReadonlyBytes header_name)
{
// A forbidden response-header name is a header name that is a byte-case-insensitive match for one of:
// - `Set-Cookie`
// - `Set-Cookie2`
return StringView { header_name }.is_one_of_ignoring_ascii_case(
"Set-Cookie"sv,
"Set-Cookie2"sv);
}
// https://fetch.spec.whatwg.org/#request-body-header-name
bool is_request_body_header_name(ReadonlyBytes header_name)
{
// A request-body-header name is a header name that is a byte-case-insensitive match for one of:
// - `Content-Encoding`
// - `Content-Language`
// - `Content-Location`
// - `Content-Type`
return StringView { header_name }.is_one_of_ignoring_ascii_case(
"Content-Encoding"sv,
"Content-Language"sv,
"Content-Location"sv,
"Content-Type"sv);
}
// https://fetch.spec.whatwg.org/#extract-header-values
Optional<Vector<ByteBuffer>> extract_header_values(Header const& header)
{
// FIXME: 1. If parsing headers value, per the ABNF for headers name, fails, then return failure.
// FIXME: 2. Return one or more values resulting from parsing headers value, per the ABNF for headers name.
// For now we only parse some headers that are of the ABNF list form "#something"
if (StringView { header.name }.is_one_of_ignoring_ascii_case(
"Access-Control-Request-Headers"sv,
"Access-Control-Expose-Headers"sv,
"Access-Control-Allow-Headers"sv,
"Access-Control-Allow-Methods"sv)
&& !header.value.is_empty()) {
auto split_values = StringView { header.value }.split_view(',');
Vector<ByteBuffer> trimmed_values;
for (auto const& value : split_values) {
auto trimmed_value = value.trim(" \t"sv);
auto trimmed_value_as_byte_buffer = MUST(ByteBuffer::copy(trimmed_value.bytes()));
trimmed_values.append(move(trimmed_value_as_byte_buffer));
}
return trimmed_values;
}
// This always ignores the ABNF rules for now and returns the header value as a single list item.
return Vector { MUST(ByteBuffer::copy(header.value)) };
}
// https://fetch.spec.whatwg.org/#extract-header-list-values
Variant<Vector<ByteBuffer>, ExtractHeaderParseFailure, Empty> extract_header_list_values(ReadonlyBytes name, HeaderList const& list)
{
// 1. If list does not contain name, then return null.
if (!list.contains(name))
return Empty {};
// FIXME: 2. If the ABNF for name allows a single header and list contains more than one, then return failure.
// NOTE: If different error handling is needed, extract the desired header first.
// 3. Let values be an empty list.
auto values = Vector<ByteBuffer> {};
// 4. For each header header list contains whose name is name:
for (auto const& header : list) {
if (!StringView { header.name }.equals_ignoring_ascii_case(name))
continue;
// 1. Let extract be the result of extracting header values from header.
auto extract = extract_header_values(header);
// 2. If extract is failure, then return failure.
if (!extract.has_value())
return ExtractHeaderParseFailure {};
// 3. Append each value in extract, in order, to values.
values.extend(extract.release_value());
}
// 5. Return values.
return values;
}
// https://fetch.spec.whatwg.org/#build-a-content-range
ByteString build_content_range(u64 const& range_start, u64 const& range_end, u64 const& full_length)
{
// 1. Let contentRange be `bytes `.
// 2. Append rangeStart, serialized and isomorphic encoded, to contentRange.
// 3. Append 0x2D (-) to contentRange.
// 4. Append rangeEnd, serialized and isomorphic encoded to contentRange.
// 5. Append 0x2F (/) to contentRange.
// 6. Append fullLength, serialized and isomorphic encoded to contentRange.
// 7. Return contentRange.
return ByteString::formatted("bytes {}-{}/{}", String::number(range_start), String::number(range_end), String::number(full_length));
}
// https://fetch.spec.whatwg.org/#simple-range-header-value
Optional<RangeHeaderValue> parse_single_range_header_value(ReadonlyBytes const value, bool const allow_whitespace)
{
// 1. Let data be the isomorphic decoding of value.
auto const data = Infra::isomorphic_decode(value);
// 2. If data does not start with "bytes", then return failure.
if (!data.starts_with_bytes("bytes"sv))
return {};
// 3. Let position be a position variable for data, initially pointing at the 5th code point of data.
auto lexer = GenericLexer { data };
lexer.ignore(5);
// 4. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from data given position.
if (allow_whitespace)
lexer.consume_while(is_http_tab_or_space);
// 5. If the code point at position within data is not U+003D (=), then return failure.
// 6. Advance position by 1.
if (!lexer.consume_specific('='))
return {};
// 7. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from data given position.
if (allow_whitespace)
lexer.consume_while(is_http_tab_or_space);
// 8. Let rangeStart be the result of collecting a sequence of code points that are ASCII digits, from data given position.
auto range_start = lexer.consume_while(is_ascii_digit);
// 9. Let rangeStartValue be rangeStart, interpreted as decimal number, if rangeStart is not the empty string; otherwise null.
auto range_start_value = range_start.to_number<u64>();
// 10. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from data given position.
if (allow_whitespace)
lexer.consume_while(is_http_tab_or_space);
// 11. If the code point at position within data is not U+002D (-), then return failure.
// 12. Advance position by 1.
if (!lexer.consume_specific('-'))
return {};
// 13. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from data given position.
if (allow_whitespace)
lexer.consume_while(is_http_tab_or_space);
// 14. Let rangeEnd be the result of collecting a sequence of code points that are ASCII digits, from data given position.
auto range_end = lexer.consume_while(is_ascii_digit);
// 15. Let rangeEndValue be rangeEnd, interpreted as decimal number, if rangeEnd is not the empty string; otherwise null.
auto range_end_value = range_end.to_number<u64>();
// 16. If position is not past the end of data, then return failure.
if (!lexer.is_eof())
return {};
// 17. If rangeEndValue and rangeStartValue are null, then return failure.
if (!range_end_value.has_value() && !range_start_value.has_value())
return {};
// 18. If rangeStartValue and rangeEndValue are numbers, and rangeStartValue is greater than rangeEndValue, then return failure.
if (range_start_value.has_value() && range_end_value.has_value() && *range_start_value > *range_end_value)
return {};
// 19. Return (rangeStartValue, rangeEndValue).
return RangeHeaderValue { move(range_start_value), move(range_end_value) };
}
// https://fetch.spec.whatwg.org/#default-user-agent-value
ByteBuffer default_user_agent_value()
{

View file

@ -25,12 +25,14 @@ namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-header
// A header is a tuple that consists of a name (a header name) and value (a header value).
struct WEB_API Header {
ByteBuffer name;
ByteBuffer value;
[[nodiscard]] static Header copy(Header const&);
[[nodiscard]] static Header from_string_pair(StringView, StringView);
[[nodiscard]] static Header from_latin1_pair(StringView, StringView);
[[nodiscard]] Optional<Vector<ByteBuffer>> extract_header_values() const;
ByteBuffer name;
ByteBuffer value;
};
// https://fetch.spec.whatwg.org/#concept-header-list
@ -42,13 +44,13 @@ class WEB_API HeaderList final
GC_DECLARE_ALLOCATOR(HeaderList);
public:
[[nodiscard]] static GC::Ref<HeaderList> create(JS::VM&);
using Vector::begin;
using Vector::clear;
using Vector::end;
using Vector::is_empty;
[[nodiscard]] static GC::Ref<HeaderList> create(JS::VM&);
[[nodiscard]] bool contains(ReadonlyBytes) const;
[[nodiscard]] Optional<ByteBuffer> get(ReadonlyBytes) const;
[[nodiscard]] Optional<Vector<String>> get_decode_and_split(ReadonlyBytes) const;
@ -58,10 +60,11 @@ public:
void combine(Header);
[[nodiscard]] Vector<Header> sort_and_combine() const;
struct ExtractLengthFailure { };
using ExtractLengthResult = Variant<u64, ExtractLengthFailure, Empty>;
struct ExtractHeaderParseFailure { };
[[nodiscard]] Variant<Empty, Vector<ByteBuffer>, ExtractHeaderParseFailure> extract_header_list_values(ReadonlyBytes) const;
[[nodiscard]] ExtractLengthResult extract_length() const;
struct ExtractLengthFailure { };
[[nodiscard]] Variant<Empty, u64, ExtractLengthFailure> extract_length() const;
[[nodiscard]] Optional<MimeSniff::MimeType> extract_mime_type() const;
@ -73,15 +76,20 @@ struct RangeHeaderValue {
Optional<u64> end;
};
struct ExtractHeaderParseFailure {
};
[[nodiscard]] bool is_header_name(ReadonlyBytes);
[[nodiscard]] bool is_header_value(ReadonlyBytes);
[[nodiscard]] ByteBuffer normalize_header_value(ReadonlyBytes);
[[nodiscard]] bool is_forbidden_request_header(Header const&);
[[nodiscard]] bool is_forbidden_response_header_name(ReadonlyBytes);
[[nodiscard]] WEB_API StringView legacy_extract_an_encoding(Optional<MimeSniff::MimeType> const& mime_type, StringView fallback_encoding);
[[nodiscard]] Optional<Vector<String>> get_decode_and_split_header_value(ReadonlyBytes);
[[nodiscard]] OrderedHashTable<ByteBuffer> convert_header_names_to_a_sorted_lowercase_set(Span<ReadonlyBytes>);
[[nodiscard]] bool is_header_name(ReadonlyBytes);
[[nodiscard]] bool is_header_value(ReadonlyBytes);
[[nodiscard]] ByteBuffer normalize_header_value(ReadonlyBytes);
[[nodiscard]] WEB_API ByteString build_content_range(u64 const& range_start, u64 const& range_end, u64 const& full_length);
[[nodiscard]] WEB_API Optional<RangeHeaderValue> parse_single_range_header_value(ReadonlyBytes, bool);
[[nodiscard]] bool is_cors_safelisted_request_header(Header const&);
[[nodiscard]] bool is_cors_unsafe_request_header_byte(u8);
[[nodiscard]] WEB_API OrderedHashTable<ByteBuffer> get_cors_unsafe_header_names(HeaderList const&);
@ -90,13 +98,7 @@ struct ExtractHeaderParseFailure {
[[nodiscard]] bool is_cors_safelisted_response_header_name(ReadonlyBytes, Span<ReadonlyBytes>);
[[nodiscard]] bool is_no_cors_safelisted_request_header_name(ReadonlyBytes);
[[nodiscard]] bool is_no_cors_safelisted_request_header(Header const&);
[[nodiscard]] bool is_forbidden_request_header(Header const&);
[[nodiscard]] bool is_forbidden_response_header_name(ReadonlyBytes);
[[nodiscard]] bool is_request_body_header_name(ReadonlyBytes);
[[nodiscard]] Optional<Vector<ByteBuffer>> extract_header_values(Header const&);
[[nodiscard]] WEB_API Variant<Vector<ByteBuffer>, ExtractHeaderParseFailure, Empty> extract_header_list_values(ReadonlyBytes, HeaderList const&);
[[nodiscard]] WEB_API ByteString build_content_range(u64 const& range_start, u64 const& range_end, u64 const& full_length);
[[nodiscard]] WEB_API Optional<RangeHeaderValue> parse_single_range_header_value(ReadonlyBytes, bool);
[[nodiscard]] WEB_API ByteBuffer default_user_agent_value();
}

View file

@ -125,8 +125,8 @@ ErrorOr<Optional<URL::URL>> Response::location_url(Optional<String> const& reque
return Optional<URL::URL> {};
// 2. Let location be the result of extracting header list values given `Location` and responses header list.
auto location_values_or_failure = extract_header_list_values("Location"sv.bytes(), m_header_list);
if (location_values_or_failure.has<Infrastructure::ExtractHeaderParseFailure>() || location_values_or_failure.has<Empty>())
auto location_values_or_failure = m_header_list->extract_header_list_values("Location"sv.bytes());
if (location_values_or_failure.has<Infrastructure::HeaderList::ExtractHeaderParseFailure>() || location_values_or_failure.has<Empty>())
return Optional<URL::URL> {};
auto const& location_values = location_values_or_failure.get<Vector<ByteBuffer>>();

View file

@ -22,7 +22,7 @@ namespace Web::ReferrerPolicy {
ReferrerPolicy parse_a_referrer_policy_from_a_referrer_policy_header(Fetch::Infrastructure::Response const& response)
{
// 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and responses header list.
auto policy_tokens_or_failure = Fetch::Infrastructure::extract_header_list_values("Referrer-Policy"sv.bytes(), response.header_list());
auto policy_tokens_or_failure = response.header_list()->extract_header_list_values("Referrer-Policy"sv.bytes());
auto policy_tokens = policy_tokens_or_failure.has<Vector<ByteBuffer>>() ? policy_tokens_or_failure.get<Vector<ByteBuffer>>() : Vector<ByteBuffer> {};
// 2. Let policy be the empty string.

View file

@ -266,13 +266,13 @@ static void update(JS::VM& vm, GC::Ref<Job> job)
// 8. Let serviceWorkerAllowed be the result of extracting header list values given `Service-Worker-Allowed` and responses header list.
// Note: See the definition of the Service-Worker-Allowed header in Appendix B: Extended HTTP headers. https://w3c.github.io/ServiceWorker/#service-worker-allowed
auto service_worker_allowed = Fetch::Infrastructure::extract_header_list_values("Service-Worker-Allowed"sv.bytes(), response->header_list());
auto service_worker_allowed = response->header_list()->extract_header_list_values("Service-Worker-Allowed"sv.bytes());
// 9. Set policyContainer to the result of creating a policy container from a fetch response given response.
// FIXME: CSP not implemented yet
// 10. If serviceWorkerAllowed is failure, then:
if (service_worker_allowed.has<Fetch::Infrastructure::ExtractHeaderParseFailure>()) {
if (service_worker_allowed.has<Fetch::Infrastructure::HeaderList::ExtractHeaderParseFailure>()) {
// FIXME: Should we reject the job promise with a security error here?
// 1. Asynchronously complete these steps with a network error.