ranczo-io/libs/http.cpp
Bartosz Wieczorek 6da01a2f6b Add HTTP get
2025-12-12 16:57:05 +01:00

177 lines
5.6 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <boost/asio/redirect_error.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <ranczo-io/utils/http.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/url.hpp>
#include <string_view>
namespace ranczo {
using tcp = boost::asio::ip::tcp;
namespace asio = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;
using PmrCharAlloc = std::pmr::polymorphic_allocator< char >;
using PmrFields = http::basic_fields< PmrCharAlloc >;
using PmrStringBody = http::basic_string_body< char, std::char_traits< char >, PmrCharAlloc >;
using PmrFlatBuffer = boost::beast::basic_flat_buffer< PmrCharAlloc >;
using PmrResponse = http::response< PmrStringBody, PmrFields >;
using PmrRequest = http::request< PmrStringBody, PmrFields >;
expected< HttpGetClient::ParsedUrl > HttpGetClient::parse_url_generic(std::string_view url) {
ParsedUrl out;
namespace urls = boost::urls;
auto r = urls::parse_uri(url);
if(!r) {
return unexpected(r.error());
}
urls::url_view u = *r;
out.scheme = std::string(u.scheme());
out.host = std::string(u.host());
// ścieżka
out.target = std::string(u.encoded_path());
if(out.target.empty())
out.target = "/";
// query (bez fragmentu)
if(!u.encoded_query().empty()) {
out.target += "?";
out.target += std::string(u.encoded_query());
}
// port
if(!u.port().empty()) {
out.port = u.port_number();
} else {
if(out.scheme == "https")
out.port = 443;
else
out.port = 80;
}
out.use_ssl = (out.scheme == "https");
return out;
}
awaitable_expected< std::pmr::string > HttpGetClient::async_get(std::string_view url, std::pmr::memory_resource * mr) {
BOOST_ASSERT(mr);
namespace ssl = boost::asio::ssl;
PmrCharAlloc alloc{mr};
// 1. Parse URL
auto parsed_exp = parse_url_generic(url);
if(!parsed_exp) {
co_return unexpected(parsed_exp.error());
}
auto parsed = *parsed_exp;
beast::error_code ec;
// 2. Resolver
tcp::resolver resolver{executor()};
auto results = co_await resolver.async_resolve(parsed.host, std::to_string(parsed.port), asio::redirect_error(asio::use_awaitable, ec));
if(ec) {
co_return unexpected(ec);
}
// 3. Request na PMR
http::request< PmrStringBody, PmrFields > req{http::verb::get, parsed.target, 11, PmrStringBody::value_type{alloc}, PmrFields{alloc}};
req.set(http::field::host, parsed.host);
req.set(http::field::user_agent, "ranczo-http-client");
// 4. Wspólne rzeczy dla HTTP/HTTPS
PmrFlatBuffer buffer{alloc};
PmrResponse res{std::piecewise_construct, std::make_tuple(PmrStringBody::value_type{alloc}), std::make_tuple(PmrFields{alloc})};
if(!parsed.use_ssl) {
// ---------------------- HTTP (bez TLS) ----------------------
beast::tcp_stream stream{executor()};
stream.expires_after(_timeout);
co_await stream.async_connect(results, asio::redirect_error(asio::use_awaitable, ec));
if(ec) {
co_return unexpected(ec);
}
co_await http::async_write(stream, req, asio::redirect_error(asio::use_awaitable, ec));
if(ec) {
co_return unexpected(ec);
}
co_await http::async_read(stream, buffer, res, asio::redirect_error(asio::use_awaitable, ec));
if(ec) {
co_return unexpected(ec);
}
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
// błąd przy shutdown zwykle ignorujemy
} else {
// ---------------------- HTTPS (TLS) ----------------------
ssl::context ctx{ssl::context::tls_client};
// Uwaga: do prawdziwego użycia:
// - ctx.set_default_verify_paths();
// - ctx.set_verify_mode(ssl::verify_peer);
// Tu dla testów najprościej wyłączyć weryfikację:
ctx.set_verify_mode(ssl::verify_none);
beast::ssl_stream< beast::tcp_stream > stream{executor_, ctx};
auto & lowest = beast::get_lowest_layer(stream);
lowest.expires_after(_timeout);
co_await lowest.async_connect(results, asio::redirect_error(asio::use_awaitable, ec));
if(ec) {
co_return unexpected(ec);
}
// (opcjonalnie) SNI przydatne dla częsci serwerów
if(!SSL_set_tlsext_host_name(stream.native_handle(), parsed.host.c_str())) {
ec = beast::error_code(static_cast< int >(::ERR_get_error()), asio::error::get_ssl_category());
co_return unexpected(ec);
}
co_await stream.async_handshake(ssl::stream_base::client, asio::redirect_error(asio::use_awaitable, ec));
if(ec) {
co_return unexpected(ec);
}
co_await http::async_write(stream, req, asio::redirect_error(asio::use_awaitable, ec));
if(ec) {
co_return unexpected(ec);
}
co_await http::async_read(stream, buffer, res, asio::redirect_error(asio::use_awaitable, ec));
if(ec) {
co_return unexpected(ec);
}
// shutdown TLS
co_await stream.async_shutdown(asio::redirect_error(asio::use_awaitable, ec));
// tu też często dostaniesz EOF można zignorować
}
// Opcjonalnie możesz sprawdzić status:
// if (res.result() != http::status::ok) { ... }
// Body korzysta z tego samego PMR
std::pmr::string body{alloc};
body = res.body(); // kopia na tym samym allocatorze (bez dużych kosztów w mr)
co_return body;
}
} // namespace ranczo