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
285 lines
10 KiB
C++
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;
|
|
}
|