| // 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 <stdbool.h> |
| |
| #include <cstddef> |
| #include <memory> |
| #include <optional> |
| #include <ostream> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "openssl/base.h" |
| #include "quiche/quic/core/io/quic_default_event_loop.h" |
| #include "quiche/quic/core/io/quic_event_loop.h" |
| #include "quiche/quic/core/quic_default_clock.h" |
| #include "quiche/quic/core/quic_time.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_command_line_flags.h" |
| #include "quiche/common/platform/api/quiche_logging.h" |
| #include "quiche/common/platform/api/quiche_system_event_loop.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" |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| bool, disable_certificate_verification, false, |
| "If true, don't verify the server certificate."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG(int, address_family, 0, |
| "IP address family to use. Must be 0, 4 or 6. " |
| "Defaults to 0 which means any."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, client_cert_file, "", |
| "Path to the client certificate chain."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::string, client_cert_key_file, "", |
| "Path to the pkcs8 client certificate private key."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::string, post_data, "", |
| "When set, the client will send a POST request with this data."); |
| |
| using quiche::BinaryHttpRequest; |
| using quiche::BinaryHttpResponse; |
| using quiche::ObliviousHttpClient; |
| using quiche::ObliviousHttpHeaderKeyConfig; |
| using quiche::ObliviousHttpKeyConfigs; |
| using quiche::ObliviousHttpRequest; |
| using quiche::ObliviousHttpResponse; |
| |
| namespace quic { |
| namespace { |
| |
| class MasqueOhttpClient : public MasqueConnectionPool::Visitor { |
| public: |
| using RequestId = MasqueConnectionPool::RequestId; |
| using Message = MasqueConnectionPool::Message; |
| explicit MasqueOhttpClient(QuicEventLoop *event_loop, SSL_CTX *ssl_ctx, |
| std::vector<std::string> urls, |
| bool disable_certificate_verification, |
| int address_family_for_lookup, |
| const std::string &post_data) |
| : urls_(urls), |
| post_data_(post_data), |
| connection_pool_(event_loop, ssl_ctx, disable_certificate_verification, |
| address_family_for_lookup, this) {} |
| |
| bool Start() { |
| if (urls_.empty()) { |
| QUICHE_LOG(ERROR) << "No URLs to request"; |
| Abort(); |
| return false; |
| } |
| if (!StartKeyFetch(urls_[0])) { |
| Abort(); |
| return false; |
| } |
| return true; |
| } |
| bool IsDone() { |
| if (aborted_) { |
| return true; |
| } |
| if (!ohttp_client_.has_value()) { |
| // Key fetch request is still pending. |
| return false; |
| } |
| return pending_ohttp_requests_.empty(); |
| } |
| |
| // From MasqueConnectionPool::Visitor. |
| void OnResponse(MasqueConnectionPool * /*pool*/, RequestId request_id, |
| const absl::StatusOr<Message> &response) override { |
| if (key_fetch_request_id_.has_value() && |
| *key_fetch_request_id_ == request_id) { |
| key_fetch_request_id_ = std::nullopt; |
| HandleKeyResponse(response); |
| } else { |
| 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; |
| Abort(); |
| return; |
| } |
| if (response.ok()) { |
| if (!ohttp_client_.has_value()) { |
| QUICHE_LOG(FATAL) << "Received OHTTP response without OHTTP client"; |
| return; |
| } |
| absl::StatusOr<ObliviousHttpResponse> ohttp_response = |
| ohttp_client_->DecryptObliviousHttpResponse(response->body, |
| it->second); |
| 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:"; |
| 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(); |
| } else { |
| QUICHE_LOG(ERROR) << "Failed to parse binary response: " |
| << binary_response.status(); |
| } |
| } else { |
| QUICHE_LOG(ERROR) << "Failed to decrypt OHTTP response: " |
| << ohttp_response.status(); |
| } |
| } else { |
| QUICHE_LOG(ERROR) << "OHTTP request " << request_id |
| << " failed: " << response.status(); |
| } |
| pending_ohttp_requests_.erase(it); |
| } |
| } |
| |
| private: |
| bool 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 false; |
| } |
| Message request; |
| request.headers[":method"] = "GET"; |
| request.headers[":scheme"] = url.scheme(); |
| request.headers[":authority"] = url.HostPort(); |
| request.headers[":path"] = url.path(); |
| request.headers["host"] = url.HostPort(); |
| request.headers["accept"] = "application/ohttp-keys"; |
| request.headers["content-type"] = "application/ohttp-keys"; |
| absl::StatusOr<RequestId> request_id = |
| connection_pool_.SendRequest(request); |
| if (!request_id.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to send request: " << request_id.status(); |
| return false; |
| } |
| key_fetch_request_id_ = *request_id; |
| return true; |
| } |
| |
| void HandleKeyResponse(const absl::StatusOr<Message> &response) { |
| if (!response.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to fetch key: " << response.status(); |
| return; |
| } |
| QUICHE_LOG(INFO) << "Received key response: " |
| << response->headers.DebugString(); |
| absl::StatusOr<ObliviousHttpKeyConfigs> key_configs = |
| ObliviousHttpKeyConfigs::ParseConcatenatedKeys(response->body); |
| if (!key_configs.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to parse OHTTP keys: " |
| << key_configs.status(); |
| Abort(); |
| return; |
| } |
| QUICHE_LOG(INFO) << "Successfully got " << key_configs->NumKeys() |
| << " OHTTP keys: " << std::endl |
| << key_configs->DebugString(); |
| if (urls_.size() <= 2) { |
| QUICHE_LOG(INFO) << "No OHTTP URLs to request, exiting."; |
| Abort(); |
| return; |
| } |
| 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(); |
| Abort(); |
| return; |
| } |
| 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(); |
| Abort(); |
| return; |
| } |
| ohttp_client_.emplace(std::move(*ohttp_client)); |
| for (size_t i = 2; i < urls_.size(); ++i) { |
| SendOhttpRequestForUrl(urls_[i]); |
| } |
| } |
| |
| void 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()) { |
| QUICHE_LOG(ERROR) << "Failed to parse key URL \"" << url_string << "\""; |
| return; |
| } |
| 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.path(); |
| BinaryHttpRequest binary_request(control_data); |
| binary_request.set_body(post_data_); |
| absl::StatusOr<std::string> encoded_request = binary_request.Serialize(); |
| if (!encoded_request.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to encode request: " |
| << encoded_request.status(); |
| return; |
| } |
| if (!ohttp_client_.has_value()) { |
| QUICHE_LOG(FATAL) << "Cannot send OHTTP request without OHTTP client"; |
| return; |
| } |
| 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; |
| } |
| Message request; |
| request.headers[":method"] = "POST"; |
| request.headers[":scheme"] = relay_url_.scheme(); |
| request.headers[":authority"] = relay_url_.HostPort(); |
| request.headers[":path"] = relay_url_.path(); |
| request.headers["host"] = relay_url_.HostPort(); |
| request.headers["content-type"] = "message/ohttp-req"; |
| request.body = ohttp_request->EncapsulateAndSerialize(); |
| absl::StatusOr<RequestId> request_id = |
| connection_pool_.SendRequest(request); |
| if (!request_id.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to send request: " << request_id.status(); |
| return; |
| } |
| 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)}); |
| } |
| |
| void Abort() { |
| QUICHE_LOG(INFO) << "Aborting"; |
| aborted_ = true; |
| } |
| |
| std::vector<std::string> urls_; |
| std::string post_data_; |
| MasqueConnectionPool connection_pool_; |
| std::optional<RequestId> key_fetch_request_id_; |
| bool aborted_ = false; |
| std::optional<ObliviousHttpClient> ohttp_client_; |
| QuicUrl relay_url_; |
| absl::flat_hash_map<RequestId, ObliviousHttpRequest::Context> |
| pending_ohttp_requests_; |
| }; |
| |
| int RunMasqueOhttpClient(int argc, char *argv[]) { |
| const char *usage = |
| "Usage: masque_ohttp_client <key-url> <relay-url> <url>..."; |
| std::vector<std::string> urls = |
| quiche::QuicheParseCommandLineFlags(usage, argc, argv); |
| |
| quiche::QuicheSystemEventLoop system_event_loop("masque_ohttp_client"); |
| const bool disable_certificate_verification = |
| quiche::GetQuicheCommandLineFlag(FLAGS_disable_certificate_verification); |
| |
| absl::StatusOr<bssl::UniquePtr<SSL_CTX>> ssl_ctx = |
| MasqueConnectionPool::CreateSslCtx( |
| quiche::GetQuicheCommandLineFlag(FLAGS_client_cert_file), |
| quiche::GetQuicheCommandLineFlag(FLAGS_client_cert_key_file)); |
| if (!ssl_ctx.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to create SSL context: " << ssl_ctx.status(); |
| return 1; |
| } |
| |
| const int address_family = |
| quiche::GetQuicheCommandLineFlag(FLAGS_address_family); |
| int address_family_for_lookup; |
| if (address_family == 0) { |
| address_family_for_lookup = AF_UNSPEC; |
| } else if (address_family == 4) { |
| address_family_for_lookup = AF_INET; |
| } else if (address_family == 6) { |
| address_family_for_lookup = AF_INET6; |
| } else { |
| QUICHE_LOG(ERROR) << "Invalid address_family " << address_family; |
| return 1; |
| } |
| std::unique_ptr<QuicEventLoop> event_loop = |
| GetDefaultEventLoop()->Create(QuicDefaultClock::Get()); |
| std::string post_data = quiche::GetQuicheCommandLineFlag(FLAGS_post_data); |
| |
| MasqueOhttpClient masque_ohttp_client(event_loop.get(), ssl_ctx->get(), urls, |
| disable_certificate_verification, |
| address_family_for_lookup, post_data); |
| if (!masque_ohttp_client.Start()) { |
| return 1; |
| } |
| while (!masque_ohttp_client.IsDone()) { |
| event_loop->RunEventLoopOnce(QuicTime::Delta::FromMilliseconds(50)); |
| } |
| return 0; |
| } |
| |
| } // namespace |
| } // namespace quic |
| |
| int main(int argc, char *argv[]) { |
| return quic::RunMasqueOhttpClient(argc, argv); |
| } |