| // 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. |
| |
| // This binary contains minimal code to send an HTTP/1.1 over TLS over TCP |
| // request. It will be refactored to allow layering, with the goal of being able |
| // to use MASQUE over HTTP/2, and CONNECT in our MASQUE code. |
| |
| #include <fcntl.h> |
| #include <netdb.h> |
| #include <poll.h> |
| #include <stdbool.h> |
| #include <sys/socket.h> |
| |
| #include <cerrno> |
| #include <cstdint> |
| #include <cstdio> |
| #include <cstring> |
| #include <iostream> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "absl/status/status.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 "openssl/bio.h" |
| #include "openssl/pool.h" |
| #include "openssl/ssl.h" |
| #include "openssl/stack.h" |
| #include "quiche/quic/core/connecting_client_socket.h" |
| #include "quiche/quic/core/crypto/proof_verifier.h" |
| #include "quiche/quic/core/io/event_loop_socket_factory.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/core/quic_types.h" |
| #include "quiche/quic/masque/masque_h2_connection.h" |
| #include "quiche/quic/platform/api/quic_default_proof_providers.h" |
| #include "quiche/quic/platform/api/quic_socket_address.h" |
| #include "quiche/quic/tools/fake_proof_verifier.h" |
| #include "quiche/quic/tools/quic_name_lookup.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/platform/api/quiche_system_event_loop.h" |
| #include "quiche/common/quiche_mem_slice.h" |
| #include "quiche/common/quiche_text_utils.h" |
| #include "quiche/common/simple_buffer_allocator.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."); |
| |
| namespace quic { |
| namespace { |
| |
| std::optional<bssl::UniquePtr<SSL_CTX>> CreateSslCtx( |
| const std::string &client_cert_file, |
| const std::string &client_cert_key_file) { |
| if (client_cert_file.empty() != client_cert_key_file.empty()) { |
| QUICHE_LOG(ERROR) << "Both private key and certificate chain are required " |
| "when using client certificates"; |
| return std::nullopt; |
| } |
| bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method())); |
| |
| if (!client_cert_key_file.empty() && |
| !SSL_CTX_use_PrivateKey_file(ctx.get(), client_cert_key_file.c_str(), |
| SSL_FILETYPE_PEM)) { |
| QUICHE_LOG(ERROR) << "Failed to load client certificate private key: " |
| << client_cert_key_file; |
| return std::nullopt; |
| } |
| if (!client_cert_file.empty() && !SSL_CTX_use_certificate_chain_file( |
| ctx.get(), client_cert_file.c_str())) { |
| QUICHE_LOG(ERROR) << "Failed to load client certificate chain: " |
| << client_cert_file; |
| return std::nullopt; |
| } |
| |
| SSL_CTX_set_min_proto_version(ctx.get(), TLS1_2_VERSION); |
| SSL_CTX_set_max_proto_version(ctx.get(), TLS1_3_VERSION); |
| |
| return ctx; |
| } |
| |
| class MasqueTlsTcpClientHandler : public ConnectingClientSocket::AsyncVisitor, |
| public MasqueH2Connection::Visitor { |
| public: |
| explicit MasqueTlsTcpClientHandler(QuicEventLoop *event_loop, SSL_CTX *ctx, |
| QuicUrl url, |
| bool disable_certificate_verification, |
| int address_family_for_lookup) |
| : event_loop_(event_loop), |
| ctx_(ctx), |
| socket_factory_(std::make_unique<EventLoopSocketFactory>( |
| event_loop_, quiche::SimpleBufferAllocator::Get())), |
| url_(url), |
| disable_certificate_verification_(disable_certificate_verification), |
| address_family_for_lookup_(address_family_for_lookup) {} |
| |
| ~MasqueTlsTcpClientHandler() override { |
| if (socket_) { |
| socket_->Disconnect(); |
| } |
| } |
| |
| bool Start() { |
| if (disable_certificate_verification_) { |
| proof_verifier_ = std::make_unique<FakeProofVerifier>(); |
| } else { |
| proof_verifier_ = CreateDefaultProofVerifier(url_.host()); |
| } |
| socket_address_ = tools::LookupAddress( |
| address_family_for_lookup_, url_.host(), absl::StrCat(url_.port())); |
| if (!socket_address_.IsInitialized()) { |
| QUICHE_LOG(ERROR) << "Failed to resolve address for \"" << url_.host() |
| << "\""; |
| return false; |
| } |
| socket_ = socket_factory_->CreateTcpClientSocket(socket_address_, |
| /*receive_buffer_size=*/0, |
| /*send_buffer_size=*/0, |
| /*async_visitor=*/this); |
| if (!socket_) { |
| QUICHE_LOG(ERROR) << "Failed to create TCP socket for " |
| << socket_address_; |
| return false; |
| } |
| socket_->ConnectAsync(); |
| return true; |
| } |
| |
| static enum ssl_verify_result_t VerifyCallback(SSL *ssl, uint8_t *out_alert) { |
| return static_cast<MasqueTlsTcpClientHandler *>(SSL_get_app_data(ssl)) |
| ->VerifyCertificate(out_alert); |
| } |
| |
| enum ssl_verify_result_t VerifyCertificate(uint8_t *out_alert) { |
| const STACK_OF(CRYPTO_BUFFER) *cert_chain = |
| SSL_get0_peer_certificates(ssl_.get()); |
| if (cert_chain == nullptr) { |
| QUICHE_LOG(ERROR) << "No certificate chain"; |
| *out_alert = SSL_AD_INTERNAL_ERROR; |
| return ssl_verify_invalid; |
| } |
| std::vector<std::string> certs; |
| for (CRYPTO_BUFFER *cert : cert_chain) { |
| certs.push_back( |
| std::string(reinterpret_cast<const char *>(CRYPTO_BUFFER_data(cert)), |
| CRYPTO_BUFFER_len(cert))); |
| } |
| const uint8_t *ocsp_response_raw; |
| size_t ocsp_response_len; |
| SSL_get0_ocsp_response(ssl_.get(), &ocsp_response_raw, &ocsp_response_len); |
| std::string ocsp_response(reinterpret_cast<const char *>(ocsp_response_raw), |
| ocsp_response_len); |
| const uint8_t *sct_list_raw; |
| size_t sct_list_len; |
| SSL_get0_signed_cert_timestamp_list(ssl_.get(), &sct_list_raw, |
| &sct_list_len); |
| std::string cert_sct(reinterpret_cast<const char *>(sct_list_raw), |
| sct_list_len); |
| std::string error_details; |
| std::unique_ptr<ProofVerifyDetails> details; |
| QuicAsyncStatus verify_status = proof_verifier_->VerifyCertChain( |
| url_.host(), url_.port(), certs, ocsp_response, cert_sct, |
| /*context=*/nullptr, &error_details, &details, out_alert, |
| /*callback=*/nullptr); |
| if (verify_status != QUIC_SUCCESS) { |
| // TODO(dschinazi) properly handle QUIC_PENDING. |
| QUICHE_LOG(ERROR) << "Failed to verify certificate" |
| << (verify_status == QUIC_PENDING ? " (pending)" : "") |
| << ": " << error_details; |
| return ssl_verify_invalid; |
| } |
| QUICHE_LOG(INFO) << "Successfully verified certificate"; |
| return ssl_verify_ok; |
| } |
| |
| // From ConnectingClientSocket::AsyncVisitor. |
| void ConnectComplete(absl::Status status) override { |
| if (!status.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to TCP connect to " << socket_address_ |
| << ": " << status; |
| done_ = true; |
| return; |
| } |
| |
| QUICHE_LOG(INFO) << "TCP connected to " << socket_address_; |
| |
| ssl_.reset((SSL_new(ctx_))); |
| |
| if (SSL_set_app_data(ssl_.get(), this) != 1) { |
| QUICHE_LOG(FATAL) << "SSL_set_app_data failed"; |
| return; |
| } |
| SSL_set_custom_verify(ssl_.get(), SSL_VERIFY_PEER, &VerifyCallback); |
| |
| if (SSL_set_tlsext_host_name(ssl_.get(), url_.host().c_str()) != 1) { |
| QUICHE_LOG(FATAL) << "SSL_set_tlsext_host_name failed"; |
| return; |
| } |
| |
| static constexpr uint8_t kAlpnProtocols[] = { |
| 0x02, 'h', '2', // h2 |
| 0x08, 'h', 't', 't', 'p', '/', '1', '.', '1', // http/1.1 |
| }; |
| if (SSL_set_alpn_protos(ssl_.get(), kAlpnProtocols, |
| sizeof(kAlpnProtocols)) != 0) { |
| QUICHE_LOG(FATAL) << "SSL_set_alpn_protos failed"; |
| return; |
| } |
| |
| BIO *tls_io = nullptr; |
| if (BIO_new_bio_pair(&transport_io_, kBioBufferSize, &tls_io, |
| kBioBufferSize) != 1) { |
| QUICHE_LOG(FATAL) << "BIO_new_bio_pair failed"; |
| return; |
| } |
| SSL_set_bio(ssl_.get(), tls_io, tls_io); |
| BIO_free(tls_io); |
| |
| int ret = SSL_connect(ssl_.get()); |
| if (ret != 1) { |
| int ssl_err = SSL_get_error(ssl_.get(), ret); |
| if (ssl_err == SSL_ERROR_WANT_READ) { |
| QUICHE_DVLOG(1) << "SSL_connect will require another read"; |
| SendToTransport(); |
| socket_->ReceiveAsync(kBioBufferSize); |
| return; |
| } |
| PrintSSLError("Error while TLS connecting", ssl_err, ret); |
| done_ = true; |
| return; |
| } |
| QUICHE_LOG(INFO) << "TLS connected"; |
| |
| tls_connected_ = true; |
| MaybeSendRequest(); |
| socket_->ReceiveAsync(kBioBufferSize); |
| } |
| |
| void ReceiveComplete(absl::StatusOr<quiche::QuicheMemSlice> data) override { |
| if (!data.ok()) { |
| QUICHE_LOG(ERROR) << "Failed to receive transport data: " |
| << data.status(); |
| done_ = true; |
| return; |
| } |
| if (data->empty()) { |
| QUICHE_LOG(INFO) << "Transport read closed"; |
| done_ = true; |
| return; |
| } |
| QUICHE_DVLOG(1) << "Transport received " << data->length() << " bytes"; |
| int write_ret = BIO_write(transport_io_, data->data(), data->length()); |
| if (write_ret < 0) { |
| QUICHE_LOG(ERROR) << "Failed to write data from transport to TLS"; |
| int ssl_err = SSL_get_error(ssl_.get(), write_ret); |
| PrintSSLError("Error while writing data from transport to TLS", ssl_err, |
| write_ret); |
| done_ = true; |
| return; |
| } |
| if (write_ret != static_cast<int>(data->length())) { |
| QUICHE_LOG(ERROR) << "Short write from transport to TLS: " << write_ret |
| << " != " << data->length(); |
| done_ = true; |
| return; |
| } |
| QUICHE_DVLOG(1) << "Wrote " << data->length() |
| << " bytes from transport to TLS"; |
| if (h2_selected_) { |
| h2_connection_->OnTransportReadable(); |
| socket_->ReceiveAsync(kBioBufferSize); |
| return; |
| } |
| int handshake_ret = SSL_do_handshake(ssl_.get()); |
| if (handshake_ret != 1) { |
| int ssl_err = SSL_get_error(ssl_.get(), handshake_ret); |
| if (ssl_err == SSL_ERROR_WANT_READ) { |
| SendToTransport(); |
| socket_->ReceiveAsync(kBioBufferSize); |
| return; |
| } |
| PrintSSLError("Error while performing TLS handshake", ssl_err, |
| handshake_ret); |
| done_ = true; |
| return; |
| } |
| tls_connected_ = true; |
| MaybeSendRequest(); |
| uint8_t buffer[kBioBufferSize] = {}; |
| while (true) { |
| int ssl_read_ret = SSL_read(ssl_.get(), buffer, sizeof(buffer) - 1); |
| if (ssl_read_ret < 0) { |
| int ssl_err = SSL_get_error(ssl_.get(), ssl_read_ret); |
| if (ssl_err == SSL_ERROR_WANT_READ) { |
| SendToTransport(); |
| socket_->ReceiveAsync(kBioBufferSize); |
| return; |
| } |
| PrintSSLError("Error while reading from TLS", ssl_err, ssl_read_ret); |
| done_ = true; |
| return; |
| } |
| if (ssl_read_ret == 0) { |
| QUICHE_LOG(INFO) << "TLS read closed"; |
| done_ = true; |
| return; |
| } |
| if (!h2_selected_) { |
| QUICHE_DVLOG(1) << "TLS read " << ssl_read_ret |
| << " bytes of h1 response"; |
| std::cout << buffer << std::endl; |
| done_ = true; |
| return; |
| } |
| } |
| } |
| |
| void SendComplete(absl::Status status) override { |
| if (!status.ok()) { |
| QUICHE_LOG(ERROR) << "Transport send failed: " << status; |
| done_ = true; |
| return; |
| } |
| SendToTransport(); |
| } |
| |
| bool IsDone() const { return done_; } |
| |
| // From MasqueH2Connection::Visitor. |
| void OnConnectionReady(MasqueH2Connection * /*connection*/) override {} |
| void OnConnectionFinished(MasqueH2Connection * /*connection*/) override { |
| done_ = true; |
| } |
| |
| void OnRequest(MasqueH2Connection * /*connection*/, int32_t /*stream_id*/, |
| const quiche::HttpHeaderBlock & /*headers*/, |
| const std::string & /*body*/) override { |
| QUICHE_LOG(FATAL) << "Client cannot receive requests"; |
| } |
| |
| void OnResponse(MasqueH2Connection *connection, int32_t stream_id, |
| const quiche::HttpHeaderBlock &headers, |
| const std::string &body) override { |
| if (connection != h2_connection_.get()) { |
| QUICHE_LOG(FATAL) << "Unexpected connection"; |
| } |
| if (stream_id != stream_id_) { |
| QUICHE_LOG(FATAL) << "Unexpected stream id"; |
| } |
| QUICHE_LOG(INFO) << "Received h2 response headers: " |
| << headers.DebugString() << " body: " << body; |
| done_ = true; |
| } |
| |
| private: |
| void MaybeSendRequest() { |
| if (request_sent_ || done_ || !tls_connected_) { |
| return; |
| } |
| const uint8_t *alpn_data; |
| unsigned alpn_len; |
| SSL_get0_alpn_selected(ssl_.get(), &alpn_data, &alpn_len); |
| if (alpn_len != 0) { |
| std::string alpn(reinterpret_cast<const char *>(alpn_data), alpn_len); |
| if (alpn == "h2") { |
| h2_selected_ = true; |
| } |
| QUICHE_DVLOG(1) << "ALPN selected: " |
| << std::string(reinterpret_cast<const char *>(alpn_data), |
| alpn_len); |
| } else { |
| QUICHE_DVLOG(1) << "No ALPN selected"; |
| } |
| QUICHE_LOG(INFO) << "Using " << (h2_selected_ ? "h2" : "http/1.1"); |
| if (h2_selected_) { |
| SendH2Request(); |
| } else { |
| SendH1Request(); |
| } |
| request_sent_ = true; |
| } |
| |
| void SendToTransport() { |
| char buffer[kBioBufferSize]; |
| int read_ret = BIO_read(transport_io_, buffer, sizeof(buffer)); |
| if (read_ret == 0) { |
| QUICHE_LOG(ERROR) << "TCP closed while TLS waiting for handshake read"; |
| } else if (read_ret < 0) { |
| int ssl_err = SSL_get_error(ssl_.get(), read_ret); |
| if (ssl_err == SSL_ERROR_WANT_READ) { |
| QUICHE_DVLOG(1) << "TLS needs more bytes from underlying socket"; |
| } else if (ssl_err == SSL_ERROR_SYSCALL && errno == 0) { |
| QUICHE_DVLOG(1) << "TLS recoverable failure from underlying socket"; |
| } else { |
| PrintSSLError("Error while reading from transport_io_", ssl_err, |
| read_ret); |
| } |
| } else { |
| QUICHE_DVLOG(1) << "TLS wrote " << read_ret << " bytes to transport"; |
| socket_->SendAsync(std::string(buffer, read_ret)); |
| } |
| } |
| |
| int WriteDataToTls(absl::string_view data) { |
| QUICHE_DVLOG(2) << "Writing " << data.size() |
| << " app bytes to TLS:" << std::endl |
| << quiche::QuicheTextUtils::HexDump(data); |
| int ssl_write_ret = SSL_write(ssl_.get(), data.data(), data.size()); |
| if (ssl_write_ret <= 0) { |
| int ssl_err = SSL_get_error(ssl_.get(), ssl_write_ret); |
| PrintSSLError("Error while writing request to TLS", ssl_err, |
| ssl_write_ret); |
| done_ = true; |
| return -1; |
| } else { |
| if (ssl_write_ret == static_cast<int>(data.size())) { |
| QUICHE_DVLOG(1) << "Wrote " << data.size() << " bytes to TLS"; |
| } else { |
| QUICHE_DVLOG(1) << "Wrote " << ssl_write_ret << " / " << data.size() |
| << "bytes to TLS"; |
| } |
| SendToTransport(); |
| } |
| return ssl_write_ret; |
| } |
| |
| void SendH1Request() { |
| std::string request = absl::StrCat("GET ", url_.path(), |
| " HTTP/1.1\r\nHost: ", url_.HostPort(), |
| "\r\nConnection: close\r\n\r\n"); |
| QUICHE_DVLOG(1) << "Sending h1 request of length " << request.size() |
| << " to TLS"; |
| int write_res = WriteDataToTls(request); |
| if (write_res < 0) { |
| QUICHE_LOG(ERROR) << "Failed to write request to TLS"; |
| done_ = true; |
| return; |
| } else if (write_res != static_cast<int>(request.size())) { |
| QUICHE_LOG(ERROR) << "Request TLS short write " << write_res << " < " |
| << request.size(); |
| done_ = true; |
| return; |
| } |
| } |
| |
| void SendH2Request() { |
| h2_connection_ = |
| std::make_unique<MasqueH2Connection>(ssl_.get(), |
| /*is_server=*/false, this); |
| h2_connection_->OnTransportReadable(); |
| quiche::HttpHeaderBlock headers; |
| headers[":method"] = "GET"; |
| headers[":scheme"] = url_.scheme(); |
| headers[":authority"] = url_.HostPort(); |
| headers[":path"] = url_.path(); |
| headers["host"] = url_.HostPort(); |
| stream_id_ = h2_connection_->SendRequest(headers, std::string()); |
| h2_connection_->AttemptToSend(); |
| if (stream_id_ >= 0) { |
| QUICHE_LOG(INFO) << "Wrote h2 request to stream " << stream_id_ |
| << ", now sending to transport"; |
| SendToTransport(); |
| } else { |
| QUICHE_LOG(ERROR) << "Failed to send h2 request"; |
| done_ = true; |
| } |
| } |
| |
| static constexpr size_t kBioBufferSize = 16384; |
| QuicEventLoop *event_loop_; // Not owned. |
| SSL_CTX *ctx_; // Not owned. |
| std::unique_ptr<EventLoopSocketFactory> socket_factory_; |
| QuicUrl url_; |
| bool disable_certificate_verification_; |
| int address_family_for_lookup_; |
| std::unique_ptr<ProofVerifier> proof_verifier_; |
| QuicSocketAddress socket_address_; |
| std::unique_ptr<ConnectingClientSocket> socket_; |
| BIO *transport_io_ = nullptr; |
| bssl::UniquePtr<SSL> ssl_; |
| bool tls_connected_ = false; |
| bool h2_selected_ = false; |
| bool request_sent_ = false; |
| bool done_ = false; |
| int32_t stream_id_ = -1; |
| std::unique_ptr<MasqueH2Connection> h2_connection_; |
| }; |
| |
| int RunMasqueTcpClient(int argc, char *argv[]) { |
| const char *usage = "Usage: masque_tcp_client <url>"; |
| std::vector<std::string> urls = |
| quiche::QuicheParseCommandLineFlags(usage, argc, argv); |
| if (urls.size() != 1) { |
| quiche::QuichePrintCommandLineFlagHelp(usage); |
| return 1; |
| } |
| |
| quiche::QuicheSystemEventLoop system_event_loop("masque_client"); |
| const bool disable_certificate_verification = |
| quiche::GetQuicheCommandLineFlag(FLAGS_disable_certificate_verification); |
| |
| std::optional<bssl::UniquePtr<SSL_CTX>> ssl_ctx = CreateSslCtx( |
| quiche::GetQuicheCommandLineFlag(FLAGS_client_cert_file), |
| quiche::GetQuicheCommandLineFlag(FLAGS_client_cert_key_file)); |
| if (!ssl_ctx.has_value()) { |
| 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()); |
| |
| QuicUrl url(urls[0], "https"); |
| if (url.host().empty() && !absl::StrContains(urls[0], "://")) { |
| url = QuicUrl(absl::StrCat("https://", urls[0])); |
| } |
| if (url.host().empty()) { |
| QUICHE_LOG(ERROR) << "Failed to parse URL \"" << urls[0] << "\""; |
| return 1; |
| } |
| |
| MasqueTlsTcpClientHandler tls_handler(event_loop.get(), ssl_ctx->get(), url, |
| disable_certificate_verification, |
| address_family_for_lookup); |
| if (!tls_handler.Start()) { |
| return 1; |
| } |
| while (!tls_handler.IsDone()) { |
| event_loop->RunEventLoopOnce(QuicTime::Delta::FromMilliseconds(50)); |
| } |
| |
| return 0; |
| } |
| |
| } // namespace |
| |
| } // namespace quic |
| |
| int main(int argc, char *argv[]) { |
| return quic::RunMasqueTcpClient(argc, argv); |
| } |