From 8da3914bdcd1e0af0452e95eb922484484dd2515 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Sat, 16 Jul 2022 17:55:29 +0100 Subject: [PATCH] LibWeb: Add definitions from '2.2.6. Responses' in the Fetch spec --- Userland/Libraries/LibWeb/CMakeLists.txt | 1 + .../Fetch/Infrastructure/HTTP/Responses.cpp | 153 +++++++++++ .../Fetch/Infrastructure/HTTP/Responses.h | 253 ++++++++++++++++++ Userland/Libraries/LibWeb/Forward.h | 1 + 4 files changed, 408 insertions(+) create mode 100644 Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.cpp create mode 100644 Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.h diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 21f1e062dc..d74b1b66f9 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -123,6 +123,7 @@ set(SOURCES Fetch/Infrastructure/HTTP/Headers.cpp Fetch/Infrastructure/HTTP/Methods.cpp Fetch/Infrastructure/HTTP/Requests.cpp + Fetch/Infrastructure/HTTP/Responses.cpp Fetch/Infrastructure/HTTP/Statuses.cpp FontCache.cpp Geometry/DOMRectList.cpp diff --git a/Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.cpp b/Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.cpp new file mode 100644 index 0000000000..2eb6ec4152 --- /dev/null +++ b/Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2022, Linus Groh + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace Web::Fetch { + +// 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. + +Response Response::aborted_network_error() +{ + auto response = network_error(); + response.set_aborted(true); + return response; +} + +Response Response::network_error() +{ + Response response; + response.set_status(0); + response.set_type(Type::Error); + VERIFY(!response.body().has_value()); + return response; +} + +// 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. + return m_type == Type::Error && m_aborted; +} + +// https://fetch.spec.whatwg.org/#concept-network-error +bool Response::is_network_error() const +{ + // A response whose type is "error" is known as a network error. + return m_type == Type::Error; +} + +// https://fetch.spec.whatwg.org/#concept-response-url +Optional Response::url() const +{ + // A response has an associated URL. It is a pointer to the last URL in response’s URL list and null if response’s URL list is empty. + if (m_url_list.is_empty()) + return {}; + return m_url_list.last(); +} + +// https://fetch.spec.whatwg.org/#concept-response-location-url +ErrorOr> Response::location_url(Optional 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 response’s status is not a redirect status, then return null. + if (!is_redirect_status(m_status)) + return Optional {}; + + // FIXME: 2. Let location be the result of extracting header list values given `Location` and response’s header list. + auto location_value = ByteBuffer {}; + + // 3. If location is a header value, then set location to the result of parsing location with response’s URL. + auto location = AK::URL { StringView { location_value } }; + if (!location.is_valid()) + return Error::from_string_view("Invalid 'Location' header URL"sv); + + // 4. If location is a URL whose fragment is null, then set location’s fragment to requestFragment. + if (location.fragment().is_null()) + location.set_fragment(request_fragment.value_or({})); + + // 5. Return location. + return location; +} + +FilteredResponse::FilteredResponse(Response& internal_response) + : m_internal_response(internal_response) +{ +} + +FilteredResponse::~FilteredResponse() +{ +} + +ErrorOr BasicFilteredResponse::create(Response& internal_response) +{ + // A basic filtered response is a filtered response whose type is "basic" and header list excludes + // any headers in internal response’s header list whose name is a forbidden response-header name. + HeaderList header_list; + for (auto const& header : internal_response.header_list()) { + if (!is_forbidden_response_header_name(header.name)) + TRY(header_list.append(header)); + } + + return BasicFilteredResponse(internal_response, move(header_list)); +} + +BasicFilteredResponse::BasicFilteredResponse(Response& internal_response, HeaderList header_list) + : FilteredResponse(internal_response) + , m_header_list(move(header_list)) +{ +} + +ErrorOr CORSFilteredResponse::create(Response& internal_response) +{ + // A CORS filtered response is a filtered response whose type is "cors" and header list excludes + // any headers in internal response’s header list whose name is not a CORS-safelisted response-header + // name, given internal response’s CORS-exposed header-name list. + Vector 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()); + + HeaderList header_list; + for (auto const& header : internal_response.header_list()) { + if (is_cors_safelisted_response_header_name(header.name, cors_exposed_header_name_list)) + TRY(header_list.append(header)); + } + + return CORSFilteredResponse(internal_response, move(header_list)); +} + +CORSFilteredResponse::CORSFilteredResponse(Response& internal_response, HeaderList header_list) + : FilteredResponse(internal_response) + , m_header_list(move(header_list)) +{ +} + +OpaqueFilteredResponse OpaqueFilteredResponse::create(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 OpaqueFilteredResponse(internal_response); +} + +OpaqueFilteredResponse::OpaqueFilteredResponse(Response& internal_response) + : FilteredResponse(internal_response) +{ +} + +OpaqueRedirectFilteredResponse OpaqueRedirectFilteredResponse::create(Response& internal_response) +{ + return OpaqueRedirectFilteredResponse(internal_response); +} + +OpaqueRedirectFilteredResponse::OpaqueRedirectFilteredResponse(Response& internal_response) + : FilteredResponse(internal_response) +{ +} + +} diff --git a/Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.h b/Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.h new file mode 100644 index 0000000000..58b21e4891 --- /dev/null +++ b/Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.h @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2022, Linus Groh + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Fetch { + +// https://fetch.spec.whatwg.org/#concept-response +class 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 }; + }; + + [[nodiscard]] static Response aborted_network_error(); + [[nodiscard]] static Response network_error(); + + Response() = default; + 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 const& url_list() const { return m_url_list; } + [[nodiscard]] Vector& url_list() { return m_url_list; } + void set_url_list(Vector 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 HeaderList const& header_list() const { return m_header_list; } + [[nodiscard]] HeaderList& header_list() { return m_header_list; } + void set_header_list(HeaderList header_list) { m_header_list = move(header_list); } + + [[nodiscard]] virtual Optional const& body() const { return m_body; } + [[nodiscard]] Optional& body() { return m_body; } + void set_body(Optional body) { m_body = move(body); } + + [[nodiscard]] virtual Optional const& cache_state() const { return m_cache_state; } + void set_cache_state(Optional cache_state) { m_cache_state = move(cache_state); } + + [[nodiscard]] virtual Vector const& cors_exposed_header_name_list() const { return m_cors_exposed_header_name_list; } + [[nodiscard]] Vector& cors_exposed_header_name_list() { return m_cors_exposed_header_name_list; } + void set_cors_exposed_header_name_list(Vector 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; } + [[nodiscard]] BodyInfo& body_info() { return m_body_info; } + void set_body_info(BodyInfo body_info) { m_body_info = body_info; } + + [[nodiscard]] bool is_aborted_network_error() const; + [[nodiscard]] bool is_network_error() const; + + [[nodiscard]] Optional url() const; + [[nodiscard]] ErrorOr> location_url(Optional const& request_fragment) const; + +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 }; + + // A response has an associated URL list (a list of zero or more URLs). Unless stated otherwise, it is the empty list. + Vector 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. + 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. + Optional 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 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 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/#concept-filtered-response +class FilteredResponse : protected Response { +public: + explicit FilteredResponse(Response&); + 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 const& url_list() const 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 HeaderList const& header_list() const override { return m_internal_response.header_list(); } + [[nodiscard]] virtual Optional const& body() const override { return m_internal_response.body(); } + [[nodiscard]] virtual Optional const& cache_state() const override { return m_internal_response.cache_state(); } + [[nodiscard]] virtual Vector 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]] Response const& internal_response() const { return m_internal_response; } + [[nodiscard]] Response& internal_response() { return m_internal_response; } + +protected: + // https://fetch.spec.whatwg.org/#concept-internal-response + Response& m_internal_response; +}; + +// https://fetch.spec.whatwg.org/#concept-filtered-response-basic +class BasicFilteredResponse final : public FilteredResponse { +public: + static ErrorOr create(Response&); + + [[nodiscard]] virtual Type type() const override { return Type::Basic; } + [[nodiscard]] virtual HeaderList const& header_list() const override { return m_header_list; } + +private: + BasicFilteredResponse(Response&, HeaderList); + + HeaderList m_header_list; +}; + +// https://fetch.spec.whatwg.org/#concept-filtered-response-cors +class CORSFilteredResponse final : public FilteredResponse { +public: + static ErrorOr create(Response&); + + [[nodiscard]] virtual Type type() const override { return Type::CORS; } + [[nodiscard]] virtual HeaderList const& header_list() const override { return m_header_list; } + +private: + CORSFilteredResponse(Response&, HeaderList); + + HeaderList m_header_list; +}; + +// https://fetch.spec.whatwg.org/#concept-filtered-response-opaque +class OpaqueFilteredResponse final : public FilteredResponse { +public: + static OpaqueFilteredResponse create(Response&); + + [[nodiscard]] virtual Type type() const override { return Type::Opaque; } + [[nodiscard]] virtual Vector const& url_list() const override { return m_url_list; } + [[nodiscard]] virtual Status status() const override { return 0; } + [[nodiscard]] virtual ReadonlyBytes status_message() const override { return {}; } + [[nodiscard]] virtual HeaderList const& header_list() const override { return m_header_list; } + [[nodiscard]] virtual Optional const& body() const override { return m_body; } + +private: + explicit OpaqueFilteredResponse(Response&); + + Vector m_url_list; + HeaderList m_header_list; + Optional m_body; +}; + +// https://fetch.spec.whatwg.org/#concept-filtered-response-opaque-redirect +class OpaqueRedirectFilteredResponse final : public FilteredResponse { +public: + static OpaqueRedirectFilteredResponse create(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 HeaderList const& header_list() const override { return m_header_list; } + [[nodiscard]] virtual Optional const& body() const override { return m_body; } + +private: + explicit OpaqueRedirectFilteredResponse(Response&); + + HeaderList m_header_list; + Optional m_body; +}; + +} diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 98b5cc2227..431d31d69e 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -174,6 +174,7 @@ class Body; struct Header; class HeaderList; class Request; +class Response; } namespace Web::Geometry {