Files
scylladb/utils/rest/client.cc
Avi Kivity 6df04c9e5b Update seastar submodule
Changed seastar::http::experimental to seastar::http to reflect
graduation of the seastar http API.

Changed call to seastar::rename_file() (in sstables/storage.cc,
sstables/sstable_directory.cc, sstable/sstables.cc and
db/hints/internal/hint_storage.cc) to reflect new default parameter.

Updated scylla_gdb test helper get_task() to work with updated
accept loop in Seatar. This is just test code (attempts to find
a task to operate on), not used in real scylla-gdb.py work, but
nevertheless the adjustment keeps backward compatibility.

Fixes https://scylladb.atlassian.net/browse/SCYLLADB-1798
Fixes https://scylladb.atlassian.net/browse/SCYLLADB-2043

* seastar 485a62b2...510f3148 (43):
  > reactor_backend: fix iocb double-free and shutdown hang during AIO teardown
  > file: fix default DMA alignment
  > http: add to_reply() to redirect_exception with extra-header support
  > core: propagate syscall errors via `coroutine::exception`
  > file: assert dma alignments are powers of two
  > doc: Document undocumented io_tester features and fix output example
  > backtrace: print the build_id along with the backtrace
  > reactor: default to oneline backtraces
  > Merge 'json: formatter: support types with user-defined conversion to sstring' from Benny Halevy
    tests: json_formatter: test formatter::write with string types
    json: formatter: support types with user-defined conversion to sstring
  > httpd_test: fix build failure with Seastar_SSTRING=OFF
  > net/tls: introduce ssl_call wrapper for SSL I/O
  > build: disable unused command line argument error for C++ module
  > coroutine/generator: fix setup of generator's waiting task
  > tests/tls: set 1000-day validity for self-signed CA cert
  > net: tls: openssl: disable certificate compression
  > reactor: reduce steady_clock::now() calls per scheduling quantum
  > fair_queue: remove notify_request_finished()
  > loop: use small_vector for parallel_for_each_state incomplete futures
  > dodge false sharing in spinlock
  > Merge 'Handle nowait support for reads and writes independently' from Pavel Emelyanov
    file: Change nowait_works mode detection
    file: Introduce read-only nowait_mode
    filesystem: Make nowait_works bit a enum class too
    file: Make nowait_works bit a enum class
  > Merge 'net/tls: improve OpenSSL error queue hygiene' from Gellért Peresztegi-Nagy
    net/tls: assert clean error queue before SSL operations
    net/tls: clear error queue after successful SSL operations
    net/tls: clear error queue after successful SSL_CTX_new
    net/tls: drain error queue on unexpected error codes
    net/tls: use make_openssl_error for BIO creation failure
  > vla.hh: add missing includes
  > Merge 'smp: make smp::count non-static' from Avi Kivity
    smp: convert all smp::count usages to instance-aware alternatives
    smp: add per-instance shard_count and this_smp() infrastructure
    disk_params: document pre-init smp::count access with explicit 0
    reactor_backend: document pre-init smp::count access with explicit 0
    tests: alien_test: pass shard count to alien thread explicitly
  > build: fix cmake missing ninja on Ubuntu 26.04
  > rpc: Fix uint64 wraparound of expired timeout in send_entry()
  > Merge 'Generalize some RPC tests' from Pavel Emelyanov
    tests: Generalize async connection-based scheduling RPC tests
    tests: Generalize sync connection-based scheduling RPC tests
    tests: Remove redundant variadic/nonvariadic RPC tuple tests
    tests: Generalize max timeout RPC tests
  > net: tls: openssl: Share BIO ptrs across shards
  > http: fix compilation on clang 22 with c++26
  > build: openssl tools needed for test cert generation
  > reactor: support rename2
  > future: fix forwarding of reference types
  > Merge 'Zero-copy http chunked data sink' from Pavel Emelyanov
    http: Make chunked data sink zero-copy
    tests/prometheus_http: Rewrite on top of http::client
    tests/httpd: Rewrite content_length_limit on top of http::client
  > tests: Replace ad-hoc http_consumer with production HTTP parser
  > Merge 'co_return to accept same expressions and types as return' from Alexey Bashtanov
    tests/unit/{coroutines,futures}: strict types on co_return and set_value
    api: introduce version 10:
    core/{coroutine,future}: make `co_return` more strict with types
    core/{coroutine,future}: preparations to fix `co_return` type semantics
  > Merge 'Perftune.py: add special handling for mlx5 rss queues number calculation' from Vladislav Zolotarov
    perftune.py: NetPerfTuner: enhance RSS (a.k.a. "Rx") queues accounting for mlx5 devices
    perftune.py: update docstring of NetPerfTuner.__get_rps_cpus() method
    perftune.py: add a method that parses and models the output of the 'ethtool -l' command for a given interface
  > httpd: rewrite do_accepts/do_accept_one as coroutines
  > file: add mmap support to file
  > http: Move client code out of experimental namespace
  > file: add hugetlbfs support to file system detection
  > tests: Replace test_source_impl with util::as_input_stream
  > tests: Replace buf_source_impl with util::as_input_stream
  > Merge 'rpc_tester: expose throuput for rpc tester' from Marcin Szopa
    rpc_tester: remove unused payload size variable from job_rpc_streaming class
    rpc_tester: add start time tracking for throughput calculation, print throughput and msg/s for job_rpc
    rpc_tester: refactor result emission to use dedicated functions for messages and throughput
  > iostream: cast first argument of `std::min` to `size_t`

Closes scylladb/scylladb#29952
2026-05-20 13:47:12 +03:00

281 lines
10 KiB
C++

/*
* Copyright (C) 2025 ScyllaDB
*
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
*/
#include <boost/regex.hpp>
#include <seastar/net/tls.hh>
#include <seastar/net/dns.hh>
#include <seastar/util/short_streams.hh>
#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<tls::certificate_credentials> creds, std::optional<tls::tls_options> 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::result_type> 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::connection_factory {
socket_address _addr;
shared_ptr<tls::certificate_credentials> _creds;
tls::tls_options _tls_options;
sstring _host;
public:
my_connection_factory(socket_address addr, shared_ptr<tls::certificate_credentials> 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<connected_socket> 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::client client(std::make_unique<my_connection_factory>(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<char>& 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::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::client& client, seastar::http::request& req, const handler_func_ex& f, const http::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<char>&& 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<char>&& 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<rjson::value> rest::send_request(std::string_view uri
, seastar::shared_ptr<seastar::tls::certificate_credentials> 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<rjson::value> rest::send_request(std::string_view uri
, seastar::shared_ptr<seastar::tls::certificate_credentials> 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<key_value> 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<seastar::tls::certificate_credentials> creds
, std::string body
, std::string_view content_type
, const std::function<void(const http::reply&, std::string_view)>& 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<rest::httpclient::result_type>::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<rest::redacted_request_type>::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<rest::redacted_result_type>::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;
}