ladybird/Libraries/LibHTTP/HeaderList.cpp
Timothy Flynn 9375660b64 LibHTTP+LibWeb+RequestServer: Move Fetch's HTTP header infra to LibHTTP
The end goal here is for LibHTTP to be the home of our RFC 9111 (HTTP
caching) implementation. We currently have one implementation in LibWeb
for our in-memory cache and another in RequestServer for our disk cache.

The implementations both largely revolve around interacting with HTTP
headers. But in LibWeb, we are using Fetch's header infra, and in RS we
are using are home-grown header infra from LibHTTP.

So to give these a common denominator, this patch replaces the LibHTTP
implementation with Fetch's infra. Our existing LibHTTP implementation
was not particularly compliant with any spec, so this at least gives us
a standards-based common implementation.

This migration also required moving a handful of other Fetch AOs over
to LibHTTP. (It turns out these AOs were all from the Fetch/Infra/HTTP
folder, so perhaps it makes sense for LibHTTP to be the implementation
of that entire set of facilities.)
2025-11-27 14:57:29 +01:00

276 lines
9.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, Kenneth Myhra <kennethmyhra@serenityos.org>
* Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringUtils.h>
#include <LibHTTP/HeaderList.h>
namespace HTTP {
NonnullRefPtr<HeaderList> HeaderList::create(Vector<Header> headers)
{
return adopt_ref(*new HeaderList { move(headers) });
}
HeaderList::HeaderList(Vector<Header> headers)
: m_headers(move(headers))
{
}
// https://fetch.spec.whatwg.org/#header-list-contains
bool HeaderList::contains(StringView name) const
{
// A header list list contains a header name name if list contains a header whose name is a byte-case-insensitive
// match for name.
return any_of(m_headers, [&](auto const& header) {
return header.name.equals_ignoring_ascii_case(name);
});
}
// https://fetch.spec.whatwg.org/#concept-header-list-get
Optional<ByteString> HeaderList::get(StringView name) const
{
// 1. If list does not contain name, then return null.
if (!contains(name))
return {};
// 2. Return the values of all headers in list whose name is a byte-case-insensitive match for name, separated from
// each other by 0x2C 0x20, in order.
StringBuilder builder;
bool first = true;
for (auto const& header : *this) {
if (!header.name.equals_ignoring_ascii_case(name))
continue;
if (!first)
builder.append(", "sv);
builder.append(header.value);
first = false;
}
return builder.to_byte_string();
}
// https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split
Optional<Vector<String>> HeaderList::get_decode_and_split(StringView name) const
{
// 1. Let value be the result of getting name from list.
auto value = get(name);
// 2. If value is null, then return null.
if (!value.has_value())
return {};
// 3. Return the result of getting, decoding, and splitting value.
return get_decode_and_split_header_value(*value);
}
// https://fetch.spec.whatwg.org/#concept-header-list-append
void HeaderList::append(Header header)
{
// 1. If list contains name, then set name to the first such headers name.
// NOTE: This reuses the casing of the name of the header already in list, if any. If there are multiple matched
// headers their names will all be identical.
auto matching_header = m_headers.first_matching([&](auto const& existing_header) {
return existing_header.name.equals_ignoring_ascii_case(header.name);
});
if (matching_header.has_value())
header.name = matching_header->name;
// 2. Append (name, value) to list.
m_headers.append(move(header));
}
// https://fetch.spec.whatwg.org/#concept-header-list-delete
void HeaderList::delete_(StringView name)
{
// To delete a header name name from a header list list, remove all headers whose name is a byte-case-insensitive
// match for name from list.
m_headers.remove_all_matching([&](auto const& header) {
return header.name.equals_ignoring_ascii_case(name);
});
}
// https://fetch.spec.whatwg.org/#concept-header-list-set
void HeaderList::set(Header header)
{
// 1. If list contains name, then set the value of the first such header to value and remove the others.
auto it = m_headers.find_if([&](auto const& existing_header) {
return existing_header.name.equals_ignoring_ascii_case(header.name);
});
if (it != m_headers.end()) {
it->value = move(header.value);
size_t i = 0;
m_headers.remove_all_matching([&](auto const& existing_header) {
if (i++ <= it.index())
return false;
return existing_header.name.equals_ignoring_ascii_case(it->name);
});
}
// 2. Otherwise, append header (name, value) to list.
else {
append(move(header));
}
}
// https://fetch.spec.whatwg.org/#concept-header-list-combine
void HeaderList::combine(Header header)
{
// 1. If list contains name, then set the value of the first such header to its value, followed by 0x2C 0x20,
// followed by value.
auto matching_header = m_headers.first_matching([&](auto const& existing_header) {
return existing_header.name.equals_ignoring_ascii_case(header.name);
});
if (matching_header.has_value()) {
matching_header->value = ByteString::formatted("{}, {}", matching_header->value, header.value);
}
// 2. Otherwise, append (name, value) to list.
else {
append(move(header));
}
}
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
Vector<Header> HeaderList::sort_and_combine() const
{
// 1. Let headers be an empty list of headers with the key being the name and value the value.
Vector<Header> headers;
// 2. Let names be the result of convert header names to a sorted-lowercase set with all the names of the headers
// in list.
Vector<ByteString> names_list;
names_list.ensure_capacity(m_headers.size());
for (auto const& header : *this)
names_list.unchecked_append(header.name);
auto names = convert_header_names_to_a_sorted_lowercase_set(names_list);
// 3. For each name of names:
for (auto& name : names) {
// 1. If name is `set-cookie`, then:
if (name == "set-cookie"sv) {
// 1. Let values be a list of all values of headers in list whose name is a byte-case-insensitive match for
// name, in order.
// 2. For each value of values:
for (auto const& [header_name, value] : *this) {
if (header_name.equals_ignoring_ascii_case(name)) {
// 1. Append (name, value) to headers.
headers.empend(name, value);
}
}
}
// 2. Otherwise:
else {
// 1. Let value be the result of getting name from list.
auto value = get(name);
// 2. Assert: value is not null.
VERIFY(value.has_value());
// 3. Append (name, value) to headers.
headers.empend(move(name), value.release_value());
}
}
// 4. Return headers.
return headers;
}
// https://fetch.spec.whatwg.org/#extract-header-list-values
Variant<Empty, Vector<ByteString>, HeaderList::ExtractHeaderParseFailure> HeaderList::extract_header_list_values(StringView name) const
{
// 1. If list does not contain name, then return null.
if (!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.
Vector<ByteString> values;
// 4. For each header header list contains whose name is name:
for (auto const& header : m_headers) {
if (!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
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);
// 2. If values is null, then return null.
if (!values.has_value())
return {};
// 3. Let candidateValue be null.
Optional<String> candidate_value;
// 4. For each value of values:
for (auto const& value : *values) {
// 1. If candidateValue is null, then set candidateValue to value.
if (!candidate_value.has_value()) {
candidate_value = value;
}
// 2. Otherwise, if value is not candidateValue, return failure.
else if (candidate_value.value() != value) {
return ExtractLengthFailure {};
}
}
// 5. If candidateValue is the empty string or has a code point that is not an ASCII digit, then return null.
// 6. Return candidateValue, interpreted as decimal number.
// FIXME: This will return an empty Optional if it cannot fit into a u64, is this correct?
auto result = candidate_value->to_number<u64>(TrimWhitespace::No);
if (!result.has_value())
return {};
return *result;
}
// Non-standard
Vector<ByteString> HeaderList::unique_names() const
{
HashTable<StringView, CaseInsensitiveStringTraits> header_names_seen;
Vector<ByteString> header_names;
for (auto const& header : m_headers) {
if (header_names_seen.contains(header.name))
continue;
header_names_seen.set(header.name);
header_names.append(header.name);
}
return header_names;
}
}