| // 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["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["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); | |
| } |