| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "quiche/quic/tools/quic_memory_cache_backend.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/strings/match.h" |
| #include "absl/strings/numbers.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/synchronization/mutex.h" |
| #include "quiche/quic/core/http/spdy_utils.h" |
| #include "quiche/quic/platform/api/quic_bug_tracker.h" |
| #include "quiche/quic/platform/api/quic_logging.h" |
| #include "quiche/quic/tools/web_transport_test_visitors.h" |
| #include "quiche/common/platform/api/quiche_file_utils.h" |
| #include "quiche/common/quiche_text_utils.h" |
| |
| using quiche::HttpHeaderBlock; |
| using spdy::kV3LowestPriority; |
| |
| namespace quic { |
| |
| QuicMemoryCacheBackend::ResourceFile::ResourceFile(const std::string& file_name) |
| : file_name_(file_name) {} |
| |
| QuicMemoryCacheBackend::ResourceFile::~ResourceFile() = default; |
| |
| void QuicMemoryCacheBackend::ResourceFile::Read() { |
| std::optional<std::string> maybe_file_contents = |
| quiche::ReadFileContents(file_name_); |
| if (!maybe_file_contents) { |
| QUIC_LOG(DFATAL) << "Failed to read file for the memory cache backend: " |
| << file_name_; |
| return; |
| } |
| file_contents_ = *std::move(maybe_file_contents); |
| |
| // First read the headers. |
| for (size_t start = 0; start < file_contents_.length();) { |
| size_t pos = file_contents_.find('\n', start); |
| if (pos == std::string::npos) { |
| QUIC_LOG(DFATAL) << "Headers invalid or empty, ignoring: " << file_name_; |
| return; |
| } |
| size_t len = pos - start; |
| // Support both dos and unix line endings for convenience. |
| if (file_contents_[pos - 1] == '\r') { |
| len -= 1; |
| } |
| auto line = absl::string_view(file_contents_).substr(start, len); |
| start = pos + 1; |
| // Headers end with an empty line. |
| if (line.empty()) { |
| body_ = absl::string_view(file_contents_).substr(start); |
| break; |
| } |
| // Extract the status from the HTTP first line. |
| if (line.substr(0, 4) == "HTTP") { |
| pos = line.find(' '); |
| if (pos == std::string::npos) { |
| QUIC_LOG(DFATAL) << "Headers invalid or empty, ignoring: " |
| << file_name_; |
| return; |
| } |
| spdy_headers_[":status"] = line.substr(pos + 1, 3); |
| continue; |
| } |
| // Headers are "key: value". |
| pos = line.find(": "); |
| if (pos == std::string::npos) { |
| QUIC_LOG(DFATAL) << "Headers invalid or empty, ignoring: " << file_name_; |
| return; |
| } |
| spdy_headers_.AppendValueOrAddHeader( |
| quiche::QuicheTextUtils::ToLower(line.substr(0, pos)), |
| line.substr(pos + 2)); |
| } |
| |
| // The connection header is prohibited in HTTP/2. |
| spdy_headers_.erase("connection"); |
| |
| // Override the URL with the X-Original-Url header, if present. |
| if (auto it = spdy_headers_.find("x-original-url"); |
| it != spdy_headers_.end()) { |
| x_original_url_ = it->second; |
| HandleXOriginalUrl(); |
| } |
| } |
| |
| void QuicMemoryCacheBackend::ResourceFile::SetHostPathFromBase( |
| absl::string_view base) { |
| QUICHE_DCHECK(base[0] != '/') << base; |
| size_t path_start = base.find_first_of('/'); |
| if (path_start == absl::string_view::npos) { |
| host_ = std::string(base); |
| path_ = ""; |
| return; |
| } |
| |
| host_ = std::string(base.substr(0, path_start)); |
| size_t query_start = base.find_first_of(','); |
| if (query_start > 0) { |
| path_ = std::string(base.substr(path_start, query_start - 1)); |
| } else { |
| path_ = std::string(base.substr(path_start)); |
| } |
| } |
| |
| absl::string_view QuicMemoryCacheBackend::ResourceFile::RemoveScheme( |
| absl::string_view url) { |
| if (absl::StartsWith(url, "https://")) { |
| url.remove_prefix(8); |
| } else if (absl::StartsWith(url, "http://")) { |
| url.remove_prefix(7); |
| } |
| return url; |
| } |
| |
| void QuicMemoryCacheBackend::ResourceFile::HandleXOriginalUrl() { |
| absl::string_view url(x_original_url_); |
| SetHostPathFromBase(RemoveScheme(url)); |
| } |
| |
| const QuicBackendResponse* QuicMemoryCacheBackend::GetResponse( |
| absl::string_view host, absl::string_view path) const { |
| absl::WriterMutexLock lock(&response_mutex_); |
| |
| auto it = responses_.find(GetKey(host, path)); |
| if (it == responses_.end()) { |
| uint64_t ignored = 0; |
| if (generate_bytes_response_) { |
| if (absl::SimpleAtoi(path.substr(1), &ignored)) { |
| // The actual parsed length is ignored here and will be recomputed |
| // by the caller. |
| return generate_bytes_response_.get(); |
| } |
| } |
| QUIC_DVLOG(1) << "Get response for resource failed: host " << host |
| << " path " << path; |
| if (default_response_) { |
| return default_response_.get(); |
| } |
| return nullptr; |
| } |
| return it->second.get(); |
| } |
| |
| using SpecialResponseType = QuicBackendResponse::SpecialResponseType; |
| |
| void QuicMemoryCacheBackend::AddSimpleResponse(absl::string_view host, |
| absl::string_view path, |
| int response_code, |
| absl::string_view body) { |
| HttpHeaderBlock response_headers; |
| response_headers[":status"] = absl::StrCat(response_code); |
| response_headers["content-length"] = absl::StrCat(body.length()); |
| AddResponse(host, path, std::move(response_headers), body); |
| } |
| |
| void QuicMemoryCacheBackend::AddDefaultResponse(QuicBackendResponse* response) { |
| absl::WriterMutexLock lock(&response_mutex_); |
| default_response_.reset(response); |
| } |
| |
| void QuicMemoryCacheBackend::AddResponse(absl::string_view host, |
| absl::string_view path, |
| HttpHeaderBlock response_headers, |
| absl::string_view response_body) { |
| AddResponseImpl(host, path, QuicBackendResponse::REGULAR_RESPONSE, |
| std::move(response_headers), response_body, HttpHeaderBlock(), |
| std::vector<quiche::HttpHeaderBlock>()); |
| } |
| |
| void QuicMemoryCacheBackend::AddResponse(absl::string_view host, |
| absl::string_view path, |
| HttpHeaderBlock response_headers, |
| absl::string_view response_body, |
| HttpHeaderBlock response_trailers) { |
| AddResponseImpl(host, path, QuicBackendResponse::REGULAR_RESPONSE, |
| std::move(response_headers), response_body, |
| std::move(response_trailers), |
| std::vector<quiche::HttpHeaderBlock>()); |
| } |
| |
| bool QuicMemoryCacheBackend::SetResponseDelay(absl::string_view host, |
| absl::string_view path, |
| QuicTime::Delta delay) { |
| absl::WriterMutexLock lock(&response_mutex_); |
| auto it = responses_.find(GetKey(host, path)); |
| if (it == responses_.end()) return false; |
| |
| it->second->set_delay(delay); |
| return true; |
| } |
| |
| void QuicMemoryCacheBackend::AddResponseWithEarlyHints( |
| absl::string_view host, absl::string_view path, |
| quiche::HttpHeaderBlock response_headers, absl::string_view response_body, |
| const std::vector<quiche::HttpHeaderBlock>& early_hints) { |
| AddResponseImpl(host, path, QuicBackendResponse::REGULAR_RESPONSE, |
| std::move(response_headers), response_body, HttpHeaderBlock(), |
| early_hints); |
| } |
| |
| void QuicMemoryCacheBackend::AddSpecialResponse( |
| absl::string_view host, absl::string_view path, |
| SpecialResponseType response_type) { |
| AddResponseImpl(host, path, response_type, HttpHeaderBlock(), "", |
| HttpHeaderBlock(), std::vector<quiche::HttpHeaderBlock>()); |
| } |
| |
| void QuicMemoryCacheBackend::AddSpecialResponse( |
| absl::string_view host, absl::string_view path, |
| quiche::HttpHeaderBlock response_headers, absl::string_view response_body, |
| SpecialResponseType response_type) { |
| AddResponseImpl(host, path, response_type, std::move(response_headers), |
| response_body, HttpHeaderBlock(), |
| std::vector<quiche::HttpHeaderBlock>()); |
| } |
| |
| QuicMemoryCacheBackend::QuicMemoryCacheBackend() : cache_initialized_(false) {} |
| |
| bool QuicMemoryCacheBackend::InitializeBackend( |
| const std::string& cache_directory) { |
| if (cache_directory.empty()) { |
| QUIC_BUG(quic_bug_10932_1) << "cache_directory must not be empty."; |
| return false; |
| } |
| QUIC_LOG(INFO) |
| << "Attempting to initialize QuicMemoryCacheBackend from directory: " |
| << cache_directory; |
| std::vector<std::string> files; |
| if (!quiche::EnumerateDirectoryRecursively(cache_directory, files)) { |
| QUIC_BUG(QuicMemoryCacheBackend unreadable directory) |
| << "Can't read QuicMemoryCacheBackend directory: " << cache_directory; |
| return false; |
| } |
| for (const auto& filename : files) { |
| std::unique_ptr<ResourceFile> resource_file(new ResourceFile(filename)); |
| |
| // Tease apart filename into host and path. |
| std::string base(resource_file->file_name()); |
| // Transform windows path separators to URL path separators. |
| for (size_t i = 0; i < base.length(); ++i) { |
| if (base[i] == '\\') { |
| base[i] = '/'; |
| } |
| } |
| base.erase(0, cache_directory.length()); |
| if (base[0] == '/') { |
| base.erase(0, 1); |
| } |
| |
| resource_file->SetHostPathFromBase(base); |
| resource_file->Read(); |
| |
| AddResponse(resource_file->host(), resource_file->path(), |
| resource_file->spdy_headers().Clone(), resource_file->body()); |
| } |
| |
| cache_initialized_ = true; |
| return true; |
| } |
| |
| void QuicMemoryCacheBackend::GenerateDynamicResponses() { |
| absl::WriterMutexLock lock(&response_mutex_); |
| // Add a generate bytes response. |
| quiche::HttpHeaderBlock response_headers; |
| response_headers[":status"] = "200"; |
| generate_bytes_response_ = std::make_unique<QuicBackendResponse>(); |
| generate_bytes_response_->set_headers(std::move(response_headers)); |
| generate_bytes_response_->set_response_type( |
| QuicBackendResponse::GENERATE_BYTES); |
| } |
| |
| void QuicMemoryCacheBackend::EnableWebTransport() { |
| enable_webtransport_ = true; |
| } |
| |
| bool QuicMemoryCacheBackend::IsBackendInitialized() const { |
| return cache_initialized_; |
| } |
| |
| void QuicMemoryCacheBackend::FetchResponseFromBackend( |
| const HttpHeaderBlock& request_headers, const std::string& request_body, |
| QuicSimpleServerBackend::RequestHandler* quic_stream) { |
| const QuicBackendResponse* quic_response = nullptr; |
| // Find response in cache. If not found, send error response. |
| auto authority = request_headers.find(":authority"); |
| auto path_it = request_headers.find(":path"); |
| const absl::string_view* path = nullptr; |
| if (path_it != request_headers.end()) { |
| path = &path_it->second; |
| } |
| auto method = request_headers.find(":method"); |
| std::unique_ptr<QuicBackendResponse> echo_response; |
| if (path && *path == "/echo" && method != request_headers.end() && |
| method->second == "POST") { |
| echo_response = std::make_unique<QuicBackendResponse>(); |
| quiche::HttpHeaderBlock response_headers; |
| response_headers[":status"] = "200"; |
| echo_response->set_headers(std::move(response_headers)); |
| echo_response->set_body(request_body); |
| quic_response = echo_response.get(); |
| } else if (authority != request_headers.end() && path) { |
| quic_response = GetResponse(authority->second, *path); |
| } |
| |
| std::string request_url; |
| if (authority != request_headers.end()) { |
| request_url = std::string(authority->second); |
| } |
| if (path) { |
| request_url += std::string(*path); |
| } |
| QUIC_DVLOG(1) |
| << "Fetching QUIC response from backend in-memory cache for url " |
| << request_url; |
| quic_stream->OnResponseBackendComplete(quic_response); |
| } |
| |
| // The memory cache does not have a per-stream handler |
| void QuicMemoryCacheBackend::CloseBackendResponseStream( |
| QuicSimpleServerBackend::RequestHandler* /*quic_stream*/) {} |
| |
| QuicMemoryCacheBackend::WebTransportResponse |
| QuicMemoryCacheBackend::ProcessWebTransportRequest( |
| const quiche::HttpHeaderBlock& request_headers, |
| WebTransportSession* session) { |
| if (!SupportsWebTransport()) { |
| return QuicSimpleServerBackend::ProcessWebTransportRequest(request_headers, |
| session); |
| } |
| |
| auto path_it = request_headers.find(":path"); |
| if (path_it == request_headers.end()) { |
| WebTransportResponse response; |
| response.response_headers[":status"] = "400"; |
| return response; |
| } |
| absl::string_view path = path_it->second; |
| if (path == "/echo") { |
| WebTransportResponse response; |
| response.response_headers[":status"] = "200"; |
| response.visitor = |
| std::make_unique<EchoWebTransportSessionVisitor>(session); |
| return response; |
| } |
| |
| WebTransportResponse response; |
| response.response_headers[":status"] = "404"; |
| return response; |
| } |
| |
| QuicMemoryCacheBackend::~QuicMemoryCacheBackend() { |
| { |
| absl::WriterMutexLock lock(&response_mutex_); |
| responses_.clear(); |
| } |
| } |
| |
| void QuicMemoryCacheBackend::AddResponseImpl( |
| absl::string_view host, absl::string_view path, |
| SpecialResponseType response_type, HttpHeaderBlock response_headers, |
| absl::string_view response_body, HttpHeaderBlock response_trailers, |
| const std::vector<quiche::HttpHeaderBlock>& early_hints) { |
| absl::WriterMutexLock lock(&response_mutex_); |
| |
| QUICHE_DCHECK(!host.empty()) |
| << "Host must be populated, e.g. \"www.google.com\""; |
| std::string key = GetKey(host, path); |
| if (responses_.contains(key)) { |
| QUIC_BUG(quic_bug_10932_3) |
| << "Response for '" << key << "' already exists!"; |
| return; |
| } |
| auto new_response = std::make_unique<QuicBackendResponse>(); |
| new_response->set_response_type(response_type); |
| new_response->set_headers(std::move(response_headers)); |
| new_response->set_body(response_body); |
| new_response->set_trailers(std::move(response_trailers)); |
| for (auto& headers : early_hints) { |
| new_response->AddEarlyHints(headers); |
| } |
| QUIC_DVLOG(1) << "Add response with key " << key; |
| responses_[key] = std::move(new_response); |
| } |
| |
| std::string QuicMemoryCacheBackend::GetKey(absl::string_view host, |
| absl::string_view path) const { |
| std::string host_string = std::string(host); |
| size_t port = host_string.find(':'); |
| if (port != std::string::npos) |
| host_string = std::string(host_string.c_str(), port); |
| return host_string + std::string(path); |
| } |
| |
| } // namespace quic |