blob: 3f9d77457b8cc02994d860b7e7fe97a3a9549dfb [file] [log] [blame] [edit]
// 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);
}