| // Copyright 2025 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/masque/masque_ohttp_client.h" |
| |
| #include <optional> |
| #include <ostream> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/cleanup/cleanup.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/numbers.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_split.h" |
| #include "absl/strings/string_view.h" |
| #include "openssl/base.h" |
| #include "quiche/quic/masque/masque_connection_pool.h" |
| #include "quiche/quic/tools/quic_url.h" |
| #include "quiche/binary_http/binary_http_message.h" |
| #include "quiche/common/platform/api/quiche_logging.h" |
| #include "quiche/common/quiche_status_utils.h" |
| #include "quiche/common/quiche_text_utils.h" |
| #include "quiche/oblivious_http/buffers/oblivious_http_request.h" |
| #include "quiche/oblivious_http/buffers/oblivious_http_response.h" |
| #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" |
| #include "quiche/oblivious_http/oblivious_http_client.h" |
| |
| namespace quic { |
| |
| using ::quic::MasqueConnectionPool; |
| using ::quic::QuicUrl; |
| using ::quiche::BinaryHttpRequest; |
| using ::quiche::BinaryHttpResponse; |
| using ::quiche::ObliviousHttpClient; |
| using ::quiche::ObliviousHttpHeaderKeyConfig; |
| using ::quiche::ObliviousHttpKeyConfigs; |
| using ::quiche::ObliviousHttpRequest; |
| using ::quiche::ObliviousHttpResponse; |
| using RequestId = ::quic::MasqueConnectionPool::RequestId; |
| using Message = ::quic::MasqueConnectionPool::Message; |
| |
| absl::Status MasqueOhttpClient::Start() { |
| if (urls_.empty()) { |
| QUICHE_LOG(ERROR) << "No URLs to request"; |
| Abort(absl::InvalidArgumentError("No URLs to request")); |
| return status(); |
| } |
| absl::Status status = StartKeyFetch(urls_[0]); |
| if (!status.ok()) { |
| Abort(status); |
| return status; |
| } |
| return absl::OkStatus(); |
| } |
| bool MasqueOhttpClient::IsDone() { |
| if (aborted_) { |
| return true; |
| } |
| if (!ohttp_client_.has_value()) { |
| // Key fetch request is still pending. |
| return false; |
| } |
| return pending_ohttp_requests_.empty(); |
| } |
| |
| void MasqueOhttpClient::Abort(absl::Status status) { |
| QUICHE_CHECK(!status.ok()); |
| QUICHE_LOG(ERROR) << "Aborting: " << status; |
| aborted_ = true; |
| if (status_.ok()) { // Only keep the first abort status. |
| status_ = status; |
| } |
| } |
| |
| absl::StatusOr<QuicUrl> ParseUrl(const std::string& url_string) { |
| QuicUrl url(url_string, "https"); |
| if (url.host().empty() && !absl::StrContains(url_string, "://")) { |
| url = QuicUrl(absl::StrCat("https://", url_string)); |
| } |
| if (url.host().empty()) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Failed to parse key URL ", url_string)); |
| } |
| return url; |
| } |
| |
| absl::Status MasqueOhttpClient::StartKeyFetch(const std::string& url_string) { |
| QuicUrl url(url_string, "https"); |
| if (url.host().empty() && !absl::StrContains(url_string, "://")) { |
| url = QuicUrl(absl::StrCat("https://", url_string)); |
| } |
| if (url.host().empty()) { |
| QUICHE_LOG(ERROR) << "Failed to parse key URL \"" << url_string << "\""; |
| return absl::InvalidArgumentError( |
| absl::StrCat("Failed to parse key URL ", url_string)); |
| } |
| Message request; |
| request.headers[":method"] = "GET"; |
| request.headers[":scheme"] = url.scheme(); |
| request.headers[":authority"] = url.HostPort(); |
| request.headers[":path"] = url.PathParamsQuery(); |
| request.headers["accept"] = "application/ohttp-keys"; |
| |
| QUICHE_ASSIGN_OR_RETURN(key_fetch_request_id_, |
| connection_pool_.SendRequest(request, /*mtls=*/false), |
| [](const absl::Status& status) { |
| QUICHE_LOG(ERROR) |
| << "Failed to send request: " << status; |
| return status; |
| }); |
| return absl::OkStatus(); |
| } |
| |
| absl::Status MasqueOhttpClient::CheckStatusAndContentType( |
| const Message& response, const std::string& content_type) { |
| auto status_it = response.headers.find(":status"); |
| if (status_it == response.headers.end()) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("No :status header in ", content_type, " response.")); |
| } |
| int status_code; |
| if (!absl::SimpleAtoi(status_it->second, &status_code)) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Failed to parse ", content_type, " status code.")); |
| } |
| if (status_code < 200 || status_code >= 300) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Unexpected status in ", content_type, |
| " response: ", status_it->second)); |
| } |
| auto content_type_it = response.headers.find("content-type"); |
| if (content_type_it == response.headers.end()) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("No content-type header in ", content_type, " response.")); |
| } |
| std::vector<absl::string_view> content_type_split = |
| absl::StrSplit(content_type_it->second, absl::MaxSplits(';', 1)); |
| absl::string_view content_type_without_params = content_type_split[0]; |
| quiche::QuicheTextUtils::RemoveLeadingAndTrailingWhitespace( |
| &content_type_without_params); |
| if (content_type_without_params != content_type) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Unexpected content-type in ", content_type, |
| " response: ", content_type_it->second)); |
| } |
| return absl::OkStatus(); |
| } |
| |
| absl::Status MasqueOhttpClient::HandleKeyResponse( |
| const absl::StatusOr<Message>& response) { |
| key_fetch_request_id_ = std::nullopt; |
| |
| if (!response.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to fetch key: " << response.status(); |
| return response.status(); |
| } |
| QUICHE_LOG(INFO) << "Received key response: " |
| << response->headers.DebugString(); |
| QUICHE_RETURN_IF_ERROR( |
| CheckStatusAndContentType(*response, "application/ohttp-keys")); |
| absl::StatusOr<ObliviousHttpKeyConfigs> key_configs = |
| ObliviousHttpKeyConfigs::ParseConcatenatedKeys(response->body); |
| if (!key_configs.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to parse OHTTP keys: " << key_configs.status(); |
| return key_configs.status(); |
| } |
| QUICHE_LOG(INFO) << "Successfully got " << key_configs->NumKeys() |
| << " OHTTP keys: " << std::endl |
| << key_configs->DebugString(); |
| if (urls_.size() <= 2) { |
| return absl::InvalidArgumentError("No OHTTP URLs to request, exiting."); |
| } |
| relay_url_ = QuicUrl(urls_[1], "https"); |
| if (relay_url_.host().empty() && !absl::StrContains(urls_[1], "://")) { |
| relay_url_ = QuicUrl(absl::StrCat("https://", urls_[1])); |
| } |
| QUICHE_LOG(INFO) << "Using relay URL: " << relay_url_.ToString(); |
| ObliviousHttpHeaderKeyConfig key_config = key_configs->PreferredConfig(); |
| absl::StatusOr<absl::string_view> public_key = |
| key_configs->GetPublicKeyForId(key_config.GetKeyId()); |
| if (!public_key.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to get public key for key ID " |
| << static_cast<int>(key_config.GetKeyId()) << ": " |
| << public_key.status(); |
| return public_key.status(); |
| } |
| absl::StatusOr<ObliviousHttpClient> ohttp_client = |
| ObliviousHttpClient::Create(*public_key, key_config); |
| if (!ohttp_client.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to create OHTTP client: " |
| << ohttp_client.status(); |
| return ohttp_client.status(); |
| } |
| ohttp_client_.emplace(std::move(*ohttp_client)); |
| for (size_t i = 2; i < urls_.size(); ++i) { |
| QUICHE_RETURN_IF_ERROR(SendOhttpRequestForUrl(urls_[i])); |
| } |
| return absl::OkStatus(); |
| } |
| |
| absl::Status MasqueOhttpClient::SendOhttpRequestForUrl( |
| const std::string& url_string) { |
| QuicUrl url(url_string, "https"); |
| if (url.host().empty() && !absl::StrContains(url_string, "://")) { |
| url = QuicUrl(absl::StrCat("https://", url_string)); |
| } |
| if (url.host().empty()) { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Failed to parse key URL ", url_string)); |
| } |
| BinaryHttpRequest::ControlData control_data; |
| control_data.method = post_data_.empty() ? "GET" : "POST"; |
| control_data.scheme = url.scheme(); |
| control_data.authority = url.HostPort(); |
| control_data.path = url.PathParamsQuery(); |
| BinaryHttpRequest binary_request(control_data); |
| binary_request.set_body(post_data_); |
| absl::StatusOr<std::string> encoded_request = binary_request.Serialize(); |
| if (!encoded_request.ok()) { |
| return encoded_request.status(); |
| } |
| if (!ohttp_client_.has_value()) { |
| QUICHE_LOG(FATAL) << "Cannot send OHTTP request without OHTTP client"; |
| return absl::InternalError( |
| "Cannot send OHTTP request without OHTTP client"); |
| } |
| absl::StatusOr<ObliviousHttpRequest> ohttp_request = |
| ohttp_client_->CreateObliviousHttpRequest(*encoded_request); |
| if (!ohttp_request.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to create OHTTP request: " |
| << ohttp_request.status(); |
| return ohttp_request.status(); |
| } |
| Message request; |
| request.headers[":method"] = "POST"; |
| request.headers[":scheme"] = relay_url_.scheme(); |
| request.headers[":authority"] = relay_url_.HostPort(); |
| request.headers[":path"] = relay_url_.PathParamsQuery(); |
| request.headers["content-type"] = "message/ohttp-req"; |
| request.body = ohttp_request->EncapsulateAndSerialize(); |
| absl::StatusOr<RequestId> request_id = |
| connection_pool_.SendRequest(request, /*mtls=*/true); |
| if (!request_id.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to send request: " << request_id.status(); |
| return request_id.status(); |
| } |
| QUICHE_LOG(INFO) << "Sent OHTTP request for " << url_string; |
| auto context = std::move(*ohttp_request).ReleaseContext(); |
| pending_ohttp_requests_.insert({*request_id, std::move(context)}); |
| return absl::OkStatus(); |
| } |
| |
| absl::StatusOr<BinaryHttpResponse> MasqueOhttpClient::TryExtractBinaryResponse( |
| const RequestId request_id, quiche::ObliviousHttpRequest::Context& context, |
| const Message& response) { |
| if (!ohttp_client_.has_value()) { |
| QUICHE_LOG(FATAL) << "Received OHTTP response without OHTTP client"; |
| return absl::InternalError("Received OHTTP response without OHTTP client"); |
| } |
| absl::StatusOr<ObliviousHttpResponse> ohttp_response = |
| ohttp_client_->DecryptObliviousHttpResponse(response.body, context); |
| if (ohttp_response.ok()) { |
| QUICHE_LOG(INFO) << "Received OHTTP response for " << request_id; |
| absl::StatusOr<BinaryHttpResponse> binary_response = |
| BinaryHttpResponse::Create(ohttp_response->GetPlaintextData()); |
| if (binary_response.ok()) { |
| QUICHE_LOG(INFO) << "Successfully decoded OHTTP response:"; |
| QUICHE_LOG(INFO) << "Status: " << binary_response->status_code(); |
| for (const quiche::BinaryHttpMessage::Field& field : |
| binary_response->GetHeaderFields()) { |
| QUICHE_LOG(INFO) << field.name << ": " << field.value; |
| } |
| QUICHE_LOG(INFO) << "Body:" << std::endl << binary_response->body(); |
| return binary_response; |
| } else { |
| QUICHE_LOG(ERROR) << "Failed to parse binary response: " |
| << binary_response.status(); |
| return binary_response; |
| } |
| } else { |
| QUICHE_LOG(ERROR) << "Failed to decrypt OHTTP response: " |
| << ohttp_response.status(); |
| return ohttp_response.status(); |
| } |
| } |
| |
| absl::Status MasqueOhttpClient::ProcessOhttpResponse( |
| RequestId request_id, const absl::StatusOr<Message>& response) { |
| auto it = pending_ohttp_requests_.find(request_id); |
| if (it == pending_ohttp_requests_.end()) { |
| QUICHE_LOG(ERROR) << "Received unexpected response for unknown request " |
| << request_id; |
| return absl::InternalError( |
| "Received unexpected response for unknown request"); |
| } |
| auto cleanup = |
| absl::MakeCleanup([this, it]() { pending_ohttp_requests_.erase(it); }); |
| QUICHE_RETURN_IF_ERROR(response.status()); |
| QUICHE_RETURN_IF_ERROR(CheckGatewayResponse(*response)); |
| absl::Status status = |
| CheckStatusAndContentType(*response, "message/ohttp-res"); |
| if (!status.ok()) { |
| if (!response->body.empty()) { |
| QUICHE_LOG(ERROR) << "Bad ohttp-res with body:" << std::endl |
| << response->body; |
| } else { |
| QUICHE_LOG(ERROR) << "Bad ohttp-res with empty body"; |
| } |
| return status; |
| } |
| absl::StatusOr<BinaryHttpResponse> binary_response = |
| TryExtractBinaryResponse(request_id, it->second, *response); |
| QUICHE_RETURN_IF_ERROR(binary_response.status()); |
| QUICHE_RETURN_IF_ERROR(CheckEncapsulatedResponse(*binary_response)); |
| return absl::OkStatus(); |
| } |
| |
| void MasqueOhttpClient::OnPoolResponse(MasqueConnectionPool* /*pool*/, |
| RequestId request_id, |
| absl::StatusOr<Message>&& response) { |
| if (key_fetch_request_id_.has_value() && |
| *key_fetch_request_id_ == request_id) { |
| auto status = HandleKeyResponse(response); |
| if (!status.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to handle key response: " << status; |
| Abort(status); |
| } |
| } else { |
| auto status = ProcessOhttpResponse(request_id, response); |
| if (!status.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to handle OHTTP response: " << status; |
| Abort(status); |
| } |
| } |
| } |
| } // namespace quic |