diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index c27d9d957f1..bd7f2dcbae3 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -262,7 +262,7 @@ ErrorOr Application::initialize(Main::Arguments const& arguments) m_request_server_options = { .certificates = move(certificates), - .enable_http_disk_cache = enable_http_disk_cache ? EnableHTTPDiskCache::Yes : EnableHTTPDiskCache::No, + .http_disk_cache_mode = enable_http_disk_cache ? HTTPDiskCacheMode::Enabled : HTTPDiskCacheMode::Disabled, }; m_web_content_options = { diff --git a/Libraries/LibWebView/HelperProcess.cpp b/Libraries/LibWebView/HelperProcess.cpp index a72116ae2e3..e7be431c165 100644 --- a/Libraries/LibWebView/HelperProcess.cpp +++ b/Libraries/LibWebView/HelperProcess.cpp @@ -213,8 +213,19 @@ ErrorOr> launch_request_server_process() for (auto const& certificate : request_server_options.certificates) arguments.append(ByteString::formatted("--certificate={}", certificate)); - if (request_server_options.enable_http_disk_cache == EnableHTTPDiskCache::Yes) - arguments.append("--enable-http-disk-cache"sv); + arguments.append("--http-disk-cache-mode"sv); + + switch (request_server_options.http_disk_cache_mode) { + case HTTPDiskCacheMode::Disabled: + arguments.append("disabled"sv); + break; + case HTTPDiskCacheMode::Enabled: + arguments.append("enabled"sv); + break; + case HTTPDiskCacheMode::Testing: + arguments.append("testing"sv); + break; + } if (auto server = mach_server_name(); server.has_value()) { arguments.append("--mach-server-name"sv); diff --git a/Libraries/LibWebView/Options.h b/Libraries/LibWebView/Options.h index 15a6ff9917d..f478adbcf1d 100644 --- a/Libraries/LibWebView/Options.h +++ b/Libraries/LibWebView/Options.h @@ -93,14 +93,15 @@ struct BrowserOptions { EnableContentFilter enable_content_filter { EnableContentFilter::Yes }; }; -enum class EnableHTTPDiskCache { - No, - Yes, +enum class HTTPDiskCacheMode { + Disabled, + Enabled, + Testing, }; struct RequestServerOptions { Vector certificates; - EnableHTTPDiskCache enable_http_disk_cache { EnableHTTPDiskCache::No }; + HTTPDiskCacheMode http_disk_cache_mode { HTTPDiskCacheMode::Disabled }; }; enum class IsLayoutTestMode { diff --git a/Services/RequestServer/Cache/DiskCache.cpp b/Services/RequestServer/Cache/DiskCache.cpp index eb15e9902bf..4682b223237 100644 --- a/Services/RequestServer/Cache/DiskCache.cpp +++ b/Services/RequestServer/Cache/DiskCache.cpp @@ -15,21 +15,26 @@ namespace RequestServer { static constexpr auto INDEX_DATABASE = "INDEX"sv; -ErrorOr DiskCache::create() +ErrorOr DiskCache::create(Mode mode) { - auto cache_directory = LexicalPath::join(Core::StandardPaths::cache_directory(), "Ladybird"sv, "Cache"sv); + auto cache_name = mode == Mode::Normal ? "Cache"sv : "TestCache"sv; + auto cache_directory = LexicalPath::join(Core::StandardPaths::cache_directory(), "Ladybird"sv, cache_name); auto database = TRY(Database::Database::create(cache_directory.string(), INDEX_DATABASE)); auto index = TRY(CacheIndex::create(database)); - return DiskCache { move(database), move(cache_directory), move(index) }; + return DiskCache { mode, move(database), move(cache_directory), move(index) }; } -DiskCache::DiskCache(NonnullRefPtr database, LexicalPath cache_directory, CacheIndex index) - : m_database(move(database)) +DiskCache::DiskCache(Mode mode, NonnullRefPtr database, LexicalPath cache_directory, CacheIndex index) + : m_mode(mode) + , m_database(move(database)) , m_cache_directory(move(cache_directory)) , m_index(move(index)) { + // Start with a clean slate in test mode. + if (m_mode == Mode::Testing) + remove_entries_accessed_since(UnixDateTime::earliest()); } Variant, DiskCache::CacheHasOpenEntry> DiskCache::create_entry(Request& request) @@ -37,6 +42,11 @@ Variant, DiskCache::CacheHasOpenEntry> DiskCache::cr if (!is_cacheable(request.method())) return Optional {}; + if (m_mode == Mode::Testing) { + if (!request.request_headers().contains(TEST_CACHE_ENABLED_HEADER)) + return Optional {}; + } + auto serialized_url = serialize_url_for_cache_storage(request.url()); auto cache_key = create_cache_key(serialized_url, request.method()); diff --git a/Services/RequestServer/Cache/DiskCache.h b/Services/RequestServer/Cache/DiskCache.h index 91aa2e3694f..a9a2182dfa4 100644 --- a/Services/RequestServer/Cache/DiskCache.h +++ b/Services/RequestServer/Cache/DiskCache.h @@ -21,7 +21,16 @@ namespace RequestServer { class DiskCache { public: - static ErrorOr create(); + enum class Mode { + Normal, + + // In test mode, we only enable caching of responses on a per-request basis, signified by a request header. The + // response headers will include some status on how the request was handled. + Testing, + }; + static ErrorOr create(Mode); + + Mode mode() const { return m_mode; } struct CacheHasOpenEntry { }; Variant, CacheHasOpenEntry> create_entry(Request&); @@ -35,7 +44,7 @@ public: void cache_entry_closed(Badge, CacheEntry const&); private: - DiskCache(NonnullRefPtr, LexicalPath cache_directory, CacheIndex); + DiskCache(Mode, NonnullRefPtr, LexicalPath cache_directory, CacheIndex); enum class CheckReaderEntries { No, @@ -43,6 +52,8 @@ private: }; bool check_if_cache_has_open_entry(Request&, u64 cache_key, CheckReaderEntries); + Mode m_mode; + NonnullRefPtr m_database; HashMap, 1>> m_open_cache_entries; diff --git a/Services/RequestServer/Cache/Utilities.cpp b/Services/RequestServer/Cache/Utilities.cpp index f494458e905..41dc02f92b0 100644 --- a/Services/RequestServer/Cache/Utilities.cpp +++ b/Services/RequestServer/Cache/Utilities.cpp @@ -192,7 +192,7 @@ bool is_header_exempted_from_storage(StringView name) "Proxy-Connection"sv, "TE"sv, "Transfer-Encoding"sv, - "Upgrade"sv + "Upgrade"sv, // * Likewise, some fields' semantics require them to be removed before forwarding the message, and this MAY be // implemented by doing so before storage; see Section 7.6.1 of [HTTP] for some examples. @@ -204,7 +204,10 @@ bool is_header_exempted_from_storage(StringView name) // unless the cache incorporates the identity of the proxy into the cache key. Effectively, this is limited to // Proxy-Authenticate (Section 11.7.1 of [HTTP]), Proxy-Authentication-Info (Section 11.7.3 of [HTTP]), and // Proxy-Authorization (Section 11.7.2 of [HTTP]). - ); + + // AD-HOC: Exclude headers used only for testing. + TEST_CACHE_ENABLED_HEADER, + TEST_CACHE_STATUS_HEADER); } // https://httpwg.org/specs/rfc9111.html#heuristic.freshness diff --git a/Services/RequestServer/Cache/Utilities.h b/Services/RequestServer/Cache/Utilities.h index cf044afe73a..30bfd61d47d 100644 --- a/Services/RequestServer/Cache/Utilities.h +++ b/Services/RequestServer/Cache/Utilities.h @@ -15,6 +15,9 @@ namespace RequestServer { +constexpr inline auto TEST_CACHE_ENABLED_HEADER = "X-Ladybird-Enable-Disk-Cache"sv; +constexpr inline auto TEST_CACHE_STATUS_HEADER = "X-Ladybird-Disk-Cache-Status"sv; + String serialize_url_for_cache_storage(URL::URL const&); u64 create_cache_key(StringView url, StringView method); LexicalPath path_for_cache_key(LexicalPath const& cache_directory, u64 cache_key); diff --git a/Services/RequestServer/Request.cpp b/Services/RequestServer/Request.cpp index d3a2e0731f5..bc63d783c96 100644 --- a/Services/RequestServer/Request.cpp +++ b/Services/RequestServer/Request.cpp @@ -211,6 +211,9 @@ void Request::handle_initial_state() m_disk_cache->create_entry(*this).visit( [&](Optional cache_entry_writer) { m_cache_entry_writer = cache_entry_writer; + + if (!m_cache_entry_writer.has_value()) + m_cache_status = CacheStatus::NotCached; }, [&](DiskCache::CacheHasOpenEntry) { // If an existing entry is open for reading or writing, we must wait for it to complete. An entry being @@ -228,15 +231,13 @@ void Request::handle_initial_state() void Request::handle_read_cache_state() { - m_status_code = m_cache_entry_reader->status_code(); m_reason_phrase = m_cache_entry_reader->reason_phrase(); m_response_headers = m_cache_entry_reader->response_headers(); + m_cache_status = CacheStatus::ReadFromCache; if (inform_client_request_started().is_error()) return; - - m_client.async_headers_became_available(m_request_id, m_response_headers, m_status_code, m_reason_phrase); - m_sent_response_headers_to_client = true; + transfer_headers_to_client_if_needed(); m_cache_entry_reader->pipe_to( m_client_request_pipe->writer_fd(), @@ -556,13 +557,37 @@ void Request::transfer_headers_to_client_if_needed() if (exchange(m_sent_response_headers_to_client, true)) return; - m_status_code = acquire_status_code(); - m_client.async_headers_became_available(m_request_id, m_response_headers, m_status_code, m_reason_phrase); + if (m_cache_entry_reader.has_value()) + m_status_code = m_cache_entry_reader->status_code(); + else + m_status_code = acquire_status_code(); if (m_cache_entry_writer.has_value()) { - if (m_cache_entry_writer->write_status_and_reason(m_status_code, m_reason_phrase, m_response_headers).is_error()) + if (m_cache_entry_writer->write_status_and_reason(m_status_code, m_reason_phrase, m_response_headers).is_error()) { + m_cache_status = CacheStatus::NotCached; m_cache_entry_writer.clear(); + } else { + m_cache_status = CacheStatus::WrittenToCache; + } } + + if (m_disk_cache.has_value() && m_disk_cache->mode() == DiskCache::Mode::Testing) { + switch (m_cache_status) { + case CacheStatus::Unknown: + break; + case CacheStatus::NotCached: + m_response_headers.set(TEST_CACHE_STATUS_HEADER, "not-cached"sv); + break; + case CacheStatus::WrittenToCache: + m_response_headers.set(TEST_CACHE_STATUS_HEADER, "written-to-cache"sv); + break; + case CacheStatus::ReadFromCache: + m_response_headers.set(TEST_CACHE_STATUS_HEADER, "read-from-cache"sv); + break; + } + } + + m_client.async_headers_became_available(m_request_id, m_response_headers, m_status_code, m_reason_phrase); } ErrorOr Request::write_queued_bytes_without_blocking() diff --git a/Services/RequestServer/Request.h b/Services/RequestServer/Request.h index 7e178c33a60..01f840f23eb 100644 --- a/Services/RequestServer/Request.h +++ b/Services/RequestServer/Request.h @@ -53,6 +53,7 @@ public: URL::URL const& url() const { return m_url; } ByteString const& method() const { return m_method; } + HTTP::HeaderMap const& request_headers() const { return m_request_headers; } UnixDateTime request_start_time() const { return m_request_start_time; } void notify_request_unblocked(Badge); @@ -75,6 +76,13 @@ private: Error, // Any error occured during the request's lifetime. }; + enum class CacheStatus : u8 { + Unknown, + NotCached, + WrittenToCache, + ReadFromCache, + }; + Request( i32 request_id, Optional disk_cache, @@ -159,6 +167,7 @@ private: Optional m_cache_entry_reader; Optional m_cache_entry_writer; + CacheStatus m_cache_status { CacheStatus::Unknown }; Optional m_network_error; }; diff --git a/Services/RequestServer/main.cpp b/Services/RequestServer/main.cpp index 5ccc71b46fc..1e82f7fbae2 100644 --- a/Services/RequestServer/main.cpp +++ b/Services/RequestServer/main.cpp @@ -34,13 +34,13 @@ ErrorOr ladybird_main(Main::Arguments arguments) Vector certificates; StringView mach_server_name; - bool enable_http_disk_cache = false; + StringView http_disk_cache_mode; bool wait_for_debugger = false; Core::ArgsParser args_parser; args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate"); args_parser.add_option(mach_server_name, "Mach server name", "mach-server-name", 0, "mach_server_name"); - args_parser.add_option(enable_http_disk_cache, "Enable HTTP disk cache", "enable-http-disk-cache"); + args_parser.add_option(http_disk_cache_mode, "HTTP disk cache mode", "http-disk-cache-mode", 0, "mode"); args_parser.add_option(wait_for_debugger, "Wait for debugger", "wait-for-debugger"); args_parser.parse(arguments); @@ -58,8 +58,12 @@ ErrorOr ladybird_main(Main::Arguments arguments) Core::Platform::register_with_mach_server(mach_server_name); #endif - if (enable_http_disk_cache) { - if (auto cache = RequestServer::DiskCache::create(); cache.is_error()) + if (http_disk_cache_mode.is_one_of("enabled"sv, "testing"sv)) { + auto mode = http_disk_cache_mode == "enabled"sv + ? RequestServer::DiskCache::Mode::Normal + : RequestServer::DiskCache::Mode::Testing; + + if (auto cache = RequestServer::DiskCache::create(mode); cache.is_error()) warnln("Unable to create disk cache: {}", cache.error()); else RequestServer::g_disk_cache = cache.release_value(); diff --git a/Tests/LibWeb/test-web/Application.cpp b/Tests/LibWeb/test-web/Application.cpp index 4a32c7027a2..f07d14f3dda 100644 --- a/Tests/LibWeb/test-web/Application.cpp +++ b/Tests/LibWeb/test-web/Application.cpp @@ -57,11 +57,13 @@ void Application::create_platform_arguments(Core::ArgsParser& args_parser) }); } -void Application::create_platform_options(WebView::BrowserOptions& browser_options, WebView::RequestServerOptions&, WebView::WebContentOptions& web_content_options) +void Application::create_platform_options(WebView::BrowserOptions& browser_options, WebView::RequestServerOptions& request_server_options, WebView::WebContentOptions& web_content_options) { browser_options.headless_mode = WebView::HeadlessMode::Test; browser_options.disable_sql_database = WebView::DisableSQLDatabase::Yes; + request_server_options.http_disk_cache_mode = WebView::HTTPDiskCacheMode::Testing; + web_content_options.is_layout_test_mode = WebView::IsLayoutTestMode::Yes; // Allow window.open() to succeed for tests.