mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-10-19 07:33:20 +00:00
LibRequests+RequestServer: Begin implementing an HTTP disk cache
This adds a disk cache for HTTP responses received from the network. For now, we take a rather conservative approach to caching. We don't cache a response until we're 100% sure it is cacheable (there are heuristics we can implement in the future based on the absence of specific headers). The cache is broken into 2 categories of files: 1. An index file. This is a SQL database containing metadata about each cache entry (URL, timestamps, etc.). 2. Cache files. Each cached response is in its own file. The file is an amalgamation of all info needed to reconstruct an HTTP response. This includes the status code, headers, body, etc. A cache entry is created once we receive the headers for a response. The index, however, is not updated at this point. We stream the body into the cache entry as it is received. Once we've successfully cached the entire body, we create an index entry in the database. If any of these steps failed along the way, the cache entry is removed and the index is left untouched. Subsequent requests are checked for cache hits from the index. If a hit is found, we read just enough of the cache entry to inform WebContent of the status code and headers. The body of the response is piped to WC via syscalls, such that the transfer happens entirely in the kernel; no need to allocate the memory for the body in userspace (WC still allocates a buffer to hold the data, of course). If an error occurs while piping the body, we currently error out the request. There is a FIXME to switch to a network request. Cache hits are also validated for freshness before they are used. If a response has expired, we remove it and its index entry, and proceed with a network request.
This commit is contained in:
parent
411aed96ab
commit
3516a2344f
Notes:
github-actions[bot]
2025-10-14 11:41:51 +00:00
Author: https://github.com/trflynn89
Commit: 3516a2344f
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/6435
13 changed files with 1114 additions and 7 deletions
|
@ -21,6 +21,7 @@ enum class NetworkError {
|
|||
MalformedUrl,
|
||||
InvalidContentEncoding,
|
||||
RequestServerDied,
|
||||
CacheReadFailed,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
|
@ -47,6 +48,8 @@ constexpr StringView network_error_to_string(NetworkError network_error)
|
|||
return "Response could not be decoded with its Content-Encoding"sv;
|
||||
case NetworkError::RequestServerDied:
|
||||
return "RequestServer is currently unavailable"sv;
|
||||
case NetworkError::CacheReadFailed:
|
||||
return "RequestServer encountered an error reading a cached HTTP response"sv;
|
||||
case NetworkError::Unknown:
|
||||
return "An unexpected network error occurred"sv;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,10 @@ set(CMAKE_AUTORCC OFF)
|
|||
set(CMAKE_AUTOUIC OFF)
|
||||
|
||||
set(SOURCES
|
||||
Cache/CacheEntry.cpp
|
||||
Cache/CacheIndex.cpp
|
||||
Cache/DiskCache.cpp
|
||||
Cache/Utilities.cpp
|
||||
ConnectionFromClient.cpp
|
||||
WebSocketImplCurl.cpp
|
||||
)
|
||||
|
@ -33,7 +37,7 @@ target_include_directories(requestserverservice PRIVATE ${CMAKE_CURRENT_BINARY_D
|
|||
target_include_directories(requestserverservice PRIVATE ${LADYBIRD_SOURCE_DIR}/Services/)
|
||||
|
||||
target_link_libraries(RequestServer PRIVATE requestserverservice)
|
||||
target_link_libraries(requestserverservice PUBLIC LibCore LibDNS LibMain LibCrypto LibFileSystem LibIPC LibMain LibTLS LibWebSocket LibURL LibTextCodec LibThreading CURL::libcurl)
|
||||
target_link_libraries(requestserverservice PUBLIC LibCore LibDatabase LibDNS LibCrypto LibFileSystem LibIPC LibMain LibTLS LibWebSocket LibURL LibTextCodec LibThreading CURL::libcurl)
|
||||
target_link_libraries(requestserverservice PRIVATE OpenSSL::Crypto OpenSSL::SSL)
|
||||
|
||||
if (${CMAKE_SYSTEM_NAME} MATCHES "SunOS")
|
||||
|
|
346
Services/RequestServer/Cache/CacheEntry.cpp
Normal file
346
Services/RequestServer/Cache/CacheEntry.cpp
Normal file
|
@ -0,0 +1,346 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/JsonArray.h>
|
||||
#include <AK/JsonArraySerializer.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/JsonObjectSerializer.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/ScopeGuard.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibFileSystem/FileSystem.h>
|
||||
#include <RequestServer/Cache/CacheEntry.h>
|
||||
#include <RequestServer/Cache/CacheIndex.h>
|
||||
#include <RequestServer/Cache/DiskCache.h>
|
||||
#include <RequestServer/Cache/Utilities.h>
|
||||
|
||||
namespace RequestServer {
|
||||
|
||||
static LexicalPath path_for_cache_key(LexicalPath const& cache_directory, u64 cache_key)
|
||||
{
|
||||
return cache_directory.append(MUST(String::formatted("{:016x}", cache_key)));
|
||||
}
|
||||
|
||||
ErrorOr<CacheHeader> CacheHeader::read_from_stream(Stream& stream)
|
||||
{
|
||||
CacheHeader header;
|
||||
header.magic = TRY(stream.read_value<u32>());
|
||||
header.version = TRY(stream.read_value<u32>());
|
||||
header.url_size = TRY(stream.read_value<u32>());
|
||||
header.url_hash = TRY(stream.read_value<u32>());
|
||||
header.status_code = TRY(stream.read_value<u32>());
|
||||
header.reason_phrase_size = TRY(stream.read_value<u32>());
|
||||
header.reason_phrase_hash = TRY(stream.read_value<u32>());
|
||||
header.headers_size = TRY(stream.read_value<u32>());
|
||||
header.headers_hash = TRY(stream.read_value<u32>());
|
||||
return header;
|
||||
}
|
||||
|
||||
ErrorOr<void> CacheHeader::write_to_stream(Stream& stream) const
|
||||
{
|
||||
TRY(stream.write_value(magic));
|
||||
TRY(stream.write_value(version));
|
||||
TRY(stream.write_value(url_size));
|
||||
TRY(stream.write_value(url_hash));
|
||||
TRY(stream.write_value(status_code));
|
||||
TRY(stream.write_value(reason_phrase_size));
|
||||
TRY(stream.write_value(reason_phrase_hash));
|
||||
TRY(stream.write_value(headers_size));
|
||||
TRY(stream.write_value(headers_hash));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> CacheFooter::write_to_stream(Stream& stream) const
|
||||
{
|
||||
TRY(stream.write_value(data_size));
|
||||
TRY(stream.write_value(crc32));
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<CacheFooter> CacheFooter::read_from_stream(Stream& stream)
|
||||
{
|
||||
CacheFooter footer;
|
||||
footer.data_size = TRY(stream.read_value<u64>());
|
||||
footer.crc32 = TRY(stream.read_value<u32>());
|
||||
return footer;
|
||||
}
|
||||
|
||||
CacheEntry::CacheEntry(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, CacheHeader cache_header)
|
||||
: m_disk_cache(disk_cache)
|
||||
, m_index(index)
|
||||
, m_cache_key(cache_key)
|
||||
, m_url(move(url))
|
||||
, m_path(move(path))
|
||||
, m_cache_header(cache_header)
|
||||
{
|
||||
}
|
||||
|
||||
void CacheEntry::remove()
|
||||
{
|
||||
(void)FileSystem::remove(m_path.string(), FileSystem::RecursionMode::Disallowed);
|
||||
m_index.remove_entry(m_cache_key);
|
||||
}
|
||||
|
||||
void CacheEntry::close_and_destory_cache_entry()
|
||||
{
|
||||
m_disk_cache.cache_entry_closed({}, *this);
|
||||
}
|
||||
|
||||
ErrorOr<NonnullOwnPtr<CacheEntryWriter>> CacheEntryWriter::create(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, u32 status_code, Optional<String> reason_phrase, HTTP::HeaderMap const& headers, UnixDateTime request_time)
|
||||
{
|
||||
auto path = path_for_cache_key(disk_cache.cache_directory(), cache_key);
|
||||
|
||||
auto unbuffered_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
|
||||
auto file = TRY(Core::OutputBufferedFile::create(move(unbuffered_file)));
|
||||
|
||||
CacheHeader cache_header;
|
||||
|
||||
auto result = [&]() -> ErrorOr<void> {
|
||||
StringBuilder builder;
|
||||
auto headers_serializer = TRY(JsonArraySerializer<>::try_create(builder));
|
||||
|
||||
for (auto const& header : headers.headers()) {
|
||||
if (is_header_exempted_from_storage(header.name))
|
||||
continue;
|
||||
|
||||
auto header_serializer = TRY(headers_serializer.add_object());
|
||||
TRY(header_serializer.add("name"sv, header.name));
|
||||
TRY(header_serializer.add("value"sv, header.value));
|
||||
TRY(header_serializer.finish());
|
||||
}
|
||||
|
||||
TRY(headers_serializer.finish());
|
||||
|
||||
cache_header.url_size = url.byte_count();
|
||||
cache_header.url_hash = url.hash();
|
||||
|
||||
cache_header.status_code = status_code;
|
||||
cache_header.reason_phrase_size = reason_phrase.has_value() ? reason_phrase->byte_count() : 0;
|
||||
cache_header.reason_phrase_hash = reason_phrase.has_value() ? reason_phrase->hash() : 0;
|
||||
|
||||
auto serialized_headers = builder.string_view();
|
||||
cache_header.headers_size = serialized_headers.length();
|
||||
cache_header.headers_hash = serialized_headers.hash();
|
||||
|
||||
TRY(file->write_value(cache_header));
|
||||
TRY(file->write_until_depleted(url));
|
||||
if (reason_phrase.has_value())
|
||||
TRY(file->write_until_depleted(*reason_phrase));
|
||||
TRY(file->write_until_depleted(serialized_headers));
|
||||
|
||||
return {};
|
||||
}();
|
||||
|
||||
if (result.is_error()) {
|
||||
(void)FileSystem::remove(path.string(), FileSystem::RecursionMode::Disallowed);
|
||||
return result.release_error();
|
||||
}
|
||||
|
||||
return adopt_own(*new CacheEntryWriter { disk_cache, index, cache_key, move(url), path, move(file), cache_header, request_time });
|
||||
}
|
||||
|
||||
CacheEntryWriter::CacheEntryWriter(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, NonnullOwnPtr<Core::OutputBufferedFile> file, CacheHeader cache_header, UnixDateTime request_time)
|
||||
: CacheEntry(disk_cache, index, cache_key, move(url), move(path), cache_header)
|
||||
, m_file(move(file))
|
||||
, m_request_time(request_time)
|
||||
, m_response_time(UnixDateTime::now())
|
||||
{
|
||||
}
|
||||
|
||||
ErrorOr<void> CacheEntryWriter::write_data(ReadonlyBytes data)
|
||||
{
|
||||
if (auto result = m_file->write_until_depleted(data); result.is_error()) {
|
||||
dbgln("\033[31;1mUnable to write to cache entry for{}\033[0m {}: {}", m_url, result.error());
|
||||
|
||||
remove();
|
||||
close_and_destory_cache_entry();
|
||||
|
||||
return result.release_error();
|
||||
}
|
||||
|
||||
m_cache_footer.data_size += data.size();
|
||||
|
||||
// FIXME: Update the crc.
|
||||
|
||||
dbgln("\033[36;1mSaved {} bytes for\033[0m {}", data.size(), m_url);
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void> CacheEntryWriter::flush()
|
||||
{
|
||||
ScopeGuard guard { [&]() { close_and_destory_cache_entry(); } };
|
||||
|
||||
if (auto result = m_file->write_value(m_cache_footer); result.is_error()) {
|
||||
dbgln("\033[31;1mUnable to flush cache entry for{}\033[0m {}: {}", m_url, result.error());
|
||||
remove();
|
||||
|
||||
return result.release_error();
|
||||
}
|
||||
|
||||
m_index.create_entry(m_cache_key, m_url, m_cache_footer.data_size, m_request_time, m_response_time);
|
||||
|
||||
dbgln("\033[34;1mFinished caching\033[0m {} ({} bytes)", m_url, m_cache_footer.data_size);
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<NonnullOwnPtr<CacheEntryReader>> CacheEntryReader::create(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, u64 data_size)
|
||||
{
|
||||
auto path = path_for_cache_key(disk_cache.cache_directory(), cache_key);
|
||||
|
||||
auto file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Read));
|
||||
auto fd = file->fd();
|
||||
|
||||
CacheHeader cache_header;
|
||||
|
||||
String url;
|
||||
Optional<String> reason_phrase;
|
||||
HTTP::HeaderMap headers;
|
||||
|
||||
auto result = [&]() -> ErrorOr<void> {
|
||||
cache_header = TRY(file->read_value<CacheHeader>());
|
||||
|
||||
if (cache_header.magic != CacheHeader::CACHE_MAGIC)
|
||||
return Error::from_string_literal("Magic value mismatch");
|
||||
if (cache_header.version != CacheHeader::CACHE_VERSION)
|
||||
return Error::from_string_literal("Version mismatch");
|
||||
|
||||
url = TRY(String::from_stream(*file, cache_header.url_size));
|
||||
if (url.hash() != cache_header.url_hash)
|
||||
return Error::from_string_literal("URL hash mismatch");
|
||||
|
||||
if (cache_header.reason_phrase_size != 0) {
|
||||
reason_phrase = TRY(String::from_stream(*file, cache_header.reason_phrase_size));
|
||||
if (reason_phrase->hash() != cache_header.reason_phrase_hash)
|
||||
return Error::from_string_literal("Reason phrase hash mismatch");
|
||||
}
|
||||
|
||||
auto serialized_headers = TRY(String::from_stream(*file, cache_header.headers_size));
|
||||
if (serialized_headers.hash() != cache_header.headers_hash)
|
||||
return Error::from_string_literal("HTTP headers hash mismatch");
|
||||
|
||||
auto json_headers = TRY(JsonValue::from_string(serialized_headers));
|
||||
if (!json_headers.is_array())
|
||||
return Error::from_string_literal("Expected HTTP headers to be a JSON array");
|
||||
|
||||
TRY(json_headers.as_array().try_for_each([&](JsonValue const& header) -> ErrorOr<void> {
|
||||
if (!header.is_object())
|
||||
return Error::from_string_literal("Expected headers entry to be a JSON object");
|
||||
|
||||
auto name = header.as_object().get_string("name"sv);
|
||||
auto value = header.as_object().get_string("value"sv);
|
||||
|
||||
if (!name.has_value() || !value.has_value())
|
||||
return Error::from_string_literal("Missing/invalid data in headers entry");
|
||||
|
||||
headers.set(name->to_byte_string(), value->to_byte_string());
|
||||
return {};
|
||||
}));
|
||||
|
||||
return {};
|
||||
}();
|
||||
|
||||
if (result.is_error()) {
|
||||
(void)FileSystem::remove(path.string(), FileSystem::RecursionMode::Disallowed);
|
||||
return result.release_error();
|
||||
}
|
||||
|
||||
auto data_offset = sizeof(CacheHeader) + cache_header.url_size + cache_header.reason_phrase_size + cache_header.headers_size;
|
||||
|
||||
return adopt_own(*new CacheEntryReader { disk_cache, index, cache_key, move(url), move(path), move(file), fd, cache_header, move(reason_phrase), move(headers), data_offset, data_size });
|
||||
}
|
||||
|
||||
CacheEntryReader::CacheEntryReader(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, NonnullOwnPtr<Core::File> file, int fd, CacheHeader cache_header, Optional<String> reason_phrase, HTTP::HeaderMap header_map, u64 data_offset, u64 data_size)
|
||||
: CacheEntry(disk_cache, index, cache_key, move(url), move(path), cache_header)
|
||||
, m_file(move(file))
|
||||
, m_fd(fd)
|
||||
, m_reason_phrase(move(reason_phrase))
|
||||
, m_headers(move(header_map))
|
||||
, m_data_offset(data_offset)
|
||||
, m_data_size(data_size)
|
||||
{
|
||||
}
|
||||
|
||||
void CacheEntryReader::pipe_to(int pipe_fd, Function<void(u64)> on_complete, Function<void(u64)> on_error)
|
||||
{
|
||||
VERIFY(m_pipe_fd == -1);
|
||||
m_pipe_fd = pipe_fd;
|
||||
|
||||
m_on_pipe_complete = move(on_complete);
|
||||
m_on_pipe_error = move(on_error);
|
||||
|
||||
m_pipe_write_notifier = Core::Notifier::construct(m_pipe_fd, Core::NotificationType::Write);
|
||||
m_pipe_write_notifier->set_enabled(false);
|
||||
|
||||
m_pipe_write_notifier->on_activation = [this]() {
|
||||
m_pipe_write_notifier->set_enabled(false);
|
||||
pipe_without_blocking();
|
||||
};
|
||||
|
||||
pipe_without_blocking();
|
||||
}
|
||||
|
||||
void CacheEntryReader::pipe_without_blocking()
|
||||
{
|
||||
auto result = Core::System::transfer_file_through_pipe(m_fd, m_pipe_fd, m_data_offset + m_bytes_piped, m_data_size - m_bytes_piped);
|
||||
|
||||
if (result.is_error()) {
|
||||
if (result.error().code() != EAGAIN && result.error().code() != EWOULDBLOCK) {
|
||||
dbgln("\033[31;1mError transferring cache to pipe for\033[0m {}: {}", m_url, result.error());
|
||||
|
||||
if (m_on_pipe_error)
|
||||
m_on_pipe_error(m_bytes_piped);
|
||||
|
||||
close_and_destory_cache_entry();
|
||||
} else {
|
||||
m_pipe_write_notifier->set_enabled(true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
m_bytes_piped += result.value();
|
||||
|
||||
if (m_bytes_piped == m_data_size) {
|
||||
pipe_complete();
|
||||
return;
|
||||
}
|
||||
|
||||
pipe_without_blocking();
|
||||
}
|
||||
|
||||
void CacheEntryReader::pipe_complete()
|
||||
{
|
||||
if (auto result = read_and_validate_footer(); result.is_error()) {
|
||||
dbgln("\033[31;1mError validating cache entry for\033[0m {}: {}", m_url, result.error());
|
||||
remove();
|
||||
|
||||
if (m_on_pipe_error)
|
||||
m_on_pipe_error(m_bytes_piped);
|
||||
} else {
|
||||
m_index.update_last_access_time(m_cache_key);
|
||||
|
||||
if (m_on_pipe_complete)
|
||||
m_on_pipe_complete(m_bytes_piped);
|
||||
}
|
||||
|
||||
close_and_destory_cache_entry();
|
||||
}
|
||||
|
||||
ErrorOr<void> CacheEntryReader::read_and_validate_footer()
|
||||
{
|
||||
TRY(m_file->seek(m_data_offset + m_data_size, SeekMode::SetPosition));
|
||||
m_cache_footer = TRY(m_file->read_value<CacheFooter>());
|
||||
|
||||
if (m_cache_footer.data_size != m_data_size)
|
||||
return Error::from_string_literal("Invalid data size in footer");
|
||||
|
||||
// FIXME: Validate the crc.
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
130
Services/RequestServer/Cache/CacheEntry.h
Normal file
130
Services/RequestServer/Cache/CacheEntry.h
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Types.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibHTTP/HeaderMap.h>
|
||||
#include <RequestServer/Forward.h>
|
||||
|
||||
namespace RequestServer {
|
||||
|
||||
struct [[gnu::packed]] CacheHeader {
|
||||
static ErrorOr<CacheHeader> read_from_stream(Stream&);
|
||||
ErrorOr<void> write_to_stream(Stream&) const;
|
||||
|
||||
static constexpr auto CACHE_MAGIC = 0xcafef00du;
|
||||
static constexpr auto CACHE_VERSION = 1;
|
||||
|
||||
u32 magic { CACHE_MAGIC };
|
||||
u32 version { CACHE_VERSION };
|
||||
|
||||
u32 url_size { 0 };
|
||||
u32 url_hash { 0 };
|
||||
|
||||
u32 status_code { 0 };
|
||||
u32 reason_phrase_size { 0 };
|
||||
u32 reason_phrase_hash { 0 };
|
||||
|
||||
u32 headers_size { 0 };
|
||||
u32 headers_hash { 0 };
|
||||
};
|
||||
|
||||
struct [[gnu::packed]] CacheFooter {
|
||||
static ErrorOr<CacheFooter> read_from_stream(Stream&);
|
||||
ErrorOr<void> write_to_stream(Stream&) const;
|
||||
|
||||
u64 data_size { 0 };
|
||||
u32 crc32 { 0 };
|
||||
};
|
||||
|
||||
// A cache entry is an amalgamation of all information needed to reconstruct HTTP responses. It is created once we have
|
||||
// received the response headers for a request. The body is streamed into the entry as it is received. The cache format
|
||||
// on disk is:
|
||||
//
|
||||
// [CacheHeader][URL][ReasonPhrase][HttpHeaders][Data][CacheFooter]
|
||||
class CacheEntry {
|
||||
public:
|
||||
virtual ~CacheEntry() = default;
|
||||
|
||||
void remove();
|
||||
|
||||
protected:
|
||||
CacheEntry(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, CacheHeader);
|
||||
|
||||
void close_and_destory_cache_entry();
|
||||
|
||||
DiskCache& m_disk_cache;
|
||||
CacheIndex& m_index;
|
||||
|
||||
u64 m_cache_key { 0 };
|
||||
|
||||
String m_url;
|
||||
LexicalPath m_path;
|
||||
|
||||
CacheHeader m_cache_header;
|
||||
CacheFooter m_cache_footer;
|
||||
};
|
||||
|
||||
class CacheEntryWriter : public CacheEntry {
|
||||
public:
|
||||
static ErrorOr<NonnullOwnPtr<CacheEntryWriter>> create(DiskCache&, CacheIndex&, u64 cache_key, String url, u32 status_code, Optional<String> reason_phrase, HTTP::HeaderMap const&, UnixDateTime request_time);
|
||||
virtual ~CacheEntryWriter() override = default;
|
||||
|
||||
ErrorOr<void> write_data(ReadonlyBytes);
|
||||
ErrorOr<void> flush();
|
||||
|
||||
private:
|
||||
CacheEntryWriter(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, NonnullOwnPtr<Core::OutputBufferedFile>, CacheHeader, UnixDateTime request_time);
|
||||
|
||||
NonnullOwnPtr<Core::OutputBufferedFile> m_file;
|
||||
|
||||
UnixDateTime m_request_time;
|
||||
UnixDateTime m_response_time;
|
||||
};
|
||||
|
||||
class CacheEntryReader : public CacheEntry {
|
||||
public:
|
||||
static ErrorOr<NonnullOwnPtr<CacheEntryReader>> create(DiskCache&, CacheIndex&, u64 cache_key, u64 data_size);
|
||||
virtual ~CacheEntryReader() override = default;
|
||||
|
||||
void pipe_to(int pipe_fd, Function<void(u64 bytes_piped)> on_complete, Function<void(u64 bytes_piped)> on_error);
|
||||
|
||||
u32 status_code() const { return m_cache_header.status_code; }
|
||||
Optional<String> const& reason_phrase() const { return m_reason_phrase; }
|
||||
HTTP::HeaderMap const& headers() const { return m_headers; }
|
||||
|
||||
private:
|
||||
CacheEntryReader(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, NonnullOwnPtr<Core::File>, int fd, CacheHeader, Optional<String> reason_phrase, HTTP::HeaderMap, u64 data_offset, u64 data_size);
|
||||
|
||||
void pipe_without_blocking();
|
||||
void pipe_complete();
|
||||
|
||||
ErrorOr<void> read_and_validate_footer();
|
||||
|
||||
NonnullOwnPtr<Core::File> m_file;
|
||||
int m_fd { -1 };
|
||||
|
||||
RefPtr<Core::Notifier> m_pipe_write_notifier;
|
||||
int m_pipe_fd { -1 };
|
||||
|
||||
Function<void(u64)> m_on_pipe_complete;
|
||||
Function<void(u64)> m_on_pipe_error;
|
||||
u64 m_bytes_piped { 0 };
|
||||
|
||||
Optional<String> m_reason_phrase;
|
||||
HTTP::HeaderMap m_headers;
|
||||
|
||||
u64 const m_data_offset { 0 };
|
||||
u64 const m_data_size { 0 };
|
||||
};
|
||||
|
||||
}
|
99
Services/RequestServer/Cache/CacheIndex.cpp
Normal file
99
Services/RequestServer/Cache/CacheIndex.cpp
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <RequestServer/Cache/CacheIndex.h>
|
||||
|
||||
namespace RequestServer {
|
||||
|
||||
ErrorOr<CacheIndex> CacheIndex::create(Database::Database& database)
|
||||
{
|
||||
auto create_table = TRY(database.prepare_statement(R"#(
|
||||
CREATE TABLE IF NOT EXISTS CacheIndex (
|
||||
cache_key INTEGER,
|
||||
url TEXT,
|
||||
data_size INTEGER,
|
||||
request_time INTEGER,
|
||||
response_time INTEGER,
|
||||
last_access_time INTEGER,
|
||||
PRIMARY KEY(cache_key)
|
||||
);)#"sv));
|
||||
database.execute_statement(create_table, {});
|
||||
|
||||
Statements statements {};
|
||||
statements.insert_entry = TRY(database.prepare_statement("INSERT OR REPLACE INTO CacheIndex VALUES (?, ?, ?, ?, ?, ?);"sv));
|
||||
statements.remove_entry = TRY(database.prepare_statement("DELETE FROM CacheIndex WHERE cache_key = ?;"sv));
|
||||
statements.select_entry = TRY(database.prepare_statement("SELECT * FROM CacheIndex WHERE cache_key = ?;"sv));
|
||||
statements.update_last_access_time = TRY(database.prepare_statement("UPDATE CacheIndex SET last_access_time = ? WHERE cache_key = ?;"sv));
|
||||
|
||||
return CacheIndex { database, statements };
|
||||
}
|
||||
|
||||
CacheIndex::CacheIndex(Database::Database& database, Statements statements)
|
||||
: m_database(database)
|
||||
, m_statements(statements)
|
||||
{
|
||||
}
|
||||
|
||||
void CacheIndex::create_entry(u64 cache_key, String url, u64 data_size, UnixDateTime request_time, UnixDateTime response_time)
|
||||
{
|
||||
auto now = UnixDateTime::now();
|
||||
|
||||
Entry entry {
|
||||
.cache_key = cache_key,
|
||||
.url = move(url),
|
||||
.data_size = data_size,
|
||||
.request_time = request_time,
|
||||
.response_time = response_time,
|
||||
.last_access_time = now,
|
||||
};
|
||||
|
||||
m_database.execute_statement(m_statements.insert_entry, {}, entry.cache_key, entry.url, entry.data_size, entry.request_time, entry.response_time, entry.last_access_time);
|
||||
m_entries.set(cache_key, move(entry));
|
||||
}
|
||||
|
||||
void CacheIndex::remove_entry(u64 cache_key)
|
||||
{
|
||||
m_database.execute_statement(m_statements.remove_entry, {}, cache_key);
|
||||
m_entries.remove(cache_key);
|
||||
}
|
||||
|
||||
void CacheIndex::update_last_access_time(u64 cache_key)
|
||||
{
|
||||
auto entry = m_entries.get(cache_key);
|
||||
if (!entry.has_value())
|
||||
return;
|
||||
|
||||
auto now = UnixDateTime::now();
|
||||
|
||||
m_database.execute_statement(m_statements.update_last_access_time, {}, now, cache_key);
|
||||
entry->last_access_time = now;
|
||||
}
|
||||
|
||||
Optional<CacheIndex::Entry&> CacheIndex::find_entry(u64 cache_key)
|
||||
{
|
||||
if (auto entry = m_entries.get(cache_key); entry.has_value())
|
||||
return entry;
|
||||
|
||||
m_database.execute_statement(
|
||||
m_statements.select_entry, [&](auto statement_id) {
|
||||
int column = 0;
|
||||
|
||||
auto cache_key = m_database.result_column<u64>(statement_id, column++);
|
||||
auto url = m_database.result_column<String>(statement_id, column++);
|
||||
auto data_size = m_database.result_column<u64>(statement_id, column++);
|
||||
auto request_time = m_database.result_column<UnixDateTime>(statement_id, column++);
|
||||
auto response_time = m_database.result_column<UnixDateTime>(statement_id, column++);
|
||||
auto last_access_time = m_database.result_column<UnixDateTime>(statement_id, column++);
|
||||
|
||||
Entry entry { cache_key, move(url), data_size, request_time, response_time, last_access_time };
|
||||
m_entries.set(cache_key, move(entry));
|
||||
},
|
||||
cache_key);
|
||||
|
||||
return m_entries.get(cache_key);
|
||||
}
|
||||
|
||||
}
|
57
Services/RequestServer/Cache/CacheIndex.h
Normal file
57
Services/RequestServer/Cache/CacheIndex.h
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/Time.h>
|
||||
#include <AK/Types.h>
|
||||
#include <LibDatabase/Database.h>
|
||||
|
||||
namespace RequestServer {
|
||||
|
||||
// The cache index is a SQL database containing metadata about each cache entry. An entry in the index is created once
|
||||
// the entire cache entry has been successfully written to disk.
|
||||
class CacheIndex {
|
||||
struct Entry {
|
||||
u64 cache_key { 0 };
|
||||
|
||||
String url;
|
||||
u64 data_size { 0 };
|
||||
|
||||
UnixDateTime request_time;
|
||||
UnixDateTime response_time;
|
||||
UnixDateTime last_access_time;
|
||||
};
|
||||
|
||||
public:
|
||||
static ErrorOr<CacheIndex> create(Database::Database&);
|
||||
|
||||
void create_entry(u64 cache_key, String url, u64 data_size, UnixDateTime request_time, UnixDateTime response_time);
|
||||
void remove_entry(u64 cache_key);
|
||||
|
||||
Optional<Entry&> find_entry(u64 cache_key);
|
||||
|
||||
void update_last_access_time(u64 cache_key);
|
||||
|
||||
private:
|
||||
struct Statements {
|
||||
Database::StatementID insert_entry { 0 };
|
||||
Database::StatementID remove_entry { 0 };
|
||||
Database::StatementID select_entry { 0 };
|
||||
Database::StatementID update_last_access_time { 0 };
|
||||
};
|
||||
|
||||
CacheIndex(Database::Database&, Statements);
|
||||
|
||||
Database::Database& m_database;
|
||||
Statements m_statements;
|
||||
|
||||
HashMap<u32, Entry> m_entries;
|
||||
};
|
||||
|
||||
}
|
99
Services/RequestServer/Cache/DiskCache.cpp
Normal file
99
Services/RequestServer/Cache/DiskCache.cpp
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibCore/StandardPaths.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <RequestServer/Cache/DiskCache.h>
|
||||
#include <RequestServer/Cache/Utilities.h>
|
||||
|
||||
namespace RequestServer {
|
||||
|
||||
static constexpr auto INDEX_DATABASE = "INDEX"sv;
|
||||
|
||||
ErrorOr<DiskCache> DiskCache::create()
|
||||
{
|
||||
auto cache_directory = LexicalPath::join(Core::StandardPaths::cache_directory(), "Ladybird"sv, "Cache"sv);
|
||||
|
||||
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) };
|
||||
}
|
||||
|
||||
DiskCache::DiskCache(NonnullRefPtr<Database::Database> database, LexicalPath cache_directory, CacheIndex index)
|
||||
: m_database(move(database))
|
||||
, m_cache_directory(move(cache_directory))
|
||||
, m_index(move(index))
|
||||
{
|
||||
}
|
||||
|
||||
Optional<CacheEntryWriter&> DiskCache::create_entry(URL::URL const& url, StringView method, u32 status_code, Optional<String> reason_phrase, HTTP::HeaderMap const& headers, UnixDateTime request_time)
|
||||
{
|
||||
if (!is_cacheable(method, status_code, headers))
|
||||
return {};
|
||||
|
||||
if (auto freshness = calculate_freshness_lifetime(headers); freshness.is_negative() || freshness.is_zero())
|
||||
return {};
|
||||
|
||||
auto serialized_url = serialize_url_for_cache_storage(url);
|
||||
auto cache_key = create_cache_key(serialized_url, method);
|
||||
|
||||
auto cache_entry = CacheEntryWriter::create(*this, m_index, cache_key, move(serialized_url), status_code, move(reason_phrase), headers, request_time);
|
||||
if (cache_entry.is_error()) {
|
||||
dbgln("\033[31;1mUnable to create cache entry for\033[0m {}: {}", url, cache_entry.error());
|
||||
return {};
|
||||
}
|
||||
|
||||
dbgln("\033[32;1mCreated disk cache entry for\033[0m {}", url);
|
||||
|
||||
auto address = reinterpret_cast<FlatPtr>(cache_entry.value().ptr());
|
||||
m_open_cache_entries.set(address, cache_entry.release_value());
|
||||
|
||||
return static_cast<CacheEntryWriter&>(**m_open_cache_entries.get(address));
|
||||
}
|
||||
|
||||
Optional<CacheEntryReader&> DiskCache::open_entry(URL::URL const& url, StringView method)
|
||||
{
|
||||
auto serialized_url = serialize_url_for_cache_storage(url);
|
||||
auto cache_key = create_cache_key(serialized_url, method);
|
||||
|
||||
auto index_entry = m_index.find_entry(cache_key);
|
||||
if (!index_entry.has_value()) {
|
||||
dbgln("\033[35;1mNo disk cache entry for\033[0m {}", url);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto cache_entry = CacheEntryReader::create(*this, m_index, cache_key, index_entry->data_size);
|
||||
if (cache_entry.is_error()) {
|
||||
dbgln("\033[31;1mUnable to open cache entry for\033[0m {}: {}", url, cache_entry.error());
|
||||
m_index.remove_entry(cache_key);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto freshness_lifetime = calculate_freshness_lifetime(cache_entry.value()->headers());
|
||||
auto current_age = calculate_age(cache_entry.value()->headers(), index_entry->request_time, index_entry->response_time);
|
||||
|
||||
if (!is_response_fresh(freshness_lifetime, current_age)) {
|
||||
dbgln("\033[33;1mCache entry expired for\033[0m {} (lifetime={}s age={}s)", url, freshness_lifetime.to_seconds(), current_age.to_seconds());
|
||||
cache_entry.value()->remove();
|
||||
return {};
|
||||
}
|
||||
|
||||
dbgln("\033[32;1mOpened disk cache entry for\033[0m {} (lifetime={}s age={}s) ({} bytes)", url, freshness_lifetime.to_seconds(), current_age.to_seconds(), index_entry->data_size);
|
||||
|
||||
auto address = reinterpret_cast<FlatPtr>(cache_entry.value().ptr());
|
||||
m_open_cache_entries.set(address, cache_entry.release_value());
|
||||
|
||||
return static_cast<CacheEntryReader&>(**m_open_cache_entries.get(address));
|
||||
}
|
||||
|
||||
void DiskCache::cache_entry_closed(Badge<CacheEntry>, CacheEntry const& cache_entry)
|
||||
{
|
||||
auto address = reinterpret_cast<FlatPtr>(&cache_entry);
|
||||
m_open_cache_entries.remove(address);
|
||||
}
|
||||
|
||||
}
|
45
Services/RequestServer/Cache/DiskCache.h
Normal file
45
Services/RequestServer/Cache/DiskCache.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Time.h>
|
||||
#include <AK/Types.h>
|
||||
#include <LibDatabase/Database.h>
|
||||
#include <LibHTTP/HeaderMap.h>
|
||||
#include <LibURL/Forward.h>
|
||||
#include <RequestServer/Cache/CacheEntry.h>
|
||||
#include <RequestServer/Cache/CacheIndex.h>
|
||||
|
||||
namespace RequestServer {
|
||||
|
||||
class DiskCache {
|
||||
public:
|
||||
static ErrorOr<DiskCache> create();
|
||||
|
||||
Optional<CacheEntryWriter&> create_entry(URL::URL const&, StringView method, u32 status_code, Optional<String> reason_phrase, HTTP::HeaderMap const&, UnixDateTime request_time);
|
||||
Optional<CacheEntryReader&> open_entry(URL::URL const&, StringView method);
|
||||
|
||||
LexicalPath const& cache_directory() { return m_cache_directory; }
|
||||
|
||||
void cache_entry_closed(Badge<CacheEntry>, CacheEntry const&);
|
||||
|
||||
private:
|
||||
DiskCache(NonnullRefPtr<Database::Database>, LexicalPath cache_directory, CacheIndex);
|
||||
|
||||
NonnullRefPtr<Database::Database> m_database;
|
||||
|
||||
HashMap<FlatPtr, NonnullOwnPtr<CacheEntry>> m_open_cache_entries;
|
||||
|
||||
LexicalPath m_cache_directory;
|
||||
CacheIndex m_index;
|
||||
};
|
||||
|
||||
}
|
220
Services/RequestServer/Cache/Utilities.cpp
Normal file
220
Services/RequestServer/Cache/Utilities.cpp
Normal file
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibCrypto/Hash/SHA1.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <RequestServer/Cache/Utilities.h>
|
||||
|
||||
namespace RequestServer {
|
||||
|
||||
static Optional<StringView> extract_cache_control_directive(StringView cache_control, StringView directive)
|
||||
{
|
||||
Optional<StringView> result;
|
||||
|
||||
cache_control.for_each_split_view(","sv, SplitBehavior::Nothing, [&](StringView candidate) {
|
||||
if (!candidate.contains(directive, CaseSensitivity::CaseInsensitive))
|
||||
return IterationDecision::Continue;
|
||||
|
||||
auto index = candidate.find('=');
|
||||
if (!index.has_value())
|
||||
return IterationDecision::Continue;
|
||||
|
||||
result = candidate.substring_view(*index + 1);
|
||||
return IterationDecision::Break;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc9110.html#field.date
|
||||
static Optional<UnixDateTime> parse_http_date(Optional<ByteString const&> date)
|
||||
{
|
||||
// <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
|
||||
if (date.has_value())
|
||||
return UnixDateTime::parse("%a, %d %b %Y %T GMT"sv, *date, true);
|
||||
return {};
|
||||
}
|
||||
|
||||
String serialize_url_for_cache_storage(URL::URL const& url)
|
||||
{
|
||||
if (!url.fragment().has_value())
|
||||
return url.serialize();
|
||||
|
||||
auto sanitized = url;
|
||||
sanitized.set_fragment({});
|
||||
return sanitized.serialize();
|
||||
}
|
||||
|
||||
u64 create_cache_key(StringView url, StringView method)
|
||||
{
|
||||
auto hasher = Crypto::Hash::SHA1::create();
|
||||
hasher->update(url);
|
||||
hasher->update(method);
|
||||
|
||||
auto digest = hasher->digest();
|
||||
auto bytes = digest.bytes();
|
||||
|
||||
u64 result = 0;
|
||||
result |= static_cast<u64>(bytes[0]) << 56;
|
||||
result |= static_cast<u64>(bytes[1]) << 48;
|
||||
result |= static_cast<u64>(bytes[2]) << 40;
|
||||
result |= static_cast<u64>(bytes[3]) << 32;
|
||||
result |= static_cast<u64>(bytes[4]) << 24;
|
||||
result |= static_cast<u64>(bytes[5]) << 16;
|
||||
result |= static_cast<u64>(bytes[6]) << 8;
|
||||
result |= static_cast<u64>(bytes[7]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc9111.html#response.cacheability
|
||||
bool is_cacheable(StringView method, u32 status_code, HTTP::HeaderMap const& headers)
|
||||
{
|
||||
// A cache MUST NOT store a response to a request unless:
|
||||
|
||||
// * the request method is understood by the cache;
|
||||
if (!method.is_one_of("GET"sv, "HEAD"sv))
|
||||
return false;
|
||||
|
||||
// * the response status code is final (see Section 15 of [HTTP]);
|
||||
if (status_code < 200)
|
||||
return false;
|
||||
|
||||
auto cache_control = headers.get("Cache-Control"sv);
|
||||
if (!cache_control.has_value())
|
||||
return false;
|
||||
|
||||
// * if the response status code is 206 or 304, or the must-understand cache directive (see Section 5.2.2.3) is
|
||||
// present: the cache understands the response status code;
|
||||
|
||||
// * the no-store cache directive is not present in the response (see Section 5.2.2.5);
|
||||
if (cache_control->contains("no-store"sv, CaseSensitivity::CaseInsensitive))
|
||||
return false;
|
||||
|
||||
// * if the cache is shared: the private response directive is either not present or allows a shared cache to store
|
||||
// a modified response; see Section 5.2.2.7);
|
||||
|
||||
// * if the cache is shared: the Authorization header field is not present in the request (see Section 11.6.2 of
|
||||
// [HTTP]) or a response directive is present that explicitly allows shared caching (see Section 3.5); and
|
||||
|
||||
// * the response contains at least one of the following:
|
||||
// - a public response directive (see Section 5.2.2.9);
|
||||
// - a private response directive, if the cache is not shared (see Section 5.2.2.7);
|
||||
// - an Expires header field (see Section 5.3);
|
||||
// - a max-age response directive (see Section 5.2.2.1);
|
||||
// - if the cache is shared: an s-maxage response directive (see Section 5.2.2.10);
|
||||
// - a cache extension that allows it to be cached (see Section 5.2.3); or
|
||||
// - a status code that is defined as heuristically cacheable (see Section 4.2.2).
|
||||
|
||||
// FIXME: Implement cache revalidation.
|
||||
if (cache_control->contains("no-cache"sv, CaseSensitivity::CaseInsensitive))
|
||||
return false;
|
||||
if (cache_control->contains("revalidate"sv, CaseSensitivity::CaseInsensitive))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc9111.html#storing.fields
|
||||
bool is_header_exempted_from_storage(StringView name)
|
||||
{
|
||||
// Caches MUST include all received response header fields — including unrecognized ones — when storing a response;
|
||||
// this assures that new HTTP header fields can be successfully deployed. However, the following exceptions are made:
|
||||
return name.is_one_of_ignoring_ascii_case(
|
||||
// * The Connection header field and fields whose names are listed in it are required by Section 7.6.1 of [HTTP]
|
||||
// to be removed before forwarding the message. This MAY be implemented by doing so before storage.
|
||||
"Connection"sv,
|
||||
"Keep-Alive"sv,
|
||||
"Proxy-Connection"sv,
|
||||
"TE"sv,
|
||||
"Transfer-Encoding"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.
|
||||
|
||||
// * The no-cache (Section 5.2.2.4) and private (Section 5.2.2.7) cache directives can have arguments that
|
||||
// prevent storage of header fields by all caches and shared caches, respectively.
|
||||
|
||||
// * Header fields that are specific to the proxy that a cache uses when forwarding a request MUST NOT be stored,
|
||||
// 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]).
|
||||
);
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc9111.html#calculating.freshness.lifetime
|
||||
AK::Duration calculate_freshness_lifetime(HTTP::HeaderMap const& headers)
|
||||
{
|
||||
// A cache can calculate the freshness lifetime (denoted as freshness_lifetime) of a response by evaluating the
|
||||
// following rules and using the first match:
|
||||
|
||||
// * If the cache is shared and the s-maxage response directive (Section 5.2.2.10) is present, use its value, or
|
||||
|
||||
// * If the max-age response directive (Section 5.2.2.1) is present, use its value, or
|
||||
if (auto cache_control = headers.get("Cache-Control"sv); cache_control.has_value()) {
|
||||
if (auto max_age = extract_cache_control_directive(*cache_control, "max-age"sv); max_age.has_value()) {
|
||||
if (auto seconds = max_age->to_number<i64>(); seconds.has_value())
|
||||
return AK::Duration::from_seconds(*seconds);
|
||||
}
|
||||
}
|
||||
|
||||
// * 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
|
||||
if (auto expires = parse_http_date(headers.get("Expires"sv)); expires.has_value()) {
|
||||
auto date = parse_http_date(headers.get("Date"sv)).value_or_lazy_evaluated([]() {
|
||||
return UnixDateTime::now();
|
||||
});
|
||||
|
||||
return *expires - date;
|
||||
}
|
||||
|
||||
// * Otherwise, no explicit expiration time is present in the response. A heuristic freshness lifetime might be
|
||||
// applicable; see Section 4.2.2.
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc9111.html#age.calculations
|
||||
AK::Duration calculate_age(HTTP::HeaderMap const& headers, UnixDateTime request_time, UnixDateTime response_time)
|
||||
{
|
||||
// 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.
|
||||
AK::Duration age_value;
|
||||
|
||||
if (auto age = headers.get("Age"sv); age.has_value()) {
|
||||
if (auto seconds = age->to_number<i64>(); seconds.has_value())
|
||||
age_value = AK::Duration::from_seconds(*seconds);
|
||||
}
|
||||
|
||||
// The term "now" means the current value of this implementation's clock (Section 5.6.7 of [HTTP]).
|
||||
auto now = UnixDateTime::now();
|
||||
|
||||
// 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.
|
||||
auto date_value = parse_http_date(headers.get("Date"sv)).value_or(now);
|
||||
|
||||
auto apparent_age = max(0LL, (response_time - date_value).to_seconds());
|
||||
|
||||
auto response_delay = response_time - request_time;
|
||||
auto corrected_age_value = age_value + response_delay;
|
||||
|
||||
auto corrected_initial_age = max(apparent_age, corrected_age_value.to_seconds());
|
||||
|
||||
auto resident_time = (now - response_time).to_seconds();
|
||||
auto current_age = corrected_initial_age + resident_time;
|
||||
|
||||
return AK::Duration::from_seconds(current_age);
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc9111.html#expiration.model
|
||||
bool is_response_fresh(AK::Duration freshness_lifetime, AK::Duration current_age)
|
||||
{
|
||||
return freshness_lifetime > current_age;
|
||||
}
|
||||
|
||||
}
|
27
Services/RequestServer/Cache/Utilities.h
Normal file
27
Services/RequestServer/Cache/Utilities.h
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Time.h>
|
||||
#include <AK/Types.h>
|
||||
#include <LibHTTP/HeaderMap.h>
|
||||
#include <LibURL/Forward.h>
|
||||
|
||||
namespace RequestServer {
|
||||
|
||||
String serialize_url_for_cache_storage(URL::URL const&);
|
||||
u64 create_cache_key(StringView url, StringView method);
|
||||
|
||||
bool is_cacheable(StringView method, u32 status_code, HTTP::HeaderMap const&);
|
||||
bool is_header_exempted_from_storage(StringView name);
|
||||
|
||||
AK::Duration calculate_freshness_lifetime(HTTP::HeaderMap const&);
|
||||
AK::Duration calculate_age(HTTP::HeaderMap const&, UnixDateTime request_time, UnixDateTime response_time);
|
||||
bool is_response_fresh(AK::Duration freshness_lifetime, AK::Duration current_age);
|
||||
|
||||
}
|
|
@ -20,12 +20,15 @@
|
|||
#include <LibTextCodec/Decoder.h>
|
||||
#include <LibWebSocket/ConnectionInfo.h>
|
||||
#include <LibWebSocket/Message.h>
|
||||
#include <RequestServer/Cache/DiskCache.h>
|
||||
#include <RequestServer/ConnectionFromClient.h>
|
||||
#include <RequestServer/RequestClientEndpoint.h>
|
||||
|
||||
#ifdef AK_OS_WINDOWS
|
||||
// needed because curl.h includes winsock2.h
|
||||
# include <AK/Windows.h>
|
||||
#endif
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
namespace RequestServer {
|
||||
|
@ -42,6 +45,8 @@ static struct {
|
|||
bool validate_dnssec_locally = false;
|
||||
} g_dns_info;
|
||||
|
||||
Optional<DiskCache> g_disk_cache;
|
||||
|
||||
static WeakPtr<Resolver> s_resolver {};
|
||||
static NonnullRefPtr<Resolver> default_resolver()
|
||||
{
|
||||
|
@ -116,13 +121,17 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
|
|||
bool got_all_headers { false };
|
||||
bool is_connect_only { false };
|
||||
size_t downloaded_so_far { 0 };
|
||||
String url;
|
||||
URL::URL url;
|
||||
ByteString method;
|
||||
Optional<String> reason_phrase;
|
||||
ByteBuffer body;
|
||||
AllocatingMemoryStream send_buffer;
|
||||
NonnullRefPtr<Core::Notifier> write_notifier;
|
||||
bool done_fetching { false };
|
||||
|
||||
Optional<CacheEntryWriter&> cache_entry;
|
||||
UnixDateTime request_start_time;
|
||||
|
||||
ActiveRequest(ConnectionFromClient& client, CURLM* multi, CURL* easy, i32 request_id, int writer_fd)
|
||||
: multi(multi)
|
||||
, easy(easy)
|
||||
|
@ -130,6 +139,7 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
|
|||
, client(client)
|
||||
, writer_fd(writer_fd)
|
||||
, write_notifier(Core::Notifier::construct(writer_fd, Core::NotificationType::Write))
|
||||
, request_start_time(UnixDateTime::now())
|
||||
{
|
||||
write_notifier->set_enabled(false);
|
||||
write_notifier->on_activation = [this] {
|
||||
|
@ -163,6 +173,13 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
|
|||
return {};
|
||||
}
|
||||
|
||||
if (cache_entry.has_value()) {
|
||||
auto bytes_sent = bytes_to_send.span().slice(0, result.value());
|
||||
|
||||
if (cache_entry->write_data(bytes_sent).is_error())
|
||||
cache_entry.clear();
|
||||
}
|
||||
|
||||
MUST(send_buffer.discard(result.value()));
|
||||
write_notifier->set_enabled(!send_buffer.is_eof());
|
||||
if (send_buffer.is_eof() && done_fetching)
|
||||
|
@ -193,6 +210,9 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
|
|||
|
||||
for (auto* string_list : curl_string_lists)
|
||||
curl_slist_free_all(string_list);
|
||||
|
||||
if (cache_entry.has_value())
|
||||
(void)cache_entry->flush();
|
||||
}
|
||||
|
||||
void flush_headers_if_needed()
|
||||
|
@ -204,6 +224,9 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
|
|||
auto result = curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &http_status_code);
|
||||
VERIFY(result == CURLE_OK);
|
||||
client->async_headers_became_available(request_id, headers, http_status_code, reason_phrase);
|
||||
|
||||
if (g_disk_cache.has_value())
|
||||
cache_entry = g_disk_cache->create_entry(url, method, http_status_code, reason_phrase, headers, request_start_time);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -464,6 +487,33 @@ void ConnectionFromClient::start_request(i32, ByteString, URL::URL, HTTP::Header
|
|||
void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL::URL url, HTTP::HeaderMap request_headers, ByteBuffer request_body, Core::ProxyData proxy_data)
|
||||
{
|
||||
dbgln_if(REQUESTSERVER_DEBUG, "RequestServer: start_request({}, {})", request_id, url);
|
||||
|
||||
if (g_disk_cache.has_value()) {
|
||||
if (auto cache_entry = g_disk_cache->open_entry(url, method); cache_entry.has_value()) {
|
||||
auto fds = MUST(Core::System::pipe2(O_NONBLOCK));
|
||||
auto writer_fd = fds[1];
|
||||
auto reader_fd = fds[0];
|
||||
|
||||
async_request_started(request_id, IPC::File::adopt_fd(reader_fd));
|
||||
async_headers_became_available(request_id, cache_entry->headers(), cache_entry->status_code(), cache_entry->reason_phrase());
|
||||
|
||||
cache_entry->pipe_to(
|
||||
writer_fd,
|
||||
[this, request_id, writer_fd](auto bytes_sent) {
|
||||
// FIXME: Implement timing info for cache hits.
|
||||
async_request_finished(request_id, bytes_sent, {}, {});
|
||||
MUST(Core::System::close(writer_fd));
|
||||
},
|
||||
[this, request_id, writer_fd](auto bytes_sent) {
|
||||
// FIXME: We should switch to a network request automatically if reading from cache has failed.
|
||||
async_request_finished(request_id, bytes_sent, {}, Requests::NetworkError::CacheReadFailed);
|
||||
(void)Core::System::close(writer_fd);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto host = url.serialized_host().to_byte_string();
|
||||
|
||||
m_resolver->dns.lookup(host, DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA }, { .validate_dnssec_locally = g_dns_info.validate_dnssec_locally })
|
||||
|
@ -500,7 +550,8 @@ void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL:
|
|||
async_request_started(request_id, IPC::File::adopt_fd(reader_fd));
|
||||
|
||||
auto request = make<ActiveRequest>(*this, m_curl_multi, easy, request_id, writer_fd);
|
||||
request->url = url.to_string();
|
||||
request->url = url;
|
||||
request->method = method;
|
||||
|
||||
auto set_option = [easy](auto option, auto value) {
|
||||
auto result = curl_easy_setopt(easy, option, value);
|
||||
|
@ -760,8 +811,6 @@ Messages::RequestServer::SetCertificateResponse ConnectionFromClient::set_certif
|
|||
|
||||
void ConnectionFromClient::ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level)
|
||||
{
|
||||
auto const url_string_value = url.to_string();
|
||||
|
||||
if (cache_level == CacheLevel::CreateConnection) {
|
||||
auto* easy = curl_easy_init();
|
||||
if (!easy) {
|
||||
|
@ -781,11 +830,11 @@ void ConnectionFromClient::ensure_connection(URL::URL url, ::RequestServer::Cach
|
|||
auto connect_only_request_id = get_random<i32>();
|
||||
|
||||
auto request = make<ActiveRequest>(*this, m_curl_multi, easy, connect_only_request_id, 0);
|
||||
request->url = url_string_value;
|
||||
request->url = url;
|
||||
request->is_connect_only = true;
|
||||
|
||||
set_option(CURLOPT_PRIVATE, request.ptr());
|
||||
set_option(CURLOPT_URL, url_string_value.to_byte_string().characters());
|
||||
set_option(CURLOPT_URL, url.to_byte_string().characters());
|
||||
set_option(CURLOPT_PORT, url.port_or_default());
|
||||
set_option(CURLOPT_CONNECTTIMEOUT, s_connect_timeout_seconds);
|
||||
set_option(CURLOPT_CONNECT_ONLY, 1L);
|
||||
|
|
17
Services/RequestServer/Forward.h
Normal file
17
Services/RequestServer/Forward.h
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace RequestServer {
|
||||
|
||||
class CacheEntry;
|
||||
class CacheEntryReader;
|
||||
class CacheEntryWriter;
|
||||
class CacheIndex;
|
||||
class DiskCache;
|
||||
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
#include <LibCore/Process.h>
|
||||
#include <LibIPC/SingleServer.h>
|
||||
#include <LibMain/Main.h>
|
||||
#include <RequestServer/Cache/DiskCache.h>
|
||||
#include <RequestServer/ConnectionFromClient.h>
|
||||
|
||||
#if defined(AK_OS_MACOS)
|
||||
|
@ -23,6 +24,7 @@
|
|||
namespace RequestServer {
|
||||
|
||||
extern ByteString g_default_certificate_path;
|
||||
extern Optional<DiskCache> g_disk_cache;
|
||||
|
||||
}
|
||||
|
||||
|
@ -32,11 +34,13 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
|
|||
|
||||
Vector<ByteString> certificates;
|
||||
StringView mach_server_name;
|
||||
bool enable_http_disk_cache = false;
|
||||
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(wait_for_debugger, "Wait for debugger", "wait-for-debugger");
|
||||
args_parser.parse(arguments);
|
||||
|
||||
|
@ -54,6 +58,13 @@ ErrorOr<int> 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())
|
||||
warnln("Unable to create disk cache: {}", cache.error());
|
||||
else
|
||||
RequestServer::g_disk_cache = cache.release_value();
|
||||
}
|
||||
|
||||
auto client = TRY(IPC::take_over_accepted_client_from_system_server<RequestServer::ConnectionFromClient>());
|
||||
|
||||
return event_loop.exec();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue