| // 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 <cstddef> |
| #include <cstdint> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/status/status.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "openssl/base.h" |
| #include "quiche/quic/masque/masque_connection_pool.h" |
| #include "quiche/quic/masque/masque_ohttp_client.h" |
| #include "quiche/quic/masque/private_tokens.h" |
| #include "quiche/common/platform/api/quiche_command_line_flags.h" |
| #include "quiche/common/platform/api/quiche_file_utils.h" |
| #include "quiche/common/platform/api/quiche_logging.h" |
| #include "quiche/common/quiche_status_utils.h" |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| bool, disable_certificate_verification, false, |
| "If true, don't verify the server certificate."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| bool, use_mtls_for_key_fetch, false, |
| "If true, use mTLS when fetching the OHTTP/HPKE keys."); |
| |
| 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."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::string, post_data_file, "", |
| "When set, the client will send a POST request with the contents of this " |
| "file."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::optional<std::string>, method, std::nullopt, |
| "Sets the method of the encapsulated request. Defaults to GET, or POST if " |
| "--post_data or --post_data_file is set."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| int, num_bhttp_chunks, -1, |
| "Number of indeterminate-length BHTTP chunks to split post data into. If " |
| "not set or if set to -1, it will match the chunked mode (see " |
| "--num_ohttp_chunks). If set to " |
| "0, the client will use known-length BHTTP."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| int, num_ohttp_chunks, 0, |
| "Number of OHTTP chunks to split serialized BHTTP request into. If not set " |
| "or if set to 0, the client will use standard non-chunked OHTTP."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::vector<std::string>, header, {}, |
| "Adds a header field to the encapsulated binary request. Separate the " |
| "header name and value with a colon. Can be specified multiple times."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::vector<std::string>, key_fetch_header, {}, |
| "Adds a header field to the key fetch request. Separate the header name " |
| "and value with a colon. Can be specified multiple times."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::vector<std::string>, outer_header, {}, |
| "Adds a header field to the outer gateway request. Separate the header " |
| "name and value with a colon. Can be specified multiple times."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::string, private_token, "", |
| "When set, the client will attach a base64-encoded private token to the " |
| "encapsulated request. Accepts any base64 encoding."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::string, private_token_private_key_file, "", |
| "Path to the PEM-encoded RSA private key for private tokens."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::string, private_token_public_key_file, "", |
| "Path to the PEM-encoded RSA public key for private tokens."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::string, dns_override, "", |
| "Allows replacing DNS resolution results, similar to curl --connect-to. " |
| "Format is HOST1:PORT1:HOST2:PORT2 where HOST1:PORT1 will be replaced by " |
| "HOST2:PORT2. HOST1 and PORT1 can be empty, which matches any host and " |
| "port. PORT2 can be empty to not override ports. Multiple overrides can be " |
| "specified separated by semi-colons."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG(std::optional<std::string>, |
| expect_gateway_error, std::nullopt, |
| "If set, the client will expect this text in " |
| "the error message for the gateway response."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::optional<int16_t>, expect_gateway_response_code, std::nullopt, |
| "If set, the client will expect this response code from the gateway."); |
| |
| namespace quic { |
| namespace { |
| absl::Status 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); |
| |
| const bool disable_certificate_verification = |
| quiche::GetQuicheCommandLineFlag(FLAGS_disable_certificate_verification); |
| const bool use_mtls_for_key_fetch = |
| quiche::GetQuicheCommandLineFlag(FLAGS_use_mtls_for_key_fetch); |
| const std::string client_cert_file = |
| quiche::GetQuicheCommandLineFlag(FLAGS_client_cert_file); |
| const std::string client_cert_key_file = |
| quiche::GetQuicheCommandLineFlag(FLAGS_client_cert_key_file); |
| const std::optional<std::string> expect_gateway_error = |
| quiche::GetQuicheCommandLineFlag(FLAGS_expect_gateway_error); |
| const std::optional<int16_t> expect_gateway_response_code = |
| quiche::GetQuicheCommandLineFlag(FLAGS_expect_gateway_response_code); |
| |
| MasqueConnectionPool::DnsConfig dns_config; |
| QUICHE_RETURN_IF_ERROR(dns_config.SetAddressFamily( |
| quiche::GetQuicheCommandLineFlag(FLAGS_address_family))); |
| QUICHE_RETURN_IF_ERROR(dns_config.SetOverrides( |
| quiche::GetQuicheCommandLineFlag(FLAGS_dns_override))); |
| std::string post_data = quiche::GetQuicheCommandLineFlag(FLAGS_post_data); |
| std::string post_data_file = |
| quiche::GetQuicheCommandLineFlag(FLAGS_post_data_file); |
| if (!post_data_file.empty()) { |
| if (!post_data.empty()) { |
| return absl::InvalidArgumentError( |
| "Only one of --post_data and --post_data_file can be set."); |
| } |
| std::optional<std::string> post_data_from_file = |
| quiche::ReadFileContents(post_data_file); |
| if (!post_data_from_file.has_value()) { |
| return absl::InvalidArgumentError(absl::StrCat( |
| "Failed to read post data from file \"", post_data_file, "\"")); |
| } |
| post_data = *post_data_from_file; |
| } |
| std::optional<std::string> method = |
| quiche::GetQuicheCommandLineFlag(FLAGS_method); |
| const int num_ohttp_chunks = |
| quiche::GetQuicheCommandLineFlag(FLAGS_num_ohttp_chunks); |
| const int num_bhttp_chunks = |
| quiche::GetQuicheCommandLineFlag(FLAGS_num_bhttp_chunks); |
| std::vector<std::string> headers = |
| quiche::GetQuicheCommandLineFlag(FLAGS_header); |
| std::vector<std::string> key_fetch_headers = |
| quiche::GetQuicheCommandLineFlag(FLAGS_key_fetch_header); |
| std::vector<std::string> outer_headers = |
| quiche::GetQuicheCommandLineFlag(FLAGS_outer_header); |
| std::string private_token = |
| quiche::GetQuicheCommandLineFlag(FLAGS_private_token); |
| std::string private_token_private_key_file = |
| quiche::GetQuicheCommandLineFlag(FLAGS_private_token_private_key_file); |
| std::string private_token_public_key_file = |
| quiche::GetQuicheCommandLineFlag(FLAGS_private_token_public_key_file); |
| if (!private_token_private_key_file.empty() && !private_token.empty()) { |
| return absl::InvalidArgumentError( |
| "Cannot use both --private_token and " |
| "--private_token_private_key_file."); |
| } else if (private_token_private_key_file.empty() != |
| private_token_public_key_file.empty()) { |
| return absl::InvalidArgumentError( |
| "Both or neither of --private_token_private_key_file and " |
| "--private_token_public_key_file must be set."); |
| } |
| bssl::UniquePtr<RSA> private_token_private_key; |
| if (!private_token_private_key_file.empty()) { |
| QUICHE_ASSIGN_OR_RETURN( |
| private_token_private_key, |
| ParseRsaPrivateKeyFile(private_token_private_key_file)); |
| } |
| bssl::UniquePtr<RSA> private_token_public_key; |
| if (!private_token_public_key_file.empty()) { |
| QUICHE_ASSIGN_OR_RETURN( |
| private_token_public_key, |
| ParseRsaPublicKeyFile(private_token_public_key_file)); |
| } |
| |
| if (urls.size() < 3) { |
| return absl::InvalidArgumentError(usage); |
| } |
| MasqueOhttpClient::Config config(/*key_fetch_url=*/urls[0], |
| /*relay_url=*/urls[1]); |
| if (use_mtls_for_key_fetch) { |
| QUICHE_RETURN_IF_ERROR(config.ConfigureKeyFetchClientCert( |
| client_cert_file, client_cert_key_file)); |
| } |
| QUICHE_RETURN_IF_ERROR( |
| config.ConfigureOhttpMtls(client_cert_file, client_cert_key_file)); |
| config.SetDisableCertificateVerification(disable_certificate_verification); |
| config.SetDnsConfig(dns_config); |
| QUICHE_RETURN_IF_ERROR(config.AddKeyFetchHeaders(key_fetch_headers)); |
| for (size_t i = 2; i < urls.size(); ++i) { |
| MasqueOhttpClient::Config::PerRequestConfig per_request_config(urls[i]); |
| per_request_config.SetPostData(post_data); |
| if (method.has_value()) { |
| per_request_config.SetMethod(*method); |
| } |
| QUICHE_RETURN_IF_ERROR(per_request_config.AddHeaders(headers)); |
| QUICHE_RETURN_IF_ERROR(per_request_config.AddOuterHeaders(outer_headers)); |
| if (!private_token.empty()) { |
| QUICHE_RETURN_IF_ERROR(per_request_config.AddPrivateToken(private_token)); |
| } else if (private_token_private_key != nullptr && |
| private_token_public_key != nullptr) { |
| QUICHE_ASSIGN_OR_RETURN( |
| std::string generated_private_token, |
| CreateTokenLocally(private_token_private_key.get(), |
| private_token_public_key.get())); |
| QUICHE_RETURN_IF_ERROR( |
| per_request_config.AddPrivateToken(generated_private_token)); |
| } |
| per_request_config.SetNumOhttpChunks(num_ohttp_chunks); |
| per_request_config.SetNumBhttpChunks(num_bhttp_chunks); |
| if (expect_gateway_error.has_value()) { |
| per_request_config.SetExpectedGatewayError(*expect_gateway_error); |
| } |
| if (expect_gateway_response_code.has_value()) { |
| per_request_config.SetExpectedGatewayStatusCode( |
| *expect_gateway_response_code); |
| } |
| config.AddPerRequestConfig(per_request_config); |
| } |
| return MasqueOhttpClient::Run(std::move(config)); |
| } |
| |
| } // namespace |
| } // namespace quic |
| |
| int main(int argc, char* argv[]) { |
| absl::Status status = quic::RunMasqueOhttpClient(argc, argv); |
| if (!status.ok()) { |
| QUICHE_LOG(ERROR) << status.message(); |
| return 1; |
| } |
| QUICHE_LOG(INFO) << "MasqueOhttpClient finished successfully"; |
| return 0; |
| } |