Files
scylladb/utils/rest/client.cc
Ernest Zaslavsky e56081d588 treewide: seastar module update and fix broken rest client
start using `write_body` in `rest/client` to properly set headers due to changes applied to seastar's http client

Seastar module update
```
b6be384e Merge 'http: generalize Content-Type setting' from Nadav Har'El
74472298 http: generalize request's Content-Type setting
9fd5a1cc http: generalize reply's Content-Type setting
a2665f38 memory: Remove deprecated enable_abort_on_allocation_failure()
d2a5a8a9 resource.cc: Remove some dead code
7ad9f424 http: Add support of multiple key repetitions for the request
a636baca task: Move task::get_backtrace() definition in its class
a0101efa Fixed "doxygen" spelling in error message
db969482 Merge 'http/reply: introduce set_cookie()' from Botond Dénes
5357b434 http/reply: introduce set_cookie()
1ddcf05f http/reply: make write_reply*() public
4b782d73 http/connection: start_response(): fix indentation
720feca0 http/reply: encapsulate reply writing in write_reply()
3e19917d Merge 'exceptions: log thrown and propagated exception with distinct log levels' from Botond Dénes
db9aea93 Merge 'Correctly wrap up abandoned yielding directory lister' from Pavel Emelyanov
dbb2bf3f test: Add test for input_stream::read_exactly()
a5308ec9 file/directory_lister: Correctly wrap up fallback generator
4f0811f4 file/directory_lister: Convert on-stack queue to shared pointer
59801da7 tests: Add directory lister early drop cases
33233032 http/reply: s/write_reply_to_connection/write_reply/
69b93620 http/reply: write_reply_{to_connection,headers}(): pass output stream
56e9bda7 test: Convert directory_test into seastar test
96782358 Merge 'Improve io_tester's seqwrite and append workloads' from Pavel Emelyanov
8b46e3d4 SEASTAR_ASSERT: assert to stderr and flush stream
3370e22a tutorial.md: use current_exception_as_future()
e977453a Add fixture support for seastar::testing
3e70d7f7 io_tester: Do not set append_is_unlikely unconditionally
2a4ae7b4 io_tester: Count file size overflows
5e678bb5 io_tester: Tuneup size overflow check
d5dad8ce io_tester: Move position management code to io_class_data
5586a056 io_tester: Rename seqwrite -> overwrite
92df2fb2 io_tester: Relax return value of create_and_fill_file()
03d9500d io_tester: Dont fill file for APPEND
d6844a7b io_tester: Indentation fix after previous patch
fb9e0088 io_tester: Coroutinize create_and_fill_file()
2f802f57 exceptions: log thrown and propagated exception with distinct log levels
4971fa70 util: move log-level into own header
39448fc1 Merge 'Fix and tune http::request setup by client' from Pavel Emelyanov
52d0c4fb iostream: Move output_stream::write(scattered_message) lower
7a52f734 Merge 'read_first_line: Missing pragma and licence' from Ernest Zaslavsky
d0881b7e read_first_line: Add missing license boilerplate
988a0e99 read_first_line:: Add missing `#pragma once`
42675266 http: Make client::make_request accept const request&
c7709fb5 http: Make request making API return exceptional future not throw
b68ed89b http: Move request content length header setup
1d96dac6 http: Move request version configuration
072e86f6 http: Setup request once
```

Closes scylladb/scylladb#25915

(cherry picked from commit 44d34663bc)

Closes scylladb/scylladb#26100
2025-09-19 11:40:59 +03:00

285 lines
10 KiB
C++

/*
* Copyright (C) 2025 ScyllaDB
*
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
*/
#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() {
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;
});
co_return res;
}
seastar::future<> rest::httpclient::send(const handler_func& f) {
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<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::experimental::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);
});
} 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) {
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";
}
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);
});
}
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)
{
return send_request(uri, std::move(creds), rjson::print(body), "application/json", op, std::move(headers));
}
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)
{
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));
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)
{
// 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);
});
}
constexpr auto linesep = '\n';
auto
fmt::formatter<rest::httpclient::request_type>::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<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<seastar::http::reply>::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<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;
}