eth/rpc_http_transport.cpp¶
Namespaces¶
| Name |
|---|
| eth |
| eth::rpc |
Source code¶
// Copyright 2026 Genius Ventures, Inc.
// SPDX-License-Identifier: MIT
#include <eth/rpc_http_transport.hpp>
#include <boost/json/serialize.hpp>
#include <boost/asio/ssl.hpp>
#include <openssl/ssl.h>
#include <cctype>
#include <utility>
namespace eth::rpc {
namespace {
namespace asio = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;
namespace ssl = asio::ssl;
using tcp = asio::ip::tcp;
constexpr auto kHttpVersion = 11;
[[nodiscard]] std::optional<std::pair<std::string, std::string>> split_host_port(
std::string_view authority,
bool is_https)
{
const auto colon = authority.rfind(':');
if (colon == std::string_view::npos)
{
return std::pair<std::string, std::string>{
std::string(authority),
is_https ? "443" : "80"};
}
const auto host = authority.substr(0, colon);
const auto port = authority.substr(colon + 1);
if (host.empty() || port.empty())
{
return std::nullopt;
}
return std::pair<std::string, std::string>{std::string(host), std::string(port)};
}
[[nodiscard]] std::optional<std::string> read_body_from_response(const http::response<http::string_body>& res)
{
if (res.result_int() < 200 || res.result_int() >= 300)
{
return std::nullopt;
}
return res.body();
}
[[nodiscard]] std::optional<std::string> read_https_response(
const std::string& host,
const std::string& port,
const std::string& target,
const std::string& body,
const RpcHttpTransportOptions& options,
boost::system::error_code& ec)
{
asio::io_context io;
beast::flat_buffer buffer;
tcp::resolver resolver(io);
const auto results = resolver.resolve(host, port, ec);
if (ec)
{
return std::nullopt;
}
ssl::context ssl_ctx(ssl::context::tls_client);
ssl_ctx.set_default_verify_paths(ec);
{
const char* env_cert_file = std::getenv("SSL_CERT_FILE");
if (env_cert_file != nullptr && env_cert_file[0] != '\0')
{
boost::system::error_code load_ec;
ssl_ctx.load_verify_file(env_cert_file, load_ec);
}
}
{
static const char* kFallbackCaPaths[] = {
"/etc/ssl/cert.pem",
"/opt/homebrew/etc/openssl@3/cert.pem",
"/opt/homebrew/etc/ca-certificates/cert.pem",
"/usr/local/etc/openssl@3/cert.pem",
"/usr/local/etc/openssl/cert.pem",
"/etc/ssl/certs/ca-certificates.crt",
};
for (const auto* ca_path : kFallbackCaPaths)
{
boost::system::error_code load_ec;
ssl_ctx.load_verify_file(ca_path, load_ec);
}
}
ssl::stream<beast::tcp_stream> stream(io, ssl_ctx);
if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
{
return std::nullopt;
}
stream.set_verify_mode(options.verify_peer ? ssl::verify_peer : ssl::verify_none);
if (options.verify_peer)
{
stream.set_verify_callback(ssl::rfc2818_verification(host));
}
beast::get_lowest_layer(stream).expires_after(options.timeout);
beast::get_lowest_layer(stream).connect(results, ec);
if (ec)
{
return std::nullopt;
}
stream.handshake(ssl::stream_base::client, ec);
if (ec)
{
return std::nullopt;
}
http::request<http::string_body> req{http::verb::post, target, kHttpVersion};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.set(http::field::content_type, "application/json");
req.body() = body;
req.prepare_payload();
http::write(stream, req, ec);
if (ec)
{
return std::nullopt;
}
http::response<http::string_body> res;
http::read(stream, buffer, res, ec);
if (ec)
{
return std::nullopt;
}
const auto response_body = read_body_from_response(res);
if (!response_body.has_value())
{
return std::nullopt;
}
stream.shutdown(ec);
return response_body;
}
} // namespace
RpcHttpTransport::RpcHttpTransport(
std::string endpoint_url,
RpcHttpTransportOptions options)
: endpoint_url_(std::move(endpoint_url))
, options_(options)
{
}
std::optional<RpcHttpTransport::ParsedUrl> RpcHttpTransport::parse_url(std::string_view endpoint_url)
{
const auto scheme_end = endpoint_url.find("://");
if (scheme_end == std::string_view::npos)
{
return std::nullopt;
}
ParsedUrl parsed;
parsed.scheme = std::string(endpoint_url.substr(0, scheme_end));
parsed.is_https = parsed.scheme == "https";
if (!parsed.is_https && parsed.scheme != "http")
{
return std::nullopt;
}
const auto authority_begin = scheme_end + 3;
const auto path_begin = endpoint_url.find('/', authority_begin);
const auto authority = endpoint_url.substr(
authority_begin,
path_begin == std::string_view::npos ? std::string_view::npos : path_begin - authority_begin);
if (authority.empty())
{
return std::nullopt;
}
const auto host_port = split_host_port(authority, parsed.is_https);
if (!host_port.has_value())
{
return std::nullopt;
}
parsed.host = std::move(host_port->first);
parsed.port = std::move(host_port->second);
parsed.target = path_begin == std::string_view::npos ? "/" : std::string(endpoint_url.substr(path_begin));
if (parsed.target.empty())
{
parsed.target = "/";
}
return parsed;
}
std::optional<std::string> RpcHttpTransport::call(const boost::json::object& request)
{
const auto parsed = parse_url(endpoint_url_);
if (!parsed.has_value())
{
return std::nullopt;
}
const auto body = boost::json::serialize(request);
if (parsed->is_https)
{
boost::system::error_code ec;
return read_https_response(
parsed->host,
parsed->port,
parsed->target,
body,
options_,
ec);
}
asio::io_context io;
beast::flat_buffer buffer;
boost::system::error_code ec;
tcp::resolver resolver(io);
const auto results = resolver.resolve(parsed->host, parsed->port, ec);
if (ec)
{
return std::nullopt;
}
http::request<http::string_body> req{http::verb::post, parsed->target, kHttpVersion};
req.set(http::field::host, parsed->host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.set(http::field::content_type, "application/json");
req.body() = body;
req.prepare_payload();
beast::tcp_stream stream(io);
stream.expires_after(options_.timeout);
stream.connect(results, ec);
if (ec)
{
return std::nullopt;
}
http::write(stream, req, ec);
if (ec)
{
return std::nullopt;
}
http::response<http::string_body> res;
http::read(stream, buffer, res, ec);
if (ec)
{
return std::nullopt;
}
const auto response_body = read_body_from_response(res);
if (!response_body.has_value())
{
return std::nullopt;
}
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return response_body;
}
} // namespace eth::rpc
Updated on 2026-06-05 at 17:22:19 -0700