177 lines
5.6 KiB
C++
177 lines
5.6 KiB
C++
#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
|