Files
MicrOBU/obu-firmware/external/vanetza-idf/tools/pki/http.cpp
T
Ashin Walpola d107534eb2 Keep vanetza-idf in obu-firmware, so a plain clone builds the firmware
obu-firmware builds against the vanetza-idf C-ITS library, which until now
came from the colleague's microbu-esp32c5 tree beside the repository and was
not tracked here, so a clone of this repository could not build the firmware
it ships. The library alone is now part of obu-firmware, as
obu-firmware/external/vanetza-idf: their external/vanetza-idf at commit
cf4b99f, unchanged (9775 files; see its PROVENANCE.md). CMake takes it from
there by default; -DVANETZA_IDF_DIR still points the build elsewhere.

The rest of the colleague's tree (their own VAM firmware, PKI tooling,
station-link Python tools, the V2X2MAP bridge) stays out of this repository
and gitignored; nothing is pushed to their repository. NOTES.md, docs/06,
TODO.md and the pcap verifier's usage line point at the new location.
2026-09-24 10:56:05 +02:00

364 lines
12 KiB
C++

#include "http.hpp"
#include <boost/asio/connect.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/ssl.hpp>
#include <chrono>
#include <functional>
#include <ostream>
#include <regex>
namespace vanetza
{
namespace pki
{
namespace asio = boost::asio;
namespace beast = boost::beast;
namespace ssl = boost::asio::ssl;
std::ostream& operator<<(std::ostream& os, const HttpException& e)
{
os << e.what();
if (const auto& response = e.response()) {
os << " (HTTP " << response->result_int();
if (!response->body().empty()) {
os << ": " << response->body();
}
os << ")";
}
return os;
}
HttpQuery HttpQuery::from_url(const std::string& url)
{
std::regex url_regex { "^(https?)://([^:/]+)(?::([[:digit:]]+))?(/?|/.*)$" };
std::smatch match;
if (std::regex_match(url, match, url_regex) && match.size() == 5) {
HttpQuery query;
query.secure = (match[1] == "https");
query.host = match[2];
query.path = match[4];
if (match[3].matched) {
query.service = match[3];
} else {
query.service = match[1];
}
return query;
} else {
throw HttpException("URL is not acceptable");
}
}
std::string resolve_url(const std::string& base, const std::string& reference)
{
if (reference.empty()) {
return base;
}
// A reference with its own scheme is already absolute (RFC 3986 §5.2.2).
static const std::regex scheme_re { "^[a-zA-Z][a-zA-Z0-9+.-]*://" };
if (std::regex_search(reference, scheme_re)) {
return reference;
}
// Split the base into scheme, authority and path (RFC 3986 §3); drop any query/fragment.
static const std::regex base_re { "^([a-zA-Z][a-zA-Z0-9+.-]*)://([^/?#]*)(/[^?#]*)?.*$" };
std::smatch m;
if (!std::regex_match(base, m, base_re)) {
throw HttpException("base URL is not acceptable");
}
const std::string scheme = m[1];
const std::string origin = scheme + "://" + std::string(m[2]); // scheme + authority
const std::string base_path = m[3].matched ? std::string(m[3]) : "/";
if (reference.compare(0, 2, "//") == 0) {
// network-path reference: keep the base scheme, replace authority and path
return scheme + ":" + reference;
} else if (reference.front() == '/') {
// absolute-path reference: replace the whole path
return origin + reference;
} else {
// relative-path reference: merge onto the base's directory
const std::string dir = base_path.substr(0, base_path.find_last_of('/') + 1);
return origin + dir + reference;
}
}
const std::string& HttpQuery::which_service() const
{
if (service.empty()) {
static const std::string http_service = "http";
static const std::string https_service = "https";
return secure ? https_service : http_service;
} else {
return service;
}
}
namespace
{
// resolve and connect lifecycle of one address family taking part in a Happy Eyeballs race
class Family
{
public:
explicit Family(asio::io_context& ctx) : m_socket(ctx) {}
bool resolved() const { return static_cast<bool>(m_addresses); }
bool connected() const { return m_error && !*m_error; }
bool failed() const { return m_error && *m_error; }
boost::system::error_code error() const { return m_error.value_or(boost::system::error_code {}); }
void resolve(asio::ip::tcp::resolver& resolver, const asio::ip::tcp& protocol,
const std::string& host, const std::string& service, const std::function<void()>& notify)
{
resolver.async_resolve(protocol, host, service,
[this, notify](boost::system::error_code ec, asio::ip::tcp::resolver::results_type results) {
if (ec) {
m_error = ec;
} else {
m_addresses = results;
}
notify();
});
}
void connect(const std::function<void()>& notify)
{
if (resolved() && !m_connecting && !m_error) {
m_connecting = true;
asio::async_connect(m_socket, *m_addresses,
[this, notify](boost::system::error_code ec, const asio::ip::tcp::endpoint&) {
m_connecting = false;
m_error = ec;
notify();
});
}
}
// hand the connected socket over to another io_context
asio::ip::tcp::socket release(asio::io_context& io)
{
return asio::ip::tcp::socket { io, m_socket.local_endpoint().protocol(), m_socket.release() };
}
private:
asio::ip::tcp::socket m_socket;
boost::optional<asio::ip::tcp::resolver::results_type> m_addresses;
boost::optional<boost::system::error_code> m_error;
bool m_connecting = false; // a connect attempt is in flight
};
// Happy Eyeballs (RFC 8305): race AAAA/A lookups and per-family connect attempts, preferring IPv6
class HappyEyeballsRace
{
public:
// winning socket is adopted by the caller's io_context
static asio::ip::tcp::socket connect(asio::io_context& io, const std::string& host,
const std::string& service, std::chrono::milliseconds deadline)
{
HappyEyeballsRace race;
return race.run(host, service, deadline).release(io);
}
private:
// run the race and return the winning family, throw when nobody wins
Family& run(const std::string& host, const std::string& service, std::chrono::milliseconds deadline)
{
timeout.expires_after(deadline);
timeout.async_wait([this](boost::system::error_code ec) {
if (!ec) {
race.stop();
}
});
v6.resolve(resolver, asio::ip::tcp::v6(), host, service, [this]() { on_v6_resolve_done(); });
v4.resolve(resolver, asio::ip::tcp::v4(), host, service, [this]() { on_v4_resolve_done(); });
race.run();
if (v6.connected() || v4.connected()) {
return v6.connected() ? v6 : v4;
} else if (!v6.failed() || !v4.failed()) {
throw HttpException("connect timed out");
} else if (v6.resolved() || v4.resolved()) {
throw HttpException("connect failed: " + (v6.resolved() ? v6.error() : v4.error()).message());
} else {
throw HttpException("resolve failed: " + (v6.error() ? v6.error() : v4.error()).message());
}
}
void on_v6_resolve_done()
{
if (v6.resolved()) {
v6.connect([this]() { on_v6_connect_done(); });
open_v4_gate(stagger_delay); // IPv4 may compete once the attempt delay elapsed
} else {
open_v4_gate(std::chrono::milliseconds::zero()); // no AAAA answer, IPv4 goes at once
}
settle();
}
void on_v4_resolve_done()
{
if (is_v4_gate_open()) {
v4.connect([this]() { settle(); });
} else if (v4.resolved() && !v6.resolved()) {
open_v4_gate(resolution_delay); // pending AAAA answer gets a short head start
}
settle();
}
void on_v6_connect_done()
{
if (v6.failed()) {
open_v4_gate(std::chrono::milliseconds::zero()); // IPv6 lost, IPv4 takes over
}
settle();
}
void open_v4_gate(std::chrono::milliseconds delay)
{
v4_gate.expires_after(delay);
v4_gate.async_wait([this](boost::system::error_code ec) {
if (!ec) {
v4.connect([this]() { settle(); });
}
});
}
bool is_v4_gate_open() const
{
return v4_gate.expiry() <= asio::steady_timer::clock_type::now();
}
void settle()
{
// stop the race once a connection is established or both families have failed
if (v6.connected() || v4.connected() || (v6.failed() && v4.failed())) {
race.stop();
}
}
static constexpr std::chrono::milliseconds resolution_delay { 50 }; // RFC 8305 head start for the AAAA answer
static constexpr std::chrono::milliseconds stagger_delay { 200 }; // connection attempt delay
asio::io_context race;
asio::ip::tcp::resolver resolver { race };
Family v6 { race };
Family v4 { race };
asio::steady_timer v4_gate { race, asio::steady_timer::time_point::max() }; // gate starts closed
asio::steady_timer timeout { race };
};
} // namespace
static bool should_follow(int status)
{
switch (status) {
case 301:
case 302:
case 303:
case 307:
case 308:
return true;
default:
return false;
}
}
template<typename T>
HttpResponse query_http(const HttpQuery& query, const beast::http::request<T>& request, asio::io_context& io,
ssl::context& ssl)
{
constexpr std::chrono::seconds connect_deadline { 10 };
HttpResponse response;
beast::flat_buffer buffer;
asio::ip::tcp::socket socket = HappyEyeballsRace::connect(io, query.host, query.which_service(), connect_deadline);
if (query.secure) {
ssl::stream<asio::ip::tcp::socket> stream(std::move(socket), ssl);
SSL_set_tlsext_host_name(stream.native_handle(), query.host.c_str());
stream.lowest_layer().set_option(asio::ip::tcp::no_delay(true));
stream.handshake(ssl::stream_base::client);
beast::http::write(stream, request);
beast::http::read(stream, buffer, response);
beast::error_code ec;
if (stream.shutdown(ec)) {
// could log error in verbose mode
}
} else {
beast::http::write(socket, request);
beast::http::read(socket, buffer, response);
}
return response;
}
template<typename T>
HttpResponse query_http_redirections(HttpQuery query, const beast::http::request<T>& request, asio::io_context& io,
ssl::context& ssl)
{
constexpr unsigned max_hops = 5;
for (unsigned hop = 0; hop < max_hops; ++hop) {
auto resp = query_http(query, request, io, ssl);
if (should_follow(resp.result_int())) {
std::string location { resp[beast::http::field::location] };
query = HttpQuery::from_url(location);
} else {
return resp;
}
}
throw HttpException("too many redirects");
}
template<typename T> HttpResponse query_http(const HttpQuery& query, const beast::http::request<T>& request)
{
asio::io_context io;
ssl::context ssl(ssl::context::tlsv13_client);
ssl.set_default_verify_paths();
ssl.set_verify_mode(ssl::context::verify_peer);
ssl.set_verify_callback(ssl::host_name_verification(query.host));
auto resp = query_http(query, request, io, ssl);
if (should_follow(resp.result_int())) {
std::string location { resp[beast::http::field::location] };
auto redirection = HttpQuery::from_url(location);
return query_http_redirections(std::move(redirection), request, io, ssl);
} else {
return resp;
}
}
HttpResponse http_get(const HttpQuery& query)
{
beast::http::request<beast::http::empty_body> request;
request.method(beast::http::verb::get);
request.target(query.path);
request.set(beast::http::field::host, query.host);
return query_http(query, request);
}
HttpResponse http_post(const HttpQuery& query, const std::string& content_type, const ByteBuffer& data)
{
beast::http::request<beast::http::string_body> request;
request.method(beast::http::verb::post);
request.target(query.path);
request.set(beast::http::field::host, query.host);
request.set(beast::http::field::content_type, content_type);
request.body().assign(data.begin(), data.end());
request.prepare_payload();
return query_http(query, request);
}
} // namespace pki
} // namespace vanetza