/* * Copyright (C) 2025 ScyllaDB * */ /* * SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0 */ #include #include #include #include #include "client.hh" #include "utils/http.hh" using namespace seastar; rest::request_wrapper::request_wrapper(std::string_view host) : _req(http::request::make(httpd::operation_type::GET, sstring(host), "")) { _req._version = "1.1"; } rest::request_wrapper::request_wrapper(request_wrapper&&) = default; rest::request_wrapper& rest::request_wrapper::add_header(std::string_view key, std::string_view value) { _req._headers[sstring(key)] = sstring(value); return *this; } void rest::request_wrapper::clear_headers() { _req._headers.clear(); } void rest::request_wrapper::method(method_type type) { _req._method = httpd::type2str(type); } void rest::request_wrapper::content(std::string_view content_type, std::string_view content) { _req.write_body(sstring(content_type), sstring(content)); } void rest::request_wrapper::content(std::string_view content_type, body_writer w, size_t len) { _req.write_body(sstring(content_type), len, std::move(w)); } void rest::request_wrapper::target(std::string_view s) { _req._url = sstring(s); } static std::string default_server_name(const std::string& host) { // don't verify host cert name if "host" is just an ip address. // typically testing. bool is_numeric_host = seastar::net::inet_address::parse_numerical(host).has_value(); return is_numeric_host ? std::string{} : host; } rest::httpclient::httpclient(std::string host, uint16_t port, shared_ptr creds, std::optional options) : request_wrapper(host) , _host(std::move(host)) , _port(port) , _creds(std::move(creds)) , _tls_options(options.value_or(tls::tls_options{ .server_name = default_server_name(_host) })) {} seastar::future rest::httpclient::send(seastar::abort_source* as) { result_type res; co_await send([&](const http::reply& r, std::string_view body) { res.reply._status = r._status; res.reply._content = sstring(body); res.reply._headers = r._headers; res.reply._version = r._version; }, as); co_return res; } seastar::future<> rest::httpclient::send(const handler_func& f, seastar::abort_source* as) { auto addr = co_await net::dns::resolve_name(_host, net::inet_address::family::INET /* TODO: our client does not handle ipv6 well?*/); // NOTE: similar to utils::http::dns_connection_factory, but that type does // not properly handle numeric hosts (don't validate certs for those) class my_connection_factory : public http::experimental::connection_factory { socket_address _addr; shared_ptr _creds; tls::tls_options _tls_options; sstring _host; public: my_connection_factory(socket_address addr, shared_ptr creds, tls::tls_options options, sstring host) : _addr(std::move(addr)) , _creds(std::move(creds)) , _tls_options(std::move(options)) , _host(std::move(host)) {} future make(abort_source* as) override { connected_socket s = co_await (_creds ? tls::connect(_creds, _addr, _tls_options) : seastar::connect(_addr, {}, transport::TCP) ); s.set_nodelay(true); co_return s; } }; http::experimental::client client(std::make_unique(socket_address(addr, _port), _creds, _tls_options, _host)); std::exception_ptr p; try { co_await simple_send(client, _req, [&](const seastar::http::reply& rep, seastar::input_stream& in_stream) -> future<> { // ensure these are on our coroutine frame. auto& resp_handler = f; auto result = co_await util::read_entire_stream_contiguous(in_stream); resp_handler(rep, result); }, as); } catch (...) { p = std::current_exception(); } co_await client.close(); if (p) { std::rethrow_exception(p); } } seastar::future<> rest::simple_send(seastar::http::experimental::client& client, seastar::http::request& req, const handler_func_ex& f, seastar::abort_source* as) { co_await simple_send(client, req, f, nullptr, as); } seastar::future<> rest::simple_send(seastar::http::experimental::client& client, seastar::http::request& req, const handler_func_ex& f, const http::experimental::retry_strategy* strategy, seastar::abort_source* as) { if (as) { as->check(); } if (req._url.empty()) { req._url = "/"; } if (req._version.empty()) { req._version = "1.1"; } if (!req._headers.count(httpclient::CONTENT_TYPE_HEADER)) { req._headers[httpclient::CONTENT_TYPE_HEADER] = "application/x-www-form-urlencoded"; } if (strategy) { co_return co_await client.make_request(std::move(req), [&](const http::reply& rep, input_stream&& in) -> future<> { // ensure these are on our coroutine frame. auto& resp_handler = f; auto in_stream = std::move(in); co_await resp_handler(rep, in_stream); }, *strategy, std::nullopt, as); } co_await client.make_request(std::move(req), [&](const http::reply& rep, input_stream&& in) -> future<> { // ensure these are on our coroutine frame. auto& resp_handler = f; auto in_stream = std::move(in); co_await resp_handler(rep, in_stream); }, std::nullopt, as); } rest::unexpected_status_error::unexpected_status_error(seastar::http::reply::status_type status, key_values headers) : httpd::unexpected_status_error(status) , _headers(headers.begin(), headers.end()) {} future rest::send_request(std::string_view uri , seastar::shared_ptr creds , const rjson::value& body , httpclient::method_type op , key_values headers , seastar::abort_source* as) { return send_request(uri, std::move(creds), rjson::print(body), "application/json", op, std::move(headers), as); } future rest::send_request(std::string_view uri , seastar::shared_ptr creds , std::string body , std::string_view content_type , httpclient::method_type op , key_values headers , seastar::abort_source* as) { rjson::value v; co_await send_request(uri, std::move(creds), std::move(body), content_type, [&](const http::reply& rep, std::string_view s) { if (rep._status != http::reply::status_type::ok) { std::vector tmp(rep._headers.begin(), rep._headers.end()); throw unexpected_status_error(rep._status, tmp); } v = rjson::parse(s); }, op, std::move(headers), as); co_return v; } future<> rest::send_request(std::string_view uri , seastar::shared_ptr creds , std::string body , std::string_view content_type , const std::function& handler , httpd::operation_type op , key_values headers , seastar::abort_source* as) { // Extremely simplified URI parsing. Does not handle any params etc. But we do not expect such here. static boost::regex simple_url(R"foo((https?):\/\/([^\/:]+)(:\d+)?(\/.*)?)foo"); boost::smatch m; std::string tmp(uri); if (!boost::regex_match(tmp, m, simple_url)) { throw std::invalid_argument(fmt::format("Could not parse URI {}", uri)); } auto scheme = m[1].str(); auto host = m[2].str(); auto port = m[3].str(); auto path = m[4].str(); if (scheme != "https") { creds = nullptr; } else if (!creds) { creds = co_await utils::http::system_trust_credentials(); } uint16_t pi = port.empty() ? (creds ? 443 : 80) : uint16_t(std::stoi(port.substr(1))); httpclient client(host, pi, std::move(creds)); client.target(path); client.method(op); for (auto& [k, v] : headers) { client.add_header(k, v); } if (!body.empty()) { if (content_type.empty()) { content_type = "application/x-www-form-urlencoded"; } client.content(content_type, std::move(body)); } co_await client.send([&] (const http::reply& rep, std::string_view result) { handler(rep, result); }, as); } constexpr auto linesep = '\n'; auto fmt::formatter::format(const rest::httpclient::request_type& r, fmt::format_context& ctx) const -> decltype(ctx.out()) { auto os = fmt::format_to(ctx.out(), "{} {} HTTP/{}{}", r._method, r._url, r._version, linesep); for (auto& [k, v] : r._headers) { os = fmt::format_to(os, "{}: {}{}", k, v, linesep); } os = fmt::format_to(os, "{}{}", linesep, r.content); return os; } auto fmt::formatter::format(const rest::httpclient::result_type& r, fmt::format_context& ctx) const -> decltype(ctx.out()) { return fmt::format_to(ctx.out(), "{}", r.reply); } auto fmt::formatter::format(const seastar::http::reply& r, fmt::format_context& ctx) const -> decltype(ctx.out()) { auto s = r.response_line(); // remove the trailing \r\n from response_line string. we want our own linebreak, hence substr. auto os = fmt::format_to(ctx.out(), "{}{}", std::string_view(s).substr(0, s.size()-2), linesep); for (auto& [k, v] : r._headers) { os = fmt::format_to(os, "{}: {}{}", k, v, linesep); } os = fmt::format_to(os, "{}{}", linesep, r._content); return os; } auto fmt::formatter::format(const rest::redacted_request_type& rr, fmt::format_context& ctx) const -> decltype(ctx.out()) { const auto& r = rr.original; auto os = fmt::format_to(ctx.out(), "{} {} HTTP/{}{}", r._method, r._url, r._version, linesep); for (auto& [k, v] : r._headers) { os = fmt::format_to(os, "{}: {}{}", k, rr.filter_header(k, v).value_or(v), linesep); } os = fmt::format_to(os, "{}{}", linesep, rr.filter_body(r.content).value_or(r.content)); return os; } auto fmt::formatter::format(const rest::redacted_result_type& rr, fmt::format_context& ctx) const -> decltype(ctx.out()) { const auto& r = rr.original; auto s = r.reply.response_line(); // remove the trailing \r\n from response_line string. we want our own linebreak, hence substr. auto os = fmt::format_to(ctx.out(), "{}{}", std::string_view(s).substr(0, s.size()-2), linesep); for (auto& [k, v] : r.reply._headers) { os = fmt::format_to(os, "{}: {}{}", k, rr.filter_header(k, v).value_or(v), linesep); } auto redacted_body_opt = rr.filter_body(r.body()); auto redacted_body_view = redacted_body_opt.has_value() ? *redacted_body_opt : r.body(); os = fmt::format_to(os, "{}{}", linesep, redacted_body_view); return os; }