From 02c5867b2ff474a05eede34092227c0378d17efb Mon Sep 17 00:00:00 2001 From: David Yu Date: Tue, 22 Sep 2026 09:59:48 -0700 Subject: [PATCH] http: build the Host header from an address in authority form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit request::make takes the Host header as a string, so a caller with a socket_address formats it, and the obvious "{}:{}" of address and port produces "fd00::5:8080" for IPv6 — a different address, not an authority. Seastar had no formatter to reach for. Add http::internal::format_authority(), which brackets an IPv6 address per RFC 3986 §3.2.2 and drops a zone index (an authority cannot carry one), and request::make overloads taking a socket_address. --- include/seastar/http/request.hh | 27 +++++++++++++++++++++++++++ src/http/request.cc | 19 +++++++++++++++++++ tests/unit/httpd_test.cc | 16 ++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/include/seastar/http/request.hh b/include/seastar/http/request.hh index 3e7757c1eae..a348915222d 100644 --- a/include/seastar/http/request.hh +++ b/include/seastar/http/request.hh @@ -422,6 +422,28 @@ public: */ static request make(httpd::operation_type type, sstring host, sstring path); + /** + * \brief Make simple request to an address + * + * \param method - method to use, e.g. "GET" + * \param addr - the address to contact. The "Host" header is its RFC 3986 + * authority form, i.e. an IPv6 address is bracketed. + * \path - the URL to send the request to + * + */ + static request make(sstring method, const socket_address& addr, sstring path); + + /** + * \brief Make simple request to an address + * + * \param method - method to use, e.g. operation_type::GET + * \param addr - the address to contact. The "Host" header is its RFC 3986 + * authority form, i.e. an IPv6 address is bracketed. + * \path - the URL to send the request to + * + */ + static request make(httpd::operation_type type, const socket_address& addr, sstring path); + sstring request_line() const; future<> write_request_headers(output_stream& out) const; private: @@ -430,6 +452,11 @@ private: }; namespace internal { + +/// The RFC 3986 §3.2.2 authority for \param addr: "host:port", with an IPv6 +/// address bracketed. A zone index is not part of an authority and is dropped. +sstring format_authority(const socket_address& addr); + #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" inline sstring& deprecated_content(request& req) noexcept { return req.content; } diff --git a/src/http/request.cc b/src/http/request.cc index 169e2d9432a..b4ecdf094ff 100644 --- a/src/http/request.cc +++ b/src/http/request.cc @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -168,5 +169,23 @@ request request::make(httpd::operation_type type, sstring host, sstring path) { return make(httpd::type2str(type), std::move(host), std::move(path)); } +request request::make(sstring method, const socket_address& addr, sstring path) { + return make(std::move(method), internal::format_authority(addr), std::move(path)); +} + +request request::make(httpd::operation_type type, const socket_address& addr, sstring path) { + return make(httpd::type2str(type), internal::format_authority(addr), std::move(path)); +} + +sstring internal::format_authority(const socket_address& addr) { + auto ip = addr.addr(); + if (ip.is_ipv6()) { + // inet_address prints a scope as "%", which an authority cannot + // carry; build the address without one. + return format("[{}]:{:d}", net::inet_address(static_cast<::in6_addr>(ip)), addr.port()); + } + return format("{}:{:d}", ip, addr.port()); +} + } // http namespace } // seastar namespace diff --git a/tests/unit/httpd_test.cc b/tests/unit/httpd_test.cc index 2f08c6ac8a9..3f207de008f 100644 --- a/tests/unit/httpd_test.cc +++ b/tests/unit/httpd_test.cc @@ -2434,6 +2434,22 @@ BOOST_AUTO_TEST_CASE(test_http_status_classification) { BOOST_REQUIRE_EQUAL(unclassified, 300); } +SEASTAR_TEST_CASE(test_format_authority) { + BOOST_REQUIRE_EQUAL(http::internal::format_authority(socket_address(ipv4_addr("10.0.0.1", 80))), "10.0.0.1:80"); + BOOST_REQUIRE_EQUAL(http::internal::format_authority(socket_address(ipv6_addr("2001:db8::1", 8080))), "[2001:db8::1]:8080"); + BOOST_REQUIRE_EQUAL(http::internal::format_authority(socket_address(ipv6_addr("::1", 0))), "[::1]:0"); + // a zone belongs to the host, not to the authority + BOOST_REQUIRE_EQUAL(http::internal::format_authority(socket_address(ipv6_addr("fe80::1", 8080), 2)), "[fe80::1]:8080"); + + // and the request overload puts exactly that in the Host header + auto v6 = http::request::make("GET", socket_address(ipv6_addr("::1", 8080)), "/test"); + BOOST_REQUIRE_EQUAL(v6.get_header("Host"), "[::1]:8080"); + BOOST_REQUIRE_EQUAL(v6.get_url(), "http://[::1]:8080/test"); + auto v4 = http::request::make(GET, socket_address(ipv4_addr("127.0.0.1", 80)), "/test"); + BOOST_REQUIRE_EQUAL(v4.get_header("Host"), "127.0.0.1:80"); + return make_ready_future(); +} + // An IPv4 client of a `[::]` listener is an IPv4 client to the handler, not the // ::ffff:a.b.c.d the kernel reports. SEASTAR_THREAD_TEST_CASE(test_dual_stack_listener_reports_ipv4_client) {