blob: db529035f3892fe929105e68f9f7a1c4ed2ab05b [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 create an HTTP/2 server with TLS and
// TCP. 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 <netinet/in.h>
#include <sys/socket.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <utility>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "openssl/base.h"
#include "openssl/bio.h"
#include "openssl/hpke.h"
#include "openssl/ssl.h"
#include "openssl/x509.h"
#include "quiche/quic/core/io/quic_default_event_loop.h"
#include "quiche/quic/core/io/quic_event_loop.h"
#include "quiche/quic/core/io/socket.h"
#include "quiche/quic/core/quic_default_clock.h"
#include "quiche/quic/core/quic_time.h"
#include "quiche/quic/masque/masque_h2_connection.h"
#include "quiche/binary_http/binary_http_message.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_ip_address.h"
#include "quiche/common/quiche_ip_address_family.h"
#include "quiche/common/quiche_socket_address.h"
#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h"
#include "quiche/oblivious_http/oblivious_http_gateway.h"
DEFINE_QUICHE_COMMAND_LINE_FLAG(int32_t, port, 9661,
"The port the MASQUE server will listen on.");
DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, certificate_file, "",
"Path to the certificate chain.");
DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, key_file, "",
"Path to the pkcs8 private key.");
DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, client_root_ca_file, "",
"Path to the PEM file containing root CAs.");
DEFINE_QUICHE_COMMAND_LINE_FLAG(
std::string, ohttp_key, "",
"Hex-encoded bytes of the OHTTP HPKE private key.");
using quiche::BinaryHttpRequest;
using quiche::BinaryHttpResponse;
using quiche::ObliviousHttpGateway;
using quiche::ObliviousHttpHeaderKeyConfig;
using quiche::ObliviousHttpKeyConfigs;
using quiche::ObliviousHttpRequest;
using quiche::ObliviousHttpResponse;
namespace quic {
namespace {
class MasqueOhttpGateway {
public:
MasqueOhttpGateway() {}
bool Setup(const std::string &ohttp_key) {
hpke_key_.reset(EVP_HPKE_KEY_new());
if (!ohttp_key.empty()) {
if (!absl::HexStringToBytes(ohttp_key, &hpke_private_key_)) {
QUICHE_LOG(ERROR) << "OHTTP key is not a valid hex string";
return false;
}
if (EVP_HPKE_KEY_init(
hpke_key_.get(), kem_,
reinterpret_cast<const uint8_t *>(hpke_private_key_.data()),
hpke_private_key_.size()) != 1) {
QUICHE_LOG(ERROR) << "Failed to ingest HPKE key";
return false;
}
} else {
if (EVP_HPKE_KEY_generate(hpke_key_.get(), kem_) != 1) {
QUICHE_LOG(ERROR) << "Failed to generate new HPKE key";
return false;
}
size_t private_key_len = EVP_HPKE_KEM_private_key_len(kem_);
hpke_private_key_ = std::string(private_key_len, '0');
if (EVP_HPKE_KEY_private_key(
hpke_key_.get(),
reinterpret_cast<uint8_t *>(hpke_private_key_.data()),
&private_key_len, private_key_len) != 1 ||
private_key_len != hpke_private_key_.size()) {
QUICHE_LOG(ERROR) << "Failed to extract new HPKE private key";
return false;
}
QUICHE_LOG(INFO) << "Generated new HPKE private key: "
<< absl::BytesToHexString(hpke_private_key_);
}
size_t public_key_len = EVP_HPKE_KEM_public_key_len(kem_);
hpke_public_key_ = std::string(public_key_len, '0');
if (EVP_HPKE_KEY_public_key(
hpke_key_.get(),
reinterpret_cast<uint8_t *>(hpke_public_key_.data()),
&public_key_len, public_key_len) != 1 ||
public_key_len != hpke_public_key_.size()) {
QUICHE_LOG(ERROR) << "Failed to extract new HPKE public key";
return false;
}
static constexpr uint8_t kOhttpKeyId = 0x01;
static constexpr uint16_t kOhttpKemId = EVP_HPKE_DHKEM_X25519_HKDF_SHA256;
static constexpr uint16_t kOhttpKdfId = EVP_HPKE_HKDF_SHA256;
static constexpr uint16_t kOhttpAeadId = EVP_HPKE_AES_128_GCM;
absl::StatusOr<ObliviousHttpHeaderKeyConfig> ohttp_header_key_config =
ObliviousHttpHeaderKeyConfig::Create(kOhttpKeyId, kOhttpKemId,
kOhttpKdfId, kOhttpAeadId);
if (!ohttp_header_key_config.ok()) {
QUICHE_LOG(ERROR) << "Failed to create OHTTP header key config: "
<< ohttp_header_key_config.status();
return false;
}
QUICHE_LOG(INFO) << "Using OHTTP header key config: "
<< ohttp_header_key_config->DebugString();
absl::StatusOr<ObliviousHttpKeyConfigs> ohttp_key_configs =
ObliviousHttpKeyConfigs::Create(*ohttp_header_key_config,
hpke_public_key_);
if (!ohttp_key_configs.ok()) {
QUICHE_LOG(ERROR) << "Failed to create OHTTP key configs: "
<< ohttp_key_configs.status();
return false;
}
QUICHE_LOG(INFO) << "Using OHTTP key configs: " << std::endl
<< ohttp_key_configs->DebugString();
absl::StatusOr<std::string> concatenated_keys =
ohttp_key_configs->GenerateConcatenatedKeys();
if (!concatenated_keys.ok()) {
QUICHE_LOG(ERROR) << "Failed to generate concatenated keys: "
<< concatenated_keys.status();
return false;
}
concatenated_keys_ = *concatenated_keys;
absl::StatusOr<ObliviousHttpGateway> ohttp_gateway =
ObliviousHttpGateway::Create(hpke_private_key_,
*ohttp_header_key_config);
if (!ohttp_gateway.ok()) {
QUICHE_LOG(ERROR) << "Failed to create OHTTP gateway: "
<< ohttp_gateway.status();
return false;
}
ohttp_gateway_.emplace(std::move(*ohttp_gateway));
return true;
}
bool HandleRequest(MasqueH2Connection *connection, int32_t stream_id,
const std::string &encapsulated_request) {
if (!ohttp_gateway_.has_value()) {
QUICHE_LOG(ERROR) << "Not ready to handle OHTTP request";
return false;
}
absl::StatusOr<ObliviousHttpRequest> decrypted_request =
ohttp_gateway_->DecryptObliviousHttpRequest(encapsulated_request);
if (!decrypted_request.ok()) {
QUICHE_LOG(ERROR) << "Failed to decrypt OHTTP request: "
<< decrypted_request.status();
return false;
}
absl::StatusOr<BinaryHttpRequest> binary_request =
BinaryHttpRequest::Create(decrypted_request->GetPlaintextData());
if (!binary_request.ok()) {
QUICHE_LOG(ERROR) << "Failed to parse binary request: "
<< binary_request.status();
return false;
}
const BinaryHttpRequest::ControlData &control_data =
binary_request->control_data();
// TODO(dschinazi): Send the decapsulated request to the authority instead
// of replying with a fake local response.
absl::string_view request_body = binary_request->body();
std::string response_body = absl::StrCat(
"OHTTP Response! Request method: ", control_data.method,
" scheme: ", control_data.scheme, " path: ", control_data.path,
" authority: ", control_data.authority, " body: \"", request_body,
"\"");
BinaryHttpResponse binary_response(/*status_code=*/200);
binary_response.swap_body(response_body);
absl::StatusOr<std::string> encoded_response = binary_response.Serialize();
if (!encoded_response.ok()) {
QUICHE_LOG(ERROR) << "Failed to encode response: "
<< encoded_response.status();
return false;
}
auto context = std::move(*decrypted_request).ReleaseContext();
absl::StatusOr<ObliviousHttpResponse> ohttp_response =
ohttp_gateway_->CreateObliviousHttpResponse(*encoded_response, context);
if (!ohttp_response.ok()) {
QUICHE_LOG(ERROR) << "Failed to create OHTTP response: "
<< ohttp_response.status();
return false;
}
std::string encapsulated_response =
ohttp_response->EncapsulateAndSerialize();
QUICHE_LOG(INFO) << "Sending OHTTP response";
quiche::HttpHeaderBlock response_headers;
response_headers[":status"] = "200";
response_headers["content-type"] = "message/ohttp-res";
connection->SendResponse(stream_id, response_headers,
encapsulated_response);
return true;
}
std::string concatenated_keys() const { return concatenated_keys_; }
private:
std::string hpke_private_key_;
std::string hpke_public_key_;
const EVP_HPKE_KEM *kem_ = EVP_hpke_x25519_hkdf_sha256();
bssl::UniquePtr<EVP_HPKE_KEY> hpke_key_;
std::string concatenated_keys_;
std::optional<ObliviousHttpGateway> ohttp_gateway_;
};
static int SelectAlpnCallback(SSL * /*ssl*/, const uint8_t **out,
uint8_t *out_len, const uint8_t *in,
unsigned in_len, void * /*arg*/) {
unsigned i = 0;
while (i < in_len) {
uint8_t alpn_length = in[i];
if (i + alpn_length > in_len) {
// Client sent a malformed ALPN extension.
break;
}
if (alpn_length == 2 && in[i + 1] == 'h' && in[i + 2] == '2') {
// Found "h2".
*out = in + i + 1;
*out_len = alpn_length;
return SSL_TLSEXT_ERR_OK;
}
i += alpn_length + 1;
}
*out = nullptr;
*out_len = 0;
return SSL_TLSEXT_ERR_ALERT_FATAL;
}
class MasqueH2SocketConnection : public QuicSocketEventListener {
public:
explicit MasqueH2SocketConnection(SocketFd connected_socket,
QuicEventLoop *event_loop, SSL_CTX *ctx,
bool is_server,
MasqueH2Connection::Visitor *visitor)
: socket_(connected_socket),
event_loop_(event_loop),
connection_(CreateSsl(ctx), is_server, visitor) {
if (!event_loop_->RegisterSocket(socket_, kSocketEventReadable, this)) {
QUICHE_LOG(FATAL)
<< "Failed to register connection socket with the event loop";
}
}
~MasqueH2SocketConnection() {
if (socket_ != kInvalidSocketFd) {
if (!event_loop_->UnregisterSocket(socket_)) {
QUICHE_LOG(ERROR) << "Failed to unregister socket";
}
close(socket_);
socket_ = kInvalidSocketFd;
}
}
bool Start() {
connection_.OnTransportReadable();
return !connection_.aborted();
}
// From QuicSocketEventListener.
void OnSocketEvent(QuicEventLoop * /*event_loop*/, SocketFd fd,
QuicSocketEventMask events) {
if (fd != socket_ || ((events & kSocketEventReadable) == 0)) {
return;
}
connection_.OnTransportReadable();
}
MasqueH2Connection *connection() { return &connection_; }
private:
SSL *CreateSsl(SSL_CTX *ctx) {
ssl_.reset(SSL_new(ctx));
SSL_set_accept_state(ssl_.get());
BIO *bio = BIO_new_socket(socket_, BIO_CLOSE);
SSL_set_bio(ssl_.get(), bio, bio);
// `SSL_set_bio` causes `ssl_` to take ownership of `bio`.
return ssl_.get();
}
SocketFd socket_;
bssl::UniquePtr<SSL> ssl_;
QuicEventLoop *event_loop_; // Unowned.
MasqueH2Connection connection_;
};
class MasqueTcpServer : public QuicSocketEventListener,
public MasqueH2Connection::Visitor {
public:
explicit MasqueTcpServer(MasqueOhttpGateway *masque_ohttp_gateway)
: event_loop_(GetDefaultEventLoop()->Create(QuicDefaultClock::Get())),
masque_ohttp_gateway_(masque_ohttp_gateway) {}
MasqueTcpServer(const MasqueTcpServer &) = delete;
MasqueTcpServer(MasqueTcpServer &&) = delete;
MasqueTcpServer &operator=(const MasqueTcpServer &) = delete;
MasqueTcpServer &operator=(MasqueTcpServer &&) = delete;
~MasqueTcpServer() {
if (server_socket_ != kInvalidSocketFd) {
if (!event_loop_->UnregisterSocket(server_socket_)) {
QUICHE_LOG(ERROR) << "Failed to unregister socket";
}
close(server_socket_);
server_socket_ = kInvalidSocketFd;
}
}
bool SetupSslCtx(const std::string &certificate_file,
const std::string &key_file,
const std::string &client_root_ca_file) {
ctx_.reset(SSL_CTX_new(TLS_method()));
if (!SSL_CTX_use_PrivateKey_file(ctx_.get(), key_file.c_str(),
SSL_FILETYPE_PEM)) {
QUICHE_LOG(ERROR) << "Failed to load private key: " << key_file;
return false;
}
if (!SSL_CTX_use_certificate_chain_file(ctx_.get(),
certificate_file.c_str())) {
QUICHE_LOG(ERROR) << "Failed to load cert chain: " << certificate_file;
return false;
}
if (!client_root_ca_file.empty()) {
X509_STORE *store = SSL_CTX_get_cert_store(ctx_.get());
if (store == nullptr) {
QUICHE_LOG(ERROR) << "Failed to get certificate store";
return false;
}
if (X509_STORE_load_locations(store, client_root_ca_file.c_str(),
/*dir=*/nullptr) != 1) {
QUICHE_LOG(ERROR) << "Failed to load client root CA file: "
<< client_root_ca_file;
return false;
}
SSL_CTX_set_verify(ctx_.get(),
SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
/*callback=*/nullptr);
}
SSL_CTX_set_alpn_select_cb(ctx_.get(), &SelectAlpnCallback, this);
SSL_CTX_set_min_proto_version(ctx_.get(), TLS1_2_VERSION);
SSL_CTX_set_max_proto_version(ctx_.get(), TLS1_3_VERSION);
return true;
}
bool SetupSocket(uint16_t server_port) {
if (server_socket_ != kInvalidSocketFd) {
QUICHE_LOG(ERROR) << "Socket already set up";
return false;
}
absl::StatusOr<SocketFd> create_result = socket_api::CreateSocket(
quiche::IpAddressFamily::IP_V6, socket_api::SocketProtocol::kTcp,
/*blocking=*/false);
if (!create_result.ok() || create_result.value() == kInvalidSocketFd) {
QUICHE_LOG(ERROR) << "Failed to create socket: "
<< create_result.status();
return false;
}
server_socket_ = create_result.value();
const int enable = 1;
if (setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR,
(const char *)&enable, sizeof(enable)) < 0) {
QUICHE_LOG(ERROR) << "Failed to set SO_REUSEADDR on socket";
return false;
}
absl::Status bind_result = socket_api::Bind(
server_socket_, quiche::QuicheSocketAddress(
quiche::QuicheIpAddress::Any6(), server_port));
if (!bind_result.ok()) {
QUICHE_LOG(ERROR) << "Failed to bind socket: " << bind_result;
return false;
}
absl::Status listen_result = socket_api::Listen(server_socket_, SOMAXCONN);
if (!listen_result.ok()) {
QUICHE_LOG(ERROR) << "Failed to listen on socket: " << listen_result;
return false;
}
if (!event_loop_->RegisterSocket(server_socket_, kSocketEventReadable,
this)) {
QUICHE_LOG(ERROR) << "Failed to register socket with the event loop";
return false;
}
QUICHE_LOG(INFO) << "Started listening on port " << server_port;
return true;
}
void Run() {
while (true) {
event_loop_->RunEventLoopOnce(QuicTime::Delta::FromMilliseconds(50));
}
}
void OnSocketEvent(QuicEventLoop * /*event_loop*/, SocketFd fd,
QuicSocketEventMask events) override {
if (fd != server_socket_ || ((events & kSocketEventReadable) == 0)) {
return;
}
AcceptConnection();
}
// From MasqueH2Connection::Visitor.
void OnConnectionReady(MasqueH2Connection * /*connection*/) override {}
void OnConnectionFinished(MasqueH2Connection *connection) override {
connections_.erase(
std::remove_if(connections_.begin(), connections_.end(),
[connection](const auto &socket_connection) {
return socket_connection->connection() == connection;
}),
connections_.end());
}
bool HandleOhttpRequest(MasqueH2Connection *connection, int32_t stream_id,
const std::string &encapsulated_request) {
return masque_ohttp_gateway_->HandleRequest(connection, stream_id,
encapsulated_request);
}
void OnRequest(MasqueH2Connection *connection, int32_t stream_id,
const quiche::HttpHeaderBlock &headers,
const std::string &body) override {
quiche::HttpHeaderBlock response_headers;
std::string response_body;
auto path_pair = headers.find(":path");
auto method_pair = headers.find(":method");
auto content_type_pair = headers.find("content-type");
if (path_pair == headers.end() || method_pair == headers.end()) {
// This should never happen because the h2 adapter should have rejected
// the request, but handle it gracefully just in case.
response_headers[":status"] = "400";
response_body = "Request missing pseudo-headers";
} else if (method_pair->second == "GET" &&
content_type_pair != headers.end() &&
content_type_pair->second == "application/ohttp-keys") {
response_headers[":status"] = "200";
response_headers["content-type"] = "application/ohttp-keys";
response_body = masque_ohttp_gateway_->concatenated_keys();
} else if (method_pair->second == "POST" &&
content_type_pair != headers.end() &&
content_type_pair->second == "message/ohttp-req") {
if (HandleOhttpRequest(connection, stream_id, body)) {
return;
} else {
response_headers[":status"] = "500";
response_body = "Failed to handle OHTTP request";
}
} else if (method_pair->second == "GET" && path_pair->second == "/") {
response_headers[":status"] = "200";
response_body = "<h1>This is a response body</h1>";
} else {
response_headers[":status"] = "404";
response_body = "Path not found";
}
connection->SendResponse(stream_id, response_headers, response_body);
}
void OnResponse(MasqueH2Connection * /*connection*/, int32_t /*stream_id*/,
const quiche::HttpHeaderBlock & /*headers*/,
const std::string & /*body*/) override {
QUICHE_LOG(FATAL) << "Server cannot receive responses";
}
private:
void AcceptConnection() {
absl::StatusOr<socket_api::AcceptResult> accept_result =
socket_api::Accept(server_socket_, /*blocking=*/false);
if (!accept_result.ok()) {
QUICHE_LOG(ERROR) << "Failed to accept connection: "
<< accept_result.status();
return;
}
QUICHE_LOG(INFO) << "Accepted TCP connection from "
<< accept_result.value().peer_address;
// `connection` takes ownership of the socket.
auto connection = std::make_unique<MasqueH2SocketConnection>(
accept_result.value().fd, event_loop_.get(), ctx_.get(),
/*is_server=*/true, this);
if (!connection->Start()) {
QUICHE_LOG(ERROR) << "Failed to start connection handler from "
<< accept_result.value().peer_address;
return;
}
QUICHE_LOG(INFO) << "Started connection from "
<< accept_result.value().peer_address;
connections_.push_back(std::move(connection));
}
std::unique_ptr<QuicEventLoop> event_loop_;
bssl::UniquePtr<SSL_CTX> ctx_;
MasqueOhttpGateway *masque_ohttp_gateway_; // Unowned.
SocketFd server_socket_ = kInvalidSocketFd;
std::vector<std::unique_ptr<MasqueH2SocketConnection>> connections_;
};
int RunMasqueTcpServer(int argc, char *argv[]) {
const char *usage = "Usage: masque_server [options]";
std::vector<std::string> non_option_args =
quiche::QuicheParseCommandLineFlags(usage, argc, argv);
if (!non_option_args.empty()) {
quiche::QuichePrintCommandLineFlagHelp(usage);
return 1;
}
std::string certificate_file =
quiche::GetQuicheCommandLineFlag(FLAGS_certificate_file);
if (certificate_file.empty()) {
QUICHE_LOG(ERROR) << "--certificate_file cannot be empty";
return 1;
}
std::string key_file = quiche::GetQuicheCommandLineFlag(FLAGS_key_file);
if (key_file.empty()) {
QUICHE_LOG(ERROR) << "--key_file cannot be empty";
return 1;
}
std::string client_root_ca_file =
quiche::GetQuicheCommandLineFlag(FLAGS_client_root_ca_file);
quiche::QuicheSystemEventLoop system_event_loop("masque_tcp_server");
MasqueOhttpGateway masque_ohttp_gateway;
if (!masque_ohttp_gateway.Setup(
quiche::GetQuicheCommandLineFlag(FLAGS_ohttp_key))) {
QUICHE_LOG(ERROR) << "Failed to setup OHTTP";
return 1;
}
MasqueTcpServer server(&masque_ohttp_gateway);
if (!server.SetupSslCtx(certificate_file, key_file, client_root_ca_file)) {
QUICHE_LOG(ERROR) << "Failed to setup SSL context";
return 1;
}
if (!server.SetupSocket(quiche::GetQuicheCommandLineFlag(FLAGS_port))) {
QUICHE_LOG(ERROR) << "Failed to setup socket";
return 1;
}
server.Run();
return 0;
}
} // namespace
} // namespace quic
int main(int argc, char *argv[]) {
return quic::RunMasqueTcpServer(argc, argv);
}