Everywhere: Hoist the Libraries folder to the top-level

This commit is contained in:
Timothy Flynn 2024-11-09 12:25:08 -05:00 committed by Andreas Kling
commit 93712b24bf
Notes: github-actions[bot] 2024-11-10 11:51:52 +00:00
4547 changed files with 104 additions and 113 deletions

View file

@ -0,0 +1,144 @@
/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/PromiseCapability.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Fetch/BodyInit.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h>
#include <LibWeb/Fetch/Infrastructure/IncrementalReadLoopReadRequest.h>
#include <LibWeb/Fetch/Infrastructure/Task.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/Streams/AbstractOperations.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::Fetch::Infrastructure {
JS_DEFINE_ALLOCATOR(Body);
JS::NonnullGCPtr<Body> Body::create(JS::VM& vm, JS::NonnullGCPtr<Streams::ReadableStream> stream)
{
return vm.heap().allocate_without_realm<Body>(stream);
}
JS::NonnullGCPtr<Body> Body::create(JS::VM& vm, JS::NonnullGCPtr<Streams::ReadableStream> stream, SourceType source, Optional<u64> length)
{
return vm.heap().allocate_without_realm<Body>(stream, source, length);
}
Body::Body(JS::NonnullGCPtr<Streams::ReadableStream> stream)
: m_stream(move(stream))
{
}
Body::Body(JS::NonnullGCPtr<Streams::ReadableStream> stream, SourceType source, Optional<u64> length)
: m_stream(move(stream))
, m_source(move(source))
, m_length(move(length))
{
}
void Body::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_stream);
}
// https://fetch.spec.whatwg.org/#concept-body-clone
JS::NonnullGCPtr<Body> Body::clone(JS::Realm& realm)
{
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// To clone a body body, run these steps:
// 1. Let « out1, out2 » be the result of teeing bodys stream.
auto [out1, out2] = m_stream->tee().release_value_but_fixme_should_propagate_errors();
// 2. Set bodys stream to out1.
m_stream = out1;
// 3. Return a body whose stream is out2 and other members are copied from body.
return Body::create(realm.vm(), *out2, m_source, m_length);
}
// https://fetch.spec.whatwg.org/#body-fully-read
void Body::fully_read(JS::Realm& realm, Web::Fetch::Infrastructure::Body::ProcessBodyCallback process_body, Web::Fetch::Infrastructure::Body::ProcessBodyErrorCallback process_body_error, TaskDestination task_destination) const
{
// FIXME: 1. If taskDestination is null, then set taskDestination to the result of starting a new parallel queue.
// FIXME: Handle 'parallel queue' task destination
VERIFY(!task_destination.has<Empty>());
auto task_destination_object = task_destination.get<JS::NonnullGCPtr<JS::Object>>();
// 2. Let successSteps given a byte sequence bytes be to queue a fetch task to run processBody given bytes, with taskDestination.
auto success_steps = [&realm, process_body, task_destination_object = task_destination_object](ReadonlyBytes bytes) -> ErrorOr<void> {
// Make a copy of the bytes, as the source of the bytes may disappear between the time the task is queued and executed.
auto bytes_copy = TRY(ByteBuffer::copy(bytes));
queue_fetch_task(*task_destination_object, JS::create_heap_function(realm.heap(), [process_body, bytes_copy = move(bytes_copy)]() mutable {
process_body->function()(move(bytes_copy));
}));
return {};
};
// 3. Let errorSteps optionally given an exception exception be to queue a fetch task to run processBodyError given exception, with taskDestination.
auto error_steps = [&realm, process_body_error, task_destination_object](JS::GCPtr<WebIDL::DOMException> exception) {
queue_fetch_task(*task_destination_object, JS::create_heap_function(realm.heap(), [process_body_error, exception]() {
process_body_error->function()(exception);
}));
};
// 4. Let reader be the result of getting a reader for bodys stream. If that threw an exception, then run errorSteps with that exception and return.
// 5. Read all bytes from reader, given successSteps and errorSteps.
// FIXME: Use streams for these steps.
m_source.visit(
[&](ByteBuffer const& byte_buffer) {
if (auto result = success_steps(byte_buffer); result.is_error())
error_steps(WebIDL::UnknownError::create(realm, "Out-of-memory"_string));
},
[&](JS::Handle<FileAPI::Blob> const& blob) {
if (auto result = success_steps(blob->raw_bytes()); result.is_error())
error_steps(WebIDL::UnknownError::create(realm, "Out-of-memory"_string));
},
[&](Empty) {
error_steps(WebIDL::DOMException::create(realm, "DOMException"_fly_string, "Reading from Blob, FormData or null source is not yet implemented"_string));
});
}
// https://fetch.spec.whatwg.org/#body-incrementally-read
void Body::incrementally_read(ProcessBodyChunkCallback process_body_chunk, ProcessEndOfBodyCallback process_end_of_body, ProcessBodyErrorCallback process_body_error, TaskDestination task_destination)
{
HTML::TemporaryExecutionContext const execution_context { m_stream->realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
VERIFY(task_destination.has<JS::NonnullGCPtr<JS::Object>>());
// FIXME: 1. If taskDestination is null, then set taskDestination to the result of starting a new parallel queue.
// FIXME: Handle 'parallel queue' task destination
// 2. Let reader be the result of getting a reader for bodys stream.
// NOTE: This operation will not throw an exception.
auto reader = MUST(Streams::acquire_readable_stream_default_reader(m_stream));
// 3. Perform the incrementally-read loop given reader, taskDestination, processBodyChunk, processEndOfBody, and processBodyError.
incrementally_read_loop(reader, task_destination.get<JS::NonnullGCPtr<JS::Object>>(), process_body_chunk, process_end_of_body, process_body_error);
}
// https://fetch.spec.whatwg.org/#incrementally-read-loop
void Body::incrementally_read_loop(Streams::ReadableStreamDefaultReader& reader, JS::NonnullGCPtr<JS::Object> task_destination, ProcessBodyChunkCallback process_body_chunk, ProcessEndOfBodyCallback process_end_of_body, ProcessBodyErrorCallback process_body_error)
{
auto& realm = reader.realm();
// 1. Let readRequest be the following read request:
auto read_request = realm.heap().allocate<IncrementalReadLoopReadRequest>(realm, *this, reader, task_destination, process_body_chunk, process_end_of_body, process_body_error);
// 2. Read a chunk from reader given readRequest.
reader.read_a_chunk(read_request);
}
// https://fetch.spec.whatwg.org/#byte-sequence-as-a-body
WebIDL::ExceptionOr<JS::NonnullGCPtr<Body>> byte_sequence_as_body(JS::Realm& realm, ReadonlyBytes bytes)
{
// To get a byte sequence bytes as a body, return the body of the result of safely extracting bytes.
auto [body, _] = TRY(safely_extract_body(realm, bytes));
return body;
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/Forward.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <AK/Variant.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibJS/Heap/Handle.h>
#include <LibWeb/Fetch/Infrastructure/Task.h>
#include <LibWeb/FileAPI/Blob.h>
#include <LibWeb/Streams/ReadableStream.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-body
class Body final : public JS::Cell {
JS_CELL(Body, JS::Cell);
JS_DECLARE_ALLOCATOR(Body);
public:
using SourceType = Variant<Empty, ByteBuffer, JS::Handle<FileAPI::Blob>>;
// processBody must be an algorithm accepting a byte sequence.
using ProcessBodyCallback = JS::NonnullGCPtr<JS::HeapFunction<void(ByteBuffer)>>;
// processBodyError must be an algorithm optionally accepting an exception.
using ProcessBodyErrorCallback = JS::NonnullGCPtr<JS::HeapFunction<void(JS::Value)>>;
// processBodyChunk must be an algorithm accepting a byte sequence.
using ProcessBodyChunkCallback = JS::NonnullGCPtr<JS::HeapFunction<void(ByteBuffer)>>;
// processEndOfBody must be an algorithm accepting no arguments
using ProcessEndOfBodyCallback = JS::NonnullGCPtr<JS::HeapFunction<void()>>;
[[nodiscard]] static JS::NonnullGCPtr<Body> create(JS::VM&, JS::NonnullGCPtr<Streams::ReadableStream>);
[[nodiscard]] static JS::NonnullGCPtr<Body> create(JS::VM&, JS::NonnullGCPtr<Streams::ReadableStream>, SourceType, Optional<u64>);
[[nodiscard]] JS::NonnullGCPtr<Streams::ReadableStream> stream() const { return *m_stream; }
void set_stream(JS::NonnullGCPtr<Streams::ReadableStream> value) { m_stream = value; }
[[nodiscard]] SourceType const& source() const { return m_source; }
[[nodiscard]] Optional<u64> const& length() const { return m_length; }
[[nodiscard]] JS::NonnullGCPtr<Body> clone(JS::Realm&);
void fully_read(JS::Realm&, ProcessBodyCallback process_body, ProcessBodyErrorCallback process_body_error, TaskDestination task_destination) const;
void incrementally_read(ProcessBodyChunkCallback process_body_chunk, ProcessEndOfBodyCallback process_end_of_body, ProcessBodyErrorCallback process_body_error, TaskDestination task_destination);
void incrementally_read_loop(Streams::ReadableStreamDefaultReader& reader, JS::NonnullGCPtr<JS::Object> task_destination, ProcessBodyChunkCallback process_body_chunk, ProcessEndOfBodyCallback process_end_of_body, ProcessBodyErrorCallback process_body_error);
virtual void visit_edges(JS::Cell::Visitor&) override;
private:
explicit Body(JS::NonnullGCPtr<Streams::ReadableStream>);
Body(JS::NonnullGCPtr<Streams::ReadableStream>, SourceType, Optional<u64>);
// https://fetch.spec.whatwg.org/#concept-body-stream
// A stream (a ReadableStream object).
JS::NonnullGCPtr<Streams::ReadableStream> m_stream;
// https://fetch.spec.whatwg.org/#concept-body-source
// A source (null, a byte sequence, a Blob object, or a FormData object), initially null.
SourceType m_source;
// https://fetch.spec.whatwg.org/#concept-body-total-bytes
// A length (null or an integer), initially null.
Optional<u64> m_length;
};
// https://fetch.spec.whatwg.org/#body-with-type
// A body with type is a tuple that consists of a body (a body) and a type (a header value or null).
struct BodyWithType {
JS::NonnullGCPtr<Body> body;
Optional<ByteBuffer> type;
};
WebIDL::ExceptionOr<JS::NonnullGCPtr<Body>> byte_sequence_as_body(JS::Realm&, ReadonlyBytes);
}

View file

@ -0,0 +1,890 @@
/*
* 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/CharacterTypes.h>
#include <AK/Checked.h>
#include <AK/GenericLexer.h>
#include <AK/QuickSort.h>
#include <AK/ScopeGuard.h>
#include <AK/StringUtils.h>
#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/VM.h>
#include <LibRegex/Regex.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/Fetch/Infrastructure/HTTP.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Headers.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Methods.h>
#include <LibWeb/Infra/ByteSequences.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/MimeSniff/MimeType.h>
namespace Web::Fetch::Infrastructure {
JS_DEFINE_ALLOCATOR(HeaderList);
template<typename T>
requires(IsSameIgnoringCV<T, u8>) struct CaseInsensitiveBytesTraits : public Traits<Span<T>> {
static constexpr bool equals(Span<T> const& a, Span<T> const& b)
{
return StringView { a }.equals_ignoring_ascii_case(StringView { b });
}
static constexpr unsigned hash(Span<T> const& span)
{
if (span.is_empty())
return 0;
return AK::case_insensitive_string_hash(reinterpret_cast<char const*>(span.data()), span.size());
}
};
Header Header::copy(Header const& header)
{
return Header {
.name = MUST(ByteBuffer::copy(header.name)),
.value = MUST(ByteBuffer::copy(header.value)),
};
}
Header Header::from_string_pair(StringView name, StringView value)
{
return Header {
.name = Infra::isomorphic_encode(name),
.value = Infra::isomorphic_encode(value),
};
}
JS::NonnullGCPtr<HeaderList> HeaderList::create(JS::VM& vm)
{
return vm.heap().allocate_without_realm<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
{
// 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(*this, [&](auto const& header) {
return StringView { header.name }.equals_ignoring_ascii_case(name);
});
}
// https://fetch.spec.whatwg.org/#concept-header-list-get
Optional<ByteBuffer> HeaderList::get(ReadonlyBytes name) const
{
// To get a header name name from a header list list, run these steps:
// 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.
ByteBuffer buffer;
auto first = true;
for (auto const& header : *this) {
if (!StringView { header.name }.equals_ignoring_ascii_case(name))
continue;
if (first) {
first = false;
} else {
buffer.append(0x2c);
buffer.append(0x20);
}
buffer.append(header.value);
}
return buffer;
}
// https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split
Optional<Vector<String>> HeaderList::get_decode_and_split(ReadonlyBytes name) const
{
// To get, decode, and split a header name name from header list list, run these steps:
// 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/#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)
{
// To append a header (name, value) to a header list list, run these steps:
// NOTE: Can't use structured bindings captured in the lambda due to https://github.com/llvm/llvm-project/issues/48582
auto& name = header.name;
// 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.
if (contains(name)) {
auto matching_header = first_matching([&](auto const& existing_header) {
return StringView { existing_header.name }.equals_ignoring_ascii_case(name);
});
name.overwrite(0, matching_header->name.data(), matching_header->name.size());
}
// 2. Append (name, value) to list.
Vector<Header>::append(move(header));
}
// https://fetch.spec.whatwg.org/#concept-header-list-delete
void HeaderList::delete_(ReadonlyBytes 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.
remove_all_matching([&](auto const& header) {
return StringView { header.name }.equals_ignoring_ascii_case(name);
});
}
// https://fetch.spec.whatwg.org/#concept-header-list-set
void HeaderList::set(Header header)
{
// To set a header (name, value) in a header list list, run these steps:
// NOTE: Can't use structured bindings captured in the lambda due to https://github.com/llvm/llvm-project/issues/48582
auto const& name = header.name;
auto const& value = header.value;
// 1. If list contains name, then set the value of the first such header to value and remove the others.
if (contains(name)) {
auto matching_index = find_if([&](auto const& existing_header) {
return StringView { existing_header.name }.equals_ignoring_ascii_case(name);
}).index();
auto& matching_header = at(matching_index);
matching_header.value = MUST(ByteBuffer::copy(value));
size_t i = 0;
remove_all_matching([&](auto const& existing_header) {
ScopeGuard increment_i = [&]() { i++; };
if (i <= matching_index)
return false;
return StringView { existing_header.name }.equals_ignoring_ascii_case(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)
{
// To combine a header (name, value) in a header list list, run these steps:
// NOTE: Can't use structured bindings captured in the lambda due to https://github.com/llvm/llvm-project/issues/48582
auto const& name = header.name;
auto const& value = header.value;
// 1. If list contains name, then set the value of the first such header to its value, followed by 0x2C 0x20, followed by value.
if (contains(name)) {
auto matching_header = first_matching([&](auto const& existing_header) {
return StringView { existing_header.name }.equals_ignoring_ascii_case(name);
});
matching_header->value.append(0x2c);
matching_header->value.append(0x20);
matching_header->value.append(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
{
// To sort and combine a header list list, run these steps:
// 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<ReadonlyBytes> names_list;
names_list.ensure_capacity(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.bytes()) {
// 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 (StringView { header_name }.equals_ignoring_ascii_case(name)) {
// 1. Append (name, value) to headers.
auto header = Header::from_string_pair(name, value);
headers.append(move(header));
}
}
}
// 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.
auto header = Header {
.name = move(name),
.value = value.release_value(),
};
headers.append(move(header));
}
}
// 4. Return headers.
return headers;
}
// https://fetch.spec.whatwg.org/#header-list-extract-a-length
HeaderList::ExtractLengthResult 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 {};
// 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.
// 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() };
}
// https://fetch.spec.whatwg.org/#concept-header-extract-mime-type
Optional<MimeSniff::MimeType> HeaderList::extract_mime_type() const
{
// 1. Let charset be null.
Optional<String> charset;
// 2. Let essence be null.
Optional<String> essence;
// 3. Let mimeType be null.
Optional<MimeSniff::MimeType> mime_type;
// 4. Let values be the result of getting, decoding, and splitting `Content-Type` from headers.
auto values = get_decode_and_split("Content-Type"sv.bytes());
// 5. If values is null, then return failure.
if (!values.has_value())
return {};
// 6. For each value of values:
for (auto const& value : *values) {
// 1. Let temporaryMimeType be the result of parsing value.
auto temporary_mime_type = MimeSniff::MimeType::parse(value);
// 2. If temporaryMimeType is failure or its essence is "*/*", then continue.
if (!temporary_mime_type.has_value() || temporary_mime_type->essence() == "*/*"sv)
continue;
// 3. Set mimeType to temporaryMimeType.
mime_type = temporary_mime_type;
// 4. If mimeTypes essence is not essence, then:
if (!essence.has_value() || (mime_type->essence() != essence->bytes_as_string_view())) {
// 1. Set charset to null.
charset = {};
// 2. If mimeTypes parameters["charset"] exists, then set charset to mimeTypes parameters["charset"].
auto it = mime_type->parameters().find("charset"sv);
if (it != mime_type->parameters().end())
charset = it->value;
// 3. Set essence to mimeTypes essence.
essence = mime_type->essence();
}
// 5. Otherwise, if mimeTypes parameters["charset"] does not exist, and charset is non-null, set mimeTypes parameters["charset"] to charset.
else if (!mime_type->parameters().contains("charset"sv) && charset.has_value()) {
mime_type->set_parameter("charset"_string, charset.release_value());
}
}
// 7. If mimeType is null, then return failure.
// 8. Return mimeType.
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)
{
// 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))
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/#header-name
bool is_header_name(ReadonlyBytes header_name)
{
// A header name is a byte sequence that matches the field-name token production.
Regex<ECMA262Parser> regex { R"~~~(^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$)~~~" };
return regex.has_match(StringView { header_name });
}
// https://fetch.spec.whatwg.org/#header-value
bool is_header_value(ReadonlyBytes header_value)
{
// A header value is a byte sequence that matches the following conditions:
// - Has no leading or trailing HTTP tab or space bytes.
// - Contains no 0x00 (NUL) or HTTP newline bytes.
if (header_value.is_empty())
return true;
auto first_byte = header_value[0];
auto last_byte = header_value[header_value.size() - 1];
if (HTTP_TAB_OR_SPACE_BYTES.span().contains_slow(first_byte) || HTTP_TAB_OR_SPACE_BYTES.span().contains_slow(last_byte))
return false;
return !any_of(header_value, [](auto byte) {
return byte == 0x00 || HTTP_NEWLINE_BYTES.span().contains_slow(byte);
});
}
// https://fetch.spec.whatwg.org/#concept-header-value-normalize
ByteBuffer normalize_header_value(ReadonlyBytes potential_value)
{
// To normalize a byte sequence potentialValue, remove any leading and trailing HTTP whitespace bytes from potentialValue.
if (potential_value.is_empty())
return {};
auto trimmed = StringView { potential_value }.trim(HTTP_WHITESPACE, TrimMode::Both);
return MUST(ByteBuffer::copy(trimmed.bytes()));
}
// https://fetch.spec.whatwg.org/#cors-safelisted-request-header
bool is_cors_safelisted_request_header(Header const& header)
{
// To determine whether a header (name, value) is a CORS-safelisted request-header, run these steps:
auto const& value = header.value;
// 1. If values length is greater than 128, then return false.
if (value.size() > 128)
return false;
// 2. Byte-lowercase name and switch on the result:
auto name = StringView { header.name };
// `accept`
if (name.equals_ignoring_ascii_case("accept"sv)) {
// If value contains a CORS-unsafe request-header byte, then return false.
if (any_of(value.span(), is_cors_unsafe_request_header_byte))
return false;
}
// `accept-language`
// `content-language`
else if (name.is_one_of_ignoring_ascii_case("accept-language"sv, "content-language"sv)) {
// If value contains a byte that is not in the range 0x30 (0) to 0x39 (9), inclusive, is not in the range 0x41 (A) to 0x5A (Z), inclusive, is not in the range 0x61 (a) to 0x7A (z), inclusive, and is not 0x20 (SP), 0x2A (*), 0x2C (,), 0x2D (-), 0x2E (.), 0x3B (;), or 0x3D (=), then return false.
if (any_of(value.span(), [](auto byte) {
return !(is_ascii_digit(byte) || is_ascii_alpha(byte) || " *,-.;="sv.contains(static_cast<char>(byte)));
}))
return false;
}
// `content-type`
else if (name.equals_ignoring_ascii_case("content-type"sv)) {
// 1. If value contains a CORS-unsafe request-header byte, then return false.
if (any_of(value.span(), is_cors_unsafe_request_header_byte))
return false;
// 2. Let mimeType be the result of parsing the result of isomorphic decoding value.
auto decoded = Infra::isomorphic_decode(value);
auto mime_type = MimeSniff::MimeType::parse(decoded);
// 3. If mimeType is failure, then return false.
if (!mime_type.has_value())
return false;
// 4. If mimeTypes essence is not "application/x-www-form-urlencoded", "multipart/form-data", or "text/plain", then return false.
if (!mime_type->essence().is_one_of("application/x-www-form-urlencoded"sv, "multipart/form-data"sv, "text/plain"sv))
return false;
}
// `range`
else if (name.equals_ignoring_ascii_case("range"sv)) {
// 1. Let rangeValue be the result of parsing a single range header value given value.
auto range_value = parse_single_range_header_value(value);
// 2. If rangeValue is failure, then return false.
if (!range_value.has_value())
return false;
// 3. If rangeValue[0] is null, then return false.
// NOTE: As web browsers have historically not emitted ranges such as `bytes=-500` this algorithm does not safelist them.
if (!range_value->start.has_value())
return false;
}
// Otherwise
else {
// Return false.
return false;
}
// 3. Return true.
return true;
}
// https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte
bool is_cors_unsafe_request_header_byte(u8 byte)
{
// A CORS-unsafe request-header byte is a byte byte for which one of the following is true:
// - byte is less than 0x20 and is not 0x09 HT
// - byte is 0x22 ("), 0x28 (left parenthesis), 0x29 (right parenthesis), 0x3A (:), 0x3C (<), 0x3E (>), 0x3F (?), 0x40 (@), 0x5B ([), 0x5C (\), 0x5D (]), 0x7B ({), 0x7D (}), or 0x7F DEL.
return (byte < 0x20 && byte != 0x09)
|| (Array<u8, 14> { 0x22, 0x28, 0x29, 0x3A, 0x3C, 0x3E, 0x3F, 0x40, 0x5B, 0x5C, 0x5D, 0x7B, 0x7D, 0x7F }.span().contains_slow(byte));
}
// https://fetch.spec.whatwg.org/#cors-unsafe-request-header-names
OrderedHashTable<ByteBuffer> get_cors_unsafe_header_names(HeaderList const& headers)
{
// The CORS-unsafe request-header names, given a header list headers, are determined as follows:
// 1. Let unsafeNames be a new list.
Vector<ReadonlyBytes> unsafe_names;
// 2. Let potentiallyUnsafeNames be a new list.
Vector<ReadonlyBytes> potentially_unsafe_names;
// 3. Let safelistValueSize be 0.
Checked<size_t> safelist_value_size = 0;
// 4. For each header of headers:
for (auto const& header : headers) {
// 1. If header is not a CORS-safelisted request-header, then append headers name to unsafeNames.
if (!is_cors_safelisted_request_header(header)) {
unsafe_names.append(header.name.span());
}
// 2. Otherwise, append headers name to potentiallyUnsafeNames and increase safelistValueSize by headers values length.
else {
potentially_unsafe_names.append(header.name.span());
safelist_value_size += header.value.size();
}
}
// 5. If safelistValueSize is greater than 1024, then for each name of potentiallyUnsafeNames, append name to unsafeNames.
if (safelist_value_size.has_overflow() || safelist_value_size.value() > 1024) {
for (auto const& name : potentially_unsafe_names)
unsafe_names.append(name);
}
// 6. Return the result of convert header names to a sorted-lowercase set with unsafeNames.
return convert_header_names_to_a_sorted_lowercase_set(unsafe_names.span());
}
// https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
bool is_cors_non_wildcard_request_header_name(ReadonlyBytes header_name)
{
// A CORS non-wildcard request-header name is a header name that is a byte-case-insensitive match for `Authorization`.
return StringView { header_name }.equals_ignoring_ascii_case("Authorization"sv);
}
// https://fetch.spec.whatwg.org/#privileged-no-cors-request-header-name
bool is_privileged_no_cors_request_header_name(ReadonlyBytes header_name)
{
// A privileged no-CORS request-header name is a header name that is a byte-case-insensitive match for one of
// - `Range`.
return StringView { header_name }.equals_ignoring_ascii_case("Range"sv);
}
// https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
bool is_cors_safelisted_response_header_name(ReadonlyBytes header_name, Span<ReadonlyBytes> list)
{
// A CORS-safelisted response-header name, given a list of header names list, is a header name that is a byte-case-insensitive match for one of
// - `Cache-Control`
// - `Content-Language`
// - `Content-Length`
// - `Content-Type`
// - `Expires`
// - `Last-Modified`
// - `Pragma`
// - Any item in list that is not a forbidden response-header name.
return StringView { header_name }.is_one_of_ignoring_ascii_case(
"Cache-Control"sv,
"Content-Language"sv,
"Content-Length"sv,
"Content-Type"sv,
"Expires"sv,
"Last-Modified"sv,
"Pragma"sv)
|| any_of(list, [&](auto list_header_name) {
return StringView { header_name }.equals_ignoring_ascii_case(list_header_name)
&& !is_forbidden_response_header_name(list_header_name);
});
}
// https://fetch.spec.whatwg.org/#no-cors-safelisted-request-header-name
bool is_no_cors_safelisted_request_header_name(ReadonlyBytes header_name)
{
// A no-CORS-safelisted request-header name is a header name that is a byte-case-insensitive match for one of
// - `Accept`
// - `Accept-Language`
// - `Content-Language`
// - `Content-Type`
return StringView { header_name }.is_one_of_ignoring_ascii_case(
"Accept"sv,
"Accept-Language"sv,
"Content-Language"sv,
"Content-Type"sv);
}
// https://fetch.spec.whatwg.org/#no-cors-safelisted-request-header
bool is_no_cors_safelisted_request_header(Header const& header)
{
// To determine whether a header (name, value) is a no-CORS-safelisted request-header, run these steps:
// 1. If name is not a no-CORS-safelisted request-header name, then return false.
if (!is_no_cors_safelisted_request_header_name(header.name))
return false;
// 2. Return whether (name, value) is a CORS-safelisted request-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/#simple-range-header-value
Optional<RangeHeaderValue> parse_single_range_header_value(ReadonlyBytes value)
{
// 1. Let data be the isomorphic decoding of value.
auto 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 6th code point of data.
auto lexer = GenericLexer { data };
lexer.ignore(6);
// 4. 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);
// 5. 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>();
// 6. If the code point at position within data is not U+002D (-), then return failure.
// 7. Advance position by 1.
if (!lexer.consume_specific('-'))
return {};
// 8. 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);
// 9. 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>();
// 10. If position is not past the end of data, then return failure.
if (!lexer.is_eof())
return {};
// 11. If rangeEndValue and rangeStartValue are null, then return failure.
if (!range_end_value.has_value() && !range_start_value.has_value())
return {};
// 12. 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 {};
// 13. Return (rangeStartValue, rangeEndValue).
// NOTE: The range end or start can be omitted, e.g., `bytes=0-` or `bytes=-500` are valid ranges.
return RangeHeaderValue { move(range_start_value), move(range_end_value) };
}
// https://fetch.spec.whatwg.org/#default-user-agent-value
ByteBuffer default_user_agent_value()
{
// A default `User-Agent` value is an implementation-defined header value for the `User-Agent` header.
return MUST(ByteBuffer::copy(ResourceLoader::the().user_agent().bytes()));
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/Error.h>
#include <AK/Forward.h>
#include <AK/HashTable.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibJS/Forward.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibJS/Heap/Heap.h>
#include <LibWeb/MimeSniff/MimeType.h>
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 Header {
ByteBuffer name;
ByteBuffer value;
[[nodiscard]] static Header copy(Header const&);
[[nodiscard]] static Header from_string_pair(StringView, StringView);
};
// https://fetch.spec.whatwg.org/#concept-header-list
// A header list is a list of zero or more headers. It is initially the empty list.
class HeaderList final
: public JS::Cell
, public Vector<Header> {
JS_CELL(HeaderList, JS::Cell);
JS_DECLARE_ALLOCATOR(HeaderList);
public:
using Vector::begin;
using Vector::clear;
using Vector::end;
using Vector::is_empty;
[[nodiscard]] static JS::NonnullGCPtr<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;
void append(Header);
void delete_(ReadonlyBytes name);
void set(Header);
void combine(Header);
[[nodiscard]] Vector<Header> sort_and_combine() const;
struct ExtractLengthFailure { };
using ExtractLengthResult = Variant<u64, ExtractLengthFailure, Empty>;
[[nodiscard]] ExtractLengthResult extract_length() const;
[[nodiscard]] Optional<MimeSniff::MimeType> extract_mime_type() const;
[[nodiscard]] Vector<ByteBuffer> unique_names() const;
};
struct RangeHeaderValue {
Optional<u64> start;
Optional<u64> end;
};
struct ExtractHeaderParseFailure {
};
[[nodiscard]] 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]] bool is_cors_safelisted_request_header(Header const&);
[[nodiscard]] bool is_cors_unsafe_request_header_byte(u8);
[[nodiscard]] OrderedHashTable<ByteBuffer> get_cors_unsafe_header_names(HeaderList const&);
[[nodiscard]] bool is_cors_non_wildcard_request_header_name(ReadonlyBytes);
[[nodiscard]] bool is_privileged_no_cors_request_header_name(ReadonlyBytes);
[[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]] Variant<Vector<ByteBuffer>, ExtractHeaderParseFailure, Empty> extract_header_list_values(ReadonlyBytes, HeaderList const&);
[[nodiscard]] Optional<RangeHeaderValue> parse_single_range_header_value(ReadonlyBytes);
[[nodiscard]] ByteBuffer default_user_agent_value();
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, Kenneth Myhra <kennethmyhra@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteBuffer.h>
#include <AK/StringView.h>
#include <LibRegex/Regex.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Methods.h>
#include <LibWeb/Infra/ByteSequences.h>
namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-method
bool is_method(ReadonlyBytes method)
{
// A method is a byte sequence that matches the method token production.
Regex<ECMA262Parser> regex { R"~~~(^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$)~~~" };
return regex.has_match(StringView { method });
}
// https://fetch.spec.whatwg.org/#cors-safelisted-method
bool is_cors_safelisted_method(ReadonlyBytes method)
{
// A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`.
return StringView { method }.is_one_of("GET"sv, "HEAD"sv, "POST"sv);
}
// https://fetch.spec.whatwg.org/#forbidden-method
bool is_forbidden_method(ReadonlyBytes method)
{
// A forbidden method is a method that is a byte-case-insensitive match for `CONNECT`, `TRACE`, or `TRACK`.
return StringView { method }.is_one_of_ignoring_ascii_case("CONNECT"sv, "TRACE"sv, "TRACK"sv);
}
// https://fetch.spec.whatwg.org/#concept-method-normalize
ByteBuffer normalize_method(ReadonlyBytes method)
{
// To normalize a method, if it is a byte-case-insensitive match for `DELETE`, `GET`, `HEAD`, `OPTIONS`, `POST`, or `PUT`, byte-uppercase it.
auto bytes = MUST(ByteBuffer::copy(method));
if (StringView { method }.is_one_of_ignoring_ascii_case("DELETE"sv, "GET"sv, "HEAD"sv, "OPTIONS"sv, "POST"sv, "PUT"sv))
Infra::byte_uppercase(bytes);
return bytes;
}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
namespace Web::Fetch::Infrastructure {
[[nodiscard]] bool is_method(ReadonlyBytes);
[[nodiscard]] bool is_cors_safelisted_method(ReadonlyBytes);
[[nodiscard]] bool is_forbidden_method(ReadonlyBytes);
[[nodiscard]] ByteBuffer normalize_method(ReadonlyBytes);
}

View file

@ -0,0 +1,453 @@
/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Array.h>
#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/Fetch/Fetching/PendingResponse.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
namespace Web::Fetch::Infrastructure {
JS_DEFINE_ALLOCATOR(Request);
Request::Request(JS::NonnullGCPtr<HeaderList> header_list)
: m_header_list(header_list)
{
}
void Request::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_header_list);
visitor.visit(m_client);
m_body.visit(
[&](JS::NonnullGCPtr<Body>& body) { visitor.visit(body); },
[](auto&) {});
visitor.visit(m_reserved_client);
m_window.visit(
[&](JS::GCPtr<HTML::EnvironmentSettingsObject> const& value) { visitor.visit(value); },
[](auto const&) {});
visitor.visit(m_pending_responses);
}
JS::NonnullGCPtr<Request> Request::create(JS::VM& vm)
{
return vm.heap().allocate_without_realm<Request>(HeaderList::create(vm));
}
// https://fetch.spec.whatwg.org/#concept-request-url
URL::URL& Request::url()
{
// A request has an associated URL (a URL).
// NOTE: Implementations are encouraged to make this a pointer to the first URL in requests URL list. It is provided as a distinct field solely for the convenience of other standards hooking into Fetch.
VERIFY(!m_url_list.is_empty());
return m_url_list.first();
}
// https://fetch.spec.whatwg.org/#concept-request-url
URL::URL const& Request::url() const
{
return const_cast<Request&>(*this).url();
}
// https://fetch.spec.whatwg.org/#concept-request-current-url
URL::URL& Request::current_url()
{
// A request has an associated current URL. It is a pointer to the last URL in requests URL list.
VERIFY(!m_url_list.is_empty());
return m_url_list.last();
}
// https://fetch.spec.whatwg.org/#concept-request-current-url
URL::URL const& Request::current_url() const
{
return const_cast<Request&>(*this).current_url();
}
void Request::set_url(URL::URL url)
{
// Sometimes setting the URL and URL list are done as two distinct steps in the spec,
// but since we know the URL is always the URL list's first item and doesn't change later
// on, we can combine them.
if (!m_url_list.is_empty())
m_url_list.clear();
m_url_list.append(move(url));
}
// https://fetch.spec.whatwg.org/#request-destination-script-like
bool Request::destination_is_script_like() const
{
// A requests destination is script-like if it is "audioworklet", "paintworklet", "script", "serviceworker", "sharedworker", or "worker".
static constexpr Array script_like_destinations = {
Destination::AudioWorklet,
Destination::PaintWorklet,
Destination::Script,
Destination::ServiceWorker,
Destination::SharedWorker,
Destination::Worker,
};
return any_of(script_like_destinations, [this](auto destination) {
return m_destination == destination;
});
}
// https://fetch.spec.whatwg.org/#subresource-request
bool Request::is_subresource_request() const
{
// A subresource request is a request whose destination is "audio", "audioworklet", "font", "image", "json", "manifest", "paintworklet", "script", "style", "track", "video", "xslt", or the empty string.
static constexpr Array subresource_request_destinations = {
Destination::Audio,
Destination::AudioWorklet,
Destination::Font,
Destination::Image,
Destination::JSON,
Destination::Manifest,
Destination::PaintWorklet,
Destination::Script,
Destination::Style,
Destination::Track,
Destination::Video,
Destination::XSLT,
};
return any_of(subresource_request_destinations, [this](auto destination) {
return m_destination == destination;
}) || !m_destination.has_value();
}
// https://fetch.spec.whatwg.org/#non-subresource-request
bool Request::is_non_subresource_request() const
{
// A non-subresource request is a request whose destination is "document", "embed", "frame", "iframe", "object", "report", "serviceworker", "sharedworker", or "worker".
static constexpr Array non_subresource_request_destinations = {
Destination::Document,
Destination::Embed,
Destination::Frame,
Destination::IFrame,
Destination::Object,
Destination::Report,
Destination::ServiceWorker,
Destination::SharedWorker,
Destination::Worker,
};
return any_of(non_subresource_request_destinations, [this](auto destination) {
return m_destination == destination;
});
}
// https://fetch.spec.whatwg.org/#navigation-request
bool Request::is_navigation_request() const
{
// A navigation request is a request whose destination is "document", "embed", "frame", "iframe", or "object".
static constexpr Array navigation_request_destinations = {
Destination::Document,
Destination::Embed,
Destination::Frame,
Destination::IFrame,
Destination::Object,
};
return any_of(navigation_request_destinations, [this](auto destination) {
return m_destination == destination;
});
}
// https://fetch.spec.whatwg.org/#concept-request-tainted-origin
bool Request::has_redirect_tainted_origin() const
{
// A request request has a redirect-tainted origin if these steps return true:
// 1. Let lastURL be null.
Optional<URL::URL const&> last_url;
// 2. For each url of requests URL list:
for (auto const& url : m_url_list) {
// 1. If lastURL is null, then set lastURL to url and continue.
if (!last_url.has_value()) {
last_url = url;
continue;
}
// 2. If urls origin is not same origin with lastURLs origin and requests origin is not same origin with lastURLs origin, then return true.
auto const* request_origin = m_origin.get_pointer<URL::Origin>();
if (!url.origin().is_same_origin(last_url->origin())
&& (request_origin == nullptr || !request_origin->is_same_origin(last_url->origin()))) {
return true;
}
// 3. Set lastURL to url.
last_url = url;
}
// 3. Return false.
return false;
}
// https://fetch.spec.whatwg.org/#serializing-a-request-origin
String Request::serialize_origin() const
{
// 1. If request has a redirect-tainted origin, then return "null".
if (has_redirect_tainted_origin())
return "null"_string;
// 2. Return requests origin, serialized.
return MUST(String::from_byte_string(m_origin.get<URL::Origin>().serialize()));
}
// https://fetch.spec.whatwg.org/#byte-serializing-a-request-origin
ByteBuffer Request::byte_serialize_origin() const
{
// Byte-serializing a request origin, given a request request, is to return the result of serializing a request origin with request, isomorphic encoded.
return MUST(ByteBuffer::copy(serialize_origin().bytes()));
}
// https://fetch.spec.whatwg.org/#concept-request-clone
JS::NonnullGCPtr<Request> Request::clone(JS::Realm& realm) const
{
// To clone a request request, run these steps:
auto& vm = realm.vm();
// 1. Let newRequest be a copy of request, except for its body.
auto new_request = Infrastructure::Request::create(vm);
new_request->set_method(m_method);
new_request->set_local_urls_only(m_local_urls_only);
for (auto const& header : *m_header_list)
new_request->header_list()->append(header);
new_request->set_unsafe_request(m_unsafe_request);
new_request->set_client(m_client);
new_request->set_reserved_client(m_reserved_client);
new_request->set_replaces_client_id(m_replaces_client_id);
new_request->set_window(m_window);
new_request->set_keepalive(m_keepalive);
new_request->set_initiator_type(m_initiator_type);
new_request->set_service_workers_mode(m_service_workers_mode);
new_request->set_initiator(m_initiator);
new_request->set_destination(m_destination);
new_request->set_priority(m_priority);
new_request->set_origin(m_origin);
new_request->set_policy_container(m_policy_container);
new_request->set_referrer(m_referrer);
new_request->set_referrer_policy(m_referrer_policy);
new_request->set_mode(m_mode);
new_request->set_use_cors_preflight(m_use_cors_preflight);
new_request->set_credentials_mode(m_credentials_mode);
new_request->set_use_url_credentials(m_use_url_credentials);
new_request->set_cache_mode(m_cache_mode);
new_request->set_redirect_mode(m_redirect_mode);
new_request->set_integrity_metadata(m_integrity_metadata);
new_request->set_cryptographic_nonce_metadata(m_cryptographic_nonce_metadata);
new_request->set_parser_metadata(m_parser_metadata);
new_request->set_reload_navigation(m_reload_navigation);
new_request->set_history_navigation(m_history_navigation);
new_request->set_user_activation(m_user_activation);
new_request->set_render_blocking(m_render_blocking);
new_request->set_url_list(m_url_list);
new_request->set_redirect_count(m_redirect_count);
new_request->set_response_tainting(m_response_tainting);
new_request->set_prevent_no_cache_cache_control_header_modification(m_prevent_no_cache_cache_control_header_modification);
new_request->set_done(m_done);
new_request->set_timing_allow_failed(m_timing_allow_failed);
new_request->set_buffer_policy(m_buffer_policy);
// 2. If requests body is non-null, set newRequests body to the result of cloning requests body.
if (auto const* body = m_body.get_pointer<JS::NonnullGCPtr<Body>>())
new_request->set_body((*body)->clone(realm));
// 3. Return newRequest.
return new_request;
}
// https://fetch.spec.whatwg.org/#concept-request-add-range-header
void Request::add_range_header(u64 first, Optional<u64> const& last)
{
// To add a range header to a request request, with an integer first, and an optional integer last, run these steps:
// 1. Assert: last is not given, or first is less than or equal to last.
VERIFY(!last.has_value() || first <= last.value());
// 2. Let rangeValue be `bytes=`.
auto range_value = MUST(ByteBuffer::copy("bytes"sv.bytes()));
// 3. Serialize and isomorphic encode first, and append the result to rangeValue.
range_value.append(String::number(first).bytes());
// 4. Append 0x2D (-) to rangeValue.
range_value.append('-');
// 5. If last is given, then serialize and isomorphic encode it, and append the result to rangeValue.
if (last.has_value())
range_value.append(String::number(*last).bytes());
// 6. Append (`Range`, rangeValue) to requests header list.
auto header = Header {
.name = MUST(ByteBuffer::copy("Range"sv.bytes())),
.value = move(range_value),
};
m_header_list->append(move(header));
}
// https://fetch.spec.whatwg.org/#append-a-request-origin-header
void Request::add_origin_header()
{
// 1. Let serializedOrigin be the result of byte-serializing a request origin with request.
auto serialized_origin = byte_serialize_origin();
// 2. If requests response tainting is "cors" or requests mode is "websocket", then append (`Origin`, serializedOrigin) to requests header list.
if (m_response_tainting == ResponseTainting::CORS || m_mode == Mode::WebSocket) {
auto header = Header {
.name = MUST(ByteBuffer::copy("Origin"sv.bytes())),
.value = move(serialized_origin),
};
m_header_list->append(move(header));
}
// 3. Otherwise, if requests method is neither `GET` nor `HEAD`, then:
else if (!StringView { m_method }.is_one_of("GET"sv, "HEAD"sv)) {
// 1. If requests mode is not "cors", then switch on requests referrer policy:
if (m_mode != Mode::CORS) {
switch (m_referrer_policy) {
// -> "no-referrer"
case ReferrerPolicy::ReferrerPolicy::NoReferrer:
// Set serializedOrigin to `null`.
serialized_origin = MUST(ByteBuffer::copy("null"sv.bytes()));
break;
// -> "no-referrer-when-downgrade"
// -> "strict-origin"
// -> "strict-origin-when-cross-origin"
case ReferrerPolicy::ReferrerPolicy::NoReferrerWhenDowngrade:
case ReferrerPolicy::ReferrerPolicy::StrictOrigin:
case ReferrerPolicy::ReferrerPolicy::StrictOriginWhenCrossOrigin:
// If requests origin is a tuple origin, its scheme is "https", and requests current URLs scheme is
// not "https", then set serializedOrigin to `null`.
if (m_origin.has<URL::Origin>() && m_origin.get<URL::Origin>().scheme() == "https"sv && current_url().scheme() != "https"sv)
serialized_origin = MUST(ByteBuffer::copy("null"sv.bytes()));
break;
// -> "same-origin"
case ReferrerPolicy::ReferrerPolicy::SameOrigin:
// If requests origin is not same origin with requests current URLs origin, then set serializedOrigin
// to `null`.
if (m_origin.has<URL::Origin>() && !m_origin.get<URL::Origin>().is_same_origin(current_url().origin()))
serialized_origin = MUST(ByteBuffer::copy("null"sv.bytes()));
break;
// -> Otherwise
default:
// Do nothing.
break;
}
}
// 2. Append (`Origin`, serializedOrigin) to requests header list.
auto header = Header {
.name = MUST(ByteBuffer::copy("Origin"sv.bytes())),
.value = move(serialized_origin),
};
m_header_list->append(move(header));
}
}
// https://fetch.spec.whatwg.org/#cross-origin-embedder-policy-allows-credentials
bool Request::cross_origin_embedder_policy_allows_credentials() const
{
// 1. If requests mode is not "no-cors", then return true.
if (m_mode != Mode::NoCORS)
return true;
// 2. If requests client is null, then return true.
if (m_client == nullptr)
return true;
// 3. If requests clients policy containers embedder policys value is not "credentialless", then return true.
if (m_policy_container.has<HTML::PolicyContainer>() && m_policy_container.get<HTML::PolicyContainer>().embedder_policy.value != HTML::EmbedderPolicyValue::Credentialless)
return true;
// 4. If requests origin is same origin with requests current URLs origin and request does not have a redirect-tainted origin, then return true.
// 5. Return false.
auto const* request_origin = m_origin.get_pointer<URL::Origin>();
if (request_origin == nullptr)
return false;
return request_origin->is_same_origin(current_url().origin()) && !has_redirect_tainted_origin();
}
StringView request_destination_to_string(Request::Destination destination)
{
switch (destination) {
case Request::Destination::Audio:
return "audio"sv;
case Request::Destination::AudioWorklet:
return "audioworklet"sv;
case Request::Destination::Document:
return "document"sv;
case Request::Destination::Embed:
return "embed"sv;
case Request::Destination::Font:
return "font"sv;
case Request::Destination::Frame:
return "frame"sv;
case Request::Destination::IFrame:
return "iframe"sv;
case Request::Destination::Image:
return "image"sv;
case Request::Destination::JSON:
return "json"sv;
case Request::Destination::Manifest:
return "manifest"sv;
case Request::Destination::Object:
return "object"sv;
case Request::Destination::PaintWorklet:
return "paintworklet"sv;
case Request::Destination::Report:
return "report"sv;
case Request::Destination::Script:
return "script"sv;
case Request::Destination::ServiceWorker:
return "serviceworker"sv;
case Request::Destination::SharedWorker:
return "sharedworker"sv;
case Request::Destination::Style:
return "style"sv;
case Request::Destination::Track:
return "track"sv;
case Request::Destination::Video:
return "video"sv;
case Request::Destination::WebIdentity:
return "webidentity"sv;
case Request::Destination::Worker:
return "worker"sv;
case Request::Destination::XSLT:
return "xslt"sv;
}
VERIFY_NOT_REACHED();
}
StringView request_mode_to_string(Request::Mode mode)
{
switch (mode) {
case Request::Mode::SameOrigin:
return "same-origin"sv;
case Request::Mode::CORS:
return "cors"sv;
case Request::Mode::NoCORS:
return "no-cors"sv;
case Request::Mode::Navigate:
return "navigate"sv;
case Request::Mode::WebSocket:
return "websocket"sv;
}
VERIFY_NOT_REACHED();
}
Optional<Request::Priority> request_priority_from_string(StringView string)
{
if (string.equals_ignoring_ascii_case("high"sv))
return Request::Priority::High;
if (string.equals_ignoring_ascii_case("low"sv))
return Request::Priority::Low;
if (string.equals_ignoring_ascii_case("auto"sv))
return Request::Priority::Auto;
return {};
}
}

View file

@ -0,0 +1,537 @@
/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2023, networkException <networkexception@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/Error.h>
#include <AK/Forward.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibJS/Forward.h>
#include <LibJS/Heap/Cell.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibURL/Origin.h>
#include <LibURL/URL.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Headers.h>
#include <LibWeb/HTML/PolicyContainers.h>
#include <LibWeb/HTML/Scripting/Environments.h>
namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-request
class Request final : public JS::Cell {
JS_CELL(Request, JS::Cell);
JS_DECLARE_ALLOCATOR(Request);
public:
enum class CacheMode {
Default,
NoStore,
Reload,
NoCache,
ForceCache,
OnlyIfCached,
};
enum class CredentialsMode {
Omit,
SameOrigin,
Include,
};
enum class Destination {
Audio,
AudioWorklet,
Document,
Embed,
Font,
Frame,
IFrame,
Image,
JSON,
Manifest,
Object,
PaintWorklet,
Report,
Script,
ServiceWorker,
SharedWorker,
Style,
Track,
Video,
WebIdentity,
Worker,
XSLT,
};
enum class Initiator {
Download,
ImageSet,
Manifest,
Prefetch,
Prerender,
XSLT,
};
enum class InitiatorType {
Audio,
Beacon,
Body,
CSS,
EarlyHint,
Embed,
Fetch,
Font,
Frame,
IFrame,
Image,
IMG,
Input,
Link,
Object,
Ping,
Script,
Track,
Video,
XMLHttpRequest,
Other,
};
enum class Mode {
SameOrigin,
CORS,
NoCORS,
Navigate,
WebSocket,
};
enum class Origin {
Client,
};
enum class ParserMetadata {
ParserInserted,
NotParserInserted,
};
enum class PolicyContainer {
Client,
};
enum class RedirectMode {
Follow,
Error,
Manual,
};
enum class Referrer {
NoReferrer,
Client,
};
enum class ResponseTainting {
Basic,
CORS,
Opaque,
};
enum class ServiceWorkersMode {
All,
None,
};
enum class Window {
NoWindow,
Client,
};
enum class Priority {
High,
Low,
Auto
};
// AD-HOC: Some web features need to receive data as it arrives, rather than when the response is fully complete
// or when enough data has been buffered. Use this buffer policy to inform fetch of that requirement.
enum class BufferPolicy {
BufferResponse,
DoNotBufferResponse,
};
// Members are implementation-defined
struct InternalPriority { };
using BodyType = Variant<Empty, ByteBuffer, JS::NonnullGCPtr<Body>>;
using OriginType = Variant<Origin, URL::Origin>;
using PolicyContainerType = Variant<PolicyContainer, HTML::PolicyContainer>;
using ReferrerType = Variant<Referrer, URL::URL>;
using ReservedClientType = JS::GCPtr<HTML::Environment>;
using WindowType = Variant<Window, JS::GCPtr<HTML::EnvironmentSettingsObject>>;
[[nodiscard]] static JS::NonnullGCPtr<Request> create(JS::VM&);
[[nodiscard]] ReadonlyBytes method() const { return m_method; }
void set_method(ByteBuffer method) { m_method = move(method); }
[[nodiscard]] bool local_urls_only() const { return m_local_urls_only; }
void set_local_urls_only(bool local_urls_only) { m_local_urls_only = local_urls_only; }
[[nodiscard]] JS::NonnullGCPtr<HeaderList> header_list() const { return m_header_list; }
void set_header_list(JS::NonnullGCPtr<HeaderList> header_list) { m_header_list = header_list; }
[[nodiscard]] bool unsafe_request() const { return m_unsafe_request; }
void set_unsafe_request(bool unsafe_request) { m_unsafe_request = unsafe_request; }
[[nodiscard]] BodyType const& body() const { return m_body; }
[[nodiscard]] BodyType& body() { return m_body; }
void set_body(BodyType body) { m_body = move(body); }
[[nodiscard]] JS::GCPtr<HTML::EnvironmentSettingsObject const> client() const { return m_client; }
[[nodiscard]] JS::GCPtr<HTML::EnvironmentSettingsObject> client() { return m_client; }
void set_client(HTML::EnvironmentSettingsObject* client) { m_client = client; }
[[nodiscard]] ReservedClientType const& reserved_client() const { return m_reserved_client; }
[[nodiscard]] ReservedClientType& reserved_client() { return m_reserved_client; }
void set_reserved_client(ReservedClientType reserved_client) { m_reserved_client = move(reserved_client); }
[[nodiscard]] String const& replaces_client_id() const { return m_replaces_client_id; }
void set_replaces_client_id(String replaces_client_id) { m_replaces_client_id = move(replaces_client_id); }
[[nodiscard]] WindowType const& window() const { return m_window; }
void set_window(WindowType window) { m_window = move(window); }
[[nodiscard]] bool keepalive() const { return m_keepalive; }
void set_keepalive(bool keepalive) { m_keepalive = keepalive; }
[[nodiscard]] Optional<InitiatorType> const& initiator_type() const { return m_initiator_type; }
void set_initiator_type(Optional<InitiatorType> initiator_type) { m_initiator_type = move(initiator_type); }
[[nodiscard]] ServiceWorkersMode service_workers_mode() const { return m_service_workers_mode; }
void set_service_workers_mode(ServiceWorkersMode service_workers_mode) { m_service_workers_mode = service_workers_mode; }
[[nodiscard]] Optional<Initiator> const& initiator() const { return m_initiator; }
void set_initiator(Optional<Initiator> initiator) { m_initiator = move(initiator); }
[[nodiscard]] Optional<Destination> const& destination() const { return m_destination; }
void set_destination(Optional<Destination> destination) { m_destination = move(destination); }
[[nodiscard]] Priority const& priority() const { return m_priority; }
void set_priority(Priority priority) { m_priority = priority; }
[[nodiscard]] OriginType const& origin() const { return m_origin; }
void set_origin(OriginType origin) { m_origin = move(origin); }
[[nodiscard]] PolicyContainerType const& policy_container() const { return m_policy_container; }
void set_policy_container(PolicyContainerType policy_container) { m_policy_container = move(policy_container); }
[[nodiscard]] Mode mode() const { return m_mode; }
void set_mode(Mode mode) { m_mode = mode; }
[[nodiscard]] bool use_cors_preflight() const { return m_use_cors_preflight; }
void set_use_cors_preflight(bool use_cors_preflight) { m_use_cors_preflight = use_cors_preflight; }
[[nodiscard]] CredentialsMode credentials_mode() const { return m_credentials_mode; }
void set_credentials_mode(CredentialsMode credentials_mode) { m_credentials_mode = credentials_mode; }
[[nodiscard]] bool use_url_credentials() const { return m_use_url_credentials; }
void set_use_url_credentials(bool use_url_credentials) { m_use_url_credentials = use_url_credentials; }
[[nodiscard]] CacheMode cache_mode() const { return m_cache_mode; }
void set_cache_mode(CacheMode cache_mode) { m_cache_mode = cache_mode; }
[[nodiscard]] RedirectMode redirect_mode() const { return m_redirect_mode; }
void set_redirect_mode(RedirectMode redirect_mode) { m_redirect_mode = redirect_mode; }
[[nodiscard]] String const& integrity_metadata() const { return m_integrity_metadata; }
void set_integrity_metadata(String integrity_metadata) { m_integrity_metadata = move(integrity_metadata); }
[[nodiscard]] String const& cryptographic_nonce_metadata() const { return m_cryptographic_nonce_metadata; }
void set_cryptographic_nonce_metadata(String cryptographic_nonce_metadata) { m_cryptographic_nonce_metadata = move(cryptographic_nonce_metadata); }
[[nodiscard]] Optional<ParserMetadata> const& parser_metadata() const { return m_parser_metadata; }
void set_parser_metadata(Optional<ParserMetadata> parser_metadata) { m_parser_metadata = move(parser_metadata); }
[[nodiscard]] bool reload_navigation() const { return m_reload_navigation; }
void set_reload_navigation(bool reload_navigation) { m_reload_navigation = reload_navigation; }
[[nodiscard]] bool history_navigation() const { return m_history_navigation; }
void set_history_navigation(bool history_navigation) { m_history_navigation = history_navigation; }
[[nodiscard]] bool user_activation() const { return m_user_activation; }
void set_user_activation(bool user_activation) { m_user_activation = user_activation; }
[[nodiscard]] bool render_blocking() const { return m_render_blocking; }
void set_render_blocking(bool render_blocking) { m_render_blocking = render_blocking; }
[[nodiscard]] Vector<URL::URL> const& url_list() const { return m_url_list; }
[[nodiscard]] Vector<URL::URL>& url_list() { return m_url_list; }
void set_url_list(Vector<URL::URL> url_list) { m_url_list = move(url_list); }
[[nodiscard]] u8 redirect_count() const { return m_redirect_count; }
void set_redirect_count(u8 redirect_count) { m_redirect_count = redirect_count; }
[[nodiscard]] ReferrerType const& referrer() const { return m_referrer; }
void set_referrer(ReferrerType referrer) { m_referrer = move(referrer); }
[[nodiscard]] ReferrerPolicy::ReferrerPolicy const& referrer_policy() const { return m_referrer_policy; }
void set_referrer_policy(ReferrerPolicy::ReferrerPolicy referrer_policy) { m_referrer_policy = move(referrer_policy); }
[[nodiscard]] ResponseTainting response_tainting() const { return m_response_tainting; }
void set_response_tainting(ResponseTainting response_tainting) { m_response_tainting = response_tainting; }
[[nodiscard]] bool prevent_no_cache_cache_control_header_modification() const { return m_prevent_no_cache_cache_control_header_modification; }
void set_prevent_no_cache_cache_control_header_modification(bool prevent_no_cache_cache_control_header_modification) { m_prevent_no_cache_cache_control_header_modification = prevent_no_cache_cache_control_header_modification; }
[[nodiscard]] bool done() const { return m_done; }
void set_done(bool done) { m_done = done; }
[[nodiscard]] bool timing_allow_failed() const { return m_timing_allow_failed; }
void set_timing_allow_failed(bool timing_allow_failed) { m_timing_allow_failed = timing_allow_failed; }
[[nodiscard]] URL::URL& url();
[[nodiscard]] URL::URL const& url() const;
[[nodiscard]] URL::URL& current_url();
[[nodiscard]] URL::URL const& current_url() const;
void set_url(URL::URL url);
[[nodiscard]] bool destination_is_script_like() const;
[[nodiscard]] bool is_subresource_request() const;
[[nodiscard]] bool is_non_subresource_request() const;
[[nodiscard]] bool is_navigation_request() const;
[[nodiscard]] bool has_redirect_tainted_origin() const;
[[nodiscard]] String serialize_origin() const;
[[nodiscard]] ByteBuffer byte_serialize_origin() const;
[[nodiscard]] JS::NonnullGCPtr<Request> clone(JS::Realm&) const;
void add_range_header(u64 first, Optional<u64> const& last);
void add_origin_header();
[[nodiscard]] bool cross_origin_embedder_policy_allows_credentials() const;
// Non-standard
void add_pending_response(Badge<Fetching::PendingResponse>, JS::NonnullGCPtr<Fetching::PendingResponse> pending_response)
{
VERIFY(!m_pending_responses.contains_slow(pending_response));
m_pending_responses.append(pending_response);
}
void remove_pending_response(Badge<Fetching::PendingResponse>, JS::NonnullGCPtr<Fetching::PendingResponse> pending_response)
{
m_pending_responses.remove_first_matching([&](auto gc_ptr) { return gc_ptr == pending_response; });
}
[[nodiscard]] BufferPolicy buffer_policy() const { return m_buffer_policy; }
void set_buffer_policy(BufferPolicy buffer_policy) { m_buffer_policy = buffer_policy; }
private:
explicit Request(JS::NonnullGCPtr<HeaderList>);
virtual void visit_edges(JS::Cell::Visitor&) override;
// https://fetch.spec.whatwg.org/#concept-request-method
// A request has an associated method (a method). Unless stated otherwise it is `GET`.
ByteBuffer m_method { ByteBuffer::copy("GET"sv.bytes()).release_value() };
// https://fetch.spec.whatwg.org/#local-urls-only-flag
// A request has an associated local-URLs-only flag. Unless stated otherwise it is unset.
bool m_local_urls_only { false };
// https://fetch.spec.whatwg.org/#concept-request-header-list
// A request has an associated header list (a header list). Unless stated otherwise it is empty.
JS::NonnullGCPtr<HeaderList> m_header_list;
// https://fetch.spec.whatwg.org/#unsafe-request-flag
// A request has an associated unsafe-request flag. Unless stated otherwise it is unset.
bool m_unsafe_request { false };
// https://fetch.spec.whatwg.org/#concept-request-body
// A request has an associated body (null, a byte sequence, or a body). Unless stated otherwise it is null.
BodyType m_body;
// https://fetch.spec.whatwg.org/#concept-request-client
// A request has an associated client (null or an environment settings object).
JS::GCPtr<HTML::EnvironmentSettingsObject> m_client;
// https://fetch.spec.whatwg.org/#concept-request-reserved-client
// A request has an associated reserved client (null, an environment, or an environment settings object). Unless
// stated otherwise it is null.
ReservedClientType m_reserved_client;
// https://fetch.spec.whatwg.org/#concept-request-replaces-client-id
// A request has an associated replaces client id (a string). Unless stated otherwise it is the empty string.
String m_replaces_client_id;
// https://fetch.spec.whatwg.org/#concept-request-window
// A request has an associated window ("no-window", "client", or an environment settings object whose global object
// is a Window object). Unless stated otherwise it is "client".
WindowType m_window { Window::Client };
// https://fetch.spec.whatwg.org/#request-keepalive-flag
// A request has an associated boolean keepalive. Unless stated otherwise it is false.
bool m_keepalive { false };
// https://fetch.spec.whatwg.org/#request-initiator-type
// A request has an associated initiator type, which is null, "audio", "beacon", "body", "css", "early-hint",
// "embed", "fetch", "font", "frame", "iframe", "image", "img", "input", "link", "object", "ping", "script",
// "track", "video", "xmlhttprequest", or "other". Unless stated otherwise it is null. [RESOURCE-TIMING]
Optional<InitiatorType> m_initiator_type;
// https://fetch.spec.whatwg.org/#request-service-workers-mode
// A request has an associated service-workers mode, that is "all" or "none". Unless stated otherwise it is "all".
ServiceWorkersMode m_service_workers_mode { ServiceWorkersMode::All };
// https://fetch.spec.whatwg.org/#concept-request-initiator
// A request has an associated initiator, which is the empty string, "download", "imageset", "manifest",
// "prefetch", "prerender", or "xslt". Unless stated otherwise it is the empty string.
Optional<Initiator> m_initiator;
// https://fetch.spec.whatwg.org/#concept-request-destination
// A request has an associated destination, which is the empty string, "audio", "audioworklet", "document",
// "embed", "font", "frame", "iframe", "image", "json", "manifest", "object", "paintworklet", "report", "script",
// "serviceworker", "sharedworker", "style", "track", "video", "webidentity", "worker", or "xslt". Unless stated
// otherwise it is the empty string.
// NOTE: These are reflected on RequestDestination except for "serviceworker" and "webidentity" as fetches with
// those destinations skip service workers.
Optional<Destination> m_destination;
// https://fetch.spec.whatwg.org/#request-priority
// A request has an associated priority, which is "high", "low", or "auto". Unless stated otherwise it is "auto".
Priority m_priority { Priority::Auto };
// https://fetch.spec.whatwg.org/#request-internal-priority
// A request has an associated internal priority (null or an implementation-defined object). Unless otherwise stated it is null.
Optional<InternalPriority> m_internal_priority;
// https://fetch.spec.whatwg.org/#concept-request-origin
// A request has an associated origin, which is "client" or an origin. Unless stated otherwise it is "client".
OriginType m_origin { Origin::Client };
// https://fetch.spec.whatwg.org/#concept-request-policy-container
// A request has an associated policy container, which is "client" or a policy container. Unless stated otherwise
// it is "client".
PolicyContainerType m_policy_container { PolicyContainer::Client };
// https://fetch.spec.whatwg.org/#concept-request-referrer
// A request has an associated referrer, which is "no-referrer", "client", or a URL. Unless stated otherwise it is
// "client".
ReferrerType m_referrer { Referrer::Client };
// https://fetch.spec.whatwg.org/#concept-request-referrer-policy
// A request has an associated referrer policy, which is a referrer policy. Unless stated otherwise it is the empty
// string.
ReferrerPolicy::ReferrerPolicy m_referrer_policy { ReferrerPolicy::ReferrerPolicy::EmptyString };
// https://fetch.spec.whatwg.org/#concept-request-mode
// A request has an associated mode, which is "same-origin", "cors", "no-cors", "navigate", or "websocket". Unless
// stated otherwise, it is "no-cors".
Mode m_mode { Mode::NoCORS };
// https://fetch.spec.whatwg.org/#use-cors-preflight-flag
// A request has an associated use-CORS-preflight flag. Unless stated otherwise, it is unset.
bool m_use_cors_preflight { false };
// https://fetch.spec.whatwg.org/#concept-request-credentials-mode
// A request has an associated credentials mode, which is "omit", "same-origin", or "include". Unless stated
// otherwise, it is "same-origin".
CredentialsMode m_credentials_mode { CredentialsMode::SameOrigin };
// https://fetch.spec.whatwg.org/#concept-request-use-url-credentials-flag
// A request has an associated use-URL-credentials flag. Unless stated otherwise, it is unset.
// NOTE: When this flag is set, when a requests URL has a username and password, and there is an available
// authentication entry for the request, then the URLs credentials are preferred over that of the
// authentication entry. Modern specifications avoid setting this flag, since putting credentials in URLs is
// discouraged, but some older features set it for compatibility reasons.
bool m_use_url_credentials { false };
// https://fetch.spec.whatwg.org/#concept-request-cache-mode
// A request has an associated cache mode, which is "default", "no-store", "reload", "no-cache", "force-cache", or
// "only-if-cached". Unless stated otherwise, it is "default".
CacheMode m_cache_mode { CacheMode::Default };
// https://fetch.spec.whatwg.org/#concept-request-redirect-mode
// A request has an associated redirect mode, which is "follow", "error", or "manual". Unless stated otherwise, it
// is "follow".
RedirectMode m_redirect_mode { RedirectMode::Follow };
// https://fetch.spec.whatwg.org/#concept-request-integrity-metadata
// A request has associated integrity metadata (a string). Unless stated otherwise, it is the empty string.
String m_integrity_metadata;
// https://fetch.spec.whatwg.org/#concept-request-nonce-metadata
// A request has associated cryptographic nonce metadata (a string). Unless stated otherwise, it is the empty
// string.
String m_cryptographic_nonce_metadata;
// https://fetch.spec.whatwg.org/#concept-request-parser-metadata
// A request has associated parser metadata which is the empty string, "parser-inserted", or
// "not-parser-inserted". Unless otherwise stated, it is the empty string.
Optional<ParserMetadata> m_parser_metadata;
// https://fetch.spec.whatwg.org/#concept-request-reload-navigation-flag
// A request has an associated reload-navigation flag. Unless stated otherwise, it is unset.
bool m_reload_navigation { false };
// https://fetch.spec.whatwg.org/#concept-request-history-navigation-flag
// A request has an associated history-navigation flag. Unless stated otherwise, it is unset.
bool m_history_navigation { false };
// https://fetch.spec.whatwg.org/#request-user-activation
// A request has an associated boolean user-activation. Unless stated otherwise, it is false.
bool m_user_activation { false };
// https://fetch.spec.whatwg.org/#request-render-blocking
// A request has an associated boolean render-blocking. Unless stated otherwise, it is false.
bool m_render_blocking { false };
// https://fetch.spec.whatwg.org/#concept-request-url-list
// A request has an associated URL list (a list of one or more URLs). Unless stated otherwise, it is a list
// containing a copy of requests URL.
Vector<URL::URL> m_url_list;
// https://fetch.spec.whatwg.org/#concept-request-redirect-count
// A request has an associated redirect count. Unless stated otherwise, it is zero.
// NOTE: '4.4. HTTP-redirect fetch' infers a limit of 20.
u8 m_redirect_count { 0 };
// https://fetch.spec.whatwg.org/#concept-request-response-tainting
// A request has an associated response tainting, which is "basic", "cors", or "opaque". Unless stated otherwise,
// it is "basic".
ResponseTainting m_response_tainting { ResponseTainting::Basic };
// https://fetch.spec.whatwg.org/#no-cache-prevent-cache-control
// A request has an associated prevent no-cache cache-control header modification flag. Unless stated otherwise, it
// is unset.
bool m_prevent_no_cache_cache_control_header_modification { false };
// https://fetch.spec.whatwg.org/#done-flag
// A request has an associated done flag. Unless stated otherwise, it is unset.
bool m_done { false };
// https://fetch.spec.whatwg.org/#timing-allow-failed
// A request has an associated timing allow failed flag. Unless stated otherwise, it is unset.
bool m_timing_allow_failed { false };
// Non-standard
Vector<JS::NonnullGCPtr<Fetching::PendingResponse>> m_pending_responses;
BufferPolicy m_buffer_policy { BufferPolicy::BufferResponse };
};
StringView request_destination_to_string(Request::Destination);
StringView request_mode_to_string(Request::Mode);
Optional<Request::Priority> request_priority_from_string(StringView);
}

View file

@ -0,0 +1,448 @@
/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <AK/TypeCasts.h>
#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/Fetch/Infrastructure/FetchParams.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
namespace Web::Fetch::Infrastructure {
JS_DEFINE_ALLOCATOR(Response);
JS_DEFINE_ALLOCATOR(BasicFilteredResponse);
JS_DEFINE_ALLOCATOR(CORSFilteredResponse);
JS_DEFINE_ALLOCATOR(OpaqueFilteredResponse);
JS_DEFINE_ALLOCATOR(OpaqueRedirectFilteredResponse);
Response::Response(JS::NonnullGCPtr<HeaderList> header_list)
: m_header_list(header_list)
, m_response_time(UnixDateTime::now())
{
}
void Response::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_header_list);
visitor.visit(m_body);
}
JS::NonnullGCPtr<Response> Response::create(JS::VM& vm)
{
return vm.heap().allocate_without_realm<Response>(HeaderList::create(vm));
}
// https://fetch.spec.whatwg.org/#ref-for-concept-network-error%E2%91%A3
// A network error is a response whose status is always 0, status message is always
// the empty byte sequence, header list is always empty, and body is always null.
JS::NonnullGCPtr<Response> Response::aborted_network_error(JS::VM& vm)
{
auto response = network_error(vm, "Fetch has been aborted"sv);
response->set_aborted(true);
return response;
}
JS::NonnullGCPtr<Response> Response::network_error(JS::VM& vm, Variant<String, StringView> message)
{
dbgln_if(WEB_FETCH_DEBUG, "Fetch: Creating network error response with message: {}", message.visit([](auto const& s) -> StringView { return s; }));
auto response = Response::create(vm);
response->set_status(0);
response->set_type(Type::Error);
VERIFY(!response->body());
response->m_network_error_message = move(message);
return response;
}
// https://fetch.spec.whatwg.org/#appropriate-network-error
JS::NonnullGCPtr<Response> Response::appropriate_network_error(JS::VM& vm, FetchParams const& fetch_params)
{
// 1. Assert: fetchParams is canceled.
VERIFY(fetch_params.is_canceled());
// 2. Return an aborted network error if fetchParams is aborted; otherwise return a network error.
return fetch_params.is_aborted()
? aborted_network_error(vm)
: network_error(vm, "Fetch has been terminated"sv);
}
// https://fetch.spec.whatwg.org/#concept-aborted-network-error
bool Response::is_aborted_network_error() const
{
// A response whose type is "error" and aborted flag is set is known as an aborted network error.
// NOTE: We have to use the virtual getter here to not bypass filtered responses.
return type() == Type::Error && aborted();
}
// https://fetch.spec.whatwg.org/#concept-network-error
bool Response::is_network_error() const
{
// A network error is a response whose type is "error", status is 0, status message is the empty byte sequence,
// header list is « », body is null, and body info is a new response body info.
// NOTE: We have to use the virtual getter here to not bypass filtered responses.
if (type() != Type::Error)
return false;
if (status() != 0)
return false;
if (!status_message().is_empty())
return false;
if (!header_list()->is_empty())
return false;
if (body())
return false;
if (body_info() != BodyInfo {})
return false;
return true;
}
// https://fetch.spec.whatwg.org/#concept-response-url
Optional<URL::URL const&> Response::url() const
{
// A response has an associated URL. It is a pointer to the last URL in responses URL list and null if responses URL list is empty.
// NOTE: We have to use the virtual getter here to not bypass filtered responses.
if (url_list().is_empty())
return {};
return url_list().last();
}
// https://fetch.spec.whatwg.org/#concept-response-location-url
ErrorOr<Optional<URL::URL>> Response::location_url(Optional<String> const& request_fragment) const
{
// The location URL of a response response, given null or an ASCII string requestFragment, is the value returned by the following steps. They return null, failure, or a URL.
// 1. If responses status is not a redirect status, then return null.
// NOTE: We have to use the virtual getter here to not bypass filtered responses.
if (!is_redirect_status(status()))
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>())
return Optional<URL::URL> {};
auto const& location_values = location_values_or_failure.get<Vector<ByteBuffer>>();
if (location_values.size() != 1)
return Optional<URL::URL> {};
// 3. If location is a header value, then set location to the result of parsing location with responses URL.
auto location = DOMURL::parse(location_values.first(), url());
if (!location.is_valid())
return Error::from_string_literal("Invalid 'Location' header URL");
// 4. If location is a URL whose fragment is null, then set locations fragment to requestFragment.
if (!location.fragment().has_value())
location.set_fragment(request_fragment);
// 5. Return location.
return location;
}
// https://fetch.spec.whatwg.org/#concept-response-clone
JS::NonnullGCPtr<Response> Response::clone(JS::Realm& realm) const
{
// To clone a response response, run these steps:
auto& vm = realm.vm();
// 1. If response is a filtered response, then return a new identical filtered response whose internal response is a clone of responses internal response.
if (is<FilteredResponse>(*this)) {
auto internal_response = static_cast<FilteredResponse const&>(*this).internal_response()->clone(realm);
if (is<BasicFilteredResponse>(*this))
return BasicFilteredResponse::create(vm, internal_response);
if (is<CORSFilteredResponse>(*this))
return CORSFilteredResponse::create(vm, internal_response);
if (is<OpaqueFilteredResponse>(*this))
return OpaqueFilteredResponse::create(vm, internal_response);
if (is<OpaqueRedirectFilteredResponse>(*this))
return OpaqueRedirectFilteredResponse::create(vm, internal_response);
VERIFY_NOT_REACHED();
}
// 2. Let newResponse be a copy of response, except for its body.
auto new_response = Infrastructure::Response::create(vm);
new_response->set_type(m_type);
new_response->set_aborted(m_aborted);
new_response->set_url_list(m_url_list);
new_response->set_status(m_status);
new_response->set_status_message(m_status_message);
for (auto const& header : *m_header_list)
new_response->header_list()->append(header);
new_response->set_cache_state(m_cache_state);
new_response->set_cors_exposed_header_name_list(m_cors_exposed_header_name_list);
new_response->set_range_requested(m_range_requested);
new_response->set_request_includes_credentials(m_request_includes_credentials);
new_response->set_timing_allow_passed(m_timing_allow_passed);
new_response->set_body_info(m_body_info);
// FIXME: service worker timing info
// 3. If responses body is non-null, then set newResponses body to the result of cloning responses body.
if (m_body)
new_response->set_body(m_body->clone(realm));
// 4. Return newResponse.
return new_response;
}
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#unsafe-response
JS::NonnullGCPtr<Response> Response::unsafe_response()
{
// A response's unsafe response is its internal response if it has one, and the response itself otherwise.
if (is<FilteredResponse>(this))
return static_cast<FilteredResponse&>(*this).internal_response();
return *this;
}
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-cross-origin
bool Response::is_cors_cross_origin() const
{
// A response whose type is "opaque" or "opaqueredirect" is CORS-cross-origin.
return type() == Type::Opaque || type() == Type::OpaqueRedirect;
}
// https://fetch.spec.whatwg.org/#concept-fresh-response
bool Response::is_fresh() const
{
// A fresh response is a response whose current age is within its freshness lifetime.
return current_age() < freshness_lifetime();
}
// https://fetch.spec.whatwg.org/#concept-stale-while-revalidate-response
bool Response::is_stale_while_revalidate() const
{
// A stale-while-revalidate response is a response that is not a fresh response and whose current age is within the stale-while-revalidate lifetime.
return !is_fresh() && current_age() < stale_while_revalidate_lifetime();
}
// https://fetch.spec.whatwg.org/#concept-stale-response
bool Response::is_stale() const
{
// A stale response is a response that is not a fresh response or a stale-while-revalidate response.
return !is_fresh() && !is_stale_while_revalidate();
}
// https://httpwg.org/specs/rfc9111.html#age.calculations
u64 Response::current_age() const
{
// The term "age_value" denotes the value of the Age header field (Section 5.1), in a form appropriate for arithmetic operation; or 0, if not available.
Optional<AK::Duration> age;
if (auto const age_header = header_list()->get("Age"sv.bytes()); age_header.has_value()) {
if (auto converted_age = StringView { *age_header }.to_number<u64>(); converted_age.has_value())
age = AK::Duration::from_seconds(converted_age.value());
}
auto const age_value = age.value_or(AK::Duration::from_seconds(0));
// The term "date_value" denotes the value of the Date header field, in a form appropriate for arithmetic operations. See Section 6.6.1 of [HTTP] for the definition of the Date header field and for requirements regarding responses without it.
// FIXME: Do we have a parser for HTTP-date?
auto const date_value = UnixDateTime::now() - AK::Duration::from_seconds(5);
// The term "now" means the current value of this implementation's clock (Section 5.6.7 of [HTTP]).
auto const now = UnixDateTime::now();
// The value of the clock at the time of the request that resulted in the stored response.
// FIXME: Let's get the correct time.
auto const request_time = UnixDateTime::now() - AK::Duration::from_seconds(5);
// The value of the clock at the time the response was received.
auto const response_time = m_response_time;
auto const apparent_age = max(0, (response_time - date_value).to_seconds());
auto const response_delay = response_time - request_time;
auto const corrected_age_value = age_value + response_delay;
auto const corrected_initial_age = max(apparent_age, corrected_age_value.to_seconds());
auto const resident_time = (now - response_time).to_seconds();
return corrected_initial_age + resident_time;
}
// https://httpwg.org/specs/rfc9111.html#calculating.freshness.lifetime
u64 Response::freshness_lifetime() const
{
auto const elem = header_list()->get_decode_and_split("Cache-Control"sv.bytes());
if (!elem.has_value())
return 0;
// FIXME: If the cache is shared and the s-maxage response directive (Section 5.2.2.10) is present, use its value
// If the max-age response directive (Section 5.2.2.1) is present, use its value, or
for (auto const& directive : *elem) {
if (directive.starts_with_bytes("max-age"sv)) {
auto equal_offset = directive.find_byte_offset('=');
if (!equal_offset.has_value()) {
dbgln("Bogus directive: '{}'", directive);
continue;
}
auto const value_string = directive.bytes_as_string_view().substring_view(equal_offset.value() + 1);
auto maybe_value = value_string.to_number<u64>();
if (!maybe_value.has_value()) {
dbgln("Bogus directive: '{}'", directive);
continue;
}
return maybe_value.value();
}
}
// FIXME: If the Expires response header field (Section 5.3) is present, use its value minus the value of the Date response header field (using the time the message was received if it is not present, as per Section 6.6.1 of [HTTP]), or
// FIXME: Otherwise, no explicit expiration time is present in the response. A heuristic freshness lifetime might be applicable; see Section 4.2.2.
return 0;
}
// https://httpwg.org/specs/rfc5861.html#n-the-stale-while-revalidate-cache-control-extension
u64 Response::stale_while_revalidate_lifetime() const
{
auto const elem = header_list()->get_decode_and_split("Cache-Control"sv.bytes());
if (!elem.has_value())
return 0;
for (auto const& directive : *elem) {
if (directive.starts_with_bytes("stale-while-revalidate"sv)) {
auto equal_offset = directive.find_byte_offset('=');
if (!equal_offset.has_value()) {
dbgln("Bogus directive: '{}'", directive);
continue;
}
auto const value_string = directive.bytes_as_string_view().substring_view(equal_offset.value() + 1);
auto maybe_value = value_string.to_number<u64>();
if (!maybe_value.has_value()) {
dbgln("Bogus directive: '{}'", directive);
continue;
}
return maybe_value.value();
}
}
return 0;
}
// Non-standard
Optional<StringView> Response::network_error_message() const
{
if (!m_network_error_message.has_value())
return {};
return m_network_error_message->visit([](auto const& s) -> StringView { return s; });
}
FilteredResponse::FilteredResponse(JS::NonnullGCPtr<Response> internal_response, JS::NonnullGCPtr<HeaderList> header_list)
: Response(header_list)
, m_internal_response(internal_response)
{
}
FilteredResponse::~FilteredResponse()
{
}
void FilteredResponse::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_internal_response);
}
JS::NonnullGCPtr<BasicFilteredResponse> BasicFilteredResponse::create(JS::VM& vm, JS::NonnullGCPtr<Response> internal_response)
{
// A basic filtered response is a filtered response whose type is "basic" and header list excludes
// any headers in internal responses header list whose name is a forbidden response-header name.
auto header_list = HeaderList::create(vm);
for (auto const& header : *internal_response->header_list()) {
if (!is_forbidden_response_header_name(header.name))
header_list->append(header);
}
return vm.heap().allocate_without_realm<BasicFilteredResponse>(internal_response, header_list);
}
BasicFilteredResponse::BasicFilteredResponse(JS::NonnullGCPtr<Response> internal_response, JS::NonnullGCPtr<HeaderList> header_list)
: FilteredResponse(internal_response, header_list)
, m_header_list(header_list)
{
}
void BasicFilteredResponse::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_header_list);
}
JS::NonnullGCPtr<CORSFilteredResponse> CORSFilteredResponse::create(JS::VM& vm, JS::NonnullGCPtr<Response> internal_response)
{
// A CORS filtered response is a filtered response whose type is "cors" and header list excludes
// any headers in internal responses header list whose name is not a CORS-safelisted response-header
// name, given internal responses CORS-exposed header-name list.
Vector<ReadonlyBytes> cors_exposed_header_name_list;
for (auto const& header_name : internal_response->cors_exposed_header_name_list())
cors_exposed_header_name_list.append(header_name.span());
auto header_list = HeaderList::create(vm);
for (auto const& header : *internal_response->header_list()) {
if (is_cors_safelisted_response_header_name(header.name, cors_exposed_header_name_list))
header_list->append(header);
}
return vm.heap().allocate_without_realm<CORSFilteredResponse>(internal_response, header_list);
}
CORSFilteredResponse::CORSFilteredResponse(JS::NonnullGCPtr<Response> internal_response, JS::NonnullGCPtr<HeaderList> header_list)
: FilteredResponse(internal_response, header_list)
, m_header_list(header_list)
{
}
void CORSFilteredResponse::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_header_list);
}
JS::NonnullGCPtr<OpaqueFilteredResponse> OpaqueFilteredResponse::create(JS::VM& vm, JS::NonnullGCPtr<Response> internal_response)
{
// An opaque filtered response is a filtered response whose type is "opaque", URL list is the empty list,
// status is 0, status message is the empty byte sequence, header list is empty, and body is null.
return vm.heap().allocate_without_realm<OpaqueFilteredResponse>(internal_response, HeaderList::create(vm));
}
OpaqueFilteredResponse::OpaqueFilteredResponse(JS::NonnullGCPtr<Response> internal_response, JS::NonnullGCPtr<HeaderList> header_list)
: FilteredResponse(internal_response, header_list)
, m_header_list(header_list)
{
}
void OpaqueFilteredResponse::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_header_list);
visitor.visit(m_body);
}
JS::NonnullGCPtr<OpaqueRedirectFilteredResponse> OpaqueRedirectFilteredResponse::create(JS::VM& vm, JS::NonnullGCPtr<Response> internal_response)
{
// An opaque-redirect filtered response is a filtered response whose type is "opaqueredirect",
// status is 0, status message is the empty byte sequence, header list is empty, and body is null.
return vm.heap().allocate_without_realm<OpaqueRedirectFilteredResponse>(internal_response, HeaderList::create(vm));
}
OpaqueRedirectFilteredResponse::OpaqueRedirectFilteredResponse(JS::NonnullGCPtr<Response> internal_response, JS::NonnullGCPtr<HeaderList> header_list)
: FilteredResponse(internal_response, header_list)
, m_header_list(header_list)
{
}
void OpaqueRedirectFilteredResponse::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_header_list);
visitor.visit(m_body);
}
}

View file

@ -0,0 +1,332 @@
/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/Error.h>
#include <AK/Forward.h>
#include <AK/Optional.h>
#include <AK/Time.h>
#include <AK/Vector.h>
#include <LibJS/Forward.h>
#include <LibJS/Heap/Cell.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibURL/URL.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Headers.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Statuses.h>
namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-response
class Response : public JS::Cell {
JS_CELL(Response, JS::Cell);
JS_DECLARE_ALLOCATOR(Response);
public:
enum class CacheState {
Local,
Validated,
};
enum class Type {
Basic,
CORS,
Default,
Error,
Opaque,
OpaqueRedirect,
};
// https://fetch.spec.whatwg.org/#response-body-info
struct BodyInfo {
// https://fetch.spec.whatwg.org/#fetch-timing-info-encoded-body-size
u64 encoded_size { 0 };
// https://fetch.spec.whatwg.org/#fetch-timing-info-decoded-body-size
u64 decoded_size { 0 };
// https://fetch.spec.whatwg.org/#response-body-info-content-type
String content_type {};
bool operator==(BodyInfo const&) const = default;
};
[[nodiscard]] static JS::NonnullGCPtr<Response> create(JS::VM&);
[[nodiscard]] static JS::NonnullGCPtr<Response> aborted_network_error(JS::VM&);
[[nodiscard]] static JS::NonnullGCPtr<Response> network_error(JS::VM&, Variant<String, StringView> message);
[[nodiscard]] static JS::NonnullGCPtr<Response> appropriate_network_error(JS::VM&, FetchParams const&);
virtual ~Response() = default;
[[nodiscard]] virtual Type type() const { return m_type; }
void set_type(Type type) { m_type = type; }
[[nodiscard]] virtual bool aborted() const { return m_aborted; }
void set_aborted(bool aborted) { m_aborted = aborted; }
[[nodiscard]] virtual Vector<URL::URL> const& url_list() const { return m_url_list; }
[[nodiscard]] virtual Vector<URL::URL>& url_list() { return m_url_list; }
void set_url_list(Vector<URL::URL> url_list) { m_url_list = move(url_list); }
[[nodiscard]] virtual Status status() const { return m_status; }
void set_status(Status status) { m_status = status; }
[[nodiscard]] virtual ReadonlyBytes status_message() const { return m_status_message; }
void set_status_message(ByteBuffer status_message) { m_status_message = move(status_message); }
[[nodiscard]] virtual JS::NonnullGCPtr<HeaderList> header_list() const { return m_header_list; }
void set_header_list(JS::NonnullGCPtr<HeaderList> header_list) { m_header_list = header_list; }
[[nodiscard]] virtual JS::GCPtr<Body> const& body() const { return m_body; }
[[nodiscard]] virtual JS::GCPtr<Body>& body() { return m_body; }
void set_body(JS::GCPtr<Body> body) { m_body = move(body); }
[[nodiscard]] virtual Optional<CacheState> const& cache_state() const { return m_cache_state; }
void set_cache_state(Optional<CacheState> cache_state) { m_cache_state = move(cache_state); }
[[nodiscard]] virtual Vector<ByteBuffer> const& cors_exposed_header_name_list() const { return m_cors_exposed_header_name_list; }
void set_cors_exposed_header_name_list(Vector<ByteBuffer> cors_exposed_header_name_list) { m_cors_exposed_header_name_list = move(cors_exposed_header_name_list); }
[[nodiscard]] virtual bool range_requested() const { return m_range_requested; }
void set_range_requested(bool range_requested) { m_range_requested = range_requested; }
[[nodiscard]] virtual bool request_includes_credentials() const { return m_request_includes_credentials; }
void set_request_includes_credentials(bool request_includes_credentials) { m_request_includes_credentials = request_includes_credentials; }
[[nodiscard]] virtual bool timing_allow_passed() const { return m_timing_allow_passed; }
void set_timing_allow_passed(bool timing_allow_passed) { m_timing_allow_passed = timing_allow_passed; }
[[nodiscard]] virtual BodyInfo const& body_info() const { return m_body_info; }
void set_body_info(BodyInfo body_info) { m_body_info = body_info; }
[[nodiscard]] bool has_cross_origin_redirects() const { return m_has_cross_origin_redirects; }
void set_has_cross_origin_redirects(bool has_cross_origin_redirects) { m_has_cross_origin_redirects = has_cross_origin_redirects; }
[[nodiscard]] bool is_aborted_network_error() const;
[[nodiscard]] bool is_network_error() const;
[[nodiscard]] Optional<URL::URL const&> url() const;
[[nodiscard]] ErrorOr<Optional<URL::URL>> location_url(Optional<String> const& request_fragment) const;
[[nodiscard]] JS::NonnullGCPtr<Response> clone(JS::Realm&) const;
[[nodiscard]] JS::NonnullGCPtr<Response> unsafe_response();
[[nodiscard]] bool is_cors_cross_origin() const;
[[nodiscard]] bool is_fresh() const;
[[nodiscard]] bool is_stale_while_revalidate() const;
[[nodiscard]] bool is_stale() const;
// Non-standard
[[nodiscard]] Optional<StringView> network_error_message() const;
protected:
explicit Response(JS::NonnullGCPtr<HeaderList>);
virtual void visit_edges(JS::Cell::Visitor&) override;
private:
// https://fetch.spec.whatwg.org/#concept-response-type
// A response has an associated type which is "basic", "cors", "default", "error", "opaque", or "opaqueredirect". Unless stated otherwise, it is "default".
Type m_type { Type::Default };
// https://fetch.spec.whatwg.org/#concept-response-aborted
// A response can have an associated aborted flag, which is initially unset.
bool m_aborted { false };
// https://fetch.spec.whatwg.org/#concept-response-url-list
// A response has an associated URL list (a list of zero or more URLs). Unless stated otherwise, it is the empty list.
Vector<URL::URL> m_url_list;
// https://fetch.spec.whatwg.org/#concept-response-status
// A response has an associated status, which is a status. Unless stated otherwise it is 200.
Status m_status { 200 };
// https://fetch.spec.whatwg.org/#concept-response-status-message
// A response has an associated status message. Unless stated otherwise it is the empty byte sequence.
ByteBuffer m_status_message;
// https://fetch.spec.whatwg.org/#concept-response-header-list
// A response has an associated header list (a header list). Unless stated otherwise it is empty.
JS::NonnullGCPtr<HeaderList> m_header_list;
// https://fetch.spec.whatwg.org/#concept-response-body
// A response has an associated body (null or a body). Unless stated otherwise it is null.
JS::GCPtr<Body> m_body;
// https://fetch.spec.whatwg.org/#concept-response-cache-state
// A response has an associated cache state (the empty string, "local", or "validated"). Unless stated otherwise, it is the empty string.
Optional<CacheState> m_cache_state;
// https://fetch.spec.whatwg.org/#concept-response-cors-exposed-header-name-list
// A response has an associated CORS-exposed header-name list (a list of zero or more header names). The list is empty unless otherwise specified.
Vector<ByteBuffer> m_cors_exposed_header_name_list;
// https://fetch.spec.whatwg.org/#concept-response-range-requested-flag
// A response has an associated range-requested flag, which is initially unset.
bool m_range_requested { false };
// https://fetch.spec.whatwg.org/#response-request-includes-credentials
// A response has an associated request-includes-credentials (a boolean), which is initially true.
bool m_request_includes_credentials { true };
// https://fetch.spec.whatwg.org/#concept-response-timing-allow-passed
// A response has an associated timing allow passed flag, which is initially unset.
bool m_timing_allow_passed { false };
// https://fetch.spec.whatwg.org/#concept-response-body-info
// A response has an associated body info (a response body info). Unless stated otherwise, it is a new response body info.
BodyInfo m_body_info;
// https://fetch.spec.whatwg.org/#response-service-worker-timing-info
// FIXME: A response has an associated service worker timing info (null or a service worker timing info), which is initially null.
// https://fetch.spec.whatwg.org/#response-has-cross-origin-redirects
// A response has an associated has-cross-origin-redirects (a boolean), which is initially false.
bool m_has_cross_origin_redirects { false };
// FIXME is the type correct?
u64 current_age() const;
u64 freshness_lifetime() const;
u64 stale_while_revalidate_lifetime() const;
// Non-standard
ByteBuffer m_method;
UnixDateTime m_response_time;
Optional<Variant<String, StringView>> m_network_error_message;
public:
[[nodiscard]] ByteBuffer const& method() const { return m_method; }
void set_method(ByteBuffer method) { m_method = method; }
};
// https://fetch.spec.whatwg.org/#concept-filtered-response
class FilteredResponse : public Response {
JS_CELL(FilteredResponse, Response);
public:
FilteredResponse(JS::NonnullGCPtr<Response>, JS::NonnullGCPtr<HeaderList>);
virtual ~FilteredResponse() = 0;
[[nodiscard]] virtual Type type() const override { return m_internal_response->type(); }
[[nodiscard]] virtual bool aborted() const override { return m_internal_response->aborted(); }
[[nodiscard]] virtual Vector<URL::URL> const& url_list() const override { return m_internal_response->url_list(); }
[[nodiscard]] virtual Vector<URL::URL>& url_list() override { return m_internal_response->url_list(); }
[[nodiscard]] virtual Status status() const override { return m_internal_response->status(); }
[[nodiscard]] virtual ReadonlyBytes status_message() const override { return m_internal_response->status_message(); }
[[nodiscard]] virtual JS::NonnullGCPtr<HeaderList> header_list() const override { return m_internal_response->header_list(); }
[[nodiscard]] virtual JS::GCPtr<Body> const& body() const override { return m_internal_response->body(); }
[[nodiscard]] virtual JS::GCPtr<Body>& body() override { return m_internal_response->body(); }
[[nodiscard]] virtual Optional<CacheState> const& cache_state() const override { return m_internal_response->cache_state(); }
[[nodiscard]] virtual Vector<ByteBuffer> const& cors_exposed_header_name_list() const override { return m_internal_response->cors_exposed_header_name_list(); }
[[nodiscard]] virtual bool range_requested() const override { return m_internal_response->range_requested(); }
[[nodiscard]] virtual bool request_includes_credentials() const override { return m_internal_response->request_includes_credentials(); }
[[nodiscard]] virtual bool timing_allow_passed() const override { return m_internal_response->timing_allow_passed(); }
[[nodiscard]] virtual BodyInfo const& body_info() const override { return m_internal_response->body_info(); }
[[nodiscard]] JS::NonnullGCPtr<Response> internal_response() const { return m_internal_response; }
protected:
virtual void visit_edges(JS::Cell::Visitor&) override;
private:
// https://fetch.spec.whatwg.org/#concept-internal-response
JS::NonnullGCPtr<Response> m_internal_response;
};
// https://fetch.spec.whatwg.org/#concept-filtered-response-basic
class BasicFilteredResponse final : public FilteredResponse {
JS_CELL(BasicFilteredResponse, FilteredResponse);
JS_DECLARE_ALLOCATOR(BasicFilteredResponse);
public:
[[nodiscard]] static JS::NonnullGCPtr<BasicFilteredResponse> create(JS::VM&, JS::NonnullGCPtr<Response>);
[[nodiscard]] virtual Type type() const override { return Type::Basic; }
[[nodiscard]] virtual JS::NonnullGCPtr<HeaderList> header_list() const override { return m_header_list; }
private:
BasicFilteredResponse(JS::NonnullGCPtr<Response>, JS::NonnullGCPtr<HeaderList>);
virtual void visit_edges(JS::Cell::Visitor&) override;
JS::NonnullGCPtr<HeaderList> m_header_list;
};
// https://fetch.spec.whatwg.org/#concept-filtered-response-cors
class CORSFilteredResponse final : public FilteredResponse {
JS_CELL(CORSFilteredResponse, FilteredResponse);
JS_DECLARE_ALLOCATOR(CORSFilteredResponse);
public:
[[nodiscard]] static JS::NonnullGCPtr<CORSFilteredResponse> create(JS::VM&, JS::NonnullGCPtr<Response>);
[[nodiscard]] virtual Type type() const override { return Type::CORS; }
[[nodiscard]] virtual JS::NonnullGCPtr<HeaderList> header_list() const override { return m_header_list; }
private:
CORSFilteredResponse(JS::NonnullGCPtr<Response>, JS::NonnullGCPtr<HeaderList>);
virtual void visit_edges(JS::Cell::Visitor&) override;
JS::NonnullGCPtr<HeaderList> m_header_list;
};
// https://fetch.spec.whatwg.org/#concept-filtered-response-opaque
class OpaqueFilteredResponse final : public FilteredResponse {
JS_CELL(OpaqueFilteredResponse, FilteredResponse);
JS_DECLARE_ALLOCATOR(OpaqueFilteredResponse);
public:
[[nodiscard]] static JS::NonnullGCPtr<OpaqueFilteredResponse> create(JS::VM&, JS::NonnullGCPtr<Response>);
[[nodiscard]] virtual Type type() const override { return Type::Opaque; }
[[nodiscard]] virtual Vector<URL::URL> const& url_list() const override { return m_url_list; }
[[nodiscard]] virtual Vector<URL::URL>& url_list() override { return m_url_list; }
[[nodiscard]] virtual Status status() const override { return 0; }
[[nodiscard]] virtual ReadonlyBytes status_message() const override { return {}; }
[[nodiscard]] virtual JS::NonnullGCPtr<HeaderList> header_list() const override { return m_header_list; }
[[nodiscard]] virtual JS::GCPtr<Body> const& body() const override { return m_body; }
[[nodiscard]] virtual JS::GCPtr<Body>& body() override { return m_body; }
private:
OpaqueFilteredResponse(JS::NonnullGCPtr<Response>, JS::NonnullGCPtr<HeaderList>);
virtual void visit_edges(JS::Cell::Visitor&) override;
Vector<URL::URL> m_url_list;
JS::NonnullGCPtr<HeaderList> m_header_list;
JS::GCPtr<Body> m_body;
};
// https://fetch.spec.whatwg.org/#concept-filtered-response-opaque-redirect
class OpaqueRedirectFilteredResponse final : public FilteredResponse {
JS_CELL(OpaqueRedirectFilteredResponse, FilteredResponse);
JS_DECLARE_ALLOCATOR(OpaqueRedirectFilteredResponse);
public:
[[nodiscard]] static JS::NonnullGCPtr<OpaqueRedirectFilteredResponse> create(JS::VM&, JS::NonnullGCPtr<Response>);
[[nodiscard]] virtual Type type() const override { return Type::OpaqueRedirect; }
[[nodiscard]] virtual Status status() const override { return 0; }
[[nodiscard]] virtual ReadonlyBytes status_message() const override { return {}; }
[[nodiscard]] virtual JS::NonnullGCPtr<HeaderList> header_list() const override { return m_header_list; }
[[nodiscard]] virtual JS::GCPtr<Body> const& body() const override { return m_body; }
[[nodiscard]] virtual JS::GCPtr<Body>& body() override { return m_body; }
private:
OpaqueRedirectFilteredResponse(JS::NonnullGCPtr<Response>, JS::NonnullGCPtr<HeaderList>);
virtual void visit_edges(JS::Cell::Visitor&) override;
JS::NonnullGCPtr<HeaderList> m_header_list;
JS::GCPtr<Body> m_body;
};
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/AnyOf.h>
#include <AK/Array.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Statuses.h>
namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#null-body-status
bool is_null_body_status(Status status)
{
// A null body status is a status that is 101, 103, 204, 205, or 304.
return any_of(Array<Status, 5> { 101, 103, 204, 205, 304 }, [&](auto redirect_status) {
return status == redirect_status;
});
}
// https://fetch.spec.whatwg.org/#ok-status
bool is_ok_status(Status status)
{
// An ok status is a status in the range 200 to 299, inclusive.
return status >= 200 && status <= 299;
}
// https://fetch.spec.whatwg.org/#redirect-status
bool is_redirect_status(Status status)
{
// A redirect status is a status that is 301, 302, 303, 307, or 308.
return any_of(Array<Status, 5> { 301, 302, 303, 307, 308 }, [&](auto redirect_status) {
return status == redirect_status;
});
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-status
// A status is an integer in the range 0 to 999, inclusive.
using Status = u16;
[[nodiscard]] bool is_null_body_status(Status);
[[nodiscard]] bool is_ok_status(Status);
[[nodiscard]] bool is_redirect_status(Status);
}