Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions include/anyhttp/client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ class Response
public:
int status_code() const noexcept;

/// The response header fields, without HTTP/2 and HTTP/3 pseudo-headers.
const Fields& fields() const;

public:
/**
* Reads a part of the response body.
Expand Down
1 change: 1 addition & 0 deletions include/anyhttp/client_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Response::Impl : public impl::Reader

virtual unsigned int status_code() const noexcept = 0;
virtual boost::url_view url() const = 0;
virtual const Fields& fields() const = 0;

using ReaderOrWriter = impl::Reader;
};
Expand Down
1 change: 1 addition & 0 deletions include/anyhttp/detail/h2_session_details.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ awaitable<void> ServerSession<Stream>::do_session(Buffer&& buffer)
auto stream = this->create_stream(1);
stream->method = std::move(m_upgrade->method);
stream->url = std::move(m_upgrade->url);
stream->fields = std::move(m_upgrade->fields);
mlogd("upgraded from HTTP/1.1: {} {}", stream->method, stream->url.buffer());
stream->on_request();
stream->on_eof(session, 1);
Expand Down
1 change: 1 addition & 0 deletions include/anyhttp/h2_backend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct Upgrade
std::string settings; ///< decoded payload of the HTTP2-Settings header
std::string method;
boost::urls::url url;
Fields fields; ///< request headers, without the connection-specific ones
};

std::shared_ptr<Session::Impl> make_server_session(server::Server::Impl& server,
Expand Down
2 changes: 2 additions & 0 deletions include/anyhttp/h2_stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class NGHttp2Reader : public Interface

unsigned int status_code() const noexcept override;
boost::url_view url() const override;
const Fields& fields() const override;

NGHttp2Stream* stream;
asio::any_io_executor executor; // kept as a copy so a detached reader can still complete
Expand Down Expand Up @@ -179,6 +180,7 @@ class NGHttp2Stream : public std::enable_shared_from_this<NGHttp2Stream>
std::vector<std::pair<std::string, std::string>> received_headers;
std::optional<unsigned int> status_code;
std::optional<size_t> content_length;
Fields fields; // all received headers except the pseudo-headers

bool closed = false; // set to true after on_stream_close_callback

Expand Down
6 changes: 6 additions & 0 deletions include/anyhttp/h3_stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ class Http3Reader : public Interface
return stream->url;
}

const Fields& fields() const override
{
assert(stream);
return stream->fields;
}

void async_read_some(asio::mutable_buffer buffer, ReadSomeHandler&& handler) override
{
//
Expand Down
3 changes: 3 additions & 0 deletions include/anyhttp/server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ class Request
boost::url_view url() const;
std::optional<size_t> content_length() const noexcept;

/// The request header fields, without HTTP/2 and HTTP/3 pseudo-headers.
const Fields& fields() const;

/**
* Looks up a query parameter and converts its value to \c T.
*
Expand Down
1 change: 1 addition & 0 deletions include/anyhttp/server_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Request::Impl : public impl::Reader
// FIXME: doesn't make sense to have a status_code() for a server request, but keeps beast happy
virtual unsigned int status_code() const noexcept = 0;
virtual boost::url_view url() const = 0;
virtual const Fields& fields() const = 0;

using ReaderOrWriter = impl::Reader;
};
Expand Down
1 change: 1 addition & 0 deletions src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Response::~Response() { reset(); }
// -------------------------------------------------------------------------------------------------

int Response::status_code() const noexcept { return impl->status_code(); }
const Fields& Response::fields() const { return impl->fields(); }

void Response::async_read_some_any(boost::asio::mutable_buffer buffer, ReadSomeHandler&& handler)
{
Expand Down
21 changes: 21 additions & 0 deletions src/h1_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class BeastReader : public Interface
return parser.get().result_int();
}
boost::url_view url() const override { return m_url; }
const Fields& fields() const override { return parser.get(); }
std::optional<size_t> content_length() const noexcept override
{
if (parser.content_length())
Expand Down Expand Up @@ -739,6 +740,26 @@ static std::optional<nghttp2::Upgrade> h2c_upgrade(const http::request<http::buf

upgrade.method = request.method_string();
upgrade.url = url;

//
// HTTP/2 has no connection-specific header fields (RFC 9113, section 8.2.2), and the upgrade
// ones are used up by now.
//
for (const auto& field : request)
{
switch (field.name())
{
case http::field::connection:
case http::field::proxy_connection:
case http::field::keep_alive:
case http::field::transfer_encoding:
case http::field::upgrade:
case http::field::http2_settings:
break;
default:
upgrade.fields.insert(field.name_string(), field.value());
}
}
return upgrade;
}

Expand Down
3 changes: 3 additions & 0 deletions src/h2_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ int on_header_callback(nghttp2_session* session, const nghttp2_frame* frame, con
stream->content_length.emplace();
std::from_chars(value.begin(), value.end(), *stream->content_length);
}

if (!name.starts_with(':'))
stream->fields.insert(name, value);
}
catch (std::exception& ex)
{
Expand Down
7 changes: 7 additions & 0 deletions src/h2_stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ boost::url_view NGHttp2Reader<Base>::url() const
return {stream->url};
}

template <typename Base>
const Fields& NGHttp2Reader<Base>::fields() const
{
assert(stream);
return stream->fields;
}

template <typename Base>
std::optional<size_t> NGHttp2Reader<Base>::content_length() const noexcept
{
Expand Down
10 changes: 7 additions & 3 deletions src/h3_stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -622,15 +622,19 @@ void Http3Stream::on_header(std::string_view name, std::string_view value)
try
{
if (name.starts_with(':'))
{
on_pseudo_header(name, value);
else if (name == "content-length")
return;
}

if (name == "content-length")
{
size_t len = 0;
if (std::from_chars(value.begin(), value.end(), len).ec == std::errc{})
content_length = len;
}
else
fields.set(name, value);

fields.insert(name, value); // insert, not set: repeated fields must all be kept
}
catch (const std::exception& ex)
{
Expand Down
4 changes: 4 additions & 0 deletions src/request_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ awaitable<void> dump(server::Request request, server::Response response)
std::println(str, " {}={} ({})", key, EscapedString(value), _);
std::println(str, "fragment: {} ({})", url.fragment(), url.encoded_fragment());

std::println(str, "headers:");
for (const auto& field : request.fields())
std::println(str, " {}: {}", field.name_string(), EscapedString(field.value()));

auto body = str.str();
co_await response.async_submit(
200, fields({{"Content-Length", body.size()}, {"Content-Type", "text/plain"}}));
Expand Down
6 changes: 6 additions & 0 deletions src/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ std::optional<size_t> Request::content_length() const noexcept
return impl->content_length();
}

const Fields& Request::fields() const
{
assert(impl);
return impl->fields();
}

void Request::async_read_some_any(asio::mutable_buffer buffer, ReadSomeHandler&& handler)
{
assert(impl);
Expand Down
10 changes: 6 additions & 4 deletions test/test_client_async.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
#include <ranges>
#include <span>

using namespace testing;

// =================================================================================================

INSTANTIATE_TEST_SUITE_P(ClientAsync, ClientAsync,
::testing::Values(anyhttp::Protocol::http11, anyhttp::Protocol::h2,
anyhttp::Protocol::h3),
Values(anyhttp::Protocol::http11, anyhttp::Protocol::h2,
anyhttp::Protocol::h3),
NameGenerator);

// -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -761,8 +763,8 @@ TEST_P(ClientAsync, Dump)
co_await send_eof(request);
auto response = co_await request.async_get_response();
auto dump = co_await read(response);
EXPECT_THAT(dump, testing::HasSubstr("path: /dump space"));
EXPECT_THAT(dump, testing::HasSubstr(" blah=white space"));
EXPECT_THAT(dump, HasSubstr("path: /dump space"));
EXPECT_THAT(dump, HasSubstr(" blah=white space"));
};
}

Expand Down
10 changes: 6 additions & 4 deletions test/test_client_async_cancellation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <print>
#include <ranges>

using namespace testing;

// =================================================================================================

//
Expand All @@ -14,8 +16,8 @@ class ClientAsyncCancellation : public ClientAsync
};

INSTANTIATE_TEST_SUITE_P(ClientAsyncCancellation, ClientAsyncCancellation,
::testing::Values(anyhttp::Protocol::http11, anyhttp::Protocol::h2,
anyhttp::Protocol::h3),
Values(anyhttp::Protocol::http11, anyhttp::Protocol::h2,
anyhttp::Protocol::h3),
NameGenerator);

// -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -233,8 +235,8 @@ TEST_P(ClientAsyncCancellation, WHEN_send_more_than_content_length_THEN_connecti
// ECONNRESET, any later one with EPIPE. Single-threaded we reliably hit the former, with
// more than one thread the latter; both mean the same thing here.
//
EXPECT_THAT(code(ep), testing::AnyOf(boost::system::errc::connection_reset,
boost::system::errc::broken_pipe));
EXPECT_THAT(code(ep),
AnyOf(boost::system::errc::connection_reset, boost::system::errc::broken_pipe));
};
}

Expand Down
4 changes: 3 additions & 1 deletion test/test_client_connect.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#include "test_fixtures.hpp"

using namespace testing;

// =================================================================================================

class ClientConnect : public testing::Test
class ClientConnect : public Test
{
public:
void SetUp() override { setupLogging(); }
Expand Down
10 changes: 9 additions & 1 deletion test/test_external.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ TEST_F(ExternalCustom, curl_h2c_upgrade)
// clang-format off
Args args = {"-sS", "-v", "--http2",
"-w", "%{http_code} HTTP/%{http_version}\n",
url + "?first", url + "?second"};
url + "?first", "-H", "x-custom:value", url + "?second"};
// clang-format on
auto future = spawn(CURL_PATH, std::move(args));
run();
Expand All @@ -432,6 +432,14 @@ TEST_F(ExternalCustom, curl_h2c_upgrade)
EXPECT_THAT(output, testing::HasSubstr("query: first"));
EXPECT_THAT(output, testing::HasSubstr("query: second"));

// the header goes along with both requests, the upgraded one included
std::string_view headers = output;
size_t with_header = 0;
for (size_t pos; (pos = headers.find("\n x-custom: value\n")) != std::string_view::npos;
++with_header)
headers.remove_prefix(pos + 1);
EXPECT_EQ(with_header, 2) << output;

std::string_view rest = output;
size_t upgraded = 0;
for (size_t pos; (pos = rest.find("200 HTTP/2\n")) != std::string_view::npos; ++upgraded)
Expand Down
6 changes: 4 additions & 2 deletions test/test_file_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <filesystem>
#include <fstream>

using namespace testing;

// =================================================================================================

//
Expand Down Expand Up @@ -86,8 +88,8 @@ class FileHandler : public ClientAsync
// -------------------------------------------------------------------------------------------------

INSTANTIATE_TEST_SUITE_P(FileHandler, FileHandler,
::testing::Values(anyhttp::Protocol::http11, anyhttp::Protocol::h2,
anyhttp::Protocol::h3),
Values(anyhttp::Protocol::http11, anyhttp::Protocol::h2,
anyhttp::Protocol::h3),
NameGenerator);

// =================================================================================================
Expand Down
40 changes: 33 additions & 7 deletions test/test_h2c_upgrade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include <string>
#include <vector>

using namespace testing;

// =================================================================================================

//
Expand Down Expand Up @@ -54,7 +56,9 @@ class H2CUpgrade : public Server
}

/// Upgrades a GET for the first target and sends GETs for the others as HTTP/2 streams.
awaitable<Responses> upgrade(std::vector<std::string> targets)
/// \p fields go along with the upgrade request.
awaitable<Responses> upgrade(std::vector<std::string> targets,
boost::beast::http::fields fields = {})
{
namespace http = boost::beast::http;

Expand Down Expand Up @@ -111,6 +115,8 @@ class H2CUpgrade : public Server
EXPECT_GT(len, 0);

http::request<http::empty_body> request{http::verb::get, targets.front(), 11};
for (const auto& field : fields)
request.insert(field.name_string(), field.value());
request.set(http::field::host, authority);
request.set(http::field::connection, "Upgrade, HTTP2-Settings");
request.set(http::field::upgrade, "h2c");
Expand Down Expand Up @@ -246,8 +252,28 @@ TEST_F(H2CUpgrade, WHEN_upgrade_is_requested_THEN_request_continues_as_stream_1)
ASSERT_TRUE(responses.contains(1));
EXPECT_EQ(responses[1].status, 200);
EXPECT_TRUE(responses[1].closed);
EXPECT_THAT(responses[1].body, testing::HasSubstr("path: /dump"));
EXPECT_THAT(responses[1].body, testing::HasSubstr("query: first"));
EXPECT_THAT(responses[1].body, HasSubstr("path: /dump"));
EXPECT_THAT(responses[1].body, HasSubstr("query: first"));
}

TEST_F(H2CUpgrade, WHEN_upgraded_THEN_request_headers_are_passed_on_to_stream_1)
{
boost::beast::http::fields fields;
fields.set("x-custom", "value");
fields.set(boost::beast::http::field::keep_alive, "timeout=5");
auto responses = run(upgrade({"/dump?first"}, std::move(fields)));

ASSERT_TRUE(responses.contains(1));
EXPECT_EQ(responses[1].status, 200);
const auto& body = responses[1].body;
EXPECT_THAT(body, HasSubstr("\n x-custom: value\n"));
EXPECT_THAT(body, HasSubstr("\n Host: 127.0.0.2:"));

// connection-specific fields do not exist in HTTP/2 (RFC 9113, section 8.2.2)
EXPECT_THAT(body, Not(HasSubstr("Connection:")));
EXPECT_THAT(body, Not(HasSubstr("Upgrade:")));
EXPECT_THAT(body, Not(HasSubstr("HTTP2-Settings:")));
EXPECT_THAT(body, Not(HasSubstr("Keep-Alive:")));
}

TEST_F(H2CUpgrade, WHEN_upgraded_THEN_connection_takes_more_streams)
Expand All @@ -256,9 +282,9 @@ TEST_F(H2CUpgrade, WHEN_upgraded_THEN_connection_takes_more_streams)

ASSERT_EQ(responses.size(), 3);
EXPECT_EQ(responses[1].status, 200);
EXPECT_THAT(responses[1].body, testing::HasSubstr("query: first"));
EXPECT_THAT(responses[1].body, HasSubstr("query: first"));
EXPECT_EQ(responses[3].status, 200);
EXPECT_THAT(responses[3].body, testing::HasSubstr("query: second"));
EXPECT_THAT(responses[3].body, HasSubstr("query: second"));
EXPECT_EQ(responses[5].status, 404);
}

Expand All @@ -279,7 +305,7 @@ TEST_F(H2CUpgrade, WHEN_http2_settings_are_missing_THEN_is_served_as_http11)
auto response = run(http11(std::move(request)));

EXPECT_EQ(response.result_int(), 200);
EXPECT_THAT(response.body(), testing::HasSubstr("query: no-settings"));
EXPECT_THAT(response.body(), HasSubstr("query: no-settings"));
}

TEST_F(H2CUpgrade, WHEN_http2_settings_are_invalid_THEN_is_served_as_http11)
Expand All @@ -289,7 +315,7 @@ TEST_F(H2CUpgrade, WHEN_http2_settings_are_invalid_THEN_is_served_as_http11)
auto response = run(http11(std::move(request)));

EXPECT_EQ(response.result_int(), 200);
EXPECT_THAT(response.body(), testing::HasSubstr("query: invalid"));
EXPECT_THAT(response.body(), HasSubstr("query: invalid"));
}

// =================================================================================================
Loading