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();
}
// Beast's expires_after() only governs async operations, so the former sync
// connect/handshake/write/read chain could block a caller forever on a stalled
// peer (io_context threads then never joined at shutdown). One deadline timer
// bounds the whole exchange, including DNS resolution.
template <typename Stream, typename Handshake>
[[nodiscard]] std::optional<std::string> exchange(
asio::io_context& io,
Stream& stream,
const std::string& host,
const std::string& port,
http::request<http::string_body> req,
std::chrono::seconds timeout,
Handshake handshake)
{
tcp::resolver resolver(io);
asio::steady_timer deadline(io);
beast::flat_buffer buffer;
http::response<http::string_body> res;
std::optional<std::string> result;
deadline.expires_after(timeout);
deadline.async_wait([&](boost::system::error_code ec)
{
if (!ec)
{
resolver.cancel();
beast::get_lowest_layer(stream).close();
}
});
auto done = [&]() { deadline.cancel(); };
resolver.async_resolve(host, port, [&](boost::system::error_code ec, tcp::resolver::results_type results)
{
if (ec) { return done(); }
beast::get_lowest_layer(stream).async_connect(results, [&](boost::system::error_code ec, auto)
{
if (ec) { return done(); }
handshake([&](boost::system::error_code ec)
{
if (ec) { return done(); }
http::async_write(stream, req, [&](boost::system::error_code ec, std::size_t)
{
if (ec) { return done(); }
http::async_read(stream, buffer, res, [&](boost::system::error_code ec, std::size_t)
{
if (!ec)
{
result = read_body_from_response(res);
}
done();
});
});
});
});
});
io.run();
return result;
}
[[nodiscard]] http::request<http::string_body> make_request(
http::verb verb,
const std::string& host,
const std::string& target,
const std::string& body)
{
http::request<http::string_body> req{verb, target, kHttpVersion};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
if (verb != http::verb::get && verb != http::verb::head)
{
req.set(http::field::content_type, "application/json");
req.body() = body;
}
req.prepare_payload();
return req;
}
[[nodiscard]] std::optional<std::string> read_https_response(
http::verb verb,
const std::string& host,
const std::string& port,
const std::string& target,
const std::string& body,
const RpcHttpTransportOptions& options)
{
asio::io_context io;
boost::system::error_code ec;
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));
}
return exchange(io, stream, host, port, make_request(verb, host, target, body), options.timeout,
[&](auto&& on_done) { stream.async_handshake(ssl::stream_base::client, on_done); });
}
} // 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)
{
return read_https_response(
http::verb::post,
parsed->host,
parsed->port,
parsed->target,
body,
options_);
}
asio::io_context io;
beast::tcp_stream stream(io);
return exchange(io, stream, parsed->host, parsed->port,
make_request(http::verb::post, parsed->host, parsed->target, body), options_.timeout,
[](auto&& on_done) { on_done(boost::system::error_code{}); });
}
std::optional<std::string> RpcHttpTransport::HttpsGet(
const std::string& url,
const RpcHttpTransportOptions& options)
{
const auto parsed = parse_url(url);
if ( !parsed.has_value() || !parsed->is_https )
{
return std::nullopt;
}
return read_https_response(
http::verb::get,
parsed->host,
parsed->port,
parsed->target,
{},
options );
}
} // namespace eth::rpc
Updated on 2026-09-25 at 15:46:10 +0000