| // 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. | 
 |  | 
 | // A binary wrapper for QuicClient. | 
 | // Connects to a host using QUIC, sends a request to the provided URL, and | 
 | // displays the response. | 
 | // | 
 | // Some usage examples: | 
 | // | 
 | // Standard request/response: | 
 | //   quic_client www.google.com | 
 | //   quic_client www.google.com --quiet | 
 | //   quic_client www.google.com --port=443 | 
 | // | 
 | // Use a specific version: | 
 | //   quic_client www.google.com --quic_version=23 | 
 | // | 
 | // Send a POST instead of a GET: | 
 | //   quic_client www.google.com --body="this is a POST body" | 
 | // | 
 | // Append additional headers to the request: | 
 | //   quic_client www.google.com --headers="Header-A: 1234; Header-B: 5678" | 
 | // | 
 | // Connect to a host different to the URL being requested: | 
 | //   quic_client mail.google.com --host=www.google.com | 
 | // | 
 | // Connect to a specific IP: | 
 | //   IP=`dig www.google.com +short | head -1` | 
 | //   quic_client www.google.com --host=${IP} | 
 | // | 
 | // Send repeated requests and change ephemeral port between requests | 
 | //   quic_client www.google.com --num_requests=10 | 
 | // | 
 | // Try to connect to a host which does not speak QUIC: | 
 | //   quic_client www.example.com | 
 | // | 
 | // This tool is available as a built binary at: | 
 | // /google/data/ro/teams/quic/tools/quic_client | 
 | // After submitting changes to this file, you will need to follow the | 
 | // instructions at go/quic_client_binary_update | 
 |  | 
 | #include "quiche/quic/tools/quic_toy_client.h" | 
 |  | 
 | #include <fstream> | 
 | #include <iostream> | 
 | #include <memory> | 
 | #include <string> | 
 | #include <utility> | 
 | #include <vector> | 
 |  | 
 | #include "absl/strings/escaping.h" | 
 | #include "absl/strings/str_split.h" | 
 | #include "absl/strings/string_view.h" | 
 | #include "quiche/quic/core/crypto/quic_client_session_cache.h" | 
 | #include "quiche/quic/core/quic_packets.h" | 
 | #include "quiche/quic/core/quic_server_id.h" | 
 | #include "quiche/quic/core/quic_utils.h" | 
 | #include "quiche/quic/core/quic_versions.h" | 
 | #include "quiche/quic/platform/api/quic_default_proof_providers.h" | 
 | #include "quiche/quic/platform/api/quic_ip_address.h" | 
 | #include "quiche/quic/platform/api/quic_socket_address.h" | 
 | #include "quiche/quic/tools/fake_proof_verifier.h" | 
 | #include "quiche/quic/tools/quic_url.h" | 
 | #include "quiche/common/http/http_header_block.h" | 
 | #include "quiche/common/platform/api/quiche_command_line_flags.h" | 
 | #include "quiche/common/platform/api/quiche_logging.h" | 
 | #include "quiche/common/quiche_text_utils.h" | 
 |  | 
 | namespace { | 
 |  | 
 | using quiche::QuicheTextUtils; | 
 |  | 
 | }  // namespace | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, host, "", | 
 |     "The IP or hostname to connect to. If not provided, the host " | 
 |     "will be derived from the provided URL."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(int32_t, port, 0, "The port to connect to."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, ip_version_for_host_lookup, "", | 
 |                                 "Only used if host address lookup is needed. " | 
 |                                 "4=ipv4; 6=ipv6; otherwise=don't care."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, body, "", | 
 |                                 "If set, send a POST with this body."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, body_hex, "", | 
 |     "If set, contents are converted from hex to ascii, before " | 
 |     "sending as body of a POST. e.g. --body_hex=\"68656c6c6f\""); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, headers, "", | 
 |     "A semicolon separated list of key:value pairs to " | 
 |     "add to request headers."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(bool, quiet, false, | 
 |                                 "Set to true for a quieter output experience."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     bool, output_resolved_server_address, false, | 
 |     "Set to true to print the resolved IP of the server."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, quic_version, "", | 
 |     "QUIC version to speak, e.g. 21. If not set, then all available " | 
 |     "versions are offered in the handshake. Also supports wire versions " | 
 |     "such as Q043 or T099."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, connection_options, "", | 
 |     "Connection options as ASCII tags separated by commas, " | 
 |     "e.g. \"ABCD,EFGH\""); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, client_connection_options, "", | 
 |     "Client connection options as ASCII tags separated by commas, " | 
 |     "e.g. \"ABCD,EFGH\""); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     bool, version_mismatch_ok, false, | 
 |     "If true, a version mismatch in the handshake is not considered a " | 
 |     "failure. Useful for probing a server to determine if it speaks " | 
 |     "any version of QUIC."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     bool, force_version_negotiation, false, | 
 |     "If true, start by proposing a version that is reserved for version " | 
 |     "negotiation."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     bool, multi_packet_chlo, false, | 
 |     "If true, add a transport parameter to make the ClientHello span two " | 
 |     "packets. Only works with QUIC+TLS."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     bool, redirect_is_success, true, | 
 |     "If true, an HTTP response code of 3xx is considered to be a " | 
 |     "successful response, otherwise a failure."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(int32_t, initial_mtu, 0, | 
 |                                 "Initial MTU of the connection."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     int32_t, num_requests, 1, | 
 |     "How many sequential requests to make on a single connection."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(bool, ignore_errors, false, | 
 |                                 "If true, ignore connection/response errors " | 
 |                                 "and send all num_requests anyway."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     bool, disable_certificate_verification, false, | 
 |     "If true, don't verify the server certificate."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, default_client_cert, "", | 
 |     "The path to the file containing PEM-encoded client default certificate to " | 
 |     "be sent to the server, if server requested client certs."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, default_client_cert_key, "", | 
 |     "The path to the file containing PEM-encoded private key of the client's " | 
 |     "default certificate for signing, if server requested client certs."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     bool, drop_response_body, false, | 
 |     "If true, drop response body immediately after it is received."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     bool, disable_port_changes, false, | 
 |     "If true, do not change local port after each request."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(bool, one_connection_per_request, false, | 
 |                                 "If true, close the connection after each " | 
 |                                 "request. This allows testing 0-RTT."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, server_connection_id, "", | 
 |     "If non-empty, the client will use the given server connection id for all " | 
 |     "connections. The flag value is the hex-string of the on-wire connection id" | 
 |     " bytes, e.g. '--server_connection_id=0123456789abcdef'."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     int32_t, server_connection_id_length, -1, | 
 |     "Length of the server connection ID used. This flag has no effects if " | 
 |     "--server_connection_id is non-empty."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(int32_t, client_connection_id_length, -1, | 
 |                                 "Length of the client connection ID used."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(int32_t, max_time_before_crypto_handshake_ms, | 
 |                                 10000, | 
 |                                 "Max time to wait before handshake completes."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     int32_t, max_inbound_header_list_size, 128 * 1024, | 
 |     "Max inbound header list size. 0 means default."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, interface_name, "", | 
 |                                 "Interface name to bind QUIC UDP sockets to."); | 
 |  | 
 | DEFINE_QUICHE_COMMAND_LINE_FLAG( | 
 |     std::string, signing_algorithms_pref, "", | 
 |     "A textual specification of a set of signature algorithms that can be " | 
 |     "accepted by boring SSL SSL_set1_sigalgs_list()"); | 
 |  | 
 | namespace quic { | 
 | namespace { | 
 |  | 
 | // Creates a ClientProofSource which only contains a default client certificate. | 
 | // Return nullptr for failure. | 
 | std::unique_ptr<ClientProofSource> CreateTestClientProofSource( | 
 |     absl::string_view default_client_cert_file, | 
 |     absl::string_view default_client_cert_key_file) { | 
 |   std::ifstream cert_stream(std::string{default_client_cert_file}, | 
 |                             std::ios::binary); | 
 |   std::vector<std::string> certs = | 
 |       CertificateView::LoadPemFromStream(&cert_stream); | 
 |   if (certs.empty()) { | 
 |     std::cerr << "Failed to load client certs." << std::endl; | 
 |     return nullptr; | 
 |   } | 
 |  | 
 |   std::ifstream key_stream(std::string{default_client_cert_key_file}, | 
 |                            std::ios::binary); | 
 |   std::unique_ptr<CertificatePrivateKey> private_key = | 
 |       CertificatePrivateKey::LoadPemFromStream(&key_stream); | 
 |   if (private_key == nullptr) { | 
 |     std::cerr << "Failed to load client cert key." << std::endl; | 
 |     return nullptr; | 
 |   } | 
 |  | 
 |   auto proof_source = std::make_unique<DefaultClientProofSource>(); | 
 |   proof_source->AddCertAndKey( | 
 |       {"*"}, | 
 |       quiche::QuicheReferenceCountedPointer<ClientProofSource::Chain>( | 
 |           new ClientProofSource::Chain(certs)), | 
 |       std::move(*private_key)); | 
 |  | 
 |   return proof_source; | 
 | } | 
 |  | 
 | }  // namespace | 
 |  | 
 | QuicToyClient::QuicToyClient(ClientFactory* client_factory) | 
 |     : client_factory_(client_factory) {} | 
 |  | 
 | int QuicToyClient::SendRequestsAndPrintResponses( | 
 |     std::vector<std::string> urls) { | 
 |   QuicUrl url(urls[0], "https"); | 
 |   std::string host = quiche::GetQuicheCommandLineFlag(FLAGS_host); | 
 |   if (host.empty()) { | 
 |     host = url.host(); | 
 |   } | 
 |   int port = quiche::GetQuicheCommandLineFlag(FLAGS_port); | 
 |   if (port == 0) { | 
 |     port = url.port(); | 
 |   } | 
 |  | 
 |   quic::ParsedQuicVersionVector versions = quic::CurrentSupportedVersions(); | 
 |  | 
 |   std::string quic_version_string = | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_quic_version); | 
 |   if (!quic_version_string.empty()) { | 
 |     versions = quic::ParseQuicVersionVectorString(quic_version_string); | 
 |   } | 
 |  | 
 |   if (versions.empty()) { | 
 |     std::cerr << "No known version selected." << std::endl; | 
 |     return 1; | 
 |   } | 
 |  | 
 |   for (const quic::ParsedQuicVersion& version : versions) { | 
 |     quic::QuicEnableVersion(version); | 
 |   } | 
 |  | 
 |   if (quiche::GetQuicheCommandLineFlag(FLAGS_force_version_negotiation)) { | 
 |     versions.insert(versions.begin(), | 
 |                     quic::QuicVersionReservedForNegotiation()); | 
 |   } | 
 |  | 
 |   const int32_t num_requests( | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_num_requests)); | 
 |   std::unique_ptr<quic::ProofVerifier> proof_verifier; | 
 |   if (quiche::GetQuicheCommandLineFlag( | 
 |           FLAGS_disable_certificate_verification)) { | 
 |     proof_verifier = std::make_unique<FakeProofVerifier>(); | 
 |   } else { | 
 |     proof_verifier = quic::CreateDefaultProofVerifier(url.host()); | 
 |   } | 
 |   std::unique_ptr<quic::SessionCache> session_cache; | 
 |   if (num_requests > 1 && | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_one_connection_per_request)) { | 
 |     session_cache = std::make_unique<QuicClientSessionCache>(); | 
 |   } | 
 |  | 
 |   QuicConfig config; | 
 |   std::string connection_options_string = | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_connection_options); | 
 |   if (!connection_options_string.empty()) { | 
 |     config.SetConnectionOptionsToSend( | 
 |         ParseQuicTagVector(connection_options_string)); | 
 |   } | 
 |   std::string client_connection_options_string = | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_client_connection_options); | 
 |   if (!client_connection_options_string.empty()) { | 
 |     config.SetClientConnectionOptions( | 
 |         ParseQuicTagVector(client_connection_options_string)); | 
 |   } | 
 |   if (quiche::GetQuicheCommandLineFlag(FLAGS_multi_packet_chlo)) { | 
 |     // Make the ClientHello span multiple packets by adding a large 'discard' | 
 |     // transport parameter. | 
 |     config.SetDiscardLengthToSend(2000); | 
 |   } | 
 |   config.set_max_time_before_crypto_handshake( | 
 |       QuicTime::Delta::FromMilliseconds(quiche::GetQuicheCommandLineFlag( | 
 |           FLAGS_max_time_before_crypto_handshake_ms))); | 
 |  | 
 |   int address_family_for_lookup = AF_UNSPEC; | 
 |   if (quiche::GetQuicheCommandLineFlag(FLAGS_ip_version_for_host_lookup) == | 
 |       "4") { | 
 |     address_family_for_lookup = AF_INET; | 
 |   } else if (quiche::GetQuicheCommandLineFlag( | 
 |                  FLAGS_ip_version_for_host_lookup) == "6") { | 
 |     address_family_for_lookup = AF_INET6; | 
 |   } | 
 |  | 
 |   // Build the client, and try to connect. | 
 |   std::unique_ptr<QuicSpdyClientBase> client = client_factory_->CreateClient( | 
 |       url.host(), host, address_family_for_lookup, port, versions, config, | 
 |       std::move(proof_verifier), std::move(session_cache)); | 
 |  | 
 |   if (client == nullptr) { | 
 |     std::cerr << "Failed to create client." << std::endl; | 
 |     return 1; | 
 |   } | 
 |  | 
 |   if (!quiche::GetQuicheCommandLineFlag(FLAGS_default_client_cert).empty() && | 
 |       !quiche::GetQuicheCommandLineFlag(FLAGS_default_client_cert_key) | 
 |            .empty()) { | 
 |     std::unique_ptr<ClientProofSource> proof_source = | 
 |         CreateTestClientProofSource( | 
 |             quiche::GetQuicheCommandLineFlag(FLAGS_default_client_cert), | 
 |             quiche::GetQuicheCommandLineFlag(FLAGS_default_client_cert_key)); | 
 |     if (proof_source == nullptr) { | 
 |       std::cerr << "Failed to create client proof source." << std::endl; | 
 |       return 1; | 
 |     } | 
 |     client->crypto_config()->set_proof_source(std::move(proof_source)); | 
 |   } | 
 |  | 
 |   int32_t initial_mtu = quiche::GetQuicheCommandLineFlag(FLAGS_initial_mtu); | 
 |   client->set_initial_max_packet_length( | 
 |       initial_mtu != 0 ? initial_mtu : quic::kDefaultMaxPacketSize); | 
 |   client->set_drop_response_body( | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_drop_response_body)); | 
 |   const std::string server_connection_id_hex_string = | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_server_connection_id); | 
 |   QUICHE_CHECK(server_connection_id_hex_string.size() % 2 == 0) | 
 |       << "The length of --server_connection_id must be even. It is " | 
 |       << server_connection_id_hex_string.size() << "-byte long."; | 
 |   if (!server_connection_id_hex_string.empty()) { | 
 |     std::string server_connection_id_bytes; | 
 |     QUICHE_CHECK(absl::HexStringToBytes(server_connection_id_hex_string, | 
 |                                         &server_connection_id_bytes)) | 
 |         << "Failed to parse --server_connection_id hex string."; | 
 |     client->set_server_connection_id_override(QuicConnectionId( | 
 |         server_connection_id_bytes.data(), server_connection_id_bytes.size())); | 
 |   } | 
 |   const int32_t server_connection_id_length = | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_server_connection_id_length); | 
 |   if (server_connection_id_length >= 0) { | 
 |     client->set_server_connection_id_length(server_connection_id_length); | 
 |   } | 
 |   const int32_t client_connection_id_length = | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_client_connection_id_length); | 
 |   if (client_connection_id_length >= 0) { | 
 |     client->set_client_connection_id_length(client_connection_id_length); | 
 |   } | 
 |   const size_t max_inbound_header_list_size = | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_max_inbound_header_list_size); | 
 |   if (max_inbound_header_list_size > 0) { | 
 |     client->set_max_inbound_header_list_size(max_inbound_header_list_size); | 
 |   } | 
 |   const std::string interface_name = | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_interface_name); | 
 |   if (!interface_name.empty()) { | 
 |     client->set_interface_name(interface_name); | 
 |   } | 
 |   const std::string signing_algorithms_pref = | 
 |       quiche::GetQuicheCommandLineFlag(FLAGS_signing_algorithms_pref); | 
 |   if (!signing_algorithms_pref.empty()) { | 
 |     client->SetTlsSignatureAlgorithms(signing_algorithms_pref); | 
 |   } | 
 |   if (!client->Initialize()) { | 
 |     std::cerr << "Failed to initialize client." << std::endl; | 
 |     return 1; | 
 |   } | 
 |   if (!client->Connect()) { | 
 |     quic::QuicErrorCode error = client->session()->error(); | 
 |     if (error == quic::QUIC_INVALID_VERSION) { | 
 |       std::cerr << "Failed to negotiate version with " << host << ":" << port | 
 |                 << ". " << client->session()->error_details() << std::endl; | 
 |       // 0: No error. | 
 |       // 20: Failed to connect due to QUIC_INVALID_VERSION. | 
 |       return quiche::GetQuicheCommandLineFlag(FLAGS_version_mismatch_ok) ? 0 | 
 |                                                                          : 20; | 
 |     } | 
 |     std::cerr << "Failed to connect to " << host << ":" << port << ". " | 
 |               << quic::QuicErrorCodeToString(error) << " " | 
 |               << client->session()->error_details() << std::endl; | 
 |     return 1; | 
 |   } | 
 |  | 
 |   std::cout << "Connected to " << host << ":" << port; | 
 |   if (quiche::GetQuicheCommandLineFlag(FLAGS_output_resolved_server_address)) { | 
 |     std::cout << ", resolved IP " << client->server_address().host().ToString(); | 
 |   } | 
 |   std::cout << std::endl; | 
 |  | 
 |   // Construct the string body from flags, if provided. | 
 |   std::string body = quiche::GetQuicheCommandLineFlag(FLAGS_body); | 
 |   if (!quiche::GetQuicheCommandLineFlag(FLAGS_body_hex).empty()) { | 
 |     QUICHE_DCHECK(quiche::GetQuicheCommandLineFlag(FLAGS_body).empty()) | 
 |         << "Only set one of --body and --body_hex."; | 
 |     const bool success = absl::HexStringToBytes( | 
 |         quiche::GetQuicheCommandLineFlag(FLAGS_body_hex), &body); | 
 |     QUICHE_DCHECK(success) << "Failed to parse --body_hex."; | 
 |   } | 
 |  | 
 |   // Construct a GET or POST request for supplied URL. | 
 |   quiche::HttpHeaderBlock header_block; | 
 |   header_block[":method"] = body.empty() ? "GET" : "POST"; | 
 |   header_block[":scheme"] = url.scheme(); | 
 |   header_block[":authority"] = url.HostPort(); | 
 |   header_block[":path"] = url.PathParamsQuery(); | 
 |  | 
 |   // Append any additional headers supplied on the command line. | 
 |   const std::string headers = quiche::GetQuicheCommandLineFlag(FLAGS_headers); | 
 |   for (absl::string_view sp : absl::StrSplit(headers, ';')) { | 
 |     QuicheTextUtils::RemoveLeadingAndTrailingWhitespace(&sp); | 
 |     if (sp.empty()) { | 
 |       continue; | 
 |     } | 
 |     std::vector<absl::string_view> kv = | 
 |         absl::StrSplit(sp, absl::MaxSplits(':', 1)); | 
 |     QuicheTextUtils::RemoveLeadingAndTrailingWhitespace(&kv[0]); | 
 |     QuicheTextUtils::RemoveLeadingAndTrailingWhitespace(&kv[1]); | 
 |     header_block[kv[0]] = kv[1]; | 
 |   } | 
 |  | 
 |   // Make sure to store the response, for later output. | 
 |   client->set_store_response(true); | 
 |  | 
 |   for (int i = 0; i < num_requests; ++i) { | 
 |     // Send the request. | 
 |     client->SendRequestAndWaitForResponse(header_block, body, /*fin=*/true); | 
 |  | 
 |     // Print request and response details. | 
 |     if (!quiche::GetQuicheCommandLineFlag(FLAGS_quiet)) { | 
 |       std::cout << "Request:" << std::endl; | 
 |       std::cout << "headers:" << header_block.DebugString(); | 
 |       if (!quiche::GetQuicheCommandLineFlag(FLAGS_body_hex).empty()) { | 
 |         // Print the user provided hex, rather than binary body. | 
 |         std::cout << "body:\n" << QuicheTextUtils::HexDump(body) << std::endl; | 
 |       } else { | 
 |         std::cout << "body: " << body << std::endl; | 
 |       } | 
 |       std::cout << std::endl; | 
 |  | 
 |       if (!client->preliminary_response_headers().empty()) { | 
 |         std::cout << "Preliminary response headers: " | 
 |                   << client->preliminary_response_headers() << std::endl; | 
 |         std::cout << std::endl; | 
 |       } | 
 |  | 
 |       std::cout << "Response:" << std::endl; | 
 |       std::cout << "headers: " << client->latest_response_headers() | 
 |                 << std::endl; | 
 |       std::string response_body = client->latest_response_body(); | 
 |       if (!quiche::GetQuicheCommandLineFlag(FLAGS_body_hex).empty()) { | 
 |         // Assume response is binary data. | 
 |         std::cout << "body:\n" | 
 |                   << QuicheTextUtils::HexDump(response_body) << std::endl; | 
 |       } else { | 
 |         std::cout << "body: " << response_body << std::endl; | 
 |       } | 
 |       std::cout << "trailers: " << client->latest_response_trailers() | 
 |                 << std::endl; | 
 |       std::cout << "early data accepted: " << client->EarlyDataAccepted() | 
 |                 << std::endl; | 
 |       QUIC_LOG(INFO) << "Request completed with TTFB(us): " | 
 |                      << client->latest_ttfb().ToMicroseconds() << ", TTLB(us): " | 
 |                      << client->latest_ttlb().ToMicroseconds(); | 
 |     } | 
 |  | 
 |     if (!client->connected()) { | 
 |       std::cerr << "Request caused connection failure. Error: " | 
 |                 << quic::QuicErrorCodeToString(client->session()->error()) | 
 |                 << std::endl; | 
 |       if (!quiche::GetQuicheCommandLineFlag(FLAGS_ignore_errors)) { | 
 |         return 1; | 
 |       } | 
 |     } | 
 |  | 
 |     int response_code = client->latest_response_code(); | 
 |     if (response_code >= 200 && response_code < 300) { | 
 |       std::cout << "Request succeeded (" << response_code << ")." << std::endl; | 
 |     } else if (response_code >= 300 && response_code < 400) { | 
 |       if (quiche::GetQuicheCommandLineFlag(FLAGS_redirect_is_success)) { | 
 |         std::cout << "Request succeeded (redirect " << response_code << ")." | 
 |                   << std::endl; | 
 |       } else { | 
 |         std::cout << "Request failed (redirect " << response_code << ")." | 
 |                   << std::endl; | 
 |         if (!quiche::GetQuicheCommandLineFlag(FLAGS_ignore_errors)) { | 
 |           return 1; | 
 |         } | 
 |       } | 
 |     } else { | 
 |       std::cout << "Request failed (" << response_code << ")." << std::endl; | 
 |       if (!quiche::GetQuicheCommandLineFlag(FLAGS_ignore_errors)) { | 
 |         return 1; | 
 |       } | 
 |     } | 
 |  | 
 |     if (i + 1 < num_requests) {  // There are more requests to perform. | 
 |       if (quiche::GetQuicheCommandLineFlag(FLAGS_one_connection_per_request)) { | 
 |         std::cout << "Disconnecting client between requests." << std::endl; | 
 |         client->Disconnect(); | 
 |         if (!client->Initialize()) { | 
 |           std::cerr << "Failed to reinitialize client between requests." | 
 |                     << std::endl; | 
 |           return 1; | 
 |         } | 
 |         if (!client->Connect()) { | 
 |           std::cerr << "Failed to reconnect client between requests." | 
 |                     << std::endl; | 
 |           if (!quiche::GetQuicheCommandLineFlag(FLAGS_ignore_errors)) { | 
 |             return 1; | 
 |           } | 
 |         } | 
 |       } else if (!quiche::GetQuicheCommandLineFlag( | 
 |                      FLAGS_disable_port_changes)) { | 
 |         // Change the ephemeral port. | 
 |         if (!client->ChangeEphemeralPort()) { | 
 |           std::cerr << "Failed to change ephemeral port." << std::endl; | 
 |           return 1; | 
 |         } | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   return 0; | 
 | } | 
 |  | 
 | }  // namespace quic |